initial commit

This commit is contained in:
hgranthorner
2026-08-04 10:12:15 -04:00
commit cb5a1d9555
26 changed files with 1792 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.gradle
.idea
.intellijPlatform
build

25
.run/runIde.run.xml Normal file
View File

@@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run IDE with Plugin" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/.intellijPlatform/sandbox/*/*/log/idea.log" show_all="true"/>
<ExternalSystemSettings>
<option name="executionName"/>
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
<option name="externalSystemIdString" value="GRADLE"/>
<option name="scriptParameters" value=""/>
<option name="taskDescriptions">
<list/>
</option>
<option name="taskNames">
<list>
<option value="runIde"/>
</list>
</option>
<option name="vmOptions" value=""/>
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2"/>
</configuration>
</component>

26
.run/runTests.run.xml Normal file
View File

@@ -0,0 +1,26 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Tests" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/.intellijPlatform/sandbox/*/*/log-test/idea.log"
show_all="true"/>
<ExternalSystemSettings>
<option name="executionName"/>
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
<option name="externalSystemIdString" value="GRADLE"/>
<option name="scriptParameters" value=""/>
<option name="taskDescriptions">
<list/>
</option>
<option name="taskNames">
<list>
<option value="check"/>
</list>
</option>
<option name="vmOptions" value=""/>
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>true</RunAsTest>
<method v="2"/>
</configuration>
</component>

View File

@@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Verifications" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/.intellijPlatform/sandbox/*/*/log/idea.log" show_all="true"/>
<ExternalSystemSettings>
<option name="executionName"/>
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
<option name="externalSystemIdString" value="GRADLE"/>
<option name="scriptParameters" value=""/>
<option name="taskDescriptions">
<list/>
</option>
<option name="taskNames">
<list>
<option value="verifyPlugin"/>
</list>
</option>
<option name="vmOptions" value=""/>
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2"/>
</configuration>
</component>

145
AGENTS.md Normal file
View File

@@ -0,0 +1,145 @@
# AGENTS.md — Jai IntelliJ Plugin
Orientation for agents working on this repo. Read this first.
**Goal:** an IntelliJ plugin for the **Jai** programming language.
**Hard rule:** do **not** research Jai online — public material is outdated and
wrong. The only source of truth is the local install at `~/.local/jai`
(`how_to/`, `modules/`, `examples/`). Researching *IntelliJ plugin development*
online is fine and encouraged.
---
## Run anything with `./jaigradle`
```bash
./jaigradle test # unit tests
./jaigradle check # full verification
./jaigradle tasks # discover tasks
```
**Never call `./gradlew` directly.** There is no JDK on `PATH` and `JAVA_HOME`
is unset in a fresh shell. `./jaigradle` resolves the JDK via mise, then
delegates. It works from an environment with no Java at all.
Do not try to fix this with `org.gradle.java.home` in `gradle.properties`
already tried, it does not work, because `gradlew` is a shell script that needs
a JVM to launch *itself* before it ever reads that property.
**Java must be 21+.** The IntelliJ Platform test-framework jars are Java 21
bytecode (class major version 65); on JDK 17 every test fails with
`UnsupportedClassVersionError`. `mise.toml` pins `java = "temurin-21"`.
Note that `./jaigradle compileKotlin` **passes on JDK 17** — the template
sources never touch the test framework. Compiling is not evidence the toolchain
is correct. Run tests.
---
## Verify with the JUnit XML, not the exit code
A green `test` task does not prove tests ran. Always confirm:
```bash
cat build/test-results/test/*.xml | head -3
```
Look for `tests="N"` with `failures="0" errors="0"` and **N > 0**.
Last verified state (all green):
```text
TEST-dev.hgh.HarnessSmokeTest.xml -> tests=2 skipped=0 failures=0 errors=0
```
---
## Current state
### Done
- `docs/JAI_LANGUAGE_REFERENCE.md` — language reference derived from
`~/.local/jai`, keywords/tokens transcribed from the compiler's own lexer.
**Read this before writing any lexer or parser code.**
- `docs/BUILD_PLAN.md` — phased plan, testing strategy, risks, open questions.
- `mise.toml``java = "temurin-21"` (was 17, which was broken).
- `jaigradle` — JDK-resolving Gradle wrapper. Executable, verified.
- `src/test/kotlin/dev/hgh/HarnessSmokeTest.kt` — proves the platform boots
headlessly. **If this fails, no other test can be trusted.**
### Not done — pick up here
1. **`git init`.** Still not a repository. Do this first; there is no rollback
point right now. Add `.kotlin` to `.gitignore` (currently untracked and
unignored).
2. **Strip the template.** `src/main/kotlin/MyToolWindowFactory.kt`,
`MyMessageBundle.kt`, `messages/MyMessageBundle.properties`, and the
`toolWindow` registration + `resource-bundle` in `plugin.xml` are all
JetBrains scaffold and must go. `plugin.xml` metadata (name, vendor,
description) is still placeholder text.
3. **Phase 1: lexer** — see the plan.
### Open questions for the user (unanswered)
Scope (Phases 04 vs 07), compiler integration, target IDEs, JDK policy. See
`docs/BUILD_PLAN.md` §6. Scope is the one that most affects the work.
---
## Architecture decisions already made
Both are justified in `docs/BUILD_PLAN.md` §2. Do not silently reverse them.
- **Hand-written lexer, not JFlex.** Jai here-strings (`#string DONE … DONE`)
have a *runtime-captured* terminator, which a DFA generator cannot express.
Nested block comments need a depth counter. Both would end up as hand-written
Java inside JFlex actions anyway.
- **Grammar-Kit BNF for parser + PSI.** Verified headless: the plugin ID is
`org.jetbrains.intellij.platform.grammarkit`, bundled in the IntelliJ Platform
Gradle Plugin since 2.12.0 (we run 2.18.1). `generateLexer` / `generateParser`
already appear in `./jaigradle tasks --all` — no extra dependency needed.
- **Ship highlighting before the parser.** Highlighting needs only the lexer.
---
## The test corpus is the main asset
`~/.local/jai` holds **714 `.jai` files, 16.4 MB** of real code by the language
authors. Use it as the primary correctness gate instead of hand-written samples.
The highest-value assertion, per the plan: lex every file and check the
concatenation of all token texts reproduces the source **byte for byte**. An
IntelliJ lexer must tile its input with no gaps or overlaps, so this single
invariant over 16 MB catches nearly every lexer bug — with no fixture boot and
no human review. Also assert no `BAD_CHARACTER` and that every `advance()`
strictly increases the offset (catches infinite loops, which hang the IDE rather
than failing visibly).
Same idea later: zero `PsiErrorElement` across the corpus for the parser;
formatting is idempotent for the formatter.
---
## Environment gotchas
- **Stale Gradle daemons.** Mixing JDK 17 and 21 daemons caused
`Timeout waiting to lock journal cache`. Fix: `./jaigradle --stop`.
- **`timeout` does not exist on this macOS box.** Do not use it in scripts.
- **Noisy test stderr.** Fixture runs log Vue/JS `PluginException`s from bundled
plugins. Tests pass regardless, but real failures can be buried — check the
XML. Narrowing the test fixture later would help.
- **`runIde` is never required.** Everything is verifiable headlessly; that is a
hard project requirement, not a preference. Do not ask the user to open an IDE
to check your work.
---
## Working agreement
- Verify claims by running commands; prefer empirical checks over docs. Several
plan decisions came from testing assumptions that turned out false.
- Keep the working tree clean — revert throwaway probes.
- Generated sources (once Grammar-Kit is wired up) go in `src/main/gen`, are
committed, and are **never hand-edited**; regenerate instead.
- Update this file when project state changes.

