diff --git a/.gitignore b/.gitignore index 21c7a23..87cff3f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # Stores VSCode versions used for testing VSCode extensions /extension/.vscode-test/ /extension/out/ +/extension/server/ /.serena/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 442a06c..2e623b9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,10 +6,10 @@ "source.fixAll": "explicit" }, "files.exclude": { - "extension/out": false // set this to true to hide the "out" folder with the compiled JS files + "extension/out": true }, "search.exclude": { - "extension/out": true // set this to false to include "out" folder in search results + "extension/out": true }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "js/ts.tsc.autoDetect": "off" diff --git a/README.md b/README.md index 5f6e34f..fc38b75 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # openfasttrace-vscode-extension -This is the README for your extension "openfasttrace". After writing up a brief description, we recommend including the following sections. +OpenFastTrace provides a VS Code client for the OpenFastTrace language server. +This initial slice launches the bundled server and supports the standard +`workspace/symbol` request. [![Build](https://github.com/itsallcode/openfasttrace-vscode-extension/actions/workflows/build.yml/badge.svg)](https://github.com/itsallcode/openfasttrace-vscode-extension/actions/workflows/build.yml) [![Quality gate status](https://sonarcloud.io/api/project_badges/measure?project=itsallcode_openfasttrace-vscode-extension&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=itsallcode_openfasttrace-vscode-extension) @@ -18,17 +20,17 @@ This is the README for your extension "openfasttrace". After writing up a brief ## Features -Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. - -For example if there is an image subfolder under your extension project workspace: - -\!\[feature X\]\(images/feature-x.png\) - -> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. +The extension starts the staged Java language-server distribution and uses LSP +for portable editor behavior. The first supported request is +`workspace/symbol`; it currently returns no symbols until workspace indexing is +implemented. ## Requirements -If you have any requirements or dependencies, add a section describing those and how to install and configure them. +The extension requires Java 21 or later. It uses `openfasttrace.java.home` when +configured, then `JAVA_HOME`, then the `java` executable on `PATH`. The server +distribution is packaged with the extension, so no global server installation +is needed. ## Extension Settings @@ -36,10 +38,9 @@ Include if your extension adds any VS Code settings through the `contributes.con For example: -This extension contributes the following settings: - -* `myExtension.enable`: Enable/disable this extension. -* `myExtension.thing`: Set to `blah` to do something. +This extension contributes `openfasttrace.java.home`, an absolute path to a +Java 21 or later runtime. Configure it when VS Code cannot find a suitable +runtime through `JAVA_HOME` or `PATH`. ## Known Issues diff --git a/build.gradle b/build.gradle index e6668d2..55f6a42 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,6 @@ sonar { requirementTracing { inputDirectories = files('doc') reportVerbosity = 'FAILURE_DETAILS' - filteredArtifactTypes = ['feat', 'req', 'scn'] filteredTags = ['mvp'] filterAcceptsItemsWithoutTag = false } diff --git a/doc/changesets/02-integrate-lsp.md b/doc/changesets/02-integrate-lsp.md new file mode 100644 index 0000000..2672a3e --- /dev/null +++ b/doc/changesets/02-integrate-lsp.md @@ -0,0 +1,95 @@ +# 02 Integrate the Language Server + +## Goal + +Connect the VS Code extension to the standalone Java language server and prove +that the packaged server can complete a standard LSP request from the VS Code +client. + +## Scope + +In scope: + +* Launch the Java 21 or later LSP server over stdio from the server application + distribution. +* Start and stop the VS Code language client with the extension lifecycle. +* Stage the server distribution as an extension asset for development and + packaging. +* Advertise and handle the initial `workspace/symbol` capability so the + integration test exercises a real request/response round trip. +* Add the required extension integration test, dependency updates, trace + coverage, and developer documentation. + +Out of scope: + +* Full OFT parsing, indexing, navigation, completion, semantic tokens, and + trace execution. +* Trace profiles, structured trace views, snippets, and editor highlighting. +* Packaging or publishing workflow beyond making the server distribution + available in the extension asset layout. + +## Design References + +* [System Requirements](../system_requirements.md) +* [Design](../design.md) +* [LSP-first Architecture](../design/architecture.md) +* [Quality Requirements](../design/quality_requirements.md) + +## Strategy + +Use `vscode-languageclient/node` with a stdio `ServerOptions` executable. The +client resolves Java in the documented order (`openfasttrace.java.home`, +`JAVA_HOME`, `PATH`), validates Java 21 or later before launch, and reports an actionable +startup error. The client launches the staged Gradle `installDist` executable; +the server keeps stdout exclusively for LSP traffic. The first portable server +feature is `workspace/symbol`, initially returning an empty result while the +workspace index is implemented in a later changeset. + +## Task List + +### Requirements And Design + +- [x] Confirm the existing MVP requirements and LSP-first architecture remain + accurate for this incremental integration. +- [x] Add or update traced `impl` and `itest` items for client startup, + capability negotiation, and the workspace-symbol round trip, all tagged + `mvp`. +- [x] Extend the requirements-tracing verification configuration to include the + new design, implementation, and integration-test chain. + +### Implementation + +- [x] Add the locked `vscode-languageclient` runtime dependency. +- [x] Implement extension client lifecycle, document selector, Java resolution, + server executable selection, startup error handling, and disposal. +- [x] Add Gradle tasks/dependencies to build `:server:installDist` and stage its + output under the extension’s ignored server asset directory. +- [x] Advertise `workspaceSymbolProvider` in the Java server and implement the + workspace-symbol request with an empty, non-null result. +- [x] Remove the placeholder hello-world activation path and register only the + LSP-backed extension behavior for this slice. + +### Verification + +- [x] Add a VS Code integration test that activates the extension, waits for the + client to become ready, sends `workspace/symbol`, and asserts a successful + empty response from the live Java server. +- [x] Keep the existing Java capability test and add protocol-level coverage for + workspace-symbol capability negotiation and response compatibility. +- [x] Run server tests, extension compile/lint/unit/integration tests, the full + Gradle build, and the MVP requirements trace. +- [x] Keep the OpenFastTrace trace clean and ensure all new MVP implementation + and test items are covered. + +### Update User Documentation + +- [x] Update `README.md` and `doc/developer_guide.md` with LSP startup, + Java 21 or later, and server-distribution build/test requirements. + +## Version and Changelog Update + +- [ ] Check the current version against the latest GitHub release. +- [ ] Increment the semantic version for this feature integration. +- [ ] Add the release changelog entry, including the bundled OpenFastTrace + version from resolved Gradle dependency metadata. +- [ ] Update the release date and issue list when a tracker number is assigned. diff --git a/doc/design.md b/doc/design.md index 4dd900e..e3415ca 100644 --- a/doc/design.md +++ b/doc/design.md @@ -27,7 +27,7 @@ material advantage for this standalone, standard-protocol server. The server is a Gradle project using the Groovy DSL and the `application` plugin. Gradle produces the runnable distribution that the VS Code extension -launches. The server targets Java 21. The VS Code extension is also a Gradle +launches. The server targets Java 21 as its minimum runtime version. The VS Code extension is also a Gradle subproject: its lifecycle tasks install locked Node.js dependencies, compile, lint, and test the TypeScript adapter. When available, extension integration tests run under Xvfb; otherwise they run directly. Consequently, `./gradlew @@ -36,7 +36,7 @@ runs the requirements trace as an independent verification task and does not define the language-server build. VS Code extensions execute in a Node.js extension host and do not receive a -Java runtime from VS Code. The extension therefore obtains a Java 21 runtime +Java runtime from VS Code. The extension therefore obtains a Java 21 or later runtime from the configured `openfasttrace.java.home`, then `JAVA_HOME`, then `PATH`. It validates the selected runtime before launching the server and reports an actionable configuration error when no compatible runtime is available. diff --git a/doc/design/architecture.md b/doc/design/architecture.md index 4ff3101..77c6cf0 100644 --- a/doc/design/architecture.md +++ b/doc/design/architecture.md @@ -3,7 +3,7 @@ ## Runtime Boundary ### OFT Language Server -`dsn~oft-language-server~1` +`dsn~oft-language-server~2` The standalone Java LSP server owns parsing of OFT declarations and coverage tags, a workspace index, link resolution, ranked completion, and invocation of @@ -17,19 +17,17 @@ protocol messages; diagnostics and operational logs are written elsewhere. Tags: mvp Covers: -- `scn~discover-and-follow-a-trace-link~1` -- `scn~author-an-oft-link~1` -- `scn~trace-the-mvp-scope~1` +- `scn~start-and-negotiate-language-server~1` Needs: impl ### VS Code Language Client -`dsn~vscode-language-client~1` +`dsn~vscode-language-client~2` The TypeScript extension launches the language server, maps the workspace and supported document selectors, forwards configuration and file events, and registers VS Code commands. It contains no competing OFT parser or index. As -VS Code does not provide Java to extensions, the client resolves Java 21 from +VS Code does not provide Java to extensions, the client resolves Java 21 or later from `openfasttrace.java.home`, then `JAVA_HOME`, then `PATH`, validates it before startup, and reports a configuration error if none is suitable. It launches the server using the Gradle `application` distribution rather than assuming a @@ -38,12 +36,51 @@ globally installed server executable. Tags: mvp Covers: -- `scn~discover-and-follow-a-trace-link~1` -- `scn~author-an-oft-link~1` -- `scn~trace-the-mvp-scope~1` +- `scn~start-and-negotiate-language-server~1` Needs: impl +### LSP Client Startup +`impl~lsp-client-startup~1` + +The extension validates a configured Java 21 or later runtime before launching +the staged server distribution. It prefers `openfasttrace.java.home`, then +`JAVA_HOME`, then `PATH`; it sets `JAVA_HOME` for a selected explicit runtime +and stops the language client when VS Code deactivates the extension. + +Tags: mvp + +Covers: +- `dsn~vscode-language-client~2` + +Needs: itest + +### Workspace Symbol Protocol Slice +`impl~workspace-symbol-protocol-slice~1` + +The server advertises the standard `workspace/symbol` capability and returns +an empty, non-null result until workspace indexing is introduced. + +Tags: mvp + +Covers: +- `dsn~oft-language-server~2` + +Needs: itest + +### Workspace Symbol End-To-End Test +`itest~workspace-symbol-end-to-end~1` + +The VS Code integration test activates the extension, waits for the language +client, sends `workspace/symbol`, and verifies that the staged server returns +an empty result. + +Tags: mvp + +Covers: +- `impl~lsp-client-startup~1` +- `impl~workspace-symbol-protocol-slice~1` + ### Editor Presentation `dsn~editor-presentation~1` @@ -52,8 +89,6 @@ TextMate injection grammar for immediate declaration coloring, and maps LSP semantic tokens to OFT coverage-tag styling. It preserves the host language of every document. -Tags: mvp - Covers: - `scn~recognize-and-highlight-oft-content~1` - `scn~author-an-oft-link~1` @@ -68,8 +103,6 @@ request with workspace-relative inputs and filters. The server returns a typed trace report containing status, messages, and source locations. The adapter renders it in an Output channel and opens locations from report entries. -Tags: mvp - Covers: - `scn~trace-the-mvp-scope~1` @@ -122,7 +155,7 @@ opening remain client adapters because they are not LSP UI concepts. ## Build And Distribution The language server is a standalone Gradle project using the Groovy DSL and -the `application` plugin, targeting Java 21. Its distribution contains the +the `application` plugin, targeting Java 21 as its minimum runtime. Its distribution contains the server launcher and all runtime dependencies. The extension packages and launches the matching distribution for the user's platform. The OpenFastTrace Gradle task remains separate from the server build. diff --git a/doc/developer_guide.md b/doc/developer_guide.md index 2f94fbc..edc6173 100644 --- a/doc/developer_guide.md +++ b/doc/developer_guide.md @@ -4,7 +4,7 @@ Prerequisites: -* Java 21 +* Java 21 or later * Node.js 24 and its bundled `npm` * On Linux, `xvfb-run` is optional and enables headless VS Code integration tests @@ -26,6 +26,12 @@ these focused tasks when needed: ./gradlew :extension:integrationTestExtension ``` +The extension test tasks first run `:server:installDist` and stage its +distribution under `extension/server`. The staged executable is the only +server executable used by the extension. At runtime, it accepts Java 21 or a +newer release, resolving it from `openfasttrace.java.home`, `JAVA_HOME`, then +`PATH`. + When `xvfb-run` is available, `integrationTestExtension` launches the VS Code test runtime through `xvfb-run -a`; otherwise it invokes the npm integration-test script directly. @@ -38,9 +44,8 @@ Run the current MVP requirements trace locally with Java 17 or later: ./gradlew traceRequirements ``` -The CI workflow runs this command. It intentionally validates the completed -feature-to-scenario chain. Add design, implementation, and test artifact types -when their coverage tags are implemented. +The CI workflow runs this command. It validates the MVP requirements, design, +implementation, and integration-test chain. ## Server diff --git a/doc/system_requirements.md b/doc/system_requirements.md index a0a2e62..b6a6709 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -26,6 +26,14 @@ only that scope. The product recognizes OFT declarations in Markdown, reStructuredText, and coverage tags in supported workspace files. +Needs: req + +### Connect the Language Server +`feat~connect-language-server~1` + +The product launches the packaged OpenFastTrace language server and negotiates +its initial standard LSP capabilities with VS Code. + Tags: mvp Needs: req @@ -36,8 +44,6 @@ Needs: req The product finds OFT declarations and follows coverage links throughout the workspace. -Tags: mvp - Needs: req ### Author OFT Items and Links @@ -46,8 +52,6 @@ Needs: req The product provides item templates and context-aware completion of declared OFT IDs. -Tags: mvp - Needs: req ### Run OFT Traces @@ -56,8 +60,6 @@ Needs: req The product runs OpenFastTrace for the workspace and presents a navigable report. -Tags: mvp - Needs: req ### Configure Repeatable Traces @@ -91,10 +93,21 @@ Needs: req The editor highlights OFT declarations in `.md`, `.markdown`, and `.rst` documents without changing the documents' ordinary language mode. +Covers: +- `feat~recognize-oft-content~1` + +Needs: scn + +### Start the Language Server +`req~start-language-server~1` + +When the extension activates, it starts the packaged language server with a +Java 21 or later runtime and establishes the initial standard LSP connection. + Tags: mvp Covers: -- `feat~recognize-oft-content~1` +- `feat~connect-language-server~1` Needs: scn @@ -104,8 +117,6 @@ Needs: scn The editor highlights OFT coverage tags in source, configuration, and markup files supported by the OpenFastTrace tag importer. -Tags: mvp - Covers: - `feat~recognize-oft-content~1` @@ -117,8 +128,6 @@ Needs: scn Users can find workspace declarations by full or partial OFT item ID and open the matching declaration. -Tags: mvp - Covers: - `feat~discover-and-navigate-oft-items~1` @@ -131,8 +140,6 @@ Users can go from a `Covers:` ID or either side of a coverage tag to the corresponding declaration. Missing name and revision on a coverage-tag left side are resolved from its right-side target. -Tags: mvp - Covers: - `feat~discover-and-navigate-oft-items~1` @@ -144,8 +151,6 @@ Needs: scn Users can find `Covers:` entries and source-side coverage tags that cover an OFT declaration. -Tags: mvp - Covers: - `feat~discover-and-navigate-oft-items~1` @@ -158,8 +163,6 @@ Users can insert bundled templates for common OFT artifacts, including `feat`, `req`, `scn`, `dsn`, architecture, and constraint items. The scenario template includes a Given-When-Then skeleton and a covered-item placeholder. -Tags: mvp - Covers: - `feat~author-oft-items-and-links~1` @@ -171,8 +174,6 @@ Needs: scn While editing a `Covers:` entry, users receive workspace declaration IDs ranked by full-ID prefix, name prefix, name substring, and artifact-type prefix. -Tags: mvp - Covers: - `feat~author-oft-items-and-links~1` @@ -184,8 +185,6 @@ Needs: scn After an artifact type and `->` in a likely coverage tag, users receive the same ranked declaration completion for the tag target. -Tags: mvp - Covers: - `feat~author-oft-items-and-links~1` @@ -198,8 +197,6 @@ Users can run an OFT trace using the workspace folder as the default input and read a plain-text result in the IDE. Reported IDs and defects navigate to their known source locations. -Tags: mvp - Covers: - `feat~run-oft-traces~1` @@ -270,14 +267,27 @@ Needs: scn **Then** the extension highlights declarations or coverage tags in the relevant range while preserving the file's normal language support. -Tags: mvp - Covers: - `req~highlight-oft-declarations~1` - `req~highlight-coverage-tags~1` Needs: dsn +### Start and Negotiate the Language Server +`scn~start-and-negotiate-language-server~1` + +**Given** a VS Code workspace and a Java 21 or later runtime +**When** the OpenFastTrace extension activates +**Then** it starts the packaged language server and can complete an initial +`workspace/symbol` request with an empty result. + +Tags: mvp + +Covers: +- `req~start-language-server~1` + +Needs: dsn + ### Discover and Follow a Trace Link `scn~discover-and-follow-a-trace-link~1` @@ -285,8 +295,6 @@ Needs: dsn **When** the user searches, follows the reference, or requests implementations **Then** the IDE presents the declaration or all covering locations. -Tags: mvp - Covers: - `req~search-workspace-oft-items~1` - `req~navigate-oft-links~1` @@ -302,8 +310,6 @@ Needs: dsn reference position **Then** the editor inserts the template or offers ranked workspace IDs. -Tags: mvp - Covers: - `req~insert-oft-templates~1` - `req~complete-covers-references~1` @@ -320,8 +326,6 @@ filter **Then** the server traces only that tagged scope and the extension presents a navigable plain-text report. -Tags: mvp - Covers: - `req~trace-the-workspace~1` diff --git a/extension/build.gradle b/extension/build.gradle index 969c258..7ecf753 100644 --- a/extension/build.gradle +++ b/extension/build.gradle @@ -8,6 +8,14 @@ def xvfbRun = isLinux ? System.getenv('PATH').split(File.pathSeparator) .collect { new File(it, 'xvfb-run') } .find { it.canExecute() } : null +def stageLanguageServer = tasks.register('stageLanguageServer', Copy) { + group = 'build setup' + description = 'Stages the language server distribution for the extension.' + dependsOn ':server:installDist' + from project(':server').layout.buildDirectory.dir('install/openfasttrace-language-server') + into layout.projectDirectory.dir('server') +} + def npmInstall = tasks.register('npmInstall', Exec) { group = 'build setup' description = 'Installs the extension Node.js dependencies.' @@ -19,7 +27,7 @@ def npmInstall = tasks.register('npmInstall', Exec) { def compileExtension = tasks.register('compileExtension', Exec) { group = 'build' description = 'Compiles the VS Code extension.' - dependsOn npmInstall + dependsOn npmInstall, stageLanguageServer commandLine npmCommand, 'run', 'compile' inputs.files('package.json', 'tsconfig.json') inputs.dir('src') diff --git a/extension/package-lock.json b/extension/package-lock.json index b041b17..8f7092b 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "openfasttrace", "version": "0.0.1", + "dependencies": { + "vscode-languageclient": "10.1.0" + }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "26.x", @@ -2996,7 +2999,6 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3444,6 +3446,88 @@ "node": ">=10.12.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", + "integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.1.0.tgz", + "integrity": "sha512-XXRx6lqVitQy/oOLr9MfNYRG+MbQkhXkDaxbQMiKxEm8zZNfheRFUKNb8UYNh2stn9btl2wQM5wZFJjJvoc+jA==", + "license": "MIT", + "dependencies": { + "minimatch": "^10.2.5", + "semver": "^7.8.1", + "vscode-languageserver-protocol": "3.18.2", + "vscode-languageserver-textdocument": "1.0.13" + }, + "engines": { + "vscode": "^1.91.0" + } + }, + "node_modules/vscode-languageclient/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/vscode-languageclient/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.2.tgz", + "integrity": "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.1", + "vscode-languageserver-types": "3.18.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.13.tgz", + "integrity": "sha512-nx0ZHwMGIsVkzFG3/VLeJYBLTaFBRuNdGDvevvjuoayU5EOS2fEYazOhtCM3PI9ClMMg5igc0uwXtAq4tJj+Dw==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", + "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/extension/package.json b/extension/package.json index 59b4628..d92d99e 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1,5 +1,6 @@ { "name": "openfasttrace", + "publisher": "itsallcode", "displayName": "openfasttrace-vscode-extension", "description": "Manage requirements using OpenFastTrace", "version": "0.0.1", @@ -9,15 +10,20 @@ "categories": [ "Other" ], - "activationEvents": [], + "activationEvents": [ + "onStartupFinished" + ], "main": "./out/extension.js", "contributes": { - "commands": [ - { - "command": "openfasttrace.helloWorld", - "title": "Hello World" + "configuration": { + "title": "OpenFastTrace", + "properties": { + "openfasttrace.java.home": { + "type": "string", + "description": "Absolute path to a Java 21 or later runtime used by the OpenFastTrace language server." + } } - ] + } }, "scripts": { "vscode:prepublish": "npm run compile", @@ -32,11 +38,14 @@ "@types/mocha": "^10.0.10", "@types/node": "26.x", "@types/vscode": "^1.125.0", + "@typescript/native": "npm:typescript@^7.0.2", "@vscode/test-cli": "^0.0.15", "@vscode/test-electron": "^3.1.0", "eslint": "^10.8.0", "typescript": "npm:@typescript/typescript6@^6.0.2", - "@typescript/native": "npm:typescript@^7.0.2", "typescript-eslint": "^8.66.0" + }, + "dependencies": { + "vscode-languageclient": "10.1.0" } } diff --git a/extension/src/extension.ts b/extension/src/extension.ts index a4dbb8d..d4a5843 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -1,28 +1,53 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below +import * as path from 'node:path'; import * as vscode from 'vscode'; +import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'; +import { resolveJavaExecutable } from './javaRuntime'; -// This method is called when your extension is activated -// Your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { +let client: LanguageClient | undefined; - // Use the console to output diagnostic information (console.log) and errors (console.error) - // This line of code will only be executed once when your extension is activated - console.log('Congratulations, your extension "openfasttrace" is now active!'); +export async function activate(context: vscode.ExtensionContext): Promise<{ getLanguageClient: typeof getLanguageClient }> { + try { + const configuredJavaHome = vscode.workspace.getConfiguration('openfasttrace').get('java.home'); + const javaExecutable = await resolveJavaExecutable(configuredJavaHome || undefined, process.env); + client = new LanguageClient( + 'openfasttraceLanguageServer', + 'OpenFastTrace Language Server', + serverOptions(context, javaExecutable), + clientOptions, + ); + context.subscriptions.push(client); + await client.start(); + return { getLanguageClient }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + void vscode.window.showErrorMessage(`Unable to start the OpenFastTrace language server: ${message}`); + throw error; + } +} - // The command has been defined in the package.json file - // Now provide the implementation of the command with registerCommand - // The commandId parameter must match the command field in package.json - const disposable = vscode.commands.registerCommand('openfasttrace.helloWorld', () => { - // The code you place here will be executed every time your command is executed - // Display a message box to the user - vscode.window.showInformationMessage('Hello World from openfasttrace-vscode-extension!'); - }); +export function deactivate(): Thenable | undefined { + return client?.stop(); +} - context.subscriptions.push(disposable); +export function getLanguageClient(): LanguageClient | undefined { + return client; } -// This method is called when your extension is deactivated -export function deactivate() { - // Nothing to do +function serverOptions(context: vscode.ExtensionContext, javaExecutable: string): ServerOptions { + const languageServerExecutable = process.platform === 'win32' ? 'openfasttrace-language-server.bat' : 'openfasttrace-language-server'; + return { + command: path.join(context.extensionPath, 'server', 'bin', languageServerExecutable), + options: { env: serverEnvironment(javaExecutable) }, + }; } + +function serverEnvironment(javaExecutable: string): NodeJS.ProcessEnv { + if (javaExecutable === 'java' || javaExecutable === 'java.exe') { + return process.env; + } + return { ...process.env, JAVA_HOME: path.dirname(path.dirname(javaExecutable)) }; +} + +const clientOptions: LanguageClientOptions = { + documentSelector: ['markdown', 'restructuredtext'], +}; diff --git a/extension/src/javaRuntime.ts b/extension/src/javaRuntime.ts new file mode 100644 index 0000000..419d125 --- /dev/null +++ b/extension/src/javaRuntime.ts @@ -0,0 +1,48 @@ +import { execFile } from 'node:child_process'; +import * as path from 'node:path'; +import { promisify } from 'node:util'; + +const execFileAsync = promisify(execFile); +const minimumJavaVersion = 21; + +export type JavaVersionCommand = (command: string) => Promise<{ stderr: string }>; + +export async function resolveJavaExecutable( + configuredJavaHome: string | undefined, + environment: NodeJS.ProcessEnv, + runJavaVersion: JavaVersionCommand = javaVersion, +): Promise { + const executable = javaExecutable(configuredJavaHome ?? environment.JAVA_HOME); + let versionOutput: string; + try { + versionOutput = (await runJavaVersion(executable)).stderr; + } catch { + throw javaVersionError(); + } + if (!isSupportedJavaVersion(versionOutput)) { + throw javaVersionError(); + } + return executable; +} + +function javaExecutable(javaHome: string | undefined): string { + const javaExecutableName = process.platform === 'win32' ? 'java.exe' : 'java'; + if (javaHome === undefined) { + return javaExecutableName; + } + return path.join(javaHome, 'bin', javaExecutableName); +} + +async function javaVersion(command: string): Promise<{ stderr: string }> { + const output = await execFileAsync(command, ['-version']); + return { stderr: output.stderr }; +} + +function isSupportedJavaVersion(versionOutput: string): boolean { + const match = /(?:openjdk )?version "(\d+)/.exec(versionOutput); + return match !== null && Number(match[1]) >= minimumJavaVersion; +} + +function javaVersionError(): Error { + return new Error('OpenFastTrace requires Java 21 or later. Configure openfasttrace.java.home or JAVA_HOME.'); +} diff --git a/extension/src/test/integration/extension.test.ts b/extension/src/test/integration/extension.test.ts index d17f5cc..0e789de 100644 --- a/extension/src/test/integration/extension.test.ts +++ b/extension/src/test/integration/extension.test.ts @@ -1,15 +1,22 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; -import * as extension from '../../extension'; -suite('Extension Test Suite', () => { - test('activates and executes the extension command', async () => { - const subscriptions: vscode.Disposable[] = []; +interface OpenFastTraceExports { + getLanguageClient(): { + sendRequest(method: string, parameters: { query: string }): Thenable; + } | undefined; +} - extension.activate({ subscriptions } as unknown as vscode.ExtensionContext); +suite('OpenFastTrace extension', () => { + test('starts the language server and returns an empty workspace symbol result', async () => { + const extension = vscode.extensions.getExtension('itsallcode.openfasttrace'); - assert.strictEqual(subscriptions.length, 1); - await vscode.commands.executeCommand('openfasttrace.helloWorld'); - subscriptions.forEach((subscription) => subscription.dispose()); - }); + assert.ok(extension, 'OpenFastTrace extension must be available to the test host'); + await extension.activate(); + const client = extension.exports.getLanguageClient(); + + assert.ok(client, 'OpenFastTrace language client must be started'); + const symbols = await client.sendRequest('workspace/symbol', { query: '' }); + assert.deepStrictEqual(symbols, []); + }); }); diff --git a/extension/src/test/unit/extension.test.ts b/extension/src/test/unit/extension.test.ts index 8d15c76..d348aa5 100644 --- a/extension/src/test/unit/extension.test.ts +++ b/extension/src/test/unit/extension.test.ts @@ -1,7 +1,36 @@ import { strict as assert } from 'node:assert'; import { test } from 'node:test'; +import { resolveJavaExecutable } from '../../javaRuntime'; -test('sample unit test', () => { - assert.equal(-1, [1, 2, 3].indexOf(5)); - assert.equal(-1, [1, 2, 3].indexOf(0)); +test('uses the configured Java 21 runtime before environment and path candidates', async () => { + const checkedCommands: string[] = []; + + const executable = await resolveJavaExecutable( + '/configured/java', + { JAVA_HOME: '/environment/java' }, + async command => { + checkedCommands.push(command); + return { stderr: 'openjdk version "21.0.7"' }; + }, + ); + + assert.equal(executable, '/configured/java/bin/java'); + assert.deepEqual(checkedCommands, ['/configured/java/bin/java']); +}); + +test('accepts a Java runtime newer than version 21', async () => { + const executable = await resolveJavaExecutable( + undefined, + {}, + async () => ({ stderr: 'openjdk version "25.0.1"' }), + ); + + assert.equal(executable, 'java'); +}); + +test('rejects a Java runtime older than version 21', async () => { + await assert.rejects( + resolveJavaExecutable(undefined, {}, async () => ({ stderr: 'openjdk version "17.0.15"' })), + /requires Java 21 or later/, + ); }); diff --git a/server/build.gradle b/server/build.gradle index 7ba8830..0ae4918 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -15,6 +15,7 @@ java { application { mainClass = 'org.itsallcode.openfasttrace.lsp.OpenFastTraceLanguageServerMain' + applicationName = 'openfasttrace-language-server' } sonar { diff --git a/server/src/main/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceLanguageServer.java b/server/src/main/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceLanguageServer.java index 29b35cb..64d4d6a 100644 --- a/server/src/main/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceLanguageServer.java +++ b/server/src/main/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceLanguageServer.java @@ -24,6 +24,7 @@ public final class OpenFastTraceLanguageServer implements LanguageServer, Langua public CompletableFuture initialize(final InitializeParams parameters) { final ServerCapabilities capabilities = new ServerCapabilities(); capabilities.setTextDocumentSync(TextDocumentSyncKind.Full); + capabilities.setWorkspaceSymbolProvider(true); return CompletableFuture.completedFuture(new InitializeResult(capabilities)); } diff --git a/server/src/main/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceWorkspaceService.java b/server/src/main/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceWorkspaceService.java index abcbf07..8aba9ca 100644 --- a/server/src/main/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceWorkspaceService.java +++ b/server/src/main/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceWorkspaceService.java @@ -1,7 +1,14 @@ package org.itsallcode.openfasttrace.lsp; +import java.util.List; +import java.util.concurrent.CompletableFuture; + import org.eclipse.lsp4j.DidChangeConfigurationParams; import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.eclipse.lsp4j.WorkspaceSymbolParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.WorkspaceService; /** Placeholder for workspace indexing, configuration, and trace commands. */ @@ -15,4 +22,10 @@ public void didChangeConfiguration(final DidChangeConfigurationParams parameters public void didChangeWatchedFiles(final DidChangeWatchedFilesParams parameters) { // Workspace indexing is added with the parser. } + + @Override + public CompletableFuture, List>> symbol( + final WorkspaceSymbolParams parameters) { + return CompletableFuture.completedFuture(Either.forLeft(List.of())); + } } diff --git a/server/src/test/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceLanguageServerTest.java b/server/src/test/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceLanguageServerTest.java index a7723fd..34dced7 100644 --- a/server/src/test/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceLanguageServerTest.java +++ b/server/src/test/java/org/itsallcode/openfasttrace/lsp/OpenFastTraceLanguageServerTest.java @@ -1,9 +1,13 @@ package org.itsallcode.openfasttrace.lsp; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.TextDocumentSyncKind; +import org.eclipse.lsp4j.WorkspaceSymbolParams; import org.junit.jupiter.api.Test; class OpenFastTraceLanguageServerTest { @@ -16,4 +20,14 @@ void negotiatesFullDocumentSynchronization() { assertEquals(TextDocumentSyncKind.Full, result.getCapabilities().getTextDocumentSync().getLeft()); } + + @Test + void negotiatesWorkspaceSymbolsAndReturnsAnEmptyResult() { + final OpenFastTraceLanguageServer server = new OpenFastTraceLanguageServer(); + + final var result = server.initialize(new InitializeParams()).join(); + + assertTrue(result.getCapabilities().getWorkspaceSymbolProvider().getLeft()); + assertEquals(List.of(), server.getWorkspaceService().symbol(new WorkspaceSymbolParams()).join().getLeft()); + } }