From 711334885b7f9506afa546ba7ecaf5ebaa60c2cf Mon Sep 17 00:00:00 2001 From: hgranthorner <37941012+hgranthorner@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:19:11 -0400 Subject: [PATCH] Phase 0: strip JetBrains template, register Jai language and file type - delete demo tool window + message bundle, rewrite plugin.xml metadata - add JaiLanguage, JaiFileType, file icon, JaiTokenTypes (from the compiler's own lexer: modules/Jai_Lexer/module.jai) - add ./jaitest: runs tests and reports the JUnit XML, not just the exit code - ignore .kotlin --- .gitignore | 3 +- jaitest | 46 +++ src/main/kotlin/MyMessageBundle.kt | 22 - src/main/kotlin/MyToolWindowFactory.kt | 37 -- src/main/kotlin/dev/hgh/jai/JaiFileType.kt | 20 + src/main/kotlin/dev/hgh/jai/JaiLanguage.kt | 11 + .../kotlin/dev/hgh/jai/lexer/JaiTokenTypes.kt | 390 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 28 +- src/main/resources/icons/jai.svg | 6 + .../messages/MyMessageBundle.properties | 3 - .../kotlin/dev/hgh/jai/JaiFileTypeTest.kt | 25 ++ 11 files changed, 510 insertions(+), 81 deletions(-) create mode 100755 jaitest delete mode 100644 src/main/kotlin/MyMessageBundle.kt delete mode 100644 src/main/kotlin/MyToolWindowFactory.kt create mode 100644 src/main/kotlin/dev/hgh/jai/JaiFileType.kt create mode 100644 src/main/kotlin/dev/hgh/jai/JaiLanguage.kt create mode 100644 src/main/kotlin/dev/hgh/jai/lexer/JaiTokenTypes.kt create mode 100644 src/main/resources/icons/jai.svg delete mode 100644 src/main/resources/messages/MyMessageBundle.properties create mode 100644 src/test/kotlin/dev/hgh/jai/JaiFileTypeTest.kt diff --git a/.gitignore b/.gitignore index 42685c0..55f0c72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .gradle .idea .intellijPlatform -build \ No newline at end of file +.kotlin +build diff --git a/jaitest b/jaitest new file mode 100755 index 0000000..22cacdb --- /dev/null +++ b/jaitest @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Run the test suite and report the JUnit XML, not just the exit code. +# +# A green `test` task does not prove tests ran (see AGENTS.md), so this always +# prints tests/failures/errors per suite and dumps failure messages. +# +# Usage: ./jaitest [gradle args...] e.g. ./jaitest --tests '*Lexer*' +set -uo pipefail + +cd "$(dirname "$0")" || exit 1 + +rm -rf build/test-results/test +./jaigradle test "$@" 2>&1 | grep -vE '^\[[0-9.]+s\]\[warning\]\[cds\]' +gradle_status=${PIPESTATUS[0]} + +echo +echo "=== JUnit XML ===" +shopt -s nullglob +xml=(build/test-results/test/*.xml) +if [ ${#xml[@]} -eq 0 ]; then + echo "NO TEST XML PRODUCED — tests did not run." + exit 1 +fi + +total=0 +bad=0 +for f in "${xml[@]}"; do + line=$(sed -n '2p' "$f") + name=$(sed -nE 's/^ { - return instance.getLazyMessage(key, *params) - } -} diff --git a/src/main/kotlin/MyToolWindowFactory.kt b/src/main/kotlin/MyToolWindowFactory.kt deleted file mode 100644 index 02b006c..0000000 --- a/src/main/kotlin/MyToolWindowFactory.kt +++ /dev/null @@ -1,37 +0,0 @@ -package dev.hgh - -import com.intellij.openapi.project.Project -import com.intellij.openapi.wm.ToolWindow -import com.intellij.openapi.wm.ToolWindowFactory -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBPanel -import com.intellij.ui.content.ContentFactory -import javax.swing.JButton -import kotlin.random.Random - -class MyToolWindowFactory : ToolWindowFactory { - override fun shouldBeAvailable(project: Project) = true - - override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - val myToolWindow = MyToolWindow() - val content = ContentFactory.getInstance().createContent(myToolWindow.getContent(), null, false) - toolWindow.contentManager.addContent(content) - } - - class MyToolWindow { - private val content = JBPanel>().apply { - val label = JBLabel(MyMessageBundle.message("toolwindow.MyToolWindow.number.label", "?")) - - add(label) - add(JButton(MyMessageBundle.message("toolwindow.MyToolWindow.shuffle.button")).apply { - addActionListener { - label.text = MyMessageBundle.message( - "toolwindow.MyToolWindow.number.label", Random(System.currentTimeMillis()).nextInt(1000) - ) - } - }) - } - - fun getContent(): JBPanel> = content - } -} diff --git a/src/main/kotlin/dev/hgh/jai/JaiFileType.kt b/src/main/kotlin/dev/hgh/jai/JaiFileType.kt new file mode 100644 index 0000000..d37b640 --- /dev/null +++ b/src/main/kotlin/dev/hgh/jai/JaiFileType.kt @@ -0,0 +1,20 @@ +package dev.hgh.jai + +import com.intellij.openapi.fileTypes.LanguageFileType +import com.intellij.openapi.util.IconLoader +import javax.swing.Icon + +object JaiIcons { + @JvmField + val FILE: Icon = IconLoader.getIcon("/icons/jai.svg", JaiIcons::class.java) +} + +object JaiFileType : LanguageFileType(JaiLanguage) { + override fun getName(): String = "Jai" + + override fun getDescription(): String = "Jai source file" + + override fun getDefaultExtension(): String = "jai" + + override fun getIcon(): Icon = JaiIcons.FILE +} diff --git a/src/main/kotlin/dev/hgh/jai/JaiLanguage.kt b/src/main/kotlin/dev/hgh/jai/JaiLanguage.kt new file mode 100644 index 0000000..f78259b --- /dev/null +++ b/src/main/kotlin/dev/hgh/jai/JaiLanguage.kt @@ -0,0 +1,11 @@ +package dev.hgh.jai + +import com.intellij.lang.Language + +object JaiLanguage : Language("Jai") { + private fun readResolve(): Any = JaiLanguage + + override fun getDisplayName(): String = "Jai" + + override fun isCaseSensitive(): Boolean = true +} diff --git a/src/main/kotlin/dev/hgh/jai/lexer/JaiTokenTypes.kt b/src/main/kotlin/dev/hgh/jai/lexer/JaiTokenTypes.kt new file mode 100644 index 0000000..33ca631 --- /dev/null +++ b/src/main/kotlin/dev/hgh/jai/lexer/JaiTokenTypes.kt @@ -0,0 +1,390 @@ +package dev.hgh.jai.lexer + +import com.intellij.psi.tree.IElementType +import com.intellij.psi.tree.TokenSet +import dev.hgh.jai.JaiLanguage + +class JaiTokenType( + debugName: String, +) : IElementType(debugName, JaiLanguage) { + override fun toString(): String = "Jai:" + super.toString() +} + +/** + * The Jai token set. + * + * Derived from the compiler's own lexer, `~/.local/jai/modules/Jai_Lexer/module.jai` + * (`Token_Type` at :17, `check_for_keyword` at :758, `compose_new_token` at :1503). + * See `docs/JAI_LANGUAGE_REFERENCE.md` §3/§4. + * + * Deliberate divergences from the compiler lexer, all in favour of the IDE: + * - `#ident` is a single [DIRECTIVE] token (the compiler emits `#` then an ident). + * - `#string ID ... ID` is a single [HERE_STRING] token, so the lexer never has to + * carry the terminator across a token boundary and can restart anywhere. + * - `::` and `:=` are single tokens (the compiler emits separate `:` `:` / `:` `=`). + * - The backtick of a backticked identifier is its own [BACKTICK] token (the compiler + * folds it into the identifier token as a flag). + */ +object JaiTokenTypes { + // --- trivia ----------------------------------------------------------- + @JvmField val LINE_COMMENT = JaiTokenType("LINE_COMMENT") + + @JvmField val BLOCK_COMMENT = JaiTokenType("BLOCK_COMMENT") + + // --- literals and names ----------------------------------------------- + @JvmField val IDENT = JaiTokenType("IDENT") + + @JvmField val NUMBER = JaiTokenType("NUMBER") + + @JvmField val STRING = JaiTokenType("STRING") + + @JvmField val HERE_STRING = JaiTokenType("HERE_STRING") + + @JvmField val DIRECTIVE = JaiTokenType("DIRECTIVE") + + @JvmField val NOTE = JaiTokenType("NOTE") + + // --- keywords --------------------------------------------------------- + @JvmField val KW_IF = JaiTokenType("if") + + @JvmField val KW_XX = JaiTokenType("xx") + + @JvmField val KW_IFX = JaiTokenType("ifx") + + @JvmField val KW_FOR = JaiTokenType("for") + + @JvmField val KW_THEN = JaiTokenType("then") + + @JvmField val KW_ELSE = JaiTokenType("else") + + @JvmField val KW_NULL = JaiTokenType("null") + + @JvmField val KW_CASE = JaiTokenType("case") + + @JvmField val KW_ENUM = JaiTokenType("enum") + + @JvmField val KW_TRUE = JaiTokenType("true") + + @JvmField val KW_CAST = JaiTokenType("cast") + + @JvmField val KW_WHILE = JaiTokenType("while") + + @JvmField val KW_BREAK = JaiTokenType("break") + + @JvmField val KW_USING = JaiTokenType("using") + + @JvmField val KW_DEFER = JaiTokenType("defer") + + @JvmField val KW_FALSE = JaiTokenType("false") + + @JvmField val KW_UNION = JaiTokenType("union") + + @JvmField val KW_RETURN = JaiTokenType("return") + + @JvmField val KW_STRUCT = JaiTokenType("struct") + + @JvmField val KW_REMOVE = JaiTokenType("remove") + + @JvmField val KW_INLINE = JaiTokenType("inline") + + @JvmField val KW_SIZE_OF = JaiTokenType("size_of") + + @JvmField val KW_TYPE_OF = JaiTokenType("type_of") + + @JvmField val KW_CODE_OF = JaiTokenType("code_of") + + @JvmField val KW_CONTEXT = JaiTokenType("context") + + @JvmField val KW_CONTINUE = JaiTokenType("continue") + + @JvmField val KW_OPERATOR = JaiTokenType("operator") + + @JvmField val KW_TYPE_INFO = JaiTokenType("type_info") + + @JvmField val KW_NO_INLINE = JaiTokenType("no_inline") + + @JvmField val KW_INTERFACE = JaiTokenType("interface") + + @JvmField val KW_ENUM_FLAGS = JaiTokenType("enum_flags") + + @JvmField val KW_IS_CONSTANT = JaiTokenType("is_constant") + + @JvmField val KW_PUSH_CONTEXT = JaiTokenType("push_context") + + @JvmField val KW_INITIALIZER_OF = JaiTokenType("initializer_of") + + /** `name` -> keyword token. Exactly `check_for_keyword`, nothing more. */ + @JvmField + val KEYWORD_MAP: Map = + mapOf( + "if" to KW_IF, + "xx" to KW_XX, + "ifx" to KW_IFX, + "for" to KW_FOR, + "then" to KW_THEN, + "else" to KW_ELSE, + "null" to KW_NULL, + "case" to KW_CASE, + "enum" to KW_ENUM, + "true" to KW_TRUE, + "cast" to KW_CAST, + "while" to KW_WHILE, + "break" to KW_BREAK, + "using" to KW_USING, + "defer" to KW_DEFER, + "false" to KW_FALSE, + "union" to KW_UNION, + "return" to KW_RETURN, + "struct" to KW_STRUCT, + "remove" to KW_REMOVE, + "inline" to KW_INLINE, + "size_of" to KW_SIZE_OF, + "type_of" to KW_TYPE_OF, + "code_of" to KW_CODE_OF, + "context" to KW_CONTEXT, + "continue" to KW_CONTINUE, + "operator" to KW_OPERATOR, + "type_info" to KW_TYPE_INFO, + "no_inline" to KW_NO_INLINE, + "interface" to KW_INTERFACE, + "enum_flags" to KW_ENUM_FLAGS, + "is_constant" to KW_IS_CONSTANT, + "push_context" to KW_PUSH_CONTEXT, + "initializer_of" to KW_INITIALIZER_OF, + ) + + /** + * Built-in type names. These are ordinary identifiers to the compiler — they are + * *not* reserved — but they get their own highlight. Kept as a name set, not a + * token type, so the parser can never depend on them being keywords. + * See `docs/JAI_LANGUAGE_REFERENCE.md` §3. + */ + @JvmField + val BUILTIN_TYPE_NAMES: Set = + setOf( + "s8", + "s16", + "s32", + "s64", + "u8", + "u16", + "u32", + "u64", + "int", + "float", + "float32", + "float64", + "bool", + "string", + "void", + "Type", + "Any", + "Code", + ) + + /** Implicit `for` loop variables. Ordinary identifiers, highlighted specially. */ + @JvmField + val LOOP_VARIABLE_NAMES: Set = setOf("it", "it_index") + + // --- operators and punctuation ---------------------------------------- + @JvmField val PLUS = JaiTokenType("+") + + @JvmField val MINUS = JaiTokenType("-") + + @JvmField val STAR = JaiTokenType("*") + + @JvmField val SLASH = JaiTokenType("/") + + @JvmField val PERCENT = JaiTokenType("%") + + @JvmField val PLUS_EQ = JaiTokenType("+=") + + @JvmField val MINUS_EQ = JaiTokenType("-=") + + @JvmField val STAR_EQ = JaiTokenType("*=") + + @JvmField val SLASH_EQ = JaiTokenType("/=") + + @JvmField val PERCENT_EQ = JaiTokenType("%=") + + @JvmField val EQ = JaiTokenType("=") + + @JvmField val EQ_EQ = JaiTokenType("==") + + @JvmField val EQ_EQ_EQ = JaiTokenType("===") + + @JvmField val NOT_EQ = JaiTokenType("!=") + + @JvmField val NOT = JaiTokenType("!") + + @JvmField val LT = JaiTokenType("<") + + @JvmField val GT = JaiTokenType(">") + + @JvmField val LT_EQ = JaiTokenType("<=") + + @JvmField val GT_EQ = JaiTokenType(">=") + + @JvmField val AND_AND = JaiTokenType("&&") + + @JvmField val OR_OR = JaiTokenType("||") + + @JvmField val AND_AND_EQ = JaiTokenType("&&=") + + @JvmField val OR_OR_EQ = JaiTokenType("||=") + + @JvmField val AND = JaiTokenType("&") + + @JvmField val OR = JaiTokenType("|") + + @JvmField val XOR = JaiTokenType("^") + + @JvmField val TILDE = JaiTokenType("~") + + @JvmField val AND_EQ = JaiTokenType("&=") + + @JvmField val OR_EQ = JaiTokenType("|=") + + @JvmField val XOR_EQ = JaiTokenType("^=") + + @JvmField val SHL = JaiTokenType("<<") + + @JvmField val SHR = JaiTokenType(">>") + + @JvmField val SHL_EQ = JaiTokenType("<<=") + + @JvmField val SHR_EQ = JaiTokenType(">>=") + + @JvmField val ROL = JaiTokenType("<<<") + + @JvmField val ROR = JaiTokenType(">>>") + + @JvmField val ROL_EQ = JaiTokenType("<<<=") + + @JvmField val ROR_EQ = JaiTokenType(">>>=") + + @JvmField val RIGHT_ARROW = JaiTokenType("->") + + @JvmField val QUICK_LAMBDA = JaiTokenType("=>") + + @JvmField val DOUBLE_DOT = JaiTokenType("..") + + @JvmField val TRIPLE_MINUS = JaiTokenType("---") + + @JvmField val DOUBLE_MINUS = JaiTokenType("--") + + @JvmField val DOUBLE_COMMA = JaiTokenType(",,") + + @JvmField val BEGIN_STRUCT_LITERAL = JaiTokenType(".{") + + @JvmField val BEGIN_ARRAY_LITERAL = JaiTokenType(".[") + + @JvmField val POSTFIX_DEREFERENCE = JaiTokenType(".*") + + @JvmField val DOLLAR = JaiTokenType("$") + + @JvmField val DOUBLE_DOLLAR = JaiTokenType("$$") + + @JvmField val COLON = JaiTokenType(":") + + @JvmField val COLON_COLON = JaiTokenType("::") + + @JvmField val COLON_EQ = JaiTokenType(":=") + + @JvmField val SEMICOLON = JaiTokenType(";") + + @JvmField val COMMA = JaiTokenType(",") + + @JvmField val DOT = JaiTokenType(".") + + @JvmField val QUESTION = JaiTokenType("?") + + @JvmField val BACKTICK = JaiTokenType("`") + + @JvmField val BACKSLASH = JaiTokenType("\\") + + @JvmField val HASH = JaiTokenType("#") + + @JvmField val AT = JaiTokenType("@") + + @JvmField val LPAREN = JaiTokenType("(") + + @JvmField val RPAREN = JaiTokenType(")") + + @JvmField val LBRACKET = JaiTokenType("[") + + @JvmField val RBRACKET = JaiTokenType("]") + + @JvmField val LBRACE = JaiTokenType("{") + + @JvmField val RBRACE = JaiTokenType("}") + + // --- token sets ------------------------------------------------------- + @JvmField val COMMENTS: TokenSet = TokenSet.create(LINE_COMMENT, BLOCK_COMMENT) + + @JvmField val STRINGS: TokenSet = TokenSet.create(STRING, HERE_STRING) + + @JvmField val KEYWORDS: TokenSet = TokenSet.create(*KEYWORD_MAP.values.toTypedArray()) + + @JvmField + val OPERATORS: TokenSet = + TokenSet.create( + PLUS, + MINUS, + STAR, + SLASH, + PERCENT, + PLUS_EQ, + MINUS_EQ, + STAR_EQ, + SLASH_EQ, + PERCENT_EQ, + EQ, + EQ_EQ, + EQ_EQ_EQ, + NOT_EQ, + NOT, + LT, + GT, + LT_EQ, + GT_EQ, + AND_AND, + OR_OR, + AND_AND_EQ, + OR_OR_EQ, + AND, + OR, + XOR, + TILDE, + AND_EQ, + OR_EQ, + XOR_EQ, + SHL, + SHR, + SHL_EQ, + SHR_EQ, + ROL, + ROR, + ROL_EQ, + ROR_EQ, + RIGHT_ARROW, + QUICK_LAMBDA, + DOUBLE_DOT, + DOUBLE_MINUS, + BEGIN_STRUCT_LITERAL, + BEGIN_ARRAY_LITERAL, + POSTFIX_DEREFERENCE, + DOLLAR, + DOUBLE_DOLLAR, + COLON, + COLON_COLON, + COLON_EQ, + DOUBLE_COMMA, + DOT, + QUESTION, + BACKTICK, + BACKSLASH, + HASH, + AT, + ) +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index fabc42d..e3f1e0a 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,33 +1,25 @@ - dev.hgh.intellijai - - Intellijai + Jai - - YourCompany + hgh - - most HTML tags may be used + Support for the Jai programming language.
+ Syntax highlighting for .jai files. ]]>
- com.intellij.modules.platform - messages.MyMessageBundle - - - +
diff --git a/src/main/resources/icons/jai.svg b/src/main/resources/icons/jai.svg new file mode 100644 index 0000000..a8addd1 --- /dev/null +++ b/src/main/resources/icons/jai.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/messages/MyMessageBundle.properties b/src/main/resources/messages/MyMessageBundle.properties deleted file mode 100644 index 9d4b0fa..0000000 --- a/src/main/resources/messages/MyMessageBundle.properties +++ /dev/null @@ -1,3 +0,0 @@ -toolwindow.stripe.MyToolWindow=My Tool Window -toolwindow.MyToolWindow.number.label=The random number is: {0} -toolwindow.MyToolWindow.shuffle.button=Shuffle diff --git a/src/test/kotlin/dev/hgh/jai/JaiFileTypeTest.kt b/src/test/kotlin/dev/hgh/jai/JaiFileTypeTest.kt new file mode 100644 index 0000000..ba50ea9 --- /dev/null +++ b/src/test/kotlin/dev/hgh/jai/JaiFileTypeTest.kt @@ -0,0 +1,25 @@ +package dev.hgh.jai + +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +/** + * Verifies plugin.xml wiring: the `.jai` extension resolves to our file type and + * our language, without launching an IDE. + */ +class JaiFileTypeTest : BasePlatformTestCase() { + fun testJaiExtensionIsRegistered() { + val type = FileTypeManager.getInstance().getFileTypeByExtension("jai") + assertEquals(JaiFileType, type) + } + + /** + * The *virtual* file already resolves to our type. The PSI file's language stays + * plain text until a ParserDefinition is registered (phase 3) — asserting that here + * would be asserting the platform's fallback, not our wiring. + */ + fun testVirtualFileUsesJaiFileType() { + val file = myFixture.configureByText("sample.jai", "main :: () { }") + assertEquals(JaiFileType, file.virtualFile.fileType) + } +}