From 300302d86b5568c752435dab0658a3c2cdececc9 Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Mon, 4 May 2026 14:22:41 +0200 Subject: [PATCH 01/22] Fix project metadata --- extension/media/esmf-logo.png | Bin 0 -> 24145 bytes extension/package-lock.json | 6 +++--- extension/package.json | 22 +++++++++++++++++----- extension/tsconfig.json | 3 ++- 4 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 extension/media/esmf-logo.png diff --git a/extension/media/esmf-logo.png b/extension/media/esmf-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..183282dfa1cbee4cf83cec7e5f00f1e8b2aa73d0 GIT binary patch literal 24145 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 literal 0 HcmV?d00001 diff --git a/extension/package-lock.json b/extension/package-lock.json index 0f869ff..a7436d0 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,18 +1,18 @@ { - "name": "extension", + "name": "turtle", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "extension", + "name": "turtle", "version": "0.0.1", + "license": "MPL-2.0", "dependencies": { "vscode-languageclient": "^9.0.1" }, "devDependencies": { "@types/jest": "^30.0.0", - "@types/mocha": "^10.0.10", "@types/node": "22.x", "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.12", diff --git a/extension/package.json b/extension/package.json index e254f2e..ed485fd 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1,8 +1,18 @@ { - "name": "extension", - "displayName": "Turtle LSP", - "description": "VS Code extension for Turtle LSP", + "name": "turtle", + "displayName": "RDF/Turtle and SAMM Aspect Models", + "description": "Support for RDF/Turtle and SAMM Aspect Models", "version": "0.0.1", + "publisher": "ESMF", + "icon": "media/esmf-logo.png", + "license": "MPL-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-esmf/esmf-vs-code-plugin.git" + }, + "bugs": { + "url": "https://github.com/eclipse-esmf/eclipse-vs-code-plugin/issues" + }, "engines": { "vscode": "^1.110.0" }, @@ -28,7 +38,7 @@ "aliases": [ "Turtle", "turtle", - "RDF Turtle" + "RDF/Turtle" ], "extensions": [ ".ttl" @@ -38,6 +48,9 @@ ] }, "scripts": { + "vscode:prepublish": "npm run build", + "watch": "tsc -watch -p ./", + "pretest": "npm run build && npm run lint", "build": "tsc -p tsconfig.json", "build-watch": "tsc -p tsconfig.json --watch", "prettier": "prettier --config .prettierrc --write './src/**/*{.ts,.js,.json}'", @@ -49,7 +62,6 @@ }, "devDependencies": { "@types/jest": "^30.0.0", - "@types/mocha": "^10.0.10", "@types/node": "22.x", "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.12", diff --git a/extension/tsconfig.json b/extension/tsconfig.json index 9559ed1..096e80d 100644 --- a/extension/tsconfig.json +++ b/extension/tsconfig.json @@ -8,6 +8,7 @@ ], "sourceMap": true, "rootDir": "src", - "strict": true + "strict": true, + "skipLibCheck": true } } From a4182db3a9e093f8ddc9a3b6c88449630279e5dd Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Mon, 4 May 2026 14:23:03 +0200 Subject: [PATCH 02/22] Remove LSP server implementation --- lsp-server/.development/esmf-checkstyle.xml | 341 ---- .../.development/esmf-eclipse-codestyle.xml | 346 ---- .../.development/esmf-intellij-codestyle.xml | 137 -- .../esmf-intellij-inspections.xml | 1669 ----------------- lsp-server/.gitattributes | 12 - lsp-server/.gitignore | 4 - lsp-server/CONTRIBUTING.md | 186 -- lsp-server/CONVENTIONS.md | 102 - lsp-server/LICENSE | 373 ---- lsp-server/NOTICE.md | 573 ------ lsp-server/README.md | 45 - lsp-server/pom.xml | 119 -- .../main/java/com/example/turtlelsp/App.java | 33 - .../turtlelsp/TurtleLanguageServer.java | 80 - .../diagnostics/AspectDiagnosticMapper.java | 49 - .../aspect/model/AspectValidationError.java | 7 - .../model/AspectValidationErrorType.java | 8 - .../aspect/model/AspectValidationResult.java | 11 - .../aspect/model/AspectViolationInfo.java | 12 - .../request/ValidateDocumentParams.java | 7 - .../service/AspectModelValidationService.java | 14 - .../service/AspectValidationCoordinator.java | 89 - .../DefaultAspectModelValidationService.java | 129 -- .../common/uri/DocumentUriResolver.java | 18 - .../lsp/text/AspectDiagnosticsWorkflow.java | 58 - .../text/DocumentAspectValidationService.java | 110 -- .../lsp/text/DocumentDiagnosticsService.java | 46 - .../lsp/text/DocumentDiagnosticsStore.java | 37 - .../turtlelsp/lsp/text/DocumentStore.java | 24 - .../lsp/text/TextDocumentClientNotifier.java | 43 - .../text/TurtleSyntaxValidationService.java | 56 - .../lsp/text/TurtleTextDocumentService.java | 130 -- .../lsp/workspace/TurtleWorkspaceService.java | 20 - .../TurtlePrefixDefinitionService.java | 95 - lsp-server/src/main/resources/log4j2.xml | 28 - .../turtlelsp/TurtleDefinitionTest.java | 63 - ...faultAspectModelValidationServiceTest.java | 83 - .../text/AspectDiagnosticsWorkflowTest.java | 118 -- .../text/DocumentDiagnosticsServiceTest.java | 99 - .../text/DocumentDiagnosticsStoreTest.java | 43 - .../text/TextDocumentClientNotifierTest.java | 56 - .../TurtlePrefixDefinitionServiceTest.java | 57 - 42 files changed, 5530 deletions(-) delete mode 100644 lsp-server/.development/esmf-checkstyle.xml delete mode 100644 lsp-server/.development/esmf-eclipse-codestyle.xml delete mode 100644 lsp-server/.development/esmf-intellij-codestyle.xml delete mode 100644 lsp-server/.development/esmf-intellij-inspections.xml delete mode 100644 lsp-server/.gitattributes delete mode 100644 lsp-server/.gitignore delete mode 100644 lsp-server/CONTRIBUTING.md delete mode 100644 lsp-server/CONVENTIONS.md delete mode 100644 lsp-server/LICENSE delete mode 100644 lsp-server/NOTICE.md delete mode 100644 lsp-server/README.md delete mode 100644 lsp-server/pom.xml delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/App.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/TurtleLanguageServer.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/diagnostics/AspectDiagnosticMapper.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationError.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationErrorType.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationResult.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectViolationInfo.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/request/ValidateDocumentParams.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectModelValidationService.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectValidationCoordinator.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationService.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/common/uri/DocumentUriResolver.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflow.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentAspectValidationService.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsService.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStore.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentStore.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifier.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleSyntaxValidationService.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleTextDocumentService.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/workspace/TurtleWorkspaceService.java delete mode 100644 lsp-server/src/main/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionService.java delete mode 100644 lsp-server/src/main/resources/log4j2.xml delete mode 100644 lsp-server/src/test/java/com/example/turtlelsp/TurtleDefinitionTest.java delete mode 100644 lsp-server/src/test/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationServiceTest.java delete mode 100644 lsp-server/src/test/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflowTest.java delete mode 100644 lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsServiceTest.java delete mode 100644 lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStoreTest.java delete mode 100644 lsp-server/src/test/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifierTest.java delete mode 100644 lsp-server/src/test/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionServiceTest.java diff --git a/lsp-server/.development/esmf-checkstyle.xml b/lsp-server/.development/esmf-checkstyle.xml deleted file mode 100644 index 37be7d4..0000000 --- a/lsp-server/.development/esmf-checkstyle.xml +++ /dev/null @@ -1,341 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lsp-server/.development/esmf-eclipse-codestyle.xml b/lsp-server/.development/esmf-eclipse-codestyle.xml deleted file mode 100644 index f812391..0000000 --- a/lsp-server/.development/esmf-eclipse-codestyle.xml +++ /dev/null @@ -1,346 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lsp-server/.development/esmf-intellij-codestyle.xml b/lsp-server/.development/esmf-intellij-codestyle.xml deleted file mode 100644 index dc231a6..0000000 --- a/lsp-server/.development/esmf-intellij-codestyle.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lsp-server/.development/esmf-intellij-inspections.xml b/lsp-server/.development/esmf-intellij-inspections.xml deleted file mode 100644 index 6aee320..0000000 --- a/lsp-server/.development/esmf-intellij-inspections.xml +++ /dev/null @@ -1,1669 +0,0 @@ - - - - diff --git a/lsp-server/.gitattributes b/lsp-server/.gitattributes deleted file mode 100644 index f91f646..0000000 --- a/lsp-server/.gitattributes +++ /dev/null @@ -1,12 +0,0 @@ -# -# https://help.github.com/articles/dealing-with-line-endings/ -# -# Linux start script should use lf -/gradlew text eol=lf - -# These are Windows script files and should use crlf -*.bat text eol=crlf - -# Binary files should be left untouched -*.jar binary - diff --git a/lsp-server/.gitignore b/lsp-server/.gitignore deleted file mode 100644 index 8b75bac..0000000 --- a/lsp-server/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.idea -target -logs -lsp-server.iml diff --git a/lsp-server/CONTRIBUTING.md b/lsp-server/CONTRIBUTING.md deleted file mode 100644 index 00c1ad9..0000000 --- a/lsp-server/CONTRIBUTING.md +++ /dev/null @@ -1,186 +0,0 @@ -# Contribution Guideline ESMF Visual Studio Code Plugin Conde - -Thank you for your interest in contributing to the ESMF Visual Studio Code Plugin Conde. Use this repository to contribute to -the project as easy and transparent as possible, whether it is: - -* Reporting a bug -* Submitting a fix -* Proposing new features -* something else - -The ESMF Visual Studio Code Plugin Conde is developed in the context of the [Eclipse Semantic Modeling -Framework](https://projects.eclipse.org/projects/dt.esmf/). It is based on the Semantic Aspect Meta -Model (SAMM) and supports its use. - -# Contributing Source Code (using GitHub) - -* We use this GitHub repository to track issues and feature requests. -* For general discussions of the ESMF, modeling questions etc. we use the [ESMF Chat](https://chat.eclipse.org/#/room/#eclipse-semantic-modeling-framework:matrix.eclipse.org). -* For discussions specific to development, the preferred way is the [developer mailing list](https://accounts.eclipse.org/mailing-list/esmf-dev). - -## Branching -We follow the [Git branching guidance](https://docs.microsoft.com/en-us/azure/devops/repos/git/git-branching-guidance?view=azure-devops). - -More specifically the repository has the following branches: - -name of branch | description -----| ---- -`main` | Contains the latest state of the repository -`v{version_number}-RC{rc_number}` | A "release candidate": A version that freezes major features and -can be considered a pre-release of the next full release. -`v{version_number}` | A full release of the respective version. -`feature/#{issue_number}-{feature_name}` | Contains the development on a specific feature and is -intended to be merged back into the `main` branch as soon as possible. Note, that it is recommended -for contributors to create and develop feature branches in a personal fork and not the upstream -repository. -`bug/#{issue_number}-{bug_name}` | Contains the development of (usually smaller) changes in files of -the repository that do not introduce new functionality but fix mistakes, errors or inconsistencies. -These branches should be merged back into the `main`branch as soon as possible. - -## Issues -We use the `Issues` feature of GitHub for tracking all types of work in the repository. - -We distinguish between the following types of issues; - -Issue Types | Description --------------------| ------------------------------------------------------ -`Bug Report` | This `Issue` is dedicated to reporting a problem. - `Task` | This `Issue` is used for describing and proposing a new work item (e.g., a new feature) - - If there are issues that link to the same topic, the creator of the issue shall mention those other tasks in the - description. To group tasks that can belong together, one could further create an issue mentioning and describing - the overall user story for the referenced tasks. - -## Pull Requests -Proposals for changes to the content of the repository are managed through Pull Requests (`PRs`). - -### Opening Pull Requests -To open such a `PR`, implement the changes in a new `feature branch`. Each `PR` must reference an issue and follows the -naming schema: `-`. For a new `PR` the target branch is the `main` branch while the source -branch is your `feature branch` The `feature branch` branch should be developed in a fork of the upstream repository. -So before working on your first feature, you need to create such a fork (e.g., by pressing the `Fork` button in the top -right corner of the GitHub page) - -When opening a `PR` please consider the following topics: - -* optional: Rebase your development on the branch to which you plan to create the `PR`. -* Each `PR` must be linked to an `Issue`: - - Reference the `Issue` number in the name of your `feature branch` and the description of the `PR`. - - Mention the `Issue` in one of the commit messages associated to the `PR` together with a GitHub keyword like - `closes #IssueNumber` or `fixes #IssuesNumber`. For more details visit the - [GitHub documentation on linking PR with Issues](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) -* Each `PR` should only contain changes related to a single work item. If the changes cover more than one work item or - feature, then create one `PR` per work item. You may need to create new more specific `Issues` to reference if you - split up the work into multiple `feature branches`. -* Commit changes often. A `PR` may contain one or more commits. - -## Eclipse Development Process - -This Eclipse Foundation open project is governed by the Eclipse Foundation Development Process and -operates under the terms of the Eclipse IP Policy. - -* https://eclipse.org/projects/dev_process -* https://www.eclipse.org/org/documents/Eclipse_IP_Policy.pdf - -## Eclipse Contributor Agreement - -In order to be able to contribute to Eclipse Foundation projects you must electronically sign the -Eclipse Contributor Agreement (ECA). - -* http://www.eclipse.org/legal/ECA.php - -The ECA provides the Eclipse Foundation with a permanent record that you agree that each of your -contributions will comply with the commitments documented in the Developer Certificate of Origin -(DCO). Having an ECA on file associated with the email address matching the "Author" field of your -contribution's Git commits fulfills the DCO's requirement that you sign-off on your contributions. - -For more information, please see the Eclipse Committer Handbook: -https://www.eclipse.org/projects/handbook/#resources-commit - -## Commit Messages -Separate the subject from the body with a blank line because the subject line is shown in the Git -history and should summarize the commit body. Use the body to explain what and why with less focus -on the details of the how. This [blog post](https://chris.beams.io/posts/git-commit/#seven-rules) -has more tips and details. Before you push your commits to a repository, you should squash your -commits into one or more logical units of work, e.g., you should organize a new feature in a single -commit. - -## License Headers & Licensing -All files contributed require headers - this will ensure the license and copyright clearing at the -end. Also, all contributions must have the same license as the source. The header should follow the -following template: - -``` -/* - * Copyright (c) {YEAR} {NAME OF COMPANY X} - * Copyright (c) {YEAR} {NAME OF COMPANY Y} - * - * See the AUTHORS file(s) distributed with this work for additional - * information regarding authorship. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - */ -``` - -When using the template, one must replace "{NAME OF COMPANY X}" with the name of the involved -companies and "{YEAR}" with the year of the contribution. For each involved company you need a new -line starting with "Copyright" as outlined in the example. - -The example is taken from a Java source file. If your file is of another type you may have to adapt -the comment syntax accordingly. - -If you use third-party content (e.g., import / include ...), you are required to list each -third-party content explicitly with its version number in the documentation and your pull-request -comment. Please also check used third party material for license compatibility with the MPL-2.0. -E.g. software licensed under GPL, AGPL or, a similar strong copy-left license cannot be approved. - -# Code Conventions -The ESMF Visual Studio Code Plugin Conde is written in the Java Programming Language. Please have a look into our [Code -Conventions](CONVENTIONS.md). - -## Versioning -We use Semantic Versioning to identify released versions of the ESMF Visual Studio Code Plugin Conde. Semantic Versioning is -documented [here](https://semver.org). It proposes to have a versioning number with the following -elements: - -```` -Given a version number MAJOR.MINOR.PATCH, increment the: -- MAJOR version when you make incompatible API changes, -- MINOR version when you add functionality in a backwards compatible manner, and -- PATCH version when you make backwards compatible bug fixes. -Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. -```` - -Whereas the Major version must be incremented if the API has backward-incompatible changes (e.g., has breaking changes), -the Minor version must be changed if new backward-compatible features are introduced and, -the Patch version must be incremented if backward-compatible bugfixes are introduced. - -### Breaking Changes -For the definition of a breaking change, we follow the definition as in the [Microsoft REST API -Guidelines](https://github.com/Microsoft/api-guidelines/blob/vNext/Guidelines.md#123-definition-of-a-breaking-change) -which are licensed under [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0). This definition -states: -```` -Changes to the contract of an API are considered a breaking change. Changes that impact the backwards compatibility -of an API are a breaking change. -````` - -### Version Syntax for Specific Environments - -Git version tag - -vX.Y.Z-[pre-release-identifier] - -Examples: - -v1.0.0-RC1, v1.0.0 - -# Resources - -* [For a Repo](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) -* [Issue Creation](https://help.github.com/en/github/managing-your-work-on-github/creating-an-issue) -* [PR Creation](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) diff --git a/lsp-server/CONVENTIONS.md b/lsp-server/CONVENTIONS.md deleted file mode 100644 index 9c3292f..0000000 --- a/lsp-server/CONVENTIONS.md +++ /dev/null @@ -1,102 +0,0 @@ -# ESMF Code Conventions -The following document contains a compilation of conventions and guidelines to format, structure and -write code for the ESMF VS Code Plugin. - -## General Conventions -Our code conventions are loosely based on the [Google Java Style -Guide](https://google.github.io/styleguide/javaguide.html) but detailed and adjusted for the needs -of the ESMF VS Code Plugin. The code style is described using the Eclipse code style formatter XML and can be -found in the file `.development/esmf-eclipse-codestyle.xml`. - -* If you develop using the Eclipse IDE, you can import this file as project code style. -* If you develop using IntelliJ, you can install the (third part) "Adapter for Eclipse Code - Formatter" plugin, then configure the plugin to use the file. -* In any way, you can use `mvn spotless:check` to validate the code style of current state of your - copy of the code base; and `mvn spotless:apply` to automatically apply the code style to the whole - code base. - -Additional conventions are described using [Checkstyle](https://checkstyle.sourceforge.io/) which can -be found in the file `.development/esmf-checkstyle.xml`. You can validate if your code adheres to the -rules using `mvn checkstyle:check`. - -Furthermore, the files `.development/esmf-intellij-codestyle.xml` and -`.development/esmf-intellij-inspections.xml` are provided that can be -[imported](https://www.jetbrains.com/help/idea/configuring-code-style.html#import-export-schemes) in -the Java code style settings and -[imported](https://www.jetbrains.com/help/idea/inspections-settings.html#profile_management) in the -Inspections, respectively, in the IntelliJ IDEA IDE. Note however, that there might be slight -differences in automatic formatting due to technical limitations; the leading code style description -is esmf-eclipse-codestyle.xml as described above. - -## Copyright header -See [CONTRIBUTING](CONTRIBUTING.md) - -## Code Recommendations - -### Utility Classes -[Utility classes](https://wiki.c2.com/?UtilityClasses) as such should be avoided - domain concepts -must be expressed in domain classes. Thus, only for "real", non-domain operations not belonging to -any class, utility methods and classes may be used. However, chances are pretty close to 100% that -all of everyday utility needs are already covered by high-quality 3rd-party libraries. - -Usually we apply the following rule to decide on the introduction of new libraries -1. Check your framework's and its dependency's utility/static constants classes (e.g. Spring or Vert.x) -2. If not covered, use Guava (https://github.com/google/guava/wiki) -3. If not covered, use Apache Commons (usually .lang module, https://commons.apache.org) -4. If really not covered, write your own (highly unlikely) - -### Optional<> usage -The Optional<> type, common for some time in Guava and in the Java core since Version 8, has found -widespread use for return values, however still a lot of discussions emerge concerning a fitting -scope of usage. You may not return null where an Optional<> is expected Whenever an Optional<> is -passed, you may safely assume it to be non-null. So the following snippet must never appear -anywhere: -``` -if (someOptional != null && someOptional.isPresent()) ... -``` - -* Using Optional<> as return types for values that might be missing is always fine -* Using Optional<> for fields is fine (see notes about "Avoid optionality" though) -* Using Optional<> for method parameters is fine (see notes about "Avoid optionality" though) -* Writing if-Statements checking for a present instance (and calling .get() explicitly) is considered an anti-pattern. You should - 1. Use .map() or .ifPresent() functional style patterns - 2. Use .orElse*() methods for clearly defined fallbacks (or exceptions) -* When having collections/streams of Optionals use .filter(Optional::isPresent) accordingly -* Using Optional.ofNullable(someValue).orElseThrow() to create one-liner check/assignment combinations is considered an anti-pattern. -* You should be using Objects.requireNonNull() for those sort of checks (or Guava's Preconditions if you're having more types of assertions than non-null and aim for a maximum of consistency). - -### Lombok -Lombok was used in the past, but is not used anymore in the ESMF VS Code Plugin. Instead of the `@Value` or -`@Data` annotations, consider using Java records. - -## Documentation - -### Source Code Documentation -Public classes and interfaces should carry appropriate JavaDoc explaining the responsibility of the -class. All public methods except getters/setters/toString etc. must be documented as well. Private -methods should be simple enough and well-named such that they don't need documentation. If -appropriate they of course may be documented as well. Inline comments, especially those that merely -separate logical blocks of code, must be avoided as they are usually an indicator that a private -method can be extracted or that bad naming was used that needs explaining. - -### Developer Documentation -Developer documentation is put into a README.md placed in the project root. This should contain documentation like: -* Checking out the source code and getting it to run/build -* Mandatory (external system) dependencies and how to set them up (e.g. databases) -* Configuration options and how to apply them -* General important concepts that are relevant to working on the project but are not directly obvious from the source code -itself. Links to further readings and information, e.g. wiki or other external sources. - -### User documentation -User documentation (this includes technical documentation on how to use an application or tool from the project) should be on -its own. -It is written in AsciiDoc, rendered with [Antora](https://antora.org) and the generated static content is -publically hosted for direct user access. -The source files of the documentation are placed in a subfolder /documentation from the project root. -Documentation is structured so that it can be processed by Antora. This e.g. involves structuring the documentation files -according to [Antora's specification](https://docs.antora.org/antora/2.3/organize-content-files/) and organizing resources -so that Antora [can handle them](https://docs.antora.org/antora/2.3/page/resource-id/). -[AsciiDoc's syntax](https://docs.antora.org/antora/2.3/asciidoc/asciidoc/) is pretty close to Markdown, however it is -way more targeted towards writing fully fledged documents and with its multitude of backends (HTML, PDF, ...) it is a -very good source format. -Publishing is realized by means of [Github pages](https://docs.antora.org/antora/2.3/publish-to-github-pages/). diff --git a/lsp-server/LICENSE b/lsp-server/LICENSE deleted file mode 100644 index a612ad9..0000000 --- a/lsp-server/LICENSE +++ /dev/null @@ -1,373 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. diff --git a/lsp-server/NOTICE.md b/lsp-server/NOTICE.md deleted file mode 100644 index 4f646ee..0000000 --- a/lsp-server/NOTICE.md +++ /dev/null @@ -1,573 +0,0 @@ -## panzoom:9.4.2 - -This package contains the following copyright statements and is licensed under the following declared licenses. -### Copyright Statements - -Copyright (c) 2016 - 2021 Andrei Kashcha - -### Declared License (MIT) -``` -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -``` - -## tailwindcss:2.2.7 - -This package contains the following copyright statements and is licensed under the following declared licenses. -### Copyright Statements - -Copyright (c) Adam Wathan -Copyright (c) Jonathan Reinink - -### Declared License (MIT) -``` -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -``` - -## tcobot:4.11-1 - -This package contains the following copyright statements and is licensed under the following declared licenses. -### Copyright Statements - -Copyright (c) 2016 Tim Scanlin - -### Declared License (MIT) -``` -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -``` - - -## RobotoCondensed-Regular - -This package contains the following copyright statements and is licensed under the following declared licenses. -### Copyright Statements - -Copyright (C) Christian Robertson - - -### Declared License (Apache-2.0) -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -``` - -## normalize.css:2.1.2 - -This package contains the following copyright statements and is licensed under the following declared licenses. -### Copyright Statements - -Copyright (C) Nicolas Gallagher - -### Declared License (MIT) -``` -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -``` - -## Font Awesome:4.7.0 - -This package contains the following copyright statements and is licensed under the following declared licenses. -### Copyright Statements - -Copyright (C) @davegandy - -### Declared License (MIT) -``` -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -``` - -### Declared License (SIL OFL 1.1) -``` -Copyright (c) , (), -with Reserved Font Name . -Copyright (c) , (), -with Reserved Font Name . -Copyright (c) , (). - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. - -``` - -## Font Cairo:3.116 - -This package contains the following copyright statements and is licensed under the following declared licenses. -### Copyright Statements - -Copyright (C) Mohamed Gaber - -### Declared License (SIL OFL 1.1) -``` -Copyright (c) , (), -with Reserved Font Name . -Copyright (c) , (), -with Reserved Font Name . -Copyright (c) , (). - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. - -``` diff --git a/lsp-server/README.md b/lsp-server/README.md deleted file mode 100644 index 2949415..0000000 --- a/lsp-server/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# ESMF VS Code Plugin - -## Table of Contents - -- [Introduction](#introduction) -- [Build and contribute](#build-and-contribute) -- [Project Structure](#project-structure) -- [License](#license) - -## Introduction - -TODO - -## Build and contribute - -The top level elements of the Project Structure are all carried out as Maven multimodule projects. -Building the SDK requires Java 25. - -To build the project, run the following command: -```bash -mvn clean install -``` - -We are always looking forward to your contributions. For more details on how to contribute just take -a look at the [contribution guidelines](CONTRIBUTING.md). Please create an issue first before -opening a pull request. - -To quickly check if your contribution adheres to the project conventions, you can run `mvn -spotless:check` and `mvn checkstyle:check`; to automatically apply the project code style to your -changes, you can also use `mvn spotless:apply`. For more details, please see our -[conventions](CONVENTIONS.md.) - -## Project Structure - -TODO - - -## License - -SPDX-License-Identifier: MPL-2.0 - -This program and the accompanying materials are made available under the terms of the -[Mozilla Public License, v. 2.0](LICENSE). - -The [Notice file](NOTICE.md) details contained third party materials. diff --git a/lsp-server/pom.xml b/lsp-server/pom.xml deleted file mode 100644 index d66c9d9..0000000 --- a/lsp-server/pom.xml +++ /dev/null @@ -1,119 +0,0 @@ - - 4.0.0 - - com.example - lsp-server - 1.0-SNAPSHOT - jar - - - UTF-8 - 25 - com.example.turtlelsp.App - 6.0.1 - 3.27.6 - 0.23.1 - 5.6.0 - 2.0.17 - 2.25.3 - - - - - org.eclipse.lsp4j - org.eclipse.lsp4j - ${lsp4j.version} - - - org.apache.jena - jena-arq - ${jena.version} - - - org.eclipse.esmf - esmf-aspect-model-starter - 2.14.2 - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-slf4j2-impl - ${log4j.version} - - - org.junit.jupiter - junit-jupiter - ${junit.jupiter.version} - test - - - org.assertj - assertj-core - ${assertj.version} - test - - - org.mockito - mockito-core - 5.20.0 - test - - - - - lsp-server - - - org.apache.maven.plugins - maven-compiler-plugin - 3.14.1 - - ${maven.compiler.release} - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.4 - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.1 - - - package - - shade - - - false - - - - ${main.class} - - - - - - - - - diff --git a/lsp-server/src/main/java/com/example/turtlelsp/App.java b/lsp-server/src/main/java/com/example/turtlelsp/App.java deleted file mode 100644 index 9cbedb6..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/App.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.turtlelsp; - -import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.eclipse.lsp4j.services.LanguageClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class App { - private static final Logger LOGGER = LoggerFactory.getLogger(App.class); - - public static void main(String[] args) { - LOGGER.info("Starting lsp-server"); - TurtleLanguageServer server = new TurtleLanguageServer(); - - try { - Launcher launcher = Launcher.createLauncher( - server, - LanguageClient.class, - System.in, - System.out - ); - - server.connect(launcher.getRemoteProxy()); - launcher.startListening().get(); - LOGGER.info("Language server listener stopped"); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - LOGGER.error("Language server listener was interrupted", ex); - } catch (Exception ex) { - LOGGER.error("Language server terminated with an error", ex); - } - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/TurtleLanguageServer.java b/lsp-server/src/main/java/com/example/turtlelsp/TurtleLanguageServer.java deleted file mode 100644 index b98fc7e..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/TurtleLanguageServer.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.example.turtlelsp; - -import java.util.concurrent.CompletableFuture; - -import com.example.turtlelsp.aspect.model.AspectValidationResult; -import com.example.turtlelsp.aspect.request.ValidateDocumentParams; -import com.example.turtlelsp.aspect.service.AspectModelValidationService; -import com.example.turtlelsp.lsp.text.TurtleTextDocumentService; -import com.example.turtlelsp.lsp.workspace.TurtleWorkspaceService; -import org.eclipse.lsp4j.InitializeParams; -import org.eclipse.lsp4j.InitializeResult; -import org.eclipse.lsp4j.SaveOptions; -import org.eclipse.lsp4j.ServerCapabilities; -import org.eclipse.lsp4j.TextDocumentSyncKind; -import org.eclipse.lsp4j.TextDocumentSyncOptions; -import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.LanguageClientAware; -import org.eclipse.lsp4j.services.LanguageServer; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.eclipse.lsp4j.services.WorkspaceService; - -public class TurtleLanguageServer implements LanguageServer, LanguageClientAware { - private final TurtleTextDocumentService textDocumentService; - private final TurtleWorkspaceService workspaceService; - - public TurtleLanguageServer() { - this( new TurtleTextDocumentService() ); - } - - TurtleLanguageServer( TurtleTextDocumentService textDocumentService ) { - this.textDocumentService = textDocumentService; - this.workspaceService = new TurtleWorkspaceService( textDocumentService ); - } - - @Override - public CompletableFuture initialize( InitializeParams params ) { - ServerCapabilities capabilities = new ServerCapabilities(); - TextDocumentSyncOptions syncOptions = new TextDocumentSyncOptions(); - syncOptions.setOpenClose( true ); - syncOptions.setChange( TextDocumentSyncKind.Full ); - syncOptions.setSave( new SaveOptions( true ) ); - capabilities.setTextDocumentSync( syncOptions ); - capabilities.setDefinitionProvider( true ); - - return CompletableFuture.completedFuture( new InitializeResult( capabilities ) ); - } - - @Override - public CompletableFuture shutdown() { - textDocumentService.shutdown(); - return CompletableFuture.completedFuture( null ); - } - - @Override - public void exit() { - throw new UnsupportedOperationException(); - } - - @Override - public TextDocumentService getTextDocumentService() { - return textDocumentService; - } - - @Override - public WorkspaceService getWorkspaceService() { - return workspaceService; - } - - @Override - public void connect( LanguageClient client ) { - textDocumentService.connect( client ); - } - - @JsonRequest("turtle/aspectValidation/validateDocument") - public CompletableFuture validateDocument( ValidateDocumentParams params ) { - String uri = params != null ? params.uri() : null; - return CompletableFuture.completedFuture( textDocumentService.validateDocument( uri ) ); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/aspect/diagnostics/AspectDiagnosticMapper.java b/lsp-server/src/main/java/com/example/turtlelsp/aspect/diagnostics/AspectDiagnosticMapper.java deleted file mode 100644 index f944f45..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/aspect/diagnostics/AspectDiagnosticMapper.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.turtlelsp.aspect.diagnostics; - -import java.net.URI; -import java.util.List; - -import com.example.turtlelsp.aspect.model.AspectValidationResult; -import com.example.turtlelsp.aspect.model.AspectViolationInfo; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.DiagnosticSeverity; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.jsonrpc.messages.Either; - -public final class AspectDiagnosticMapper { - public static final String SOURCE = "lsp-server.aspect"; - - public List toDiagnostics(String documentUri, AspectValidationResult result) { - return result.violations().stream() - .filter(violation -> appliesToDocument(documentUri, violation)) - .map(this::toDiagnostic) - .toList(); - } - - private boolean appliesToDocument(String documentUri, AspectViolationInfo violation) { - if (violation.sourceLocation() == null) { - return true; - } - - return URI.create(documentUri).equals(violation.sourceLocation()); - } - - private Diagnostic toDiagnostic(AspectViolationInfo violation) { - Diagnostic diagnostic = new Diagnostic(); - diagnostic.setSource(SOURCE); - diagnostic.setSeverity(DiagnosticSeverity.Error); - diagnostic.setMessage(violation.message()); - diagnostic.setCode(Either.forLeft(violation.code())); - diagnostic.setRange(toRange(violation)); - return diagnostic; - } - - private Range toRange(AspectViolationInfo violation) { - long line = violation.line() != null ? violation.line() : 1L; - long column = violation.column() != null ? violation.column() : 1L; - int safeLine = (int) Math.max(0, line - 1); - int safeColumn = (int) Math.max(0, column - 1); - return new Range(new Position(safeLine, safeColumn), new Position(safeLine, safeColumn + 1)); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationError.java b/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationError.java deleted file mode 100644 index de75991..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationError.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.turtlelsp.aspect.model; - -public record AspectValidationError( - AspectValidationErrorType type, - String message -) { -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationErrorType.java b/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationErrorType.java deleted file mode 100644 index e24c7e2..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationErrorType.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.turtlelsp.aspect.model; - -public enum AspectValidationErrorType { - LOAD, - PARSE, - RESOLVE, - PROCESSING -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationResult.java b/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationResult.java deleted file mode 100644 index 794570b..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationResult.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.turtlelsp.aspect.model; - -import java.util.List; - -public record AspectValidationResult( - boolean valid, - String report, - List violations, - AspectValidationError error -) { -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectViolationInfo.java b/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectViolationInfo.java deleted file mode 100644 index d2730d2..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectViolationInfo.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.turtlelsp.aspect.model; - -import java.net.URI; - -public record AspectViolationInfo( - String code, - String message, - URI sourceLocation, - Long line, - Long column -) { -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/aspect/request/ValidateDocumentParams.java b/lsp-server/src/main/java/com/example/turtlelsp/aspect/request/ValidateDocumentParams.java deleted file mode 100644 index 7141a3e..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/aspect/request/ValidateDocumentParams.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.turtlelsp.aspect.request; - -public record ValidateDocumentParams( - String uri, - String reason -) { -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectModelValidationService.java b/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectModelValidationService.java deleted file mode 100644 index 1f8ee1e..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectModelValidationService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.turtlelsp.aspect.service; - -import java.io.File; -import java.nio.file.Path; - -import com.example.turtlelsp.aspect.model.AspectValidationResult; - -public interface AspectModelValidationService { - AspectValidationResult validate(Path path); - - default AspectValidationResult validate(File file) { - return validate(file.toPath()); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectValidationCoordinator.java b/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectValidationCoordinator.java deleted file mode 100644 index 92543c3..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectValidationCoordinator.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.example.turtlelsp.aspect.service; - -import java.nio.file.Path; -import java.util.Map; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.BiConsumer; - -import com.example.turtlelsp.aspect.model.AspectValidationError; -import com.example.turtlelsp.aspect.model.AspectValidationErrorType; -import com.example.turtlelsp.aspect.model.AspectValidationResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class AspectValidationCoordinator implements AutoCloseable { - private static final Logger LOGGER = LoggerFactory.getLogger(AspectValidationCoordinator.class); - - private final AspectModelValidationService validationService; - private final ExecutorService executorService; - private final Map> inFlight = new ConcurrentHashMap<>(); - private final Map generations = new ConcurrentHashMap<>(); - - public AspectValidationCoordinator(AspectModelValidationService validationService) { - this(validationService, Executors.newSingleThreadExecutor(Thread.ofPlatform().name("aspect-validation-", 0).factory())); - } - - AspectValidationCoordinator(AspectModelValidationService validationService, ExecutorService executorService) { - this.validationService = validationService; - this.executorService = executorService; - } - - public long nextGeneration(String uri) { - return generations.computeIfAbsent(uri, ignored -> new AtomicLong()).incrementAndGet(); - } - - public long currentGeneration(String uri) { - AtomicLong generation = generations.get(uri); - return generation != null ? generation.get() : 0L; - } - - public void cancel(String uri) { - CompletableFuture previous = inFlight.remove(uri); - if (previous != null) { - LOGGER.debug("[cancel] cancelling previous aspect validation for {}", uri); - previous.cancel(true); - } - } - - public void submit(String uri, Path path, long generation, BiConsumer callback) { - cancel(uri); - CompletableFuture future = CompletableFuture.supplyAsync( - () -> validationService.validate(path), - executorService - ); - inFlight.put(uri, future); - future.whenComplete((result, throwable) -> { - inFlight.remove(uri, future); - if (throwable instanceof CancellationException || future.isCancelled()) { - LOGGER.debug("[cancel] aspect validation cancelled for {}", uri); - return; - } - if (throwable != null) { - LOGGER.error("[publish diagnostics] aspect validation failed for {}", uri, throwable); - callback.accept(generation, new AspectValidationResult( - false, - throwable.getMessage(), - java.util.List.of(), - new AspectValidationError( AspectValidationErrorType.PROCESSING, throwable.getMessage()) - )); - return; - } - callback.accept(generation, result); - }); - } - - public AspectValidationResult validateSync(Path path) { - return validationService.validate(path); - } - - @Override - public void close() { - inFlight.values().forEach(future -> future.cancel(true)); - executorService.shutdownNow(); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationService.java b/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationService.java deleted file mode 100644 index a705597..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationService.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.example.turtlelsp.aspect.service; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Optional; - -import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader; -import org.eclipse.esmf.aspectmodel.shacl.violation.Violation; -import org.eclipse.esmf.aspectmodel.validation.InvalidLexicalValueViolation; -import org.eclipse.esmf.aspectmodel.validation.InvalidSyntaxViolation; -import org.eclipse.esmf.aspectmodel.validation.ProcessingViolation; -import org.eclipse.esmf.aspectmodel.validation.services.AspectModelValidator; -import org.eclipse.esmf.aspectmodel.validation.services.DetailedViolationFormatter; -import org.eclipse.esmf.metamodel.AspectModel; - -import com.example.turtlelsp.aspect.model.AspectValidationError; -import com.example.turtlelsp.aspect.model.AspectValidationErrorType; -import com.example.turtlelsp.aspect.model.AspectValidationResult; -import com.example.turtlelsp.aspect.model.AspectViolationInfo; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DefaultAspectModelValidationService implements AspectModelValidationService { - private static final Logger LOAD_LOGGER = LoggerFactory.getLogger("com.example.turtlelsp.validation.aspect.load"); - private static final Logger RESOLVE_LOGGER = LoggerFactory.getLogger("com.example.turtlelsp.validation.aspect.resolve"); - private static final Logger VALIDATE_LOGGER = LoggerFactory.getLogger("com.example.turtlelsp.validation.aspect.validate"); - - private final AspectModelLoader loader; - private final AspectModelValidator validator; - - public DefaultAspectModelValidationService() { - this(new AspectModelLoader(), new AspectModelValidator()); - } - - DefaultAspectModelValidationService(AspectModelLoader loader, AspectModelValidator validator) { - this.loader = loader; - this.validator = validator; - } - - @Override - public AspectValidationResult validate(Path path) { - if (path == null) { - return failedResult( AspectValidationErrorType.LOAD, "Path must not be null"); - } - - if (!Files.exists(path)) { - return failedResult(AspectValidationErrorType.LOAD, "Aspect model file does not exist: " + path); - } - - if (!Files.isRegularFile(path) || !Files.isReadable(path)) { - return failedResult(AspectValidationErrorType.LOAD, "Aspect model file is not readable: " + path); - } - - try { - LOAD_LOGGER.debug("[load] loading aspect model from {}", path); - List violations = validator.validateModel(() -> loadAspectModel(path)); - VALIDATE_LOGGER.debug("[validate] validation finished for {} with {} violation(s)", path, violations.size()); - String report = new DetailedViolationFormatter().apply(violations); - AspectValidationError error = classifyError(violations); - return new AspectValidationResult(violations.isEmpty(), report, violations.stream().map(this::toViolationInfo).toList(), error); - } catch (Exception exception) { - VALIDATE_LOGGER.error("[validate] unexpected runtime failure for {}", path, exception); - return failedResult(AspectValidationErrorType.PROCESSING, exception.getMessage()); - } - } - - private AspectModel loadAspectModel(Path path) { - RESOLVE_LOGGER.debug("[resolve imports] resolving imports for {}", path); - return loader.load(path.toFile()); - } - - private AspectValidationResult failedResult(AspectValidationErrorType type, String message) { - return new AspectValidationResult(false, message, List.of(), new AspectValidationError(type, message)); - } - - private AspectValidationError classifyError(List violations) { - Optional firstFailure = violations.stream() - .filter(violation -> violation instanceof InvalidSyntaxViolation || violation instanceof InvalidLexicalValueViolation || violation instanceof ProcessingViolation) - .findFirst(); - - if (firstFailure.isEmpty()) { - return null; - } - - Violation violation = firstFailure.get(); - if (violation instanceof InvalidSyntaxViolation syntaxViolation) { - return new AspectValidationError(AspectValidationErrorType.PARSE, syntaxViolation.message()); - } - if (violation instanceof InvalidLexicalValueViolation lexicalValueViolation) { - return new AspectValidationError(AspectValidationErrorType.PARSE, lexicalValueViolation.message()); - } - - String message = violation.message(); - AspectValidationErrorType type = message != null && message.toLowerCase().contains("resolve") - ? AspectValidationErrorType.RESOLVE - : AspectValidationErrorType.PROCESSING; - return new AspectValidationError(type, message); - } - - private AspectViolationInfo toViolationInfo(Violation violation) { - if (violation instanceof InvalidSyntaxViolation syntaxViolation) { - return new AspectViolationInfo( - syntaxViolation.errorCode(), - syntaxViolation.message(), - syntaxViolation.sourceLocation().orElse(null), - syntaxViolation.line(), - syntaxViolation.column() - ); - } - if (violation instanceof InvalidLexicalValueViolation lexicalValueViolation) { - return new AspectViolationInfo( - lexicalValueViolation.errorCode(), - lexicalValueViolation.message(), - lexicalValueViolation.sourceLocation().orElse(null), - (long) lexicalValueViolation.line(), - (long) lexicalValueViolation.column() - ); - } - - return new AspectViolationInfo( - violation.errorCode(), - violation.message(), - violation.sourceLocation().orElse(null), - null, - null - ); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/common/uri/DocumentUriResolver.java b/lsp-server/src/main/java/com/example/turtlelsp/common/uri/DocumentUriResolver.java deleted file mode 100644 index cb9d750..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/common/uri/DocumentUriResolver.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.turtlelsp.common.uri; - -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; - -public final class DocumentUriResolver { - private DocumentUriResolver() { - } - - public static Path toPath(String uri) { - if (uri == null || !uri.startsWith("file:")) { - return null; - } - - return Paths.get(URI.create(uri)); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflow.java b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflow.java deleted file mode 100644 index 8f0895d..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflow.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import java.nio.file.Path; - -import com.example.turtlelsp.aspect.service.AspectValidationCoordinator; -import com.example.turtlelsp.common.uri.DocumentUriResolver; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class AspectDiagnosticsWorkflow { - private static final Logger LOGGER = LoggerFactory.getLogger( AspectDiagnosticsWorkflow.class ); - - private final AspectValidationCoordinator aspectValidationCoordinator; - private final DocumentDiagnosticsService diagnosticsService; - private final TextDocumentClientNotifier clientNotifier; - - public AspectDiagnosticsWorkflow( - AspectValidationCoordinator aspectValidationCoordinator, - DocumentDiagnosticsService diagnosticsService, - TextDocumentClientNotifier clientNotifier ) { - this.aspectValidationCoordinator = aspectValidationCoordinator; - this.diagnosticsService = diagnosticsService; - this.clientNotifier = clientNotifier; - } - - public void onDocumentChanged( String uri ) { - aspectValidationCoordinator.cancel( uri ); - diagnosticsService.clearAspect( uri ); - } - - public void onDocumentClosed( String uri ) { - aspectValidationCoordinator.cancel( uri ); - diagnosticsService.clearAll( uri ); - } - - public void onDocumentSaved( String uri ) { - Path path = DocumentUriResolver.toPath( uri ); - if ( path == null ) { - LOGGER.info( "[scheduleAspectValidation] unsupported non-file uri={}, skipping aspect validation", uri ); - diagnosticsService.clearAspect( uri ); - clientNotifier.publishCombinedDiagnostics( uri ); - return; - } - - long generation = aspectValidationCoordinator.nextGeneration( uri ); - aspectValidationCoordinator.submit( uri, path, generation, ( completedGeneration, result ) -> { - long currentGeneration = aspectValidationCoordinator.currentGeneration( uri ); - if ( completedGeneration != currentGeneration ) { - LOGGER.debug( "[publish diagnostics] ignoring stale aspect diagnostics for uri={}, generation={}, current={}", uri, - completedGeneration, currentGeneration ); - return; - } - - diagnosticsService.updateAspect( uri, result ); - clientNotifier.publishCombinedDiagnostics( uri ); - } ); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentAspectValidationService.java b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentAspectValidationService.java deleted file mode 100644 index 5bf7f45..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentAspectValidationService.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.List; -import java.util.Objects; - -import com.example.turtlelsp.aspect.model.AspectValidationError; -import com.example.turtlelsp.aspect.model.AspectValidationErrorType; -import com.example.turtlelsp.aspect.model.AspectValidationResult; -import com.example.turtlelsp.aspect.model.AspectViolationInfo; -import com.example.turtlelsp.aspect.service.AspectValidationCoordinator; -import com.example.turtlelsp.common.uri.DocumentUriResolver; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DocumentAspectValidationService { - private static final Logger LOGGER = LoggerFactory.getLogger( DocumentAspectValidationService.class ); - - private final AspectValidationCoordinator aspectValidationCoordinator; - - public DocumentAspectValidationService( AspectValidationCoordinator aspectValidationCoordinator ) { - this.aspectValidationCoordinator = aspectValidationCoordinator; - } - - public AspectValidationResult validateDocument( String uri, String content ) { - if ( content == null ) { - return failedValidation( AspectValidationErrorType.LOAD, "Document is not available in memory: " + uri ); - } - - Path path = DocumentUriResolver.toPath( uri ); - if ( path == null ) { - return failedValidation( AspectValidationErrorType.LOAD, "Aspect validation supports only file URIs: " + uri ); - } - - return validateOpenDocument( uri, path, content ); - } - - private AspectValidationResult validateOpenDocument( String uri, Path originalPath, String content ) { - Path parent = originalPath.getParent(); - if ( parent == null ) { - return failedValidation( AspectValidationErrorType.LOAD, "Document path has no parent directory: " + originalPath ); - } - - String originalFileName = originalPath.getFileName() != null ? originalPath.getFileName().toString() : "aspect"; - String tempPrefix = originalFileName.replaceAll( "[^A-Za-z0-9._-]", "_" ) + "-"; - if ( tempPrefix.length() < 3 ) { - tempPrefix = "ttl-"; - } - - Path tempFile = null; - try { - tempFile = Files.createTempFile( parent, tempPrefix, ".ttl" ); - Files.writeString( tempFile, content, StandardOpenOption.TRUNCATE_EXISTING ); - AspectValidationResult result = aspectValidationCoordinator.validateSync( tempFile ); - return remapValidationResult( result, tempFile, originalPath, uri ); - } catch ( IOException exception ) { - LOGGER.error( "[validateDocument] failed to prepare in-memory validation for {}", uri, exception ); - return failedValidation( AspectValidationErrorType.PROCESSING, exception.getMessage() ); - } finally { - if ( tempFile != null ) { - try { - Files.deleteIfExists( tempFile ); - } catch ( IOException exception ) { - LOGGER.warn( "[validateDocument] failed to delete temp file {}", tempFile, exception ); - } - } - } - } - - private AspectValidationResult remapValidationResult( AspectValidationResult result, Path tempFile, Path originalPath, String originalUri ) { - URI tempUri = tempFile.toUri(); - List remappedViolations = result.violations().stream() - .map( violation -> remapViolation( violation, tempUri, originalUri ) ) - .toList(); - String remappedReport = remapReport( result.report(), tempFile, originalPath, originalUri ); - return new AspectValidationResult( result.valid(), remappedReport, remappedViolations, result.error() ); - } - - private AspectViolationInfo remapViolation( AspectViolationInfo violation, URI tempUri, String originalUri ) { - if ( !Objects.equals( violation.sourceLocation(), tempUri ) ) { - return violation; - } - - return new AspectViolationInfo( - violation.code(), - violation.message(), - URI.create( originalUri ), - violation.line(), - violation.column() - ); - } - - private String remapReport( String report, Path tempFile, Path originalPath, String originalUri ) { - if ( report == null || report.isBlank() ) { - return report; - } - - return report - .replace( tempFile.toUri().toString(), originalUri ) - .replace( tempFile.toAbsolutePath().toString(), originalPath.toAbsolutePath().toString() ); - } - - private AspectValidationResult failedValidation( AspectValidationErrorType type, String message ) { - return new AspectValidationResult( false, message, List.of(), new AspectValidationError( type, message ) ); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsService.java b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsService.java deleted file mode 100644 index 5fada7f..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsService.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import java.util.List; - -import com.example.turtlelsp.aspect.diagnostics.AspectDiagnosticMapper; -import com.example.turtlelsp.aspect.model.AspectValidationResult; -import org.eclipse.lsp4j.Diagnostic; - -public class DocumentDiagnosticsService { - private final TurtleSyntaxValidationService syntaxValidationService; - private final AspectDiagnosticMapper aspectDiagnosticMapper; - private final DocumentDiagnosticsStore diagnosticsStore; - - public DocumentDiagnosticsService() { - this( new TurtleSyntaxValidationService(), new AspectDiagnosticMapper(), new DocumentDiagnosticsStore() ); - } - - DocumentDiagnosticsService( - TurtleSyntaxValidationService syntaxValidationService, - AspectDiagnosticMapper aspectDiagnosticMapper, - DocumentDiagnosticsStore diagnosticsStore ) { - this.syntaxValidationService = syntaxValidationService; - this.aspectDiagnosticMapper = aspectDiagnosticMapper; - this.diagnosticsStore = diagnosticsStore; - } - - public void updateSyntax( String uri, String content ) { - diagnosticsStore.putSyntax( uri, syntaxValidationService.validate( content ) ); - } - - public void updateAspect( String uri, AspectValidationResult result ) { - diagnosticsStore.putAspect( uri, aspectDiagnosticMapper.toDiagnostics( uri, result ) ); - } - - public void clearAspect( String uri ) { - diagnosticsStore.clearAspect( uri ); - } - - public void clearAll( String uri ) { - diagnosticsStore.clear( uri ); - } - - public List getCombined( String uri ) { - return diagnosticsStore.getCombined( uri ); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStore.java b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStore.java deleted file mode 100644 index 66142c1..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStore.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.eclipse.lsp4j.Diagnostic; - -public class DocumentDiagnosticsStore { - private final Map> syntaxDiagnostics = new ConcurrentHashMap<>(); - private final Map> aspectDiagnostics = new ConcurrentHashMap<>(); - - public void putSyntax( String uri, List diagnostics ) { - syntaxDiagnostics.put( uri, List.copyOf( diagnostics ) ); - } - - public void putAspect( String uri, List diagnostics ) { - aspectDiagnostics.put( uri, List.copyOf( diagnostics ) ); - } - - public void clearAspect( String uri ) { - aspectDiagnostics.remove( uri ); - } - - public void clear( String uri ) { - syntaxDiagnostics.remove( uri ); - aspectDiagnostics.remove( uri ); - } - - public List getCombined( String uri ) { - List diagnostics = new ArrayList<>(); - diagnostics.addAll( syntaxDiagnostics.getOrDefault( uri, List.of() ) ); - diagnostics.addAll( aspectDiagnostics.getOrDefault( uri, List.of() ) ); - return diagnostics; - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentStore.java b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentStore.java deleted file mode 100644 index e2fe287..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentStore.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class DocumentStore { - private final Map documents = new ConcurrentHashMap<>(); - - public void put( String uri, String content ) { - documents.put( uri, content ); - } - - public String get( String uri ) { - return documents.get( uri ); - } - - public String getOrDefault( String uri, String fallback ) { - return documents.getOrDefault( uri, fallback ); - } - - public void remove( String uri ) { - documents.remove( uri ); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifier.java b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifier.java deleted file mode 100644 index fde7cf8..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifier.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import java.util.List; - -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.PublishDiagnosticsParams; -import org.eclipse.lsp4j.services.LanguageClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class TextDocumentClientNotifier { - private static final Logger LOGGER = LoggerFactory.getLogger( TextDocumentClientNotifier.class ); - - private final DocumentDiagnosticsService diagnosticsService; - private LanguageClient client; - - public TextDocumentClientNotifier( DocumentDiagnosticsService diagnosticsService ) { - this.diagnosticsService = diagnosticsService; - } - - public void connect( LanguageClient client ) { - this.client = client; - } - - public void publishCombinedDiagnostics( String uri ) { - if ( client == null ) { - LOGGER.warn( "[publishDiagnostics] client is null, skipping for uri={}", uri ); - return; - } - - List diagnostics = diagnosticsService.getCombined( uri ); - LOGGER.debug( "[publish diagnostics] publishing {} diagnostic(s) for uri={}", diagnostics.size(), uri ); - client.publishDiagnostics( new PublishDiagnosticsParams( uri, diagnostics ) ); - } - - public void publishEmptyDiagnostics( String uri ) { - if ( client == null ) { - return; - } - - client.publishDiagnostics( new PublishDiagnosticsParams( uri, List.of() ) ); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleSyntaxValidationService.java b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleSyntaxValidationService.java deleted file mode 100644 index 7c994bb..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleSyntaxValidationService.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.jena.riot.Lang; -import org.apache.jena.riot.RDFParser; -import org.apache.jena.riot.RiotException; -import org.apache.jena.riot.RiotParseException; -import org.apache.jena.riot.system.ErrorHandlerFactory; -import org.apache.jena.riot.system.StreamRDFLib; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.DiagnosticSeverity; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class TurtleSyntaxValidationService { - private static final Logger LOGGER = LoggerFactory.getLogger( TurtleSyntaxValidationService.class ); - private static final String SYNTAX_SOURCE = "lsp-server.syntax"; - - public List validate( String content ) { - List diagnostics = new ArrayList<>(); - - try { - RDFParser.create() - .fromString( content ) - .lang( Lang.TTL ) - .errorHandler( ErrorHandlerFactory.errorHandlerStrictNoLogging ) - .parse( StreamRDFLib.sinkNull() ); - LOGGER.debug( "[validate] turtle parsing successful" ); - } catch ( RiotParseException exception ) { - LOGGER.warn( "[validate] parse error at line={}, col={}: {}", exception.getLine(), exception.getCol(), exception.getMessage() ); - diagnostics.add( toDiagnostic( exception.getMessage(), exception.getLine(), exception.getCol() ) ); - } catch ( RiotException exception ) { - LOGGER.warn( "[validate] rdf error: {}", exception.getMessage() ); - diagnostics.add( toDiagnostic( exception.getMessage(), 1, 1 ) ); - } - - LOGGER.debug( "[validate] found {} diagnostic(s)", diagnostics.size() ); - return diagnostics; - } - - private Diagnostic toDiagnostic( String message, long line, long column ) { - int safeLine = (int) Math.max( 0, line - 1 ); - int safeColumn = (int) Math.max( 0, column - 1 ); - - Diagnostic diagnostic = new Diagnostic(); - diagnostic.setSource( SYNTAX_SOURCE ); - diagnostic.setSeverity( DiagnosticSeverity.Error ); - diagnostic.setMessage( message != null ? message : "Invalid Turtle syntax" ); - diagnostic.setRange( new Range( new Position( safeLine, safeColumn ), new Position( safeLine, safeColumn + 1 ) ) ); - return diagnostic; - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleTextDocumentService.java b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleTextDocumentService.java deleted file mode 100644 index fcbfb5a..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleTextDocumentService.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import com.example.turtlelsp.aspect.model.AspectValidationResult; -import com.example.turtlelsp.aspect.service.AspectModelValidationService; -import com.example.turtlelsp.aspect.service.AspectValidationCoordinator; -import com.example.turtlelsp.aspect.service.DefaultAspectModelValidationService; -import com.example.turtlelsp.turtle.navigation.TurtlePrefixDefinitionService; -import org.eclipse.lsp4j.DefinitionParams; -import org.eclipse.lsp4j.DidChangeTextDocumentParams; -import org.eclipse.lsp4j.DidCloseTextDocumentParams; -import org.eclipse.lsp4j.DidOpenTextDocumentParams; -import org.eclipse.lsp4j.DidSaveTextDocumentParams; -import org.eclipse.lsp4j.Location; -import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class TurtleTextDocumentService implements TextDocumentService { - private static final Logger LOGGER = LoggerFactory.getLogger( TurtleTextDocumentService.class ); - private final DocumentStore documentStore; - private final DocumentDiagnosticsService diagnosticsService; - private final TextDocumentClientNotifier clientNotifier; - private final TurtlePrefixDefinitionService prefixDefinitionService; - private final DocumentAspectValidationService documentValidationService; - private final AspectDiagnosticsWorkflow aspectDiagnosticsWorkflow; - private final AspectValidationCoordinator aspectValidationCoordinator; - - public TurtleTextDocumentService() { - this( new DefaultAspectModelValidationService() ); - } - - public TurtleTextDocumentService( AspectModelValidationService aspectValidationService ) { - this( - new DocumentStore(), - new DocumentDiagnosticsService(), - new TurtlePrefixDefinitionService(), - new AspectValidationCoordinator( aspectValidationService ) - ); - } - - TurtleTextDocumentService( - DocumentStore documentStore, - DocumentDiagnosticsService diagnosticsService, - TurtlePrefixDefinitionService prefixDefinitionService, - AspectValidationCoordinator aspectValidationCoordinator ) { - this.documentStore = documentStore; - this.diagnosticsService = diagnosticsService; - this.prefixDefinitionService = prefixDefinitionService; - this.aspectValidationCoordinator = aspectValidationCoordinator; - this.clientNotifier = new TextDocumentClientNotifier( diagnosticsService ); - this.documentValidationService = new DocumentAspectValidationService( aspectValidationCoordinator ); - this.aspectDiagnosticsWorkflow = new AspectDiagnosticsWorkflow( aspectValidationCoordinator, diagnosticsService, clientNotifier ); - } - - public void connect( LanguageClient client ) { - clientNotifier.connect( client ); - } - - public void shutdown() { - aspectValidationCoordinator.close(); - } - - public AspectValidationResult validateDocument( String uri ) { - return documentValidationService.validateDocument( uri, documentStore.get( uri ) ); - } - - @Override - public void didOpen( DidOpenTextDocumentParams params ) { - String uri = params.getTextDocument().getUri(); - String content = params.getTextDocument().getText(); - LOGGER.info( "[didOpen] uri={}, contentLength={}", uri, content.length() ); - documentStore.put( uri, content ); - diagnosticsService.updateSyntax( uri, content ); - clientNotifier.publishCombinedDiagnostics( uri ); - } - - @Override - public void didChange( DidChangeTextDocumentParams params ) { - String uri = params.getTextDocument().getUri(); - String content = params.getContentChanges().isEmpty() ? - documentStore.getOrDefault( uri, "" ) : - params.getContentChanges().getLast().getText(); - LOGGER.debug( "[didChange] uri={}, contentLength={}, changes={}", uri, content.length(), params.getContentChanges().size() ); - documentStore.put( uri, content ); - diagnosticsService.updateSyntax( uri, content ); - aspectDiagnosticsWorkflow.onDocumentChanged( uri ); - clientNotifier.publishCombinedDiagnostics( uri ); - } - - @Override - public void didClose( DidCloseTextDocumentParams params ) { - String uri = params.getTextDocument().getUri(); - LOGGER.info( "[didClose] uri={}", uri ); - documentStore.remove( uri ); - aspectDiagnosticsWorkflow.onDocumentClosed( uri ); - clientNotifier.publishEmptyDiagnostics( uri ); - } - - @Override - public void didSave( DidSaveTextDocumentParams params ) { - String uri = params.getTextDocument().getUri(); - String content = documentStore.getOrDefault( uri, "" ); - LOGGER.info( "[didSave] uri={}, contentLength={}", uri, content.length() ); - diagnosticsService.updateSyntax( uri, content ); - clientNotifier.publishCombinedDiagnostics( uri ); - aspectDiagnosticsWorkflow.onDocumentSaved( uri ); - } - - @Override - public CompletableFuture, List>> definition( - DefinitionParams params ) { - String uri = params.getTextDocument().getUri(); - String content = documentStore.get( uri ); - if ( content == null ) { - return CompletableFuture.completedFuture( Either.forLeft( List.of() ) ); - } - - Location declaration = prefixDefinitionService.findPrefixDeclaration( uri, content, params.getPosition() ); - if ( declaration == null ) { - return CompletableFuture.completedFuture( Either.forLeft( List.of() ) ); - } - - return CompletableFuture.completedFuture( Either.forLeft( List.of( declaration ) ) ); - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/lsp/workspace/TurtleWorkspaceService.java b/lsp-server/src/main/java/com/example/turtlelsp/lsp/workspace/TurtleWorkspaceService.java deleted file mode 100644 index e5bfb28..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/lsp/workspace/TurtleWorkspaceService.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.turtlelsp.lsp.workspace; - -import com.example.turtlelsp.lsp.text.TurtleTextDocumentService; - -import org.eclipse.lsp4j.DidChangeConfigurationParams; -import org.eclipse.lsp4j.DidChangeWatchedFilesParams; -import org.eclipse.lsp4j.services.WorkspaceService; - -public class TurtleWorkspaceService implements WorkspaceService { - public TurtleWorkspaceService( TurtleTextDocumentService textDocumentService ) { - } - - @Override - public void didChangeConfiguration( DidChangeConfigurationParams params ) { - } - - @Override - public void didChangeWatchedFiles( DidChangeWatchedFilesParams params ) { - } -} diff --git a/lsp-server/src/main/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionService.java b/lsp-server/src/main/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionService.java deleted file mode 100644 index 65f36e8..0000000 --- a/lsp-server/src/main/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionService.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.example.turtlelsp.turtle.navigation; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.eclipse.lsp4j.Location; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; - -public class TurtlePrefixDefinitionService { - private static final Pattern PREFIX_DECLARATION_PATTERN = Pattern.compile( - "^\\s*@prefix\\s+([A-Za-z][A-Za-z0-9_-]*)?:\\s*<[^>]*>\\s*\\.", - Pattern.CASE_INSENSITIVE - ); - - public Location findPrefixDeclaration(String uri, String content, Position position) { - String prefix = findPrefixAtPosition(content, position); - if (prefix == null) { - return null; - } - - String[] lines = content.split("\\R", -1); - for (int line = 0; line < lines.length; line++) { - Matcher matcher = PREFIX_DECLARATION_PATTERN.matcher(lines[line]); - if (!matcher.find()) { - continue; - } - - String declaredPrefix = matcher.group(1); - String normalizedPrefix = declaredPrefix == null ? "" : declaredPrefix; - if (!normalizedPrefix.equals(prefix)) { - continue; - } - - return new Location(uri, new Range(new Position(line, 0), new Position(line, lines[line].length()))); - } - - return null; - } - - public String findPrefixAtPosition(String content, Position position) { - int lineStart = 0; - int currentLine = 0; - while (currentLine < position.getLine() && lineStart < content.length()) { - if (content.charAt(lineStart++) == '\n') { - currentLine++; - } - } - if (currentLine != position.getLine()) { - return null; - } - - int lineEnd = lineStart; - while (lineEnd < content.length() && content.charAt(lineEnd) != '\n') { - lineEnd++; - } - - int character = Math.max(0, Math.min(position.getCharacter(), lineEnd - lineStart)); - int offset = lineStart + character; - if (offset > lineStart && (offset == lineEnd || !isPrefixedNameChar(content.charAt(offset)))) { - offset--; - } - if (offset < lineStart || offset >= lineEnd || !isPrefixedNameChar(content.charAt(offset))) { - return null; - } - - int start = offset; - while (start > lineStart && isPrefixedNameChar(content.charAt(start - 1))) { - start--; - } - - int end = offset + 1; - while (end < lineEnd && isPrefixedNameChar(content.charAt(end))) { - end++; - } - - String token = content.substring(start, end); - int colonIndex = token.indexOf(':'); - if (colonIndex < 0 || colonIndex == token.length() - 1) { - return null; - } - - String prefix = token.substring(0, colonIndex); - String localPart = token.substring(colonIndex + 1); - if (localPart.isEmpty()) { - return null; - } - - return prefix; - } - - private boolean isPrefixedNameChar(char ch) { - return Character.isLetterOrDigit(ch) || ch == ':' || ch == '_' || ch == '-'; - } -} diff --git a/lsp-server/src/main/resources/log4j2.xml b/lsp-server/src/main/resources/log4j2.xml deleted file mode 100644 index b66e901..0000000 --- a/lsp-server/src/main/resources/log4j2.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - logs - ${logDir}/lsp-server.log - %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %c{1} - %msg%n - - - - - - - - - - - - - - - - - - - diff --git a/lsp-server/src/test/java/com/example/turtlelsp/TurtleDefinitionTest.java b/lsp-server/src/test/java/com/example/turtlelsp/TurtleDefinitionTest.java deleted file mode 100644 index b5ef12b..0000000 --- a/lsp-server/src/test/java/com/example/turtlelsp/TurtleDefinitionTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.example.turtlelsp; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; - -import com.example.turtlelsp.lsp.text.TurtleTextDocumentService; -import org.eclipse.lsp4j.DefinitionParams; -import org.eclipse.lsp4j.DidOpenTextDocumentParams; -import org.eclipse.lsp4j.InitializeResult; -import org.eclipse.lsp4j.Location; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.TextDocumentIdentifier; -import org.eclipse.lsp4j.TextDocumentItem; -import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.junit.jupiter.api.Test; - -class TurtleDefinitionTest { - @Test - void initializeAdvertisesDefinitionProvider() { - TurtleLanguageServer server = new TurtleLanguageServer(); - - InitializeResult result = server.initialize(null).join(); - - assertThat(result.getCapabilities().getDefinitionProvider().getLeft()).isTrue(); - } - - @Test - void findsDeclaredPrefixDefinition() { - String content = """ - @prefix ex: . - - ex:Alice ex:name "Alice" . - """; - - List locations = definition(content, new Position(2, 1)); - - assertThat(locations).hasSize(1); - assertThat(locations.getFirst().getRange().getStart().getLine()).isZero(); - } - - @Test - void returnsEmptyListWhenDocumentWasNotOpened() { - TurtleTextDocumentService service = new TurtleTextDocumentService(); - Either, List> result = service.definition( - new DefinitionParams(new TextDocumentIdentifier("file:///missing.ttl"), new Position(0, 0)) - ).join(); - - assertThat(result.getLeft()).isEmpty(); - } - - private List definition(String content, Position position) { - TurtleTextDocumentService service = new TurtleTextDocumentService(); - String uri = "file:///test.ttl"; - service.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(uri, "turtle", 1, content))); - - Either, List> result = service.definition( - new DefinitionParams(new TextDocumentIdentifier(uri), position) - ).join(); - - return result.getLeft(); - } -} diff --git a/lsp-server/src/test/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationServiceTest.java b/lsp-server/src/test/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationServiceTest.java deleted file mode 100644 index 94e7507..0000000 --- a/lsp-server/src/test/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationServiceTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.turtlelsp.aspect.service; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.nio.file.Files; -import java.nio.file.Path; - -import com.example.turtlelsp.aspect.model.AspectValidationErrorType; -import com.example.turtlelsp.aspect.model.AspectValidationResult; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class DefaultAspectModelValidationServiceTest { - private final DefaultAspectModelValidationService service = new DefaultAspectModelValidationService(); - - @TempDir - Path tempDir; - - @Test - void validatesAspectModelWhenFileIsValid() throws Exception { - Path modelFile = tempDir.resolve("Aspect.ttl"); - Files.writeString(modelFile, validAspectModel()); - - AspectValidationResult result = service.validate(modelFile); - - assertThat(result.valid()).isTrue(); - assertThat(result.violations()).isEmpty(); - assertThat(result.error()).isNull(); - } - - @Test - void returnsViolationsWhenAspectModelIsInvalid() throws Exception { - Path modelFile = tempDir.resolve("InvalidAspect.ttl"); - Files.writeString(modelFile, invalidSyntaxAspectModel()); - - AspectValidationResult result = service.validate(modelFile); - - assertThat(result.valid()).isFalse(); - assertThat(result.violations()).isNotEmpty(); - assertThat(result.report()).isNotBlank(); - assertThat(result.error()).isNotNull(); - assertThat(result.error().type()).isEqualTo( AspectValidationErrorType.PARSE); - } - - @Test - void returnsLoadErrorWhenFileDoesNotExist() { - Path missingFile = tempDir.resolve("missing.ttl"); - - AspectValidationResult result = service.validate(missingFile); - - assertThat(result.valid()).isFalse(); - assertThat(result.violations()).isEmpty(); - assertThat(result.error()).isNotNull(); - assertThat(result.error().type()).isEqualTo(AspectValidationErrorType.LOAD); - } - - private String validAspectModel() { - return """ - @prefix : . - @prefix samm: . - @prefix samm-c: . - @prefix xsd: . - - :Aspect a samm:Aspect ; - samm:preferredName "Test Aspect"@en ; - samm:description "This is a test description"@en ; - samm:properties ( ) ; - samm:operations ( ) . - """; - } - - private String invalidSyntaxAspectModel() { - return """ - @prefix : . - @prefix samm: . - - :InvalidSyntax a samm:Aspect; - samm:preferredName "Test Aspect"@en - samm:properties () ; - samm:operations () . - """; - } -} diff --git a/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflowTest.java b/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflowTest.java deleted file mode 100644 index c2760c6..0000000 --- a/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflowTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.nio.file.Path; -import java.util.function.BiConsumer; - -import com.example.turtlelsp.aspect.model.AspectValidationResult; -import com.example.turtlelsp.aspect.service.AspectValidationCoordinator; -import org.junit.jupiter.api.Test; - -class AspectDiagnosticsWorkflowTest { - @Test - void onDocumentChangedCancelsValidationAndClearsAspectDiagnostics() { - AspectValidationCoordinator coordinator = mock(AspectValidationCoordinator.class); - DocumentDiagnosticsService diagnosticsService = mock(DocumentDiagnosticsService.class); - TextDocumentClientNotifier notifier = mock(TextDocumentClientNotifier.class); - AspectDiagnosticsWorkflow workflow = new AspectDiagnosticsWorkflow(coordinator, diagnosticsService, notifier); - - workflow.onDocumentChanged("file:///test.ttl"); - - verify(coordinator).cancel("file:///test.ttl"); - verify(diagnosticsService).clearAspect("file:///test.ttl"); - } - - @Test - void onDocumentClosedCancelsValidationAndClearsAllDiagnostics() { - AspectValidationCoordinator coordinator = mock(AspectValidationCoordinator.class); - DocumentDiagnosticsService diagnosticsService = mock(DocumentDiagnosticsService.class); - TextDocumentClientNotifier notifier = mock(TextDocumentClientNotifier.class); - AspectDiagnosticsWorkflow workflow = new AspectDiagnosticsWorkflow(coordinator, diagnosticsService, notifier); - - workflow.onDocumentClosed("file:///test.ttl"); - - verify(coordinator).cancel("file:///test.ttl"); - verify(diagnosticsService).clearAll("file:///test.ttl"); - } - - @Test - void onDocumentSavedForNonFileUriClearsAspectDiagnosticsAndPublishesCombined() { - AspectValidationCoordinator coordinator = mock(AspectValidationCoordinator.class); - DocumentDiagnosticsService diagnosticsService = mock(DocumentDiagnosticsService.class); - TextDocumentClientNotifier notifier = mock(TextDocumentClientNotifier.class); - AspectDiagnosticsWorkflow workflow = new AspectDiagnosticsWorkflow(coordinator, diagnosticsService, notifier); - - workflow.onDocumentSaved("untitled:Aspect.ttl"); - - verify(diagnosticsService).clearAspect("untitled:Aspect.ttl"); - verify(notifier).publishCombinedDiagnostics("untitled:Aspect.ttl"); - verify(coordinator, never()).submit(any(), any(), any(Long.class), any()); - } - - @Test - void onDocumentSavedForFileUriSubmitsValidation() { - AspectValidationCoordinator coordinator = mock(AspectValidationCoordinator.class); - DocumentDiagnosticsService diagnosticsService = mock(DocumentDiagnosticsService.class); - TextDocumentClientNotifier notifier = mock(TextDocumentClientNotifier.class); - AspectDiagnosticsWorkflow workflow = new AspectDiagnosticsWorkflow(coordinator, diagnosticsService, notifier); - String uri = Path.of("pom.xml").toAbsolutePath().toUri().toString(); - when(coordinator.nextGeneration(uri)).thenReturn(7L); - - workflow.onDocumentSaved(uri); - - verify(coordinator).nextGeneration(uri); - verify(coordinator).submit(eq(uri), any(Path.class), eq(7L), any()); - } - - @Test - void onDocumentSavedUpdatesDiagnosticsAndPublishesWhenResultIsCurrent() { - AspectValidationCoordinator coordinator = mock(AspectValidationCoordinator.class); - DocumentDiagnosticsService diagnosticsService = mock(DocumentDiagnosticsService.class); - TextDocumentClientNotifier notifier = mock(TextDocumentClientNotifier.class); - AspectDiagnosticsWorkflow workflow = new AspectDiagnosticsWorkflow(coordinator, diagnosticsService, notifier); - String uri = Path.of("pom.xml").toAbsolutePath().toUri().toString(); - AspectValidationResult result = mock(AspectValidationResult.class); - when(coordinator.nextGeneration(uri)).thenReturn(3L); - when(coordinator.currentGeneration(uri)).thenReturn(3L); - doAnswer(invocation -> { - BiConsumer callback = invocation.getArgument(3); - callback.accept(3L, result); - return null; - }).when(coordinator).submit(eq(uri), any(Path.class), eq(3L), any()); - - workflow.onDocumentSaved(uri); - - verify(diagnosticsService).updateAspect(uri, result); - verify(notifier).publishCombinedDiagnostics(uri); - } - - @Test - void onDocumentSavedIgnoresStaleResult() { - AspectValidationCoordinator coordinator = mock(AspectValidationCoordinator.class); - DocumentDiagnosticsService diagnosticsService = mock(DocumentDiagnosticsService.class); - TextDocumentClientNotifier notifier = mock(TextDocumentClientNotifier.class); - AspectDiagnosticsWorkflow workflow = new AspectDiagnosticsWorkflow(coordinator, diagnosticsService, notifier); - String uri = Path.of("pom.xml").toAbsolutePath().toUri().toString(); - AspectValidationResult result = mock(AspectValidationResult.class); - when(coordinator.nextGeneration(uri)).thenReturn(3L); - when(coordinator.currentGeneration(uri)).thenReturn(4L); - doAnswer(invocation -> { - BiConsumer callback = invocation.getArgument(3); - callback.accept(3L, result); - return null; - }).when(coordinator).submit(eq(uri), any(Path.class), eq(3L), any()); - - workflow.onDocumentSaved(uri); - - verify(diagnosticsService, never()).updateAspect(uri, result); - verify(notifier, never()).publishCombinedDiagnostics(uri); - } - -} diff --git a/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsServiceTest.java b/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsServiceTest.java deleted file mode 100644 index 388d37c..0000000 --- a/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsServiceTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; - -import com.example.turtlelsp.aspect.diagnostics.AspectDiagnosticMapper; -import com.example.turtlelsp.aspect.model.AspectValidationResult; -import org.eclipse.lsp4j.Diagnostic; -import org.junit.jupiter.api.Test; - -class DocumentDiagnosticsServiceTest { - @Test - void updateSyntaxDelegatesToValidatorAndStore() { - TurtleSyntaxValidationService syntaxValidationService = mock(TurtleSyntaxValidationService.class); - AspectDiagnosticMapper aspectDiagnosticMapper = mock(AspectDiagnosticMapper.class); - DocumentDiagnosticsStore diagnosticsStore = mock(DocumentDiagnosticsStore.class); - DocumentDiagnosticsService service = new DocumentDiagnosticsService( - syntaxValidationService, - aspectDiagnosticMapper, - diagnosticsStore - ); - List diagnostics = List.of(new Diagnostic()); - when(syntaxValidationService.validate("content")).thenReturn(diagnostics); - - service.updateSyntax("file:///test.ttl", "content"); - - verify(syntaxValidationService).validate("content"); - verify(diagnosticsStore).putSyntax("file:///test.ttl", diagnostics); - } - - @Test - void updateAspectDelegatesToMapperAndStore() { - TurtleSyntaxValidationService syntaxValidationService = mock(TurtleSyntaxValidationService.class); - AspectDiagnosticMapper aspectDiagnosticMapper = mock(AspectDiagnosticMapper.class); - DocumentDiagnosticsStore diagnosticsStore = mock(DocumentDiagnosticsStore.class); - DocumentDiagnosticsService service = new DocumentDiagnosticsService( - syntaxValidationService, - aspectDiagnosticMapper, - diagnosticsStore - ); - AspectValidationResult result = mock(AspectValidationResult.class); - List diagnostics = List.of(new Diagnostic()); - when(aspectDiagnosticMapper.toDiagnostics("file:///test.ttl", result)).thenReturn(diagnostics); - - service.updateAspect("file:///test.ttl", result); - - verify(aspectDiagnosticMapper).toDiagnostics("file:///test.ttl", result); - verify(diagnosticsStore).putAspect("file:///test.ttl", diagnostics); - } - - @Test - void clearAspectDelegatesToStore() { - DocumentDiagnosticsStore diagnosticsStore = mock(DocumentDiagnosticsStore.class); - DocumentDiagnosticsService service = new DocumentDiagnosticsService( - mock(TurtleSyntaxValidationService.class), - mock(AspectDiagnosticMapper.class), - diagnosticsStore - ); - - service.clearAspect("file:///test.ttl"); - - verify(diagnosticsStore).clearAspect("file:///test.ttl"); - } - - @Test - void clearAllDelegatesToStore() { - DocumentDiagnosticsStore diagnosticsStore = mock(DocumentDiagnosticsStore.class); - DocumentDiagnosticsService service = new DocumentDiagnosticsService( - mock(TurtleSyntaxValidationService.class), - mock(AspectDiagnosticMapper.class), - diagnosticsStore - ); - - service.clearAll("file:///test.ttl"); - - verify(diagnosticsStore).clear("file:///test.ttl"); - } - - @Test - void getCombinedDelegatesToStore() { - DocumentDiagnosticsStore diagnosticsStore = mock(DocumentDiagnosticsStore.class); - DocumentDiagnosticsService service = new DocumentDiagnosticsService( - mock(TurtleSyntaxValidationService.class), - mock(AspectDiagnosticMapper.class), - diagnosticsStore - ); - List diagnostics = List.of(new Diagnostic()); - when(diagnosticsStore.getCombined("file:///test.ttl")).thenReturn(diagnostics); - - List result = service.getCombined("file:///test.ttl"); - - assertThat(result).isSameAs(diagnostics); - verify(diagnosticsStore).getCombined("file:///test.ttl"); - } -} diff --git a/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStoreTest.java b/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStoreTest.java deleted file mode 100644 index 7424b66..0000000 --- a/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStoreTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; - -import org.eclipse.lsp4j.Diagnostic; -import org.junit.jupiter.api.Test; - -class DocumentDiagnosticsStoreTest { - @Test - void combinesSyntaxAndAspectDiagnosticsInOrder() { - DocumentDiagnosticsStore store = new DocumentDiagnosticsStore(); - Diagnostic syntax = new Diagnostic(); - syntax.setMessage("syntax"); - Diagnostic aspect = new Diagnostic(); - aspect.setMessage("aspect"); - - store.putSyntax("file:///test.ttl", List.of(syntax)); - store.putAspect("file:///test.ttl", List.of(aspect)); - - assertThat(store.getCombined("file:///test.ttl")) - .extracting(Diagnostic::getMessage) - .containsExactly("syntax", "aspect"); - } - - @Test - void clearAspectKeepsSyntaxDiagnostics() { - DocumentDiagnosticsStore store = new DocumentDiagnosticsStore(); - Diagnostic syntax = new Diagnostic(); - syntax.setMessage("syntax"); - Diagnostic aspect = new Diagnostic(); - aspect.setMessage("aspect"); - - store.putSyntax("file:///test.ttl", List.of(syntax)); - store.putAspect("file:///test.ttl", List.of(aspect)); - store.clearAspect("file:///test.ttl"); - - assertThat(store.getCombined("file:///test.ttl")) - .extracting(Diagnostic::getMessage) - .containsExactly("syntax"); - } -} diff --git a/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifierTest.java b/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifierTest.java deleted file mode 100644 index 2360635..0000000 --- a/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifierTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.turtlelsp.lsp.text; - -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; - -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.services.LanguageClient; -import org.junit.jupiter.api.Test; - -class TextDocumentClientNotifierTest { - @Test - void publishCombinedDiagnosticsUsesStoredDiagnostics() { - DocumentDiagnosticsService diagnosticsService = mock(DocumentDiagnosticsService.class); - LanguageClient client = mock(LanguageClient.class); - TextDocumentClientNotifier notifier = new TextDocumentClientNotifier(diagnosticsService); - List diagnostics = List.of(new Diagnostic()); - when(diagnosticsService.getCombined("file:///test.ttl")).thenReturn(diagnostics); - notifier.connect(client); - - notifier.publishCombinedDiagnostics("file:///test.ttl"); - - verify(diagnosticsService).getCombined("file:///test.ttl"); - verify(client).publishDiagnostics(argThat(params -> - "file:///test.ttl".equals(params.getUri()) && params.getDiagnostics().equals(diagnostics) - )); - } - - @Test - void publishEmptyDiagnosticsSendsEmptyList() { - LanguageClient client = mock(LanguageClient.class); - TextDocumentClientNotifier notifier = new TextDocumentClientNotifier(mock(DocumentDiagnosticsService.class)); - notifier.connect(client); - - notifier.publishEmptyDiagnostics("file:///test.ttl"); - - verify(client).publishDiagnostics(argThat(params -> - "file:///test.ttl".equals(params.getUri()) && params.getDiagnostics().isEmpty() - )); - } - - @Test - void publishMethodsDoNothingWithoutClient() { - DocumentDiagnosticsService diagnosticsService = mock(DocumentDiagnosticsService.class); - TextDocumentClientNotifier notifier = new TextDocumentClientNotifier(diagnosticsService); - - notifier.publishCombinedDiagnostics("file:///test.ttl"); - notifier.publishEmptyDiagnostics("file:///test.ttl"); - - verify(diagnosticsService, never()).getCombined("file:///test.ttl"); - } -} diff --git a/lsp-server/src/test/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionServiceTest.java b/lsp-server/src/test/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionServiceTest.java deleted file mode 100644 index 7c77938..0000000 --- a/lsp-server/src/test/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionServiceTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.turtlelsp.turtle.navigation; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.eclipse.lsp4j.Location; -import org.eclipse.lsp4j.Position; -import org.junit.jupiter.api.Test; - -class TurtlePrefixDefinitionServiceTest { - private final TurtlePrefixDefinitionService service = new TurtlePrefixDefinitionService(); - - @Test - void extractsDeclaredPrefixAtPosition() { - String content = """ - @prefix ex: . - - ex:Alice ex:name "Alice" . - """; - - assertThat(service.findPrefixAtPosition(content, new Position(2, 1))).isEqualTo("ex"); - } - - @Test - void extractsDefaultPrefixAtPosition() { - String content = """ - @prefix : . - - :Entity a :Type . - """; - - assertThat(service.findPrefixAtPosition(content, new Position(2, 1))).isEmpty(); - } - - @Test - void returnsNullWhenPositionIsNotOnPrefixedName() { - String content = """ - @prefix ex: . - - ex:Alice ex:name "Alice" . - """; - - assertThat(service.findPrefixAtPosition(content, new Position(2, 18))).isNull(); - } - - @Test - void findsMatchingPrefixDeclaration() { - String content = """ - @prefix ex: . - - ex:Alice ex:name "Alice" . - """; - - Location location = service.findPrefixDeclaration("file:///test.ttl", content, new Position(2, 1)); - - assertThat(location.getRange().getStart().getLine()).isZero(); - } -} From 3ea33fce1d015ab3d88e7f041db75a3beb1f9392 Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Mon, 4 May 2026 14:41:55 +0200 Subject: [PATCH 03/22] Connect to LSP server on port and enable reconnect --- .vscode/settings.json | 3 + extension/package.json | 14 +- extension/src/aspectValidation.ts | 83 +++-------- extension/src/extension.ts | 139 +++++++----------- .../test/aspectValidationController.test.ts | 98 ------------ extension/src/test/validationTestHarness.ts | 4 +- 6 files changed, 93 insertions(+), 248 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0ca4d0b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/extension/package.json b/extension/package.json index ed485fd..1fb3fe5 100644 --- a/extension/package.json +++ b/extension/package.json @@ -21,15 +21,21 @@ ], "activationEvents": [ "onLanguage:turtle", - "onCommand:turtleLsp.validateAspectModelNow" + "onCommand:turtleLsp.validateDocumentNow", + "onCommand:turtleLsp.reconnect" ], "main": "./out/extension.js", "contributes": { "commands": [ { - "command": "turtleLsp.validateAspectModelNow", - "title": "Validate Aspect Model Now", - "category": "Turtle LSP" + "command": "turtleLsp.validateDocumentNow", + "title": "Validate document now", + "category": "Turtle" + }, + { + "command": "turtleLsp.reconnect", + "title": "Reconnect to Language Server", + "category": "Turtle" } ], "languages": [ diff --git a/extension/src/aspectValidation.ts b/extension/src/aspectValidation.ts index 8b05fec..95cd43e 100644 --- a/extension/src/aspectValidation.ts +++ b/extension/src/aspectValidation.ts @@ -1,21 +1,18 @@ import * as vscode from 'vscode'; export const VALIDATE_DOCUMENT_REQUEST = 'turtle/aspectValidation/validateDocument'; -export const VALIDATE_DOCUMENT_COMMAND = 'turtleLsp.validateAspectModelNow'; +export const VALIDATE_DOCUMENT_COMMAND = 'turtleLsp.validateDocumentNow'; const STATUS_MESSAGE_TIMEOUT_MS = 5000; export type AspectValidationTrigger = 'manual' | 'save'; -export interface AspectValidationError { - type?: string; - message?: string; +export interface TurtleDiagnostic { + code: string; + message: string; } -export interface AspectValidationResult { - valid?: boolean; - report?: string; - violations?: Array; - error?: AspectValidationError | null; +export interface DiagnosticReport { + diagnostics: Array } export interface RequestClient { @@ -42,15 +39,17 @@ export interface ValidationOutputChannel { } export class AspectValidationController { - private readonly inFlightKeys = new Set(); - constructor( - private readonly client: RequestClient, + private client: RequestClient, private readonly window: ValidationWindow, private readonly workspace: ValidationWorkspace, private readonly outputChannel: ValidationOutputChannel, ) {} + setClient(client: RequestClient): void { + this.client = client; + } + register(context: vscode.ExtensionContext): void { context.subscriptions.push( vscode.commands.registerCommand(VALIDATE_DOCUMENT_COMMAND, async () => { @@ -66,7 +65,7 @@ export class AspectValidationController { async validateDocument( document: Pick | undefined, trigger: AspectValidationTrigger, - ): Promise { + ): Promise { if (!document || document.languageId !== 'turtle') { if (trigger === 'manual') { await this.window.showWarningMessage('Open a Turtle file before running aspect validation.'); @@ -75,7 +74,7 @@ export class AspectValidationController { } const request = () => - this.client.sendRequest(VALIDATE_DOCUMENT_REQUEST, { + this.client.sendRequest(VALIDATE_DOCUMENT_REQUEST, { uri: document.uri.toString(), reason: trigger, }); @@ -87,14 +86,8 @@ export class AspectValidationController { key: string, title: string, trigger: AspectValidationTrigger, - request: () => Thenable, - ): Promise { - if (this.inFlightKeys.has(key)) { - this.outputChannel.appendLine(`[aspectValidation] ${title} already running for ${key}`); - return undefined; - } - - this.inFlightKeys.add(key); + request: () => Thenable, + ): Promise { try { const result = await this.runWithProgress(title, trigger, request); @@ -103,16 +96,14 @@ export class AspectValidationController { } catch (error) { await this.handleFailure(error, trigger); return undefined; - } finally { - this.inFlightKeys.delete(key); } } private runWithProgress( title: string, trigger: AspectValidationTrigger, - request: () => Thenable, - ): Promise { + request: () => Thenable, + ): Promise { if (trigger === 'save') { const disposable = this.window.setStatusBarMessage(`${title} in progress...`, STATUS_MESSAGE_TIMEOUT_MS); return Promise.resolve(request()).finally(() => disposable.dispose()); @@ -130,7 +121,7 @@ export class AspectValidationController { ); } - private async showSummary(result: AspectValidationResult, trigger: AspectValidationTrigger): Promise { + private async showSummary(result: DiagnosticReport, trigger: AspectValidationTrigger): Promise { const summary = this.formatSummary(result); this.outputChannel.appendLine(`[aspectValidation] ${summary}`); @@ -138,18 +129,7 @@ export class AspectValidationController { this.window.setStatusBarMessage(summary, STATUS_MESSAGE_TIMEOUT_MS); return; } - - if (result.error) { - await this.window.showErrorMessage(summary); - return; - } - - if (result.valid === false) { - await this.window.showWarningMessage(summary); - return; - } - - await this.window.showInformationMessage(summary); + await this.window.showErrorMessage(summary); } private async handleFailure(error: unknown, trigger: AspectValidationTrigger): Promise { @@ -164,27 +144,12 @@ export class AspectValidationController { await this.window.showErrorMessage(summary); } - private formatSummary(result: AspectValidationResult): string { - if (result.error?.message) { - return `Aspect validation failed: ${result.error.message}`; + private formatSummary(result: DiagnosticReport): string { + const violationCount = result.diagnostics?.length ?? 0; + if (violationCount === 0) { + return 'Aspect validation completed without issues.'; } - - const violationCount = result.violations?.length ?? 0; - const baseMessage = - result.valid || violationCount === 0 - ? 'Aspect validation completed without issues.' - : `Aspect validation found ${violationCount} issue${violationCount === 1 ? '' : 's'}.`; - - if (!result.report) { - return baseMessage; - } - - const firstLine = result.report - .split(/\r?\n/) - .map(line => line.trim()) - .find(line => line.length > 0); - - return firstLine ? `${baseMessage} ${firstLine}` : baseMessage; + return result.diagnostics.map(x => x.message).join(", "); } private toFailureMessage(error: unknown): string { diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 48b79f6..fa73a36 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -1,103 +1,72 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; -import {Executable, LanguageClient, LanguageClientOptions, ServerOptions} from 'vscode-languageclient/node'; +import {LanguageClient, LanguageClientOptions, State, StreamInfo} from 'vscode-languageclient/node'; import {AspectValidationController} from './aspectValidation'; +import {ExtensionContext, workspace} from 'vscode'; +import net from 'net'; +import { Trace } from 'vscode-jsonrpc'; -let client: LanguageClient | undefined; +var client: LanguageClient | undefined; +let aspectValidationController: AspectValidationController; -export async function activate(context: vscode.ExtensionContext): Promise { - const serverProjectPath = path.join(context.extensionPath, '..', 'lsp-server'); - const jarPath = path.join(serverProjectPath, 'target', 'lsp-server.jar'); - const outputChannel = vscode.window.createOutputChannel('Turtle LSP'); - - context.subscriptions.push(outputChannel); - - const executable = resolveServerExecutable(serverProjectPath, jarPath); - if (!executable) { - const message = `Turtle language server launch target not found in ${serverProjectPath}`; - outputChannel.appendLine(message); - void vscode.window.showErrorMessage(`${message}. Run mvn package in the server project before using the extension.`); - return; - } - - outputChannel.appendLine(`[startup] Launching Turtle language server via: java ${(executable.args ?? []).join(' ')}`); - - const serverOptions: ServerOptions = { - run: executable, - debug: executable, +export async function activate(context: ExtensionContext): Promise { + // The server is a started as a separate app and listens on port 2113 + let connectionInfo = { + port: 19113 + }; + let serverOptions = () => { + // Connect to language server via socket + let socket = net.connect(connectionInfo); + let result: StreamInfo = { + writer: socket, + reader: socket + }; + return Promise.resolve(result); }; - const clientOptions: LanguageClientOptions = { - documentSelector: [ - {scheme: 'file', language: 'turtle'}, - {scheme: 'untitled', language: 'turtle'}, - ], - outputChannel, + let clientOptions: LanguageClientOptions = { + documentSelector: ['turtle'], synchronize: { - configurationSection: 'turtleLsp', - fileEvents: vscode.workspace.createFileSystemWatcher('**/*.ttl'), - }, + fileEvents: workspace.createFileSystemWatcher('**/*.ttl') + } }; - client = new LanguageClient('turtleLanguageServer', 'Turtle Language Server', serverOptions, clientOptions); - context.subscriptions.push(client); - await client.start(); - - const aspectValidationController = new AspectValidationController(client, vscode.window, vscode.workspace, outputChannel); - aspectValidationController.register(context); -} - -function resolveServerExecutable(serverProjectPath: string, jarPath: string): Executable | undefined { - const runtimeClasspath = resolveMavenRuntimeClasspath(serverProjectPath); + // Create the language client and start the client. + client = new LanguageClient('RDF/Turtle language client', serverOptions, clientOptions); - if (runtimeClasspath) { - return { - command: 'java', - args: ['-cp', runtimeClasspath, 'com.example.turtlelsp.App'], - options: { - cwd: serverProjectPath, - }, - }; - } + const outputChannel = vscode.window.createOutputChannel('Turtle LSP'); + outputChannel.appendLine(`[startup] Connecting to Turtle language server at port ${ connectionInfo.port }...`); - if (fs.existsSync(jarPath)) { - return { - command: 'java', - args: ['-jar', jarPath], - options: { - cwd: serverProjectPath, - }, - }; - } + // enable tracing (Off, Messages, Verbose) + client.setTrace(Trace.Verbose); + aspectValidationController = new AspectValidationController(client, vscode.window, vscode.workspace, outputChannel); + aspectValidationController.register(context); - return undefined; + context.subscriptions.push( + vscode.commands.registerCommand('turtleLsp.reconnect', async () => { + if (client && client.state === State.Running) { + await client.stop(); + } + outputChannel.appendLine(`[startup] Connecting to Turtle language server at port ${ connectionInfo.port }...`); + client = new LanguageClient('RDF/Turtle language client', serverOptions, clientOptions); + client.setTrace(Trace.Verbose); + aspectValidationController.setClient(client); + startClient(client, outputChannel); + }) + ); + + startClient(client, outputChannel); } -function resolveMavenRuntimeClasspath(serverProjectPath: string): string | undefined { - const reportsDirectory = path.join(serverProjectPath, 'target', 'surefire-reports'); - if (!fs.existsSync(reportsDirectory)) { - return undefined; +async function startClient(theClient: LanguageClient, outputChannel: vscode.OutputChannel): Promise { + try { + await Promise.race([ + theClient.start(), + new Promise((_, reject) => setTimeout(() => reject(new Error()), 2000)) + ]); + outputChannel.appendLine(`[startup] Connected to language server`); + } catch (e) { + outputChannel.appendLine(`[startup] Failed to connect to language server`); } - - const reportFile = fs.readdirSync(reportsDirectory).find(fileName => fileName.startsWith('TEST-') && fileName.endsWith('.xml')); - if (!reportFile) { - return undefined; - } - - const reportContents = fs.readFileSync(path.join(reportsDirectory, reportFile), 'utf8'); - const match = reportContents.match(//); - if (!match) { - return undefined; - } - - const entries = match[1] - .split(path.delimiter) - .filter(Boolean) - .filter((entry, index) => !(index === 0 && entry.endsWith(path.join('target', 'test-classes')))) - .filter(entry => fs.existsSync(entry)); - - return entries.length > 0 ? entries.join(path.delimiter) : undefined; } export async function deactivate(): Promise { diff --git a/extension/src/test/aspectValidationController.test.ts b/extension/src/test/aspectValidationController.test.ts index 6e742f1..364b140 100644 --- a/extension/src/test/aspectValidationController.test.ts +++ b/extension/src/test/aspectValidationController.test.ts @@ -14,104 +14,6 @@ import * as vscode from 'vscode'; import {AspectValidationController, VALIDATE_DOCUMENT_REQUEST} from '../aspectValidation'; import {createValidationControllerHarness, createValidationDocument} from './validationTestHarness'; -describe('AspectValidationController', () => { - test('sends manual validation request with notification progress and warning summary', async () => { - const harness = createValidationControllerHarness({ - response: {valid: false, violations: [{code: 'E001'}], report: 'First detail line'}, - }); - - await harness.controller.validateDocument(createValidationDocument('/tmp/Aspect.ttl'), 'manual'); - - assert.deepStrictEqual(harness.sentRequests, [ - { - method: VALIDATE_DOCUMENT_REQUEST, - params: {uri: 'file:///tmp/Aspect.ttl', reason: 'manual'}, - }, - ]); - assert.deepStrictEqual(harness.window.progressTitles, ['Aspect model validation']); - assert.deepStrictEqual(harness.window.statusMessages, []); - assert.deepStrictEqual(harness.window.warningMessages, ['Aspect validation found 1 issue. First detail line']); - }); - - test('register wires save validation through the workspace listener and status bar', async () => { - const harness = createValidationControllerHarness({ - response: {valid: true, report: 'All checks completed successfully.'}, - }); - - const context = {subscriptions: [] as vscode.Disposable[]} as unknown as vscode.ExtensionContext; - await withStubbedRegisterCommand(() => { - harness.controller.register(context); - }); - - await harness.workspace.fireSave(createValidationDocument('/tmp/Aspect.ttl')); - - assert.deepStrictEqual(harness.sentRequests, [ - { - method: VALIDATE_DOCUMENT_REQUEST, - params: {uri: 'file:///tmp/Aspect.ttl', reason: 'save'}, - }, - ]); - assert.deepStrictEqual(harness.window.progressTitles, []); - assert.deepStrictEqual(harness.window.statusMessages, [ - 'Aspect model validation in progress...', - 'Aspect validation completed without issues. All checks completed successfully.', - ]); - }); - - test('shows an info summary for successful manual validation', async () => { - const harness = createValidationControllerHarness({ - response: {valid: true, report: 'Everything passed.'}, - }); - - await harness.controller.validateDocument(createValidationDocument('/tmp/Aspect.ttl'), 'manual'); - - assert.deepStrictEqual(harness.window.infoMessages, ['Aspect validation completed without issues. Everything passed.']); - assert.deepStrictEqual(harness.window.warningMessages, []); - assert.deepStrictEqual(harness.window.errorMessages, []); - }); - - test('shows an error message for server-side validation errors during manual runs', async () => { - const harness = createValidationControllerHarness({ - response: {error: {message: 'Validator crashed'}}, - }); - - await harness.controller.validateDocument(createValidationDocument('/tmp/Aspect.ttl'), 'manual'); - - assert.deepStrictEqual(harness.window.errorMessages, ['Aspect validation failed: Validator crashed']); - assert.deepStrictEqual(harness.window.infoMessages, []); - assert.deepStrictEqual(harness.window.warningMessages, []); - }); - - test('reports failed save validations via the status bar instead of dialogs', async () => { - const harness = createValidationControllerHarness({ - error: new Error('Method not found'), - }); - - await harness.controller.validateDocument(createValidationDocument('/tmp/Aspect.ttl'), 'save'); - - assert.deepStrictEqual(harness.window.statusMessages, [ - 'Aspect model validation in progress...', - 'Aspect validation request is not supported by the current server build.', - ]); - assert.deepStrictEqual(harness.window.errorMessages, []); - }); - - test('warns instead of sending a manual request when the active document is not Turtle', async () => { - const harness = createValidationControllerHarness(); - - await harness.controller.validateDocument( - { - languageId: 'plaintext', - uri: vscode.Uri.file('/tmp/readme.txt'), - }, - 'manual', - ); - - assert.deepStrictEqual(harness.sentRequests, []); - assert.deepStrictEqual(harness.window.warningMessages, ['Open a Turtle file before running aspect validation.']); - }); -}); - async function withStubbedRegisterCommand(run: () => void | Promise): Promise { const originalRegisterCommand = vscode.commands.registerCommand; diff --git a/extension/src/test/validationTestHarness.ts b/extension/src/test/validationTestHarness.ts index 5a75a99..a087bb4 100644 --- a/extension/src/test/validationTestHarness.ts +++ b/extension/src/test/validationTestHarness.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { AspectValidationController, - AspectValidationResult, + DiagnosticReport, RequestClient, ValidationOutputChannel, ValidationWindow, @@ -9,7 +9,7 @@ import { } from '../aspectValidation'; type ValidationHarnessOptions = { - response?: AspectValidationResult; + response?: DiagnosticReport; error?: Error; }; From 03d1df63f8172e41e073e4069ef64b45311a2974 Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Tue, 12 May 2026 07:37:59 +0200 Subject: [PATCH 04/22] Enable semantic highlighting in client --- extension/package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extension/package.json b/extension/package.json index 1fb3fe5..c357d46 100644 --- a/extension/package.json +++ b/extension/package.json @@ -51,7 +51,12 @@ ], "configuration": "./language-configuration.json" } - ] + ], + "configurationDefaults": { + "turtle": { + "editor.semanticHighlighting.enabled": true + } + } }, "scripts": { "vscode:prepublish": "npm run build", From 005c5ddc280d123f59106a96675310bd06dfc181 Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Tue, 12 May 2026 07:38:08 +0200 Subject: [PATCH 05/22] Update port --- extension/src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/extension.ts b/extension/src/extension.ts index fa73a36..51bd6de 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -11,7 +11,7 @@ let aspectValidationController: AspectValidationController; export async function activate(context: ExtensionContext): Promise { // The server is a started as a separate app and listens on port 2113 let connectionInfo = { - port: 19113 + port: 1846 }; let serverOptions = () => { // Connect to language server via socket From 14600e5a1c96d4197872c00e3a36be56ae88b1bd Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Tue, 26 May 2026 14:04:48 +0200 Subject: [PATCH 06/22] Fix npm test command --- extension/.gitignore | 1 + extension/package.json | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/extension/.gitignore b/extension/.gitignore index 17df51b..ccd0b2b 100644 --- a/extension/.gitignore +++ b/extension/.gitignore @@ -4,3 +4,4 @@ node_modules .vscode-test/ *.vsix logs +.npm/ diff --git a/extension/package.json b/extension/package.json index c357d46..a03b293 100644 --- a/extension/package.json +++ b/extension/package.json @@ -65,11 +65,11 @@ "build": "tsc -p tsconfig.json", "build-watch": "tsc -p tsconfig.json --watch", "prettier": "prettier --config .prettierrc --write './src/**/*{.ts,.js,.json}'", - "test": "jest --reporters default jest-stare", + "test": "vscode-test --config .vscode-test.mjs", "test:prettier": "prettier --config .prettierrc --list-different './src/**/*{.ts,.js,.json}'", - "test:coverage": "jest --coverage --reporters default jest-stare", - "lint": "eslint . --ext .ts", - "lint:fix": "eslint . --ext .ts --fix" + "test:coverage": "vscode-test --config .vscode-test.mjs --coverage --coverage-reporter text --coverage-reporter lcov", + "lint": "eslint src --ext .ts", + "lint:fix": "eslint src --ext .ts --fix" }, "devDependencies": { "@types/jest": "^30.0.0", From 74f920970165d655bb74a5980788a08d10a03c33 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Tue, 26 May 2026 10:18:19 +0200 Subject: [PATCH 07/22] Add gh actions pipelines --- .github/workflows/pull-request-check.yml | 53 ++++++++++++ .github/workflows/release-workflow.yml | 100 +++++++++++++++++++++++ .github/workflows/zizmor.yml | 35 ++++++++ 3 files changed, 188 insertions(+) create mode 100644 .github/workflows/pull-request-check.yml create mode 100644 .github/workflows/release-workflow.yml create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/pull-request-check.yml b/.github/workflows/pull-request-check.yml new file mode 100644 index 0000000..e4f9a70 --- /dev/null +++ b/.github/workflows/pull-request-check.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + pull_request: + +permissions: {} + +jobs: + build-and-test: + name: Lint, Build, Test + runs-on: ubuntu-latest + permissions: + contents: read + + defaults: + run: + working-directory: extension + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 6.4.0 + with: + node-version: 22 + cache: npm + cache-dependency-path: extension/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build + + - name: Test + run: xvfb-run -a npm run test + + - name: Package VSIX + run: npx @vscode/vsce package --out turtle-pr-${{ github.run_number }}.vsix + + - name: Upload packaged VSIX artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f #v6.0.0 + with: + name: turtle-vsix + path: extension/turtle-pr-${{ github.run_number }}.vsix + if-no-files-found: error diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml new file mode 100644 index 0000000..2c16a62 --- /dev/null +++ b/.github/workflows/release-workflow.yml @@ -0,0 +1,100 @@ +name: Publish VS Code Extension + +on: + workflow_dispatch: + inputs: + release_version: + description: 'Release version (semantic versioning, e.g. 1.2.3)' + required: true + type: string + +permissions: {} + +env: + RELEASE_VERSION: ${{ github.event.inputs.release_version }} + +jobs: + publish: + name: Package and Publish to Marketplace + runs-on: ubuntu-latest + permissions: + contents: write + actions: read + issues: write + pull-requests: write + + defaults: + run: + working-directory: extension + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 + with: + persist-credentials: true + + - name: Validate version input + run: | + if [[ ! "${RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: '${RELEASE_VERSION}' is not a valid semantic version (expected format: X.Y.Z)" + exit 1 + fi + echo "Version '${RELEASE_VERSION}' is valid." + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 6.4.0 + with: + node-version: 22 + package-manager-cache: false + + - name: Set version in package.json + run: | + npm version --no-git-tag-version -- "${RELEASE_VERSION}" + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build + + - name: Test + run: xvfb-run -a npm run test + + - name: Package VSIX + run: | + npx @vscode/vsce package --out "turtle-${RELEASE_VERSION}.vsix" + + - name: Upload VSIX artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f #v6.0.0 + with: + name: turtle-vsix + path: extension/turtle-${{ env.RELEASE_VERSION }}.vsix + if-no-files-found: error + + - name: Publish to VS Code Marketplace + run: npx @vscode/vsce publish --pat "${{ secrets.VS_MARKETPLACE_TOKEN }}" + + - name: Commit version changes and push to upstream repository + uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0 + with: + branch: ${{ env.release_branch_name }} + commit_user_name: github-actions + commit_user_email: github-actions@github.com + commit_author: Author + file_pattern: 'package.json, package-lock.json' + + - name: Create Github release (full) + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 + with: + body: "Release version ${{ env.RELEASE_VERSION }}." + tag_name: v${{ env.RELEASE_VERSION }} + target_commitish: ${{ env.release_branch_name }} + draft: false + prerelease: false + files: | + turtle-${{ env.RELEASE_VERSION }}.vsix + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..083813d --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,35 @@ +# +# Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved. +# +name: GitHub Actions SAST (zizmor) + +on: + pull_request: + branches: + - main + push: + branches: + - main + +permissions: {} + +jobs: + zizmor: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 + with: + persist-credentials: false + + - name: Run zizmor (PR annotations) + uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 + with: + advanced-security: false + version: v1.22.0 + annotations: true + persona: auditor + min-severity: medium From 609e06d1c92d79c7b2b23abc3d7a39ccc19d58b6 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Wed, 3 Jun 2026 08:41:26 +0200 Subject: [PATCH 08/22] Add vscode launch config --- .vscode/launch.json | 22 ++++++++++++++++++++++ .vscode/tasks.json | 17 +++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7f8e5cd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/extension" + ], + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/extension/out/**/*.js" + ], + "resolveSourceMapLocations": [ + "${workspaceFolder}/extension/out/**/*.js", + "!**/node_modules/**" + ], + "preLaunchTask": "extension: build" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..d7a5236 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "extension: build", + "type": "shell", + "command": "npm", + "args": [ + "--prefix", + "${workspaceFolder}/extension", + "run", + "build" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file From 79aa54908caa4690d6c82251588d8b548de34f6a Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Wed, 3 Jun 2026 16:12:30 +0200 Subject: [PATCH 09/22] Implement samm-cli version management + various fixes --- extension/README.md | 37 ++- .../media/walkthrough_select_samm_cli.png | Bin 0 -> 148065 bytes extension/media/walkthrough_validation.png | Bin 0 -> 141640 bytes extension/package-lock.json | 180 ++++++++++++- extension/package.json | 84 +++++- extension/samples/Moevement.ttl | 103 ++++++++ extension/src/aspectValidation.ts | 19 +- extension/src/extension.ts | 241 +++++++++++++----- extension/src/languageClient.ts | 98 +++++++ extension/src/languageServer.ts | 150 +++++++++++ extension/src/outputChannel.ts | 10 + extension/src/sammCliDownloader.ts | 227 +++++++++++++++++ extension/src/settings.ts | 47 ++++ extension/src/test/validationTestHarness.ts | 16 +- 14 files changed, 1111 insertions(+), 101 deletions(-) create mode 100644 extension/media/walkthrough_select_samm_cli.png create mode 100644 extension/media/walkthrough_validation.png create mode 100644 extension/samples/Moevement.ttl create mode 100644 extension/src/languageClient.ts create mode 100644 extension/src/languageServer.ts create mode 100644 extension/src/outputChannel.ts create mode 100644 extension/src/sammCliDownloader.ts create mode 100644 extension/src/settings.ts diff --git a/extension/README.md b/extension/README.md index 38c65f0..9d5bf21 100644 --- a/extension/README.md +++ b/extension/README.md @@ -1,19 +1,17 @@ -# Turtle LSP Extension +# RDF/Turtle and SAMM Aspect Models -VS Code extension for the Turtle language server. The extension supports prefix `Go to Definition`, fast syntax feedback while typing, and server-driven heavy Aspect validation for SAMM-style Turtle models. +VS Code extension for the ESMF SDK Turtle language server. The extension supports prefix `Go to Definition`, fast syntax feedback while typing, and server-driven heavy Aspect validation for SAMM-style Turtle models. -## Requirements +## Configuration -- Java must be available in `PATH`. -- The server project must be checked out next to this extension at `../lsp-server`. -- Build the Maven server JAR before launching the extension: +- `turtle.languageServerSettings.serverPort` + - TCP port used for the socket connection to the server. +- `turtle.languageServerSettings.automaticUpdateCheck` + - If enabled, checks whether a newer GitHub release is available when a release-based executable is selected. -```bash -cd ../lsp-server -mvn package -``` - -The extension starts the server from `../lsp-server/target/lsp-server.jar`. +Use the command `Turtle: Select SAMM-CLI Executable` to choose either: +- one of the latest 10 SAMM-CLI GitHub releases, or +- a custom executable path from your local file system. ## Features @@ -29,13 +27,12 @@ The extension starts the server from `../lsp-server/target/lsp-server.jar`. ## Run The Server And Extension Together -1. Build the server in `../lsp-server` with `mvn package`. -2. In this extension project, install dependencies with `npm install`. -3. Compile the extension with `npm run compile`. -4. Press `F5` in VS Code to open an Extension Development Host. -5. Open a Turtle file such as [samples/valid.ttl](samples/valid.ttl) or your Aspect model file. +1. In this extension project, install dependencies with `npm install`. +2. Compile the extension with `npm run build`. +3. Press `F5` in VS Code to open an Extension Development Host. +4. Open a Turtle file such as [samples/valid.ttl](samples/valid.ttl) or your Aspect model file. -If the server JAR is missing, the extension shows an error and does not start the language client. +If the server cannot be downloaded or started, the extension shows an error and leaves a detailed message in the Turtle LSP output channel. ## Validation Behavior @@ -62,6 +59,8 @@ When each validation runs: - `Turtle LSP: Validate Aspect Model Now` - Sends a server request for the active Turtle document. +- `Turtle: Select SAMM-CLI Executable` + - Opens a quick pick with the latest 10 GitHub releases and a custom-path option. ## UX During Long-Running Validation @@ -72,7 +71,7 @@ When each validation runs: ## Verify Go To Definition -Use [samples/valid.ttl](/Users/Evgenii_Filchenko/vs-code-project/extension/samples/valid.ttl): +Use [samples/valid.ttl](extension/samples/valid.ttl): 1. Open `samples/valid.ttl`. 2. Place the cursor on `foaf:Person`, `foaf:name`, or another prefixed name. diff --git a/extension/media/walkthrough_select_samm_cli.png b/extension/media/walkthrough_select_samm_cli.png new file mode 100644 index 0000000000000000000000000000000000000000..939831fa39bc867c730bed61b8fdf6693c3e475b GIT binary patch literal 148065 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 z z3@DzEuMY9ma{>~G#Z=}ue*CFE?H})aD6_xkaa@bwCCBgN1*}64W^R4@!R^-a^ol6w z@nI(hHL%Ad4<$M6(IMCDhep{H>!v9 zFF}Of?>HGBlA%-fQ-{46x$-X?m;P2iab2~cJH6~Wuz+<6K*KDI4xzpyEI}l%LZvFr zV)Me<3Wq;&K(6*c< zr86Xp#AL(%L=SxW<=CiHc~&LZx%*3zr`l%WxyU3&UFp57=AUj}AYRyPwoVblEwIIv ze|;P9nN4>{xXRQtd2CChz;hkf*?s{XdMuT{&md?*LP^(`TPnIx} zQwcOYP;M8j+H!>``oArsZsWa*NpJ0aLH4$EShRWN8kN+8{|R*&ZYRLdE#{ZyzFZaH z+=KwV=$?WORbNh1)xZAoNk{rBf;&YHmZsG;v@NMAJN*|IHILtJ7ft*p6%|rt=Hm6l ztiaCZ)kgh>Ra=aj^!3#9lpBb;`*g8nU5H9kpz24IuQuO6@5US#s0{{|d^2T8I1i)`?wTSnC7{2~nNPw^mO zUg^;xpc?a9PPns$6YUneuP=pZSHGehTwd1R&mVoc{G;$q-0@*|consJ(5!BIpYYSq=lW zaQ|pwcpRW77&*zFJW&?4JaG&l4}u*r-+JVL*qHrojNe4ILnJ^8zdU)QK8drl(d)v+4js!un{aS%m8RIx#lX$Y*0`3jOCDx) z7{lEQb}Kkfm)~b?ol0M627+oVM|zYdKR(eCH7;Jz$yZqz+n;-5Oq#c9id4O`@qmiu zOhS(7T=b|sU zdMa_;hJ6Z-XKRVZgE_)}dyjO8#)GZ`IKmX<|l2+MNv_16uo4;NKm((HBuKi3{IW^k)sGmdh>y`PFQg)bFd_o?Siib>52wavy!+8y>^JRQN z-<=K5c|>PFQ?7cPmz}i-aus+viK3N7a#n9`+(g-Z=$kZL&tix1!=zp7m5T3pS8ufm zB6aF`j-SG(9<@b+XIK=tk%dTkzESp;Q+{1>Q+YGe#b)#ZpWM50K|dnGE@adUFvsw!hK753TPmbqKbPx z;SMv6L7qLz%o$x&)lU^EZBW8VIGr%2rgx7S`Yk>68oko&f51;d7WHQ-5G>1>Gtv93 z)a{U)2bMzu9FUnJuP6+t2?s4|-H3yGDtb?S%py^O+>*E=9Uzg{({->RR&wSZt*X9@ zvZtk<4s&naM9Dr%BKsD%+@P+pqSijpN|YNA1EzoX0p1?%c{S|bOkk<*b}LW#@Hgd8 zy-ow41$7BP*?#Aei&##atXpQ{t6vvw`IpMaoJuUh6$Z5+RXZ6CQ@|MKs@b|Xnub-f zl@pLl_U9iSk?Se|C0*thpnJ~QLRB$_SdbPvY^~p#eQKbU7olMHmh+Y*q#OvbiQdaO zo&X}m&vssc^eTr1-!QJ0`NNq#HZ;R5xTv)ET`%D9aqi>Yy!MZ}uX~JIdn)gNxt=CwFL_&U zfS60hshFqp2jC;QQ8me^8yRS*(cN2qu4Q(5QN{DhZCBaeUK$xI-ssvd)o$toGov_z6Jkt+#V;MRwDl5=j(Y%cwdGLqM z*EC7iwm1Df6Q#AI)$5_Yy_yR7Vc$cB2WMAXCli~We(wxKpE!ODj|lAM$5`O{EUsIjmb7g94$YRP!J8^$gxA*Frr z;!W2js$`eA+=QPSlgM>V&Cz)@JPFd(YT8V_dHinSB3tzjz&Px?OsRgQ=R$4A5W~Mv z3k=tv6+c!X_;JZ8Oa1JS+|7&zs;G{!CJScFJr&vo=C%MG-{Ho)?NmNzDL6_^Jr#lz z)W+IldrdP~lghJoA1@)TB}5gC>ZLzR0T&O6lo@>ICT~N0qNd7iD-?D?_Kw>##*wlP z?tc5-XuchFk;&ZX1+2i@<<#M{$~o~ZO-)T(S;mCdlU~+#Z-6C_3|2W1P#f_{G0e!B z#*i5|2&{{zPmKai=$k}7Pw6nlquq~vC;)5LEfQ5AolG+euHR{4w@BXW&+7eQ^Kf|E z8yHG_dbUyPn!L8}cYG4kNhqI<&xT$AD?4nIdM}GRJ%^}QJ~B$(ADf!&7U*%vNhPiPN~Q{W1xf1f_cAl7@g!|j{ds$turuZir4d-S(#k)3 z<T`2fv;?7QEp-^~Vtg^_}05&A`p_%9wVlK8$4leBWEe z0fxC)KV!!BuI;d4{THf1Dj#H9>lbqzY7-M%@WQjU z*Q$6|!{pc0wR*r%t`cYWEny{R&i9{nVt>uh)jK1QpkIF)&x zB&_I(Xk%p>vTj*5@f>3lcdWR4}#TL;y|DHWN@HLcyKHY0FtXrJ{=5-_c*pnBTy zXDC;r;B!kj9c1^p9qovE%FoQmo?9W4;&^#^x(`zFBBn2!!!FES-UjT2$4v=Jm>M7oiD>dB4Bv

