Skip to content

Latest commit

 

History

History
183 lines (124 loc) · 15.4 KB

File metadata and controls

183 lines (124 loc) · 15.4 KB

AGENTS.md — lsFusion IntelliJ plugin

General engineering and process notes for AI coding agents (Codex, Cursor, Claude Code, Gemini CLI, GitHub Copilot) working on the lsFusion IntelliJ plugin. Feature-level lore stays in commit messages, code, and per-agent memory — this file is for durable cross-cutting process knowledge.

Repo layout

Single-module Gradle project (Kotlin DSL).

  • src/ — plugin sources (Java + Kotlin).
  • src/com/lsfusion/lang/LSF.bnf — Grammar-Kit BNF defining the lsFusion language parser.
  • src/com/lsfusion/lang/LSF.flex — JFlex lexer source.
  • src/com/lsfusion/lang/psi/references/ — PSI reference contracts; references/impl/ holds implementations.
  • gen/ — auto-generated parser, lexer, and PSI classes. Never edited by hand.
  • META-INF/plugin.xml — plugin manifest; the <version> element is the source of truth for the artifact version.
  • resources/ — bundled icons, message bundles, etc.

Sister repository

The lsFusion platform (Java server, ANTLR3 grammar, docs) lives at ../platform/. A platform syntax change usually requires more than one plugin edit:

  • LSF.bnf — add or update the parser rules.
  • LSF.flex — when the change introduces a new keyword or literal form, add it to the lexer's keyword/literal list (otherwise the lexer won't recognize the new token even if the BNF accepts it).
  • Parser/lexer regeneration via ./gradlew generateLsfParser generateLsfLexer so gen/ matches the new sources.
  • Annotator / inspection support — wire into LSFReferenceAnnotator per the Code conventions section, when the change adds new references, deprecates an old form, or warrants a highlight.

Coordinate commits across the two repos (cross-link in commit messages, or close the same GitHub issue from both sides).

Build and run

./gradlew build                  # full build; generates parser/lexer first
./gradlew generateLsfParser      # regenerate just the parser from LSF.bnf
./gradlew generateLsfLexer       # regenerate just the lexer from LSF.flex
./gradlew generateMigrationParser
./gradlew generateMigrationLexer
./gradlew runIde                 # launch IntelliJ sandbox with the plugin loaded

Toolchain: Java 21 JDK (required by IntelliJ Platform 2025.3). Source and bytecode level are pinned to javaVersion in build.gradle.kts (currently 21) — check that file rather than hard-coding here.

After editing LSF.bnf or LSF.flex, regenerate before the next build or sandbox launch so gen/ matches the sources. gen/ is gitignored — don't stage it.

Testing changes

./gradlew runIde launches an IntelliJ sandbox with the plugin loaded — open or create a .lsf file to exercise highlighting, references, completion, annotations. Sandbox state lives under .intellijPlatform/sandbox/ (outside build/, so ./gradlew clean keeps it); deleting it forces a clean re-init.

No headless test rig — UI verification is interactive in the sandbox. No test sourceSet is currently configured either: main maps everything under src/ (and gen/) as production code, so a file at src/test/... would ship in the plugin artifact. To add unit tests (PSI walkers, reference resolution helpers), first wire a dedicated test sourceSet pointing at a directory that doesn't overlap with src/ (e.g. a sibling tests/), then run ./gradlew test.

