LSP plugins: VS Code parity, hardening, IntelliJ verifier cleanup, tree-sitter race fix#467
Merged
Conversation
…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.
ggleyzer
approved these changes
May 23, 2026
…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.
…ciations on activation
ezekielelvis
approved these changes
May 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Polishes both LSP client plugins for stability + parity. Twelve focused commits, each independently revertable. Headline items:
@vscode/test-electronbased, runs in:lang:vscode-extension:testVscodeExtension), wired into CI under the samelang/**paths-filter as the IntelliJ plugin. Headless via auto-detectedxvfb-runon Linux; pops a brief Electron window on macOS local-dev only.PluginManagerCore.getPluginAPI usage with the publicPluginManager.findEnabledPlugin(JetBrains verifier was flagging it);verifyPluginnow reportsCompatibleagainst bothIU-261.25134.12andIU-262.6228.19with 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).XTC. Mirrors the convention from the IntelliJplugin.xml's<name>Ecstasy Language Support</name>.downloadTreeSitterSourcenow declares its output file properly, so Gradle's UP-TO-DATE check invalidates when the tar.gz goes missing externally. Closes theextractTreeSitterSource: FileNotFoundExceptionfailure mode.runCodePATH preflight — fails fast with a useful message instead ofexit code 127when thecodeCLI isn't on PATH.package-lock.jsonnow tracked (npm's recommendation for apps);npm overridespin two mocha-transitive deps to safe versions;npm audit: 3 vulnerabilities → 0..intellijPlatform/andnode_modules/ignore rules moved from root.gitignoreto a newlang/.gitignoresolang/is more self-contained ahead of a possible future extraction.MANUAL_TEST_PLAN.md(covers all 19 LSP features + 16 VS Code-specific concerns); newplugin-stability-followups.mdplan tracking deferred items.Commits (chronological)
Test plan
paths-filterdetectslang/**changes → lang validation step runs (it should — every commit in this PR toucheslang/):lang:tree-sitter:testTreeSitterParse881/881 (no grammar regressions):lang:lsp-server:test -Plsp.adapter=treesittergreen:lang:intellij-plugin:verifyPluginCompatible against IU-261.x and IU-262.x:lang:intellij-plugin:test22/22 passing (was 13):lang:vscode-extension:testVscodeExtension7/7 passing (new — first PR to exercise this on CI)./gradlew :lang:vscode-extension:runCode -PincludeBuildLang=true -PincludeBuildAttachLang=truelaunches VS Code withhello.xopencodenot on PATH, the same task fails with the new "Install 'code' command in PATH" message rather thanexit code 127rm build/tree-sitter-source/*.tar.gz; ./gradlew :lang:tree-sitter:extractTreeSitterSource ...no longer throwsFileNotFoundExceptionDeferred (see
lang/doc/plans/plugin-stability-followups.md)XtcEnterHandlerDelegate.kt/XtcCommenter.kt/ etc. for the remaining "XTC → Ecstasy" naming passlang/lsp-server/+lang/dap-server/source-quality review