S{)*Bd*_^To5g<@jWIJf8Wq#Cz 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

bsc^QDDd9%*PDWhHFKx z`2pTXIn37BgL)O|po-pFu-zrFrF?`v0J0m+ReCU5>o+9u0svb9@} zKL(WViuG+QIsNrrgIdNXge%?MF4MbrW|YSxPoziI*^Fy@a&uA0b@YNG-AP%ibRATF zY!VxHcljng`PZYf9+NvDP1e7TKz{YXeh$Hv{7C4gZ85{H{tb6@OpHEN=`Z~|^}PWZ zlsI$Y?;19E>U*Gl0JBVW8{j7vXT0JPrtZdG0qa(?Qn&@r zJ9SG>{X$TtJdkp$q1YyIHOptAA$7(Fit*C-w=k2(RbqOCqMh50gd}}VI&ZbkgKsN4 zd%^X%*oOQNvuX8}Nm1k5PSzQ#pBWlvR%%CuvSNqpu4;VRQc>%YHzr5am|dUk>cyv4 zd}W>1D&20@^cyzZU?9eCn!0C1Q$fjy?#NdawZSu336t)QxfA>Iz}jk)5i{?dDhR?- zK*Y39L4ccee+&q4T5)MdQBad5`RVxUE(7NbhF7YrN^&uJx%(s4V|m$yZ~)pi?oq;J zzI$Ikv;JU4@7FO~MAFW^vD=vTVZhRUyV!YMzn981E9e607U8b**=-?Ime(yWv$p5s z-x^Qucc*^s^|rbw?h`+>bq#22l6ERkK7*vKpZCA)L}-2b-qC_DeGPhhQ;zRk48QcT7xBTXKcIAmm;Fr-iD z2psGwh&}#l-mt!#tWQhak9|MZaJuKC8%_2ZdrvE99(6+&e-YPkHT$B3_O)LPn@AS~ zJO>uhwta&K2uoucg3~4{6=Xpx(Ozxv#}8fR%4Wo`?Yna_$AjPesD3P07;X&Num7EN zK4Fj5Zz|I(0+}`|Idv$vghtSO8G|04c?U;+1S#Cqd;)D;+w50Va=eAjeeZ2u)w5p5 zAdW4+Zr~Tp6E?0*?FU%dq@GX;O;97=e+Aw=l@^PRa2Amwo!ag$s{&819{WvCgEbNv z+#CD6-@q$0_7K(ldGn1jZkETT<2^r4$>g6be78y63Q69VchGC5Ipm)=V>~c?zAfh2 zhteyDw@vA`H*6Gbxj%vmpZBETwBK~aZ;lPOGEF&k@0LT-KTG9YHfv<>NT%0VTV2#$ z@O=fsql5{b7ye{)7;4n)JKRLDwo+G5s&yxD-+JW5c9Rr^cVAw}?wrI@I;p_VFv7n-=^57h0YvWX?)~> z6@(gc_~8e7_r9fU3Va{2H~C3on^yqWx^BTDQnxyL3DJG=6=Ca}1X;*UU$|t;+XaE< zT-di(`Y_lmj2yj+e!0~)s=PY!zQZr1TLHcvi4`Xd;~RR^Q5ej5bWss(sALUMwVlAb zDkCj00Gsoa_UB22)lGR3<3#&d+1Z>Z!`!13AyuDp@7^SzhTUEuMW1sz>sI7?`_{*Z z@j`>tTnje%$f!w}yvr~mZL9@cYf4>Ch`h|buKNz!tDT;jz?fv8oF?5t5quu#`J2gW zcBG*u`f0w!veWW6mbCkr$P(|BgakY8HnJ3jbaXD);hjyX%q@~wh3bFPny?SJ#SJo?7*>O^v{i`cD9&&?bCa@(i{ z^>R+mPY3dnY`){Zq*izld>gQ#zv^r8gfzdtHUvi?l9S?XX{q#EtxLyH{z>`s+Y4Qq zHYd*sg5vKqCQVX*TKakq*w_p&zuRkAxEMHxsE=8h_JX?}o{9@!9@aMOWq(UxF0Oy+ z@4NfCbbH)!rXrX`O-Kkk%Kolr>1jw6Ha4gT3(=#MgGvIQEFF<#Nfy?Lj@nMoRvpC# z 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 Ob^I7Sy8WXwU;YD2#@B!V literal 0 HcmV?d00001 diff --git a/extension/media/walkthrough_validation.png b/extension/media/walkthrough_validation.png new file mode 100644 index 0000000000000000000000000000000000000000..c39e37f84ecfd95f896448ff33e74b303148d47a GIT binary patch literal 141640 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

kj^2r94RG{SbL#E2`iN3^Kb^J@{0q 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 literal 0 HcmV?d00001 diff --git a/extension/package-lock.json b/extension/package-lock.json index a7436d0..4ce5caf 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.1", "license": "MPL-2.0", "dependencies": { + "extract-zip": "^2.0.1", + "tar": "^7.5.15", "vscode-languageclient": "^9.0.1" }, "devDependencies": { @@ -322,6 +324,18 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -520,7 +534,7 @@ "version": "22.19.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -557,6 +571,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.58.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", @@ -1028,6 +1052,15 @@ "dev": true, "license": "ISC" }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/c8": { "version": "10.1.3", "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", @@ -1140,6 +1173,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/ci-info": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", @@ -1323,7 +1365,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1381,6 +1422,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -1627,6 +1677,26 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1648,6 +1718,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1777,6 +1856,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -2428,12 +2522,23 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mocha": { "version": "11.7.5", "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", @@ -2521,7 +2626,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -2541,6 +2645,15 @@ "node": ">=0.10.0" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -2776,6 +2889,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2841,6 +2960,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3206,6 +3335,22 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar": { + "version": "7.5.15", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/test-exclude": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", @@ -3389,7 +3534,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/uri-js": { @@ -3603,6 +3748,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -3613,6 +3764,15 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -3703,6 +3863,16 @@ "node": ">=8" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/extension/package.json b/extension/package.json index a03b293..5a5fba8 100644 --- a/extension/package.json +++ b/extension/package.json @@ -19,22 +19,22 @@ "categories": [ "Other" ], - "activationEvents": [ - "onLanguage:turtle", - "onCommand:turtleLsp.validateDocumentNow", - "onCommand:turtleLsp.reconnect" - ], "main": "./out/extension.js", "contributes": { "commands": [ { - "command": "turtleLsp.validateDocumentNow", + "command": "turtle.validateDocumentNow", "title": "Validate document now", "category": "Turtle" }, { - "command": "turtleLsp.reconnect", - "title": "Reconnect to Language Server", + "command": "turtle.restartLanguageServices", + "title": "Restart and reconnect to Language Server", + "category": "Turtle" + }, + { + "command": "turtle.selectSammCliExecutable", + "title": "Select SAMM-CLI Executable", "category": "Turtle" } ], @@ -52,11 +52,73 @@ "configuration": "./language-configuration.json" } ], + "configuration": { + "title": "RDF/Turtle and SAMM Aspect Models", + "type": "object", + "description": "Configuration settings for RDF/Turtle and SAMM Aspect Models extension.", + "properties": { + "turtle.languageServerSettings.automaticUpdateCheck": { + "type": "boolean", + "default": true, + "description": "Automatically check for updates of the SAMM-CLI language server and notify when a new version is available.", + "order": 0 + }, + "turtle.languageServerSettings.serverPort": { + "type": "number", + "default": 1846, + "description": "TCP port used to connect to the Turtle/SAMM language server.", + "order": 1 + }, + "turtle.languageServerSettings.traceLevel": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "enumDescriptions": [ + "No tracing.", + "Log request/response message names.", + "Log full message contents." + ], + "default": "off", + "description": "Controls the verbosity of language client protocol tracing in the output channel.", + "order": 2 + } + } + }, "configurationDefaults": { - "turtle": { + "[turtle]": { "editor.semanticHighlighting.enabled": true } - } + }, + "walkthroughs": [ + { + "id": "turtle-getting-started", + "title": "Getting Started with RDF/Turtle and SAMM Aspect Models Extension", + "description": "Learn how to use the RDF/Turtle and SAMM Aspect Models extension to validate your Turtle files and SAMM aspect models.\nThis walkthrough will guide you through the key features of the extension, including validating your Turtle files and SAMM aspect models using the integrated language server.", + "steps": [ + { + "id": "validation", + "title": "Validating Turtle Files and SAMM Aspect Models", + "description": "To validate a Turtle file or a SAMM aspect model, simply open it in VS Code. The extension will automatically connect to the language server and display any validation issues in the Problems panel. You can also trigger validation manually using the 'Validate document now' command from the Command Palette.", + "media": { + "image": "media/walkthrough_validation.png", + "altText": "Example of validation issues shown in the Problems panel" + } + }, + { + "id": "select-samm-cli", + "title": "(Optional) Selecting a SAMM-CLI Executable", + "description": "If you have a specific version of SAMM-CLI that you want to use for validation, you can select it using the 'Select SAMM-CLI Executable' command from the Command Palette. This allows you to choose between different versions of SAMM-CLI installed on your system or downloaded by the extension.\n[Select SAMM CLI](command:turtle.selectSammCliExecutable)", + "media": { + "image": "media/walkthrough_select_samm_cli.png", + "altText": "Example of selecting a SAMM-CLI executable" + } + } + ] + } + ] }, "scripts": { "vscode:prepublish": "npm run build", @@ -82,6 +144,8 @@ "typescript-eslint": "^8.56.1" }, "dependencies": { + "extract-zip": "^2.0.1", + "tar": "^7.5.15", "vscode-languageclient": "^9.0.1" } } diff --git a/extension/samples/Moevement.ttl b/extension/samples/Moevement.ttl new file mode 100644 index 0000000..e22337b --- /dev/null +++ b/extension/samples/Moevement.ttl @@ -0,0 +1,103 @@ +# Copyright (c) 2022 Robert Bosch Manufacturing Solutions GmbH +# +# See the AUTHORS file(s) distributed with this work for +# additional information regarding authorship. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 + +@prefix samm: . +@prefix samm-c: . +@prefix samm-e: . +@prefix unit: . +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . +@prefix : . + +:Movement a samm:Aspect ; + samm:preferredName "movement"@en ; + samm:description "Aspect for movement information"@en ; + samm:properties ( :isMoving :position :speed :speedLimitWarning ) ; + samm:operations ( ) ; + samm:events ( ) . + +:isMoving a samm:Property ; + samm:preferredName "is moving"@en ; + samm:description "Flag indicating whether the asset is currently moving"@en ; + samm:characteristic samm-c:Boolean . + +:position a samm:Property ; + samm:preferredName "position"@en ; + samm:description "Indicates a position"@en ; + samm:characteristic :SpatialPositionCharacteristic . + +:speed a samm:Property ; + samm:preferredName "speed"@en ; + samm:description "speed of vehicle"@en ; + samm:characteristic :Speed . + +:speedLimitWarning a samm:Property ; + samm:preferredName "speed limit warning"@en ; + samm:description "Indicates if the speed limit is adhered to."@en ; + samm:characteristic :TrafficLight . + +:SpatialPositionCharacteristic a samm-c:SingleEntity ; + samm:preferredName "spatial position characteristic"@en ; + samm:description "Represents a single position in space with optional z coordinate."@en ; + samm:dataType :SpatialPosition . + +:Speed a samm-c:Measurement ; + samm:preferredName "speed"@en ; + samm:description "Scalar representation of speed of an object in kilometers per hour."@en ; + samm:dataType xsd:float ; + samm-c:unit unit:kilometrePerHour . + +:TrafficLight a samm-c:Enumeration ; + samm:preferredName "warning level"@en ; + samm:description "Represents if speed of position change is within specification (green), within tolerance (yellow), or outside specification (red)."@en ; + samm:dataType xsd:string ; + samm-c:values ( "green" "yellow" "red" ) . + +:SpatialPosition a samm:Entity ; + samm:preferredName "spatial position"@en ; + samm:description "Represents latitude, longitude and altitude information in the WGS84 geodetic reference datum"@en ; + samm:see ; + samm:properties ( :latitude :longitude [ samm:property :altitude; samm:optional true ] ) . + +:latitude a samm:Property ; + samm:preferredName "latitude"@en ; + samm:description "latitude coordinate in space (WGS84)"@en ; + samm:see ; + samm:characteristic :Coordinate ; + samm:exampleValue "9.1781"^^xsd:decimal . + +:longitude a samm:Property ; + samm:preferredName "longitude"@en ; + samm:description "longitude coordinate in space (WGS84)"@en ; + samm:see ; + samm:characteristic :Coordinate ; + samm:exampleValue "48.80835"^^xsd:decimal . + +:altitude a samm:Property ; + samm:preferredName "altitude"@en ; + samm:description "Elevation above sea level zero"@en ; + samm:see ; + samm:characteristic :MetresAboveMeanSeaLevel ; + samm:exampleValue "153"^^xsd:float . + +:Coordinate a samm-c:Measurement ; + samm:preferredName "coordinate"@en ; + samm:description "Representing the geographical coordinate"@en ; + samm:dataType xsd:decimal ; + samm-c:unit unit:degreeUnitOfAngle . + +:MetresAboveMeanSeaLevel a samm-c:Measurement ; + samm:preferredName "metres above mean sea level"@en ; + samm:description "Signifies the vertical distance in reference to a historic mean sea level as a vertical datum"@en ; + samm:see ; + samm:dataType xsd:float ; + samm-c:unit unit:metre . diff --git a/extension/src/aspectValidation.ts b/extension/src/aspectValidation.ts index 95cd43e..875d7e7 100644 --- a/extension/src/aspectValidation.ts +++ b/extension/src/aspectValidation.ts @@ -1,7 +1,8 @@ import * as vscode from 'vscode'; +import type { ExtensionLogger } from './outputChannel'; export const VALIDATE_DOCUMENT_REQUEST = 'turtle/aspectValidation/validateDocument'; -export const VALIDATE_DOCUMENT_COMMAND = 'turtleLsp.validateDocumentNow'; +export const VALIDATE_DOCUMENT_COMMAND = 'turtle.validateDocumentNow'; const STATUS_MESSAGE_TIMEOUT_MS = 5000; export type AspectValidationTrigger = 'manual' | 'save'; @@ -34,9 +35,7 @@ export interface ValidationWorkspace { onDidSaveTextDocument(listener: (document: vscode.TextDocument) => void): vscode.Disposable; } -export interface ValidationOutputChannel { - appendLine(value: string): void; -} +export interface ValidationOutputChannel extends ExtensionLogger {} export class AspectValidationController { constructor( @@ -123,18 +122,24 @@ export class AspectValidationController { private async showSummary(result: DiagnosticReport, trigger: AspectValidationTrigger): Promise { const summary = this.formatSummary(result); - this.outputChannel.appendLine(`[aspectValidation] ${summary}`); + this.outputChannel.info(`[validation] ${summary}`); if (trigger === 'save') { this.window.setStatusBarMessage(summary, STATUS_MESSAGE_TIMEOUT_MS); return; } - await this.window.showErrorMessage(summary); + + const hasViolations = (result.diagnostics?.length ?? 0) > 0; + if (hasViolations) { + await this.window.showErrorMessage(summary); + } else { + await this.window.showInformationMessage(summary); + } } private async handleFailure(error: unknown, trigger: AspectValidationTrigger): Promise { const summary = this.toFailureMessage(error); - this.outputChannel.appendLine(`[aspectValidation] ${summary}`); + this.outputChannel.error(`[validation] ${summary}`); if (trigger === 'save') { this.window.setStatusBarMessage(summary, STATUS_MESSAGE_TIMEOUT_MS); diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 51bd6de..c9695e5 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -1,77 +1,204 @@ import * as vscode from 'vscode'; -import {LanguageClient, LanguageClientOptions, State, StreamInfo} from 'vscode-languageclient/node'; -import {AspectValidationController} from './aspectValidation'; -import {ExtensionContext, workspace} from 'vscode'; -import net from 'net'; -import { Trace } from 'vscode-jsonrpc'; +import { AspectValidationController, RequestClient } from './aspectValidation'; +import { TurtleLanguageServer } from './languageServer'; +import { SammCliDownloader } from './sammCliDownloader'; +import { TurtleExtensionSettings, SammCliSelection } from './settings'; +import { TurtleLanguageClient } from './languageClient'; +import type { ExtensionLogger } from './outputChannel'; -var client: LanguageClient | undefined; -let aspectValidationController: AspectValidationController; - -export async function activate(context: ExtensionContext): Promise { - // The server is a started as a separate app and listens on port 2113 - let connectionInfo = { - port: 1846 - }; - let serverOptions = () => { - // Connect to language server via socket - let socket = net.connect(connectionInfo); - let result: StreamInfo = { - writer: socket, - reader: socket - }; - return Promise.resolve(result); - }; - - let clientOptions: LanguageClientOptions = { - documentSelector: ['turtle'], - synchronize: { - fileEvents: workspace.createFileSystemWatcher('**/*.ttl') - } - }; +const SELECT_EXECUTABLE_COMMAND = 'turtle.selectSammCliExecutable'; +const RESTART_LANGUAGE_SERVICES_COMMAND = 'turtle.restartLanguageServices'; - // Create the language client and start the client. - client = new LanguageClient('RDF/Turtle language client', serverOptions, clientOptions); +let settings: TurtleExtensionSettings; +let languageServer: TurtleLanguageServer | undefined; +let languageClient: TurtleLanguageClient; +let aspectValidationController: AspectValidationController; +let sammCliDownloader: SammCliDownloader; - const outputChannel = vscode.window.createOutputChannel('Turtle LSP'); - outputChannel.appendLine(`[startup] Connecting to Turtle language server at port ${ connectionInfo.port }...`); +let outputChannel: ExtensionLogger; +let context: vscode.ExtensionContext; +let restartChain: Promise = Promise.resolve(); - // enable tracing (Off, Messages, Verbose) - client.setTrace(Trace.Verbose); - aspectValidationController = new AspectValidationController(client, vscode.window, vscode.workspace, outputChannel); +export async function activate(ctx: vscode.ExtensionContext): Promise { + context = ctx; + const logOutputChannel = vscode.window.createOutputChannel('RDF/Turtle and SAMM Aspect Models Language Server', { log: true }); + context.subscriptions.push(logOutputChannel); + outputChannel = logOutputChannel; + settings = new TurtleExtensionSettings(context); + sammCliDownloader = new SammCliDownloader(context, settings, outputChannel); + languageClient = new TurtleLanguageClient(outputChannel, settings.getSammCliLspServerPort(), settings.getLanguageClientTraceLevel()); + aspectValidationController = new AspectValidationController(createUnavailableClient(), vscode.window, vscode.workspace, outputChannel); aspectValidationController.register(context); + if (settings.sammCliAutoUpdateIsEnabled()) { + const selectedSammCliVersion = settings.getSammCliSelection(); + if (selectedSammCliVersion.kind === 'release') { + sammCliDownloader.checkForSammCliUpdates().catch(error => { + outputChannel.error(`Failed to check for SAMM-CLI updates: ${error instanceof Error ? error.message : String(error)}`); + }); + } + } + context.subscriptions.push( - vscode.commands.registerCommand('turtleLsp.reconnect', async () => { - if (client && client.state === State.Running) { - await client.stop(); + vscode.commands.registerCommand(SELECT_EXECUTABLE_COMMAND, async () => { + await selectSammCliExecutable(); + }), + vscode.commands.registerCommand(RESTART_LANGUAGE_SERVICES_COMMAND, async () => { + await queueLanguageServicesRestart('Manual restart command'); + }), + vscode.workspace.onDidChangeConfiguration((e: vscode.ConfigurationChangeEvent) => { + if (e.affectsConfiguration('turtle.languageServerSettings')) { + void queueLanguageServicesRestart('Configuration change detected'); } - outputChannel.appendLine(`[startup] Connecting to Turtle language server at port ${ connectionInfo.port }...`); - client = new LanguageClient('RDF/Turtle language client', serverOptions, clientOptions); - client.setTrace(Trace.Verbose); - aspectValidationController.setClient(client); - startClient(client, outputChannel); }) ); - startClient(client, outputChannel); + + void queueLanguageServicesRestart('extension activation'); } -async function startClient(theClient: LanguageClient, outputChannel: vscode.OutputChannel): Promise { +function queueLanguageServicesRestart(reason: string): Promise { + restartChain = restartChain + .then(() => restartLanguageServices(reason)) + .catch(error => { + outputChannel.error(`Restart pipeline failed: ${error instanceof Error ? error.message : String(error)}`); + }); + + return restartChain; +} + +async function startLanguageServer(): Promise { + const executablePath = await sammCliDownloader.getSammCli(); + languageServer = new TurtleLanguageServer(context, outputChannel, executablePath, settings.getSammCliLspServerPort()); + await languageServer.start(); +} + +async function stopLanguageServer(): Promise { + if (!languageServer) { + return; + } + + await languageServer.stop(); + languageServer = undefined; +} + +async function restartLanguageServices(reason: string): Promise { + outputChannel.info(`Restarting language services (${reason}).`); + + aspectValidationController.setClient(createUnavailableClient()); + await languageClient.disconnect(); + await stopLanguageServer(); + try { - await Promise.race([ - theClient.start(), - new Promise((_, reject) => setTimeout(() => reject(new Error()), 2000)) - ]); - outputChannel.appendLine(`[startup] Connected to language server`); - } catch (e) { - outputChannel.appendLine(`[startup] Failed to connect to language server`); + const selection = settings.getSammCliSelection(); + if (selection.kind === 'noSammCli') { + outputChannel.info('Integrated SAMM-CLI is disabled. Assuming an external server is already running.'); + } else { + await startLanguageServer(); + } + } catch (error) { + await stopLanguageServer().catch(() => undefined); + aspectValidationController.setClient(createUnavailableClient()); + + const message = formatStartupError(error); + outputChannel.error(message); + vscode.window.showErrorMessage(message); } + + const nextClient = new TurtleLanguageClient(outputChannel, settings.getSammCliLspServerPort(), settings.getLanguageClientTraceLevel()); + await nextClient.connect(); + languageClient = nextClient; + aspectValidationController.setClient(nextClient); } -export async function deactivate(): Promise { - if (client) { - await client.stop(); - client = undefined; +type SammCliQuickPickItem = vscode.QuickPickItem & { + selection: SammCliSelection; +}; + +async function selectSammCliExecutable(): Promise { + const releases = await sammCliDownloader.getRecentSammCliReleaseTags(10); + const currentSelection = settings.getSammCliSelection(); + + const releaseItems: SammCliQuickPickItem[] = releases.map(releaseTag => ({ + label: releaseTag, + detail: currentSelection?.kind === 'release' && currentSelection.releaseTag === releaseTag ? 'Currently selected' : 'Download and use this GitHub release', + selection: { kind: 'release', releaseTag }, + })); + + const customPathItem: SammCliQuickPickItem = { + label: '$(folder-opened) Use custom SAMM CLI executable or jar. Jar requires Java to be installed', + detail: currentSelection?.kind === 'customPath' ? `Currently selected: ${currentSelection.path}` : 'Choose an executable from your file system', + selection: { kind: 'customPath', path: '' }, + }; + + const noSammCliItem: SammCliQuickPickItem = { + label: 'Do not start integrated SAMM CLI LSP', + detail: currentSelection?.kind === 'noSammCli' ? 'Currently selected' : 'Do not start integrated SAMM CLI LSP. Must be managed by user.', + selection: { kind: 'noSammCli' }, + }; + + const pick = await vscode.window.showQuickPick([customPathItem, noSammCliItem, ...releaseItems], { + title: 'Select SAMM-CLI executable', + placeHolder: 'Choose a recent release or select a custom executable path', + matchOnDetail: true, + }); + + if (!pick) { + return; } + + const selection = pick.selection; + let restartReason = ''; + + if (selection.kind === 'customPath') { + const selectedPath = await promptForCustomExecutablePath(); + if (!selectedPath) { + return; + } + selection.path = selectedPath; + + restartReason = 'Changed SAMM-CLI version to custom SAMM-CLI executable'; + } else if (selection.kind === 'noSammCli') { + restartReason = 'Changed SAMM-CLI version to external SAMM-CLI management'; + } else { + restartReason = `Changed SAMM-CLI version to release ${selection.releaseTag}`; + } + + await settings.setSammCliSelection(selection); + vscode.window.showInformationMessage(restartReason); + await queueLanguageServicesRestart(restartReason); +} + +async function promptForCustomExecutablePath(): Promise { + const selection = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + openLabel: 'Use this executable / jar', + title: 'Select SAMM-CLI executable / jar', + }); + + return selection?.[0]?.fsPath; +} + +function createUnavailableClient(): RequestClient { + return { + sendRequest: async () => { + throw new Error(`The Turtle language server is not available yet. Run '${SELECT_EXECUTABLE_COMMAND}' and then reconnect.`); + }, + }; +} + +function formatStartupError(error: unknown): string { + if (error instanceof Error) { + return `Failed to start the Turtle language server: ${error.message}`; + } + + return 'Failed to start the Turtle language server.'; +} + +export async function deactivate(): Promise { + await restartChain; + await languageClient.disconnect(); + await stopLanguageServer(); } diff --git a/extension/src/languageClient.ts b/extension/src/languageClient.ts new file mode 100644 index 0000000..4ea5c34 --- /dev/null +++ b/extension/src/languageClient.ts @@ -0,0 +1,98 @@ +import { Trace } from 'vscode-jsonrpc'; +import * as net from 'node:net'; +import * as vscode from 'vscode'; +import { LanguageClient, LanguageClientOptions, State, StreamInfo } from 'vscode-languageclient/node'; +import type { RequestClient } from './aspectValidation'; +import type { ExtensionLogger } from './outputChannel'; + +const CLIENT_START_TIMEOUT_MS = 5000; + +export class TurtleLanguageClient implements RequestClient { + private client: LanguageClient; + + constructor( + private outputChannel: ExtensionLogger, + private readonly serverPort: number, + private readonly traceLevel: 'off' | 'messages' | 'verbose' = 'off' + ) { + this.client = this.initLanguageClient(this.serverPort); + } + + private toTrace(level: 'off' | 'messages' | 'verbose'): Trace { + switch (level) { + case 'messages': return Trace.Messages; + case 'verbose': return Trace.Verbose; + default: return Trace.Off; + } + } + + private initLanguageClient(serverPort: number): LanguageClient { + const serverOptions = async (): Promise => new Promise((resolve, reject) => { + const socket = net.connect({ host: '127.0.0.1', port: serverPort }, () => { + resolve({ reader: socket, writer: socket }); + }); + + socket.once('error', error => { + socket.destroy(); + reject(error); + }); + }); + + const clientOptions: LanguageClientOptions = { + documentSelector: ['turtle'], + synchronize: { + fileEvents: vscode.workspace.createFileSystemWatcher('**/*.ttl'), + }, + }; + + const client = new LanguageClient('RDF/Turtle and SAMM Aspect Models Language Client', serverOptions, clientOptions); + client.setTrace(this.toTrace(this.traceLevel)); + return client; + } + + async connect(): Promise { + let timeoutHandle: ReturnType | undefined; + // Hold a reference so we can suppress an unhandled rejection if the + // race is won by the timeout and start() rejects later. + const startPromise = this.client.start(); + try { + const timeout = new Promise((_, reject) => { + timeoutHandle = setTimeout(() => reject(new Error('Timed out while starting the language client.')), CLIENT_START_TIMEOUT_MS); + }); + + await Promise.race([startPromise, timeout]); + this.outputChannel.info('Language client started.'); + } + catch (error) { + // Prevent an unhandled-rejection warning if start() rejects after + // the timeout already won the race. + startPromise.catch(() => undefined); + await this.client.stop().catch(() => undefined); + const message = error instanceof Error ? error.message : 'An unknown error occurred while starting the language client.'; + this.outputChannel.error(`Failed to start language client: ${message}`); + throw error; + } + finally { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + } + } + + async disconnect(): Promise { + if (this.client.state === State.Stopped) { + return; + } + + await this.client.stop(); + } + + sendRequest(method: string, params?: unknown): Promise { + if (this.client.state === State.Stopped) { + return Promise.reject(new Error('The Turtle language client is not connected.')); + } + + return this.client.sendRequest(method, params) as Promise; + } + +} diff --git a/extension/src/languageServer.ts b/extension/src/languageServer.ts new file mode 100644 index 0000000..1a05ec7 --- /dev/null +++ b/extension/src/languageServer.ts @@ -0,0 +1,150 @@ +import * as vscode from 'vscode'; +import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; +import * as net from 'node:net'; +import type { ExtensionLogger } from './outputChannel'; + +const SERVER_READY_TIMEOUT_MS = 30_000; +const SERVER_READY_RETRY_DELAY_MS = 250; + +export class TurtleLanguageServer { + private serverProcess: ChildProcessWithoutNullStreams | undefined; + + constructor( + private readonly context: vscode.ExtensionContext, + private readonly outputChannel: ExtensionLogger, + private readonly sammCliExecutablePath: string, + private readonly serverPort: number + ) { } + + async start(): Promise { + + const [executable, args] = this.sammCliExecutablePath.endsWith('.jar') + ? ['java', ['-jar', this.sammCliExecutablePath, 'lsp', '--port', String(this.serverPort)]] + : [this.sammCliExecutablePath, ['lsp', '--port', String(this.serverPort)]]; + + this.outputChannel.info(`Starting language server: ${executable} ${args.join(' ')}`); + + this.serverProcess = this.spawnProcess(executable, args); + + try { + await this.waitForServerPort(this.serverPort, this.serverProcess); + } catch (error) { + await this.stop(); + throw error; + } + this.outputChannel.info('Language server started successfully.'); + } + + async stop(): Promise { + const process = this.serverProcess; + this.serverProcess = undefined; + + if (!process) { + return; + } + + await new Promise(resolve => { + const fallback = setTimeout(() => { + try { + process.kill('SIGKILL'); + } catch { + // The process may already have exited. + } + resolve(); + }, 3000); + + process.once('exit', () => { + clearTimeout(fallback); + resolve(); + }); + + try { + process.kill(); + } catch { + clearTimeout(fallback); + resolve(); + } + }); + } + + private spawnProcess(executable: string, args: string[]): ChildProcessWithoutNullStreams { + const spawnOptions = { + cwd: this.context.extensionPath, + env: process.env, + stdio: 'pipe' as const, + }; + + const child = spawn(executable, args, spawnOptions) as ChildProcessWithoutNullStreams; + + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + + child.stdout.on('data', data => { + this.outputChannel.trace(String(data).trimEnd()); + }); + + child.stderr.on('data', data => { + this.outputChannel.warn(`[server stderr] ${String(data).trimEnd()}`); + }); + + child.once('error', error => { + this.outputChannel.error(`Server process error: ${String(error instanceof Error ? error.message : error)}`); + }); + + return child; + } + + private async waitForServerPort(port: number, process: ChildProcessWithoutNullStreams): Promise { + const deadline = Date.now() + SERVER_READY_TIMEOUT_MS; + + while (Date.now() < deadline) { + if (await this.isServerListening(port, process)) { + return; + } + + await this.delay(SERVER_READY_RETRY_DELAY_MS); + } + + throw new Error(`Timed out waiting for the Turtle language server to start on port ${port}.`); + } + + private async isServerListening(port: number, process: ChildProcessWithoutNullStreams): Promise { + return new Promise((resolve, reject) => { + let settled = false; + + const finish = (callback: () => void): void => { + if (settled) { + return; + } + + settled = true; + process.removeListener('exit', exitListener); + callback(); + }; + + const exitListener = (): void => { + finish(() => reject(new Error('The Turtle language server exited before it became ready.'))); + }; + + process.once('exit', exitListener); + + const socket = net.connect({ host: '127.0.0.1', port }, () => { + finish(() => { + socket.end(); + resolve(true); + }); + }); + + socket.once('error', () => { + finish(() => { + socket.destroy(); + resolve(false); + }); + }); + }); + } + + private delay(milliseconds: number): Promise { + return new Promise(resolve => setTimeout(resolve, milliseconds)); + } +} \ No newline at end of file diff --git a/extension/src/outputChannel.ts b/extension/src/outputChannel.ts new file mode 100644 index 0000000..f999dd2 --- /dev/null +++ b/extension/src/outputChannel.ts @@ -0,0 +1,10 @@ +/** + * Minimal structured-logging interface shared by all extension modules. + * The real implementation is `vscode.LogOutputChannel`; tests supply a fake. + */ +export interface ExtensionLogger { + trace(message: string): void; + info(message: string): void; + warn(message: string): void; + error(message: string | Error): void; +} diff --git a/extension/src/sammCliDownloader.ts b/extension/src/sammCliDownloader.ts new file mode 100644 index 0000000..db51a32 --- /dev/null +++ b/extension/src/sammCliDownloader.ts @@ -0,0 +1,227 @@ +import * as vscode from 'vscode'; +import { constants, createWriteStream } from 'node:fs'; +import { access, mkdir, rm, stat } from 'node:fs/promises'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import extractZip = require('extract-zip'); +import * as tar from 'tar'; +import {TurtleExtensionSettings, SammCliSelection } from './settings'; +import type { ExtensionLogger } from './outputChannel'; + +interface GitHubReleaseAsset { + name: string; + browser_download_url: string; +} + +interface GitHubRelease { + tag_name: string; + assets: Array; + draft?: boolean; + prerelease?: boolean; +} + +const GITHUB_RELEASE_REPOSITORY = 'eclipse-esmf/esmf-sdk'; +const SAMM_CLI_STORAGE_DIR = 'samm-cli'; + +export class SammCliDownloader { + constructor( + private readonly context: vscode.ExtensionContext, + private readonly settings: TurtleExtensionSettings, + private readonly outputChannel: ExtensionLogger, + ) { } + + async checkForSammCliUpdates(): Promise { + const selection = this.settings.getSammCliSelection(); + if (selection.kind !== 'release') { + return; + } + + const configuredVersion = selection.releaseTag; + const latestVersion = await this.getLatestAvailabeSammCliReleaseTag(); + if (configuredVersion !== latestVersion) { + vscode.window.showInformationMessage(`There is a new SAMM-CLI release available: ${configuredVersion} -> ${latestVersion}`, 'Download & Use').then(async (selection) => { + if (selection === 'Download & Use') { + try { + await this.settings.setSammCliSelection({ kind: 'release', releaseTag: latestVersion }); + await this.getSammCli(); + vscode.window.showInformationMessage(`SAMM-CLI ${latestVersion} has been downloaded and configured for use.`); + } catch (error) { + await this.settings.setSammCliSelection({ kind: 'release', releaseTag: configuredVersion }).catch(() => undefined); + const message = error instanceof Error ? error.message : 'An unknown error occurred while downloading the latest SAMM-CLI release.'; + vscode.window.showErrorMessage(message); + } + } + }); + } + } + + private async getLatestAvailabeSammCliReleaseTag(): Promise { + const releases = await this.getRecentSammCliReleaseTags(1); + + if (releases.length === 0) { + throw new Error('No SAMM-CLI releases are available on GitHub.'); + } + + return releases[0]; + } + + async getRecentSammCliReleaseTags(limit: number): Promise> { + const response = await fetch(`https://api.github.com/repos/${GITHUB_RELEASE_REPOSITORY}/releases?per_page=${limit}`, { + headers: { + 'User-Agent': 'esmf-vs-code-plugin', + Accept: 'application/vnd.github+json', + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch samm-cli releases: ${response.status} ${response.statusText}`); + } + + const fetchedReleases = response.json() as Promise>; + return fetchedReleases.then(releases => releases + .filter(release => !release.draft && !release.prerelease) + .map(release => release.tag_name)); + } + + async getSammCli(): Promise { + const releaseSelection = this.settings.getSammCliSelection(); + if (releaseSelection.kind === 'customPath') { + return releaseSelection.path; + } else if (releaseSelection.kind === 'noSammCli') { + throw new Error('integrated SAMM-CLI is disabled by user selection.'); + } + + const platform = process.platform; + const release = await this.fetchRelease(releaseSelection.releaseTag.trim()); + const asset = this.selectReleaseAsset(release.assets, platform); + const executableName = platform === 'win32' ? 'samm.exe' : 'samm'; + + if (!asset) { + throw new Error(`No matching release asset was found in ${GITHUB_RELEASE_REPOSITORY}.`); + } + + const targetDirectory = vscode.Uri.joinPath(this.context.globalStorageUri, SAMM_CLI_STORAGE_DIR, release.tag_name); + const targetPath = vscode.Uri.joinPath(targetDirectory, executableName); + + if (await this.fileExists(targetPath.fsPath)) { + await this.ensureExecutableIsReady(targetPath.fsPath, platform, release.tag_name); + return targetPath.fsPath; + } + + await mkdir(targetDirectory.fsPath, { recursive: true }); + this.outputChannel.info(`Downloading ${asset.name} (${release.tag_name})...`); + await this.downloadAndExtractAsset(asset.browser_download_url, targetDirectory.fsPath, platform); + await this.ensureExecutableIsReady(targetPath.fsPath, platform, release.tag_name); + + return targetPath.fsPath; + } + + private async fetchRelease(releaseVersion: string): Promise { + const releasePath = releaseVersion && releaseVersion !== 'latest' + ? `releases/tags/${encodeURIComponent(releaseVersion)}` + : 'releases/latest'; + + const response = await fetch(`https://api.github.com/repos/${GITHUB_RELEASE_REPOSITORY}/${releasePath}`, { + headers: { + 'User-Agent': 'esmf-vs-code-plugin', + Accept: 'application/vnd.github+json', + }, + }); + + if (!response.ok) { + const target = releaseVersion && releaseVersion !== 'latest' ? `release ${releaseVersion}` : 'the latest release'; + throw new Error(`Failed to fetch samm-cli ${target}: ${response.status} ${response.statusText}`); + } + + return response.json() as Promise; + } + + private async downloadAndExtractAsset(downloadUrl: string, targetDirectory: string, platform: string): Promise { + const archivePath = `${targetDirectory}.download`; + + await rm(archivePath, { force: true }); + + const response = await fetch(downloadUrl, { + headers: { + 'User-Agent': 'esmf-vs-code-plugin', + Accept: 'application/octet-stream', + }, + }); + + if (!response.ok || !response.body) { + throw new Error(`Failed to download the language server from ${downloadUrl}: ${response.status} ${response.statusText}`); + } + + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Downloading Language Server (samm-cli)...' }, async () => { + try { + await pipeline( + Readable.fromWeb(response.body as unknown as globalThis.ReadableStream), + createWriteStream(archivePath), + ); + await this.extractArchive(archivePath, targetDirectory, platform); + } finally { + await rm(archivePath, { force: true }); + } + }); + } + + private async extractArchive(archivePath: string, extractionPath: string, platform: string): Promise { + if (platform === 'win32') { + await extractZip(archivePath, { dir: extractionPath }); + return; + } + + await tar.x({ file: archivePath, cwd: extractionPath }); + } + + private selectReleaseAsset(assets: Array, platform: string): GitHubReleaseAsset { + let assetOsIdentifier: string; + switch (platform) { + case 'win32': + assetOsIdentifier = "windows"; + break; + case 'darwin': + assetOsIdentifier = "macos"; + break; + case 'linux': + assetOsIdentifier = "linux"; + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + + const foundAsset = assets.find(asset => asset.name.includes(assetOsIdentifier)); + + if (!foundAsset) { + throw new Error(`No matching release asset found for ${assetOsIdentifier} in the available assets: ${assets.map(a => a.name).join(', ')}`); + } + + return foundAsset; + } + + private async ensureExecutableIsReady(executablePath: string, platform: string, releaseTag: string): Promise { + if (!await this.fileExists(executablePath)) { + throw new Error(`Downloaded samm-cli ${releaseTag} did not contain expected executable at ${executablePath}.`); + } + + if (platform === 'win32') { + return; + } + + try { + await access(executablePath, constants.X_OK); + } catch { + throw new Error(`Downloaded samm-cli executable is not marked as executable: ${executablePath}`); + } + } + + private async fileExists(filePath: string): Promise { + try { + return (await stat(filePath)).isFile(); + } catch { + return false; + } + } +} + + diff --git a/extension/src/settings.ts b/extension/src/settings.ts new file mode 100644 index 0000000..2c1944e --- /dev/null +++ b/extension/src/settings.ts @@ -0,0 +1,47 @@ +import * as vscode from 'vscode'; + +const SAMM_CLI_SELECTION_CONFIG_KEY = 'sammCliSelection'; +const DEFAULT_SAMM_CLI_RELEASE: ReleaseSammCliSelection = { kind: 'release', releaseTag: 'v2.14.3' }; + +type ReleaseSammCliSelection = { + kind: 'release'; + releaseTag: string; +}; + +type CustomPathSammCliSelection = { + kind: 'customPath'; + path: string; +}; + +type NoSammCliSelection = { + kind: 'noSammCli'; +}; + +export type SammCliSelection = ReleaseSammCliSelection | CustomPathSammCliSelection | NoSammCliSelection; + +export class TurtleExtensionSettings { + constructor( + private readonly context: vscode.ExtensionContext, + ) { } + + getSammCliSelection(): SammCliSelection { + return this.context.globalState.get(SAMM_CLI_SELECTION_CONFIG_KEY, DEFAULT_SAMM_CLI_RELEASE); + } + + async setSammCliSelection(selection: SammCliSelection): Promise { + await this.context.globalState.update(SAMM_CLI_SELECTION_CONFIG_KEY, selection); + } + + sammCliAutoUpdateIsEnabled(): boolean { + return vscode.workspace.getConfiguration('turtle.languageServerSettings').get('automaticUpdateCheck', true); + } + + getSammCliLspServerPort(): number { + return vscode.workspace.getConfiguration('turtle.languageServerSettings').get('serverPort', 1846); + } + + getLanguageClientTraceLevel(): 'off' | 'messages' | 'verbose' { + return vscode.workspace.getConfiguration('turtle.languageServerSettings').get<'off' | 'messages' | 'verbose'>('traceLevel', 'off'); + } + +} \ No newline at end of file diff --git a/extension/src/test/validationTestHarness.ts b/extension/src/test/validationTestHarness.ts index a087bb4..f5f3255 100644 --- a/extension/src/test/validationTestHarness.ts +++ b/extension/src/test/validationTestHarness.ts @@ -7,6 +7,7 @@ import { ValidationWindow, ValidationWorkspace, } from '../aspectValidation'; +import type { ExtensionLogger } from '../outputChannel'; type ValidationHarnessOptions = { response?: DiagnosticReport; @@ -30,7 +31,7 @@ type FakeWorkspace = ValidationWorkspace & { fireSave(document: Pick): Promise; }; -type FakeOutputChannel = ValidationOutputChannel & { +type FakeOutputChannel = ValidationOutputChannel & ExtensionLogger & { lines: string[]; }; @@ -117,8 +118,17 @@ function createFakeWindow(): FakeWindow { function createFakeOutputChannel(): FakeOutputChannel { return { lines: [], - appendLine(value: string) { - this.lines.push(value); + trace(message: string) { + this.lines.push(`[trace] ${message}`); + }, + info(message: string) { + this.lines.push(`[info] ${message}`); + }, + warn(message: string) { + this.lines.push(`[warn] ${message}`); + }, + error(message: string | Error) { + this.lines.push(`[error] ${message instanceof Error ? message.message : message}`); }, }; } From c866bb03e6e8bda74b224b72229bd09e54b52186 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Fri, 5 Jun 2026 14:32:39 +0200 Subject: [PATCH 10/22] Add menu bar config --- extension/package.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/extension/package.json b/extension/package.json index 5a5fba8..d605341 100644 --- a/extension/package.json +++ b/extension/package.json @@ -92,6 +92,22 @@ "editor.semanticHighlighting.enabled": true } }, + "menus": { + "editor/title": [ + { + "command": "turtle.validateDocumentNow", + "when": "resourceLangId == turtle", + "group": "navigation" + } + ], + "editor/context": [ + { + "command": "turtle.validateDocumentNow", + "when": "resourceLangId == turtle", + "group": "1_modification" + } + ] + }, "walkthroughs": [ { "id": "turtle-getting-started", From d651f96d8b8239b30db75742d4478e0287f8607d Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Tue, 9 Jun 2026 09:58:20 +0200 Subject: [PATCH 11/22] Update package-lock --- extension/package-lock.json | 451 +++++++++++++++++++----------------- 1 file changed, 243 insertions(+), 208 deletions(-) diff --git a/extension/package-lock.json b/extension/package-lock.json index 4ce5caf..6a2d563 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -28,13 +28,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -43,9 +43,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -120,9 +120,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -194,9 +194,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -255,29 +255,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -326,7 +340,7 @@ }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "license": "ISC", "dependencies": { @@ -337,9 +351,9 @@ } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", "engines": { @@ -347,9 +361,9 @@ } }, "node_modules/@jest/diff-sequences": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", - "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", "dev": true, "license": "MIT", "engines": { @@ -357,9 +371,9 @@ } }, "node_modules/@jest/expect-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", - "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -380,23 +394,23 @@ } }, "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", - "jest-regex-util": "30.0.1" + "jest-regex-util": "30.4.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", "dev": true, "license": "MIT", "dependencies": { @@ -407,14 +421,14 @@ } }, "node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", @@ -472,9 +486,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -531,9 +545,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", - "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "version": "22.19.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz", + "integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -548,9 +562,9 @@ "license": "MIT" }, "node_modules/@types/vscode": { - "version": "1.110.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", - "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", + "version": "1.120.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.120.0.tgz", + "integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==", "dev": true, "license": "MIT" }, @@ -573,7 +587,7 @@ }, "node_modules/@types/yauzl": { "version": "2.10.3", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/@types/yauzl/-/yauzl-2.10.3.tgz", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "license": "MIT", "optional": true, @@ -582,17 +596,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", - "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/type-utils": "8.58.0", - "@typescript-eslint/utils": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -605,7 +619,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.58.0", + "@typescript-eslint/parser": "^8.61.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -621,16 +635,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", - "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3" }, "engines": { @@ -646,14 +660,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", - "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.0", - "@typescript-eslint/types": "^8.58.0", + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", "debug": "^4.4.3" }, "engines": { @@ -668,14 +682,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", - "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0" + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -686,9 +700,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", - "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", "dev": true, "license": "MIT", "engines": { @@ -703,15 +717,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", - "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -728,9 +742,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", - "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", "dev": true, "license": "MIT", "engines": { @@ -742,16 +756,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", - "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.58.0", - "@typescript-eslint/tsconfig-utils": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0", + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -780,9 +794,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -809,16 +823,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", - "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0" + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -833,13 +847,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", - "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/types": "8.61.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -938,9 +952,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1024,9 +1038,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -1054,7 +1068,7 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "license": "MIT", "engines": { @@ -1175,7 +1189,7 @@ }, "node_modules/chownr": { "version": "3.0.0", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/chownr/-/chownr-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "license": "BlueOak-1.0.0", "engines": { @@ -1424,7 +1438,7 @@ }, "node_modules/end-of-stream": { "version": "1.4.5", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/end-of-stream/-/end-of-stream-1.4.5.tgz", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { @@ -1432,14 +1446,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", + "integrity": "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -1559,9 +1573,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -1660,18 +1674,18 @@ } }, "node_modules/expect": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.3.0", + "@jest/expect-utils": "30.4.1", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-util": "30.3.0" + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1679,7 +1693,7 @@ }, "node_modules/extract-zip": { "version": "2.0.1", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/extract-zip/-/extract-zip-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "license": "BSD-2-Clause", "dependencies": { @@ -1720,7 +1734,7 @@ }, "node_modules/fd-slicer": { "version": "1.1.0", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/fd-slicer/-/fd-slicer-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "license": "MIT", "dependencies": { @@ -1844,9 +1858,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", "engines": { @@ -1858,7 +1872,7 @@ }, "node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/get-stream/-/get-stream-5.2.0.tgz", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "license": "MIT", "dependencies": { @@ -2217,51 +2231,52 @@ } }, "node_modules/jest-diff": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", - "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.3.0", + "@jest/diff-sequences": "30.4.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.3.0" + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", - "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.3.0", - "pretty-format": "30.3.0" + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", - "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", "picomatch": "^4.0.3", - "pretty-format": "30.3.0", + "pretty-format": "30.4.1", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -2283,24 +2298,24 @@ } }, "node_modules/jest-mock": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", - "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/node": "*", - "jest-util": "30.3.0" + "jest-util": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", "dev": true, "license": "MIT", "engines": { @@ -2308,13 +2323,13 @@ } }, "node_modules/jest-util": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", - "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.4.1", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -2346,10 +2361,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2529,7 +2554,7 @@ }, "node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/minizlib/-/minizlib-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { @@ -2540,9 +2565,9 @@ } }, "node_modules/mocha": { - "version": "11.7.5", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", - "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "version": "11.7.6", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz", + "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==", "dev": true, "license": "MIT", "dependencies": { @@ -2647,7 +2672,7 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/once/-/once-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { @@ -2891,7 +2916,7 @@ }, "node_modules/pend": { "version": "1.2.0", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/pend/-/pend-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, @@ -2926,15 +2951,16 @@ } }, "node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2962,7 +2988,7 @@ }, "node_modules/pump": { "version": "3.0.4", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/pump/-/pump-3.0.4.tgz", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { @@ -2990,13 +3016,22 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/react-is": { + "node_modules/react-is-18": { + "name": "react-is", "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, "license": "MIT" }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -3071,9 +3106,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz", + "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3322,9 +3357,9 @@ } }, "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -3336,9 +3371,9 @@ } }, "node_modules/tar": { - "version": "7.5.15", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -3377,9 +3412,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -3406,14 +3441,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -3507,16 +3542,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", - "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.58.0", - "@typescript-eslint/parser": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/utils": "8.58.0" + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3750,7 +3785,7 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/wrappy/-/wrappy-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, @@ -3766,7 +3801,7 @@ }, "node_modules/yallist": { "version": "5.0.0", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/yallist/-/yallist-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "license": "BlueOak-1.0.0", "engines": { @@ -3865,7 +3900,7 @@ }, "node_modules/yauzl": { "version": "2.10.0", - "resolved": "https://artifactory.boschdevcloud.com/artifactory/api/npm/lab000003-bci-npm-virtual/yauzl/-/yauzl-2.10.0.tgz", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "license": "MIT", "dependencies": { From 2bca70b1cb30c5fdf6485efaccd19f9346f8bf42 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Tue, 9 Jun 2026 10:33:42 +0200 Subject: [PATCH 12/22] Fix samm cli selection on network errors --- extension/src/extension.ts | 35 ++++++++++++++++++++++-------- extension/src/sammCliDownloader.ts | 14 +++++++----- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/extension/src/extension.ts b/extension/src/extension.ts index c9695e5..1385312 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -116,15 +116,12 @@ type SammCliQuickPickItem = vscode.QuickPickItem & { }; async function selectSammCliExecutable(): Promise { - const releases = await sammCliDownloader.getRecentSammCliReleaseTags(10); + const releases = await sammCliDownloader.getRecentSammCliReleaseTags(10).catch(error => { + outputChannel.error(`Failed to fetch SAMM-CLI releases for quick pick: ${error instanceof Error ? error.message : String(error)}`); + return []; + }); const currentSelection = settings.getSammCliSelection(); - const releaseItems: SammCliQuickPickItem[] = releases.map(releaseTag => ({ - label: releaseTag, - detail: currentSelection?.kind === 'release' && currentSelection.releaseTag === releaseTag ? 'Currently selected' : 'Download and use this GitHub release', - selection: { kind: 'release', releaseTag }, - })); - const customPathItem: SammCliQuickPickItem = { label: '$(folder-opened) Use custom SAMM CLI executable or jar. Jar requires Java to be installed', detail: currentSelection?.kind === 'customPath' ? `Currently selected: ${currentSelection.path}` : 'Choose an executable from your file system', @@ -137,13 +134,33 @@ async function selectSammCliExecutable(): Promise { selection: { kind: 'noSammCli' }, }; - const pick = await vscode.window.showQuickPick([customPathItem, noSammCliItem, ...releaseItems], { + const separator: SammCliQuickPickItem = { + label: 'GitHub Releases', + kind: vscode.QuickPickItemKind.Separator, + selection: { kind: 'release', releaseTag: 'this is not clickable, so does not matter' }, + }; + + const releaseItems: SammCliQuickPickItem[] = releases.map(releaseTag => ({ + label: releaseTag, + detail: currentSelection?.kind === 'release' && currentSelection.releaseTag === releaseTag ? 'Currently selected' : 'Download and use this GitHub release', + selection: { kind: 'release', releaseTag }, + })); + + if (releaseItems.length === 0) { + releaseItems.push({ + label: '$(error) No GitHub releases available', + detail: 'Failed to fetch releases from GitHub. Check output channel for details.', + selection: { kind: 'release', releaseTag: '' }, + }); + } + + const pick = await vscode.window.showQuickPick([customPathItem, noSammCliItem, separator, ...releaseItems], { title: 'Select SAMM-CLI executable', placeHolder: 'Choose a recent release or select a custom executable path', matchOnDetail: true, }); - if (!pick) { + if (!pick || (pick.selection.kind === 'release' && !pick.selection.releaseTag)) { return; } diff --git a/extension/src/sammCliDownloader.ts b/extension/src/sammCliDownloader.ts index db51a32..1977135 100644 --- a/extension/src/sammCliDownloader.ts +++ b/extension/src/sammCliDownloader.ts @@ -5,7 +5,7 @@ import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import extractZip = require('extract-zip'); import * as tar from 'tar'; -import {TurtleExtensionSettings, SammCliSelection } from './settings'; +import { TurtleExtensionSettings, SammCliSelection } from './settings'; import type { ExtensionLogger } from './outputChannel'; interface GitHubReleaseAsset { @@ -37,7 +37,9 @@ export class SammCliDownloader { } const configuredVersion = selection.releaseTag; - const latestVersion = await this.getLatestAvailabeSammCliReleaseTag(); + const latestVersion = await this.getLatestAvailabeSammCliReleaseTag().catch(error => { + throw new Error(`Failed to check for latest SAMM-CLI release: ${error instanceof Error ? error.message : String(error)}`); + }); if (configuredVersion !== latestVersion) { vscode.window.showInformationMessage(`There is a new SAMM-CLI release available: ${configuredVersion} -> ${latestVersion}`, 'Download & Use').then(async (selection) => { if (selection === 'Download & Use') { @@ -74,7 +76,7 @@ export class SammCliDownloader { }); if (!response.ok) { - throw new Error(`Failed to fetch samm-cli releases: ${response.status} ${response.statusText}`); + return Promise.reject(new Error(`Failed to fetch SAMM-CLI releases from GitHub: ${response.status} ${response.statusText}`)); } const fetchedReleases = response.json() as Promise>; @@ -82,7 +84,7 @@ export class SammCliDownloader { .filter(release => !release.draft && !release.prerelease) .map(release => release.tag_name)); } - + async getSammCli(): Promise { const releaseSelection = this.settings.getSammCliSelection(); if (releaseSelection.kind === 'customPath') { @@ -109,7 +111,7 @@ export class SammCliDownloader { } await mkdir(targetDirectory.fsPath, { recursive: true }); - this.outputChannel.info(`Downloading ${asset.name} (${release.tag_name})...`); + this.outputChannel.info(`Downloading ${asset.name}(${release.tag_name})...`); await this.downloadAndExtractAsset(asset.browser_download_url, targetDirectory.fsPath, platform); await this.ensureExecutableIsReady(targetPath.fsPath, platform, release.tag_name); @@ -118,7 +120,7 @@ export class SammCliDownloader { private async fetchRelease(releaseVersion: string): Promise { const releasePath = releaseVersion && releaseVersion !== 'latest' - ? `releases/tags/${encodeURIComponent(releaseVersion)}` + ? `releases / tags / ${encodeURIComponent(releaseVersion)}` : 'releases/latest'; const response = await fetch(`https://api.github.com/repos/${GITHUB_RELEASE_REPOSITORY}/${releasePath}`, { From 042efd129de7864e76cbceea3543496422c4ccd9 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Tue, 9 Jun 2026 13:50:16 +0200 Subject: [PATCH 13/22] Refactor samm cli config and download functionality --- extension/README.md | 24 +++++--- extension/package.json | 18 +++++- extension/src/extension.ts | 89 +++++++++++++++--------------- extension/src/sammCliDownloader.ts | 64 +++++++++++++-------- extension/src/settings.ts | 33 +++-------- 5 files changed, 128 insertions(+), 100 deletions(-) diff --git a/extension/README.md b/extension/README.md index 9d5bf21..99ac52d 100644 --- a/extension/README.md +++ b/extension/README.md @@ -4,10 +4,16 @@ VS Code extension for the ESMF SDK Turtle language server. The extension support ## Configuration -- `turtle.languageServerSettings.serverPort` - - TCP port used for the socket connection to the server. -- `turtle.languageServerSettings.automaticUpdateCheck` - - If enabled, checks whether a newer GitHub release is available when a release-based executable is selected. +- `turtle.languageServerSettings.activateEmbeddedLanguageServer` (boolean, default: `true`) + - When enabled, the extension starts the SAMM-CLI language server process. When disabled, an external language server must be started manually. +- `turtle.languageServerSettings.automaticUpdateCheck` (boolean, default: `true`) + - Automatically check for updates of the SAMM-CLI language server and notify when a new version is available. +- `turtle.languageServerSettings.sammCliPath` (string) + - Path to the SAMM CLI executable or jar file to use as the language server. Can be downloaded / set via the 'Select SAMM-CLI Executable' command. +- `turtle.languageServerSettings.serverPort` (number, default: `1846`) + - TCP port used to connect to the Turtle/SAMM language server. +- `turtle.languageServerSettings.traceLevel` (string, default: `off`) + - Controls the verbosity of language client protocol tracing. Options: `off`, `messages`, `verbose`. Use the command `Turtle: Select SAMM-CLI Executable` to choose either: - one of the latest 10 SAMM-CLI GitHub releases, or @@ -20,7 +26,7 @@ Use the command `Turtle: Select SAMM-CLI Executable` to choose either: - Fast feedback on type from the regular Turtle parser diagnostics provided by the server. - Heavy Aspect validation from the server for model-level issues. - Manual validation command: - - `Validate Aspect Model Now` + - `Turtle: Validate document now` - Standard diagnostics flow: - errors appear in the editor - errors appear in `Problems` @@ -53,14 +59,16 @@ When each validation runs: - On type: fast syntax feedback only. - On save: heavy Aspect validation for Turtle documents. -- Manual: `Validate Aspect Model Now` for the active Turtle document. +- Manual: `Turtle: Validate document now` for the active Turtle document. ## Commands -- `Turtle LSP: Validate Aspect Model Now` +- `Turtle: Validate document now` - Sends a server request for the active Turtle document. - `Turtle: Select SAMM-CLI Executable` - Opens a quick pick with the latest 10 GitHub releases and a custom-path option. +- `Turtle: Restart and reconnect to Language Server` + - Restarts the language server and reconnects the client. ## UX During Long-Running Validation @@ -100,7 +108,7 @@ Use an Aspect model file, for example: Manual check: 1. Open the model file. -2. Run `Turtle LSP: Validate Aspect Model Now`. +2. Run `Turtle: Validate document now`. 3. Wait for the progress indicator to finish. 4. Confirm that diagnostics appear in the editor and in `Problems`. diff --git a/extension/package.json b/extension/package.json index d605341..a4e08ce 100644 --- a/extension/package.json +++ b/extension/package.json @@ -57,17 +57,29 @@ "type": "object", "description": "Configuration settings for RDF/Turtle and SAMM Aspect Models extension.", "properties": { + "turtle.languageServerSettings.activateEmbeddedLanguageServer": { + "type": "boolean", + "default": true, + "description": "When enabled, the extension starts the SAMM-CLI language server process at the path configured below. When disabled, an external language server must be started manually.", + "order": 0 + }, "turtle.languageServerSettings.automaticUpdateCheck": { "type": "boolean", "default": true, "description": "Automatically check for updates of the SAMM-CLI language server and notify when a new version is available.", - "order": 0 + "order": 1 + }, + "turtle.languageServerSettings.sammCliPath": { + "type": "string", + "default": "", + "description": "Path to the SAMM CLI executable or jar file to use as the language server. Can be downloaded / set via the 'Select SAMM-CLI Executable' command.", + "order": 2 }, "turtle.languageServerSettings.serverPort": { "type": "number", "default": 1846, "description": "TCP port used to connect to the Turtle/SAMM language server.", - "order": 1 + "order": 3 }, "turtle.languageServerSettings.traceLevel": { "type": "string", @@ -83,7 +95,7 @@ ], "default": "off", "description": "Controls the verbosity of language client protocol tracing in the output channel.", - "order": 2 + "order": 4 } } }, diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 1385312..3028e43 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { AspectValidationController, RequestClient } from './aspectValidation'; import { TurtleLanguageServer } from './languageServer'; import { SammCliDownloader } from './sammCliDownloader'; -import { TurtleExtensionSettings, SammCliSelection } from './settings'; +import { TurtleExtensionSettings } from './settings'; import { TurtleLanguageClient } from './languageClient'; import type { ExtensionLogger } from './outputChannel'; @@ -30,15 +30,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { aspectValidationController = new AspectValidationController(createUnavailableClient(), vscode.window, vscode.workspace, outputChannel); aspectValidationController.register(context); - if (settings.sammCliAutoUpdateIsEnabled()) { - const selectedSammCliVersion = settings.getSammCliSelection(); - if (selectedSammCliVersion.kind === 'release') { - sammCliDownloader.checkForSammCliUpdates().catch(error => { - outputChannel.error(`Failed to check for SAMM-CLI updates: ${error instanceof Error ? error.message : String(error)}`); - }); - } - } - context.subscriptions.push( vscode.commands.registerCommand(SELECT_EXECUTABLE_COMMAND, async () => { await selectSammCliExecutable(); @@ -53,8 +44,24 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }) ); + if (settings.sammCliAutoUpdateIsEnabled() && settings.isEmbeddedLanguageServerStartEnabled() && settings.getSammCliPath()) { + sammCliDownloader.checkForSammCliUpdates().catch(error => { + outputChannel.error(`Failed to check for SAMM-CLI updates: ${error instanceof Error ? error.message : String(error)}`); + }); + } + + if (settings.isEmbeddedLanguageServerStartEnabled() && !settings.getSammCliPath()) { + await vscode.window.showErrorMessage('No SAMM CLI path configured. Required for Language Server functionality.', 'Select or download SAMM CLI Executable') + .then(selection => { + if (selection) { + selectSammCliExecutable(); + } + }); + return; + } else { + queueLanguageServicesRestart('extension activation'); + } - void queueLanguageServicesRestart('extension activation'); } function queueLanguageServicesRestart(reason: string): Promise { @@ -68,7 +75,7 @@ function queueLanguageServicesRestart(reason: string): Promise { } async function startLanguageServer(): Promise { - const executablePath = await sammCliDownloader.getSammCli(); + const executablePath = settings.getSammCliPath(); languageServer = new TurtleLanguageServer(context, outputChannel, executablePath, settings.getSammCliLspServerPort()); await languageServer.start(); } @@ -90,9 +97,8 @@ async function restartLanguageServices(reason: string): Promise { await stopLanguageServer(); try { - const selection = settings.getSammCliSelection(); - if (selection.kind === 'noSammCli') { - outputChannel.info('Integrated SAMM-CLI is disabled. Assuming an external server is already running.'); + if (!settings.isEmbeddedLanguageServerStartEnabled()) { + outputChannel.info('SAMM CLI LSP activation is disabled. Assuming an external server is already running.'); } else { await startLanguageServer(); } @@ -112,7 +118,8 @@ async function restartLanguageServices(reason: string): Promise { } type SammCliQuickPickItem = vscode.QuickPickItem & { - selection: SammCliSelection; + action: 'customPath' | 'release'; + releaseTag?: string; }; async function selectSammCliExecutable(): Promise { @@ -120,68 +127,64 @@ async function selectSammCliExecutable(): Promise { outputChannel.error(`Failed to fetch SAMM-CLI releases for quick pick: ${error instanceof Error ? error.message : String(error)}`); return []; }); - const currentSelection = settings.getSammCliSelection(); - const customPathItem: SammCliQuickPickItem = { - label: '$(folder-opened) Use custom SAMM CLI executable or jar. Jar requires Java to be installed', - detail: currentSelection?.kind === 'customPath' ? `Currently selected: ${currentSelection.path}` : 'Choose an executable from your file system', - selection: { kind: 'customPath', path: '' }, - }; + const currentPath = settings.getSammCliPath(); - const noSammCliItem: SammCliQuickPickItem = { - label: 'Do not start integrated SAMM CLI LSP', - detail: currentSelection?.kind === 'noSammCli' ? 'Currently selected' : 'Do not start integrated SAMM CLI LSP. Must be managed by user.', - selection: { kind: 'noSammCli' }, + const customPathItem: SammCliQuickPickItem = { + label: '$(folder-opened) Use custom SAMM CLI executable or jar', + detail: currentPath ? `Currently configured: ${currentPath}` : 'Choose an executable from your file system', + action: 'customPath', }; const separator: SammCliQuickPickItem = { label: 'GitHub Releases', kind: vscode.QuickPickItemKind.Separator, - selection: { kind: 'release', releaseTag: 'this is not clickable, so does not matter' }, + action: 'release', }; const releaseItems: SammCliQuickPickItem[] = releases.map(releaseTag => ({ label: releaseTag, - detail: currentSelection?.kind === 'release' && currentSelection.releaseTag === releaseTag ? 'Currently selected' : 'Download and use this GitHub release', - selection: { kind: 'release', releaseTag }, + detail: 'Download and use this GitHub release', + action: 'release', + releaseTag, })); if (releaseItems.length === 0) { releaseItems.push({ label: '$(error) No GitHub releases available', detail: 'Failed to fetch releases from GitHub. Check output channel for details.', - selection: { kind: 'release', releaseTag: '' }, + action: 'release', }); } - const pick = await vscode.window.showQuickPick([customPathItem, noSammCliItem, separator, ...releaseItems], { + const pick = await vscode.window.showQuickPick([customPathItem, separator, ...releaseItems], { title: 'Select SAMM-CLI executable', - placeHolder: 'Choose a recent release or select a custom executable path', + placeHolder: 'Choose a GitHub release or select a custom executable path', matchOnDetail: true, }); - if (!pick || (pick.selection.kind === 'release' && !pick.selection.releaseTag)) { + if (!pick) { return; } - const selection = pick.selection; - let restartReason = ''; + let restartReason: string; - if (selection.kind === 'customPath') { + if (pick.action === 'customPath') { const selectedPath = await promptForCustomExecutablePath(); if (!selectedPath) { return; } - selection.path = selectedPath; - - restartReason = 'Changed SAMM-CLI version to custom SAMM-CLI executable'; - } else if (selection.kind === 'noSammCli') { - restartReason = 'Changed SAMM-CLI version to external SAMM-CLI management'; + await settings.setSammCliPath(selectedPath); + restartReason = `Changed SAMM CLI to custom executable: ${selectedPath}`; } else { - restartReason = `Changed SAMM-CLI version to release ${selection.releaseTag}`; + if (!pick.releaseTag) { + return; + } + const downloadedPath = await sammCliDownloader.downloadRelease(pick.releaseTag); + await settings.setSammCliPath(downloadedPath); + restartReason = `Changed SAMM CLI to GitHub release ${pick.releaseTag}`; } - await settings.setSammCliSelection(selection); vscode.window.showInformationMessage(restartReason); await queueLanguageServicesRestart(restartReason); } diff --git a/extension/src/sammCliDownloader.ts b/extension/src/sammCliDownloader.ts index 1977135..f71add0 100644 --- a/extension/src/sammCliDownloader.ts +++ b/extension/src/sammCliDownloader.ts @@ -1,11 +1,12 @@ import * as vscode from 'vscode'; import { constants, createWriteStream } from 'node:fs'; import { access, mkdir, rm, stat } from 'node:fs/promises'; +import { spawn } from 'node:child_process'; import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import extractZip = require('extract-zip'); import * as tar from 'tar'; -import { TurtleExtensionSettings, SammCliSelection } from './settings'; +import { TurtleExtensionSettings } from './settings'; import type { ExtensionLogger } from './outputChannel'; interface GitHubReleaseAsset { @@ -31,24 +32,28 @@ export class SammCliDownloader { ) { } async checkForSammCliUpdates(): Promise { - const selection = this.settings.getSammCliSelection(); - if (selection.kind !== 'release') { - return; + const currentPath = this.settings.getSammCliPath(); + + let currentVersion: string | undefined; + if (currentPath) { + currentVersion = await this.runVersionCommand(currentPath).catch(() => undefined); } - const configuredVersion = selection.releaseTag; const latestVersion = await this.getLatestAvailabeSammCliReleaseTag().catch(error => { throw new Error(`Failed to check for latest SAMM-CLI release: ${error instanceof Error ? error.message : String(error)}`); }); - if (configuredVersion !== latestVersion) { - vscode.window.showInformationMessage(`There is a new SAMM-CLI release available: ${configuredVersion} -> ${latestVersion}`, 'Download & Use').then(async (selection) => { + + if (currentVersion !== latestVersion) { + const label = currentVersion + ? `There is a new SAMM-CLI release available: ${currentVersion} -> ${latestVersion}` + : `SAMM-CLI ${latestVersion} is available`; + vscode.window.showInformationMessage(label, 'Download & Use').then(async (selection) => { if (selection === 'Download & Use') { try { - await this.settings.setSammCliSelection({ kind: 'release', releaseTag: latestVersion }); - await this.getSammCli(); + const newPath = await this.downloadRelease(latestVersion); + await this.settings.setSammCliPath(newPath); vscode.window.showInformationMessage(`SAMM-CLI ${latestVersion} has been downloaded and configured for use.`); } catch (error) { - await this.settings.setSammCliSelection({ kind: 'release', releaseTag: configuredVersion }).catch(() => undefined); const message = error instanceof Error ? error.message : 'An unknown error occurred while downloading the latest SAMM-CLI release.'; vscode.window.showErrorMessage(message); } @@ -85,16 +90,9 @@ export class SammCliDownloader { .map(release => release.tag_name)); } - async getSammCli(): Promise { - const releaseSelection = this.settings.getSammCliSelection(); - if (releaseSelection.kind === 'customPath') { - return releaseSelection.path; - } else if (releaseSelection.kind === 'noSammCli') { - throw new Error('integrated SAMM-CLI is disabled by user selection.'); - } - + async downloadRelease(releaseTag: string): Promise { const platform = process.platform; - const release = await this.fetchRelease(releaseSelection.releaseTag.trim()); + const release = await this.fetchRelease(releaseTag); const asset = this.selectReleaseAsset(release.assets, platform); const executableName = platform === 'win32' ? 'samm.exe' : 'samm'; @@ -118,9 +116,33 @@ export class SammCliDownloader { return targetPath.fsPath; } + private async runVersionCommand(executablePath: string): Promise { + return new Promise((resolve, reject) => { + const [executable, args] = executablePath.endsWith('.jar') + ? ['java', ['-jar', executablePath, '--version']] + : [executablePath, ['--version']]; + + const child = spawn(executable, args, { env: process.env }); + let output = ''; + child.stdout.on('data', (data: Buffer) => { output += data.toString(); }); + child.stderr.on('data', (data: Buffer) => { output += data.toString(); }); + child.once('close', (code: number | null) => { + const match = output.match(/v\d+\.\d+\.\d+/); + if (match) { + resolve(match[0]); + } else if (code === 0) { + resolve(output.trim()); + } else { + reject(new Error(`--version exited with code ${String(code)}: ${output.trim()}`)); + } + }); + child.once('error', reject); + }); + } + private async fetchRelease(releaseVersion: string): Promise { const releasePath = releaseVersion && releaseVersion !== 'latest' - ? `releases / tags / ${encodeURIComponent(releaseVersion)}` + ? `releases/tags/${encodeURIComponent(releaseVersion)}` : 'releases/latest'; const response = await fetch(`https://api.github.com/repos/${GITHUB_RELEASE_REPOSITORY}/${releasePath}`, { @@ -225,5 +247,3 @@ export class SammCliDownloader { } } } - - diff --git a/extension/src/settings.ts b/extension/src/settings.ts index 2c1944e..92df53e 100644 --- a/extension/src/settings.ts +++ b/extension/src/settings.ts @@ -1,35 +1,20 @@ import * as vscode from 'vscode'; -const SAMM_CLI_SELECTION_CONFIG_KEY = 'sammCliSelection'; -const DEFAULT_SAMM_CLI_RELEASE: ReleaseSammCliSelection = { kind: 'release', releaseTag: 'v2.14.3' }; - -type ReleaseSammCliSelection = { - kind: 'release'; - releaseTag: string; -}; - -type CustomPathSammCliSelection = { - kind: 'customPath'; - path: string; -}; - -type NoSammCliSelection = { - kind: 'noSammCli'; -}; - -export type SammCliSelection = ReleaseSammCliSelection | CustomPathSammCliSelection | NoSammCliSelection; - export class TurtleExtensionSettings { constructor( private readonly context: vscode.ExtensionContext, ) { } - getSammCliSelection(): SammCliSelection { - return this.context.globalState.get(SAMM_CLI_SELECTION_CONFIG_KEY, DEFAULT_SAMM_CLI_RELEASE); + isEmbeddedLanguageServerStartEnabled(): boolean { + return vscode.workspace.getConfiguration('turtle.languageServerSettings').get('activateEmbeddedLanguageServer', true); + } + + getSammCliPath(): string { + return vscode.workspace.getConfiguration('turtle.languageServerSettings').get('sammCliPath', ''); } - async setSammCliSelection(selection: SammCliSelection): Promise { - await this.context.globalState.update(SAMM_CLI_SELECTION_CONFIG_KEY, selection); + async setSammCliPath(path: string): Promise { + await vscode.workspace.getConfiguration('turtle.languageServerSettings').update('sammCliPath', path, vscode.ConfigurationTarget.Global); } sammCliAutoUpdateIsEnabled(): boolean { @@ -44,4 +29,4 @@ export class TurtleExtensionSettings { return vscode.workspace.getConfiguration('turtle.languageServerSettings').get<'off' | 'messages' | 'verbose'>('traceLevel', 'off'); } -} \ No newline at end of file +} From 037a3d0bc59a9056a5b11c2a107e59b40624f692 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Tue, 9 Jun 2026 15:03:45 +0200 Subject: [PATCH 14/22] Fix update checker --- extension/src/extension.ts | 56 +++++++++++++++--------- extension/src/sammCliDownloader.ts | 68 +++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 39 deletions(-) diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 3028e43..8e2b91e 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -44,12 +44,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }) ); - if (settings.sammCliAutoUpdateIsEnabled() && settings.isEmbeddedLanguageServerStartEnabled() && settings.getSammCliPath()) { - sammCliDownloader.checkForSammCliUpdates().catch(error => { - outputChannel.error(`Failed to check for SAMM-CLI updates: ${error instanceof Error ? error.message : String(error)}`); - }); - } - if (settings.isEmbeddedLanguageServerStartEnabled() && !settings.getSammCliPath()) { await vscode.window.showErrorMessage('No SAMM CLI path configured. Required for Language Server functionality.', 'Select or download SAMM CLI Executable') .then(selection => { @@ -62,6 +56,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { queueLanguageServicesRestart('extension activation'); } + if (settings.sammCliAutoUpdateIsEnabled() && settings.isEmbeddedLanguageServerStartEnabled()) { + sammCliDownloader.checkForSammCliUpdates().catch(error => { + outputChannel.error(`Failed to check for SAMM-CLI updates: ${error instanceof Error ? error.message : String(error)}`); + }); + } } function queueLanguageServicesRestart(reason: string): Promise { @@ -106,9 +105,7 @@ async function restartLanguageServices(reason: string): Promise { await stopLanguageServer().catch(() => undefined); aspectValidationController.setClient(createUnavailableClient()); - const message = formatStartupError(error); - outputChannel.error(message); - vscode.window.showErrorMessage(message); + throw error; } const nextClient = new TurtleLanguageClient(outputChannel, settings.getSammCliLspServerPort(), settings.getLanguageClientTraceLevel()); @@ -129,6 +126,7 @@ async function selectSammCliExecutable(): Promise { }); const currentPath = settings.getSammCliPath(); + const currentVersion = currentPath ? await sammCliDownloader.runVersionCommand(currentPath).catch(() => undefined) : undefined; const customPathItem: SammCliQuickPickItem = { label: '$(folder-opened) Use custom SAMM CLI executable or jar', @@ -144,7 +142,7 @@ async function selectSammCliExecutable(): Promise { const releaseItems: SammCliQuickPickItem[] = releases.map(releaseTag => ({ label: releaseTag, - detail: 'Download and use this GitHub release', + detail: `Download and use this GitHub release${currentVersion === releaseTag ? ' (currently configured)' : ''}`, action: 'release', releaseTag, })); @@ -180,15 +178,41 @@ async function selectSammCliExecutable(): Promise { if (!pick.releaseTag) { return; } - const downloadedPath = await sammCliDownloader.downloadRelease(pick.releaseTag); + const downloadType = await promptForDownloadType(); + if (!downloadType) { + return; + } + const downloadedPath = await sammCliDownloader.downloadRelease(pick.releaseTag, downloadType); await settings.setSammCliPath(downloadedPath); - restartReason = `Changed SAMM CLI to GitHub release ${pick.releaseTag}`; + restartReason = `Changed SAMM CLI to GitHub release ${pick.releaseTag} (${downloadType === 'jar' ? 'JAR' : 'native executable'})`; } vscode.window.showInformationMessage(restartReason); await queueLanguageServicesRestart(restartReason); } +async function promptForDownloadType(): Promise<'native' | 'jar' | undefined> { + const items: Array = [ + { + label: '$(file-binary) Native Executable', + detail: 'Platform-specific binary. No Java required.', + value: 'native', + }, + { + label: '$(file-code) JAR (Java Archive)', + detail: 'Platform-independent. Requires a Java runtime on your system.', + value: 'jar', + }, + ]; + + const pick = await vscode.window.showQuickPick(items, { + title: 'Select download type', + placeHolder: 'Choose between native executable or JAR', + }); + + return pick?.value; +} + async function promptForCustomExecutablePath(): Promise { const selection = await vscode.window.showOpenDialog({ canSelectFiles: true, @@ -209,14 +233,6 @@ function createUnavailableClient(): RequestClient { }; } -function formatStartupError(error: unknown): string { - if (error instanceof Error) { - return `Failed to start the Turtle language server: ${error.message}`; - } - - return 'Failed to start the Turtle language server.'; -} - export async function deactivate(): Promise { await restartChain; await languageClient.disconnect(); diff --git a/extension/src/sammCliDownloader.ts b/extension/src/sammCliDownloader.ts index f71add0..a4f1c9e 100644 --- a/extension/src/sammCliDownloader.ts +++ b/extension/src/sammCliDownloader.ts @@ -33,12 +33,14 @@ export class SammCliDownloader { async checkForSammCliUpdates(): Promise { const currentPath = this.settings.getSammCliPath(); - - let currentVersion: string | undefined; - if (currentPath) { - currentVersion = await this.runVersionCommand(currentPath).catch(() => undefined); + if (!currentPath) { + this.outputChannel.info('SAMM-CLI update check skipped: No SAMM-CLI path configured.'); + return; } - + const type = currentPath.endsWith('.jar') ? 'jar' : 'native'; + const currentVersion = await this.runVersionCommand(currentPath).catch(error => { + throw new Error(`Failed to get current SAMM-CLI version: ${error instanceof Error ? error.message : String(error)}`); + }); const latestVersion = await this.getLatestAvailabeSammCliReleaseTag().catch(error => { throw new Error(`Failed to check for latest SAMM-CLI release: ${error instanceof Error ? error.message : String(error)}`); }); @@ -50,7 +52,7 @@ export class SammCliDownloader { vscode.window.showInformationMessage(label, 'Download & Use').then(async (selection) => { if (selection === 'Download & Use') { try { - const newPath = await this.downloadRelease(latestVersion); + const newPath = await this.downloadRelease(latestVersion, type); await this.settings.setSammCliPath(newPath); vscode.window.showInformationMessage(`SAMM-CLI ${latestVersion} has been downloaded and configured for use.`); } catch (error) { @@ -90,33 +92,43 @@ export class SammCliDownloader { .map(release => release.tag_name)); } - async downloadRelease(releaseTag: string): Promise { + async downloadRelease(releaseTag: string, type: 'native' | 'jar' = 'native'): Promise { const platform = process.platform; const release = await this.fetchRelease(releaseTag); - const asset = this.selectReleaseAsset(release.assets, platform); - const executableName = platform === 'win32' ? 'samm.exe' : 'samm'; + const asset = type === 'jar' + // cut 'v' prefix from tag name if present to match asset naming convention (e.g. v1.2.3 -> 1.2.3) + ? release.assets.find(a => a.name.endsWith(`${releaseTag.replace(/^v/, '')}.jar`)) + : this.selectReleaseAsset(release.assets, platform); if (!asset) { throw new Error(`No matching release asset was found in ${GITHUB_RELEASE_REPOSITORY}.`); } + const executableName = type === 'jar' ? 'samm.jar' : (platform === 'win32' ? 'samm.exe' : 'samm'); const targetDirectory = vscode.Uri.joinPath(this.context.globalStorageUri, SAMM_CLI_STORAGE_DIR, release.tag_name); const targetPath = vscode.Uri.joinPath(targetDirectory, executableName); if (await this.fileExists(targetPath.fsPath)) { - await this.ensureExecutableIsReady(targetPath.fsPath, platform, release.tag_name); + if (type === 'native') { + await this.ensureExecutableIsReady(targetPath.fsPath, platform, release.tag_name); + } return targetPath.fsPath; } await mkdir(targetDirectory.fsPath, { recursive: true }); - this.outputChannel.info(`Downloading ${asset.name}(${release.tag_name})...`); - await this.downloadAndExtractAsset(asset.browser_download_url, targetDirectory.fsPath, platform); - await this.ensureExecutableIsReady(targetPath.fsPath, platform, release.tag_name); + this.outputChannel.info(`Downloading ${asset.name} (${release.tag_name})...`); + + if (type === 'jar') { + await this.downloadJarAsset(asset.browser_download_url, targetPath.fsPath); + } else { + await this.downloadAndExtractAsset(asset.browser_download_url, targetDirectory.fsPath, platform); + await this.ensureExecutableIsReady(targetPath.fsPath, platform, release.tag_name); + } return targetPath.fsPath; } - private async runVersionCommand(executablePath: string): Promise { + public async runVersionCommand(executablePath: string): Promise { return new Promise((resolve, reject) => { const [executable, args] = executablePath.endsWith('.jar') ? ['java', ['-jar', executablePath, '--version']] @@ -127,13 +139,11 @@ export class SammCliDownloader { child.stdout.on('data', (data: Buffer) => { output += data.toString(); }); child.stderr.on('data', (data: Buffer) => { output += data.toString(); }); child.once('close', (code: number | null) => { - const match = output.match(/v\d+\.\d+\.\d+/); + const match = output.match(/Version:\s*(\d+\.\d+\.\d+)/i); if (match) { - resolve(match[0]); - } else if (code === 0) { - resolve(output.trim()); + resolve("v" + match[1]); } else { - reject(new Error(`--version exited with code ${String(code)}: ${output.trim()}`)); + reject(new Error(`Unexpected version command output: ${output}`)); } }); child.once('error', reject); @@ -198,6 +208,26 @@ export class SammCliDownloader { await tar.x({ file: archivePath, cwd: extractionPath }); } + private async downloadJarAsset(downloadUrl: string, targetPath: string): Promise { + const response = await fetch(downloadUrl, { + headers: { + 'User-Agent': 'esmf-vs-code-plugin', + Accept: 'application/octet-stream', + }, + }); + + if (!response.ok || !response.body) { + throw new Error(`Failed to download the SAMM-CLI JAR from ${downloadUrl}: ${response.status} ${response.statusText}`); + } + + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Downloading SAMM-CLI JAR...' }, async () => { + await pipeline( + Readable.fromWeb(response.body as unknown as globalThis.ReadableStream), + createWriteStream(targetPath), + ); + }); + } + private selectReleaseAsset(assets: Array, platform: string): GitHubReleaseAsset { let assetOsIdentifier: string; switch (platform) { From e80c2f99a2f6be54b81aaf33f5ac32a09a9ec28c Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Wed, 10 Jun 2026 14:41:42 +0200 Subject: [PATCH 15/22] fix java options + language server timeout --- extension/src/extension.ts | 1 + extension/src/languageServer.ts | 5 +++-- extension/src/sammCliDownloader.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 8e2b91e..930cb24 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -68,6 +68,7 @@ function queueLanguageServicesRestart(reason: string): Promise { .then(() => restartLanguageServices(reason)) .catch(error => { outputChannel.error(`Restart pipeline failed: ${error instanceof Error ? error.message : String(error)}`); + throw error; }); return restartChain; diff --git a/extension/src/languageServer.ts b/extension/src/languageServer.ts index 1a05ec7..5d0eaa2 100644 --- a/extension/src/languageServer.ts +++ b/extension/src/languageServer.ts @@ -3,8 +3,9 @@ import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; import * as net from 'node:net'; import type { ExtensionLogger } from './outputChannel'; -const SERVER_READY_TIMEOUT_MS = 30_000; +const SERVER_READY_TIMEOUT_MS = 10_000; const SERVER_READY_RETRY_DELAY_MS = 250; +export const JAVA_OPTIONS = ['--enable-native-access=ALL-UNNAMED', '--sun-misc-unsafe-memory-access=allow', '-Dpolyglotimpl.DisableMultiReleaseCheck=true']; export class TurtleLanguageServer { private serverProcess: ChildProcessWithoutNullStreams | undefined; @@ -19,7 +20,7 @@ export class TurtleLanguageServer { async start(): Promise { const [executable, args] = this.sammCliExecutablePath.endsWith('.jar') - ? ['java', ['-jar', this.sammCliExecutablePath, 'lsp', '--port', String(this.serverPort)]] + ? ['java', [...JAVA_OPTIONS, '-jar', this.sammCliExecutablePath, 'lsp', '--port', String(this.serverPort)]] : [this.sammCliExecutablePath, ['lsp', '--port', String(this.serverPort)]]; this.outputChannel.info(`Starting language server: ${executable} ${args.join(' ')}`); diff --git a/extension/src/sammCliDownloader.ts b/extension/src/sammCliDownloader.ts index a4f1c9e..15698d4 100644 --- a/extension/src/sammCliDownloader.ts +++ b/extension/src/sammCliDownloader.ts @@ -8,6 +8,7 @@ import extractZip = require('extract-zip'); import * as tar from 'tar'; import { TurtleExtensionSettings } from './settings'; import type { ExtensionLogger } from './outputChannel'; +import {JAVA_OPTIONS} from './languageServer'; interface GitHubReleaseAsset { name: string; @@ -131,7 +132,7 @@ export class SammCliDownloader { public async runVersionCommand(executablePath: string): Promise { return new Promise((resolve, reject) => { const [executable, args] = executablePath.endsWith('.jar') - ? ['java', ['-jar', executablePath, '--version']] + ? ['java', [...JAVA_OPTIONS, '-jar', executablePath, '--version']] : [executablePath, ['--version']]; const child = spawn(executable, args, { env: process.env }); From 5eb16801bfff7d05da59b47590c17ade3df98760 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Wed, 10 Jun 2026 15:23:06 +0200 Subject: [PATCH 16/22] Code style improvements --- extension/package.json | 5 +- extension/src/aspectValidation.ts | 15 ++++- extension/src/extension.ts | 33 ++++++--- extension/src/languageClient.ts | 13 ++++ extension/src/languageServer.ts | 15 ++++- extension/src/outputChannel.ts | 13 ++++ extension/src/sammCliDownloader.ts | 74 ++++++++++++++++++--- extension/src/settings.ts | 13 ++++ extension/src/test/validationTestHarness.ts | 13 ++++ 9 files changed, 171 insertions(+), 23 deletions(-) diff --git a/extension/package.json b/extension/package.json index a4e08ce..cd8b5d9 100644 --- a/extension/package.json +++ b/extension/package.json @@ -11,7 +11,7 @@ "url": "https://github.com/eclipse-esmf/esmf-vs-code-plugin.git" }, "bugs": { - "url": "https://github.com/eclipse-esmf/eclipse-vs-code-plugin/issues" + "url": "https://github.com/eclipse-esmf/esmf-vs-code-plugin/issues" }, "engines": { "vscode": "^1.110.0" @@ -20,6 +20,9 @@ "Other" ], "main": "./out/extension.js", + "activationEvents": [ + "onLanguage:turtle" + ], "contributes": { "commands": [ { diff --git a/extension/src/aspectValidation.ts b/extension/src/aspectValidation.ts index 875d7e7..3e121e2 100644 --- a/extension/src/aspectValidation.ts +++ b/extension/src/aspectValidation.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + import * as vscode from 'vscode'; import type { ExtensionLogger } from './outputChannel'; @@ -35,7 +48,7 @@ export interface ValidationWorkspace { onDidSaveTextDocument(listener: (document: vscode.TextDocument) => void): vscode.Disposable; } -export interface ValidationOutputChannel extends ExtensionLogger {} +export type ValidationOutputChannel = ExtensionLogger; export class AspectValidationController { constructor( diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 930cb24..ae724f6 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + import * as vscode from 'vscode'; import { AspectValidationController, RequestClient } from './aspectValidation'; import { TurtleLanguageServer } from './languageServer'; @@ -7,6 +20,7 @@ import { TurtleLanguageClient } from './languageClient'; import type { ExtensionLogger } from './outputChannel'; const SELECT_EXECUTABLE_COMMAND = 'turtle.selectSammCliExecutable'; +const SELECT_EXECUTABLE_TITLE = 'Select SAMM-CLI Executable'; const RESTART_LANGUAGE_SERVICES_COMMAND = 'turtle.restartLanguageServices'; let settings: TurtleExtensionSettings; @@ -45,17 +59,18 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ); if (settings.isEmbeddedLanguageServerStartEnabled() && !settings.getSammCliPath()) { - await vscode.window.showErrorMessage('No SAMM CLI path configured. Required for Language Server functionality.', 'Select or download SAMM CLI Executable') - .then(selection => { - if (selection) { - selectSammCliExecutable(); - } - }); + const selection = await vscode.window.showErrorMessage( + 'No SAMM CLI path configured. Required for Language Server functionality.', + 'Select or download SAMM CLI Executable', + ); + if (selection) { + await selectSammCliExecutable(); + } return; - } else { - queueLanguageServicesRestart('extension activation'); } + void queueLanguageServicesRestart('extension activation'); + if (settings.sammCliAutoUpdateIsEnabled() && settings.isEmbeddedLanguageServerStartEnabled()) { sammCliDownloader.checkForSammCliUpdates().catch(error => { outputChannel.error(`Failed to check for SAMM-CLI updates: ${error instanceof Error ? error.message : String(error)}`); @@ -229,7 +244,7 @@ async function promptForCustomExecutablePath(): Promise { function createUnavailableClient(): RequestClient { return { sendRequest: async () => { - throw new Error(`The Turtle language server is not available yet. Run '${SELECT_EXECUTABLE_COMMAND}' and then reconnect.`); + throw new Error(`The Turtle language server is not available yet. Run '${SELECT_EXECUTABLE_TITLE}' and then reconnect.`); }, }; } diff --git a/extension/src/languageClient.ts b/extension/src/languageClient.ts index 4ea5c34..2e12733 100644 --- a/extension/src/languageClient.ts +++ b/extension/src/languageClient.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + import { Trace } from 'vscode-jsonrpc'; import * as net from 'node:net'; import * as vscode from 'vscode'; diff --git a/extension/src/languageServer.ts b/extension/src/languageServer.ts index 5d0eaa2..4bda81d 100644 --- a/extension/src/languageServer.ts +++ b/extension/src/languageServer.ts @@ -1,11 +1,24 @@ +/* + * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + import * as vscode from 'vscode'; import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; import * as net from 'node:net'; import type { ExtensionLogger } from './outputChannel'; +import { JAVA_OPTIONS } from './constants'; const SERVER_READY_TIMEOUT_MS = 10_000; const SERVER_READY_RETRY_DELAY_MS = 250; -export const JAVA_OPTIONS = ['--enable-native-access=ALL-UNNAMED', '--sun-misc-unsafe-memory-access=allow', '-Dpolyglotimpl.DisableMultiReleaseCheck=true']; export class TurtleLanguageServer { private serverProcess: ChildProcessWithoutNullStreams | undefined; diff --git a/extension/src/outputChannel.ts b/extension/src/outputChannel.ts index f999dd2..3d559ca 100644 --- a/extension/src/outputChannel.ts +++ b/extension/src/outputChannel.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + /** * Minimal structured-logging interface shared by all extension modules. * The real implementation is `vscode.LogOutputChannel`; tests supply a fake. diff --git a/extension/src/sammCliDownloader.ts b/extension/src/sammCliDownloader.ts index 15698d4..8acdba7 100644 --- a/extension/src/sammCliDownloader.ts +++ b/extension/src/sammCliDownloader.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + import * as vscode from 'vscode'; import { constants, createWriteStream } from 'node:fs'; import { access, mkdir, rm, stat } from 'node:fs/promises'; @@ -5,10 +18,11 @@ import { spawn } from 'node:child_process'; import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import extractZip = require('extract-zip'); +import * as path from 'node:path'; import * as tar from 'tar'; import { TurtleExtensionSettings } from './settings'; import type { ExtensionLogger } from './outputChannel'; -import {JAVA_OPTIONS} from './languageServer'; +import { GITHUB_RELEASE_REPOSITORY, JAVA_OPTIONS } from './constants'; interface GitHubReleaseAsset { name: string; @@ -22,8 +36,10 @@ interface GitHubRelease { prerelease?: boolean; } -const GITHUB_RELEASE_REPOSITORY = 'eclipse-esmf/esmf-sdk'; const SAMM_CLI_STORAGE_DIR = 'samm-cli'; +// GitHub returns up to 100 releases per page; fetch a full page so that +// filtering out drafts/prereleases still leaves enough stable releases. +const RELEASES_PAGE_SIZE = 100; export class SammCliDownloader { constructor( @@ -42,11 +58,11 @@ export class SammCliDownloader { const currentVersion = await this.runVersionCommand(currentPath).catch(error => { throw new Error(`Failed to get current SAMM-CLI version: ${error instanceof Error ? error.message : String(error)}`); }); - const latestVersion = await this.getLatestAvailabeSammCliReleaseTag().catch(error => { + const latestVersion = await this.getLatestAvailableSammCliReleaseTag().catch(error => { throw new Error(`Failed to check for latest SAMM-CLI release: ${error instanceof Error ? error.message : String(error)}`); }); - if (currentVersion !== latestVersion) { + if (this.compareVersions(latestVersion, currentVersion) > 0) { const label = currentVersion ? `There is a new SAMM-CLI release available: ${currentVersion} -> ${latestVersion}` : `SAMM-CLI ${latestVersion} is available`; @@ -65,7 +81,7 @@ export class SammCliDownloader { } } - private async getLatestAvailabeSammCliReleaseTag(): Promise { + private async getLatestAvailableSammCliReleaseTag(): Promise { const releases = await this.getRecentSammCliReleaseTags(1); if (releases.length === 0) { @@ -76,7 +92,7 @@ export class SammCliDownloader { } async getRecentSammCliReleaseTags(limit: number): Promise> { - const response = await fetch(`https://api.github.com/repos/${GITHUB_RELEASE_REPOSITORY}/releases?per_page=${limit}`, { + const response = await fetch(`https://api.github.com/repos/${GITHUB_RELEASE_REPOSITORY}/releases?per_page=${RELEASES_PAGE_SIZE}`, { headers: { 'User-Agent': 'esmf-vs-code-plugin', Accept: 'application/vnd.github+json', @@ -84,13 +100,14 @@ export class SammCliDownloader { }); if (!response.ok) { - return Promise.reject(new Error(`Failed to fetch SAMM-CLI releases from GitHub: ${response.status} ${response.statusText}`)); + return Promise.reject(new Error(this.describeGitHubError('Failed to fetch SAMM-CLI releases from GitHub', response))); } - const fetchedReleases = response.json() as Promise>; - return fetchedReleases.then(releases => releases + const releases = await response.json() as Array; + return releases .filter(release => !release.draft && !release.prerelease) - .map(release => release.tag_name)); + .map(release => release.tag_name) + .slice(0, limit); } async downloadRelease(releaseTag: string, type: 'native' | 'jar' = 'native'): Promise { @@ -165,7 +182,7 @@ export class SammCliDownloader { if (!response.ok) { const target = releaseVersion && releaseVersion !== 'latest' ? `release ${releaseVersion}` : 'the latest release'; - throw new Error(`Failed to fetch samm-cli ${target}: ${response.status} ${response.statusText}`); + throw new Error(this.describeGitHubError(`Failed to fetch samm-cli ${target}`, response)); } return response.json() as Promise; @@ -270,6 +287,41 @@ export class SammCliDownloader { } } + /** + * Compares two SAMM-CLI version tags (e.g. `v2.10.0`, `2.12.0`). + * Returns a positive number when `a` is newer than `b`, a negative number + * when it is older, and `0` when they are equal. Pre-release suffixes are + * ignored. An unparsable/empty version is treated as the oldest. + */ + private compareVersions(a: string, b: string): number { + const parse = (version: string): Array => { + const match = version.match(/(\d+)\.(\d+)\.(\d+)/); + return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : [-1, -1, -1]; + }; + + const left = parse(a); + const right = parse(b); + + for (let i = 0; i < left.length; i++) { + if (left[i] !== right[i]) { + return left[i] - right[i]; + } + } + + return 0; + } + + private describeGitHubError(prefix: string, response: Response): string { + const isRateLimited = response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0'; + if (isRateLimited) { + const reset = response.headers.get('x-ratelimit-reset'); + const resetHint = reset ? ` Try again after ${new Date(Number(reset) * 1000).toLocaleTimeString()}.` : ''; + return `${prefix}: GitHub API rate limit exceeded.${resetHint}`; + } + + return `${prefix}: ${response.status} ${response.statusText}`; + } + private async fileExists(filePath: string): Promise { try { return (await stat(filePath)).isFile(); diff --git a/extension/src/settings.ts b/extension/src/settings.ts index 92df53e..58c8e9d 100644 --- a/extension/src/settings.ts +++ b/extension/src/settings.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + import * as vscode from 'vscode'; export class TurtleExtensionSettings { diff --git a/extension/src/test/validationTestHarness.ts b/extension/src/test/validationTestHarness.ts index f5f3255..cc83f9f 100644 --- a/extension/src/test/validationTestHarness.ts +++ b/extension/src/test/validationTestHarness.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + import * as vscode from 'vscode'; import { AspectValidationController, From a0b354fe0d8a43c256f141a508a67a586b241d1f Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Wed, 10 Jun 2026 15:43:35 +0200 Subject: [PATCH 17/22] Fix docs --- README.md | 76 ++++++++++++++++++++++++++- extension/README.md | 32 ++++------- extension/package.json | 29 ++++------ extension/src/constants.ts | 22 ++++++++ extension/vsc-extension-quickstart.md | 32 ++++++----- 5 files changed, 137 insertions(+), 54 deletions(-) create mode 100644 extension/src/constants.ts diff --git a/README.md b/README.md index 0c725c6..3983303 100644 --- a/README.md +++ b/README.md @@ -1 +1,75 @@ -# esmf-vs-code-plugin \ No newline at end of file +# esmf-vs-code-plugin + +VS Code extension for the ESMF SDK Turtle language server. The extension supports prefix Go to Definition, fast syntax feedback while typing, and server-driven heavy Aspect validation for SAMM-style Turtle models. + +## Project Structure + +This repository contains the VS Code extension source code. For implementation details and architecture, see the [extension documentation](extension/README.md). + +``` +extension/ - VS Code extension source code and build outputs + src/ - TypeScript source files + samples/ - Example Turtle and SAMM Aspect Model files + media/ - Walkthrough and logo images + package.json - Extension manifest and dependencies + README.md - Extension user and feature documentation +``` + +## Getting Started + +### Prerequisites + +- Node.js and npm +- VS Code 1.110.0 or later + +### Development Setup + +1. Clone and navigate to the extension folder: + ```bash + cd extension + npm install + ``` + +2. Build the extension: + ```bash + npm run build + ``` + +3. Run or debug the extension: + - Press `F5` in VS Code to launch the Extension Development Host with the extension loaded. + - Open a `.ttl` file (e.g., `extension/samples/valid.ttl`) to activate the extension. + +### Available Commands + +- `npm run build` - Compile TypeScript +- `npm run watch` - Watch and recompile on file changes +- `npm run lint` - Run ESLint +- `npm run lint:fix` - Fix linting issues +- `npm run prettier` - Format code with Prettier +- `npm run test` - Run tests +- `npm run test:coverage` - Run tests with coverage report + +## Features + +- **Prefix Go to Definition** - Jump to prefix definitions in Turtle files. +- **Fast Validation** - Real-time syntax feedback via the language server. +- **Aspect Validation** - Heavy model-level validation for SAMM Aspect models (on save and on-demand). +- **Interactive Setup** - Download or select SAMM-CLI executable via the command palette. +- **Configuration** - Customize language server settings (port, trace level, auto-update). + +See [extension/README.md](extension/README.md) for complete feature documentation and usage instructions. + +## Contributing + +We welcome contributions! Please see [extension/CONTRIBUTING.md](extension/CONTRIBUTING.md) for: +- Contribution guidelines +- Branch and PR naming conventions +- Commit message standards +- License and copyright header requirements +- Code conventions and style guides + +All contributions must comply with the Eclipse Contributor Agreement (ECA) and Eclipse IP Policy. + +## License + +This project is licensed under the Mozilla Public License v. 2.0. See LICENSE file for details. diff --git a/extension/README.md b/extension/README.md index 99ac52d..2d92a27 100644 --- a/extension/README.md +++ b/extension/README.md @@ -23,13 +23,10 @@ Use the command `Turtle: Select SAMM-CLI Executable` to choose either: - Prefix `Go to Definition` inside Turtle files. - Two-level validation: - - Fast feedback on type from the regular Turtle parser diagnostics provided by the server. - - Heavy Aspect validation from the server for model-level issues. + - Fast feedback on type from the regular Turtle parser diagnostics provided by the server (appear in the editor and `Problems`). + - Heavy Aspect validation from the server for model-level issues (results shown in notifications and status bar). - Manual validation command: - `Turtle: Validate document now` -- Standard diagnostics flow: - - errors appear in the editor - - errors appear in `Problems` ## Run The Server And Extension Together @@ -45,12 +42,13 @@ If the server cannot be downloaded or started, the extension shows an error and Fast feedback on type: - Driven by the server's regular Turtle parsing diagnostics. +- Results appear in the editor and `Problems` panel. - Intended for quick editor feedback while you type. Heavy Aspect validation: - Runs on the server, not in the extension. -- Uses standard LSP diagnostics so the result appears in the editor and in `Problems`. +- Results are displayed in notification messages (for manual validation) or status bar (for save-triggered validation). - Always uses detailed server validation messaging when the server returns report text. - Always shows visible progress for long-running validation. - Runs automatically on save and can also be triggered manually. @@ -74,12 +72,12 @@ When each validation runs: - Manual validation shows a progress notification while the request is running. - Save-triggered validation always uses a short status-bar progress indicator instead of repeated popups. -- After completion, the user gets a summary that includes the first detailed server report line when available. +- After completion, the user gets a summary message with validation results. - Automatic save validation keeps progress and completion feedback in the status bar. ## Verify Go To Definition -Use [samples/valid.ttl](extension/samples/valid.ttl): +Use [samples/valid.ttl](samples/valid.ttl): 1. Open `samples/valid.ttl`. 2. Place the cursor on `foaf:Person`, `foaf:name`, or another prefixed name. @@ -93,27 +91,17 @@ Expected behavior: ## Verify Aspect Validation -Use an Aspect model file, for example: - -```turtle -@prefix : . -@prefix samm: . - -:InvalidSyntax a samm:Aspect; - samm:preferredName "Test Aspect"@en - samm:properties () ; - samm:operations () . -``` +Use [samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl](samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl) or [samples/invalid.ttl](samples/invalid.ttl). Manual check: -1. Open the model file. +1. Open an Aspect model file. 2. Run `Turtle: Validate document now`. 3. Wait for the progress indicator to finish. -4. Confirm that diagnostics appear in the editor and in `Problems`. +4. Confirm that validation results appear in a notification message. On-save check: 1. Save the model file. 2. Confirm that the status bar shows validation progress. -3. Confirm that diagnostics are refreshed after completion. +3. Confirm that a summary message appears in the status bar after completion. diff --git a/extension/package.json b/extension/package.json index cd8b5d9..f49d0ea 100644 --- a/extension/package.json +++ b/extension/package.json @@ -108,13 +108,6 @@ } }, "menus": { - "editor/title": [ - { - "command": "turtle.validateDocumentNow", - "when": "resourceLangId == turtle", - "group": "navigation" - } - ], "editor/context": [ { "command": "turtle.validateDocumentNow", @@ -129,6 +122,15 @@ "title": "Getting Started with RDF/Turtle and SAMM Aspect Models Extension", "description": "Learn how to use the RDF/Turtle and SAMM Aspect Models extension to validate your Turtle files and SAMM aspect models.\nThis walkthrough will guide you through the key features of the extension, including validating your Turtle files and SAMM aspect models using the integrated language server.", "steps": [ + { + "id": "select-samm-cli", + "title": "Selecting a SAMM-CLI Executable", + "description": "Initial setup requires you to either Download or manually select a SAMM CLI, containing the required SAMM Lanuage Server \n[Download or Select SAMM CLI](command:turtle.selectSammCliExecutable)", + "media": { + "image": "media/walkthrough_select_samm_cli.png", + "altText": "Example of selecting a SAMM-CLI executable" + } + }, { "id": "validation", "title": "Validating Turtle Files and SAMM Aspect Models", @@ -136,16 +138,7 @@ "media": { "image": "media/walkthrough_validation.png", "altText": "Example of validation issues shown in the Problems panel" - } - }, - { - "id": "select-samm-cli", - "title": "(Optional) Selecting a SAMM-CLI Executable", - "description": "If you have a specific version of SAMM-CLI that you want to use for validation, you can select it using the 'Select SAMM-CLI Executable' command from the Command Palette. This allows you to choose between different versions of SAMM-CLI installed on your system or downloaded by the extension.\n[Select SAMM CLI](command:turtle.selectSammCliExecutable)", - "media": { - "image": "media/walkthrough_select_samm_cli.png", - "altText": "Example of selecting a SAMM-CLI executable" - } + } } ] } @@ -179,4 +172,4 @@ "tar": "^7.5.15", "vscode-languageclient": "^9.0.1" } -} +} \ No newline at end of file diff --git a/extension/src/constants.ts b/extension/src/constants.ts new file mode 100644 index 0000000..8b2a984 --- /dev/null +++ b/extension/src/constants.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +/** JVM options passed to the SAMM-CLI when it is launched from a JAR. */ +export const JAVA_OPTIONS = [ + '--enable-native-access=ALL-UNNAMED', + '--sun-misc-unsafe-memory-access=allow', + '-Dpolyglotimpl.DisableMultiReleaseCheck=true', +]; + +/** GitHub repository hosting the SAMM-CLI releases. */ +export const GITHUB_RELEASE_REPOSITORY = 'eclipse-esmf/esmf-sdk'; diff --git a/extension/vsc-extension-quickstart.md b/extension/vsc-extension-quickstart.md index d727ac7..f56d544 100644 --- a/extension/vsc-extension-quickstart.md +++ b/extension/vsc-extension-quickstart.md @@ -1,17 +1,23 @@ -# Welcome to your VS Code Extension +# RDF/Turtle and SAMM Aspect Models VS Code Extension -## What's in the folder +## Project Structure -* This folder contains all of the files necessary for your extension. -* `package.json` - this is the manifest file in which you declare your extension and command. - * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. -* `src/extension.ts` - this is the main file where you will provide the implementation of your command. - * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. - * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. +This folder contains the VS Code extension for the ESMF SDK Turtle language server. -## Get up and running straight away +* `package.json` - Extension manifest declaring commands, language support, settings, and walkthrough. +* `src/extension.ts` - Main extension entry point; handles activation, configuration, and language server lifecycle. +* `src/languageServer.ts` - Spawns and manages the SAMM-CLI language server process. +* `src/languageClient.ts` - Connects the VS Code language client to the server. +* `src/aspectValidation.ts` - Handles aspect model validation workflows (manual and on-save). +* `src/sammCliDownloader.ts` - Downloads and manages SAMM-CLI releases from GitHub. +* `src/settings.ts` - Reads and manages extension configuration. +* `samples/` - Example Turtle and Aspect model files for testing. -* Press `F5` to open a new window with your extension loaded. -* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. -* Set breakpoints in your code inside `src/extension.ts` to debug your extension. -* Find output from your extension in the debug console. +## Getting Started + +1. Install dependencies: `npm install` (in `extension/` folder) +2. Build the extension: `npm run build` +3. Press `F5` to open an Extension Development Host with the extension loaded. +4. Open a `.ttl` file (e.g., `samples/valid.ttl`) to activate the extension. + +See [README.md](README.md) for full feature documentation and usage instructions. From abda2cd8044fd9d70c6813764a55ebbd0426b805 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Thu, 11 Jun 2026 09:03:02 +0200 Subject: [PATCH 18/22] Fix file path in release pipeline --- .github/workflows/release-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index 2c16a62..5a8beec 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -71,7 +71,7 @@ jobs: uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f #v6.0.0 with: name: turtle-vsix - path: extension/turtle-${{ env.RELEASE_VERSION }}.vsix + path: turtle-${{ env.RELEASE_VERSION }}.vsix if-no-files-found: error - name: Publish to VS Code Marketplace From 6eb606888e69171a2ad56e04f7e674539c9f0de8 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Thu, 11 Jun 2026 09:37:35 +0200 Subject: [PATCH 19/22] Re-organize folder structure --- .gitignore | 8 +- extension/.prettierrc => .prettierrc | 0 .../.vscode-test.mjs => .vscode-test.mjs | 0 .vscode/launch.json | 2 +- .vscode/settings.json | 12 +- .vscode/tasks.json | 2 +- extension/.vscodeignore => .vscodeignore | 0 extension/CONTRIBUTING.md => CONTRIBUTING.md | 0 extension/CONVENTIONS.md => CONVENTIONS.md | 0 extension/LICENSE => LICENSE | 0 README.md | 140 +++++++++++------- .../eslint.config.mjs => eslint.config.mjs | 0 extension/.gitignore | 7 - extension/.vscode/extensions.json | 8 - extension/.vscode/launch.json | 21 --- extension/.vscode/settings.json | 11 -- extension/.vscode/tasks.json | 20 --- extension/README.md | 107 ------------- extension/vsc-extension-quickstart.md | 23 --- ...ration.json => language-configuration.json | 0 {extension/media => media}/esmf-logo.png | Bin .../walkthrough_select_samm_cli.png | Bin .../walkthrough_validation.png | Bin .../package-lock.json => package-lock.json | 0 extension/package.json => package.json | 0 {extension/samples => samples}/Moevement.ttl | 0 {extension/samples => samples}/invalid.ttl | 0 .../org.eclipse.esmf.test/1.0.0/Aspect.ttl | 0 {extension/samples => samples}/valid.ttl | 0 {extension/src => src}/aspectValidation.ts | 0 {extension/src => src}/constants.ts | 0 {extension/src => src}/extension.ts | 3 +- {extension/src => src}/languageClient.ts | 0 {extension/src => src}/languageServer.ts | 0 {extension/src => src}/outputChannel.ts | 0 {extension/src => src}/sammCliDownloader.ts | 5 +- {extension/src => src}/settings.ts | 0 .../test/aspectValidationController.test.ts | 0 .../src => src}/test/validationTestHarness.ts | 0 extension/tsconfig.json => tsconfig.json | 0 40 files changed, 107 insertions(+), 262 deletions(-) rename extension/.prettierrc => .prettierrc (100%) rename extension/.vscode-test.mjs => .vscode-test.mjs (100%) rename extension/.vscodeignore => .vscodeignore (100%) rename extension/CONTRIBUTING.md => CONTRIBUTING.md (100%) rename extension/CONVENTIONS.md => CONVENTIONS.md (100%) rename extension/LICENSE => LICENSE (100%) rename extension/eslint.config.mjs => eslint.config.mjs (100%) delete mode 100644 extension/.gitignore delete mode 100644 extension/.vscode/extensions.json delete mode 100644 extension/.vscode/launch.json delete mode 100644 extension/.vscode/settings.json delete mode 100644 extension/.vscode/tasks.json delete mode 100644 extension/README.md delete mode 100644 extension/vsc-extension-quickstart.md rename extension/language-configuration.json => language-configuration.json (100%) rename {extension/media => media}/esmf-logo.png (100%) rename {extension/media => media}/walkthrough_select_samm_cli.png (100%) rename {extension/media => media}/walkthrough_validation.png (100%) rename extension/package-lock.json => package-lock.json (100%) rename extension/package.json => package.json (100%) rename {extension/samples => samples}/Moevement.ttl (100%) rename {extension/samples => samples}/invalid.ttl (100%) rename {extension/samples => samples}/org.eclipse.esmf.test/1.0.0/Aspect.ttl (100%) rename {extension/samples => samples}/valid.ttl (100%) rename {extension/src => src}/aspectValidation.ts (100%) rename {extension/src => src}/constants.ts (100%) rename {extension/src => src}/extension.ts (98%) rename {extension/src => src}/languageClient.ts (100%) rename {extension/src => src}/languageServer.ts (100%) rename {extension/src => src}/outputChannel.ts (100%) rename {extension/src => src}/sammCliDownloader.ts (98%) rename {extension/src => src}/settings.ts (100%) rename {extension/src => src}/test/aspectValidationController.test.ts (100%) rename {extension/src => src}/test/validationTestHarness.ts (100%) rename extension/tsconfig.json => tsconfig.json (100%) diff --git a/.gitignore b/.gitignore index 723ef36..ccd0b2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -.idea \ No newline at end of file +out +dist +node_modules +.vscode-test/ +*.vsix +logs +.npm/ diff --git a/extension/.prettierrc b/.prettierrc similarity index 100% rename from extension/.prettierrc rename to .prettierrc diff --git a/extension/.vscode-test.mjs b/.vscode-test.mjs similarity index 100% rename from extension/.vscode-test.mjs rename to .vscode-test.mjs diff --git a/.vscode/launch.json b/.vscode/launch.json index 7f8e5cd..48fe93a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "extensionHost", "request": "launch", "args": [ - "--extensionDevelopmentPath=${workspaceFolder}/extension" + "--extensionDevelopmentPath=${workspaceFolder}" ], "sourceMaps": true, "outFiles": [ diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ca4d0b..afdab66 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,11 @@ +// Place your settings in this file to overwrite default and user settings. { - "java.compile.nullAnalysis.mode": "automatic" -} \ No newline at end of file + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d7a5236..9e2efe4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "command": "npm", "args": [ "--prefix", - "${workspaceFolder}/extension", + "${workspaceFolder}", "run", "build" ], diff --git a/extension/.vscodeignore b/.vscodeignore similarity index 100% rename from extension/.vscodeignore rename to .vscodeignore diff --git a/extension/CONTRIBUTING.md b/CONTRIBUTING.md similarity index 100% rename from extension/CONTRIBUTING.md rename to CONTRIBUTING.md diff --git a/extension/CONVENTIONS.md b/CONVENTIONS.md similarity index 100% rename from extension/CONVENTIONS.md rename to CONVENTIONS.md diff --git a/extension/LICENSE b/LICENSE similarity index 100% rename from extension/LICENSE rename to LICENSE diff --git a/README.md b/README.md index 3983303..2d92a27 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,107 @@ -# esmf-vs-code-plugin +# RDF/Turtle and SAMM Aspect Models -VS Code extension for the ESMF SDK Turtle language server. The extension supports prefix Go to Definition, fast syntax feedback while typing, and server-driven heavy Aspect validation for SAMM-style Turtle models. +VS Code extension for the ESMF SDK Turtle language server. The extension supports prefix `Go to Definition`, fast syntax feedback while typing, and server-driven heavy Aspect validation for SAMM-style Turtle models. -## Project Structure +## Configuration -This repository contains the VS Code extension source code. For implementation details and architecture, see the [extension documentation](extension/README.md). +- `turtle.languageServerSettings.activateEmbeddedLanguageServer` (boolean, default: `true`) + - When enabled, the extension starts the SAMM-CLI language server process. When disabled, an external language server must be started manually. +- `turtle.languageServerSettings.automaticUpdateCheck` (boolean, default: `true`) + - Automatically check for updates of the SAMM-CLI language server and notify when a new version is available. +- `turtle.languageServerSettings.sammCliPath` (string) + - Path to the SAMM CLI executable or jar file to use as the language server. Can be downloaded / set via the 'Select SAMM-CLI Executable' command. +- `turtle.languageServerSettings.serverPort` (number, default: `1846`) + - TCP port used to connect to the Turtle/SAMM language server. +- `turtle.languageServerSettings.traceLevel` (string, default: `off`) + - Controls the verbosity of language client protocol tracing. Options: `off`, `messages`, `verbose`. -``` -extension/ - VS Code extension source code and build outputs - src/ - TypeScript source files - samples/ - Example Turtle and SAMM Aspect Model files - media/ - Walkthrough and logo images - package.json - Extension manifest and dependencies - README.md - Extension user and feature documentation -``` +Use the command `Turtle: Select SAMM-CLI Executable` to choose either: +- one of the latest 10 SAMM-CLI GitHub releases, or +- a custom executable path from your local file system. -## Getting Started +## Features -### Prerequisites +- Prefix `Go to Definition` inside Turtle files. +- Two-level validation: + - Fast feedback on type from the regular Turtle parser diagnostics provided by the server (appear in the editor and `Problems`). + - Heavy Aspect validation from the server for model-level issues (results shown in notifications and status bar). +- Manual validation command: + - `Turtle: Validate document now` -- Node.js and npm -- VS Code 1.110.0 or later +## Run The Server And Extension Together -### Development Setup +1. In this extension project, install dependencies with `npm install`. +2. Compile the extension with `npm run build`. +3. Press `F5` in VS Code to open an Extension Development Host. +4. Open a Turtle file such as [samples/valid.ttl](samples/valid.ttl) or your Aspect model file. -1. Clone and navigate to the extension folder: - ```bash - cd extension - npm install - ``` +If the server cannot be downloaded or started, the extension shows an error and leaves a detailed message in the Turtle LSP output channel. -2. Build the extension: - ```bash - npm run build - ``` +## Validation Behavior -3. Run or debug the extension: - - Press `F5` in VS Code to launch the Extension Development Host with the extension loaded. - - Open a `.ttl` file (e.g., `extension/samples/valid.ttl`) to activate the extension. +Fast feedback on type: -### Available Commands +- Driven by the server's regular Turtle parsing diagnostics. +- Results appear in the editor and `Problems` panel. +- Intended for quick editor feedback while you type. -- `npm run build` - Compile TypeScript -- `npm run watch` - Watch and recompile on file changes -- `npm run lint` - Run ESLint -- `npm run lint:fix` - Fix linting issues -- `npm run prettier` - Format code with Prettier -- `npm run test` - Run tests -- `npm run test:coverage` - Run tests with coverage report +Heavy Aspect validation: -## Features +- Runs on the server, not in the extension. +- Results are displayed in notification messages (for manual validation) or status bar (for save-triggered validation). +- Always uses detailed server validation messaging when the server returns report text. +- Always shows visible progress for long-running validation. +- Runs automatically on save and can also be triggered manually. + +When each validation runs: + +- On type: fast syntax feedback only. +- On save: heavy Aspect validation for Turtle documents. +- Manual: `Turtle: Validate document now` for the active Turtle document. + +## Commands + +- `Turtle: Validate document now` + - Sends a server request for the active Turtle document. +- `Turtle: Select SAMM-CLI Executable` + - Opens a quick pick with the latest 10 GitHub releases and a custom-path option. +- `Turtle: Restart and reconnect to Language Server` + - Restarts the language server and reconnects the client. + +## UX During Long-Running Validation + +- Manual validation shows a progress notification while the request is running. +- Save-triggered validation always uses a short status-bar progress indicator instead of repeated popups. +- After completion, the user gets a summary message with validation results. +- Automatic save validation keeps progress and completion feedback in the status bar. + +## Verify Go To Definition + +Use [samples/valid.ttl](samples/valid.ttl): + +1. Open `samples/valid.ttl`. +2. Place the cursor on `foaf:Person`, `foaf:name`, or another prefixed name. +3. Run `Go to Definition`. +4. Confirm that VS Code jumps to the matching `@prefix` declaration. + +Expected behavior: -- **Prefix Go to Definition** - Jump to prefix definitions in Turtle files. -- **Fast Validation** - Real-time syntax feedback via the language server. -- **Aspect Validation** - Heavy model-level validation for SAMM Aspect models (on save and on-demand). -- **Interactive Setup** - Download or select SAMM-CLI executable via the command palette. -- **Configuration** - Customize language server settings (port, trace level, auto-update). +- `foaf:*` resolves to `@prefix foaf: ...`. +- `ex:*` resolves to `@prefix ex: ...`. -See [extension/README.md](extension/README.md) for complete feature documentation and usage instructions. +## Verify Aspect Validation -## Contributing +Use [samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl](samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl) or [samples/invalid.ttl](samples/invalid.ttl). -We welcome contributions! Please see [extension/CONTRIBUTING.md](extension/CONTRIBUTING.md) for: -- Contribution guidelines -- Branch and PR naming conventions -- Commit message standards -- License and copyright header requirements -- Code conventions and style guides +Manual check: -All contributions must comply with the Eclipse Contributor Agreement (ECA) and Eclipse IP Policy. +1. Open an Aspect model file. +2. Run `Turtle: Validate document now`. +3. Wait for the progress indicator to finish. +4. Confirm that validation results appear in a notification message. -## License +On-save check: -This project is licensed under the Mozilla Public License v. 2.0. See LICENSE file for details. +1. Save the model file. +2. Confirm that the status bar shows validation progress. +3. Confirm that a summary message appears in the status bar after completion. diff --git a/extension/eslint.config.mjs b/eslint.config.mjs similarity index 100% rename from extension/eslint.config.mjs rename to eslint.config.mjs diff --git a/extension/.gitignore b/extension/.gitignore deleted file mode 100644 index ccd0b2b..0000000 --- a/extension/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -out -dist -node_modules -.vscode-test/ -*.vsix -logs -.npm/ diff --git a/extension/.vscode/extensions.json b/extension/.vscode/extensions.json deleted file mode 100644 index 186459d..0000000 --- a/extension/.vscode/extensions.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "dbaeumer.vscode-eslint", - "ms-vscode.extension-test-runner" - ] -} diff --git a/extension/.vscode/launch.json b/extension/.vscode/launch.json deleted file mode 100644 index 8880465..0000000 --- a/extension/.vscode/launch.json +++ /dev/null @@ -1,21 +0,0 @@ -// A launch configuration that compiles the extension and then opens it inside a new window -// Use IntelliSense to learn about possible attributes. -// Hover to view descriptions of existing attributes. -// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - } - ] -} diff --git a/extension/.vscode/settings.json b/extension/.vscode/settings.json deleted file mode 100644 index afdab66..0000000 --- a/extension/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -// Place your settings in this file to overwrite default and user settings. -{ - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" -} diff --git a/extension/.vscode/tasks.json b/extension/.vscode/tasks.json deleted file mode 100644 index 3b17e53..0000000 --- a/extension/.vscode/tasks.json +++ /dev/null @@ -1,20 +0,0 @@ -// See https://go.microsoft.com/fwlink/?LinkId=733558 -// for the documentation about the tasks.json format -{ - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "watch", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "presentation": { - "reveal": "never" - }, - "group": { - "kind": "build", - "isDefault": true - } - } - ] -} diff --git a/extension/README.md b/extension/README.md deleted file mode 100644 index 2d92a27..0000000 --- a/extension/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# RDF/Turtle and SAMM Aspect Models - -VS Code extension for the ESMF SDK Turtle language server. The extension supports prefix `Go to Definition`, fast syntax feedback while typing, and server-driven heavy Aspect validation for SAMM-style Turtle models. - -## Configuration - -- `turtle.languageServerSettings.activateEmbeddedLanguageServer` (boolean, default: `true`) - - When enabled, the extension starts the SAMM-CLI language server process. When disabled, an external language server must be started manually. -- `turtle.languageServerSettings.automaticUpdateCheck` (boolean, default: `true`) - - Automatically check for updates of the SAMM-CLI language server and notify when a new version is available. -- `turtle.languageServerSettings.sammCliPath` (string) - - Path to the SAMM CLI executable or jar file to use as the language server. Can be downloaded / set via the 'Select SAMM-CLI Executable' command. -- `turtle.languageServerSettings.serverPort` (number, default: `1846`) - - TCP port used to connect to the Turtle/SAMM language server. -- `turtle.languageServerSettings.traceLevel` (string, default: `off`) - - Controls the verbosity of language client protocol tracing. Options: `off`, `messages`, `verbose`. - -Use the command `Turtle: Select SAMM-CLI Executable` to choose either: -- one of the latest 10 SAMM-CLI GitHub releases, or -- a custom executable path from your local file system. - -## Features - -- Prefix `Go to Definition` inside Turtle files. -- Two-level validation: - - Fast feedback on type from the regular Turtle parser diagnostics provided by the server (appear in the editor and `Problems`). - - Heavy Aspect validation from the server for model-level issues (results shown in notifications and status bar). -- Manual validation command: - - `Turtle: Validate document now` - -## Run The Server And Extension Together - -1. In this extension project, install dependencies with `npm install`. -2. Compile the extension with `npm run build`. -3. Press `F5` in VS Code to open an Extension Development Host. -4. Open a Turtle file such as [samples/valid.ttl](samples/valid.ttl) or your Aspect model file. - -If the server cannot be downloaded or started, the extension shows an error and leaves a detailed message in the Turtle LSP output channel. - -## Validation Behavior - -Fast feedback on type: - -- Driven by the server's regular Turtle parsing diagnostics. -- Results appear in the editor and `Problems` panel. -- Intended for quick editor feedback while you type. - -Heavy Aspect validation: - -- Runs on the server, not in the extension. -- Results are displayed in notification messages (for manual validation) or status bar (for save-triggered validation). -- Always uses detailed server validation messaging when the server returns report text. -- Always shows visible progress for long-running validation. -- Runs automatically on save and can also be triggered manually. - -When each validation runs: - -- On type: fast syntax feedback only. -- On save: heavy Aspect validation for Turtle documents. -- Manual: `Turtle: Validate document now` for the active Turtle document. - -## Commands - -- `Turtle: Validate document now` - - Sends a server request for the active Turtle document. -- `Turtle: Select SAMM-CLI Executable` - - Opens a quick pick with the latest 10 GitHub releases and a custom-path option. -- `Turtle: Restart and reconnect to Language Server` - - Restarts the language server and reconnects the client. - -## UX During Long-Running Validation - -- Manual validation shows a progress notification while the request is running. -- Save-triggered validation always uses a short status-bar progress indicator instead of repeated popups. -- After completion, the user gets a summary message with validation results. -- Automatic save validation keeps progress and completion feedback in the status bar. - -## Verify Go To Definition - -Use [samples/valid.ttl](samples/valid.ttl): - -1. Open `samples/valid.ttl`. -2. Place the cursor on `foaf:Person`, `foaf:name`, or another prefixed name. -3. Run `Go to Definition`. -4. Confirm that VS Code jumps to the matching `@prefix` declaration. - -Expected behavior: - -- `foaf:*` resolves to `@prefix foaf: ...`. -- `ex:*` resolves to `@prefix ex: ...`. - -## Verify Aspect Validation - -Use [samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl](samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl) or [samples/invalid.ttl](samples/invalid.ttl). - -Manual check: - -1. Open an Aspect model file. -2. Run `Turtle: Validate document now`. -3. Wait for the progress indicator to finish. -4. Confirm that validation results appear in a notification message. - -On-save check: - -1. Save the model file. -2. Confirm that the status bar shows validation progress. -3. Confirm that a summary message appears in the status bar after completion. diff --git a/extension/vsc-extension-quickstart.md b/extension/vsc-extension-quickstart.md deleted file mode 100644 index f56d544..0000000 --- a/extension/vsc-extension-quickstart.md +++ /dev/null @@ -1,23 +0,0 @@ -# RDF/Turtle and SAMM Aspect Models VS Code Extension - -## Project Structure - -This folder contains the VS Code extension for the ESMF SDK Turtle language server. - -* `package.json` - Extension manifest declaring commands, language support, settings, and walkthrough. -* `src/extension.ts` - Main extension entry point; handles activation, configuration, and language server lifecycle. -* `src/languageServer.ts` - Spawns and manages the SAMM-CLI language server process. -* `src/languageClient.ts` - Connects the VS Code language client to the server. -* `src/aspectValidation.ts` - Handles aspect model validation workflows (manual and on-save). -* `src/sammCliDownloader.ts` - Downloads and manages SAMM-CLI releases from GitHub. -* `src/settings.ts` - Reads and manages extension configuration. -* `samples/` - Example Turtle and Aspect model files for testing. - -## Getting Started - -1. Install dependencies: `npm install` (in `extension/` folder) -2. Build the extension: `npm run build` -3. Press `F5` to open an Extension Development Host with the extension loaded. -4. Open a `.ttl` file (e.g., `samples/valid.ttl`) to activate the extension. - -See [README.md](README.md) for full feature documentation and usage instructions. diff --git a/extension/language-configuration.json b/language-configuration.json similarity index 100% rename from extension/language-configuration.json rename to language-configuration.json diff --git a/extension/media/esmf-logo.png b/media/esmf-logo.png similarity index 100% rename from extension/media/esmf-logo.png rename to media/esmf-logo.png diff --git a/extension/media/walkthrough_select_samm_cli.png b/media/walkthrough_select_samm_cli.png similarity index 100% rename from extension/media/walkthrough_select_samm_cli.png rename to media/walkthrough_select_samm_cli.png diff --git a/extension/media/walkthrough_validation.png b/media/walkthrough_validation.png similarity index 100% rename from extension/media/walkthrough_validation.png rename to media/walkthrough_validation.png diff --git a/extension/package-lock.json b/package-lock.json similarity index 100% rename from extension/package-lock.json rename to package-lock.json diff --git a/extension/package.json b/package.json similarity index 100% rename from extension/package.json rename to package.json diff --git a/extension/samples/Moevement.ttl b/samples/Moevement.ttl similarity index 100% rename from extension/samples/Moevement.ttl rename to samples/Moevement.ttl diff --git a/extension/samples/invalid.ttl b/samples/invalid.ttl similarity index 100% rename from extension/samples/invalid.ttl rename to samples/invalid.ttl diff --git a/extension/samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl b/samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl similarity index 100% rename from extension/samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl rename to samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl diff --git a/extension/samples/valid.ttl b/samples/valid.ttl similarity index 100% rename from extension/samples/valid.ttl rename to samples/valid.ttl diff --git a/extension/src/aspectValidation.ts b/src/aspectValidation.ts similarity index 100% rename from extension/src/aspectValidation.ts rename to src/aspectValidation.ts diff --git a/extension/src/constants.ts b/src/constants.ts similarity index 100% rename from extension/src/constants.ts rename to src/constants.ts diff --git a/extension/src/extension.ts b/src/extension.ts similarity index 98% rename from extension/src/extension.ts rename to src/extension.ts index ae724f6..e83e974 100644 --- a/extension/src/extension.ts +++ b/src/extension.ts @@ -82,8 +82,7 @@ function queueLanguageServicesRestart(reason: string): Promise { restartChain = restartChain .then(() => restartLanguageServices(reason)) .catch(error => { - outputChannel.error(`Restart pipeline failed: ${error instanceof Error ? error.message : String(error)}`); - throw error; + vscode.window.showErrorMessage(`Failed to start required language services: ${error instanceof Error ? error.message : String(error)}`); }); return restartChain; diff --git a/extension/src/languageClient.ts b/src/languageClient.ts similarity index 100% rename from extension/src/languageClient.ts rename to src/languageClient.ts diff --git a/extension/src/languageServer.ts b/src/languageServer.ts similarity index 100% rename from extension/src/languageServer.ts rename to src/languageServer.ts diff --git a/extension/src/outputChannel.ts b/src/outputChannel.ts similarity index 100% rename from extension/src/outputChannel.ts rename to src/outputChannel.ts diff --git a/extension/src/sammCliDownloader.ts b/src/sammCliDownloader.ts similarity index 98% rename from extension/src/sammCliDownloader.ts rename to src/sammCliDownloader.ts index 8acdba7..ba31691 100644 --- a/extension/src/sammCliDownloader.ts +++ b/src/sammCliDownloader.ts @@ -18,7 +18,6 @@ import { spawn } from 'node:child_process'; import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import extractZip = require('extract-zip'); -import * as path from 'node:path'; import * as tar from 'tar'; import { TurtleExtensionSettings } from './settings'; import type { ExtensionLogger } from './outputChannel'; @@ -37,9 +36,7 @@ interface GitHubRelease { } const SAMM_CLI_STORAGE_DIR = 'samm-cli'; -// GitHub returns up to 100 releases per page; fetch a full page so that -// filtering out drafts/prereleases still leaves enough stable releases. -const RELEASES_PAGE_SIZE = 100; +const RELEASES_PAGE_SIZE = 20; export class SammCliDownloader { constructor( diff --git a/extension/src/settings.ts b/src/settings.ts similarity index 100% rename from extension/src/settings.ts rename to src/settings.ts diff --git a/extension/src/test/aspectValidationController.test.ts b/src/test/aspectValidationController.test.ts similarity index 100% rename from extension/src/test/aspectValidationController.test.ts rename to src/test/aspectValidationController.test.ts diff --git a/extension/src/test/validationTestHarness.ts b/src/test/validationTestHarness.ts similarity index 100% rename from extension/src/test/validationTestHarness.ts rename to src/test/validationTestHarness.ts diff --git a/extension/tsconfig.json b/tsconfig.json similarity index 100% rename from extension/tsconfig.json rename to tsconfig.json From 98a2329392a0ab7751545719abd0efb2fc68b485 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Thu, 11 Jun 2026 10:06:18 +0200 Subject: [PATCH 20/22] Fix wdir in pipelines --- .github/workflows/pull-request-check.yml | 4 ---- .github/workflows/release-workflow.yml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/.github/workflows/pull-request-check.yml b/.github/workflows/pull-request-check.yml index e4f9a70..db66585 100644 --- a/.github/workflows/pull-request-check.yml +++ b/.github/workflows/pull-request-check.yml @@ -13,10 +13,6 @@ jobs: permissions: contents: read - defaults: - run: - working-directory: extension - steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index 5a8beec..e799a87 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -23,10 +23,6 @@ jobs: issues: write pull-requests: write - defaults: - run: - working-directory: extension - steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 From 9ae678fb67c7a367abab9bc15c77505471ad3dbe Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Thu, 11 Jun 2026 10:33:00 +0200 Subject: [PATCH 21/22] Address PR review findings --- .github/workflows/release-workflow.yml | 5 +- README.md | 12 +-- package.json | 16 ++-- samples/Moevement.ttl | 103 ------------------------- src/constants.ts | 4 +- src/extension.ts | 12 +-- src/sammCliDownloader.ts | 38 ++++----- 7 files changed, 44 insertions(+), 146 deletions(-) delete mode 100644 samples/Moevement.ttl diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index e799a87..4591e00 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -70,8 +70,9 @@ jobs: path: turtle-${{ env.RELEASE_VERSION }}.vsix if-no-files-found: error - - name: Publish to VS Code Marketplace - run: npx @vscode/vsce publish --pat "${{ secrets.VS_MARKETPLACE_TOKEN }}" + # Disabled until https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/work_items/7565 is resolved + # - name: Publish to VS Code Marketplace + # run: npx @vscode/vsce publish --pat "${{ secrets.VS_MARKETPLACE_TOKEN }}" - name: Commit version changes and push to upstream repository uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0 diff --git a/README.md b/README.md index 2d92a27..5d8fb00 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,18 @@ VS Code extension for the ESMF SDK Turtle language server. The extension support ## Configuration - `turtle.languageServerSettings.activateEmbeddedLanguageServer` (boolean, default: `true`) - - When enabled, the extension starts the SAMM-CLI language server process. When disabled, an external language server must be started manually. + - When enabled, the extension starts the SAMM CLI language server process. When disabled, an external language server must be started manually. - `turtle.languageServerSettings.automaticUpdateCheck` (boolean, default: `true`) - - Automatically check for updates of the SAMM-CLI language server and notify when a new version is available. + - Automatically check for updates of the SAMM CLI language server and notify when a new version is available. - `turtle.languageServerSettings.sammCliPath` (string) - - Path to the SAMM CLI executable or jar file to use as the language server. Can be downloaded / set via the 'Select SAMM-CLI Executable' command. + - Path to the SAMM CLI executable or jar file to use as the language server. Can be downloaded / set via the 'Select SAMM CLI Executable' command. - `turtle.languageServerSettings.serverPort` (number, default: `1846`) - TCP port used to connect to the Turtle/SAMM language server. - `turtle.languageServerSettings.traceLevel` (string, default: `off`) - Controls the verbosity of language client protocol tracing. Options: `off`, `messages`, `verbose`. -Use the command `Turtle: Select SAMM-CLI Executable` to choose either: -- one of the latest 10 SAMM-CLI GitHub releases, or +Use the command `Turtle: Select SAMM CLI Executable` to choose either: +- one of the latest SAMM CLI GitHub releases, or - a custom executable path from your local file system. ## Features @@ -63,7 +63,7 @@ When each validation runs: - `Turtle: Validate document now` - Sends a server request for the active Turtle document. -- `Turtle: Select SAMM-CLI Executable` +- `Turtle: Select SAMM CLI Executable` - Opens a quick pick with the latest 10 GitHub releases and a custom-path option. - `Turtle: Restart and reconnect to Language Server` - Restarts the language server and reconnects the client. diff --git a/package.json b/package.json index f49d0ea..5c65ebf 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, { "command": "turtle.selectSammCliExecutable", - "title": "Select SAMM-CLI Executable", + "title": "Select SAMM CLI Executable", "category": "Turtle" } ], @@ -63,19 +63,19 @@ "turtle.languageServerSettings.activateEmbeddedLanguageServer": { "type": "boolean", "default": true, - "description": "When enabled, the extension starts the SAMM-CLI language server process at the path configured below. When disabled, an external language server must be started manually.", + "description": "When enabled, the extension starts the SAMM CLI language server process at the path configured below. When disabled, an external language server must be started manually.", "order": 0 }, "turtle.languageServerSettings.automaticUpdateCheck": { "type": "boolean", "default": true, - "description": "Automatically check for updates of the SAMM-CLI language server and notify when a new version is available.", + "description": "Automatically check for updates of the SAMM CLI language server and notify when a new version is available.", "order": 1 }, "turtle.languageServerSettings.sammCliPath": { "type": "string", "default": "", - "description": "Path to the SAMM CLI executable or jar file to use as the language server. Can be downloaded / set via the 'Select SAMM-CLI Executable' command.", + "description": "Path to the SAMM CLI executable or jar file to use as the language server. Can be downloaded / set via the 'Select SAMM CLI Executable' command.", "order": 2 }, "turtle.languageServerSettings.serverPort": { @@ -123,12 +123,12 @@ "description": "Learn how to use the RDF/Turtle and SAMM Aspect Models extension to validate your Turtle files and SAMM aspect models.\nThis walkthrough will guide you through the key features of the extension, including validating your Turtle files and SAMM aspect models using the integrated language server.", "steps": [ { - "id": "select-samm-cli", - "title": "Selecting a SAMM-CLI Executable", - "description": "Initial setup requires you to either Download or manually select a SAMM CLI, containing the required SAMM Lanuage Server \n[Download or Select SAMM CLI](command:turtle.selectSammCliExecutable)", + "id": "select-SAMM CLI", + "title": "Selecting a SAMM CLI Executable", + "description": "Initial setup requires you to either download or manually select a SAMM CLI, containing the required SAMM Language Server \n[Download or Select SAMM CLI](command:turtle.selectSammCliExecutable)", "media": { "image": "media/walkthrough_select_samm_cli.png", - "altText": "Example of selecting a SAMM-CLI executable" + "altText": "Example of selecting a SAMM CLI executable" } }, { diff --git a/samples/Moevement.ttl b/samples/Moevement.ttl deleted file mode 100644 index e22337b..0000000 --- a/samples/Moevement.ttl +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) 2022 Robert Bosch Manufacturing Solutions GmbH -# -# See the AUTHORS file(s) distributed with this work for -# additional information regarding authorship. -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 - -@prefix samm: . -@prefix samm-c: . -@prefix samm-e: . -@prefix unit: . -@prefix rdf: . -@prefix rdfs: . -@prefix xsd: . -@prefix : . - -:Movement a samm:Aspect ; - samm:preferredName "movement"@en ; - samm:description "Aspect for movement information"@en ; - samm:properties ( :isMoving :position :speed :speedLimitWarning ) ; - samm:operations ( ) ; - samm:events ( ) . - -:isMoving a samm:Property ; - samm:preferredName "is moving"@en ; - samm:description "Flag indicating whether the asset is currently moving"@en ; - samm:characteristic samm-c:Boolean . - -:position a samm:Property ; - samm:preferredName "position"@en ; - samm:description "Indicates a position"@en ; - samm:characteristic :SpatialPositionCharacteristic . - -:speed a samm:Property ; - samm:preferredName "speed"@en ; - samm:description "speed of vehicle"@en ; - samm:characteristic :Speed . - -:speedLimitWarning a samm:Property ; - samm:preferredName "speed limit warning"@en ; - samm:description "Indicates if the speed limit is adhered to."@en ; - samm:characteristic :TrafficLight . - -:SpatialPositionCharacteristic a samm-c:SingleEntity ; - samm:preferredName "spatial position characteristic"@en ; - samm:description "Represents a single position in space with optional z coordinate."@en ; - samm:dataType :SpatialPosition . - -:Speed a samm-c:Measurement ; - samm:preferredName "speed"@en ; - samm:description "Scalar representation of speed of an object in kilometers per hour."@en ; - samm:dataType xsd:float ; - samm-c:unit unit:kilometrePerHour . - -:TrafficLight a samm-c:Enumeration ; - samm:preferredName "warning level"@en ; - samm:description "Represents if speed of position change is within specification (green), within tolerance (yellow), or outside specification (red)."@en ; - samm:dataType xsd:string ; - samm-c:values ( "green" "yellow" "red" ) . - -:SpatialPosition a samm:Entity ; - samm:preferredName "spatial position"@en ; - samm:description "Represents latitude, longitude and altitude information in the WGS84 geodetic reference datum"@en ; - samm:see ; - samm:properties ( :latitude :longitude [ samm:property :altitude; samm:optional true ] ) . - -:latitude a samm:Property ; - samm:preferredName "latitude"@en ; - samm:description "latitude coordinate in space (WGS84)"@en ; - samm:see ; - samm:characteristic :Coordinate ; - samm:exampleValue "9.1781"^^xsd:decimal . - -:longitude a samm:Property ; - samm:preferredName "longitude"@en ; - samm:description "longitude coordinate in space (WGS84)"@en ; - samm:see ; - samm:characteristic :Coordinate ; - samm:exampleValue "48.80835"^^xsd:decimal . - -:altitude a samm:Property ; - samm:preferredName "altitude"@en ; - samm:description "Elevation above sea level zero"@en ; - samm:see ; - samm:characteristic :MetresAboveMeanSeaLevel ; - samm:exampleValue "153"^^xsd:float . - -:Coordinate a samm-c:Measurement ; - samm:preferredName "coordinate"@en ; - samm:description "Representing the geographical coordinate"@en ; - samm:dataType xsd:decimal ; - samm-c:unit unit:degreeUnitOfAngle . - -:MetresAboveMeanSeaLevel a samm-c:Measurement ; - samm:preferredName "metres above mean sea level"@en ; - samm:description "Signifies the vertical distance in reference to a historic mean sea level as a vertical datum"@en ; - samm:see ; - samm:dataType xsd:float ; - samm-c:unit unit:metre . diff --git a/src/constants.ts b/src/constants.ts index 8b2a984..d679004 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,12 +11,12 @@ * SPDX-License-Identifier: MPL-2.0 */ -/** JVM options passed to the SAMM-CLI when it is launched from a JAR. */ +/** JVM options passed to the SAMM CLI when it is launched from a JAR. */ export const JAVA_OPTIONS = [ '--enable-native-access=ALL-UNNAMED', '--sun-misc-unsafe-memory-access=allow', '-Dpolyglotimpl.DisableMultiReleaseCheck=true', ]; -/** GitHub repository hosting the SAMM-CLI releases. */ +/** GitHub repository hosting the SAMM CLI releases. */ export const GITHUB_RELEASE_REPOSITORY = 'eclipse-esmf/esmf-sdk'; diff --git a/src/extension.ts b/src/extension.ts index e83e974..3290c0a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,7 +20,7 @@ import { TurtleLanguageClient } from './languageClient'; import type { ExtensionLogger } from './outputChannel'; const SELECT_EXECUTABLE_COMMAND = 'turtle.selectSammCliExecutable'; -const SELECT_EXECUTABLE_TITLE = 'Select SAMM-CLI Executable'; +const SELECT_EXECUTABLE_TITLE = 'Select SAMM CLI Executable'; const RESTART_LANGUAGE_SERVICES_COMMAND = 'turtle.restartLanguageServices'; let settings: TurtleExtensionSettings; @@ -73,7 +73,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { if (settings.sammCliAutoUpdateIsEnabled() && settings.isEmbeddedLanguageServerStartEnabled()) { sammCliDownloader.checkForSammCliUpdates().catch(error => { - outputChannel.error(`Failed to check for SAMM-CLI updates: ${error instanceof Error ? error.message : String(error)}`); + outputChannel.error(`Failed to check for SAMM CLI updates: ${error instanceof Error ? error.message : String(error)}`); }); } } @@ -136,7 +136,7 @@ type SammCliQuickPickItem = vscode.QuickPickItem & { async function selectSammCliExecutable(): Promise { const releases = await sammCliDownloader.getRecentSammCliReleaseTags(10).catch(error => { - outputChannel.error(`Failed to fetch SAMM-CLI releases for quick pick: ${error instanceof Error ? error.message : String(error)}`); + outputChannel.error(`Failed to fetch SAMM CLI releases for quick pick: ${error instanceof Error ? error.message : String(error)}`); return []; }); @@ -144,7 +144,7 @@ async function selectSammCliExecutable(): Promise { const currentVersion = currentPath ? await sammCliDownloader.runVersionCommand(currentPath).catch(() => undefined) : undefined; const customPathItem: SammCliQuickPickItem = { - label: '$(folder-opened) Use custom SAMM CLI executable or jar', + label: '$(folder-opened) Use custom SAMM CLI executable or jar file', detail: currentPath ? `Currently configured: ${currentPath}` : 'Choose an executable from your file system', action: 'customPath', }; @@ -171,7 +171,7 @@ async function selectSammCliExecutable(): Promise { } const pick = await vscode.window.showQuickPick([customPathItem, separator, ...releaseItems], { - title: 'Select SAMM-CLI executable', + title: 'Select SAMM CLI executable', placeHolder: 'Choose a GitHub release or select a custom executable path', matchOnDetail: true, }); @@ -234,7 +234,7 @@ async function promptForCustomExecutablePath(): Promise { canSelectFolders: false, canSelectMany: false, openLabel: 'Use this executable / jar', - title: 'Select SAMM-CLI executable / jar', + title: 'Select SAMM CLI executable / jar', }); return selection?.[0]?.fsPath; diff --git a/src/sammCliDownloader.ts b/src/sammCliDownloader.ts index ba31691..5de0d92 100644 --- a/src/sammCliDownloader.ts +++ b/src/sammCliDownloader.ts @@ -35,7 +35,7 @@ interface GitHubRelease { prerelease?: boolean; } -const SAMM_CLI_STORAGE_DIR = 'samm-cli'; +const SAMM_CLI_STORAGE_DIR = 'SAMM CLI'; const RELEASES_PAGE_SIZE = 20; export class SammCliDownloader { @@ -48,29 +48,29 @@ export class SammCliDownloader { async checkForSammCliUpdates(): Promise { const currentPath = this.settings.getSammCliPath(); if (!currentPath) { - this.outputChannel.info('SAMM-CLI update check skipped: No SAMM-CLI path configured.'); + this.outputChannel.info('SAMM CLI update check skipped: No SAMM CLI path configured.'); return; } const type = currentPath.endsWith('.jar') ? 'jar' : 'native'; const currentVersion = await this.runVersionCommand(currentPath).catch(error => { - throw new Error(`Failed to get current SAMM-CLI version: ${error instanceof Error ? error.message : String(error)}`); + throw new Error(`Failed to get current SAMM CLI version: ${error instanceof Error ? error.message : String(error)}`); }); const latestVersion = await this.getLatestAvailableSammCliReleaseTag().catch(error => { - throw new Error(`Failed to check for latest SAMM-CLI release: ${error instanceof Error ? error.message : String(error)}`); + throw new Error(`Failed to check for latest SAMM CLI release: ${error instanceof Error ? error.message : String(error)}`); }); if (this.compareVersions(latestVersion, currentVersion) > 0) { const label = currentVersion - ? `There is a new SAMM-CLI release available: ${currentVersion} -> ${latestVersion}` - : `SAMM-CLI ${latestVersion} is available`; - vscode.window.showInformationMessage(label, 'Download & Use').then(async (selection) => { - if (selection === 'Download & Use') { + ? `There is a new SAMM CLI release available: ${currentVersion} -> ${latestVersion}` + : `SAMM CLI ${latestVersion} is available`; + vscode.window.showInformationMessage(label, 'Download and use').then(async (selection) => { + if (selection === 'Download and use') { try { const newPath = await this.downloadRelease(latestVersion, type); await this.settings.setSammCliPath(newPath); - vscode.window.showInformationMessage(`SAMM-CLI ${latestVersion} has been downloaded and configured for use.`); + vscode.window.showInformationMessage(`SAMM CLI ${latestVersion} has been downloaded and configured for use.`); } catch (error) { - const message = error instanceof Error ? error.message : 'An unknown error occurred while downloading the latest SAMM-CLI release.'; + const message = error instanceof Error ? error.message : 'An unknown error occurred while downloading the latest SAMM CLI release.'; vscode.window.showErrorMessage(message); } } @@ -82,7 +82,7 @@ export class SammCliDownloader { const releases = await this.getRecentSammCliReleaseTags(1); if (releases.length === 0) { - throw new Error('No SAMM-CLI releases are available on GitHub.'); + throw new Error('No SAMM CLI releases are available on GitHub.'); } return releases[0]; @@ -97,7 +97,7 @@ export class SammCliDownloader { }); if (!response.ok) { - return Promise.reject(new Error(this.describeGitHubError('Failed to fetch SAMM-CLI releases from GitHub', response))); + return Promise.reject(new Error(this.describeGitHubError('Failed to fetch SAMM CLI releases from GitHub', response))); } const releases = await response.json() as Array; @@ -179,7 +179,7 @@ export class SammCliDownloader { if (!response.ok) { const target = releaseVersion && releaseVersion !== 'latest' ? `release ${releaseVersion}` : 'the latest release'; - throw new Error(this.describeGitHubError(`Failed to fetch samm-cli ${target}`, response)); + throw new Error(this.describeGitHubError(`Failed to fetch SAMM CLI ${target}`, response)); } return response.json() as Promise; @@ -201,7 +201,7 @@ export class SammCliDownloader { throw new Error(`Failed to download the language server from ${downloadUrl}: ${response.status} ${response.statusText}`); } - await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Downloading Language Server (samm-cli)...' }, async () => { + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Downloading Language Server (SAMM CLI)...' }, async () => { try { await pipeline( Readable.fromWeb(response.body as unknown as globalThis.ReadableStream), @@ -232,10 +232,10 @@ export class SammCliDownloader { }); if (!response.ok || !response.body) { - throw new Error(`Failed to download the SAMM-CLI JAR from ${downloadUrl}: ${response.status} ${response.statusText}`); + throw new Error(`Failed to download the SAMM CLI JAR from ${downloadUrl}: ${response.status} ${response.statusText}`); } - await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Downloading SAMM-CLI JAR...' }, async () => { + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Downloading SAMM CLI JAR...' }, async () => { await pipeline( Readable.fromWeb(response.body as unknown as globalThis.ReadableStream), createWriteStream(targetPath), @@ -270,7 +270,7 @@ export class SammCliDownloader { private async ensureExecutableIsReady(executablePath: string, platform: string, releaseTag: string): Promise { if (!await this.fileExists(executablePath)) { - throw new Error(`Downloaded samm-cli ${releaseTag} did not contain expected executable at ${executablePath}.`); + throw new Error(`Downloaded SAMM CLI ${releaseTag} did not contain expected executable at ${executablePath}.`); } if (platform === 'win32') { @@ -280,12 +280,12 @@ export class SammCliDownloader { try { await access(executablePath, constants.X_OK); } catch { - throw new Error(`Downloaded samm-cli executable is not marked as executable: ${executablePath}`); + throw new Error(`Downloaded SAMM CLI executable is not marked as executable: ${executablePath}`); } } /** - * Compares two SAMM-CLI version tags (e.g. `v2.10.0`, `2.12.0`). + * Compares two SAMM CLI version tags (e.g. `v2.10.0`, `2.12.0`). * Returns a positive number when `a` is newer than `b`, a negative number * when it is older, and `0` when they are equal. Pre-release suffixes are * ignored. An unparsable/empty version is treated as the oldest. From 4ffebe3dc007eb9f078362ebcf3de28dbc8d5532 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Thu, 11 Jun 2026 13:17:05 +0200 Subject: [PATCH 22/22] Fix extension path in pipeline --- .github/workflows/pull-request-check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request-check.yml b/.github/workflows/pull-request-check.yml index db66585..f9920cc 100644 --- a/.github/workflows/pull-request-check.yml +++ b/.github/workflows/pull-request-check.yml @@ -24,7 +24,7 @@ jobs: with: node-version: 22 cache: npm - cache-dependency-path: extension/package-lock.json + cache-dependency-path: package-lock.json - name: Install dependencies run: npm ci @@ -45,5 +45,5 @@ jobs: uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f #v6.0.0 with: name: turtle-vsix - path: extension/turtle-pr-${{ github.run_number }}.vsix + path: turtle-pr-${{ github.run_number }}.vsix if-no-files-found: error