Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sigils - IntelliLang Language Injection Support PoC #3671

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@

<lang.documentationProvider implementationClass="org.elixir_lang.documentation.ElixirDocumentationProvider"
language="Elixir"/>

<multiHostInjector implementation="org.elixir_lang.injection.ElixirSigilInjector"/>
<multiHostInjector implementation="org.elixir_lang.injection.markdown.Injector"/>

<!-- Dialyzer -->
Expand All @@ -282,6 +284,11 @@
parentId="Errors"/>
</extensions>

<extensions defaultExtensionNs="org.intellij.intelliLang">
<languageSupport language="Elixir" implementation="org.elixir_lang.injection.ElixirSigilInjectionSupport"/>
<injectionConfig config="elixirInjections.xml"/>
</extensions>

<projectListeners>
<listener class="org.elixir_lang.DepsWatcher" topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
<listener class="org.elixir_lang.mix.Watcher" topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
Expand Down
17 changes: 17 additions & 0 deletions resources/elixirInjections.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<component name="LanguageInjectionConfiguration">
<injection language="RegExp" injector-id="elixir">
<display-name>Sigil: Regular Expression</display-name>
<place><![CDATA[sigilWithName("r")]]></place>
</injection>

<injection language="HTML" injector-id="elixir">
<display-name>Sigil: (Phoenix) HTML</display-name>
<place><![CDATA[sigilWithName("H")]]></place>
</injection>

<injection language="EEx" injector-id="elixir">
<display-name>Sigil: (Phoenix) EEX</display-name>
<place><![CDATA[sigilWithName("L")]]></place>
</injection>
</component>
31 changes: 31 additions & 0 deletions src/org/elixir_lang/injection/ElixirSigilInjectionSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.elixir_lang.injection;

import com.intellij.psi.PsiLanguageInjectionHost;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.intellij.plugins.intelliLang.inject.AbstractLanguageInjectionSupport;

public final class ElixirSigilInjectionSupport extends AbstractLanguageInjectionSupport {
@NonNls public static final String ELIXIR_SUPPORT_ID = "elixir";

@Override
@NotNull
public String getId() {
return ELIXIR_SUPPORT_ID;
}

@Override
public Class<?> @NotNull [] getPatternClasses() {
return new Class[] { ElixirSigilPatterns.class };
}

@Override
public boolean isApplicableTo(com.intellij.psi.PsiLanguageInjectionHost host) {
return true;
}

@Override
public boolean useDefaultInjector(final PsiLanguageInjectionHost host) {
return true;
}
}
163 changes: 163 additions & 0 deletions src/org/elixir_lang/injection/ElixirSigilInjector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package org.elixir_lang.injection

import com.intellij.lang.Language
import com.intellij.lang.injection.MultiHostInjector
import com.intellij.lang.injection.MultiHostRegistrar
import com.intellij.lang.html.HTMLLanguage;
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import org.elixir_lang.eex.Language as EexLanguage;
import org.elixir_lang.psi.*
import java.util.regex.Pattern

