From 9ca08af3a6907e7cc829cf5fca1f32d3dc7eafcc Mon Sep 17 00:00:00 2001 From: Cory Finger Date: Fri, 27 Dec 2024 23:30:19 -0800 Subject: [PATCH 1/7] 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 = From 13ab2252c50753605c84c961c47430f0c2080920 Mon Sep 17 00:00:00 2001 From: Josh Taylor Date: Sat, 11 Jan 2025 16:42:26 +0800 Subject: [PATCH 2/7] Try to get syntax highlighting to work --- gradle.properties | 2 +- resources/META-INF/plugin.xml | 11 +- .../injection/ElixirSigilInjector.kt | 208 +++++++++--------- 3 files changed, 111 insertions(+), 110 deletions(-) diff --git a/gradle.properties b/gradle.properties index a4282d658..b0c13dc49 100644 --- a/gradle.properties +++ b/gradle.properties @@ -45,7 +45,7 @@ platformVersion=2024.3 platformPlugins = PsiViewer:243.7768, com.google.ide-perf:1.3.2, org.jetbrains.action-tracker:0.3.3, com.intellij.classic.ui:243.21565.122,krasa.CpuUsageIndicator:1.18.0-IJ2023 # Example: platformBundledPlugins = com.intellij.java # We need com.intellij.java to compile JPS, and markdown. -platformBundledPlugins=org.intellij.plugins.markdown,com.intellij.java +platformBundledPlugins=org.intellij.plugins.markdown,com.intellij.java,org.intellij.intelliLang # Gradle Releases -> https://github.com/gradle/gradle/releases # 8.5 is set because newer versions have weird run time caching issues, even with caching turned off. # See https://github.com/gradle/gradle/issues/28974 diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index d626f2165..e24508257 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -10,6 +10,7 @@ on how to target different products --> com.intellij.modules.lang org.intellij.plugins.markdown + org.intellij.intelliLang com.intellij.modules.java @@ -270,9 +271,17 @@ - + + + + - val lang = languageForSigil(sigilLine.sigilName()); - if (lang != null) { - registrar.startInjecting(lang) - registrar.addPlace(null, null, sigilLine, lineBody.textRangeInParent) - registrar.doneInjecting() - } + when (context) { + is SigilLine -> handleSigilLine(registrar, context) + is SigilHeredoc -> handleSigilHeredoc(registrar, context) + else -> return + } + } + + private fun handleSigilLine(registrar: MultiHostRegistrar, sigilLine: SigilLine) { + if (!sigilLine.isValidHost) return + + 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 + private fun handleSigilHeredoc(registrar: MultiHostRegistrar, sigilHeredoc: SigilHeredoc) { + if (!sigilHeredoc.isValidHost) return - // > to include newline - if (lineTextLength > prefixLength) { - val lineMarkdownText = lineText.substring(prefixLength) + val lang = languageForSigil(sigilHeredoc.sigilName()) ?: return - val lineOffset = line.textOffset - val lineOffsetRelativeToQuote = lineOffset - quoteOffset - val markdownOffsetRelativeToQuote = lineOffsetRelativeToQuote + prefixLength + // Find injectable content first + val injectableRanges = findInjectableRanges(sigilHeredoc) + if (injectableRanges.isEmpty()) return - val listStartMatcher = LIST_START_PATTERN.matcher(lineMarkdownText) + // Only start injection if we have content to inject + registrar.startInjecting(lang) + for (range in injectableRanges) { + registrar.addPlace(null, null, sigilHeredoc, range) + } + registrar.doneInjecting() + } - if (listStartMatcher.matches()) { - listIndent = listStartMatcher.group("indent").length + private fun findInjectableRanges(sigilHeredoc: SigilHeredoc): List { + val ranges = mutableListOf() + val prefixLength = sigilHeredoc.heredocPrefix.textLength + val quoteOffset = sigilHeredoc.textOffset + var listIndent = -1 + var inException = false - if (inCodeBlock) { - registrar.doneInjecting() + for (line in sigilHeredoc.heredocLineList) { + val lineTextLength = line.textLength + val lineText = line.text - inCodeBlock = false - } - } else { - if (listIndent > 0) { - val indentedMatcher = INDENTED_PATTERN.matcher(lineMarkdownText) + if (lineTextLength <= prefixLength) continue - if (indentedMatcher.matches() && indentedMatcher.group("indent").length < listIndent + 1) { - listIndent = -1 - } - } + val lineMarkdownText = lineText.substring(prefixLength) + val lineOffset = line.textOffset + val lineOffsetRelativeToQuote = lineOffset - quoteOffset + val markdownOffsetRelativeToQuote = lineOffsetRelativeToQuote + prefixLength - 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) - } - } + val listStartMatcher = LIST_START_PATTERN.matcher(lineMarkdownText) - if (!inException) { - val textRangeInQuote = - TextRange.from(elixirOffsetRelativeToQuote, lineElixirText.length) + when { + listStartMatcher.matches() -> { + listIndent = listStartMatcher.group("indent").length + } - val lang = languageForSigil(sigilHeredoc.sigilName()); - if (!inCodeBlock && lang != null) { - registrar.startInjecting(lang) + else -> { + if (listIndent > 0) { + val indentedMatcher = INDENTED_PATTERN.matcher(lineMarkdownText) + if (indentedMatcher.matches() && indentedMatcher.group("indent").length < listIndent + 1) { + listIndent = -1 + } + } - inCodeBlock = true - } + if (listIndent == -1 && lineMarkdownText.startsWith(CODE_BLOCK_INDENT)) { + val lineCodeText = lineMarkdownText.substring(CODE_BLOCK_INDENT_LENGTH) + val codeOffsetRelativeToQuote = markdownOffsetRelativeToQuote + CODE_BLOCK_INDENT_LENGTH + + when { + lineCodeText.startsWith(EXCEPTION_PREFIX) -> inException = true + 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 + ) + } - registrar.addPlace(null, null, sigilHeredoc, textRangeInQuote) + lineCodeText.startsWith(IEX_CONTINUATION) -> { + inException = false + Pair( + lineCodeText.substring(IEX_CONTINUATION_LENGTH), + codeOffsetRelativeToQuote + IEX_CONTINUATION_LENGTH + ) } + + else -> Pair(lineCodeText, codeOffsetRelativeToQuote) } - } else if (lineMarkdownText.isNotBlank()) { - if (inCodeBlock) { - registrar.doneInjecting() - inCodeBlock = false - inException = false + if (!inException) { + ranges.add(TextRange.from(elixirOffsetRelativeToQuote, lineElixirText.length)) } } } } } } - - if (inCodeBlock) { - registrar.doneInjecting() - } - - } else { - for (child in context.children) { - getLanguagesToInject(registrar, child) - } } + + return ranges } override fun elementsToInjectIn(): List> { - return listOf(PsiElement::class.java) + return listOf(SigilLine::class.java, SigilHeredoc::class.java) } - fun languageForSigil(sigilName: Char): Language? { - if (sigilName == 'H') { - return HTMLLanguage.INSTANCE - } else if (sigilName == 'L') { - return EexLanguage.INSTANCE + private fun languageForSigil(sigilName: Char): Language? { + return when (sigilName) { + 'H' -> HTMLLanguage.INSTANCE + 'L' -> EexLanguage.INSTANCE + else -> null + } } - return null - } + override fun shouldDelegateToTopLevel(file: PsiFile) = true companion object { private const val CODE_BLOCK_INDENT = " " From 3137e3c8faae200ba5dfae91cc6d84fbe9ed0824 Mon Sep 17 00:00:00 2001 From: Josh Taylor Date: Sun, 12 Jan 2025 00:02:12 +0800 Subject: [PATCH 3/7] Remove foreground, tweaks --- .../org.elixir_lang-withIntellijLang.xml | 18 +++++++++++ resources/META-INF/plugin.xml | 19 +++--------- resources/colorSchemes/ElixirDarcula.xml | 1 - resources/colorSchemes/ElixirDefault.xml | 1 - .../{ => injection}/elixirInjections.xml | 2 +- .../ElixirSigilInjectionSupport.java | 31 ------------------- .../injection/ElixirSigilInjectionSupport.kt | 27 ++++++++++++++++ .../injection/ElixirSigilInjector.kt | 15 +++++---- .../injection/ElixirSigilPatterns.java | 9 ++++-- 9 files changed, 63 insertions(+), 60 deletions(-) create mode 100644 resources/META-INF/optional/org.elixir_lang-withIntellijLang.xml rename resources/{ => injection}/elixirInjections.xml (98%) delete mode 100644 src/org/elixir_lang/injection/ElixirSigilInjectionSupport.java create mode 100644 src/org/elixir_lang/injection/ElixirSigilInjectionSupport.kt diff --git a/resources/META-INF/optional/org.elixir_lang-withIntellijLang.xml b/resources/META-INF/optional/org.elixir_lang-withIntellijLang.xml new file mode 100644 index 000000000..e37655cb1 --- /dev/null +++ b/resources/META-INF/optional/org.elixir_lang-withIntellijLang.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index e24508257..9fb9d4e82 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -10,8 +10,11 @@ on how to target different products --> com.intellij.modules.lang org.intellij.plugins.markdown - org.intellij.intelliLang + + + com.intellij.modules.java + org.intellij.intelliLang - - - - - - - - - diff --git a/resources/colorSchemes/ElixirDarcula.xml b/resources/colorSchemes/ElixirDarcula.xml index bb9a33023..ce3742ec0 100644 --- a/resources/colorSchemes/ElixirDarcula.xml +++ b/resources/colorSchemes/ElixirDarcula.xml @@ -184,7 +184,6 @@