From 4cbe12cf48ff6ebd636430abf6701031e28a2930 Mon Sep 17 00:00:00 2001 From: e-filchenko-bosh Date: Mon, 20 Apr 2026 12:47:29 +0300 Subject: [PATCH 01/12] init commit --- .gitignore | 1 + extension/.gitignore | 6 + extension/.prettierrc | 9 + extension/.vscode-test.mjs | 9 + extension/.vscode/extensions.json | 8 + extension/.vscode/launch.json | 21 + extension/.vscode/settings.json | 11 + extension/.vscode/tasks.json | 20 + extension/.vscodeignore | 11 + extension/eslint.config.mjs | 27 + extension/language-configuration.json | 24 + extension/package-lock.json | 3720 +++++++++++++++++ extension/package.json | 64 + extension/samples/invalid.ttl | 5 + .../org.eclipse.esmf.test/1.0.0/Aspect.ttl | 23 + extension/samples/valid.ttl | 10 + extension/src/aspectValidation.ts | 200 + extension/src/extension.ts | 108 + .../test/aspectValidationController.test.ts | 118 + extension/src/test/validationTestHarness.ts | 124 + extension/tsconfig.json | 13 + extension/vsc-extension-quickstart.md | 17 + 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/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 + 59 files changed, 8800 insertions(+) create mode 100644 .gitignore create mode 100644 extension/.gitignore create mode 100644 extension/.prettierrc create mode 100644 extension/.vscode-test.mjs create mode 100644 extension/.vscode/extensions.json create mode 100644 extension/.vscode/launch.json create mode 100644 extension/.vscode/settings.json create mode 100644 extension/.vscode/tasks.json create mode 100644 extension/.vscodeignore create mode 100644 extension/eslint.config.mjs create mode 100644 extension/language-configuration.json create mode 100644 extension/package-lock.json create mode 100644 extension/package.json create mode 100644 extension/samples/invalid.ttl create mode 100644 extension/samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl create mode 100644 extension/samples/valid.ttl create mode 100644 extension/src/aspectValidation.ts create mode 100644 extension/src/extension.ts create mode 100644 extension/src/test/aspectValidationController.test.ts create mode 100644 extension/src/test/validationTestHarness.ts create mode 100644 extension/tsconfig.json create mode 100644 extension/vsc-extension-quickstart.md create mode 100644 lsp-server/.development/esmf-checkstyle.xml create mode 100644 lsp-server/.development/esmf-eclipse-codestyle.xml create mode 100644 lsp-server/.development/esmf-intellij-codestyle.xml create mode 100644 lsp-server/.development/esmf-intellij-inspections.xml create mode 100644 lsp-server/.gitattributes create mode 100644 lsp-server/.gitignore create mode 100644 lsp-server/pom.xml create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/App.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/TurtleLanguageServer.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/diagnostics/AspectDiagnosticMapper.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationError.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationErrorType.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationResult.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectViolationInfo.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/request/ValidateDocumentParams.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectModelValidationService.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectValidationCoordinator.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationService.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/common/uri/DocumentUriResolver.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflow.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentAspectValidationService.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsService.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStore.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentStore.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifier.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleSyntaxValidationService.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleTextDocumentService.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/lsp/workspace/TurtleWorkspaceService.java create mode 100644 lsp-server/src/main/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionService.java create mode 100644 lsp-server/src/main/resources/log4j2.xml create mode 100644 lsp-server/src/test/java/com/example/turtlelsp/TurtleDefinitionTest.java create mode 100644 lsp-server/src/test/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationServiceTest.java create mode 100644 lsp-server/src/test/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflowTest.java create mode 100644 lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsServiceTest.java create mode 100644 lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStoreTest.java create mode 100644 lsp-server/src/test/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifierTest.java create mode 100644 lsp-server/src/test/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionServiceTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/extension/.gitignore b/extension/.gitignore new file mode 100644 index 0000000..17df51b --- /dev/null +++ b/extension/.gitignore @@ -0,0 +1,6 @@ +out +dist +node_modules +.vscode-test/ +*.vsix +logs diff --git a/extension/.prettierrc b/extension/.prettierrc new file mode 100644 index 0000000..d4e3781 --- /dev/null +++ b/extension/.prettierrc @@ -0,0 +1,9 @@ +{ + "printWidth": 140, + "singleQuote": true, + "useTabs": false, + "tabWidth": 4, + "semi": true, + "bracketSpacing": false, + "arrowParens": "avoid" +} diff --git a/extension/.vscode-test.mjs b/extension/.vscode-test.mjs new file mode 100644 index 0000000..d320b89 --- /dev/null +++ b/extension/.vscode-test.mjs @@ -0,0 +1,9 @@ +import { defineConfig } from '@vscode/test-cli'; + +export default defineConfig({ + files: 'out/test/**/*.test.js', + launchArgs: [ + '--user-data-dir=/tmp/extension-vscode-test-user-data', + '--extensions-dir=/tmp/extension-vscode-test-extensions' + ], +}); diff --git a/extension/.vscode/extensions.json b/extension/.vscode/extensions.json new file mode 100644 index 0000000..186459d --- /dev/null +++ b/extension/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + // 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 new file mode 100644 index 0000000..8880465 --- /dev/null +++ b/extension/.vscode/launch.json @@ -0,0 +1,21 @@ +// 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 new file mode 100644 index 0000000..afdab66 --- /dev/null +++ b/extension/.vscode/settings.json @@ -0,0 +1,11 @@ +// 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 new file mode 100644 index 0000000..3b17e53 --- /dev/null +++ b/extension/.vscode/tasks.json @@ -0,0 +1,20 @@ +// 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/.vscodeignore b/extension/.vscodeignore new file mode 100644 index 0000000..7d3e5c7 --- /dev/null +++ b/extension/.vscodeignore @@ -0,0 +1,11 @@ +.vscode/** +.vscode-test/** +src/** +.gitignore +.yarnrc +vsc-extension-quickstart.md +**/tsconfig.json +**/eslint.config.mjs +**/*.map +**/*.ts +**/.vscode-test.* diff --git a/extension/eslint.config.mjs b/extension/eslint.config.mjs new file mode 100644 index 0000000..7c51b0c --- /dev/null +++ b/extension/eslint.config.mjs @@ -0,0 +1,27 @@ +import typescriptEslint from "typescript-eslint"; + +export default [{ + files: ["**/*.ts"], +}, { + plugins: { + "@typescript-eslint": typescriptEslint.plugin, + }, + + languageOptions: { + parser: typescriptEslint.parser, + ecmaVersion: 2022, + sourceType: "module", + }, + + rules: { + "@typescript-eslint/naming-convention": ["warn", { + selector: "import", + format: ["camelCase", "PascalCase"], + }], + + curly: "warn", + eqeqeq: "warn", + "no-throw-literal": "warn", + semi: "warn", + }, +}]; \ No newline at end of file diff --git a/extension/language-configuration.json b/extension/language-configuration.json new file mode 100644 index 0000000..cad0785 --- /dev/null +++ b/extension/language-configuration.json @@ -0,0 +1,24 @@ +{ + "comments": { + "lineComment": "#" + }, + "brackets": [ + ["(", ")"], + ["[", "]"], + ["{", "}"] + ], + "autoClosingPairs": [ + { "open": "(", "close": ")" }, + { "open": "[", "close": "]" }, + { "open": "{", "close": "}" }, + { "open": "\"", "close": "\"" }, + { "open": "<", "close": ">" } + ], + "surroundingPairs": [ + ["(", ")"], + ["[", "]"], + ["{", "}"], + ["\"", "\""], + ["<", ">"] + ] +} diff --git a/extension/package-lock.json b/extension/package-lock.json new file mode 100644 index 0000000..0f869ff --- /dev/null +++ b/extension/package-lock.json @@ -0,0 +1,3720 @@ +{ + "name": "extension", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "extension", + "version": "0.0.1", + "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", + "@vscode/test-electron": "^2.5.2", + "eslint": "^9.39.3", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.1" + }, + "engines": { + "vscode": "^1.110.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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==", + "dev": true, + "license": "Apache-2.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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "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", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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==", + "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", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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==", + "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", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "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" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vscode/test-cli": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.12.tgz", + "integrity": "sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mocha": "^10.0.10", + "c8": "^10.1.3", + "chokidar": "^3.6.0", + "enhanced-resolve": "^5.18.3", + "glob": "^10.3.10", + "minimatch": "^9.0.3", + "mocha": "^11.7.4", + "supports-color": "^10.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "vscode-test": "out/bin.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vscode/test-electron": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", + "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@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" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "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==", + "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" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "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/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "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": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/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/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "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" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "license": "MIT", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/extension/package.json b/extension/package.json new file mode 100644 index 0000000..e254f2e --- /dev/null +++ b/extension/package.json @@ -0,0 +1,64 @@ +{ + "name": "extension", + "displayName": "Turtle LSP", + "description": "VS Code extension for Turtle LSP", + "version": "0.0.1", + "engines": { + "vscode": "^1.110.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onLanguage:turtle", + "onCommand:turtleLsp.validateAspectModelNow" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "turtleLsp.validateAspectModelNow", + "title": "Validate Aspect Model Now", + "category": "Turtle LSP" + } + ], + "languages": [ + { + "id": "turtle", + "aliases": [ + "Turtle", + "turtle", + "RDF Turtle" + ], + "extensions": [ + ".ttl" + ], + "configuration": "./language-configuration.json" + } + ] + }, + "scripts": { + "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: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" + }, + "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", + "@vscode/test-electron": "^2.5.2", + "eslint": "^9.39.3", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.1" + }, + "dependencies": { + "vscode-languageclient": "^9.0.1" + } +} diff --git a/extension/samples/invalid.ttl b/extension/samples/invalid.ttl new file mode 100644 index 0000000..d5a3189 --- /dev/null +++ b/extension/samples/invalid.ttl @@ -0,0 +1,5 @@ +# Invalid Turtle — missing dot after prefix declaration and bad triple +@prefix ex: . + +ex:Alice foaf:name "Alice" . # missing . terminator +ex:Bob ex:knows ex:Alice . # foaf:name used without prefix diff --git a/extension/samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl b/extension/samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl new file mode 100644 index 0000000..c5c24da --- /dev/null +++ b/extension/samples/org.eclipse.esmf.test/1.0.0/Aspect.ttl @@ -0,0 +1,23 @@ +# Copyright (c) 2023 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 : . +@prefix samm: . +@prefix samm-c: . +@prefix xsd: . +@prefix unit: . + +:Aspect a samm:Aspect ; + samm:preferredName "Test Aspect"@en ; + samm:description "This is a test ription"@en ; + samm:description "This is a taaest ription"@de ; + samm:properties ( ) ; + samm:operations ( ) . \ No newline at end of file diff --git a/extension/samples/valid.ttl b/extension/samples/valid.ttl new file mode 100644 index 0000000..b2fb87b --- /dev/null +++ b/extension/samples/valid.ttl @@ -0,0 +1,10 @@ +# Valid Turtle example +@prefix ex: . +@prefix foaf: . + +ex:Alice a foaf:Person ; + foaf:name "Alice" ; + foaf:knows ex:Bob . + +ex:Bob a foaf:Person ; + foaf:name "Bob" . diff --git a/extension/src/aspectValidation.ts b/extension/src/aspectValidation.ts new file mode 100644 index 0000000..8b05fec --- /dev/null +++ b/extension/src/aspectValidation.ts @@ -0,0 +1,200 @@ +import * as vscode from 'vscode'; + +export const VALIDATE_DOCUMENT_REQUEST = 'turtle/aspectValidation/validateDocument'; +export const VALIDATE_DOCUMENT_COMMAND = 'turtleLsp.validateAspectModelNow'; +const STATUS_MESSAGE_TIMEOUT_MS = 5000; + +export type AspectValidationTrigger = 'manual' | 'save'; + +export interface AspectValidationError { + type?: string; + message?: string; +} + +export interface AspectValidationResult { + valid?: boolean; + report?: string; + violations?: Array; + error?: AspectValidationError | null; +} + +export interface RequestClient { + sendRequest(method: string, params?: unknown): Thenable; +} + +export interface ValidationWindow { + showInformationMessage(message: string): Thenable; + showWarningMessage(message: string): Thenable; + showErrorMessage(message: string): Thenable; + withProgress( + options: vscode.ProgressOptions, + task: (progress: vscode.Progress<{message?: string; increment?: number}>, token: vscode.CancellationToken) => Thenable, + ): Thenable; + setStatusBarMessage(text: string, hideAfterTimeout: number): vscode.Disposable; +} + +export interface ValidationWorkspace { + onDidSaveTextDocument(listener: (document: vscode.TextDocument) => void): vscode.Disposable; +} + +export interface ValidationOutputChannel { + appendLine(value: string): void; +} + +export class AspectValidationController { + private readonly inFlightKeys = new Set(); + + constructor( + private readonly client: RequestClient, + private readonly window: ValidationWindow, + private readonly workspace: ValidationWorkspace, + private readonly outputChannel: ValidationOutputChannel, + ) {} + + register(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.commands.registerCommand(VALIDATE_DOCUMENT_COMMAND, async () => { + const editor = vscode.window.activeTextEditor; + await this.validateDocument(editor?.document, 'manual'); + }), + this.workspace.onDidSaveTextDocument(async document => { + await this.validateDocument(document, 'save'); + }), + ); + } + + async validateDocument( + document: Pick | undefined, + trigger: AspectValidationTrigger, + ): Promise { + if (!document || document.languageId !== 'turtle') { + if (trigger === 'manual') { + await this.window.showWarningMessage('Open a Turtle file before running aspect validation.'); + } + return undefined; + } + + const request = () => + this.client.sendRequest(VALIDATE_DOCUMENT_REQUEST, { + uri: document.uri.toString(), + reason: trigger, + }); + + return this.runValidation(`document:${document.uri.toString()}`, 'Aspect model validation', trigger, request); + } + + private async runValidation( + 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); + try { + const result = await this.runWithProgress(title, trigger, request); + + await this.showSummary(result, trigger); + return result; + } catch (error) { + await this.handleFailure(error, trigger); + return undefined; + } finally { + this.inFlightKeys.delete(key); + } + } + + private runWithProgress( + title: string, + trigger: AspectValidationTrigger, + 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()); + } + + return Promise.resolve( + this.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title, + cancellable: false, + }, + async () => request(), + ), + ); + } + + private async showSummary(result: AspectValidationResult, trigger: AspectValidationTrigger): Promise { + const summary = this.formatSummary(result); + this.outputChannel.appendLine(`[aspectValidation] ${summary}`); + + if (trigger === 'save') { + 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); + } + + private async handleFailure(error: unknown, trigger: AspectValidationTrigger): Promise { + const summary = this.toFailureMessage(error); + this.outputChannel.appendLine(`[aspectValidation] ${summary}`); + + if (trigger === 'save') { + this.window.setStatusBarMessage(summary, STATUS_MESSAGE_TIMEOUT_MS); + return; + } + + await this.window.showErrorMessage(summary); + } + + private formatSummary(result: AspectValidationResult): string { + if (result.error?.message) { + return `Aspect validation failed: ${result.error.message}`; + } + + 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; + } + + private toFailureMessage(error: unknown): string { + if (error instanceof Error) { + if (error.message.includes('Method not found')) { + return 'Aspect validation request is not supported by the current server build.'; + } + return `Aspect validation request failed: ${error.message}`; + } + + return 'Aspect validation request failed.'; + } +} diff --git a/extension/src/extension.ts b/extension/src/extension.ts new file mode 100644 index 0000000..48b79f6 --- /dev/null +++ b/extension/src/extension.ts @@ -0,0 +1,108 @@ +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 {AspectValidationController} from './aspectValidation'; + +let client: LanguageClient | undefined; + +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, + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [ + {scheme: 'file', language: 'turtle'}, + {scheme: 'untitled', language: 'turtle'}, + ], + outputChannel, + synchronize: { + configurationSection: 'turtleLsp', + fileEvents: vscode.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); + + if (runtimeClasspath) { + return { + command: 'java', + args: ['-cp', runtimeClasspath, 'com.example.turtlelsp.App'], + options: { + cwd: serverProjectPath, + }, + }; + } + + if (fs.existsSync(jarPath)) { + return { + command: 'java', + args: ['-jar', jarPath], + options: { + cwd: serverProjectPath, + }, + }; + } + + return undefined; +} + +function resolveMavenRuntimeClasspath(serverProjectPath: string): string | undefined { + const reportsDirectory = path.join(serverProjectPath, 'target', 'surefire-reports'); + if (!fs.existsSync(reportsDirectory)) { + return undefined; + } + + 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 { + if (client) { + await client.stop(); + client = undefined; + } +} diff --git a/extension/src/test/aspectValidationController.test.ts b/extension/src/test/aspectValidationController.test.ts new file mode 100644 index 0000000..6eae8d0 --- /dev/null +++ b/extension/src/test/aspectValidationController.test.ts @@ -0,0 +1,118 @@ +import * as assert from 'assert'; +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; + + Object.assign(vscode.commands, { + registerCommand: () => new vscode.Disposable(() => undefined), + }); + + try { + await run(); + } finally { + Object.assign(vscode.commands, { + registerCommand: originalRegisterCommand, + }); + } +} diff --git a/extension/src/test/validationTestHarness.ts b/extension/src/test/validationTestHarness.ts new file mode 100644 index 0000000..5a75a99 --- /dev/null +++ b/extension/src/test/validationTestHarness.ts @@ -0,0 +1,124 @@ +import * as vscode from 'vscode'; +import { + AspectValidationController, + AspectValidationResult, + RequestClient, + ValidationOutputChannel, + ValidationWindow, + ValidationWorkspace, +} from '../aspectValidation'; + +type ValidationHarnessOptions = { + response?: AspectValidationResult; + error?: Error; +}; + +type RecordedRequest = { + method: string; + params: unknown; +}; + +type FakeWindow = ValidationWindow & { + errorMessages: string[]; + infoMessages: string[]; + progressTitles: string[]; + statusMessages: string[]; + warningMessages: string[]; +}; + +type FakeWorkspace = ValidationWorkspace & { + fireSave(document: Pick): Promise; +}; + +type FakeOutputChannel = ValidationOutputChannel & { + lines: string[]; +}; + +export function createValidationControllerHarness(options: ValidationHarnessOptions = {}) { + const sentRequests: RecordedRequest[] = []; + const window = createFakeWindow(); + const workspace = createFakeWorkspace(); + const outputChannel = createFakeOutputChannel(); + const client: RequestClient = { + sendRequest: async (method: string, params?: unknown) => { + sentRequests.push({method, params}); + + if (options.error) { + throw options.error; + } + + return (options.response ?? {valid: true}) as R; + }, + }; + + return { + controller: new AspectValidationController(client, window, workspace, outputChannel), + outputChannel, + sentRequests, + window, + workspace, + }; +} + +export function createValidationDocument(filePath: string): Pick { + return { + languageId: 'turtle', + uri: vscode.Uri.file(filePath), + }; +} + +function createFakeWorkspace(): FakeWorkspace { + let saveListener: ((document: vscode.TextDocument) => void | Promise) | undefined; + + return { + onDidSaveTextDocument: (listener: (document: vscode.TextDocument) => void | Promise) => { + saveListener = listener; + return new vscode.Disposable(() => undefined); + }, + fireSave: async (document: Pick) => { + await saveListener?.(document as vscode.TextDocument); + }, + }; +} + +function createFakeWindow(): FakeWindow { + return { + errorMessages: [], + infoMessages: [], + progressTitles: [], + statusMessages: [], + warningMessages: [], + showInformationMessage(message: string) { + this.infoMessages.push(message); + return Promise.resolve(undefined); + }, + showWarningMessage(message: string) { + this.warningMessages.push(message); + return Promise.resolve(undefined); + }, + showErrorMessage(message: string) { + this.errorMessages.push(message); + return Promise.resolve(undefined); + }, + withProgress( + options: vscode.ProgressOptions, + task: (progress: vscode.Progress<{message?: string; increment?: number}>, token: vscode.CancellationToken) => Thenable, + ) { + this.progressTitles.push(options.title ?? ''); + return Promise.resolve(task({report: () => undefined}, {} as vscode.CancellationToken)); + }, + setStatusBarMessage(text: string) { + this.statusMessages.push(text); + return new vscode.Disposable(() => undefined); + }, + }; +} + +function createFakeOutputChannel(): FakeOutputChannel { + return { + lines: [], + appendLine(value: string) { + this.lines.push(value); + }, + }; +} diff --git a/extension/tsconfig.json b/extension/tsconfig.json new file mode 100644 index 0000000..9559ed1 --- /dev/null +++ b/extension/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "outDir": "out", + "lib": [ + "ES2022" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true + } +} diff --git a/extension/vsc-extension-quickstart.md b/extension/vsc-extension-quickstart.md new file mode 100644 index 0000000..d727ac7 --- /dev/null +++ b/extension/vsc-extension-quickstart.md @@ -0,0 +1,17 @@ +# Welcome to your VS Code Extension + +## What's in the folder + +* 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`. + +## Get up and running straight away + +* 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. diff --git a/lsp-server/.development/esmf-checkstyle.xml b/lsp-server/.development/esmf-checkstyle.xml new file mode 100644 index 0000000..37be7d4 --- /dev/null +++ b/lsp-server/.development/esmf-checkstyle.xml @@ -0,0 +1,341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lsp-server/.development/esmf-eclipse-codestyle.xml b/lsp-server/.development/esmf-eclipse-codestyle.xml new file mode 100644 index 0000000..f812391 --- /dev/null +++ b/lsp-server/.development/esmf-eclipse-codestyle.xml @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lsp-server/.development/esmf-intellij-codestyle.xml b/lsp-server/.development/esmf-intellij-codestyle.xml new file mode 100644 index 0000000..dc231a6 --- /dev/null +++ b/lsp-server/.development/esmf-intellij-codestyle.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lsp-server/.development/esmf-intellij-inspections.xml b/lsp-server/.development/esmf-intellij-inspections.xml new file mode 100644 index 0000000..6aee320 --- /dev/null +++ b/lsp-server/.development/esmf-intellij-inspections.xml @@ -0,0 +1,1669 @@ + + + + diff --git a/lsp-server/.gitattributes b/lsp-server/.gitattributes new file mode 100644 index 0000000..f91f646 --- /dev/null +++ b/lsp-server/.gitattributes @@ -0,0 +1,12 @@ +# +# 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 new file mode 100644 index 0000000..8b75bac --- /dev/null +++ b/lsp-server/.gitignore @@ -0,0 +1,4 @@ +.idea +target +logs +lsp-server.iml diff --git a/lsp-server/pom.xml b/lsp-server/pom.xml new file mode 100644 index 0000000..d66c9d9 --- /dev/null +++ b/lsp-server/pom.xml @@ -0,0 +1,119 @@ + + 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 new file mode 100644 index 0000000..9cbedb6 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/App.java @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..b98fc7e --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/TurtleLanguageServer.java @@ -0,0 +1,80 @@ +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 new file mode 100644 index 0000000..f944f45 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/aspect/diagnostics/AspectDiagnosticMapper.java @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..de75991 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationError.java @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..e24c7e2 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationErrorType.java @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..794570b --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectValidationResult.java @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..d2730d2 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/aspect/model/AspectViolationInfo.java @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000..7141a3e --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/aspect/request/ValidateDocumentParams.java @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..1f8ee1e --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectModelValidationService.java @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..92543c3 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/AspectValidationCoordinator.java @@ -0,0 +1,89 @@ +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 new file mode 100644 index 0000000..a705597 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationService.java @@ -0,0 +1,129 @@ +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 new file mode 100644 index 0000000..cb9d750 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/common/uri/DocumentUriResolver.java @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..8f0895d --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflow.java @@ -0,0 +1,58 @@ +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 new file mode 100644 index 0000000..5bf7f45 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentAspectValidationService.java @@ -0,0 +1,110 @@ +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 new file mode 100644 index 0000000..5fada7f --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsService.java @@ -0,0 +1,46 @@ +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 new file mode 100644 index 0000000..66142c1 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStore.java @@ -0,0 +1,37 @@ +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 new file mode 100644 index 0000000..e2fe287 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/DocumentStore.java @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..fde7cf8 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifier.java @@ -0,0 +1,43 @@ +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 new file mode 100644 index 0000000..7c994bb --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleSyntaxValidationService.java @@ -0,0 +1,56 @@ +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 new file mode 100644 index 0000000..fcbfb5a --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/lsp/text/TurtleTextDocumentService.java @@ -0,0 +1,130 @@ +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 new file mode 100644 index 0000000..e5bfb28 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/lsp/workspace/TurtleWorkspaceService.java @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..65f36e8 --- /dev/null +++ b/lsp-server/src/main/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionService.java @@ -0,0 +1,95 @@ +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 new file mode 100644 index 0000000..b66e901 --- /dev/null +++ b/lsp-server/src/main/resources/log4j2.xml @@ -0,0 +1,28 @@ + + + + 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 new file mode 100644 index 0000000..b5ef12b --- /dev/null +++ b/lsp-server/src/test/java/com/example/turtlelsp/TurtleDefinitionTest.java @@ -0,0 +1,63 @@ +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 new file mode 100644 index 0000000..94e7507 --- /dev/null +++ b/lsp-server/src/test/java/com/example/turtlelsp/aspect/service/DefaultAspectModelValidationServiceTest.java @@ -0,0 +1,83 @@ +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 new file mode 100644 index 0000000..c2760c6 --- /dev/null +++ b/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/AspectDiagnosticsWorkflowTest.java @@ -0,0 +1,118 @@ +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 new file mode 100644 index 0000000..388d37c --- /dev/null +++ b/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsServiceTest.java @@ -0,0 +1,99 @@ +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 new file mode 100644 index 0000000..7424b66 --- /dev/null +++ b/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/DocumentDiagnosticsStoreTest.java @@ -0,0 +1,43 @@ +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 new file mode 100644 index 0000000..2360635 --- /dev/null +++ b/lsp-server/src/test/java/com/example/turtlelsp/lsp/text/TextDocumentClientNotifierTest.java @@ -0,0 +1,56 @@ +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 new file mode 100644 index 0000000..7c77938 --- /dev/null +++ b/lsp-server/src/test/java/com/example/turtlelsp/turtle/navigation/TurtlePrefixDefinitionServiceTest.java @@ -0,0 +1,57 @@ +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 f1d648ae58e7e3802ff95027b17ee4146942f5db Mon Sep 17 00:00:00 2001 From: e-filchenko-bosh Date: Mon, 20 Apr 2026 14:57:34 +0300 Subject: [PATCH 02/12] make project follow conventions --- README.md | 3 +- extension/CONTRIBUTING.md | 200 +++++++++++++ extension/CONVENTIONS.md | 28 ++ extension/LICENSE | 373 ++++++++++++++++++++++++ extension/README.md | 112 ++++++++ lsp-server/CONTRIBUTING.md | 186 ++++++++++++ lsp-server/CONVENTIONS.md | 102 +++++++ lsp-server/LICENSE | 373 ++++++++++++++++++++++++ lsp-server/NOTICE.md | 573 +++++++++++++++++++++++++++++++++++++ lsp-server/README.md | 45 +++ 10 files changed, 1993 insertions(+), 2 deletions(-) create mode 100644 extension/CONTRIBUTING.md create mode 100644 extension/CONVENTIONS.md create mode 100644 extension/LICENSE create mode 100644 extension/README.md create mode 100644 lsp-server/CONTRIBUTING.md create mode 100644 lsp-server/CONVENTIONS.md create mode 100644 lsp-server/LICENSE create mode 100644 lsp-server/NOTICE.md create mode 100644 lsp-server/README.md diff --git a/README.md b/README.md index 828aaeb..0c725c6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ -# esmf-vs-code-plugin -VS Code extension for editing Aspect Models +# esmf-vs-code-plugin \ No newline at end of file diff --git a/extension/CONTRIBUTING.md b/extension/CONTRIBUTING.md new file mode 100644 index 0000000..75b71fa --- /dev/null +++ b/extension/CONTRIBUTING.md @@ -0,0 +1,200 @@ +# Contribution Guideline ESMF Visual Studio Code Plugin + +Thank you for your interest in contributing to the ESMF Visual Studio Code Plugin. 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 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 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 is written in Typescript. 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. 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/extension/CONVENTIONS.md b/extension/CONVENTIONS.md new file mode 100644 index 0000000..e21b961 --- /dev/null +++ b/extension/CONVENTIONS.md @@ -0,0 +1,28 @@ +# ESMF Code Conventions +The following document contains a compilation of conventions and guidelines to format, structure and +write code for the ESMF Visual Studio Code Plugin schematics. + +## General Conventions + +Our code conventions are based on the [Google Typescript Style Guide](https://google.github.io/styleguide/tsguide.html) +but +detailed and adjusted for the needs of the ESMF Visual Studio Code Plugin JS schematics. + +## Copyright header +See [CONTRIBUTING](CONTRIBUTING.md) + +## Code Recommendations + +This project uses the library [Prettier](https://www.npmjs.com/package/prettier) and should also be created with it, so +that a clear code can be created. + +## Documentation + +### 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. + diff --git a/extension/LICENSE b/extension/LICENSE new file mode 100644 index 0000000..fa0086a --- /dev/null +++ b/extension/LICENSE @@ -0,0 +1,373 @@ +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. \ No newline at end of file diff --git a/extension/README.md b/extension/README.md new file mode 100644 index 0000000..b283371 --- /dev/null +++ b/extension/README.md @@ -0,0 +1,112 @@ +# Turtle LSP Extension + +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. + +## Requirements + +- 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: + +```bash +cd ../lsp-server +mvn package +``` + +The extension starts the server from `../lsp-server/target/lsp-server.jar`. + +## 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. + - Heavy Aspect validation from the server for model-level issues. +- Manual validation command: + - `Validate Aspect Model Now` +- Standard diagnostics flow: + - errors appear in the editor + - errors appear in `Problems` + +## 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](/Users/Evgenii_Filchenko/vs-code-project/extension/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. + +## Validation Behavior + +Fast feedback on type: + +- Driven by the server's regular Turtle parsing diagnostics. +- 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`. +- 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: `Validate Aspect Model Now` for the active Turtle document. + +## Commands + +- `Turtle LSP: Validate Aspect Model Now` + - Sends a server request for the active Turtle document. + +## 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 that includes the first detailed server report line when available. +- Automatic save validation keeps progress and completion feedback in the status bar. + +## Verify Go To Definition + +Use [samples/valid.ttl](/Users/Evgenii_Filchenko/vs-code-project/extension/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 an Aspect model file, for example: + +```turtle +@prefix : . +@prefix samm: . + +:InvalidSyntax a samm:Aspect; + samm:preferredName "Test Aspect"@en + samm:properties () ; + samm:operations () . +``` + +Manual check: + +1. Open the model file. +2. Run `Turtle LSP: Validate Aspect Model Now`. +3. Wait for the progress indicator to finish. +4. Confirm that diagnostics appear in the editor and in `Problems`. + +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. \ No newline at end of file diff --git a/lsp-server/CONTRIBUTING.md b/lsp-server/CONTRIBUTING.md new file mode 100644 index 0000000..00c1ad9 --- /dev/null +++ b/lsp-server/CONTRIBUTING.md @@ -0,0 +1,186 @@ +# 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 new file mode 100644 index 0000000..9c3292f --- /dev/null +++ b/lsp-server/CONVENTIONS.md @@ -0,0 +1,102 @@ +# 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 new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/lsp-server/LICENSE @@ -0,0 +1,373 @@ +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 new file mode 100644 index 0000000..4f646ee --- /dev/null +++ b/lsp-server/NOTICE.md @@ -0,0 +1,573 @@ +## 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 new file mode 100644 index 0000000..2949415 --- /dev/null +++ b/lsp-server/README.md @@ -0,0 +1,45 @@ +# 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. From 10b110e9e2c9ae6a6d9e6a0b1aae02b85dc2d6e6 Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Tue, 21 Apr 2026 08:13:46 +0200 Subject: [PATCH 03/12] Remove unintended local file system path --- extension/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/README.md b/extension/README.md index b283371..38c65f0 100644 --- a/extension/README.md +++ b/extension/README.md @@ -33,7 +33,7 @@ The extension starts the server from `../lsp-server/target/lsp-server.jar`. 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](/Users/Evgenii_Filchenko/vs-code-project/extension/samples/valid.ttl) or your Aspect model file. +5. 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. @@ -109,4 +109,4 @@ 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. \ No newline at end of file +3. Confirm that diagnostics are refreshed after completion. From d307761d6c3a8660d2baed71b4d09ced5c0a0399 Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Mon, 4 May 2026 14:22:41 +0200 Subject: [PATCH 04/12] 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 22e6071c80980a9899618131f725a508921535c9 Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Mon, 4 May 2026 14:23:03 +0200 Subject: [PATCH 05/12] 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 62f190bd762ca021f9aba1cd01403a6b926a7696 Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Mon, 4 May 2026 14:41:55 +0200 Subject: [PATCH 06/12] 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 | 99 ------------- extension/src/test/validationTestHarness.ts | 4 +- 6 files changed, 93 insertions(+), 249 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 6eae8d0..b9eeddc 100644 --- a/extension/src/test/aspectValidationController.test.ts +++ b/extension/src/test/aspectValidationController.test.ts @@ -1,106 +1,7 @@ -import * as assert from 'assert'; 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 4d1112c8800ba5b36b16e88228beb9f0cd81a0cd Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Tue, 12 May 2026 07:37:59 +0200 Subject: [PATCH 07/12] 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 db29aeede44425cdb0d08673ba45597426d9f0bf Mon Sep 17 00:00:00 2001 From: Andreas Textor Date: Tue, 12 May 2026 07:38:08 +0200 Subject: [PATCH 08/12] 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 fb3b83c9d95f850ebe66ecc0a5698c0fe670421a Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Tue, 26 May 2026 14:04:48 +0200 Subject: [PATCH 09/12] 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 3b551c87579f2a7c44475daf0d85d21849868586 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Tue, 26 May 2026 10:18:19 +0200 Subject: [PATCH 10/12] 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 59b5443ca86ecf0c93d1a4430b50efb5ca42bb69 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Wed, 3 Jun 2026 08:41:26 +0200 Subject: [PATCH 11/12] 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 1d7c6ab93e0477b77662b4e594eb95251cb153d5 Mon Sep 17 00:00:00 2001 From: Andreas Wirth Date: Wed, 3 Jun 2026 16:12:30 +0200 Subject: [PATCH 12/12] 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 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

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}`); }, }; }