diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
index 83d3b7839..d626f2165 100644
--- a/resources/META-INF/plugin.xml
+++ b/resources/META-INF/plugin.xml
@@ -269,6 +269,8 @@
+
+
@@ -282,6 +284,11 @@
parentId="Errors"/>
+
+
+
+
+
diff --git a/resources/elixirInjections.xml b/resources/elixirInjections.xml
new file mode 100644
index 000000000..c20946a7c
--- /dev/null
+++ b/resources/elixirInjections.xml
@@ -0,0 +1,17 @@
+
+
+
+ Sigil: Regular Expression
+
+
+
+
+ Sigil: (Phoenix) HTML
+
+
+
+
+ Sigil: (Phoenix) EEX
+
+
+
\ No newline at end of file
diff --git a/src/org/elixir_lang/injection/ElixirSigilInjectionSupport.java b/src/org/elixir_lang/injection/ElixirSigilInjectionSupport.java
new file mode 100644
index 000000000..1a46bb4cc
--- /dev/null
+++ b/src/org/elixir_lang/injection/ElixirSigilInjectionSupport.java
@@ -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;
+ }
+}
diff --git a/src/org/elixir_lang/injection/ElixirSigilInjector.kt b/src/org/elixir_lang/injection/ElixirSigilInjector.kt
new file mode 100644
index 000000000..af7428d99
--- /dev/null
+++ b/src/org/elixir_lang/injection/ElixirSigilInjector.kt
@@ -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> {
+ 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("(?\\s*)([-*+]|\\d+\\.) \\S+.*\n")
+ private val INDENTED_PATTERN = Pattern.compile("(?\\s*).*\n")
+ }
+}
diff --git a/src/org/elixir_lang/injection/ElixirSigilPatterns.java b/src/org/elixir_lang/injection/ElixirSigilPatterns.java
new file mode 100644
index 000000000..08ee6bc67
--- /dev/null
+++ b/src/org/elixir_lang/injection/ElixirSigilPatterns.java
@@ -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 {
+ 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;
+ }
+ }
+}
diff --git a/src/org/elixir_lang/injection/PsiLanguageInjectionHost.kt b/src/org/elixir_lang/injection/PsiLanguageInjectionHost.kt
index 66235b070..a39f92912 100644
--- a/src/org/elixir_lang/injection/PsiLanguageInjectionHost.kt
+++ b/src/org/elixir_lang/injection/PsiLanguageInjectionHost.kt
@@ -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
@@ -22,6 +27,7 @@ object PsiLanguageInjectionHost {
}
else -> false
}
+ }
@JvmStatic
fun createLiteralTextEscaper(parent: Parent): LiteralTextEscaper =