Skip to content

LSP plugins: VS Code parity, hardening, IntelliJ verifier cleanup, tree-sitter race fix#467

Merged
lagergren merged 14 commits into
masterfrom
lsp/vscode2
May 26, 2026
Merged

LSP plugins: VS Code parity, hardening, IntelliJ verifier cleanup, tree-sitter race fix#467
lagergren merged 14 commits into
masterfrom
lsp/vscode2

Conversation

@lagergren
Copy link
Copy Markdown
Contributor

Summary

Polishes both LSP client plugins for stability + parity. Twelve focused commits, each independently revertable. Headline items:

  • VS Code extension — 7 new headless integration tests (@vscode/test-electron based, runs in :lang:vscode-extension:testVscodeExtension), wired into CI under the same lang/** paths-filter as the IntelliJ plugin. Headless via auto-detected xvfb-run on Linux; pops a brief Electron window on macOS local-dev only.
  • IntelliJ plugin — replaces internal PluginManagerCore.getPlugin API usage with the public PluginManager.findEnabledPlugin (JetBrains verifier was flagging it); verifyPlugin now reports Compatible against both IU-261.25134.12 and IU-262.6228.19 with zero internal/deprecated/experimental API hits. Adds 9 new manifest/live-template/bundled-resource tests for cross-IDE parity (22 tests total, up from 13).
  • Branding pass — "XTC → Ecstasy" in user-facing strings on both plugins (display names, command palette labels, dialog titles, run-config display); abbreviations + server product names + internal IDs stay XTC. Mirrors the convention from the IntelliJ plugin.xml's <name>Ecstasy Language Support</name>.
  • Tree-sitter race fixdownloadTreeSitterSource now declares its output file properly, so Gradle's UP-TO-DATE check invalidates when the tar.gz goes missing externally. Closes the extractTreeSitterSource: FileNotFoundException failure mode.
  • runCode PATH preflight — fails fast with a useful message instead of exit code 127 when the code CLI isn't on PATH.
  • Securitypackage-lock.json now tracked (npm's recommendation for apps); npm overrides pin two mocha-transitive deps to safe versions; npm audit: 3 vulnerabilities → 0.
  • Project layout.intellijPlatform/ and node_modules/ ignore rules moved from root .gitignore to a new lang/.gitignore so lang/ is more self-contained ahead of a possible future extraction.
  • Docs — new "Testing" section in the VS Code extension README; new VS Code Extension Playbook in MANUAL_TEST_PLAN.md (covers all 19 LSP features + 16 VS Code-specific concerns); new plugin-stability-followups.md plan tracking deferred items.

Commits (chronological)

1a209c054 test(intellij-plugin): add manifest / live-template / bundled-resource tests for cross-IDE parity (Tier A)
3dc18df59 ci(lang): wire :lang:vscode-extension:testVscodeExtension into the lang validation block
c49b8af21 fix(vscode-extension): runCode now preflights `code` on PATH and fails with a useful message
d00fc0787 fix(tree-sitter): declare downloaded tar.gz as a task output so Gradle invalidates on external deletion
e209b0703 docs(plans): track deferred items from lsp/vscode2 in plugin-stability-followups.md
5655c96a1 chore(vscode-extension): pin diff and serialize-javascript via npm overrides — npm audit now reports 0 vulnerabilities
c3002fe90 refactor(intellij-plugin): extract duplicated plugin-ID literal to PluginPaths.PLUGIN_ID
0e6aaa5f8 fix(intellij-plugin): replace internal PluginManagerCore.getPlugin with public PluginManager API + Ecstasy naming
3ad424cac test(vscode-extension): add 4 integration tests closing parity gaps with intellij-plugin
b4c7a4ee2 docs(vscode-extension): add Testing section to README + VS Code QA playbook in MANUAL_TEST_PLAN
7e44a09e1 chore(gitignore): move lang-scoped ignore rules into lang/.gitignore
d3e4b8d37 feat(vscode-extension): headless integration test + harden file-association + track lockfile

Test plan

  • CI: paths-filter detects lang/** changes → lang validation step runs (it should — every commit in this PR touches lang/)
  • CI: :lang:tree-sitter:testTreeSitterParse 881/881 (no grammar regressions)
  • CI: :lang:lsp-server:test -Plsp.adapter=treesitter green
  • CI: :lang:intellij-plugin:verifyPlugin Compatible against IU-261.x and IU-262.x
  • CI: :lang:intellij-plugin:test 22/22 passing (was 13)
  • CI: :lang:vscode-extension:testVscodeExtension 7/7 passing (new — first PR to exercise this on CI)
  • Manual: ./gradlew :lang:vscode-extension:runCode -PincludeBuildLang=true -PincludeBuildAttachLang=true launches VS Code with hello.x open
  • Manual: with code not on PATH, the same task fails with the new "Install 'code' command in PATH" message rather than exit code 127
  • Manual: rm build/tree-sitter-source/*.tar.gz; ./gradlew :lang:tree-sitter:extractTreeSitterSource ... no longer throws FileNotFoundException
  • Marketplace dry-run: IntelliJ alpha publish next time it's triggered should show no internal-API verifier warnings

Deferred (see lang/doc/plans/plugin-stability-followups.md)

  • IntelliJ Platform fixture-driven runtime tests (Tier B integration test parity)
  • XtcEnterHandlerDelegate.kt / XtcCommenter.kt / etc. for the remaining "XTC → Ecstasy" naming pass
  • lang/lsp-server/ + lang/dap-server/ source-quality review

lagergren added 12 commits May 23, 2026 10:34
…iation + track lockfile

Three related changes that together make the .x file-association behaviour
verifiable in CI and reproducible across developer machines.

Headless integration test
-------------------------
* New @vscode/test-electron based test that launches a real VS Code instance
  with the extension loaded from the build tree, opens a fixture .x file,
  and asserts editor.document.languageId === 'xtc'. This is the only way
  to verify the contributes.languages mapping + the runtime fallback
  short of installing the .vsix and clicking around.
* New :lang:vscode-extension:testVscodeExtension Gradle task wires the
  test into the build. Not auto-attached to `check` because on headless
  Linux it requires xvfb (or DISPLAY) — opt-in only.
* scripts/run-vscode-tests.cjs is a cross-platform launcher that detects
  Linux + no DISPLAY + xvfb-run-on-PATH and wraps with `xvfb-run -a`,
  otherwise launches directly. macOS/Windows local runs pop a brief
  Electron window (unavoidable without true headless on those platforms);
  Linux CI gets fully headless when xvfb is installed.
* src/test/fixtures/hello.x is the smallest realistic Ecstasy module,
  also used as the workspace folder for the existing `runCode` task
  (which previously pointed at a non-existent src/test/fixtures/).

Listener strengthening
----------------------
* extension.ts already had ensureXtcLanguageAssociation listening on
  onDidOpenTextDocument + the existing-documents iteration. Added the
  third signal — onDidChangeActiveTextEditor — which catches the
  "restored tab not yet loaded into vscode.workspace.textDocuments"
  case where onDidOpenTextDocument does not fire on tab restore. With
  all three, a .x file should end up at languageId === 'xtc' regardless
  of whether the user has a stale files.associations setting or another
  extension claiming .x.

Lockfile tracking
-----------------
* package-lock.json is no longer gitignored. Per npm's own documentation
  (since npm 5), apps should commit the lockfile for reproducible
  `npm ci` installs and so that `npm audit fix` results survive in git
  rather than vanishing on the next install. Without this, the audit-fix
  pass we just ran (5 of 8 dev-time vulnerabilities resolved) would
  re-disappear on every clean checkout.
* The remaining 3 vulnerabilities (diff, serialize-javascript, mocha)
  are all transitive through mocha and cannot be fixed without
  downgrading mocha to 11.3.0 — the upstream fix is to wait for mocha
  to roll its deps forward. All three are devDep-only; they never ship
  in the .vsix.

Verified
--------
* :lang:vscode-extension:testVscodeExtension → 1 passing (21 ms)
* :lang:vscode-extension:npmCompile → clean
Equivalent ignore semantics, scoped one level down so lang/ stays
self-contained for a future extraction to its own repository and so the
root .gitignore only describes patterns that are genuinely repo-wide.

What moved
----------
Removed from root .gitignore:
  .intellijPlatform/    # was only used at lang/.intellijPlatform/
  node_modules/         # was only used under lang/

Added to new lang/.gitignore:
  .intellijPlatform/
  node_modules/

What did not move
-----------------
* node_modules/ in lang/vscode-extension/.gitignore stays (per-subproject
  ignore was already there) — the new lang/.gitignore provides a second
  redundant layer that also covers any future lang/dsl/node_modules/ or
  similar without further edits.
* The subproject-level lockfile entries (package-lock.json removal in
  vscode-extension/.gitignore) are part of the previous commit, not this
  reorg.

Verified
--------
git check-ignore -v on representative paths returns hits in the new
locations:
  lang/vscode-extension/node_modules/sharp/index.js -> vscode-extension/.gitignore
  lang/.intellijPlatform/ides/IU-2026.1/build.txt   -> lang/.gitignore
  lang/dsl/node_modules/foo                         -> lang/.gitignore

Root .gitignore no longer contains intellijPlatform or node_modules
patterns.
…aybook in MANUAL_TEST_PLAN

README — new Testing section
----------------------------
Documents the three Gradle entry points (testVscodeExtension, runCode,
npmCompile), explains the platform behaviour of the headless wrapper
(macOS/Windows pop a brief window, Linux+xvfb runs fully headless, Linux
without xvfb fails fast with a clear hint), notes the .vscode-test cache
layout, and points back to MANUAL_TEST_PLAN for interactive QA.

MANUAL_TEST_PLAN — new "VS Code Extension Playbook" section
-----------------------------------------------------------
Self-contained QA runbook so a tester can verify the VS Code extension
end-to-end without flipping back to the IntelliJ-framed test cases.
Mirrors the existing feature numbering (sections 1-19) and references
the per-feature subtest IDs verbatim so the two playbooks stay in
lockstep when features evolve.

Structure:
  * Setup (build + install the .vsix, or runCode for no-install)
  * Keybindings reference table (macOS + Linux/Windows)
  * Pre-flight checks P1-P7 (extension loaded, .x maps to Ecstasy,
    LSP up, adapter selected, semantic tokens on, Java found, snippets
    work)
  * Feature playbook table — one row per LSP feature 1-19, with the
    exact VS Code action + verification surface
  * VS Code-specific concerns V1-V16 that have no IntelliJ analogue:
    stale-profile file-association recovery (V1), tab-restore
    association (V2), status bar lifecycle, trace/output channels,
    Java discovery + auto-download, Gradle tasks integration, DAP
    launch (default + custom), Create-Project command, clean
    uninstall/reinstall, file/marketplace icons.
  * Closes with a pointer to the automated headless regression test
    so testers know which surface IS covered by CI vs which still
    needs interactive verification.

The IntelliJ-only sections (16-18 in the existing plan) are
re-targeted to VS Code where applicable (snippets, code lens, comment
toggling all have direct VS Code equivalents).
…ith intellij-plugin

Audit of lang/intellij-plugin/src/test vs lang/vscode-extension/src/test
showed the two sides cover disjoint surfaces: IntelliJ has a JAR-resolution
unit test (LspServerJarResolutionTest), VS Code had only a file-association
integration test. The four tests added here are the smallest set that
closes the most common regression classes against the VS Code side.

1. Bundled server JARs (mirrors IntelliJ's LspServerJarResolutionTest)
   * lang/vscode-extension/src/test/suite/jar-bundling.test.ts
   * Asserts server/lsp-server.jar and server/dap-server.jar exist at
     the runtime path extension.ts resolves them from, and that
     lsp-server.jar is plausibly a fat JAR (>1 MB sanity floor).
   * Catches: copyLspServer / copyDapServer Gradle tasks falling out of
     sync with extension.ts's context.asAbsolutePath() calls.

2. Extension activation surfaces
   * lang/vscode-extension/src/test/suite/activation.test.ts
   * Asserts the four contributed command IDs from package.json are
     present in `vscode.commands.getCommands()` after activate(), and
     that xtc.showServerOutput executes without throwing.
   * Catches: a silent throw inside activate(), or a command-ID drift
     between commands.ts and package.json.

3. LSP startup smoke test
   * lang/vscode-extension/src/test/suite/lsp-startup.test.ts
   * Opens hello.x, fires a hover request at `console.print`, polls up
     to 30s for the LSP server to respond. End-to-end LSP RPC, not just
     startup logging.
   * Catches: LSP JVM failing to start (missing JAR, wrong Java, tree-
     sitter native lib not loaded, adapter selection broken).
   * Observed wall time on warm machine: ~1.4s (well under budget).

4. Snippet contributions (bonus, "check back" item from the audit)
   * lang/vscode-extension/src/test/suite/snippets.test.ts
   * Asserts the package.json contributes.snippets pipeline produces
     Snippet-kind completions for the "mod" prefix, and that one of
     them contains the literal "module " keyword from snippets/xtc.json.
   * Uses executeCompletionItemProvider rather than simulating
     keystrokes, which avoids the well-known @vscode/test-electron
     race on completion accept.
   * I expected this one to be flaky on CI; it ran clean.

Combined result: 7 passing, 1s total wall time on a warm machine.

   Snippet contributions
     ✔ snippet "mod" expands to a module declaration
   LSP startup
     ✔ LSP server responds to a hover request on hello.x
   Bundled server JARs
     ✔ lsp-server.jar is present at the expected runtime path
     ✔ dap-server.jar is present at the expected runtime path
   Ecstasy file association
     ✔ opens .x files with languageId "xtc"
   Extension activation surfaces
     ✔ all contributed commands are registered
     ✔ xtc.showServerOutput executes without throwing
…th public PluginManager API + Ecstasy naming

JetBrains plugin verifier flagged three call sites using the internal
PluginManagerCore.getPlugin(PluginId) API (see
https://plugins.jetbrains.com/docs/intellij/api-internal.html). The
documented public replacement is PluginManager.getInstance().findEnabledPlugin(PluginId),
which returns IdeaPluginDescriptor? with the same semantics our code uses
(read pluginPath / version). Since all three sites run from inside the
plugin's own classes — i.e. the plugin is definitionally enabled when
the code executes — the "enabled-only" restriction of the replacement is
a no-op for us.

Sites converted:
* PluginPaths.findServerJar — resolving the LSP/DAP server JAR location
* XtcTextMateBundleProvider.getBundles — resolving the TextMate bundle path
* XtcNewProjectWizardStep.setupProject — reading the plugin version

Naming pass on the same files
-----------------------------
Same XTC -> Ecstasy convention I applied in the VS Code extension, now
mirrored here: the plugin's product name is "Ecstasy Language Support"
per plugin.xml, so user-facing strings and log lines that reference the
plugin/project as an entity now read "Ecstasy". Server product names
("XTC Language Server", "XTC DAP server"), env vars (XTC_LSP_*,
XTC_LOG_LEVEL), internal IDs (`XtcRunConfiguration` configuration-type
id, `id = "XTC"` on the wizard), bundle identifiers, and command IDs
all stay as-is for stability and to match plugin.xml.

User-visible string changes:
* New Project wizard label: "XTC" -> "Ecstasy"
* Run configuration display name: "XTC Application" -> "Ecstasy Application"
* Run configuration description: "Run an XTC application" -> "Run an Ecstasy application"
* Run-config Module field tooltip: "The XTC module to run" -> "The Ecstasy (.xtc) module to run"
* Project-creation error dialog title: "XTC Project Creation Failed" -> "Ecstasy Project Creation Failed"

Log entity-reference changes:
* XtcTextMateBundleProvider: "XTC plugin not found" / "XTC plugin path" -> "Ecstasy plugin ..."
* XtcNewProjectWizardStep: "Creating/Failed XTC project" -> "... Ecstasy project"
* XtcRunConfiguration: "Running XTC: <cmd>" -> "Running Ecstasy: <cmd>"
* XtcEditorStartupActivity: "XTC editor/file diagnostics" -> "Ecstasy editor/file diagnostics"

Verified
--------
* :lang:intellij-plugin:compileKotlin clean (no PluginManagerCore left)
* :lang:intellij-plugin:test green
* :lang:intellij-plugin:buildPlugin produces a clean .zip
…uginPaths.PLUGIN_ID

`PluginPaths.PLUGIN_ID = "org.xtclang.idea"` already existed but was
declared `private const val`, so the two other call sites that needed
the same literal (XtcTextMateBundleProvider, XtcNewProjectWizardStep)
each hardcoded the string independently. Promoting the constant to
public (it remains a `const val` on the `PluginPaths` singleton, so
referenced as `PluginPaths.PLUGIN_ID`) and replacing the two duplicates
gives us one source of truth that must agree with `<id>` in plugin.xml.

Without this, if `<id>` ever changes, two of three sites silently
break — wrong plugin descriptor returned → wrong path resolved →
opaque NullPointerException at runtime.

No behaviour change.
…errides — npm audit now reports 0 vulnerabilities

Background: after committing package-lock.json in d3e4b8d, the three
remaining `npm audit` findings (transitive through mocha) became visible
in source control with no path to fix them on the next install. All
three trace to two specific subdeps:

  diff <= 8.0.2                 (GHSA-73rr-hh4g-fpgx, DoS)
  serialize-javascript <= 7.0.4 (GHSA-5c6j-r48x-rmvq, RCE / DoS)

mocha 11.7.4 still requires 8.x of diff and 6.x/7.x of
serialize-javascript, so we can't simply bump mocha to fix them — but
npm `overrides` lets us pin those subdeps to safe minimum versions
inside mocha's tree without changing the dep itself. Both pins stay
within mocha's expected major range:

  "overrides": {
    "diff": "^8.0.3",                 // mocha wants 8.x; 8.0.4 is safe
    "serialize-javascript": "^7.0.5"  // mocha wants 7.x; 7.0.5 is safe
  }

Remove the overrides the next time mocha bumps to a release that pulls
in safe transitive versions on its own.

Verified
--------
* `npm audit`: 0 vulnerabilities (was 3 — 1 low, 1 moderate, 1 high)
* `:lang:vscode-extension:testVscodeExtension`: 7 passing (was 7 passing)
* `:lang:intellij-plugin:test`, `:buildPlugin`: green
…y-followups.md

Consolidates everything we noticed but explicitly deferred during this
branch so the items don't get lost. New file
`lang/doc/plans/plugin-stability-followups.md` covers:

  Build / tooling
  1. :lang:tree-sitter:extractTreeSitterSource race — redundant onlyIf
     on the download task interacts badly with the configuration cache;
     fix is a 3-line removal in lang/tree-sitter/build.gradle.kts.
  2. :lang:vscode-extension:runCode hardcodes the `code` CLI without
     a PATH check — fails opaquely if VS Code's "Install shell command"
     step was never run.
  3. Wire :lang:vscode-extension:testVscodeExtension into CI — needs
     apt-get install -y xvfb on the Linux runner; ~10 lines of YAML.

  IntelliJ plugin
  4. Run verifyPlugin post-PluginManagerCore fix to confirm the
     warning is gone — done ahead of this commit; result: "Compatible"
     against both IU-261.25134.12 and IU-262.6228.19. Item resolved
     within this branch but documented for the historical record.
  5. Source files not yet audited for the XTC -> Ecstasy naming pass
     (XtcEnterHandlerDelegate, XtcCommenter, XtcRunConfigurationProducer,
     XtcIconProvider, XtcIntelliJLanguage). Same rules apply: product
     entity -> "Ecstasy", server product names + internal IDs stay.

  Out of scope (for context)
  6. lang/lsp-server / lang/dap-server source quality review.
  7. Bringing IntelliJ-side integration test surface coverage closer
     to the 7 tests VS Code now has.

Also adds an entry to lang/doc/plans/README.md's "Active Plans" table
pointing at the new file.
…e invalidates on external deletion

The race: when build/tree-sitter-source/tree-sitter-X.tar.gz was deleted
externally (manual cleanup, IDE cache reset, etc.) but the rest of
build/tree-sitter-source/ remained intact, the next build hit a
FileNotFoundException from extractTreeSitterSource — even though
downloadTreeSitterSource was sitting right there to provide the file.

Root cause: downloadTreeSitterSource only used the Download plugin's
`dest(...)` API to specify where the tar.gz went, plus a custom
`onlyIf { !File(destPath).exists() }` predicate. It never declared an
`outputs.file(...)` so Gradle's standard UP-TO-DATE machinery had no
opinion about whether the task should re-run. With the configuration
cache reused and the Download plugin's internal `.lastmodified` marker
still present from the prior run, all three gates (custom onlyIf,
plugin overwrite=false, plugin onlyIfModified=true) could conspire to
report the task as SKIPPED while leaving destPath nonexistent.
extractTreeSitterSource then ran (since the dependency declared "no
work needed"), called tarGzFile.get(), and threw at I/O time.

Fix: declare the tar.gz file as a proper Gradle output:

  outputs.file(destFile)

With this, Gradle's standard up-to-date logic checks the output's
existence at task scheduling time. If the file is missing, the task is
not-up-to-date and re-runs regardless of any plugin-internal markers
or cached onlyIf results. The previous custom `onlyIf` becomes
redundant and was removed; `overwrite(false)` and `onlyIfModified(true)`
are kept because they control HTTP-level semantics (don't truncate an
existing file, do honor server Last-Modified) and don't interact with
Gradle's task-lifecycle gates.

Verified
--------
1. Clean baseline:   tar.gz downloaded, source extracted -> SUCCESSFUL.
2. Simulate the race by deleting both tar.gz files from
   build/tree-sitter-source/ (the extracted source directory remains).
3. Re-run :lang:tree-sitter:extractTreeSitterSource:
   downloadTreeSitterSource RUNS (re-fetches the missing tar.gz),
   extractTreeSitterSource is UP-TO-DATE (extracted dir intact).
   No FileNotFoundException.
Configuration cache hits both runs ("Reusing configuration cache").

Also removes the now-resolved item from
lang/doc/plans/plugin-stability-followups.md and adds a brief
"Resolved during lsp/vscode2" section at the bottom of that file for
historical context.
…s with a useful message

Previously :lang:vscode-extension:runCode hardcoded `commandLine("code", ...)`
and let Gradle's Exec run unconditionally. On a machine where the VS Code
shell command was never installed (the "Shell Command: Install 'code'
command in PATH" Command Palette action is optional after a .app/.dmg
install), the task failed with the cryptic:

  Process 'command 'code'' finished with non-zero exit value 127

…which is the standard "command not found" exit code, with no hint at
the actual cause or fix.

The task now resolves PATH at config time (CC-safe via
providers.environmentVariable), captures the OS family + binary
candidates (`code` on macOS/Linux, `code.cmd` / `code.exe` on Windows),
and in a doFirst checks whether any candidate is executable in any of
the directories. If not, throws a GradleException with concrete
remediation steps:

  1. Open VS Code, Cmd+Shift+P / Ctrl+Shift+P -> "Shell Command: Install
     'code' command in PATH". Open a new shell after. Re-run the task.
  2. Or: open lang/vscode-extension/ in VS Code and press F5.

Verified
--------
* `./gradlew :lang:vscode-extension:tasks` configures cleanly with the
  new doFirst (build script CC-compatible).
* `runCode` description still resolves: "Launch VS Code with the
  extension loaded for testing".

Also marks the item resolved in
lang/doc/plans/plugin-stability-followups.md.
…ng validation block

Mirrors how :lang:tree-sitter:testTreeSitterParse / :lang:lsp-server:test /
:lang:intellij-plugin:verifyPlugin are gated: the new test runs on the
same paths-filter gate (lang/** changed, or workflow_dispatch with
include-lang / publish / force flags), so the cost (a one-off VS Code
download into the runner's .vscode-test cache) is only paid when lang/
work is actually being verified.

Adds two things to the existing "Validate" step:
  1. A xvfb install guarded by `command -v xvfb-run` — ubuntu-latest images
     ship with it sometimes but not always, so we install on demand instead
     of running an unconditional apt-get on every CI run.
  2. The Gradle invocation
     `:lang:vscode-extension:testVscodeExtension`
     which runs the 7 integration tests added earlier in this branch.

The wrapper script scripts/run-vscode-tests.cjs already handles the Linux+
no-DISPLAY case by prepending xvfb-run automatically, so the CI command line
stays identical to the local-dev invocation.

Renames:
* "Skip Tree-sitter (no lang validation triggered)" -> "Skip lang
  validation (no lang/ changes)"
* "Validate Tree-sitter Grammar and LSP Adapters" -> "Validate lang
  (tree-sitter grammar, LSP adapters, VS Code extension)"

So the step labels accurately describe everything the block now covers.

Verified
--------
* YAML parses cleanly via `ruby -ryaml`.
…e tests for cross-IDE parity (Tier A)

Counterpart to the 7 VS Code integration tests added earlier in this
branch. Tier A intentionally stays at the same JUnit + assertj layer
as the existing `LspServerJarResolutionTest` — file-system level
self-consistency tests, no IntelliJ Platform test fixtures required.
Tier B (BasePlatformTestCase-driven runtime registration tests) is
left for a separate branch; tracked in plugin-stability-followups.md.

New tests (9 cases across 3 files)
----------------------------------
* PluginManifestTest — parses META-INF/plugin.xml and asserts:
  - <id> matches PluginPaths.PLUGIN_ID (single source of truth)
  - declares the 13 extension points the Kotlin source relies on
    (newProjectWizard.generator, configurationType,
    runConfigurationProducer, iconProvider, defaultLiveTemplates,
    lang.commenter, langCodeStyleSettingsProvider,
    enterHandlerDelegate, postStartupActivity, notificationGroup,
    LSP4IJ server, fileNamePatternMapping, textmate.bundleProvider)
  - LSP4IJ server id is xtcLanguageServer and *.x routes there
  Counterpart to VS Code activation.test.ts.

* LiveTemplateRegistrationTest — parses liveTemplates/XTC.xml and
  asserts the 14 snippet shortcuts the README + MANUAL_TEST_PLAN
  document (mod, cls, iface, svc, mix, enu, con, pkg, meth, run,
  prop, construct, if, ife) are present with an OTHER context.
  Counterpart to VS Code snippets.test.ts.

* BundledResourcesTest — asserts META-INF/plugin.xml,
  liveTemplates/XTC.xml, and icons/xtc.svg are on the classpath.
  Counterpart to VS Code jar-bundling.test.ts (which checks
  server JARs; that's already covered by LspServerJarResolutionTest
  on the IntelliJ side, so we don't duplicate it here).

Implementation detail: resources are loaded via
URL.openStream() rather than File(URL.toURI()) because the test
classpath ships resources inside the instrumented plugin JAR — File(URI)
throws IllegalArgumentException for jar: URLs but openStream() handles
both shapes.

Verified
--------
:lang:intellij-plugin:test: 22 tests, 0 failures, 0 errors
  (was 13 before this commit — 9 new passing.)

Also updates plugin-stability-followups.md to scope item #3 to the
remaining Tier B work and note Tier A as resolved in this branch.
lagergren and others added 2 commits May 23, 2026 15:33
…sion stamping

Lets us produce a `.vsix` whose internal version is e.g. `0.4.4-alpha.20260523T133229`
without permanently bumping package.json or affecting anything outside
lang/vscode-extension/. The use case is publishing pre-release alphas
to the VS Code Marketplace while keeping the canonical XDK-aligned
version (0.4.4) untouched in source control.

How it works
------------
* New optional Gradle property: `-Pvscode.version.suffix=<suffix>`. Without
  it, packageExtension behaves exactly as before.
* New stampVscodeVersion task (runs before packageExtension when suffix
  is present): backs up package.json to package.json.version-backup, then
  rewrites the "version" field to "<base>-<suffix>". Existing pre-release
  tags on the base version are stripped first, so the operation is
  idempotent across repeat invocations.
* New restoreVscodeVersion task wired via finalizedBy on packageExtension:
  restores package.json from the backup and deletes the backup file. Runs
  whether packageExtension succeeded or failed, so an interrupted build
  doesn't leave the source tree mutated.
* Suffix declared as an inputs.property on packageExtension so switching
  the value invalidates the task cache (otherwise the .vsix that gets
  produced is whichever the source tree was last packaged with).

Removed the dead `outputs.file("xtc-language-${project.version}.vsix")`
declaration on packageExtension while I was here — `project.version`
evaluates to "unspecified" in this subproject, so the path it referenced
never matched the actual file vsce produces (which uses package.json's
version). The line had been a no-op for UP-TO-DATE purposes since it
was added.

Scope: ONLY affects what gets baked into the .vsix's manifest. No
mutation visible from composite root, no effect on any other subproject,
no effect on the existing tests or build outputs when the property is
absent. CC-safe (uses providers.gradleProperty + serializable values
captured into the task actions).

Verified
--------
* Without -Pvscode.version.suffix: package.json untouched,
  xtc-language-0.4.4.vsix produced, version inside .vsix = "0.4.4".
* With -Pvscode.version.suffix=alpha.20260523T133229: package.json
  briefly stamped + restored after build, the produced .vsix is named
  xtc-language-0.4.4-alpha.20260523T133229.vsix with the matching
  version inside.
* :lang:vscode-extension:testVscodeExtension still passes 7/7.
@lagergren lagergren merged commit 7820a55 into master May 26, 2026
4 checks passed
@lagergren lagergren deleted the lsp/vscode2 branch May 26, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants