diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e8ad7d65c..b9f2b21e1 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -12,5 +12,6 @@ module.exports = { extends: '@lars-reimann', rules: { 'import/extensions': 'off', + 'vitest/prefer-lowercase-title': 'off', }, }; diff --git a/esbuild.mjs b/esbuild.mjs index dd52d6066..f7a67c134 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -1,5 +1,6 @@ //@ts-check import * as esbuild from 'esbuild'; +import { copy } from 'esbuild-plugin-copy'; const watch = process.argv.includes('--watch'); const minify = process.argv.includes('--minify'); @@ -15,16 +16,26 @@ const padZeroes = function (i) { return i.toString().padStart(2, '0'); } -const plugins = [{ - name: 'watch-plugin', - setup(build) { - build.onEnd(result => { - if (result.errors.length === 0) { - console.log(getTime() + success); - } - }); +const plugins = [ + { + name: 'watch-plugin', + setup(build) { + build.onEnd(result => { + if (result.errors.length === 0) { + console.log(getTime() + success); + } + }); + }, }, -}]; + copy({ + // resolveFrom: 'cwd', + assets: { + from: ['./src/resources/**/*'], + to: ['./resources'], + }, + watch, + }), +]; const ctx = await esbuild.context({ // Entry points for the vscode extension and the language server @@ -38,7 +49,7 @@ const ctx = await esbuild.context({ outExtension: { '.js': '.cjs' }, - loader: { '.ts': 'ts' }, + loader: {'.ts': 'ts'}, external: ['vscode'], platform: 'node', sourcemap: !minify, diff --git a/package-lock.json b/package-lock.json index 0c873a20e..7f7e9bb21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@vitest/ui": "^0.34.3", "concurrently": "^8.2.1", "esbuild": "^0.19.2", + "esbuild-plugin-copy": "^2.1.1", "langium-cli": "^2.0.1", "typescript": "^5.2.2", "vitest": "^0.34.3" @@ -1202,6 +1203,19 @@ "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, + "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", @@ -1257,7 +1271,6 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -1407,6 +1420,15 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1523,6 +1545,33 @@ "chevrotain": "^11.0.0" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -1757,7 +1806,6 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "peer": true, "dependencies": { "path-type": "^4.0.0" }, @@ -1916,6 +1964,51 @@ "@esbuild/win32-x64": "0.19.2" } }, + "node_modules/esbuild-plugin-copy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/esbuild-plugin-copy/-/esbuild-plugin-copy-2.1.1.tgz", + "integrity": "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "fs-extra": "^10.0.1", + "globby": "^11.0.3" + }, + "peerDependencies": { + "esbuild": ">= 0.14.0" + } + }, + "node_modules/esbuild-plugin-copy/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, + "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/esbuild-plugin-copy/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2958,7 +3051,6 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "peer": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -3091,7 +3183,6 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true, - "peer": true, "engines": { "node": ">= 4" } @@ -3179,6 +3270,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -3905,6 +4008,15 @@ "dev": true, "peer": true }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4190,7 +4302,6 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -4388,6 +4499,18 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", @@ -4662,7 +4785,6 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -6687,6 +6809,16 @@ "color-convert": "^2.0.1" } }, + "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, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6732,8 +6864,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "peer": true + "dev": true }, "array.prototype.findlastindex": { "version": "1.2.2", @@ -6844,6 +6975,12 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6933,6 +7070,22 @@ "lodash-es": "^4.17.21" } }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.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" + } + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -7107,7 +7260,6 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "peer": true, "requires": { "path-type": "^4.0.0" } @@ -7238,6 +7390,41 @@ "@esbuild/win32-x64": "0.19.2" } }, + "esbuild-plugin-copy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/esbuild-plugin-copy/-/esbuild-plugin-copy-2.1.1.tgz", + "integrity": "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "fs-extra": "^10.0.1", + "globby": "^11.0.3" + }, + "dependencies": { + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -7996,7 +8183,6 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "peer": true, "requires": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -8089,8 +8275,7 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "peer": true + "dev": true }, "import-fresh": { "version": "3.3.0", @@ -8157,6 +8342,15 @@ "has-bigints": "^1.0.1" } }, + "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, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -8699,6 +8893,12 @@ "dev": true, "peer": true }, + "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 + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8906,8 +9106,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "peer": true + "dev": true }, "pathe": { "version": "1.1.1", @@ -9039,6 +9238,15 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, "regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", @@ -9231,8 +9439,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "peer": true + "dev": true }, "source-map": { "version": "0.6.1", diff --git a/package.json b/package.json index 0d0488334..86d599f15 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@vitest/ui": "^0.34.3", "concurrently": "^8.2.1", "esbuild": "^0.19.2", + "esbuild-plugin-copy": "^2.1.1", "langium-cli": "^2.0.1", "typescript": "^5.2.2", "vitest": "^0.34.3" diff --git a/src/language/builtins/workspaceManager.ts b/src/language/builtins/workspaceManager.ts new file mode 100644 index 000000000..0aec46d1e --- /dev/null +++ b/src/language/builtins/workspaceManager.ts @@ -0,0 +1,49 @@ +import { DefaultWorkspaceManager, LangiumDocument, LangiumDocumentFactory, LangiumSharedServices } from 'langium'; +import { WorkspaceFolder } from 'vscode-languageserver'; +import { URI } from 'vscode-uri'; +import { SAFE_DS_FILE_EXTENSIONS } from '../constants/fileExtensions.js'; +import { globSync } from 'glob'; +import path from 'path'; + +export class SafeDsWorkspaceManager extends DefaultWorkspaceManager { + private documentFactory: LangiumDocumentFactory; + + constructor(services: LangiumSharedServices) { + super(services); + this.documentFactory = services.workspace.LangiumDocumentFactory; + } + + protected override async loadAdditionalDocuments( + folders: WorkspaceFolder[], + collector: (document: LangiumDocument) => void, + ): Promise { + await super.loadAdditionalDocuments(folders, collector); + + for (const uri of listBuiltinsFiles()) { + collector(this.documentFactory.create(uri)); + } + } +} + +let builtinsPath: string; +if (__filename.endsWith('.ts')) { + // Before running ESBuild + builtinsPath = path.join(__dirname, '..', '..', 'resources', 'builtins'); +} /* c8 ignore start */ else { + // After running ESBuild + builtinsPath = path.join(__dirname, '..', 'resources', 'builtins'); +} /* c8 ignore stop */ + +/** + * Lists all Safe-DS files in `src/resources/builtins`. + * + * @return URIs of all discovered files. + */ +export const listBuiltinsFiles = (): URI[] => { + const pattern = `**/*.{${SAFE_DS_FILE_EXTENSIONS.join(',')}}`; + const relativePaths = globSync(pattern, { cwd: builtinsPath, nodir: true }); + return relativePaths.map((relativePath) => { + const absolutePath = path.join(builtinsPath, relativePath); + return URI.file(absolutePath); + }); +}; diff --git a/src/language/constant/fileExtensions.ts b/src/language/constants/fileExtensions.ts similarity index 100% rename from src/language/constant/fileExtensions.ts rename to src/language/constants/fileExtensions.ts diff --git a/src/language/safe-ds-module.ts b/src/language/safe-ds-module.ts index 38d351c1f..b7fb812c1 100644 --- a/src/language/safe-ds-module.ts +++ b/src/language/safe-ds-module.ts @@ -1,6 +1,7 @@ import { createDefaultModule, createDefaultSharedModule, + DeepPartial, DefaultSharedModuleContext, inject, LangiumServices, @@ -11,6 +12,7 @@ import { import { SafeDsGeneratedModule, SafeDsGeneratedSharedModule } from './generated/module.js'; import { SafeDsValidator, registerValidationChecks } from './validation/safe-ds-validator.js'; import { SafeDSFormatter } from './formatting/safe-ds-formatter.js'; +import { SafeDsWorkspaceManager } from './builtins/workspaceManager.js'; /** * Declaration of custom services - add your own service classes here. @@ -41,6 +43,14 @@ export const SafeDsModule: Module> = { + workspace: { + WorkspaceManager: (services) => new SafeDsWorkspaceManager(services), + }, +}; + /** * Create the full set of services required by Langium. * @@ -60,7 +70,7 @@ export const createSafeDsServices = function (context: DefaultSharedModuleContex shared: LangiumSharedServices; SafeDs: SafeDsServices; } { - const shared = inject(createDefaultSharedModule(context), SafeDsGeneratedSharedModule); + const shared = inject(createDefaultSharedModule(context), SafeDsGeneratedSharedModule, SafeDsSharedModule); const SafeDs = inject(createDefaultModule({ shared }), SafeDsGeneratedModule, SafeDsModule); shared.ServiceRegistry.register(SafeDs); registerValidationChecks(SafeDs); diff --git a/src/language/validation/nameConvention.ts b/src/language/validation/nameConvention.ts index 348391ef9..57dc02cc9 100644 --- a/src/language/validation/nameConvention.ts +++ b/src/language/validation/nameConvention.ts @@ -4,7 +4,8 @@ import { ValidationAcceptor } from 'langium'; const blockLambdaPrefix = '__block_lambda_'; export const nameMustNotStartWithBlockLambdaPrefix = (node: SdsDeclaration, accept: ValidationAcceptor) => { - if (node.name.startsWith(blockLambdaPrefix)) { + const name = node.name ?? ''; + if (name.startsWith(blockLambdaPrefix)) { accept( 'error', "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas.", @@ -55,8 +56,9 @@ export const nameShouldHaveCorrectCasing = (node: SdsDeclaration, accept: Valida } return; case 'SdsModule': - const segments = node.name.split('.'); - if (!segments.every(isLowerCamelCase)) { + const name = node.name ?? ''; + const segments = name.split('.'); + if (name !== '' && !segments.every(isLowerCamelCase)) { accept('warning', 'All segments of the qualified name of a package should be lowerCamelCase.', { node, property: 'name', diff --git a/src/resources/builtins/safeds/lang/codeGeneration.sdsstub b/src/resources/builtins/safeds/lang/codeGeneration.sdsstub new file mode 100644 index 000000000..9744f828c --- /dev/null +++ b/src/resources/builtins/safeds/lang/codeGeneration.sdsstub @@ -0,0 +1,24 @@ +package safeds.lang + +@Description("The qualified name of the corresponding Python module (default is the qualified name of the package).") +@Target(AnnotationTarget.CompilationUnit) +annotation PythonModule( + @Description("The qualified name of the corresponding Python module.") + qualifiedName: String +) + +@Description("The name of the corresponding API element in Python (default is the name of the declaration in the stubs).") +@Target( + AnnotationTarget.Attribute, + AnnotationTarget.Class, + AnnotationTarget.Enum, + AnnotationTarget.EnumVariant, + AnnotationTarget.Function, + AnnotationTarget.Parameter, + AnnotationTarget.Step, + AnnotationTarget.Pipeline +) +annotation PythonName( + @Description("The name of the corresponding API element in Python.") + name: String +) diff --git a/src/resources/builtins/safeds/lang/coreAnnotations.sdsstub b/src/resources/builtins/safeds/lang/coreAnnotations.sdsstub new file mode 100644 index 000000000..1ccdcd91d --- /dev/null +++ b/src/resources/builtins/safeds/lang/coreAnnotations.sdsstub @@ -0,0 +1,105 @@ +package safeds.lang + +@Description("The annotation can target these declaration types. If the @Target annotation is not used any declaration type can be targeted.") +@Target(AnnotationTarget.Annotation) +annotation Target( + @Description("The valid targets.") + vararg targets: AnnotationTarget +) + +@Description("The declaration types that can be targeted by annotations.") +enum AnnotationTarget { + @Description("The annotation can be called on annotations.") + Annotation + + @Description("The annotation can be called on attributes.") + Attribute + + @Description("The annotation can be called on classes.") + Class + + @Description("The annotation can be called on compilation units (i.e. files).") + CompilationUnit + + @Description("The annotation can be called on enums.") + Enum + + @Description("The annotation can be called on enum variants.") + EnumVariant + + @Description("The annotation can be called on functions.") + Function + + @Description("The annotation can be called on parameters.") + Parameter + + @Description("The annotation can be called on pipelines.") + Pipeline + + @Description("The annotation can be called on results.") + Result + + @Description("The annotation can be called on steps.") + Step + + @Description("The annotation can be called on type parameters.") + TypeParameter +} + +@Description("The annotation can be called multiple times for the same declaration.") +@Target(AnnotationTarget.Annotation) +annotation Repeatable + +@Description("The declaration should no longer be used.") +@Target( + AnnotationTarget.Annotation, + AnnotationTarget.Attribute, + AnnotationTarget.Class, + AnnotationTarget.Enum, + AnnotationTarget.EnumVariant, + AnnotationTarget.Function, + AnnotationTarget.Parameter, + AnnotationTarget.Result, + AnnotationTarget.Step, + AnnotationTarget.TypeParameter, +) +annotation Deprecated( + @Description("What to use instead.") + alternative: String? = null, + + @Description("Why the declaration was deprecated.") + reason: String? = null, + + @Description("When the declaration was deprecated.") + sinceVersion: String? = null, + + @Description("When the declaration will be removed.") + removalVersion: String? = null, +) + +@Description("The declaration might change without a major version bump.") +@Target( + AnnotationTarget.Annotation, + AnnotationTarget.Attribute, + AnnotationTarget.Class, + AnnotationTarget.Enum, + AnnotationTarget.EnumVariant, + AnnotationTarget.Function, + AnnotationTarget.Parameter, + AnnotationTarget.Result, + AnnotationTarget.Step, + AnnotationTarget.TypeParameter, +) +annotation Experimental + +@Description("The function has no side effects and returns the same results for the same arguments.") +@Target(AnnotationTarget.Function) +annotation Pure + +@Description("The function has no side effects.") +@Target(AnnotationTarget.Function) +annotation NoSideEffects + +@Description("Values assigned to this parameter must be constant.") +@Target(AnnotationTarget.Parameter) +annotation Constant diff --git a/src/resources/builtins/safeds/lang/coreClasses.sdsstub b/src/resources/builtins/safeds/lang/coreClasses.sdsstub new file mode 100644 index 000000000..857657ea9 --- /dev/null +++ b/src/resources/builtins/safeds/lang/coreClasses.sdsstub @@ -0,0 +1,22 @@ +package safeds.lang + +@Description("The common superclass of all classes.") +class Any + +@Description("The common subclass of all classes.") +class Nothing + +@Description("A truth value.") +class Boolean + +@Description("A number.") +class Number + +@Description("An integer.") +class Int sub Number + +@Description("A floating-point number.") +class Float sub Number + +@Description("Some text.") +class String diff --git a/src/resources/builtins/safeds/lang/documentation.sdsstub b/src/resources/builtins/safeds/lang/documentation.sdsstub new file mode 100644 index 000000000..92e887873 --- /dev/null +++ b/src/resources/builtins/safeds/lang/documentation.sdsstub @@ -0,0 +1,17 @@ +package safeds.lang + +@Description("The purpose of a declaration.") +annotation Description( + @Description("The purpose of a declaration.") + description: String +) + +@Description("The version in which a declaration was added.") +annotation Since( + @Description("The version in which a declaration was added.") + version: String +) + +@Description("This parameter should only be used by expert users.") +@Target(AnnotationTarget.Parameter) +annotation Expert diff --git a/src/resources/builtins/safeds/lang/schemaEffects.sdsstub b/src/resources/builtins/safeds/lang/schemaEffects.sdsstub new file mode 100644 index 000000000..9a54b5bd2 --- /dev/null +++ b/src/resources/builtins/safeds/lang/schemaEffects.sdsstub @@ -0,0 +1,43 @@ +package safeds.lang + +@Description("Reads the initial data schema of a dataset.") +abstract predicate $readSchema ( + datasetPath: String, +) -> ::InitialSchema + +@Description("Checks if the columns with the names 'columnName' exist in the data schema.") +abstract predicate $checkColumn ( + ::CurrentSchema, + vararg columnName: String, +) -> ::SchemaOut + +@Description("Remove all the columns with the names 'columnName' from the data schema.") +abstract predicate $removeColumn ( + ::CurrentSchema, + vararg columnName: String, +) -> ::SchemaOut + +@Description("Remove all the columns other than the ones with names 'columnName' from the data schema.") +abstract predicate $keepColumn ( + ::CurrentSchema, + vararg columnName: String, +) -> ::SchemaOut + +@Description("Rename the column with name 'currentColumnName' to 'newColumnName' in the data schema if the column exists.") +abstract predicate $renameColumn ( + ::CurrentSchema, + currentColumnName: String, + newColumnName: String, +) -> ::SchemaOut + +@Description("Add a column with name 'newColumnName' and datatype 'NewColumnDataType' in the data schema unless there is already one with the same name.") +abstract predicate $addColumn ( + ::CurrentSchema, + newColumnName: String, +) -> ::SchemaOut + +@Description("Change the datatype to 'NewColumnDataType' of a column with name 'ColumnName' in the data schema.") +abstract predicate $changeColumnType ( + ::CurrentSchema, + columnName: String, +) -> ::SchemaOut \ No newline at end of file diff --git a/tests/helpers/diagnostics.ts b/tests/helpers/diagnostics.ts index d36d10c46..4ae29243e 100644 --- a/tests/helpers/diagnostics.ts +++ b/tests/helpers/diagnostics.ts @@ -18,6 +18,20 @@ export const getSyntaxErrors = async (services: LangiumServices, code: string): ); }; +/** + * Get linking errors from a code snippet. + * + * @param services The language services. + * @param code The code snippet to check. + * @returns The errors. + */ +export const getLinkingErrors = async (services: LangiumServices, code: string): Promise => { + const validationResult = await validationHelper(services)(code); + return validationResult.diagnostics.filter( + (d) => d.severity === DiagnosticSeverity.Error && d.data?.code === 'linking-error', + ); +}; + /** * The code contains syntax errors. */ diff --git a/tests/helpers/testResources.ts b/tests/helpers/testResources.ts index 8a1435cda..82c5011f5 100644 --- a/tests/helpers/testResources.ts +++ b/tests/helpers/testResources.ts @@ -1,6 +1,6 @@ import path from 'path'; import { globSync } from 'glob'; -import { SAFE_DS_FILE_EXTENSIONS } from '../../src/language/constant/fileExtensions.js'; +import { SAFE_DS_FILE_EXTENSIONS } from '../../src/language/constants/fileExtensions.js'; import { group } from 'radash'; const resourcesPath = path.join(__dirname, '..', 'resources'); diff --git a/tests/language/builtins/workspaceManager.test.ts b/tests/language/builtins/workspaceManager.test.ts new file mode 100644 index 000000000..86643ec9f --- /dev/null +++ b/tests/language/builtins/workspaceManager.test.ts @@ -0,0 +1,38 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { listBuiltinsFiles } from '../../../src/language/builtins/workspaceManager.js'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { getLinkingErrors } from '../../helpers/diagnostics.js'; +import { NodeFileSystem } from 'langium/node'; + +const services = createSafeDsServices(NodeFileSystem).SafeDs; + +describe('SafeDsWorkspaceManager', () => { + beforeAll(async () => { + await services.shared.workspace.WorkspaceManager.initializeWorkspace([]); + }); + + describe('loadAdditionalDocuments', () => { + it.each(['Any', 'Boolean', 'Float', 'Int', 'Nothing', 'Number', 'String'])( + 'should be possible to refer to %s', + async (type) => { + const diagnostics = await getLinkingErrors( + services, + ` +package test + +class C { + attr a: ${type} +} + `, + ); + expect(diagnostics).toHaveLength(0); + }, + ); + }); +}); + +describe('listBuiltinsFiles', () => { + it('should not return an empty list', () => { + expect(listBuiltinsFiles().length).toBeGreaterThan(0); + }); +}); diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/no package name.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/no package name.sdstest new file mode 100644 index 000000000..ef31d98d5 --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/no package name.sdstest @@ -0,0 +1 @@ +// $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/packages with block lambda prefix.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/package name with block lambda prefix.sdstest similarity index 100% rename from tests/resources/validation/name convention/__block_lambda_ prefix/packages with block lambda prefix.sdstest rename to tests/resources/validation/name convention/__block_lambda_ prefix/package name with block lambda prefix.sdstest diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/packages without block lambda prefix.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/package name without block lambda prefix.sdstest similarity index 100% rename from tests/resources/validation/name convention/__block_lambda_ prefix/packages without block lambda prefix.sdstest rename to tests/resources/validation/name convention/__block_lambda_ prefix/package name without block lambda prefix.sdstest diff --git a/tests/resources/validation/name convention/casing of declaration names/no package name.sdstest b/tests/resources/validation/name convention/casing of declaration names/no package name.sdstest new file mode 100644 index 000000000..b31d16d34 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/no package name.sdstest @@ -0,0 +1 @@ +// $TEST$ no warning "All segments of the qualified name of a package should be lowerCamelCase."