commit cb5a1d95555e4fbc8b347d8c22eff04516df330d Author: hgranthorner <37941012+hgranthorner@users.noreply.github.com> Date: Tue Aug 4 10:12:15 2026 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42685c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.gradle +.idea +.intellijPlatform +build \ No newline at end of file diff --git a/.run/runIde.run.xml b/.run/runIde.run.xml new file mode 100644 index 0000000..ef2edf7 --- /dev/null +++ b/.run/runIde.run.xml @@ -0,0 +1,25 @@ + + + + + + + + false + true + false + false + + + \ No newline at end of file diff --git a/.run/runTests.run.xml b/.run/runTests.run.xml new file mode 100644 index 0000000..f77079f --- /dev/null +++ b/.run/runTests.run.xml @@ -0,0 +1,26 @@ + + + + + + + + true + true + false + true + + + diff --git a/.run/runVerifications.run.xml b/.run/runVerifications.run.xml new file mode 100644 index 0000000..0ca148d --- /dev/null +++ b/.run/runVerifications.run.xml @@ -0,0 +1,25 @@ + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b88df3e --- /dev/null +++ b/AGENTS.md @@ -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 0–4 vs 0–7), 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. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cc1340f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ + + +# Intellijai Changelog + +## [Unreleased] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d299cb4 --- /dev/null +++ b/README.md @@ -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 `` and `` 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 `` and `` +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 diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..abfb023 --- /dev/null +++ b/build.gradle.kts @@ -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") + } +} diff --git a/docs/BUILD_PLAN.md b/docs/BUILD_PLAN.md new file mode 100644 index 0000000..cc1b538 --- /dev/null +++ b/docs/BUILD_PLAN.md @@ -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 ``-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 | +| **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 + 0–4), 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. + +My recommendation: approve Phases 0–4 now, decide on 5–8 once the parser gate is +green and we can see how the grammar behaved against real code. diff --git a/docs/JAI_LANGUAGE_REFERENCE.md b/docs/JAI_LANGUAGE_REFERENCE.md new file mode 100644 index 0000000..06819fc --- /dev/null +++ b/docs/JAI_LANGUAGE_REFERENCE.md @@ -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 ` then a newline, then raw text, terminated by a line +that **starts with** ``. 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 :: `. + +--- + +## 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/` | diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..c234246 --- /dev/null +++ b/gradle.properties @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..dae12db --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,5 @@ +[versions] +junit = "4.13.2" + +[libraries] +junit = { module = "junit:junit", version.ref = "junit" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a9db115 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/gradlew.bat @@ -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% diff --git a/intellijai.iml b/intellijai.iml new file mode 100644 index 0000000..482334b --- /dev/null +++ b/intellijai.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/jaigradle b/jaigradle new file mode 100755 index 0000000..34ba0e5 --- /dev/null +++ b/jaigradle @@ -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 "$@" diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..4093304 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +java = "temurin-21" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..11e904d --- /dev/null +++ b/settings.gradle.kts @@ -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() + } + } +} diff --git a/src/main/kotlin/MyMessageBundle.kt b/src/main/kotlin/MyMessageBundle.kt new file mode 100644 index 0000000..a3f5848 --- /dev/null +++ b/src/main/kotlin/MyMessageBundle.kt @@ -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) + } +} diff --git a/src/main/kotlin/MyToolWindowFactory.kt b/src/main/kotlin/MyToolWindowFactory.kt new file mode 100644 index 0000000..02b006c --- /dev/null +++ b/src/main/kotlin/MyToolWindowFactory.kt @@ -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>().apply { + val label = JBLabel(MyMessageBundle.message("toolwindow.MyToolWindow.number.label", "?")) + + add(label) + add(JButton(MyMessageBundle.message("toolwindow.MyToolWindow.shuffle.button")).apply { + addActionListener { + label.text = MyMessageBundle.message( + "toolwindow.MyToolWindow.number.label", Random(System.currentTimeMillis()).nextInt(1000) + ) + } + }) + } + + fun getContent(): JBPanel> = content + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..fabc42d --- /dev/null +++ b/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,33 @@ + + + + dev.hgh.intellijai + + + Intellijai + + + YourCompany + + + + most HTML tags may be used + ]]> + + + com.intellij.modules.platform + + messages.MyMessageBundle + + + + + + + diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..dcf6b99 --- /dev/null +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/messages/MyMessageBundle.properties b/src/main/resources/messages/MyMessageBundle.properties new file mode 100644 index 0000000..9d4b0fa --- /dev/null +++ b/src/main/resources/messages/MyMessageBundle.properties @@ -0,0 +1,3 @@ +toolwindow.stripe.MyToolWindow=My Tool Window +toolwindow.MyToolWindow.number.label=The random number is: {0} +toolwindow.MyToolWindow.shuffle.button=Shuffle diff --git a/src/test/kotlin/dev/hgh/HarnessSmokeTest.kt b/src/test/kotlin/dev/hgh/HarnessSmokeTest.kt new file mode 100644 index 0000000..96624d2 --- /dev/null +++ b/src/test/kotlin/dev/hgh/HarnessSmokeTest.kt @@ -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, + ) + } +}