From 310c5f5c4ba68eb637a975d2013abd5de2737c01 Mon Sep 17 00:00:00 2001 From: Cory Finger Date: Fri, 27 Dec 2024 23:30:19 -0800 Subject: [PATCH] Seemingly working PoC --- resources/META-INF/plugin.xml | 7 + resources/elixirInjections.xml | 17 ++ .../ElixirSigilInjectionSupport.java | 31 ++++ .../injection/ElixirSigilInjector.kt | 163 ++++++++++++++++++ .../injection/ElixirSigilPatterns.java | 35 ++++ .../injection/PsiLanguageInjectionHost.kt | 10 +- 6 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 resources/elixirInjections.xml create mode 100644 src/org/elixir_lang/injection/ElixirSigilInjectionSupport.java create mode 100644 src/org/elixir_lang/injection/ElixirSigilInjector.kt create mode 100644 src/org/elixir_lang/injection/ElixirSigilPatterns.java 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 =