From fa15109f0bc47df7ddf3d9aff32ba81d25e4a4c4 Mon Sep 17 00:00:00 2001 From: hgranthorner <37941012+hgranthorner@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:29:43 -0400 Subject: [PATCH] Phase 2: commenter and brace matcher Brace matching pairs the Jai-specific single-token openers .{ and .[ with the ordinary } and ]; verified headlessly through the editor highlighter. --- .../dev/hgh/jai/editor/JaiEditorSupport.kt | 52 ++++++++++++++++ src/main/resources/META-INF/plugin.xml | 3 + .../hgh/jai/editor/JaiEditorSupportTest.kt | 60 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 src/main/kotlin/dev/hgh/jai/editor/JaiEditorSupport.kt create mode 100644 src/test/kotlin/dev/hgh/jai/editor/JaiEditorSupportTest.kt diff --git a/src/main/kotlin/dev/hgh/jai/editor/JaiEditorSupport.kt b/src/main/kotlin/dev/hgh/jai/editor/JaiEditorSupport.kt new file mode 100644 index 0000000..bb23302 --- /dev/null +++ b/src/main/kotlin/dev/hgh/jai/editor/JaiEditorSupport.kt @@ -0,0 +1,52 @@ +package dev.hgh.jai.editor + +import com.intellij.lang.BracePair +import com.intellij.lang.Commenter +import com.intellij.lang.PairedBraceMatcher +import com.intellij.psi.PsiFile +import com.intellij.psi.tree.IElementType +import dev.hgh.jai.lexer.JaiTokenTypes + +/** `//` line comments and nesting `/* */` block comments (language reference §2). */ +class JaiCommenter : Commenter { + override fun getLineCommentPrefix(): String = "//" + + override fun getBlockCommentPrefix(): String = "/*" + + override fun getBlockCommentSuffix(): String = "*/" + + // Jai block comments nest, so a commented-out region can be commented again. + override fun getCommentedBlockCommentPrefix(): String = "/*" + + override fun getCommentedBlockCommentSuffix(): String = "*/" +} + +/** + * Brace matching. Note the two Jai-specific openers: `.{` starts a struct literal and + * `.[` starts an array literal (language reference §14.4) — they are single tokens, and + * close with an ordinary `}` / `]`. + */ +class JaiBraceMatcher : PairedBraceMatcher { + override fun getPairs(): Array = PAIRS + + override fun isPairedBracesAllowedBeforeType( + lbraceType: IElementType, + next: IElementType?, + ): Boolean = true + + override fun getCodeConstructStart( + file: PsiFile?, + openingBraceOffset: Int, + ): Int = openingBraceOffset + + companion object { + private val PAIRS = + arrayOf( + BracePair(JaiTokenTypes.LBRACE, JaiTokenTypes.RBRACE, true), + BracePair(JaiTokenTypes.LPAREN, JaiTokenTypes.RPAREN, false), + BracePair(JaiTokenTypes.LBRACKET, JaiTokenTypes.RBRACKET, false), + BracePair(JaiTokenTypes.BEGIN_STRUCT_LITERAL, JaiTokenTypes.RBRACE, false), + BracePair(JaiTokenTypes.BEGIN_ARRAY_LITERAL, JaiTokenTypes.RBRACKET, false), + ) + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index aa68520..fc7c305 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -26,6 +26,9 @@ + + + diff --git a/src/test/kotlin/dev/hgh/jai/editor/JaiEditorSupportTest.kt b/src/test/kotlin/dev/hgh/jai/editor/JaiEditorSupportTest.kt new file mode 100644 index 0000000..1d55f6b --- /dev/null +++ b/src/test/kotlin/dev/hgh/jai/editor/JaiEditorSupportTest.kt @@ -0,0 +1,60 @@ +package dev.hgh.jai.editor + +import com.intellij.codeInsight.highlighting.BraceMatchingUtil +import com.intellij.lang.LanguageBraceMatching +import com.intellij.lang.LanguageCommenters +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import dev.hgh.jai.JaiFileType +import dev.hgh.jai.JaiLanguage + +class JaiEditorSupportTest : BasePlatformTestCase() { + fun testCommenterIsRegistered() { + val commenter = LanguageCommenters.INSTANCE.forLanguage(JaiLanguage) + assertTrue("expected JaiCommenter, got $commenter", commenter is JaiCommenter) + assertEquals("//", commenter.lineCommentPrefix) + assertEquals("/*", commenter.blockCommentPrefix) + assertEquals("*/", commenter.blockCommentSuffix) + } + + fun testBraceMatcherIsRegistered() { + val matcher = LanguageBraceMatching.INSTANCE.forLanguage(JaiLanguage) + assertTrue("expected JaiBraceMatcher, got $matcher", matcher is JaiBraceMatcher) + } + + fun testMatchesOrdinaryBraces() { + assertMatchForward("main :: () |{ x := 1; }", "main :: () { x := 1; |}") + } + + fun testMatchesStructLiteralBrace() { + // `.{` is one token; it still has to pair with the ordinary closing brace. + assertMatchForward("p := Vector2.|{ 1, 2 };", "p := Vector2.{ 1, 2 |};") + } + + fun testMatchesArrayLiteralBracket() { + assertMatchForward("names := .|[ \"a\", \"b\" ];", "names := .[ \"a\", \"b\" |];") + } + + /** + * Both arguments are the same text with a `|` marking the caret; the first marks the + * opening brace, the second the offset the matcher must jump to. + */ + private fun assertMatchForward( + withOpen: String, + withClose: String, + ) { + val open = withOpen.indexOf('|') + val close = withClose.indexOf('|') + val text = withOpen.replace("|", "") + assertEquals("the two markers must describe the same text", text, withClose.replace("|", "")) + + myFixture.configureByText("braces.jai", text) + val editor = myFixture.editor as EditorEx + val iterator = editor.highlighter.createIterator(open) + assertTrue( + "no forward match found for the brace at $open in $withOpen", + BraceMatchingUtil.matchBrace(editor.document.charsSequence, JaiFileType, iterator, true), + ) + assertEquals(close, iterator.start) + } +}