5
CHANGELOG.md Normal file
View File

@@ -0,0 +1,5 @@
<!-- Keep a Changelog guide -> https://keepachangelog.com -->
# Intellijai Changelog
## [Unreleased]

152
README.md Normal file
View File

@@ -0,0 +1,152 @@
# Intellijai
[![Twitter Follow](https://img.shields.io/badge/follow-%40JBPlatform-1DA1F2?logo=twitter)](https://twitter.com/JBPlatform)
[![Developers Forum](https://img.shields.io/badge/JetBrains%20Platform-Join-blue)][jb:forum]
## Overview
This repository implements an IntelliJ Platform plugin.
## Demo Functionality
The sample plugin adds a `My Tool Window` tool window with a simple functionality of shuffling a random number.
## Plugin structure
A generated project contains the following content structure:
```
.
├── .run/ Predefined Run/Debug Configurations
├── gradle
│ ├── wrapper/ Gradle Wrapper
│ ├── libs.versions.toml Version catalog
├── src Plugin sources
│ └── main
│ ├── kotlin/ Kotlin production sources
│ └── resources/ Plugin resources
│ ├── META-INF/ Plugin configuration file and logo
│ └── messages/ Message bundles
├── .gitignore Git ignoring rules
├── build.gradle.kts Gradle build configuration
├── gradle.properties Gradle configuration properties
├── gradlew *nix Gradle Wrapper script
├── gradlew.bat Windows Gradle Wrapper script
├── README.md This file
└── settings.gradle.kts Gradle project settings
```
In addition to the configuration files, the most crucial part is the `src` directory, which contains our implementation
and the manifest for our plugin [plugin.xml][file:plugin.xml].
> [!NOTE]
> To use Java in your plugin, create the `/src/main/java` directory.
The plugin logo is placed in `src/main/resources/META-INF/pluginIcon.svg`. See [Plugin Logo][docs:logo] for more
information and logo requirements.
## Build script
The [build.gradle.kts][file:build.gradle.kts] is the core of the project definition. It applies three Gradle plugins:
| Plugin | Description |
|-----------------------------------|----------------------------------------------------------------------------------|
| `org.jetbrains.kotlin.jvm` | Adds Kotlin support |
| `org.jetbrains.changelog` | Simplifies patching the [CHANGELOG.md][file:CHANGELOG.md] file |
| `org.jetbrains.intellij.platform` | The [IntelliJ Platform Gradle Plugin][docs:intellij-platform-gradle-plugin-docs] |
The `intellijPlatform` dependencies block selects the IDE to compile against:
```kotlin
intellijIdea("2025.3.5")
```
See [Target Versions][docs:target-version] for more information.
The `intellijPlatform` dependencies block also contains a dependency on the platform testing framework:
```kotlin
testFramework(TestFrameworkType.Platform)
```
See [Testing][docs:testing] for more information
## Plugin configuration file
The plugin configuration file is a [plugin.xml][file:plugin.xml] file located in the `src/main/resources/META-INF`
directory. It provides general information about the plugin, its dependencies, extensions, and listeners.
You can read more about this file in the [Plugin Configuration File][docs:plugin.xml] section of our documentation.
### Plugin ID and name
Generated plugin ID and name may require adjustment.
These values are generated based on _Group ID_ and _Artifact ID_ provided in the IDE Plugin wizard. It is recommended to
review `<id>` and `<name>` elements in the plugin.xml file, and adjust them if needed.
Please note that Gradle properties `rootProject.name` and `project.group` don't need to match the `<id>` and `<name>`
elements. There is no IntelliJ Platform-related reason they should as they serve different functions.
## Predefined Run/Debug configurations
Within the default project structure, there is a `.run` directory provided containing predefined *Run/Debug
configurations* that expose corresponding Gradle tasks:
| Configuration name | Description |
|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Run IDE with Plugin | Runs [`:runIde`][docs:intellij-platform-gradle-plugin-runIde] IntelliJ Platform Gradle Plugin task. Use the *Debug* icon for plugin debugging. |
| Run Tests | Runs [`:check`][gradle:lifecycle-tasks] Gradle task. |
| Run Verifications | Runs [`:verifyPlugin`][docs:intellij-platform-gradle-plugin-verifyPlugin] IntelliJ Platform Gradle Plugin task to check the plugin compatibility against the specified IntelliJ IDEs. |
> [!NOTE]
> You can find the logs from the running task in the `idea.log` tab.
## Publishing the plugin
> [!TIP]
> Make sure to follow all guidelines listed in [Publishing a Plugin][docs:publishing] to follow all recommended and
required steps.
Releasing a plugin to [JetBrains Marketplace](https://plugins.jetbrains.com) is a straightforward operation that uses
the `publishPlugin` Gradle task provided by
the [intellij-platform-gradle-plugin][docs:intellij-platform-gradle-plugin-docs].
You can also upload the plugin to the [JetBrains Plugin Repository](https://plugins.jetbrains.com/plugin/upload)
manually via UI.
## Useful links
- [IntelliJ Platform SDK Plugin SDK][docs]
- [IntelliJ Platform Gradle Plugin Documentation][docs:intellij-platform-gradle-plugin-docs]
- [IntelliJ Platform Explorer][jb:ipe]
- [JetBrains Marketplace Quality Guidelines][jb:quality-guidelines]
- [IntelliJ Platform UI Guidelines][jb:ui-guidelines]
- [JetBrains Marketplace Paid Plugins][jb:paid-plugins]
- [IntelliJ SDK Code Samples][gh:code-samples]
[docs]: https://plugins.jetbrains.com/docs/intellij
[docs:plugin.xml]: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html?from=IJPluginReadmeFile
[docs:publishing]: https://plugins.jetbrains.com/docs/intellij/publishing-plugin.html?from=IJPluginReadmeFile
[docs:intellij-platform-gradle-plugin-docs]: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html?from=IJPluginReadmeFile
[docs:intellij-platform-gradle-plugin-runIde]: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-tasks.html?from=IJPluginReadmeFile#runIde
[docs:intellij-platform-gradle-plugin-verifyPlugin]: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-tasks.html?from=IJPluginReadmeFile#verifyPlugin
[docs:logo]: https://plugins.jetbrains.com/docs/intellij/plugin-icon-file.html?from=IJPluginReadmeFile
[docs:target-version]: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html?from=IJPluginReadmeFile#target-versions
[docs:testing]: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html?from=IJPluginReadmeFile#testing
[file:build.gradle.kts]: ./build.gradle.kts
[file:CHANGELOG.md]: ./CHANGELOG.md
[file:gradle.properties]: ./gradle.properties
[file:plugin.xml]: ./src/main/resources/META-INF/plugin.xml
[gh:code-samples]: https://github.com/JetBrains/intellij-sdk-code-samples
[gradle:lifecycle-tasks]: https://docs.gradle.org/current/userguide/java_plugin.html#lifecycle_tasks
[jb:github]: https://github.com/JetBrains/.github/blob/main/profile/README.md
[jb:forum]: https://platform.jetbrains.com/
[jb:quality-guidelines]: https://plugins.jetbrains.com/docs/marketplace/quality-guidelines.html
[jb:paid-plugins]: https://plugins.jetbrains.com/docs/marketplace/paid-plugins-marketplace.html
[jb:ipe]: https://jb.gg/ipe
[jb:ui-guidelines]: https://jetbrains.github.io/ui

21
build.gradle.kts Normal file
View File

@@ -0,0 +1,21 @@
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
plugins {
id("org.jetbrains.kotlin.jvm")
id("org.jetbrains.changelog")
id("org.jetbrains.intellij.platform")
}
// Read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html
dependencies {
testImplementation(libs.junit)
// IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html
intellijPlatform {
intellijIdea("2025.3.5")
testFramework(TestFrameworkType.Platform)
// Add plugin dependencies for compilation here, for example:
// bundledPlugin("com.intellij.java")
}
}

210
docs/BUILD_PLAN.md Normal file
View File

@@ -0,0 +1,210 @@
# 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 `<warning>`-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 04. 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 |
| **4** | Structure view, folding, `#import`/`#load` reference resolution + go-to-definition | Tier 4 fixture tests |
| **5** | Completion (keywords, directives, module names), rename, find-usages | Tier 4 fixture tests |
| **6** | Formatter, code style settings | Formatter round-trip: formatting the corpus is idempotent |
| **7** | Inspections (e.g. `#must` misuse), quick fixes, live templates | Tier 4 + `verifyPlugin` |
| **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.
---
## 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 solid syntax highlighting + navigation (Phases
04), or go all the way to formatter and inspections (07)?
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.
My recommendation: approve Phases 04 now, decide on 58 once the parser gate is
green and we can see how the grammar behaved against real code.

View File

@@ -0,0 +1,618 @@
# Jai Language Reference (for plugin implementors)
**Provenance:** Everything here was derived from the local Jai distribution at
`~/.local/jai` — primarily `modules/Jai_Lexer/module.jai` (the compiler's own
lexer, authoritative for tokens/keywords), plus `how_to/` and `modules/`.
**No online sources were used; online Jai material is outdated.**
Anything marked ⚠️ is inferred rather than directly confirmed in the lexer.
Re-verify against: `~/.local/jai/modules/Jai_Lexer/module.jai`
---
## 1. Source layout
- Extension: `.jai`
- Encoding: UTF-8 assumed; the language imposes no encoding on `string`.
- Line endings: `\n` or `\r\n` (here-strings normalize to `\n`).
- No zero-terminated strings; no preprocessor in the C sense.
---
## 2. Comments
```jai
// line comment to end of line
/* block comment
/* THESE NEST — track depth, do not stop at the first */
still inside */
```
**Nested block comments are real.** `Jai_Lexer/module.jai:1436` maintains
`comment_depth`, incrementing on `/*` and decrementing on `*/`.
A lexer that stops at the first `*/` is wrong.
Note the source comment at line 1433: the lexer will treat `/////*` as a nested
open-comment (the author flags this as possibly undesirable). Match the simple
depth-counting behavior.
---
## 3. Keywords
Exact list, from `check_for_keyword` (`module.jai:758`). These are the *only*
identifiers promoted to keyword tokens.
| Len | Keywords |
| ----- | ---------- |
| 2 | `if` `xx` |
| 3 | `ifx` `for` |
| 4 | `then` `else` `null` `case` `enum` `true` `cast` |
| 5 | `while` `break` `using` `defer` `false` `union` |
| 6 | `return` `struct` `remove` `inline` |
| 7 | `size_of` `type_of` `code_of` `context` |
| 8 | `continue` `operator` |
| 9 | `type_info` `no_inline` `interface` |
| 10 | `enum_flags` |
| 11 | `is_constant` |
| 12 | `push_context` |
| 14 | `initializer_of` |
Flat list:
```text
if xx ifx for then else null case enum true cast while break using defer false
union return struct remove inline size_of type_of code_of context continue
operator type_info no_inline interface enum_flags is_constant push_context
initializer_of
```
Notes:
- `xx` is the auto-cast operator (`KEYWORD_AUTO_CAST`), not an identifier.
- `remove` is a loop-body statement for removing the current element.
- `then` is used by `ifx` (`ifx cond then a else b`).
- `interface` appears in type restrictions: `(x: $T/interface Matchable)`.
- `struct`, `union`, `enum`, `enum_flags` are all type-constructor keywords.
**Not keywords** (contrary to what one might assume): `int`, `float`, `bool`,
`string`, `s8`..`s64`, `u8`..`u64`, `float32`, `float64`, `void`, `Type`, `Any`,
`Code`. These are ordinary identifiers resolving to built-in types. Highlight
them as *built-in types*, in a distinct style from keywords, and do not let a
parser depend on them being reserved.
### Built-in type names (identifiers, highlight separately)
```text
s8 s16 s32 s64 u8 u16 u32 u64 int
float float32 float64 bool string void
Type Any Code
```
`int` is an alias for `s64`; `float` is an alias for `float32`.
---
## 4. Operators and punctuation
From `Token_Type` (`module.jai:17`). Multi-character tokens must be matched
longest-first.
### Arithmetic / assignment
```text
+ - * / %
+= -= *= /= %=
```
### Comparison / logical
```text
== != < > <= >= && || !
&&= ||=
```
`ISEQUAL_FOR_SWITCH_STATEMENT` is a distinct token: `==` immediately followed by
a `{` block introduces the switch form (`if x == { case ...; }`). The lexer
distinguishes it; a highlighter can ignore the distinction, a parser cannot.
### Bitwise
```text
& | ^ ~
&= |= ^=
<< >> (SHIFT_LEFT, SHIFT_RIGHT)
<<= >>=
<<< >>> (ROTATE_LEFT, ROTATE_RIGHT — disambiguated by the parser, not the lexer)
<<<= >>>=
```
### Distinctive Jai tokens
| Token | Name | Meaning |
| ------- | ------ | --------- |
| `->` | `RIGHT_ARROW` | procedure return type |
| `..` | `DOUBLE_DOT` | inclusive range, `for 0..7` |
| `$` | | polymorphic type/value capture |
| `$$` | `DOUBLE_DOLLAR` | optionally-constant parameter |
| `---` | `TRIPLE_MINUS` | "do not initialize", `x: T = ---;` |
| `--` | `DOUBLE_MINUS` | distinct token (⚠️ not a decrement operator in normal code) |
| `,,` | `DOUBLE_COMMA` | inline context modification, `join(a,, allocator=temp)` |
| `.{` | `BEGIN_STRUCT_LITERAL` | struct literal |
| `.[` | `BEGIN_ARRAY_LITERAL` | array literal |
| `=>` | `QUICK_LAMBDA` | quick lambda, `x => x.count` |
| `.*` | `POSTFIX_DEREFERENCE` | pointer dereference, `ptr.*` |
| `*` | `POINTER_DEREFERENCE` | prefix: address-of AND pointer-type marker |
| `===` | `TRIPLE_EQUALS` | `#asm` register pinning |
| `` ` `` | backtick | see §9 |
| `:` `::` `:=` | | declaration forms, see §6 |
**Critical `*` semantics — inverted vs C.** `*T` is "pointer to T" *and* `*value`
is "address of value". Dereference is postfix `.*`. There is no prefix `*`
dereference. This matters for any expression parser.
`-` lexing (`module.jai:1680`): `->` then `---` then `--` then `-=` then `-`.
---
## 5. Literals
### Integers
```jai
123
1_000_000 // underscores allowed anywhere as separators
0xfade_deaf // hex
0b101101101 // binary
```
Default type of an integer literal is `s64`, but literals implicitly convert to
any type they fit in.
### Floats
```jai
37.0
1.111
0h7fbf_ffff // IEEE-754 bits in hex ("hexfloat"), 16 hex digits max
0h8000_0000_0000_0000 // 64-bit negative zero
```
Default is `float32` unless precision demands `float64`.
`Value_Flags` tracks `HEX`, `BINARY`, `FLOAT`, `REQUIRES_FLOAT64`, `OVERFLOWED`.
### Strings
```jai
"Hello, Sailor!" // escapes: \n \t \" \\ etc.
```
Strings are `{count: s64, data: *u8}` views — **not** zero-terminated, and
subscripting yields `u8` (there is no character type).
### Here-strings (`#string`)
```jai
THE_STRING :: #string DONE
Anything at all, including "quotes" and \n literally.
DONE
```
Syntax: `#string <IDENT>` then a newline, then raw text, terminated by a line
that **starts with** `<IDENT>`. The terminator identifier is arbitrary (`DONE`
is only convention). Flagged as `Value_Flags.HERE_STRING`.
There is a `#string` variant used with imports: `#import,string #string DONE`.
For an IntelliJ lexer this needs a dedicated state with the terminator captured,
much like heredocs in shell/Perl.
### Character literals
```jai
#char "a" // yields a u8
```
`#char` is a directive, not a `'x'` literal form. **There is no single-quote
character literal in Jai.** Do not lex `'` as a string delimiter.
### Notes (`@`)
```jai
foo :: () { } @PrintLike @Deprecated
x: int; @Cleanup
```
`@Identifier` is a `NOTE` token. Common in the wild: `@Cleanup`, `@Incomplete`,
`@Speed`, `@Robustness`, `@Temporary`, `@ToDo`, `@Hack`, `@Copypasta`, `@test`,
`@PrintLike`, `@NoProfile`. Treat as metadata/annotation for highlighting.
`Note_Flags.IS_SYSTEM_LEVEL` distinguishes compiler-known notes.
---
## 6. Declarations
The universal declaration form is `name : type = value`, with parts omissible:
```jai
a : float = 37.0; // explicit type + value
b : float; // explicit type, default-initialized to zero
c := 111.0; // type inferred (: and = merge into :=)
d : float : -123.45; // CONSTANT (second colon instead of =)
e :: 42; // constant, type inferred
f : T = ---; // explicitly UNINITIALIZED (no zeroing)
x, y, z: float; // compound declaration
```
**Everything is zero-initialized by default** unless `= ---` is used.
`::` (constant) is how procedures, structs, and enums are declared — they are
just constant values:
```jai
main :: () { }
Person :: struct { }
Fruit :: enum u32 { }
```
This is the single most important structural fact for a parser: there is no
`func`/`fn`/`class` keyword. A top-level declaration is
`IDENT :: <struct|enum|enum_flags|union|(params)...|expr>`.
---
## 7. Procedures
```jai
name :: (a: int, b: float) -> ReturnType { ... }
name :: (a: int) -> named: ReturnType { ... } // named return value
name :: (a: int) -> (out: [] T) { ... } // parenthesized
name :: () -> A, B { ... } // multiple returns
name :: (x: int, y := 10) { ... } // default argument
name :: (fmt: string, args: ..Any) { ... } // variadic
```
Calls support named arguments: `f(flavor = "chocolate", num_scoops = 3)`.
### Quick lambdas
```jai
quick_sort(to_sort, x => x.count);
quick_sort(to_sort, (a, b) => ifx a.count != b.count then b.count-a.count else 0);
count1 :: x => x.count;
```
### Operator overloading
```jai
operator + :: (x: Complex, y: Complex) -> Complex { ... }
operator == :: (a: T, b: T) -> bool #symmetric { ... }
operator [] :: (a: Bit_Array, index: int) -> bool { ... }
```
`operator` is a keyword; the token following it is the operator symbol, then
`::` and the procedure. `OPERATOR_ARRAY_SUBSCRIPT` / `OPERATOR_ASSIGNMENT_TO_ARRAY_SUBSCRIPT`
exist as parser-internal token types for `[]`.
### Polymorphism
```jai
square :: (x: $T) -> T { return x*x; } // capture T from argument type
array_add :: (array: *[..] $T, item: T) { } // capture inside a compound type
multiplier :: ($T: Type) { } // $ = must be compile-time constant
divider :: (x: int, $$ y: int) -> int { } // $$ = optionally constant
discuss :: (x: $T/interface Matchable) { } // type restriction via interface
proc :: (x: $T/SomeStruct) { } // ⚠️ restriction by type
Holder :: struct ($T: Type, $N: s64) { } // polymorphic struct
```
`$` on a parameter type captures it; `$` on a parameter name requires the
*value* be compile-time constant. `/` after a polymorphic capture introduces a
restriction.
---
## 8. Types
```jai
[8] int // fixed-size array
[] float // array view {count, data}
[..] int // resizable/dynamic array
*T // pointer to T
[9] u8 // e.g. Phone_Number :: [9] u8;
```
### Structs
```jai
Rectangle :: struct {
x0, y0: float;
color_name: string;
temperature := -10.0; // default value
info: Ice_Cream_Info;
info.flavor = "chocolate"; // override a sub-struct default, inside the struct body
}
```
Struct bodies may contain assignment statements that set nested defaults.
Layout is declaration order, always, with no compiler reordering.
```jai
Video_File :: struct {
#as using base: Document; // #as enables implicit cast to Document
// using imports Document's names
}
```
### Enums
```jai
Fruits :: enum u32 {
BANANA :: 5; // explicit value
APPLE; // auto-increments
}
Fruits :: enum u32 #specified { BANANA :: 1; } // values locked for serialization
Flags :: enum_flags u8 { A :: 0x1; B :: 0x2; }
```
**Unary dot:** enum values can be written `.APRICOT` when the type is inferred.
This means a leading `.` followed by an identifier is a valid *expression*, and
must not be confused with member access. Same for `.{` and `.[`.
### Unions
```jai
union { a: int; b: float; }
```
---
## 9. Control flow
```jai
if cond { } else if cond { } else { }
if cond single_statement;
ifx cond then a else b // expression form
if value == { // switch form
case .BANANA; print("...");
case .APPLE; x := 1; print("..."); // each case is its own scope
case; print("default"); // valueless case = default
}
```
Cases **do not** fall through. To fall through, end the block with `#through`.
`#complete` on the `if` asserts all enum cases are handled.
```jai
while cond { }
break; continue;
```
### For loops — many forms
```jai
for 1..7 { print("%", it); } // range; implicit `it`
for i: 1..7 { } // named index
for a..b { } // runtime bounds
for < b..a { } // REVERSED (note the `<`)
for array { print("%", it); } // iterate values; `it` and `it_index`
for < numbers { } // reversed iteration
for value, i: numbers { } // named value and index
for * teas { } // iterate BY POINTER (`it` is a pointer)
for outer: numbers { for inner: numbers { } } // named for nesting
for numbers if (it & 1) == 0 remove it; // `remove` current element
```
Implicit loop variables are `it` (value) and `it_index` (index). They are
ordinary identifiers, but worth special highlighting.
`for <` and `for *` are modifier tokens between `for` and the iteration
expression — a parser must accept `for`, then optional `<` and/or `*`.
### Custom iteration (`for_expansion`)
```jai
for_expansion :: (holder: Holder, body: Code, flags: For_Flags) #expand {
for slot_index: 0..holder.count-1 {
`it_index := slot_index; // backtick exports the name to the caller
`it := holder.values[slot_index];
#insert body;
}
}
for :positive_vibes_only holder { } // ⚠️ select a named expansion
```
### Backticked identifiers
`` `name `` marks an identifier that a macro exports into the caller's scope.
`ident_is_backticked` is a flag on the token. Only `defer`, `return`,
`push_context`, and overloaded operator functions may be backticked among
keywords (`module.jai:1537`).
### defer
```jai
defer x += 1; // runs on ANY exit from the enclosing scope, incl. break
defer free(ptr);
```
### Context
```jai
new_context := context;
new_context.allocator = temp;
push_context new_context { some_function(); }
join(strings,, allocator=temp); // ,, = inline context change for one call
```
---
## 10. Casting
```jai
cast(u8) b
cast,no_check(u8) c // modifier on cast, suppresses range check
xx value // auto-cast (infer target type)
xx,no_check value // ⚠️ modifier form
```
`cast` takes a parenthesized type and an optional `,modifier` between the
keyword and the paren.
---
## 11. Compiler directives (`#`)
Directives are `#` immediately followed by an identifier. Full list observed in
the distribution, ordered by frequency (counts from `modules/`):
**Very common**
`#foreign` `#c_call` `#type` `#cpp_method` `#if` `#import` `#char` `#overlay`
`#elsewhere` `#as` `#run` `#through` `#scope_file` `#load` `#no_context`
`#library` `#type_info_none` `#expand` `#align` `#asm` `#assert` `#scope_module`
`#scope_export` `#caller_location` `#string`
**Common**
`#define` `#cpp_return_type_is_non_pod` `#no_padding` `#no_aoc`
`#bake_arguments` `#insert` `#complete` `#compiler` `#no_abc` `#include`
`#Context` `#code` `#module_parameters` `#endif` `#add_context` `#modify`
`#location` `#deprecated` `#ifdef` `#no_debug` `#symmetric` `#filepath`
`#compile_time` `#intrinsic`
**Occasional**
`#type_info_procedures_are_void_pointers` `#program_export` `#placeholder`
`#procedure_of_call` `#pragma` `#version` `#this` `#bytes`
`#type_info_no_size_complaint` `#discard` `#file` `#else` `#run_and_insert`
`#undef` `#place` `#bake` `#bake_constants` `#no_reset` `#poke_name`
`#specified` `#no_alias` `#dump` `#placeholders`
> ⚠️ Some of these (`#define`, `#include`, `#ifdef`, `#endif`, `#undef`,
> `#pragma`, `#elsewhere`) appear predominantly inside **C-binding files
> generated by `Bindings_Generator`**, which embed C preprocessor text. Do not
> assume they are core language directives.
>
> ⚠️ **`#must` does not exist in this distribution** — a full-tree grep of
> `~/.local/jai` returns zero hits. Older online documentation mentions it. This
> is exactly the kind of drift the "don't search online" instruction guards
> against. Do not add it to the keyword list.
### Semantics of the important ones
| Directive | Meaning |
| ----------- | --------- |
| `#import "Basic"` | import a module; also `#import,string`, `#import,file`, `#import,dir` |
| `#load "file.jai"` | textually include another file into this scope |
| `#run expr` | execute at compile time |
| `#if cond { }` / `#else` | static (compile-time) conditional; can appear at top level |
| `#assert cond` | compile-time assertion |
| `#scope_file` / `#scope_module` / `#scope_export` | change visibility of everything below, until the next scope directive |
| `#expand` | mark a procedure as a macro |
| `#insert code` | splice a `Code` value in |
| `#code expr` | produce a `Code` value |
| `#through` | fall through to next `case` |
| `#complete` | require exhaustive `case` coverage |
| `#specified` | lock enum values for forward compatibility |
| `#as` | allow implicit cast from this struct member |
| `#char "a"` | character byte literal |
| `#string ID` | here-string |
| `#asm { }` | inline x86-64 assembly block |
| `#foreign` / `#library` / `#c_call` | FFI |
| `#caller_location` / `#location()` / `#file` / `#filepath` | source location introspection |
| `#no_abc` / `#no_aoc` | disable array-bounds-check / arithmetic-overflow-check |
| `#place` | overlay a struct member at another member's offset |
| `#module_parameters` | parameterize a module |
| `#modify` | compile-time hook to inspect/alter polymorph resolution |
| `#deprecated "msg"` | deprecation warning |
| `#body_text` / `#poke_name` / `#dump` | metaprogramming utilities |
`#scope_file` etc. are *statement-position* directives that affect everything
following them in the file — relevant if the plugin does symbol visibility.
---
## 12. Modules and imports
```jai
#import "Basic";
#import "Math";
Sort :: #import "Sort"; // bind a module to a name
#load "other_file.jai";
#import "Foo"(PARAM = 3); // ⚠️ module parameters
```
Module search path: `~/.local/jai/modules/`. A module is either
`Name.jai` or a directory `Name/module.jai`. Both forms exist in the
distribution (e.g. `Bit_Array.jai` vs `Basic/module.jai`).
This is the resolution rule a "go to definition" / import-completion feature
must implement.
---
## 13. Highlighting recommendations (IntelliJ token groups)
| Group | Contents |
| ------- | ---------- |
| Keyword | §3 list |
| Built-in type | §3 built-in type names |
| Directive | `#ident` |
| Note / annotation | `@Ident` |
| Number | int/hex/binary/float/hexfloat, incl. `_` separators |
| String | `"..."` |
| Here-string | `#string ID ... ID` (own token, own lexer state) |
| Line comment | `//...` |
| Block comment | `/*...*/` **nesting** |
| Operator | §4 |
| Loop variable | `it`, `it_index` |
| Polymorph | `$T`, `$$x` |
| Backtick ident | `` `name `` |
| Uninitialized | `---` |
---
## 14. Gotchas that break naive implementations
1. **Nested block comments.** Must count depth.
2. **No `'c'` char literal.** `'` is not a string delimiter. Use `#char "c"`.
3. **`*` is address-of (prefix) and pointer-type; `.*` is dereference.** Inverted from C.
4. **`.{`, `.[`, `.IDENT`** — a leading `.` is not always member access.
5. **Here-strings** need a lexer state carrying the terminator identifier.
6. **`--` vs `---` vs `->` vs `-=`** — longest-match order matters.
7. **`,,`** is one token, not two commas.
8. **`==` before `{`** is a distinct token (switch form).
9. **Primitive type names are not keywords** — don't reserve them.
10. **`#must` is not in this version.** Don't trust old online docs.
11. **`for` takes modifiers** (`<`, `*`) before the iterable.
12. **Declarations have no introducer keyword.** `Foo :: struct {}` and
`foo :: () {}` and `foo :: 3;` are all the same syntactic shape.
13. **`remove` is a keyword**, valid only in loop bodies.
14. **`then` is a keyword**, used only by `ifx`.
15. **Struct bodies can contain assignments**, not just declarations.
---
## 15. Primary sources to re-read
| Topic | Path (under `~/.local/jai`) |
| ------- | ------------------------------ |
| Tokens, keywords, lexing | `modules/Jai_Lexer/module.jai` ← authoritative |
| Basics, declarations | `how_to/001_first.jai`, `002_number_types.jai` |
| Arrays / strings | `how_to/004_arrays.jai`, `005_strings.jai` |
| Structs / literals | `how_to/006_structs.jai`, `007_struct_literals.jai` |
| Types as values | `how_to/008_types.jai` |
| Enums | `how_to/013_enums.jai`, `014_enum_unary_dot.jai` |
| Loops | `how_to/019_looping.jai`, `730_for_expansions.jai` |
| if / ifx / switch | `how_to/022_if.jai`, `025_ifx.jai`, `027_if_case.jai` |
| Imports / scopes | `how_to/040_import_and_load/`, `080_scopes.jai`, `151_file_and_global_scopes/` |
| using | `how_to/042_using.jai`, `044_using_advanced/` |
| Operator overloading | `how_to/093_operator_overloading.jai`, `094_array_operators.jai` |
| Polymorphism | `how_to/100_`, `110_`, `115_`, `120_`, `160_type_restrictions.jai` |
| Context / `,,` | `how_to/011_context.jai`, `225_comma_comma.jai` |
| Metaprogramming | `how_to/450_basic_metaprogram/`, `600_insert.jai`, `630_compiler_get_nodes.jai` |
| Inline asm | `how_to/900_inline_assembly.jai` |
| Compiler API | `modules/Compiler/` |

8
gradle.properties Normal file
View File

@@ -0,0 +1,8 @@
group=dev.hgh
version=1.0.0-SNAPSHOT
# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
kotlin.stdlib.default.dependency=false
# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html
org.gradle.configuration-cache=true
# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
org.gradle.caching=true

View File

@@ -0,0 +1,5 @@
[versions]
junit = "4.13.2"
[libraries]
junit = { module = "junit:junit", version.ref = "junit" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

248
gradlew vendored Executable file
View File

@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

82
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,82 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%

8
intellijai.iml Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

38
jaigradle Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Gradle wrapper that resolves a JDK via mise before delegating to ./gradlew.
#
# Why this exists: ./gradlew is a shell script that needs a JVM to launch
# itself, so it cannot be fixed by org.gradle.java.home in gradle.properties.
# This project has no JDK on PATH, so agents must go through this script.
#
# ./jaigradle check # run all tests
# ./jaigradle test # unit tests only
#
# The JDK version is pinned in ./mise.toml. It must be 21+: the IntelliJ
# Platform test-framework jars are Java 21 bytecode (class major version 65)
# and throw UnsupportedClassVersionError on 17.
set -euo pipefail
cd "$(dirname "$0")"
if command -v mise >/dev/null 2>&1; then
exec mise exec -- ./gradlew "$@"
fi
# Fallback: mise is not installed. Try an already-set JAVA_HOME, then any
# JDK 21 that Gradle has auto-provisioned.
if [ ! -x "${JAVA_HOME:-}/bin/java" ]; then
for candidate in "$HOME"/.gradle/jdks/*/*/Contents/Home "$HOME"/.gradle/jdks/*/*; do
if [ -x "$candidate/bin/java" ]; then
export JAVA_HOME="$candidate"
break
fi
done
fi
if [ ! -x "${JAVA_HOME:-}/bin/java" ]; then
echo "jaigradle: no JDK found. Install mise (https://mise.jdx.dev) and run 'mise install'," >&2
echo " or set JAVA_HOME to a JDK 21+ installation." >&2
exit 1
fi
exec ./gradlew "$@"

2
mise.toml Normal file
View File

@@ -0,0 +1,2 @@
[tools]
java = "temurin-21"

27
settings.gradle.kts Normal file
View File

@@ -0,0 +1,27 @@
import org.jetbrains.intellij.platform.gradle.extensions.intellijPlatform
rootProject.name = "intellijai"
pluginManagement {
plugins {
id("org.jetbrains.kotlin.jvm") version "2.3.20"
id("org.jetbrains.changelog") version "2.5.0"
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
id("org.jetbrains.intellij.platform.settings") version "2.18.1"
}
dependencyResolutionManagement {
// Configure all projects' repositories
repositories {
mavenCentral()
// IntelliJ Platform Gradle Plugin Repositories Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-repositories-extension.html
intellijPlatform {
defaultRepositories()
}
}
}

View File

@@ -0,0 +1,22 @@
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

@@ -0,0 +1,37 @@
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,33 @@
<!-- 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>
<!-- A displayed Vendor name or Organization ID displayed on the Plugins Page. -->
<vendor url="https://www.yourcompany.com">YourCompany</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>
]]></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"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,12 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.0845 7.94025V4H24.0203V7.9896H16.029V4H7.91553V7.94025H4V36H16.0044V32.0045C16.0058 30.9457 16.4274 29.9308 17.1766 29.1826C17.9258 28.4345 18.9412 28.0143 20 28.0143C21.0588 28.0143 22.0743 28.4345 22.8234 29.1826C23.5726 29.9308 23.9942 30.9457 23.9956 32.0045V36H36V7.94025H32.0845Z"
fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="2.94192" y1="4.89955" x2="37.7772" y2="39.7345"
gradientUnits="userSpaceOnUse">
<stop offset="0.15937" stop-color="#3BEA62"/>
<stop offset="0.5404" stop-color="#3C99CC"/>
<stop offset="0.93739" stop-color="#6B57FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 818 B

View File

@@ -0,0 +1,3 @@
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,27 @@
package dev.hgh
import com.intellij.testFramework.fixtures.BasePlatformTestCase
/**
* Validates that the headless test harness works end to end:
* a JDK is resolved, the IntelliJ Platform boots, and a fixture can be driven
* without launching an IDE.
*
* If this fails, no other test in the project can be trusted.
*/
class HarnessSmokeTest : BasePlatformTestCase() {
fun testPlatformBootsHeadlessly() {
val file = myFixture.configureByText("smoke.txt", "hello jai")
assertNotNull("fixture should create a PsiFile", file)
assertEquals("hello jai", file.text)
}
fun testRunningOnJava21OrLater() {
val major = Runtime.version().feature()
assertTrue(
"IntelliJ Platform test-framework is Java 21 bytecode; running on $major",
major >= 21,
)
}
}