# Build Plan — Jai Language Plugin for IntelliJ **Design constraint:** every milestone must be verifiable by an agent from the command line. No manual IDE launch, no human eyeballing. This drives the architecture below, not just the CI config. Companion doc: `docs/JAI_LANGUAGE_REFERENCE.md` (language facts, derived from `~/.local/jai`). --- ## 0. Verified starting conditions Checked empirically before writing this plan: | Fact | Status | | --- | --- | | Scaffold is the bare JetBrains template (demo tool window) | confirmed | | Gradle 9.6.1, IntelliJ Platform 2025.3.5, Kotlin 2.3.20, IPGP 2.18.1 | confirmed | | `./gradlew help` succeeds | confirmed | | **No JDK on `PATH`; `JAVA_HOME` unset** | ⚠️ **blocker** | | Adoptium JDK 21.0.12 present at `~/.gradle/jdks/…` | confirmed working | | `org.gradle.java.home` in `gradle.properties` does **not** fix it | confirmed — `gradlew` needs a JVM to launch itself | | Wrapper script that auto-discovers the JDK **does** fix it | confirmed, `BUILD SUCCESSFUL` | | `generateLexer` / `generateParser` available via bundled `org.jetbrains.intellij.platform.grammarkit` | confirmed by `gradlew tasks --all` | | Not a git repository | ⚠️ blocker for safe agent iteration | | Jai corpus: **714 `.jai` files, 16.4 MB** | confirmed | The 714-file corpus is the single most important asset for this project — see §3. --- ## 1. Phase 0 — Make the project agent-operable (do this first) Nothing else is testable until this is done. 1. **`./jaigradle`** — committed wrapper that resolves a JDK then delegates to `./gradlew`. Verified working. Every later instruction uses this, so an agent never has to think about `JAVA_HOME`. 2. **`git init`** + `.gitignore` for `build/`, `.gradle/`, `.intellijPlatform/`, and generated sources. Agents need a clean diff and a rollback point. 3. **`AGENTS.md`** — the contract for future agents: the one command to run (`./jaigradle check`), where the corpus lives, where generated code lives and that it must never be hand-edited, and a pointer to the language reference. 4. **Strip the template** — delete `MyToolWindowFactory`, the demo tool window registration, and the message bundle stub; rewrite `plugin.xml` metadata. 5. **Prove the harness** — add one trivial passing test and confirm `./jaigradle test` runs it headlessly. This validates the whole loop before any real work. **Exit gate:** `./jaigradle check` green, from a shell with no `JAVA_HOME`. --- ## 2. Architecture decisions (chosen for testability) ### 2.1 Hand-written lexer, not JFlex — recommended Standard practice is JFlex, but two Jai features argue against it: - **Here-strings have a dynamic terminator.** `#string DONE … DONE` where the terminator is an arbitrary identifier captured at runtime. JFlex is a DFA generator; it cannot match a token captured earlier. This is only expressible as hand-written Java inside a JFlex action — i.e. the hard part isn't in the grammar anyway. - **Nested block comments** need a depth counter, again custom action code. Add the practical argument: the lexer is the component agents will iterate on most, and a hand-written `LexerBase` removes a codegen round-trip from that loop. It is also directly unit-testable with plain JUnit. I'd write `JaiLexer extends LexerBase` by hand, with `JaiTokenTypes` as the shared token holder. If this turns out badly we can retreat to JFlex later — the token set and tests stay valid either way, so the decision is reversible. ### 2.2 Grammar-Kit BNF for parser + PSI — recommended Here codegen genuinely pays: Grammar-Kit generates the PSI class hierarchy, which is a large amount of boilerplate. It's the standard path, and I verified the tasks run headlessly from our existing plugin (no extra dependency — the ID is `org.jetbrains.intellij.platform.grammarkit`, bundled since IPGP 2.12.0). The `.bnf` references the hand-written lexer's token types. Generated sources go to a dedicated `src/main/gen` root, git-tracked so diffs are reviewable, and regenerated by `./jaigradle generateParser`. Caveat: Grammar-Kit does not support two-pass generation, so no method mixins — use the `mixin`/`extends` attributes instead. ### 2.3 Sequencing: ship a useful plugin before touching the parser Syntax highlighting needs only the lexer. That's most of the perceived value and it de-risks the schedule. The parser is Phase 3, not Phase 1. --- ## 3. The testing strategy This is the part that answers "agents test it without me booting an IDE." ### Tier 0 — Corpus invariants (highest value, no IDE, plain JUnit) Run the lexer across all 714 real `.jai` files and assert: 1. **Round-trip:** concatenating every token's text reproduces the source file **byte for byte.** An IntelliJ lexer must tile the input with no gaps or overlaps; violating this corrupts the editor. Over 16 MB of real code this one assertion catches the overwhelming majority of lexer bugs. 2. **No `BAD_CHARACTER`** tokens anywhere in the distribution. 3. **Progress:** every `advance()` strictly increases the offset — catches infinite loops, which otherwise hang the IDE rather than failing visibly. This is the flagship gate. It's fast, deterministic, needs no IntelliJ fixture, and it is *real-world* coverage rather than toy snippets. Failures should report file, line, and column. ### Tier 1 — Golden token dumps (`LexerTestCase`) Hand-picked adversarial snippets, one per gotcha in the reference doc §14: nested comments, here-strings, `---` vs `--` vs `->` vs `-=`, `,,`, `.{`/`.[`/ `.IDENT`, `#char "a"` (and that `'` is *not* a delimiter), `==` before `{`, hex/binary/hexfloat/underscored numerics, backticked identifiers. Golden files make regressions obvious in a diff. ### Tier 2 — Parsing tests (`ParsingTestCase`) Fixture `.jai` files with expected PSI trees in `.txt`. Useful property for agents: **`ParsingTestCase` writes the expected file automatically if missing**, so adding a case is cheap — but the generated tree must be reviewed before committing, or the test asserts nothing. ### Tier 3 — Corpus parse gate Parse all 714 files, assert **zero `PsiErrorElement`**. This is the acceptance criterion for "the grammar is correct," and no human inspection can match it. Expect to allow a small, explicit, shrinking allowlist of known-unsupported files rather than blocking the phase on 100% — but the allowlist must be committed and visible so it can't quietly grow. ### Tier 4 — Code-insight tests (`BasePlatformTestCase`, headless) `myFixture` drives annotators, folding, brace matching, commenter, completion, formatter, and rename entirely headlessly. Highlighting is verified with `checkHighlighting` against ``-annotated fixtures — this is how you check colors without looking at a screen. ### Tier 5 — Packaging and compatibility `./jaigradle verifyPlugin` (Plugin Verifier — catches API misuse and compatibility breaks) and `buildPlugin` (produces the installable ZIP). ### Single entry point `./jaigradle check` runs Tiers 0–4. Agents run one command; `runIde` exists for you but is never required of an agent. --- ## 4. Phased milestones Each phase has a machine-checkable gate. Do not advance without a green gate. | Phase | Deliverable | Gate | | --- | --- | --- | | **0** | Env fix, git, AGENTS.md, template stripped | `./jaigradle check` green with no `JAVA_HOME` | | **1** | File type, icon, `JaiTokenTypes`, hand-written lexer | Tier 0 corpus invariants pass on all 714 files | | **2** | `SyntaxHighlighter`, color settings page, commenter, brace matcher | Tier 1 golden dumps; highlighter maps every token type | | **3** | `.bnf` grammar, generated parser + PSI, `ParserDefinition` | Tier 2 golden trees; Tier 3 corpus parse ≥ target — **done, 100.0% (714/714)** | | **4** | Structure view, folding, `#import`/`#load` reference resolution + go-to-definition | Tier 4 fixture tests — **done** | | **5** | Completion (keywords, directives, module names, visible declarations, parameters, module aliases, incomplete-expression recovery), rename, find-usages | Tier 4 fixture tests — **done** (84-test suite) | | **5a** | Configurable Jai module/import roots and indexed external search scope | Tier 4 settings, resolution, completion, navigation, and find-usages tests — **done** (93-test suite) | | **6** | Formatter, code style settings | Formatter round-trip — **done**, formatting all 714 corpus files is idempotent | | **7** | Inspections, quick fixes, live templates | Tier 4 + `verifyPlugin` — initial unresolved `#import`/`#load` path inspection implemented | | **8** | Optional: run-configuration to invoke the `jai` compiler, parse its error output | Integration test against `~/.local/jai/bin` | Phase 6's idempotence check (format twice, assert no change) is another corpus-scale invariant that needs no human judgment. `JaiFormatterTest` runs this check across all 714 files and also covers formatter registration, representative spacing/indentation, directive flags, strings, and opaque `#asm` bodies. --- ### Completion follow-up (not yet scheduled) Phase 5 completion now covers keywords, compiler directives, `#import`/`#load` module and file paths, visible same-file or imported top-level declarations, procedure parameters, and members of imported module aliases. Struct-field completion remains a possible follow-up, but it is not currently a committed milestone. ## 5. Risks and how the plan handles them | Risk | Mitigation | | --- | --- | | **Environment is the real blocker** — no JDK on PATH | Phase 0 wrapper script, already verified | | Grammar-Kit codegen needing the IDE UI | Verified false — tasks run from CLI | | Here-strings breaking a DFA lexer | Drove the hand-written lexer decision | | Nested comments / `'` / `.{` mis-lexed | Explicitly enumerated as Tier 1 cases | | Grammar churn silently breaking the parser | Tier 3 corpus gate | | Generated sources hand-edited then lost | Committed to `src/main/gen`, AGENTS.md forbids editing, regeneration is one command | | Slow feedback discouraging test runs | Tier 0 is plain JUnit with no fixture boot; keep it under a few seconds | | Corpus files using features we never support | Explicit committed allowlist, not a silently loosened assertion | --- ## 6. Open questions for you 1. **Scope/ambition** — stop at the current syntax, navigation, and basic refactoring support (Phases 0–5a), or go all the way to formatter and inspections (0–7)? 2. **Compiler integration** (Phase 8) — worth it? It's the only phase needing the actual `jai` binary, and it's the least testable. 3. **Target IDE** — IntelliJ IDEA only (current setting), or all JetBrains IDEs? The latter is mostly a `plugin.xml` dependency change, cheapest decided now. 4. **JDK policy** — rely on the Gradle-provisioned JDK via the wrapper (zero setup, works today), or install a system JDK via Homebrew? I'd default to the wrapper. Phases 0–6 are implemented and verified headlessly. Phase 7 has an initial unresolved `#import`/`#load` path inspection; additional inspections and compiler integration remain optional follow-up work.