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
This commit is contained in:
hgranthorner
2026-08-04 10:19:11 -04:00
parent cb5a1d9555
commit 711334885b
11 changed files with 510 additions and 81 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.gradle
.idea
.intellijPlatform
build
.kotlin
build

46
jaitest Executable file
View File

@@ -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/^<testsuite name="([^"]*)".*/\1/p' <<<"$line")
t=$(sed -nE 's/.*tests="([0-9]*)".*/\1/p' <<<"$line")
fa=$(sed -nE 's/.*failures="([0-9]*)".*/\1/p' <<<"$line")
er=$(sed -nE 's/.*errors="([0-9]*)".*/\1/p' <<<"$line")
sk=$(sed -nE 's/.*skipped="([0-9]*)".*/\1/p' <<<"$line")
printf '%-60s tests=%s skipped=%s failures=%s errors=%s\n' "$name" "$t" "$sk" "$fa" "$er"
total=$((total + t))
bad=$((bad + fa + er))
if [ "$fa" != "0" ] || [ "$er" != "0" ]; then
grep -E '<(failure|error) ' "$f" | head -40
fi
done
echo "--- total tests=$total failures+errors=$bad gradle_exit=$gradle_status"
if [ "$total" -eq 0 ] || [ "$bad" -ne 0 ] || [ "$gradle_status" -ne 0 ]; then
exit 1
fi
echo "OK"

View File

@@ -1,22 +0,0 @@
package dev.hgh
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.PropertyKey
import java.util.function.Supplier
private const val BUNDLE = "messages.MyMessageBundle"
internal object MyMessageBundle {
private val instance = DynamicBundle(MyMessageBundle::class.java, BUNDLE)
@JvmStatic
fun message(key: @PropertyKey(resourceBundle = BUNDLE) String, vararg params: Any?): @Nls String {
return instance.getMessage(key, *params)
}
@JvmStatic
fun lazyMessage(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any?): Supplier<@Nls String> {
return instance.getLazyMessage(key, *params)
}
}

View File

@@ -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<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<JBPanel<*>> = content
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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<String, JaiTokenType> =
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<String> =
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<String> = 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,
)
}

View File

@@ -1,33 +1,25 @@
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
<idea-plugin>
<!-- Unique identifier of the plugin. It should be FQN. It cannot be changed between the plugin versions. -->
<id>dev.hgh.intellijai</id>
<!-- Public plugin name should be written in Title Case.
Guidelines: https://plugins.jetbrains.com/docs/marketplace/best-practices-for-listing.html#plugin-name -->
<name>Intellijai</name>
<name>Jai</name>
<!-- A displayed Vendor name or Organization ID displayed on the Plugins Page. -->
<vendor url="https://www.yourcompany.com">YourCompany</vendor>
<vendor>hgh</vendor>
<!-- Description of the plugin displayed on the Plugin Page and IDE Plugin Manager.
Guidelines: https://plugins.jetbrains.com/docs/marketplace/best-practices-for-listing.html#plugin-description -->
<description><![CDATA[
Enter short description for your plugin here.<br>
<em>most HTML tags may be used</em>
Support for the <b>Jai</b> programming language.<br/>
Syntax highlighting for <code>.jai</code> files.
]]></description>
<!-- Product and plugin compatibility requirements.
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html -->
<depends>com.intellij.modules.platform</depends>
<resource-bundle>messages.MyMessageBundle</resource-bundle>
<!-- Extensions defined by the plugin.
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html -->
<extensions defaultExtensionNs="com.intellij">
<toolWindow id="MyToolWindow" factoryClass="dev.hgh.MyToolWindowFactory"
icon="AllIcons.Toolwindows.ToolWindowPalette"/>
<fileType
name="Jai"
implementationClass="dev.hgh.jai.JaiFileType"
fieldName="INSTANCE"
language="Jai"
extensions="jai"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="1" y="1" width="14" height="14" rx="3" fill="none" stroke="#3574F0" stroke-width="1.2"/>
<path d="M6.2 4.2h1.5v5.1a2.4 2.4 0 0 1-2.4 2.4H5v-1.4h.2a1.1 1.1 0 0 0 1-1.1z" fill="#3574F0"/>
<rect x="9.4" y="4.2" width="1.5" height="1.5" fill="#3574F0"/>
<rect x="9.4" y="6.4" width="1.5" height="5.3" fill="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 422 B

View File

@@ -1,3 +0,0 @@
toolwindow.stripe.MyToolWindow=My Tool Window
toolwindow.MyToolWindow.number.label=The random number is: {0}
toolwindow.MyToolWindow.shuffle.button=Shuffle

View File

@@ -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)
}
}