class ElixirSigilInjector : MultiHostInjector {
override fun getLanguagesToInject(registrar: MultiHostRegistrar, context: PsiElement) {
val sigilLine = context as? SigilLine
val sigilHeredoc = context as? SigilHeredoc

if (sigilLine != null && sigilLine.isValidHost()) {
sigilLine.body?.let { lineBody ->
val lang = languageForSigil(sigilLine.sigilName());
if (lang != null) {
registrar.startInjecting(lang)
registrar.addPlace(null, null, sigilLine, lineBody.textRangeInParent)
registrar.doneInjecting()
}
}
} else if (sigilHeredoc != null && sigilHeredoc.isValidHost()) {
val prefixLength = sigilHeredoc.heredocPrefix.textLength
val quoteOffset = sigilHeredoc.textOffset
var inCodeBlock = false
var listIndent = -1
var inException = false

for (line in sigilHeredoc.heredocLineList) {
val lineTextLength = line.textLength
val lineText = line.text

// > to include newline
if (lineTextLength > prefixLength) {
val lineMarkdownText = lineText.substring(prefixLength)

val lineOffset = line.textOffset
val lineOffsetRelativeToQuote = lineOffset - quoteOffset
val markdownOffsetRelativeToQuote = lineOffsetRelativeToQuote + prefixLength

val listStartMatcher = LIST_START_PATTERN.matcher(lineMarkdownText)

if (listStartMatcher.matches()) {
listIndent = listStartMatcher.group("indent").length

if (inCodeBlock) {
registrar.doneInjecting()

inCodeBlock = false
}
} else {
if (listIndent > 0) {
val indentedMatcher = INDENTED_PATTERN.matcher(lineMarkdownText)

if (indentedMatcher.matches() && indentedMatcher.group("indent").length < listIndent + 1) {
listIndent = -1
}
}

if (listIndent == -1) {
if (lineMarkdownText.startsWith(CODE_BLOCK_INDENT)) {
val lineCodeText = lineMarkdownText.substring(CODE_BLOCK_INDENT_LENGTH)
val codeOffsetRelativeToQuote = markdownOffsetRelativeToQuote + CODE_BLOCK_INDENT_LENGTH

if (lineCodeText.startsWith(EXCEPTION_PREFIX)) {
inException = true
} else if (lineCodeText.startsWith(DEBUG_PREFIX)) {
inException = false
} else {
val (lineElixirText, elixirOffsetRelativeToQuote) = when {
lineCodeText.startsWith(IEX_PROMPT) -> {
inException = false

Pair(
lineCodeText.substring(IEX_PROMPT_LENGTH),
codeOffsetRelativeToQuote + IEX_PROMPT_LENGTH
)
}

lineCodeText.startsWith(IEX_CONTINUATION) -> {
inException = false

Pair(
lineCodeText.substring(IEX_CONTINUATION_LENGTH),
codeOffsetRelativeToQuote + IEX_CONTINUATION_LENGTH
)
}

else -> {
Pair(lineCodeText, codeOffsetRelativeToQuote)
}
}

if (!inException) {
val textRangeInQuote =
TextRange.from(elixirOffsetRelativeToQuote, lineElixirText.length)

val lang = languageForSigil(sigilHeredoc.sigilName());
if (!inCodeBlock && lang != null) {
registrar.startInjecting(lang)

inCodeBlock = true
}

registrar.addPlace(null, null, sigilHeredoc, textRangeInQuote)
}
}
} else if (lineMarkdownText.isNotBlank()) {
if (inCodeBlock) {
registrar.doneInjecting()

inCodeBlock = false
inException = false
}
}
}
}
}
}

if (inCodeBlock) {
registrar.doneInjecting()
}

} else {
for (child in context.children) {
getLanguagesToInject(registrar, child)
}
}
}

override fun elementsToInjectIn(): List<Class<out PsiElement>> {
return listOf(PsiElement::class.java)
}

fun languageForSigil(sigilName: Char): Language? {
if (sigilName == 'H') {
return HTMLLanguage.INSTANCE
} else if (sigilName == 'L') {
return EexLanguage.INSTANCE
}

return null
}

companion object {
private const val CODE_BLOCK_INDENT = " "
private const val CODE_BLOCK_INDENT_LENGTH = CODE_BLOCK_INDENT.length
private const val IEX_PROMPT = "iex> "
private const val IEX_PROMPT_LENGTH = IEX_PROMPT.length
private const val IEX_CONTINUATION = "...> "
private const val IEX_CONTINUATION_LENGTH = IEX_CONTINUATION.length
private const val EXCEPTION_PREFIX = "** ("
private const val DEBUG_PREFIX = "*DBG* "
private val LIST_START_PATTERN = Pattern.compile("(?<indent>\\s*)([-*+]|\\d+\\.) \\S+.*\n")
private val INDENTED_PATTERN = Pattern.compile("(?<indent>\\s*).*\n")
}
}
35 changes: 35 additions & 0 deletions src/org/elixir_lang/injection/ElixirSigilPatterns.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.elixir_lang.injection;

import com.intellij.patterns.*;
import org.elixir_lang.psi.Sigil;
import com.intellij.psi.PsiElement;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;

public class ElixirSigilPatterns extends PlatformPatterns {
public static ElementPattern<?> sigil() {
return psiElement().inside(psiElement(Sigil.class));
}

public static ElementPattern<?> sigilWithName(String name) {
return and(sigil(), psiElement().with(new ElixirSigilPatterns.SigilWithName(name))) ;
}

public static class SigilWithName extends @NotNull PatternCondition<PsiElement> {
Character expectedSigil;

public SigilWithName(String name) {
super(name);
expectedSigil = name.charAt(0);
}

@Override
public boolean accepts(@NotNull PsiElement psiElement, ProcessingContext processingContext) {
if (psiElement instanceof Sigil) {
return ((Sigil) psiElement).sigilName() == expectedSigil;
}

return false;
}
}
}
10 changes: 8 additions & 2 deletions src/org/elixir_lang/injection/PsiLanguageInjectionHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import org.elixir_lang.injection.markdown.Injector
import org.elixir_lang.psi.AtUnqualifiedNoParenthesesCall
import org.elixir_lang.psi.ElixirNoParenthesesKeywords
import org.elixir_lang.psi.Parent
import org.elixir_lang.psi.Sigil

object PsiLanguageInjectionHost {
@JvmStatic
fun isValidHost(psiElement: PsiElement): Boolean =
when (val greatGrandParent = psiElement.parent?.parent?.parent) {
fun isValidHost(psiElement: PsiElement): Boolean {
if (psiElement as? Sigil != null) {
return true
}

return when (val greatGrandParent = psiElement.parent?.parent?.parent) {
is AtUnqualifiedNoParenthesesCall<*> -> Injector.isValidHost(greatGrandParent)
is ElixirNoParenthesesKeywords -> {
greatGrandParent
Expand All @@ -22,6 +27,7 @@ object PsiLanguageInjectionHost {
}
else -> false
}
}

@JvmStatic
fun createLiteralTextEscaper(parent: Parent): LiteralTextEscaper<Parent> =
Expand Down