Commits

  • Short imperative subject, ≤72 chars. Suffix (closes #NN) for plugin issues, or (lsfusion/platform#NNNN) when cross-linking to a platform issue that drove the change.
  • Body explains why. The diff shows what.
  • Never amend or force-push commits that may have been pulled by others. Always create a new commit on top.
  • Never pass --no-verify unless the user explicitly requests it. If a pre-commit hook fails, fix the underlying issue and create a new commit.
  • When the change is AI-assisted, include a Co-Authored-By: trailer naming the model.
  • Stage new files (git add <path>) at creation, not at commit time. Pending work then shows as A/M in git status instead of ??, and can't silently fall out of the commit. (gen/ and build/ are gitignored and stay unstaged.)
  • Before each commit, run git status, git diff, and git diff --cached. Stage only files relevant to the change; never sweep up unrelated working-tree edits.
  • For non-trivial changes, record in the commit body which verifications ran (parser/lexer regen, Gradle build, sandbox launch, manual highlighting/completion check) and which were skipped and why. Be precise: ./gradlew build is a build/package verification — it becomes a test verification only if Gradle actually discovered and ran tests (and no test sourceSet is currently wired, so ./gradlew test has nothing to run by default). Don't write "tests passed" unless JUnit tests actually ran.
  • Commit messages are published with the repo — nothing machine-local or session-local. Strip: absolute paths (E:/..., C:/Users/...), usernames/hostnames, agent tooling, network/regional circumstances, names of the private project tested against, session narrative (clicks, dialogs, debug history), local git mechanics (shelf/stash, unpushed hashes), work-session dates, log excerpts with machine paths, untracked promises (state the fact instead). Keep what explains the code: the why, invariants, trade-offs, public build numbers, issue refs. Test: still useful years later to a maintainer who knows nothing of your machine or session.
  • "Verified:" is a few lines of facts — what was checked and the result, environment described generically (e.g. "a real lsFusion project (~174k files)"), no step-by-step recipes. Before committing, scan the draft for drive-letter paths and usernames.

Use a HEREDOC so newlines and trailers are preserved:

git commit -m "$(cat <<'EOF'
Subject line (lsfusion/platform#NNNN)

Body paragraph explaining the why.

Co-Authored-By: <model name> <agent-noreply-email>
EOF
)"

Branches

No maintained version or backport branches; the release line is master. Feature branches for work-in-progress are fine and merge back into master.

Releases

Pushing a <version> bump to master is a release. One Jenkins run publishes it to both distribution channels:

  1. JetBrains Marketplace (plugin/7601). Every update goes through JetBrains review; Marketplace users get it once it is approved.
  2. lsfusion.org download / IDE auto-update channel. exe/ext/lsfusion-idea-plugin.zip, the file IDE → Settings → Plugins → Manage Plugin Repositories → https://www.lsfusion.org/... pulls. Uploaded as soon as the Marketplace accepts the upload, without waiting for the review.

Pipeline mechanics

  • GitHub push webhook → Jenkins job buildAndUploadPluginTrigger (freestyle, watches any branch) → triggers the buildAndUploadPlugin pipeline (vars/buildAndUploadPlugin.groovy in the lsfusion/jenkins shared library), which always builds master.
  • It compares <version> from META-INF/plugin.xml with the newest update listed by https://plugins.jetbrains.com/api/plugins/7601/updates; an update still in review is not listed. If they match, it logs Version <X.Y.Z> matches the latest version in Marketplace. Skipping build. and succeeds — pushes that don't bump are no-ops.
  • Otherwise it runs ./gradlew buildPlugin and ./gradlew publishPlugin. publishPlugin depends on verifyPlugin, which checks the plugin against the recommended() IDEs, the newest EAP included, and fails on compatibility problems and on internal or override-only API usages — what Marketplace review rejects. Then:
    • the upload is accepted: the zip goes over FTP to lsfusion.org, and Slack #jenkins gets the change-notes bullets;
    • the Marketplace answers already contains version: an earlier run uploaded this version and it is still in review, so every push until the approval ends here. The build succeeds with the description <X.Y.Z>: already uploaded, not approved yet; nothing is uploaded, no Slack message;
    • any other failure (verification, a rejected token, network): nothing reaches either channel, the build fails, and Slack gets Gradle's "What went wrong" text. The version stays unpublished, so the next push or a manual run of buildAndUploadPlugin retries it.

Bumping the version

  1. Open META-INF/plugin.xml and patch-bump <version>X.Y.Z</version> to the value that already appears in the <b>Version X.Y.Z</b> header inside the current <change-notes> block — those two MUST agree at upload time so the bundled release notes match the version users see.
  2. Verify every user-visible change since the last release has a <li> bullet in the block (see Release notes below). Add any that are missing.
  3. Run ./gradlew verifyPlugin (the first run downloads the recommended IDEs): a finding fails the release in both channels.
  4. Commit. Subject like Release X.Y.Z is fine; the body should list what's shipping, mirroring the change-notes bullets. Don't squash this with unrelated work — the release commit should diff cleanly to plugin.xml (and only plugin.xml unless the rollout literally needs another file).
  5. Push to master. Jenkins publishes to both channels as described above; Marketplace users get the version after JetBrains approves it.

The change-notes block is intentionally not reset by the release commit — it keeps showing what shipped in X.Y.Z until the next contributor with a user-visible change does the rollover (see below).

Release notes (<change-notes>)

The CDATA block in META-INF/plugin.xml is the only source of release notes; both channels render it verbatim, and there is no parallel CHANGELOG.md. The block is HTML — basic tags only (<b>, <br>, <ul>, <li>, <code>).

What to write. One terse <li> bullet per user-visible improvement, in past-or-imperative tense, framed as what the developer-of-lsFusion sees in the IDE — not as what the plugin internally does. Mention concrete syntax (<code>NEWEXECUTOR ... CLIENT conn</code>), inspection names, settings paths. Skip plugin-internal mechanics (BNF rule names, PSI class names, mixin wiring) — those belong in the commit body, not in the marketplace listing.

When to write. Append a bullet to the change-notes block in the same commit that introduces the user-visible change.

If the <b>Version X.Y.Z</b> header in the block still matches the currently-published <version> (i.e. you are the first contributor with a user-visible change after a release), do the rollover in your commit:

  1. Bump the header to the next patch version: <b>Version X.Y.Z+1</b>.
  2. Empty the <ul> list of the previous release's bullets.
  3. Add your new <li> as the first entry.

If the header is already ahead of <version> (a previous contributor has already rolled over), just append your <li> to the existing list.

This way release notes are maintained lazily — no dedicated cleanup commit, and at any point in time the block accurately reflects "what's queued for the next release."

Examples of changes that warrant an entry:

  • New / changed syntax support in the grammar (parser, lexer, annotator).
  • New inspection, intention, completion behavior, or gutter icon.
  • A deprecation warning the developer will start seeing.
  • Changes to the MCP / AI-assistant tool surface the plugin exposes.
  • Behavior changes in Go To Definition, Find Usages, Refactor, or any IDE action.

Examples of changes that do not warrant an entry:

  • Internal refactors with no observable effect.
  • Build / Gradle / CI tweaks.
  • Regenerated gen/ after a .bnf / .flex edit when the user-visible effect is already covered by another bullet for that edit.
  • Test infrastructure.

If a release accidentally landed with a bullet that turned out to be wrong, fix it in a follow-up <version> bump rather than amending the published one — both channels cache the published notes externally.

GitHub issues

GitHub issues are usually not created for plugin changes — the plugin's release notes aren't generated from closes #N references, and most plugin work either tracks a platform-side issue or is pure IDE tooling. Reference the corresponding platform issue in the commit subject when applicable (e.g. (lsfusion/platform#NNNN)); that's enough for traceability.

If a plugin-only issue is genuinely warranted (e.g. a long-standing IDE-tooling bug that needs its own tracking), use the same template as the platform repo:

### Title
<short imperative title>

### Description
<observed IDE behavior, minimal .lsf reproducer, expected vs. actual outcome>

### Reason
<motivation: why this matters to plugin users>

Frame the Description in terms of what a developer sees in the IDE — wrong highlighting, missing completion, broken Go To Definition, false-positive inspection — and include the smallest .lsf snippet that triggers it. Class names, PSI types, and BNF rule names belong in the ### Fix section (for bug reports) or in the commit body, not in the user-facing Description.

Code conventions

  • PSI mixins/implements for grammar-driven references go under references/ and references/impl/, not declarations/. New formExtendXxx-style rules in the BNF wire mixin to references.impl.*ReferenceImpl and implements to references.*Reference.
  • After adding a new BNF rule that needs validation or highlighting, wire it into LSFReferenceAnnotator (visitor pattern). Most references plug in via visitXxxUsage → checkReference(o).
  • Don't add error handling for impossible PSI states. Trust the generated grammar — for elements whose BNF rule guarantees presence, let null propagate without defensive checks; for optional elements, check explicitly.
  • Minimize call-tree depth and inter-function coupling. A helper called from only one place, doing one obvious thing, is usually clearer inlined; delegation chains where each level just forwards arguments make code harder to follow than one self-contained function. Exception: genuinely primitive or broadly-reused utilities (PSI traversal helpers, well-known infrastructure) — coupling to those is cheap and welcome.
  • Don't comment what the code already says. Add a comment only when why is non-obvious — an IDE platform quirk, a workaround, a subtle invariant.
  • No emojis in source, commits, or comments unless the user explicitly asks.
  • Prefer editing existing files to creating new ones.

Risk discipline

Treat the following as requiring explicit user authorization each time, even if a similar action was authorized earlier in the session:

  • git push and any remote-affecting operation; force-pushes are never automatic.
  • Destructive operations: git reset --hard, git clean -fd, branch deletion, force pushes, rm -rf of working trees or generated directories outside the explicit build outputs (build/, gen/).
  • Anything that publishes outside the local machine: GitHub PR/issue creation, comments, releases, JetBrains Marketplace uploads (./gradlew publishPlugin).
  • Bumping META-INF/plugin.xml <version> — only when explicitly preparing a release (see Releases: the bump publishes to both channels).

Local, reversible work — editing files, running tests, regenerating gen/ via ./gradlew generate* — doesn't need per-action confirmation.

Per-agent extension files

  • CLAUDE.md, GEMINI.md at the repo root are thin wrappers that import this file (Claude Code and Gemini CLI don't auto-read AGENTS.md out of the box).
  • .github/copilot-instructions.md — if added, GitHub Copilot surfaces that don't read AGENTS.md will pick it up. Keep canonical guidance here.