From 12ee6b3ec6b8267955ff303710666e445ca943f1 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 27 Dec 2023 16:42:46 +0800 Subject: [PATCH 01/12] feat: migrate to Volar --- .gitignore | 7 +- packages/language-server/.gitignore | 3 - packages/language-server/.npmignore | 5 - packages/language-server/bin/server.js | 13 +- packages/language-server/package.json | 79 +- packages/language-server/src/importPackage.ts | 69 - packages/language-server/src/index.ts | 37 +- .../language-server/src/languagePlugin.ts | 189 ++ .../src/lib/DiagnosticsManager.ts | 97 -- .../src/lib/FallbackWatcher.ts | 71 - .../src/lib/documents/Document.ts | 153 -- .../src/lib/documents/DocumentBase.ts | 99 -- .../src/lib/documents/DocumentManager.ts | 153 -- .../src/lib/documents/DocumentMapper.ts | 412 ----- .../src/lib/documents/configLoader.ts | 273 --- .../src/lib/documents/fileCollection.ts | 101 -- .../src/lib/documents/index.ts | 5 - .../src/lib/documents/parseHtml.ts | 216 --- .../src/lib/documents/utils.ts | 444 ----- .../src/lib/foldingRange/indentFolding.ts | 188 -- .../lib/semanticToken/semanticTokenLegend.ts | 82 - packages/language-server/src/logger.ts | 28 - packages/language-server/src/ls-config.ts | 639 ------- .../language-server/src/plugins/PluginHost.ts | 762 -------- .../src/plugins/css/CSSDocument.ts | 100 -- .../src/plugins/css/CSSPlugin.ts | 545 ------ .../src/plugins/css/FileSystemProvider.ts | 94 - .../src/plugins/css/StyleAttributeDocument.ts | 77 - .../css/features/getIdClassCompletion.ts | 69 - .../plugins/css/features/svelte-selectors.ts | 16 - .../src/plugins/css/global-vars.ts | 57 - .../src/plugins/css/service.ts | 76 - .../src/plugins/documentContext.ts | 42 - .../src/plugins/html/HTMLPlugin.ts | 444 ----- .../src/plugins/html/dataProvider.ts | 431 ----- packages/language-server/src/plugins/index.ts | 7 - .../language-server/src/plugins/interfaces.ts | 282 --- .../src/plugins/svelte/SvelteDocument.ts | 467 ----- .../src/plugins/svelte/SveltePlugin.ts | 331 ---- .../src/plugins/svelte/features/SvelteTags.ts | 137 -- .../features/getCodeActions/getQuickfixes.ts | 243 --- .../getCodeActions/getRefactorings.ts | 162 -- .../svelte/features/getCodeActions/index.ts | 34 - .../plugins/svelte/features/getCompletions.ts | 257 --- .../plugins/svelte/features/getDiagnostics.ts | 343 ---- .../plugins/svelte/features/getHoverInfo.ts | 146 -- .../svelte/features/getModifierData.ts | 63 - .../svelte/features/getSelectionRanges.ts | 68 - .../src/plugins/svelte/features/utils.ts | 11 - .../typescript/ComponentInfoProvider.ts | 113 -- .../src/plugins/typescript/DocumentMapper.ts | 33 - .../plugins/typescript/DocumentSnapshot.ts | 781 --------- .../plugins/typescript/LSAndTSDocResolver.ts | 254 --- .../src/plugins/typescript/SnapshotManager.ts | 342 ---- .../plugins/typescript/TypeScriptPlugin.ts | 660 ------- .../features/CallHierarchyProvider.ts | 471 ----- .../features/CodeActionsProvider.ts | 1525 ----------------- .../typescript/features/CompletionProvider.ts | 1023 ----------- .../features/DiagnosticsProvider.ts | 558 ------ .../FindComponentReferencesProvider.ts | 92 - .../features/FindFileReferencesProvider.ts | 50 - .../features/FindReferencesProvider.ts | 212 --- .../features/FoldingRangeProvider.ts | 349 ---- .../typescript/features/HoverProvider.ts | 80 - .../features/ImplementationProvider.ts | 63 - .../typescript/features/InlayHintProvider.ts | 324 ---- .../typescript/features/RenameProvider.ts | 671 -------- .../features/SelectionRangeProvider.ts | 70 - .../features/SemanticTokensProvider.ts | 153 -- .../features/SignatureHelpProvider.ts | 156 -- .../features/TypeDefinitionProvider.ts | 45 - .../features/UpdateImportsProvider.ts | 102 -- .../getDirectiveCommentCompletions.ts | 77 - .../features/getJsDocTemplateCompletion.ts | 79 - .../src/plugins/typescript/features/utils.ts | 422 ----- .../src/plugins/typescript/module-loader.ts | 356 ---- .../src/plugins/typescript/previewer.ts | 140 -- .../src/plugins/typescript/service.ts | 907 ---------- .../plugins/typescript/svelte-ast-utils.ts | 178 -- .../src/plugins/typescript/svelte-sys.ts | 70 - .../src/plugins/typescript/utils.ts | 373 ---- packages/language-server/src/server.ts | 552 ------ packages/language-server/src/svelte-check.ts | 319 ---- packages/language-server/src/utils.ts | 343 ---- packages/language-server/tsconfig.json | 25 +- packages/language-server/wallaby.js | 9 - packages/svelte-check/.gitignore | 3 - packages/svelte-check/bin/svelte-check | 2 - packages/svelte-check/bin/svelte-check.js | 2 + packages/svelte-check/package.json | 42 +- packages/svelte-check/rollup.config.mjs | 73 - packages/svelte-check/src/bin.ts | 12 + packages/svelte-check/src/check.ts | 160 ++ packages/svelte-check/src/index.ts | 332 ++-- packages/svelte-check/src/options.ts | 229 +-- packages/svelte-check/src/writers.ts | 209 --- packages/svelte-check/tsconfig.json | 27 +- packages/svelte-vscode/.gitignore | 4 - packages/svelte-vscode/.vscode/launch.json | 16 - packages/svelte-vscode/README.md | 100 -- packages/svelte-vscode/package.json | 547 +----- .../src/CompiledCodeContentProvider.ts | 108 -- packages/svelte-vscode/src/extension.ts | 562 +----- packages/svelte-vscode/src/html/autoClose.ts | 105 -- .../src/html/htmlEmptyTagsShared.ts | 25 - .../src/sveltekit/generateFiles/commands.ts | 12 - .../src/sveltekit/generateFiles/generate.ts | 38 - .../src/sveltekit/generateFiles/index.ts | 147 -- .../src/sveltekit/generateFiles/resources.ts | 38 - .../generateFiles/templates/error.ts | 21 - .../generateFiles/templates/layout-load.ts | 28 - .../generateFiles/templates/layout-server.ts | 28 - .../generateFiles/templates/layout.ts | 20 - .../generateFiles/templates/page-load.ts | 28 - .../generateFiles/templates/page-server.ts | 28 - .../sveltekit/generateFiles/templates/page.ts | 20 - .../generateFiles/templates/server.ts | 20 - .../src/sveltekit/generateFiles/types.ts | 41 - packages/svelte-vscode/src/sveltekit/index.ts | 83 - packages/svelte-vscode/src/sveltekit/utils.ts | 49 - packages/svelte-vscode/src/tsplugin.ts | 64 - .../src/typescript/findComponentReferences.ts | 73 - .../src/typescript/findFileReferences.ts | 84 - packages/svelte-vscode/src/utils.ts | 9 - packages/svelte-vscode/tsconfig.json | 33 +- packages/typescript-plugin/.gitignore | 2 +- packages/typescript-plugin/.npmignore | 7 - packages/typescript-plugin/package.json | 15 +- .../typescript-plugin/src/config-manager.ts | 38 - packages/typescript-plugin/src/index.ts | 274 +-- .../src/language-service/call-hierarchy.ts | 332 ---- .../src/language-service/completions.ts | 270 --- .../src/language-service/definition.ts | 72 - .../src/language-service/diagnostics.ts | 196 --- .../src/language-service/file-references.ts | 32 - .../src/language-service/find-references.ts | 148 -- .../src/language-service/host.ts | 12 - .../src/language-service/hover.ts | 41 - .../src/language-service/implementation.ts | 38 - .../src/language-service/index.ts | 127 -- .../src/language-service/inlay-hints.ts | 35 - .../src/language-service/move-to-file.ts | 43 - .../src/language-service/navigate-to-items.ts | 55 - .../src/language-service/rename.ts | 103 -- .../src/language-service/sveltekit.ts | 825 --------- .../src/language-service/update-imports.ts | 44 - packages/typescript-plugin/src/logger.ts | 41 - .../typescript-plugin/src/module-loader.ts | 262 --- .../src/project-svelte-files.ts | 203 --- .../typescript-plugin/src/source-mapper.ts | 145 -- .../typescript-plugin/src/svelte-snapshots.ts | 427 ----- packages/typescript-plugin/src/svelte-sys.ts | 40 - packages/typescript-plugin/src/utils.ts | 239 --- packages/typescript-plugin/tsconfig.json | 25 +- pnpm-lock.yaml | 1480 +++++++++------- tsconfig.base.json | 18 + tsconfig.json | 20 +- 157 files changed, 1575 insertions(+), 28060 deletions(-) delete mode 100644 packages/language-server/.gitignore delete mode 100644 packages/language-server/.npmignore delete mode 100644 packages/language-server/src/importPackage.ts create mode 100644 packages/language-server/src/languagePlugin.ts delete mode 100644 packages/language-server/src/lib/DiagnosticsManager.ts delete mode 100644 packages/language-server/src/lib/FallbackWatcher.ts delete mode 100644 packages/language-server/src/lib/documents/Document.ts delete mode 100644 packages/language-server/src/lib/documents/DocumentBase.ts delete mode 100644 packages/language-server/src/lib/documents/DocumentManager.ts delete mode 100644 packages/language-server/src/lib/documents/DocumentMapper.ts delete mode 100644 packages/language-server/src/lib/documents/configLoader.ts delete mode 100644 packages/language-server/src/lib/documents/fileCollection.ts delete mode 100644 packages/language-server/src/lib/documents/index.ts delete mode 100644 packages/language-server/src/lib/documents/parseHtml.ts delete mode 100644 packages/language-server/src/lib/documents/utils.ts delete mode 100644 packages/language-server/src/lib/foldingRange/indentFolding.ts delete mode 100644 packages/language-server/src/lib/semanticToken/semanticTokenLegend.ts delete mode 100644 packages/language-server/src/logger.ts delete mode 100644 packages/language-server/src/ls-config.ts delete mode 100644 packages/language-server/src/plugins/PluginHost.ts delete mode 100644 packages/language-server/src/plugins/css/CSSDocument.ts delete mode 100644 packages/language-server/src/plugins/css/CSSPlugin.ts delete mode 100644 packages/language-server/src/plugins/css/FileSystemProvider.ts delete mode 100644 packages/language-server/src/plugins/css/StyleAttributeDocument.ts delete mode 100644 packages/language-server/src/plugins/css/features/getIdClassCompletion.ts delete mode 100644 packages/language-server/src/plugins/css/features/svelte-selectors.ts delete mode 100644 packages/language-server/src/plugins/css/global-vars.ts delete mode 100644 packages/language-server/src/plugins/css/service.ts delete mode 100644 packages/language-server/src/plugins/documentContext.ts delete mode 100644 packages/language-server/src/plugins/html/HTMLPlugin.ts delete mode 100644 packages/language-server/src/plugins/html/dataProvider.ts delete mode 100644 packages/language-server/src/plugins/index.ts delete mode 100644 packages/language-server/src/plugins/interfaces.ts delete mode 100644 packages/language-server/src/plugins/svelte/SvelteDocument.ts delete mode 100644 packages/language-server/src/plugins/svelte/SveltePlugin.ts delete mode 100644 packages/language-server/src/plugins/svelte/features/SvelteTags.ts delete mode 100644 packages/language-server/src/plugins/svelte/features/getCodeActions/getQuickfixes.ts delete mode 100644 packages/language-server/src/plugins/svelte/features/getCodeActions/getRefactorings.ts delete mode 100644 packages/language-server/src/plugins/svelte/features/getCodeActions/index.ts delete mode 100644 packages/language-server/src/plugins/svelte/features/getCompletions.ts delete mode 100644 packages/language-server/src/plugins/svelte/features/getDiagnostics.ts delete mode 100644 packages/language-server/src/plugins/svelte/features/getHoverInfo.ts delete mode 100644 packages/language-server/src/plugins/svelte/features/getModifierData.ts delete mode 100644 packages/language-server/src/plugins/svelte/features/getSelectionRanges.ts delete mode 100644 packages/language-server/src/plugins/svelte/features/utils.ts delete mode 100644 packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/DocumentMapper.ts delete mode 100644 packages/language-server/src/plugins/typescript/DocumentSnapshot.ts delete mode 100644 packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts delete mode 100644 packages/language-server/src/plugins/typescript/SnapshotManager.ts delete mode 100644 packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/CompletionProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/FindComponentReferencesProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/FoldingRangeProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/HoverProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/RenameProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/getDirectiveCommentCompletions.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/getJsDocTemplateCompletion.ts delete mode 100644 packages/language-server/src/plugins/typescript/features/utils.ts delete mode 100644 packages/language-server/src/plugins/typescript/module-loader.ts delete mode 100644 packages/language-server/src/plugins/typescript/previewer.ts delete mode 100644 packages/language-server/src/plugins/typescript/service.ts delete mode 100644 packages/language-server/src/plugins/typescript/svelte-ast-utils.ts delete mode 100644 packages/language-server/src/plugins/typescript/svelte-sys.ts delete mode 100644 packages/language-server/src/plugins/typescript/utils.ts delete mode 100644 packages/language-server/src/server.ts delete mode 100644 packages/language-server/src/svelte-check.ts delete mode 100644 packages/language-server/src/utils.ts delete mode 100644 packages/language-server/wallaby.js delete mode 100644 packages/svelte-check/.gitignore delete mode 100755 packages/svelte-check/bin/svelte-check create mode 100644 packages/svelte-check/bin/svelte-check.js delete mode 100644 packages/svelte-check/rollup.config.mjs create mode 100644 packages/svelte-check/src/bin.ts create mode 100644 packages/svelte-check/src/check.ts delete mode 100644 packages/svelte-check/src/writers.ts delete mode 100644 packages/svelte-vscode/.gitignore delete mode 100644 packages/svelte-vscode/.vscode/launch.json delete mode 100644 packages/svelte-vscode/README.md delete mode 100644 packages/svelte-vscode/src/CompiledCodeContentProvider.ts delete mode 100644 packages/svelte-vscode/src/html/autoClose.ts delete mode 100644 packages/svelte-vscode/src/html/htmlEmptyTagsShared.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/commands.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/generate.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/index.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/resources.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/templates/error.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout-load.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout-server.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/templates/page-load.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/templates/page-server.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/templates/page.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/templates/server.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/generateFiles/types.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/index.ts delete mode 100644 packages/svelte-vscode/src/sveltekit/utils.ts delete mode 100644 packages/svelte-vscode/src/tsplugin.ts delete mode 100644 packages/svelte-vscode/src/typescript/findComponentReferences.ts delete mode 100644 packages/svelte-vscode/src/typescript/findFileReferences.ts delete mode 100644 packages/svelte-vscode/src/utils.ts delete mode 100644 packages/typescript-plugin/.npmignore delete mode 100644 packages/typescript-plugin/src/config-manager.ts delete mode 100644 packages/typescript-plugin/src/language-service/call-hierarchy.ts delete mode 100644 packages/typescript-plugin/src/language-service/completions.ts delete mode 100644 packages/typescript-plugin/src/language-service/definition.ts delete mode 100644 packages/typescript-plugin/src/language-service/diagnostics.ts delete mode 100644 packages/typescript-plugin/src/language-service/file-references.ts delete mode 100644 packages/typescript-plugin/src/language-service/find-references.ts delete mode 100644 packages/typescript-plugin/src/language-service/host.ts delete mode 100644 packages/typescript-plugin/src/language-service/hover.ts delete mode 100644 packages/typescript-plugin/src/language-service/implementation.ts delete mode 100644 packages/typescript-plugin/src/language-service/index.ts delete mode 100644 packages/typescript-plugin/src/language-service/inlay-hints.ts delete mode 100644 packages/typescript-plugin/src/language-service/move-to-file.ts delete mode 100644 packages/typescript-plugin/src/language-service/navigate-to-items.ts delete mode 100644 packages/typescript-plugin/src/language-service/rename.ts delete mode 100644 packages/typescript-plugin/src/language-service/sveltekit.ts delete mode 100644 packages/typescript-plugin/src/language-service/update-imports.ts delete mode 100644 packages/typescript-plugin/src/logger.ts delete mode 100644 packages/typescript-plugin/src/module-loader.ts delete mode 100644 packages/typescript-plugin/src/project-svelte-files.ts delete mode 100644 packages/typescript-plugin/src/source-mapper.ts delete mode 100644 packages/typescript-plugin/src/svelte-snapshots.ts delete mode 100644 packages/typescript-plugin/src/svelte-sys.ts delete mode 100644 packages/typescript-plugin/src/utils.ts create mode 100644 tsconfig.base.json diff --git a/.gitignore b/.gitignore index d64228482..ffc2f6c57 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,12 @@ typings/ .cache # tdsx -dist +out +*.tsbuildinfo # VSCode history extension .history + +# syntaxes +postcss.json +svelte.tmLanguage.json diff --git a/packages/language-server/.gitignore b/packages/language-server/.gitignore deleted file mode 100644 index 5dbbc7fbb..000000000 --- a/packages/language-server/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist/ -.vscode/ -node_modules/ diff --git a/packages/language-server/.npmignore b/packages/language-server/.npmignore deleted file mode 100644 index 1039342b3..000000000 --- a/packages/language-server/.npmignore +++ /dev/null @@ -1,5 +0,0 @@ -/node_modules -/src -/test -/dist/test -wallaby.js diff --git a/packages/language-server/bin/server.js b/packages/language-server/bin/server.js index dc8ddbe65..1bee448bf 100755 --- a/packages/language-server/bin/server.js +++ b/packages/language-server/bin/server.js @@ -1,5 +1,8 @@ -#! /usr/bin/env node - -const { startServer } = require('../dist/src/server'); - -startServer(); +#!/usr/bin/env node +if (process.argv.includes('--version')) { + const pkgJSON = require('../package.json'); + console.log(`${pkgJSON['version']}`); +} +else { + require('../out/index.js'); +} diff --git a/packages/language-server/package.json b/packages/language-server/package.json index fc66d7b54..185ce52ba 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -1,68 +1,29 @@ { "name": "svelte-language-server", - "version": "0.16.0", - "description": "A language server for Svelte", - "main": "dist/src/index.js", - "typings": "dist/src/index", - "scripts": { - "test": "cross-env TS_NODE_TRANSPILE_ONLY=true mocha --require ts-node/register \"test/**/*.ts\" --exclude \"test/**/*.d.ts\"", - "build": "tsc", - "prepublishOnly": "npm run build", - "watch": "tsc -w" - }, + "version": "0.0.0", + "main": "out/index.js", + "license": "MIT", + "files": [ + "out/**/*.js", + "out/**/*.d.ts" + ], "bin": { - "svelteserver": "bin/server.js" + "svelte-language-server": "./bin/server.js" }, "repository": { "type": "git", - "url": "git+https://github.com/sveltejs/language-tools.git" - }, - "keywords": [ - "svelte", - "vscode", - "atom", - "editor", - "language-server" - ], - "author": "James Birtles and the Svelte Language Tools contributors", - "license": "MIT", - "bugs": { - "url": "https://github.com/sveltejs/language-tools/issues" - }, - "homepage": "https://github.com/sveltejs/language-tools#readme", - "engines": { - "node": ">= 12.0.0" - }, - "devDependencies": { - "@types/estree": "^0.0.42", - "@types/lodash": "^4.14.116", - "@types/mocha": "^9.1.0", - "@types/node": "^16.0.0", - "@types/prettier": "^2.2.3", - "@types/sinon": "^7.5.2", - "cross-env": "^7.0.2", - "mocha": "^9.2.0", - "sinon": "^11.0.0", - "ts-node": "^10.0.0" + "url": "https://github.com/johnsoncodehk/volar.git", + "directory": "examples/svelte-language-server" }, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "@vscode/emmet-helper": "2.8.4", - "chokidar": "^3.4.1", - "estree-walker": "^2.0.1", - "fast-glob": "^3.2.7", - "lodash": "^4.17.21", - "prettier": "~3.1.0", - "prettier-plugin-svelte": "~3.1.1", - "svelte": "^3.57.0", - "svelte-preprocess": "~5.1.0", - "svelte2tsx": "workspace:~", - "typescript": "^5.3.2", - "vscode-css-languageservice": "~6.2.10", - "vscode-html-languageservice": "~5.1.1", - "vscode-languageserver": "8.0.2", - "vscode-languageserver-protocol": "3.17.2", - "vscode-languageserver-types": "3.17.2", - "vscode-uri": "~3.0.0" + "@jridgewell/sourcemap-codec": "^1.4.15", + "@volar/language-core": "2.0.0-alpha.13", + "@volar/language-server": "2.0.0-alpha.13", + "svelte2tsx": "^0.6.27", + "volar-service-css": "0.0.25", + "volar-service-html": "0.0.25", + "volar-service-typescript": "0.0.25", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" } -} +} \ No newline at end of file diff --git a/packages/language-server/src/importPackage.ts b/packages/language-server/src/importPackage.ts deleted file mode 100644 index b51c23a3b..000000000 --- a/packages/language-server/src/importPackage.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { dirname, resolve } from 'path'; -import * as prettier from 'prettier'; -import * as svelte from 'svelte/compiler'; -import sveltePreprocess from 'svelte-preprocess'; -import { Logger } from './logger'; - -/** - * Whether or not the current workspace can be trusted. - * TODO rework this to a class which depends on the LsConfigManager - * and inject that class into all places where it's needed (Document etc.) - */ -let isTrusted = true; - -export function setIsTrusted(_isTrusted: boolean) { - isTrusted = _isTrusted; -} - -/** - * This function encapsulates the require call in one place - * so we can replace its content inside rollup builds - * so it's not transformed. - */ -function dynamicRequire(dynamicFileToRequire: string): any { - // prettier-ignore - return require(dynamicFileToRequire); -} - -export function getPackageInfo(packageName: string, fromPath: string) { - const paths = [__dirname]; - if (isTrusted) { - paths.unshift(fromPath); - } - const packageJSONPath = require.resolve(`${packageName}/package.json`, { - paths - }); - const { version } = dynamicRequire(packageJSONPath); - const [major, minor, patch] = version.split('.'); - - return { - path: dirname(packageJSONPath), - version: { - full: version, - major: Number(major), - minor: Number(minor), - patch: Number(patch) - } - }; -} - -export function importPrettier(fromPath: string): typeof prettier { - const pkg = getPackageInfo('prettier', fromPath); - const main = resolve(pkg.path); - Logger.debug('Using Prettier v' + pkg.version.full, 'from', main); - return dynamicRequire(main); -} - -export function importSvelte(fromPath: string): typeof svelte { - const pkg = getPackageInfo('svelte', fromPath); - const main = resolve(pkg.path, 'compiler'); - Logger.debug('Using Svelte v' + pkg.version.full, 'from', main); - return dynamicRequire(main + (pkg.version.major >= 4 ? '.cjs' : '')); -} - -export function importSveltePreprocess(fromPath: string): typeof sveltePreprocess { - const pkg = getPackageInfo('svelte-preprocess', fromPath); - const main = resolve(pkg.path); - Logger.debug('Using svelte-preprocess v' + pkg.version.full, 'from', main); - return dynamicRequire(main); -} diff --git a/packages/language-server/src/index.ts b/packages/language-server/src/index.ts index aaf743855..4242fd165 100644 --- a/packages/language-server/src/index.ts +++ b/packages/language-server/src/index.ts @@ -1,3 +1,34 @@ -export * from './server'; -export { offsetAt } from './lib/documents'; -export { SvelteCheck, SvelteCheckOptions, SvelteCheckDiagnosticSource } from './svelte-check'; +import { createConnection, createNodeServer, createTypeScriptProjectProvider } from '@volar/language-server/node'; +import { create as createCssScriptServicePlugin } from 'volar-service-css'; +import { create as createHtmlServicePlugin } from 'volar-service-html'; +import { create as createTypeScriptServicePlugin } from 'volar-service-typescript'; +import { svelteLanguagePlugin } from './languagePlugin'; + +const connection = createConnection(); +const server = createNodeServer(connection); + +connection.listen(); + +connection.onInitialize(params => { + return server.initialize(params, createTypeScriptProjectProvider, { + watchFileExtensions: ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts', 'jsx', 'tsx', 'json', 'svelte'], + getServicePlugins() { + return [ + createCssScriptServicePlugin(), + createHtmlServicePlugin(), + createTypeScriptServicePlugin(server.modules.typescript!), + ] + }, + getLanguagePlugins() { + return [svelteLanguagePlugin]; + }, + }); +}); + +connection.onInitialized(() => { + server.initialized(); +}); + +connection.onShutdown(() => { + server.shutdown(); +}); diff --git a/packages/language-server/src/languagePlugin.ts b/packages/language-server/src/languagePlugin.ts new file mode 100644 index 000000000..dcfe3f214 --- /dev/null +++ b/packages/language-server/src/languagePlugin.ts @@ -0,0 +1,189 @@ +import { decode } from '@jridgewell/sourcemap-codec'; +import type { CodeMapping, LanguagePlugin, VirtualFile } from '@volar/language-core'; +import { svelte2tsx } from 'svelte2tsx'; +import type * as ts from 'typescript'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { URI } from 'vscode-uri'; + +export const svelteLanguagePlugin: LanguagePlugin = { + createVirtualFile(fileName, languageId, snapshot) { + if (languageId === 'svelte') { + return { + fileName, + languageId, + snapshot, + embeddedFiles: [ + ...getVirtualCssFiles(fileName, snapshot.getText(0, snapshot.getLength())), + getVirtualTsFile(fileName, snapshot.getText(0, snapshot.getLength())), + ].filter((v): v is VirtualFile => !!v), + mappings: [], + codegenStacks: [] + }; + } + }, + updateVirtualFile(svelteFile, snapshot) { + svelteFile.snapshot = snapshot; + svelteFile.embeddedFiles = [ + ...getVirtualCssFiles(svelteFile.fileName, snapshot.getText(0, snapshot.getLength())), + getVirtualTsFile(svelteFile.fileName, snapshot.getText(0, snapshot.getLength())), + ].filter((v): v is VirtualFile => !!v); + }, + typescript: { + extraFileExtensions: [{ + extension: 'svelte', + isMixedContent: true, + scriptKind: 7 satisfies ts.ScriptKind.Deferred, + }], + resolveSourceFileName(tsFileName) { + if (tsFileName.endsWith('.svelte.ts')) { + return tsFileName.substring(0, tsFileName.length - '.ts'.length); + } + }, + resolveModuleName(moduleName, impliedNodeFormat) { + if (impliedNodeFormat === 99 satisfies ts.ModuleKind.ESNext && moduleName.endsWith('.svelte')) { + return `${moduleName}.js`; + } + }, + }, +}; + +function* getVirtualCssFiles(fileName: string, content: string): Generator { + + const styleBlocks = [...content.matchAll(/\([\s\S]*?)\<\/style\>/g)]; + + for (let i = 0; i < styleBlocks.length; i++) { + const styleBlock = styleBlocks[i]; + if (styleBlock.index !== undefined) { + const matchText = styleBlock[1]; + yield { + fileName: fileName + '.' + i + '.css', + languageId: 'css', + snapshot: { + getText(start, end) { + return matchText.substring(start, end); + }, + getLength() { + return matchText.length; + }, + getChangeRange() { + return undefined; + }, + }, + mappings: [ + { + sourceOffsets: [styleBlock.index + styleBlock[0].indexOf(matchText)], + generatedOffsets: [0], + lengths: [matchText.length], + data: { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: false, + }, + } + ], + embeddedFiles: [], + } + } + } + +} + +function getVirtualTsFile(fileName: string, text: string): VirtualFile | undefined { + + try { + const tsx = svelte2tsx(text, { + filename: fileName, + isTsFile: true, + mode: 'ts', + }); + const v3Mappings = decode(tsx.map.mappings); + const document = TextDocument.create(URI.file(fileName).toString(), 'svelte', 0, text); + const generateDocument = TextDocument.create(URI.file(fileName + '.ts').toString(), 'typescript', 0, tsx.code); + const mappings: CodeMapping[] = []; + + let current: { + genOffset: number, + sourceOffset: number, + } | undefined; + + for (let genLine = 0; genLine < v3Mappings.length; genLine++) { + for (const segment of v3Mappings[genLine]) { + const genCharacter = segment[0]; + const genOffset = generateDocument.offsetAt({ line: genLine, character: genCharacter }); + if (current) { + let length = genOffset - current.genOffset; + const sourceText = text.substring(current.sourceOffset, current.sourceOffset + length); + const genText = tsx.code.substring(current.genOffset, current.genOffset + length); + if (sourceText !== genText) { + length = 0; + for (let i = 0; i < genOffset - current.genOffset; i++) { + if (sourceText[i] === genText[i]) { + length = i + 1; + } + else { + break; + } + } + } + if (length > 0) { + const lastMapping = mappings.length ? mappings[mappings.length - 1] : undefined; + if ( + lastMapping && + lastMapping.generatedOffsets[0] + lastMapping.lengths[0] === current.genOffset && + lastMapping.sourceOffsets[0] + lastMapping.lengths[0] === current.sourceOffset + ) { + lastMapping.lengths[0] += length; + } + else { + mappings.push({ + sourceOffsets: [current.sourceOffset], + generatedOffsets: [current.genOffset], + lengths: [length], + data: { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: false, + }, + }); + } + } + current = undefined; + } + if (segment[2] !== undefined && segment[3] !== undefined) { + const sourceOffset = document.offsetAt({ line: segment[2], character: segment[3] }); + current = { + genOffset, + sourceOffset, + }; + } + } + } + + return { + fileName: fileName + '.ts', + languageId: 'typescript', + typescript: { + scriptKind: 3, + }, + snapshot: { + getText(start, end) { + return tsx.code.substring(start, end); + }, + getLength() { + return tsx.code.length; + }, + getChangeRange() { + return undefined; + }, + }, + mappings: mappings, + embeddedFiles: [], + }; + } catch { } +} diff --git a/packages/language-server/src/lib/DiagnosticsManager.ts b/packages/language-server/src/lib/DiagnosticsManager.ts deleted file mode 100644 index ac94a25f0..000000000 --- a/packages/language-server/src/lib/DiagnosticsManager.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - Connection, - TextDocumentIdentifier, - Diagnostic, - CancellationTokenSource, - CancellationToken -} from 'vscode-languageserver'; -import { DocumentManager, Document } from './documents'; -import { debounceThrottle } from '../utils'; - -export type SendDiagnostics = Connection['sendDiagnostics']; -export type GetDiagnostics = ( - doc: TextDocumentIdentifier, - cancellationToken?: CancellationToken -) => Thenable; - -export class DiagnosticsManager { - constructor( - private sendDiagnostics: SendDiagnostics, - private docManager: DocumentManager, - private getDiagnostics: GetDiagnostics - ) {} - - private pendingUpdates = new Set(); - private cancellationTokens = new Map void }>(); - - private updateAll() { - this.docManager.getAllOpenedByClient().forEach((doc) => { - this.update(doc[1]); - }); - this.pendingUpdates.clear(); - } - - scheduleUpdateAll() { - this.cancellationTokens.forEach((token) => token.cancel()); - this.cancellationTokens.clear(); - this.pendingUpdates.clear(); - this.debouncedUpdateAll(); - } - - private debouncedUpdateAll = debounceThrottle(() => this.updateAll(), 1000); - - private async update(document: Document) { - const uri = document.getURL(); - this.cancelStarted(uri); - - const tokenSource = new CancellationTokenSource(); - this.cancellationTokens.set(uri, tokenSource); - - const diagnostics = await this.getDiagnostics( - { uri: document.getURL() }, - tokenSource.token - ); - this.sendDiagnostics({ - uri: document.getURL(), - diagnostics - }); - - tokenSource.dispose(); - - if (this.cancellationTokens.get(uri) === tokenSource) { - this.cancellationTokens.delete(uri); - } - } - - cancelStarted(uri: string) { - const started = this.cancellationTokens.get(uri); - if (started) { - started.cancel(); - } - } - - removeDiagnostics(document: Document) { - this.pendingUpdates.delete(document); - this.sendDiagnostics({ - uri: document.getURL(), - diagnostics: [] - }); - } - - scheduleUpdate(document: Document) { - if (!this.docManager.isOpenedInClient(document.getURL())) { - return; - } - - this.cancelStarted(document.getURL()); - this.pendingUpdates.add(document); - this.scheduleBatchUpdate(); - } - - private scheduleBatchUpdate = debounceThrottle(() => { - this.pendingUpdates.forEach((doc) => { - this.update(doc); - }); - this.pendingUpdates.clear(); - }, 700); -} diff --git a/packages/language-server/src/lib/FallbackWatcher.ts b/packages/language-server/src/lib/FallbackWatcher.ts deleted file mode 100644 index eac647051..000000000 --- a/packages/language-server/src/lib/FallbackWatcher.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { FSWatcher, watch } from 'chokidar'; -import { debounce } from 'lodash'; -import { join } from 'path'; -import { DidChangeWatchedFilesParams, FileChangeType, FileEvent } from 'vscode-languageserver'; -import { pathToUrl } from '../utils'; - -type DidChangeHandler = (para: DidChangeWatchedFilesParams) => void; - -const DELAY = 50; - -export class FallbackWatcher { - private readonly watcher: FSWatcher; - private readonly callbacks: DidChangeHandler[] = []; - - private undeliveredFileEvents: FileEvent[] = []; - - constructor(glob: string, workspacePaths: string[]) { - const gitOrNodeModules = /\.git|node_modules/; - this.watcher = watch( - workspacePaths.map((workspacePath) => join(workspacePath, glob)), - { - ignored: (path: string) => - gitOrNodeModules.test(path) && - // Handle Sapper's alias mapping - !path.includes('src/node_modules') && - !path.includes('src\\node_modules'), - - // typescript would scan the project files on init. - // We only need to know what got updated. - ignoreInitial: true, - ignorePermissionErrors: true - } - ); - - this.watcher - .on('add', (path) => this.onFSEvent(path, FileChangeType.Created)) - .on('unlink', (path) => this.onFSEvent(path, FileChangeType.Deleted)) - .on('change', (path) => this.onFSEvent(path, FileChangeType.Changed)); - } - - private convert(path: string, type: FileChangeType): FileEvent { - return { - type, - uri: pathToUrl(path) - }; - } - - private onFSEvent(path: string, type: FileChangeType) { - const fileEvent = this.convert(path, type); - - this.undeliveredFileEvents.push(fileEvent); - this.scheduleTrigger(); - } - - private readonly scheduleTrigger = debounce(() => { - const para: DidChangeWatchedFilesParams = { - changes: this.undeliveredFileEvents - }; - this.undeliveredFileEvents = []; - - this.callbacks.forEach((callback) => callback(para)); - }, DELAY); - - onDidChangeWatchedFiles(callback: DidChangeHandler) { - this.callbacks.push(callback); - } - - dispose() { - this.watcher.close(); - } -} diff --git a/packages/language-server/src/lib/documents/Document.ts b/packages/language-server/src/lib/documents/Document.ts deleted file mode 100644 index 369808e2c..000000000 --- a/packages/language-server/src/lib/documents/Document.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { urlToPath } from '../../utils'; -import { WritableDocument } from './DocumentBase'; -import { extractScriptTags, extractStyleTag, extractTemplateTag, TagInformation } from './utils'; -import { parseHtml } from './parseHtml'; -import { SvelteConfig, configLoader } from './configLoader'; -import { HTMLDocument } from 'vscode-html-languageservice'; -import { Range } from 'vscode-languageserver'; - -/** - * Represents a text document contains a svelte component. - */ -export class Document extends WritableDocument { - languageId = 'svelte'; - scriptInfo: TagInformation | null = null; - moduleScriptInfo: TagInformation | null = null; - styleInfo: TagInformation | null = null; - templateInfo: TagInformation | null = null; - configPromise: Promise; - config?: SvelteConfig; - html!: HTMLDocument; - openedByClient = false; - /** - * Compute and cache directly because of performance reasons - * and it will be called anyway. - */ - private path = urlToPath(this.url); - - constructor( - public url: string, - public content: string - ) { - super(); - this.configPromise = configLoader.awaitConfig(this.getFilePath() || ''); - this.updateDocInfo(); - } - - private updateDocInfo() { - this.html = parseHtml(this.content); - const update = (config: SvelteConfig | undefined) => { - const scriptTags = extractScriptTags(this.content, this.html); - this.config = config; - this.scriptInfo = this.addDefaultLanguage(config, scriptTags?.script || null, 'script'); - this.moduleScriptInfo = this.addDefaultLanguage( - config, - scriptTags?.moduleScript || null, - 'script' - ); - this.styleInfo = this.addDefaultLanguage( - config, - extractStyleTag(this.content, this.html), - 'style' - ); - this.templateInfo = this.addDefaultLanguage( - config, - extractTemplateTag(this.content, this.html), - 'markup' - ); - }; - - const config = configLoader.getConfig(this.getFilePath() || ''); - if (config && !config.loadConfigError) { - update(config); - } else { - update(undefined); - this.configPromise.then((c) => update(c)); - } - } - - /** - * Get text content - */ - getText(range?: Range): string { - if (range) { - return this.content.substring(this.offsetAt(range.start), this.offsetAt(range.end)); - } - return this.content; - } - - /** - * Set text content and increase the document version - */ - setText(text: string) { - this.content = text; - this.version++; - this.lineOffsets = undefined; - this.updateDocInfo(); - } - - /** - * Returns the file path if the url scheme is file - */ - getFilePath(): string | null { - return this.path; - } - - /** - * Get URL file path. - */ - getURL() { - return this.url; - } - - /** - * Returns the language associated to script, style or template. - * Returns an empty string if there's nothing set. - */ - getLanguageAttribute(tag: 'script' | 'style' | 'template'): string { - const attrs = - (tag === 'style' - ? this.styleInfo?.attributes - : tag === 'script' - ? this.scriptInfo?.attributes || this.moduleScriptInfo?.attributes - : this.templateInfo?.attributes) || {}; - const lang = attrs.lang || attrs.type || ''; - return lang.replace(/^text\//, ''); - } - - /** - * Returns true if there's `lang="X"` on script or style or template. - */ - hasLanguageAttribute(): boolean { - return ( - !!this.getLanguageAttribute('script') || - !!this.getLanguageAttribute('style') || - !!this.getLanguageAttribute('template') - ); - } - - /** - * @deprecated This no longer exists in svelte-preprocess v5, we leave it in in case someone is using this with v4 - */ - private addDefaultLanguage( - config: SvelteConfig | undefined, - tagInfo: TagInformation | null, - tag: 'style' | 'script' | 'markup' - ): TagInformation | null { - if (!tagInfo || !config) { - return tagInfo; - } - - const defaultLang = Array.isArray(config.preprocess) - ? config.preprocess.find((group) => group.defaultLanguages?.[tag])?.defaultLanguages?.[ - tag - ] - : config.preprocess?.defaultLanguages?.[tag]; - - if (!tagInfo.attributes.lang && !tagInfo.attributes.type && defaultLang) { - tagInfo.attributes.lang = defaultLang; - } - - return tagInfo; - } -} diff --git a/packages/language-server/src/lib/documents/DocumentBase.ts b/packages/language-server/src/lib/documents/DocumentBase.ts deleted file mode 100644 index 3a3868b57..000000000 --- a/packages/language-server/src/lib/documents/DocumentBase.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Position, Range, TextDocument } from 'vscode-languageserver'; -import { getLineOffsets, offsetAt, positionAt } from './utils'; - -/** - * Represents a textual document. - */ -export abstract class ReadableDocument implements TextDocument { - /** - * Get the text content of the document - */ - abstract getText(range?: Range): string; - - /** - * Returns the url of the document - */ - abstract getURL(): string; - - /** - * Returns the file path if the url scheme is file - */ - abstract getFilePath(): string | null; - - /** - * Current version of the document. - */ - public version = 0; - - /** - * Should be cleared when there's an update to the text - */ - protected lineOffsets?: number[]; - - /** - * Get the length of the document's content - */ - getTextLength(): number { - return this.getText().length; - } - - /** - * Get the line and character based on the offset - * @param offset The index of the position - */ - positionAt(offset: number): Position { - return positionAt(offset, this.getText(), this.getLineOffsets()); - } - - /** - * Get the index of the line and character position - * @param position Line and character position - */ - offsetAt(position: Position): number { - return offsetAt(position, this.getText(), this.getLineOffsets()); - } - - private getLineOffsets() { - if (!this.lineOffsets) { - this.lineOffsets = getLineOffsets(this.getText()); - } - return this.lineOffsets; - } - - /** - * Implements TextDocument - */ - get uri(): string { - return this.getURL(); - } - - get lineCount(): number { - return this.getText().split(/\r?\n/).length; - } - - abstract languageId: string; -} - -/** - * Represents a textual document that can be manipulated. - */ -export abstract class WritableDocument extends ReadableDocument { - /** - * Set the text content of the document. - * Implementers should set `lineOffsets` to `undefined` here. - * @param text The new text content - */ - abstract setText(text: string): void; - - /** - * Update the text between two positions. - * @param text The new text slice - * @param start Start offset of the new text - * @param end End offset of the new text - */ - update(text: string, start: number, end: number): void { - this.lineOffsets = undefined; - const content = this.getText(); - this.setText(content.slice(0, start) + text + content.slice(end)); - } -} diff --git a/packages/language-server/src/lib/documents/DocumentManager.ts b/packages/language-server/src/lib/documents/DocumentManager.ts deleted file mode 100644 index bbf86303b..000000000 --- a/packages/language-server/src/lib/documents/DocumentManager.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { EventEmitter } from 'events'; -import { - TextDocumentContentChangeEvent, - TextDocumentItem, - VersionedTextDocumentIdentifier -} from 'vscode-languageserver'; -import { Document } from './Document'; -import { normalizeUri } from '../../utils'; -import ts from 'typescript'; -import { FileMap, FileSet } from './fileCollection'; - -export type DocumentEvent = 'documentOpen' | 'documentChange' | 'documentClose'; - -/** - * Manages svelte documents - */ -export class DocumentManager { - private emitter = new EventEmitter(); - private documents: FileMap; - private locked: FileSet; - private deleteCandidates: FileSet; - - constructor( - private createDocument: (textDocument: Pick) => Document, - options: { useCaseSensitiveFileNames: boolean } = { - useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames - } - ) { - this.documents = new FileMap(options.useCaseSensitiveFileNames); - this.locked = new FileSet(options.useCaseSensitiveFileNames); - this.deleteCandidates = new FileSet(options.useCaseSensitiveFileNames); - } - - openClientDocument(textDocument: Pick): Document { - return this.openDocument(textDocument, /**openedByClient */ true); - } - - openDocument( - textDocument: Pick, - openedByClient: boolean - ): Document { - textDocument = { - ...textDocument, - uri: normalizeUri(textDocument.uri) - }; - - let document: Document; - if (this.documents.has(textDocument.uri)) { - document = this.documents.get(textDocument.uri)!; - document.setText(textDocument.text); - } else { - document = this.createDocument(textDocument); - this.documents.set(textDocument.uri, document); - this.notify('documentOpen', document); - } - - this.notify('documentChange', document); - document.openedByClient = openedByClient; - - return document; - } - - lockDocument(uri: string): void { - this.locked.add(normalizeUri(uri)); - } - - markAsOpenedInClient(uri: string): void { - const document = this.documents.get(normalizeUri(uri)); - if (document) { - document.openedByClient = true; - } - } - - getAllOpenedByClient() { - return Array.from(this.documents.entries()).filter((doc) => doc[1].openedByClient); - } - - isOpenedInClient(uri: string) { - const document = this.documents.get(normalizeUri(uri)); - return !!document?.openedByClient; - } - - releaseDocument(uri: string): void { - uri = normalizeUri(uri); - - this.locked.delete(uri); - const document = this.documents.get(uri); - if (document) { - document.openedByClient = false; - } - if (this.deleteCandidates.has(uri)) { - this.deleteCandidates.delete(uri); - this.closeDocument(uri); - } - } - - closeDocument(uri: string) { - uri = normalizeUri(uri); - - const document = this.documents.get(uri); - if (!document) { - throw new Error('Cannot call methods on an unopened document'); - } - - this.notify('documentClose', document); - - // Some plugin may prevent a document from actually being closed. - if (!this.locked.has(uri)) { - this.documents.delete(uri); - } else { - this.deleteCandidates.add(uri); - } - - document.openedByClient = false; - } - - updateDocument( - textDocument: VersionedTextDocumentIdentifier, - changes: TextDocumentContentChangeEvent[] - ) { - const document = this.documents.get(normalizeUri(textDocument.uri)); - if (!document) { - throw new Error('Cannot call methods on an unopened document'); - } - - for (const change of changes) { - let start = 0; - let end = 0; - if ('range' in change) { - start = document.offsetAt(change.range.start); - end = document.offsetAt(change.range.end); - } else { - end = document.getTextLength(); - } - - document.update(change.text, start, end); - } - - this.notify('documentChange', document); - } - - on(name: DocumentEvent, listener: (document: Document) => void) { - this.emitter.on(name, listener); - } - - get(uri: string) { - return this.documents.get(normalizeUri(uri)); - } - - private notify(name: DocumentEvent, document: Document) { - this.emitter.emit(name, document); - } -} diff --git a/packages/language-server/src/lib/documents/DocumentMapper.ts b/packages/language-server/src/lib/documents/DocumentMapper.ts deleted file mode 100644 index fe79fca5a..000000000 --- a/packages/language-server/src/lib/documents/DocumentMapper.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { - Position, - Range, - CompletionItem, - Hover, - Diagnostic, - ColorPresentation, - SymbolInformation, - LocationLink, - TextDocumentEdit, - CodeAction, - SelectionRange, - TextEdit, - InsertReplaceEdit, - Location -} from 'vscode-languageserver'; -import { TagInformation, offsetAt, positionAt, getLineOffsets } from './utils'; -import { Logger } from '../../logger'; -import { generatedPositionFor, originalPositionFor, TraceMap } from '@jridgewell/trace-mapping'; - -export interface FilePosition extends Position { - uri?: string; -} - -export interface DocumentMapper { - /** - * Map the generated position to the original position - * @param generatedPosition Position in fragment - */ - getOriginalPosition(generatedPosition: Position): Position; - - /** - * Map the generated position to the original position. - * Differs from getOriginalPosition this might maps to different file - * @param generatedPosition Position in fragment - */ - getOriginalFilePosition?(generatedPosition: Position): FilePosition; - - /** - * Map the original position to the generated position - * @param originalPosition Position in parent - */ - getGeneratedPosition(originalPosition: Position): Position; - - /** - * Returns true if the given original position is inside of the generated map - * @param pos Position in original - */ - isInGenerated(pos: Position): boolean; - - /** - * Get document URL - */ - getURL(): string; -} - -/** - * Does not map, returns positions as is. - */ -export class IdentityMapper implements DocumentMapper { - constructor( - private url: string, - private parent?: DocumentMapper - ) {} - - getOriginalPosition(generatedPosition: Position): Position { - if (this.parent) { - generatedPosition = this.getOriginalPosition(generatedPosition); - } - - return generatedPosition; - } - - getGeneratedPosition(originalPosition: Position): Position { - if (this.parent) { - originalPosition = this.getGeneratedPosition(originalPosition); - } - - return originalPosition; - } - - isInGenerated(position: Position): boolean { - if (this.parent && !this.parent.isInGenerated(position)) { - return false; - } - - return true; - } - - getURL(): string { - return this.url; - } -} - -/** - * Maps positions in a fragment relative to a parent. - */ -export class FragmentMapper implements DocumentMapper { - private lineOffsetsOriginal = getLineOffsets(this.originalText); - private lineOffsetsGenerated = getLineOffsets(this.tagInfo.content); - - constructor( - private originalText: string, - private tagInfo: TagInformation, - private url: string - ) {} - - getOriginalPosition(generatedPosition: Position): Position { - const parentOffset = this.offsetInParent( - offsetAt(generatedPosition, this.tagInfo.content, this.lineOffsetsGenerated) - ); - return positionAt(parentOffset, this.originalText, this.lineOffsetsOriginal); - } - - private offsetInParent(offset: number): number { - return this.tagInfo.start + offset; - } - - getGeneratedPosition(originalPosition: Position): Position { - const fragmentOffset = - offsetAt(originalPosition, this.originalText, this.lineOffsetsOriginal) - - this.tagInfo.start; - return positionAt(fragmentOffset, this.tagInfo.content, this.lineOffsetsGenerated); - } - - isInGenerated(pos: Position): boolean { - const offset = offsetAt(pos, this.originalText, this.lineOffsetsOriginal); - return offset >= this.tagInfo.start && offset <= this.tagInfo.end; - } - - getURL(): string { - return this.url; - } -} - -export class SourceMapDocumentMapper implements DocumentMapper { - constructor( - protected traceMap: TraceMap, - protected sourceUri: string, - private parent?: DocumentMapper - ) {} - - getOriginalPosition(generatedPosition: Position): Position { - if (this.parent) { - generatedPosition = this.parent.getOriginalPosition(generatedPosition); - } - - if (generatedPosition.line < 0) { - return { line: -1, character: -1 }; - } - - const mapped = originalPositionFor(this.traceMap, { - line: generatedPosition.line + 1, - column: generatedPosition.character - }); - - if (!mapped) { - return { line: -1, character: -1 }; - } - - if (mapped.line === 0) { - Logger.debug('Got 0 mapped line from', generatedPosition, 'col was', mapped.column); - } - - return { - line: (mapped.line || 0) - 1, - character: mapped.column || 0 - }; - } - - getGeneratedPosition(originalPosition: Position): Position { - if (this.parent) { - originalPosition = this.parent.getGeneratedPosition(originalPosition); - } - - const mapped = generatedPositionFor(this.traceMap, { - line: originalPosition.line + 1, - column: originalPosition.character, - source: this.sourceUri - }); - - if (!mapped) { - return { line: -1, character: -1 }; - } - - const result = { - line: (mapped.line || 0) - 1, - character: mapped.column || 0 - }; - - if (result.line < 0) { - return result; - } - - return result; - } - - isInGenerated(position: Position): boolean { - if (this.parent && !this.isInGenerated(position)) { - return false; - } - - const generated = this.getGeneratedPosition(position); - return generated.line >= 0; - } - - getURL(): string { - return this.sourceUri; - } -} - -export function mapRangeToOriginal( - fragment: Pick, - range: Range -): Range { - // DON'T use Range.create here! Positions might not be mapped - // and therefore return negative numbers, which makes Range.create throw. - // These invalid position need to be handled - // on a case-by-case basis in the calling functions. - const originalRange = { - start: fragment.getOriginalPosition(range.start), - end: fragment.getOriginalPosition(range.end) - }; - - checkRangeLength(originalRange, range); - - return originalRange; -} - -/** Range may be mapped one character short - reverse that for "in the same line" cases*/ -function checkRangeLength(originalRange: { start: Position; end: Position }, range: Range) { - if ( - originalRange.start.line === originalRange.end.line && - range.start.line === range.end.line && - originalRange.end.character - originalRange.start.character === - range.end.character - range.start.character - 1 - ) { - originalRange.end.character += 1; - } -} - -export function mapLocationToOriginal( - fragment: Pick, - range: Range -): Location { - const map = ( - fragment.getOriginalFilePosition ?? - (fragment.getOriginalPosition as (position: Position) => FilePosition) - ).bind(fragment); - - const start = map(range.start); - const end = map(range.end); - - const originalRange: Range = { - start: { line: start.line, character: start.character }, - end: { line: end.line, character: end.character } - }; - - checkRangeLength(originalRange, range); - - return { - range: originalRange, - uri: start.uri ? start.uri : fragment.getURL() - }; -} - -export function mapRangeToGenerated(fragment: DocumentMapper, range: Range): Range { - return Range.create( - fragment.getGeneratedPosition(range.start), - fragment.getGeneratedPosition(range.end) - ); -} - -export function mapCompletionItemToOriginal( - fragment: Pick, - item: CompletionItem -): CompletionItem { - if (!item.textEdit) { - return item; - } - - return { - ...item, - textEdit: mapEditToOriginal(fragment, item.textEdit) - }; -} - -export function mapHoverToParent( - fragment: Pick, - hover: Hover -): Hover { - if (!hover.range) { - return hover; - } - - return { ...hover, range: mapRangeToOriginal(fragment, hover.range) }; -} - -export function mapObjWithRangeToOriginal( - fragment: Pick, - objWithRange: T -): T { - return { ...objWithRange, range: mapRangeToOriginal(fragment, objWithRange.range) }; -} - -export function mapInsertReplaceEditToOriginal( - fragment: Pick, - edit: InsertReplaceEdit -): InsertReplaceEdit { - return { - ...edit, - insert: mapRangeToOriginal(fragment, edit.insert), - replace: mapRangeToOriginal(fragment, edit.replace) - }; -} - -export function mapEditToOriginal( - fragment: Pick, - edit: TextEdit | InsertReplaceEdit -): TextEdit | InsertReplaceEdit { - return TextEdit.is(edit) - ? mapObjWithRangeToOriginal(fragment, edit) - : mapInsertReplaceEditToOriginal(fragment, edit); -} - -export function mapDiagnosticToGenerated( - fragment: DocumentMapper, - diagnostic: Diagnostic -): Diagnostic { - return { ...diagnostic, range: mapRangeToGenerated(fragment, diagnostic.range) }; -} - -export function mapColorPresentationToOriginal( - fragment: Pick, - presentation: ColorPresentation -): ColorPresentation { - const item = { - ...presentation - }; - - if (item.textEdit) { - item.textEdit = mapObjWithRangeToOriginal(fragment, item.textEdit); - } - - if (item.additionalTextEdits) { - item.additionalTextEdits = item.additionalTextEdits.map((edit) => - mapObjWithRangeToOriginal(fragment, edit) - ); - } - - return item; -} - -export function mapSymbolInformationToOriginal( - fragment: Pick, - info: SymbolInformation -): SymbolInformation { - return { ...info, location: mapObjWithRangeToOriginal(fragment, info.location) }; -} - -export function mapLocationLinkToOriginal( - fragment: DocumentMapper, - def: LocationLink -): LocationLink { - return LocationLink.create( - def.targetUri, - fragment.getURL() === def.targetUri - ? mapRangeToOriginal(fragment, def.targetRange) - : def.targetRange, - fragment.getURL() === def.targetUri - ? mapRangeToOriginal(fragment, def.targetSelectionRange) - : def.targetSelectionRange, - def.originSelectionRange - ? mapRangeToOriginal(fragment, def.originSelectionRange) - : undefined - ); -} - -export function mapTextDocumentEditToOriginal(fragment: DocumentMapper, edit: TextDocumentEdit) { - if (edit.textDocument.uri !== fragment.getURL()) { - return edit; - } - - return TextDocumentEdit.create( - edit.textDocument, - edit.edits.map((textEdit) => mapObjWithRangeToOriginal(fragment, textEdit)) - ); -} - -export function mapCodeActionToOriginal(fragment: DocumentMapper, codeAction: CodeAction) { - return CodeAction.create( - codeAction.title, - { - documentChanges: codeAction.edit!.documentChanges!.map((edit) => - mapTextDocumentEditToOriginal(fragment, edit as TextDocumentEdit) - ) - }, - codeAction.kind - ); -} - -export function mapSelectionRangeToParent( - fragment: Pick, - selectionRange: SelectionRange -): SelectionRange { - const { range, parent } = selectionRange; - - return SelectionRange.create( - mapRangeToOriginal(fragment, range), - parent && mapSelectionRangeToParent(fragment, parent) - ); -} diff --git a/packages/language-server/src/lib/documents/configLoader.ts b/packages/language-server/src/lib/documents/configLoader.ts deleted file mode 100644 index 2cea65b7e..000000000 --- a/packages/language-server/src/lib/documents/configLoader.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { Logger } from '../../logger'; -// @ts-ignore -import { CompileOptions } from 'svelte/types/compiler/interfaces'; -// @ts-ignore -import { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; -import { importSveltePreprocess } from '../../importPackage'; -import _glob from 'fast-glob'; -import _path from 'path'; -import _fs from 'fs'; -import { pathToFileURL, URL } from 'url'; -import { FileMap } from './fileCollection'; - -export type InternalPreprocessorGroup = PreprocessorGroup & { - /** - * svelte-preprocess has this since 4.x - */ - defaultLanguages?: { - markup?: string; - script?: string; - style?: string; - }; -}; - -export interface SvelteConfig { - compilerOptions?: CompileOptions; - preprocess?: InternalPreprocessorGroup | InternalPreprocessorGroup[]; - loadConfigError?: any; - isFallbackConfig?: boolean; - kit?: any; -} - -const DEFAULT_OPTIONS: CompileOptions = { - dev: true -}; - -const NO_GENERATE: CompileOptions = { - generate: false -}; - -/** - * This function encapsulates the import call in a way - * that TypeScript does not transpile `import()`. - * https://github.com/microsoft/TypeScript/issues/43329 - */ -const _dynamicImport = new Function('modulePath', 'return import(modulePath)') as ( - modulePath: URL -) => Promise; - -/** - * Loads svelte.config.{js,cjs,mjs} files. Provides both a synchronous and asynchronous - * interface to get a config file because snapshots need access to it synchronously. - * This means that another instance (the ts service host on startup) should make - * sure that all config files are loaded before snapshots are retrieved. - * Asynchronousity is needed because we use the dynamic `import()` statement. - */ -export class ConfigLoader { - private configFiles = new FileMap(); - private configFilesAsync = new FileMap>(); - private filePathToConfigPath = new FileMap(); - private disabled = false; - - constructor( - private globSync: typeof _glob.sync, - private fs: Pick, - private path: Pick, - private dynamicImport: typeof _dynamicImport - ) {} - - /** - * Enable/disable loading of configs (for security reasons for example) - */ - setDisabled(disabled: boolean): void { - this.disabled = disabled; - } - - /** - * Tries to load all `svelte.config.js` files below given directory - * and the first one found inside/above that directory. - * - * @param directory Directory where to load the configs from - */ - async loadConfigs(directory: string): Promise { - Logger.log('Trying to load configs for', directory); - - try { - const pathResults = this.globSync('**/svelte.config.{js,cjs,mjs}', { - cwd: directory, - // the second pattern is necessary because else fast-glob treats .tmp/../node_modules/.. as a valid match for some reason - ignore: ['**/node_modules/**', '**/.*/**'], - onlyFiles: true - }); - const someConfigIsImmediateFileInDirectory = - pathResults.length > 0 && pathResults.some((res) => !this.path.dirname(res)); - if (!someConfigIsImmediateFileInDirectory) { - const configPathUpwards = this.searchConfigPathUpwards(directory); - if (configPathUpwards) { - pathResults.push(this.path.relative(directory, configPathUpwards)); - } - } - if (pathResults.length === 0) { - this.addFallbackConfig(directory); - return; - } - - const promises = pathResults - .map((pathResult) => this.path.join(directory, pathResult)) - .filter((pathResult) => { - const config = this.configFiles.get(pathResult); - return !config || config.loadConfigError; - }) - .map(async (pathResult) => { - await this.loadAndCacheConfig(pathResult, directory); - }); - await Promise.all(promises); - } catch (e) { - Logger.error(e); - } - } - - private addFallbackConfig(directory: string) { - const fallback = this.useFallbackPreprocessor(directory, false); - const path = this.path.join(directory, 'svelte.config.js'); - this.configFilesAsync.set(path, Promise.resolve(fallback)); - this.configFiles.set(path, fallback); - } - - private searchConfigPathUpwards(path: string) { - let currentDir = path; - let nextDir = this.path.dirname(path); - while (currentDir !== nextDir) { - const tryFindConfigPath = (ending: string) => { - const path = this.path.join(currentDir, `svelte.config.${ending}`); - return this.fs.existsSync(path) ? path : undefined; - }; - const configPath = - tryFindConfigPath('js') || tryFindConfigPath('cjs') || tryFindConfigPath('mjs'); - if (configPath) { - return configPath; - } - - currentDir = nextDir; - nextDir = this.path.dirname(currentDir); - } - } - - private async loadAndCacheConfig(configPath: string, directory: string) { - const loadingConfig = this.configFilesAsync.get(configPath); - if (loadingConfig) { - await loadingConfig; - } else { - const newConfig = this.loadConfig(configPath, directory); - this.configFilesAsync.set(configPath, newConfig); - this.configFiles.set(configPath, await newConfig); - } - } - - private async loadConfig(configPath: string, directory: string) { - try { - let config = this.disabled - ? {} - : (await this.dynamicImport(pathToFileURL(configPath)))?.default; - - if (!config) { - throw new Error( - 'Missing exports in the config. Make sure to include "export default config" or "module.exports = config"' - ); - } - config = { - ...config, - compilerOptions: { - ...DEFAULT_OPTIONS, - ...config.compilerOptions, - ...NO_GENERATE - } - }; - Logger.log('Loaded config at ', configPath); - return config; - } catch (err) { - Logger.error('Error while loading config at ', configPath); - Logger.error(err); - const config = { - ...this.useFallbackPreprocessor(directory, true), - compilerOptions: { - ...DEFAULT_OPTIONS, - ...NO_GENERATE - }, - loadConfigError: err - }; - return config; - } - } - - /** - * Returns config associated to file. If no config is found, the file - * was called in a context where no config file search was done before, - * which can happen - * - if TS intellisense is turned off and the search did not run on tsconfig init - * - if the file was opened not through the TS service crawl, but through the LSP - * - * @param file - */ - getConfig(file: string): SvelteConfig | undefined { - const cached = this.filePathToConfigPath.get(file); - if (cached) { - return this.configFiles.get(cached); - } - - let currentDir = file; - let nextDir = this.path.dirname(file); - while (currentDir !== nextDir) { - currentDir = nextDir; - const config = - this.tryGetConfig(file, currentDir, 'js') || - this.tryGetConfig(file, currentDir, 'cjs') || - this.tryGetConfig(file, currentDir, 'mjs'); - if (config) { - return config; - } - nextDir = this.path.dirname(currentDir); - } - } - - /** - * Like `getConfig`, but will search for a config above if no config found. - */ - async awaitConfig(file: string): Promise { - const config = this.getConfig(file); - if (config) { - return config; - } - - const fileDirectory = this.path.dirname(file); - const configPath = this.searchConfigPathUpwards(fileDirectory); - if (configPath) { - await this.loadAndCacheConfig(configPath, fileDirectory); - } else { - this.addFallbackConfig(fileDirectory); - } - return this.getConfig(file); - } - - private tryGetConfig(file: string, fromDirectory: string, configFileEnding: string) { - const path = this.path.join(fromDirectory, `svelte.config.${configFileEnding}`); - const config = this.configFiles.get(path); - if (config) { - this.filePathToConfigPath.set(file, path); - return config; - } - } - - private useFallbackPreprocessor(path: string, foundConfig: boolean): SvelteConfig { - Logger.log( - (foundConfig - ? 'Found svelte.config.js but there was an error loading it. ' - : 'No svelte.config.js found. ') + - 'Using https://github.com/sveltejs/svelte-preprocess as fallback' - ); - const sveltePreprocess = importSveltePreprocess(path); - return { - preprocess: sveltePreprocess({ - // 4.x does not have transpileOnly anymore, but if the user has version 3.x - // in his repo, that one is loaded instead, for which we still need this. - typescript: { - transpileOnly: true, - compilerOptions: { sourceMap: true, inlineSourceMap: false } - } - }), - isFallbackConfig: true - }; - } -} - -export const configLoader = new ConfigLoader(_glob.sync, _fs, _path, _dynamicImport); diff --git a/packages/language-server/src/lib/documents/fileCollection.ts b/packages/language-server/src/lib/documents/fileCollection.ts deleted file mode 100644 index 5ea5dd5b4..000000000 --- a/packages/language-server/src/lib/documents/fileCollection.ts +++ /dev/null @@ -1,101 +0,0 @@ -import ts from 'typescript'; -import { createGetCanonicalFileName, GetCanonicalFileName } from '../../utils'; - -/** - * wrapper around Map for case insensitive file systems - */ -export class FileMap implements Iterable<[string, T]> { - private getCanonicalFileName: GetCanonicalFileName; - private readonly map = new Map(); - - constructor(useCaseSensitiveFileNames = ts.sys.useCaseSensitiveFileNames) { - this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - } - - get(filePath: string) { - return this.map.get(this.getCanonicalFileName(filePath)); - } - - set(filePath: string, value: T) { - const canonicalFileName = this.getCanonicalFileName(filePath); - - return this.map.set(canonicalFileName, value); - } - - has(filePath: string) { - return this.map.has(this.getCanonicalFileName(filePath)); - } - - delete(filePath: string) { - return this.map.delete(this.getCanonicalFileName(filePath)); - } - - /** - * Returns an iterable of key, value pairs for every entry in the map. - * In case insensitive file system the key in the key-value pairs is in lowercase - */ - entries(): IterableIterator<[string, T]> { - return this.map.entries(); - } - - /** - * - * @param callbackfn In case insensitive file system the key parameter for the callback is in lowercase - */ - forEach(callbackfn: (value: T, key: string) => void) { - return this.map.forEach(callbackfn); - } - - values() { - return this.map.values(); - } - - clear() { - this.map.clear(); - } - - /** - * Returns an iterable of values in the map. - * In case insensitive file system the key is in lowercase - */ - keys() { - return this.map.keys(); - } - - get size() { - return this.map.size; - } - - [Symbol.iterator](): Iterator<[string, T]> { - return this.map[Symbol.iterator](); - } -} - -export class FileSet implements Iterable { - private getCanonicalFileName: GetCanonicalFileName; - private readonly set = new Set(); - - constructor(useCaseSensitiveFileNames = ts.sys.useCaseSensitiveFileNames) { - this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - } - - add(filePath: string) { - this.set.add(this.getCanonicalFileName(filePath)); - } - - has(filePath: string) { - return this.set.has(this.getCanonicalFileName(filePath)); - } - - delete(filePath: string) { - return this.set.delete(this.getCanonicalFileName(filePath)); - } - - clear() { - this.set.clear(); - } - - [Symbol.iterator](): Iterator { - return this.set[Symbol.iterator](); - } -} diff --git a/packages/language-server/src/lib/documents/index.ts b/packages/language-server/src/lib/documents/index.ts deleted file mode 100644 index 6774ac3c9..000000000 --- a/packages/language-server/src/lib/documents/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './DocumentManager'; -export * from './Document'; -export * from './DocumentBase'; -export * from './DocumentMapper'; -export * from './utils'; diff --git a/packages/language-server/src/lib/documents/parseHtml.ts b/packages/language-server/src/lib/documents/parseHtml.ts deleted file mode 100644 index 0febdf836..000000000 --- a/packages/language-server/src/lib/documents/parseHtml.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { - getLanguageService, - HTMLDocument, - TokenType, - ScannerState, - Scanner, - Node, - Position -} from 'vscode-html-languageservice'; -import { Document } from './Document'; -import { isInsideMoustacheTag } from './utils'; - -const parser = getLanguageService(); - -/** - * Parses text as HTML - */ -export function parseHtml(text: string): HTMLDocument { - const preprocessed = preprocess(text); - - // We can safely only set getText because only this is used for parsing - const parsedDoc = parser.parseHTMLDocument({ getText: () => preprocessed }); - - return parsedDoc; -} - -const createScanner = parser.createScanner as ( - input: string, - initialOffset?: number, - initialState?: ScannerState -) => Scanner; - -/** - * scan the text and remove any `>` or `<` that cause the tag to end short, - */ -function preprocess(text: string) { - let scanner = createScanner(text); - let token = scanner.scan(); - let currentStartTagStart: number | null = null; - let moustacheCheckStart = 0; - let moustacheCheckEnd = 0; - let lastToken = token; - - while (token !== TokenType.EOS) { - const offset = scanner.getTokenOffset(); - let blanked = false; - - switch (token) { - case TokenType.StartTagOpen: - if (shouldBlankStartOrEndTagLike(offset)) { - blankStartOrEndTagLike(offset); - blanked = true; - } else { - currentStartTagStart = offset; - } - break; - - case TokenType.StartTagClose: - if (shouldBlankStartOrEndTagLike(offset)) { - blankStartOrEndTagLike(offset); - blanked = true; - } else { - currentStartTagStart = null; - } - break; - - case TokenType.StartTagSelfClose: - currentStartTagStart = null; - break; - - // - // https://github.com/microsoft/vscode-html-languageservice/blob/71806ef57be07e1068ee40900ef8b0899c80e68a/src/parser/htmlScanner.ts#L327 - case TokenType.Unknown: - if ( - scanner.getScannerState() === ScannerState.WithinTag && - scanner.getTokenText() === '<' && - shouldBlankStartOrEndTagLike(offset) - ) { - blankStartOrEndTagLike(offset); - blanked = true; - } - break; - - case TokenType.Content: { - moustacheCheckEnd = scanner.getTokenEnd(); - if (token !== lastToken) { - moustacheCheckStart = offset; - } - break; - } - } - - // blanked, so the token type is invalid - if (!blanked) { - lastToken = token; - } - token = scanner.scan(); - } - - return text; - - function shouldBlankStartOrEndTagLike(offset: number) { - if (currentStartTagStart != null) { - return isInsideMoustacheTag(text, currentStartTagStart, offset); - } - - const index = text - .substring(moustacheCheckStart, moustacheCheckEnd) - .lastIndexOf('{', offset); - - const lastMustacheTagStart = index === -1 ? null : moustacheCheckStart + index; - if (lastMustacheTagStart == null) { - return false; - } - - return isInsideMoustacheTag( - text.substring(lastMustacheTagStart), - null, - offset - lastMustacheTagStart - ); - } - - function blankStartOrEndTagLike(offset: number) { - text = text.substring(0, offset) + ' ' + text.substring(offset + 1); - scanner = createScanner( - text, - offset, - currentStartTagStart != null ? ScannerState.WithinTag : ScannerState.WithinContent - ); - } -} - -export interface AttributeContext { - name: string; - inValue: boolean; - elementTag: Node; - valueRange?: [number, number]; -} - -export function getAttributeContextAtPosition( - document: Document, - position: Position -): AttributeContext | null { - const offset = document.offsetAt(position); - const { html } = document; - const tag = html.findNodeAt(offset); - - if (!inStartTag(offset, tag) || !tag.attributes) { - return null; - } - - const text = document.getText(); - const beforeStartTagEnd = - text.substring(0, tag.start) + preprocess(text.substring(tag.start, tag.startTagEnd)); - - const scanner = createScanner(beforeStartTagEnd, tag.start); - - let token = scanner.scan(); - let currentAttributeName: string | undefined; - const inTokenRange = () => - scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd(); - while (token != TokenType.EOS) { - // adopted from https://github.com/microsoft/vscode-html-languageservice/blob/2f7ae4df298ac2c299a40e9024d118f4a9dc0c68/src/services/htmlCompletion.ts#L402 - if (token === TokenType.AttributeName) { - currentAttributeName = scanner.getTokenText(); - - if (inTokenRange()) { - return { - elementTag: tag, - name: currentAttributeName, - inValue: false - }; - } - } else if (token === TokenType.DelimiterAssign) { - if (scanner.getTokenEnd() === offset && currentAttributeName) { - const nextToken = scanner.scan(); - - return { - elementTag: tag, - name: currentAttributeName, - inValue: true, - valueRange: [ - offset, - nextToken === TokenType.AttributeValue ? scanner.getTokenEnd() : offset - ] - }; - } - } else if (token === TokenType.AttributeValue) { - if (inTokenRange() && currentAttributeName) { - let start = scanner.getTokenOffset(); - let end = scanner.getTokenEnd(); - const char = text[start]; - - if (char === '"' || char === "'") { - start++; - end--; - } - - return { - elementTag: tag, - name: currentAttributeName, - inValue: true, - valueRange: [start, end] - }; - } - currentAttributeName = undefined; - } - token = scanner.scan(); - } - - return null; -} - -function inStartTag(offset: number, node: Node) { - return offset > node.start && node.startTagEnd != undefined && offset < node.startTagEnd; -} diff --git a/packages/language-server/src/lib/documents/utils.ts b/packages/language-server/src/lib/documents/utils.ts deleted file mode 100644 index cb3c0830c..000000000 --- a/packages/language-server/src/lib/documents/utils.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { clamp, isInRange, regexLastIndexOf } from '../../utils'; -import { Position, Range } from 'vscode-languageserver'; -import { Node, HTMLDocument } from 'vscode-html-languageservice'; -import * as path from 'path'; -import { parseHtml } from './parseHtml'; - -export interface TagInformation { - content: string; - attributes: Record; - start: number; - end: number; - startPos: Position; - endPos: Position; - container: { start: number; end: number }; -} - -function parseAttributes( - rawAttrs: Record | undefined -): Record { - const attrs: Record = {}; - if (!rawAttrs) { - return attrs; - } - - Object.keys(rawAttrs).forEach((attrName) => { - const attrValue = rawAttrs[attrName]; - attrs[attrName] = attrValue === null ? attrName : removeOuterQuotes(attrValue); - }); - return attrs; - - function removeOuterQuotes(attrValue: string) { - if ( - (attrValue.startsWith('"') && attrValue.endsWith('"')) || - (attrValue.startsWith("'") && attrValue.endsWith("'")) - ) { - return attrValue.slice(1, attrValue.length - 1); - } - return attrValue; - } -} - -const regexIf = new RegExp('{\\s*#if\\s.*?}', 'gms'); -const regexIfEnd = new RegExp('{\\s*/if}', 'gms'); -const regexEach = new RegExp('{\\s*#each\\s.*?}', 'gms'); -const regexEachEnd = new RegExp('{\\s*/each}', 'gms'); -const regexAwait = new RegExp('{\\s*#await\\s.*?}', 'gms'); -const regexAwaitEnd = new RegExp('{\\s*/await}', 'gms'); -const regexHtml = new RegExp('{\\s*@html\\s.*?', 'gms'); - -/** - * Extracts a tag (style or script) from the given text - * and returns its start, end and the attributes on that tag. - * @param text text content to extract tag from - * @param tag the tag to extract - */ -function extractTags( - text: string, - tag: 'script' | 'style' | 'template', - html?: HTMLDocument -): TagInformation[] { - const rootNodes = html?.roots || parseHtml(text).roots; - const matchedNodes = rootNodes - .filter((node) => node.tag === tag) - .filter((tag) => { - return isNotInsideControlFlowTag(tag) && isNotInsideHtmlTag(tag); - }); - return matchedNodes.map(transformToTagInfo); - - /** - * For every match AFTER the tag do a search for `{/X`. - * If that is BEFORE `{#X`, we are inside a moustache tag. - */ - function isNotInsideControlFlowTag(tag: Node) { - const nodes = rootNodes.slice(rootNodes.indexOf(tag)); - const rootContentAfterTag = nodes - .map((node, idx) => { - const start = node.startTagEnd ? node.end : node.start + (node.tag?.length || 0); - return text.substring(start, nodes[idx + 1]?.start); - }) - .join(''); - - return ![ - [regexIf, regexIfEnd], - [regexEach, regexEachEnd], - [regexAwait, regexAwaitEnd] - ].some((pair) => { - pair[0].lastIndex = 0; - pair[1].lastIndex = 0; - const start = pair[0].exec(rootContentAfterTag); - const end = pair[1].exec(rootContentAfterTag); - return (end?.index ?? text.length) < (start?.index ?? text.length); - }); - } - - /** - * For every match BEFORE the tag do a search for `{@html`. - * If that is BEFORE `}`, we are inside a moustache tag. - */ - function isNotInsideHtmlTag(tag: Node) { - const nodes = rootNodes.slice(0, rootNodes.indexOf(tag)); - const rootContentBeforeTag = [{ start: 0, end: 0 }, ...nodes] - .map((node, idx) => { - return text.substring(node.end, nodes[idx]?.start); - }) - .join(''); - - return !( - regexLastIndexOf(rootContentBeforeTag, regexHtml) > - rootContentBeforeTag.lastIndexOf('}') - ); - } - - function transformToTagInfo(matchedNode: Node) { - const start = matchedNode.startTagEnd ?? matchedNode.start; - const end = matchedNode.endTagStart ?? matchedNode.end; - const startPos = positionAt(start, text); - const endPos = positionAt(end, text); - const container = { - start: matchedNode.start, - end: matchedNode.end - }; - const content = text.substring(start, end); - - return { - content, - attributes: parseAttributes(matchedNode.attributes), - start, - end, - startPos, - endPos, - container - }; - } -} - -export function extractScriptTags( - source: string, - html?: HTMLDocument -): { script?: TagInformation; moduleScript?: TagInformation } | null { - const scripts = extractTags(source, 'script', html); - if (!scripts.length) { - return null; - } - - const script = scripts.find((s) => s.attributes['context'] !== 'module'); - const moduleScript = scripts.find((s) => s.attributes['context'] === 'module'); - return { script, moduleScript }; -} - -export function extractStyleTag(source: string, html?: HTMLDocument): TagInformation | null { - const styles = extractTags(source, 'style', html); - if (!styles.length) { - return null; - } - - // There can only be one style tag - return styles[0]; -} - -export function extractTemplateTag(source: string, html?: HTMLDocument): TagInformation | null { - const templates = extractTags(source, 'template', html); - if (!templates.length) { - return null; - } - - // There should only be one style tag - return templates[0]; -} - -/** - * Get the line and character based on the offset - * @param offset The index of the position - * @param text The text for which the position should be retrived - * @param lineOffsets number Array with offsets for each line. Computed if not given - */ -export function positionAt( - offset: number, - text: string, - lineOffsets = getLineOffsets(text) -): Position { - offset = clamp(offset, 0, text.length); - - let low = 0; - let high = lineOffsets.length; - if (high === 0) { - return Position.create(0, offset); - } - - while (low <= high) { - const mid = Math.floor((low + high) / 2); - const lineOffset = lineOffsets[mid]; - - if (lineOffset === offset) { - return Position.create(mid, 0); - } else if (offset > lineOffset) { - low = mid + 1; - } else { - high = mid - 1; - } - } - - // low is the least x for which the line offset is larger than the current offset - // or array.length if no line offset is larger than the current offset - const line = low - 1; - return Position.create(line, offset - lineOffsets[line]); -} - -/** - * Get the offset of the line and character position - * @param position Line and character position - * @param text The text for which the offset should be retrived - * @param lineOffsets number Array with offsets for each line. Computed if not given - */ -export function offsetAt( - position: Position, - text: string, - lineOffsets = getLineOffsets(text) -): number { - if (position.line >= lineOffsets.length) { - return text.length; - } else if (position.line < 0) { - return 0; - } - - const lineOffset = lineOffsets[position.line]; - const nextLineOffset = - position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length; - - return clamp(nextLineOffset, lineOffset, lineOffset + position.character); -} - -export function getLineOffsets(text: string) { - const lineOffsets = []; - let isLineStart = true; - - for (let i = 0; i < text.length; i++) { - if (isLineStart) { - lineOffsets.push(i); - isLineStart = false; - } - const ch = text.charAt(i); - isLineStart = ch === '\r' || ch === '\n'; - if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') { - i++; - } - } - - if (isLineStart && text.length > 0) { - lineOffsets.push(text.length); - } - - return lineOffsets; -} - -export function isInTag( - position: Position, - tagInfo: TagInformation | null -): tagInfo is TagInformation { - return !!tagInfo && isInRange(Range.create(tagInfo.startPos, tagInfo.endPos), position); -} - -export function isRangeInTag( - range: Range, - tagInfo: TagInformation | null -): tagInfo is TagInformation { - return isInTag(range.start, tagInfo) && isInTag(range.end, tagInfo); -} - -export function getTextInRange(range: Range, text: string) { - return text.substring(offsetAt(range.start, text), offsetAt(range.end, text)); -} - -export function getLineAtPosition(position: Position, text: string) { - return text.substring( - offsetAt({ line: position.line, character: 0 }, text), - offsetAt({ line: position.line, character: Number.MAX_VALUE }, text) - ); -} - -/** - * Assumption: Is called with a line. A line does only contain line break characters - * at its end. - */ -export function isAtEndOfLine(line: string, offset: number): boolean { - return [undefined, '\r', '\n'].includes(line[offset]); -} - -/** - * Updates a relative import - * - * @param oldPath Old absolute path - * @param newPath New absolute path - * @param relativeImportPath Import relative to the old path - */ -export function updateRelativeImport(oldPath: string, newPath: string, relativeImportPath: string) { - let newImportPath = path - .join(path.relative(newPath, oldPath), relativeImportPath) - .replace(/\\/g, '/'); - if (!newImportPath.startsWith('.')) { - newImportPath = './' + newImportPath; - } - return newImportPath; -} - -/** - * Returns the node if offset is inside a component's starttag - */ -export function getNodeIfIsInComponentStartTag( - html: HTMLDocument, - offset: number -): Node | undefined { - const node = html.findNodeAt(offset); - if ( - !!node.tag && - node.tag[0] === node.tag[0].toUpperCase() && - (!node.startTagEnd || offset < node.startTagEnd) - ) { - return node; - } -} - -/** - * Returns the node if offset is inside a HTML starttag - */ -export function getNodeIfIsInHTMLStartTag(html: HTMLDocument, offset: number): Node | undefined { - const node = html.findNodeAt(offset); - if ( - !!node.tag && - node.tag[0] === node.tag[0].toLowerCase() && - (!node.startTagEnd || offset < node.startTagEnd) - ) { - return node; - } -} - -/** - * Returns the node if offset is inside a starttag (HTML or component) - */ -export function getNodeIfIsInStartTag(html: HTMLDocument, offset: number): Node | undefined { - const node = html.findNodeAt(offset); - if (!!node.tag && (!node.startTagEnd || offset < node.startTagEnd)) { - return node; - } -} - -/** - * Returns `true` if `offset` is a html tag and within the name of the start tag or end tag - */ -export function isInHTMLTagRange(html: HTMLDocument, offset: number): boolean { - const node = html.findNodeAt(offset); - return ( - !!node.tag && - node.tag[0] === node.tag[0].toLowerCase() && - (node.start + node.tag.length + 1 >= offset || - (!!node.endTagStart && node.endTagStart <= offset)) - ); -} - -/** - * Gets word range at position. - * Delimiter is by default a whitespace, but can be adjusted. - */ -export function getWordRangeAt( - str: string, - pos: number, - delimiterRegex = { left: /\S+$/, right: /\s/ } -): { start: number; end: number } { - let start = str.slice(0, pos).search(delimiterRegex.left); - if (start < 0) { - start = pos; - } - - let end = str.slice(pos).search(delimiterRegex.right); - if (end < 0) { - end = str.length; - } else { - end = end + pos; - } - - return { start, end }; -} - -/** - * Gets word at position. - * Delimiter is by default a whitespace, but can be adjusted. - */ -export function getWordAt( - str: string, - pos: number, - delimiterRegex = { left: /\S+$/, right: /\s/ } -): string { - const { start, end } = getWordRangeAt(str, pos, delimiterRegex); - return str.slice(start, end); -} - -/** - * Returns start/end offset of a text into a range - */ -export function toRange(str: string, start: number, end: number): Range { - return Range.create(positionAt(start, str), positionAt(end, str)); -} - -/** - * Returns the language from the given tags, return the first from which a language is found. - * Searches inside lang and type and removes leading 'text/' - */ -export function getLangAttribute(...tags: Array): string | null { - const tag = tags.find((tag) => tag?.attributes.lang || tag?.attributes.type); - if (!tag) { - return null; - } - - const attribute = tag.attributes.lang || tag.attributes.type; - if (!attribute) { - return null; - } - - return attribute.replace(/^text\//, ''); -} - -/** - * Checks whether given position is inside a moustache tag (which includes control flow tags) - * using a simple bracket matching heuristic which might fail under conditions like - * `{#if {a: true}.a}` - */ -export function isInsideMoustacheTag(html: string, tagStart: number | null, position: number) { - if (tagStart === null) { - // Not inside - const charactersBeforePosition = html.substring(0, position); - return ( - Math.max( - // TODO make this just check for '{'? - // Theoretically, someone could do {a < b} in a simple moustache tag - charactersBeforePosition.lastIndexOf('{#'), - charactersBeforePosition.lastIndexOf('{:'), - charactersBeforePosition.lastIndexOf('{@') - ) > charactersBeforePosition.lastIndexOf('}') - ); - } else { - // Inside - const charactersInNode = html.substring(tagStart, position); - return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}'); - } -} diff --git a/packages/language-server/src/lib/foldingRange/indentFolding.ts b/packages/language-server/src/lib/foldingRange/indentFolding.ts deleted file mode 100644 index 0eda608e7..000000000 --- a/packages/language-server/src/lib/foldingRange/indentFolding.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { sum } from 'lodash'; -import { FoldingRange } from 'vscode-languageserver-types'; -import { Document, TagInformation } from '../documents'; - -/** - * - * 1. check tab and space counts for lines - * 2. if there're mixing space and tab guess the tabSize otherwise we only need to compare the numbers of spaces or tabs between lines. - */ -export function indentBasedFoldingRangeForTag( - document: Document, - tag: TagInformation -): FoldingRange[] { - if (tag.startPos.line === tag.endPos.line) { - return []; - } - - const startLine = tag.startPos.line + 1; - const endLine = tag.endPos.line - 1; - - if (startLine > endLine || startLine === endLine) { - return []; - } - - return indentBasedFoldingRange({ document, ranges: [{ startLine, endLine }] }); -} - -export interface LineRange { - startLine: number; - endLine: number; -} - -export function indentBasedFoldingRange({ - document, - ranges, - skipFold -}: { - document: Document; - ranges?: LineRange[] | undefined; - skipFold?: (startLine: number, startLineContent: string) => boolean; -}): FoldingRange[] { - const text = document.getText(); - const lines = text.split(/\r?\n/); - - const indents = lines - .map((line, index) => ({ - ...collectIndents(line), - index - })) - .filter((line) => !line.empty); - - const tabs = sum(indents.map((l) => l.tabCount)); - const spaces = sum(indents.map((l) => l.spaceCount)); - - const tabSize = tabs && spaces ? guessTabSize(indents) : 4; - - let currentIndent: number | undefined; - const result: FoldingRange[] = []; - const unfinishedFolds = new Map(); - ranges ??= [{ startLine: 0, endLine: lines.length - 1 }]; - let rangeIndex = 0; - let range = ranges[rangeIndex++]; - - if (!range) { - return []; - } - - for (const indentInfo of indents) { - if (indentInfo.index < range.startLine || indentInfo.empty) { - continue; - } - - if (indentInfo.index > range.endLine) { - for (const fold of unfinishedFolds.values()) { - fold.endLine = range.endLine; - } - - range = ranges[rangeIndex++]; - if (!range) { - break; - } - } - - const lineIndent = indentInfo.tabCount * tabSize + indentInfo.spaceCount; - - currentIndent ??= lineIndent; - - if (lineIndent > currentIndent) { - const startLine = indentInfo.index - 1; - if (!skipFold?.(startLine, lines[startLine])) { - const fold = { startLine, endLine: indentInfo.index }; - unfinishedFolds.set(currentIndent, fold); - result.push(fold); - } - - currentIndent = lineIndent; - } - - if (lineIndent < currentIndent) { - const last = unfinishedFolds.get(lineIndent); - unfinishedFolds.delete(lineIndent); - if (last) { - last.endLine = Math.max(last.endLine, indentInfo.index - 1); - } - - currentIndent = lineIndent; - } - } - - return result; -} - -function collectIndents(line: string) { - let tabCount = 0; - let spaceCount = 0; - let empty = true; - - for (let index = 0; index < line.length; index++) { - const char = line[index]; - - if (char === '\t') { - tabCount++; - } else if (char === ' ') { - spaceCount++; - } else { - empty = false; - break; - } - } - - return { tabCount, spaceCount, empty }; -} - -/** - * - * The indentation guessing is based on the indentation difference between lines. - * And if the count equals, then the one used more often takes priority. - */ -export function guessTabSize( - nonEmptyLines: Array<{ spaceCount: number; tabCount: number }> -): number { - // simplified version of - // https://github.com/microsoft/vscode/blob/559e9beea981b47ffd76d90158ccccafef663324/src/vs/editor/common/model/indentationGuesser.ts#L106 - if (nonEmptyLines.length === 1) { - return 4; - } - - const guessingTabSize = [2, 4, 6, 8, 3, 5, 7]; - const MAX_GUESS = 8; - const matchCounts = new Map(); - - for (let index = 0; index < nonEmptyLines.length; index++) { - const line = nonEmptyLines[index]; - const previousLine = nonEmptyLines[index - 1] ?? { spaceCount: 0, tabCount: 0 }; - - const spaceDiff = Math.abs(line.spaceCount - previousLine.spaceCount); - const tabDiff = Math.abs(line.tabCount - previousLine.tabCount); - const diff = - tabDiff === 0 ? spaceDiff : spaceDiff % tabDiff === 0 ? spaceDiff / tabDiff : 0; - - if (diff === 0 || diff > MAX_GUESS) { - continue; - } - - for (const guess of guessingTabSize) { - if (diff === guess) { - matchCounts.set(guess, (matchCounts.get(guess) ?? 0) + 1); - } - } - } - - let max = 0; - let tabSize: number | undefined; - for (const [size, count] of matchCounts) { - max = Math.max(max, count); - if (max === count) { - tabSize = size; - } - } - - const match4 = matchCounts.get(4); - const match2 = matchCounts.get(2); - if (tabSize === 4 && match4 && match4 > 0 && match2 && match2 > 0 && match2 >= match4 / 2) { - tabSize = 2; - } - - return tabSize ?? 4; -} diff --git a/packages/language-server/src/lib/semanticToken/semanticTokenLegend.ts b/packages/language-server/src/lib/semanticToken/semanticTokenLegend.ts deleted file mode 100644 index 736093d85..000000000 --- a/packages/language-server/src/lib/semanticToken/semanticTokenLegend.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - SemanticTokensLegend, - SemanticTokenModifiers, - SemanticTokenTypes -} from 'vscode-languageserver'; - -/** - * extended from https://github.com/microsoft/TypeScript/blob/35c8df04ad959224fad9037e340c1e50f0540a49/src/services/classifier2020.ts#L9 - * so that we don't have to map it into our own legend - */ -export const enum TokenType { - class, - enum, - interface, - namespace, - typeParameter, - type, - parameter, - variable, - enumMember, - property, - function, - member, - - // svelte - event -} - -/** - * adopted from https://github.com/microsoft/TypeScript/blob/35c8df04ad959224fad9037e340c1e50f0540a49/src/services/classifier2020.ts#L13 - * so that we don't have to map it into our own legend - */ -export const enum TokenModifier { - declaration, - static, - async, - readonly, - defaultLibrary, - local -} - -export function getSemanticTokenLegends(): SemanticTokensLegend { - const tokenModifiers: string[] = []; - - ( - [ - [TokenModifier.declaration, SemanticTokenModifiers.declaration], - [TokenModifier.static, SemanticTokenModifiers.static], - [TokenModifier.async, SemanticTokenModifiers.async], - [TokenModifier.readonly, SemanticTokenModifiers.readonly], - [TokenModifier.defaultLibrary, SemanticTokenModifiers.defaultLibrary], - [TokenModifier.local, 'local'] - ] as const - ).forEach(([tsModifier, legend]) => (tokenModifiers[tsModifier] = legend)); - - const tokenTypes: string[] = []; - - ( - [ - [TokenType.class, SemanticTokenTypes.class], - [TokenType.enum, SemanticTokenTypes.enum], - [TokenType.interface, SemanticTokenTypes.interface], - [TokenType.namespace, SemanticTokenTypes.namespace], - [TokenType.typeParameter, SemanticTokenTypes.typeParameter], - [TokenType.type, SemanticTokenTypes.type], - [TokenType.parameter, SemanticTokenTypes.parameter], - [TokenType.variable, SemanticTokenTypes.variable], - [TokenType.enumMember, SemanticTokenTypes.enumMember], - [TokenType.property, SemanticTokenTypes.property], - [TokenType.function, SemanticTokenTypes.function], - - // member is renamed to method in vscode codebase to match LSP default - [TokenType.member, SemanticTokenTypes.method], - [TokenType.event, SemanticTokenTypes.event] - ] as const - ).forEach(([tokenType, legend]) => (tokenTypes[tokenType] = legend)); - - return { - tokenModifiers, - tokenTypes - }; -} diff --git a/packages/language-server/src/logger.ts b/packages/language-server/src/logger.ts deleted file mode 100644 index fd6f50336..000000000 --- a/packages/language-server/src/logger.ts +++ /dev/null @@ -1,28 +0,0 @@ -export class Logger { - private static logErrorsOnly = false; - private static logDebug = false; - - static setLogErrorsOnly(logErrorsOnly: boolean) { - Logger.logErrorsOnly = logErrorsOnly; - } - - static setDebug(debug: boolean) { - this.logDebug = debug; - } - - static log(...args: any) { - if (!Logger.logErrorsOnly) { - console.log(...args); - } - } - - static error(...args: any) { - console.error(...args); - } - - static debug(...args: any) { - if (!Logger.logErrorsOnly && this.logDebug) { - console.log(...args); - } - } -} diff --git a/packages/language-server/src/ls-config.ts b/packages/language-server/src/ls-config.ts deleted file mode 100644 index 1511488b6..000000000 --- a/packages/language-server/src/ls-config.ts +++ /dev/null @@ -1,639 +0,0 @@ -import { get, merge } from 'lodash'; -import ts from 'typescript'; -import { VSCodeEmmetConfig } from '@vscode/emmet-helper'; -import { importPrettier } from './importPackage'; -import { Document } from './lib/documents'; -import { returnObjectIfHasKeys } from './utils'; -import path from 'path'; -import { FileMap } from './lib/documents/fileCollection'; -import { ClientCapabilities } from 'vscode-languageserver-protocol'; - -/** - * Default config for the language server. - */ -const defaultLSConfig: LSConfig = { - typescript: { - enable: true, - diagnostics: { enable: true }, - hover: { enable: true }, - completions: { enable: true }, - documentSymbols: { enable: true }, - codeActions: { enable: true }, - selectionRange: { enable: true }, - signatureHelp: { enable: true }, - semanticTokens: { enable: true } - }, - css: { - enable: true, - globals: '', - diagnostics: { enable: true }, - hover: { enable: true }, - completions: { enable: true, emmet: true }, - documentColors: { enable: true }, - colorPresentations: { enable: true }, - documentSymbols: { enable: true }, - selectionRange: { enable: true } - }, - html: { - enable: true, - hover: { enable: true }, - completions: { enable: true, emmet: true }, - tagComplete: { enable: true }, - documentSymbols: { enable: true }, - linkedEditing: { enable: true } - }, - svelte: { - enable: true, - compilerWarnings: {}, - diagnostics: { enable: true }, - rename: { enable: true }, - format: { - enable: true, - config: { - svelteSortOrder: 'options-scripts-markup-styles', - svelteStrictMode: false, - svelteAllowShorthand: true, - svelteBracketNewLine: true, - svelteIndentScriptAndStyle: true, - printWidth: 80, - singleQuote: false - } - }, - completions: { enable: true }, - hover: { enable: true }, - codeActions: { enable: true }, - selectionRange: { enable: true }, - defaultScriptLanguage: 'none' - } -}; - -/** - * Representation of the language server config. - * Should be kept in sync with infos in `packages/svelte-vscode/package.json`. - */ -export interface LSConfig { - typescript: LSTypescriptConfig; - css: LSCSSConfig; - html: LSHTMLConfig; - svelte: LSSvelteConfig; -} - -export interface LSTypescriptConfig { - enable: boolean; - diagnostics: { - enable: boolean; - }; - hover: { - enable: boolean; - }; - documentSymbols: { - enable: boolean; - }; - completions: { - enable: boolean; - }; - codeActions: { - enable: boolean; - }; - selectionRange: { - enable: boolean; - }; - signatureHelp: { - enable: boolean; - }; - semanticTokens: { - enable: boolean; - }; -} - -export interface LSCSSConfig { - enable: boolean; - globals: string; - diagnostics: { - enable: boolean; - }; - hover: { - enable: boolean; - }; - completions: { - enable: boolean; - emmet: boolean; - }; - documentColors: { - enable: boolean; - }; - colorPresentations: { - enable: boolean; - }; - documentSymbols: { - enable: boolean; - }; - selectionRange: { - enable: boolean; - }; -} - -export interface LSHTMLConfig { - enable: boolean; - hover: { - enable: boolean; - }; - completions: { - enable: boolean; - emmet: boolean; - }; - tagComplete: { - enable: boolean; - }; - documentSymbols: { - enable: boolean; - }; - linkedEditing: { - enable: boolean; - }; -} - -export type CompilerWarningsSettings = Record; - -export interface LSSvelteConfig { - enable: boolean; - compilerWarnings: CompilerWarningsSettings; - diagnostics: { - enable: boolean; - }; - format: { - enable: boolean; - config: { - svelteSortOrder: string; - svelteStrictMode: boolean; - svelteAllowShorthand: boolean; - svelteBracketNewLine: boolean; - svelteIndentScriptAndStyle: boolean; - printWidth: number; - singleQuote: boolean; - }; - }; - rename: { - enable: boolean; - }; - completions: { - enable: boolean; - }; - hover: { - enable: boolean; - }; - codeActions: { - enable: boolean; - }; - selectionRange: { - enable: boolean; - }; - defaultScriptLanguage: 'none' | 'ts'; -} - -/** - * A subset of the JS/TS VS Code settings which - * are transformed to ts.UserPreferences. - * It may not be available in other IDEs, that's why the keys are optional. - */ -export interface TSUserConfig { - preferences?: TsUserPreferencesConfig; - suggest?: TSSuggestConfig; - format?: TsFormatConfig; - inlayHints?: TsInlayHintsConfig; -} - -/** - * A subset of the JS/TS VS Code settings which - * are transformed to ts.UserPreferences. - */ -export interface TsUserPreferencesConfig { - importModuleSpecifier: ts.UserPreferences['importModuleSpecifierPreference']; - importModuleSpecifierEnding: ts.UserPreferences['importModuleSpecifierEnding']; - quoteStyle: ts.UserPreferences['quotePreference']; - autoImportFileExcludePatterns: ts.UserPreferences['autoImportFileExcludePatterns']; - - /** - * only in typescript config - */ - includePackageJsonAutoImports?: ts.UserPreferences['includePackageJsonAutoImports']; - - preferTypeOnlyAutoImports?: ts.UserPreferences['preferTypeOnlyAutoImports']; -} - -/** - * A subset of the JS/TS VS Code settings which - * are transformed to ts.UserPreferences. - */ -export interface TSSuggestConfig { - autoImports: ts.UserPreferences['includeCompletionsForModuleExports']; - includeAutomaticOptionalChainCompletions: boolean | undefined; - includeCompletionsForImportStatements: boolean | undefined; - classMemberSnippets: { enabled: boolean } | undefined; - objectLiteralMethodSnippets: { enabled: boolean } | undefined; - includeCompletionsWithSnippetText: boolean | undefined; -} - -export type TsFormatConfig = Omit< - ts.FormatCodeSettings, - 'indentMultiLineObjectLiteralBeginningOnBlankLine' | keyof ts.EditorSettings ->; -export interface TsInlayHintsConfig { - enumMemberValues: { enabled: boolean } | undefined; - functionLikeReturnTypes: { enabled: boolean } | undefined; - parameterNames: - | { - enabled: ts.UserPreferences['includeInlayParameterNameHints']; - suppressWhenArgumentMatchesName: boolean; - } - | undefined; - parameterTypes: { enabled: boolean } | undefined; - propertyDeclarationTypes: { enabled: boolean } | undefined; - variableTypes: { enabled: boolean; suppressWhenTypeMatchesName: boolean } | undefined; -} - -export type TsUserConfigLang = 'typescript' | 'javascript'; - -/** - * The config as the vscode-css-languageservice understands it - */ -export interface CssConfig { - validate?: boolean; - lint?: any; - completion?: any; - hover?: any; -} - -/** - * The config as the vscode-html-languageservice understands it - */ -export interface HTMLConfig { - customData?: string[]; -} - -type DeepPartial = T extends CompilerWarningsSettings - ? T - : { - [P in keyof T]?: DeepPartial; - }; - -export class LSConfigManager { - private config: LSConfig = defaultLSConfig; - private listeners: Array<(config: LSConfigManager) => void> = []; - private tsUserPreferences: Record = { - // populate default with _updateTsUserPreferences - typescript: {}, - javascript: {} - }; - private resolvedAutoImportExcludeCache = new FileMap(); - private tsFormatCodeOptions: Record = { - typescript: this.getDefaultFormatCodeOptions(), - javascript: this.getDefaultFormatCodeOptions() - }; - private prettierConfig: any = {}; - private emmetConfig: VSCodeEmmetConfig = {}; - private cssConfig: CssConfig | undefined; - private scssConfig: CssConfig | undefined; - private lessConfig: CssConfig | undefined; - private htmlConfig: HTMLConfig | undefined; - private isTrusted = true; - private clientCapabilities: ClientCapabilities | undefined; - - constructor() { - this._updateTsUserPreferences('javascript', {}); - this._updateTsUserPreferences('typescript', {}); - } - - /** - * Updates config. - */ - update(config: DeepPartial | undefined): void { - // Ideally we shouldn't need the merge here because all updates should be valid and complete configs. - // But since those configs come from the client they might be out of synch with the valid config: - // We might at some point in the future forget to synch config settings in all packages after updating the config. - this.config = merge({}, defaultLSConfig, this.config, config); - // Merge will keep arrays/objects if the new one is empty/has less entries, - // therefore we need some extra checks if there are new settings - if (config?.svelte?.compilerWarnings) { - this.config.svelte.compilerWarnings = config.svelte.compilerWarnings; - } - - this.notifyListeners(); - } - - /** - * Whether or not specified config is enabled - * @param key a string which is a path. Example: 'svelte.diagnostics.enable'. - */ - enabled(key: string): boolean { - return !!this.get(key); - } - - /** - * Get specific config - * @param key a string which is a path. Example: 'svelte.diagnostics.enable'. - */ - get(key: string): T { - return get(this.config, key); - } - - /** - * Get the whole config - */ - getConfig(): Readonly { - return this.config; - } - - /** - * Register a listener which is invoked when the config changed. - */ - onChange(callback: (config: LSConfigManager) => void): void { - this.listeners.push(callback); - } - - updateEmmetConfig(config: VSCodeEmmetConfig): void { - this.emmetConfig = config || {}; - this.notifyListeners(); - } - - getEmmetConfig(): VSCodeEmmetConfig { - return this.emmetConfig; - } - - updatePrettierConfig(config: any): void { - this.prettierConfig = config || {}; - this.notifyListeners(); - } - - getPrettierConfig(): any { - return this.prettierConfig; - } - - /** - * Returns a merged Prettier config following these rules: - * - If `prettierFromFileConfig` exists, that one is returned - * - Else the Svelte extension's Prettier config is used as a starting point, - * and overridden by a possible Prettier config from the Prettier extension, - * or, if that doesn't exist, a possible fallback override. - */ - getMergedPrettierConfig( - prettierFromFileConfig: any, - overridesWhenNoPrettierConfig: any = {} - ): any { - return ( - returnObjectIfHasKeys(prettierFromFileConfig) || - merge( - {}, // merge into empty obj to not manipulate own config - this.get('svelte.format.config'), - returnObjectIfHasKeys(this.getPrettierConfig()) || - overridesWhenNoPrettierConfig || - {} - ) - ); - } - - updateTsJsUserPreferences(config: Record): void { - (['typescript', 'javascript'] as const).forEach((lang) => { - if (config[lang]) { - this._updateTsUserPreferences(lang, config[lang]); - } - }); - this.notifyListeners(); - this.resolvedAutoImportExcludeCache.clear(); - } - - /** - * Whether or not the current workspace can be trusted. - * If not, certain operations should be disabled. - */ - getIsTrusted(): boolean { - return this.isTrusted; - } - - updateIsTrusted(isTrusted: boolean): void { - this.isTrusted = isTrusted; - this.notifyListeners(); - } - - private _updateTsUserPreferences(lang: TsUserConfigLang, config: TSUserConfig) { - const { inlayHints } = config; - - this.tsUserPreferences[lang] = { - ...this.tsUserPreferences[lang], - importModuleSpecifierPreference: config.preferences?.importModuleSpecifier, - importModuleSpecifierEnding: config.preferences?.importModuleSpecifierEnding, - includePackageJsonAutoImports: config.preferences?.includePackageJsonAutoImports, - quotePreference: config.preferences?.quoteStyle, - includeCompletionsForModuleExports: config.suggest?.autoImports ?? true, - includeCompletionsForImportStatements: - config.suggest?.includeCompletionsForImportStatements ?? true, - includeAutomaticOptionalChainCompletions: - config.suggest?.includeAutomaticOptionalChainCompletions ?? true, - includeCompletionsWithInsertText: true, - autoImportFileExcludePatterns: config.preferences?.autoImportFileExcludePatterns, - useLabelDetailsInCompletionEntries: true, - includeCompletionsWithSnippetText: - config.suggest?.includeCompletionsWithSnippetText ?? true, - includeCompletionsWithClassMemberSnippets: - config.suggest?.classMemberSnippets?.enabled ?? true, - includeCompletionsWithObjectLiteralMethodSnippets: - config.suggest?.objectLiteralMethodSnippets?.enabled ?? true, - preferTypeOnlyAutoImports: config.preferences?.preferTypeOnlyAutoImports, - - includeInlayEnumMemberValueHints: inlayHints?.enumMemberValues?.enabled, - includeInlayFunctionLikeReturnTypeHints: inlayHints?.functionLikeReturnTypes?.enabled, - includeInlayParameterNameHints: inlayHints?.parameterNames?.enabled, - includeInlayParameterNameHintsWhenArgumentMatchesName: - inlayHints?.parameterNames?.suppressWhenArgumentMatchesName === false, - includeInlayFunctionParameterTypeHints: inlayHints?.parameterTypes?.enabled, - includeInlayVariableTypeHints: inlayHints?.variableTypes?.enabled, - includeInlayPropertyDeclarationTypeHints: inlayHints?.propertyDeclarationTypes?.enabled, - includeInlayVariableTypeHintsWhenTypeMatchesName: - inlayHints?.variableTypes?.suppressWhenTypeMatchesName === false, - - interactiveInlayHints: true - }; - } - - getTsUserPreferences(lang: TsUserConfigLang, workspacePath: string | null): ts.UserPreferences { - const userPreferences = this.tsUserPreferences[lang]; - - if (!workspacePath || !userPreferences.autoImportFileExcludePatterns) { - return userPreferences; - } - - let autoImportFileExcludePatterns = this.resolvedAutoImportExcludeCache.get(workspacePath); - - if (!autoImportFileExcludePatterns) { - autoImportFileExcludePatterns = userPreferences.autoImportFileExcludePatterns.map( - (p) => { - // Normalization rules: https://github.com/microsoft/TypeScript/pull/49578 - const slashNormalized = p.replace(/\\/g, '/'); - const isRelative = /^\.\.?($|\/)/.test(slashNormalized); - if (path.isAbsolute(p)) { - return p; - } - - return path.join( - workspacePath, - p.startsWith('*') - ? '/' + slashNormalized - : isRelative - ? p - : '/**/' + slashNormalized - ); - } - ); - this.resolvedAutoImportExcludeCache.set(workspacePath, autoImportFileExcludePatterns); - } - - return { - ...userPreferences, - autoImportFileExcludePatterns - }; - } - - updateCssConfig(config: CssConfig | undefined): void { - this.cssConfig = config; - this.notifyListeners(); - } - - getCssConfig(): CssConfig | undefined { - return this.cssConfig; - } - - updateScssConfig(config: CssConfig | undefined): void { - this.scssConfig = config; - this.notifyListeners(); - } - - getScssConfig(): CssConfig | undefined { - return this.scssConfig; - } - - updateLessConfig(config: CssConfig | undefined): void { - this.lessConfig = config; - this.notifyListeners(); - } - - getLessConfig(): CssConfig | undefined { - return this.lessConfig; - } - - updateHTMLConfig(config: HTMLConfig | undefined): void { - this.htmlConfig = config; - this.notifyListeners(); - } - - getHTMLConfig(): HTMLConfig | undefined { - return this.htmlConfig; - } - - updateTsJsFormateConfig(config: Record): void { - (['typescript', 'javascript'] as const).forEach((lang) => { - if (config[lang]) { - this._updateTsFormatConfig(lang, config[lang]); - } - }); - this.notifyListeners(); - } - - private getDefaultFormatCodeOptions(): ts.FormatCodeSettings { - // https://github.com/microsoft/TypeScript/blob/394f51aeed80788dca72c6f6a90d1d27886b6972/src/services/types.ts#L1014 - return { - indentSize: 4, - tabSize: 4, - convertTabsToSpaces: true, - indentStyle: ts.IndentStyle.Smart, - insertSpaceAfterConstructor: false, - insertSpaceAfterCommaDelimiter: true, - insertSpaceAfterSemicolonInForStatements: true, - insertSpaceBeforeAndAfterBinaryOperators: true, - insertSpaceAfterKeywordsInControlFlowStatements: true, - insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, - insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, - insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, - insertSpaceBeforeFunctionParenthesis: false, - placeOpenBraceOnNewLineForFunctions: false, - placeOpenBraceOnNewLineForControlBlocks: false, - trimTrailingWhitespace: true, - semicolons: ts.SemicolonPreference.Ignore, - - // Override TypeScript's default because VSCode default to true - // Also this matches the style of prettier - insertSpaceAfterFunctionKeywordForAnonymousFunctions: true - }; - } - - private _updateTsFormatConfig(lang: TsUserConfigLang, config: TSUserConfig) { - this.tsFormatCodeOptions[lang] = { - ...this.tsFormatCodeOptions[lang], - ...(config.format ?? {}) - }; - } - - async getFormatCodeSettingsForFile( - document: Document, - scriptKind: ts.ScriptKind - ): Promise { - const filePath = document.getFilePath(); - const configLang = - scriptKind === ts.ScriptKind.TS || scriptKind === ts.ScriptKind.TSX - ? 'typescript' - : 'javascript'; - - const tsFormatCodeOptions = this.tsFormatCodeOptions[configLang]; - - if (!filePath) { - return tsFormatCodeOptions; - } - - const prettierConfig = this.getMergedPrettierConfig( - await importPrettier(filePath).resolveConfig(filePath, { - editorconfig: true - }) - ); - const useSemicolons = prettierConfig.semi ?? true; - const documentUseLf = - document.getText().includes('\n') && !document.getText().includes('\r\n'); - - const indentSize = - (typeof prettierConfig.tabWidth === 'number' ? prettierConfig.tabWidth : null) ?? - tsFormatCodeOptions.tabSize; - - return { - ...tsFormatCodeOptions, - - newLineCharacter: documentUseLf ? '\n' : ts.sys.newLine, - baseIndentSize: prettierConfig.svelteIndentScriptAndStyle === false ? 0 : indentSize, - indentSize, - convertTabsToSpaces: !prettierConfig.useTabs, - semicolons: useSemicolons - ? ts.SemicolonPreference.Insert - : ts.SemicolonPreference.Remove, - tabSize: indentSize - }; - } - - private scheduledUpdate: NodeJS.Timeout | undefined; - private notifyListeners() { - if (this.scheduledUpdate) { - clearTimeout(this.scheduledUpdate); - } - this.scheduledUpdate = setTimeout(() => { - this.scheduledUpdate = undefined; - this.listeners.forEach((listener) => listener(this)); - }); - } - - updateClientCapabilities(clientCapabilities: ClientCapabilities) { - this.clientCapabilities = clientCapabilities; - this.notifyListeners(); - } - - getClientCapabilities() { - return this.clientCapabilities; - } -} diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts deleted file mode 100644 index b239c21ee..000000000 --- a/packages/language-server/src/plugins/PluginHost.ts +++ /dev/null @@ -1,762 +0,0 @@ -import { flatten } from 'lodash'; -import { performance } from 'perf_hooks'; -import { - CallHierarchyIncomingCall, - CallHierarchyItem, - CallHierarchyOutgoingCall, - CancellationToken, - CodeAction, - CodeActionContext, - Color, - ColorInformation, - ColorPresentation, - CompletionContext, - CompletionItem, - CompletionList, - DefinitionLink, - Diagnostic, - FoldingRange, - FormattingOptions, - Hover, - LinkedEditingRanges, - Location, - Position, - Range, - ReferenceContext, - SelectionRange, - SemanticTokens, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - TextDocumentContentChangeEvent, - TextDocumentIdentifier, - TextEdit, - WorkspaceEdit, - InlayHint -} from 'vscode-languageserver'; -import { DocumentManager, getNodeIfIsInHTMLStartTag } from '../lib/documents'; -import { Logger } from '../logger'; -import { isNotNullOrUndefined, regexLastIndexOf } from '../utils'; -import { - AppCompletionItem, - FileRename, - LSPProviderConfig, - LSProvider, - OnWatchFileChanges, - OnWatchFileChangesPara, - Plugin -} from './interfaces'; - -enum ExecuteMode { - None, - FirstNonNull, - Collect -} - -export class PluginHost implements LSProvider, OnWatchFileChanges { - private plugins: Plugin[] = []; - private pluginHostConfig: LSPProviderConfig = { - filterIncompleteCompletions: true, - definitionLinkSupport: false - }; - private deferredRequests: Record]> = {}; - private requestTimings: Record = {}; - - constructor(private documentsManager: DocumentManager) {} - - initialize(pluginHostConfig: LSPProviderConfig) { - this.pluginHostConfig = pluginHostConfig; - } - - register(plugin: Plugin) { - this.plugins.push(plugin); - } - - didUpdateDocument() { - this.deferredRequests = {}; - } - - async getDiagnostics( - textDocument: TextDocumentIdentifier, - cancellationToken?: CancellationToken - ): Promise { - const document = this.getDocument(textDocument.uri); - - if ( - (document.getFilePath()?.includes('/node_modules/') || - document.getFilePath()?.includes('\\node_modules\\')) && - // Sapper convention: Put stuff inside node_modules below src - !( - document.getFilePath()?.includes('/src/node_modules/') || - document.getFilePath()?.includes('\\src\\node_modules\\') - ) - ) { - // Don't return diagnostics for files inside node_modules. These are considered read-only (cannot be changed) - // and in case of svelte-check they would pollute/skew the output - return []; - } - - return flatten( - await this.execute( - 'getDiagnostics', - [document, cancellationToken], - ExecuteMode.Collect, - 'high' - ) - ); - } - - async doHover(textDocument: TextDocumentIdentifier, position: Position): Promise { - const document = this.getDocument(textDocument.uri); - - return this.execute( - 'doHover', - [document, position], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - async getCompletions( - textDocument: TextDocumentIdentifier, - position: Position, - completionContext?: CompletionContext, - cancellationToken?: CancellationToken - ): Promise { - const document = this.getDocument(textDocument.uri); - - const completions = await Promise.all( - this.plugins.map(async (plugin) => { - const result = await this.tryExecutePlugin( - plugin, - 'getCompletions', - [document, position, completionContext, cancellationToken], - null - ); - if (result) { - return { result: result as CompletionList, plugin: plugin.__name }; - } - }) - ).then((completions) => completions.filter(isNotNullOrUndefined)); - - const html = completions.find((completion) => completion.plugin === 'html'); - const ts = completions.find((completion) => completion.plugin === 'ts'); - if (html && ts && getNodeIfIsInHTMLStartTag(document.html, document.offsetAt(position))) { - // Completion in a component or html start tag and both html and ts - // suggest something -> filter out all duplicates from TS completions - const htmlCompletions = new Set(html.result.items.map((item) => item.label)); - ts.result.items = ts.result.items.filter((item) => { - const label = item.label; - if (htmlCompletions.has(label)) { - return false; - } - if (label[0] === '"' && label[label.length - 1] === '"') { - // this will result in a wrong completion regardless, remove the quotes - item.label = item.label.slice(1, -1); - if (htmlCompletions.has(item.label)) { - // "aria-label" -> aria-label -> exists in html completions - return false; - } - } - if (label.startsWith('on')) { - if (htmlCompletions.has('on:' + label.slice(2))) { - // onclick -> on:click -> exists in html completions - return false; - } - } - // adjust sort text so it does appear after html completions - item.sortText = 'Z' + (item.sortText || ''); - return true; - }); - } - - let flattenedCompletions = flatten( - completions.map((completion) => completion.result.items) - ); - const isIncomplete = completions.reduce( - (incomplete, completion) => incomplete || completion.result.isIncomplete, - false as boolean - ); - - // If the result is incomplete, we need to filter the results ourselves - // to throw out non-matching results. VSCode does filter client-side, - // but other IDEs might not. - if (isIncomplete && this.pluginHostConfig.filterIncompleteCompletions) { - const offset = document.offsetAt(position); - // Assumption for performance reasons: - // Noone types import names longer than 20 characters and still expects perfect autocompletion. - const text = document.getText().substring(Math.max(0, offset - 20), offset); - const start = regexLastIndexOf(text, /[\W\s]/g) + 1; - const filterValue = text.substring(start).toLowerCase(); - flattenedCompletions = flattenedCompletions.filter((comp) => - comp.label.toLowerCase().includes(filterValue) - ); - } - - return CompletionList.create(flattenedCompletions, isIncomplete); - } - - async resolveCompletion( - textDocument: TextDocumentIdentifier, - completionItem: AppCompletionItem, - cancellationToken: CancellationToken - ): Promise { - const document = this.getDocument(textDocument.uri); - - const result = await this.execute( - 'resolveCompletion', - [document, completionItem, cancellationToken], - ExecuteMode.FirstNonNull, - 'high' - ); - - return result ?? completionItem; - } - - async formatDocument( - textDocument: TextDocumentIdentifier, - options: FormattingOptions - ): Promise { - const document = this.getDocument(textDocument.uri); - - return flatten( - await this.execute( - 'formatDocument', - [document, options], - ExecuteMode.Collect, - 'high' - ) - ); - } - - async doTagComplete( - textDocument: TextDocumentIdentifier, - position: Position - ): Promise { - const document = this.getDocument(textDocument.uri); - - return this.execute( - 'doTagComplete', - [document, position], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - async getDocumentColors(textDocument: TextDocumentIdentifier): Promise { - const document = this.getDocument(textDocument.uri); - - return flatten( - await this.execute( - 'getDocumentColors', - [document], - ExecuteMode.Collect, - 'low' - ) - ); - } - - async getColorPresentations( - textDocument: TextDocumentIdentifier, - range: Range, - color: Color - ): Promise { - const document = this.getDocument(textDocument.uri); - - return flatten( - await this.execute( - 'getColorPresentations', - [document, range, color], - ExecuteMode.Collect, - 'high' - ) - ); - } - - async getDocumentSymbols( - textDocument: TextDocumentIdentifier, - cancellationToken: CancellationToken - ): Promise { - const document = this.getDocument(textDocument.uri); - - return flatten( - await this.execute( - 'getDocumentSymbols', - [document, cancellationToken], - ExecuteMode.Collect, - 'low' - ) - ); - } - - async getDefinitions( - textDocument: TextDocumentIdentifier, - position: Position - ): Promise { - const document = this.getDocument(textDocument.uri); - - const definitions = flatten( - await this.execute( - 'getDefinitions', - [document, position], - ExecuteMode.Collect, - 'high' - ) - ); - - if (this.pluginHostConfig.definitionLinkSupport) { - return definitions; - } else { - return definitions.map( - (def) => { range: def.targetSelectionRange, uri: def.targetUri } - ); - } - } - - async getCodeActions( - textDocument: TextDocumentIdentifier, - range: Range, - context: CodeActionContext, - cancellationToken: CancellationToken - ): Promise { - const document = this.getDocument(textDocument.uri); - - const actions = flatten( - await this.execute( - 'getCodeActions', - [document, range, context, cancellationToken], - ExecuteMode.Collect, - 'high' - ) - ); - // Sort Svelte actions below other actions as they are often less relevant - actions.sort((a, b) => { - const aPrio = a.title.startsWith('(svelte)') ? 1 : 0; - const bPrio = b.title.startsWith('(svelte)') ? 1 : 0; - return aPrio - bPrio; - }); - return actions; - } - - async executeCommand( - textDocument: TextDocumentIdentifier, - command: string, - args?: any[] - ): Promise { - const document = this.getDocument(textDocument.uri); - - return await this.execute( - 'executeCommand', - [document, command, args], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - async resolveCodeAction( - textDocument: TextDocumentIdentifier, - codeAction: CodeAction, - cancellationToken: CancellationToken - ): Promise { - const document = this.getDocument(textDocument.uri); - - const result = await this.execute( - 'resolveCodeAction', - [document, codeAction, cancellationToken], - ExecuteMode.FirstNonNull, - 'high' - ); - - return result ?? codeAction; - } - - async updateImports(fileRename: FileRename): Promise { - return await this.execute( - 'updateImports', - [fileRename], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - async prepareRename( - textDocument: TextDocumentIdentifier, - position: Position - ): Promise { - const document = this.getDocument(textDocument.uri); - - return await this.execute( - 'prepareRename', - [document, position], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - async rename( - textDocument: TextDocumentIdentifier, - position: Position, - newName: string - ): Promise { - const document = this.getDocument(textDocument.uri); - - return await this.execute( - 'rename', - [document, position, newName], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - async findReferences( - textDocument: TextDocumentIdentifier, - position: Position, - context: ReferenceContext - ): Promise { - const document = this.getDocument(textDocument.uri); - - return await this.execute( - 'findReferences', - [document, position, context], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - async fileReferences(uri: string): Promise { - return await this.execute('fileReferences', [uri], ExecuteMode.FirstNonNull, 'high'); - } - - async findComponentReferences(uri: string): Promise { - return await this.execute( - 'findComponentReferences', - [uri], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - async getSignatureHelp( - textDocument: TextDocumentIdentifier, - position: Position, - context: SignatureHelpContext | undefined, - cancellationToken: CancellationToken - ): Promise { - const document = this.getDocument(textDocument.uri); - - return await this.execute( - 'getSignatureHelp', - [document, position, context, cancellationToken], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - /** - * The selection range supports multiple cursors, - * each position should return its own selection range tree like `Array.map`. - * Quote the LSP spec - * > A selection range in the return array is for the position in the provided parameters at the same index. Therefore positions[i] must be contained in result[i].range. - * @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_selectionRange - * - * Making PluginHost implement the same interface would make it quite hard to get - * the corresponding selection range of each position from different plugins. - * Therefore the special treatment here. - */ - async getSelectionRanges( - textDocument: TextDocumentIdentifier, - positions: Position[] - ): Promise { - const document = this.getDocument(textDocument.uri); - - try { - return Promise.all( - positions.map(async (position) => { - for (const plugin of this.plugins) { - const range = await plugin.getSelectionRange?.(document, position); - - if (range) { - return range; - } - } - return SelectionRange.create(Range.create(position, position)); - }) - ); - } catch (error) { - Logger.error(error); - return null; - } - } - - async getSemanticTokens( - textDocument: TextDocumentIdentifier, - range?: Range, - cancellationToken?: CancellationToken - ) { - const document = this.getDocument(textDocument.uri); - - return await this.execute( - 'getSemanticTokens', - [document, range, cancellationToken], - ExecuteMode.FirstNonNull, - 'smart' - ); - } - - async getLinkedEditingRanges( - textDocument: TextDocumentIdentifier, - position: Position - ): Promise { - const document = this.getDocument(textDocument.uri); - - return await this.execute( - 'getLinkedEditingRanges', - [document, position], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - getImplementation( - textDocument: TextDocumentIdentifier, - position: Position - ): Promise { - const document = this.getDocument(textDocument.uri); - - return this.execute( - 'getImplementation', - [document, position], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - getTypeDefinition( - textDocument: TextDocumentIdentifier, - position: Position - ): Promise { - const document = this.getDocument(textDocument.uri); - - return this.execute( - 'getTypeDefinition', - [document, position], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - getInlayHints( - textDocument: TextDocumentIdentifier, - range: Range, - cancellationToken?: CancellationToken - ): Promise { - const document = this.getDocument(textDocument.uri); - - return this.execute( - 'getInlayHints', - [document, range, cancellationToken], - ExecuteMode.FirstNonNull, - 'smart' - ); - } - - prepareCallHierarchy( - textDocument: TextDocumentIdentifier, - position: Position, - cancellationToken?: CancellationToken - ): Promise { - const document = this.getDocument(textDocument.uri); - - return this.execute( - 'prepareCallHierarchy', - [document, position, cancellationToken], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - getIncomingCalls( - item: CallHierarchyItem, - cancellationToken?: CancellationToken | undefined - ): Promise { - return this.execute( - 'getIncomingCalls', - [item, cancellationToken], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - getOutgoingCalls( - item: CallHierarchyItem, - cancellationToken?: CancellationToken | undefined - ): Promise { - return this.execute( - 'getOutgoingCalls', - [item, cancellationToken], - ExecuteMode.FirstNonNull, - 'high' - ); - } - - async getFoldingRanges(textDocument: TextDocumentIdentifier): Promise { - const document = this.getDocument(textDocument.uri); - - const result = flatten( - await this.execute( - 'getFoldingRanges', - [document], - ExecuteMode.Collect, - 'high' - ) - ); - - return result; - } - - onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void { - for (const support of this.plugins) { - support.onWatchFileChanges?.(onWatchFileChangesParas); - } - } - - updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void { - for (const support of this.plugins) { - support.updateTsOrJsFile?.(fileName, changes); - } - } - - private getDocument(uri: string) { - const document = this.documentsManager.get(uri); - if (!document) { - throw new Error('Cannot call methods on an unopened document'); - } - return document; - } - - private execute( - name: keyof LSProvider, - args: any[], - mode: ExecuteMode.FirstNonNull, - priority: 'low' | 'high' | 'smart' - ): Promise; - private execute( - name: keyof LSProvider, - args: any[], - mode: ExecuteMode.Collect, - priority: 'low' | 'high' | 'smart' - ): Promise; - private execute( - name: keyof LSProvider, - args: any[], - mode: ExecuteMode.None, - priority: 'low' | 'high' | 'smart' - ): Promise; - private async execute( - name: keyof LSProvider, - args: any[], - mode: ExecuteMode, - priority: 'low' | 'high' | 'smart' - ): Promise<(T | null) | T[] | void> { - const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function'); - // Priority 'smart' tries to aproximate how much time a method takes to execute, - // making it low priority if it takes too long or if it seems like other methods do. - const now = performance.now(); - if ( - priority === 'smart' && - (this.requestTimings[name]?.[0] > 500 || - Object.values(this.requestTimings).filter( - (t) => t[0] > 400 && now - t[1] < 60 * 1000 - ).length > 2) - ) { - Logger.debug(`Executing next invocation of "${name}" with low priority`); - priority = 'low'; - if (this.requestTimings[name]) { - this.requestTimings[name][0] = this.requestTimings[name][0] / 2 + 150; - } - } - - if (priority === 'low') { - // If a request doesn't have priority, we first wait 1 second to - // 1. let higher priority requests get through first - // 2. wait for possible document changes, which make the request wait again - // Due to waiting, low priority items should preferrably be those who do not - // rely on positions or ranges and rather on the whole document only. - const debounce = async (): Promise => { - const id = Math.random(); - this.deferredRequests[name] = [ - id, - new Promise((resolve, reject) => { - setTimeout(() => { - if ( - !this.deferredRequests[name] || - this.deferredRequests[name][0] === id - ) { - resolve(); - } else { - // We should not get into this case. According to the spec, - // the language client does not send another request - // of the same type until the previous one is answered. - reject(); - } - }, 1000); - }) - ]; - try { - await this.deferredRequests[name][1]; - if (!this.deferredRequests[name]) { - return debounce(); - } - return true; - } catch (e) { - return false; - } - }; - const shouldContinue = await debounce(); - if (!shouldContinue) { - return; - } - } - - const startTime = performance.now(); - const result = await this.executePlugins(name, args, mode, plugins); - this.requestTimings[name] = [performance.now() - startTime, startTime]; - return result; - } - - private async executePlugins( - name: keyof LSProvider, - args: any[], - mode: ExecuteMode, - plugins: Plugin[] - ) { - switch (mode) { - case ExecuteMode.FirstNonNull: - for (const plugin of plugins) { - const res = await this.tryExecutePlugin(plugin, name, args, null); - if (res != null) { - return res; - } - } - return null; - case ExecuteMode.Collect: - return Promise.all( - plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, [])) - ); - case ExecuteMode.None: - await Promise.all( - plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null)) - ); - return; - } - } - - private async tryExecutePlugin(plugin: any, fnName: string, args: any[], failValue: any) { - try { - return await plugin[fnName](...args); - } catch (e) { - Logger.error(e); - return failValue; - } - } -} diff --git a/packages/language-server/src/plugins/css/CSSDocument.ts b/packages/language-server/src/plugins/css/CSSDocument.ts deleted file mode 100644 index cb6a9ae80..000000000 --- a/packages/language-server/src/plugins/css/CSSDocument.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Stylesheet, TextDocument } from 'vscode-css-languageservice'; -import { Position } from 'vscode-languageserver'; -import { Document, DocumentMapper, ReadableDocument, TagInformation } from '../../lib/documents'; -import { CSSLanguageServices, getLanguageService } from './service'; - -export interface CSSDocumentBase extends DocumentMapper, TextDocument { - languageId: string; - stylesheet: Stylesheet; -} - -export class CSSDocument extends ReadableDocument implements DocumentMapper { - private styleInfo: Pick; - readonly version = this.parent.version; - - public stylesheet: Stylesheet; - public languageId: string; - - constructor( - private parent: Document, - languageServices: CSSLanguageServices - ) { - super(); - - if (this.parent.styleInfo) { - this.styleInfo = this.parent.styleInfo; - } else { - this.styleInfo = { - attributes: {}, - start: -1, - end: -1 - }; - } - - this.languageId = this.language; - this.stylesheet = getLanguageService(languageServices, this.languageId).parseStylesheet( - this - ); - } - - /** - * Get the fragment position relative to the parent - * @param pos Position in fragment - */ - getOriginalPosition(pos: Position): Position { - const parentOffset = this.styleInfo.start + this.offsetAt(pos); - return this.parent.positionAt(parentOffset); - } - - /** - * Get the position relative to the start of the fragment - * @param pos Position in parent - */ - getGeneratedPosition(pos: Position): Position { - const fragmentOffset = this.parent.offsetAt(pos) - this.styleInfo.start; - return this.positionAt(fragmentOffset); - } - - /** - * Returns true if the given parent position is inside of this fragment - * @param pos Position in parent - */ - isInGenerated(pos: Position): boolean { - const offset = this.parent.offsetAt(pos); - return offset >= this.styleInfo.start && offset <= this.styleInfo.end; - } - - /** - * Get the fragment text from the parent - */ - getText(): string { - return this.parent.getText().slice(this.styleInfo.start, this.styleInfo.end); - } - - /** - * Returns the length of the fragment as calculated from the start and end positon - */ - getTextLength(): number { - return this.styleInfo.end - this.styleInfo.start; - } - - /** - * Return the parent file path - */ - getFilePath(): string | null { - return this.parent.getFilePath(); - } - - getURL() { - return this.parent.getURL(); - } - - getAttributes() { - return this.styleInfo.attributes; - } - - private get language() { - const attrs = this.getAttributes(); - return attrs.lang || attrs.type || 'css'; - } -} diff --git a/packages/language-server/src/plugins/css/CSSPlugin.ts b/packages/language-server/src/plugins/css/CSSPlugin.ts deleted file mode 100644 index 1147bef81..000000000 --- a/packages/language-server/src/plugins/css/CSSPlugin.ts +++ /dev/null @@ -1,545 +0,0 @@ -import { doComplete as doEmmetComplete } from '@vscode/emmet-helper'; -import { - Color, - ColorInformation, - ColorPresentation, - CompletionContext, - CompletionList, - CompletionTriggerKind, - Diagnostic, - Hover, - Position, - Range, - SymbolInformation, - CompletionItem, - CompletionItemKind, - SelectionRange, - WorkspaceFolder -} from 'vscode-languageserver'; -import { - Document, - DocumentManager, - mapColorPresentationToOriginal, - mapCompletionItemToOriginal, - mapRangeToGenerated, - mapSymbolInformationToOriginal, - mapObjWithRangeToOriginal, - mapHoverToParent, - mapSelectionRangeToParent, - isInTag, - mapRangeToOriginal, - TagInformation -} from '../../lib/documents'; -import { LSConfigManager, LSCSSConfig } from '../../ls-config'; -import { - ColorPresentationsProvider, - CompletionsProvider, - DiagnosticsProvider, - DocumentColorsProvider, - DocumentSymbolsProvider, - FoldingRangeProvider, - HoverProvider, - SelectionRangeProvider -} from '../interfaces'; -import { CSSDocument, CSSDocumentBase } from './CSSDocument'; -import { CSSLanguageServices, getLanguage, getLanguageService } from './service'; -import { GlobalVars } from './global-vars'; -import { getIdClassCompletion } from './features/getIdClassCompletion'; -import { AttributeContext, getAttributeContextAtPosition } from '../../lib/documents/parseHtml'; -import { StyleAttributeDocument } from './StyleAttributeDocument'; -import { getDocumentContext } from '../documentContext'; -import { FoldingRange, FoldingRangeKind } from 'vscode-languageserver-types'; -import { indentBasedFoldingRangeForTag } from '../../lib/foldingRange/indentFolding'; - -export class CSSPlugin - implements - HoverProvider, - CompletionsProvider, - DiagnosticsProvider, - DocumentColorsProvider, - ColorPresentationsProvider, - DocumentSymbolsProvider, - SelectionRangeProvider, - FoldingRangeProvider -{ - __name = 'css'; - private configManager: LSConfigManager; - private cssDocuments = new WeakMap(); - private cssLanguageServices: CSSLanguageServices; - private workspaceFolders: WorkspaceFolder[]; - private triggerCharacters = ['.', ':', '-', '/']; - private globalVars = new GlobalVars(); - - constructor( - docManager: DocumentManager, - configManager: LSConfigManager, - workspaceFolders: WorkspaceFolder[], - cssLanguageServices: CSSLanguageServices - ) { - this.cssLanguageServices = cssLanguageServices; - this.workspaceFolders = workspaceFolders; - this.configManager = configManager; - this.updateConfigs(); - - this.globalVars.watchFiles(this.configManager.get('css.globals')); - this.configManager.onChange((config) => { - this.globalVars.watchFiles(config.get('css.globals')); - this.updateConfigs(); - }); - - docManager.on('documentChange', (document) => - this.cssDocuments.set(document, new CSSDocument(document, this.cssLanguageServices)) - ); - docManager.on('documentClose', (document) => this.cssDocuments.delete(document)); - } - - getSelectionRange(document: Document, position: Position): SelectionRange | null { - if (!this.featureEnabled('selectionRange') || !isInTag(position, document.styleInfo)) { - return null; - } - - const cssDocument = this.getCSSDoc(document); - const [range] = this.getLanguageService(extractLanguage(cssDocument)).getSelectionRanges( - cssDocument, - [cssDocument.getGeneratedPosition(position)], - cssDocument.stylesheet - ); - - if (!range) { - return null; - } - - return mapSelectionRangeToParent(cssDocument, range); - } - - getDiagnostics(document: Document): Diagnostic[] { - if (!this.featureEnabled('diagnostics')) { - return []; - } - - const cssDocument = this.getCSSDoc(document); - const kind = extractLanguage(cssDocument); - - if (shouldExcludeValidation(kind)) { - return []; - } - - return this.getLanguageService(kind) - .doValidation(cssDocument, cssDocument.stylesheet) - .map((diagnostic) => ({ ...diagnostic, source: getLanguage(kind) })) - .map((diagnostic) => mapObjWithRangeToOriginal(cssDocument, diagnostic)); - } - - doHover(document: Document, position: Position): Hover | null { - if (!this.featureEnabled('hover')) { - return null; - } - - const cssDocument = this.getCSSDoc(document); - if (shouldExcludeHover(cssDocument)) { - return null; - } - if (cssDocument.isInGenerated(position)) { - return this.doHoverInternal(cssDocument, position); - } - const attributeContext = getAttributeContextAtPosition(document, position); - if ( - attributeContext && - this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText()) - ) { - const [start, end] = attributeContext.valueRange; - return this.doHoverInternal( - new StyleAttributeDocument(document, start, end, this.cssLanguageServices), - position - ); - } - - return null; - } - private doHoverInternal(cssDocument: CSSDocumentBase, position: Position) { - const hoverInfo = this.getLanguageService(extractLanguage(cssDocument)).doHover( - cssDocument, - cssDocument.getGeneratedPosition(position), - cssDocument.stylesheet - ); - return hoverInfo ? mapHoverToParent(cssDocument, hoverInfo) : hoverInfo; - } - - async getCompletions( - document: Document, - position: Position, - completionContext?: CompletionContext - ): Promise { - const triggerCharacter = completionContext?.triggerCharacter; - const triggerKind = completionContext?.triggerKind; - const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter; - - if ( - isCustomTriggerCharacter && - triggerCharacter && - !this.triggerCharacters.includes(triggerCharacter) - ) { - return null; - } - - if (!this.featureEnabled('completions')) { - return null; - } - - const cssDocument = this.getCSSDoc(document); - - if (cssDocument.isInGenerated(position)) { - return this.getCompletionsInternal(document, position, cssDocument); - } - - const attributeContext = getAttributeContextAtPosition(document, position); - if (!attributeContext) { - return null; - } - - if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) { - const [start, end] = attributeContext.valueRange; - return this.getCompletionsInternal( - document, - position, - new StyleAttributeDocument(document, start, end, this.cssLanguageServices) - ); - } else { - return getIdClassCompletion(cssDocument, attributeContext); - } - } - - private inStyleAttributeWithoutInterpolation( - attrContext: AttributeContext, - text: string - ): attrContext is Required { - return ( - attrContext.name === 'style' && - !!attrContext.valueRange && - !text.substring(attrContext.valueRange[0], attrContext.valueRange[1]).includes('{') - ); - } - - private async getCompletionsInternal( - document: Document, - position: Position, - cssDocument: CSSDocumentBase - ) { - if (isSASS(cssDocument)) { - // the css language service does not support sass, still we can use - // the emmet helper directly to at least get emmet completions - return ( - doEmmetComplete(document, position, 'sass', this.configManager.getEmmetConfig()) || - null - ); - } - - const type = extractLanguage(cssDocument); - if (shouldExcludeCompletion(type)) { - return null; - } - - const lang = this.getLanguageService(type); - let emmetResults: CompletionList = { - isIncomplete: false, - items: [] - }; - if ( - this.configManager.getConfig().css.completions.emmet && - this.configManager.getEmmetConfig().showExpandedAbbreviation !== 'never' - ) { - lang.setCompletionParticipants([ - { - onCssProperty: (context) => { - if (context?.propertyName) { - emmetResults = - doEmmetComplete( - cssDocument, - cssDocument.getGeneratedPosition(position), - getLanguage(type), - this.configManager.getEmmetConfig() - ) || emmetResults; - } - }, - onCssPropertyValue: (context) => { - if (context?.propertyValue) { - emmetResults = - doEmmetComplete( - cssDocument, - cssDocument.getGeneratedPosition(position), - getLanguage(type), - this.configManager.getEmmetConfig() - ) || emmetResults; - } - } - } - ]); - } - - const results = await lang.doComplete2( - cssDocument, - cssDocument.getGeneratedPosition(position), - cssDocument.stylesheet, - getDocumentContext(cssDocument.uri, this.workspaceFolders) - ); - return CompletionList.create( - this.appendGlobalVars( - [...(results ? results.items : []), ...emmetResults.items].map((completionItem) => - mapCompletionItemToOriginal(cssDocument, completionItem) - ) - ), - // Emmet completions change on every keystroke, so they are never complete - emmetResults.items.length > 0 - ); - } - - private appendGlobalVars(items: CompletionItem[]): CompletionItem[] { - // Finding one value with that item kind means we are in a value completion scenario - const value = items.find((item) => item.kind === CompletionItemKind.Value); - if (!value) { - return items; - } - - const additionalItems: CompletionItem[] = this.globalVars - .getGlobalVars() - .map((globalVar) => ({ - label: `var(${globalVar.name})`, - sortText: '-', - detail: `${globalVar.filename}\n\n${globalVar.name}: ${globalVar.value}`, - kind: CompletionItemKind.Value - })); - return [...items, ...additionalItems]; - } - - getDocumentColors(document: Document): ColorInformation[] { - if (!this.featureEnabled('documentColors')) { - return []; - } - - const cssDocument = this.getCSSDoc(document); - - if (shouldExcludeColor(cssDocument)) { - return []; - } - - return this.getLanguageService(extractLanguage(cssDocument)) - .findDocumentColors(cssDocument, cssDocument.stylesheet) - .map((colorInfo) => mapObjWithRangeToOriginal(cssDocument, colorInfo)); - } - - getColorPresentations(document: Document, range: Range, color: Color): ColorPresentation[] { - if (!this.featureEnabled('colorPresentations')) { - return []; - } - - const cssDocument = this.getCSSDoc(document); - if ( - (!cssDocument.isInGenerated(range.start) && !cssDocument.isInGenerated(range.end)) || - shouldExcludeColor(cssDocument) - ) { - return []; - } - - return this.getLanguageService(extractLanguage(cssDocument)) - .getColorPresentations( - cssDocument, - cssDocument.stylesheet, - color, - mapRangeToGenerated(cssDocument, range) - ) - .map((colorPres) => mapColorPresentationToOriginal(cssDocument, colorPres)); - } - - getDocumentSymbols(document: Document): SymbolInformation[] { - if (!this.featureEnabled('documentColors')) { - return []; - } - - const cssDocument = this.getCSSDoc(document); - - if (shouldExcludeDocumentSymbols(cssDocument)) { - return []; - } - - return this.getLanguageService(extractLanguage(cssDocument)) - .findDocumentSymbols(cssDocument, cssDocument.stylesheet) - .map((symbol) => { - if (!symbol.containerName) { - return { - ...symbol, - // TODO: this could contain other things, e.g. style.myclass - containerName: 'style' - }; - } - - return symbol; - }) - .map((symbol) => mapSymbolInformationToOriginal(cssDocument, symbol)); - } - - getFoldingRanges(document: Document): FoldingRange[] { - if (!document.styleInfo) { - return []; - } - - const cssDocument = this.getCSSDoc(document); - - if (shouldUseIndentBasedFolding(cssDocument.languageId)) { - return this.nonSyntacticFolding(document, document.styleInfo); - } - - return this.getLanguageService(extractLanguage(cssDocument)) - .getFoldingRanges(cssDocument) - .map((range) => { - const originalRange = mapRangeToOriginal(cssDocument, { - start: { line: range.startLine, character: range.startCharacter ?? 0 }, - end: { line: range.endLine, character: range.endCharacter ?? 0 } - }); - - return { - startLine: originalRange.start.line, - endLine: originalRange.end.line, - kind: range.kind - }; - }); - } - - private nonSyntacticFolding(document: Document, styleInfo: TagInformation): FoldingRange[] { - const ranges = indentBasedFoldingRangeForTag(document, styleInfo); - const startRegion = /^\s*(\/\/|\/\*\*?)\s*#?region\b/; - const endRegion = /^\s*(\/\/|\/\*\*?)\s*#?endregion\b/; - - const lines = document - .getText() - .split(/\r?\n/) - .slice(styleInfo.startPos.line, styleInfo.endPos.line); - - let start = -1; - - for (let index = 0; index < lines.length; index++) { - const line = lines[index]; - - if (startRegion.test(line)) { - start = index; - } else if (endRegion.test(line)) { - if (start >= 0) { - ranges.push({ - startLine: start + styleInfo.startPos.line, - endLine: index + styleInfo.startPos.line, - kind: FoldingRangeKind.Region - }); - } - start = -1; - } - } - - return ranges.sort((a, b) => a.startLine - b.startLine); - } - - private getCSSDoc(document: Document) { - let cssDoc = this.cssDocuments.get(document); - if (!cssDoc || cssDoc.version < document.version) { - cssDoc = new CSSDocument(document, this.cssLanguageServices); - this.cssDocuments.set(document, cssDoc); - } - return cssDoc; - } - - private updateConfigs() { - this.getLanguageService('css')?.configure(this.configManager.getCssConfig()); - this.getLanguageService('scss')?.configure(this.configManager.getScssConfig()); - this.getLanguageService('less')?.configure(this.configManager.getLessConfig()); - } - - private featureEnabled(feature: keyof LSCSSConfig) { - return ( - this.configManager.enabled('css.enable') && - this.configManager.enabled(`css.${feature}.enable`) - ); - } - - private getLanguageService(kind: string) { - return getLanguageService(this.cssLanguageServices, kind); - } -} - -function shouldExcludeValidation(kind?: string) { - switch (kind) { - case 'postcss': - case 'sass': - case 'stylus': - case 'styl': - return true; - default: - return false; - } -} - -function shouldExcludeCompletion(kind?: string) { - switch (kind) { - case 'stylus': - case 'styl': - return true; - default: - return false; - } -} - -function shouldExcludeDocumentSymbols(document: CSSDocument) { - switch (extractLanguage(document)) { - case 'sass': - case 'stylus': - case 'styl': - return true; - default: - return false; - } -} - -function shouldExcludeHover(document: CSSDocument) { - switch (extractLanguage(document)) { - case 'sass': - case 'stylus': - case 'styl': - return true; - default: - return false; - } -} - -function shouldExcludeColor(document: CSSDocument) { - switch (extractLanguage(document)) { - case 'sass': - case 'stylus': - case 'styl': - return true; - default: - return false; - } -} - -function shouldUseIndentBasedFolding(kind?: string) { - switch (kind) { - case 'postcss': - case 'sass': - case 'stylus': - case 'styl': - return true; - default: - return false; - } -} - -function isSASS(document: CSSDocumentBase) { - switch (extractLanguage(document)) { - case 'sass': - return true; - default: - return false; - } -} - -function extractLanguage(document: CSSDocumentBase): string { - const lang = document.languageId; - return lang.replace(/^text\//, ''); -} diff --git a/packages/language-server/src/plugins/css/FileSystemProvider.ts b/packages/language-server/src/plugins/css/FileSystemProvider.ts deleted file mode 100644 index 7cace09ff..000000000 --- a/packages/language-server/src/plugins/css/FileSystemProvider.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { stat, readdir, Stats } from 'fs'; -import { promisify } from 'util'; -import { - FileStat, - FileSystemProvider as CSSFileSystemProvider, - FileType -} from 'vscode-css-languageservice'; -import { urlToPath } from '../../utils'; - -interface StatLike { - isDirectory(): boolean; - isFile(): boolean; - isSymbolicLink(): boolean; -} - -export class FileSystemProvider implements CSSFileSystemProvider { - // TODO use fs/promises after we bumps the target nodejs versions - private promisifyStat = promisify(stat); - private promisifyReaddir = promisify(readdir); - - constructor() { - this.readDirectory = this.readDirectory.bind(this); - this.stat = this.stat.bind(this); - } - - async stat(uri: string): Promise { - const path = urlToPath(uri); - - if (!path) { - return this.unknownStat(); - } - - let stat: Stats; - try { - stat = await this.promisifyStat(path); - } catch (error) { - if ( - error != null && - typeof error === 'object' && - 'code' in error && - (error as { code: string }).code === 'ENOENT' - ) { - return { - type: FileType.Unknown, - ctime: -1, - mtime: -1, - size: -1 - }; - } - - throw error; - } - - return { - ctime: stat.ctimeMs, - mtime: stat.mtimeMs, - size: stat.size, - type: this.getFileType(stat) - }; - } - - private unknownStat(): FileStat { - return { - type: FileType.Unknown, - ctime: -1, - mtime: -1, - size: -1 - }; - } - - private getFileType(stat: StatLike) { - return stat.isDirectory() - ? FileType.Directory - : stat.isFile() - ? FileType.File - : stat.isSymbolicLink() - ? FileType.SymbolicLink - : FileType.Unknown; - } - - async readDirectory(uri: string): Promise> { - const path = urlToPath(uri); - - if (!path) { - return []; - } - - const files = await this.promisifyReaddir(path, { - withFileTypes: true - }); - - return files.map((file) => [file.name, this.getFileType(file)]); - } -} diff --git a/packages/language-server/src/plugins/css/StyleAttributeDocument.ts b/packages/language-server/src/plugins/css/StyleAttributeDocument.ts deleted file mode 100644 index fca96ce8f..000000000 --- a/packages/language-server/src/plugins/css/StyleAttributeDocument.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Stylesheet } from 'vscode-css-languageservice'; -import { Position } from 'vscode-languageserver'; -import { CSSLanguageServices, getLanguageService } from './service'; -import { Document, DocumentMapper, ReadableDocument } from '../../lib/documents'; - -const PREFIX = '__ {'; -const SUFFIX = '}'; - -export class StyleAttributeDocument extends ReadableDocument implements DocumentMapper { - readonly version = this.parent.version; - - public stylesheet: Stylesheet; - public languageId = 'css'; - - constructor( - private readonly parent: Document, - private readonly attrStart: number, - private readonly attrEnd: number, - languageServices: CSSLanguageServices - ) { - super(); - - this.stylesheet = getLanguageService(languageServices).parseStylesheet(this); - } - - /** - * Get the fragment position relative to the parent - * @param pos Position in fragment - */ - getOriginalPosition(pos: Position): Position { - const parentOffset = this.attrStart + this.offsetAt(pos) - PREFIX.length; - return this.parent.positionAt(parentOffset); - } - - /** - * Get the position relative to the start of the fragment - * @param pos Position in parent - */ - getGeneratedPosition(pos: Position): Position { - const fragmentOffset = this.parent.offsetAt(pos) - this.attrStart + PREFIX.length; - return this.positionAt(fragmentOffset); - } - - /** - * Returns true if the given parent position is inside of this fragment - * @param pos Position in parent - */ - isInGenerated(pos: Position): boolean { - const offset = this.parent.offsetAt(pos); - return offset >= this.attrStart && offset <= this.attrEnd; - } - - /** - * Get the fragment text from the parent - */ - getText(): string { - return PREFIX + this.parent.getText().slice(this.attrStart, this.attrEnd) + SUFFIX; - } - - /** - * Returns the length of the fragment as calculated from the start and end position - */ - getTextLength(): number { - return PREFIX.length + this.attrEnd - this.attrStart + SUFFIX.length; - } - - /** - * Return the parent file path - */ - getFilePath(): string | null { - return this.parent.getFilePath(); - } - - getURL() { - return this.parent.getURL(); - } -} diff --git a/packages/language-server/src/plugins/css/features/getIdClassCompletion.ts b/packages/language-server/src/plugins/css/features/getIdClassCompletion.ts deleted file mode 100644 index 022fd3bf7..000000000 --- a/packages/language-server/src/plugins/css/features/getIdClassCompletion.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { CompletionItem, CompletionItemKind, CompletionList } from 'vscode-languageserver'; -import { AttributeContext } from '../../../lib/documents/parseHtml'; -import { CSSDocument } from '../CSSDocument'; - -export function getIdClassCompletion( - cssDoc: CSSDocument, - attributeContext: AttributeContext -): CompletionList | null { - const collectingType = getCollectingType(attributeContext); - - if (!collectingType) { - return null; - } - const items = collectSelectors(cssDoc.stylesheet as CSSNode, collectingType); - - return CompletionList.create(items); -} - -function getCollectingType(attributeContext: AttributeContext): number | undefined { - if (attributeContext.inValue) { - if (attributeContext.name === 'class') { - return NodeType.ClassSelector; - } - if (attributeContext.name === 'id') { - return NodeType.IdentifierSelector; - } - } else if (attributeContext.name.startsWith('class:')) { - return NodeType.ClassSelector; - } -} - -/** - * incomplete see - * https://github.com/microsoft/vscode-css-languageservice/blob/master/src/parser/cssNodes.ts#L14 - * The enum is not exported. we have to update this whenever it changes - */ -export enum NodeType { - ClassSelector = 14, - IdentifierSelector = 15 -} - -export type CSSNode = { - type: number; - children: CSSNode[] | undefined; - getText(): string; -}; - -export function collectSelectors(stylesheet: CSSNode, type: number) { - const result: CSSNode[] = []; - walk(stylesheet, (node) => { - if (node.type === type) { - result.push(node); - } - }); - - return result.map( - (node): CompletionItem => ({ - label: node.getText().substring(1), - kind: CompletionItemKind.Keyword - }) - ); -} - -function walk(node: CSSNode, callback: (node: CSSNode) => void) { - callback(node); - if (node.children) { - node.children.forEach((node) => walk(node, callback)); - } -} diff --git a/packages/language-server/src/plugins/css/features/svelte-selectors.ts b/packages/language-server/src/plugins/css/features/svelte-selectors.ts deleted file mode 100644 index f27a10a0d..000000000 --- a/packages/language-server/src/plugins/css/features/svelte-selectors.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IPseudoClassData } from 'vscode-css-languageservice'; - -export const pesudoClass: IPseudoClassData[] = [ - { - name: ':global()', - description: `[svelte] :global modifier - -Applying styles to a selector globally`, - references: [ - { - name: 'Svelte.dev Reference', - url: 'https://svelte.dev/docs#style' - } - ] - } -]; diff --git a/packages/language-server/src/plugins/css/global-vars.ts b/packages/language-server/src/plugins/css/global-vars.ts deleted file mode 100644 index 2495b13d2..000000000 --- a/packages/language-server/src/plugins/css/global-vars.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { watch, FSWatcher } from 'chokidar'; -import { readFile } from 'fs'; -import { isNotNullOrUndefined, flatten } from '../../utils'; - -const varRegex = /^\s*(--\w+.*?):\s*?([^;]*)/; - -export interface GlobalVar { - name: string; - filename: string; - value: string; -} - -export class GlobalVars { - private fsWatcher?: FSWatcher; - private globalVars = new Map(); - - watchFiles(filesToWatch: string): void { - if (!filesToWatch) { - return; - } - - if (this.fsWatcher) { - this.fsWatcher.close(); - this.globalVars.clear(); - } - - this.fsWatcher = watch(filesToWatch.split(',')) - .addListener('add', (file) => this.updateForFile(file)) - .addListener('change', (file) => { - this.updateForFile(file); - }) - .addListener('unlink', (file) => this.globalVars.delete(file)); - } - - private updateForFile(filename: string) { - // Inside a small timeout because it seems chikidar is "too fast" - // and reading the file will then return empty content - setTimeout(() => { - readFile(filename, 'utf-8', (error, contents) => { - if (error) { - return; - } - - const globalVarsForFile = contents - .split('\n') - .map((line) => line.match(varRegex)) - .filter(isNotNullOrUndefined) - .map((line) => ({ filename, name: line[1], value: line[2] })); - this.globalVars.set(filename, globalVarsForFile); - }); - }, 1000); - } - - getGlobalVars(): GlobalVar[] { - return flatten([...this.globalVars.values()]); - } -} diff --git a/packages/language-server/src/plugins/css/service.ts b/packages/language-server/src/plugins/css/service.ts deleted file mode 100644 index 365beac50..000000000 --- a/packages/language-server/src/plugins/css/service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - getCSSLanguageService, - getSCSSLanguageService, - getLESSLanguageService, - LanguageService, - ICSSDataProvider, - LanguageServiceOptions -} from 'vscode-css-languageservice'; -import { pesudoClass } from './features/svelte-selectors'; - -const customDataProvider: ICSSDataProvider = { - providePseudoClasses() { - return pesudoClass; - }, - provideProperties() { - return [ - { - name: 'vector-effect', - values: [{ name: 'non-scaling-stroke' }, { name: 'none' }], - status: 'experimental' - }, - { - name: 'print-color-adjust', - values: [{ name: 'economy' }, { name: 'exact' }], - status: 'experimental' - } - ]; - }, - provideAtDirectives() { - return []; - }, - providePseudoElements() { - return []; - } -}; - -export function getLanguage(kind?: string) { - switch (kind) { - case 'scss': - case 'text/scss': - return 'scss' as const; - case 'less': - case 'text/less': - return 'less' as const; - case 'css': - case 'text/css': - default: - return 'css' as const; - } -} - -export type CSSLanguageServices = Record<'css' | 'less' | 'scss', LanguageService>; - -export function getLanguageService(langs: CSSLanguageServices, kind?: string): LanguageService { - const lang = getLanguage(kind); - return langs[lang]; -} - -export function createLanguageServices(options?: LanguageServiceOptions): CSSLanguageServices { - const [css, less, scss] = [ - getCSSLanguageService, - getLESSLanguageService, - getSCSSLanguageService - ].map((getService) => - getService({ - customDataProviders: [customDataProvider], - ...(options ?? {}) - }) - ); - - return { - css, - less, - scss - }; -} diff --git a/packages/language-server/src/plugins/documentContext.ts b/packages/language-server/src/plugins/documentContext.ts deleted file mode 100644 index b6c279b37..000000000 --- a/packages/language-server/src/plugins/documentContext.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// adopted from https://github.com/microsoft/vscode/blob/5ffcfde11d8b1b57634627f5094907789db09776/extensions/css-language-features/server/src/utils/documentContext.ts - -import { DocumentContext } from 'vscode-css-languageservice'; -import { WorkspaceFolder } from 'vscode-languageserver'; -import { Utils, URI } from 'vscode-uri'; - -export function getDocumentContext( - documentUri: string, - workspaceFolders: WorkspaceFolder[] -): DocumentContext { - function getRootFolder(): string | undefined { - for (const folder of workspaceFolders) { - let folderURI = folder.uri; - if (!folderURI.endsWith('/')) { - folderURI = folderURI + '/'; - } - if (documentUri.startsWith(folderURI)) { - return folderURI; - } - } - return undefined; - } - - return { - resolveReference: (ref: string, base = documentUri) => { - if (ref[0] === '/') { - // resolve absolute path against the current workspace folder - const folderUri = getRootFolder(); - if (folderUri) { - return folderUri + ref.substr(1); - } - } - base = base.substr(0, base.lastIndexOf('/') + 1); - return Utils.resolvePath(URI.parse(base), ref).toString(); - } - }; -} diff --git a/packages/language-server/src/plugins/html/HTMLPlugin.ts b/packages/language-server/src/plugins/html/HTMLPlugin.ts deleted file mode 100644 index 3abffaa5f..000000000 --- a/packages/language-server/src/plugins/html/HTMLPlugin.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { doComplete as doEmmetComplete } from '@vscode/emmet-helper'; -import { - getLanguageService, - HTMLDocument, - CompletionItem as HtmlCompletionItem, - Node, - newHTMLDataProvider -} from 'vscode-html-languageservice'; -import { - CompletionList, - Hover, - Position, - SymbolInformation, - CompletionItem, - CompletionItemKind, - TextEdit, - Range, - WorkspaceEdit, - LinkedEditingRanges, - CompletionContext, - FoldingRange -} from 'vscode-languageserver'; -import { - DocumentManager, - Document, - isInTag, - getNodeIfIsInComponentStartTag -} from '../../lib/documents'; -import { LSConfigManager, LSHTMLConfig } from '../../ls-config'; -import { svelteHtmlDataProvider } from './dataProvider'; -import { - HoverProvider, - CompletionsProvider, - RenameProvider, - LinkedEditingRangesProvider, - FoldingRangeProvider -} from '../interfaces'; -import { isInsideMoustacheTag, toRange } from '../../lib/documents/utils'; -import { isNotNullOrUndefined, possiblyComponent } from '../../utils'; -import { importPrettier } from '../../importPackage'; -import path from 'path'; -import { Logger } from '../../logger'; -import { indentBasedFoldingRangeForTag } from '../../lib/foldingRange/indentFolding'; - -export class HTMLPlugin - implements - HoverProvider, - CompletionsProvider, - RenameProvider, - LinkedEditingRangesProvider, - FoldingRangeProvider -{ - __name = 'html'; - private lang = getLanguageService({ - customDataProviders: this.getCustomDataProviders(), - useDefaultDataProvider: false, - clientCapabilities: this.configManager.getClientCapabilities() - }); - private documents = new WeakMap(); - private styleScriptTemplate = new Set(['template', 'style', 'script']); - - private htmlTriggerCharacters = ['.', ':', '<', '"', '=', '/']; - - constructor( - docManager: DocumentManager, - private configManager: LSConfigManager - ) { - configManager.onChange(() => - this.lang.setDataProviders(false, this.getCustomDataProviders()) - ); - docManager.on('documentChange', (document) => { - this.documents.set(document, document.html); - }); - } - - doHover(document: Document, position: Position): Hover | null { - if (!this.featureEnabled('hover')) { - return null; - } - - const html = this.documents.get(document); - if (!html) { - return null; - } - - const node = html.findNodeAt(document.offsetAt(position)); - if (!node || possiblyComponent(node)) { - return null; - } - - return this.lang.doHover(document, position, html); - } - - async getCompletions( - document: Document, - position: Position, - completionContext?: CompletionContext - ): Promise { - if (!this.featureEnabled('completions')) { - return null; - } - - const html = this.documents.get(document); - if (!html) { - return null; - } - - if ( - this.isInsideMoustacheTag(html, document, position) || - isInTag(position, document.scriptInfo) || - isInTag(position, document.moduleScriptInfo) - ) { - return null; - } - - let emmetResults: CompletionList = { - isIncomplete: false, - items: [] - }; - - let doEmmetCompleteInner = (): CompletionList | null | undefined => null; - if ( - this.configManager.getConfig().html.completions.emmet && - this.configManager.getEmmetConfig().showExpandedAbbreviation !== 'never' - ) { - doEmmetCompleteInner = () => - doEmmetComplete(document, position, 'html', this.configManager.getEmmetConfig()); - - this.lang.setCompletionParticipants([ - { - onHtmlContent: () => (emmetResults = doEmmetCompleteInner() || emmetResults) - } - ]); - } - - if ( - completionContext?.triggerCharacter && - !this.htmlTriggerCharacters.includes(completionContext?.triggerCharacter) - ) { - return doEmmetCompleteInner() ?? null; - } - - const results = this.isInComponentTag(html, document, position) - ? // Only allow emmet inside component element tags. - // Other attributes/events would be false positives. - CompletionList.create([]) - : this.lang.doComplete(document, position, html); - const items = this.toCompletionItems(results.items); - const filePath = document.getFilePath(); - - const prettierConfig = - filePath && - items.some((item) => item.label.startsWith('on:') || item.label.startsWith('bind:')) - ? this.configManager.getMergedPrettierConfig( - await importPrettier(filePath).resolveConfig(filePath, { - editorconfig: true - }) - ) - : null; - - const svelteStrictMode = prettierConfig?.svelteStrictMode; - items.forEach((item) => { - const startQuote = svelteStrictMode ? '"{' : '{'; - const endQuote = svelteStrictMode ? '}"' : '}'; - if (!item.textEdit) { - return; - } - - if (item.label.startsWith('on:')) { - item.textEdit = { - ...item.textEdit, - newText: item.textEdit.newText.replace('="$1"', `$2=${startQuote}$1${endQuote}`) - }; - } - - if (item.label.startsWith('bind:')) { - item.textEdit = { - ...item.textEdit, - newText: item.textEdit.newText.replace('="$1"', `=${startQuote}$1${endQuote}`) - }; - } - }); - - return CompletionList.create( - [ - ...this.toCompletionItems(items), - ...this.getLangCompletions(items), - ...emmetResults.items - ], - // Emmet completions change on every keystroke, so they are never complete - emmetResults.items.length > 0 - ); - } - - /** - * The HTML language service uses newer types which clash - * without the stable ones. Transform to the stable types. - */ - private toCompletionItems(items: HtmlCompletionItem[]): CompletionItem[] { - return items.map((item) => { - if (!item.textEdit || TextEdit.is(item.textEdit)) { - return item; - } - return { - ...item, - textEdit: TextEdit.replace(item.textEdit.replace, item.textEdit.newText) - }; - }); - } - - private isInComponentTag(html: HTMLDocument, document: Document, position: Position) { - return !!getNodeIfIsInComponentStartTag(html, document.offsetAt(position)); - } - - private getLangCompletions(completions: CompletionItem[]): CompletionItem[] { - const styleScriptTemplateCompletions = completions.filter( - (completion) => - completion.kind === CompletionItemKind.Property && - this.styleScriptTemplate.has(completion.label) - ); - const langCompletions: CompletionItem[] = []; - addLangCompletion('script', ['ts']); - addLangCompletion('style', ['less', 'scss']); - addLangCompletion('template', ['pug']); - return langCompletions; - - function addLangCompletion(tag: string, languages: string[]) { - const existingCompletion = styleScriptTemplateCompletions.find( - (completion) => completion.label === tag - ); - if (!existingCompletion) { - return; - } - - languages.forEach((lang) => - langCompletions.push({ - ...existingCompletion, - label: `${tag} (lang="${lang}")`, - insertText: - existingCompletion.insertText && - `${existingCompletion.insertText} lang="${lang}"`, - textEdit: - existingCompletion.textEdit && TextEdit.is(existingCompletion.textEdit) - ? { - range: existingCompletion.textEdit.range, - newText: `${existingCompletion.textEdit.newText} lang="${lang}"` - } - : undefined - }) - ); - } - } - - doTagComplete(document: Document, position: Position): string | null { - if (!this.featureEnabled('tagComplete')) { - return null; - } - - const html = this.documents.get(document); - if (!html) { - return null; - } - - if (this.isInsideMoustacheTag(html, document, position)) { - return null; - } - - return this.lang.doTagComplete(document, position, html); - } - - private isInsideMoustacheTag(html: HTMLDocument, document: Document, position: Position) { - const offset = document.offsetAt(position); - const node = html.findNodeAt(offset); - return isInsideMoustacheTag(document.getText(), node.start, offset); - } - - getDocumentSymbols(document: Document): SymbolInformation[] { - if (!this.featureEnabled('documentSymbols')) { - return []; - } - - const html = this.documents.get(document); - if (!html) { - return []; - } - - return this.lang.findDocumentSymbols(document, html); - } - - rename(document: Document, position: Position, newName: string): WorkspaceEdit | null { - const html = this.documents.get(document); - if (!html) { - return null; - } - - const node = html.findNodeAt(document.offsetAt(position)); - if (!node || possiblyComponent(node)) { - return null; - } - - return this.lang.doRename(document, position, newName, html); - } - - prepareRename(document: Document, position: Position): Range | null { - const html = this.documents.get(document); - if (!html) { - return null; - } - - const offset = document.offsetAt(position); - const node = html.findNodeAt(offset); - if (!node || possiblyComponent(node) || !node.tag || !this.isRenameAtTag(node, offset)) { - return null; - } - const tagNameStart = node.start + '<'.length; - - return toRange(document.getText(), tagNameStart, tagNameStart + node.tag.length); - } - - getLinkedEditingRanges(document: Document, position: Position): LinkedEditingRanges | null { - if (!this.featureEnabled('linkedEditing')) { - return null; - } - - const html = this.documents.get(document); - if (!html) { - return null; - } - - const ranges = this.lang.findLinkedEditingRanges(document, position, html); - - if (!ranges) { - return null; - } - - return { ranges }; - } - - getFoldingRanges(document: Document): FoldingRange[] { - const result = this.lang.getFoldingRanges(document); - const templateRange = document.templateInfo - ? indentBasedFoldingRangeForTag(document, document.templateInfo) - : []; - - const ARROW = '=>'; - - if (!document.getText().includes(ARROW)) { - return result.concat(templateRange); - } - - const byEnd = new Map(); - for (const fold of result) { - byEnd.set(fold.endLine, (byEnd.get(fold.endLine) ?? []).concat(fold)); - } - - let startIndex = 0; - while (startIndex < document.getTextLength()) { - const index = document.getText().indexOf(ARROW, startIndex); - startIndex = index + ARROW.length; - - if (index === -1) { - break; - } - const position = document.positionAt(index); - const isInStyleOrScript = - isInTag(position, document.styleInfo) || - isInTag(position, document.scriptInfo) || - isInTag(position, document.moduleScriptInfo); - - if (isInStyleOrScript) { - continue; - } - - const tag = document.html.findNodeAt(index); - - // our version of html document patched it so it's within the start tag - // but not the folding range returned by the language service - // which uses unpatched scanner - if (!tag.startTagEnd || index > tag.startTagEnd) { - continue; - } - - const tagStartPosition = document.positionAt(tag.start); - const range = byEnd - .get(position.line) - ?.find((r) => r.startLine === tagStartPosition.line); - - const newEndLine = document.positionAt(tag.end).line - 1; - if (newEndLine <= tagStartPosition.line) { - continue; - } - - if (range) { - range.endLine = newEndLine; - } else { - result.push({ - startLine: tagStartPosition.line, - endLine: newEndLine - }); - } - } - - return result.concat(templateRange); - } - - /** - * Returns true if rename happens at the tag name, not anywhere inbetween. - */ - private isRenameAtTag(node: Node, offset: number): boolean { - if (!node.tag) { - return false; - } - - const startTagNameEnd = node.start + `<${node.tag}`.length; - const isAtStartTag = offset > node.start && offset <= startTagNameEnd; - const isAtEndTag = - node.endTagStart !== undefined && offset >= node.endTagStart && offset < node.end; - return isAtStartTag || isAtEndTag; - } - - private getCustomDataProviders() { - const providers = - this.configManager - .getHTMLConfig() - ?.customData?.map((customDataPath) => { - try { - const jsonPath = path.resolve(customDataPath); - return newHTMLDataProvider(customDataPath, require(jsonPath)); - } catch (error) { - Logger.error(error); - } - }) - .filter(isNotNullOrUndefined) ?? []; - - return [svelteHtmlDataProvider].concat(providers); - } - - private featureEnabled(feature: keyof LSHTMLConfig) { - return ( - this.configManager.enabled('html.enable') && - this.configManager.enabled(`html.${feature}.enable`) - ); - } -} diff --git a/packages/language-server/src/plugins/html/dataProvider.ts b/packages/language-server/src/plugins/html/dataProvider.ts deleted file mode 100644 index 3031552c4..000000000 --- a/packages/language-server/src/plugins/html/dataProvider.ts +++ /dev/null @@ -1,431 +0,0 @@ -import { IAttributeData, ITagData, newHTMLDataProvider } from 'vscode-html-languageservice'; -import { htmlData } from 'vscode-html-languageservice/lib/umd/languageFacts/data/webCustomData'; -import { unique } from '../../utils'; - -const svelteEvents = [ - ...(htmlData.globalAttributes?.filter(isEvent).map(mapToSvelteEvent) ?? []), - { - name: 'on:introstart', - description: 'Available when element has transition' - }, - { - name: 'on:introend', - description: 'Available when element has transition' - }, - { - name: 'on:outrostart', - description: 'Available when element has transition' - }, - { - name: 'on:outroend', - description: 'Available when element has transition' - }, - // Pointer events - { name: 'on:pointercancel' }, - { name: 'on:pointerdown' }, - { name: 'on:pointerenter' }, - { name: 'on:pointerleave' }, - { name: 'on:pointermove' }, - { name: 'on:pointerout' }, - { name: 'on:pointerover' }, - { name: 'on:pointerup' }, - // Mouse events - { name: 'on:mouseenter' }, - { name: 'on:mouseleave' }, - // Other - { name: 'on:hashchange' }, - { name: 'on:visibilitychange' } -]; -const svelteAttributes: IAttributeData[] = [ - { - name: 'bind:innerHTML', - description: 'Available when contenteditable=true' - }, - { - name: 'bind:textContent', - description: 'Available when contenteditable=true' - }, - { - name: 'bind:innerText', - description: 'Available when contenteditable=true' - }, - { - name: 'bind:clientWidth', - description: 'Available for block level elements. (read-only)' - }, - { - name: 'bind:clientHeight', - description: 'Available for block level elements. (read-only)' - }, - { - name: 'bind:offsetWidth', - description: 'Available for block level elements. (read-only)' - }, - { - name: 'bind:offsetHeight', - description: 'Available for block level elements. (read-only)' - }, - { - name: 'bind:contentRect', - description: 'Available for all elements. (read-only)' - }, - { - name: 'bind:contentBoxSize', - description: 'Available for all elements. (read-only)' - }, - { - name: 'bind:borderBoxSize', - description: 'Available for all elements. (read-only)' - }, - { - name: 'bind:devicePixelContentBoxSize', - description: 'Available for all elements. (read-only)' - }, - { - name: 'bind:this', - description: - 'To get a reference to a DOM node, use bind:this. If used on a component, gets a reference to that component instance.' - } -]; -const sveltekitAttributes: IAttributeData[] = [ - { - name: 'data-sveltekit-keepfocus', - description: - 'SvelteKit-specific attribute. Currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body.', - valueSet: 'v' - }, - { - name: 'data-sveltekit-noscroll', - description: - 'SvelteKit-specific attribute. Will prevent scrolling after the link is clicked.', - valueSet: 'v' - }, - { - name: 'data-sveltekit-preload-code', - description: - "SvelteKit-specific attribute. Will cause SvelteKit to run the page's load function as soon as the user hovers over the link (on a desktop) or touches it (on mobile), rather than waiting for the click event to trigger navigation.", - valueSet: 'v', - values: [ - { name: 'eager' }, - { name: 'viewport' }, - { name: 'hover' }, - { name: 'tap' }, - { name: 'off' } - ] - }, - { - name: 'data-sveltekit-preload-data', - description: - "SvelteKit-specific attribute. Will cause SvelteKit to run the page's load function as soon as the user hovers over the link (on a desktop) or touches it (on mobile), rather than waiting for the click event to trigger navigation.", - valueSet: 'v', - values: [{ name: 'hover' }, { name: 'tap' }, { name: 'off' }] - }, - { - name: 'data-sveltekit-reload', - description: - 'SvelteKit-specific attribute. Will cause SvelteKit to do a normal browser navigation which results in a full page reload.', - valueSet: 'v' - }, - { - name: 'data-sveltekit-replacestate', - description: - 'SvelteKit-specific attribute. Will replace the current `history` entry rather than creating a new one with `pushState` when the link is clicked.', - valueSet: 'v' - } -]; - -const svelteTags: ITagData[] = [ - { - name: 'svelte:self', - description: - 'Allows a component to include itself, recursively.\n\nIt cannot appear at the top level of your markup; it must be inside an if or each block to prevent an infinite loop.', - attributes: [] - }, - { - name: 'svelte:component', - description: - 'Renders a component dynamically, using the component constructor specified as the this property. When the property changes, the component is destroyed and recreated.\n\nIf this is falsy, no component is rendered.', - attributes: [ - { - name: 'this', - description: - 'Component to render.\n\nWhen this property changes, the component is destroyed and recreated.\nIf this is falsy, no component is rendered.' - } - ] - }, - { - name: 'svelte:element', - description: - 'Renders a DOM element dynamically, using the string as the this property. When the property changes, the element is destroyed and recreated.\n\nIf this is falsy, no element is rendered.', - attributes: [ - { - name: 'this', - description: - 'DOM element to render.\n\nWhen this property changes, the element is destroyed and recreated.\nIf this is falsy, no element is rendered.' - } - ] - }, - { - name: 'svelte:window', - description: - 'Allows you to add event listeners to the window object without worrying about removing them when the component is destroyed, or checking for the existence of window when server-side rendering.', - attributes: [ - { - name: 'bind:innerWidth', - description: 'Bind to the inner width of the window. (read-only)' - }, - { - name: 'bind:innerHeight', - description: 'Bind to the inner height of the window. (read-only)' - }, - { - name: 'bind:outerWidth', - description: 'Bind to the outer width of the window. (read-only)' - }, - { - name: 'bind:outerHeight', - description: 'Bind to the outer height of the window. (read-only)' - }, - { - name: 'bind:scrollX', - description: 'Bind to the scroll x position of the window.' - }, - { - name: 'bind:scrollY', - description: 'Bind to the scroll y position of the window.' - }, - { - name: 'bind:online', - description: 'An alias for window.navigator.onLine' - } - ] - }, - { - name: 'svelte:document', - description: - "As with , this element allows you to add listeners to events on document, such as visibilitychange, which don't fire on window.", - attributes: [ - { - name: 'bind:fullscreenElement', - description: - 'Bind to the element that is being in full screen mode in this document. (read-only)' - }, - { - name: 'bind:visibilityState', - description: 'Bind to visibility of the document. (read-only)' - } - ] - }, - { - name: 'svelte:body', - description: - "As with , this element allows you to add listeners to events on document.body, such as mouseenter and mouseleave which don't fire on window.", - attributes: [] - }, - { - name: 'svelte:head', - description: - 'This element makes it possible to insert elements into document.head. During server-side rendering, head content exposed separately to the main html content.', - attributes: [] - }, - { - name: 'svelte:options', - description: 'Provides a place to specify per-component compiler options', - attributes: [ - { - name: 'immutable', - description: - 'If true, tells the compiler that you promise not to mutate any objects. This allows it to be less conservative about checking whether values have changed.', - values: [ - { - name: '{true}', - description: - 'You never use mutable data, so the compiler can do simple referential equality checks to determine if values have changed' - }, - { - name: '{false}', - description: - 'The default. Svelte will be more conservative about whether or not mutable objects have changed' - } - ] - }, - { - name: 'accessors', - description: - "If true, getters and setters will be created for the component's props. If false, they will only be created for readonly exported values (i.e. those declared with const, class and function). If compiling with customElement: true this option defaults to true.", - values: [ - { - name: '{true}', - description: "Adds getters and setters for the component's props" - }, - { - name: '{false}', - description: 'The default.' - } - ] - }, - { - name: 'namespace', - description: 'The namespace where this component will be used, most commonly "svg"' - }, - { - name: 'tag', - description: 'The name to use when compiling this component as a custom element' - } - ] - }, - { - name: 'svelte:fragment', - description: - 'This element is useful if you want to assign a component to a named slot without creating a wrapper DOM element.', - attributes: [ - { - name: 'slot', - description: 'The name of the named slot that should be targeted.' - } - ] - }, - { - name: 'slot', - description: - 'Components can have child content, in the same way that elements can.\n\nThe content is exposed in the child component using the element, which can contain fallback content that is rendered if no children are provided.', - attributes: [ - { - name: 'name', - description: - 'Named slots allow consumers to target specific areas. They can also have fallback content.' - } - ] - } -]; - -const mediaAttributes: IAttributeData[] = [ - { - name: 'bind:duration', - description: 'The total duration of the video, in seconds. (readonly)' - }, - { - name: 'bind:buffered', - description: 'An array of {start, end} objects. (readonly)' - }, - { - name: 'bind:seekable', - description: 'An array of {start, end} objects. (readonly)' - }, - { - name: 'bind:played', - description: 'An array of {start, end} objects. (readonly)' - }, - { - name: 'bind:seeking', - description: 'boolean. (readonly)' - }, - { - name: 'bind:ended', - description: 'boolean. (readonly)' - }, - { - name: 'bind:currentTime', - description: 'The current point in the video, in seconds.' - }, - { - name: 'bind:playbackRate', - description: "how fast or slow to play the video, where 1 is 'normal'" - }, - { - name: 'bind:paused' - }, - { - name: 'bind:volume', - description: 'A value between 0 and 1' - }, - { - name: 'bind:muted' - }, - { - name: 'bind:readyState' - } -]; -const videoAttributes: IAttributeData[] = [ - { - name: 'bind:videoWidth', - description: 'readonly' - }, - { - name: 'bind:videoHeight', - description: 'readonly' - } -]; - -const indeterminateAttribute: IAttributeData = { - name: 'indeterminate', - description: 'Available for type="checkbox"' -}; - -const addAttributes: Record = { - select: [{ name: 'bind:value' }], - input: [ - { name: 'bind:value' }, - { name: 'bind:group', description: 'Available for type="radio" and type="checkbox"' }, - { name: 'bind:checked', description: 'Available for type="checkbox"' }, - { name: 'bind:files', description: 'Available for type="file" (readonly)' }, - indeterminateAttribute, - { ...indeterminateAttribute, name: 'bind:indeterminate' } - ], - img: [{ name: 'bind:naturalWidth' }, { name: 'bind:naturalHeight' }], - textarea: [{ name: 'bind:value' }], - video: [...mediaAttributes, ...videoAttributes], - audio: [...mediaAttributes], - details: [ - { - name: 'bind:open' - } - ], - script: [ - { - name: 'generics', - description: - 'Generics used within the components. Only available when using TypeScript.' - } - ] -}; - -const html5Tags = htmlData.tags!.map((tag) => { - let attributes = tag.attributes.map(mapToSvelteEvent); - if (addAttributes[tag.name]) { - attributes = [...attributes, ...addAttributes[tag.name]]; - } - return { - ...tag, - attributes - }; -}); - -export const svelteHtmlDataProvider = newHTMLDataProvider('svelte-builtin', { - version: 1, - globalAttributes: [ - ...htmlData.globalAttributes!, - ...svelteEvents, - ...svelteAttributes, - ...sveltekitAttributes - ], - tags: [...html5Tags, ...svelteTags], - - // TODO remove this after it's fixed in the html language service - valueSets: - htmlData.valueSets?.map((set) => ({ - name: set.name, - values: unique(set.values) - })) ?? [] -}); - -function isEvent(attr: IAttributeData) { - return attr.name.startsWith('on'); -} - -function mapToSvelteEvent(attr: IAttributeData) { - return { - ...attr, - name: attr.name.replace(/^on/, 'on:') - }; -} diff --git a/packages/language-server/src/plugins/index.ts b/packages/language-server/src/plugins/index.ts deleted file mode 100644 index 9b97b2911..000000000 --- a/packages/language-server/src/plugins/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './css/CSSPlugin'; -export * from './typescript/TypeScriptPlugin'; -export * from './typescript/LSAndTSDocResolver'; -export * from './svelte/SveltePlugin'; -export * from './html/HTMLPlugin'; -export * from './PluginHost'; -export * from './interfaces'; diff --git a/packages/language-server/src/plugins/interfaces.ts b/packages/language-server/src/plugins/interfaces.ts deleted file mode 100644 index cceef9558..000000000 --- a/packages/language-server/src/plugins/interfaces.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { - CancellationToken, - CompletionContext, - FileChangeType, - LinkedEditingRanges, - SemanticTokens, - SignatureHelpContext, - TextDocumentContentChangeEvent -} from 'vscode-languageserver'; -import { - CallHierarchyIncomingCall, - CallHierarchyItem, - CallHierarchyOutgoingCall, - CodeAction, - CodeActionContext, - Color, - ColorInformation, - ColorPresentation, - CompletionItem, - CompletionList, - DefinitionLink, - Diagnostic, - FoldingRange, - FormattingOptions, - Hover, - InlayHint, - Location, - Position, - Range, - ReferenceContext, - SelectionRange, - SignatureHelp, - SymbolInformation, - TextDocumentIdentifier, - TextEdit, - WorkspaceEdit -} from 'vscode-languageserver-types'; -import { Document } from '../lib/documents'; - -export type Resolvable = T | Promise; - -export interface AppCompletionItem extends CompletionItem { - data?: T; -} - -export interface AppCompletionList extends CompletionList { - items: Array>; -} - -export interface DiagnosticsProvider { - getDiagnostics(document: Document): Resolvable; -} - -export interface HoverProvider { - doHover(document: Document, position: Position): Resolvable; -} - -export interface CompletionsProvider { - getCompletions( - document: Document, - position: Position, - completionContext?: CompletionContext, - cancellationToken?: CancellationToken - ): Resolvable | null>; - - resolveCompletion?( - document: Document, - completionItem: AppCompletionItem, - cancellationToken?: CancellationToken - ): Resolvable>; -} - -export interface FormattingProvider { - formatDocument(document: Document, options: FormattingOptions): Resolvable; -} - -export interface TagCompleteProvider { - doTagComplete(document: Document, position: Position): Resolvable; -} - -export interface DocumentColorsProvider { - getDocumentColors(document: Document): Resolvable; -} - -export interface ColorPresentationsProvider { - getColorPresentations( - document: Document, - range: Range, - color: Color - ): Resolvable; -} - -export interface DocumentSymbolsProvider { - getDocumentSymbols( - document: Document, - cancellationToken?: CancellationToken - ): Resolvable; -} - -export interface DefinitionsProvider { - getDefinitions(document: Document, position: Position): Resolvable; -} - -export interface BackwardsCompatibleDefinitionsProvider { - getDefinitions( - document: Document, - position: Position - ): Resolvable; -} - -export interface CodeActionsProvider { - getCodeActions( - document: Document, - range: Range, - context: CodeActionContext, - cancellationToken?: CancellationToken - ): Resolvable; - executeCommand?( - document: Document, - command: string, - args?: any[] - ): Resolvable; - - resolveCodeAction?( - document: Document, - codeAction: CodeAction, - cancellationToken?: CancellationToken - ): Resolvable; -} - -export interface FileRename { - oldUri: string; - newUri: string; -} - -export interface UpdateImportsProvider { - updateImports(fileRename: FileRename): Resolvable; -} - -export interface RenameProvider { - rename( - document: Document, - position: Position, - newName: string - ): Resolvable; - prepareRename(document: Document, position: Position): Resolvable; -} - -export interface FindReferencesProvider { - findReferences( - document: Document, - position: Position, - context: ReferenceContext - ): Promise; -} - -export interface FileReferencesProvider { - fileReferences(uri: string): Promise; -} - -export interface FindComponentReferencesProvider { - findComponentReferences(uri: string): Promise; -} - -export interface SignatureHelpProvider { - getSignatureHelp( - document: Document, - position: Position, - context: SignatureHelpContext | undefined, - cancellationToken?: CancellationToken - ): Resolvable; -} - -export interface SelectionRangeProvider { - getSelectionRange(document: Document, position: Position): Resolvable; -} - -export interface SemanticTokensProvider { - getSemanticTokens(textDocument: Document, range?: Range): Resolvable; -} - -export interface LinkedEditingRangesProvider { - getLinkedEditingRanges( - document: Document, - position: Position - ): Resolvable; -} - -export interface ImplementationProvider { - getImplementation(document: Document, position: Position): Resolvable; -} - -export interface TypeDefinitionProvider { - getTypeDefinition(document: Document, position: Position): Resolvable; -} - -export interface CallHierarchyProvider { - prepareCallHierarchy( - document: Document, - position: Position - ): Resolvable; - - getIncomingCalls( - item: CallHierarchyItem, - cancellationToken?: CancellationToken - ): Resolvable; - - getOutgoingCalls( - item: CallHierarchyItem, - cancellationToken?: CancellationToken - ): Resolvable; -} - -export interface OnWatchFileChangesPara { - fileName: string; - changeType: FileChangeType; -} - -export interface InlayHintProvider { - getInlayHints( - document: Document, - range: Range, - cancellationToken?: CancellationToken - ): Resolvable; -} - -export interface FoldingRangeProvider { - getFoldingRanges(document: Document): Resolvable; -} - -export interface OnWatchFileChanges { - onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void; -} - -export interface UpdateTsOrJsFile { - updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void; -} - -type ProviderBase = DiagnosticsProvider & - HoverProvider & - CompletionsProvider & - FormattingProvider & - TagCompleteProvider & - DocumentColorsProvider & - ColorPresentationsProvider & - DocumentSymbolsProvider & - UpdateImportsProvider & - CodeActionsProvider & - FindReferencesProvider & - FileReferencesProvider & - FindComponentReferencesProvider & - RenameProvider & - SignatureHelpProvider & - SemanticTokensProvider & - LinkedEditingRangesProvider & - ImplementationProvider & - TypeDefinitionProvider & - InlayHintProvider & - CallHierarchyProvider & - FoldingRangeProvider; - -export type LSProvider = ProviderBase & BackwardsCompatibleDefinitionsProvider; - -export interface LSPProviderConfig { - /** - * Whether or not completion lists that are marked as imcomplete - * should be filtered server side. - */ - filterIncompleteCompletions: boolean; - /** - * Whether or not getDefinitions supports the LocationLink interface. - */ - definitionLinkSupport: boolean; -} - -export type Plugin = Partial< - ProviderBase & - DefinitionsProvider & - OnWatchFileChanges & - SelectionRangeProvider & - UpdateTsOrJsFile -> & { __name: string }; diff --git a/packages/language-server/src/plugins/svelte/SvelteDocument.ts b/packages/language-server/src/plugins/svelte/SvelteDocument.ts deleted file mode 100644 index 0f132b502..000000000 --- a/packages/language-server/src/plugins/svelte/SvelteDocument.ts +++ /dev/null @@ -1,467 +0,0 @@ -import { TraceMap } from '@jridgewell/trace-mapping'; -import type { compile } from 'svelte/compiler'; -// @ts-ignore -import { CompileOptions } from 'svelte/types/compiler/interfaces'; -// @ts-ignore -import { PreprocessorGroup, Processed } from 'svelte/types/compiler/preprocess'; -import { Position } from 'vscode-languageserver'; -import { getPackageInfo, importSvelte } from '../../importPackage'; -import { - Document, - DocumentMapper, - extractScriptTags, - extractStyleTag, - FragmentMapper, - IdentityMapper, - isInTag, - offsetAt, - positionAt, - SourceMapDocumentMapper, - TagInformation -} from '../../lib/documents'; -import { SvelteConfig } from '../../lib/documents/configLoader'; -import { getLastPartOfPath, isNotNullOrUndefined } from '../../utils'; - -export type SvelteCompileResult = ReturnType; - -export enum TranspileErrorSource { - Script = 'Script', - Style = 'Style' -} - -type PositionMapper = Pick; - -/** - * Represents a text document that contains a svelte component. - */ -export class SvelteDocument { - private transpiledDoc: ITranspiledSvelteDocument | undefined; - private compileResult: SvelteCompileResult | undefined; - - public script: TagInformation | null; - public moduleScript: TagInformation | null; - public style: TagInformation | null; - public languageId = 'svelte'; - public version = 0; - public uri = this.parent.uri; - public get config() { - return this.parent.configPromise; - } - - constructor(private parent: Document) { - this.script = this.parent.scriptInfo; - this.moduleScript = this.parent.moduleScriptInfo; - this.style = this.parent.styleInfo; - this.version = this.parent.version; - } - - getText() { - return this.parent.getText(); - } - - getFilePath(): string { - return this.parent.getFilePath() || ''; - } - - offsetAt(position: Position): number { - return this.parent.offsetAt(position); - } - - async getTranspiled(): Promise { - if (!this.transpiledDoc) { - const { - version: { major, minor } - } = getPackageInfo('svelte', this.getFilePath()); - - if (major > 3 || (major === 3 && minor >= 32)) { - this.transpiledDoc = await TranspiledSvelteDocument.create( - this.parent, - await this.config - ); - } else { - this.transpiledDoc = await FallbackTranspiledSvelteDocument.create( - this.parent, - (await this.config)?.preprocess - ); - } - } - return this.transpiledDoc; - } - - async getCompiled(): Promise { - if (!this.compileResult) { - this.compileResult = await this.getCompiledWith((await this.config)?.compilerOptions); - } - - return this.compileResult; - } - - async getCompiledWith(options: CompileOptions = {}): Promise { - const svelte = importSvelte(this.getFilePath()); - return svelte.compile((await this.getTranspiled()).getText(), options); - } -} - -export interface ITranspiledSvelteDocument extends PositionMapper { - getText(): string; -} - -export class TranspiledSvelteDocument implements ITranspiledSvelteDocument { - static async create(document: Document, config: SvelteConfig | undefined) { - if (!config?.preprocess) { - return new TranspiledSvelteDocument(document.getText()); - } - - const filename = document.getFilePath() || ''; - const svelte = importSvelte(filename); - const preprocessed = await svelte.preprocess( - document.getText(), - wrapPreprocessors(config?.preprocess), - { - filename - } - ); - - if (preprocessed.code === document.getText()) { - return new TranspiledSvelteDocument(document.getText()); - } - - return new TranspiledSvelteDocument( - preprocessed.code, - preprocessed.map - ? new SourceMapDocumentMapper( - createTraceMap(preprocessed.map), - // The "sources" array only contains the Svelte filename, not its path. - // For getting generated positions, the sourcemap consumer wants an exact match - // of the source filepath. Therefore only pass in the filename here. - getLastPartOfPath(filename) - ) - : undefined - ); - } - - constructor( - private code: string, - private mapper?: SourceMapDocumentMapper - ) {} - - getOriginalPosition(generatedPosition: Position): Position { - return this.mapper?.getOriginalPosition(generatedPosition) || generatedPosition; - } - - getText() { - return this.code; - } - - getGeneratedPosition(originalPosition: Position): Position { - return this.mapper?.getGeneratedPosition(originalPosition) || originalPosition; - } -} - -/** - * Only used when the user has an old Svelte version installed where source map support - * for preprocessors is not built in yet. - * This fallback version does not map correctly when there's both a module and instance script. - * It isn't worth fixing these cases though now that Svelte ships a preprocessor with source maps. - */ -export class FallbackTranspiledSvelteDocument implements ITranspiledSvelteDocument { - static async create( - document: Document, - preprocessors: PreprocessorGroup | PreprocessorGroup[] = [] - ) { - const { transpiled, processedScripts, processedStyles } = await transpile( - document, - preprocessors - ); - const scriptMapper = SvelteFragmentMapper.createScript( - document, - transpiled, - processedScripts - ); - const styleMapper = SvelteFragmentMapper.createStyle(document, transpiled, processedStyles); - - return new FallbackTranspiledSvelteDocument( - document, - transpiled, - scriptMapper, - styleMapper - ); - } - - private fragmentInfos = [this.scriptMapper?.fragmentInfo, this.styleMapper?.fragmentInfo] - .filter(isNotNullOrUndefined) - .sort((i1, i2) => i1.end - i2.end); - - private constructor( - private parent: Document, - private transpiled: string, - public scriptMapper: SvelteFragmentMapper | null, - public styleMapper: SvelteFragmentMapper | null - ) {} - - getOriginalPosition(generatedPosition: Position): Position { - if (this.scriptMapper?.isInTranspiledFragment(generatedPosition)) { - return this.scriptMapper.getOriginalPosition(generatedPosition); - } - if (this.styleMapper?.isInTranspiledFragment(generatedPosition)) { - return this.styleMapper.getOriginalPosition(generatedPosition); - } - - // Position is not in fragments, but we still need to account for - // the length differences of the fragments before the position. - let offset = offsetAt(generatedPosition, this.transpiled); - for (const fragmentInfo of this.fragmentInfos) { - if (offset > fragmentInfo.end) { - offset += fragmentInfo.diff; - } - } - return this.parent.positionAt(offset); - } - - getURL(): string { - return this.parent.getURL(); - } - - getText() { - return this.transpiled; - } - - getGeneratedPosition(originalPosition: Position): Position { - const { styleInfo, scriptInfo } = this.parent; - - if (isInTag(originalPosition, scriptInfo) && this.scriptMapper) { - return this.scriptMapper.getGeneratedPosition(originalPosition); - } - if (isInTag(originalPosition, styleInfo) && this.styleMapper) { - return this.styleMapper.getGeneratedPosition(originalPosition); - } - - // Add length difference of each fragment - let offset = offsetAt(originalPosition, this.parent.getText()); - for (const fragmentInfo of this.fragmentInfos) { - if (offset > fragmentInfo.end) { - offset -= fragmentInfo.diff; - } - } - - return positionAt(offset, this.getText()); - } -} - -export class SvelteFragmentMapper implements PositionMapper { - static createStyle(originalDoc: Document, transpiled: string, processed: Processed[]) { - return SvelteFragmentMapper.create( - originalDoc, - transpiled, - originalDoc.styleInfo, - extractStyleTag(transpiled), - processed - ); - } - - static createScript(originalDoc: Document, transpiled: string, processed: Processed[]) { - const scriptInfo = originalDoc.scriptInfo || originalDoc.moduleScriptInfo; - const maybeScriptTag = extractScriptTags(transpiled); - const maybeScriptTagInfo = - maybeScriptTag && (maybeScriptTag.script || maybeScriptTag.moduleScript); - - return SvelteFragmentMapper.create( - originalDoc, - transpiled, - scriptInfo, - maybeScriptTagInfo || null, - processed - ); - } - - private static create( - originalDoc: Document, - transpiled: string, - originalTagInfo: TagInformation | null, - transpiledTagInfo: TagInformation | null, - processed: Processed[] - ) { - const sourceMapper = - processed.length > 0 - ? SvelteFragmentMapper.createSourceMapper(processed, originalDoc) - : new IdentityMapper(originalDoc.uri); - - if (originalTagInfo && transpiledTagInfo) { - const sourceLength = originalTagInfo.container.end - originalTagInfo.container.start; - const transpiledLength = - transpiledTagInfo.container.end - transpiledTagInfo.container.start; - const diff = sourceLength - transpiledLength; - - return new SvelteFragmentMapper( - { end: transpiledTagInfo.container.end, diff }, - new FragmentMapper(originalDoc.getText(), originalTagInfo, originalDoc.uri), - new FragmentMapper(transpiled, transpiledTagInfo, originalDoc.uri), - sourceMapper - ); - } - - return null; - } - - private static createSourceMapper(processed: Processed[], originalDoc: Document) { - return processed.reduce( - (parent, processedSingle) => - processedSingle?.map - ? new SourceMapDocumentMapper( - createTraceMap(processedSingle.map), - originalDoc.uri, - parent - ) - : new IdentityMapper(originalDoc.uri, parent), - (undefined) - ); - } - - private constructor( - /** - * End offset + length difference to original - */ - public fragmentInfo: { end: number; diff: number }, - /** - * Maps between full original source and fragment within that original. - */ - private originalFragmentMapper: DocumentMapper, - /** - * Maps between full transpiled source and fragment within that transpiled. - */ - private transpiledFragmentMapper: DocumentMapper, - /** - * Maps between original and transpiled, within fragment. - */ - private sourceMapper: DocumentMapper - ) {} - - isInTranspiledFragment(generatedPosition: Position): boolean { - return this.transpiledFragmentMapper.isInGenerated(generatedPosition); - } - - getOriginalPosition(generatedPosition: Position): Position { - // Map the position to be relative to the transpiled fragment - const positionInTranspiledFragment = - this.transpiledFragmentMapper.getGeneratedPosition(generatedPosition); - // Map the position, using the sourcemap, to the original position in the source fragment - const positionInOriginalFragment = this.sourceMapper.getOriginalPosition( - positionInTranspiledFragment - ); - // Map the position to be in the original fragment's parent - return this.originalFragmentMapper.getOriginalPosition(positionInOriginalFragment); - } - - /** - * Reversing `getOriginalPosition` - */ - getGeneratedPosition(originalPosition: Position): Position { - const positionInOriginalFragment = - this.originalFragmentMapper.getGeneratedPosition(originalPosition); - const positionInTranspiledFragment = this.sourceMapper.getGeneratedPosition( - positionInOriginalFragment - ); - return this.transpiledFragmentMapper.getOriginalPosition(positionInTranspiledFragment); - } -} - -/** - * Wrap preprocessors and rethrow on errors with more info on where the error came from. - */ -function wrapPreprocessors(preprocessors: PreprocessorGroup | PreprocessorGroup[] = []) { - preprocessors = Array.isArray(preprocessors) ? preprocessors : [preprocessors]; - return preprocessors.map((preprocessor: any) => { - const wrappedPreprocessor: PreprocessorGroup = { markup: preprocessor.markup }; - - if (preprocessor.script) { - wrappedPreprocessor.script = async (args: any) => { - try { - return await preprocessor.script!(args); - } catch (e: any) { - e.__source = TranspileErrorSource.Script; - throw e; - } - }; - } - - if (preprocessor.style) { - wrappedPreprocessor.style = async (args: any) => { - try { - return await preprocessor.style!(args); - } catch (e: any) { - e.__source = TranspileErrorSource.Style; - throw e; - } - }; - } - - return wrappedPreprocessor; - }); -} - -async function transpile( - document: Document, - preprocessors: PreprocessorGroup | PreprocessorGroup[] = [] -) { - preprocessors = Array.isArray(preprocessors) ? preprocessors : [preprocessors]; - const processedScripts: Processed[] = []; - const processedStyles: Processed[] = []; - - const wrappedPreprocessors = preprocessors.map((preprocessor: any) => { - const wrappedPreprocessor: PreprocessorGroup = { markup: preprocessor.markup }; - - if (preprocessor.script) { - wrappedPreprocessor.script = async (args: any) => { - try { - const res = await preprocessor.script!(args); - if (res && res.map) { - processedScripts.push(res); - } - return res; - } catch (e: any) { - e.__source = TranspileErrorSource.Script; - throw e; - } - }; - } - - if (preprocessor.style) { - wrappedPreprocessor.style = async (args: any) => { - try { - const res = await preprocessor.style!(args); - if (res && res.map) { - processedStyles.push(res); - } - return res; - } catch (e: any) { - e.__source = TranspileErrorSource.Style; - throw e; - } - }; - } - - return wrappedPreprocessor; - }); - - const svelte = importSvelte(document.getFilePath() || ''); - const result = await svelte.preprocess(document.getText(), wrappedPreprocessors, { - filename: document.getFilePath() || '' - }); - const transpiled = result.code || result.toString?.() || ''; - - return { transpiled, processedScripts, processedStyles }; -} - -function createTraceMap(map: any): TraceMap { - return new TraceMap(normalizeMap(map)); - - function normalizeMap(map: any) { - // We don't know what we get, could be a stringified sourcemap, - // or a class which has the required properties on it, or a class - // which we need to call toString() on to get the correct format. - if (typeof map === 'string' || map.version) { - return map; - } - return map.toString(); - } -} diff --git a/packages/language-server/src/plugins/svelte/SveltePlugin.ts b/packages/language-server/src/plugins/svelte/SveltePlugin.ts deleted file mode 100644 index 3aeb39cf5..000000000 --- a/packages/language-server/src/plugins/svelte/SveltePlugin.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { isAbsolute } from 'path'; -import { - CancellationToken, - CodeAction, - CodeActionContext, - CompletionContext, - CompletionList, - Diagnostic, - FormattingOptions, - Hover, - Position, - Range, - SelectionRange, - TextEdit, - WorkspaceEdit -} from 'vscode-languageserver'; -import { Plugin } from 'prettier'; -import { getPackageInfo, importPrettier } from '../../importPackage'; -import { Document } from '../../lib/documents'; -import { Logger } from '../../logger'; -import { LSConfigManager, LSSvelteConfig } from '../../ls-config'; -import { isNotNullOrUndefined } from '../../utils'; -import { - CodeActionsProvider, - CompletionsProvider, - DiagnosticsProvider, - FormattingProvider, - HoverProvider, - SelectionRangeProvider -} from '../interfaces'; -import { executeCommand, getCodeActions } from './features/getCodeActions'; -import { getCompletions } from './features/getCompletions'; -import { getDiagnostics } from './features/getDiagnostics'; -import { getHoverInfo } from './features/getHoverInfo'; -import { getSelectionRange } from './features/getSelectionRanges'; -import { SvelteCompileResult, SvelteDocument } from './SvelteDocument'; - -export class SveltePlugin - implements - DiagnosticsProvider, - FormattingProvider, - CompletionsProvider, - HoverProvider, - CodeActionsProvider, - SelectionRangeProvider -{ - __name = 'svelte'; - private docManager = new Map(); - - constructor(private configManager: LSConfigManager) {} - - async getDiagnostics( - document: Document, - cancellationToken?: CancellationToken - ): Promise { - if (!this.featureEnabled('diagnostics') || !this.configManager.getIsTrusted()) { - return []; - } - - return getDiagnostics( - document, - await this.getSvelteDoc(document), - this.configManager.getConfig().svelte.compilerWarnings, - cancellationToken - ); - } - - async getCompiledResult(document: Document): Promise { - try { - const svelteDoc = await this.getSvelteDoc(document); - // @ts-ignore is 'client' in Svelte 5 - return svelteDoc.getCompiledWith({ generate: 'dom' }); - } catch (error) { - return null; - } - } - - async formatDocument(document: Document, options: FormattingOptions): Promise { - if (!this.featureEnabled('format')) { - return []; - } - - const filePath = document.getFilePath()!; - - /** - * Prettier v2 can't use v3 plugins and vice versa. Therefore, we need to check - * which version of prettier is used in the workspace and import the correct - * version of the Svelte plugin. If user uses Prettier >= 3 and has no Svelte plugin - * then fall back to our built-in versions which are both v2 and compatible with - * each other. - * TODO switch this around at some point to load Prettier v3 by default because it's - * more likely that users have that installed. - */ - const importFittingPrettier = async () => { - const getConfig = async (p: any) => { - // Try resolving the config through prettier and fall back to possible editor config - return this.configManager.getMergedPrettierConfig( - await p.resolveConfig(filePath, { editorconfig: true }), - // Be defensive here because IDEs other than VSCode might not have these settings - options && { - tabWidth: options.tabSize, - useTabs: !options.insertSpaces - } - ); - }; - - const prettier1 = importPrettier(filePath); - const config1 = await getConfig(prettier1); - const resolvedPlugins1 = resolvePlugins(config1.plugins); - const pluginLoaded = await hasSveltePluginLoaded(prettier1, resolvedPlugins1); - if (Number(prettier1.version[0]) >= 3 || pluginLoaded) { - // plugin loaded, or referenced in user config as a plugin, or same version as our fallback version -> ok - return { - prettier: prettier1, - config: config1, - isFallback: false, - resolvedPlugins: resolvedPlugins1 - }; - } - - // User either only has Plugin or incompatible Prettier major version installed or none - // -> load our fallback version - const prettier2 = importPrettier(__dirname); - const config2 = await getConfig(prettier2); - const resolvedPlugins2 = resolvePlugins(config2.plugins); - return { - prettier: prettier2, - config: config2, - isFallback: true, - resolvedPlugins: resolvedPlugins2 - }; - }; - - const { prettier, config, isFallback, resolvedPlugins } = await importFittingPrettier(); - - // If user has prettier-plugin-svelte 1.x, then remove `options` from the sort - // order or else it will throw a config error (`options` was not present back then). - if ( - config?.svelteSortOrder && - getPackageInfo('prettier-plugin-svelte', filePath)?.version.major < 2 - ) { - config.svelteSortOrder = config.svelteSortOrder - .replace('-options', '') - .replace('options-', ''); - } - // If user has prettier-plugin-svelte 3.x, then add `options` from the sort - // order or else it will throw a config error (now required). - if ( - config?.svelteSortOrder && - !config.svelteSortOrder.includes('options') && - config.svelteSortOrder !== 'none' && - getPackageInfo('prettier-plugin-svelte', filePath)?.version.major >= 3 - ) { - config.svelteSortOrder = 'options-' + config.svelteSortOrder; - } - // Take .prettierignore into account - const fileInfo = await prettier.getFileInfo(filePath, { - ignorePath: this.configManager.getPrettierConfig()?.ignorePath ?? '.prettierignore', - // Sapper places stuff within src/node_modules, we want to format that, too - withNodeModules: true - }); - if (fileInfo.ignored) { - Logger.debug('File is ignored, formatting skipped'); - return []; - } - - // Prettier v3 format is async, v2 is not - const formattedCode = await prettier.format(document.getText(), { - ...config, - plugins: Array.from( - new Set([...resolvedPlugins, ...(await getSveltePlugin(resolvedPlugins))]) - ), - parser: 'svelte' as any - }); - - return document.getText() === formattedCode - ? [] - : [ - TextEdit.replace( - Range.create( - document.positionAt(0), - document.positionAt(document.getTextLength()) - ), - formattedCode - ) - ]; - - async function getSveltePlugin(plugins: Array = []) { - // Only provide our version of the svelte plugin if the user doesn't have one in - // the workspace already. If we did it, Prettier would - for some reason - use - // the workspace version for parsing and the extension version for printing, - // which could crash if the contract of the parser output changed. - return !isFallback && (await hasSveltePluginLoaded(prettier, plugins)) - ? [] - : [require.resolve('prettier-plugin-svelte')]; - } - - async function hasSveltePluginLoaded( - p: typeof prettier, - plugins: Array = [] - ) { - if (plugins.some(SveltePlugin.isPrettierPluginSvelte)) return true; - if (Number(p.version[0]) >= 3) return false; // Prettier version 3 has removed the "search plugins" feature - // Prettier v3 getSupportInfo is async, v2 is not - const info = await p.getSupportInfo(); - return info.languages.some((l) => l.name === 'svelte'); - } - - function resolvePlugins(plugins: Array | undefined) { - return (plugins ?? []).map(resolvePlugin).filter(isNotNullOrUndefined); - } - - function resolvePlugin(plugin: string | Plugin) { - // https://github.com/prettier/prettier-vscode/blob/160b0e92d88fa19003dce2745d5ab8c67e886a04/src/ModuleResolver.ts#L373 - if (typeof plugin != 'string' || isAbsolute(plugin) || plugin.startsWith('.')) { - return plugin; - } - - try { - return require.resolve(plugin, { - paths: [filePath] - }); - } catch (error) { - Logger.error(`failed to resolve plugin ${plugin} with error:\n`, error); - } - } - } - - private static isPrettierPluginSvelte(plugin: string | Plugin): boolean { - if (typeof plugin === 'string') { - return plugin.includes('prettier-plugin-svelte'); - } - - return !!plugin?.languages?.find((l) => l.name === 'svelte'); - } - - async getCompletions( - document: Document, - position: Position, - _?: CompletionContext, - cancellationToken?: CancellationToken - ): Promise { - if (!this.featureEnabled('completions')) { - return null; - } - - const svelteDoc = await this.getSvelteDoc(document); - if (cancellationToken?.isCancellationRequested) { - return null; - } - - return getCompletions(document, svelteDoc, position); - } - - async doHover(document: Document, position: Position): Promise { - if (!this.featureEnabled('hover')) { - return null; - } - - return getHoverInfo(document, await this.getSvelteDoc(document), position); - } - - async getCodeActions( - document: Document, - range: Range, - context: CodeActionContext, - cancellationToken?: CancellationToken - ): Promise { - if (!this.featureEnabled('codeActions')) { - return []; - } - - const svelteDoc = await this.getSvelteDoc(document); - - if (cancellationToken?.isCancellationRequested) { - return []; - } - - try { - return getCodeActions(svelteDoc, range, context); - } catch (error) { - return []; - } - } - - async executeCommand( - document: Document, - command: string, - args?: any[] - ): Promise { - if (!this.featureEnabled('codeActions')) { - return null; - } - - const svelteDoc = await this.getSvelteDoc(document); - try { - return executeCommand(svelteDoc, command, args); - } catch (error) { - return null; - } - } - - async getSelectionRange( - document: Document, - position: Position - ): Promise { - if (!this.featureEnabled('selectionRange')) { - return null; - } - - const svelteDoc = await this.getSvelteDoc(document); - - return getSelectionRange(svelteDoc, position); - } - - private featureEnabled(feature: keyof LSSvelteConfig) { - return ( - this.configManager.enabled('svelte.enable') && - this.configManager.enabled(`svelte.${feature}.enable`) - ); - } - - private async getSvelteDoc(document: Document) { - let svelteDoc = this.docManager.get(document); - if (!svelteDoc || svelteDoc.version !== document.version) { - svelteDoc = new SvelteDocument(document); - this.docManager.set(document, svelteDoc); - } - return svelteDoc; - } -} diff --git a/packages/language-server/src/plugins/svelte/features/SvelteTags.ts b/packages/language-server/src/plugins/svelte/features/SvelteTags.ts deleted file mode 100644 index 12d53a1b5..000000000 --- a/packages/language-server/src/plugins/svelte/features/SvelteTags.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { SvelteDocument } from '../SvelteDocument'; - -/** - * Special svelte syntax tags that do template logic. - */ -export type SvelteLogicTag = 'each' | 'if' | 'await' | 'key' | 'snippet'; - -/** - * Special svelte syntax tags. - */ -export type SvelteTag = SvelteLogicTag | 'html' | 'debug' | 'const' | 'render'; - -/** - * For each tag, a documentation in markdown format. - */ -export const documentation = { - await: - `\`{#await ...}\`\\ -Await blocks allow you to branch on the three possible states of a Promise — pending, ` + - `fulfilled or rejected. -#### Usage: -\`{#await expression}...{:then name}...{:catch name}...{/await}\`\\ -\`{#await expression}...{:then name}...{/await}\`\\ -\`{#await expression then name}...{/await}\`\\ -\\ -https://svelte.dev/docs#template-syntax-await -`, - each: `\`{#each ...}\`\\ -Iterating over lists of values can be done with an each block. -#### Usage: -\`{#each expression as name}...{/each}\`\\ -\`{#each expression as name, index}...{/each}\`\\ -\`{#each expression as name, index (key)}...{/each}\`\\ -\`{#each expression as name}...{:else}...{/each}\`\\ -\\ -https://svelte.dev/docs#template-syntax-each -`, - if: `\`{#if ...}\`\\ -Content that is conditionally rendered can be wrapped in an if block. -#### Usage: -\`{#if expression}...{/if}\`\\ -\`{#if expression}...{:else if expression}...{/if}\`\\ -\`{#if expression}...{:else}...{/if}\`\\ -\\ -https://svelte.dev/docs#template-syntax-if -`, - key: `\`{#key expression}...{/key}\`\\ -Key blocks destroy and recreate their contents when the value of an expression changes.\\ -This is useful if you want an element to play its transition whenever a value changes.\\ -When used around components, this will cause them to be reinstantiated and reinitialised. -#### Usage: -\`{#key expression}...{/key}\`\\ -\\ -https://svelte.dev/docs#template-syntax-key -`, - snippet: `\`{#snippet identifier(parameter)}...{/snippet}\`\\ -Snippets allow you to create reusable UI blocks you can render with the {@render ...} tag. -They also function as slot props for components. -`, - render: `\`{@render ...}\`\\ -Renders a snippet with the given parameters. -`, - html: - `\`{@html ...}\`\\ -In a text expression, characters like < and > are escaped; however, ` + - `with HTML expressions, they're not. -The expression should be valid standalone HTML. -#### Caution -Svelte does not sanitize expressions before injecting HTML. -If the data comes from an untrusted source, you must sanitize it, ` + - `or you are exposing your users to an XSS vulnerability. -#### Usage: -\`{@html expression}\`\\ -\\ -https://svelte.dev/docs#template-syntax-html -`, - debug: - `\`{@debug ...}\`\\ -Offers an alternative to \`console.log(...)\`. -It logs the values of specific variables whenever they change, ` + - `and pauses code execution if you have devtools open. -It accepts a comma-separated list of variable names (not arbitrary expressions). -#### Usage: -\`{@debug}\` -\`{@debug var1, var2, ..., varN}\`\\ -\\ -https://svelte.dev/docs#template-syntax-debug -`, - const: `\`{@const ...}\`\\ -Defines a local constant}\\ -#### Usage: -\`{@const a = b + c}\`\\ -\\ -https://svelte.dev/docs/special-tags#const -` -}; - -/** - * Get the last tag that is opened but not closed. - */ -export function getLatestOpeningTag( - svelteDoc: SvelteDocument, - offset: number -): SvelteLogicTag | null { - // Only use content up to the position and strip out html comments - const content = svelteDoc - .getText() - .substring(0, offset) - .replace(//g, ''); - const lastIdxs = [ - idxOfLastOpeningTag(content, 'each'), - idxOfLastOpeningTag(content, 'if'), - idxOfLastOpeningTag(content, 'await'), - idxOfLastOpeningTag(content, 'key'), - idxOfLastOpeningTag(content, 'snippet') - ]; - const lastIdx = lastIdxs.sort((i1, i2) => i2.lastIdx - i1.lastIdx); - return lastIdx[0].lastIdx === -1 ? null : lastIdx[0].tag; -} - -/** - * Get the last tag and its index that is opened but not closed. - */ -function idxOfLastOpeningTag(content: string, tag: SvelteLogicTag) { - const nrOfEndingTags = content.match(new RegExp(`{\\s*/${tag}`, 'g'))?.length ?? 0; - - let lastIdx = -1; - let nrOfOpeningTags = 0; - let match: RegExpExecArray | null; - const regexp = new RegExp(`{\\s*#${tag}`, 'g'); - while ((match = regexp.exec(content)) != null) { - nrOfOpeningTags += 1; - lastIdx = match.index; - } - - return { lastIdx: nrOfOpeningTags <= nrOfEndingTags ? -1 : lastIdx, tag }; -} diff --git a/packages/language-server/src/plugins/svelte/features/getCodeActions/getQuickfixes.ts b/packages/language-server/src/plugins/svelte/features/getCodeActions/getQuickfixes.ts deleted file mode 100644 index 917325c88..000000000 --- a/packages/language-server/src/plugins/svelte/features/getCodeActions/getQuickfixes.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { walk } from 'estree-walker'; -import { EOL } from 'os'; -// @ts-ignore -import { TemplateNode } from 'svelte/types/compiler/interfaces'; -import { - CodeAction, - CodeActionKind, - Diagnostic, - DiagnosticSeverity, - OptionalVersionedTextDocumentIdentifier, - Position, - TextDocumentEdit, - TextEdit -} from 'vscode-languageserver'; -import { - getLineOffsets, - mapObjWithRangeToOriginal, - offsetAt, - positionAt -} from '../../../../lib/documents'; -import { getIndent, pathToUrl } from '../../../../utils'; -import { ITranspiledSvelteDocument, SvelteDocument } from '../../SvelteDocument'; -import ts from 'typescript'; -// estree does not have start/end in their public Node interface, -// but the AST returned by svelte/compiler does. Type as any as a workaround. -type Node = any; - -/** - * Get applicable quick fixes. - */ -export async function getQuickfixActions( - svelteDoc: SvelteDocument, - svelteDiagnostics: Diagnostic[] -): Promise { - const textDocument = OptionalVersionedTextDocumentIdentifier.create( - pathToUrl(svelteDoc.getFilePath()), - null - ); - - const { ast } = await svelteDoc.getCompiled(); - const transpiled = await svelteDoc.getTranspiled(); - const content = transpiled.getText(); - const lineOffsets = getLineOffsets(content); - const { html } = ast; - - const codeActions: CodeAction[] = []; - - for (const diagnostic of svelteDiagnostics) { - codeActions.push( - ...(await createQuickfixActions( - textDocument, - transpiled, - content, - lineOffsets, - html, - diagnostic - )) - ); - } - - return codeActions; -} - -async function createQuickfixActions( - textDocument: OptionalVersionedTextDocumentIdentifier, - transpiled: ITranspiledSvelteDocument, - content: string, - lineOffsets: number[], - html: TemplateNode, - diagnostic: Diagnostic -): Promise { - const { - range: { start, end } - } = diagnostic; - const generatedStart = transpiled.getGeneratedPosition(start); - const generatedEnd = transpiled.getGeneratedPosition(end); - const diagnosticStartOffset = offsetAt(generatedStart, content, lineOffsets); - const diagnosticEndOffset = offsetAt(generatedEnd, content, lineOffsets); - const offsetRange: ts.TextRange = { - pos: diagnosticStartOffset, - end: diagnosticEndOffset - }; - const node = findTagForRange(html, offsetRange); - - const codeActions: CodeAction[] = []; - - if (diagnostic.code == 'security-anchor-rel-noreferrer') { - codeActions.push( - createSvelteAnchorMissingAttributeQuickfixAction( - textDocument, - transpiled, - content, - lineOffsets, - node - ) - ); - } - - codeActions.push( - createSvelteIgnoreQuickfixAction( - textDocument, - transpiled, - content, - lineOffsets, - node, - diagnostic - ) - ); - - return codeActions; -} -function createSvelteAnchorMissingAttributeQuickfixAction( - textDocument: OptionalVersionedTextDocumentIdentifier, - transpiled: ITranspiledSvelteDocument, - content: string, - lineOffsets: number[], - node: Node -): CodeAction { - // Assert non-null because the node target attribute is required for 'security-anchor-rel-noreferrer' - const targetAttribute = node.attributes.find((i: any) => i.name == 'target')!; - const relAttribute = node.attributes.find((i: any) => i.name == 'rel'); - - const codeActionTextEdit = relAttribute - ? TextEdit.insert(positionAt(relAttribute.end - 1, content, lineOffsets), ' noreferrer') - : TextEdit.insert( - positionAt(targetAttribute.end, content, lineOffsets), - ' rel="noreferrer"' - ); - - return CodeAction.create( - '(svelte) Add missing attribute rel="noreferrer"', - { - documentChanges: [ - TextDocumentEdit.create(textDocument, [ - mapObjWithRangeToOriginal(transpiled, codeActionTextEdit) - ]) - ] - }, - CodeActionKind.QuickFix - ); -} - -function createSvelteIgnoreQuickfixAction( - textDocument: OptionalVersionedTextDocumentIdentifier, - transpiled: ITranspiledSvelteDocument, - content: string, - lineOffsets: number[], - node: Node, - diagnostic: Diagnostic -): CodeAction { - return CodeAction.create( - getCodeActionTitle(diagnostic), - { - documentChanges: [ - TextDocumentEdit.create(textDocument, [ - getSvelteIgnoreEdit(transpiled, content, lineOffsets, node, diagnostic) - ]) - ] - }, - CodeActionKind.QuickFix - ); -} - -function getCodeActionTitle(diagnostic: Diagnostic) { - // make it distinguishable with eslint's code action - return `(svelte) Disable ${diagnostic.code} for this line`; -} - -/** - * Whether or not the given diagnostic can be ignored via a - * - */ -export function isIgnorableSvelteDiagnostic(diagnostic: Diagnostic) { - const { source, severity, code } = diagnostic; - return ( - code && - !nonIgnorableWarnings.includes(code) && - source === 'svelte' && - severity !== DiagnosticSeverity.Error - ); -} -const nonIgnorableWarnings = [ - 'missing-custom-element-compile-options', - 'unused-export-let', - 'css-unused-selector' -]; - -function getSvelteIgnoreEdit( - transpiled: ITranspiledSvelteDocument, - content: string, - lineOffsets: number[], - node: Node, - diagnostic: Diagnostic -) { - const { code } = diagnostic; - - const nodeStartPosition = positionAt(node.start, content, lineOffsets); - const nodeLineStart = offsetAt( - { - line: nodeStartPosition.line, - character: 0 - }, - content, - lineOffsets - ); - const afterStartLineStart = content.slice(nodeLineStart); - const indent = getIndent(afterStartLineStart); - - // TODO: Make all code action's new line consistent - const ignore = `${indent}${EOL}`; - const position = Position.create(nodeStartPosition.line, 0); - - return mapObjWithRangeToOriginal(transpiled, TextEdit.insert(position, ignore)); -} - -const elementOrComponent = ['Component', 'Element', 'InlineComponent']; - -function findTagForRange(html: Node, range: ts.TextRange) { - let nearest = html; - - walk(html, { - enter(node, parent) { - const { type } = node; - const isBlock = 'block' in node || node.type.toLowerCase().includes('block'); - const isFragment = type === 'Fragment'; - const keepLooking = isFragment || elementOrComponent.includes(type) || isBlock; - if (!keepLooking) { - this.skip(); - return; - } - - if (within(node, range) && parent === nearest) { - nearest = node; - } - } - }); - - return nearest; -} - -function within(node: Node, range: ts.TextRange) { - return node.end >= range.end && node.start <= range.pos; -} diff --git a/packages/language-server/src/plugins/svelte/features/getCodeActions/getRefactorings.ts b/packages/language-server/src/plugins/svelte/features/getCodeActions/getRefactorings.ts deleted file mode 100644 index 23bd11890..000000000 --- a/packages/language-server/src/plugins/svelte/features/getCodeActions/getRefactorings.ts +++ /dev/null @@ -1,162 +0,0 @@ -import * as path from 'path'; -import { - CreateFile, - OptionalVersionedTextDocumentIdentifier, - Position, - Range, - TextDocumentEdit, - TextEdit, - WorkspaceEdit -} from 'vscode-languageserver'; -import { isRangeInTag, TagInformation, updateRelativeImport } from '../../../../lib/documents'; -import { pathToUrl } from '../../../../utils'; -import { SvelteDocument } from '../../SvelteDocument'; - -export interface ExtractComponentArgs { - uri: string; - range: Range; - filePath: string; -} - -export const extractComponentCommand = 'extract_to_svelte_component'; - -export async function executeRefactoringCommand( - svelteDoc: SvelteDocument, - command: string, - args?: any[] -): Promise { - if (command === extractComponentCommand && args) { - return executeExtractComponentCommand(svelteDoc, args[1]); - } - - return null; -} - -async function executeExtractComponentCommand( - svelteDoc: SvelteDocument, - refactorArgs: ExtractComponentArgs -): Promise { - const { range } = refactorArgs; - - if (isInvalidSelectionRange()) { - return 'Invalid selection range'; - } - - let filePath = refactorArgs.filePath || './NewComponent.svelte'; - if (!filePath.endsWith('.svelte')) { - filePath += '.svelte'; - } - if (!filePath.startsWith('.')) { - filePath = './' + filePath; - } - const componentName = filePath.split('/').pop()?.split('.svelte')[0] || ''; - const newFileUri = pathToUrl(path.join(path.dirname(svelteDoc.getFilePath()), filePath)); - - return { - documentChanges: [ - TextDocumentEdit.create( - OptionalVersionedTextDocumentIdentifier.create(svelteDoc.uri, null), - [ - TextEdit.replace(range, `<${componentName}>`), - createComponentImportTextEdit() - ] - ), - CreateFile.create(newFileUri, { overwrite: true }), - createNewFileEdit() - ] - }; - - function isInvalidSelectionRange() { - const text = svelteDoc.getText(); - const offsetStart = svelteDoc.offsetAt(range.start); - const offsetEnd = svelteDoc.offsetAt(range.end); - const validStart = offsetStart === 0 || /[\s\W]/.test(text[offsetStart - 1]); - const validEnd = offsetEnd === text.length - 1 || /[\s\W]/.test(text[offsetEnd]); - return ( - !validStart || - !validEnd || - isRangeInTag(range, svelteDoc.style) || - isRangeInTag(range, svelteDoc.script) || - isRangeInTag(range, svelteDoc.moduleScript) - ); - } - - function createNewFileEdit() { - const text = svelteDoc.getText(); - const newText = [ - getTemplate(), - getTag(svelteDoc.script, false), - getTag(svelteDoc.moduleScript, false), - getTag(svelteDoc.style, true) - ] - .filter((tag) => tag.start >= 0) - .sort((a, b) => a.start - b.start) - .map((tag) => tag.text) - .join(''); - - return TextDocumentEdit.create( - OptionalVersionedTextDocumentIdentifier.create(newFileUri, null), - [TextEdit.insert(Position.create(0, 0), newText)] - ); - - function getTemplate() { - const startOffset = svelteDoc.offsetAt(range.start); - return { - text: text.substring(startOffset, svelteDoc.offsetAt(range.end)) + '\n\n', - start: startOffset - }; - } - - function getTag(tag: TagInformation | null, isStyleTag: boolean) { - if (!tag) { - return { text: '', start: -1 }; - } - - const tagText = updateRelativeImports( - svelteDoc, - text.substring(tag.container.start, tag.container.end), - filePath, - isStyleTag - ); - return { - text: `${tagText}\n\n`, - start: tag.container.start - }; - } - } - - function createComponentImportTextEdit(): TextEdit { - const startPos = (svelteDoc.script || svelteDoc.moduleScript)?.startPos; - const importText = `\n import ${componentName} from '${filePath}';\n`; - return TextEdit.insert( - startPos || Position.create(0, 0), - startPos ? importText : `` - ); - } -} - -// `import {...} from '..'` or `import ... from '..'` -const scriptRelativeImportRegex = - /import\s+{[^}]*}.*['"`](((\.\/)|(\.\.\/)).*?)['"`]|import\s+\w+\s+from\s+['"`](((\.\/)|(\.\.\/)).*?)['"`]/g; -// `@import '..'` -const styleRelativeImportRege = /@import\s+['"`](((\.\/)|(\.\.\/)).*?)['"`]/g; - -function updateRelativeImports( - svelteDoc: SvelteDocument, - tagText: string, - newComponentRelativePath: string, - isStyleTag: boolean -) { - const oldPath = path.dirname(svelteDoc.getFilePath()); - const newPath = path.dirname(path.join(oldPath, newComponentRelativePath)); - const regex = isStyleTag ? styleRelativeImportRege : scriptRelativeImportRegex; - let match = regex.exec(tagText); - while (match) { - // match[1]: match before | and style regex. match[5]: match after | (script regex) - const importPath = match[1] || match[5]; - const newImportPath = updateRelativeImport(oldPath, newPath, importPath); - tagText = tagText.replace(importPath, newImportPath); - match = regex.exec(tagText); - } - return tagText; -} diff --git a/packages/language-server/src/plugins/svelte/features/getCodeActions/index.ts b/packages/language-server/src/plugins/svelte/features/getCodeActions/index.ts deleted file mode 100644 index da5953c51..000000000 --- a/packages/language-server/src/plugins/svelte/features/getCodeActions/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - CodeAction, - CodeActionContext, - CodeActionKind, - Range, - WorkspaceEdit -} from 'vscode-languageserver'; -import { SvelteDocument } from '../../SvelteDocument'; -import { getQuickfixActions, isIgnorableSvelteDiagnostic } from './getQuickfixes'; -import { executeRefactoringCommand } from './getRefactorings'; - -export async function getCodeActions( - svelteDoc: SvelteDocument, - range: Range, - context: CodeActionContext -): Promise { - const svelteDiagnostics = context.diagnostics.filter(isIgnorableSvelteDiagnostic); - if ( - svelteDiagnostics.length && - (!context.only || context.only.includes(CodeActionKind.QuickFix)) - ) { - return await getQuickfixActions(svelteDoc, svelteDiagnostics); - } - - return []; -} - -export async function executeCommand( - svelteDoc: SvelteDocument, - command: string, - args?: any[] -): Promise { - return await executeRefactoringCommand(svelteDoc, command, args); -} diff --git a/packages/language-server/src/plugins/svelte/features/getCompletions.ts b/packages/language-server/src/plugins/svelte/features/getCompletions.ts deleted file mode 100644 index eefe5c3f7..000000000 --- a/packages/language-server/src/plugins/svelte/features/getCompletions.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { EOL } from 'os'; -import { SvelteDocument } from '../SvelteDocument'; -import { - Position, - CompletionList, - CompletionItemKind, - CompletionItem, - InsertTextFormat, - MarkupKind -} from 'vscode-languageserver'; -import { SvelteTag, documentation, getLatestOpeningTag } from './SvelteTags'; -import { isInTag, Document } from '../../../lib/documents'; -import { AttributeContext, getAttributeContextAtPosition } from '../../../lib/documents/parseHtml'; -import { getModifierData } from './getModifierData'; -import { attributeCanHaveEventModifier } from './utils'; - -const HTML_COMMENT_START = ' find out which one - return getLatestOpeningTag(svelteDoc, offset); -} - -function isAroundOffset( - charactersOffset: number, - charactersAroundOffset: string, - toFind: string, - offset: number -) { - const match = charactersAroundOffset.match(toFind); - if (!match || match.index === undefined) { - return false; - } - const idx = match.index + charactersOffset; - return idx <= offset && idx + toFind.length >= offset; -} - -const tagPossibilities: Array<{ tag: SvelteTag | ':else'; values: string[] }> = [ - { tag: 'if' as const, values: ['#if', '/if', ':else if'] }, - // each - { tag: 'each' as const, values: ['#each', '/each'] }, - // await - { tag: 'await' as const, values: ['#await', '/await', ':then', ':catch'] }, - // key - { tag: 'key' as const, values: ['#key', '/key'] }, - // snippet - { tag: 'snippet' as const, values: ['#snippet', '/snippet'] }, - // @ - { tag: 'html' as const, values: ['@html'] }, - { tag: 'debug' as const, values: ['@debug'] }, - { tag: 'const' as const, values: ['@const'] }, - { tag: 'render' as const, values: ['@render'] }, - // this tag has multiple possibilities - { tag: ':else' as const, values: [':else'] } -]; - -const tagRegexp = new RegExp( - `[\\s\\S]*{\\s*(${flatten(tagPossibilities.map((p) => p.values)).join('|')})(\\s|})` -); -function getEventModifierHoverInfo( - attributeContext: AttributeContext, - attributeOffset: number, - offset: number -): Hover | null { - const { name } = attributeContext; - - const modifierData = getModifierData(); - - const found = modifierData.find((modifier) => - isAroundOffset(attributeOffset, name, modifier.modifier, offset) - ); - - if (!found) { - return null; - } - - return { - contents: found.documentation - }; -} diff --git a/packages/language-server/src/plugins/svelte/features/getModifierData.ts b/packages/language-server/src/plugins/svelte/features/getModifierData.ts deleted file mode 100644 index ab077c6b0..000000000 --- a/packages/language-server/src/plugins/svelte/features/getModifierData.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MarkupContent, MarkupKind } from 'vscode-languageserver'; - -export interface ModifierData { - modifier: string; - documentation: MarkupContent; - modifiersInvalidWith?: string[]; -} - -export function getModifierData(): ModifierData[] { - return [ - { - modifier: 'preventDefault', - documentation: 'calls `event.preventDefault()` before running the handler', - modifiersInvalidWith: ['passive'] - }, - { - modifier: 'stopPropagation', - documentation: - 'calls `event.stopPropagation()`, preventing the event reaching the next element' - }, - { - modifier: 'passive', - documentation: - 'improves scrolling performance on touch/wheel events ' + - "(Svelte will add it automatically where it's safe to do so)", - modifiersInvalidWith: ['nopassive', 'preventDefault'] - }, - { - modifier: 'nonpassive', - documentation: 'explicitly set `passive: false`', - modifiersInvalidWith: ['passive'] - }, - { - modifier: 'capture', - documentation: - 'fires the handler during the capture phase instead of the bubbling phase' - }, - { - modifier: 'once', - documentation: 'remove the handler after the first time it runs' - }, - { - modifier: 'self', - documentation: 'only trigger handler if `event.target` is the element itself' - }, - { - modifier: 'trusted', - documentation: - 'only trigger handler if event.isTrusted is true. ' + - 'I.e. if the event is triggered by a user action' - } - ].map((item) => ({ - ...item, - documentation: { - kind: MarkupKind.Markdown, - value: `\`${item.modifier}\` event modifier - -${item.documentation} - -https://svelte.dev/docs#template-syntax-element-directives-on-eventname` - } - })); -} diff --git a/packages/language-server/src/plugins/svelte/features/getSelectionRanges.ts b/packages/language-server/src/plugins/svelte/features/getSelectionRanges.ts deleted file mode 100644 index c9cbdadc5..000000000 --- a/packages/language-server/src/plugins/svelte/features/getSelectionRanges.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { walk } from 'estree-walker'; -import { Position, SelectionRange } from 'vscode-languageserver'; -import { isInTag, mapSelectionRangeToParent, offsetAt, toRange } from '../../../lib/documents'; -import { SvelteDocument } from '../SvelteDocument'; - -// estree does not have start/end in their public Node interface, -// but the AST returned by svelte/compiler does. Type as any as a workaround. -type Node = any; - -type OffsetRange = { - start: number; - end: number; -}; - -export async function getSelectionRange(svelteDoc: SvelteDocument, position: Position) { - const { script, style, moduleScript } = svelteDoc; - const { - ast: { html } - } = await svelteDoc.getCompiled(); - const transpiled = await svelteDoc.getTranspiled(); - const content = transpiled.getText(); - const offset = offsetAt(transpiled.getGeneratedPosition(position), content); - - const embedded = [script, style, moduleScript]; - for (const info of embedded) { - if (isInTag(position, info)) { - // let other plugins do it - return null; - } - } - - let nearest: OffsetRange = html; - let result: SelectionRange | undefined; - - walk(html, { - enter(node: Node, parent: Node) { - if (!parent) { - // keep looking - return; - } - - if (!('start' in node && 'end' in node)) { - this.skip(); - return; - } - - const { start, end } = node; - const isWithin = start <= offset && end >= offset; - - if (!isWithin) { - this.skip(); - return; - } - - if (nearest === parent) { - nearest = node; - result = createSelectionRange(node, result); - } - } - }); - - return result ? mapSelectionRangeToParent(transpiled, result) : null; - - function createSelectionRange(node: OffsetRange, parent?: SelectionRange) { - const range = toRange(content, node.start, node.end); - return SelectionRange.create(range, parent); - } -} diff --git a/packages/language-server/src/plugins/svelte/features/utils.ts b/packages/language-server/src/plugins/svelte/features/utils.ts deleted file mode 100644 index 9a1619cbe..000000000 --- a/packages/language-server/src/plugins/svelte/features/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AttributeContext } from '../../../lib/documents/parseHtml'; -import { possiblyComponent } from '../../../utils'; - -export function attributeCanHaveEventModifier(attributeContext: AttributeContext) { - return ( - !attributeContext.inValue && - !possiblyComponent(attributeContext.elementTag) && - attributeContext.name.startsWith('on:') && - attributeContext.name.includes('|') - ); -} diff --git a/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts b/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts deleted file mode 100644 index a28f89368..000000000 --- a/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts +++ /dev/null @@ -1,113 +0,0 @@ -import ts from 'typescript'; -import { isNotNullOrUndefined } from '../../utils'; -import { findContainingNode } from './features/utils'; - -export type ComponentPartInfo = Array<{ name: string; type: string; doc?: string }>; - -export interface ComponentInfoProvider { - getEvents(): ComponentPartInfo; - getSlotLets(slot?: string): ComponentPartInfo; - getProps(): ComponentPartInfo; -} - -export class JsOrTsComponentInfoProvider implements ComponentInfoProvider { - private constructor( - private readonly typeChecker: ts.TypeChecker, - private readonly classType: ts.Type - ) {} - - getEvents(): ComponentPartInfo { - const eventType = this.getType('$$events_def'); - if (!eventType) { - return []; - } - - return this.mapPropertiesOfType(eventType); - } - - getSlotLets(slot = 'default'): ComponentPartInfo { - const slotType = this.getType('$$slot_def'); - if (!slotType) { - return []; - } - - const slotLets = slotType.getProperties().find((prop) => prop.name === slot); - if (!slotLets?.valueDeclaration) { - return []; - } - - const slotLetsType = this.typeChecker.getTypeOfSymbolAtLocation( - slotLets, - slotLets.valueDeclaration - ); - - return this.mapPropertiesOfType(slotLetsType); - } - - getProps() { - const props = this.getType('$$prop_def'); - if (!props) { - return []; - } - - return this.mapPropertiesOfType(props); - } - - private getType(classProperty: string) { - const symbol = this.classType.getProperty(classProperty); - if (!symbol?.valueDeclaration) { - return null; - } - - return this.typeChecker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration); - } - - private mapPropertiesOfType(type: ts.Type): ComponentPartInfo { - return type - .getProperties() - .map((prop) => { - // type would still be correct when there're multiple declarations - const declaration = prop.valueDeclaration ?? prop.declarations?.[0]; - if (!declaration) { - return; - } - - return { - name: prop.name, - type: this.typeChecker.typeToString( - this.typeChecker.getTypeOfSymbolAtLocation(prop, declaration) - ), - doc: ts.displayPartsToString(prop.getDocumentationComment(this.typeChecker)) - }; - }) - .filter(isNotNullOrUndefined); - } - - /** - * The result of this shouldn't be cached as it could lead to memory leaks. The type checker - * could become old and then multiple versions of it could exist. - */ - static create(lang: ts.LanguageService, def: ts.DefinitionInfo): ComponentInfoProvider | null { - const program = lang.getProgram(); - const sourceFile = program?.getSourceFile(def.fileName); - - if (!program || !sourceFile) { - return null; - } - - const defClass = findContainingNode(sourceFile, def.textSpan, ts.isClassDeclaration); - - if (!defClass) { - return null; - } - - const typeChecker = program.getTypeChecker(); - const classType = typeChecker.getTypeAtLocation(defClass); - - if (!classType) { - return null; - } - - return new JsOrTsComponentInfoProvider(typeChecker, classType); - } -} diff --git a/packages/language-server/src/plugins/typescript/DocumentMapper.ts b/packages/language-server/src/plugins/typescript/DocumentMapper.ts deleted file mode 100644 index 9b73504ad..000000000 --- a/packages/language-server/src/plugins/typescript/DocumentMapper.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { TraceMap } from '@jridgewell/trace-mapping'; -import { Position } from 'vscode-languageserver'; -import { SourceMapDocumentMapper } from '../../lib/documents'; - -export class ConsumerDocumentMapper extends SourceMapDocumentMapper { - constructor( - traceMap: TraceMap, - sourceUri: string, - private nrPrependesLines: number - ) { - super(traceMap, sourceUri); - } - - getOriginalPosition(generatedPosition: Position): Position { - return super.getOriginalPosition( - Position.create( - generatedPosition.line - this.nrPrependesLines, - generatedPosition.character - ) - ); - } - - getGeneratedPosition(originalPosition: Position): Position { - const result = super.getGeneratedPosition(originalPosition); - result.line += this.nrPrependesLines; - return result; - } - - isInGenerated(): boolean { - // always return true and map outliers case by case - return true; - } -} diff --git a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts deleted file mode 100644 index da5c09351..000000000 --- a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts +++ /dev/null @@ -1,781 +0,0 @@ -import { EncodedSourceMap, TraceMap, originalPositionFor } from '@jridgewell/trace-mapping'; -// @ts-ignore -import { TemplateNode } from 'svelte/types/compiler/interfaces'; -import { svelte2tsx, IExportedNames, internalHelpers } from 'svelte2tsx'; -import ts from 'typescript'; -import { Position, Range, TextDocumentContentChangeEvent } from 'vscode-languageserver'; -import { - Document, - DocumentMapper, - FragmentMapper, - IdentityMapper, - offsetAt, - positionAt, - TagInformation, - isInTag, - getLineOffsets, - FilePosition -} from '../../lib/documents'; -import { pathToUrl, urlToPath } from '../../utils'; -import { ConsumerDocumentMapper } from './DocumentMapper'; -import { SvelteNode, SvelteNodeWalker, walkSvelteAst } from './svelte-ast-utils'; -import { - getScriptKindFromAttributes, - getScriptKindFromFileName, - isSvelteFilePath, - getTsCheckComment -} from './utils'; -import { Logger } from '../../logger'; -import { dirname, resolve } from 'path'; -import { URI } from 'vscode-uri'; -import { surroundWithIgnoreComments } from './features/utils'; -import { configLoader } from '../../lib/documents/configLoader'; - -/** - * An error which occurred while trying to parse/preprocess the svelte file contents. - */ -export interface ParserError { - message: string; - range: Range; - code: number; -} - -/** - * Initial version of snapshots. - */ -export const INITIAL_VERSION = 0; - -/** - * A document snapshot suitable for the ts language service and the plugin. - * Can be a real ts/js file or a virtual ts/js file which is generated from a Svelte file. - */ -export interface DocumentSnapshot extends ts.IScriptSnapshot, DocumentMapper { - version: number; - filePath: string; - scriptKind: ts.ScriptKind; - scriptInfo: TagInformation | null; - positionAt(offset: number): Position; - offsetAt(position: Position): number; - /** - * Convenience function for getText(0, getLength()) - */ - getFullText(): string; - isOpenedInClient(): boolean; -} - -/** - * Options that apply to svelte files. - */ -export interface SvelteSnapshotOptions { - parse: typeof import('svelte/compiler').parse | undefined; - version: string | undefined; - transformOnTemplateError: boolean; - typingsNamespace: string; -} - -export namespace DocumentSnapshot { - /** - * Returns a svelte snapshot from a svelte document. - * @param document the svelte document - * @param options options that apply to the svelte document - */ - export function fromDocument(document: Document, options: SvelteSnapshotOptions) { - const { tsxMap, htmlAst, text, exportedNames, parserError, nrPrependedLines, scriptKind } = - preprocessSvelteFile(document, options); - - return new SvelteDocumentSnapshot( - document, - parserError, - scriptKind, - text, - nrPrependedLines, - exportedNames, - tsxMap, - htmlAst - ); - } - - /** - * Returns a svelte or ts/js snapshot from a file path, depending on the file contents. - * @param filePath path to the js/ts/svelte file - * @param createDocument function that is used to create a document in case it's a Svelte file - * @param options options that apply in case it's a svelte file - */ - export function fromFilePath( - filePath: string, - createDocument: (filePath: string, text: string) => Document, - options: SvelteSnapshotOptions, - tsSystem: ts.System - ) { - if (isSvelteFilePath(filePath)) { - return DocumentSnapshot.fromSvelteFilePath(filePath, createDocument, options); - } else { - return DocumentSnapshot.fromNonSvelteFilePath(filePath, tsSystem); - } - } - - /** - * Returns a ts/js snapshot from a file path. - * @param filePath path to the js/ts file - * @param options options that apply in case it's a svelte file - */ - export function fromNonSvelteFilePath(filePath: string, tsSystem: ts.System) { - let originalText = ''; - - // The following (very hacky) code makes sure that the ambient module definitions - // that tell TS "every import ending with .svelte is a valid module" are removed. - // They exist in svelte2tsx and svelte to make sure that people don't - // get errors in their TS files when importing Svelte files and not using our TS plugin. - // If someone wants to get back the behavior they can add an ambient module definition - // on their own. - const normalizedPath = filePath.replace(/\\/g, '/'); - if (!normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) { - originalText = tsSystem.readFile(filePath) || ''; - } - - if (normalizedPath.endsWith('node_modules/svelte/types/index.d.ts')) { - const startIdx = originalText.indexOf(`declare module '*.svelte' {`); - const endIdx = originalText.indexOf(`}`, originalText.indexOf(';', startIdx)) + 1; - originalText = - originalText.substring(0, startIdx) + - ' '.repeat(endIdx - startIdx) + - originalText.substring(endIdx); - } else if ( - normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts') || - normalizedPath.endsWith('svelte-check/dist/src/svelte-shims.d.ts') - ) { - // If not present, the LS uses an older version of svelte2tsx - if (originalText.includes('// -- start svelte-ls-remove --')) { - originalText = - originalText.substring( - 0, - originalText.indexOf('// -- start svelte-ls-remove --') - ) + - originalText.substring(originalText.indexOf('// -- end svelte-ls-remove --')); - } - } - - const declarationExtensions = [ts.Extension.Dcts, ts.Extension.Dts, ts.Extension.Dmts]; - if (declarationExtensions.some((ext) => normalizedPath.endsWith(ext))) { - return new DtsDocumentSnapshot(INITIAL_VERSION, filePath, originalText, tsSystem); - } - - return new JSOrTSDocumentSnapshot(INITIAL_VERSION, filePath, originalText); - } - - /** - * Returns a svelte snapshot from a file path. - * @param filePath path to the svelte file - * @param createDocument function that is used to create a document - * @param options options that apply in case it's a svelte file - */ - export function fromSvelteFilePath( - filePath: string, - createDocument: (filePath: string, text: string) => Document, - options: SvelteSnapshotOptions - ) { - const originalText = ts.sys.readFile(filePath) ?? ''; - return fromDocument(createDocument(filePath, originalText), options); - } -} - -/** - * Tries to preprocess the svelte document and convert the contents into better analyzable js/ts(x) content. - */ -function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions) { - let tsxMap: EncodedSourceMap | undefined; - let parserError: ParserError | null = null; - let nrPrependedLines = 0; - let text = document.getText(); - let exportedNames: IExportedNames = { has: () => false }; - let htmlAst: TemplateNode | undefined; - - const scriptKind = [ - getScriptKindFromAttributes(document.scriptInfo?.attributes ?? {}), - getScriptKindFromAttributes(document.moduleScriptInfo?.attributes ?? {}) - ].includes(ts.ScriptKind.TSX) - ? ts.ScriptKind.TS - : ts.ScriptKind.JS; - - try { - const tsx = svelte2tsx(text, { - parse: options.parse, - version: options.version, - filename: document.getFilePath() ?? undefined, - isTsFile: scriptKind === ts.ScriptKind.TS, - mode: 'ts', - typingsNamespace: options.typingsNamespace, - emitOnTemplateError: options.transformOnTemplateError, - namespace: document.config?.compilerOptions?.namespace, - accessors: - document.config?.compilerOptions?.accessors ?? - document.config?.compilerOptions?.customElement - }); - text = tsx.code; - tsxMap = tsx.map as EncodedSourceMap; - exportedNames = tsx.exportedNames; - // We know it's there, it's not part of the public API so people don't start using it - htmlAst = (tsx as any).htmlAst; - - if (tsxMap) { - tsxMap.sources = [document.uri]; - - const scriptInfo = document.scriptInfo || document.moduleScriptInfo; - const tsCheck = getTsCheckComment(scriptInfo?.content); - if (tsCheck) { - text = tsCheck + text; - nrPrependedLines = 1; - } - } - } catch (e: any) { - // Error start/end logic is different and has different offsets for line, so we need to convert that - const start: Position = { - line: (e.start?.line ?? 1) - 1, - character: e.start?.column ?? 0 - }; - const end: Position = e.end ? { line: e.end.line - 1, character: e.end.column } : start; - - parserError = { - range: { start, end }, - message: e.message, - code: -1 - }; - - // fall back to extracted script, if any - const scriptInfo = document.scriptInfo || document.moduleScriptInfo; - text = scriptInfo ? scriptInfo.content : ''; - } - - return { - tsxMap, - text, - exportedNames, - htmlAst, - parserError, - nrPrependedLines, - scriptKind - }; -} - -/** - * A svelte document snapshot suitable for the TS language service and the plugin. - * It contains the generated code (Svelte->TS/JS) so the TS language service can understand it. - */ -export class SvelteDocumentSnapshot implements DocumentSnapshot { - private mapper?: DocumentMapper; - private lineOffsets?: number[]; - private url = pathToUrl(this.filePath); - - version = this.parent.version; - - constructor( - public readonly parent: Document, - public readonly parserError: ParserError | null, - public readonly scriptKind: ts.ScriptKind, - private readonly text: string, - private readonly nrPrependedLines: number, - private readonly exportedNames: IExportedNames, - private readonly tsxMap?: EncodedSourceMap, - private readonly htmlAst?: TemplateNode - ) {} - - get filePath() { - return this.parent.getFilePath() || ''; - } - - get scriptInfo() { - return this.parent.scriptInfo; - } - - get moduleScriptInfo() { - return this.parent.moduleScriptInfo; - } - - getOriginalText(range?: Range) { - return this.parent.getText(range); - } - - getText(start: number, end: number) { - return this.text.substring(start, end); - } - - getLength() { - return this.text.length; - } - - getFullText() { - return this.text; - } - - getChangeRange() { - return undefined; - } - - positionAt(offset: number) { - return positionAt(offset, this.text, this.getLineOffsets()); - } - - offsetAt(position: Position): number { - return offsetAt(position, this.text, this.getLineOffsets()); - } - - getLineContainingOffset(offset: number) { - const chunks = this.getText(0, offset).split('\n'); - return chunks[chunks.length - 1]; - } - - hasProp(name: string): boolean { - return this.exportedNames.has(name); - } - - svelteNodeAt(positionOrOffset: number | Position): SvelteNode | null { - if (!this.htmlAst) { - return null; - } - const offset = - typeof positionOrOffset === 'number' - ? positionOrOffset - : this.parent.offsetAt(positionOrOffset); - - let foundNode: SvelteNode | null = null; - this.walkSvelteAst({ - enter(node) { - // In case the offset is at a point where a node ends and a new one begins, - // the node where the code ends is used. If this introduces problems, introduce - // an affinity parameter to prefer the node where it ends/starts. - if (node.start > offset || node.end < offset) { - this.skip(); - return; - } - const parent = foundNode; - // Spread so the "parent" property isn't added to the original ast, - // causing an infinite loop - foundNode = { ...node }; - if (parent) { - foundNode.parent = parent; - } - } - }); - - return foundNode; - } - - walkSvelteAst(walker: SvelteNodeWalker) { - if (!this.htmlAst) { - return; - } - - walkSvelteAst(this.htmlAst, walker); - } - - getOriginalPosition(pos: Position): Position { - return this.getMapper().getOriginalPosition(pos); - } - - getGeneratedPosition(pos: Position): Position { - return this.getMapper().getGeneratedPosition(pos); - } - - isInGenerated(pos: Position): boolean { - return !isInTag(pos, this.parent.styleInfo); - } - - getURL(): string { - return this.url; - } - - isOpenedInClient() { - return this.parent.openedByClient; - } - - private getLineOffsets() { - if (!this.lineOffsets) { - this.lineOffsets = getLineOffsets(this.text); - } - return this.lineOffsets; - } - - private getMapper() { - if (!this.mapper) { - this.mapper = this.initMapper(); - } - return this.mapper; - } - - private initMapper() { - const scriptInfo = this.parent.scriptInfo || this.parent.moduleScriptInfo; - - if (!this.tsxMap) { - if (!scriptInfo) { - return new IdentityMapper(this.url); - } - - return new FragmentMapper(this.parent.getText(), scriptInfo, this.url); - } - - return new ConsumerDocumentMapper( - new TraceMap(this.tsxMap), - this.url, - this.nrPrependedLines - ); - } -} - -/** - * A js/ts document snapshot suitable for the ts language service and the plugin. - * Since no mapping has to be done here, it also implements the mapper interface. - * If it's a SvelteKit file (e.g. +page.ts), types will be auto-added if not explicitly typed. - */ -export class JSOrTSDocumentSnapshot extends IdentityMapper implements DocumentSnapshot { - scriptKind = getScriptKindFromFileName(this.filePath); - scriptInfo = null; - originalText = this.text; - kitFile = false; - private lineOffsets?: number[]; - private internalLineOffsets?: number[]; - private addedCode: Array<{ - generatedPos: number; - originalPos: number; - length: number; - inserted: string; - total: number; - }> = []; - private paramsPath = 'src/params'; - private serverHooksPath = 'src/hooks.server'; - private clientHooksPath = 'src/hooks.client'; - - private openedByClient = false; - - isOpenedInClient(): boolean { - return this.openedByClient; - } - - constructor( - public version: number, - public readonly filePath: string, - private text: string - ) { - super(pathToUrl(filePath)); - this.adjustText(); - } - - getText(start: number, end: number) { - return this.text.substring(start, end); - } - - getLength() { - return this.text.length; - } - - getFullText() { - return this.text; - } - - getChangeRange() { - return undefined; - } - - positionAt(offset: number) { - return positionAt(offset, this.text, this.getLineOffsets()); - } - - offsetAt(position: Position): number { - return offsetAt(position, this.text, this.getLineOffsets()); - } - - getGeneratedPosition(originalPosition: Position): Position { - if (!this.kitFile || this.addedCode.length === 0) { - return super.getGeneratedPosition(originalPosition); - } - const pos = this.originalOffsetAt(originalPosition); - - let total = 0; - for (const added of this.addedCode) { - if (pos < added.generatedPos) break; - total += added.length; - } - - return this.positionAt(pos + total); - } - - getOriginalPosition(generatedPosition: Position): Position { - if (!this.kitFile || this.addedCode.length === 0) { - return super.getOriginalPosition(generatedPosition); - } - const pos = this.offsetAt(generatedPosition); - - let total = 0; - let idx = 0; - for (; idx < this.addedCode.length; idx++) { - const added = this.addedCode[idx]; - if (pos < added.generatedPos) break; - total += added.length; - } - - if (idx > 0) { - const prev = this.addedCode[idx - 1]; - // Special case: pos is in the middle of an added range - if (pos > prev.generatedPos && pos < prev.generatedPos + prev.length) { - return this.originalPositionAt(prev.originalPos); - } - } - - return this.originalPositionAt(pos - total); - } - - update(changes: TextDocumentContentChangeEvent[]): void { - for (const change of changes) { - let start = 0; - let end = 0; - if ('range' in change) { - start = this.originalOffsetAt(change.range.start); - end = this.originalOffsetAt(change.range.end); - } else { - end = this.originalText.length; - } - - this.originalText = - this.originalText.slice(0, start) + change.text + this.originalText.slice(end); - } - - this.adjustText(); - this.version++; - this.lineOffsets = undefined; - this.internalLineOffsets = undefined; - // only client can have incremental updates - this.openedByClient = true; - } - - protected getLineOffsets() { - if (!this.lineOffsets) { - this.lineOffsets = getLineOffsets(this.text); - } - return this.lineOffsets; - } - - private originalOffsetAt(position: Position): number { - return offsetAt(position, this.originalText, this.getOriginalLineOffsets()); - } - - private originalPositionAt(offset: number): Position { - return positionAt(offset, this.originalText, this.getOriginalLineOffsets()); - } - - private getOriginalLineOffsets() { - if (!this.kitFile) { - return this.getLineOffsets(); - } - if (!this.internalLineOffsets) { - this.internalLineOffsets = getLineOffsets(this.originalText); - } - return this.internalLineOffsets; - } - - private adjustText() { - const result = internalHelpers.upsertKitFile( - ts, - this.filePath, - { - clientHooksPath: this.clientHooksPath, - paramsPath: this.paramsPath, - serverHooksPath: this.serverHooksPath - }, - () => this.createSource(), - surroundWithIgnoreComments - ); - if (!result) { - this.kitFile = false; - this.addedCode = []; - this.text = this.originalText; - return; - } - - if (!this.kitFile) { - const files = configLoader.getConfig(this.filePath)?.kit?.files; - if (files) { - this.paramsPath ||= files.params; - this.serverHooksPath ||= files.hooks?.server; - this.clientHooksPath ||= files.hooks?.client; - } - } - - const { text, addedCode } = result; - - this.kitFile = true; - this.addedCode = addedCode; - this.text = text; - } - - private createSource() { - return ts.createSourceFile( - this.filePath, - this.originalText, - ts.ScriptTarget.Latest, - true, - this.scriptKind - ); - } -} - -const sourceMapCommentRegExp = /^\/\/[@#] source[M]appingURL=(.+)\r?\n?$/; -const whitespaceOrMapCommentRegExp = /^\s*(\/\/[@#] .*)?$/; -const base64UrlRegExp = - /^data:(?:application\/json(?:;charset=[uU][tT][fF]-8);base64,([A-Za-z0-9+\/=]+)$)?/; - -export class DtsDocumentSnapshot extends JSOrTSDocumentSnapshot implements DocumentMapper { - private traceMap: TraceMap | undefined; - private mapperInitialized = false; - - constructor( - version: number, - filePath: string, - text: string, - private tsSys: ts.System - ) { - super(version, filePath, text); - } - - getOriginalFilePosition(generatedPosition: Position): FilePosition { - if (!this.mapperInitialized) { - this.traceMap = this.initMapper(); - this.mapperInitialized = true; - } - - const mapped = this.traceMap - ? originalPositionFor(this.traceMap, { - line: generatedPosition.line + 1, - column: generatedPosition.character - }) - : undefined; - - if (!mapped || mapped.line == null || !mapped.source) { - return generatedPosition; - } - - const originalFilePath = URI.isUri(mapped.source) - ? urlToPath(mapped.source) - : this.filePath - ? resolve(dirname(this.filePath), mapped.source).toString() - : undefined; - - // ex: library publish with declarationMap but npmignore the original files - if (!originalFilePath || !this.tsSys.fileExists(originalFilePath)) { - return generatedPosition; - } - - return { - line: mapped.line, - character: mapped.column, - uri: pathToUrl(originalFilePath) - }; - } - - private initMapper() { - const sourceMapUrl = tryGetSourceMappingURL(this.getLineOffsets(), this.getFullText()); - - if (!sourceMapUrl) { - return; - } - - const match = sourceMapUrl.match(base64UrlRegExp); - if (match) { - const base64Json = match[1]; - if (!base64Json || !this.tsSys.base64decode) { - return; - } - - return this.initMapperByRawSourceMap(this.tsSys.base64decode(base64Json)); - } - - const tryingLocations = new Set([ - resolve(dirname(this.filePath), sourceMapUrl), - this.filePath + '.map' - ]); - - for (const mapFilePath of tryingLocations) { - if (!this.tsSys.fileExists(mapFilePath)) { - continue; - } - - const mapFileContent = this.tsSys.readFile(mapFilePath); - if (mapFileContent) { - return this.initMapperByRawSourceMap(mapFileContent); - } - } - - this.logFailedToResolveSourceMap("can't find valid sourcemap file"); - } - - private initMapperByRawSourceMap(input: string) { - const map = tryParseRawSourceMap(input); - - // don't support inline sourcemap because - // it must be a file that editor can point to - if ( - !map || - !map.mappings || - map.sourcesContent?.some((content) => typeof content === 'string') - ) { - this.logFailedToResolveSourceMap('invalid or unsupported sourcemap'); - return; - } - - return new TraceMap(map); - } - - private logFailedToResolveSourceMap(...errors: any[]) { - Logger.debug(`Resolving declaration map for ${this.filePath} failed. `, ...errors); - } -} - -// https://github.com/microsoft/TypeScript/blob/1dc5b28b94b4a63f735a42d6497d538434d69b66/src/compiler/sourcemap.ts#L381 -function tryGetSourceMappingURL(lineOffsets: number[], text: string) { - for (let index = lineOffsets.length - 1; index >= 0; index--) { - const line = text.slice(lineOffsets[index], lineOffsets[index + 1]); - const comment = sourceMapCommentRegExp.exec(line); - if (comment) { - return comment[1].trimEnd(); - } - // If we see a non-whitespace/map comment-like line, break, to avoid scanning up the entire file - else if (!line.match(whitespaceOrMapCommentRegExp)) { - break; - } - } -} - -// https://github.com/microsoft/TypeScript/blob/1dc5b28b94b4a63f735a42d6497d538434d69b66/src/compiler/sourcemap.ts#L402 - -function isRawSourceMap(x: any): x is EncodedSourceMap { - return ( - x !== null && - typeof x === 'object' && - x.version === 3 && - typeof x.file === 'string' && - typeof x.mappings === 'string' && - Array.isArray(x.sources) && - x.sources.every((source: any) => typeof source === 'string') && - (x.sourceRoot === undefined || x.sourceRoot === null || typeof x.sourceRoot === 'string') && - (x.sourcesContent === undefined || - x.sourcesContent === null || - (Array.isArray(x.sourcesContent) && - x.sourcesContent.every( - (content: any) => typeof content === 'string' || content === null - ))) && - (x.names === undefined || - x.names === null || - (Array.isArray(x.names) && x.names.every((name: any) => typeof name === 'string'))) - ); -} - -function tryParseRawSourceMap(text: string) { - try { - const parsed = JSON.parse(text); - if (isRawSourceMap(parsed)) { - return parsed; - } - } catch { - // empty - } - - return undefined; -} diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts deleted file mode 100644 index 2d438cedb..000000000 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { dirname } from 'path'; -import ts from 'typescript'; -import { TextDocumentContentChangeEvent } from 'vscode-languageserver'; -import { Document, DocumentManager } from '../../lib/documents'; -import { LSConfigManager } from '../../ls-config'; -import { - createGetCanonicalFileName, - debounceSameArg, - GetCanonicalFileName, - normalizePath, - pathToUrl, - urlToPath -} from '../../utils'; -import { DocumentSnapshot, SvelteDocumentSnapshot } from './DocumentSnapshot'; -import { - getService, - getServiceForTsconfig, - forAllServices, - LanguageServiceContainer, - LanguageServiceDocumentContext -} from './service'; -import { GlobalSnapshotsManager, SnapshotManager } from './SnapshotManager'; -import { isSubPath } from './utils'; - -interface LSAndTSDocResolverOptions { - notifyExceedSizeLimit?: () => void; - /** - * True, if used in the context of svelte-check - */ - isSvelteCheck?: boolean; - - /** - * This should only be set via svelte-check. Makes sure all documents are resolved to that tsconfig. Has to be absolute. - */ - tsconfigPath?: string; - - onProjectReloaded?: () => void; - watch?: boolean; - tsSystem?: ts.System; -} - -export class LSAndTSDocResolver { - constructor( - private readonly docManager: DocumentManager, - private readonly workspaceUris: string[], - private readonly configManager: LSConfigManager, - private readonly options?: LSAndTSDocResolverOptions - ) { - const handleDocumentChange = (document: Document) => { - // This refreshes the document in the ts language service - this.getSnapshot(document); - }; - docManager.on( - 'documentChange', - debounceSameArg( - handleDocumentChange, - (newDoc, prevDoc) => newDoc.uri === prevDoc?.uri, - 1000 - ) - ); - - // New files would cause typescript to rebuild its type-checker. - // Open it immediately to reduce rebuilds in the startup - // where multiple files and their dependencies - // being loaded in a short period of times - docManager.on('documentOpen', (document) => { - handleDocumentChange(document); - docManager.lockDocument(document.uri); - }); - - this.getCanonicalFileName = createGetCanonicalFileName( - (options?.tsSystem ?? ts.sys).useCaseSensitiveFileNames - ); - } - - /** - * Create a svelte document -> should only be invoked with svelte files. - */ - private createDocument = (fileName: string, content: string) => { - const uri = pathToUrl(fileName); - const document = this.docManager.openDocument( - { - text: content, - uri - }, - /* openedByClient */ false - ); - this.docManager.lockDocument(uri); - return document; - }; - - private globalSnapshotsManager = new GlobalSnapshotsManager( - this.lsDocumentContext.tsSystem, - /* watchPackageJson */ !!this.options?.watch - ); - private extendedConfigCache = new Map(); - private getCanonicalFileName: GetCanonicalFileName; - - private get lsDocumentContext(): LanguageServiceDocumentContext { - return { - ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx', - createDocument: this.createDocument, - transformOnTemplateError: !this.options?.isSvelteCheck, - globalSnapshotsManager: this.globalSnapshotsManager, - notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit, - extendedConfigCache: this.extendedConfigCache, - onProjectReloaded: this.options?.onProjectReloaded, - watchTsConfig: !!this.options?.watch, - tsSystem: this.options?.tsSystem ?? ts.sys - }; - } - - async getLSForPath(path: string) { - return (await this.getTSService(path)).getService(); - } - - async getLSAndTSDoc(document: Document): Promise<{ - tsDoc: SvelteDocumentSnapshot; - lang: ts.LanguageService; - userPreferences: ts.UserPreferences; - }> { - const { tsDoc, lsContainer, userPreferences } = await this.getLSAndTSDocWorker(document); - - return { tsDoc, lang: lsContainer.getService(), userPreferences }; - } - - /** - * Retrieves the LS for operations that don't need cross-files information. - * can save some time by not synchronizing languageService program - */ - async getLsForSyntheticOperations(document: Document): Promise<{ - tsDoc: SvelteDocumentSnapshot; - lang: ts.LanguageService; - userPreferences: ts.UserPreferences; - }> { - const { tsDoc, lsContainer, userPreferences } = await this.getLSAndTSDocWorker(document); - - return { tsDoc, userPreferences, lang: lsContainer.getService(/* skipSynchronize */ true) }; - } - - private async getLSAndTSDocWorker(document: Document) { - const lsContainer = await this.getTSService(document.getFilePath() || ''); - const tsDoc = await this.getSnapshot(document); - const userPreferences = this.getUserPreferences(tsDoc); - - return { tsDoc, lsContainer, userPreferences }; - } - - /** - * Retrieves and updates the snapshot for the given document or path from - * the ts service it primarily belongs into. - * The update is mirrored in all other services, too. - */ - async getSnapshot(document: Document): Promise; - async getSnapshot(pathOrDoc: string | Document): Promise; - async getSnapshot(pathOrDoc: string | Document) { - const filePath = typeof pathOrDoc === 'string' ? pathOrDoc : pathOrDoc.getFilePath() || ''; - const tsService = await this.getTSService(filePath); - return tsService.updateSnapshot(pathOrDoc); - } - - /** - * Updates snapshot path in all existing ts services and retrieves snapshot - */ - async updateSnapshotPath(oldPath: string, newPath: string): Promise { - for (const snapshot of this.globalSnapshotsManager.getByPrefix(oldPath)) { - await this.deleteSnapshot(snapshot.filePath); - } - // This may not be a file but a directory, still try - await this.getSnapshot(newPath); - } - - /** - * Deletes snapshot in all existing ts services - */ - async deleteSnapshot(filePath: string) { - await forAllServices((service) => service.deleteSnapshot(filePath)); - const uri = pathToUrl(filePath); - if (this.docManager.get(uri)) { - // Guard this call, due to race conditions it may already have been closed; - // also this may not be a Svelte file - this.docManager.closeDocument(uri); - } - this.docManager.releaseDocument(uri); - } - - async invalidateModuleCache(filePath: string) { - await forAllServices((service) => service.invalidateModuleCache(filePath)); - } - - /** - * Updates project files in all existing ts services - */ - async updateProjectFiles() { - await forAllServices((service) => service.updateProjectFiles()); - } - - /** - * Updates file in all ts services where it exists - */ - async updateExistingTsOrJsFile( - path: string, - changes?: TextDocumentContentChangeEvent[] - ): Promise { - path = normalizePath(path); - // Only update once because all snapshots are shared between - // services. Since we don't have a current version of TS/JS - // files, the operation wouldn't be idempotent. - let didUpdate = false; - await forAllServices((service) => { - if (service.hasFile(path) && !didUpdate) { - didUpdate = true; - service.updateTsOrJsFile(path, changes); - } - }); - } - - /** - * @internal Public for tests only - */ - async getSnapshotManager(filePath: string): Promise { - return (await this.getTSService(filePath)).snapshotManager; - } - - async getTSService(filePath?: string): Promise { - if (this.options?.tsconfigPath) { - return getServiceForTsconfig( - this.options?.tsconfigPath, - dirname(this.options.tsconfigPath), - this.lsDocumentContext - ); - } - if (!filePath) { - throw new Error('Cannot call getTSService without filePath and without tsconfigPath'); - } - return getService(filePath, this.workspaceUris, this.lsDocumentContext); - } - - private getUserPreferences(tsDoc: DocumentSnapshot): ts.UserPreferences { - const configLang = - tsDoc.scriptKind === ts.ScriptKind.TS || tsDoc.scriptKind === ts.ScriptKind.TSX - ? 'typescript' - : 'javascript'; - - const nearestWorkspaceUri = this.workspaceUris.find((workspaceUri) => - isSubPath(workspaceUri, tsDoc.filePath, this.getCanonicalFileName) - ); - - return this.configManager.getTsUserPreferences( - configLang, - nearestWorkspaceUri ? urlToPath(nearestWorkspaceUri) : null - ); - } -} diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts deleted file mode 100644 index 8b035e65a..000000000 --- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts +++ /dev/null @@ -1,342 +0,0 @@ -import ts from 'typescript'; -import { DocumentSnapshot, JSOrTSDocumentSnapshot } from './DocumentSnapshot'; -import { Logger } from '../../logger'; -import { TextDocumentContentChangeEvent } from 'vscode-languageserver'; -import { createGetCanonicalFileName, GetCanonicalFileName, normalizePath } from '../../utils'; -import { EventEmitter } from 'events'; -import { FileMap } from '../../lib/documents/fileCollection'; -import { dirname } from 'path'; - -type SnapshotChangeHandler = (fileName: string, newDocument: DocumentSnapshot | undefined) => void; - -/** - * Every snapshot corresponds to a unique file on disk. - * A snapshot can be part of multiple projects, but for a given file path - * there can be only one snapshot. - */ -export class GlobalSnapshotsManager { - private emitter = new EventEmitter(); - private documents: FileMap; - private getCanonicalFileName: GetCanonicalFileName; - private packageJsonCache: PackageJsonCache; - - constructor( - private readonly tsSystem: ts.System, - watchPackageJson = false - ) { - this.documents = new FileMap(tsSystem.useCaseSensitiveFileNames); - this.getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); - this.packageJsonCache = new PackageJsonCache( - tsSystem, - watchPackageJson, - this.getCanonicalFileName, - this.updateSnapshotsInDirectory.bind(this) - ); - } - - get(fileName: string) { - fileName = normalizePath(fileName); - return this.documents.get(fileName); - } - - getByPrefix(path: string) { - path = this.getCanonicalFileName(normalizePath(path)); - return Array.from(this.documents.entries()) - .filter((doc) => doc[0].startsWith(path)) - .map((doc) => doc[1]); - } - - set(fileName: string, document: DocumentSnapshot) { - fileName = normalizePath(fileName); - this.documents.set(fileName, document); - this.emitter.emit('change', fileName, document); - } - - delete(fileName: string) { - fileName = normalizePath(fileName); - this.documents.delete(fileName); - this.emitter.emit('change', fileName, undefined); - } - - updateTsOrJsFile( - fileName: string, - changes?: TextDocumentContentChangeEvent[] - ): JSOrTSDocumentSnapshot | undefined { - fileName = normalizePath(fileName); - const previousSnapshot = this.get(fileName); - - if (changes) { - if (!(previousSnapshot instanceof JSOrTSDocumentSnapshot)) { - return; - } - previousSnapshot.update(changes); - this.emitter.emit('change', fileName, previousSnapshot); - return previousSnapshot; - } else { - const newSnapshot = DocumentSnapshot.fromNonSvelteFilePath(fileName, this.tsSystem); - - if (previousSnapshot) { - newSnapshot.version = previousSnapshot.version + 1; - } else { - // ensure it's greater than initial version - // so that ts server picks up the change - newSnapshot.version += 1; - } - this.set(fileName, newSnapshot); - return newSnapshot; - } - } - - onChange(listener: SnapshotChangeHandler) { - this.emitter.on('change', listener); - } - - removeChangeListener(listener: SnapshotChangeHandler) { - this.emitter.off('change', listener); - } - - getPackageJson(path: string) { - return this.packageJsonCache.getPackageJson(path); - } - - private updateSnapshotsInDirectory(dir: string) { - this.getByPrefix(dir).forEach((snapshot) => { - this.updateTsOrJsFile(snapshot.filePath); - }); - } -} - -export interface TsFilesSpec { - include?: readonly string[]; - exclude?: readonly string[]; -} - -/** - * Should only be used by `service.ts` - */ -export class SnapshotManager { - private readonly documents: FileMap; - private lastLogged = new Date(new Date().getTime() - 60_001); - - private readonly projectFileToOriginalCasing: Map; - private getCanonicalFileName: GetCanonicalFileName; - - private readonly watchExtensions = [ - ts.Extension.Dts, - ts.Extension.Js, - ts.Extension.Jsx, - ts.Extension.Ts, - ts.Extension.Tsx, - ts.Extension.Json - ]; - - constructor( - private globalSnapshotsManager: GlobalSnapshotsManager, - private fileSpec: TsFilesSpec, - private workspaceRoot: string, - projectFiles: string[], - useCaseSensitiveFileNames = ts.sys.useCaseSensitiveFileNames - ) { - this.onSnapshotChange = this.onSnapshotChange.bind(this); - this.globalSnapshotsManager.onChange(this.onSnapshotChange); - this.documents = new FileMap(useCaseSensitiveFileNames); - this.projectFileToOriginalCasing = new Map(); - this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - - projectFiles.forEach((originalCasing) => - this.projectFileToOriginalCasing.set( - this.getCanonicalFileName(originalCasing), - originalCasing - ) - ); - } - - private onSnapshotChange(fileName: string, document: DocumentSnapshot | undefined) { - // Only delete/update snapshots, don't add new ones, - // as they could be from another TS service and this - // snapshot manager can't reach this file. - // For these, instead wait on a `get` method invocation - // and set them "manually" in the set/update methods. - if (!document) { - this.documents.delete(fileName); - this.projectFileToOriginalCasing.delete(this.getCanonicalFileName(fileName)); - } else if (this.documents.has(fileName)) { - this.documents.set(fileName, document); - } - } - - updateProjectFiles(): void { - const { include, exclude } = this.fileSpec; - - // Since we default to not include anything, - // just don't waste time on this - if (include?.length === 0) { - return; - } - - const projectFiles = ts.sys - .readDirectory(this.workspaceRoot, this.watchExtensions, exclude, include) - .map(normalizePath); - - projectFiles.forEach((projectFile) => - this.projectFileToOriginalCasing.set( - this.getCanonicalFileName(projectFile), - projectFile - ) - ); - } - - updateTsOrJsFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void { - const snapshot = this.globalSnapshotsManager.updateTsOrJsFile(fileName, changes); - // This isn't duplicated logic to the listener, because this could - // be a new snapshot which the listener wouldn't add. - if (snapshot) { - this.documents.set(normalizePath(fileName), snapshot); - } - } - - has(fileName: string): boolean { - fileName = normalizePath(fileName); - return ( - this.projectFileToOriginalCasing.has(this.getCanonicalFileName(fileName)) || - this.documents.has(fileName) - ); - } - - set(fileName: string, snapshot: DocumentSnapshot): void { - this.globalSnapshotsManager.set(fileName, snapshot); - // This isn't duplicated logic to the listener, because this could - // be a new snapshot which the listener wouldn't add. - this.documents.set(normalizePath(fileName), snapshot); - this.logStatistics(); - } - - get(fileName: string): DocumentSnapshot | undefined { - fileName = normalizePath(fileName); - let snapshot = this.documents.get(fileName); - if (!snapshot) { - snapshot = this.globalSnapshotsManager.get(fileName); - if (snapshot) { - this.documents.set(fileName, snapshot); - } - } - return snapshot; - } - - delete(fileName: string): void { - fileName = normalizePath(fileName); - this.globalSnapshotsManager.delete(fileName); - } - - getClientFileNames(): string[] { - return Array.from(this.documents.values()) - .filter((doc) => doc.isOpenedInClient()) - .map((doc) => doc.filePath); - } - - getProjectFileNames(): string[] { - return Array.from(this.projectFileToOriginalCasing.values()); - } - - private logStatistics() { - const date = new Date(); - // Don't use setInterval because that will keep tests running forever - if (date.getTime() - this.lastLogged.getTime() > 60_000) { - this.lastLogged = date; - - const allFiles = Array.from( - new Set([...this.projectFileToOriginalCasing.keys(), ...this.documents.keys()]) - ); - Logger.log( - 'SnapshotManager File Statistics:\n' + - `Project files: ${this.projectFileToOriginalCasing.size}\n` + - `Svelte files: ${ - allFiles.filter((name) => name.endsWith('.svelte')).length - }\n` + - `From node_modules: ${ - allFiles.filter((name) => name.includes('node_modules')).length - }\n` + - `Total: ${allFiles.length}` - ); - } - } - - dispose() { - this.globalSnapshotsManager.removeChangeListener(this.onSnapshotChange); - } -} - -export const ignoredBuildDirectories = ['__sapper__', '.svelte-kit']; - -class PackageJsonCache { - constructor( - private readonly tsSystem: ts.System, - private readonly watchPackageJson: boolean, - private readonly getCanonicalFileName: GetCanonicalFileName, - private readonly updateSnapshotsInDirectory: (directory: string) => void - ) { - this.watchers = new FileMap(tsSystem.useCaseSensitiveFileNames); - } - - private readonly watchers: FileMap; - - private packageJsonCache = new FileMap< - { text: string; modifiedTime: number | undefined } | undefined - >(); - - getPackageJson(path: string) { - if (!this.packageJsonCache.has(path)) { - this.packageJsonCache.set(path, this.initWatcherAndRead(path)); - } - - return this.packageJsonCache.get(path); - } - - private initWatcherAndRead(path: string) { - if (this.watchPackageJson) { - this.tsSystem.watchFile?.(path, this.onPackageJsonWatchChange.bind(this), 3_000); - } - const exist = this.tsSystem.fileExists(path); - - if (!exist) { - return undefined; - } - - return this.readPackageJson(path); - } - - private readPackageJson(path: string) { - return { - text: this.tsSystem.readFile(path) ?? '', - modifiedTime: this.tsSystem.getModifiedTime?.(path)?.valueOf() - }; - } - - private onPackageJsonWatchChange(path: string, onWatchChange: ts.FileWatcherEventKind) { - const dir = dirname(path); - - if (onWatchChange === ts.FileWatcherEventKind.Deleted) { - this.packageJsonCache.delete(path); - this.watchers.get(path)?.close(); - this.watchers.delete(path); - } else { - this.packageJsonCache.set(path, this.readPackageJson(path)); - } - - if (!path.includes('node_modules')) { - return; - } - - setTimeout(() => { - this.updateSnapshotsInDirectory(dir); - const realPath = - this.tsSystem.realpath && - this.getCanonicalFileName(normalizePath(this.tsSystem.realpath?.(dir))); - - // pnpm - if (realPath && realPath !== dir) { - this.updateSnapshotsInDirectory(realPath); - } - }, 500); - } -} diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts deleted file mode 100644 index af6fad0e0..000000000 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ /dev/null @@ -1,660 +0,0 @@ -import ts, { NavigationTree } from 'typescript'; -import { - CallHierarchyIncomingCall, - CallHierarchyItem, - CallHierarchyOutgoingCall, - CancellationToken, - CodeAction, - CodeActionContext, - CompletionContext, - CompletionList, - DefinitionLink, - Diagnostic, - FileChangeType, - FoldingRange, - Hover, - InlayHint, - Location, - LocationLink, - Position, - Range, - ReferenceContext, - SelectionRange, - SemanticTokens, - SignatureHelp, - SignatureHelpContext, - SymbolInformation, - SymbolKind, - TextDocumentContentChangeEvent, - WorkspaceEdit -} from 'vscode-languageserver'; -import { Document, getTextInRange, mapSymbolInformationToOriginal } from '../../lib/documents'; -import { LSConfigManager, LSTypescriptConfig } from '../../ls-config'; -import { isNotNullOrUndefined, isZeroLengthRange } from '../../utils'; -import { - AppCompletionItem, - AppCompletionList, - CallHierarchyProvider, - CodeActionsProvider, - CompletionsProvider, - DefinitionsProvider, - DiagnosticsProvider, - DocumentSymbolsProvider, - FileReferencesProvider, - FileRename, - FindComponentReferencesProvider, - FindReferencesProvider, - FoldingRangeProvider, - HoverProvider, - ImplementationProvider, - InlayHintProvider, - OnWatchFileChanges, - OnWatchFileChangesPara, - RenameProvider, - SelectionRangeProvider, - SemanticTokensProvider, - SignatureHelpProvider, - TypeDefinitionProvider, - UpdateImportsProvider, - UpdateTsOrJsFile -} from '../interfaces'; -import { LSAndTSDocResolver } from './LSAndTSDocResolver'; -import { ignoredBuildDirectories } from './SnapshotManager'; -import { CallHierarchyProviderImpl } from './features/CallHierarchyProvider'; -import { CodeActionsProviderImpl } from './features/CodeActionsProvider'; -import { - CompletionEntryWithIdentifier, - CompletionsProviderImpl -} from './features/CompletionProvider'; -import { DiagnosticsProviderImpl } from './features/DiagnosticsProvider'; -import { FindComponentReferencesProviderImpl } from './features/FindComponentReferencesProvider'; -import { FindFileReferencesProviderImpl } from './features/FindFileReferencesProvider'; -import { FindReferencesProviderImpl } from './features/FindReferencesProvider'; -import { FoldingRangeProviderImpl } from './features/FoldingRangeProvider'; -import { HoverProviderImpl } from './features/HoverProvider'; -import { ImplementationProviderImpl } from './features/ImplementationProvider'; -import { InlayHintProviderImpl } from './features/InlayHintProvider'; -import { RenameProviderImpl } from './features/RenameProvider'; -import { SelectionRangeProviderImpl } from './features/SelectionRangeProvider'; -import { SemanticTokensProviderImpl } from './features/SemanticTokensProvider'; -import { SignatureHelpProviderImpl } from './features/SignatureHelpProvider'; -import { TypeDefinitionProviderImpl } from './features/TypeDefinitionProvider'; -import { UpdateImportsProviderImpl } from './features/UpdateImportsProvider'; -import { getDirectiveCommentCompletions } from './features/getDirectiveCommentCompletions'; -import { - SnapshotMap, - is$storeVariableIn$storeDeclaration, - isTextSpanInGeneratedCode -} from './features/utils'; -import { isAttributeName, isAttributeShorthand, isEventHandler } from './svelte-ast-utils'; -import { - convertToLocationForReferenceOrDefinition, - convertToLocationRange, - getScriptKindFromFileName, - isInScript, - symbolKindFromString -} from './utils'; - -export class TypeScriptPlugin - implements - DiagnosticsProvider, - HoverProvider, - DocumentSymbolsProvider, - DefinitionsProvider, - CodeActionsProvider, - UpdateImportsProvider, - RenameProvider, - FindReferencesProvider, - FileReferencesProvider, - FindComponentReferencesProvider, - SelectionRangeProvider, - SignatureHelpProvider, - SemanticTokensProvider, - ImplementationProvider, - TypeDefinitionProvider, - InlayHintProvider, - CallHierarchyProvider, - FoldingRangeProvider, - OnWatchFileChanges, - CompletionsProvider, - UpdateTsOrJsFile -{ - __name = 'ts'; - private readonly configManager: LSConfigManager; - private readonly lsAndTsDocResolver: LSAndTSDocResolver; - private readonly completionProvider: CompletionsProviderImpl; - private readonly codeActionsProvider: CodeActionsProviderImpl; - private readonly updateImportsProvider: UpdateImportsProviderImpl; - private readonly diagnosticsProvider: DiagnosticsProviderImpl; - private readonly renameProvider: RenameProviderImpl; - private readonly hoverProvider: HoverProviderImpl; - private readonly findReferencesProvider: FindReferencesProviderImpl; - private readonly findFileReferencesProvider: FindFileReferencesProviderImpl; - private readonly findComponentReferencesProvider: FindComponentReferencesProviderImpl; - - private readonly selectionRangeProvider: SelectionRangeProviderImpl; - private readonly signatureHelpProvider: SignatureHelpProviderImpl; - private readonly semanticTokensProvider: SemanticTokensProviderImpl; - private readonly implementationProvider: ImplementationProviderImpl; - private readonly typeDefinitionProvider: TypeDefinitionProviderImpl; - private readonly inlayHintProvider: InlayHintProviderImpl; - private readonly foldingRangeProvider: FoldingRangeProviderImpl; - private readonly callHierarchyProvider: CallHierarchyProviderImpl; - - constructor( - configManager: LSConfigManager, - lsAndTsDocResolver: LSAndTSDocResolver, - workspaceUris: string[] - ) { - this.configManager = configManager; - this.lsAndTsDocResolver = lsAndTsDocResolver; - this.completionProvider = new CompletionsProviderImpl( - this.lsAndTsDocResolver, - this.configManager - ); - this.codeActionsProvider = new CodeActionsProviderImpl( - this.lsAndTsDocResolver, - this.completionProvider, - configManager - ); - this.updateImportsProvider = new UpdateImportsProviderImpl(this.lsAndTsDocResolver); - this.diagnosticsProvider = new DiagnosticsProviderImpl( - this.lsAndTsDocResolver, - configManager - ); - this.renameProvider = new RenameProviderImpl(this.lsAndTsDocResolver, configManager); - this.hoverProvider = new HoverProviderImpl(this.lsAndTsDocResolver); - this.findFileReferencesProvider = new FindFileReferencesProviderImpl( - this.lsAndTsDocResolver - ); - this.findComponentReferencesProvider = new FindComponentReferencesProviderImpl( - this.lsAndTsDocResolver - ); - this.findReferencesProvider = new FindReferencesProviderImpl( - this.lsAndTsDocResolver, - this.findComponentReferencesProvider - ); - this.selectionRangeProvider = new SelectionRangeProviderImpl(this.lsAndTsDocResolver); - this.signatureHelpProvider = new SignatureHelpProviderImpl(this.lsAndTsDocResolver); - this.semanticTokensProvider = new SemanticTokensProviderImpl(this.lsAndTsDocResolver); - this.implementationProvider = new ImplementationProviderImpl(this.lsAndTsDocResolver); - this.typeDefinitionProvider = new TypeDefinitionProviderImpl(this.lsAndTsDocResolver); - this.inlayHintProvider = new InlayHintProviderImpl(this.lsAndTsDocResolver); - this.callHierarchyProvider = new CallHierarchyProviderImpl( - this.lsAndTsDocResolver, - workspaceUris - ); - this.foldingRangeProvider = new FoldingRangeProviderImpl( - this.lsAndTsDocResolver, - configManager - ); - } - - async getDiagnostics( - document: Document, - cancellationToken?: CancellationToken - ): Promise { - if (!this.featureEnabled('diagnostics')) { - return []; - } - - return this.diagnosticsProvider.getDiagnostics(document, cancellationToken); - } - - async doHover(document: Document, position: Position): Promise { - if (!this.featureEnabled('hover')) { - return null; - } - - return this.hoverProvider.doHover(document, position); - } - - async getDocumentSymbols( - document: Document, - cancellationToken?: CancellationToken - ): Promise { - if (!this.featureEnabled('documentSymbols')) { - return []; - } - - const { lang, tsDoc } = await this.lsAndTsDocResolver.getLsForSyntheticOperations(document); - - if (cancellationToken?.isCancellationRequested) { - return []; - } - - const navTree = lang.getNavigationTree(tsDoc.filePath); - - const symbols: SymbolInformation[] = []; - collectSymbols(navTree, undefined, (symbol) => symbols.push(symbol)); - - const topContainerName = symbols[0].name; - const result: SymbolInformation[] = []; - - for (let symbol of symbols.slice(1)) { - if (symbol.containerName === topContainerName) { - symbol.containerName = 'script'; - } - - symbol = mapSymbolInformationToOriginal(tsDoc, symbol); - - if ( - symbol.location.range.start.line < 0 || - symbol.location.range.end.line < 0 || - isZeroLengthRange(symbol.location.range) || - symbol.name.startsWith('__sveltets_') - ) { - continue; - } - - if ( - (symbol.kind === SymbolKind.Property || symbol.kind === SymbolKind.Method) && - !isInScript(symbol.location.range.start, document) - ) { - if ( - symbol.name === 'props' && - document.getText().charAt(document.offsetAt(symbol.location.range.start)) !== - 'p' - ) { - // This is the "props" of a generated component constructor - continue; - } - const node = tsDoc.svelteNodeAt(symbol.location.range.start); - if ( - (node && (isAttributeName(node) || isAttributeShorthand(node))) || - isEventHandler(node) - ) { - // This is a html or component property, they are not treated as a new symbol - // in JSX and so we do the same for the new transformation. - continue; - } - } - - if (symbol.name === '') { - let name = getTextInRange(symbol.location.range, document.getText()).trimLeft(); - if (name.length > 50) { - name = name.substring(0, 50) + '...'; - } - symbol.name = name; - } - - if (symbol.name.startsWith('$$_')) { - if (!symbol.name.includes('$on')) { - continue; - } - // on:foo={() => ''} -> $on("foo") callback - symbol.name = symbol.name.substring(symbol.name.indexOf('$on')); - } - - result.push(symbol); - } - - return result; - - function collectSymbols( - tree: NavigationTree, - container: string | undefined, - cb: (symbol: SymbolInformation) => void - ) { - const start = tree.spans[0]; - const end = tree.spans[tree.spans.length - 1]; - if (start && end) { - cb( - SymbolInformation.create( - tree.text, - symbolKindFromString(tree.kind), - Range.create( - tsDoc.positionAt(start.start), - tsDoc.positionAt(end.start + end.length) - ), - tsDoc.getURL(), - container - ) - ); - } - if (tree.childItems) { - for (const child of tree.childItems) { - collectSymbols(child, tree.text, cb); - } - } - } - } - - async getCompletions( - document: Document, - position: Position, - completionContext?: CompletionContext, - cancellationToken?: CancellationToken - ): Promise | null> { - if (!this.featureEnabled('completions')) { - return null; - } - - const tsDirectiveCommentCompletions = getDirectiveCommentCompletions( - position, - document, - completionContext - ); - - const completions = await this.completionProvider.getCompletions( - document, - position, - completionContext, - cancellationToken - ); - - if (completions && tsDirectiveCommentCompletions) { - return CompletionList.create( - completions.items.concat(tsDirectiveCommentCompletions.items), - completions.isIncomplete - ); - } - - return completions ?? tsDirectiveCommentCompletions; - } - - async resolveCompletion( - document: Document, - completionItem: AppCompletionItem, - cancellationToken?: CancellationToken - ): Promise> { - return this.completionProvider.resolveCompletion( - document, - completionItem, - cancellationToken - ); - } - - async getDefinitions(document: Document, position: Position): Promise { - const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); - - const defs = lang.getDefinitionAndBoundSpan( - tsDoc.filePath, - tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)) - ); - - if (!defs || !defs.definitions) { - return []; - } - - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); - snapshots.set(tsDoc.filePath, tsDoc); - - const result = await Promise.all( - defs.definitions.map(async (def) => { - if (def.fileName.endsWith('svelte-shims.d.ts')) { - return; - } - - let snapshot = await snapshots.retrieve(def.fileName); - - // Go from generated $store to store if user wants to find definition for $store - if (isTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) { - if ( - !is$storeVariableIn$storeDeclaration( - snapshot.getFullText(), - def.textSpan.start - ) - ) { - return; - } - // there will be exactly one definition, the store - def = lang.getDefinitionAndBoundSpan( - tsDoc.filePath, - tsDoc.getFullText().indexOf(');', def.textSpan.start) - 1 - )!.definitions![0]; - snapshot = await snapshots.retrieve(def.fileName); - } - - const defLocation = convertToLocationForReferenceOrDefinition( - snapshot, - def.textSpan - ); - return LocationLink.create( - defLocation.uri, - defLocation.range, - defLocation.range, - convertToLocationRange(tsDoc, defs.textSpan) - ); - }) - ); - return result.filter(isNotNullOrUndefined); - } - - async prepareRename(document: Document, position: Position): Promise { - return this.renameProvider.prepareRename(document, position); - } - - async rename( - document: Document, - position: Position, - newName: string - ): Promise { - return this.renameProvider.rename(document, position, newName); - } - - async getCodeActions( - document: Document, - range: Range, - context: CodeActionContext, - cancellationToken?: CancellationToken - ): Promise { - if (!this.featureEnabled('codeActions')) { - return []; - } - - return this.codeActionsProvider.getCodeActions(document, range, context, cancellationToken); - } - - async resolveCodeAction( - document: Document, - codeAction: CodeAction, - cancellationToken?: CancellationToken | undefined - ): Promise { - return this.codeActionsProvider.resolveCodeAction(document, codeAction, cancellationToken); - } - - async executeCommand( - document: Document, - command: string, - args?: any[] - ): Promise { - if (!this.featureEnabled('codeActions')) { - return null; - } - - return this.codeActionsProvider.executeCommand(document, command, args); - } - - async updateImports(fileRename: FileRename): Promise { - if ( - !( - this.configManager.enabled('svelte.enable') && - this.configManager.enabled('svelte.rename.enable') - ) - ) { - return null; - } - - return this.updateImportsProvider.updateImports(fileRename); - } - - async findReferences( - document: Document, - position: Position, - context: ReferenceContext - ): Promise { - return this.findReferencesProvider.findReferences(document, position, context); - } - - async fileReferences(uri: string): Promise { - return this.findFileReferencesProvider.fileReferences(uri); - } - - async findComponentReferences(uri: string): Promise { - return this.findComponentReferencesProvider.findComponentReferences(uri); - } - - async onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): Promise { - let doneUpdateProjectFiles = false; - - for (const { fileName, changeType } of onWatchFileChangesParas) { - const pathParts = fileName.split(/\/|\\/); - const dirPathParts = pathParts.slice(0, pathParts.length - 1); - const declarationExtensions = [ts.Extension.Dcts, ts.Extension.Dts, ts.Extension.Dmts]; - const canSafelyIgnore = - declarationExtensions.every((ext) => !fileName.endsWith(ext)) && - ignoredBuildDirectories.some((dir) => { - const index = dirPathParts.indexOf(dir); - - return ( - // Files in .svelte-kit/types should always come through - index > 0 && (dir !== '.svelte-kit' || dirPathParts[index + 1] !== 'types') - ); - }); - if (canSafelyIgnore) { - continue; - } - - const scriptKind = getScriptKindFromFileName(fileName); - if (scriptKind === ts.ScriptKind.Unknown) { - // We don't deal with svelte files here - continue; - } - - if (changeType === FileChangeType.Deleted) { - await this.lsAndTsDocResolver.deleteSnapshot(fileName); - continue; - } - - if (changeType === FileChangeType.Created) { - if (!doneUpdateProjectFiles) { - doneUpdateProjectFiles = true; - await this.lsAndTsDocResolver.updateProjectFiles(); - } - await this.lsAndTsDocResolver.invalidateModuleCache(fileName); - continue; - } - - await this.lsAndTsDocResolver.updateExistingTsOrJsFile(fileName); - } - } - - async updateTsOrJsFile( - fileName: string, - changes: TextDocumentContentChangeEvent[] - ): Promise { - await this.lsAndTsDocResolver.updateExistingTsOrJsFile(fileName, changes); - } - - async getSelectionRange( - document: Document, - position: Position - ): Promise { - if (!this.featureEnabled('selectionRange')) { - return null; - } - - return this.selectionRangeProvider.getSelectionRange(document, position); - } - - async getSignatureHelp( - document: Document, - position: Position, - context: SignatureHelpContext | undefined, - cancellationToken?: CancellationToken - ): Promise { - if (!this.featureEnabled('signatureHelp')) { - return null; - } - - return this.signatureHelpProvider.getSignatureHelp( - document, - position, - context, - cancellationToken - ); - } - - async getSemanticTokens( - textDocument: Document, - range?: Range, - cancellationToken?: CancellationToken - ): Promise { - if (!this.featureEnabled('semanticTokens')) { - return { - data: [] - }; - } - - return this.semanticTokensProvider.getSemanticTokens( - textDocument, - range, - cancellationToken - ); - } - - async getImplementation(document: Document, position: Position): Promise { - return this.implementationProvider.getImplementation(document, position); - } - - async getTypeDefinition(document: Document, position: Position): Promise { - return this.typeDefinitionProvider.getTypeDefinition(document, position); - } - - async getInlayHints( - document: Document, - range: Range, - cancellationToken?: CancellationToken - ): Promise { - if (!this.configManager.enabled('typescript.enable')) { - return null; - } - - return this.inlayHintProvider.getInlayHints(document, range, cancellationToken); - } - - prepareCallHierarchy( - document: Document, - position: Position, - cancellationToken?: CancellationToken - ): Promise { - return this.callHierarchyProvider.prepareCallHierarchy( - document, - position, - cancellationToken - ); - } - - getIncomingCalls( - item: CallHierarchyItem, - cancellationToken?: CancellationToken | undefined - ): Promise { - return this.callHierarchyProvider.getIncomingCalls(item, cancellationToken); - } - - async getOutgoingCalls( - item: CallHierarchyItem, - cancellationToken?: CancellationToken | undefined - ): Promise { - return this.callHierarchyProvider.getOutgoingCalls(item, cancellationToken); - } - - async getFoldingRanges(document: Document): Promise { - return this.foldingRangeProvider.getFoldingRanges(document); - } - - /** - * @internal Public for tests only - */ - public getSnapshotManager(fileName: string) { - return this.lsAndTsDocResolver.getSnapshotManager(fileName); - } - - private featureEnabled(feature: keyof LSTypescriptConfig) { - return ( - this.configManager.enabled('typescript.enable') && - this.configManager.enabled(`typescript.${feature}.enable`) - ); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts b/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts deleted file mode 100644 index b58e797a2..000000000 --- a/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts +++ /dev/null @@ -1,471 +0,0 @@ -import path, { basename, dirname } from 'path'; -import ts from 'typescript'; -import { CancellationToken, Range, SymbolKind, SymbolTag } from 'vscode-languageserver'; -import { - CallHierarchyIncomingCall, - CallHierarchyItem, - CallHierarchyOutgoingCall, - Position -} from 'vscode-languageserver-types'; -import { Document, mapRangeToOriginal } from '../../../lib/documents'; -import { - createGetCanonicalFileName, - isNotNullOrUndefined, - pathToUrl, - urlToPath -} from '../../../utils'; -import { CallHierarchyProvider } from '../../interfaces'; -import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { - convertRange, - getNearestWorkspaceUri, - isGeneratedSvelteComponentName, - isSvelteFilePath, - offsetOfGeneratedComponentExport, - symbolKindFromString, - toGeneratedSvelteComponentName -} from '../utils'; -import { findNodeAtSpan, gatherDescendants, SnapshotMap } from './utils'; - -const ENSURE_COMPONENT_HELPER = '__sveltets_2_ensureComponent'; - -export class CallHierarchyProviderImpl implements CallHierarchyProvider { - constructor( - private readonly lsAndTsDocResolver: LSAndTSDocResolver, - private readonly workspaceUris: string[] - ) {} - - async prepareCallHierarchy( - document: Document, - position: Position, - cancellationToken?: CancellationToken - ): Promise { - const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); - - if (cancellationToken?.isCancellationRequested) { - return null; - } - - const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); - const items = lang.prepareCallHierarchy(tsDoc.filePath, offset); - - const itemsArray = Array.isArray(items) ? items : items ? [items] : []; - - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); - snapshots.set(tsDoc.filePath, tsDoc); - - const program = lang.getProgram(); - const result = await Promise.all( - itemsArray.map((item) => this.convertCallHierarchyItem(snapshots, item, program)) - ); - - return result.filter(isNotNullOrUndefined); - } - - private isSourceFileItem(item: ts.CallHierarchyItem) { - return ( - item.kind === ts.ScriptElementKind.scriptElement || - (item.kind === ts.ScriptElementKind.moduleElement && item.selectionSpan.start === 0) - ); - } - - private async convertCallHierarchyItem( - snapshots: SnapshotMap, - item: ts.CallHierarchyItem, - program: ts.Program | undefined - ): Promise { - const snapshot = await snapshots.retrieve(item.file); - - const redirectedCallHierarchyItem = this.redirectCallHierarchyItem(snapshot, program, item); - - if (redirectedCallHierarchyItem) { - return redirectedCallHierarchyItem; - } - - const { name, detail } = this.getNameAndDetailForItem(this.isSourceFileItem(item), item); - - const selectionRange = mapRangeToOriginal( - snapshot, - convertRange(snapshot, item.selectionSpan) - ); - - if (selectionRange.start.line < 0 || selectionRange.end.line < 0) { - return null; - } - - const range = mapRangeToOriginal(snapshot, convertRange(snapshot, item.span)); - - if (range.start.line < 0 || range.end.line < 0) { - return null; - } - - return { - kind: symbolKindFromString(item.kind), - name, - range, - selectionRange, - uri: pathToUrl(item.file), - detail, - tags: item.kindModifiers?.includes('deprecated') ? [SymbolTag.Deprecated] : undefined - }; - } - - private getNameAndDetailForItem(useFileName: boolean, item: ts.CallHierarchyItem) { - const nearestRootUri = getNearestWorkspaceUri( - this.workspaceUris, - item.file, - createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames) - ); - const nearestRoot = nearestRootUri && (urlToPath(nearestRootUri) ?? undefined); - - const name = useFileName ? basename(item.file) : item.name; - const detail = useFileName - ? nearestRoot && path.relative(nearestRoot, dirname(item.file)) - : item.containerName; - return { name, detail }; - } - - async getIncomingCalls( - previousItem: CallHierarchyItem, - cancellationToken?: CancellationToken | undefined - ): Promise { - const prepareResult = await this.prepareFurtherCalls(previousItem, cancellationToken); - if (!prepareResult) { - return null; - } - - const { - lang, - filePath, - program, - snapshots, - isComponentModulePosition, - tsDoc, - getNonComponentOffset - } = prepareResult; - - const componentExportOffset = - isComponentModulePosition && tsDoc instanceof SvelteDocumentSnapshot - ? offsetOfGeneratedComponentExport(tsDoc) - : -1; - const offset = componentExportOffset >= 0 ? componentExportOffset : getNonComponentOffset(); - - const incomingCalls: ts.CallHierarchyIncomingCall[] = lang - .provideCallHierarchyIncomingCalls(filePath, offset) - .concat(this.getInComingCallsForComponent(lang, program, filePath, offset) ?? []); - - const result = await Promise.all( - incomingCalls.map(async (item): Promise => { - const snapshot = await snapshots.retrieve(item.from.file); - const from = await this.convertCallHierarchyItem(snapshots, item.from, program); - - if (!from) { - return null; - } - - return { - from, - fromRanges: this.convertFromRanges(snapshot, item.fromSpans) - }; - }) - ); - - return result.filter(isNotNullOrUndefined); - } - - async getOutgoingCalls( - previousItem: CallHierarchyItem, - cancellationToken?: CancellationToken | undefined - ): Promise { - const prepareResult = await this.prepareFurtherCalls(previousItem, cancellationToken); - if (!prepareResult) { - return null; - } - - const { - lang, - filePath, - program, - snapshots, - isComponentModulePosition, - tsDoc, - getNonComponentOffset - } = prepareResult; - - const sourceFile = program?.getSourceFile(filePath); - const renderFunctionOffset = - isComponentModulePosition && tsDoc instanceof SvelteDocumentSnapshot && sourceFile - ? sourceFile.statements - .find( - (statement): statement is ts.FunctionDeclaration => - ts.isFunctionDeclaration(statement) && - statement.name?.getText() === 'render' - ) - ?.name?.getStart() - : -1; - const offset = - renderFunctionOffset != null && renderFunctionOffset >= 0 - ? renderFunctionOffset - : getNonComponentOffset(); - - const outgoingCalls = lang - .provideCallHierarchyOutgoingCalls(filePath, offset) - .concat( - isComponentModulePosition - ? this.getOutgoingCallsForComponent(program, filePath) ?? [] - : [] - ); - - const result = await Promise.all( - outgoingCalls.map(async (item): Promise => { - if ( - item.to.name.startsWith('__sveltets') || - item.to.containerName === 'svelteHTML' - ) { - return null; - } - - const to = await this.convertCallHierarchyItem(snapshots, item.to, program); - - if (!to) { - return null; - } - return { - to, - fromRanges: this.convertFromRanges(tsDoc, item.fromSpans) - }; - }) - ); - - return result.filter(isNotNullOrUndefined).filter((item) => item.fromRanges.length); - } - - private async prepareFurtherCalls( - item: CallHierarchyItem, - cancellationToken: CancellationToken | undefined - ) { - const filePath = urlToPath(item.uri); - - if (!filePath) { - return null; - } - - const lang = await this.lsAndTsDocResolver.getLSForPath(filePath); - const tsDoc = await this.lsAndTsDocResolver.getSnapshot(filePath); - - if (cancellationToken?.isCancellationRequested) { - return null; - } - - const program = lang.getProgram(); - - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); - snapshots.set(tsDoc.filePath, tsDoc); - - const isComponentModulePosition = - isSvelteFilePath(item.name) && - item.selectionRange.start.line === 0 && - item.range.start.line === 0; - - return { - snapshots, - filePath, - program, - tsDoc, - lang, - isComponentModulePosition, - getNonComponentOffset: () => - tsDoc.offsetAt(tsDoc.getGeneratedPosition(item.selectionRange.start)) - }; - } - - private redirectCallHierarchyItem( - snapshot: DocumentSnapshot, - program: ts.Program | undefined, - item: ts.CallHierarchyItem - ): CallHierarchyItem | null { - if ( - !isSvelteFilePath(item.file) || - !program || - !(snapshot instanceof SvelteDocumentSnapshot) - ) { - return null; - } - - const sourceFile = program.getSourceFile(item.file); - - if (!sourceFile) { - return null; - } - - if (isGeneratedSvelteComponentName(item.name)) { - return this.toComponentCallHierarchyItem(snapshot, item); - } - - if (item.name === 'render') { - const end = item.selectionSpan.start + item.selectionSpan.length; - const renderFunction = sourceFile.statements.find( - (statement) => - statement.getStart() <= item.selectionSpan.start && statement.getEnd() >= end - ); - - if (!renderFunction || !sourceFile.statements.includes(renderFunction)) { - return null; - } - return this.toComponentCallHierarchyItem(snapshot, item); - } - - return null; - } - - private toComponentCallHierarchyItem( - snapshot: SvelteDocumentSnapshot, - item: ts.CallHierarchyItem - ) { - const fileStartPosition = Position.create(0, 0); - const fileRange = Range.create( - fileStartPosition, - snapshot.parent.positionAt(snapshot.parent.getTextLength()) - ); - - return { - ...this.getNameAndDetailForItem(true, item), - kind: SymbolKind.Module, - range: fileRange, - selectionRange: Range.create(fileStartPosition, fileStartPosition), - uri: pathToUrl(item.file) - }; - } - - private convertFromRanges(snapshot: DocumentSnapshot, spans: ts.TextSpan[]) { - return spans - .map((item) => mapRangeToOriginal(snapshot, convertRange(snapshot, item))) - .filter((range) => range.start.line >= 0 && range.end.line >= 0); - } - - private getInComingCallsForComponent( - lang: ts.LanguageService, - program: ts.Program | undefined, - filePath: string, - offset: number - ): ts.CallHierarchyIncomingCall[] | null { - if (!program || !isSvelteFilePath(filePath)) { - return null; - } - - const groups = lang - .findReferences(filePath, offset) - ?.map( - (entry) => - [ - entry.definition.fileName, - entry.references - .map((ref) => this.getComponentStartTagFromReference(program, ref)) - .filter(isNotNullOrUndefined) - ] as const - ) - .filter(([_, group]) => group.length); - - return ( - groups?.map(([file, group]) => ({ - from: { - file, - kind: ts.ScriptElementKind.scriptElement, - name: toGeneratedSvelteComponentName(''), - // doesn't matter, will be override later - selectionSpan: { start: 0, length: 0 }, - span: { start: 0, length: 0 } - }, - fromSpans: group.map((g) => g.textSpan) - })) ?? null - ); - } - - private getComponentStartTagFromReference( - program: ts.Program, - ref: ts.ReferenceEntry - ): ts.ReferenceEntry | null { - const sourceFile = program.getSourceFile(ref.fileName); - - if (!sourceFile) { - return null; - } - - const node = findNodeAtSpan(sourceFile, ref.textSpan, this.isComponentStartTag); - - if (node) { - return ref; - } - - return null; - } - - private isComponentStartTag(node: ts.Node | undefined): node is ts.Identifier { - return ( - !!node && - node.parent && - ts.isCallExpression(node.parent) && - ts.isIdentifier(node.parent.expression) && - node.parent.expression.text === ENSURE_COMPONENT_HELPER && - ts.isIdentifier(node) && - node === node.parent.arguments[0] - ); - } - - private getOutgoingCallsForComponent( - program: ts.Program | undefined, - filePath: string - ): ts.CallHierarchyOutgoingCall[] | null { - const sourceFile = program?.getSourceFile(filePath); - if (!program || !sourceFile) { - return null; - } - - const groups = new Map(); - - const startTags = gatherDescendants(sourceFile, this.isComponentStartTag); - const typeChecker = program.getTypeChecker(); - - for (const startTag of startTags) { - const type = typeChecker.getTypeAtLocation(startTag); - const symbol = type.aliasSymbol ?? type.symbol; - const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0]; - - if (!declaration || !ts.isClassDeclaration(declaration)) { - continue; - } - - let group = groups.get(declaration); - - if (!group) { - group = []; - groups.set(declaration, group); - } - - group.push({ start: startTag.getStart(), length: startTag.getWidth() }); - } - - return ( - Array.from(groups).map(([declaration, group]) => { - const file = declaration.getSourceFile().fileName; - const name = declaration.name?.getText() ?? basename(file); - const span = { start: declaration.getStart(), length: declaration.getWidth() }; - const selectionSpan = declaration.name - ? { start: declaration.name.getStart(), length: declaration.name.getWidth() } - : span; - - return { - to: { - file, - kind: ts.ScriptElementKind.classElement, - name, - selectionSpan, - span - }, - fromSpans: group - }; - }) ?? null - ); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts deleted file mode 100644 index f12f8427a..000000000 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ /dev/null @@ -1,1525 +0,0 @@ -import ts from 'typescript'; -import { - CancellationToken, - CodeAction, - CodeActionContext, - CodeActionKind, - Diagnostic, - LSPAny, - OptionalVersionedTextDocumentIdentifier, - Position, - Range, - TextDocumentEdit, - TextDocumentIdentifier, - TextEdit, - WorkspaceEdit -} from 'vscode-languageserver'; -import { - Document, - getLineAtPosition, - isAtEndOfLine, - isInTag, - isRangeInTag, - mapRangeToOriginal -} from '../../../lib/documents'; -import { LSConfigManager } from '../../../ls-config'; -import { - flatten, - getIndent, - isNotNullOrUndefined, - memoize, - modifyLines, - normalizePath, - pathToUrl, - possiblyComponent, - removeLineWithString -} from '../../../utils'; -import { CodeActionsProvider } from '../../interfaces'; -import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { - changeSvelteComponentName, - convertRange, - isInScript, - toGeneratedSvelteComponentName -} from '../utils'; -import { CompletionsProviderImpl } from './CompletionProvider'; -import { - findClosestContainingNode, - findContainingNode, - FormatCodeBasis, - getFormatCodeBasis, - getNewScriptStartTag, - getQuotePreference, - isTextSpanInGeneratedCode, - SnapshotMap -} from './utils'; -import { DiagnosticCode } from './DiagnosticsProvider'; -import { createGetCanonicalFileName } from '../../../utils'; - -/** - * TODO change this to protocol constant if it's part of the protocol - */ -export const SORT_IMPORT_CODE_ACTION_KIND = 'source.sortImports'; - -interface RefactorArgs { - type: 'refactor'; - refactorName: string; - textRange: ts.TextRange; - originalRange: Range; -} - -interface CustomFixCannotFindNameInfo extends ts.CodeFixAction { - position: Position; -} - -interface QuickFixConversionOptions { - fix: ts.CodeFixAction | CustomFixCannotFindNameInfo; - snapshots: SnapshotMap; - document: Document; - formatCodeSettings: ts.FormatCodeSettings; - formatCodeBasis: FormatCodeBasis; - getDiagnostics: () => Diagnostic[]; - skipAddScriptTag?: boolean; -} - -type FixId = NonNullable; - -interface QuickFixAllResolveInfo extends TextDocumentIdentifier { - fixId: FixId; - fixName: string; -} - -const FIX_IMPORT_FIX_NAME = 'import'; -const FIX_IMPORT_FIX_ID = 'fixMissingImport'; -const FIX_IMPORT_FIX_DESCRIPTION = 'Add all missing imports'; -const nonIdentifierRegex = /[\`\~\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]/; - -export class CodeActionsProviderImpl implements CodeActionsProvider { - constructor( - private readonly lsAndTsDocResolver: LSAndTSDocResolver, - private readonly completionProvider: CompletionsProviderImpl, - private readonly configManager: LSConfigManager - ) {} - - async getCodeActions( - document: Document, - range: Range, - context: CodeActionContext, - cancellationToken?: CancellationToken - ): Promise { - if (context.only?.[0] === CodeActionKind.SourceOrganizeImports) { - return await this.organizeImports(document, cancellationToken); - } - - if (context.only?.[0] === SORT_IMPORT_CODE_ACTION_KIND) { - return await this.organizeImports( - document, - cancellationToken, - /**skipDestructiveCodeActions */ true - ); - } - - // for source action command (all source.xxx) - // vscode would show different source code action kinds to choose from - if (context.only?.[0] === CodeActionKind.Source) { - return [ - ...(await this.organizeImports(document, cancellationToken)), - ...(await this.organizeImports( - document, - cancellationToken, - /**skipDestructiveCodeActions */ true - )) - ]; - } - - if ( - context.diagnostics.length && - (!context.only || context.only.includes(CodeActionKind.QuickFix)) - ) { - return await this.applyQuickfix(document, range, context, cancellationToken); - } - - if (!context.only || context.only.includes(CodeActionKind.Refactor)) { - return await this.getApplicableRefactors(document, range, cancellationToken); - } - - return []; - } - - async resolveCodeAction( - document: Document, - codeAction: CodeAction, - cancellationToken?: CancellationToken | undefined - ): Promise { - if (!this.isQuickFixAllResolveInfo(codeAction.data)) { - return codeAction; - } - - const { lang, tsDoc, userPreferences } = - await this.lsAndTsDocResolver.getLSAndTSDoc(document); - if (cancellationToken?.isCancellationRequested) { - return codeAction; - } - - const formatCodeSettings = await this.configManager.getFormatCodeSettingsForFile( - document, - tsDoc.scriptKind - ); - const formatCodeBasis = getFormatCodeBasis(formatCodeSettings); - - const getDiagnostics = memoize(() => - lang.getSemanticDiagnostics(tsDoc.filePath).map( - (dia): Diagnostic => ({ - range: mapRangeToOriginal(tsDoc, convertRange(tsDoc, dia)), - message: '', - code: dia.code - }) - ) - ); - - const isImportFix = codeAction.data.fixName === FIX_IMPORT_FIX_NAME; - const virtualDocInfo = isImportFix - ? await this.createVirtualDocumentForCombinedImportCodeFix( - document, - getDiagnostics(), - tsDoc, - lang - ) - : undefined; - - const fix = lang.getCombinedCodeFix( - { - type: 'file', - fileName: (virtualDocInfo?.virtualDoc ?? document).getFilePath()! - }, - codeAction.data.fixId, - formatCodeSettings, - userPreferences - ); - - if (virtualDocInfo) { - const getCanonicalFileName = createGetCanonicalFileName( - ts.sys.useCaseSensitiveFileNames - ); - - const virtualDocPath = getCanonicalFileName( - normalizePath(virtualDocInfo.virtualDoc.getFilePath()!) - ); - - for (const change of fix.changes) { - if (getCanonicalFileName(normalizePath(change.fileName)) === virtualDocPath) { - change.fileName = tsDoc.filePath; - - this.removeDuplicatedComponentImport(virtualDocInfo.insertedNames, change); - } - } - - await this.lsAndTsDocResolver.deleteSnapshot(virtualDocPath); - } - - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); - const fixActions: ts.CodeFixAction[] = [ - { - fixName: codeAction.data.fixName, - changes: Array.from(fix.changes), - description: '' - } - ]; - - const documentChangesPromises = fixActions.map((fix) => - this.convertAndFixCodeFixAction({ - document, - fix, - formatCodeBasis, - formatCodeSettings, - getDiagnostics, - snapshots, - skipAddScriptTag: true - }) - ); - const documentChanges = (await Promise.all(documentChangesPromises)).flat(); - - if (cancellationToken?.isCancellationRequested) { - return codeAction; - } - - if (isImportFix) { - this.fixCombinedImportQuickFix(documentChanges, document, formatCodeBasis); - } - - codeAction.edit = { - documentChanges - }; - - return codeAction; - } - - /** - * Do not use this in regular code action - * This'll cause TypeScript to rebuild and invalidate caches every time. It'll be slow - */ - private async createVirtualDocumentForCombinedImportCodeFix( - document: Document, - diagnostics: Diagnostic[], - tsDoc: DocumentSnapshot, - lang: ts.LanguageService - ) { - const virtualUri = document.uri + '.__virtual__.svelte'; - const names = new Set(); - const sourceFile = lang.getProgram()?.getSourceFile(tsDoc.filePath); - if (!sourceFile) { - return undefined; - } - - for (const diagnostic of diagnostics) { - if ( - diagnostic.range.start.line < 0 || - diagnostic.range.end.line < 0 || - diagnostic.code !== DiagnosticCode.CANNOT_FIND_NAME - ) { - continue; - } - const identifier = this.findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile); - const name = identifier?.text; - if (!name || names.has(name)) { - continue; - } - - if (name.startsWith('$')) { - names.add(name.slice(1)); - } else if (!isInScript(diagnostic.range.start, document)) { - if (this.isComponentStartTag(identifier)) { - names.add(toGeneratedSvelteComponentName(name)); - } - } - } - - if (!names.size) { - return undefined; - } - - const inserts = Array.from(names.values()) - .map((name) => name + ';') - .join(''); - - // assumption: imports are always at the top of the script tag - // so these appends won't change the position of the edits - const text = document.getText(); - const newText = document.scriptInfo - ? text.slice(0, document.scriptInfo.end) + inserts + text.slice(document.scriptInfo.end) - : `${document.getText()}`; - - const virtualDoc = new Document(virtualUri, newText); - virtualDoc.openedByClient = true; - // let typescript know about the virtual document - await this.lsAndTsDocResolver.getSnapshot(virtualDoc); - - return { - virtualDoc: new Document(virtualUri, newText), - insertedNames: names - }; - } - - /** - * Remove component default import if there is a named import with the same name - * Usually happens with reexport or inheritance of component library - */ - private removeDuplicatedComponentImport( - insertedNames: Set, - change: ts.FileTextChanges - ) { - for (const name of insertedNames) { - const unSuffixedNames = changeSvelteComponentName(name); - const matchRegex = unSuffixedNames != name && this.toImportMemberRegex(unSuffixedNames); - if ( - !matchRegex || - !change.textChanges.some((textChange) => textChange.newText.match(matchRegex)) - ) { - continue; - } - - const importRegex = new RegExp(`\\s+import ${name} from ('|")(.*)('|");?\r?\n?`); - change.textChanges = change.textChanges - .map((textChange) => ({ - ...textChange, - newText: textChange.newText.replace(importRegex, (match) => { - if (match.split('\n').length > 2) { - return '\n'; - } else { - return ''; - } - }) - })) - // in case there are replacements - .filter((change) => change.span.length || change.newText); - } - } - - private fixCombinedImportQuickFix( - documentChanges: TextDocumentEdit[], - document: Document, - formatCodeBasis: FormatCodeBasis - ) { - if (!documentChanges.length || document.scriptInfo || document.moduleScriptInfo) { - return; - } - - const editForThisFile = documentChanges.find( - (change) => change.textDocument.uri === document.uri - ); - - if (editForThisFile?.edits.length) { - const [first] = editForThisFile.edits; - first.newText = - getNewScriptStartTag(this.configManager.getConfig()) + - formatCodeBasis.baseIndent + - first.newText.trimStart(); - - const last = editForThisFile.edits[editForThisFile.edits.length - 1]; - last.newText = last.newText + '' + formatCodeBasis.newLine; - } - } - - private toImportMemberRegex(name: string) { - return new RegExp(`${name}($| |,)`); - } - - private isQuickFixAllResolveInfo(data: LSPAny): data is QuickFixAllResolveInfo { - const asserted = data as QuickFixAllResolveInfo | undefined; - return asserted?.fixId != undefined && typeof asserted.fixName === 'string'; - } - - private async organizeImports( - document: Document, - cancellationToken: CancellationToken | undefined, - skipDestructiveCodeActions = false - ): Promise { - if (!document.scriptInfo && !document.moduleScriptInfo) { - return []; - } - - const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document); - - if (cancellationToken?.isCancellationRequested || tsDoc.parserError) { - // If there's a parser error, we fall back to only the script contents, - // so organize imports likely throws out a lot of seemingly unused imports - // because they are only used in the template. Therefore do nothing in this case. - return []; - } - - const changes = lang.organizeImports( - { - fileName: tsDoc.filePath, - type: 'file', - skipDestructiveCodeActions - }, - { - ...(await this.configManager.getFormatCodeSettingsForFile( - document, - tsDoc.scriptKind - )), - - // handle it on our own - baseIndentSize: undefined - }, - userPreferences - ); - - const documentChanges = await Promise.all( - changes.map(async (change) => { - // Organize Imports will only affect the current file, so no need to check the file path - return TextDocumentEdit.create( - OptionalVersionedTextDocumentIdentifier.create(document.url, null), - change.textChanges - .map((edit) => { - const range = this.checkRemoveImportCodeActionRange( - edit, - tsDoc, - mapRangeToOriginal(tsDoc, convertRange(tsDoc, edit.span)) - ); - - edit.newText = removeLineWithString( - edit.newText, - 'SvelteComponentTyped as __SvelteComponentTyped__' - ); - - return this.fixIndentationOfImports( - TextEdit.replace(range, edit.newText), - document - ); - }) - .filter( - (edit) => - // The __SvelteComponentTyped__ import is added by us and will have a negative mapped line - edit.range.start.line !== -1 - ) - ); - }) - ); - - return [ - CodeAction.create( - skipDestructiveCodeActions ? 'Sort Imports' : 'Organize Imports', - { documentChanges }, - skipDestructiveCodeActions - ? SORT_IMPORT_CODE_ACTION_KIND - : CodeActionKind.SourceOrganizeImports - ) - ]; - } - - private fixIndentationOfImports(edit: TextEdit, document: Document): TextEdit { - // "Organize Imports" will have edits that delete a group of imports by return empty edits - // and one edit which contains all the organized imports of the group. Fix indentation - // of that one by prepending all lines with the indentation of the first line. - const { newText, range } = edit; - if (!newText || range.start.character === 0) { - return edit; - } - - const line = getLineAtPosition(range.start, document.getText()); - const leadingChars = line.substring(0, range.start.character); - if (leadingChars.trim() !== '') { - return edit; - } - - const fixedNewText = modifyLines(edit.newText, (line, idx) => - idx === 0 || !line ? line : leadingChars + line - ); - - if (range.end.character > 0) { - const endLine = getLineAtPosition(range.end, document.getText()); - const isIndent = !endLine.substring(0, range.end.character).trim(); - - if (isIndent) { - const trimmedEndLine = endLine.trim(); - - // imports that would be removed by the next delete edit - if (trimmedEndLine && !trimmedEndLine.startsWith('import')) { - range.end.character = 0; - } - } - } - - return TextEdit.replace(range, fixedNewText); - } - - private checkRemoveImportCodeActionRange( - edit: ts.TextChange, - snapshot: DocumentSnapshot, - range: Range - ) { - // Handle svelte2tsx wrong import mapping: - // The character after the last import maps to the start of the script - // TODO find a way to fix this in svelte2tsx and then remove this - if ( - (range.end.line === 0 && range.end.character === 1) || - range.end.line < range.start.line - ) { - edit.span.length -= 1; - range = mapRangeToOriginal(snapshot, convertRange(snapshot, edit.span)); - - if (!(snapshot instanceof SvelteDocumentSnapshot)) { - range.end.character += 1; - return range; - } - - const line = getLineAtPosition(range.end, snapshot.getOriginalText()); - // remove-import code action will removes the - // line break generated by svelte2tsx, - // but when there's no line break in the source - // move back to next character would remove the next character - if ([';', '"', "'"].includes(line[range.end.character])) { - range.end.character += 1; - } - - if (isAtEndOfLine(line, range.end.character)) { - range.end.line += 1; - range.end.character = 0; - } - } - - return range; - } - - private async applyQuickfix( - document: Document, - range: Range, - context: CodeActionContext, - cancellationToken: CancellationToken | undefined - ) { - const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document); - - if (cancellationToken?.isCancellationRequested) { - return []; - } - - const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.start)); - const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.end)); - const errorCodes: number[] = context.diagnostics.map((diag) => Number(diag.code)); - const cannotFindNameDiagnostic = context.diagnostics.filter( - (diagnostic) => diagnostic.code === DiagnosticCode.CANNOT_FIND_NAME - ); - - const formatCodeSettings = await this.configManager.getFormatCodeSettingsForFile( - document, - tsDoc.scriptKind - ); - const formatCodeBasis = getFormatCodeBasis(formatCodeSettings); - - let codeFixes: Array | undefined = - cannotFindNameDiagnostic.length - ? this.getComponentImportQuickFix( - document, - lang, - tsDoc, - userPreferences, - cannotFindNameDiagnostic, - formatCodeSettings - ) - : undefined; - codeFixes = - // either-or situation - codeFixes || [ - ...lang.getCodeFixesAtPosition( - tsDoc.filePath, - start, - end, - errorCodes, - formatCodeSettings, - userPreferences - ), - ...this.getSvelteQuickFixes( - lang, - document, - cannotFindNameDiagnostic, - tsDoc, - formatCodeBasis, - userPreferences, - formatCodeSettings - ) - ]; - - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); - snapshots.set(tsDoc.filePath, tsDoc); - - const codeActionsPromises = codeFixes.map(async (fix) => { - const documentChanges = await this.convertAndFixCodeFixAction({ - fix, - snapshots, - document, - formatCodeSettings, - formatCodeBasis, - getDiagnostics: () => context.diagnostics - }); - - const codeAction = CodeAction.create( - fix.description, - { - documentChanges - }, - CodeActionKind.QuickFix - ); - - return { - fix, - codeAction - }; - }); - - const identifier: TextDocumentIdentifier = { - uri: document.uri - }; - - const codeActions = await Promise.all(codeActionsPromises); - if (cancellationToken?.isCancellationRequested) { - return []; - } - - const codeActionsNotFilteredOut = codeActions.filter( - ({ codeAction }) => - codeAction.edit?.documentChanges?.every( - (change) => (change).edits.length > 0 - ) - ); - - const fixAllActions = this.getFixAllActions( - codeActionsNotFilteredOut.map(({ fix }) => fix), - identifier, - tsDoc.filePath, - lang - ); - - // filter out empty code action - return codeActionsNotFilteredOut.map(({ codeAction }) => codeAction).concat(fixAllActions); - } - - private async convertAndFixCodeFixAction({ - fix, - snapshots, - document, - formatCodeSettings, - formatCodeBasis, - getDiagnostics, - skipAddScriptTag - }: QuickFixConversionOptions) { - const documentChangesPromises = fix.changes.map(async (change) => { - const snapshot = await snapshots.retrieve(change.fileName); - return TextDocumentEdit.create( - OptionalVersionedTextDocumentIdentifier.create(pathToUrl(change.fileName), null), - change.textChanges - .map((edit) => { - if ( - fix.fixName === FIX_IMPORT_FIX_NAME && - snapshot instanceof SvelteDocumentSnapshot - ) { - const namePosition = 'position' in fix ? fix.position : undefined; - const startPos = - namePosition ?? - this.findDiagnosticForImportFix(document, edit, getDiagnostics()) - ?.range?.start ?? - Position.create(0, 0); - - return this.completionProvider.codeActionChangeToTextEdit( - document, - snapshot, - edit, - true, - startPos, - formatCodeBasis.newLine, - undefined, - skipAddScriptTag - ); - } - - if (isTextSpanInGeneratedCode(snapshot.getFullText(), edit.span)) { - return undefined; - } - - let originalRange = mapRangeToOriginal( - snapshot, - convertRange(snapshot, edit.span) - ); - - if (fix.fixName === 'unusedIdentifier') { - originalRange = this.checkRemoveImportCodeActionRange( - edit, - snapshot, - originalRange - ); - } - - if (fix.fixName === 'fixMissingFunctionDeclaration') { - const position = 'position' in fix ? fix.position : undefined; - const checkRange = position - ? Range.create(position, position) - : this.findDiagnosticForQuickFix( - document, - DiagnosticCode.CANNOT_FIND_NAME, - getDiagnostics(), - (possiblyIdentifier) => { - return edit.newText.includes( - 'function ' + possiblyIdentifier + '(' - ); - } - )?.range; - - originalRange = this.checkEndOfFileCodeInsert( - originalRange, - checkRange, - document - ); - - // ts doesn't add base indent to the first line - if (formatCodeSettings.baseIndentSize) { - const emptyLine = formatCodeBasis.newLine.repeat(2); - edit.newText = - emptyLine + - formatCodeBasis.baseIndent + - edit.newText.trimLeft(); - } - } - - if (fix.fixName === 'disableJsDiagnostics') { - if (edit.newText.includes('ts-nocheck')) { - return this.checkTsNoCheckCodeInsert(document, edit); - } - - return this.checkDisableJsDiagnosticsCodeInsert( - originalRange, - document, - edit - ); - } - - if (fix.fixName === 'inferFromUsage') { - originalRange = this.checkAddJsDocCodeActionRange( - snapshot, - originalRange, - document - ); - } - - if (fix.fixName === 'fixConvertConstToLet') { - const offset = document.offsetAt(originalRange.start); - const constOffset = document.getText().indexOf('const', offset); - if (constOffset < 0) { - return undefined; - } - const beforeConst = document.getText().slice(0, constOffset); - if ( - beforeConst[beforeConst.length - 1] === '@' && - beforeConst - .slice(0, beforeConst.length - 1) - .trimEnd() - .endsWith('{') - ) { - return undefined; - } - } - - if (originalRange.start.line < 0 || originalRange.end.line < 0) { - return undefined; - } - - return TextEdit.replace(originalRange, edit.newText); - }) - .filter(isNotNullOrUndefined) - ); - }); - const documentChanges = await Promise.all(documentChangesPromises); - return documentChanges; - } - - private findDiagnosticForImportFix( - document: Document, - edit: ts.TextChange, - diagnostics: Diagnostic[] - ) { - return this.findDiagnosticForQuickFix( - document, - DiagnosticCode.CANNOT_FIND_NAME, - diagnostics, - (possibleIdentifier) => - !nonIdentifierRegex.test(possibleIdentifier) && - this.toImportMemberRegex(possibleIdentifier).test(edit.newText) - ); - } - - private findDiagnosticForQuickFix( - document: Document, - targetCode: number, - diagnostics: Diagnostic[], - match: (identifier: string) => boolean - ) { - const diagnostic = diagnostics.find((diagnostic) => { - if (diagnostic.code !== targetCode) { - return false; - } - - const possibleIdentifier = document.getText(diagnostic.range); - if (possibleIdentifier) { - return match(possibleIdentifier); - } - - return false; - }); - - return diagnostic; - } - - private getFixAllActions( - codeFixes: readonly ts.CodeFixAction[], - identifier: TextDocumentIdentifier, - fileName: string, - lang: ts.LanguageService - ) { - const checkedFixIds = new Set(); - const fixAll: CodeAction[] = []; - - for (const codeFix of codeFixes) { - if (!codeFix.fixId || !codeFix.fixAllDescription || checkedFixIds.has(codeFix.fixId)) { - continue; - } - - // we have custom fix for import - // check it again if fix-all might be necessary - if (codeFix.fixName === FIX_IMPORT_FIX_NAME) { - const allCannotFindNameDiagnostics = lang - .getSemanticDiagnostics(fileName) - .filter((diagnostic) => diagnostic.code === DiagnosticCode.CANNOT_FIND_NAME); - - if (allCannotFindNameDiagnostics.length < 2) { - checkedFixIds.add(codeFix.fixId); - continue; - } - } - - const codeAction = CodeAction.create( - codeFix.fixAllDescription, - CodeActionKind.QuickFix - ); - - const data: QuickFixAllResolveInfo = { - ...identifier, - fixName: codeFix.fixName, - fixId: codeFix.fixId - }; - - codeAction.data = data; - checkedFixIds.add(codeFix.fixId); - fixAll.push(codeAction); - } - - return fixAll; - } - - /** - * import quick fix requires the symbol name to be the same as where it's defined. - * But we have suffix on component default export to prevent conflict with - * a local variable. So we use auto-import completion as a workaround here. - */ - private getComponentImportQuickFix( - document: Document, - lang: ts.LanguageService, - tsDoc: DocumentSnapshot, - userPreferences: ts.UserPreferences, - diagnostics: Diagnostic[], - formatCodeSetting: ts.FormatCodeSettings - ): CustomFixCannotFindNameInfo[] | undefined { - const sourceFile = lang.getProgram()?.getSourceFile(tsDoc.filePath); - - if (!sourceFile) { - return; - } - - const nameToPosition = new Map(); - - for (const diagnostic of diagnostics) { - if (isInScript(diagnostic.range.start, document)) { - continue; - } - const possibleIdentifier = document.getText(diagnostic.range); - if ( - !possibleIdentifier || - !possiblyComponent(possibleIdentifier) || - nameToPosition.has(possibleIdentifier) - ) { - continue; - } - - const node = this.findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile); - if (!node || !this.isComponentStartTag(node)) { - return; - } - - const tagNameEnd = node.getEnd(); - const name = node.getText(); - - if (possiblyComponent(name)) { - nameToPosition.set(name, tagNameEnd); - } - } - - if (!nameToPosition.size) { - return; - } - - const result: CustomFixCannotFindNameInfo[] = []; - for (const [name, position] of nameToPosition) { - const errorPreventingUserPreferences = - this.completionProvider.fixUserPreferencesForSvelteComponentImport(userPreferences); - - const resolvedCompletion = (c: ts.CompletionEntry) => - lang.getCompletionEntryDetails( - tsDoc.filePath, - position, - c.name, - formatCodeSetting, - c.source, - errorPreventingUserPreferences, - c.data - ); - - const toFix = (c: ts.CompletionEntryDetails) => - c.codeActions?.map( - (a): CustomFixCannotFindNameInfo => ({ - ...a, - description: changeSvelteComponentName(a.description), - fixName: FIX_IMPORT_FIX_NAME, - fixId: FIX_IMPORT_FIX_ID, - fixAllDescription: FIX_IMPORT_FIX_DESCRIPTION, - position: originalPosition - }) - ) ?? []; - - const completion = lang.getCompletionsAtPosition( - tsDoc.filePath, - position, - userPreferences, - formatCodeSetting - ); - - const entries = completion?.entries - .filter((c) => c.name === name || c.name === toGeneratedSvelteComponentName(name)) - .map(resolvedCompletion) - .sort( - (a, b) => - this.numberOfDirectorySeparators( - ts.displayPartsToString(a?.sourceDisplay ?? []) - ) - - this.numberOfDirectorySeparators( - ts.displayPartsToString(b?.sourceDisplay ?? []) - ) - ) - .filter(isNotNullOrUndefined); - - if (!entries?.length) { - continue; - } - - const originalPosition = tsDoc.getOriginalPosition(tsDoc.positionAt(position)); - const resultForName = entries.flatMap(toFix); - - result.push(...resultForName); - } - - return result; - } - - private isComponentStartTag(node: ts.Identifier) { - return ( - ts.isCallExpression(node.parent) && - ts.isIdentifier(node.parent.expression) && - node.parent.expression.text === '__sveltets_2_ensureComponent' && - ts.isIdentifier(node) - ); - } - - private numberOfDirectorySeparators(path: string) { - return path.split('/').length - 1; - } - - private getSvelteQuickFixes( - lang: ts.LanguageService, - document: Document, - cannotFindNameDiagnostics: Diagnostic[], - tsDoc: DocumentSnapshot, - formatCodeBasis: FormatCodeBasis, - userPreferences: ts.UserPreferences, - formatCodeSettings: ts.FormatCodeSettings - ): CustomFixCannotFindNameInfo[] { - const program = lang.getProgram(); - const sourceFile = program?.getSourceFile(tsDoc.filePath); - if (!program || !sourceFile) { - return []; - } - - const typeChecker = program.getTypeChecker(); - const results: CustomFixCannotFindNameInfo[] = []; - const quote = getQuotePreference(sourceFile, userPreferences); - const getGlobalCompletion = memoize(() => - lang.getCompletionsAtPosition(tsDoc.filePath, 0, userPreferences, formatCodeSettings) - ); - const [tsMajorStr] = ts.version.split('.'); - const tsSupportHandlerQuickFix = parseInt(tsMajorStr) >= 5; - - for (const diagnostic of cannotFindNameDiagnostics) { - const identifier = this.findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile); - - if (!identifier) { - continue; - } - - const isQuickFixTargetTargetStore = identifier?.escapedText.toString().startsWith('$'); - - const fixes: ts.CodeFixAction[] = []; - if (isQuickFixTargetTargetStore) { - fixes.push( - ...this.getSvelteStoreQuickFixes( - identifier, - lang, - tsDoc, - userPreferences, - formatCodeSettings, - getGlobalCompletion - ) - ); - } - - if (!tsSupportHandlerQuickFix) { - const isQuickFixTargetEventHandler = this.isQuickFixForEventHandler( - document, - diagnostic - ); - if (isQuickFixTargetEventHandler) { - fixes.push( - ...this.getEventHandlerQuickFixes( - identifier, - tsDoc, - typeChecker, - quote, - formatCodeBasis - ) - ); - } - } - - if (!fixes.length) { - continue; - } - - const originalPosition = tsDoc.getOriginalPosition(tsDoc.positionAt(identifier.pos)); - results.push( - ...fixes.map((fix) => ({ - name: identifier.getText(), - position: originalPosition, - ...fix - })) - ); - } - - return results; - } - - private findIdentifierForDiagnostic( - tsDoc: DocumentSnapshot, - diagnostic: Diagnostic, - sourceFile: ts.SourceFile - ) { - const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.start)); - const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.end)); - - const identifier = findClosestContainingNode( - sourceFile, - { start, length: end - start }, - ts.isIdentifier - ); - - return identifier; - } - - // TODO: Remove this in late 2023 - // when most users have upgraded to TS 5.0+ - private getSvelteStoreQuickFixes( - identifier: ts.Identifier, - lang: ts.LanguageService, - tsDoc: DocumentSnapshot, - userPreferences: ts.UserPreferences, - formatCodeSettings: ts.FormatCodeSettings, - getCompletions: () => ts.CompletionInfo | undefined - ): ts.CodeFixAction[] { - const storeIdentifier = identifier.escapedText.toString().substring(1); - const completion = getCompletions(); - - if (!completion) { - return []; - } - - const toFix = (c: ts.CompletionEntry) => - lang - .getCompletionEntryDetails( - tsDoc.filePath, - 0, - c.name, - formatCodeSettings, - c.source, - userPreferences, - c.data - ) - ?.codeActions?.map((a) => ({ - ...a, - changes: a.changes.map((change) => { - return { - ...change, - textChanges: change.textChanges.map((textChange) => { - // For some reason, TS sometimes adds the `type` modifier. Remove it. - return { - ...textChange, - newText: textChange.newText.replace(' type ', ' ') - }; - }) - }; - }), - fixName: FIX_IMPORT_FIX_NAME, - fixId: FIX_IMPORT_FIX_ID, - fixAllDescription: FIX_IMPORT_FIX_DESCRIPTION - })) ?? []; - - return flatten(completion.entries.filter((c) => c.name === storeIdentifier).map(toFix)); - } - - /** - * Workaround for TypeScript doesn't provide a quick fix if the signature is typed as union type, like `(() => void) | null` - * We can remove this once TypeScript doesn't have this limitation. - */ - private getEventHandlerQuickFixes( - identifier: ts.Identifier, - tsDoc: DocumentSnapshot, - typeChecker: ts.TypeChecker, - quote: string, - formatCodeBasis: FormatCodeBasis - ): ts.CodeFixAction[] { - const type = identifier && typeChecker.getContextualType(identifier); - - // if it's not union typescript should be able to do it. no need to enhance - if (!type || !type.isUnion()) { - return []; - } - - const nonNullable = type.getNonNullableType(); - - if ( - !( - nonNullable.flags & ts.TypeFlags.Object && - (nonNullable as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous - ) - ) { - return []; - } - - const signature = typeChecker.getSignaturesOfType(nonNullable, ts.SignatureKind.Call)[0]; - - const parameters = signature.parameters.map((p) => { - const declaration = p.valueDeclaration ?? p.declarations?.[0]; - const typeString = declaration - ? typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(p, declaration)) - : ''; - - return { name: p.name, typeString }; - }); - - const returnType = typeChecker.typeToString(signature.getReturnType()); - const useJsDoc = - tsDoc.scriptKind === ts.ScriptKind.JS || tsDoc.scriptKind === ts.ScriptKind.JSX; - const parametersText = ( - useJsDoc - ? parameters.map((p) => p.name) - : parameters.map((p) => p.name + (p.typeString ? ': ' + p.typeString : '')) - ).join(', '); - - const jsDoc = useJsDoc - ? ['/**', ...parameters.map((p) => ` * @param {${p.typeString}} ${p.name}`), ' */'] - : []; - - const newText = [ - ...jsDoc, - `function ${identifier.text}(${parametersText})${ - useJsDoc || returnType === 'any' ? '' : ': ' + returnType - } {`, - formatCodeBasis.indent + - `throw new Error(${quote}Function not implemented.${quote})` + - formatCodeBasis.semi, - '}' - ] - .map((line) => formatCodeBasis.baseIndent + line + formatCodeBasis.newLine) - .join(''); - - return [ - { - description: `Add missing function declaration '${identifier.text}'`, - fixName: 'fixMissingFunctionDeclaration', - changes: [ - { - fileName: tsDoc.filePath, - textChanges: [ - { - newText, - span: { start: 0, length: 0 } - } - ] - } - ] - } - ]; - } - - private isQuickFixForEventHandler(document: Document, diagnostic: Diagnostic) { - const htmlNode = document.html.findNodeAt(document.offsetAt(diagnostic.range.start)); - if ( - !htmlNode.attributes || - !Object.keys(htmlNode.attributes).some((attr) => attr.startsWith('on:')) - ) { - return false; - } - - return true; - } - - private async getApplicableRefactors( - document: Document, - range: Range, - cancellationToken: CancellationToken | undefined - ): Promise { - if ( - !isRangeInTag(range, document.scriptInfo) && - !isRangeInTag(range, document.moduleScriptInfo) - ) { - return []; - } - - // Don't allow refactorings when there is likely a store subscription. - // Reason: Extracting that would lead to svelte2tsx' transformed store representation - // showing up, which will confuse the user. In the long run, we maybe have to - // setup a separate ts language service which only knows of the original script. - const textInRange = document - .getText() - .substring(document.offsetAt(range.start), document.offsetAt(range.end)); - if (textInRange.includes('$')) { - return []; - } - - const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document); - - if (cancellationToken?.isCancellationRequested) { - return []; - } - - const textRange = { - pos: tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.start)), - end: tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.end)) - }; - const applicableRefactors = lang.getApplicableRefactors( - document.getFilePath() || '', - textRange, - userPreferences - ); - - return ( - this.applicableRefactorsToCodeActions(applicableRefactors, document, range, textRange) - // Only allow refactorings from which we know they work - .filter( - (refactor) => - refactor.command?.command.includes('function_scope') || - refactor.command?.command.includes('constant_scope') || - refactor.command?.command === 'Infer function return type' - ) - // The language server also proposes extraction into const/function in module scope, - // which is outside of the render function, which is svelte2tsx-specific and unmapped, - // so it would both not work and confuse the user ("What is this render? Never declared that"). - // So filter out the module scope proposal and rename the render-title - .filter((refactor) => !refactor.title.includes('module scope')) - .map((refactor) => ({ - ...refactor, - title: refactor.title - .replace( - "Extract to inner function in function 'render'", - 'Extract to function' - ) - .replace("Extract to constant in function 'render'", 'Extract to constant') - })) - ); - } - - private applicableRefactorsToCodeActions( - applicableRefactors: ts.ApplicableRefactorInfo[], - document: Document, - originalRange: Range, - textRange: { pos: number; end: number } - ) { - return flatten( - applicableRefactors.map((applicableRefactor) => { - if (applicableRefactor.inlineable === false) { - return [ - CodeAction.create(applicableRefactor.description, { - title: applicableRefactor.description, - command: applicableRefactor.name, - arguments: [ - document.uri, - { - type: 'refactor', - textRange, - originalRange, - refactorName: 'Extract Symbol' - } - ] - }) - ]; - } - - return applicableRefactor.actions.map((action) => { - return CodeAction.create(action.description, { - title: action.description, - command: action.name, - arguments: [ - document.uri, - { - type: 'refactor', - textRange, - originalRange, - refactorName: applicableRefactor.name - } - ] - }); - }); - }) - ); - } - - async executeCommand( - document: Document, - command: string, - args?: any[] - ): Promise { - if (!(args?.[1]?.type === 'refactor')) { - return null; - } - - const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document); - const path = document.getFilePath() || ''; - const { refactorName, originalRange, textRange } = args[1]; - - const edits = lang.getEditsForRefactor( - path, - {}, - textRange, - refactorName, - command, - userPreferences - ); - if (!edits || edits.edits.length === 0) { - return null; - } - - const documentChanges = edits?.edits.map((edit) => - TextDocumentEdit.create( - OptionalVersionedTextDocumentIdentifier.create(document.uri, null), - edit.textChanges.map((edit) => { - const range = mapRangeToOriginal(tsDoc, convertRange(tsDoc, edit.span)); - - return TextEdit.replace( - this.checkEndOfFileCodeInsert(range, originalRange, document), - edit.newText - ); - }) - ) - ); - - return { documentChanges }; - } - - /** - * Some refactorings place the new code at the end of svelte2tsx' render function, - * which is unmapped. In this case, add it to the end of the script tag ourselves. - */ - private checkEndOfFileCodeInsert( - resultRange: Range, - targetRange: Range | undefined, - document: Document - ) { - if (resultRange.start.line < 0 || resultRange.end.line < 0) { - if ( - document.moduleScriptInfo && - (!targetRange || isRangeInTag(targetRange, document.moduleScriptInfo)) - ) { - return Range.create( - document.moduleScriptInfo.endPos, - document.moduleScriptInfo.endPos - ); - } - - if (document.scriptInfo) { - return Range.create(document.scriptInfo.endPos, document.scriptInfo.endPos); - } - } - - // don't add script tag here because the code action is calculated - // when the file is treated as js - // but user might want a ts version of the code action - return resultRange; - } - - private checkTsNoCheckCodeInsert( - document: Document, - edit: ts.TextChange - ): TextEdit | undefined { - const scriptInfo = document.moduleScriptInfo ?? document.scriptInfo; - if (!scriptInfo) { - return undefined; - } - - const newText = ts.sys.newLine + edit.newText; - - return TextEdit.insert(scriptInfo.startPos, newText); - } - - private checkDisableJsDiagnosticsCodeInsert( - originalRange: Range, - document: Document, - edit: ts.TextChange - ): TextEdit | null { - const inModuleScript = isInTag(originalRange.start, document.moduleScriptInfo); - if (!isInTag(originalRange.start, document.scriptInfo) && !inModuleScript) { - return null; - } - - const position = inModuleScript - ? originalRange.start - : this.fixPropsCodeActionRange(originalRange.start, document) ?? originalRange.start; - - // fix the length of trailing indent - const linesOfNewText = edit.newText.split('\n'); - if (/^[ \t]*$/.test(linesOfNewText[linesOfNewText.length - 1])) { - const line = getLineAtPosition(originalRange.start, document.getText()); - const indent = getIndent(line); - linesOfNewText[linesOfNewText.length - 1] = indent; - } - - return TextEdit.insert(position, linesOfNewText.join('\n')); - } - - /** - * svelte2tsx removes export in instance script - */ - private fixPropsCodeActionRange(start: Position, document: Document): Position | undefined { - const documentText = document.getText(); - const offset = document.offsetAt(start); - const exportKeywordOffset = documentText.lastIndexOf('export', offset); - - // export let a; - if ( - exportKeywordOffset < 0 || - documentText.slice(exportKeywordOffset + 'export'.length, offset).trim() - ) { - return; - } - - const charBeforeExport = documentText[exportKeywordOffset - 1]; - if ( - (charBeforeExport !== undefined && !charBeforeExport.trim()) || - charBeforeExport === ';' - ) { - return document.positionAt(exportKeywordOffset); - } - } - - private checkAddJsDocCodeActionRange( - snapshot: DocumentSnapshot, - originalRange: Range, - document: Document - ): Range { - if ( - snapshot.scriptKind !== ts.ScriptKind.JS && - snapshot.scriptKind !== ts.ScriptKind.JSX && - !isInTag(originalRange.start, document.scriptInfo) - ) { - return originalRange; - } - - const position = this.fixPropsCodeActionRange(originalRange.start, document); - - if (position) { - return { - start: position, - end: position - }; - } - - return originalRange; - } - - private async getLSAndTSDoc(document: Document) { - return this.lsAndTsDocResolver.getLSAndTSDoc(document); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts deleted file mode 100644 index 94284de22..000000000 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ /dev/null @@ -1,1023 +0,0 @@ -import { basename, dirname } from 'path'; -import ts from 'typescript'; -import { - CancellationToken, - CompletionContext, - CompletionItem, - CompletionItemKind, - CompletionList, - CompletionTriggerKind, - InsertTextFormat, - MarkupContent, - MarkupKind, - Position, - Range, - TextDocumentIdentifier, - TextEdit -} from 'vscode-languageserver'; -import { - Document, - getNodeIfIsInHTMLStartTag, - getNodeIfIsInStartTag, - getWordRangeAt, - isInTag, - mapCompletionItemToOriginal, - mapRangeToOriginal, - toRange -} from '../../../lib/documents'; -import { AttributeContext, getAttributeContextAtPosition } from '../../../lib/documents/parseHtml'; -import { LSConfigManager } from '../../../ls-config'; -import { - flatten, - getRegExpMatches, - isNotNullOrUndefined, - modifyLines, - pathToUrl -} from '../../../utils'; -import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces'; -import { ComponentInfoProvider, ComponentPartInfo } from '../ComponentInfoProvider'; -import { SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { getMarkdownDocumentation } from '../previewer'; -import { - changeSvelteComponentName, - convertRange, - isInScript, - isGeneratedSvelteComponentName, - scriptElementKindToCompletionItemKind -} from '../utils'; -import { getJsDocTemplateCompletion } from './getJsDocTemplateCompletion'; -import { - getComponentAtPosition, - getFormatCodeBasis, - getNewScriptStartTag, - isKitTypePath, - isPartOfImportStatement -} from './utils'; -import { isInTag as svelteIsInTag } from '../svelte-ast-utils'; - -export interface CompletionEntryWithIdentifier extends ts.CompletionEntry, TextDocumentIdentifier { - position: Position; - __is_sveltekit$typeImport?: boolean; -} - -type validTriggerCharacter = '.' | '"' | "'" | '`' | '/' | '@' | '<' | '#'; - -type LastCompletion = { - key: string; - position: Position; - completionList: AppCompletionList | null; -}; - -export class CompletionsProviderImpl implements CompletionsProvider { - constructor( - private readonly lsAndTsDocResolver: LSAndTSDocResolver, - private readonly configManager: LSConfigManager - ) {} - - /** - * The language service throws an error if the character is not a valid trigger character. - * Also, the completions are worse. - * Therefore, only use the characters the typescript compiler treats as valid. - */ - private readonly validTriggerCharacters = ['.', '"', "'", '`', '/', '@', '<', '#'] as const; - private commitCharacters = ['.', ',', ';', '(']; - /** - * For performance reasons, try to reuse the last completion if possible. - */ - private lastCompletion?: LastCompletion; - - private isValidTriggerCharacter( - character: string | undefined - ): character is validTriggerCharacter { - return this.validTriggerCharacters.includes(character as validTriggerCharacter); - } - - async getCompletions( - document: Document, - position: Position, - completionContext?: CompletionContext, - cancellationToken?: CancellationToken - ): Promise | null> { - if (isInTag(position, document.styleInfo)) { - return null; - } - - const { - lang: langForSyntheticOperations, - tsDoc, - userPreferences - } = await this.lsAndTsDocResolver.getLsForSyntheticOperations(document); - - const filePath = tsDoc.filePath; - if (!filePath) { - return null; - } - - const triggerCharacter = completionContext?.triggerCharacter; - const triggerKind = completionContext?.triggerKind; - - const validTriggerCharacter = this.isValidTriggerCharacter(triggerCharacter) - ? triggerCharacter - : undefined; - const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter; - const isJsDocTriggerCharacter = triggerCharacter === '*'; - const isEventOrSlotLetTriggerCharacter = triggerCharacter === ':'; - - // ignore any custom trigger character specified in server capabilities - // and is not allow by ts - if ( - isCustomTriggerCharacter && - !validTriggerCharacter && - !isJsDocTriggerCharacter && - !isEventOrSlotLetTriggerCharacter - ) { - return null; - } - - if ( - this.canReuseLastCompletion( - this.lastCompletion, - triggerKind, - triggerCharacter, - document, - position - ) - ) { - this.lastCompletion.position = position; - return this.lastCompletion.completionList; - } else { - this.lastCompletion = undefined; - } - - if (!tsDoc.isInGenerated(position)) { - return null; - } - - const originalOffset = document.offsetAt(position); - const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); - - if (isJsDocTriggerCharacter) { - return getJsDocTemplateCompletion(tsDoc, langForSyntheticOperations, filePath, offset); - } - - const svelteNode = tsDoc.svelteNodeAt(originalOffset); - if ( - // Cursor is somewhere in regular HTML text - (svelteNode?.type === 'Text' && - ['Element', 'InlineComponent', 'Fragment', 'SlotTemplate'].includes( - svelteNode.parent?.type as any - )) || - // Cursor is at
|
in which case there's no TextNode inbetween - document.getText().substring(originalOffset - 1, originalOffset + 2) === '> 500 && - svelteNode?.type === 'Element' && - completions[0].kind !== ts.ScriptElementKind.memberVariableElement - ) { - // False global completions inside element start tag - return null; - } - - if ( - completions.length > 500 && - svelteNode?.type === 'InlineComponent' && - this.mightBeAtStartTagWhitespace(document, originalOffset) - ) { - // Very likely false global completions inside component start tag -> narrow - return this.getCompletionListForDirectiveOrProps( - attributeContext, - componentInfo, - document, - wordRange, - eventAndSlotLetCompletions, - tsDoc - ); - } - - // moved here due to perf reasons - const existingImports = this.getExistingImports(document); - const wordRangeStartPosition = document.positionAt(wordRange.start); - const word = document.getText().substring(wordRange.start, wordRange.end); - const fileUrl = pathToUrl(tsDoc.filePath); - const isCompletionInTag = svelteIsInTag(svelteNode, originalOffset); - - const completionItems: CompletionItem[] = eventAndSlotLetCompletions; - const isValidCompletion = createIsValidCompletion(document, position, !!tsDoc.parserError); - const addCompletion = (entry: ts.CompletionEntry, asStore: boolean) => { - if (isValidCompletion(entry)) { - let completion = this.toCompletionItem( - tsDoc, - entry, - fileUrl, - position, - isCompletionInTag, - addCommitCharacters, - asStore, - existingImports - ); - if (completion) { - completionItems.push( - this.fixTextEditRange( - wordRangeStartPosition, - mapCompletionItemToOriginal(tsDoc, completion) - ) - ); - } - } - }; - - // If completion is about a store which is not imported yet, do another - // completion request at the beginning of the file to get all global - // import completions and then filter them down to likely matches. - if (word.charAt(0) === '$') { - const storeName = word.substring(1); - const text = '__sveltets_2_store_get(' + storeName; - if (!tsDoc.getFullText().includes(text)) { - const pos = (tsDoc.scriptInfo || tsDoc.moduleScriptInfo)?.endPos ?? { - line: 0, - character: 0 - }; - const virtualOffset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(pos)); - const storeCompletions = lang.getCompletionsAtPosition( - filePath, - virtualOffset, - { - ...userPreferences, - triggerCharacter: validTriggerCharacter - }, - formatSettings - ); - for (const entry of storeCompletions?.entries || []) { - if (entry.name.startsWith(storeName)) { - addCompletion(entry, true); - } - } - } - } - - for (const entry of completions) { - addCompletion(entry, false); - } - - // Add ./$types imports for SvelteKit since TypeScript is bad at it - if (basename(filePath).startsWith('+')) { - const $typeImports = new Map(); - for (const c of completionItems) { - if (isKitTypePath(c.data?.source)) { - $typeImports.set(c.label, c); - } - } - for (const $typeImport of $typeImports.values()) { - // resolve path from filePath to svelte-kit/types - // src/routes/foo/+page.svelte -> .svelte-kit/types/foo/$types.d.ts - const routesFolder = document.config?.kit?.files?.routes || 'src/routes'; - const relativeFileName = filePath.split(routesFolder)[1]?.slice(1); - if (relativeFileName) { - const relativePath = - dirname(relativeFileName) === '.' ? '' : `${dirname(relativeFileName)}/`; - const modifiedSource = - $typeImport.data.source.split('.svelte-kit/types')[0] + - // note the missing .d.ts at the end - TS wants it that way for some reason - `.svelte-kit/types/${routesFolder}/${relativePath}$types`; - completionItems.push({ - ...$typeImport, - // Ensure it's sorted above the other imports - sortText: !isNaN(Number($typeImport.sortText)) - ? String(Number($typeImport.sortText) - 1) - : $typeImport.sortText, - data: { - ...$typeImport.data, - __is_sveltekit$typeImport: true, - source: modifiedSource, - data: undefined - } - }); - } - } - } - - const completionList = CompletionList.create(completionItems, !!tsDoc.parserError); - this.lastCompletion = { key: document.getFilePath() || '', position, completionList }; - - return completionList; - } - - private mightBeAtStartTagWhitespace(document: Document, originalOffset: number) { - return [' ', ' >', ' /'].includes( - document.getText().substring(originalOffset - 1, originalOffset + 1) - ); - } - - private canReuseLastCompletion( - lastCompletion: LastCompletion | undefined, - triggerKind: number | undefined, - triggerCharacter: string | undefined, - document: Document, - position: Position - ): lastCompletion is LastCompletion { - return ( - !!lastCompletion && - lastCompletion.key === document.getFilePath() && - lastCompletion.position.line === position.line && - ((Math.abs(lastCompletion.position.character - position.character) < 2 && - (triggerKind === CompletionTriggerKind.TriggerForIncompleteCompletions || - // Special case: `.` is a trigger character, but inside import path completions - // it shouldn't trigger another completion because we can reuse the old one - (triggerCharacter === '.' && - isPartOfImportStatement(document.getText(), position)))) || - // `let:` or `on:` -> up to 3 previous characters allowed - (Math.abs(lastCompletion.position.character - position.character) < 4 && - triggerCharacter === ':' && - !!getNodeIfIsInStartTag(document.html, document.offsetAt(position)))) - ); - } - - private getExistingImports(document: Document) { - const rawImports = getRegExpMatches(scriptImportRegex, document.getText()).map((match) => - (match[1] ?? match[2]).split(',') - ); - const tidiedImports = flatten(rawImports).map((match) => match.trim()); - return new Set(tidiedImports); - } - - private getEventAndSlotLetCompletions( - componentInfo: ComponentInfoProvider | null, - document: Document, - attributeContext: AttributeContext | null, - wordRange: { start: number; end: number } - ): Array> { - if (componentInfo === null) { - return []; - } - - if (attributeContext?.inValue) { - return []; - } - - return [ - ...componentInfo - .getEvents() - .map((event) => - this.componentInfoToCompletionEntry( - event, - 'on:', - undefined, - document, - wordRange - ) - ), - ...componentInfo - .getSlotLets() - .map((slot) => - this.componentInfoToCompletionEntry( - slot, - 'let:', - undefined, - document, - wordRange - ) - ) - ]; - } - - private componentInfoToCompletionEntry( - info: ComponentPartInfo[0], - prefix: string, - kind: CompletionItemKind | undefined, - doc: Document, - wordRange: { start: number; end: number } - ): AppCompletionItem { - const { start, end } = wordRange; - const name = prefix + info.name; - return { - label: name, - kind, - sortText: '-1', - detail: info.name + ': ' + info.type, - documentation: info.doc && { kind: MarkupKind.Markdown, value: info.doc }, - textEdit: - start !== end - ? TextEdit.replace(toRange(doc.getText(), start, end), name) - : undefined - }; - } - - private getCompletionListForDirectiveOrProps( - attributeContext: AttributeContext | null, - componentInfo: ComponentInfoProvider | null, - document: Document, - wordRange: { start: number; end: number }, - eventAndSlotLetCompletions: AppCompletionItem[], - tsDoc: SvelteDocumentSnapshot - ) { - const props = - (!attributeContext?.inValue && - componentInfo - ?.getProps() - .map((entry) => - this.componentInfoToCompletionEntry( - entry, - '', - CompletionItemKind.Field, - document, - wordRange - ) - )) || - []; - return CompletionList.create( - [...eventAndSlotLetCompletions, ...props], - !!tsDoc.parserError - ); - } - - private toCompletionItem( - snapshot: SvelteDocumentSnapshot, - comp: ts.CompletionEntry, - uri: string, - position: Position, - isCompletionInTag: boolean, - addCommitCharacters: boolean, - asStore: boolean, - existingImports: Set - ): AppCompletionItem | null { - const completionLabelAndInsert = this.getCompletionLabelAndInsert(snapshot, comp); - if (!completionLabelAndInsert) { - return null; - } - - let { label, insertText, isSvelteComp, isRunesCompletion, replacementSpan } = - completionLabelAndInsert; - // TS may suggest another Svelte component even if there already exists an import - // with the same name, because under the hood every Svelte component is postfixed - // with `__SvelteComponent`. In this case, filter out this completion by returning null. - if (isSvelteComp && existingImports.has(label)) { - return null; - } - // Remove wrong quotes, for example when using --css-props - if ( - isCompletionInTag && - !insertText && - label[0] === '"' && - label[label.length - 1] === '"' - ) { - label = label.slice(1, -1); - } else if (asStore) { - // only modify label, so that the data property is untouched, which is important so the resolving still works - label = `$${label}`; - } - - const textEdit = replacementSpan - ? TextEdit.replace(convertRange(snapshot, replacementSpan), insertText ?? label) - : undefined; - - const labelDetails = - comp.labelDetails ?? - (comp.sourceDisplay - ? { - description: ts.displayPartsToString(comp.sourceDisplay) - } - : undefined); - - return { - label, - insertText, - kind: scriptElementKindToCompletionItemKind(comp.kind), - commitCharacters: addCommitCharacters ? this.commitCharacters : undefined, - // Make sure svelte component and runes take precedence - sortText: isRunesCompletion || isSvelteComp ? '-1' : comp.sortText, - preselect: isRunesCompletion || isSvelteComp ? true : comp.isRecommended, - insertTextFormat: comp.isSnippet ? InsertTextFormat.Snippet : undefined, - labelDetails, - textEdit, - // pass essential data for resolving completion - data: { - ...comp, - uri, - position - } - }; - } - - private getCompletionLabelAndInsert( - snapshot: SvelteDocumentSnapshot, - comp: ts.CompletionEntry - ) { - let { name, insertText, kindModifiers } = comp; - const isScriptElement = comp.kind === ts.ScriptElementKind.scriptElement; - const hasModifier = Boolean(comp.kindModifiers); - const isRunesCompletion = - name === '$props' || name === '$state' || name === '$derived' || name === '$effect'; - const isSvelteComp = !isRunesCompletion && isGeneratedSvelteComponentName(name); - if (isSvelteComp) { - name = changeSvelteComponentName(name); - - if (this.isExistingSvelteComponentImport(snapshot, name, comp.source)) { - return null; - } - } - - if (isScriptElement && hasModifier) { - const label = - kindModifiers && !name.endsWith(kindModifiers) ? name + kindModifiers : name; - return { - insertText: name, - label, - isSvelteComp, - isRunesCompletion - }; - } - - if (comp.replacementSpan) { - return { - label: name, - isSvelteComp, - isRunesCompletion, - insertText: insertText ? changeSvelteComponentName(insertText) : undefined, - replacementSpan: comp.replacementSpan - }; - } - - return { - label: name, - insertText, - isSvelteComp, - isRunesCompletion - }; - } - - private isExistingSvelteComponentImport( - snapshot: SvelteDocumentSnapshot, - name: string, - source?: string - ): boolean { - const importStatement = new RegExp(`import ${name} from ["'\`][\\s\\S]+\\.svelte["'\`]`); - return !!source && !!snapshot.getFullText().match(importStatement); - } - - /** - * If the textEdit is out of the word range of the triggered position - * vscode would refuse to show the completions - * split those edits into additionalTextEdit to fix it - */ - private fixTextEditRange(wordRangePosition: Position, completionItem: CompletionItem) { - const { textEdit } = completionItem; - if (!textEdit || !TextEdit.is(textEdit)) { - return completionItem; - } - - const { - newText, - range: { start } - } = textEdit; - - const wordRangeStartCharacter = wordRangePosition.character; - if ( - wordRangePosition.line !== wordRangePosition.line || - start.character > wordRangePosition.character - ) { - return completionItem; - } - - textEdit.newText = newText.substring(wordRangeStartCharacter - start.character); - textEdit.range.start = { - line: start.line, - character: wordRangeStartCharacter - }; - completionItem.additionalTextEdits = [ - TextEdit.replace( - { - start, - end: { - line: start.line, - character: wordRangeStartCharacter - } - }, - newText.substring(0, wordRangeStartCharacter - start.character) - ) - ]; - - return completionItem; - } - - /** - * TypeScript throws a debug assertion error if the importModuleSpecifierEnding config is - * 'js' and there's an unknown file extension - which is the case for `.svelte`. Therefore - * rewrite the importModuleSpecifierEnding for this case to silence the error. - */ - fixUserPreferencesForSvelteComponentImport( - userPreferences: ts.UserPreferences - ): ts.UserPreferences { - if (userPreferences.importModuleSpecifierEnding === 'js') { - return { - ...userPreferences, - importModuleSpecifierEnding: 'index' - }; - } - - return userPreferences; - } - - async resolveCompletion( - document: Document, - completionItem: AppCompletionItem, - cancellationToken?: CancellationToken - ): Promise> { - const { data: comp } = completionItem; - const { tsDoc, lang, userPreferences } = - await this.lsAndTsDocResolver.getLSAndTSDoc(document); - - const filePath = tsDoc.filePath; - - const formatCodeOptions = await this.configManager.getFormatCodeSettingsForFile( - document, - tsDoc.scriptKind - ); - if (!comp || !filePath || cancellationToken?.isCancellationRequested) { - return completionItem; - } - - const is$typeImport = !!comp.__is_sveltekit$typeImport; - - const errorPreventingUserPreferences = comp.source?.endsWith('.svelte') - ? this.fixUserPreferencesForSvelteComponentImport(userPreferences) - : userPreferences; - - const detail = lang.getCompletionEntryDetails( - filePath, - tsDoc.offsetAt(tsDoc.getGeneratedPosition(comp.position)), - comp.name, - formatCodeOptions, - comp.source, - errorPreventingUserPreferences, - comp.data - ); - - if (detail) { - const { detail: itemDetail, documentation: itemDocumentation } = - this.getCompletionDocument(detail, is$typeImport); - - // VSCode + tsserver won't have this pop-in effect - // because tsserver has internal APIs for caching - // TODO: consider if we should adopt the internal APIs - if (detail.sourceDisplay && !completionItem.labelDetails) { - completionItem.labelDetails = { - description: ts.displayPartsToString(detail.sourceDisplay) - }; - } - - completionItem.detail = itemDetail; - completionItem.documentation = itemDocumentation; - } - - const actions = detail?.codeActions; - const isImport = !!detail?.source; - - if (actions) { - const edit: TextEdit[] = []; - - const formatCodeBasis = getFormatCodeBasis(formatCodeOptions); - for (const action of actions) { - for (const change of action.changes) { - edit.push( - ...this.codeActionChangesToTextEdit( - document, - tsDoc, - change, - isImport, - comp.position, - formatCodeBasis.newLine, - is$typeImport - ) - ); - } - } - - completionItem.additionalTextEdits = (completionItem.additionalTextEdits ?? []).concat( - edit - ); - } - - return completionItem; - } - - private getCompletionDocument(compDetail: ts.CompletionEntryDetails, is$typeImport: boolean) { - const { sourceDisplay, documentation: tsDocumentation, displayParts, tags } = compDetail; - let parts = compDetail.codeActions?.map((codeAction) => codeAction.description) ?? []; - - if (sourceDisplay && is$typeImport) { - const importPath = ts.displayPartsToString(sourceDisplay); - - // Take into account Node16 moduleResolution - parts = parts.map((detail) => - detail.replace(importPath, `'./$types${importPath.endsWith('.js') ? '.js' : ''}'`) - ); - } - - parts.push(changeSvelteComponentName(ts.displayPartsToString(displayParts))); - - const markdownDoc = getMarkdownDocumentation(tsDocumentation, tags); - const documentation: MarkupContent | undefined = markdownDoc - ? { value: markdownDoc, kind: MarkupKind.Markdown } - : undefined; - - return { - documentation, - detail: parts.filter(Boolean).join('\n\n') - }; - } - - private codeActionChangesToTextEdit( - doc: Document, - snapshot: SvelteDocumentSnapshot, - changes: ts.FileTextChanges, - isImport: boolean, - originalTriggerPosition: Position, - newLine: string, - is$typeImport?: boolean - ): TextEdit[] { - return changes.textChanges.map((change) => - this.codeActionChangeToTextEdit( - doc, - snapshot, - change, - isImport, - originalTriggerPosition, - newLine, - is$typeImport - ) - ); - } - - codeActionChangeToTextEdit( - doc: Document, - snapshot: SvelteDocumentSnapshot, - change: ts.TextChange, - isImport: boolean, - originalTriggerPosition: Position, - newLine: string, - is$typeImport?: boolean, - isCombinedCodeAction?: boolean - ): TextEdit { - change.newText = isCombinedCodeAction - ? modifyLines(change.newText, (line) => - this.fixImportNewText( - line, - isInScript(originalTriggerPosition, doc), - is$typeImport - ) - ) - : this.fixImportNewText( - change.newText, - isInScript(originalTriggerPosition, doc), - is$typeImport - ); - - const scriptTagInfo = snapshot.scriptInfo || snapshot.moduleScriptInfo; - // no script tag defined yet, add it. - if (!scriptTagInfo) { - if (isCombinedCodeAction) { - return TextEdit.insert(Position.create(0, 0), change.newText); - } - - const config = this.configManager.getConfig(); - return TextEdit.replace( - beginOfDocumentRange, - `${getNewScriptStartTag(config)}${change.newText}${newLine}` - ); - } - - const { span } = change; - - const virtualRange = convertRange(snapshot, span); - let range: Range; - const isNewImport = isImport && virtualRange.start.character === 0; - - // Since new import always can't be mapped, we'll have special treatment here - // but only hack this when there is multiple line in script - if (isNewImport && virtualRange.start.line > 1) { - range = this.mapRangeForNewImport(snapshot, virtualRange); - } else { - range = mapRangeToOriginal(snapshot, virtualRange); - } - - // If range is somehow not mapped in parent, - // the import is mapped wrong or is outside script tag, - // use script starting point instead. - // This happens among other things if the completion is the first import of the file. - if ( - range.start.line === -1 || - (range.start.line === 0 && range.start.character <= 1 && span.length === 0) || - !isInScript(range.start, snapshot) - ) { - range = convertRange(doc, { - start: isInTag(originalTriggerPosition, doc.scriptInfo) - ? snapshot.scriptInfo?.start || scriptTagInfo.start - : isInTag(originalTriggerPosition, doc.moduleScriptInfo) - ? snapshot.moduleScriptInfo?.start || scriptTagInfo.start - : scriptTagInfo.start, - length: span.length - }); - } - // prevent newText from being placed like this: ' - }; - } - - return diagnostic; -} - -/** - * Due to source mapping, some ranges may be swapped: Start is end. Swap back in this case. - */ -function swapDiagRangeStartEndIfNecessary(diag: Diagnostic): Diagnostic { - diag.range = swapRangeStartEndIfNecessary(diag.range); - return diag; -} - -/** - * Checks if diagnostic is not within a section that should be completely ignored - * because it's purely generated. - */ -function isNotGenerated(text: string) { - return (diagnostic: ts.Diagnostic) => { - if (diagnostic.start === undefined || diagnostic.length === undefined) { - return true; - } - return !isInGeneratedCode(text, diagnostic.start, diagnostic.start + diagnostic.length); - }; -} - -function isUnusedReactiveStatementLabel(diagnostic: ts.Diagnostic) { - if (diagnostic.code !== DiagnosticCode.UNUSED_LABEL) { - return false; - } - - const diagNode = findDiagnosticNode(diagnostic); - if (!diagNode) { - return false; - } - - // TS warning targets the identifier - if (!ts.isIdentifier(diagNode)) { - return false; - } - - if (!diagNode.parent) { - return false; - } - return isReactiveStatement(diagNode.parent); -} - -/** - * Checks if diagnostics should be ignored because they report an unused expression* in - * a reactive statement, and those actually have side effects in Svelte (hinting deps). - * - * $: x, update() - * - * Only `let` (i.e. reactive) variables are ignored. For the others, new diagnostics are - * emitted, centered on the (non reactive) identifiers in the initial warning. - */ -function resolveNoopsInReactiveStatements(lang: ts.LanguageService, diagnostics: ts.Diagnostic[]) { - const isLet = (file: ts.SourceFile) => (node: ts.Node) => { - const defs = lang.getDefinitionAtPosition(file.fileName, node.getStart()); - return !!defs && defs.some((def) => def.fileName === file.fileName && def.kind === 'let'); - }; - - const expandRemainingNoopWarnings = (diagnostic: ts.Diagnostic): void | ts.Diagnostic[] => { - const { code, file } = diagnostic; - - // guard: missing info - if (!file) { - return; - } - - // guard: not target error - const isNoopDiag = code === DiagnosticCode.NOOP_IN_COMMAS; - if (!isNoopDiag) { - return; - } - - const diagNode = findDiagnosticNode(diagnostic); - if (!diagNode) { - return; - } - - if (!isInReactiveStatement(diagNode)) { - return; - } - - return ( - // for all identifiers in diagnostic node - gatherIdentifiers(diagNode) - // ignore `let` (i.e. reactive) variables - .filter(not(isLet(file))) - // and create targeted diagnostics just for the remaining ids - .map(copyDiagnosticAndChangeNode(diagnostic)) - ); - }; - - const expandedDiagnostics = flatten(passMap(diagnostics, expandRemainingNoopWarnings)); - return expandedDiagnostics.length === diagnostics.length - ? expandedDiagnostics - : // This can generate duplicate diagnostics - expandedDiagnostics.filter(dedupDiagnostics()); -} - -function dedupDiagnostics() { - const hashDiagnostic = (diag: ts.Diagnostic) => - [diag.start, diag.length, diag.category, diag.source, diag.code] - .map((x) => JSON.stringify(x)) - .join(':'); - - const known = new Set(); - - return (diag: ts.Diagnostic) => { - const key = hashDiagnostic(diag); - if (known.has(key)) { - return false; - } else { - known.add(key); - return true; - } - }; -} - -function get$$PropsAliasForInfo( - get$$PropsDefWithCache: () => ReturnType, - lang: ts.LanguageService, - document: Document -) { - if (!/type\s+\$\$Props[\s\n]+=/.test(document.getText())) { - return; - } - - const propsDef = get$$PropsDefWithCache(); - if (!propsDef || !ts.isTypeAliasDeclaration(propsDef)) { - return; - } - - const type = lang.getProgram()?.getTypeChecker()?.getTypeAtLocation(propsDef.name); - if (!type) { - return; - } - - // TS says symbol is always defined but it's not - const rootSymbolName = (type.aliasSymbol ?? type.symbol)?.name; - if (!rootSymbolName) { - return; - } - - return [rootSymbolName, propsDef] as const; -} - -function get$$PropsDef(lang: ts.LanguageService, snapshot: SvelteDocumentSnapshot) { - const program = lang.getProgram(); - const sourceFile = program?.getSourceFile(snapshot.filePath); - if (!program || !sourceFile) { - return undefined; - } - - const renderFunction = sourceFile.statements.find( - (statement): statement is ts.FunctionDeclaration => - ts.isFunctionDeclaration(statement) && statement.name?.getText() === 'render' - ); - return renderFunction?.body?.statements.find( - (node): node is ts.TypeAliasDeclaration | ts.InterfaceDeclaration => - (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) && - node.name.getText() === '$$Props' - ); -} - -function movePropsErrorRangeBackIfNecessary( - diagnostic: Diagnostic, - snapshot: SvelteDocumentSnapshot, - get$$PropsDefWithCache: () => ReturnType, - get$$PropsAliasForWithCache: () => ReturnType -): Range | undefined { - const possibly$$PropsError = isAfterSvelte2TsxPropsReturn( - snapshot.getFullText(), - snapshot.offsetAt(diagnostic.range.start) - ); - if (!possibly$$PropsError) { - return; - } - - if (diagnostic.message.includes('$$Props')) { - const propsDef = get$$PropsDefWithCache(); - const generatedPropsStart = propsDef?.name.getStart(); - const propsStart = - generatedPropsStart != null && - snapshot.getOriginalPosition(snapshot.positionAt(generatedPropsStart)); - - if (propsStart) { - return { - start: propsStart, - end: { ...propsStart, character: propsStart.character + '$$Props'.length } - }; - } - - return; - } - - const aliasForInfo = get$$PropsAliasForWithCache(); - if (!aliasForInfo) { - return; - } - - const [aliasFor, propsDef] = aliasForInfo; - if (diagnostic.message.includes(aliasFor)) { - return mapRangeToOriginal(snapshot, { - start: snapshot.positionAt(propsDef.name.getStart()), - end: snapshot.positionAt(propsDef.name.getEnd()) - }); - } -} - -function expectedTransitionThirdArgument( - diagnostic: ts.Diagnostic, - tsDoc: SvelteDocumentSnapshot, - lang: ts.LanguageService -) { - if ( - diagnostic.code !== DiagnosticCode.EXPECTED_N_ARGUMENTS || - !diagnostic.start || - !tsDoc.getText(0, diagnostic.start).endsWith('__sveltets_2_ensureTransition(') - ) { - return false; - } - - const node = findDiagnosticNode(diagnostic); - if (!node) { - return false; - } - - const callExpression = findNodeAtSpan( - node, - { start: node.getStart(), length: node.getWidth() }, - ts.isCallExpression - ); - const signature = - callExpression && lang.getProgram()?.getTypeChecker().getResolvedSignature(callExpression); - - return ( - signature?.parameters.filter((parameter) => !(parameter.flags & ts.SymbolFlags.Optional)) - .length === 3 - ); -} diff --git a/packages/language-server/src/plugins/typescript/features/FindComponentReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindComponentReferencesProvider.ts deleted file mode 100644 index a0bcca91d..000000000 --- a/packages/language-server/src/plugins/typescript/features/FindComponentReferencesProvider.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Location, Position, Range } from 'vscode-languageserver'; -import { flatten, isNotNullOrUndefined, pathToUrl, urlToPath } from '../../../utils'; -import { FindComponentReferencesProvider } from '../../interfaces'; -import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { - convertToLocationRange, - hasNonZeroRange, - offsetOfGeneratedComponentExport -} from '../utils'; -import { isTextSpanInGeneratedCode, SnapshotMap } from './utils'; - -export class FindComponentReferencesProviderImpl implements FindComponentReferencesProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} - - async findComponentReferences(uri: string): Promise { - // No document available, just the uri, because it could be called on an unopened file - const fileName = urlToPath(uri); - if (!fileName) { - return null; - } - - const lang = await this.lsAndTsDocResolver.getLSForPath(fileName); - const tsDoc = await this.lsAndTsDocResolver.getSnapshot(fileName); - if (!(tsDoc instanceof SvelteDocumentSnapshot)) { - return null; - } - - const references = lang.findReferences( - tsDoc.filePath, - offsetOfGeneratedComponentExport(tsDoc) - ); - if (!references) { - return null; - } - - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); - snapshots.set(tsDoc.filePath, tsDoc); - - const locations = await Promise.all( - flatten(references.map((ref) => ref.references)).map(async (ref) => { - if (ref.isDefinition) { - return null; - } - - const snapshot = await snapshots.retrieve(ref.fileName); - - if (isTextSpanInGeneratedCode(snapshot.getFullText(), ref.textSpan)) { - return null; - } - - const refLocation = Location.create( - pathToUrl(ref.fileName), - convertToLocationRange(snapshot, ref.textSpan) - ); - - //Only report starting tags - if (this.isEndTag(refLocation, snapshot)) { - return null; - } - - // Some references are in generated code but not wrapped with explicit ignore comments. - // These show up as zero-length ranges, so filter them out. - if (!hasNonZeroRange(refLocation)) { - return null; - } - - return refLocation; - }) - ); - - return locations.filter(isNotNullOrUndefined); - } - - private isEndTag(element: Location, snapshot: DocumentSnapshot) { - if (!(snapshot instanceof SvelteDocumentSnapshot)) { - return false; - } - - const testEndTagRange = Range.create( - Position.create(element.range.start.line, element.range.start.character - 1), - element.range.end - ); - - const text = snapshot.getOriginalText(testEndTagRange); - if (text.substring(0, 1) == '/') { - return true; - } - - return false; - } -} diff --git a/packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts deleted file mode 100644 index c9ee7d213..000000000 --- a/packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Location } from 'vscode-languageserver'; -import { URI } from 'vscode-uri'; -import { FileReferencesProvider } from '../../interfaces'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { convertToLocationRange, hasNonZeroRange } from '../utils'; -import { SnapshotMap } from './utils'; -import { pathToUrl } from '../../../utils'; - -export class FindFileReferencesProviderImpl implements FileReferencesProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} - - async fileReferences(uri: string): Promise { - const u = URI.parse(uri); - const fileName = u.fsPath; - - const lang = await this.getLSForPath(fileName); - const tsDoc = await this.getSnapshotForPath(fileName); - - const references = lang.getFileReferences(fileName); - - if (!references) { - return null; - } - - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); - snapshots.set(tsDoc.filePath, tsDoc); - - const locations = await Promise.all( - references.map(async (ref) => { - const snapshot = await snapshots.retrieve(ref.fileName); - - return Location.create( - pathToUrl(ref.fileName), - convertToLocationRange(snapshot, ref.textSpan) - ); - }) - ); - // Some references are in generated code but not wrapped with explicit ignore comments. - // These show up as zero-length ranges, so filter them out. - return locations.filter(hasNonZeroRange); - } - - private async getLSForPath(path: string) { - return this.lsAndTsDocResolver.getLSForPath(path); - } - - private async getSnapshotForPath(path: string) { - return this.lsAndTsDocResolver.getSnapshot(path); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts deleted file mode 100644 index c530398ea..000000000 --- a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts +++ /dev/null @@ -1,212 +0,0 @@ -import ts from 'typescript'; -import { Location, Position, ReferenceContext } from 'vscode-languageserver'; -import { Document } from '../../../lib/documents'; -import { flatten, isNotNullOrUndefined, pathToUrl } from '../../../utils'; -import { FindComponentReferencesProvider, FindReferencesProvider } from '../../interfaces'; -import { SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { - convertToLocationForReferenceOrDefinition, - hasNonZeroRange, - isGeneratedSvelteComponentName -} from '../utils'; -import { - get$storeOffsetOf$storeDeclaration, - getStoreOffsetOf$storeDeclaration, - is$storeVariableIn$storeDeclaration, - isStoreVariableIn$storeDeclaration, - isTextSpanInGeneratedCode, - SnapshotMap -} from './utils'; - -export class FindReferencesProviderImpl implements FindReferencesProvider { - constructor( - private readonly lsAndTsDocResolver: LSAndTSDocResolver, - private readonly componentReferencesProvider: FindComponentReferencesProvider - ) {} - - async findReferences( - document: Document, - position: Position, - context: ReferenceContext - ): Promise { - if (this.isScriptStartOrEndTag(position, document)) { - return this.componentReferencesProvider.findComponentReferences(document.uri); - } - - const { lang, tsDoc } = await this.getLSAndTSDoc(document); - const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); - - const rawReferences = lang.findReferences( - tsDoc.filePath, - tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)) - ); - if (!rawReferences) { - return null; - } - - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); - snapshots.set(tsDoc.filePath, tsDoc); - - if (rawReferences.some((ref) => ref.definition.kind === ts.ScriptElementKind.alias)) { - const componentReferences = await this.checkIfHasAliasedComponentReference( - offset, - tsDoc, - lang - ); - - if (componentReferences?.length) { - return componentReferences; - } - } - const references = flatten(rawReferences.map((ref) => ref.references)); - - references.push(...(await this.getStoreReferences(references, tsDoc, snapshots, lang))); - - const locations = await Promise.all( - references.map(async (ref) => this.mapReference(ref, context, snapshots)) - ); - - return ( - locations - .filter(isNotNullOrUndefined) - // Possible $store references are added afterwards, sort for correct order - .sort(sortLocationByFileAndRange) - ); - } - - private isScriptStartOrEndTag(position: Position, document: Document) { - if (!document.scriptInfo) { - return false; - } - const { start, end } = document.scriptInfo.container; - - const offset = document.offsetAt(position); - return ( - (offset >= start && offset <= start + '= end - ''.length && offset <= end) - ); - } - - /** - * If references of a $store are searched, also find references for the corresponding store - * and vice versa. - */ - private async getStoreReferences( - references: ts.ReferencedSymbolEntry[], - tsDoc: SvelteDocumentSnapshot, - snapshots: SnapshotMap, - lang: ts.LanguageService - ): Promise { - // If user started finding references at $store, find references for store, too - let storeReferences: ts.ReferencedSymbolEntry[] = []; - const storeReference = references.find( - (ref) => - ref.fileName === tsDoc.filePath && - isTextSpanInGeneratedCode(tsDoc.getFullText(), ref.textSpan) && - is$storeVariableIn$storeDeclaration(tsDoc.getFullText(), ref.textSpan.start) - ); - if (storeReference) { - const additionalReferences = - lang.findReferences( - tsDoc.filePath, - getStoreOffsetOf$storeDeclaration( - tsDoc.getFullText(), - storeReference.textSpan.start - ) - ) || []; - storeReferences = flatten(additionalReferences.map((ref) => ref.references)); - } - - // If user started finding references at store, find references for $store, too - // If user started finding references at $store, find references for $store in other files - const $storeReferences: ts.ReferencedSymbolEntry[] = []; - for (const ref of [...references, ...storeReferences]) { - const snapshot = await snapshots.retrieve(ref.fileName); - if ( - !( - isTextSpanInGeneratedCode(snapshot.getFullText(), ref.textSpan) && - isStoreVariableIn$storeDeclaration(snapshot.getFullText(), ref.textSpan.start) - ) - ) { - continue; - } - if (storeReference?.fileName === ref.fileName) { - // $store in X -> usages of store -> store in X -> we would add duplicate $store references - continue; - } - - const additionalReferences = - lang.findReferences( - snapshot.filePath, - get$storeOffsetOf$storeDeclaration(snapshot.getFullText(), ref.textSpan.start) - ) || []; - $storeReferences.push(...flatten(additionalReferences.map((ref) => ref.references))); - } - - return [...storeReferences, ...$storeReferences]; - } - - private async checkIfHasAliasedComponentReference( - offset: number, - tsDoc: SvelteDocumentSnapshot, - lang: ts.LanguageService - ) { - const definitions = lang.getDefinitionAtPosition(tsDoc.filePath, offset); - if (!definitions?.length) { - return null; - } - - const nonAliasDefinitions = definitions.filter((definition) => - isGeneratedSvelteComponentName(definition.name) - ); - const references = await Promise.all( - nonAliasDefinitions.map((definition) => - this.componentReferencesProvider.findComponentReferences( - pathToUrl(definition.fileName) - ) - ) - ); - - return flatten(references.filter(isNotNullOrUndefined)); - } - - private async mapReference( - ref: ts.ReferencedSymbolEntry, - context: ReferenceContext, - snapshots: SnapshotMap - ) { - if (!context.includeDeclaration && ref.isDefinition) { - return null; - } - - const snapshot = await snapshots.retrieve(ref.fileName); - - if (isTextSpanInGeneratedCode(snapshot.getFullText(), ref.textSpan)) { - return null; - } - - // TODO we should deduplicate if we support finding references from multiple language service - const location = convertToLocationForReferenceOrDefinition(snapshot, ref.textSpan); - - // Some references are in generated code but not wrapped with explicit ignore comments. - // These show up as zero-length ranges, so filter them out. - if (!hasNonZeroRange(location)) { - return null; - } - - return location; - } - - private async getLSAndTSDoc(document: Document) { - return this.lsAndTsDocResolver.getLSAndTSDoc(document); - } -} - -function sortLocationByFileAndRange(l1: Location, l2: Location): number { - const localeCompare = l1.uri.localeCompare(l2.uri); - return localeCompare === 0 - ? (l1.range.start.line - l2.range.start.line) * 10000 + - (l1.range.start.character - l2.range.start.character) - : localeCompare; -} diff --git a/packages/language-server/src/plugins/typescript/features/FoldingRangeProvider.ts b/packages/language-server/src/plugins/typescript/features/FoldingRangeProvider.ts deleted file mode 100644 index 022d58801..000000000 --- a/packages/language-server/src/plugins/typescript/features/FoldingRangeProvider.ts +++ /dev/null @@ -1,349 +0,0 @@ -import ts from 'typescript'; -import { FoldingRangeKind, Range } from 'vscode-languageserver'; -import { FoldingRange } from 'vscode-languageserver-types'; -import { Document, isInTag, mapRangeToOriginal, toRange } from '../../../lib/documents'; -import { isNotNullOrUndefined } from '../../../utils'; -import { FoldingRangeProvider } from '../../interfaces'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { convertRange } from '../utils'; -import { isTextSpanInGeneratedCode } from './utils'; -import { LSConfigManager } from '../../../ls-config'; -import { LineRange, indentBasedFoldingRange } from '../../../lib/foldingRange/indentFolding'; -import { SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { - SvelteNode, - SvelteNodeWalker, - findElseBlockTagStart, - findIfBlockEndTagStart, - hasElseBlock, - isAwaitBlock, - isEachBlock, - isElseBlockWithElseIf -} from '../svelte-ast-utils'; - -export class FoldingRangeProviderImpl implements FoldingRangeProvider { - constructor( - private readonly lsAndTsDocResolver: LSAndTSDocResolver, - private readonly configManager: LSConfigManager - ) {} - private readonly foldEndPairCharacters = ['}', ']', ')', '`', '>']; - - async getFoldingRanges(document: Document): Promise { - // don't use ls.getProgram unless it's necessary - // this feature is pure syntactic and doesn't need type information - - const { lang, tsDoc } = await this.lsAndTsDocResolver.getLsForSyntheticOperations(document); - - const foldingRanges = - tsDoc.parserError && !document.moduleScriptInfo && !document.scriptInfo - ? [] - : lang.getOutliningSpans(tsDoc.filePath); - - const lineFoldingOnly = - !!this.configManager.getClientCapabilities()?.textDocument?.foldingRange - ?.lineFoldingOnly; - - const result = foldingRanges - .filter((span) => !isTextSpanInGeneratedCode(tsDoc.getFullText(), span.textSpan)) - .map((span) => ({ - originalRange: this.mapToOriginalRange(tsDoc, span.textSpan, document), - span - })) - .map(({ originalRange, span }) => - this.convertOutliningSpan(span, document, originalRange, lineFoldingOnly) - ) - .filter(isNotNullOrUndefined) - .concat(this.collectSvelteBlockFolding(document, tsDoc, lineFoldingOnly)) - .concat(this.getSvelteTagFoldingIfParserError(document, tsDoc)) - .filter((r) => (lineFoldingOnly ? r.startLine < r.endLine : r.startLine <= r.endLine)); - - return result; - } - - private mapToOriginalRange( - tsDoc: SvelteDocumentSnapshot, - textSpan: ts.TextSpan, - document: Document - ) { - const range = mapRangeToOriginal(tsDoc, convertRange(tsDoc, textSpan)); - const startOffset = document.offsetAt(range.start); - - if (range.start.line < 0 || range.end.line < 0 || range.start.line > range.end.line) { - return; - } - - if ( - isInTag(range.start, document.scriptInfo) || - isInTag(range.start, document.moduleScriptInfo) - ) { - return range; - } - - const endOffset = document.offsetAt(range.end); - const originalText = document.getText().slice(startOffset, endOffset); - - if (originalText.length === 0) { - return; - } - - const generatedText = tsDoc.getText(textSpan.start, textSpan.start + textSpan.length); - const oneToOne = originalText.trim() === generatedText.trim(); - - if (oneToOne) { - return range; - } - } - - /** - * Doing this here with the svelte2tsx's svelte ast is slightly - * less prone to error and faster than - * using the svelte ast in the svelte plugins. - */ - private collectSvelteBlockFolding( - document: Document, - tsDoc: SvelteDocumentSnapshot, - lineFoldingOnly: boolean - ) { - if (tsDoc.parserError) { - return []; - } - - const ranges: FoldingRange[] = []; - - const provider = this; - const enter: SvelteNodeWalker['enter'] = function (node, parent, key) { - if (key === 'attributes') { - this.skip(); - } - - // use sub-block for await block - if (!node.type.endsWith('Block') || node.type === 'AwaitBlock') { - return; - } - - if (node.type === 'IfBlock') { - provider.getIfBlockFolding(node, document, ranges); - return; - } - - if (isElseBlockWithElseIf(node)) { - return; - } - - if ((node.type === 'CatchBlock' || node.type === 'ThenBlock') && isAwaitBlock(parent)) { - const expressionEnd = - (node.type === 'CatchBlock' ? parent.error?.end : parent.value?.end) ?? - document.getText().indexOf('}', node.start); - - const beforeBlockStartTagEnd = document.getText().indexOf('}', expressionEnd); - if (beforeBlockStartTagEnd == -1) { - return; - } - ranges.push( - provider.createFoldingRange(document, beforeBlockStartTagEnd + 1, node.end) - ); - - return; - } - - if (isEachBlock(node)) { - const start = document.getText().indexOf('}', (node.key ?? node.expression).end); - const elseStart = node.else - ? findElseBlockTagStart(document.getText(), node.else) - : -1; - - ranges.push( - provider.createFoldingRange( - document, - start, - elseStart === -1 ? node.end : elseStart - ) - ); - - return; - } - - if ('expression' in node && node.expression && typeof node.expression === 'object') { - const start = provider.getStartForNodeWithExpression( - node as SvelteNode & { expression: SvelteNode }, - document - ); - const end = node.end; - - ranges.push(provider.createFoldingRange(document, start, end)); - return; - } - - if (node.start != null && node.end != null) { - const start = node.start; - const end = node.end; - - ranges.push(provider.createFoldingRange(document, start, end)); - } - }; - - tsDoc.walkSvelteAst({ - enter - }); - - if (lineFoldingOnly) { - return ranges.map((r) => ({ - startLine: r.startLine, - endLine: this.previousLineOfEndLine(r.startLine, r.endLine) - })); - } - - return ranges; - } - - private getIfBlockFolding(node: SvelteNode, document: Document, ranges: FoldingRange[]) { - const typed = node as SvelteNode & { - else?: SvelteNode; - expression: SvelteNode; - }; - - const documentText = document.getText(); - const start = this.getStartForNodeWithExpression(typed, document); - const end = hasElseBlock(typed) - ? findElseBlockTagStart(documentText, typed.else) - : findIfBlockEndTagStart(documentText, typed); - - ranges.push(this.createFoldingRange(document, start, end)); - } - - private getStartForNodeWithExpression( - node: SvelteNode & { expression: SvelteNode }, - document: Document - ) { - return document.getText().indexOf('}', node.expression.end) + 1; - } - - private createFoldingRange(document: Document, start: number, end: number) { - const range = toRange(document.getText(), start, end); - return { - startLine: range.start.line, - startCharacter: range.start.character, - endLine: range.end.line, - endCharacter: range.end.character - }; - } - - private convertOutliningSpan( - span: ts.OutliningSpan, - document: Document, - originalRange: Range | undefined, - lineFoldingOnly: boolean - ): FoldingRange | null { - if (!originalRange) { - return null; - } - - const end = lineFoldingOnly - ? this.adjustFoldingEndToNotHideEnd(originalRange, document) - : originalRange.end; - - const result = { - startLine: originalRange.start.line, - endLine: end.line, - kind: this.getFoldingRangeKind(span), - startCharacter: lineFoldingOnly ? undefined : originalRange.start.character, - endCharacter: lineFoldingOnly ? undefined : end.character - }; - - return result; - } - - private getFoldingRangeKind(span: ts.OutliningSpan): FoldingRangeKind | undefined { - switch (span.kind) { - case ts.OutliningSpanKind.Comment: - return FoldingRangeKind.Comment; - case ts.OutliningSpanKind.Region: - return FoldingRangeKind.Region; - case ts.OutliningSpanKind.Imports: - return FoldingRangeKind.Imports; - case ts.OutliningSpanKind.Code: - default: - return undefined; - } - } - - private adjustFoldingEndToNotHideEnd( - range: Range, - document: Document - ): { line: number; character?: number } { - // don't fold end bracket, brace... - if (range.end.character > 0) { - const text = document.getText(); - const offsetBeforeEnd = document.offsetAt({ - line: range.end.line, - character: range.end.character - 1 - }); - const foldEndCharacter = text[offsetBeforeEnd]; - if (this.foldEndPairCharacters.includes(foldEndCharacter)) { - return { line: this.previousLineOfEndLine(range.start.line, range.end.line) }; - } - } - - return range.end; - } - - private getSvelteTagFoldingIfParserError(document: Document, tsDoc: SvelteDocumentSnapshot) { - if (!tsDoc.parserError) { - return []; - } - - const htmlTemplateRanges = this.getHtmlTemplateRangesForChecking(document); - - return indentBasedFoldingRange({ - document, - skipFold: (_, lineContent) => { - return !/{\s*(#|\/|:)/.test(lineContent); - }, - ranges: htmlTemplateRanges - }); - } - - private getHtmlTemplateRangesForChecking(document: Document) { - const ranges: LineRange[] = []; - - const excludeTags = [ - document.templateInfo, - document.moduleScriptInfo, - document.scriptInfo, - document.styleInfo - ] - .filter(isNotNullOrUndefined) - .map((info) => ({ - startLine: document.positionAt(info.container.start).line, - endLine: document.positionAt(info.container.end).line - })) - .sort((a, b) => a.startLine - b.startLine); - - if (excludeTags.length === 0) { - return [{ startLine: 0, endLine: document.lineCount - 1 }]; - } - - if (excludeTags[0].startLine > 0) { - ranges.push({ - startLine: 0, - endLine: excludeTags[0].startLine - 1 - }); - } - - for (let index = 0; index < excludeTags.length; index++) { - const element = excludeTags[index]; - const next = excludeTags[index + 1]; - - ranges.push({ - startLine: element.endLine + 1, - endLine: next ? next.startLine - 1 : document.lineCount - 1 - }); - } - - return ranges; - } - - private previousLineOfEndLine(startLine: number, endLine: number) { - return Math.max(endLine - 1, startLine); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/HoverProvider.ts b/packages/language-server/src/plugins/typescript/features/HoverProvider.ts deleted file mode 100644 index feebf5717..000000000 --- a/packages/language-server/src/plugins/typescript/features/HoverProvider.ts +++ /dev/null @@ -1,80 +0,0 @@ -import ts from 'typescript'; -import { Hover, Position } from 'vscode-languageserver'; -import { Document, getWordAt, mapObjWithRangeToOriginal } from '../../../lib/documents'; -import { HoverProvider } from '../../interfaces'; -import { SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { getMarkdownDocumentation } from '../previewer'; -import { convertRange } from '../utils'; -import { getComponentAtPosition } from './utils'; - -export class HoverProviderImpl implements HoverProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} - - async doHover(document: Document, position: Position): Promise { - const { lang, tsDoc } = await this.getLSAndTSDoc(document); - - const eventHoverInfo = this.getEventHoverInfo(lang, document, tsDoc, position); - if (eventHoverInfo) { - return eventHoverInfo; - } - - const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); - const info = lang.getQuickInfoAtPosition(tsDoc.filePath, offset); - if (!info) { - return null; - } - - const declaration = ts.displayPartsToString(info.displayParts); - const documentation = getMarkdownDocumentation(info.documentation, info.tags); - - // https://microsoft.github.io/language-server-protocol/specification#textDocument_hover - const contents = ['```typescript', declaration, '```'] - .concat(documentation ? ['---', documentation] : []) - .join('\n'); - - return mapObjWithRangeToOriginal(tsDoc, { - range: convertRange(tsDoc, info.textSpan), - contents - }); - } - - private getEventHoverInfo( - lang: ts.LanguageService, - doc: Document, - tsDoc: SvelteDocumentSnapshot, - originalPosition: Position - ): Hover | null { - const possibleEventName = getWordAt(doc.getText(), doc.offsetAt(originalPosition), { - left: /\S+$/, - right: /[\s=]/ - }); - if (!possibleEventName.startsWith('on:')) { - return null; - } - - const component = getComponentAtPosition(lang, doc, tsDoc, originalPosition); - if (!component) { - return null; - } - - const eventName = possibleEventName.substr('on:'.length); - const event = component.getEvents().find((event) => event.name === eventName); - if (!event) { - return null; - } - - return { - contents: [ - '```typescript', - `${event.name}: ${event.type}`, - '```', - event.doc || '' - ].join('\n') - }; - } - - private async getLSAndTSDoc(document: Document) { - return this.lsAndTsDocResolver.getLSAndTSDoc(document); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts b/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts deleted file mode 100644 index a74bb550d..000000000 --- a/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Position, Location } from 'vscode-languageserver-protocol'; -import { Document, mapLocationToOriginal } from '../../../lib/documents'; -import { isNotNullOrUndefined } from '../../../utils'; -import { ImplementationProvider } from '../../interfaces'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { convertRange } from '../utils'; -import { - is$storeVariableIn$storeDeclaration, - isTextSpanInGeneratedCode, - SnapshotMap -} from './utils'; - -export class ImplementationProviderImpl implements ImplementationProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} - - async getImplementation(document: Document, position: Position): Promise { - const { tsDoc, lang } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); - const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); - const implementations = lang.getImplementationAtPosition(tsDoc.filePath, offset); - - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); - snapshots.set(tsDoc.filePath, tsDoc); - - if (!implementations) { - return null; - } - - const result = await Promise.all( - implementations.map(async (implementation) => { - let snapshot = await snapshots.retrieve(implementation.fileName); - - // Go from generated $store to store if user wants to find implementation for $store - if (isTextSpanInGeneratedCode(snapshot.getFullText(), implementation.textSpan)) { - if ( - !is$storeVariableIn$storeDeclaration( - snapshot.getFullText(), - implementation.textSpan.start - ) - ) { - return; - } - // there will be exactly one definition, the store - implementation = lang.getImplementationAtPosition( - tsDoc.filePath, - tsDoc.getFullText().indexOf(');', implementation.textSpan.start) - 1 - )![0]; - snapshot = await snapshots.retrieve(implementation.fileName); - } - - const location = mapLocationToOriginal( - snapshot, - convertRange(snapshot, implementation.textSpan) - ); - - if (location.range.start.line >= 0 && location.range.end.line >= 0) { - return location; - } - }) - ); - - return result.filter(isNotNullOrUndefined); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts b/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts deleted file mode 100644 index 63d1e98ae..000000000 --- a/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts +++ /dev/null @@ -1,324 +0,0 @@ -import ts from 'typescript'; -import { CancellationToken } from 'vscode-languageserver'; -import { - Position, - Range, - InlayHint, - InlayHintKind, - InlayHintLabelPart -} from 'vscode-languageserver-types'; -import { Document, isInTag, mapLocationToOriginal } from '../../../lib/documents'; -import { getAttributeContextAtPosition } from '../../../lib/documents/parseHtml'; -import { InlayHintProvider } from '../../interfaces'; -import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { - findContainingNode, - isInGeneratedCode, - findChildOfKind, - findRenderFunction, - findClosestContainingNode, - SnapshotMap -} from './utils'; -import { convertRange } from '../utils'; - -export class InlayHintProviderImpl implements InlayHintProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} - - async getInlayHints( - document: Document, - range: Range, - cancellationToken?: CancellationToken - ): Promise { - // Don't sync yet so we can skip TypeScript's synchronizeHostData if inlay hints are disabled - const { userPreferences } = - await this.lsAndTsDocResolver.getLsForSyntheticOperations(document); - - if ( - cancellationToken?.isCancellationRequested || - !this.areInlayHintsEnabled(userPreferences) - ) { - return null; - } - - const { tsDoc, lang } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); - - const inlayHints = lang.provideInlayHints( - tsDoc.filePath, - this.convertToTargetTextSpan(range, tsDoc), - userPreferences - ); - - const sourceFile = lang.getProgram()?.getSourceFile(tsDoc.filePath); - - if (!sourceFile) { - return []; - } - - const renderFunction = findRenderFunction(sourceFile); - const renderFunctionReturnTypeLocation = - renderFunction && this.getTypeAnnotationPosition(renderFunction); - - const snapshotMap = new SnapshotMap(this.lsAndTsDocResolver); - snapshotMap.set(tsDoc.filePath, tsDoc); - - const convertPromises = inlayHints - .filter( - (inlayHint) => - !isInGeneratedCode(tsDoc.getFullText(), inlayHint.position) && - inlayHint.position !== renderFunctionReturnTypeLocation && - !this.isSvelte2tsxFunctionHints(sourceFile, inlayHint) && - !this.isGeneratedVariableTypeHint(sourceFile, inlayHint) && - !this.isGeneratedFunctionReturnType(sourceFile, inlayHint) - ) - .map(async (inlayHint) => ({ - label: await this.convertInlayHintLabelParts(inlayHint, snapshotMap), - position: this.getOriginalPosition(document, tsDoc, inlayHint), - kind: this.convertInlayHintKind(inlayHint.kind), - paddingLeft: inlayHint.whitespaceBefore, - paddingRight: inlayHint.whitespaceAfter - })); - - return (await Promise.all(convertPromises)).filter( - (inlayHint) => - inlayHint.position.line >= 0 && - inlayHint.position.character >= 0 && - !this.checkGeneratedFunctionHintWithSource(inlayHint, document) - ); - } - - private areInlayHintsEnabled(preferences: ts.UserPreferences) { - return ( - preferences.includeInlayParameterNameHints === 'literals' || - preferences.includeInlayParameterNameHints === 'all' || - preferences.includeInlayEnumMemberValueHints || - preferences.includeInlayFunctionLikeReturnTypeHints || - preferences.includeInlayFunctionParameterTypeHints || - preferences.includeInlayPropertyDeclarationTypeHints || - preferences.includeInlayVariableTypeHints - ); - } - - private convertToTargetTextSpan(range: Range, snapshot: DocumentSnapshot) { - const generatedStartOffset = snapshot.getGeneratedPosition(range.start); - const generatedEndOffset = snapshot.getGeneratedPosition(range.end); - - const start = generatedStartOffset.line < 0 ? 0 : snapshot.offsetAt(generatedStartOffset); - const end = - generatedEndOffset.line < 0 - ? snapshot.getLength() - : snapshot.offsetAt(generatedEndOffset); - - return { - start, - length: end - start - }; - } - - private async convertInlayHintLabelParts(inlayHint: ts.InlayHint, snapshotMap: SnapshotMap) { - if (!inlayHint.displayParts) { - return inlayHint.text; - } - - const convertPromises = inlayHint.displayParts.map( - async (part): Promise => { - if (!part.file || !part.span) { - return { - value: part.text - }; - } - - const snapshot = await snapshotMap.retrieve(part.file); - if (!snapshot) { - return { - value: part.text - }; - } - - const originalLocation = mapLocationToOriginal( - snapshot, - convertRange(snapshot, part.span) - ); - - return { - value: part.text, - location: originalLocation.range.start.line < 0 ? undefined : originalLocation - }; - } - ); - - const parts = await Promise.all(convertPromises); - - return parts; - } - - private getOriginalPosition( - document: Document, - tsDoc: SvelteDocumentSnapshot, - inlayHint: ts.InlayHint - ): Position { - let originalPosition = tsDoc.getOriginalPosition(tsDoc.positionAt(inlayHint.position)); - if (inlayHint.kind === ts.InlayHintKind.Type) { - const originalOffset = document.offsetAt(originalPosition); - const source = document.getText(); - // detect if inlay hint position is off by one - // by checking if source[offset] is part of an identifier - // https://github.com/sveltejs/language-tools/pull/2070 - if ( - originalOffset < source.length && - !/[\x00-\x23\x25-\x2F\x3A-\x40\x5B\x5D-\x5E\x60\x7B-\x7F]/.test( - source[originalOffset] - ) - ) { - originalPosition.character += 1; - } - } - - return originalPosition; - } - - private convertInlayHintKind(kind: ts.InlayHintKind): InlayHintKind | undefined { - switch (kind) { - case 'Parameter': - return InlayHintKind.Parameter; - case 'Type': - return InlayHintKind.Type; - case 'Enum': - return undefined; - default: - return undefined; - } - } - - private isSvelte2tsxFunctionHints(sourceFile: ts.SourceFile, inlayHint: ts.InlayHint): boolean { - if (inlayHint.kind !== ts.InlayHintKind.Parameter) { - return false; - } - - const node = findClosestContainingNode( - sourceFile, - { start: inlayHint.position, length: 0 }, - ts.isCallOrNewExpression - ); - - if (!node) { - return false; - } - - const expressionText = node.expression.getText(); - const isComponentEventHandler = expressionText.includes('.$on'); - - return ( - isComponentEventHandler || - expressionText.includes('.createElement') || - expressionText.includes('__sveltets_') || - expressionText.startsWith('$$_') - ); - } - - private isGeneratedVariableTypeHint( - sourceFile: ts.SourceFile, - inlayHint: ts.InlayHint - ): boolean { - if (inlayHint.kind !== ts.InlayHintKind.Type) { - return false; - } - - const declaration = findContainingNode( - sourceFile, - { start: inlayHint.position, length: 0 }, - ts.isVariableDeclaration - ); - - if (!declaration) { - return false; - } - - // $$_tnenopmoC, $$_value, $$props, $$slots, $$restProps... - return ( - isInGeneratedCode(sourceFile.text, declaration.pos) || - declaration.name.getText().startsWith('$$') - ); - } - - private isGeneratedFunctionReturnType(sourceFile: ts.SourceFile, inlayHint: ts.InlayHint) { - if (inlayHint.kind !== ts.InlayHintKind.Type) { - return false; - } - - // $: a = something - // it's always top level and shouldn't be under other function call - // so we don't need to use findClosestContainingNode - const expression = findContainingNode( - sourceFile, - { start: inlayHint.position, length: 0 }, - (node): node is IdentifierCallExpression => - ts.isCallExpression(node) && ts.isIdentifier(node.expression) - ); - - if (!expression) { - return false; - } - - return ( - expression.expression.text === '__sveltets_2_invalidate' && - ts.isArrowFunction(expression.arguments[0]) && - this.getTypeAnnotationPosition(expression.arguments[0]) === inlayHint.position - ); - } - - private getTypeAnnotationPosition( - decl: - | ts.FunctionDeclaration - | ts.ArrowFunction - | ts.FunctionExpression - | ts.MethodDeclaration - | ts.GetAccessorDeclaration - ) { - const closeParenToken = findChildOfKind(decl, ts.SyntaxKind.CloseParenToken); - if (closeParenToken) { - return closeParenToken.end; - } - return decl.parameters.end; - } - - private checkGeneratedFunctionHintWithSource(inlayHint: InlayHint, document: Document) { - if (isInTag(inlayHint.position, document.moduleScriptInfo)) { - return false; - } - - if (isInTag(inlayHint.position, document.scriptInfo)) { - return document - .getText() - .slice(document.offsetAt(inlayHint.position)) - .trimStart() - .startsWith('$:'); - } - - const attributeContext = getAttributeContextAtPosition(document, inlayHint.position); - - if (!attributeContext || attributeContext.inValue || !attributeContext.name.includes(':')) { - return false; - } - - const { name, elementTag } = attributeContext; - - //
- if (name.startsWith('on:') && !elementTag.attributes?.[attributeContext.name]) { - return true; - } - - const directives = ['in', 'out', 'animate', 'transition', 'use']; - - // hide - // - transitionCall: for __sveltets_2_ensureTransition - // - tag: for svelteHTML.mapElementTag inside transition call and action call - // - animationCall: for __sveltets_2_ensureAnimation - // - actionCall for __sveltets_2_ensureAction - return directives.some((directive) => name.startsWith(directive + ':')); - } -} - -interface IdentifierCallExpression extends ts.CallExpression { - expression: ts.Identifier; -} diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts deleted file mode 100644 index ff1902766..000000000 --- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts +++ /dev/null @@ -1,671 +0,0 @@ -import { Position, WorkspaceEdit, Range } from 'vscode-languageserver'; -import { - Document, - mapRangeToOriginal, - getLineAtPosition, - getNodeIfIsInStartTag, - isInHTMLTagRange, - getNodeIfIsInHTMLStartTag -} from '../../../lib/documents'; -import { - createGetCanonicalFileName, - filterAsync, - isNotNullOrUndefined, - pathToUrl, - unique -} from '../../../utils'; -import { RenameProvider } from '../../interfaces'; -import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { convertRange } from '../utils'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import ts from 'typescript'; -import { - isComponentAtPosition, - isAfterSvelte2TsxPropsReturn, - isTextSpanInGeneratedCode, - SnapshotMap, - isStoreVariableIn$storeDeclaration, - get$storeOffsetOf$storeDeclaration, - getStoreOffsetOf$storeDeclaration, - is$storeVariableIn$storeDeclaration -} from './utils'; -import { LSConfigManager } from '../../../ls-config'; -import { isAttributeName, isEventHandler } from '../svelte-ast-utils'; - -interface TsRenameLocation extends ts.RenameLocation { - range: Range; - newName?: string; -} - -export class RenameProviderImpl implements RenameProvider { - constructor( - private readonly lsAndTsDocResolver: LSAndTSDocResolver, - private readonly configManager: LSConfigManager - ) {} - - // TODO props written as `export {x as y}` are not supported yet. - - async prepareRename(document: Document, position: Position): Promise { - const { lang, tsDoc } = await this.getLSAndTSDoc(document); - - const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); - const renameInfo = this.getRenameInfo(lang, tsDoc, document, position, offset); - if (!renameInfo) { - return null; - } - - return this.mapRangeToOriginal(tsDoc, renameInfo.triggerSpan); - } - - async rename( - document: Document, - position: Position, - newName: string - ): Promise { - const { lang, tsDoc } = await this.getLSAndTSDoc(document); - - const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); - - const renameInfo = this.getRenameInfo(lang, tsDoc, document, position, offset); - if (!renameInfo) { - return null; - } - - const renameLocations = lang.findRenameLocations( - tsDoc.filePath, - offset, - false, - false, - true - ); - if (!renameLocations) { - return null; - } - - const docs = new SnapshotMap(this.lsAndTsDocResolver); - docs.set(tsDoc.filePath, tsDoc); - - let convertedRenameLocations: TsRenameLocation[] = await this.mapAndFilterRenameLocations( - renameLocations, - docs, - renameInfo.isStore ? `$${newName}` : undefined - ); - - convertedRenameLocations.push( - ...(await this.enhanceRenamesInCaseOf$Store(renameLocations, newName, docs, lang)) - ); - - convertedRenameLocations = this.checkShortHandBindingOrSlotLetLocation( - lang, - convertedRenameLocations, - docs - ); - - const additionalRenameForPropRenameInsideComponentWithProp = - await this.getAdditionLocationsForRenameOfPropInsideComponentWithProp( - document, - tsDoc, - position, - convertedRenameLocations, - docs, - lang, - newName - ); - const additionalRenamesForPropRenameOutsideComponentWithProp = - // This is an either-or-situation, don't do both - additionalRenameForPropRenameInsideComponentWithProp.length > 0 - ? [] - : await this.getAdditionalLocationsForRenameOfPropInsideOtherComponent( - convertedRenameLocations, - docs, - lang, - tsDoc.filePath - ); - convertedRenameLocations = [ - ...convertedRenameLocations, - ...additionalRenameForPropRenameInsideComponentWithProp, - ...additionalRenamesForPropRenameOutsideComponentWithProp - ]; - - return unique( - convertedRenameLocations.filter( - (loc) => loc.range.start.line >= 0 && loc.range.end.line >= 0 - ) - ).reduce( - (acc, loc) => { - const uri = pathToUrl(loc.fileName); - if (!acc.changes[uri]) { - acc.changes[uri] = []; - } - acc.changes[uri].push({ - newText: - (loc.prefixText || '') + (loc.newName || newName) + (loc.suffixText || ''), - range: loc.range - }); - return acc; - }, - >>{ changes: {} } - ); - } - - private getRenameInfo( - lang: ts.LanguageService, - tsDoc: SvelteDocumentSnapshot, - doc: Document, - originalPosition: Position, - generatedOffset: number - ): - | (ts.RenameInfoSuccess & { - isStore?: boolean; - }) - | null { - // Don't allow renames in error-state, because then there is no generated svelte2tsx-code - // and rename cannot work - if (tsDoc.parserError) { - return null; - } - - const renameInfo = lang.getRenameInfo(tsDoc.filePath, generatedOffset, { - allowRenameOfImportPath: false - }); - - if ( - !renameInfo.canRename || - renameInfo.fullDisplayName?.includes('JSX.IntrinsicElements') || - (renameInfo.kind === ts.ScriptElementKind.jsxAttribute && - !isComponentAtPosition(doc, tsDoc, originalPosition)) - ) { - return null; - } - - const svelteNode = tsDoc.svelteNodeAt(originalPosition); - if ( - isInHTMLTagRange(doc.html, doc.offsetAt(originalPosition)) || - isAttributeName(svelteNode, 'Element') || - isEventHandler(svelteNode, 'Element') - ) { - return null; - } - - // If $store is renamed, only allow rename for $|store| - if (tsDoc.getFullText().charAt(renameInfo.triggerSpan.start) === '$') { - const definition = lang.getDefinitionAndBoundSpan(tsDoc.filePath, generatedOffset) - ?.definitions?.[0]; - if (definition && isTextSpanInGeneratedCode(tsDoc.getFullText(), definition.textSpan)) { - renameInfo.triggerSpan.start++; - renameInfo.triggerSpan.length--; - (renameInfo as any).isStore = true; - } - } - - return renameInfo; - } - - /** - * If the user renames a store variable, we need to rename the corresponding $store variables - * and vice versa. - */ - private async enhanceRenamesInCaseOf$Store( - renameLocations: readonly ts.RenameLocation[], - newName: string, - docs: SnapshotMap, - lang: ts.LanguageService - ): Promise { - for (const loc of renameLocations) { - const snapshot = await docs.retrieve(loc.fileName); - if (isTextSpanInGeneratedCode(snapshot.getFullText(), loc.textSpan)) { - if ( - isStoreVariableIn$storeDeclaration(snapshot.getFullText(), loc.textSpan.start) - ) { - // User renamed store, also rename corresponding $store locations - const storeRenameLocations = lang.findRenameLocations( - snapshot.filePath, - get$storeOffsetOf$storeDeclaration( - snapshot.getFullText(), - loc.textSpan.start - ), - false, - false, - true - ); - return await this.mapAndFilterRenameLocations( - storeRenameLocations!, - docs, - `$${newName}` - ); - } else if ( - is$storeVariableIn$storeDeclaration(snapshot.getFullText(), loc.textSpan.start) - ) { - // User renamed $store, also rename corresponding store - const storeRenameLocations = lang.findRenameLocations( - snapshot.filePath, - getStoreOffsetOf$storeDeclaration( - snapshot.getFullText(), - loc.textSpan.start - ), - false, - false, - true - ); - return await this.mapAndFilterRenameLocations(storeRenameLocations!, docs); - // TODO once we allow providePrefixAndSuffixTextForRename to be configurable, - // we need to add one more step to update all other $store usages in other files - } - } - } - return []; - } - - /** - * If user renames prop of component A inside component A, - * we need to handle the rename of the prop of A ourselves. - * Reason: the rename will do {oldPropName: newPropName}, meaning - * the rename will not propagate further, so we have to handle - * the conversion to {newPropName: newPropName} ourselves. - */ - private async getAdditionLocationsForRenameOfPropInsideComponentWithProp( - document: Document, - tsDoc: SvelteDocumentSnapshot, - position: Position, - convertedRenameLocations: TsRenameLocation[], - snapshots: SnapshotMap, - lang: ts.LanguageService, - newName: string - ) { - // First find out if it's really the "rename prop inside component with that prop" case - // Use original document for that because only there the `export` is present. - // ':' for typescript's type operator (`export let bla: boolean`) - // '//' and '/*' for comments (`export let bla// comment` or `export let bla/* comment */`) - const regex = new RegExp( - `export\\s+let\\s+${this.getVariableAtPosition( - tsDoc, - lang, - position - )}($|\\s|;|:|\/\*|\/\/)` - ); - const isRenameInsideComponentWithProp = regex.test( - getLineAtPosition(position, document.getText()) - ); - if (!isRenameInsideComponentWithProp) { - return []; - } - // We now know that the rename happens at `export let X` -> let's find the corresponding - // prop rename further below in the document. - const updatePropLocation = this.findLocationWhichWantsToUpdatePropName( - convertedRenameLocations, - snapshots - ); - if (!updatePropLocation) { - return []; - } - // Typescript does a rename of `oldPropName: newPropName` -> find oldPropName and rename that, too. - const idxOfOldPropName = tsDoc - .getFullText() - .lastIndexOf(':', updatePropLocation.textSpan.start); - // This requires svelte2tsx to have the properties written down like `return props: {bla: bla}`. - // It would not work for `return props: {bla}` because then typescript would do a rename of `{bla: renamed}`, - // so other locations would not be affected. - const replacementsForProp = ( - lang.findRenameLocations( - updatePropLocation.fileName, - idxOfOldPropName, - false, - false, - true - ) || [] - ).filter( - (rename) => - // filter out all renames inside the component except the prop rename, - // because the others were done before and then would show up twice, making a wrong rename. - rename.fileName !== updatePropLocation.fileName || - this.isInSvelte2TsxPropLine(tsDoc, rename) - ); - - const renameLocations = await this.mapAndFilterRenameLocations( - replacementsForProp, - snapshots - ); - const bind = 'bind:'; - - // Adjust shorthands - return renameLocations.map((location) => { - if (updatePropLocation.fileName === location.fileName) { - return location; - } - - const sourceFile = lang.getProgram()?.getSourceFile(location.fileName); - - if ( - !sourceFile || - location.fileName !== sourceFile.fileName || - location.range.start.line < 0 || - location.range.end.line < 0 - ) { - return location; - } - - const snapshot = snapshots.get(location.fileName); - if (!(snapshot instanceof SvelteDocumentSnapshot)) { - return location; - } - - const { parent } = snapshot; - - let rangeStart = parent.offsetAt(location.range.start); - let suffixText = location.suffixText?.trimStart(); - - // suffix is of the form `: oldVarName` -> hints at a shorthand - if (!suffixText?.startsWith(':') || !getNodeIfIsInStartTag(parent.html, rangeStart)) { - return location; - } - - const original = parent.getText({ - start: Position.create( - location.range.start.line, - location.range.start.character - bind.length - ), - end: location.range.end - }); - - if (original.startsWith(bind)) { - // bind:|foo| -> bind:|newName|={foo} - return { - ...location, - prefixText: '', - suffixText: `={${original.slice(bind.length)}}` - }; - } - - if (snapshot.getOriginalText().charAt(rangeStart - 1) === '{') { - // {|foo|} -> |{foo|} - rangeStart--; - return { - ...location, - range: { - start: parent.positionAt(rangeStart), - end: location.range.end - }, - // |{foo|} -> newName=|{foo|} - newName: parent.getText(location.range), - prefixText: `${newName}={`, - suffixText: '' - }; - } - - return location; - }); - } - - /** - * If user renames prop of component A inside component B, - * we need to handle the rename of the prop of A ourselves. - * Reason: the rename will rename the prop in the computed svelte2tsx code, - * but not the `export let X` code in the original because the - * rename does not propagate further than the prop. - * This additional logic/propagation is done in this method. - */ - private async getAdditionalLocationsForRenameOfPropInsideOtherComponent( - convertedRenameLocations: TsRenameLocation[], - snapshots: SnapshotMap, - lang: ts.LanguageService, - requestedFileName: string - ) { - // Check if it's a prop rename - const updatePropLocation = this.findLocationWhichWantsToUpdatePropName( - convertedRenameLocations, - snapshots - ); - if (!updatePropLocation) { - return []; - } - const getCanonicalFileName = createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames); - if ( - getCanonicalFileName(updatePropLocation.fileName) === - getCanonicalFileName(requestedFileName) - ) { - return []; - } - // Find generated `export let` - const doc = snapshots.get(updatePropLocation.fileName); - const match = this.matchGeneratedExportLet(doc, updatePropLocation); - if (!match) { - return []; - } - // Use match to replace that let, too. - const idx = (match.index || 0) + match[0].lastIndexOf(match[1]); - const replacementsForProp = - lang.findRenameLocations(updatePropLocation.fileName, idx, false, false) || []; - - return this.checkShortHandBindingOrSlotLetLocation( - lang, - await this.mapAndFilterRenameLocations(replacementsForProp, snapshots), - snapshots - ); - } - - // --------> svelte2tsx? - private matchGeneratedExportLet( - snapshot: SvelteDocumentSnapshot, - updatePropLocation: ts.RenameLocation - ) { - const regex = new RegExp( - // no 'export let', only 'let', because that's what it's translated to in svelte2tsx - // '//' and '/*' for comments (`let bla/*Ωignore_startΩ*/`) - `\\s+let\\s+(${snapshot - .getFullText() - .substring( - updatePropLocation.textSpan.start, - updatePropLocation.textSpan.start + updatePropLocation.textSpan.length - )})($|\\s|;|:|\/\*|\/\/)` - ); - const match = snapshot.getFullText().match(regex); - return match; - } - - private findLocationWhichWantsToUpdatePropName( - convertedRenameLocations: TsRenameLocation[], - snapshots: SnapshotMap - ) { - return convertedRenameLocations.find((loc) => { - // Props are not in mapped range - if (loc.range.start.line >= 0 && loc.range.end.line >= 0) { - return; - } - - const snapshot = snapshots.get(loc.fileName); - // Props are in svelte snapshots only - if (!(snapshot instanceof SvelteDocumentSnapshot)) { - return false; - } - - return this.isInSvelte2TsxPropLine(snapshot, loc); - }); - } - - // --------> svelte2tsx? - private isInSvelte2TsxPropLine(snapshot: SvelteDocumentSnapshot, loc: ts.RenameLocation) { - return isAfterSvelte2TsxPropsReturn(snapshot.getFullText(), loc.textSpan.start); - } - - /** - * The rename locations the ts language services hands back are relative to the - * svelte2tsx generated code -> map it back to the original document positions. - * Some of those positions could be unmapped (line=-1), these are handled elsewhere. - * Also filter out wrong renames. - */ - private async mapAndFilterRenameLocations( - renameLocations: readonly ts.RenameLocation[], - snapshots: SnapshotMap, - newName?: string - ): Promise { - const mappedLocations = await Promise.all( - renameLocations.map(async (loc) => { - const snapshot = await snapshots.retrieve(loc.fileName); - - if (!isTextSpanInGeneratedCode(snapshot.getFullText(), loc.textSpan)) { - return { - ...loc, - range: this.mapRangeToOriginal(snapshot, loc.textSpan), - newName - }; - } - }) - ); - return this.filterWrongRenameLocations(mappedLocations.filter(isNotNullOrUndefined)); - } - - private filterWrongRenameLocations( - mappedLocations: TsRenameLocation[] - ): Promise { - return filterAsync(mappedLocations, async (loc) => { - const snapshot = await this.getSnapshot(loc.fileName); - if (!(snapshot instanceof SvelteDocumentSnapshot)) { - return true; - } - - const content = snapshot.getText(0, snapshot.getLength()); - // When the user renames a Svelte component, ts will also want to rename - // `__sveltets_2_instanceOf(TheComponentToRename)` or - // `__sveltets_1_ensureType(TheComponentToRename,..`. Prevent that. - // Additionally, we cannot rename the hidden variable containing the store value - return ( - notPrecededBy('__sveltets_2_instanceOf(') && - notPrecededBy('__sveltets_1_ensureType(') && // no longer necessary for new transformation - notPrecededBy('= __sveltets_2_store_get(') - ); - - function notPrecededBy(str: string) { - return ( - content.lastIndexOf(str, loc.textSpan.start) !== loc.textSpan.start - str.length - ); - } - }); - } - - private mapRangeToOriginal(snapshot: DocumentSnapshot, textSpan: ts.TextSpan): Range { - // We need to work around a current svelte2tsx limitation: Replacements and - // source mapping is done in such a way that sometimes the end of the range is unmapped - // and the index of the last character is returned instead (which is one less). - // Most of the time this is not much of a problem, but in the context of renaming, it is. - // We work around that by adding +1 to the end, if necessary. - // This can be done because - // 1. we know renames can only ever occur in one line - // 2. the generated svelte2tsx code will not modify variable names, so we know - // the original range should be the same length as the textSpan's length - const range = mapRangeToOriginal(snapshot, convertRange(snapshot, textSpan)); - if (range.end.character - range.start.character < textSpan.length) { - range.end.character++; - } - return range; - } - - private getVariableAtPosition( - tsDoc: SvelteDocumentSnapshot, - lang: ts.LanguageService, - position: Position - ) { - const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); - const { start, length } = lang.getSmartSelectionRange(tsDoc.filePath, offset).textSpan; - return tsDoc.getText(start, start + length); - } - - private async getLSAndTSDoc(document: Document) { - return this.lsAndTsDocResolver.getLSAndTSDoc(document); - } - - private getSnapshot(filePath: string) { - return this.lsAndTsDocResolver.getSnapshot(filePath); - } - - private checkShortHandBindingOrSlotLetLocation( - lang: ts.LanguageService, - renameLocations: TsRenameLocation[], - snapshots: SnapshotMap - ): TsRenameLocation[] { - const bind = 'bind:'; - - return renameLocations.map((location) => { - const sourceFile = lang.getProgram()?.getSourceFile(location.fileName); - - if ( - !sourceFile || - location.fileName !== sourceFile.fileName || - location.range.start.line < 0 || - location.range.end.line < 0 - ) { - return location; - } - - const snapshot = snapshots.get(location.fileName); - if (!(snapshot instanceof SvelteDocumentSnapshot)) { - return location; - } - - const { parent } = snapshot; - - let rangeStart = parent.offsetAt(location.range.start); - let prefixText = location.prefixText?.trimRight(); - - // rename needs to be prefixed in case of a bind shorthand on a HTML element - if (!prefixText) { - const original = parent.getText({ - start: Position.create( - location.range.start.line, - location.range.start.character - bind.length - ), - end: location.range.end - }); - if ( - original.startsWith(bind) && - getNodeIfIsInHTMLStartTag(parent.html, rangeStart) - ) { - return { - ...location, - prefixText: original.slice(bind.length) + '={', - suffixText: '}' - }; - } - } - - if (!prefixText || prefixText.slice(-1) !== ':') { - return location; - } - - // prefix is of the form `oldVarName: ` -> hints at a shorthand - // we need to make sure we only adjust shorthands on elements/components - if ( - !getNodeIfIsInStartTag(parent.html, rangeStart) || - // shorthands: let:xx, bind:xx, {xx} - (parent.getText().charAt(rangeStart - 1) !== ':' && - // not use:action={{foo}} - !/[^{]\s+{$/.test( - parent.getText({ - start: Position.create(0, 0), - end: location.range.start - }) - )) - ) { - return location; - } - - prefixText = prefixText.slice(0, -1) + '={'; - location = { - ...location, - prefixText, - suffixText: '}' - }; - - // rename range needs to be adjusted in case of an attribute shorthand - if (snapshot.getOriginalText().charAt(rangeStart - 1) === '{') { - rangeStart--; - const rangeEnd = parent.offsetAt(location.range.end) + 1; - location.range = { - start: parent.positionAt(rangeStart), - end: parent.positionAt(rangeEnd) - }; - } - - return location; - }); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts b/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts deleted file mode 100644 index bd7cb1c35..000000000 --- a/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts +++ /dev/null @@ -1,70 +0,0 @@ -import ts from 'typescript'; -import { Position, Range, SelectionRange } from 'vscode-languageserver'; -import { Document, mapSelectionRangeToParent } from '../../../lib/documents'; -import { SelectionRangeProvider } from '../../interfaces'; -import { SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { convertRange } from '../utils'; - -export class SelectionRangeProviderImpl implements SelectionRangeProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} - - async getSelectionRange( - document: Document, - position: Position - ): Promise { - const { tsDoc, lang } = await this.lsAndTsDocResolver.getLsForSyntheticOperations(document); - - const tsSelectionRange = lang.getSmartSelectionRange( - tsDoc.filePath, - tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)) - ); - const selectionRange = this.toSelectionRange(tsDoc, tsSelectionRange); - const mappedRange = mapSelectionRangeToParent(tsDoc, selectionRange); - - return this.filterOutUnmappedRange(mappedRange); - } - - private toSelectionRange( - snapshot: SvelteDocumentSnapshot, - { textSpan, parent }: ts.SelectionRange - ): SelectionRange { - return { - range: convertRange(snapshot, textSpan), - parent: parent && this.toSelectionRange(snapshot, parent) - }; - } - - private filterOutUnmappedRange(selectionRange: SelectionRange): SelectionRange | null { - const flattened = this.flattenAndReverseSelectionRange(selectionRange); - const filtered = flattened.filter((range) => range.start.line > 0 && range.end.line > 0); - if (!filtered.length) { - return null; - } - - let result: SelectionRange | undefined; - - for (const selectionRange of filtered) { - result = SelectionRange.create(selectionRange, result); - } - - return result ?? null; - } - - /** - * flatten the selection range and its parent to an array in reverse order - * so it's easier to filter out unmapped selection and create a new tree of - * selection range - */ - private flattenAndReverseSelectionRange(selectionRange: SelectionRange) { - const result: Range[] = []; - let current = selectionRange; - - while (current.parent) { - result.unshift(current.range); - current = current.parent; - } - - return result; - } -} diff --git a/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts b/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts deleted file mode 100644 index 9af4f4239..000000000 --- a/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts +++ /dev/null @@ -1,153 +0,0 @@ -import ts from 'typescript'; -import { - CancellationToken, - Range, - SemanticTokens, - SemanticTokensBuilder -} from 'vscode-languageserver'; -import { Document, mapRangeToOriginal } from '../../../lib/documents'; -import { SemanticTokensProvider } from '../../interfaces'; -import { SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { convertToTextSpan } from '../utils'; -import { isInGeneratedCode } from './utils'; - -const CONTENT_LENGTH_LIMIT = 50000; - -export class SemanticTokensProviderImpl implements SemanticTokensProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} - - async getSemanticTokens( - textDocument: Document, - range?: Range, - cancellationToken?: CancellationToken - ): Promise { - const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(textDocument); - - // for better performance, don't do full-file semantic tokens when the file is too big - if ( - (!range && tsDoc.getLength() > CONTENT_LENGTH_LIMIT) || - cancellationToken?.isCancellationRequested - ) { - return null; - } - - // No script tags -> nothing to analyse semantic tokens for - if (!textDocument.scriptInfo && !textDocument.moduleScriptInfo) { - return null; - } - - const textSpan = range - ? convertToTextSpan(range, tsDoc) - : { - start: 0, - length: tsDoc.parserError - ? tsDoc.getLength() - : // This is appended by svelte2tsx, there's nothing mappable afterwards - tsDoc.getFullText().lastIndexOf('return { props:') || tsDoc.getLength() - }; - - const { spans } = lang.getEncodedSemanticClassifications( - tsDoc.filePath, - textSpan, - ts.SemanticClassificationFormat.TwentyTwenty - ); - - const data: Array<[number, number, number, number, number]> = []; - let index = 0; - - while (index < spans.length) { - // [start, length, encodedClassification, start2, length2, encodedClassification2] - const generatedOffset = spans[index++]; - const generatedLength = spans[index++]; - const encodedClassification = spans[index++]; - const classificationType = this.getTokenTypeFromClassification(encodedClassification); - if (classificationType < 0) { - continue; - } - - const originalPosition = this.mapToOrigin( - textDocument, - tsDoc, - generatedOffset, - generatedLength, - encodedClassification - ); - if (!originalPosition) { - continue; - } - - const [line, character, length] = originalPosition; - - // remove identifiers whose start and end mapped to the same location, - // like the svelte2tsx inserted render function, - // or reversed like Component.$on - if (length <= 0) { - continue; - } - - const modifier = this.getTokenModifierFromClassification(encodedClassification); - - data.push([line, character, length, classificationType, modifier]); - } - - const sorted = data.sort((a, b) => { - const [lineA, charA] = a; - const [lineB, charB] = b; - - return lineA - lineB || charA - charB; - }); - - const builder = new SemanticTokensBuilder(); - sorted.forEach((tokenData) => builder.push(...tokenData)); - return builder.build(); - } - - private mapToOrigin( - document: Document, - snapshot: SvelteDocumentSnapshot, - generatedOffset: number, - generatedLength: number, - token: number - ): [line: number, character: number, length: number, start: number] | undefined { - const text = snapshot.getFullText(); - if ( - isInGeneratedCode(text, generatedOffset, generatedOffset + generatedLength) || - (token === 2817 /* top level function */ && - text.substring(generatedOffset, generatedOffset + generatedLength) === 'render') - ) { - return; - } - - const range = { - start: snapshot.positionAt(generatedOffset), - end: snapshot.positionAt(generatedOffset + generatedLength) - }; - const { start: startPosition, end: endPosition } = mapRangeToOriginal(snapshot, range); - - if (startPosition.line < 0 || endPosition.line < 0) { - return; - } - - const startOffset = document.offsetAt(startPosition); - const endOffset = document.offsetAt(endPosition); - - return [startPosition.line, startPosition.character, endOffset - startOffset, startOffset]; - } - - /** - * TSClassification = (TokenType + 1) << TokenEncodingConsts.typeOffset + TokenModifier - */ - private getTokenTypeFromClassification(tsClassification: number): number { - return (tsClassification >> TokenEncodingConsts.typeOffset) - 1; - } - - private getTokenModifierFromClassification(tsClassification: number) { - return tsClassification & TokenEncodingConsts.modifierMask; - } -} - -const enum TokenEncodingConsts { - typeOffset = 8, - modifierMask = (1 << typeOffset) - 1 -} diff --git a/packages/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts b/packages/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts deleted file mode 100644 index 29d8f1e7d..000000000 --- a/packages/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts +++ /dev/null @@ -1,156 +0,0 @@ -import ts from 'typescript'; -import { - Position, - SignatureHelpContext, - SignatureHelp, - SignatureHelpTriggerKind, - SignatureInformation, - ParameterInformation, - MarkupKind, - CancellationToken -} from 'vscode-languageserver'; -import { SignatureHelpProvider } from '../..'; -import { Document } from '../../../lib/documents'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { getMarkdownDocumentation } from '../previewer'; - -export class SignatureHelpProviderImpl implements SignatureHelpProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} - - private static readonly triggerCharacters = ['(', ',', '<']; - private static readonly retriggerCharacters = [')']; - - async getSignatureHelp( - document: Document, - position: Position, - context: SignatureHelpContext | undefined, - cancellationToken?: CancellationToken - ): Promise { - const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); - - if (cancellationToken?.isCancellationRequested) { - return null; - } - - const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); - const triggerReason = this.toTsTriggerReason(context); - const info = lang.getSignatureHelpItems( - tsDoc.filePath, - offset, - triggerReason ? { triggerReason } : undefined - ); - if ( - !info || - info.items.some((signature) => this.isInSvelte2tsxGeneratedFunction(signature)) - ) { - return null; - } - - const signatures = info.items.map(this.toSignatureHelpInformation); - - return { - signatures, - activeSignature: info.selectedItemIndex, - activeParameter: info.argumentIndex - }; - } - - private isReTrigger( - isRetrigger: boolean, - triggerCharacter: string - ): triggerCharacter is ts.SignatureHelpRetriggerCharacter { - return ( - isRetrigger && - (this.isTriggerCharacter(triggerCharacter) || - SignatureHelpProviderImpl.retriggerCharacters.includes(triggerCharacter)) - ); - } - - private isTriggerCharacter( - triggerCharacter: string - ): triggerCharacter is ts.SignatureHelpTriggerCharacter { - return SignatureHelpProviderImpl.triggerCharacters.includes(triggerCharacter); - } - - /** - * adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L103 - */ - private toTsTriggerReason( - context: SignatureHelpContext | undefined - ): ts.SignatureHelpTriggerReason { - switch (context?.triggerKind) { - case SignatureHelpTriggerKind.TriggerCharacter: - if (context.triggerCharacter) { - if (this.isReTrigger(context.isRetrigger, context.triggerCharacter)) { - return { kind: 'retrigger', triggerCharacter: context.triggerCharacter }; - } - if (this.isTriggerCharacter(context.triggerCharacter)) { - return { - kind: 'characterTyped', - triggerCharacter: context.triggerCharacter - }; - } - } - return { kind: 'invoked' }; - case SignatureHelpTriggerKind.ContentChange: - return context.isRetrigger ? { kind: 'retrigger' } : { kind: 'invoked' }; - - case SignatureHelpTriggerKind.Invoked: - default: - return { kind: 'invoked' }; - } - } - - /** - * adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L73 - */ - private toSignatureHelpInformation(item: ts.SignatureHelpItem): SignatureInformation { - const [prefixLabel, separatorLabel, suffixLabel] = [ - item.prefixDisplayParts, - item.separatorDisplayParts, - item.suffixDisplayParts - ].map(ts.displayPartsToString); - - let textIndex = prefixLabel.length; - let signatureLabel = ''; - const parameters: ParameterInformation[] = []; - const lastIndex = item.parameters.length - 1; - - item.parameters.forEach((parameter, index) => { - const label = ts.displayPartsToString(parameter.displayParts); - - const startIndex = textIndex; - const endIndex = textIndex + label.length; - const doc = ts.displayPartsToString(parameter.documentation); - - signatureLabel += label; - parameters.push(ParameterInformation.create([startIndex, endIndex], doc)); - - if (index < lastIndex) { - textIndex = endIndex + separatorLabel.length; - signatureLabel += separatorLabel; - } - }); - const signatureDocumentation = getMarkdownDocumentation( - item.documentation, - item.tags.filter((tag) => tag.name !== 'param') - ); - - return { - label: prefixLabel + signatureLabel + suffixLabel, - documentation: signatureDocumentation - ? { - value: signatureDocumentation, - kind: MarkupKind.Markdown - } - : undefined, - parameters - }; - } - - private isInSvelte2tsxGeneratedFunction(signatureHelpItem: ts.SignatureHelpItem) { - return signatureHelpItem.prefixDisplayParts.some((part) => - part.text.includes('__sveltets') - ); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts b/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts deleted file mode 100644 index 93674fd9e..000000000 --- a/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Position, Location } from 'vscode-languageserver-protocol'; -import { Document, mapLocationToOriginal } from '../../../lib/documents'; -import { isNotNullOrUndefined } from '../../../utils'; -import { TypeDefinitionProvider } from '../../interfaces'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { convertRange } from '../utils'; -import { isTextSpanInGeneratedCode, SnapshotMap } from './utils'; - -export class TypeDefinitionProviderImpl implements TypeDefinitionProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} - - async getTypeDefinition(document: Document, position: Position): Promise { - const { tsDoc, lang } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); - const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); - const typeDefs = lang.getTypeDefinitionAtPosition(tsDoc.filePath, offset); - - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); - snapshots.set(tsDoc.filePath, tsDoc); - - if (!typeDefs) { - return null; - } - - const result = await Promise.all( - typeDefs.map(async (typeDef) => { - const snapshot = await snapshots.retrieve(typeDef.fileName); - - if (isTextSpanInGeneratedCode(snapshot.getFullText(), typeDef.textSpan)) { - return; - } - - const location = mapLocationToOriginal( - snapshot, - convertRange(snapshot, typeDef.textSpan) - ); - - if (location.range.start.line >= 0 && location.range.end.line >= 0) { - return location; - } - }) - ); - - return result.filter(isNotNullOrUndefined); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts deleted file mode 100644 index aa46594ff..000000000 --- a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts +++ /dev/null @@ -1,102 +0,0 @@ -import path from 'path'; -import { - OptionalVersionedTextDocumentIdentifier, - TextDocumentEdit, - TextEdit, - WorkspaceEdit -} from 'vscode-languageserver'; -import { mapRangeToOriginal } from '../../../lib/documents'; -import { urlToPath } from '../../../utils'; -import { FileRename, UpdateImportsProvider } from '../../interfaces'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { convertRange } from '../utils'; -import { isKitTypePath, SnapshotMap } from './utils'; - -export class UpdateImportsProviderImpl implements UpdateImportsProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} - - async updateImports(fileRename: FileRename): Promise { - // TODO does this handle folder moves/renames correctly? old/new path isn't a file then - const oldPath = urlToPath(fileRename.oldUri); - const newPath = urlToPath(fileRename.newUri); - if (!oldPath || !newPath) { - return null; - } - - const ls = await this.getLSForPath(newPath); - const oldPathTsProgramCasing = ls.getProgram()?.getSourceFile(oldPath)?.fileName ?? oldPath; - // `getEditsForFileRename` might take a while - const fileChanges = ls - .getEditsForFileRename(oldPathTsProgramCasing, newPath, {}, {}) - // Assumption: Updating imports will not create new files, and to make sure just filter those out - // who - for whatever reason - might be new ones. - .filter((change) => !change.isNewFile || change.fileName === oldPathTsProgramCasing); - - await this.lsAndTsDocResolver.updateSnapshotPath(oldPathTsProgramCasing, newPath); - - const editInOldPath = fileChanges.find( - (change) => - change.fileName.startsWith(oldPathTsProgramCasing) && - (oldPathTsProgramCasing.includes(newPath) || !change.fileName.startsWith(newPath)) - ); - const editInNewPath = fileChanges.find( - (change) => - change.fileName.startsWith(newPath) && - (newPath.includes(oldPathTsProgramCasing) || - !change.fileName.startsWith(oldPathTsProgramCasing)) - ); - const updateImportsChanges = fileChanges - .filter((change) => { - if (isKitTypePath(change.fileName)) { - // These types are generated from the route files, so we don't want to update them - return false; - } - if (!editInOldPath || !editInNewPath) { - return true; - } - // If both present, take the one that has more text changes to it (more likely to be the correct one) - return editInOldPath.textChanges.length > editInNewPath.textChanges.length - ? change !== editInNewPath - : change !== editInOldPath; - }) - .map((change) => { - if (change === editInOldPath) { - // The language service might want to do edits to the old path, not the new path -> rewire it. - // If there is a better solution for this, please file a PR :) - change.fileName = change.fileName.replace(oldPathTsProgramCasing, newPath); - } - change.textChanges = change.textChanges.filter( - (textChange) => - // Filter out changes to './$type' imports for Kit route files, - // you'll likely want these to stay as-is - !isKitTypePath(textChange.newText) || - !path.basename(change.fileName).startsWith('+') - ); - return change; - }); - - const docs = new SnapshotMap(this.lsAndTsDocResolver); - const documentChanges = await Promise.all( - updateImportsChanges.map(async (change) => { - const snapshot = await docs.retrieve(change.fileName); - - return TextDocumentEdit.create( - OptionalVersionedTextDocumentIdentifier.create(snapshot.getURL(), null), - change.textChanges.map((edit) => { - const range = mapRangeToOriginal( - snapshot, - convertRange(snapshot, edit.span) - ); - return TextEdit.replace(range, edit.newText); - }) - ); - }) - ); - - return { documentChanges }; - } - - private async getLSForPath(path: string) { - return this.lsAndTsDocResolver.getLSForPath(path); - } -} diff --git a/packages/language-server/src/plugins/typescript/features/getDirectiveCommentCompletions.ts b/packages/language-server/src/plugins/typescript/features/getDirectiveCommentCompletions.ts deleted file mode 100644 index 825a9e4d0..000000000 --- a/packages/language-server/src/plugins/typescript/features/getDirectiveCommentCompletions.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Document, isInTag } from '../../../lib/documents'; -import { - Position, - CompletionItemKind, - CompletionItem, - TextEdit, - Range, - CompletionList, - CompletionContext -} from 'vscode-languageserver'; - -/** - * from https://github.com/microsoft/vscode/blob/157255fa4b0775c5ab8729565faf95927b610cac/extensions/typescript-language-features/src/languageFeatures/directiveCommentCompletions.ts#L19 - */ -export const tsDirectives = [ - { - value: '@ts-check', - description: 'Enables semantic checking in a JavaScript file. Must be at the top of a file.' - }, - { - value: '@ts-nocheck', - description: - 'Disables semantic checking in a JavaScript file. Must be at the top of a file.' - }, - { - value: '@ts-ignore', - description: 'Suppresses @ts-check errors on the next line of a file.' - }, - { - value: '@ts-expect-error', - description: - 'Suppresses @ts-check errors on the next line of a file, expecting at least one to exist.' - } -]; - -/** - * from https://github.com/microsoft/vscode/blob/157255fa4b0775c5ab8729565faf95927b610cac/extensions/typescript-language-features/src/languageFeatures/directiveCommentCompletions.ts#L64 - */ -export function getDirectiveCommentCompletions( - position: Position, - document: Document, - completionContext: CompletionContext | undefined -) { - // don't trigger until // @ - if (completionContext?.triggerCharacter === '/') { - return null; - } - - const inScript = isInTag(position, document.scriptInfo); - const inModule = isInTag(position, document.moduleScriptInfo); - if (!inModule && !inScript) { - return null; - } - - const lineStart = document.offsetAt(Position.create(position.line, 0)); - const offset = document.offsetAt(position); - const prefix = document.getText().slice(lineStart, offset); - const match = prefix.match(/^\s*\/\/+\s?(@[a-zA-Z-]*)?$/); - - if (!match) { - return null; - } - const startCharacter = Math.max(0, position.character - (match[1]?.length ?? 0)); - const start = Position.create(position.line, startCharacter); - - const items = tsDirectives.map(({ value, description }) => ({ - detail: description, - label: value, - kind: CompletionItemKind.Snippet, - textEdit: TextEdit.replace( - Range.create(start, Position.create(start.line, start.character + value.length)), - value - ) - })); - - return CompletionList.create(items, false); -} diff --git a/packages/language-server/src/plugins/typescript/features/getJsDocTemplateCompletion.ts b/packages/language-server/src/plugins/typescript/features/getJsDocTemplateCompletion.ts deleted file mode 100644 index 89303f297..000000000 --- a/packages/language-server/src/plugins/typescript/features/getJsDocTemplateCompletion.ts +++ /dev/null @@ -1,79 +0,0 @@ -import ts from 'typescript'; -import { - CompletionItem, - CompletionItemKind, - CompletionList, - InsertTextFormat, - Range, - TextEdit -} from 'vscode-languageserver'; -import { mapRangeToOriginal } from '../../../lib/documents'; -import { SvelteDocumentSnapshot } from '../DocumentSnapshot'; - -const DEFAULT_SNIPPET = `/**${ts.sys.newLine} * $0${ts.sys.newLine} */`; - -export function getJsDocTemplateCompletion( - snapshot: SvelteDocumentSnapshot, - lang: ts.LanguageService, - filePath: string, - offset: number -): CompletionList | null { - const template = lang.getDocCommentTemplateAtPosition(filePath, offset); - - if (!template) { - return null; - } - const text = snapshot.getFullText(); - const lineStart = text.lastIndexOf('\n', offset); - const lineEnd = text.indexOf('\n', offset); - const isLastLine = lineEnd === -1; - - const line = text.substring(lineStart, isLastLine ? undefined : lineEnd); - const character = offset - lineStart; - - const start = line.lastIndexOf('/**', character) + lineStart; - const suffix = line.slice(character).match(/^\s*\**\//); - const textEditRange = mapRangeToOriginal( - snapshot, - Range.create( - snapshot.positionAt(start), - snapshot.positionAt(offset + (suffix?.[0]?.length ?? 0)) - ) - ); - const { newText } = template; - const snippet = - // When typescript returns an empty single line template - // return the default multi-lines snippet, - // making it consistent with VSCode typescript - newText === '/** */' ? DEFAULT_SNIPPET : templateToSnippet(newText); - - const item: CompletionItem = { - label: '/** */', - detail: 'JSDoc comment', - sortText: '\0', - kind: CompletionItemKind.Snippet, - textEdit: TextEdit.replace(textEditRange, snippet), - insertTextFormat: InsertTextFormat.Snippet - }; - - return CompletionList.create([item]); -} - -/** - * adopted from https://github.com/microsoft/vscode/blob/a4b011697892ab656e1071b42c8af4b192078f28/extensions/typescript-language-features/src/languageFeatures/jsDocCompletions.ts#L94 - * Currently typescript won't return `@param` type template for files - * that has extension other than `.js` and `.jsx` - * So we don't need to insert snippet-tab-stop for it - */ -function templateToSnippet(text: string) { - return ( - text - // $ is for snippet tab stop - .replace(/\$/g, '\\$') - .split('\n') - // remove indent but not line break and let client handle it - .map((part) => part.replace(/^\s*(?=(\/|[ ]\*))/g, '')) - .join('\n') - .replace(/^(\/\*\*\s*\*[ ]*)$/m, (x) => x + '$0') - ); -} diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts deleted file mode 100644 index bd2e5cc7e..000000000 --- a/packages/language-server/src/plugins/typescript/features/utils.ts +++ /dev/null @@ -1,422 +0,0 @@ -import ts from 'typescript'; -import { Position } from 'vscode-languageserver'; -import { - Document, - getLineAtPosition, - getNodeIfIsInComponentStartTag, - isInTag -} from '../../../lib/documents'; -import { ComponentInfoProvider, JsOrTsComponentInfoProvider } from '../ComponentInfoProvider'; -import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot'; -import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; -import { or } from '../../../utils'; -import { FileMap } from '../../../lib/documents/fileCollection'; -import { LSConfig } from '../../../ls-config'; - -type NodePredicate = (node: ts.Node) => boolean; - -type NodeTypePredicate = (node: ts.Node) => node is T; - -/** - * If the given original position is within a Svelte starting tag, - * return the snapshot of that component. - */ -export function getComponentAtPosition( - lang: ts.LanguageService, - doc: Document, - tsDoc: SvelteDocumentSnapshot, - originalPosition: Position -): ComponentInfoProvider | null { - if (tsDoc.parserError) { - return null; - } - - if ( - isInTag(originalPosition, doc.scriptInfo) || - isInTag(originalPosition, doc.moduleScriptInfo) - ) { - // Inside script tags -> not a component - return null; - } - - const node = getNodeIfIsInComponentStartTag(doc.html, doc.offsetAt(originalPosition)); - if (!node) { - return null; - } - - const symbolPosWithinNode = node.tag?.includes('.') ? node.tag.lastIndexOf('.') + 1 : 0; - - const generatedPosition = tsDoc.getGeneratedPosition( - doc.positionAt(node.start + symbolPosWithinNode + 1) - ); - - let def = lang.getDefinitionAtPosition(tsDoc.filePath, tsDoc.offsetAt(generatedPosition))?.[0]; - - while (def != null && def.kind !== ts.ScriptElementKind.classElement) { - const newDef = lang.getDefinitionAtPosition(tsDoc.filePath, def.textSpan.start)?.[0]; - if (newDef?.fileName === def.fileName && newDef?.textSpan.start === def.textSpan.start) { - break; - } - def = newDef; - } - - if (!def) { - return null; - } - - return JsOrTsComponentInfoProvider.create(lang, def); -} - -export function isComponentAtPosition( - doc: Document, - tsDoc: SvelteDocumentSnapshot, - originalPosition: Position -): boolean { - if (tsDoc.parserError) { - return false; - } - - if ( - isInTag(originalPosition, doc.scriptInfo) || - isInTag(originalPosition, doc.moduleScriptInfo) - ) { - // Inside script tags -> not a component - return false; - } - - return !!getNodeIfIsInComponentStartTag(doc.html, doc.offsetAt(originalPosition)); -} - -export const IGNORE_START_COMMENT = '/*Ωignore_startΩ*/'; -export const IGNORE_END_COMMENT = '/*Ωignore_endΩ*/'; - -/** - * Surrounds given string with a start/end comment which marks it - * to be ignored by tooling. - */ -export function surroundWithIgnoreComments(str: string): string { - return IGNORE_START_COMMENT + str + IGNORE_END_COMMENT; -} - -/** - * Checks if this a section that should be completely ignored - * because it's purely generated. - */ -export function isInGeneratedCode(text: string, start: number, end: number = start) { - const lastStart = text.lastIndexOf(IGNORE_START_COMMENT, start); - const lastEnd = text.lastIndexOf(IGNORE_END_COMMENT, start); - const nextEnd = text.indexOf(IGNORE_END_COMMENT, end); - // if lastEnd === nextEnd, this means that the str was found at the index - // up to which is searched for it - return (lastStart > lastEnd || lastEnd === nextEnd) && lastStart < nextEnd; -} - -/** - * Checks if this is a text span that is inside svelte2tsx-generated code - * (has no mapping to the original) - */ -export function isTextSpanInGeneratedCode(text: string, span: ts.TextSpan) { - return isInGeneratedCode(text, span.start, span.start + span.length); -} - -export function isPartOfImportStatement(text: string, position: Position): boolean { - const line = getLineAtPosition(position, text); - return /\s*from\s+["'][^"']*/.test(line.slice(0, position.character)); -} - -export function isStoreVariableIn$storeDeclaration(text: string, varStart: number) { - return ( - text.lastIndexOf('__sveltets_2_store_get(', varStart) === - varStart - '__sveltets_2_store_get('.length - ); -} - -export function get$storeOffsetOf$storeDeclaration(text: string, storePosition: number) { - return text.lastIndexOf(' =', storePosition) - 1; -} - -export function is$storeVariableIn$storeDeclaration(text: string, varStart: number) { - return /^\$\w+ = __sveltets_2_store_get/.test(text.substring(varStart)); -} - -export function getStoreOffsetOf$storeDeclaration(text: string, $storeVarStart: number) { - return text.indexOf(');', $storeVarStart) - 1; -} - -export class SnapshotMap { - private map = new FileMap(); - constructor(private resolver: LSAndTSDocResolver) {} - - set(fileName: string, snapshot: DocumentSnapshot) { - this.map.set(fileName, snapshot); - } - - get(fileName: string) { - return this.map.get(fileName); - } - - async retrieve(fileName: string) { - let snapshot = this.get(fileName); - if (!snapshot) { - const snap = await this.resolver.getSnapshot(fileName); - this.set(fileName, snap); - snapshot = snap; - } - return snapshot; - } -} - -export function isAfterSvelte2TsxPropsReturn(text: string, end: number) { - const textBeforeProp = text.substring(0, end); - // This is how svelte2tsx writes out the props - if (textBeforeProp.includes('\nreturn { props: {')) { - return true; - } -} - -export function findContainingNode( - node: ts.Node, - textSpan: ts.TextSpan, - predicate: (node: ts.Node) => node is T -): T | undefined { - const children = node.getChildren(); - const end = textSpan.start + textSpan.length; - - for (const child of children) { - if (!(child.getStart() <= textSpan.start && child.getEnd() >= end)) { - continue; - } - - if (predicate(child)) { - return child; - } - - const foundInChildren = findContainingNode(child, textSpan, predicate); - if (foundInChildren) { - return foundInChildren; - } - } -} - -export function findClosestContainingNode( - node: ts.Node, - textSpan: ts.TextSpan, - predicate: (node: ts.Node) => node is T -): T | undefined { - let current = findContainingNode(node, textSpan, predicate); - if (!current) { - return; - } - - let closest = current; - - while (current) { - const foundInChildren: T | undefined = findContainingNode(current, textSpan, predicate); - - closest = current; - current = foundInChildren; - } - - return closest; -} - -/** - * Finds node exactly matching span {start, length}. - */ -export function findNodeAtSpan( - node: ts.Node, - span: { start: number; length: number }, - predicate?: NodeTypePredicate -): T | void { - const { start, length } = span; - - const end = start + length; - - for (const child of node.getChildren()) { - const childStart = child.getStart(); - if (end <= childStart) { - return; - } - - const childEnd = child.getEnd(); - if (start >= childEnd) { - continue; - } - - if (start === childStart && end === childEnd) { - if (!predicate) { - return child as T; - } - if (predicate(child)) { - return child; - } - } - - const foundInChildren = findNodeAtSpan(child, span, predicate); - if (foundInChildren) { - return foundInChildren; - } - } -} - -function isSomeAncestor(node: ts.Node, predicate: NodePredicate) { - for (let parent = node.parent; parent; parent = parent.parent) { - if (predicate(parent)) { - return true; - } - } - return false; -} - -/** - * Tests a node then its parent and successive ancestors for some respective predicates. - */ -function nodeAndParentsSatisfyRespectivePredicates( - selfPredicate: NodePredicate | NodeTypePredicate, - ...predicates: NodePredicate[] -) { - return (node: ts.Node | undefined | void | null): node is T => { - let next = node; - return [selfPredicate, ...predicates].every((predicate) => { - if (!next) { - return false; - } - const current = next; - next = next.parent; - return predicate(current); - }); - }; -} - -const isRenderFunction = nodeAndParentsSatisfyRespectivePredicates< - ts.FunctionDeclaration & { name: ts.Identifier } ->((node) => ts.isFunctionDeclaration(node) && node?.name?.getText() === 'render', ts.isSourceFile); - -const isRenderFunctionBody = nodeAndParentsSatisfyRespectivePredicates( - ts.isBlock, - isRenderFunction -); - -export const isReactiveStatement = nodeAndParentsSatisfyRespectivePredicates( - (node) => ts.isLabeledStatement(node) && node.label.getText() === '$', - or( - // function render() { - // $: x2 = __sveltets_2_invalidate(() => x * x) - // } - isRenderFunctionBody, - // function render() { - // ;() => {$: x, update(); - // } - nodeAndParentsSatisfyRespectivePredicates( - ts.isBlock, - ts.isArrowFunction, - ts.isExpressionStatement, - isRenderFunctionBody - ) - ) -); - -export function findRenderFunction(sourceFile: ts.SourceFile) { - // only search top level - for (const child of sourceFile.statements) { - if (isRenderFunction(child)) { - return child; - } - } -} - -export const isInReactiveStatement = (node: ts.Node) => isSomeAncestor(node, isReactiveStatement); - -export function gatherDescendants( - node: ts.Node, - predicate: NodeTypePredicate, - dest: T[] = [] -) { - if (predicate(node)) { - dest.push(node); - } else { - for (const child of node.getChildren()) { - gatherDescendants(child, predicate, dest); - } - } - return dest; -} - -export const gatherIdentifiers = (node: ts.Node) => gatherDescendants(node, ts.isIdentifier); - -export function isKitTypePath(path?: string): boolean { - return !!path?.includes('.svelte-kit/types'); -} - -export function getFormatCodeBasis(formatCodeSetting: ts.FormatCodeSettings): FormatCodeBasis { - const { baseIndentSize, indentSize, convertTabsToSpaces } = formatCodeSetting; - const baseIndent = convertTabsToSpaces - ? ' '.repeat(baseIndentSize ?? 4) - : baseIndentSize - ? '\t' - : ''; - const indent = convertTabsToSpaces ? ' '.repeat(indentSize ?? 4) : baseIndentSize ? '\t' : ''; - const semi = formatCodeSetting.semicolons === 'remove' ? '' : ';'; - const newLine = formatCodeSetting.newLineCharacter ?? ts.sys.newLine; - - return { - baseIndent, - indent, - semi, - newLine - }; -} - -export interface FormatCodeBasis { - baseIndent: string; - indent: string; - semi: string; - newLine: string; -} - -/** - * https://github.com/microsoft/TypeScript/blob/00dc0b6674eef3fbb3abb86f9d71705b11134446/src/services/utilities.ts#L2452 - */ -export function getQuotePreference( - sourceFile: ts.SourceFile, - preferences: ts.UserPreferences -): '"' | "'" { - const single = "'"; - const double = '"'; - if (preferences.quotePreference && preferences.quotePreference !== 'auto') { - return preferences.quotePreference === 'single' ? single : double; - } - - const firstModuleSpecifier = Array.from(sourceFile.statements).find( - ( - statement - ): statement is Omit & { - moduleSpecifier: ts.StringLiteral; - } => ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) - )?.moduleSpecifier; - - return firstModuleSpecifier - ? sourceFile.getText()[firstModuleSpecifier.pos] === '"' - ? double - : single - : double; -} -export function findChildOfKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined { - for (const child of node.getChildren()) { - if (child.kind === kind) { - return child; - } - - const foundInChildren = findChildOfKind(child, kind); - - if (foundInChildren) { - return foundInChildren; - } - } -} - -export function getNewScriptStartTag(lsConfig: Readonly) { - const lang = lsConfig.svelte.defaultScriptLanguage; - const scriptLang = lang === 'none' ? '' : ` lang="${lang}"`; - return `${ts.sys.newLine}`; -} diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts deleted file mode 100644 index 007415a1b..000000000 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ /dev/null @@ -1,356 +0,0 @@ -import ts from 'typescript'; -import { FileMap, FileSet } from '../../lib/documents/fileCollection'; -import { createGetCanonicalFileName, getLastPartOfPath, toFileNameLowerCase } from '../../utils'; -import { DocumentSnapshot } from './DocumentSnapshot'; -import { createSvelteSys } from './svelte-sys'; -import { - ensureRealSvelteFilePath, - getExtensionFromScriptKind, - isSvelteFilePath, - isVirtualSvelteFilePath, - toVirtualSvelteFilePath -} from './utils'; - -const CACHE_KEY_SEPARATOR = ':::'; -/** - * Caches resolved modules. - */ -class ModuleResolutionCache { - private cache = new FileMap(); - private pendingInvalidations = new FileSet(); - private getCanonicalFileName = createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames); - - /** - * Tries to get a cached module. - * Careful: `undefined` can mean either there's no match found, or that the result resolved to `undefined`. - */ - get(moduleName: string, containingFile: string): ts.ResolvedModule | undefined { - return this.cache.get(this.getKey(moduleName, containingFile)); - } - - /** - * Checks if has cached module. - */ - has(moduleName: string, containingFile: string): boolean { - return this.cache.has(this.getKey(moduleName, containingFile)); - } - - /** - * Caches resolved module (or undefined). - */ - set(moduleName: string, containingFile: string, resolvedModule: ts.ResolvedModule | undefined) { - this.cache.set(this.getKey(moduleName, containingFile), resolvedModule); - } - - /** - * Deletes module from cache. Call this if a file was deleted. - * @param resolvedModuleName full path of the module - */ - delete(resolvedModuleName: string): void { - resolvedModuleName = this.getCanonicalFileName(resolvedModuleName); - this.cache.forEach((val, key) => { - if (val && this.getCanonicalFileName(val.resolvedFileName) === resolvedModuleName) { - this.cache.delete(key); - this.pendingInvalidations.add(key.split(CACHE_KEY_SEPARATOR).shift() || ''); - } - }); - } - - /** - * Deletes everything from cache that resolved to `undefined` - * and which might match the path. - */ - deleteUnresolvedResolutionsFromCache(path: string): void { - const fileNameWithoutEnding = - getLastPartOfPath(this.getCanonicalFileName(path)).split('.').shift() || ''; - this.cache.forEach((val, key) => { - const [containingFile, moduleName = ''] = key.split(CACHE_KEY_SEPARATOR); - if (!val && moduleName.includes(fileNameWithoutEnding)) { - this.cache.delete(key); - this.pendingInvalidations.add(containingFile); - } - }); - } - - private getKey(moduleName: string, containingFile: string) { - return containingFile + CACHE_KEY_SEPARATOR + ensureRealSvelteFilePath(moduleName); - } - - clearPendingInvalidations() { - this.pendingInvalidations.clear(); - } - - oneOfResolvedModuleChanged(path: string) { - return this.pendingInvalidations.has(path); - } -} - -class ImpliedNodeFormatResolver { - private alreadyResolved = new FileMap>(); - - resolve( - importPath: string, - importIdxInFile: number, - sourceFile: ts.SourceFile | undefined, - compilerOptions: ts.CompilerOptions - ) { - if (isSvelteFilePath(importPath)) { - // Svelte imports should use the old resolution algorithm, else they are not found - return undefined; - } - - let mode = undefined; - if (sourceFile) { - this.cacheImpliedNodeFormat(sourceFile, compilerOptions); - mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile); - } - return mode; - } - - private cacheImpliedNodeFormat(sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions) { - if (!sourceFile.impliedNodeFormat && isSvelteFilePath(sourceFile.fileName)) { - // impliedNodeFormat is not set for Svelte files, because the TS function which - // calculates this works with a fixed set of file extensions, - // which .svelte is obv not part of. Make it work by faking a TS file. - if (!this.alreadyResolved.has(sourceFile.fileName)) { - sourceFile.impliedNodeFormat = ts.getImpliedNodeFormatForFile( - toVirtualSvelteFilePath(sourceFile.fileName) as any, - undefined, - ts.sys, - compilerOptions - ); - this.alreadyResolved.set(sourceFile.fileName, sourceFile.impliedNodeFormat); - } else { - sourceFile.impliedNodeFormat = this.alreadyResolved.get(sourceFile.fileName); - } - } - } - - resolveForTypeReference( - entry: string | ts.FileReference, - sourceFile: ts.SourceFile | undefined, - compilerOptions: ts.CompilerOptions - ) { - let mode = undefined; - if (sourceFile) { - this.cacheImpliedNodeFormat(sourceFile, compilerOptions); - mode = ts.getModeForFileReference(entry, sourceFile?.impliedNodeFormat); - } - return mode; - } -} - -// https://github.com/microsoft/TypeScript/blob/dddd0667f012c51582c2ac92c08b8e57f2456587/src/compiler/program.ts#L989 -function getTypeReferenceResolutionName(entry: T) { - return typeof entry !== 'string' ? toFileNameLowerCase(entry.fileName) : entry; -} - -/** - * Creates a module loader specifically for `.svelte` files. - * - * The typescript language service tries to look up other files that are referenced in the currently open svelte file. - * For `.ts`/`.js` files this works, for `.svelte` files it does not by default. - * Reason: The typescript language service does not know about the `.svelte` file ending, - * so it assumes it's a normal typescript file and searches for files like `../Component.svelte.ts`, which is wrong. - * In order to fix this, we need to wrap typescript's module resolution and reroute all `.svelte.ts` file lookups to .svelte. - * - * @param getSnapshot A function which returns a (in case of svelte file fully preprocessed) typescript/javascript snapshot - * @param compilerOptions The typescript compiler options - */ -export function createSvelteModuleLoader( - getSnapshot: (fileName: string) => DocumentSnapshot, - compilerOptions: ts.CompilerOptions, - tsSystem: ts.System, - tsModule: typeof ts -) { - const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); - const svelteSys = createSvelteSys(tsSystem); - // tsModuleCache caches package.json parsing and module resolution for directory - const tsModuleCache = tsModule.createModuleResolutionCache( - tsSystem.getCurrentDirectory(), - createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames) - ); - const tsTypeReferenceDirectiveCache = tsModule.createTypeReferenceDirectiveResolutionCache( - tsSystem.getCurrentDirectory(), - getCanonicalFileName, - undefined, - tsModuleCache.getPackageJsonInfoCache() - ); - const moduleCache = new ModuleResolutionCache(); - const typeReferenceCache = new Map< - string, - ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations - >(); - - const impliedNodeFormatResolver = new ImpliedNodeFormatResolver(); - const failedPathToContainingFile = new FileMap(); - const failedLocationInvalidated = new FileSet(); - - return { - svelteFileExists: svelteSys.svelteFileExists, - fileExists: svelteSys.fileExists, - readFile: svelteSys.readFile, - readDirectory: svelteSys.readDirectory, - deleteFromModuleCache: (path: string) => { - svelteSys.deleteFromCache(path); - moduleCache.delete(path); - }, - deleteUnresolvedResolutionsFromCache: (path: string) => { - svelteSys.deleteFromCache(path); - moduleCache.deleteUnresolvedResolutionsFromCache(path); - - const previousTriedButFailed = failedPathToContainingFile.get(path); - - for (const containingFile of previousTriedButFailed ?? []) { - failedLocationInvalidated.add(containingFile); - } - }, - resolveModuleNames, - resolveTypeReferenceDirectiveReferences, - mightHaveInvalidatedResolutions, - clearPendingInvalidations - }; - - function resolveModuleNames( - moduleNames: string[], - containingFile: string, - _reusedNames: string[] | undefined, - _redirectedReference: ts.ResolvedProjectReference | undefined, - _options: ts.CompilerOptions, - containingSourceFile?: ts.SourceFile | undefined - ): Array { - return moduleNames.map((moduleName, index) => { - if (moduleCache.has(moduleName, containingFile)) { - return moduleCache.get(moduleName, containingFile); - } - - const resolvedModule = resolveModuleName( - moduleName, - containingFile, - containingSourceFile, - index - ); - - resolvedModule?.failedLookupLocations?.forEach((failedLocation) => { - const failedPaths = failedPathToContainingFile.get(failedLocation) ?? new FileSet(); - failedPaths.add(containingFile); - failedPathToContainingFile.set(failedLocation, failedPaths); - }); - - moduleCache.set(moduleName, containingFile, resolvedModule?.resolvedModule); - return resolvedModule?.resolvedModule; - }); - } - - function resolveModuleName( - name: string, - containingFile: string, - containingSourceFile: ts.SourceFile | undefined, - index: number - ): ts.ResolvedModuleWithFailedLookupLocations { - const mode = impliedNodeFormatResolver.resolve( - name, - index, - containingSourceFile, - compilerOptions - ); - // Delegate to the TS resolver first. - // If that does not bring up anything, try the Svelte Module loader - // which is able to deal with .svelte files. - const tsResolvedModuleWithFailedLookup = tsModule.resolveModuleName( - name, - containingFile, - compilerOptions, - ts.sys, - tsModuleCache, - undefined, - mode - ); - - const tsResolvedModule = tsResolvedModuleWithFailedLookup.resolvedModule; - if (tsResolvedModule) { - return tsResolvedModuleWithFailedLookup; - } - - const svelteResolvedModuleWithFailedLookup = tsModule.resolveModuleName( - name, - containingFile, - compilerOptions, - svelteSys, - undefined, - undefined, - mode - ); - - const svelteResolvedModule = svelteResolvedModuleWithFailedLookup.resolvedModule; - if ( - !svelteResolvedModule || - !isVirtualSvelteFilePath(svelteResolvedModule.resolvedFileName) - ) { - return svelteResolvedModuleWithFailedLookup; - } - - const resolvedFileName = ensureRealSvelteFilePath(svelteResolvedModule.resolvedFileName); - const snapshot = getSnapshot(resolvedFileName); - - const resolvedSvelteModule: ts.ResolvedModuleFull = { - extension: getExtensionFromScriptKind(snapshot && snapshot.scriptKind), - resolvedFileName, - isExternalLibraryImport: svelteResolvedModule.isExternalLibraryImport - }; - return { - ...svelteResolvedModuleWithFailedLookup, - resolvedModule: resolvedSvelteModule - }; - } - - function resolveTypeReferenceDirectiveReferences( - typeDirectiveNames: readonly T[], - containingFile: string, - redirectedReference: ts.ResolvedProjectReference | undefined, - options: ts.CompilerOptions, - containingSourceFile: ts.SourceFile | undefined - ): readonly ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations[] { - return typeDirectiveNames.map((typeDirectiveName) => { - const entry = getTypeReferenceResolutionName(typeDirectiveName); - const mode = impliedNodeFormatResolver.resolveForTypeReference( - entry, - containingSourceFile, - options - ); - - const key = `${entry}|${mode}`; - let result = typeReferenceCache.get(key); - if (!result) { - result = ts.resolveTypeReferenceDirective( - entry, - containingFile, - options, - { - ...tsSystem - }, - redirectedReference, - tsTypeReferenceDirectiveCache, - mode - ); - - typeReferenceCache.set(key, result); - } - - return result; - }); - } - - function mightHaveInvalidatedResolutions(path: string) { - return ( - moduleCache.oneOfResolvedModuleChanged(path) || - // tried but failed file might now exist - failedLocationInvalidated.has(path) - ); - } - - function clearPendingInvalidations() { - moduleCache.clearPendingInvalidations(); - failedLocationInvalidated.clear(); - } -} diff --git a/packages/language-server/src/plugins/typescript/previewer.ts b/packages/language-server/src/plugins/typescript/previewer.ts deleted file mode 100644 index 168b91abf..000000000 --- a/packages/language-server/src/plugins/typescript/previewer.ts +++ /dev/null @@ -1,140 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * adopted from https://github.com/microsoft/vscode/blob/10722887b8629f90cc38ee7d90d54e8246dc895f/extensions/typescript-language-features/src/utils/previewer.ts - */ - -import ts from 'typescript'; -import { isNotNullOrUndefined } from '../../utils'; - -function replaceLinks(text: string): string { - return ( - text - // Http(s) links - .replace( - /\{@(link|linkplain|linkcode) (https?:\/\/[^ |}]+?)(?:[| ]([^{}\n]+?))?\}/gi, - (_, tag: string, link: string, text?: string) => { - switch (tag) { - case 'linkcode': - return `[\`${text ? text.trim() : link}\`](${link})`; - - default: - return `[${text ? text.trim() : link}](${link})`; - } - } - ) - ); -} - -function processInlineTags(text: string): string { - return replaceLinks(text); -} - -function getTagBodyText(tag: ts.JSDocTagInfo): string | undefined { - if (!tag.text) { - return undefined; - } - - // Convert to markdown code block if it is not already one - function makeCodeblock(text: string): string { - if (text.match(/^\s*[~`]{3}/g)) { - return text; - } - return '```\n' + text + '\n```'; - } - - function makeExampleTag(text: string) { - // check for caption tags, fix for https://github.com/microsoft/vscode/issues/79704 - const captionTagMatches = text.match(/(.*?)<\/caption>\s*(\r\n|\n)/); - if (captionTagMatches && captionTagMatches.index === 0) { - return ( - captionTagMatches[1] + - '\n\n' + - makeCodeblock(text.substr(captionTagMatches[0].length)) - ); - } else { - return makeCodeblock(text); - } - } - - function makeEmailTag(text: string) { - // fix obsucated email address, https://github.com/microsoft/vscode/issues/80898 - const emailMatch = text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/); - - if (emailMatch === null) { - return text; - } else { - return `${emailMatch[1]} ${emailMatch[2]}`; - } - } - - switch (tag.name) { - case 'example': - return makeExampleTag(ts.displayPartsToString(tag.text)); - case 'author': - return makeEmailTag(ts.displayPartsToString(tag.text)); - case 'default': - return makeCodeblock(ts.displayPartsToString(tag.text)); - } - - return processInlineTags(ts.displayPartsToString(tag.text)); -} - -export function getTagDocumentation(tag: ts.JSDocTagInfo): string | undefined { - function getWithType() { - const body = (ts.displayPartsToString(tag.text) || '').split(/^(\S+)\s*-?\s*/); - if (body?.length === 3) { - const param = body[1]; - const doc = body[2]; - const label = `*@${tag.name}* \`${param}\``; - if (!doc) { - return label; - } - return ( - label + - (doc.match(/\r\n|\n/g) - ? ' \n' + processInlineTags(doc) - : ` — ${processInlineTags(doc)}`) - ); - } - } - - switch (tag.name) { - case 'augments': - case 'extends': - case 'param': - case 'template': - return getWithType(); - } - - // Generic tag - const label = `*@${tag.name}*`; - const text = getTagBodyText(tag); - if (!text) { - return label; - } - return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`); -} - -export function plain(parts: ts.SymbolDisplayPart[] | string): string { - return processInlineTags(typeof parts === 'string' ? parts : ts.displayPartsToString(parts)); -} - -export function getMarkdownDocumentation( - documentation: ts.SymbolDisplayPart[] | undefined, - tags: ts.JSDocTagInfo[] | undefined -) { - let result: Array = []; - if (documentation) { - result.push(plain(documentation)); - } - - if (tags) { - result = result.concat(tags.map(getTagDocumentation)); - } - - return result.filter(isNotNullOrUndefined).join('\n\n'); -} diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts deleted file mode 100644 index 09cac7fba..000000000 --- a/packages/language-server/src/plugins/typescript/service.ts +++ /dev/null @@ -1,907 +0,0 @@ -import { basename, dirname, join, resolve } from 'path'; -import ts from 'typescript'; -import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; -import { getPackageInfo, importSvelte } from '../../importPackage'; -import { Document } from '../../lib/documents'; -import { configLoader } from '../../lib/documents/configLoader'; -import { FileMap, FileSet } from '../../lib/documents/fileCollection'; -import { Logger } from '../../logger'; -import { createGetCanonicalFileName, normalizePath, urlToPath } from '../../utils'; -import { DocumentSnapshot, SvelteSnapshotOptions } from './DocumentSnapshot'; -import { createSvelteModuleLoader } from './module-loader'; -import { - GlobalSnapshotsManager, - ignoredBuildDirectories, - SnapshotManager -} from './SnapshotManager'; -import { - ensureRealSvelteFilePath, - findTsConfigPath, - getNearestWorkspaceUri, - hasTsExtensions, - isSvelteFilePath -} from './utils'; - -export interface LanguageServiceContainer { - readonly tsconfigPath: string; - readonly compilerOptions: ts.CompilerOptions; - readonly configErrors: ts.Diagnostic[]; - /** - * @internal Public for tests only - */ - readonly snapshotManager: SnapshotManager; - getService(skipSynchronize?: boolean): ts.LanguageService; - updateSnapshot(documentOrFilePath: Document | string): DocumentSnapshot; - deleteSnapshot(filePath: string): void; - invalidateModuleCache(filePath: string): void; - updateProjectFiles(): void; - updateTsOrJsFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void; - /** - * Checks if a file is present in the project. - * Unlike `fileBelongsToProject`, this doesn't run a file search on disk. - */ - hasFile(filePath: string): boolean; - /** - * Careful, don't call often, or it will hurt performance. - * Only works for TS versions that have ScriptKind.Deferred - */ - fileBelongsToProject(filePath: string, isNew: boolean): boolean; - - dispose(): void; -} - -declare module 'typescript' { - interface LanguageServiceHost { - /** - * @internal - * This is needed for the languageService program to know that there is a new file - * that might change the module resolution results - */ - hasInvalidatedResolutions?: (sourceFile: string) => boolean; - } - - interface ResolvedModuleWithFailedLookupLocations { - /** @internal */ - failedLookupLocations?: string[]; - /** @internal */ - affectingLocations?: string[]; - /** @internal */ - resolutionDiagnostics?: ts.Diagnostic[]; - /** - * @internal - * Used to issue a diagnostic if typings for a non-relative import couldn't be found - * while respecting package.json `exports`, but were found when disabling `exports`. - */ - node10Result?: string; - } -} - -const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; // 20 MB -const services = new FileMap>(); -const serviceSizeMap = new FileMap(); -const configWatchers = new FileMap(); -const extendedConfigWatchers = new FileMap(); -const extendedConfigToTsConfigPath = new FileMap(); -const configFileModifiedTime = new FileMap(); -const configFileForOpenFiles = new FileMap(); -const pendingReloads = new FileSet(); -const documentRegistries = new Map(); - -/** - * For testing only: Reset the cache for services. - * Try to refactor this some day so that this file provides - * a setup function which creates all this nicely instead. - */ -export function __resetCache() { - services.clear(); - serviceSizeMap.clear(); - configFileForOpenFiles.clear(); -} - -export interface LanguageServiceDocumentContext { - ambientTypesSource: string; - transformOnTemplateError: boolean; - createDocument: (fileName: string, content: string) => Document; - globalSnapshotsManager: GlobalSnapshotsManager; - notifyExceedSizeLimit: (() => void) | undefined; - extendedConfigCache: Map; - onProjectReloaded: (() => void) | undefined; - watchTsConfig: boolean; - tsSystem: ts.System; -} - -export async function getService( - path: string, - workspaceUris: string[], - docContext: LanguageServiceDocumentContext -): Promise { - const getCanonicalFileName = createGetCanonicalFileName( - docContext.tsSystem.useCaseSensitiveFileNames - ); - - const tsconfigPath = - configFileForOpenFiles.get(path) ?? - findTsConfigPath(path, workspaceUris, docContext.tsSystem.fileExists, getCanonicalFileName); - - if (tsconfigPath) { - configFileForOpenFiles.set(path, tsconfigPath); - return getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), docContext); - } - - // Find closer boundary: workspace uri or node_modules - const nearestWorkspaceUri = getNearestWorkspaceUri(workspaceUris, path, getCanonicalFileName); - const lastNodeModulesIdx = path.split('/').lastIndexOf('node_modules') + 2; - const nearestNodeModulesBoundary = - lastNodeModulesIdx === 1 - ? undefined - : path.split('/').slice(0, lastNodeModulesIdx).join('/'); - const nearestBoundary = - (nearestNodeModulesBoundary?.length ?? 0) > (nearestWorkspaceUri?.length ?? 0) - ? nearestNodeModulesBoundary - : nearestWorkspaceUri; - - return getServiceForTsconfig( - tsconfigPath, - (nearestBoundary && urlToPath(nearestBoundary)) ?? - docContext.tsSystem.getCurrentDirectory(), - docContext - ); -} - -export async function forAllServices( - cb: (service: LanguageServiceContainer) => any -): Promise { - for (const service of services.values()) { - cb(await service); - } -} - -/** - * @param tsconfigPath has to be absolute - * @param docContext - */ -export async function getServiceForTsconfig( - tsconfigPath: string, - workspacePath: string, - docContext: LanguageServiceDocumentContext -): Promise { - const tsconfigPathOrWorkspacePath = tsconfigPath || workspacePath; - const reloading = pendingReloads.has(tsconfigPath); - - let service: LanguageServiceContainer; - - if (reloading || !services.has(tsconfigPathOrWorkspacePath)) { - if (reloading) { - Logger.log('Reloading ts service at ', tsconfigPath, ' due to config updated'); - } else { - Logger.log('Initialize new ts service at ', tsconfigPath); - } - - pendingReloads.delete(tsconfigPath); - const newService = createLanguageService(tsconfigPath, workspacePath, docContext); - services.set(tsconfigPathOrWorkspacePath, newService); - service = await newService; - } else { - service = await services.get(tsconfigPathOrWorkspacePath)!; - } - - return service; -} - -async function createLanguageService( - tsconfigPath: string, - workspacePath: string, - docContext: LanguageServiceDocumentContext -): Promise { - const { tsSystem } = docContext; - - const { - options: compilerOptions, - errors: configErrors, - fileNames: files, - raw, - extendedConfigPaths - } = getParsedConfig(); - // raw is the tsconfig merged with extending config - // see: https://github.com/microsoft/TypeScript/blob/08e4f369fbb2a5f0c30dee973618d65e6f7f09f8/src/compiler/commandLineParser.ts#L2537 - const snapshotManager = new SnapshotManager( - docContext.globalSnapshotsManager, - raw, - workspacePath, - files - ); - - // Load all configs within the tsconfig scope and the one above so that they are all loaded - // by the time they need to be accessed synchronously by DocumentSnapshots. - await configLoader.loadConfigs(workspacePath); - const tsSystemWithPackageJsonCache = { - ...tsSystem, - /** - * While TypeScript doesn't cache package.json in the tsserver, they do cache the - * information they get from it within other internal APIs. We'll somewhat do the same - * by caching the text of the package.json file here. - */ - readFile: (path: string, encoding?: string | undefined) => { - if (basename(path) === 'package.json') { - return docContext.globalSnapshotsManager.getPackageJson(path)?.text; - } - - return tsSystem.readFile(path, encoding); - } - }; - - const svelteModuleLoader = createSvelteModuleLoader( - getSnapshot, - compilerOptions, - tsSystemWithPackageJsonCache, - ts - ); - - let svelteTsPath: string; - try { - // For when svelte2tsx/svelte-check is part of node_modules, for example VS Code extension - svelteTsPath = dirname(require.resolve(docContext.ambientTypesSource)); - } catch (e) { - // Fall back to dirname - svelteTsPath = __dirname; - } - const sveltePackageInfo = getPackageInfo('svelte', tsconfigPath || workspacePath); - // Svelte 5 has new features, but we don't want to add the new compiler into language-tools. In the future it's probably - // best to shift more and more of this into user's node_modules for better handling of multiple Svelte versions. - const svelteCompiler = - sveltePackageInfo.version.major >= 5 - ? importSvelte(tsconfigPath || workspacePath) - : undefined; - - const isSvelte3 = sveltePackageInfo.version.major === 3; - const svelteHtmlDeclaration = isSvelte3 - ? undefined - : join(sveltePackageInfo.path, 'svelte-html.d.ts'); - const svelteHtmlFallbackIfNotExist = - svelteHtmlDeclaration && tsSystem.fileExists(svelteHtmlDeclaration) - ? svelteHtmlDeclaration - : './svelte-jsx-v4.d.ts'; - - const svelteTsxFiles = ( - isSvelte3 - ? ['./svelte-shims.d.ts', './svelte-jsx.d.ts', './svelte-native-jsx.d.ts'] - : ['./svelte-shims-v4.d.ts', svelteHtmlFallbackIfNotExist, './svelte-native-jsx.d.ts'] - ).map((f) => tsSystem.resolvePath(resolve(svelteTsPath, f))); - - let languageServiceReducedMode = false; - let projectVersion = 0; - let dirty = false; - - const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); - - const host: ts.LanguageServiceHost = { - log: (message) => Logger.debug(`[ts] ${message}`), - getCompilationSettings: () => compilerOptions, - getScriptFileNames, - getScriptVersion: (fileName: string) => - getSnapshotIfExists(fileName)?.version.toString() || '', - getScriptSnapshot: getSnapshotIfExists, - getCurrentDirectory: () => workspacePath, - getDefaultLibFileName: ts.getDefaultLibFilePath, - fileExists: svelteModuleLoader.fileExists, - readFile: svelteModuleLoader.readFile, - resolveModuleNames: svelteModuleLoader.resolveModuleNames, - readDirectory: svelteModuleLoader.readDirectory, - getDirectories: tsSystem.getDirectories, - useCaseSensitiveFileNames: () => tsSystem.useCaseSensitiveFileNames, - getScriptKind: (fileName: string) => getSnapshot(fileName).scriptKind, - getProjectVersion: () => projectVersion.toString(), - getNewLine: () => tsSystem.newLine, - resolveTypeReferenceDirectiveReferences: - svelteModuleLoader.resolveTypeReferenceDirectiveReferences, - hasInvalidatedResolutions: svelteModuleLoader.mightHaveInvalidatedResolutions - }; - - const documentRegistry = getOrCreateDocumentRegistry( - host.getCurrentDirectory(), - tsSystem.useCaseSensitiveFileNames - ); - - const languageService = ts.createLanguageService(host, documentRegistry); - const transformationConfig: SvelteSnapshotOptions = { - parse: svelteCompiler?.parse, - version: svelteCompiler?.VERSION, - transformOnTemplateError: docContext.transformOnTemplateError, - typingsNamespace: raw?.svelteOptions?.namespace || 'svelteHTML' - }; - - docContext.globalSnapshotsManager.onChange(scheduleUpdate); - - reduceLanguageServiceCapabilityIfFileSizeTooBig(); - updateExtendedConfigDependents(); - watchConfigFile(); - - return { - tsconfigPath, - compilerOptions, - configErrors, - getService, - updateSnapshot, - deleteSnapshot, - updateProjectFiles, - updateTsOrJsFile, - hasFile, - fileBelongsToProject, - snapshotManager, - invalidateModuleCache, - dispose - }; - - function getService(skipSynchronize?: boolean) { - if (!skipSynchronize) { - updateIfDirty(); - } - - return languageService; - } - - function deleteSnapshot(filePath: string): void { - svelteModuleLoader.deleteFromModuleCache(filePath); - snapshotManager.delete(filePath); - configFileForOpenFiles.delete(filePath); - } - - function invalidateModuleCache(filePath: string) { - svelteModuleLoader.deleteFromModuleCache(filePath); - svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); - - scheduleUpdate(); - } - - function updateSnapshot(documentOrFilePath: Document | string): DocumentSnapshot { - return typeof documentOrFilePath === 'string' - ? updateSnapshotFromFilePath(documentOrFilePath) - : updateSnapshotFromDocument(documentOrFilePath); - } - - function updateSnapshotFromDocument(document: Document): DocumentSnapshot { - const filePath = document.getFilePath() || ''; - const prevSnapshot = snapshotManager.get(filePath); - if (prevSnapshot?.version === document.version) { - return prevSnapshot; - } - - if (!prevSnapshot) { - svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); - } - - const newSnapshot = DocumentSnapshot.fromDocument(document, transformationConfig); - - snapshotManager.set(filePath, newSnapshot); - - return newSnapshot; - } - - function updateSnapshotFromFilePath(filePath: string): DocumentSnapshot { - const prevSnapshot = snapshotManager.get(filePath); - if (prevSnapshot) { - return prevSnapshot; - } - - svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); - const newSnapshot = DocumentSnapshot.fromFilePath( - filePath, - docContext.createDocument, - transformationConfig, - tsSystem - ); - snapshotManager.set(filePath, newSnapshot); - return newSnapshot; - } - - /** - * Deleted files will still be requested during the program update. - * Don't create snapshots for them. - * Otherwise, deleteUnresolvedResolutionsFromCache won't be called when the file is created again - */ - function getSnapshotIfExists(fileName: string): DocumentSnapshot | undefined { - const svelteFileName = ensureRealSvelteFilePath(fileName); - - let doc = snapshotManager.get(fileName) ?? snapshotManager.get(svelteFileName); - if (doc) { - return doc; - } - - if (!svelteModuleLoader.fileExists(fileName)) { - return undefined; - } - - return createSnapshot( - svelteModuleLoader.svelteFileExists(fileName) ? svelteFileName : fileName, - doc - ); - } - - function getSnapshot(fileName: string): DocumentSnapshot { - const svelteFileName = ensureRealSvelteFilePath(fileName); - - let doc = snapshotManager.get(fileName) ?? snapshotManager.get(svelteFileName); - if (doc) { - return doc; - } - - return createSnapshot(fileName, doc); - } - - function createSnapshot(fileName: string, doc: DocumentSnapshot | undefined) { - svelteModuleLoader.deleteUnresolvedResolutionsFromCache(fileName); - doc = DocumentSnapshot.fromFilePath( - fileName, - docContext.createDocument, - transformationConfig, - tsSystem - ); - snapshotManager.set(fileName, doc); - return doc; - } - - function updateProjectFiles(): void { - projectVersion++; - dirty = true; - const projectFileCountBefore = snapshotManager.getProjectFileNames().length; - snapshotManager.updateProjectFiles(); - const projectFileCountAfter = snapshotManager.getProjectFileNames().length; - - if (projectFileCountAfter <= projectFileCountBefore) { - return; - } - - reduceLanguageServiceCapabilityIfFileSizeTooBig(); - } - - function getScriptFileNames() { - const projectFiles = languageServiceReducedMode - ? [] - : snapshotManager.getProjectFileNames(); - const canonicalProjectFileNames = new Set(projectFiles.map(getCanonicalFileName)); - - return Array.from( - new Set([ - ...projectFiles, - // project file is read from the file system so it's more likely to have - // the correct casing - ...snapshotManager - .getClientFileNames() - .filter((file) => !canonicalProjectFileNames.has(getCanonicalFileName(file))), - ...svelteTsxFiles - ]) - ); - } - - function hasFile(filePath: string): boolean { - return snapshotManager.has(filePath); - } - - function fileBelongsToProject(filePath: string, isNew: boolean): boolean { - filePath = normalizePath(filePath); - return hasFile(filePath) || (isNew && getParsedConfig().fileNames.includes(filePath)); - } - - function updateTsOrJsFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void { - if (!snapshotManager.has(fileName)) { - svelteModuleLoader.deleteUnresolvedResolutionsFromCache(fileName); - } - snapshotManager.updateTsOrJsFile(fileName, changes); - } - - function getParsedConfig() { - const forcedCompilerOptions: ts.CompilerOptions = { - allowNonTsExtensions: true, - target: ts.ScriptTarget.Latest, - allowJs: true, - noEmit: true, - declaration: false, - skipLibCheck: true - }; - - // always let ts parse config to get default compilerOption - let configJson = - (tsconfigPath && ts.readConfigFile(tsconfigPath, tsSystem.readFile).config) || - getDefaultJsConfig(); - - // Only default exclude when no extends for now - if (!configJson.extends) { - configJson = Object.assign( - { - exclude: getDefaultExclude() - }, - configJson - ); - } - - const extendedConfigPaths = new Set(); - const { extendedConfigCache } = docContext; - const cacheMonitorProxy = { - ...docContext.extendedConfigCache, - get(key: string) { - extendedConfigPaths.add(key); - return extendedConfigCache.get(key); - }, - has(key: string) { - extendedConfigPaths.add(key); - return extendedConfigCache.has(key); - }, - set(key: string, value: ts.ExtendedConfigCacheEntry) { - extendedConfigPaths.add(key); - return extendedConfigCache.set(key, value); - } - }; - - const parsedConfig = ts.parseJsonConfigFileContent( - configJson, - tsSystem, - workspacePath, - forcedCompilerOptions, - tsconfigPath, - undefined, - [ - { - extension: 'svelte', - isMixedContent: true, - // Deferred was added in a later TS version, fall back to tsx - // If Deferred exists, this means that all Svelte files are included - // in parsedConfig.fileNames - scriptKind: ts.ScriptKind.Deferred ?? ts.ScriptKind.TS - } - ], - cacheMonitorProxy - ); - - const compilerOptions: ts.CompilerOptions = { - ...parsedConfig.options, - ...forcedCompilerOptions - }; - if ( - !compilerOptions.moduleResolution || - compilerOptions.moduleResolution === ts.ModuleResolutionKind.Classic - ) { - compilerOptions.moduleResolution = - // NodeJS: up to 4.9, Node10: since 5.0 - (ts.ModuleResolutionKind as any).NodeJs ?? ts.ModuleResolutionKind.Node10; - } - if ( - !compilerOptions.module || - [ - ts.ModuleKind.AMD, - ts.ModuleKind.CommonJS, - ts.ModuleKind.ES2015, - ts.ModuleKind.None, - ts.ModuleKind.System, - ts.ModuleKind.UMD - ].includes(compilerOptions.module) - ) { - compilerOptions.module = ts.ModuleKind.ESNext; - } - - // detect which JSX namespace to use (svelte | svelteNative) if not specified or not compatible - if (!compilerOptions.jsxFactory || !compilerOptions.jsxFactory.startsWith('svelte')) { - //override if we detect svelte-native - if (workspacePath) { - try { - const svelteNativePkgInfo = getPackageInfo('svelte-native', workspacePath); - if (svelteNativePkgInfo.path) { - // For backwards compatibility - parsedConfig.raw.svelteOptions = parsedConfig.raw.svelteOptions || {}; - parsedConfig.raw.svelteOptions.namespace = 'svelteNative.JSX'; - } - } catch (e) { - //we stay regular svelte - } - } - } - - return { - ...parsedConfig, - fileNames: parsedConfig.fileNames.map(normalizePath), - options: compilerOptions, - extendedConfigPaths - }; - } - - /** - * This should only be used when there's no jsconfig/tsconfig at all - */ - function getDefaultJsConfig(): { - compilerOptions: ts.CompilerOptions; - include: string[]; - } { - return { - compilerOptions: { - maxNodeModuleJsDepth: 2, - allowSyntheticDefaultImports: true - }, - // Necessary to not flood the initial files - // with potentially completely unrelated .ts/.js files: - include: [] - }; - } - - function getDefaultExclude() { - return ['node_modules', ...ignoredBuildDirectories]; - } - - /** - * Disable usage of project files. - * running language service in a reduced mode for - * large projects with improperly excluded tsconfig. - */ - function reduceLanguageServiceCapabilityIfFileSizeTooBig() { - if ( - exceedsTotalSizeLimitForNonTsFiles( - compilerOptions, - tsconfigPath, - snapshotManager, - tsSystem - ) - ) { - languageService.cleanupSemanticCache(); - languageServiceReducedMode = true; - docContext.notifyExceedSizeLimit?.(); - } - } - - function dispose() { - languageService.dispose(); - snapshotManager.dispose(); - configWatchers.get(tsconfigPath)?.close(); - configWatchers.delete(tsconfigPath); - configFileForOpenFiles.clear(); - docContext.globalSnapshotsManager.removeChangeListener(scheduleUpdate); - } - - function updateExtendedConfigDependents() { - extendedConfigPaths.forEach((extendedConfig) => { - let dependedTsConfig = extendedConfigToTsConfigPath.get(extendedConfig); - if (!dependedTsConfig) { - dependedTsConfig = new FileSet(tsSystem.useCaseSensitiveFileNames); - extendedConfigToTsConfigPath.set(extendedConfig, dependedTsConfig); - } - - dependedTsConfig.add(tsconfigPath); - }); - } - - function watchConfigFile() { - if (!tsSystem.watchFile || !docContext.watchTsConfig) { - return; - } - - if (!configWatchers.has(tsconfigPath) && tsconfigPath) { - configFileModifiedTime.set(tsconfigPath, tsSystem.getModifiedTime?.(tsconfigPath)); - configWatchers.set( - tsconfigPath, - // for some reason setting the polling interval is necessary, else some error in TS is thrown - tsSystem.watchFile(tsconfigPath, watchConfigCallback, 1000) - ); - } - - for (const config of extendedConfigPaths) { - if (extendedConfigWatchers.has(config)) { - continue; - } - - configFileModifiedTime.set(config, tsSystem.getModifiedTime?.(config)); - extendedConfigWatchers.set( - config, - // for some reason setting the polling interval is necessary, else some error in TS is thrown - tsSystem.watchFile(config, createWatchExtendedConfigCallback(docContext), 1000) - ); - } - } - - async function watchConfigCallback( - fileName: string, - kind: ts.FileWatcherEventKind, - modifiedTime: Date | undefined - ) { - if ( - kind === ts.FileWatcherEventKind.Changed && - !configFileModified(fileName, modifiedTime ?? tsSystem.getModifiedTime?.(fileName)) - ) { - return; - } - - dispose(); - - if (kind === ts.FileWatcherEventKind.Changed) { - scheduleReload(fileName); - } else if (kind === ts.FileWatcherEventKind.Deleted) { - services.delete(fileName); - configFileForOpenFiles.clear(); - } - - docContext.onProjectReloaded?.(); - } - - function updateIfDirty() { - if (!dirty) { - return; - } - - languageService.getProgram(); - svelteModuleLoader.clearPendingInvalidations(); - - dirty = false; - } - - function scheduleUpdate() { - if (dirty) { - return; - } - - projectVersion++; - dirty = true; - } -} - -/** - * adopted from https://github.com/microsoft/TypeScript/blob/3c8e45b304b8572094c5d7fbb9cd768dbf6417c0/src/server/editorServices.ts#L1955 - */ -function exceedsTotalSizeLimitForNonTsFiles( - compilerOptions: ts.CompilerOptions, - tsconfigPath: string, - snapshotManager: SnapshotManager, - tsSystem: ts.System -): boolean { - if (compilerOptions.disableSizeLimit) { - return false; - } - - let availableSpace = maxProgramSizeForNonTsFiles; - serviceSizeMap.set(tsconfigPath, 0); - - serviceSizeMap.forEach((size) => { - availableSpace -= size; - }); - - let totalNonTsFileSize = 0; - - const fileNames = snapshotManager.getProjectFileNames(); - for (const fileName of fileNames) { - if (hasTsExtensions(fileName)) { - continue; - } - - totalNonTsFileSize += tsSystem.getFileSize?.(fileName) ?? 0; - - if (totalNonTsFileSize > availableSpace) { - const top5LargestFiles = fileNames - .filter((name) => !hasTsExtensions(name)) - .map((name) => ({ name, size: tsSystem.getFileSize?.(name) ?? 0 })) - .sort((a, b) => b.size - a.size) - .slice(0, 5); - - Logger.log( - `Non TS file size exceeded limit (${totalNonTsFileSize}). ` + - `Largest files: ${top5LargestFiles - .map((file) => `${file.name}:${file.size}`) - .join(', ')}` - ); - - return true; - } - } - - serviceSizeMap.set(tsconfigPath, totalNonTsFileSize); - return false; -} - -/** - * shared watcher callback can't be within `createLanguageService` - * because it would reference the closure - * So that GC won't drop it and cause memory leaks - */ -function createWatchExtendedConfigCallback(docContext: LanguageServiceDocumentContext) { - return async ( - fileName: string, - kind: ts.FileWatcherEventKind, - modifiedTime: Date | undefined - ) => { - if ( - kind === ts.FileWatcherEventKind.Changed && - !configFileModified( - fileName, - modifiedTime ?? docContext.tsSystem.getModifiedTime?.(fileName) - ) - ) { - return; - } - - docContext.extendedConfigCache.delete(fileName); - - const promises = Array.from(extendedConfigToTsConfigPath.get(fileName) ?? []).map( - async (config) => { - const oldService = services.get(config); - scheduleReload(config); - (await oldService)?.dispose(); - } - ); - - await Promise.all(promises); - docContext.onProjectReloaded?.(); - }; -} - -/** - * check if file content is modified instead of attributes changed - */ -function configFileModified(fileName: string, modifiedTime: Date | undefined) { - const previousModifiedTime = configFileModifiedTime.get(fileName); - if (!modifiedTime || !previousModifiedTime) { - return true; - } - - if (previousModifiedTime >= modifiedTime) { - return false; - } - - configFileModifiedTime.set(fileName, modifiedTime); - return true; -} - -/** - * schedule to the service reload to the next time the - * service in requested - * if there's still files opened it should be restarted - * in the onProjectReloaded hooks - */ -function scheduleReload(fileName: string) { - // don't delete service from map yet as it could result in a race condition - // where a file update is received before the service is reloaded, swallowing the update - pendingReloads.add(fileName); -} - -function getOrCreateDocumentRegistry( - currentDirectory: string, - useCaseSensitiveFileNames: boolean -): ts.DocumentRegistry { - // unless it's a multi root workspace, there's only one registry - const key = [currentDirectory, useCaseSensitiveFileNames].join('|'); - - let registry = documentRegistries.get(key); - if (registry) { - return registry; - } - - registry = ts.createDocumentRegistry(useCaseSensitiveFileNames, currentDirectory); - - // impliedNodeFormat is always undefined when the svelte source file is created - // We might patched it later but the registry doesn't know about it - const releaseDocumentWithKey = registry.releaseDocumentWithKey; - registry.releaseDocumentWithKey = ( - path: ts.Path, - key: ts.DocumentRegistryBucketKey, - scriptKind: ts.ScriptKind, - impliedNodeFormat?: ts.ResolutionMode - ) => { - if (isSvelteFilePath(path)) { - releaseDocumentWithKey(path, key, scriptKind, undefined); - return; - } - - releaseDocumentWithKey(path, key, scriptKind, impliedNodeFormat); - }; - - registry.releaseDocument = ( - fileName: string, - compilationSettings: ts.CompilerOptions, - scriptKind: ts.ScriptKind, - impliedNodeFormat?: ts.ResolutionMode - ) => { - if (isSvelteFilePath(fileName)) { - registry?.releaseDocument(fileName, compilationSettings, scriptKind, undefined); - return; - } - - registry?.releaseDocument(fileName, compilationSettings, scriptKind, impliedNodeFormat); - }; - - documentRegistries.set(key, registry); - - return registry; -} diff --git a/packages/language-server/src/plugins/typescript/svelte-ast-utils.ts b/packages/language-server/src/plugins/typescript/svelte-ast-utils.ts deleted file mode 100644 index df51edb26..000000000 --- a/packages/language-server/src/plugins/typescript/svelte-ast-utils.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Node } from 'estree'; -import { walk } from 'estree-walker'; -// @ts-ignore -import { TemplateNode } from 'svelte/types/compiler/interfaces'; - -export interface SvelteNode { - start: number; - end: number; - type: string; - parent?: SvelteNode; -} - -type HTMLLike = 'Element' | 'InlineComponent' | 'Body' | 'Window'; - -export interface AwaitBlock extends SvelteNode { - type: 'AwaitBlock'; - expression: SvelteNode & Node; - value: (SvelteNode & Node) | null; - error: (SvelteNode & Node) | null; - pending: AwaitSubBlock; - then: AwaitSubBlock; - catch: AwaitSubBlock; -} - -export interface AwaitSubBlock extends SvelteNode { - skip: boolean; - children: SvelteNode[]; -} - -export interface EachBlock extends SvelteNode { - type: 'EachBlock'; - expression: SvelteNode & Node; - context: SvelteNode & Node; - key?: SvelteNode & Node; - else?: SvelteNode; - children: SvelteNode[]; -} - -function matchesOnly(type: string | undefined, only?: 'Element' | 'InlineComponent'): boolean { - return ( - !only || - // We hide the detail that body/window are also like elements in the context of this usage - (only === 'Element' && ['Element', 'Body', 'Window'].includes(type as HTMLLike)) || - (only === 'InlineComponent' && type === 'InlineComponent') - ); -} - -/** - * Returns true if given node is a component or html element, or if the offset is at the end of the node - * and its parent is a component or html element. - */ -export function isInTag(node: SvelteNode | null | undefined, offset: number): boolean { - return ( - node?.type === 'InlineComponent' || - node?.type === 'Element' || - (node?.end === offset && - (node?.parent?.type === 'InlineComponent' || node?.parent?.type === 'Element')) - ); -} - -/** - * Returns when given node represents an HTML Attribute. - * Example: The `class` in `
[1]; -type ESTreeEnterFunc = NonNullable; -type ESTreeLeaveFunc = NonNullable; - -export interface SvelteNodeWalker { - enter?: ( - this: { - skip: () => void; - remove: () => void; - replace: (node: SvelteNode) => void; - }, - node: SvelteNode, - parent: SvelteNode, - key: Parameters[2], - index: Parameters[3] - ) => void; - leave?: ( - this: { - skip: () => void; - remove: () => void; - replace: (node: SvelteNode) => void; - }, - node: SvelteNode, - parent: SvelteNode, - key: Parameters[2], - index: Parameters[3] - ) => void; -} - -// wrap the estree-walker to make it svelte specific -// the type casting is necessary because estree-walker is not designed for this -// especially in v3 which svelte 4 uses -export function walkSvelteAst(htmlAst: TemplateNode, walker: SvelteNodeWalker) { - walk(htmlAst as any, { - enter(node, parent, key, index) { - walker.enter?.call(this as any, node as SvelteNode, parent as SvelteNode, key, index); - }, - leave(node, parent, key, index) { - walker.leave?.call(this as any, node as SvelteNode, parent as SvelteNode, key, index); - } - }); -} - -export function isAwaitBlock(node: SvelteNode): node is AwaitBlock { - return node.type === 'AwaitBlock'; -} - -export function isEachBlock(node: SvelteNode): node is EachBlock { - return node.type === 'EachBlock'; -} diff --git a/packages/language-server/src/plugins/typescript/svelte-sys.ts b/packages/language-server/src/plugins/typescript/svelte-sys.ts deleted file mode 100644 index 4fb3c205c..000000000 --- a/packages/language-server/src/plugins/typescript/svelte-sys.ts +++ /dev/null @@ -1,70 +0,0 @@ -import ts from 'typescript'; -import { ensureRealSvelteFilePath, isVirtualSvelteFilePath, toRealSvelteFilePath } from './utils'; -import { FileMap } from '../../lib/documents/fileCollection'; - -/** - * This should only be accessed by TS svelte module resolution. - */ -export function createSvelteSys(tsSystem: ts.System) { - const fileExistsCache = new FileMap(); - - function svelteFileExists(path: string) { - if (isVirtualSvelteFilePath(path)) { - const sveltePath = toRealSvelteFilePath(path); - const sveltePathExists = - fileExistsCache.get(sveltePath) ?? tsSystem.fileExists(sveltePath); - fileExistsCache.set(sveltePath, sveltePathExists); - return sveltePathExists; - } else { - return false; - } - } - - const svelteSys: ts.System & { - deleteFromCache: (path: string) => void; - svelteFileExists: (path: string) => boolean; - } = { - ...tsSystem, - svelteFileExists, - fileExists(path: string) { - // We need to check both .svelte and .svelte.ts/js because that's how Svelte 5 will likely mark files with runes in them - const sveltePathExists = svelteFileExists(path); - const exists = - sveltePathExists || (fileExistsCache.get(path) ?? tsSystem.fileExists(path)); - fileExistsCache.set(path, exists); - return exists; - }, - readFile(path: string) { - // No getSnapshot here, because TS will very rarely call this and only for files that are not in the project - return tsSystem.readFile(svelteFileExists(path) ? toRealSvelteFilePath(path) : path); - }, - readDirectory(path, extensions, exclude, include, depth) { - const extensionsWithSvelte = extensions ? [...extensions, '.svelte'] : undefined; - - return tsSystem.readDirectory(path, extensionsWithSvelte, exclude, include, depth); - }, - deleteFile(path) { - // assumption: never a foo.svelte.ts file next to a foo.svelte file - fileExistsCache.delete(ensureRealSvelteFilePath(path)); - fileExistsCache.delete(path); - return tsSystem.deleteFile?.(path); - }, - deleteFromCache(path) { - // assumption: never a foo.svelte.ts file next to a foo.svelte file - fileExistsCache.delete(ensureRealSvelteFilePath(path)); - fileExistsCache.delete(path); - } - }; - - if (tsSystem.realpath) { - const realpath = tsSystem.realpath; - svelteSys.realpath = function (path) { - if (svelteFileExists(path)) { - return realpath(toRealSvelteFilePath(path)) + '.ts'; - } - return realpath(path); - }; - } - - return svelteSys; -} diff --git a/packages/language-server/src/plugins/typescript/utils.ts b/packages/language-server/src/plugins/typescript/utils.ts deleted file mode 100644 index b9c00d1f2..000000000 --- a/packages/language-server/src/plugins/typescript/utils.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { dirname } from 'path'; -import ts from 'typescript'; -import { - CompletionItemKind, - DiagnosticSeverity, - DiagnosticTag, - Position, - Range, - SymbolKind, - Location -} from 'vscode-languageserver'; -import { Document, isInTag, mapLocationToOriginal, mapRangeToOriginal } from '../../lib/documents'; -import { GetCanonicalFileName, pathToUrl } from '../../utils'; -import { DocumentSnapshot, SvelteDocumentSnapshot } from './DocumentSnapshot'; - -export function getScriptKindFromFileName(fileName: string): ts.ScriptKind { - const ext = fileName.substr(fileName.lastIndexOf('.')); - switch (ext.toLowerCase()) { - case ts.Extension.Js: - return ts.ScriptKind.JS; - case ts.Extension.Jsx: - return ts.ScriptKind.JSX; - case ts.Extension.Ts: - return ts.ScriptKind.TS; - case ts.Extension.Tsx: - return ts.ScriptKind.TSX; - case ts.Extension.Json: - return ts.ScriptKind.JSON; - default: - return ts.ScriptKind.Unknown; - } -} - -export function getExtensionFromScriptKind(kind: ts.ScriptKind | undefined): ts.Extension { - switch (kind) { - case ts.ScriptKind.JSX: - return ts.Extension.Jsx; - case ts.ScriptKind.TS: - return ts.Extension.Ts; - case ts.ScriptKind.TSX: - return ts.Extension.Tsx; - case ts.ScriptKind.JSON: - return ts.Extension.Json; - case ts.ScriptKind.JS: - default: - return ts.Extension.Js; - } -} - -export function getScriptKindFromAttributes( - attrs: Record -): ts.ScriptKind.TSX | ts.ScriptKind.JSX { - const type = attrs.lang || attrs.type; - - switch (type) { - case 'ts': - case 'typescript': - case 'text/ts': - case 'text/typescript': - return ts.ScriptKind.TSX; - case 'javascript': - case 'text/javascript': - default: - return ts.ScriptKind.JSX; - } -} - -export function isSvelteFilePath(filePath: string) { - return filePath.endsWith('.svelte'); -} - -export function isVirtualSvelteFilePath(filePath: string) { - return filePath.endsWith('.svelte.ts'); -} - -export function toRealSvelteFilePath(filePath: string) { - return filePath.slice(0, -'.ts'.length); -} - -export function toVirtualSvelteFilePath(filePath: string) { - return filePath.endsWith('.ts') ? filePath : filePath + '.ts'; -} - -export function ensureRealSvelteFilePath(filePath: string) { - return isVirtualSvelteFilePath(filePath) ? toRealSvelteFilePath(filePath) : filePath; -} - -export function convertRange( - document: { positionAt: (offset: number) => Position }, - range: { start?: number; length?: number } -): Range { - return Range.create( - document.positionAt(range.start || 0), - document.positionAt((range.start || 0) + (range.length || 0)) - ); -} - -export function convertToLocationRange(snapshot: DocumentSnapshot, textSpan: ts.TextSpan): Range { - const range = mapRangeToOriginal(snapshot, convertRange(snapshot, textSpan)); - - mapUnmappedToTheStartOfFile(range); - - return range; -} - -export function convertToLocationForReferenceOrDefinition( - snapshot: DocumentSnapshot, - textSpan: ts.TextSpan -): Location { - const location = mapLocationToOriginal(snapshot, convertRange(snapshot, textSpan)); - - mapUnmappedToTheStartOfFile(location.range); - - return location; -} - -/**Some definition like the svelte component class definition don't exist in the original, so we map to 0,1*/ -function mapUnmappedToTheStartOfFile(range: Range) { - if (range.start.line < 0) { - range.start.line = 0; - range.start.character = 1; - } - if (range.end.line < 0) { - range.end = range.start; - } -} - -export function hasNonZeroRange({ range }: { range?: Range }): boolean { - return ( - !!range && - (range.start.line !== range.end.line || range.start.character !== range.end.character) - ); -} - -export function rangeToTextSpan( - range: Range, - document: { offsetAt: (position: Position) => number } -): ts.TextSpan { - const start = document.offsetAt(range.start); - const end = document.offsetAt(range.end); - return { start, length: end - start }; -} - -export function findTsConfigPath( - fileName: string, - rootUris: string[], - fileExists: (path: string) => boolean, - getCanonicalFileName: GetCanonicalFileName -) { - const searchDir = dirname(fileName); - - const tsconfig = ts.findConfigFile(searchDir, fileExists, 'tsconfig.json') || ''; - const jsconfig = ts.findConfigFile(searchDir, fileExists, 'jsconfig.json') || ''; - // Prefer closest config file - const config = tsconfig.length >= jsconfig.length ? tsconfig : jsconfig; - - // Don't return config files that exceed the current workspace context or cross a node_modules folder - return !!config && - rootUris.some((rootUri) => isSubPath(rootUri, config, getCanonicalFileName)) && - !fileName - .substring(config.length - 13) - .split('/') - .includes('node_modules') - ? config - : ''; -} - -export function isSubPath( - uri: string, - possibleSubPath: string, - getCanonicalFileName: GetCanonicalFileName -): boolean { - // URL escape codes are in upper-case - // so getCanonicalFileName should be called after converting to file url - return getCanonicalFileName(pathToUrl(possibleSubPath)).startsWith(getCanonicalFileName(uri)); -} - -export function getNearestWorkspaceUri( - workspaceUris: string[], - path: string, - getCanonicalFileName: GetCanonicalFileName -) { - return Array.from(workspaceUris) - .sort((a, b) => b.length - a.length) - .find((workspaceUri) => isSubPath(workspaceUri, path, getCanonicalFileName)); -} - -export function symbolKindFromString(kind: string): SymbolKind { - switch (kind) { - case 'module': - return SymbolKind.Module; - case 'class': - return SymbolKind.Class; - case 'local class': - return SymbolKind.Class; - case 'interface': - return SymbolKind.Interface; - case 'enum': - return SymbolKind.Enum; - case 'enum member': - return SymbolKind.Constant; - case 'var': - return SymbolKind.Variable; - case 'local var': - return SymbolKind.Variable; - case 'function': - return SymbolKind.Function; - case 'local function': - return SymbolKind.Function; - case 'method': - return SymbolKind.Method; - case 'getter': - return SymbolKind.Method; - case 'setter': - return SymbolKind.Method; - case 'property': - return SymbolKind.Property; - case 'constructor': - return SymbolKind.Constructor; - case 'parameter': - return SymbolKind.Variable; - case 'type parameter': - return SymbolKind.Variable; - case 'alias': - return SymbolKind.Variable; - case 'let': - return SymbolKind.Variable; - case 'const': - return SymbolKind.Constant; - case 'JSX attribute': - return SymbolKind.Property; - default: - return SymbolKind.Variable; - } -} - -export function scriptElementKindToCompletionItemKind( - kind: ts.ScriptElementKind -): CompletionItemKind { - switch (kind) { - case ts.ScriptElementKind.primitiveType: - case ts.ScriptElementKind.keyword: - return CompletionItemKind.Keyword; - case ts.ScriptElementKind.constElement: - return CompletionItemKind.Constant; - case ts.ScriptElementKind.letElement: - case ts.ScriptElementKind.variableElement: - case ts.ScriptElementKind.localVariableElement: - case ts.ScriptElementKind.alias: - return CompletionItemKind.Variable; - case ts.ScriptElementKind.memberVariableElement: - case ts.ScriptElementKind.memberGetAccessorElement: - case ts.ScriptElementKind.memberSetAccessorElement: - return CompletionItemKind.Field; - case ts.ScriptElementKind.functionElement: - return CompletionItemKind.Function; - case ts.ScriptElementKind.memberFunctionElement: - case ts.ScriptElementKind.constructSignatureElement: - case ts.ScriptElementKind.callSignatureElement: - case ts.ScriptElementKind.indexSignatureElement: - return CompletionItemKind.Method; - case ts.ScriptElementKind.enumElement: - return CompletionItemKind.Enum; - case ts.ScriptElementKind.moduleElement: - case ts.ScriptElementKind.externalModuleName: - return CompletionItemKind.Module; - case ts.ScriptElementKind.classElement: - case ts.ScriptElementKind.typeElement: - return CompletionItemKind.Class; - case ts.ScriptElementKind.interfaceElement: - return CompletionItemKind.Interface; - case ts.ScriptElementKind.warning: - case ts.ScriptElementKind.scriptElement: - return CompletionItemKind.File; - case ts.ScriptElementKind.directory: - return CompletionItemKind.Folder; - case ts.ScriptElementKind.string: - return CompletionItemKind.Constant; - } - return CompletionItemKind.Property; -} - -export function mapSeverity(category: ts.DiagnosticCategory): DiagnosticSeverity { - switch (category) { - case ts.DiagnosticCategory.Error: - return DiagnosticSeverity.Error; - case ts.DiagnosticCategory.Warning: - return DiagnosticSeverity.Warning; - case ts.DiagnosticCategory.Suggestion: - return DiagnosticSeverity.Hint; - case ts.DiagnosticCategory.Message: - return DiagnosticSeverity.Information; - } - - return DiagnosticSeverity.Error; -} - -// Matches comments that come before any non-comment content -const commentsRegex = /^(\s*\/\/.*\s*)*/; -// The following regex matches @ts-check or @ts-nocheck if: -// - must be @ts-(no)check -// - the comment which has @ts-(no)check can have any type of whitespace before it, but not other characters -// - what's coming after @ts-(no)check is irrelevant as long there is any kind of whitespace or line break, so this would be picked up, too: // @ts-check asdasd -// [ \t\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff] -// is just \s (a.k.a any whitespace character) without linebreak and vertical tab -const tsCheckRegex = - /\/\/[ \t\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*(@ts-(no)?check)($|\s)/; - -/** - * Returns `// @ts-check` or `// @ts-nocheck` if content starts with comments and has one of these - * in its comments. - */ -export function getTsCheckComment(str = ''): string | undefined { - const comments = str.match(commentsRegex)?.[0]; - if (comments) { - const tsCheck = comments.match(tsCheckRegex); - if (tsCheck) { - // second-last entry is the capturing group with the exact ts-check wording - return `// ${tsCheck[tsCheck.length - 3]}${ts.sys.newLine}`; - } - } -} - -export function convertToTextSpan(range: Range, snapshot: DocumentSnapshot): ts.TextSpan { - const start = snapshot.offsetAt(snapshot.getGeneratedPosition(range.start)); - const end = snapshot.offsetAt(snapshot.getGeneratedPosition(range.end)); - - return { - start, - length: end - start - }; -} - -export function isInScript(position: Position, snapshot: SvelteDocumentSnapshot | Document) { - return isInTag(position, snapshot.scriptInfo) || isInTag(position, snapshot.moduleScriptInfo); -} - -export function getDiagnosticTag(diagnostic: ts.Diagnostic): DiagnosticTag[] { - const tags: DiagnosticTag[] = []; - if (diagnostic.reportsUnnecessary) { - tags.push(DiagnosticTag.Unnecessary); - } - if (diagnostic.reportsDeprecated) { - tags.push(DiagnosticTag.Deprecated); - } - return tags; -} - -export function changeSvelteComponentName(name: string) { - return name.replace(/(\w+)__SvelteComponent_/, '$1'); -} - -const COMPONENT_SUFFIX = '__SvelteComponent_'; - -export function isGeneratedSvelteComponentName(className: string) { - return className.endsWith(COMPONENT_SUFFIX); -} - -export function offsetOfGeneratedComponentExport(snapshot: SvelteDocumentSnapshot) { - return snapshot.getFullText().lastIndexOf(COMPONENT_SUFFIX); -} - -export function toGeneratedSvelteComponentName(className: string) { - return className + COMPONENT_SUFFIX; -} - -export function hasTsExtensions(fileName: string) { - return ( - fileName.endsWith(ts.Extension.Dts) || - fileName.endsWith(ts.Extension.Tsx) || - fileName.endsWith(ts.Extension.Ts) - ); -} diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts deleted file mode 100644 index fcc031466..000000000 --- a/packages/language-server/src/server.ts +++ /dev/null @@ -1,552 +0,0 @@ -import { - ApplyWorkspaceEditParams, - ApplyWorkspaceEditRequest, - CodeActionKind, - DocumentUri, - Connection, - MessageType, - RenameFile, - RequestType, - ShowMessageNotification, - TextDocumentIdentifier, - TextDocumentPositionParams, - TextDocumentSyncKind, - WorkspaceEdit, - SemanticTokensRequest, - SemanticTokensRangeRequest, - DidChangeWatchedFilesParams, - LinkedEditingRangeRequest, - CallHierarchyPrepareRequest, - CallHierarchyIncomingCallsRequest, - CallHierarchyOutgoingCallsRequest, - InlayHintRequest, - SemanticTokensRefreshRequest, - InlayHintRefreshRequest, - DidChangeWatchedFilesNotification -} from 'vscode-languageserver'; -import { IPCMessageReader, IPCMessageWriter, createConnection } from 'vscode-languageserver/node'; -import { DiagnosticsManager } from './lib/DiagnosticsManager'; -import { Document, DocumentManager } from './lib/documents'; -import { getSemanticTokenLegends } from './lib/semanticToken/semanticTokenLegend'; -import { Logger } from './logger'; -import { LSConfigManager } from './ls-config'; -import { - AppCompletionItem, - CSSPlugin, - HTMLPlugin, - PluginHost, - SveltePlugin, - TypeScriptPlugin, - OnWatchFileChangesPara, - LSAndTSDocResolver -} from './plugins'; -import { debounceThrottle, isNotNullOrUndefined, normalizeUri, urlToPath } from './utils'; -import { FallbackWatcher } from './lib/FallbackWatcher'; -import { configLoader } from './lib/documents/configLoader'; -import { setIsTrusted } from './importPackage'; -import { SORT_IMPORT_CODE_ACTION_KIND } from './plugins/typescript/features/CodeActionsProvider'; -import { createLanguageServices } from './plugins/css/service'; -import { FileSystemProvider } from './plugins/css/FileSystemProvider'; - -namespace TagCloseRequest { - export const type: RequestType = - new RequestType('html/tag'); -} - -export interface LSOptions { - /** - * If you have a connection already that the ls should use, pass it in. - * Else the connection will be created from `process`. - */ - connection?: Connection; - /** - * If you want only errors getting logged. - * Defaults to false. - */ - logErrorsOnly?: boolean; -} - -/** - * Starts the language server. - * - * @param options Options to customize behavior - */ -export function startServer(options?: LSOptions) { - let connection = options?.connection; - if (!connection) { - if (process.argv.includes('--stdio')) { - console.log = (...args: any[]) => { - console.warn(...args); - }; - connection = createConnection(process.stdin, process.stdout); - } else { - connection = createConnection( - new IPCMessageReader(process), - new IPCMessageWriter(process) - ); - } - } - - if (options?.logErrorsOnly !== undefined) { - Logger.setLogErrorsOnly(options.logErrorsOnly); - } - - const docManager = new DocumentManager( - (textDocument) => new Document(textDocument.uri, textDocument.text) - ); - const configManager = new LSConfigManager(); - const pluginHost = new PluginHost(docManager); - let sveltePlugin: SveltePlugin = undefined as any; - let watcher: FallbackWatcher | undefined; - - connection.onInitialize((evt) => { - const workspaceUris = evt.workspaceFolders?.map((folder) => folder.uri.toString()) ?? [ - evt.rootUri ?? '' - ]; - Logger.log('Initialize language server at ', workspaceUris.join(', ')); - if (workspaceUris.length === 0) { - Logger.error('No workspace path set'); - } - - if (!evt.capabilities.workspace?.didChangeWatchedFiles) { - const workspacePaths = workspaceUris.map(urlToPath).filter(isNotNullOrUndefined); - watcher = new FallbackWatcher('**/*.{ts,js}', workspacePaths); - watcher.onDidChangeWatchedFiles(onDidChangeWatchedFiles); - } - - const isTrusted: boolean = evt.initializationOptions?.isTrusted ?? true; - configLoader.setDisabled(!isTrusted); - setIsTrusted(isTrusted); - configManager.updateIsTrusted(isTrusted); - if (!isTrusted) { - Logger.log('Workspace is not trusted, running with reduced capabilities.'); - } - - Logger.setDebug( - (evt.initializationOptions?.configuration?.svelte || - evt.initializationOptions?.config)?.['language-server']?.debug - ); - // Backwards-compatible way of setting initialization options (first `||` is the old style) - configManager.update( - evt.initializationOptions?.configuration?.svelte?.plugin || - evt.initializationOptions?.config || - {} - ); - configManager.updateTsJsUserPreferences( - evt.initializationOptions?.configuration || - evt.initializationOptions?.typescriptConfig || - {} - ); - configManager.updateTsJsFormateConfig( - evt.initializationOptions?.configuration || - evt.initializationOptions?.typescriptConfig || - {} - ); - configManager.updateEmmetConfig( - evt.initializationOptions?.configuration?.emmet || - evt.initializationOptions?.emmetConfig || - {} - ); - configManager.updatePrettierConfig( - evt.initializationOptions?.configuration?.prettier || - evt.initializationOptions?.prettierConfig || - {} - ); - // no old style as these were added later - configManager.updateCssConfig(evt.initializationOptions?.configuration?.css); - configManager.updateScssConfig(evt.initializationOptions?.configuration?.scss); - configManager.updateLessConfig(evt.initializationOptions?.configuration?.less); - configManager.updateHTMLConfig(evt.initializationOptions?.configuration?.html); - configManager.updateClientCapabilities(evt.capabilities); - - pluginHost.initialize({ - filterIncompleteCompletions: - !evt.initializationOptions?.dontFilterIncompleteCompletions, - definitionLinkSupport: !!evt.capabilities.textDocument?.definition?.linkSupport - }); - // Order of plugin registration matters for FirstNonNull, which affects for example hover info - pluginHost.register((sveltePlugin = new SveltePlugin(configManager))); - pluginHost.register(new HTMLPlugin(docManager, configManager)); - - const cssLanguageServices = createLanguageServices({ - clientCapabilities: evt.capabilities, - fileSystemProvider: new FileSystemProvider() - }); - const workspaceFolders = evt.workspaceFolders ?? [{ name: '', uri: evt.rootUri ?? '' }]; - pluginHost.register( - new CSSPlugin(docManager, configManager, workspaceFolders, cssLanguageServices) - ); - const normalizedWorkspaceUris = workspaceUris.map(normalizeUri); - pluginHost.register( - new TypeScriptPlugin( - configManager, - new LSAndTSDocResolver(docManager, normalizedWorkspaceUris, configManager, { - notifyExceedSizeLimit: notifyTsServiceExceedSizeLimit, - onProjectReloaded: refreshCrossFilesSemanticFeatures, - watch: true - }), - normalizedWorkspaceUris - ) - ); - - const clientSupportApplyEditCommand = !!evt.capabilities.workspace?.applyEdit; - const clientCodeActionCapabilities = evt.capabilities.textDocument?.codeAction; - const clientSupportedCodeActionKinds = - clientCodeActionCapabilities?.codeActionLiteralSupport?.codeActionKind.valueSet; - - return { - capabilities: { - textDocumentSync: { - openClose: true, - change: TextDocumentSyncKind.Incremental, - save: { - includeText: false - } - }, - hoverProvider: true, - completionProvider: { - resolveProvider: true, - triggerCharacters: [ - '.', - '"', - "'", - '`', - '/', - '@', - '<', - - // Emmet - '>', - '*', - '#', - '$', - '+', - '^', - '(', - '[', - '@', - '-', - // No whitespace because - // it makes for weird/too many completions - // of other completion providers - - // Svelte - ':', - '|' - ], - completionItem: { - labelDetailsSupport: true - } - }, - documentFormattingProvider: true, - colorProvider: true, - documentSymbolProvider: true, - definitionProvider: true, - codeActionProvider: clientCodeActionCapabilities?.codeActionLiteralSupport - ? { - codeActionKinds: [ - CodeActionKind.QuickFix, - CodeActionKind.SourceOrganizeImports, - SORT_IMPORT_CODE_ACTION_KIND, - ...(clientSupportApplyEditCommand ? [CodeActionKind.Refactor] : []) - ].filter( - clientSupportedCodeActionKinds && - evt.initializationOptions?.shouldFilterCodeActionKind - ? (kind) => clientSupportedCodeActionKinds.includes(kind) - : () => true - ), - resolveProvider: true - } - : true, - executeCommandProvider: clientSupportApplyEditCommand - ? { - commands: [ - 'function_scope_0', - 'function_scope_1', - 'function_scope_2', - 'function_scope_3', - 'constant_scope_0', - 'constant_scope_1', - 'constant_scope_2', - 'constant_scope_3', - 'extract_to_svelte_component', - 'Infer function return type' - ] - } - : undefined, - renameProvider: evt.capabilities.textDocument?.rename?.prepareSupport - ? { prepareProvider: true } - : true, - referencesProvider: true, - selectionRangeProvider: true, - signatureHelpProvider: { - triggerCharacters: ['(', ',', '<'], - retriggerCharacters: [')'] - }, - semanticTokensProvider: { - legend: getSemanticTokenLegends(), - range: true, - full: true - }, - linkedEditingRangeProvider: true, - implementationProvider: true, - typeDefinitionProvider: true, - inlayHintProvider: true, - callHierarchyProvider: true, - foldingRangeProvider: true - } - }; - }); - - connection.onInitialized(() => { - if ( - !watcher && - configManager.getClientCapabilities()?.workspace?.didChangeWatchedFiles - ?.dynamicRegistration - ) { - connection?.client.register(DidChangeWatchedFilesNotification.type, { - watchers: [ - { - globPattern: '**/*.{ts,js,mts,mjs,cjs,cts,json}' - } - ] - }); - } - }); - - function notifyTsServiceExceedSizeLimit() { - connection?.sendNotification(ShowMessageNotification.type, { - message: - 'Svelte language server detected a large amount of JS/Svelte files. ' + - 'To enable project-wide JavaScript/TypeScript language features for Svelte files, ' + - 'exclude large folders in the tsconfig.json or jsconfig.json with source files that you do not work on.', - type: MessageType.Warning - }); - } - - connection.onExit(() => { - watcher?.dispose(); - }); - - connection.onRenameRequest((req) => - pluginHost.rename(req.textDocument, req.position, req.newName) - ); - connection.onPrepareRename((req) => pluginHost.prepareRename(req.textDocument, req.position)); - - connection.onDidChangeConfiguration(({ settings }) => { - configManager.update(settings.svelte?.plugin); - configManager.updateTsJsUserPreferences(settings); - configManager.updateTsJsFormateConfig(settings); - configManager.updateEmmetConfig(settings.emmet); - configManager.updatePrettierConfig(settings.prettier); - configManager.updateCssConfig(settings.css); - configManager.updateScssConfig(settings.scss); - configManager.updateLessConfig(settings.less); - configManager.updateHTMLConfig(settings.html); - Logger.setDebug(settings.svelte?.['language-server']?.debug); - }); - - connection.onDidOpenTextDocument((evt) => { - const document = docManager.openClientDocument(evt.textDocument); - diagnosticsManager.scheduleUpdate(document); - }); - - connection.onDidCloseTextDocument((evt) => docManager.closeDocument(evt.textDocument.uri)); - connection.onDidChangeTextDocument((evt) => { - diagnosticsManager.cancelStarted(evt.textDocument.uri); - docManager.updateDocument(evt.textDocument, evt.contentChanges); - pluginHost.didUpdateDocument(); - }); - connection.onHover((evt) => pluginHost.doHover(evt.textDocument, evt.position)); - connection.onCompletion((evt, cancellationToken) => - pluginHost.getCompletions(evt.textDocument, evt.position, evt.context, cancellationToken) - ); - connection.onDocumentFormatting((evt) => - pluginHost.formatDocument(evt.textDocument, evt.options) - ); - connection.onRequest(TagCloseRequest.type, (evt) => - pluginHost.doTagComplete(evt.textDocument, evt.position) - ); - connection.onDocumentColor((evt) => pluginHost.getDocumentColors(evt.textDocument)); - connection.onColorPresentation((evt) => - pluginHost.getColorPresentations(evt.textDocument, evt.range, evt.color) - ); - connection.onDocumentSymbol((evt, cancellationToken) => - pluginHost.getDocumentSymbols(evt.textDocument, cancellationToken) - ); - connection.onDefinition((evt) => pluginHost.getDefinitions(evt.textDocument, evt.position)); - connection.onReferences((evt) => - pluginHost.findReferences(evt.textDocument, evt.position, evt.context) - ); - - connection.onCodeAction((evt, cancellationToken) => - pluginHost.getCodeActions(evt.textDocument, evt.range, evt.context, cancellationToken) - ); - connection.onExecuteCommand(async (evt) => { - const result = await pluginHost.executeCommand( - { uri: evt.arguments?.[0] }, - evt.command, - evt.arguments - ); - if (WorkspaceEdit.is(result)) { - const edit: ApplyWorkspaceEditParams = { edit: result }; - connection?.sendRequest(ApplyWorkspaceEditRequest.type.method, edit); - } else if (result) { - connection?.sendNotification(ShowMessageNotification.type.method, { - message: result, - type: MessageType.Error - }); - } - }); - connection.onCodeActionResolve((codeAction, cancellationToken) => { - const data = codeAction.data as TextDocumentIdentifier; - return pluginHost.resolveCodeAction(data, codeAction, cancellationToken); - }); - - connection.onCompletionResolve((completionItem, cancellationToken) => { - const data = (completionItem as AppCompletionItem).data as TextDocumentIdentifier; - - if (!data) { - return completionItem; - } - - return pluginHost.resolveCompletion(data, completionItem, cancellationToken); - }); - - connection.onSignatureHelp((evt, cancellationToken) => - pluginHost.getSignatureHelp(evt.textDocument, evt.position, evt.context, cancellationToken) - ); - - connection.onSelectionRanges((evt) => - pluginHost.getSelectionRanges(evt.textDocument, evt.positions) - ); - - connection.onImplementation((evt) => - pluginHost.getImplementation(evt.textDocument, evt.position) - ); - - connection.onTypeDefinition((evt) => - pluginHost.getTypeDefinition(evt.textDocument, evt.position) - ); - - connection.onFoldingRanges((evt) => pluginHost.getFoldingRanges(evt.textDocument)); - - const diagnosticsManager = new DiagnosticsManager( - connection.sendDiagnostics, - docManager, - pluginHost.getDiagnostics.bind(pluginHost) - ); - - const refreshSemanticTokens = debounceThrottle(() => { - if (configManager?.getClientCapabilities()?.workspace?.semanticTokens?.refreshSupport) { - connection?.sendRequest(SemanticTokensRefreshRequest.method); - } - }, 1500); - - const refreshInlayHints = debounceThrottle(() => { - if (configManager?.getClientCapabilities()?.workspace?.inlayHint?.refreshSupport) { - connection?.sendRequest(InlayHintRefreshRequest.method); - } - }, 1500); - - const refreshCrossFilesSemanticFeatures = () => { - diagnosticsManager.scheduleUpdateAll(); - refreshInlayHints(); - refreshSemanticTokens(); - }; - - connection.onDidChangeWatchedFiles(onDidChangeWatchedFiles); - function onDidChangeWatchedFiles(para: DidChangeWatchedFilesParams) { - const onWatchFileChangesParas = para.changes - .map((change) => ({ - fileName: urlToPath(change.uri), - changeType: change.type - })) - .filter((change): change is OnWatchFileChangesPara => !!change.fileName); - - pluginHost.onWatchFileChanges(onWatchFileChangesParas); - - refreshCrossFilesSemanticFeatures(); - } - - connection.onDidSaveTextDocument(diagnosticsManager.scheduleUpdateAll.bind(diagnosticsManager)); - connection.onNotification('$/onDidChangeTsOrJsFile', async (e: any) => { - const path = urlToPath(e.uri); - if (path) { - pluginHost.updateTsOrJsFile(path, e.changes); - } - - refreshCrossFilesSemanticFeatures(); - }); - - connection.onRequest(SemanticTokensRequest.type, (evt, cancellationToken) => - pluginHost.getSemanticTokens(evt.textDocument, undefined, cancellationToken) - ); - connection.onRequest(SemanticTokensRangeRequest.type, (evt, cancellationToken) => - pluginHost.getSemanticTokens(evt.textDocument, evt.range, cancellationToken) - ); - - connection.onRequest( - LinkedEditingRangeRequest.type, - async (evt) => await pluginHost.getLinkedEditingRanges(evt.textDocument, evt.position) - ); - - connection.onRequest(InlayHintRequest.type, (evt, cancellationToken) => - pluginHost.getInlayHints(evt.textDocument, evt.range, cancellationToken) - ); - - connection.onRequest( - CallHierarchyPrepareRequest.type, - async (evt, token) => - await pluginHost.prepareCallHierarchy(evt.textDocument, evt.position, token) - ); - - connection.onRequest( - CallHierarchyIncomingCallsRequest.type, - async (evt, token) => await pluginHost.getIncomingCalls(evt.item, token) - ); - - connection.onRequest( - CallHierarchyOutgoingCallsRequest.type, - async (evt, token) => await pluginHost.getOutgoingCalls(evt.item, token) - ); - - docManager.on('documentChange', diagnosticsManager.scheduleUpdate.bind(diagnosticsManager)); - docManager.on('documentClose', (document: Document) => - diagnosticsManager.removeDiagnostics(document) - ); - - // The language server protocol does not have a specific "did rename/move files" event, - // so we create our own in the extension client and handle it here - connection.onRequest('$/getEditsForFileRename', async (fileRename: RenameFile) => - pluginHost.updateImports(fileRename) - ); - - connection.onRequest('$/getFileReferences', async (uri: string) => { - return pluginHost.fileReferences(uri); - }); - - connection.onRequest('$/getComponentReferences', async (uri: string) => { - return pluginHost.findComponentReferences(uri); - }); - - connection.onRequest('$/getCompiledCode', async (uri: DocumentUri) => { - const doc = docManager.get(uri); - if (!doc) { - return null; - } - - if (doc) { - const compiled = await sveltePlugin.getCompiledResult(doc); - if (compiled) { - const js = compiled.js; - const css = compiled.css; - return { js, css }; - } else { - return null; - } - } - }); - - connection.listen(); -} diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts deleted file mode 100644 index 4d863a882..000000000 --- a/packages/language-server/src/svelte-check.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { isAbsolute } from 'path'; -import ts from 'typescript'; -import { Diagnostic, Position, Range } from 'vscode-languageserver'; -import { WorkspaceFolder } from 'vscode-languageserver-protocol'; -import { Document, DocumentManager } from './lib/documents'; -import { Logger } from './logger'; -import { LSConfigManager } from './ls-config'; -import { - CSSPlugin, - LSAndTSDocResolver, - PluginHost, - SveltePlugin, - TypeScriptPlugin -} from './plugins'; -import { FileSystemProvider } from './plugins/css/FileSystemProvider'; -import { createLanguageServices } from './plugins/css/service'; -import { JSOrTSDocumentSnapshot } from './plugins/typescript/DocumentSnapshot'; -import { isInGeneratedCode } from './plugins/typescript/features/utils'; -import { convertRange, getDiagnosticTag, mapSeverity } from './plugins/typescript/utils'; -import { pathToUrl, urlToPath } from './utils'; - -export type SvelteCheckDiagnosticSource = 'js' | 'css' | 'svelte'; - -export interface SvelteCheckOptions { - compilerWarnings?: Record; - diagnosticSources?: SvelteCheckDiagnosticSource[]; - /** - * Path has to be absolute - */ - tsconfig?: string; - onProjectReload?: () => void; - watch?: boolean; -} - -/** - * Small wrapper around PluginHost's Diagnostic Capabilities - * for svelte-check, without the overhead of the lsp. - */ -export class SvelteCheck { - private docManager = new DocumentManager( - (textDocument) => new Document(textDocument.uri, textDocument.text) - ); - private configManager = new LSConfigManager(); - private pluginHost = new PluginHost(this.docManager); - private lsAndTSDocResolver?: LSAndTSDocResolver; - - constructor( - workspacePath: string, - private options: SvelteCheckOptions = {} - ) { - Logger.setLogErrorsOnly(true); - this.initialize(workspacePath, options); - } - - private async initialize(workspacePath: string, options: SvelteCheckOptions) { - if (options.tsconfig && !isAbsolute(options.tsconfig)) { - throw new Error('tsconfigPath needs to be absolute, got ' + options.tsconfig); - } - - this.configManager.update({ - svelte: { - compilerWarnings: options.compilerWarnings - } - }); - // No HTMLPlugin, it does not provide diagnostics - if (shouldRegister('svelte')) { - this.pluginHost.register(new SveltePlugin(this.configManager)); - } - if (shouldRegister('css')) { - const services = createLanguageServices({ - fileSystemProvider: new FileSystemProvider() - }); - const workspaceFolders: WorkspaceFolder[] = [ - { - name: '', - uri: pathToUrl(workspacePath) - } - ]; - this.pluginHost.register( - new CSSPlugin(this.docManager, this.configManager, workspaceFolders, services) - ); - } - if (shouldRegister('js') || options.tsconfig) { - const workspaceUris = [pathToUrl(workspacePath)]; - this.lsAndTSDocResolver = new LSAndTSDocResolver( - this.docManager, - workspaceUris, - this.configManager, - { - tsconfigPath: options.tsconfig, - isSvelteCheck: true, - onProjectReloaded: options.onProjectReload, - watch: options.watch - } - ); - this.pluginHost.register( - new TypeScriptPlugin(this.configManager, this.lsAndTSDocResolver, workspaceUris) - ); - } - - function shouldRegister(source: SvelteCheckDiagnosticSource) { - return !options.diagnosticSources || options.diagnosticSources.includes(source); - } - } - - /** - * Creates/updates given document - * - * @param doc Text and Uri of the document - * @param isNew Whether or not this is the creation of the document - */ - async upsertDocument(doc: { text: string; uri: string }, isNew: boolean): Promise { - const filePath = urlToPath(doc.uri) || ''; - - if (this.options.tsconfig) { - const lsContainer = await this.getLSContainer(this.options.tsconfig); - if (!lsContainer.fileBelongsToProject(filePath, isNew)) { - return; - } - } - - if ( - doc.uri.endsWith('.ts') || - doc.uri.endsWith('.js') || - doc.uri.endsWith('.tsx') || - doc.uri.endsWith('.jsx') || - doc.uri.endsWith('.mjs') || - doc.uri.endsWith('.cjs') || - doc.uri.endsWith('.mts') || - doc.uri.endsWith('.cts') - ) { - this.pluginHost.updateTsOrJsFile(filePath, [ - { - range: Range.create( - Position.create(0, 0), - Position.create(Number.MAX_VALUE, Number.MAX_VALUE) - ), - text: doc.text - } - ]); - } else { - this.docManager.openClientDocument({ - text: doc.text, - uri: doc.uri - }); - } - } - - /** - * Removes/closes document - * - * @param uri Uri of the document - */ - async removeDocument(uri: string): Promise { - if (!this.docManager.get(uri)) { - return; - } - - this.docManager.closeDocument(uri); - this.docManager.releaseDocument(uri); - if (this.options.tsconfig) { - const lsContainer = await this.getLSContainer(this.options.tsconfig); - lsContainer.deleteSnapshot(urlToPath(uri) || ''); - } - } - - /** - * Gets the diagnostics for all currently open files. - */ - async getDiagnostics(): Promise< - Array<{ filePath: string; text: string; diagnostics: Diagnostic[] }> - > { - if (this.options.tsconfig) { - return this.getDiagnosticsForTsconfig(this.options.tsconfig); - } - return await Promise.all( - this.docManager.getAllOpenedByClient().map(async (doc) => { - const uri = doc[1].uri; - return await this.getDiagnosticsForFile(uri); - }) - ); - } - - private async getDiagnosticsForTsconfig(tsconfigPath: string) { - const lsContainer = await this.getLSContainer(tsconfigPath); - - const noInputsFoundError = lsContainer.configErrors?.find((e) => e.code === 18003); - if (noInputsFoundError) { - throw new Error(noInputsFoundError.messageText.toString()); - } - - const lang = lsContainer.getService(); - const files = lang.getProgram()?.getSourceFiles() || []; - const options = lang.getProgram()?.getCompilerOptions() || {}; - - return await Promise.all( - files.map((file) => { - const uri = pathToUrl(file.fileName); - const doc = this.docManager.get(uri); - if (doc) { - this.docManager.markAsOpenedInClient(uri); - return this.getDiagnosticsForFile(uri); - } else { - // This check is done inside TS mostly, too, but for some diagnostics like suggestions it - // doesn't apply to all code paths. That's why we do it here, too. - const skipDiagnosticsForFile = - (options.skipLibCheck && file.isDeclarationFile) || - (options.skipDefaultLibCheck && file.hasNoDefaultLib) || - // ignore JS files in node_modules - /\/node_modules\/.+\.(c|m)?js$/.test(file.fileName); - const snapshot = lsContainer.snapshotManager.get(file.fileName) as - | JSOrTSDocumentSnapshot - | undefined; - const isKitFile = snapshot?.kitFile ?? false; - const diagnostics: Diagnostic[] = []; - const map = (diagnostic: ts.Diagnostic, range?: Range) => ({ - range: - range ?? - convertRange( - { positionAt: file.getLineAndCharacterOfPosition.bind(file) }, - diagnostic - ), - severity: mapSeverity(diagnostic.category), - source: diagnostic.source, - message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'), - code: diagnostic.code, - tags: getDiagnosticTag(diagnostic) - }); - - if (!skipDiagnosticsForFile) { - const originalDiagnostics = [ - ...lang.getSyntacticDiagnostics(file.fileName), - ...lang.getSuggestionDiagnostics(file.fileName), - ...lang.getSemanticDiagnostics(file.fileName) - ]; - - for (let diagnostic of originalDiagnostics) { - if (!diagnostic.start || !diagnostic.length || !isKitFile) { - diagnostics.push(map(diagnostic)); - continue; - } - - let range: Range | undefined = undefined; - const inGenerated = isInGeneratedCode( - file.text, - diagnostic.start, - diagnostic.start + diagnostic.length - ); - if (inGenerated && snapshot) { - const pos = snapshot.getOriginalPosition( - snapshot.positionAt(diagnostic.start) - ); - range = { - start: pos, - end: { - line: pos.line, - // adjust length so it doesn't spill over to the next line - character: pos.character + 1 - } - }; - // If not one of the specific error messages then filter out - if (diagnostic.code === 2307) { - diagnostic = { - ...diagnostic, - messageText: - typeof diagnostic.messageText === 'string' && - diagnostic.messageText.includes('./$types') - ? diagnostic.messageText + - ` (this likely means that SvelteKit's type generation didn't run yet - try running it by executing 'npm run dev' or 'npm run build')` - : diagnostic.messageText - }; - } else if (diagnostic.code === 2694) { - diagnostic = { - ...diagnostic, - messageText: - typeof diagnostic.messageText === 'string' && - diagnostic.messageText.includes('/$types') - ? diagnostic.messageText + - ` (this likely means that SvelteKit's generated types are out of date - try rerunning it by executing 'npm run dev' or 'npm run build')` - : diagnostic.messageText - }; - } else if ( - diagnostic.code !== - 2355 /* A function whose declared type is neither 'void' nor 'any' must return a value */ - ) { - continue; - } - } - - diagnostics.push(map(diagnostic, range)); - } - } - - return { - filePath: file.fileName, - text: snapshot?.originalText ?? file.text, - diagnostics - }; - } - }) - ); - } - - private async getDiagnosticsForFile(uri: string) { - const diagnostics = await this.pluginHost.getDiagnostics({ uri }); - return { - filePath: urlToPath(uri) || '', - text: this.docManager.get(uri)?.getText() || '', - diagnostics - }; - } - - private getLSContainer(tsconfigPath: string) { - if (!this.lsAndTSDocResolver) { - throw new Error('Cannot run with tsconfig path without LS/TSdoc resolver'); - } - return this.lsAndTSDocResolver.getTSService(tsconfigPath); - } -} diff --git a/packages/language-server/src/utils.ts b/packages/language-server/src/utils.ts deleted file mode 100644 index 34d828937..000000000 --- a/packages/language-server/src/utils.ts +++ /dev/null @@ -1,343 +0,0 @@ -import { isEqual, sum, uniqWith } from 'lodash'; -import { FoldingRange, Node } from 'vscode-html-languageservice'; -import { Position, Range } from 'vscode-languageserver'; -import { URI } from 'vscode-uri'; -import { Document, TagInformation } from './lib/documents'; - -type Predicate = (x: T) => boolean; - -export function not(predicate: Predicate) { - return (x: T) => !predicate(x); -} - -export function or(...predicates: Array>) { - return (x: T) => predicates.some((predicate) => predicate(x)); -} - -export function and(...predicates: Array>) { - return (x: T) => predicates.every((predicate) => predicate(x)); -} - -export function unique(array: T[]): T[] { - return uniqWith(array, isEqual); -} - -export function clamp(num: number, min: number, max: number): number { - return Math.max(min, Math.min(max, num)); -} - -export function urlToPath(stringUrl: string): string | null { - const url = URI.parse(stringUrl); - if (url.scheme !== 'file') { - return null; - } - return url.fsPath.replace(/\\/g, '/'); -} - -export function pathToUrl(path: string) { - return URI.file(path).toString(); -} - -/** - * Some paths (on windows) start with a upper case driver letter, some don't. - * This is normalized here. - */ -export function normalizePath(path: string): string { - return URI.file(path).fsPath.replace(/\\/g, '/'); -} - -/** - * URIs coming from the client could be encoded in a different - * way than expected / than the internal services create them. - * This normalizes them to be the same as the internally generated ones. - */ -export function normalizeUri(uri: string): string { - return URI.parse(uri).toString(); -} - -/** - * Given a path like foo/bar or foo/bar.svelte , returns its last path - * (bar or bar.svelte in this example). - */ -export function getLastPartOfPath(path: string): string { - return path.replace(/\\/g, '/').split('/').pop() || ''; -} - -export function flatten(arr: Array): T[] { - return arr.reduce( - (all: T[], item) => (Array.isArray(item) ? [...all, ...item] : [...all, item]), - [] - ); -} - -/** - * Map or keep original (passthrough) if the mapper returns undefined. - */ -export function passMap(array: T[], mapper: (x: T) => void | T[]) { - return array.map((x) => { - const mapped = mapper(x); - return mapped === undefined ? x : mapped; - }); -} - -export function isInRange(range: Range, positionToTest: Position): boolean { - return ( - isBeforeOrEqualToPosition(range.end, positionToTest) && - isBeforeOrEqualToPosition(positionToTest, range.start) - ); -} - -export function isZeroLengthRange(range: Range): boolean { - return isPositionEqual(range.start, range.end); -} - -export function isRangeStartAfterEnd(range: Range): boolean { - return ( - range.end.line < range.start.line || - (range.end.line === range.start.line && range.end.character < range.start.character) - ); -} - -export function swapRangeStartEndIfNecessary(range: Range): Range { - if (isRangeStartAfterEnd(range)) { - const start = range.start; - range.start = range.end; - range.end = start; - } - return range; -} - -export function moveRangeStartToEndIfNecessary(range: Range): Range { - if (isRangeStartAfterEnd(range)) { - range.start = range.end; - } - return range; -} - -export function isBeforeOrEqualToPosition(position: Position, positionToTest: Position): boolean { - return ( - positionToTest.line < position.line || - (positionToTest.line === position.line && positionToTest.character <= position.character) - ); -} - -export function isPositionEqual(position1: Position, position2: Position): boolean { - return position1.line === position2.line && position1.character === position2.character; -} - -export function isNotNullOrUndefined(val: T | undefined | null): val is T { - return val !== undefined && val !== null; -} - -/** - * Debounces a function but cancels previous invocation only if - * a second function determines it should. - * - * @param fn The function with it's argument - * @param determineIfSame The function which determines if the previous invocation should be canceld or not - * @param miliseconds Number of miliseconds to debounce - */ -export function debounceSameArg( - fn: (arg: T) => void, - shouldCancelPrevious: (newArg: T, prevArg?: T) => boolean, - miliseconds: number -): (arg: T) => void { - let timeout: any; - let prevArg: T | undefined; - - return (arg: T) => { - if (shouldCancelPrevious(arg, prevArg)) { - clearTimeout(timeout); - } - - prevArg = arg; - timeout = setTimeout(() => { - fn(arg); - prevArg = undefined; - }, miliseconds); - }; -} - -/** - * Debounces a function but also waits at minimum the specified number of miliseconds until - * the next invocation. This avoids needless calls when a synchronous call (like diagnostics) - * took too long and the whole timeout of the next call was eaten up already. - * - * @param fn The function - * @param miliseconds Number of miliseconds to debounce/throttle - */ -export function debounceThrottle(fn: () => void, miliseconds: number): () => void { - let timeout: any; - let lastInvocation = Date.now() - miliseconds; - - function maybeCall() { - clearTimeout(timeout); - - timeout = setTimeout(() => { - if (Date.now() - lastInvocation < miliseconds) { - maybeCall(); - return; - } - - fn(); - lastInvocation = Date.now(); - }, miliseconds); - } - - return maybeCall; -} - -/** - * Like str.lastIndexOf, but for regular expressions. Note that you need to provide the g-flag to your RegExp! - */ -export function regexLastIndexOf(text: string, regex: RegExp, endPos?: number) { - if (endPos === undefined) { - endPos = text.length; - } else if (endPos < 0) { - endPos = 0; - } - - const stringToWorkWith = text.substring(0, endPos + 1); - let lastIndexOf = -1; - let result: RegExpExecArray | null = null; - while ((result = regex.exec(stringToWorkWith)) !== null) { - lastIndexOf = result.index; - } - return lastIndexOf; -} - -/** - * Like str.indexOf, but for regular expressions. - */ -export function regexIndexOf(text: string, regex: RegExp, startPos?: number) { - if (startPos === undefined || startPos < 0) { - startPos = 0; - } - - const stringToWorkWith = text.substring(startPos); - const result: RegExpExecArray | null = regex.exec(stringToWorkWith); - return result?.index ?? -1; -} - -/** - * Get all matches of a regexp. - */ -export function getRegExpMatches(regex: RegExp, str: string) { - const matches: RegExpExecArray[] = []; - let match: RegExpExecArray | null; - while ((match = regex.exec(str))) { - matches.push(match); - } - return matches; -} - -/** - * Function to modify each line of a text, preserving the line break style (`\n` or `\r\n`) - */ -export function modifyLines( - text: string, - replacementFn: (line: string, lineIdx: number) => string -): string { - let idx = 0; - return text - .split('\r\n') - .map((l1) => - l1 - .split('\n') - .map((line) => replacementFn(line, idx++)) - .join('\n') - ) - .join('\r\n'); -} - -/** - * Like array.filter, but asynchronous - */ -export async function filterAsync( - array: T[], - predicate: (t: T, idx: number) => Promise -): Promise { - const fail = Symbol(); - return ( - await Promise.all( - array.map(async (item, idx) => ((await predicate(item, idx)) ? item : fail)) - ) - ).filter((i) => i !== fail) as T[]; -} - -export function getIndent(text: string) { - return /^[ |\t]+/.exec(text)?.[0] ?? ''; -} - -/** - * - * The html language service is case insensitive, and would provide - * hover/ completion info for Svelte components like `Option` which have - * the same name like a html tag. - * - * Also, svelte directives like action and event modifier only work - * with element not component - */ -export function possiblyComponent(node: Node): boolean; -export function possiblyComponent(tagName: string): boolean; -export function possiblyComponent(nodeOrTagName: Node | string): boolean { - return !!(typeof nodeOrTagName === 'object' ? nodeOrTagName.tag : nodeOrTagName)?.[0].match( - /[A-Z]/ - ); -} - -/** - * If the object if it has entries, else undefined - */ -export function returnObjectIfHasKeys(obj: T | undefined): T | undefined { - if (Object.keys(obj || {}).length > 0) { - return obj; - } -} - -const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g; - -/** - * adopted from https://github.com/microsoft/TypeScript/blob/8192d550496d884263e292488e325ae96893dc78/src/compiler/core.ts#L1769-L1807 - * see the comment there about why we can't just use String.prototype.toLowerCase() here - */ -export function toFileNameLowerCase(x: string) { - return fileNameLowerCaseRegExp.test(x) ? x.replace(fileNameLowerCaseRegExp, toLowerCase) : x; -} - -function toLowerCase(x: string) { - return x.toLowerCase(); -} - -export type GetCanonicalFileName = (fileName: string) => string; -/** - * adopted from https://github.com/microsoft/TypeScript/blob/8192d550496d884263e292488e325ae96893dc78/src/compiler/core.ts#L2312 - */ -export function createGetCanonicalFileName( - useCaseSensitiveFileNames: boolean -): GetCanonicalFileName { - return useCaseSensitiveFileNames ? identity : toFileNameLowerCase; -} - -function identity(x: T) { - return x; -} - -export function memoize(callback: () => T): () => T { - let value: T; - let callbackInner: typeof callback | undefined = callback; - - return () => { - if (callbackInner) { - value = callback(); - callbackInner = undefined; - } - return value; - }; -} - -export function removeLineWithString(str: string, keyword: string) { - const lines = str.split('\n'); - const filteredLines = lines.filter((line) => !line.includes(keyword)); - return filteredLines.join('\n'); -} diff --git a/packages/language-server/tsconfig.json b/packages/language-server/tsconfig.json index d2feb8d20..bd374ccc4 100644 --- a/packages/language-server/tsconfig.json +++ b/packages/language-server/tsconfig.json @@ -1,17 +1,10 @@ { - "compilerOptions": { - "lib": ["es2021"], - "target": "es2021", - "moduleResolution": "node", - "module": "CommonJS", - - "outDir": "dist", - "strict": true, - "declaration": true, - "esModuleInterop": true, - "sourceMap": true, - "composite": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - } -} + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "out", + "rootDir": "src", + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/language-server/wallaby.js b/packages/language-server/wallaby.js deleted file mode 100644 index afe061c53..000000000 --- a/packages/language-server/wallaby.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = function (_w) { - return { - files: ['src/**/*.ts'], - tests: ['test/**/*.ts'], - env: { - type: 'node' - } - }; -}; diff --git a/packages/svelte-check/.gitignore b/packages/svelte-check/.gitignore deleted file mode 100644 index 5dbbc7fbb..000000000 --- a/packages/svelte-check/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist/ -.vscode/ -node_modules/ diff --git a/packages/svelte-check/bin/svelte-check b/packages/svelte-check/bin/svelte-check deleted file mode 100755 index 2d3922d75..000000000 --- a/packages/svelte-check/bin/svelte-check +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -require('../dist/src/index.js'); diff --git a/packages/svelte-check/bin/svelte-check.js b/packages/svelte-check/bin/svelte-check.js new file mode 100644 index 000000000..b76fecbae --- /dev/null +++ b/packages/svelte-check/bin/svelte-check.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../out/bin.js'); diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json index ac5594b8b..d5b98cc5a 100644 --- a/packages/svelte-check/package.json +++ b/packages/svelte-check/package.json @@ -1,8 +1,9 @@ { "name": "svelte-check", "description": "Svelte Code Checker Terminal Interface", - "version": "3.0.0", - "main": "./dist/src/index.js", + "version": "0.0.0", + "type": "module", + "main": "./out/index.js", "bin": "./bin/svelte-check", "author": "The Svelte Community", "license": "MIT", @@ -16,21 +17,23 @@ ], "files": [ "bin", - "dist" + "out" ], "bugs": { "url": "https://github.com/sveltejs/language-tools/issues" }, "homepage": "https://github.com/sveltejs/language-tools#readme", + "devDependencies": { + "@types/yargs": "^17.0.32" + }, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@volar/kit": "2.0.0-alpha.13", + "volar-service-typescript": "0.0.25", + "svelte-language-server": "0.0.0", "chokidar": "^3.4.1", - "fast-glob": "^3.2.7", - "import-fresh": "^3.2.1", - "picocolors": "^1.0.0", - "sade": "^1.7.4", - "svelte-preprocess": "^5.1.0", - "typescript": "^5.0.3" + "kleur": "^4.1.5", + "yargs": "^17.7.2", + "fast-glob": "^3.3.2" }, "peerDependencies": { "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" @@ -39,22 +42,5 @@ "build": "rollup -c && node ./dist/src/index.js --workspace ./test --tsconfig ./tsconfig.json", "prepublishOnly": "npm run build", "test": "npm run build" - }, - "devDependencies": { - "@rollup/plugin-commonjs": "^24.0.0", - "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^15.0.0", - "@rollup/plugin-replace": "5.0.2", - "@rollup/plugin-typescript": "^10.0.0", - "@types/sade": "^1.7.2", - "builtin-modules": "^3.3.0", - "rollup": "3.7.5", - "rollup-plugin-cleanup": "^3.2.0", - "rollup-plugin-copy": "^3.4.0", - "svelte-language-server": "workspace:*", - "vscode-languageserver": "8.0.2", - "vscode-languageserver-protocol": "3.17.2", - "vscode-languageserver-types": "3.17.2", - "vscode-uri": "~3.0.0" } -} +} \ No newline at end of file diff --git a/packages/svelte-check/rollup.config.mjs b/packages/svelte-check/rollup.config.mjs deleted file mode 100644 index 1f5194f6e..000000000 --- a/packages/svelte-check/rollup.config.mjs +++ /dev/null @@ -1,73 +0,0 @@ -import typescript from '@rollup/plugin-typescript'; -import commonjs from '@rollup/plugin-commonjs'; -import resolve from '@rollup/plugin-node-resolve'; -import json from '@rollup/plugin-json'; -import replace from '@rollup/plugin-replace'; -import cleanup from 'rollup-plugin-cleanup'; -import copy from 'rollup-plugin-copy'; -import builtins from 'builtin-modules'; - -export default [ - { - input: 'src/index.ts', - output: [ - { - sourcemap: false, - format: 'cjs', - file: 'dist/src/index.js' - } - ], - plugins: [ - replace({ - // This replace-step is a hacky workaround to not transform the dynamic - // requires inside importPackage.ts of svelte-language-server in any way - 'return require(dynamicFileToRequire);': 'return "XXXXXXXXXXXXXXXXXXXXX";', - delimiters: ['', ''] - }), - resolve({ browser: false, preferBuiltins: true }), - commonjs(), - json(), - typescript(), - replace({ - // This replace-step is a hacky workaround to not transform the dynamic - // requires inside importPackage.ts of svelte-language-server in any way - 'return "XXXXXXXXXXXXXXXXXXXXX";': 'return require(dynamicFileToRequire);', - delimiters: ['', ''] - }), - cleanup({ comments: ['some', 'ts', 'ts3s'] }), - copy({ - targets: [ - // copy over d.ts files of svelte2tsx - { - src: [ - // workspace - '../svelte2tsx/svelte*.d.ts', - // standalone - 'node_modules/svelte2tsx/svelte*.d.ts' - ], - dest: 'dist/src' - } - ] - }) - ], - watch: { - clearScreen: false - }, - external: [ - ...builtins, - // svelte-check dependencies that are system-dependent and should - // be installed as dependencies through npm - 'picocolors', - 'chokidar', - // Dependencies of svelte-language-server - // we don't want to bundle and instead require them as dependencies - 'typescript', - 'sade', - 'svelte', - 'svelte/compiler', - 'svelte-preprocess', - 'import-fresh', // because of https://github.com/sindresorhus/import-fresh/issues/18 - '@jridgewell/trace-mapping' - ] - } -]; diff --git a/packages/svelte-check/src/bin.ts b/packages/svelte-check/src/bin.ts new file mode 100644 index 000000000..a809fb716 --- /dev/null +++ b/packages/svelte-check/src/bin.ts @@ -0,0 +1,12 @@ +import * as path from 'node:path'; +import { check, parseArgsAsCheckConfig } from './index.js'; + +const args = parseArgsAsCheckConfig(process.argv); + +console.info(`Getting diagnostics for Svelte files in ${path.resolve(args.root)}...`); + +const result = await check(args); + +if (typeof result === 'boolean') { + process.exit(result ? 1 : 0); +} diff --git a/packages/svelte-check/src/check.ts b/packages/svelte-check/src/check.ts new file mode 100644 index 000000000..5102d2b2d --- /dev/null +++ b/packages/svelte-check/src/check.ts @@ -0,0 +1,160 @@ +import * as kit from '@volar/kit'; +import { Diagnostic, DiagnosticSeverity } from '@volar/kit'; +import * as fg from 'fast-glob'; +import { existsSync } from 'node:fs'; +import { pathToFileURL } from 'node:url'; +import { svelteLanguagePlugin } from 'svelte-language-server/out/languagePlugin.js'; +import { create as createTypeScriptServicePlugin } from 'volar-service-typescript'; + +// Export those for downstream consumers +export { Diagnostic, DiagnosticSeverity }; + +export interface CheckResult { + status: 'completed' | 'cancelled' | undefined; + fileChecked: number; + errors: number; + warnings: number; + hints: number; + fileResult: { + errors: kit.Diagnostic[]; + fileUrl: URL; + fileContent: string; + text: string; + }[]; +} + +export class SvelteCheck { + private ts!: typeof import('typescript/lib/tsserverlibrary.js'); + public linter!: ReturnType<(typeof kit)['createTypeScriptChecker']>; + + constructor( + private readonly workspacePath: string, + private readonly typescriptPath: string | undefined, + private readonly tsconfigPath: string | undefined + ) { + this.initialize(); + } + + /** + * Lint a list of files or the entire project and optionally log the errors found + * @param fileNames List of files to lint, if undefined, all files included in the project will be linted + * @param logErrors Whether to log errors by itself. This is disabled by default. + * @return {CheckResult} The result of the lint, including a list of errors, the file's content and its file path. + */ + public async lint({ + fileNames = undefined, + cancel = () => false, + logErrors = undefined, + }: { + fileNames?: string[] | undefined; + cancel?: () => boolean; + logErrors?: + | { + level: 'error' | 'warning' | 'hint'; + } + | undefined; + }): Promise { + const files = + fileNames !== undefined ? fileNames : this.linter.languageHost.getScriptFileNames(); + + const result: CheckResult = { + status: undefined, + fileChecked: 0, + errors: 0, + warnings: 0, + hints: 0, + fileResult: [], + }; + for (const file of files) { + if (cancel()) { + result.status = 'cancelled'; + return result; + } + const fileDiagnostics = await this.linter.check(file); + + // Filter diagnostics based on the logErrors level + const fileDiagnosticsToPrint = fileDiagnostics.filter((diag) => { + const severity = diag.severity ?? 1 satisfies typeof DiagnosticSeverity.Error; + switch (logErrors?.level ?? 'hint') { + case 'error': + return severity <= (1 satisfies typeof DiagnosticSeverity.Error); + case 'warning': + return severity <= (2 satisfies typeof DiagnosticSeverity.Warning); + case 'hint': + return severity <= (4 satisfies typeof DiagnosticSeverity.Hint); + } + }); + + if (fileDiagnostics.length > 0) { + const errorText = this.linter.printErrors(file, fileDiagnosticsToPrint); + + if (logErrors !== undefined && errorText) { + console.info(errorText); + } + + const fileSnapshot = this.linter.languageHost.getScriptSnapshot(file); + const fileContent = fileSnapshot?.getText(0, fileSnapshot.getLength()); + + result.fileResult.push({ + errors: fileDiagnostics, + fileContent: fileContent ?? '', + fileUrl: pathToFileURL(file), + text: errorText, + }); + + result.errors += fileDiagnostics.filter( + (diag) => diag.severity === (1 satisfies typeof DiagnosticSeverity.Error) + ).length; + result.warnings += fileDiagnostics.filter( + (diag) => diag.severity === (2 satisfies typeof DiagnosticSeverity.Warning) + ).length; + result.hints += fileDiagnostics.filter( + (diag) => diag.severity === (4 satisfies typeof DiagnosticSeverity.Hint) + ).length; + } + + result.fileChecked += 1; + } + + result.status = 'completed'; + return result; + } + + private initialize() { + this.ts = this.typescriptPath ? require(this.typescriptPath) : require('typescript'); + const tsconfigPath = this.getTsconfig(); + + const languages = [svelteLanguagePlugin]; + const services = [createTypeScriptServicePlugin(this.ts)]; + + if (tsconfigPath) { + this.linter = kit.createTypeScriptChecker(languages, services, tsconfigPath); + } else { + this.linter = kit.createTypeScriptInferredChecker(languages, services, () => { + return fg.sync('**/*.svelte', { + cwd: this.workspacePath, + ignore: ['node_modules'], + absolute: true, + }); + }); + } + } + + private getTsconfig() { + if (this.tsconfigPath) { + if (!existsSync(this.tsconfigPath)) { + throw new Error(`Specified tsconfig file \`${this.tsconfigPath}\` does not exist.`); + } + + return this.tsconfigPath; + } + + const searchPath = this.workspacePath; + + const tsconfig = + this.ts.findConfigFile(searchPath, this.ts.sys.fileExists) || + this.ts.findConfigFile(searchPath, this.ts.sys.fileExists, 'jsconfig.json'); + + return tsconfig; + } +} diff --git a/packages/svelte-check/src/index.ts b/packages/svelte-check/src/index.ts index 6e64414ed..de5825d7c 100644 --- a/packages/svelte-check/src/index.ts +++ b/packages/svelte-check/src/index.ts @@ -1,225 +1,119 @@ -/** - * This code's groundwork is taken from https://github.com/vuejs/vetur/tree/master/vti - */ - import { watch } from 'chokidar'; -import * as fs from 'fs'; -import glob from 'fast-glob'; -import * as path from 'path'; -import { SvelteCheck, SvelteCheckOptions } from 'svelte-language-server'; -import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-protocol'; -import { URI } from 'vscode-uri'; -import { parseOptions, SvelteCheckCliOptions } from './options'; -import { - DEFAULT_FILTER, - DiagnosticFilter, - HumanFriendlyWriter, - MachineFriendlyWriter, - Writer -} from './writers'; - -type Result = { - fileCount: number; - errorCount: number; - warningCount: number; - fileCountWithProblems: number; -}; - -async function openAllDocuments( - workspaceUri: URI, - filePathsToIgnore: string[], - svelteCheck: SvelteCheck -) { - const files = await glob('**/*.svelte', { - cwd: workspaceUri.fsPath, - ignore: ['node_modules/**'].concat(filePathsToIgnore.map((ignore) => `${ignore}/**`)) - }); - const absFilePaths = files.map((f) => path.resolve(workspaceUri.fsPath, f)); - - for (const absFilePath of absFilePaths) { - const text = fs.readFileSync(absFilePath, 'utf-8'); - svelteCheck.upsertDocument( - { - uri: URI.file(absFilePath).toString(), - text - }, - true - ); - } -} - -async function getDiagnostics( - workspaceUri: URI, - writer: Writer, - svelteCheck: SvelteCheck -): Promise { - writer.start(workspaceUri.fsPath); - - try { - const diagnostics = await svelteCheck.getDiagnostics(); - - const result: Result = { - fileCount: diagnostics.length, - errorCount: 0, - warningCount: 0, - fileCountWithProblems: 0 - }; - - for (const diagnostic of diagnostics) { - writer.file( - diagnostic.diagnostics, - workspaceUri.fsPath, - path.relative(workspaceUri.fsPath, diagnostic.filePath), - diagnostic.text - ); - - let fileHasProblems = false; - - diagnostic.diagnostics.forEach((d: Diagnostic) => { - if (d.severity === DiagnosticSeverity.Error) { - result.errorCount += 1; - fileHasProblems = true; - } else if (d.severity === DiagnosticSeverity.Warning) { - result.warningCount += 1; - fileHasProblems = true; - } - }); - - if (fileHasProblems) { - result.fileCountWithProblems += 1; - } - } - - writer.completion( - result.fileCount, - result.errorCount, - result.warningCount, - result.fileCountWithProblems - ); - return result; - } catch (err: any) { - writer.failure(err); - return null; - } -} +import { bold, dim, red, yellow } from 'kleur/colors'; +import { createRequire } from 'node:module'; +import * as path from 'node:path'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { options } from './options.js'; +import { SvelteCheck } from './check.js'; -class DiagnosticsWatcher { - private updateDiagnostics: any; - - constructor( - private workspaceUri: URI, - private svelteCheck: SvelteCheck, - private writer: Writer, - filePathsToIgnore: string[], - ignoreInitialAdd: boolean - ) { - watch(`${workspaceUri.fsPath}/**/*.{svelte,d.ts,ts,js,jsx,tsx,mjs,cjs,mts,cts}`, { - ignored: ['node_modules', 'vite.config.{js,ts}.timestamp-*'] - .concat(filePathsToIgnore) - .map((ignore) => path.join(workspaceUri.fsPath, ignore)), - ignoreInitial: ignoreInitialAdd - }) - .on('add', (path) => this.updateDocument(path, true)) - .on('unlink', (path) => this.removeDocument(path)) - .on('change', (path) => this.updateDocument(path, false)); - - if (ignoreInitialAdd) { - this.scheduleDiagnostics(); - } - } - - private async updateDocument(path: string, isNew: boolean) { - const text = fs.readFileSync(path, 'utf-8'); - await this.svelteCheck.upsertDocument({ text, uri: URI.file(path).toString() }, isNew); - this.scheduleDiagnostics(); - } - - private async removeDocument(path: string) { - await this.svelteCheck.removeDocument(URI.file(path).toString()); - this.scheduleDiagnostics(); - } - - scheduleDiagnostics() { - clearTimeout(this.updateDiagnostics); - this.updateDiagnostics = setTimeout( - () => getDiagnostics(this.workspaceUri, this.writer, this.svelteCheck), - 1000 - ); - } -} - -function createFilter(opts: SvelteCheckCliOptions): DiagnosticFilter { - switch (opts.threshold) { - case 'error': - return (d) => d.severity === DiagnosticSeverity.Error; - case 'warning': - return (d) => - d.severity === DiagnosticSeverity.Error || - d.severity === DiagnosticSeverity.Warning; - default: - return DEFAULT_FILTER; - } +/** + * Given a list of arguments from the command line (such as `process.argv`), return parsed and processed options + */ +export function parseArgsAsCheckConfig(args: string[]) { + return yargs(hideBin(args)).options(options).parseSync(); } -function instantiateWriter(opts: SvelteCheckCliOptions): Writer { - const filter = createFilter(opts); +export type Flags = Pick, keyof typeof options>; - if (opts.outputFormat === 'human-verbose' || opts.outputFormat === 'human') { - return new HumanFriendlyWriter( - process.stdout, - opts.outputFormat === 'human-verbose', - opts.watch, - !opts.preserveWatchOutput, - filter - ); - } else { - return new MachineFriendlyWriter( - process.stdout, - opts.outputFormat === 'machine-verbose', - filter - ); - } +export async function check(flags: Partial & { watch: true }): Promise; +export async function check(flags: Partial & { watch: false }): Promise; +export async function check(flags: Partial): Promise; +/** + * Print diagnostics according to the given flags, and return whether or not the program should exit with an error code. + */ +export async function check(flags: Partial): Promise { + const workspaceRoot = path.resolve(flags.root ?? process.cwd()); + const require = createRequire(import.meta.url); + const checker = new SvelteCheck( + workspaceRoot, + require.resolve('typescript/lib/tsserverlibrary.js'), + flags.tsconfig + ); + + let req = 0; + + if (flags.watch) { + function createWatcher(rootPath: string, extensions: string[]) { + return watch(`${rootPath}/**/*{${extensions.join(',')}}`, { + ignored: (ignoredPath) => ignoredPath.includes('node_modules'), + ignoreInitial: true, + }); + } + + // Dynamically get the list of extensions to watch from the files already included in the project + const checkedExtensions = Array.from( + new Set( + checker.linter.languageHost.getScriptFileNames().map((fileName) => path.extname(fileName)) + ) + ); + createWatcher(workspaceRoot, checkedExtensions) + .on('add', (fileName) => { + checker.linter.fileCreated(fileName); + update(); + }) + .on('unlink', (fileName) => { + checker.linter.fileDeleted(fileName); + update(); + }) + .on('change', (fileName) => { + checker.linter.fileUpdated(fileName); + update(); + }); + } + + async function update() { + if (!flags.preserveWatchOutput) process.stdout.write('\x1Bc'); + await lint(); + } + + async function lint() { + const currentReq = ++req; + await new Promise((resolve) => setTimeout(resolve, 100)); + const isCanceled = () => currentReq !== req; + if (isCanceled()) return; + + const minimumSeverity = flags.minimumSeverity || 'hint'; + const result = await checker.lint({ + logErrors: { + level: minimumSeverity, + }, + cancel: isCanceled, + }); + console.info( + [ + bold(`Result (${result.fileChecked} file${result.fileChecked === 1 ? '' : 's'}): `), + ['error', 'warning', 'hint'].includes(minimumSeverity) + ? bold(red(`${result.errors} ${result.errors === 1 ? 'error' : 'errors'}`)) + : undefined, + ['warning', 'hint'].includes(minimumSeverity) + ? bold(yellow(`${result.warnings} ${result.warnings === 1 ? 'warning' : 'warnings'}`)) + : undefined, + ['hint'].includes(minimumSeverity) + ? dim(`${result.hints} ${result.hints === 1 ? 'hint' : 'hints'}\n`) + : undefined, + ] + .filter(Boolean) + .join(`\n${dim('-')} `) + ); + + if (flags.watch) { + console.info('Watching for changes...'); + } else { + switch (flags.minimumFailingSeverity) { + case 'error': + return result.errors > 0; + case 'warning': + return result.errors + result.warnings > 0; + case 'hint': + return result.errors + result.warnings + result.hints > 0; + default: + return result.errors > 0; + } + } + } + + // Always lint on first run, even in watch mode. + const lintResult = await lint(); + if (!flags.watch) return lintResult; } - -parseOptions(async (opts) => { - try { - const writer = instantiateWriter(opts); - - const svelteCheckOptions: SvelteCheckOptions = { - compilerWarnings: opts.compilerWarnings, - diagnosticSources: opts.diagnosticSources, - tsconfig: opts.tsconfig, - watch: opts.watch - }; - - if (opts.watch) { - svelteCheckOptions.onProjectReload = () => watcher.scheduleDiagnostics(); - const watcher = new DiagnosticsWatcher( - opts.workspaceUri, - new SvelteCheck(opts.workspaceUri.fsPath, svelteCheckOptions), - writer, - opts.filePathsToIgnore, - !!opts.tsconfig - ); - } else { - const svelteCheck = new SvelteCheck(opts.workspaceUri.fsPath, svelteCheckOptions); - - if (!opts.tsconfig) { - await openAllDocuments(opts.workspaceUri, opts.filePathsToIgnore, svelteCheck); - } - const result = await getDiagnostics(opts.workspaceUri, writer, svelteCheck); - if ( - result && - result.errorCount === 0 && - (!opts.failOnWarnings || result.warningCount === 0) - ) { - process.exit(0); - } else { - process.exit(1); - } - } - } catch (_err) { - console.error(_err); - console.error('svelte-check failed'); - } -}); diff --git a/packages/svelte-check/src/options.ts b/packages/svelte-check/src/options.ts index bf270e3c4..81c8e591e 100644 --- a/packages/svelte-check/src/options.ts +++ b/packages/svelte-check/src/options.ts @@ -1,197 +1,32 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import sade from 'sade'; -import { URI } from 'vscode-uri'; - -export interface SvelteCheckCliOptions { - workspaceUri: URI; - outputFormat: OutputFormat; - watch: boolean; - preserveWatchOutput: boolean; - tsconfig?: string; - filePathsToIgnore: string[]; - failOnWarnings: boolean; - compilerWarnings: Record; - diagnosticSources: DiagnosticSource[]; - threshold: Threshold; -} - -export function parseOptions(cb: (opts: SvelteCheckCliOptions) => any) { - const prog = sade('svelte-check', true) - .version(require('../../package.json').version) // ends up in dist/src, that's why we go two levels up - .option( - '--workspace', - 'Path to your workspace. All subdirectories except node_modules and those listed in `--ignore` are checked. Defaults to current working directory.' - ) - .option( - '--output', - 'What output format to use. Options are human, human-verbose, machine, machine-verbose.', - 'human-verbose' - ) - .option( - '--watch', - 'Will not exit after one pass but keep watching files for changes and rerun diagnostics', - false - ) - .option('--preserveWatchOutput', 'Do not clear the screen in watch mode', false) - .option( - '--tsconfig', - 'Pass a path to a tsconfig or jsconfig file. The path can be relative to the workspace path or absolute. Doing this means that only files matched by the files/include/exclude pattern of the config file are diagnosed. It also means that errors from TypeScript and JavaScript files are reported. When not given, searches for the next upper tsconfig/jsconfig in the workspace path.' - ) - .option( - '--no-tsconfig', - 'Use this if you only want to check the Svelte files found in the current directory and below and ignore any JS/TS files (they will not be type-checked)', - false - ) - .option( - '--ignore', - 'Only has an effect when using `--no-tsconfig` option. Files/folders to ignore - relative to workspace root, comma-separated, inside quotes. Example: `--ignore "dist,build"`' - ) - .option( - '--fail-on-warnings', - 'Will also exit with error code when there are warnings', - false - ) - .option( - '--compiler-warnings', - 'A list of Svelte compiler warning codes. Each entry defines whether that warning should be ignored or treated as an error. Warnings are comma-separated, between warning code and error level is a colon; all inside quotes. Example: `--compiler-warnings "css-unused-selector:ignore,unused-export-let:error"`' - ) - .option( - '--diagnostic-sources', - 'A list of diagnostic sources which should run diagnostics on your code. Possible values are `js` (includes TS), `svelte`, `css`. Comma-separated, inside quotes. By default all are active. Example: `--diagnostic-sources "js,svelte"`' - ) - .option( - '--threshold', - 'Filters the diagnostics to display. `error` will output only errors while `warning` will output warnings and errors.', - 'warning' - ) - .action((opts) => { - const workspaceUri = getWorkspaceUri(opts); - cb({ - workspaceUri, - outputFormat: getOutputFormat(opts), - watch: !!opts.watch, - preserveWatchOutput: !!opts.preserveWatchOutput, - tsconfig: getTsconfig(opts, workspaceUri.fsPath), - filePathsToIgnore: getFilepathsToIgnore(opts), - failOnWarnings: !!opts['fail-on-warnings'], - compilerWarnings: getCompilerWarnings(opts), - diagnosticSources: getDiagnosticSources(opts), - threshold: getThreshold(opts) - }); - }); - - prog.parse(process.argv, { - unknown: (arg) => `Unknown option ${arg}` - }); -} - -const outputFormats = ['human', 'human-verbose', 'machine', 'machine-verbose'] as const; -type OutputFormat = (typeof outputFormats)[number]; - -function getOutputFormat(opts: Record): OutputFormat { - return outputFormats.includes(opts.output) ? opts.output : 'human-verbose'; -} - -function getWorkspaceUri(opts: Record) { - let workspaceUri; - let workspacePath = opts.workspace; - if (workspacePath) { - if (!path.isAbsolute(workspacePath)) { - workspacePath = path.resolve(process.cwd(), workspacePath); - } - workspaceUri = URI.file(workspacePath); - } else { - workspaceUri = URI.file(process.cwd()); - } - return workspaceUri; -} - -function findFile(searchPath: string, fileName: string) { - try { - for (;;) { - const filePath = path.join(searchPath, fileName); - if (fs.existsSync(filePath)) { - return filePath; - } - const parentPath = path.dirname(searchPath); - if (parentPath === searchPath) { - return; - } - searchPath = parentPath; - } - } catch (e) { - return; - } -} - -function getTsconfig(myArgs: Record, workspacePath: string) { - // Work around undocumented behavior in Sade where `no-tsconfig` is never true / means "tsconfig is false" - if (myArgs['no-tsconfig'] || process.argv.includes('--no-tsconfig')) { - return undefined; - } - let tsconfig: string | undefined = - typeof myArgs.tsconfig === 'string' ? myArgs.tsconfig : undefined; - if (!tsconfig) { - const ts = findFile(workspacePath, 'tsconfig.json'); - const js = findFile(workspacePath, 'jsconfig.json'); - tsconfig = !!ts && (!js || ts.length >= js.length) ? ts : js; - } - if (tsconfig && !path.isAbsolute(tsconfig)) { - tsconfig = path.join(workspacePath, tsconfig); - } - if (tsconfig && !fs.existsSync(tsconfig)) { - throw new Error('Could not find tsconfig/jsconfig file at ' + myArgs.tsconfig); - } - return tsconfig; -} - -function getCompilerWarnings(opts: Record) { - return stringToObj(opts['compiler-warnings']); - - function stringToObj(str = '') { - return str - .split(',') - .map((s) => s.trim()) - .filter((s) => !!s) - .reduce( - (settings, setting) => { - const [name, val] = setting.split(':'); - if (val === 'error' || val === 'ignore') { - settings[name] = val; - } - return settings; - }, - >{} - ); - } -} - -const diagnosticSources = ['js', 'css', 'svelte'] as const; -type DiagnosticSource = (typeof diagnosticSources)[number]; - -function getDiagnosticSources(opts: Record): DiagnosticSource[] { - const sources = opts['diagnostic-sources']; - return sources - ? sources - .split(',') - ?.map((s: string) => s.trim()) - .filter((s: any) => diagnosticSources.includes(s)) - : diagnosticSources; -} - -function getFilepathsToIgnore(opts: Record): string[] { - return opts.ignore?.split(',') || []; -} - -const thresholds = ['warning', 'error'] as const; -type Threshold = (typeof thresholds)[number]; - -function getThreshold(opts: Record): Threshold { - if (thresholds.includes(opts.threshold)) { - return opts.threshold; - } else { - console.warn(`Invalid threshold "${opts.threshold}", using "warning" instead`); - return 'warning'; - } -} +export const options = { + root: { + type: 'string', + default: process.cwd(), + description: + 'Manually specify a root dir to check in. By default, the current working directory is used.', + }, + watch: { type: 'boolean', default: false, alias: 'w' }, + tsconfig: { + type: 'string', + description: + "Manually specify a path to a `tsconfig.json` or `jsconfig.json` to use. If not specified, the program will attempt to find a config, if it cannot it'll attempt to automatically infer the project's configuration.", + default: undefined, + }, + minimumFailingSeverity: { + choices: ['error', 'warning', 'hint'] as const, + description: + "Minimum error severity needed to exit with an error code. Choosing 'hint' will for example cause the program to exit with an error if there's any unfixed hints.", + default: 'error', + }, + minimumSeverity: { + choices: ['error', 'warning', 'hint'] as const, + description: + 'Minimum diagnostic severity to show. Choosing `warning` will, for example, show both errors and warnings, but not hints. ', + default: 'hint', + }, + preserveWatchOutput: { + type: 'boolean', + description: "If set to false, output won't be cleared between checks in watch mode.", + default: false, + }, +} as const; diff --git a/packages/svelte-check/src/writers.ts b/packages/svelte-check/src/writers.ts deleted file mode 100644 index 37c6823b8..000000000 --- a/packages/svelte-check/src/writers.ts +++ /dev/null @@ -1,209 +0,0 @@ -import pc from 'picocolors'; -import { sep } from 'path'; -import { Writable } from 'stream'; -import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-protocol'; -import { offsetAt } from 'svelte-language-server'; - -export interface Writer { - start: (workspaceDir: string) => void; - file: (d: Diagnostic[], workspaceDir: string, filename: string, text: string) => void; - completion: ( - fileCount: number, - errorCount: number, - warningCount: number, - fileCountWithProblems: number - ) => void; - failure: (err: Error) => void; -} - -export type DiagnosticFilter = (diagnostic: Diagnostic) => boolean; -export const DEFAULT_FILTER: DiagnosticFilter = () => true; - -export class HumanFriendlyWriter implements Writer { - constructor( - private stream: Writable, - private isVerbose = true, - private isWatchMode = false, - private clearScreen = true, - private diagnosticFilter: DiagnosticFilter = DEFAULT_FILTER - ) {} - - start(workspaceDir: string) { - if (process.stdout.isTTY && this.isWatchMode && this.clearScreen) { - // Clear screen - const blank = '\n'.repeat(process.stdout.rows); - this.stream.write(blank); - process.stdout.cursorTo(0, 0); - process.stdout.clearScreenDown(); - } - - if (this.isVerbose) { - this.stream.write('\n'); - this.stream.write('====================================\n'); - this.stream.write(`Loading svelte-check in workspace: ${workspaceDir}`); - this.stream.write('\n'); - this.stream.write('Getting Svelte diagnostics...\n'); - this.stream.write('\n'); - } - } - - file(diagnostics: Diagnostic[], workspaceDir: string, filename: string, text: string): void { - diagnostics.filter(this.diagnosticFilter).forEach((diagnostic) => { - const source = diagnostic.source ? `(${diagnostic.source})` : ''; - - // Display location in a format that IDEs will turn into file links - const { line, character } = diagnostic.range.start; - this.stream.write( - `${workspaceDir}${sep}${pc.green(filename)}:${line + 1}:${character + 1}\n` - ); - - // Show some context around diagnostic range - const codePrevLine = this.getLine(diagnostic.range.start.line - 1, text); - const codeLine = this.getCodeLine(diagnostic, text); - const codeNextLine = this.getLine(diagnostic.range.end.line + 1, text); - const code = codePrevLine + codeLine + codeNextLine; - - let msg; - if (this.isVerbose) { - msg = `${diagnostic.message} ${source}\n${pc.cyan(code)}`; - } else { - msg = `${diagnostic.message} ${source}`; - } - - if (diagnostic.severity === DiagnosticSeverity.Error) { - this.stream.write(`${pc.red('Error')}: ${msg}\n`); - } else if (diagnostic.severity === DiagnosticSeverity.Warning) { - this.stream.write(`${pc.yellow('Warn')}: ${msg}\n`); - } - - this.stream.write('\n'); - }); - } - - private getCodeLine(diagnostic: Diagnostic, text: string) { - const startOffset = offsetAt(diagnostic.range.start, text); - const endOffset = offsetAt(diagnostic.range.end, text); - const codePrev = text.substring( - offsetAt({ line: diagnostic.range.start.line, character: 0 }, text), - startOffset - ); - const codeHighlight = pc.magenta(text.substring(startOffset, endOffset)); - const codePost = text.substring( - endOffset, - offsetAt({ line: diagnostic.range.end.line, character: Number.MAX_SAFE_INTEGER }, text) - ); - return codePrev + codeHighlight + codePost; - } - - private getLine(line: number, text: string): string { - return text.substring( - offsetAt({ line, character: 0 }, text), - offsetAt({ line, character: Number.MAX_SAFE_INTEGER }, text) - ); - } - - completion( - _f: number, - errorCount: number, - warningCount: number, - fileCountWithProblems: number - ) { - this.stream.write('====================================\n'); - const message = [ - 'svelte-check found ', - `${errorCount} ${errorCount === 1 ? 'error' : 'errors'} and `, - `${warningCount} ${warningCount === 1 ? 'warning' : 'warnings'}`, - `${ - fileCountWithProblems - ? // prettier-ignore - ` in ${fileCountWithProblems} ${fileCountWithProblems === 1 ? 'file' : 'files'}` - : '' - }\n` - ].join(''); - if (errorCount !== 0) { - this.stream.write(pc.red(message)); - } else if (warningCount !== 0) { - this.stream.write(pc.yellow(message)); - } else { - this.stream.write(pc.green(message)); - } - if (this.isWatchMode) { - this.stream.write('Watching for file changes...'); - } - } - - failure(err: Error) { - this.stream.write(`${err}\n`); - } -} - -export class MachineFriendlyWriter implements Writer { - constructor( - private stream: Writable, - private isVerbose = false, - private diagnosticFilter = DEFAULT_FILTER - ) {} - - private log(msg: string) { - this.stream.write(`${new Date().getTime()} ${msg}\n`); - } - - start(workspaceDir: string) { - this.log(`START ${JSON.stringify(workspaceDir)}`); - } - - file(diagnostics: Diagnostic[], workspaceDir: string, filename: string, _text: string) { - diagnostics.filter(this.diagnosticFilter).forEach((d) => { - const { message, severity, range, code, codeDescription, source } = d; - const type = - severity === DiagnosticSeverity.Error - ? 'ERROR' - : severity === DiagnosticSeverity.Warning - ? 'WARNING' - : null; - - if (type) { - const { start, end } = range; - if (this.isVerbose) { - this.log( - JSON.stringify({ - type, - filename, - start, - end, - message, - code, - codeDescription, - source - }) - ); - } else { - const fn = JSON.stringify(filename); - const msg = JSON.stringify(message); - this.log(`${type} ${fn} ${start.line + 1}:${start.character + 1} ${msg}`); - } - } - }); - } - - completion( - fileCount: number, - errorCount: number, - warningCount: number, - fileCountWithProblems: number - ) { - this.log( - [ - 'COMPLETED', - `${fileCount} FILES`, - `${errorCount} ERRORS`, - `${warningCount} WARNINGS`, - `${fileCountWithProblems} FILES_WITH_PROBLEMS` - ].join(' ') - ); - } - - failure(err: Error) { - this.log(`FAILURE ${JSON.stringify(err.message)}`); - } -} diff --git a/packages/svelte-check/tsconfig.json b/packages/svelte-check/tsconfig.json index 1c997fd3d..50307beda 100644 --- a/packages/svelte-check/tsconfig.json +++ b/packages/svelte-check/tsconfig.json @@ -1,14 +1,15 @@ { - "compilerOptions": { - "lib": ["es2021"], - "module": "ES2022", - "target": "es2021", - "moduleResolution": "bundler", - - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*"] -} + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "out", + "rootDir": "src", + }, + "include": [ + "src", + ], + "references": [ + { + "path": "../language-server/tsconfig.json" + }, + ] +} \ No newline at end of file diff --git a/packages/svelte-vscode/.gitignore b/packages/svelte-vscode/.gitignore deleted file mode 100644 index e6a74554d..000000000 --- a/packages/svelte-vscode/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/dist -/node_modules -/syntaxes/svelte.tmLanguage.json -/syntaxes/postcss.json diff --git a/packages/svelte-vscode/.vscode/launch.json b/packages/svelte-vscode/.vscode/launch.json deleted file mode 100644 index 7fda744b0..000000000 --- a/packages/svelte-vscode/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "0.2.0", - // List of configurations. Add new configurations or edit existing ones. - "configurations": [ - { - "name": "Launch Client", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}"], - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": ["${workspaceRoot}/dist/**/*.js"] - } - ] -} diff --git a/packages/svelte-vscode/README.md b/packages/svelte-vscode/README.md deleted file mode 100644 index 94c28dfe9..000000000 --- a/packages/svelte-vscode/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# Svelte for VS Code - -Provides syntax highlighting and rich intellisense for Svelte components in VS Code, using the [svelte language server](/packages/language-server). - -## Setup - -If you added `"files.associations": {"*.svelte": "html" }` to your VSCode settings, remove it. - -If you have previously installed the old "Svelte" extension by James Birtles, uninstall it: - -- Through the UI: You can find it when searching for `@installed` in the extensions window (searching `Svelte` won't work). -- Command line: `code --uninstall-extension JamesBirtles.svelte-vscode` - -This extension comes bundled with a formatter for Svelte files. To let this extension format Svelte files, adjust your VS Code settings: - -``` - "[svelte]": { - "editor.defaultFormatter": "svelte.svelte-vscode" - }, -``` - -The formatter is a [Prettier](https://prettier.io/) [plugin](https://prettier.io/docs/en/plugins.html), which means some formatting options of Prettier apply. There are also Svelte specific settings like the sort order of scripts, markup, styles. More info about them and how to configure it can be found [here](https://github.com/sveltejs/prettier-plugin-svelte). - -You need at least VSCode version `1.52.0`. - -Do you want to use TypeScript/SCSS/Less/..? [See the docs](/docs/README.md#language-specific-setup). - -More docs and troubleshooting: [See here](/docs/README.md). - -## Features - -You can expect the following within Svelte files: - -- Diagnostic messages -- Support for svelte preprocessors that provide source maps -- Formatting (via [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte)) -- A command to preview the compiled code (DOM mode): "Svelte: Show Compiled Code" -- A command to extract template content into a new component: "Svelte: Extract Component" -- Hover info -- Autocompletions -- [Emmet](https://emmet.io/) -- Symbols in outline panel -- CSS Color highlighting and color picker -- Go to definition -- Code Actions - -The extension also comes packaged with a TypeScript plugin, which when activated provides intellisense within JavaScript and TypeScript files for interacting with Svelte files. - -### Settings - -##### `svelte.enable-ts-plugin` - -Enables a TypeScript plugin which provides intellisense for Svelte files inside TS/JS files. _Default_: `false` - -##### `svelte.language-server.runtime` - -Path to the node executable you would like to use to run the language server. -This is useful when you depend on native modules such as node-sass as without this they will run in the context of vscode, meaning node version mismatch is likely. -Minimum required node version is `12.17`. -This setting can only be changed in user settings for security reasons. - -##### `svelte.language-server.ls-path` - -You normally don't set this. Path to the language server executable. If you installed the `svelte-language-server` npm package, it's within there at `bin/server.js`. Path can be either relative to your workspace root or absolute. Set this only if you want to use a custom version of the language server. This will then also use the workspace version of TypeScript. -This setting can only be changed in user settings for security reasons. - -#### `svelte.language-server.runtime-args` - -You normally don't set this. Additional arguments to pass to Node when spawning the language server. -This is useful when you use something like Yarn PnP and need its loader arguments `["--loader", ".pnp.loader.mjs"]`. - -##### `svelte.language-server.port` - -You normally don't set this. At which port to spawn the language server. -Can be used for attaching to the process for debugging / profiling. -If you experience crashes due to "port already in use", try setting the port. --1 = default port is used. - -##### `svelte.trace.server` - -Traces the communication between VS Code and the Svelte Language Server. _Default_: `off` - -Value can be `off`, `messages`, or `verbose`. -You normally don't set this. Can be used in debugging language server features. -If enabled you can see the logging in the output channel near the integrated terminal. - -##### `svelte.plugin.XXX` - -Settings to toggle specific features of the extension. The full list of all settings [is here](/packages/language-server/README.md#List-of-settings). - -### Usage with Yarn 2 PnP - -1. Run `yarn add -D svelte-language-server` to install svelte-language-server as a dev dependency -2. Run `yarn dlx @yarnpkg/sdks vscode` to generate or update the VSCode/Yarn integration SDKs. This also sets the `svelte.language-server.ls-path` and `svelte.language-server.runtime-args` setting for the workspace, pointing it to the workspace-installed language server. Note that this requires workspace trust - else set the `svelte.language-server.ls-path` and `svelte.language-server.runtime-args` setting in your user configuration. -3. Restart VSCode. -4. Commit the changes to `.yarn/sdks` - -### Credits - -- The PostCSS grammar is based on [hudochenkov/Syntax-highlighting-for-PostCSS](https://github.com/hudochenkov/Syntax-highlighting-for-PostCSS) diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index 4306e6b65..940570eec 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -2,7 +2,7 @@ "name": "svelte-vscode", "version": "0.5.0", "description": "Svelte language support for VS Code", - "main": "dist/src/extension.js", + "main": "out/extension.js", "scripts": { "build:grammar": "npx js-yaml syntaxes/svelte.tmLanguage.src.yaml > syntaxes/svelte.tmLanguage.json && npx js-yaml syntaxes/postcss.src.yaml > syntaxes/postcss.json", "build:ts": "tsc -p ./", @@ -37,25 +37,11 @@ "Formatters" ], "engines": { - "vscode": "^1.67.0" + "vscode": "^1.82.0" }, "activationEvents": [ - "onLanguage:svelte", - "onCommand:svelte.restartLanguageServer", - "onLanguage:javascript", - "onLanguage:typescript" + "onLanguage:svelte" ], - "capabilities": { - "untrustedWorkspaces": { - "supported": "limited", - "restrictedConfigurations": [ - "svelte.language-server.runtime", - "svelte.language-server.ls-path", - "svelte.language-server.runtime-args" - ], - "description": "The extension requires workspace trust because it executes code specified by the workspace. Loading the user's node_modules and loading svelte config files is disabled when untrusted" - } - }, "contributes": { "typescriptServerPlugins": [ { @@ -63,339 +49,6 @@ "enableForWorkspaceTypeScriptVersions": true } ], - "configuration": { - "type": "object", - "title": "Svelte", - "properties": { - "svelte.enable-ts-plugin": { - "type": "boolean", - "default": false, - "title": "Enable TypeScript Svelte plugin", - "description": "Enables a TypeScript plugin which provides intellisense for Svelte files inside TS/JS files." - }, - "svelte.ask-to-enable-ts-plugin": { - "type": "boolean", - "default": true, - "title": "Ask to enable TypeScript Svelte plugin", - "description": "Ask on startup to enable the TypeScript plugin." - }, - "svelte.language-server.runtime": { - "type": "string", - "title": "Language Server Runtime", - "description": "- You normally don't need this - Path to the node executable to use to spawn the language server. This is useful when you depend on native modules such as node-sass as without this they will run in the context of vscode, meaning node version mismatch is likely. Minimum required node version is 12.17. This setting can only be changed in user settings for security reasons." - }, - "svelte.language-server.ls-path": { - "type": "string", - "title": "Language Server Path", - "description": "- You normally don't set this - Path to the language server executable. If you installed the \"svelte-language-server\" npm package, it's within there at \"bin/server.js\". Path can be either relative to your workspace root or absolute. Set this only if you want to use a custom version of the language server. This will then also use the workspace version of TypeScript. This setting can only be changed in user settings for security reasons." - }, - "svelte.language-server.runtime-args": { - "type": "array", - "title": "Language Server Runtime Args", - "description": "You normally don't set this. Additional arguments to pass to the node executable when spawning the language server. This is useful when you use something like Yarn PnP and need its loader arguments `[\"--loader\", \".pnp.loader.mjs\"]`." - }, - "svelte.language-server.port": { - "type": "number", - "title": "Language Server Port", - "description": "- You normally don't set this - At which port to spawn the language server. Can be used for attaching to the process for debugging / profiling. If you experience crashes due to \"port already in use\", try setting the port. -1 = default port is used.", - "default": -1 - }, - "svelte.language-server.debug": { - "type": "boolean", - "title": "Language Server Debug Mode", - "description": "- You normally don't set this - Enable more verbose logging for the language server useful for debugging language server execution." - }, - "svelte.trace.server": { - "type": "string", - "enum": [ - "off", - "messages", - "verbose" - ], - "default": "off", - "description": "Traces the communication between VS Code and the Svelte Language Server." - }, - "svelte.ui.svelteKitFilesContextMenu.enable": { - "type": "string", - "default": "auto", - "enum": [ - "auto", - "never", - "always" - ], - "description": "Show a context menu to generate SvelteKit files. \"always\" to always show it. \"never\" to always disable it. \"auto\" to show it when in a SvelteKit project. " - }, - "svelte.plugin.typescript.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript", - "description": "Enable the TypeScript plugin" - }, - "svelte.plugin.typescript.diagnostics.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript: Diagnostics", - "description": "Enable diagnostic messages for TypeScript" - }, - "svelte.plugin.typescript.hover.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript: Hover Info", - "description": "Enable hover info for TypeScript" - }, - "svelte.plugin.typescript.documentSymbols.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript: Symbols in Outline", - "description": "Enable document symbols for TypeScript" - }, - "svelte.plugin.typescript.completions.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript: Completions", - "description": "Enable completions for TypeScript" - }, - "svelte.plugin.typescript.codeActions.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript: Code Actions", - "description": "Enable code actions for TypeScript" - }, - "svelte.plugin.typescript.selectionRange.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript: Selection Range", - "description": "Enable selection range for TypeScript" - }, - "svelte.plugin.typescript.signatureHelp.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript: Signature Help", - "description": "Enable signature help (parameter hints) for TypeScript" - }, - "svelte.plugin.typescript.semanticTokens.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript: Semantic Tokens", - "description": "Enable semantic tokens (semantic highlight) for TypeScript." - }, - "svelte.plugin.css.enable": { - "type": "boolean", - "default": true, - "title": "CSS", - "description": "Enable the CSS plugin" - }, - "svelte.plugin.css.globals": { - "type": "string", - "default": "", - "title": "CSS: Global Files", - "description": "Which css files should be checked for global variables (`--global-var: value;`). These variables are added to the css completions. String of comma-separated file paths or globs relative to workspace root." - }, - "svelte.plugin.css.diagnostics.enable": { - "type": "boolean", - "default": true, - "title": "CSS: Diagnostics", - "description": "Enable diagnostic messages for CSS" - }, - "svelte.plugin.css.hover.enable": { - "type": "boolean", - "default": true, - "title": "CSS: Hover Info", - "description": "Enable hover info for CSS" - }, - "svelte.plugin.css.completions.enable": { - "type": "boolean", - "default": true, - "title": "CSS: Auto Complete", - "description": "Enable auto completions for CSS" - }, - "svelte.plugin.css.completions.emmet": { - "type": "boolean", - "default": true, - "title": "CSS: Include Emmet Completions", - "description": "Enable emmet auto completions for CSS" - }, - "svelte.plugin.css.documentColors.enable": { - "type": "boolean", - "default": true, - "title": "CSS: Document Colors", - "description": "Enable document colors for CSS" - }, - "svelte.plugin.css.colorPresentations.enable": { - "type": "boolean", - "default": true, - "title": "CSS: Color Picker", - "description": "Enable color picker for CSS" - }, - "svelte.plugin.css.documentSymbols.enable": { - "type": "boolean", - "default": true, - "title": "CSS: Symbols in Outline", - "description": "Enable document symbols for CSS" - }, - "svelte.plugin.css.selectionRange.enable": { - "type": "boolean", - "default": true, - "title": "CSS: SelectionRange", - "description": "Enable selection range for CSS" - }, - "svelte.plugin.html.enable": { - "type": "boolean", - "default": true, - "title": "HTML", - "description": "Enable the HTML plugin" - }, - "svelte.plugin.html.hover.enable": { - "type": "boolean", - "default": true, - "title": "HTML: Hover Info", - "description": "Enable hover info for HTML" - }, - "svelte.plugin.html.completions.enable": { - "type": "boolean", - "default": true, - "title": "HTML: Auto Complete", - "description": "Enable auto completions for HTML" - }, - "svelte.plugin.html.completions.emmet": { - "type": "boolean", - "default": true, - "title": "HTML: Include Emmet Completions", - "description": "Enable emmet auto completions for HTML" - }, - "svelte.plugin.html.tagComplete.enable": { - "type": "boolean", - "default": true, - "title": "HTML: Tag Auto Closing", - "description": "Enable HTML tag auto closing" - }, - "svelte.plugin.html.documentSymbols.enable": { - "type": "boolean", - "default": true, - "title": "HTML: Symbols in Outline", - "description": "Enable document symbols for HTML" - }, - "svelte.plugin.html.linkedEditing.enable": { - "type": "boolean", - "default": true, - "title": "HTML: Linked Editing", - "description": "Enable Linked Editing for HTML" - }, - "svelte.plugin.svelte.enable": { - "type": "boolean", - "default": true, - "title": "Svelte", - "description": "Enable the Svelte plugin" - }, - "svelte.plugin.svelte.diagnostics.enable": { - "type": "boolean", - "default": true, - "title": "Svelte: Diagnostics", - "description": "Enable diagnostic messages for Svelte" - }, - "svelte.plugin.svelte.compilerWarnings": { - "type": "object", - "additionalProperties": { - "type": "string", - "enum": [ - "ignore", - "error" - ] - }, - "default": {}, - "title": "Svelte: Compiler Warnings Settings", - "description": "Svelte compiler warning codes to ignore or to treat as errors. Example: { 'css-unused-selector': 'ignore', 'unused-export-let': 'error'}" - }, - "svelte.plugin.svelte.format.enable": { - "type": "boolean", - "default": true, - "title": "Svelte: Format", - "description": "Enable formatting for Svelte (includes css & js). You can set some formatting options through this extension. They will be ignored if there's any kind of configuration file, for example a `.prettierrc` file." - }, - "svelte.plugin.svelte.format.config.svelteSortOrder": { - "type": "string", - "default": "options-scripts-markup-styles", - "title": "Svelte Format: Sort Order", - "description": "Format: join the keys `options`, `scripts`, `markup`, `styles` with a - in the order you want. This option is ignored if there's any kind of configuration file, for example a `.prettierrc` file." - }, - "svelte.plugin.svelte.format.config.svelteStrictMode": { - "type": "boolean", - "default": false, - "title": "Svelte Format: Strict Mode", - "description": "More strict HTML syntax. This option is ignored if there's any kind of configuration file, for example a `.prettierrc` file." - }, - "svelte.plugin.svelte.format.config.svelteAllowShorthand": { - "type": "boolean", - "default": true, - "title": "Svelte Format: Allow Shorthand", - "description": "Option to enable/disable component attribute shorthand if attribute name and expression are the same. This option is ignored if there's any kind of configuration file, for example a `.prettierrc` file." - }, - "svelte.plugin.svelte.format.config.svelteBracketNewLine": { - "type": "boolean", - "default": true, - "title": "Svelte Format: Bracket New Line", - "description": "Put the `>` of a multiline element on a new line. This option is ignored if there's any kind of configuration file, for example a `.prettierrc` file." - }, - "svelte.plugin.svelte.format.config.svelteIndentScriptAndStyle": { - "type": "boolean", - "default": true, - "title": "Svelte Format: Indent Script And Style", - "description": "Whether or not to indent code inside ` - -

{$page.status}: {$page.error?.message}

- `.trim(); - - const js = ` - - -

{$page.status}: {$page.error.message}

- `.trim(); - - return config.type === 'js' ? js : ts; -} diff --git a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout-load.ts b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout-load.ts deleted file mode 100644 index 4259a54c5..000000000 --- a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout-load.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GenerateConfig } from '../types'; - -export default async function (config: GenerateConfig) { - const ts = ` -import type { LayoutLoad } from './$types'; - -export const load: LayoutLoad = async () => { - return {}; -}; - `.trim(); - - const tsSatisfies = ` -import type { LayoutLoad } from './$types'; - -export const load = (async () => { - return {}; -}) satisfies LayoutLoad; - `.trim(); - - const js = ` -/** @type {import('./$types').LayoutLoad} */ -export async function load() { - return {}; -} - `.trim(); - - return config.type === 'js' ? js : config.type === 'ts' ? ts : tsSatisfies; -} diff --git a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout-server.ts b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout-server.ts deleted file mode 100644 index e467ba5d8..000000000 --- a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout-server.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GenerateConfig } from '../types'; - -export default async function (config: GenerateConfig) { - const ts = ` -import type { LayoutServerLoad } from './$types'; - -export const load: LayoutServerLoad = async () => { - return {}; -}; - `.trim(); - - const tsSatisfies = ` -import type { LayoutServerLoad } from './$types'; - -export const load = (async () => { - return {}; -}) satisfies LayoutServerLoad; - `.trim(); - - const js = ` -/** @type {import('./$types').LayoutServerLoad} */ -export async function load() { - return {}; -} - `.trim(); - - return config.type === 'js' ? js : config.type === 'ts' ? ts : tsSatisfies; -} diff --git a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout.ts b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout.ts deleted file mode 100644 index 00563d89a..000000000 --- a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { GenerateConfig } from '../types'; - -export default async function (config: GenerateConfig) { - const ts = ` - - `.trim(); - - const js = ` - - `.trim(); - - return config.type === 'js' ? js : ts; -} diff --git a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page-load.ts b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page-load.ts deleted file mode 100644 index 6fb2ca84c..000000000 --- a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page-load.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GenerateConfig } from '../types'; - -export default async function (config: GenerateConfig) { - const ts = ` -import type { PageLoad } from './$types'; - -export const load: PageLoad = async () => { - return {}; -}; - `.trim(); - - const tsSatisfies = ` -import type { PageLoad } from './$types'; - -export const load = (async () => { - return {}; -}) satisfies PageLoad; - `.trim(); - - const js = ` -/** @type {import('./$types').PageLoad} */ -export async function load() { - return {}; -}; - `.trim(); - - return config.type === 'js' ? js : config.type === 'ts' ? ts : tsSatisfies; -} diff --git a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page-server.ts b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page-server.ts deleted file mode 100644 index bbb2ef7e9..000000000 --- a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page-server.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GenerateConfig } from '../types'; - -export default async function (config: GenerateConfig) { - const ts = ` -import type { PageServerLoad } from './$types'; - -export const load: PageServerLoad = async () => { - return {}; -}; - `.trim(); - - const tsSatisfies = ` -import type { PageServerLoad } from './$types'; - -export const load = (async () => { - return {}; -}) satisfies PageServerLoad; - `.trim(); - - const js = ` -/** @type {import('./$types').PageServerLoad} */ -export async function load() { - return {}; -}; - `.trim(); - - return config.type === 'js' ? js : config.type === 'ts' ? ts : tsSatisfies; -} diff --git a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page.ts b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page.ts deleted file mode 100644 index abaf94924..000000000 --- a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { GenerateConfig } from '../types'; - -export default async function (config: GenerateConfig) { - const ts = ` - - `.trim(); - - const js = ` - - `.trim(); - - return config.type === 'js' ? js : ts; -} diff --git a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/server.ts b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/server.ts deleted file mode 100644 index 826b0b660..000000000 --- a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { GenerateConfig } from '../types'; - -export default async function generate(config: GenerateConfig) { - const ts = ` -import type { RequestHandler } from './$types'; - -export const GET: RequestHandler = async () => { - return new Response(); -}; - `.trim(); - - const js = ` -/** @type {import('./$types').RequestHandler} */ -export async function GET() { - return new Response(); -}; - `.trim(); - - return config.type === 'js' ? js : ts; -} diff --git a/packages/svelte-vscode/src/sveltekit/generateFiles/types.ts b/packages/svelte-vscode/src/sveltekit/generateFiles/types.ts deleted file mode 100644 index e50f4225c..000000000 --- a/packages/svelte-vscode/src/sveltekit/generateFiles/types.ts +++ /dev/null @@ -1,41 +0,0 @@ -export enum CommandType { - PAGE = 'svelte.kit.generatePage', - PAGE_LOAD = 'svelte.kit.generatePageLoad', - PAGE_SERVER = 'svelte.kit.generatePageServerLoad', - LAYOUT = 'svelte.kit.generateLayout', - LAYOUT_LOAD = 'svelte.kit.generateLayoutLoad', - LAYOUT_SERVER = 'svelte.kit.generateLayoutServerLoad', - SERVER = 'svelte.kit.generateServer', - ERROR = 'svelte.kit.generateError', - MULTIPLE = 'svelte.kit.generateMultipleFiles' -} - -export enum FileType { - SCRIPT, - PAGE -} - -export enum ResourceType { - PAGE, - PAGE_LOAD, - PAGE_SERVER, - LAYOUT, - LAYOUT_LOAD, - LAYOUT_SERVER, - SERVER, - ERROR -} - -export type Resource = { - type: FileType; - filename: string; - generate: (config: GenerateConfig) => Promise; -}; - -export interface GenerateConfig { - path: string; - type: 'js' | 'ts' | 'ts-satisfies'; - pageExtension: string; - scriptExtension: string; - resources: Resource[]; -} diff --git a/packages/svelte-vscode/src/sveltekit/index.ts b/packages/svelte-vscode/src/sveltekit/index.ts deleted file mode 100644 index 5066a05f3..000000000 --- a/packages/svelte-vscode/src/sveltekit/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { TextDecoder } from 'util'; -import { ExtensionContext, commands, workspace } from 'vscode'; -import { addGenerateKitRouteFilesCommand } from './generateFiles'; - -type ShowSvelteKitFilesContextMenuConfig = 'auto' | 'always' | 'never'; - -export function setupSvelteKit(context: ExtensionContext) { - let contextMenuEnabled = false; - context.subscriptions.push( - workspace.onDidChangeConfiguration(() => { - enableContextMenu(); - }) - ); - - addGenerateKitRouteFilesCommand(context); - enableContextMenu(); - - async function enableContextMenu() { - const config = getConfig(); - if (config === 'never') { - if (contextMenuEnabled) { - setEnableContext(false); - } - return; - } - - if (config === 'always') { - // Force on. The condition is defined in the extension manifest - return; - } - - const enabled = await detect(20); - if (enabled !== contextMenuEnabled) { - setEnableContext(enabled); - contextMenuEnabled = enabled; - } - } -} - -function getConfig() { - return ( - workspace - .getConfiguration('svelte.ui.svelteKitFilesContextMenu') - .get('enable') ?? 'auto' - ); -} - -async function detect(nrRetries: number): Promise { - const packageJsonList = await workspace.findFiles('**/package.json', '**/node_modules/**'); - - if (packageJsonList.length === 0 && nrRetries > 0) { - // We assume that the user has not setup their project yet, so try again after a while - await new Promise((resolve) => setTimeout(resolve, 10000)); - return detect(nrRetries - 1); - } - - for (const fileUri of packageJsonList) { - try { - const text = new TextDecoder().decode(await workspace.fs.readFile(fileUri)); - const pkg = JSON.parse(text); - const hasKit = Object.keys(pkg.devDependencies ?? {}) - .concat(Object.keys(pkg.dependencies ?? {})) - .includes('@sveltejs/kit'); - - if (hasKit) { - return true; - } - } catch (error) { - console.error(error); - } - } - - return false; -} - -function setEnableContext(enable: boolean) { - // https://code.visualstudio.com/api/references/when-clause-contexts#add-a-custom-when-clause-context - commands.executeCommand( - 'setContext', - 'svelte.uiContext.svelteKitFilesContextMenu.enable', - enable - ); -} diff --git a/packages/svelte-vscode/src/sveltekit/utils.ts b/packages/svelte-vscode/src/sveltekit/utils.ts deleted file mode 100644 index 25701c1be..000000000 --- a/packages/svelte-vscode/src/sveltekit/utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as path from 'path'; -import { Uri, workspace } from 'vscode'; - -export async function fileExists(file: string) { - try { - await workspace.fs.stat(Uri.file(file)); - return true; - } catch (err) { - return false; - } -} - -export async function findFile(searchPath: string, fileName: string) { - for (;;) { - const filePath = path.join(searchPath, fileName); - if (await fileExists(filePath)) { - return filePath; - } - const parentPath = path.dirname(searchPath); - if (parentPath === searchPath) { - return; - } - searchPath = parentPath; - } -} - -export async function checkProjectType(path: string) { - const tsconfig = await findFile(path, 'tsconfig.json'); - const jsconfig = await findFile(path, 'jsconfig.json'); - const isTs = !!tsconfig && (!jsconfig || tsconfig.length >= jsconfig.length); - if (isTs) { - try { - const packageJSONPath = require.resolve('typescript/package.json', { - paths: [tsconfig] - }); - const { version } = require(packageJSONPath); - const [major, minor] = version.split('.'); - if ((Number(major) === 4 && Number(minor) >= 9) || Number(major) > 4) { - return 'ts-satisfies'; - } else { - return 'ts'; - } - } catch (e) { - return 'ts'; - } - } else { - return 'js'; - } -} diff --git a/packages/svelte-vscode/src/tsplugin.ts b/packages/svelte-vscode/src/tsplugin.ts deleted file mode 100644 index 066b2afe6..000000000 --- a/packages/svelte-vscode/src/tsplugin.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { commands, ExtensionContext, extensions, window, workspace } from 'vscode'; - -export class TsPlugin { - private enabled: boolean; - - constructor(context: ExtensionContext) { - this.enabled = TsPlugin.isEnabled(); - this.toggleTsPlugin(this.enabled); - - context.subscriptions.push( - workspace.onDidChangeConfiguration(() => { - const enabled = TsPlugin.isEnabled(); - if (enabled !== this.enabled) { - this.enabled = enabled; - this.toggleTsPlugin(this.enabled); - } - }) - ); - } - - static isEnabled(): boolean { - return workspace.getConfiguration('svelte').get('enable-ts-plugin') ?? false; - } - - private async toggleTsPlugin(enable: boolean) { - const extension = extensions.getExtension('vscode.typescript-language-features'); - - if (!extension) { - return; - } - - // This somewhat semi-public command configures our TypeScript plugin. - // The plugin itself is always present, but enabled/disabled depending on this config. - // It is done this way because it allows us to toggle the plugin without restarting VS Code - // and without having to do hacks like updating the extension's package.json. - commands.executeCommand('_typescript.configurePlugin', 'typescript-svelte-plugin', { - enable - }); - } - - async askToEnable() { - const shouldAsk = workspace - .getConfiguration('svelte') - .get('ask-to-enable-ts-plugin'); - if (this.enabled || !shouldAsk) { - return; - } - - const answers = ['Ask again later', "Don't show this message again", 'Enable Plugin']; - const response = await window.showInformationMessage( - 'The Svelte for VS Code extension now contains a TypeScript plugin. ' + - 'Enabling it will provide intellisense for Svelte files from TS/JS files. ' + - 'Would you like to enable it? ' + - 'You can always enable/disable it later on through the extension settings.', - ...answers - ); - - if (response === answers[2]) { - workspace.getConfiguration('svelte').update('enable-ts-plugin', true, true); - } else if (response === answers[1]) { - workspace.getConfiguration('svelte').update('ask-to-enable-ts-plugin', false, true); - } - } -} diff --git a/packages/svelte-vscode/src/typescript/findComponentReferences.ts b/packages/svelte-vscode/src/typescript/findComponentReferences.ts deleted file mode 100644 index 1fd471131..000000000 --- a/packages/svelte-vscode/src/typescript/findComponentReferences.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - commands, - ExtensionContext, - ProgressLocation, - Uri, - window, - workspace, - Position, - Location, - Range -} from 'vscode'; -import { LanguageClient } from 'vscode-languageclient/node'; -import { Location as LSLocation } from 'vscode-languageclient'; - -export async function addFindComponentReferencesListener( - getLS: () => LanguageClient, - context: ExtensionContext -) { - const disposable = commands.registerCommand( - 'svelte.typescript.findComponentReferences', - handler - ); - - context.subscriptions.push(disposable); - - async function handler(resource?: Uri) { - if (!resource) { - resource = window.activeTextEditor?.document.uri; - } - - if (!resource || resource.scheme !== 'file') { - return; - } - - const document = await workspace.openTextDocument(resource); - - await window.withProgress( - { - location: ProgressLocation.Window, - title: 'Finding component references' - }, - async (_, token) => { - const lsLocations = await getLS().sendRequest( - '$/getComponentReferences', - document.uri.toString(), - token - ); - - if (!lsLocations) { - return; - } - - await commands.executeCommand( - 'editor.action.showReferences', - resource, - new Position(0, 0), - lsLocations.map( - (ref) => - new Location( - Uri.parse(ref.uri), - new Range( - ref.range.start.line, - ref.range.start.character, - ref.range.end.line, - ref.range.end.character - ) - ) - ) - ); - } - ); - } -} diff --git a/packages/svelte-vscode/src/typescript/findFileReferences.ts b/packages/svelte-vscode/src/typescript/findFileReferences.ts deleted file mode 100644 index 69da5ad6d..000000000 --- a/packages/svelte-vscode/src/typescript/findFileReferences.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - commands, - ExtensionContext, - ProgressLocation, - Uri, - window, - workspace, - Position, - Location, - Range -} from 'vscode'; -import { LanguageClient } from 'vscode-languageclient/node'; -import { Location as LSLocation } from 'vscode-languageclient'; - -/** - * adopted from https://github.com/microsoft/vscode/blob/5f3e9c120a4407de3e55465588ce788618526eb0/extensions/typescript-language-features/src/languageFeatures/fileReferences.ts - */ -export async function addFindFileReferencesListener( - getLS: () => LanguageClient, - context: ExtensionContext -) { - const disposable = commands.registerCommand('svelte.typescript.findAllFileReferences', handler); - - context.subscriptions.push(disposable); - - async function handler(resource?: Uri) { - if (!resource) { - resource = window.activeTextEditor?.document.uri; - } - - if (!resource || resource.scheme !== 'file') { - return; - } - - const document = await workspace.openTextDocument(resource); - - await window.withProgress( - { - location: ProgressLocation.Window, - title: 'Finding file references' - }, - async (_, token) => { - const lsLocations = await getLS().sendRequest( - '$/getFileReferences', - document.uri.toString(), - token - ); - - if (!lsLocations) { - return; - } - - const config = workspace.getConfiguration('references'); - const existingSetting = config.inspect('preferredLocation'); - - await config.update('preferredLocation', 'view'); - try { - await commands.executeCommand( - 'editor.action.showReferences', - resource, - new Position(0, 0), - lsLocations.map( - (ref) => - new Location( - Uri.parse(ref.uri), - new Range( - ref.range.start.line, - ref.range.start.character, - ref.range.end.line, - ref.range.end.character - ) - ) - ) - ); - } finally { - await config.update( - 'preferredLocation', - existingSetting?.workspaceFolderValue ?? existingSetting?.workspaceValue - ); - } - } - ); - } -} diff --git a/packages/svelte-vscode/src/utils.ts b/packages/svelte-vscode/src/utils.ts deleted file mode 100644 index 867bf5154..000000000 --- a/packages/svelte-vscode/src/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function atob(encoded: string) { - const buffer = Buffer.from(encoded, 'base64'); - return buffer.toString('utf8'); -} - -export function btoa(decoded: string) { - const buffer = Buffer.from(decoded, 'utf8'); - return buffer.toString('base64'); -} diff --git a/packages/svelte-vscode/tsconfig.json b/packages/svelte-vscode/tsconfig.json index f0c980a77..7217e8475 100644 --- a/packages/svelte-vscode/tsconfig.json +++ b/packages/svelte-vscode/tsconfig.json @@ -1,17 +1,18 @@ { - "compilerOptions": { - "lib": ["es2021"], - "module": "CommonJS", - "target": "es2021", - "moduleResolution": "node", - - "outDir": "dist", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "sourceMap": true, - "composite": true - } -} + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "out", + "rootDir": "src", + }, + "include": [ + "src", + ], + "references": [ + { + "path": "../language-server/tsconfig.json" + }, + { + "path": "../typescript-plugin/tsconfig.json" + } + ] +} \ No newline at end of file diff --git a/packages/typescript-plugin/.gitignore b/packages/typescript-plugin/.gitignore index 96a7feca7..8bff63b2f 100644 --- a/packages/typescript-plugin/.gitignore +++ b/packages/typescript-plugin/.gitignore @@ -1,3 +1,3 @@ -dist +out node_modules tsconfig.tsbuildinfo \ No newline at end of file diff --git a/packages/typescript-plugin/.npmignore b/packages/typescript-plugin/.npmignore deleted file mode 100644 index bc3ec15c4..000000000 --- a/packages/typescript-plugin/.npmignore +++ /dev/null @@ -1,7 +0,0 @@ -/node_modules -/src -tsconfig.json -.gitignore -internal.md -dist/tsconfig.tsbuildinfo -dist/**/*.map \ No newline at end of file diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index e47708ffa..b1e48a91e 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -1,8 +1,12 @@ { "name": "typescript-svelte-plugin", - "version": "0.2.0", + "version": "0.0.0", "description": "A TypeScript Plugin providing Svelte intellisense", - "main": "dist/src/index.js", + "main": "out/index.js", + "files": [ + "out/**.js", + "out/**.d.ts" + ], "scripts": { "build": "tsc -p ./", "watch": "tsc -w -p ./", @@ -18,11 +22,10 @@ "author": "The Svelte Community", "license": "MIT", "devDependencies": { - "@types/node": "^16.0.0", - "typescript": "^5.3.2" + "@types/node": "latest" }, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.14", - "svelte2tsx": "workspace:~" + "@volar/typescript": "2.0.0-alpha.13", + "svelte-language-server": "0.0.0" } } diff --git a/packages/typescript-plugin/src/config-manager.ts b/packages/typescript-plugin/src/config-manager.ts deleted file mode 100644 index 82f046598..000000000 --- a/packages/typescript-plugin/src/config-manager.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { EventEmitter } from 'events'; - -const configurationEventName = 'configuration-changed'; - -export interface Configuration { - enable: boolean; - /** Skip the Svelte detection and assume this is a Svelte project */ - assumeIsSvelteProject: boolean; -} - -export class ConfigManager { - private emitter = new EventEmitter(); - private config: Configuration = { - enable: true, - assumeIsSvelteProject: false - }; - - onConfigurationChanged(listener: (config: Configuration) => void) { - this.emitter.on(configurationEventName, listener); - } - - isConfigChanged(config: Configuration) { - // right now we only care about enable - return config.enable !== this.config.enable; - } - - updateConfigFromPluginConfig(config: Configuration) { - this.config = { - ...this.config, - ...config - }; - this.emitter.emit(configurationEventName, config); - } - - getConfig() { - return this.config; - } -} diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts index 3c85949ae..538e70352 100644 --- a/packages/typescript-plugin/src/index.ts +++ b/packages/typescript-plugin/src/index.ts @@ -1,267 +1,11 @@ -import { dirname, join, resolve } from 'path'; -import { decorateLanguageService, isPatched } from './language-service'; -import { Logger } from './logger'; -import { patchModuleLoader } from './module-loader'; -import { SvelteSnapshotManager } from './svelte-snapshots'; -import type ts from 'typescript/lib/tsserverlibrary'; -import { ConfigManager, Configuration } from './config-manager'; -import { ProjectSvelteFilesManager } from './project-svelte-files'; -import { getConfigPathForProject, hasNodeModule } from './utils'; +import { createTSServerPlugin } from '@volar/typescript/lib/quickstart/createTSServerPlugin'; +import { svelteLanguagePlugin } from 'svelte-language-server/out/languagePlugin'; -function init(modules: { typescript: typeof ts }): ts.server.PluginModule { - const configManager = new ConfigManager(); - let resolvedSvelteTsxFiles: string[] | undefined; - - function create(info: ts.server.PluginCreateInfo) { - const logger = new Logger(info.project.projectService.logger); - if ( - !(info.config as Configuration)?.assumeIsSvelteProject && - !isSvelteProject(info.project.getCompilerOptions()) - ) { - logger.log('Detected that this is not a Svelte project, abort patching TypeScript'); - return info.languageService; - } - - if (isPatched(info.project)) { - logger.log('Already patched. Checking tsconfig updates.'); - - ProjectSvelteFilesManager.getInstance( - info.project.getProjectName() - )?.updateProjectConfig(info.languageServiceHost); - - return info.languageService; - } - - configManager.updateConfigFromPluginConfig(info.config); - if (configManager.getConfig().enable) { - logger.log('Starting Svelte plugin'); - } else { - logger.log('Svelte plugin disabled'); - logger.log(info.config); - } - - // This call the ConfiguredProject.getParsedCommandLine - // where it'll try to load the cached version of the parsedCommandLine - const parsedCommandLine = info.languageServiceHost.getParsedCommandLine?.( - getConfigPathForProject(info.project) - ); - - // For some reason it's no longer enough to patch this at the projectService level, so we do it here, too - // TODO investigate if we can use the script snapshot for all Svelte files, too, enabling Svelte file - // updates getting picked up without a file save - move this logic into the snapshot manager then? - const getScriptSnapshot = info.languageServiceHost.getScriptSnapshot.bind( - info.languageServiceHost - ); - info.languageServiceHost.getScriptSnapshot = (fileName) => { - const normalizedPath = fileName.replace(/\\/g, '/'); - if (normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) { - return modules.typescript.ScriptSnapshot.fromString(''); - } else if (normalizedPath.endsWith('node_modules/svelte/types/index.d.ts')) { - const snapshot = getScriptSnapshot(fileName); - if (snapshot) { - const originalText = snapshot.getText(0, snapshot.getLength()); - const startIdx = originalText.indexOf(`declare module '*.svelte' {`); - const endIdx = - originalText.indexOf(`}`, originalText.indexOf(';', startIdx)) + 1; - return modules.typescript.ScriptSnapshot.fromString( - originalText.substring(0, startIdx) + - ' '.repeat(endIdx - startIdx) + - originalText.substring(endIdx) - ); - } - } else if (normalizedPath.endsWith('svelte2tsx/svelte-jsx.d.ts')) { - // Remove the dom lib reference to not load these ambient types in case - // the user has a tsconfig.json with different lib settings like in - // https://github.com/sveltejs/language-tools/issues/1733 - const snapshot = getScriptSnapshot(fileName); - if (snapshot) { - const originalText = snapshot.getText(0, snapshot.getLength()); - const toReplace = '/// '; - return modules.typescript.ScriptSnapshot.fromString( - originalText.replace(toReplace, ' '.repeat(toReplace.length)) - ); - } - return snapshot; - } else if (normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts')) { - const snapshot = getScriptSnapshot(fileName); - if (snapshot) { - let originalText = snapshot.getText(0, snapshot.getLength()); - if (!originalText.includes('// -- start svelte-ls-remove --')) { - return snapshot; // uses an older version of svelte2tsx or is already patched - } - const startIdx = originalText.indexOf('// -- start svelte-ls-remove --'); - const endIdx = originalText.indexOf('// -- end svelte-ls-remove --'); - originalText = - originalText.substring(0, startIdx) + - ' '.repeat(endIdx - startIdx) + - originalText.substring(endIdx); - return modules.typescript.ScriptSnapshot.fromString(originalText); - } - return snapshot; - } - return getScriptSnapshot(fileName); - }; - - const svelteOptions = parsedCommandLine?.raw?.svelteOptions || { namespace: 'svelteHTML' }; - logger.log('svelteOptions:', svelteOptions); - logger.debug(parsedCommandLine?.wildcardDirectories); - - const snapshotManager = new SvelteSnapshotManager( - modules.typescript, - info.project.projectService, - svelteOptions, - logger, - configManager - ); - - const projectSvelteFilesManager = parsedCommandLine - ? new ProjectSvelteFilesManager( - modules.typescript, - info.project, - info.serverHost, - snapshotManager, - logger, - parsedCommandLine, - configManager - ) - : undefined; - - patchModuleLoader( - logger, - snapshotManager, - modules.typescript, - info.languageServiceHost, - info.project, - configManager - ); - - configManager.onConfigurationChanged(() => { - // enabling/disabling the plugin means TS has to recompute stuff - // don't clear semantic cache here - // typescript now expected the program updates to be completely in their control - // doing so will result in a crash - info.project.markAsDirty(); - - // updateGraph checks for new root files - // if there's no tsconfig there isn't root files to check - if (projectSvelteFilesManager) { - info.project.updateGraph(); - } - }); - - return decorateLanguageService( - info.languageService, - snapshotManager, - logger, - configManager, - info, - modules.typescript, - () => projectSvelteFilesManager?.dispose() - ); +module.exports = createTSServerPlugin( + () => { + return { + languagePlugins: [svelteLanguagePlugin], + extensions: ['.svelte'], } - - function getExternalFiles(project: ts.server.Project) { - if (!isSvelteProject(project.getCompilerOptions()) || !configManager.getConfig().enable) { - return []; - } - - const configFilePath = project.getCompilerOptions().configFilePath; - - // Needed so the ambient definitions are known inside the tsx files - const svelteTsxFiles = resolveSvelteTsxFiles( - typeof configFilePath === 'string' ? configFilePath : undefined - ); - - if (!configFilePath) { - svelteTsxFiles.forEach((file) => { - openSvelteTsxFileForInferredProject(project, file); - }); - } - - // let ts know project svelte files to do its optimization - return svelteTsxFiles.concat( - ProjectSvelteFilesManager.getInstance(project.getProjectName())?.getFiles() ?? [] - ); - } - - function resolveSvelteTsxFiles(configFilePath: string | undefined) { - if (resolvedSvelteTsxFiles) { - return resolvedSvelteTsxFiles; - } - - const svelteTsPath = dirname(require.resolve('svelte2tsx')); - const sveltePath = require.resolve( - 'svelte/compiler', - configFilePath ? { paths: [configFilePath] } : undefined - ); - const VERSION = require(sveltePath).VERSION; - const isSvelte3 = VERSION.split('.')[0] === '3'; - const svelteHtmlDeclaration = isSvelte3 - ? undefined - : join(dirname(sveltePath), 'svelte-html.d.ts'); - const svelteHtmlFallbackIfNotExist = - svelteHtmlDeclaration && modules.typescript.sys.fileExists(svelteHtmlDeclaration) - ? svelteHtmlDeclaration - : './svelte-jsx-v4.d.ts'; - const svelteTsxFiles = ( - isSvelte3 - ? ['./svelte-shims.d.ts', './svelte-jsx.d.ts', './svelte-native-jsx.d.ts'] - : [ - './svelte-shims-v4.d.ts', - svelteHtmlFallbackIfNotExist, - './svelte-native-jsx.d.ts' - ] - ).map((f) => modules.typescript.sys.resolvePath(resolve(svelteTsPath, f))); - - resolvedSvelteTsxFiles = svelteTsxFiles; - - return svelteTsxFiles; - } - - function isSvelteProject(compilerOptions: ts.CompilerOptions) { - // Add more checks like "no Svelte file found" or "no config file found"? - return hasNodeModule(compilerOptions, 'svelte'); - } - - function onConfigurationChanged(config: Configuration) { - if (configManager.isConfigChanged(config)) { - configManager.updateConfigFromPluginConfig(config); - } - } - - /** - * TypeScript doesn't load the external files in projects without a config file. So we load it by ourselves. - * TypeScript also seems to expect files added to the root to be opened by the client in this situation. - */ - function openSvelteTsxFileForInferredProject(project: ts.server.Project, file: string) { - const normalizedPath = modules.typescript.server.toNormalizedPath(file); - if (project.containsFile(normalizedPath)) { - return; - } - - const scriptInfo = project.projectService.getOrCreateScriptInfoForNormalizedPath( - normalizedPath, - /*openedByClient*/ true, - project.readFile(file) - ); - - if (!scriptInfo) { - return; - } - - if (!project.projectService.openFiles.has(scriptInfo.path)) { - project.projectService.openFiles.set(scriptInfo.path, undefined); - } - - if ((project as any).projectRootPath) { - // Only add the file to the project if it has a projectRootPath, because else - // a ts.Assert error will be thrown when multiple inferred projects are tried - // to be merged. - project.addRoot(scriptInfo); - } - } - - return { create, getExternalFiles, onConfigurationChanged }; -} - -export = init; + } +) diff --git a/packages/typescript-plugin/src/language-service/call-hierarchy.ts b/packages/typescript-plugin/src/language-service/call-hierarchy.ts deleted file mode 100644 index bfd7248eb..000000000 --- a/packages/typescript-plugin/src/language-service/call-hierarchy.ts +++ /dev/null @@ -1,332 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { SvelteSnapshot, SvelteSnapshotManager } from '../svelte-snapshots'; -import { - findNodeAtSpan, - gatherDescendants, - isGeneratedSvelteComponentName, - isNotNullOrUndefined, - isSvelteFilePath, - offsetOfGeneratedComponentExport -} from '../utils'; - -const ENSURE_COMPONENT_HELPER = '__sveltets_2_ensureComponent'; - -export function decorateCallHierarchy( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager, - typescript: typeof ts -): void { - // don't need to patch prepare. It's always a ts/js file - // const prepareCallHierarchy = ls.prepareCallHierarchy; - const provideCallHierarchyIncomingCalls = ls.provideCallHierarchyIncomingCalls; - const provideCallHierarchyOutgoingCalls = ls.provideCallHierarchyOutgoingCalls; - - ls.provideCallHierarchyIncomingCalls = (fileName: string, position: number) => { - const program = ls.getProgram(); - // probably won't happen - if (!program) { - return provideCallHierarchyIncomingCalls(fileName, position); - } - - const snapshot = snapshotManager.get(fileName); - const componentExportOffset = - isComponentModulePosition(fileName, position) && snapshot - ? offsetOfGeneratedComponentExport(snapshot) - : -1; - const redirectedPosition = componentExportOffset >= 0 ? componentExportOffset : position; - const tsResult = provideCallHierarchyIncomingCalls(fileName, redirectedPosition); - - return tsResult - .map((item): ts.CallHierarchyIncomingCall | null => { - if (!isSvelteFilePath(item.from.file)) { - return item; - } - - const snapshot = snapshotManager.get(item.from.file); - const from = convertSvelteCallHierarchyItem(item.from, program); - - if (!from || !snapshot) { - return null; - } - - const fromSpans = item.fromSpans - .map((span) => snapshot.getOriginalTextSpan(span)) - .filter(isNotNullOrUndefined); - - return { - from, - fromSpans: fromSpans - }; - }) - .concat(getInComingCallsForComponent(ls, program, fileName, redirectedPosition) ?? []) - .filter(isNotNullOrUndefined); - }; - - ls.provideCallHierarchyOutgoingCalls = (fileName: string, position: number) => { - const program = ls.getProgram(); - // probably won't happen - if (!program) { - return provideCallHierarchyOutgoingCalls(fileName, position); - } - - const sourceFile = program?.getSourceFile(fileName); - const renderFunctionOffset = - isComponentModulePosition(fileName, position) && sourceFile - ? sourceFile.statements - .find( - (statement): statement is ts.FunctionDeclaration => - typescript.isFunctionDeclaration(statement) && - statement.name?.getText() === 'render' - ) - ?.name?.getStart() - : -1; - const offset = - renderFunctionOffset != null && renderFunctionOffset >= 0 - ? renderFunctionOffset - : position; - const snapshot = snapshotManager.get(fileName); - - return provideCallHierarchyOutgoingCalls(fileName, offset) - .concat( - program && sourceFile && isComponentModulePosition(fileName, position) - ? getOutgoingCallsForComponent(program, sourceFile) ?? [] - : [] - ) - .map((item): ts.CallHierarchyOutgoingCall | null => { - const to = convertSvelteCallHierarchyItem(item.to, program); - - if ( - !to || - item.to.name.startsWith('__sveltets') || - item.to.containerName === 'svelteHTML' - ) { - return null; - } - - const fromSpans = snapshot - ? item.fromSpans - .map((span) => snapshot.getOriginalTextSpan(span)) - .filter(isNotNullOrUndefined) - : item.fromSpans; - - if (!fromSpans.length) { - return null; - } - - return { - to, - fromSpans - }; - }) - .filter(isNotNullOrUndefined); - }; - - function isComponentModulePosition(fileName: string, position: number) { - return isSvelteFilePath(fileName) && position === 0; - } - - function convertSvelteCallHierarchyItem( - item: ts.CallHierarchyItem, - program: ts.Program - ): ts.CallHierarchyItem | null { - if (!isSvelteFilePath(item.file)) { - return item; - } - - const snapshot = snapshotManager.get(item.file); - if (!snapshot) { - return null; - } - - const redirectedCallHierarchyItem = redirectSvelteCallHierarchyItem( - snapshot, - program, - item - ); - - if (redirectedCallHierarchyItem) { - return redirectedCallHierarchyItem; - } - - const selectionSpan = snapshot.getOriginalTextSpan(item.selectionSpan); - - if (!selectionSpan) { - return null; - } - - const span = snapshot.getOriginalTextSpan(item.span); - if (!span) { - return null; - } - - return { - ...item, - span, - selectionSpan - }; - } - - function redirectSvelteCallHierarchyItem( - snapshot: SvelteSnapshot, - program: ts.Program, - item: ts.CallHierarchyItem - ): ts.CallHierarchyItem | null { - const sourceFile = program.getSourceFile(item.file); - - if (!sourceFile) { - return null; - } - - if (isGeneratedSvelteComponentName(item.name)) { - return toComponentCallHierarchyItem(snapshot, item.file); - } - - if (item.name === 'render') { - const end = item.selectionSpan.start + item.selectionSpan.length; - const renderFunction = sourceFile.statements.find( - (statement) => - statement.getStart() <= item.selectionSpan.start && statement.getEnd() >= end - ); - if (!renderFunction || !sourceFile.statements.includes(renderFunction)) { - return null; - } - return toComponentCallHierarchyItem(snapshot, item.file); - } - - return null; - } - - function toComponentCallHierarchyItem( - snapshot: SvelteSnapshot, - file: string - ): ts.CallHierarchyItem { - const fileSpan = { start: 0, length: snapshot.getOriginalText().length }; - - return { - kind: typescript.ScriptElementKind.moduleElement, - file: file, - name: '', - selectionSpan: { start: 0, length: 0 }, - span: fileSpan - }; - } - - function getInComingCallsForComponent( - ls: ts.LanguageService, - program: ts.Program, - fileName: string, - position: number - ): ts.CallHierarchyIncomingCall[] | null { - if (!isSvelteFilePath(fileName)) { - return null; - } - - return ( - ls - .findReferences(fileName, position) - ?.map((ref) => componentRefToIncomingCall(ref, program)) - .filter(isNotNullOrUndefined) ?? null - ); - } - - function componentRefToIncomingCall( - ref: ts.ReferencedSymbol, - program: ts.Program - ): ts.CallHierarchyIncomingCall | null { - const snapshot = - isSvelteFilePath(ref.definition.fileName) && - snapshotManager.get(ref.definition.fileName); - const sourceFile = program.getSourceFile(ref.definition.fileName); - - if (!snapshot || !sourceFile) { - return null; - } - - const startTags = ref.references - .map((ref) => { - const generatedTextSpan = snapshot.getGeneratedTextSpan(ref.textSpan); - const node = - generatedTextSpan && - findNodeAtSpan(sourceFile, generatedTextSpan, isComponentStartTag); - - if (node) { - return ref; - } - - return null; - }) - .filter(isNotNullOrUndefined); - - if (!startTags.length) { - return null; - } - - return { - from: toComponentCallHierarchyItem(snapshot, ref.definition.fileName), - fromSpans: startTags.map((tag) => tag.textSpan) - }; - } - - function isComponentStartTag(node: ts.Node | undefined): node is ts.Identifier { - return ( - !!node && - node.parent && - typescript.isCallExpression(node.parent) && - typescript.isIdentifier(node.parent.expression) && - node.parent.expression.text === ENSURE_COMPONENT_HELPER && - typescript.isIdentifier(node) && - node === node.parent.arguments[0] - ); - } - - function getOutgoingCallsForComponent( - program: ts.Program, - sourceFile: ts.SourceFile - ): ts.CallHierarchyOutgoingCall[] | null { - const groups = new Map(); - - const startTags = gatherDescendants(sourceFile, isComponentStartTag); - const typeChecker = program.getTypeChecker(); - - for (const startTag of startTags) { - const type = typeChecker.getTypeAtLocation(startTag); - const symbol = type.aliasSymbol ?? type.symbol; - const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0]; - - if (!declaration || !typescript.isClassDeclaration(declaration)) { - continue; - } - - let group = groups.get(declaration); - - if (!group) { - group = []; - groups.set(declaration, group); - } - - group.push({ start: startTag.getStart(), length: startTag.getWidth() }); - } - - return ( - Array.from(groups).map(([declaration, group]) => { - const file = declaration.getSourceFile().fileName; - const name = declaration.name?.getText() ?? file.slice(file.lastIndexOf('.')); - const span = { start: declaration.getStart(), length: declaration.getWidth() }; - const selectionSpan = declaration.name - ? { start: declaration.name.getStart(), length: declaration.name.getWidth() } - : span; - - return { - to: { - file, - kind: typescript.ScriptElementKind.classElement, - name, - selectionSpan, - span - }, - fromSpans: group - }; - }) ?? null - ); - } -} diff --git a/packages/typescript-plugin/src/language-service/completions.ts b/packages/typescript-plugin/src/language-service/completions.ts deleted file mode 100644 index b03b28da4..000000000 --- a/packages/typescript-plugin/src/language-service/completions.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { basename, dirname } from 'path'; -import type ts from 'typescript/lib/tsserverlibrary'; -import { Logger } from '../logger'; -import { findNodeAtPosition, isSvelteFilePath, isTopLevelExport, replaceDeep } from '../utils'; -import { getVirtualLS, isKitRouteExportAllowedIn, kitExports } from './sveltekit'; - -type _ts = typeof ts; - -const componentPostfix = '__SvelteComponent_'; - -export function decorateCompletions( - ls: ts.LanguageService, - info: ts.server.PluginCreateInfo, - ts: _ts, - logger: Logger -): void { - const getCompletionsAtPosition = ls.getCompletionsAtPosition; - ls.getCompletionsAtPosition = (fileName, position, options, settings) => { - let completions; - - const result = getVirtualLS(fileName, info, ts); - if (result) { - const { languageService, toVirtualPos, toOriginalPos } = result; - completions = languageService.getCompletionsAtPosition( - fileName, - toVirtualPos(position), - options, - settings - ); - if (completions) { - completions.entries = completions.entries.map((c) => { - if (c.replacementSpan) { - return { - ...c, - replacementSpan: { - ...c.replacementSpan, - start: toOriginalPos(c.replacementSpan.start).pos - } - }; - } - return c; - }); - } - } - - completions = - completions ?? getCompletionsAtPosition(fileName, position, options, settings); - if (!completions) { - // No completions hints at a top level export in the making - const source = ls.getProgram()?.getSourceFile(fileName); - const node = source && findNodeAtPosition(source, position); - if (node && isTopLevelExport(ts, node, source)) { - return { - entries: Object.entries(kitExports) - .filter(([, value]) => isKitRouteExportAllowedIn(basename(fileName), value)) - .map(([key, value]) => ({ - kind: ts.ScriptElementKind.constElement, - name: key, - labelDetails: { - description: value.documentation.map((d) => d.text).join('') - }, - sortText: '0', - data: { - __sveltekit: key, - exportName: key // TS needs this - } as any - })), - isGlobalCompletion: false, - isMemberCompletion: false, - isNewIdentifierLocation: false, - isIncomplete: true - }; - } - - return completions; - } - - // Add ./$types imports for SvelteKit since TypeScript is bad at it - if (basename(fileName).startsWith('+')) { - const $typeImports = new Map(); - for (const c of completions.entries) { - if (c.source?.includes('.svelte-kit/types') && c.data) { - $typeImports.set(c.name, c); - } - } - for (const $typeImport of $typeImports.values()) { - // resolve path from FileName to svelte-kit/types - // src/routes/foo/+page.svelte -> .svelte-kit/types/foo/$types.d.ts - const routesFolder = 'src/routes'; // TODO somehow get access to kit.files.routes in here - const relativeFileName = fileName.split(routesFolder)[1]?.slice(1); - - if (relativeFileName) { - const relativePath = - dirname(relativeFileName) === '.' ? '' : `${dirname(relativeFileName)}/`; - const modifiedSource = - $typeImport.source!.split('.svelte-kit/types')[0] + - // note the missing .d.ts at the end - TS wants it that way for some reason - `.svelte-kit/types/${routesFolder}/${relativePath}$types`; - completions.entries.push({ - ...$typeImport, - // Ensure it's sorted above the other imports - sortText: !isNaN(Number($typeImport.sortText)) - ? String(Number($typeImport.sortText) - 1) - : $typeImport.sortText, - source: modifiedSource, - data: { - ...$typeImport.data, - fileName: $typeImport.data!.fileName?.replace( - $typeImport.source!, - modifiedSource - ), - moduleSpecifier: $typeImport.data!.moduleSpecifier?.replace( - $typeImport.source!, - modifiedSource - ), - __is_sveltekit$typeImport: true - } as any - }); - } - } - } - - return { - ...completions, - entries: completions.entries.map((entry) => { - if ( - !isSvelteFilePath(entry.source || '') || - !entry.name.endsWith(componentPostfix) - ) { - return entry; - } - return { - ...entry, - insertText: entry.insertText?.replace(componentPostfix, ''), - name: entry.name.slice(0, -componentPostfix.length) - }; - }) - }; - }; - - const getCompletionEntryDetails = ls.getCompletionEntryDetails; - ls.getCompletionEntryDetails = ( - fileName, - position, - entryName, - formatOptions, - source, - preferences, - data - ) => { - if ((data as any)?.__sveltekit) { - const key = (data as any)?.__sveltekit; - return { - name: key, - kind: ts.ScriptElementKind.constElement, - kindModifiers: ts.ScriptElementKindModifier.none, - displayParts: kitExports[key].displayParts, - documentation: kitExports[key].documentation - }; - } - - const is$typeImport = (data as any)?.__is_sveltekit$typeImport; - let details: ts.CompletionEntryDetails | undefined; - - const result = getVirtualLS(fileName, info, ts); - if (result) { - const { languageService, toVirtualPos, toOriginalPos } = result; - details = languageService.getCompletionEntryDetails( - fileName, - toVirtualPos(position), - entryName, - formatOptions, - source, - preferences, - data - ); - if (details) { - details.codeActions = details.codeActions?.map((codeAction) => { - codeAction.changes = codeAction.changes.map((change) => { - change.textChanges = change.textChanges.map((textChange) => { - return { - ...textChange, - span: { - ...textChange.span, - start: toOriginalPos(textChange.span.start).pos - } - }; - }); - return change; - }); - return codeAction; - }); - } - } - - details = - details ?? - getCompletionEntryDetails( - fileName, - position, - entryName, - formatOptions, - source, - preferences, - data - ); - - if (details) { - if (is$typeImport) { - details.codeActions = details.codeActions?.map((codeAction) => { - codeAction.description = adjustPath(codeAction.description); - codeAction.changes = codeAction.changes.map((change) => { - change.textChanges = change.textChanges.map((textChange) => { - textChange.newText = adjustPath(textChange.newText); - return textChange; - }); - return change; - }); - return codeAction; - }); - return details; - } else if (isSvelteFilePath(source || '')) { - logger.debug('TS found Svelte Component import completion details'); - return replaceDeep(details, componentPostfix, ''); - } else { - return details; - } - } - if (!isSvelteFilePath(source || '')) { - return details; - } - - // In the completion list we removed the component postfix. Internally, - // the language service saved the list with the postfix, so details - // won't match anything. Therefore add it back and remove it afterwards again. - const svelteDetails = getCompletionEntryDetails( - fileName, - position, - entryName + componentPostfix, - formatOptions, - source, - preferences, - data - ); - if (!svelteDetails) { - return undefined; - } - logger.debug('Found Svelte Component import completion details'); - - return replaceDeep(svelteDetails, componentPostfix, ''); - }; - - const getSignatureHelpItems = ls.getSignatureHelpItems; - ls.getSignatureHelpItems = (fileName, position, options) => { - const result = getVirtualLS(fileName, info, ts); - if (result) { - const { languageService, toVirtualPos } = result; - return languageService.getSignatureHelpItems(fileName, toVirtualPos(position), options); - } - return getSignatureHelpItems(fileName, position, options); - }; -} - -function adjustPath(path: string) { - return path.replace( - /(['"])(.+?)['"]/, - // .js logic for node16 module resolution - (_match, quote, path) => `${quote}./$types${path.endsWith('.js') ? '.js' : ''}${quote}` - ); -} diff --git a/packages/typescript-plugin/src/language-service/definition.ts b/packages/typescript-plugin/src/language-service/definition.ts deleted file mode 100644 index 40f0d27b2..000000000 --- a/packages/typescript-plugin/src/language-service/definition.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { Logger } from '../logger'; -import { SvelteSnapshotManager } from '../svelte-snapshots'; -import { isNotNullOrUndefined, isSvelteFilePath } from '../utils'; -import { getVirtualLS } from './sveltekit'; - -type _ts = typeof ts; - -export function decorateGetDefinition( - ls: ts.LanguageService, - info: ts.server.PluginCreateInfo, - ts: _ts, - snapshotManager: SvelteSnapshotManager, - logger: Logger -): void { - const getDefinitionAndBoundSpan = ls.getDefinitionAndBoundSpan; - ls.getDefinitionAndBoundSpan = (fileName, position) => { - const definition = getDefinitionAndBoundSpan(fileName, position); - if (!definition?.definitions) { - return getKitDefinitions(ts, info, fileName, position); - } - - return { - ...definition, - definitions: definition.definitions - .map((def) => { - if (!isSvelteFilePath(def.fileName)) { - return def; - } - - let textSpan = snapshotManager - .get(def.fileName) - ?.getOriginalTextSpan(def.textSpan); - if (!textSpan) { - // Unmapped positions are for example the default export. - // Fall back to the start of the file to at least go to the correct file. - textSpan = { start: 0, length: 1 }; - } - return { - ...def, - textSpan, - // Spare the work for now - originalTextSpan: undefined, - contextSpan: undefined, - originalContextSpan: undefined - }; - }) - .filter(isNotNullOrUndefined) - }; - }; -} - -function getKitDefinitions( - ts: _ts, - info: ts.server.PluginCreateInfo, - fileName: string, - position: number -) { - const result = getVirtualLS(fileName, info, ts); - if (!result) return; - const { languageService, toOriginalPos, toVirtualPos } = result; - const virtualPos = toVirtualPos(position); - const definitions = languageService.getDefinitionAndBoundSpan(fileName, virtualPos); - if (!definitions) return; - // Assumption: This is only called when the original definitions didn't turn up anything. - // Therefore we are called on things like export function load ({ fetch }) . - // This means the textSpan needs conversion but none of the definitions because they are all referencing other files. - return { - ...definitions, - textSpan: { ...definitions.textSpan, start: toOriginalPos(definitions.textSpan.start).pos } - }; -} diff --git a/packages/typescript-plugin/src/language-service/diagnostics.ts b/packages/typescript-plugin/src/language-service/diagnostics.ts deleted file mode 100644 index d0d35cc68..000000000 --- a/packages/typescript-plugin/src/language-service/diagnostics.ts +++ /dev/null @@ -1,196 +0,0 @@ -import path from 'path'; -import { internalHelpers } from 'svelte2tsx'; -import type ts from 'typescript/lib/tsserverlibrary'; -import { Logger } from '../logger'; -import { findIdentifier, isSvelteFilePath } from '../utils'; -import { getVirtualLS, isKitRouteExportAllowedIn, kitExports } from './sveltekit'; - -type _ts = typeof ts; - -export function decorateDiagnostics( - ls: ts.LanguageService, - info: ts.server.PluginCreateInfo, - typescript: typeof ts, - logger: Logger -): void { - decorateSyntacticDiagnostics(ls, info, typescript, logger); - decorateSemanticDiagnostics(ls, info, typescript, logger); - decorateSuggestionDiagnostics(ls, info, typescript, logger); -} - -function decorateSyntacticDiagnostics( - ls: ts.LanguageService, - info: ts.server.PluginCreateInfo, - typescript: typeof ts, - logger: Logger -): void { - const getSyntacticDiagnostics = ls.getSyntacticDiagnostics; - ls.getSyntacticDiagnostics = (fileName: string) => { - // Diagnostics inside Svelte files are done - // by the svelte-language-server / Svelte for VS Code extension - if (isSvelteFilePath(fileName)) { - return []; - } - - const kitDiagnostics = getKitDiagnostics( - 'getSyntacticDiagnostics', - fileName, - info, - typescript, - logger - ); - return kitDiagnostics ?? getSyntacticDiagnostics(fileName); - }; -} - -function decorateSemanticDiagnostics( - ls: ts.LanguageService, - info: ts.server.PluginCreateInfo, - typescript: typeof ts, - logger: Logger -): void { - const getSemanticDiagnostics = ls.getSemanticDiagnostics; - ls.getSemanticDiagnostics = (fileName: string) => { - // Diagnostics inside Svelte files are done - // by the svelte-language-server / Svelte for VS Code extension - if (isSvelteFilePath(fileName)) { - return []; - } - - const kitDiagnostics = getKitDiagnostics( - 'getSemanticDiagnostics', - fileName, - info, - typescript, - logger - ); - return kitDiagnostics ?? getSemanticDiagnostics(fileName); - }; -} - -function decorateSuggestionDiagnostics( - ls: ts.LanguageService, - info: ts.server.PluginCreateInfo, - typescript: typeof ts, - logger: Logger -): void { - const getSuggestionDiagnostics = ls.getSuggestionDiagnostics; - ls.getSuggestionDiagnostics = (fileName: string) => { - // Diagnostics inside Svelte files are done - // by the svelte-language-server / Svelte for VS Code extension - if (isSvelteFilePath(fileName)) { - return []; - } - - const kitDiagnostics = getKitDiagnostics( - 'getSuggestionDiagnostics', - fileName, - info, - typescript, - logger - ); - return kitDiagnostics ?? getSuggestionDiagnostics(fileName); - }; -} - -function getKitDiagnostics< - T extends 'getSemanticDiagnostics' | 'getSuggestionDiagnostics' | 'getSyntacticDiagnostics' ->( - methodName: T, - fileName: string, - info: ts.server.PluginCreateInfo, - ts: _ts, - logger?: Logger -): ReturnType | undefined { - const result = getVirtualLS(fileName, info, ts, logger); - if (!result) return; - - const { languageService, toOriginalPos } = result; - - const diagnostics = []; - for (let diagnostic of languageService[methodName](fileName)) { - if (!diagnostic.start || !diagnostic.length) { - diagnostics.push(diagnostic); - continue; - } - - const mapped = toOriginalPos(diagnostic.start); - if (mapped.inGenerated) { - // If not "Cannot find module './$types' .." then filter out - if (diagnostic.code === 2307) { - diagnostic = { - ...diagnostic, - // adjust length so it doesn't spill over to the next line - length: 1, - messageText: - typeof diagnostic.messageText === 'string' && - diagnostic.messageText.includes('./$types') - ? diagnostic.messageText + - ` (this likely means that SvelteKit's type generation didn't run yet - try running it by executing 'npm run dev' or 'npm run build')` - : diagnostic.messageText - }; - } else if (diagnostic.code === 2694) { - diagnostic = { - ...diagnostic, - // adjust length so it doesn't spill over to the next line - length: 1, - messageText: - typeof diagnostic.messageText === 'string' && - diagnostic.messageText.includes('/$types') - ? diagnostic.messageText + - ` (this likely means that SvelteKit's generated types are out of date - try rerunning it by executing 'npm run dev' or 'npm run build')` - : diagnostic.messageText - }; - } else if (diagnostic.code === 2355) { - // A function whose declared type is neither 'void' nor 'any' must return a value - diagnostic = { - ...diagnostic, - // adjust length so it doesn't spill over to the next line - length: 1 - }; - } else { - continue; - } - } - - diagnostic = { - ...diagnostic, - start: mapped.pos - }; - - diagnostics.push(diagnostic); - } - - if (methodName === 'getSemanticDiagnostics') { - // We're in a Svelte file - check top level exports - // We're using the original file to have the correct position without mapping - const source = info.languageService.getProgram()?.getSourceFile(fileName); - const basename = path.basename(fileName); - const validExports = Object.keys(kitExports).filter((key) => - isKitRouteExportAllowedIn(basename, kitExports[key]) - ); - if (source && basename.startsWith('+')) { - const exports = internalHelpers.findExports(ts, source, /* irrelevant */ false); - for (const exportName of exports.keys()) { - if (!validExports.includes(exportName) && !exportName.startsWith('_')) { - const node = exports.get(exportName)!.node; - const identifier = findIdentifier(ts, node) ?? node; - - diagnostics.push({ - file: source, - start: identifier.getStart(), - length: identifier.getEnd() - identifier.getStart(), - messageText: `Invalid export '${exportName}' (valid exports are ${validExports.join( - ', ' - )}, or anything with a '_' prefix)`, - category: ts.DiagnosticCategory.Error, - code: 71001 // arbitrary - }); - } - } - } - } - - // @ts-ignore TS doesn't get the return type right - return diagnostics; -} diff --git a/packages/typescript-plugin/src/language-service/file-references.ts b/packages/typescript-plugin/src/language-service/file-references.ts deleted file mode 100644 index 78563f985..000000000 --- a/packages/typescript-plugin/src/language-service/file-references.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { isNotNullOrUndefined, isSvelteFilePath } from '../utils'; -import { SvelteSnapshotManager } from '../svelte-snapshots'; - -export function decorateFileReferences( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager -): void { - const getFileReferences = ls.getFileReferences; - ls.getFileReferences = (fileName: string) => { - const references = getFileReferences(fileName); - - return references - .map((ref) => { - if (!isSvelteFilePath(ref.fileName)) { - return ref; - } - - let textSpan = snapshotManager.get(ref.fileName)?.getOriginalTextSpan(ref.textSpan); - - if (!textSpan) { - return; - } - - return { - ...ref, - textSpan - }; - }) - .filter(isNotNullOrUndefined); - }; -} diff --git a/packages/typescript-plugin/src/language-service/find-references.ts b/packages/typescript-plugin/src/language-service/find-references.ts deleted file mode 100644 index 53406d157..000000000 --- a/packages/typescript-plugin/src/language-service/find-references.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { Logger } from '../logger'; -import { SvelteSnapshotManager } from '../svelte-snapshots'; -import { - get$storeOffsetOf$storeDeclaration, - isStoreVariableIn$storeDeclaration, - isNotNullOrUndefined, - isSvelteFilePath -} from '../utils'; - -export function decorateFindReferences( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager, - logger: Logger -): void { - decorateGetReferencesAtPosition(ls, snapshotManager, logger); - _decorateFindReferences(ls, snapshotManager, logger); -} - -function _decorateFindReferences( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager, - logger: Logger -) { - const findReferences = ls.findReferences; - - const getReferences = (fileName: string, position: number): ts.ReferenceEntry[] | undefined => - findReferences(fileName, position)?.reduce( - (acc, curr) => acc.concat(curr.references), - [] - ); - - ls.findReferences = (fileName, position) => { - const references = findReferences(fileName, position); - return references - ?.map((reference) => { - const snapshot = snapshotManager.get(reference.definition.fileName); - if (!isSvelteFilePath(reference.definition.fileName) || !snapshot) { - return { - ...reference, - references: mapReferences( - reference.references, - snapshotManager, - logger, - getReferences - ) - }; - } - - const textSpan = snapshot.getOriginalTextSpan(reference.definition.textSpan); - if (!textSpan) { - return null; - } - return { - definition: { - ...reference.definition, - textSpan, - // Spare the work for now - originalTextSpan: undefined - }, - references: mapReferences( - reference.references, - snapshotManager, - logger, - getReferences - ) - }; - }) - .filter(isNotNullOrUndefined); - }; -} - -function decorateGetReferencesAtPosition( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager, - logger: Logger -) { - const getReferencesAtPosition = ls.getReferencesAtPosition; - ls.getReferencesAtPosition = (fileName, position) => { - const references = getReferencesAtPosition(fileName, position); - return ( - references && - mapReferences(references, snapshotManager, logger, getReferencesAtPosition) - ); - }; -} - -function mapReferences( - references: ts.ReferenceEntry[], - snapshotManager: SvelteSnapshotManager, - logger: Logger, - getReferences: (fileName: string, position: number) => ts.ReferenceEntry[] | undefined -): ts.ReferenceEntry[] { - const additionalStoreReferences: ts.ReferenceEntry[] = []; - const mappedReferences: ts.ReferenceEntry[] = []; - - for (const reference of references) { - const snapshot = snapshotManager.get(reference.fileName); - if (!isSvelteFilePath(reference.fileName) || !snapshot) { - mappedReferences.push(reference); - continue; - } - - const textSpan = snapshot.getOriginalTextSpan(reference.textSpan); - if (textSpan) { - mappedReferences.push(mapReference(reference, textSpan)); - } else { - if (isStoreVariableIn$storeDeclaration(snapshot.getText(), reference.textSpan.start)) { - additionalStoreReferences.push( - ...(getReferences( - reference.fileName, - get$storeOffsetOf$storeDeclaration( - snapshot.getText(), - reference.textSpan.start - ) - ) || []) - ); - } - } - } - - for (const reference of additionalStoreReferences) { - // We know these are Svelte files - const snapshot = snapshotManager.get(reference.fileName)!; - - const textSpan = snapshot.getOriginalTextSpan(reference.textSpan); - if (!textSpan) { - continue; - } - - mappedReferences.push(mapReference(reference, textSpan)); - } - - return mappedReferences; - - function mapReference(reference: ts.ReferenceEntry, textSpan: ts.TextSpan) { - logger.debug('Find references; map textSpan: changed', reference.textSpan, 'to', textSpan); - - return { - ...reference, - textSpan, - // Spare the work for now - contextSpan: undefined, - originalTextSpan: undefined, - originalContextSpan: undefined - }; - } -} diff --git a/packages/typescript-plugin/src/language-service/host.ts b/packages/typescript-plugin/src/language-service/host.ts deleted file mode 100644 index 03c74ed60..000000000 --- a/packages/typescript-plugin/src/language-service/host.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; - -export function decorateLanguageServiceHost(host: ts.LanguageServiceHost) { - const originalReadDirectory = host.readDirectory?.bind(host); - host.readDirectory = originalReadDirectory - ? (path, extensions, exclude, include, depth) => { - const extensionsWithSvelte = extensions ? [...extensions, '.svelte'] : undefined; - - return originalReadDirectory(path, extensionsWithSvelte, exclude, include, depth); - } - : undefined; -} diff --git a/packages/typescript-plugin/src/language-service/hover.ts b/packages/typescript-plugin/src/language-service/hover.ts deleted file mode 100644 index 7e29850c1..000000000 --- a/packages/typescript-plugin/src/language-service/hover.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { Logger } from '../logger'; -import { findNodeAtPosition, isTopLevelExport } from '../utils'; -import { getVirtualLS, kitExports } from './sveltekit'; - -type _ts = typeof ts; - -export function decorateHover( - ls: ts.LanguageService, - info: ts.server.PluginCreateInfo, - ts: _ts, - logger: Logger -): void { - const getQuickInfoAtPosition = ls.getQuickInfoAtPosition; - - ls.getQuickInfoAtPosition = (fileName: string, position: number) => { - const result = getVirtualLS(fileName, info, ts); - if (!result) return getQuickInfoAtPosition(fileName, position); - - const { languageService, toOriginalPos, toVirtualPos } = result; - const virtualPos = toVirtualPos(position); - const quickInfo = languageService.getQuickInfoAtPosition(fileName, virtualPos); - if (!quickInfo) return quickInfo; - - const source = languageService.getProgram()?.getSourceFile(fileName); - const node = source && findNodeAtPosition(source, virtualPos); - if (node && isTopLevelExport(ts, node, source) && ts.isIdentifier(node)) { - const name = node.text; - if (name in kitExports) { - quickInfo.documentation = !quickInfo.documentation?.length - ? kitExports[name].documentation - : quickInfo.documentation; - } - } - - return { - ...quickInfo, - textSpan: { ...quickInfo.textSpan, start: toOriginalPos(quickInfo.textSpan.start).pos } - }; - }; -} diff --git a/packages/typescript-plugin/src/language-service/implementation.ts b/packages/typescript-plugin/src/language-service/implementation.ts deleted file mode 100644 index d719714c6..000000000 --- a/packages/typescript-plugin/src/language-service/implementation.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { Logger } from '../logger'; -import { SvelteSnapshotManager } from '../svelte-snapshots'; -import { isNotNullOrUndefined, isSvelteFilePath } from '../utils'; - -export function decorateGetImplementation( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager, - logger: Logger -): void { - const getImplementationAtPosition = ls.getImplementationAtPosition; - ls.getImplementationAtPosition = (fileName, position) => { - const implementation = getImplementationAtPosition(fileName, position); - return implementation - ?.map((impl) => { - if (!isSvelteFilePath(impl.fileName)) { - return impl; - } - - const textSpan = snapshotManager - .get(impl.fileName) - ?.getOriginalTextSpan(impl.textSpan); - if (!textSpan) { - return undefined; - } - - return { - ...impl, - textSpan, - // Spare the work for now - contextSpan: undefined, - originalTextSpan: undefined, - originalContextSpan: undefined - }; - }) - .filter(isNotNullOrUndefined); - }; -} diff --git a/packages/typescript-plugin/src/language-service/index.ts b/packages/typescript-plugin/src/language-service/index.ts deleted file mode 100644 index 98b3af210..000000000 --- a/packages/typescript-plugin/src/language-service/index.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { ConfigManager } from '../config-manager'; -import { Logger } from '../logger'; -import { SvelteSnapshotManager } from '../svelte-snapshots'; -import { isSvelteFilePath } from '../utils'; -import { decorateCallHierarchy } from './call-hierarchy'; -import { decorateCompletions } from './completions'; -import { decorateGetDefinition } from './definition'; -import { decorateDiagnostics } from './diagnostics'; -import { decorateFindReferences } from './find-references'; -import { decorateHover } from './hover'; -import { decorateGetImplementation } from './implementation'; -import { decorateInlayHints } from './inlay-hints'; -import { decorateRename } from './rename'; -import { decorateUpdateImports } from './update-imports'; -import { decorateLanguageServiceHost } from './host'; -import { decorateNavigateToItems } from './navigate-to-items'; -import { decorateFileReferences } from './file-references'; -import { decorateMoveToRefactoringFileSuggestions } from './move-to-file'; - -const patchedProject = new Set(); - -export function isPatched(project: ts.server.Project) { - return patchedProject.has(project.getProjectName()); -} - -export function decorateLanguageService( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager, - logger: Logger, - configManager: ConfigManager, - info: ts.server.PluginCreateInfo, - typescript: typeof ts, - onDispose: () => void -) { - patchedProject.add(info.project.getProjectName()); - - // Decorate using a proxy so we can dynamically enable/disable method - // patches depending on the enabled state of our config - const proxy = new Proxy(ls, createProxyHandler(configManager)); - decorateLanguageServiceHost(info.languageServiceHost); - decorateLanguageServiceInner(proxy, snapshotManager, logger, info, typescript, onDispose); - - return proxy; -} - -function decorateLanguageServiceInner( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager, - logger: Logger, - info: ts.server.PluginCreateInfo, - typescript: typeof ts, - onDispose: () => void -): ts.LanguageService { - patchLineColumnOffset(ls, snapshotManager); - decorateRename(ls, snapshotManager, logger); - decorateDiagnostics(ls, info, typescript, logger); - decorateFindReferences(ls, snapshotManager, logger); - decorateCompletions(ls, info, typescript, logger); - decorateGetDefinition(ls, info, typescript, snapshotManager, logger); - decorateGetImplementation(ls, snapshotManager, logger); - decorateUpdateImports(ls, snapshotManager, logger); - decorateCallHierarchy(ls, snapshotManager, typescript); - decorateHover(ls, info, typescript, logger); - decorateInlayHints(ls, info, typescript, logger); - decorateNavigateToItems(ls, snapshotManager); - decorateFileReferences(ls, snapshotManager); - decorateMoveToRefactoringFileSuggestions(ls); - decorateDispose(ls, info.project, onDispose); - return ls; -} - -function createProxyHandler(configManager: ConfigManager): ProxyHandler { - const decorated: Partial = {}; - - return { - get(target, p) { - // always check for decorated dispose - if (!configManager.getConfig().enable && p !== 'dispose') { - return target[p as keyof ts.LanguageService]; - } - - return ( - decorated[p as keyof ts.LanguageService] ?? target[p as keyof ts.LanguageService] - ); - }, - set(_, p, value) { - decorated[p as keyof ts.LanguageService] = value; - - return true; - } - }; -} - -function patchLineColumnOffset(ls: ts.LanguageService, snapshotManager: SvelteSnapshotManager) { - if (!ls.toLineColumnOffset) { - return; - } - - // We need to patch this because (according to source, only) getDefinition uses this - const toLineColumnOffset = ls.toLineColumnOffset; - ls.toLineColumnOffset = (fileName, position) => { - if (isSvelteFilePath(fileName)) { - const snapshot = snapshotManager.get(fileName); - if (snapshot) { - return snapshot.positionAt(position); - } - } - return toLineColumnOffset(fileName, position); - }; -} - -function decorateDispose( - ls: ts.LanguageService, - project: ts.server.Project, - onDispose: () => void -) { - const dispose = ls.dispose; - - ls.dispose = () => { - patchedProject.delete(project.getProjectName()); - onDispose(); - dispose(); - }; - - return ls; -} diff --git a/packages/typescript-plugin/src/language-service/inlay-hints.ts b/packages/typescript-plugin/src/language-service/inlay-hints.ts deleted file mode 100644 index bc108abae..000000000 --- a/packages/typescript-plugin/src/language-service/inlay-hints.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { Logger } from '../logger'; -import { getVirtualLS } from './sveltekit'; - -type _ts = typeof ts; - -export function decorateInlayHints( - ls: ts.LanguageService, - info: ts.server.PluginCreateInfo, - ts: _ts, - logger: Logger -): void { - const provideInlayHints = ls.provideInlayHints; - ls.provideInlayHints = (fileName, span, preferences) => { - const result = getVirtualLS(fileName, info, ts); - if (!result) { - return provideInlayHints(fileName, span, preferences); - } - - const { languageService, toVirtualPos, toOriginalPos } = result; - return languageService - .provideInlayHints( - fileName, - { - start: toVirtualPos(span.start), - length: span.length - }, - preferences - ) - .map((hint) => ({ - ...hint, - position: toOriginalPos(hint.position).pos - })); - }; -} diff --git a/packages/typescript-plugin/src/language-service/move-to-file.ts b/packages/typescript-plugin/src/language-service/move-to-file.ts deleted file mode 100644 index 858ecd66a..000000000 --- a/packages/typescript-plugin/src/language-service/move-to-file.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; - -export function decorateMoveToRefactoringFileSuggestions(ls: ts.LanguageService): void { - const getMoveToRefactoringFileSuggestions = ls.getMoveToRefactoringFileSuggestions; - - ls.getMoveToRefactoringFileSuggestions = ( - fileName, - positionOrRange, - preferences, - triggerReason, - kind - ) => { - const program = ls.getProgram(); - - if (!program) { - return getMoveToRefactoringFileSuggestions( - fileName, - positionOrRange, - preferences, - triggerReason, - kind - ); - } - - const getSourceFiles = program.getSourceFiles; - try { - // typescript currently only allows js/ts files to be moved to. - // Once there isn't a restriction anymore, we can remove this. - program.getSourceFiles = () => - getSourceFiles().filter((file) => !file.fileName.endsWith('.svelte')); - - return getMoveToRefactoringFileSuggestions( - fileName, - positionOrRange, - preferences, - triggerReason, - kind - ); - } finally { - program.getSourceFiles = getSourceFiles; - } - }; -} diff --git a/packages/typescript-plugin/src/language-service/navigate-to-items.ts b/packages/typescript-plugin/src/language-service/navigate-to-items.ts deleted file mode 100644 index 815c6e8b1..000000000 --- a/packages/typescript-plugin/src/language-service/navigate-to-items.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { isGeneratedSvelteComponentName, isNotNullOrUndefined, isSvelteFilePath } from '../utils'; -import { SvelteSnapshotManager } from '../svelte-snapshots'; - -export function decorateNavigateToItems( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager -): void { - const getNavigateToItems = ls.getNavigateToItems; - ls.getNavigateToItems = ( - searchValue: string, - maxResultCount?: number, - fileName?: string, - excludeDtsFiles?: boolean - ) => { - const navigationToItems = getNavigateToItems( - searchValue, - maxResultCount, - fileName, - excludeDtsFiles - ); - - return navigationToItems - .map((item) => { - if (!isSvelteFilePath(item.fileName)) { - return item; - } - - if ( - item.name.startsWith('__sveltets_') || - (item.name === 'render' && !item.containerName) - ) { - return; - } - - let textSpan = snapshotManager - .get(item.fileName) - ?.getOriginalTextSpan(item.textSpan); - - if (!textSpan) { - if (isGeneratedSvelteComponentName(item.name)) { - textSpan = { start: 0, length: 1 }; - } else { - return; - } - } - - return { - ...item, - textSpan - }; - }) - .filter(isNotNullOrUndefined); - }; -} diff --git a/packages/typescript-plugin/src/language-service/rename.ts b/packages/typescript-plugin/src/language-service/rename.ts deleted file mode 100644 index 5abf5b360..000000000 --- a/packages/typescript-plugin/src/language-service/rename.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { Logger } from '../logger'; -import { SvelteSnapshotManager } from '../svelte-snapshots'; -import { - get$storeOffsetOf$storeDeclaration, - isStoreVariableIn$storeDeclaration, - isSvelteFilePath -} from '../utils'; - -export function decorateRename( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager, - logger: Logger -): void { - const findRenameLocations = ls.findRenameLocations; - ls.findRenameLocations = ( - fileName, - position, - findInStrings, - findInComments, - providePrefixAndSuffixTextForRename - ) => { - const renameLocations = findRenameLocations( - fileName, - position, - findInStrings, - findInComments, - // @ts-expect-error overload shenanigans - providePrefixAndSuffixTextForRename - ); - if (!renameLocations) { - return undefined; - } - - const convertedRenameLocations: ts.RenameLocation[] = []; - const additionalStoreRenameLocations: ts.RenameLocation[] = []; - - for (const renameLocation of renameLocations) { - const snapshot = snapshotManager.get(renameLocation.fileName); - if (!isSvelteFilePath(renameLocation.fileName) || !snapshot) { - convertedRenameLocations.push(renameLocation); - continue; - } - - // TODO more needed to filter invalid locations, see RenameProvider - const textSpan = snapshot.getOriginalTextSpan(renameLocation.textSpan); - if (!textSpan) { - if ( - isStoreVariableIn$storeDeclaration( - snapshot.getText(), - renameLocation.textSpan.start - ) - ) { - additionalStoreRenameLocations.push( - ...findRenameLocations( - renameLocation.fileName, - get$storeOffsetOf$storeDeclaration( - snapshot.getText(), - renameLocation.textSpan.start - ), - false, - false, - false - )! - ); - } - continue; - } - - convertedRenameLocations.push(convert(renameLocation, textSpan)); - } - - for (const renameLocation of additionalStoreRenameLocations) { - // We know these are Svelte files - const snapshot = snapshotManager.get(renameLocation.fileName)!; - - const textSpan = snapshot.getOriginalTextSpan(renameLocation.textSpan); - if (!textSpan) { - continue; - } - - // |$store| would be renamed, make it $|store| - textSpan.start += 1; - textSpan.length -= 1; - convertedRenameLocations.push(convert(renameLocation, textSpan)); - } - - return convertedRenameLocations; - }; - - function convert(renameLocation: ts.RenameLocation, textSpan: ts.TextSpan) { - const converted = { - ...renameLocation, - textSpan - }; - if (converted.contextSpan) { - // Not important, spare the work - converted.contextSpan = undefined; - } - logger.debug('Converted rename location ', converted); - return converted; - } -} diff --git a/packages/typescript-plugin/src/language-service/sveltekit.ts b/packages/typescript-plugin/src/language-service/sveltekit.ts deleted file mode 100644 index b7c1e4945..000000000 --- a/packages/typescript-plugin/src/language-service/sveltekit.ts +++ /dev/null @@ -1,825 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { Logger } from '../logger'; -import { hasNodeModule } from '../utils'; -import { InternalHelpers, internalHelpers } from 'svelte2tsx'; -type _ts = typeof ts; - -interface KitSnapshot { - file: ts.IScriptSnapshot; - version: string; - addedCode: InternalHelpers.AddedCode[]; -} - -const cache = new WeakMap< - ts.server.PluginCreateInfo, - { - languageService: ts.LanguageService; - languageServiceHost: ts.LanguageServiceHost & { - getKitScriptSnapshotIfUpToDate: (fileName: string) => KitSnapshot | undefined; - upsertKitFile: (fileName: string) => void; - }; - } | null ->(); - -function createApiExport(name: string) { - return { - allowedIn: ['api', 'server'] as ['api', 'server'], - displayParts: [ - { - text: 'export', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'async', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'function', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: name, - kind: 'localName' - }, - { - text: '(', - kind: 'punctuation' - }, - { - text: 'event', - kind: 'parameterName' - }, - { - text: ': ', - kind: 'punctuation' - }, - { - text: 'RequestEvent', - kind: 'interfaceName' - }, - { - text: ')', - kind: 'punctuation' - }, - { - text: ': ', - kind: 'punctuation' - }, - { - text: 'Promise', - kind: 'keyword' - }, - { - text: '<', - kind: 'punctuation' - }, - { - text: 'Response', - kind: 'interfaceName' - }, - { - text: '>', - kind: 'punctuation' - } - ], - documentation: [ - { - text: `Handles ${name} requests. More info: https://kit.svelte.dev/docs/routing#server`, - kind: 'text' - } - ] - }; -} - -export const kitExports: Record< - string, - { - displayParts: ts.SymbolDisplayPart[]; - documentation: ts.SymbolDisplayPart[]; - allowedIn: Array<'server' | 'universal' | 'layout' | 'page' | 'api'>; - } -> = { - prerender: { - allowedIn: ['layout', 'page', 'api', 'server', 'universal'], - displayParts: [ - { - text: 'const', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'prerender', - kind: 'localName' - }, - { - text: ': ', - kind: 'punctuation' - }, - { - text: 'boolean', - kind: 'keyword' - }, - { - text: ' | ', - kind: 'punctuation' - }, - { - text: "'auto'", - kind: 'stringLiteral' - } - ], - documentation: [ - { - text: 'Control whether or not this page is prerendered. More info: https://kit.svelte.dev/docs/page-options#prerender', - kind: 'text' - } - ] - }, - ssr: { - allowedIn: ['layout', 'page', 'server', 'universal'], - displayParts: [ - { - text: 'const', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'ssr', - kind: 'localName' - }, - { - text: ': ', - kind: 'punctuation' - }, - { - text: 'boolean', - kind: 'keyword' - } - ], - documentation: [ - { - text: 'Control whether or not this page is server-side rendered. More info: https://kit.svelte.dev/docs/page-options#ssr', - kind: 'text' - } - ] - }, - csr: { - allowedIn: ['layout', 'page', 'server', 'universal'], - displayParts: [ - { - text: 'const', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'csr', - kind: 'localName' - }, - { - text: ': ', - kind: 'punctuation' - }, - { - text: 'boolean', - kind: 'keyword' - } - ], - documentation: [ - { - text: 'Control whether or not this page is hydrated (i.e. if JS is delivered to the client). More info: https://kit.svelte.dev/docs/page-options#csr', - kind: 'text' - } - ] - }, - trailingSlash: { - allowedIn: ['layout', 'page', 'api', 'server', 'universal'], - displayParts: [ - { - text: 'const', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'trailingSlash', - kind: 'localName' - }, - { - text: ': ', - kind: 'punctuation' - }, - { - text: "'auto' | 'always' | 'never'", - kind: 'stringLiteral' - } - ], - documentation: [ - { - text: 'Control how SvelteKit should handle (missing) trailing slashes in the URL. More info: https://kit.svelte.dev/docs/page-options#trailingslash', - kind: 'text' - } - ] - }, - config: { - allowedIn: ['layout', 'page', 'api', 'server', 'universal'], - displayParts: [ - { - text: 'const', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'config', - kind: 'localName' - }, - { - text: ': ', - kind: 'punctuation' - }, - { - text: 'Config', - kind: 'interfaceName' - } - ], - documentation: [ - { - text: - `With the concept of adapters, SvelteKit is able to run on a variety of platforms. ` + - `Each of these might have specific configuration to further tweak the deployment, which you can configure here. ` + - `More info: https://kit.svelte.dev/docs/page-options#config`, - kind: 'text' - } - ] - }, - actions: { - allowedIn: ['page', 'server'], - displayParts: [ - { - text: 'const', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'actions', - kind: 'localName' - }, - { - text: ': ', - kind: 'punctuation' - }, - { - text: 'Actions', - kind: 'interfaceName' - } - ], - documentation: [ - { - text: - `An object of methods which handle form POST requests. ` + - `More info: https://kit.svelte.dev/docs/form-actions`, - kind: 'text' - } - ] - }, - load: { - allowedIn: ['layout', 'page', 'server', 'universal'], - displayParts: [ - { - text: 'export', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'function', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'load', - kind: 'localName' - }, - { - text: '(', - kind: 'punctuation' - }, - { - text: 'event', - kind: 'parameterName' - }, - { - text: ': ', - kind: 'punctuation' - }, - { - text: 'LoadEvent', - kind: 'interfaceName' - }, - { - text: ')', - kind: 'punctuation' - }, - { - text: ': ', - kind: 'punctuation' - }, - { - text: 'Promise', - kind: 'keyword' - }, - { - text: '<', - kind: 'punctuation' - }, - { - text: 'LoadOutput', - kind: 'interfaceName' - }, - { - text: '>', - kind: 'punctuation' - } - ], - documentation: [ - { - text: 'Loads data for the given page or layout. More info: https://kit.svelte.dev/docs/load', - kind: 'text' - } - ] - }, - entries: { - allowedIn: ['api', 'page', 'server', 'universal'], - displayParts: [ - { - text: 'export', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'function', - kind: 'keyword' - }, - { - text: ' ', - kind: 'space' - }, - { - text: 'entries', - kind: 'functionName' - }, - { - text: '() {}', - kind: 'punctuation' - } - ], - documentation: [ - { - text: - 'Generate values for dynamic parameters in prerendered pages.\n' + - 'More info: https://kit.svelte.dev/docs/page-options#entries', - kind: 'text' - } - ] - }, - GET: createApiExport('GET'), - POST: createApiExport('POST'), - PUT: createApiExport('PUT'), - PATCH: createApiExport('PATCH'), - DELETE: createApiExport('DELETE'), - OPTIONS: createApiExport('OPTIONS'), - HEAD: createApiExport('HEAD'), - fallback: createApiExport('fallback'), - // param matching - match: { - allowedIn: [], - displayParts: [], - documentation: [ - { - text: - `A parameter matcher. ` + - `More info: https://kit.svelte.dev/docs/advanced-routing#matching`, - kind: 'text' - } - ] - }, - // hooks - handle: { - allowedIn: [], - displayParts: [], - documentation: [ - { - text: - `The handle hook runs every time the SvelteKit server receives a request and determines the response. ` + - `It receives an 'event' object representing the request and a function called 'resolve', which renders the route and generates a Response. ` + - `This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example). ` + - `More info: https://kit.svelte.dev/docs/hooks#server-hooks-handle`, - kind: 'text' - } - ] - }, - handleFetch: { - allowedIn: [], - displayParts: [], - documentation: [ - { - text: - `The handleFetch hook allows you to modify (or replace) a 'fetch' request that happens inside a 'load' function that runs on the server (or during pre-rendering). ` + - `More info: https://kit.svelte.dev/docs/hooks#server-hooks-handlefetch`, - kind: 'text' - } - ] - }, - handleError: { - allowedIn: [], - displayParts: [], - documentation: [ - { - text: - `The handleError hook runs when an unexpected error is thrown while responding to a request. ` + - `If an unexpected error is thrown during loading or rendering, this function will be called with the error and the event. ` + - `Make sure that this function _never_ throws an error. ` + - `More info: https://kit.svelte.dev/docs/hooks#shared-hooks-handleerror`, - kind: 'text' - } - ] - } -}; - -const FORCE_UPDATE_VERSION = 'FORCE_UPDATE_VERSION'; - -export function isKitRouteExportAllowedIn( - basename: string, - kitExport: (typeof kitExports)[keyof typeof kitExports] -) { - if (!basename.startsWith('+')) { - return false; - } - - const allowedIn = kitExport.allowedIn; - return ( - (basename.includes('layout') - ? allowedIn.includes('layout') - : basename.includes('+server') - ? allowedIn.includes('api') - : allowedIn.includes('page')) && - (basename.includes('server') - ? allowedIn.includes('server') - : allowedIn.includes('universal')) - ); -} - -function getProxiedLanguageService(info: ts.server.PluginCreateInfo, ts: _ts, logger?: Logger) { - const cachedProxiedLanguageService = cache.get(info); - if (cachedProxiedLanguageService !== undefined) { - return cachedProxiedLanguageService ?? undefined; - } - - if (!hasNodeModule(info.project.getCompilerOptions(), '@sveltejs/kit')) { - // Not a SvelteKit project, do nothing - cache.set(info, null); - return; - } - - const originalLanguageServiceHost = info.languageServiceHost; - - class ProxiedLanguageServiceHost implements ts.LanguageServiceHost { - private files: Record = {}; - paramsPath = 'src/params'; - serverHooksPath = 'src/hooks.server'; - clientHooksPath = 'src/hooks.client'; - - constructor() { - const configPath = info.project.getCurrentDirectory() + '/svelte.config.js'; - import(configPath) - .then((module) => { - const config = module.default; - if (config.kit && config.kit.files) { - if (config.kit.files.params) { - this.paramsPath = config.kit.files.params; - } - if (config.kit.files.hooks) { - this.serverHooksPath ||= config.kit.files.hooks.server; - this.clientHooksPath ||= config.kit.files.hooks.client; - } - // We could be more sophisticated with only removing the files that are actually - // wrong but this is good enough given how rare it is that this setting is used - Object.keys(this.files) - .filter((name) => { - return !name.includes('src/hooks') && !name.includes('src/params'); - }) - .forEach((name) => { - delete this.files[name]; - }); - } - }) - .catch(() => {}); - } - - log() {} - - trace() {} - - error() {} - - getCompilationSettings() { - return originalLanguageServiceHost.getCompilationSettings(); - } - - getCurrentDirectory() { - return originalLanguageServiceHost.getCurrentDirectory(); - } - - getDefaultLibFileName(o: any) { - return originalLanguageServiceHost.getDefaultLibFileName(o); - } - - resolveModuleNames = originalLanguageServiceHost.resolveModuleNames - ? (...args: any[]) => { - return originalLanguageServiceHost.resolveModuleNames!( - // @ts-ignore - ...args - ); - } - : undefined; - - resolveModuleNameLiterals = originalLanguageServiceHost.resolveModuleNameLiterals - ? (...args: any[]) => { - return originalLanguageServiceHost.resolveModuleNameLiterals!( - // @ts-ignore - ...args - ); - } - : undefined; - - getScriptVersion(fileName: string) { - const file = this.files[fileName]; - if (!file) return originalLanguageServiceHost.getScriptVersion(fileName); - return file.version.toString(); - } - - getScriptSnapshot(fileName: string) { - const file = this.files[fileName]; - if (!file) return originalLanguageServiceHost.getScriptSnapshot(fileName); - return file.file; - } - - getScriptFileNames(): string[] { - const names: Set = new Set(Object.keys(this.files)); - const files = originalLanguageServiceHost.getScriptFileNames(); - for (const file of files) { - names.add(file); - } - return [...names]; - } - - getKitScriptSnapshotIfUpToDate(fileName: string) { - const scriptVersion = this.getScriptVersion(fileName); - if ( - !this.files[fileName] || - scriptVersion !== originalLanguageServiceHost.getScriptVersion(fileName) || - scriptVersion === FORCE_UPDATE_VERSION - ) { - return undefined; - } - return this.files[fileName]; - } - - upsertKitFile(fileName: string) { - const result = internalHelpers.upsertKitFile( - ts, - fileName, - { - clientHooksPath: this.clientHooksPath, - paramsPath: this.paramsPath, - serverHooksPath: this.serverHooksPath - }, - () => info.languageService.getProgram()?.getSourceFile(fileName) - ); - if (!result) { - return; - } - - const { text, addedCode } = result; - const snap = ts.ScriptSnapshot.fromString(text); - snap.getChangeRange = (_) => undefined; - - // If this is a new file, typescript might have cached the unpatched version - // It won't update even if we return the patched version in getScriptSnapshot, so we force an update - // This should only happen to files that are opened by the client after the first compilation of the proxy language service - // and won't happen if there are any updates to the file afterwards - this.files[fileName] = { - version: - this.files[fileName] === undefined - ? FORCE_UPDATE_VERSION - : originalLanguageServiceHost.getScriptVersion(fileName), - file: snap, - addedCode - }; - return this.files[fileName]; - } - - // needed for path auto completions - readDirectory = originalLanguageServiceHost.readDirectory - ? (...args: Parameters>) => { - return originalLanguageServiceHost.readDirectory!(...args); - } - : undefined; - - getDirectories = originalLanguageServiceHost.getDirectories - ? (...args: Parameters>) => { - return originalLanguageServiceHost.getDirectories!(...args); - } - : undefined; - - readFile(fileName: string) { - const file = this.files[fileName]; - return file - ? file.file.getText(0, file.file.getLength()) - : originalLanguageServiceHost.readFile(fileName); - } - - fileExists(fileName: string) { - return ( - this.files[fileName] !== undefined || - originalLanguageServiceHost.fileExists(fileName) - ); - } - - getCancellationToken = originalLanguageServiceHost.getCancellationToken - ? () => originalLanguageServiceHost.getCancellationToken!() - : undefined; - - getNewLine = originalLanguageServiceHost.getNewLine - ? () => originalLanguageServiceHost.getNewLine!() - : undefined; - - useCaseSensitiveFileNames = originalLanguageServiceHost.useCaseSensitiveFileNames - ? () => originalLanguageServiceHost.useCaseSensitiveFileNames!() - : undefined; - - realpath = originalLanguageServiceHost.realpath - ? (...args: Parameters>) => - originalLanguageServiceHost.realpath!(...args) - : undefined; - } - - // Ideally we'd create a full Proxy of the language service, but that seems to have cache issues - // with diagnostics, which makes positions go out of sync. - const languageServiceHost = new ProxiedLanguageServiceHost(); - const languageService = ts.createLanguageService( - languageServiceHost, - createProxyRegistry(ts, originalLanguageServiceHost, languageServiceHost) - ); - cache.set(info, { languageService, languageServiceHost }); - return { - languageService, - languageServiceHost - }; -} - -function createProxyRegistry( - ts: _ts, - originalLanguageServiceHost: ts.LanguageServiceHost, - options: InternalHelpers.KitFilesSettings -) { - // Don't destructure options param, as the value may be mutated through a svelte.config.js later - const registry = ts.createDocumentRegistry(); - return registry; - // TODO check why this fails on linux and reenable later - // const originalRegistry = (originalLanguageServiceHost as any).documentRegistry; - // const proxyRegistry: ts.DocumentRegistry = { - // ...originalRegistry, - // acquireDocumentWithKey( - // fileName, - // tsPath, - // compilationSettingsOrHost, - // key, - // scriptSnapshot, - // version, - // scriptKind, - // sourceFileOptions - // ) { - // if (internalHelpers.isKitFile(fileName, options)) { - // return registry.acquireDocumentWithKey( - // fileName, - // tsPath, - // compilationSettingsOrHost, - // key, - // scriptSnapshot, - // version, - // scriptKind, - // sourceFileOptions - // ); - // } - - // return originalRegistry.acquireDocumentWithKey( - // fileName, - // tsPath, - // compilationSettingsOrHost, - // key, - // scriptSnapshot, - // version, - // scriptKind, - // sourceFileOptions - // ); - // }, - // updateDocumentWithKey( - // fileName, - // tsPath, - // compilationSettingsOrHost, - // key, - // scriptSnapshot, - // version, - // scriptKind, - // sourceFileOptions - // ) { - // if (internalHelpers.isKitFile(fileName, options)) { - // return registry.updateDocumentWithKey( - // fileName, - // tsPath, - // compilationSettingsOrHost, - // key, - // scriptSnapshot, - // version, - // scriptKind, - // sourceFileOptions - // ); - // } - - // return originalRegistry.updateDocumentWithKey( - // fileName, - // tsPath, - // compilationSettingsOrHost, - // key, - // scriptSnapshot, - // version, - // scriptKind, - // sourceFileOptions - // ); - // } - // }; - - // return proxyRegistry; -} - -export function getVirtualLS( - fileName: string, - info: ts.server.PluginCreateInfo, - ts: _ts, - logger?: Logger -) { - const proxy = getProxiedLanguageService(info, ts, logger); - if (!proxy) { - return; - } - - const result = - proxy.languageServiceHost.getKitScriptSnapshotIfUpToDate(fileName) ?? - proxy.languageServiceHost.upsertKitFile(fileName); - - if (result) { - return { - languageService: proxy.languageService, - addedCode: result.addedCode, - toVirtualPos: (pos: number) => internalHelpers.toVirtualPos(pos, result.addedCode), - toOriginalPos: (pos: number) => internalHelpers.toOriginalPos(pos, result.addedCode) - }; - } -} diff --git a/packages/typescript-plugin/src/language-service/update-imports.ts b/packages/typescript-plugin/src/language-service/update-imports.ts deleted file mode 100644 index 507b963f5..000000000 --- a/packages/typescript-plugin/src/language-service/update-imports.ts +++ /dev/null @@ -1,44 +0,0 @@ -import path from 'path'; -import type ts from 'typescript/lib/tsserverlibrary'; -import { Logger } from '../logger'; -import { SvelteSnapshotManager } from '../svelte-snapshots'; -import { isSvelteFilePath } from '../utils'; - -export function decorateUpdateImports( - ls: ts.LanguageService, - snapshotManager: SvelteSnapshotManager, - logger: Logger -): void { - const getEditsForFileRename = ls.getEditsForFileRename; - ls.getEditsForFileRename = (oldFilePath, newFilePath, formatOptions, preferences) => { - const renameLocations = getEditsForFileRename( - oldFilePath, - newFilePath, - formatOptions, - preferences - ); - return renameLocations - ?.filter((renameLocation) => { - // If a file move/rename of a TS/JS file results a Svelte file change, - // the Svelte extension will notice that, too, and adjusts the same imports. - // This results in duplicate adjustments or race conditions with conflicting text spans - // which can break imports in some cases. - // Therefore don't do any updates of Svelte files and and also no updates of mixed TS files - // and let the Svelte extension handle that. - return ( - !isSvelteFilePath(renameLocation.fileName) && - !renameLocation.textChanges.some((change) => change.newText.endsWith('.svelte')) - ); - }) - .map((renameLocation) => { - if (path.basename(renameLocation.fileName).startsWith('+')) { - // Filter out changes to './$type' imports for Kit route files, - // you'll likely want these to stay as-is - renameLocation.textChanges = renameLocation.textChanges.filter((change) => { - return !change.newText.includes('.svelte-kit/types/'); - }); - } - return renameLocation; - }); - }; -} diff --git a/packages/typescript-plugin/src/logger.ts b/packages/typescript-plugin/src/logger.ts deleted file mode 100644 index 76fa0e8e2..000000000 --- a/packages/typescript-plugin/src/logger.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; - -export class Logger { - constructor( - private tsLogService: ts.server.Logger, - suppressNonSvelteLogs = false, - private logDebug = false - ) { - if (suppressNonSvelteLogs) { - const log = this.tsLogService.info.bind(this.tsLogService); - this.tsLogService.info = (s: string) => { - if (s.startsWith('-Svelte Plugin-')) { - log(s); - } - }; - } - } - - log(...args: any[]) { - const str = args - .map((arg) => { - if (typeof arg === 'object') { - try { - return JSON.stringify(arg); - } catch (e) { - return '[object that cannot by stringified]'; - } - } - return arg; - }) - .join(' '); - this.tsLogService.info('-Svelte Plugin- ' + str); - } - - debug(...args: any[]) { - if (!this.logDebug) { - return; - } - this.log(...args); - } -} diff --git a/packages/typescript-plugin/src/module-loader.ts b/packages/typescript-plugin/src/module-loader.ts deleted file mode 100644 index 6da7c5cc9..000000000 --- a/packages/typescript-plugin/src/module-loader.ts +++ /dev/null @@ -1,262 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { ConfigManager } from './config-manager'; -import { Logger } from './logger'; -import { SvelteSnapshotManager } from './svelte-snapshots'; -import { createSvelteSys } from './svelte-sys'; -import { ensureRealSvelteFilePath, isVirtualSvelteFilePath } from './utils'; - -// TODO remove when we update to typescript 5.0 -declare module 'typescript/lib/tsserverlibrary' { - interface LanguageServiceHost { - /** @deprecated supply resolveModuleNameLiterals instead for resolution that can handle newer resolution modes like nodenext */ - resolveModuleNames?( - moduleNames: string[], - containingFile: string, - reusedNames: string[] | undefined, - redirectedReference: ts.ResolvedProjectReference | undefined, - options: ts.CompilerOptions, - containingSourceFile?: ts.SourceFile - ): (ts.ResolvedModule | undefined)[]; - resolveModuleNameLiterals?( - moduleLiterals: readonly ts.StringLiteralLike[], - containingFile: string, - redirectedReference: ts.ResolvedProjectReference | undefined, - options: ts.CompilerOptions, - containingSourceFile: ts.SourceFile, - reusedNames: readonly ts.StringLiteralLike[] | undefined - ): readonly ts.ResolvedModuleWithFailedLookupLocations[]; - } -} - -/** - * Caches resolved modules. - */ -class ModuleResolutionCache { - constructor(private readonly projectService: ts.server.ProjectService) {} - - private cache = new Map(); - - /** - * Tries to get a cached module. - */ - get(moduleName: string, containingFile: string): ts.ResolvedModuleFull | undefined { - return this.cache.get(this.getKey(moduleName, containingFile)); - } - - /** - * Caches resolved module, if it is not undefined. - */ - set( - moduleName: string, - containingFile: string, - resolvedModule: ts.ResolvedModuleFull | undefined - ) { - if (!resolvedModule) { - return; - } - this.cache.set(this.getKey(moduleName, containingFile), resolvedModule); - } - - /** - * Deletes module from cache. Call this if a file was deleted. - * @param resolvedModuleName full path of the module - */ - delete(resolvedModuleName: string): void { - resolvedModuleName = this.projectService.toCanonicalFileName(resolvedModuleName); - this.cache.forEach((val, key) => { - if ( - this.projectService.toCanonicalFileName(val.resolvedFileName) === resolvedModuleName - ) { - this.cache.delete(key); - } - }); - } - - clear() { - this.cache.clear(); - } - - private getKey(moduleName: string, containingFile: string) { - return ( - this.projectService.toCanonicalFileName(containingFile) + - ':::' + - this.projectService.toCanonicalFileName(ensureRealSvelteFilePath(moduleName)) - ); - } -} - -/** - * Creates a module loader than can also resolve `.svelte` files. - * - * The typescript language service tries to look up other files that are referenced in the currently open svelte file. - * For `.ts`/`.js` files this works, for `.svelte` files it does not by default. - * Reason: The typescript language service does not know about the `.svelte` file ending, - * so it assumes it's a normal typescript file and searches for files like `../Component.svelte.ts`, which is wrong. - * In order to fix this, we need to wrap typescript's module resolution and reroute all `.svelte.ts` file lookups to .svelte. - */ -export function patchModuleLoader( - logger: Logger, - snapshotManager: SvelteSnapshotManager, - typescript: typeof ts, - lsHost: ts.LanguageServiceHost, - project: ts.server.Project, - configManager: ConfigManager -): void { - const svelteSys = createSvelteSys(typescript, logger); - const moduleCache = new ModuleResolutionCache(project.projectService); - const origResolveModuleNames = lsHost.resolveModuleNames?.bind(lsHost); - const origResolveModuleNamLiterals = lsHost.resolveModuleNameLiterals?.bind(lsHost); - - if (lsHost.resolveModuleNameLiterals) { - lsHost.resolveModuleNameLiterals = resolveModuleNameLiterals; - } else { - lsHost.resolveModuleNames = resolveModuleNames; - } - - const origRemoveFile = project.removeFile.bind(project); - project.removeFile = (info, fileExists, detachFromProject) => { - logger.log('File is being removed. Delete from cache: ', info.fileName); - moduleCache.delete(info.fileName); - return origRemoveFile(info, fileExists, detachFromProject); - }; - - configManager.onConfigurationChanged(() => { - moduleCache.clear(); - }); - - function resolveModuleNames( - moduleNames: string[], - containingFile: string, - reusedNames: string[] | undefined, - redirectedReference: ts.ResolvedProjectReference | undefined, - compilerOptions: ts.CompilerOptions, - containingSourceFile?: ts.SourceFile - ): Array { - logger.debug('Resolving modules names for ' + containingFile); - // Try resolving all module names with the original method first. - // The ones that are undefined will be re-checked if they are a - // Svelte file and if so, are resolved, too. This way we can defer - // all module resolving logic except for Svelte files to TypeScript. - const resolved = - origResolveModuleNames?.( - moduleNames, - containingFile, - reusedNames, - redirectedReference, - compilerOptions, - containingSourceFile - ) || Array.from(Array(moduleNames.length)); - - if (!configManager.getConfig().enable) { - return resolved; - } - - return resolved.map((tsResolvedModule, idx) => { - const moduleName = moduleNames[idx]; - if (tsResolvedModule || !ensureRealSvelteFilePath(moduleName).endsWith('.svelte')) { - return tsResolvedModule; - } - - return resolveSvelteModuleNameFromCache(moduleName, containingFile, compilerOptions) - .resolvedModule; - }); - } - - function resolveSvelteModuleName( - name: string, - containingFile: string, - compilerOptions: ts.CompilerOptions - ): ts.ResolvedModuleFull | undefined { - const svelteResolvedModule = typescript.resolveModuleName( - name, - containingFile, - compilerOptions, - svelteSys - // don't set mode or else .svelte imports couldn't be resolved - ).resolvedModule; - if ( - !svelteResolvedModule || - !isVirtualSvelteFilePath(svelteResolvedModule.resolvedFileName) - ) { - return svelteResolvedModule; - } - - const resolvedFileName = ensureRealSvelteFilePath(svelteResolvedModule.resolvedFileName); - logger.log('Resolved', name, 'to Svelte file', resolvedFileName); - const snapshot = snapshotManager.create(resolvedFileName); - if (!snapshot) { - return undefined; - } - - const resolvedSvelteModule: ts.ResolvedModuleFull = { - extension: snapshot.isTsFile ? typescript.Extension.Ts : typescript.Extension.Js, - resolvedFileName, - isExternalLibraryImport: svelteResolvedModule.isExternalLibraryImport - }; - return resolvedSvelteModule; - } - - function resolveModuleNameLiterals( - moduleLiterals: readonly ts.StringLiteralLike[], - containingFile: string, - redirectedReference: ts.ResolvedProjectReference | undefined, - options: ts.CompilerOptions, - containingSourceFile: ts.SourceFile, - reusedNames: readonly ts.StringLiteralLike[] | undefined - ): readonly ts.ResolvedModuleWithFailedLookupLocations[] { - logger.debug('Resolving modules names for ' + containingFile); - // Try resolving all module names with the original method first. - // The ones that are undefined will be re-checked if they are a - // Svelte file and if so, are resolved, too. This way we can defer - // all module resolving logic except for Svelte files to TypeScript. - const resolved = - origResolveModuleNamLiterals?.( - moduleLiterals, - containingFile, - redirectedReference, - options, - containingSourceFile, - reusedNames - ) ?? - moduleLiterals.map( - (): ts.ResolvedModuleWithFailedLookupLocations => ({ - resolvedModule: undefined - }) - ); - - if (!configManager.getConfig().enable) { - return resolved; - } - - return resolved.map((tsResolvedModule, idx) => { - const moduleName = moduleLiterals[idx].text; - if ( - tsResolvedModule.resolvedModule || - !ensureRealSvelteFilePath(moduleName).endsWith('.svelte') - ) { - return tsResolvedModule; - } - - return resolveSvelteModuleNameFromCache(moduleName, containingFile, options); - }); - } - - function resolveSvelteModuleNameFromCache( - moduleName: string, - containingFile: string, - options: ts.CompilerOptions - ) { - const cachedModule = moduleCache.get(moduleName, containingFile); - if (cachedModule) { - return { - resolvedModule: cachedModule - }; - } - - const resolvedModule = resolveSvelteModuleName(moduleName, containingFile, options); - moduleCache.set(moduleName, containingFile, resolvedModule); - return { - resolvedModule: resolvedModule - }; - } -} diff --git a/packages/typescript-plugin/src/project-svelte-files.ts b/packages/typescript-plugin/src/project-svelte-files.ts deleted file mode 100644 index 58b315b30..000000000 --- a/packages/typescript-plugin/src/project-svelte-files.ts +++ /dev/null @@ -1,203 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { ConfigManager, Configuration } from './config-manager'; -import { SvelteSnapshotManager } from './svelte-snapshots'; -import { getConfigPathForProject, isSvelteFilePath } from './utils'; -import { Logger } from './logger'; - -export interface TsFilesSpec { - include?: readonly string[]; - exclude?: readonly string[]; -} - -export class ProjectSvelteFilesManager { - private projectFileToOriginalCasing = new Map(); - private directoryWatchers = new Set(); - - private static instances = new Map(); - - static getInstance(projectName: string) { - return this.instances.get(projectName); - } - - constructor( - private readonly typescript: typeof ts, - private readonly project: ts.server.Project, - private readonly serverHost: ts.server.ServerHost, - private readonly snapshotManager: SvelteSnapshotManager, - private readonly logger: Logger, - private parsedCommandLine: ts.ParsedCommandLine, - configManager: ConfigManager - ) { - if (configManager.getConfig().enable) { - this.setupWatchers(); - this.updateProjectSvelteFiles(); - } - - configManager.onConfigurationChanged(this.onConfigChanged.bind(this)); - ProjectSvelteFilesManager.instances.set(project.getProjectName(), this); - } - - updateProjectConfig(serviceHost: ts.LanguageServiceHost) { - const parsedCommandLine = serviceHost.getParsedCommandLine?.( - getConfigPathForProject(this.project) - ); - - if (!parsedCommandLine) { - return; - } - - this.disposeWatchers(); - this.clearProjectFile(); - this.parsedCommandLine = parsedCommandLine; - this.setupWatchers(); - this.updateProjectSvelteFiles(); - } - - getFiles() { - return Array.from(this.projectFileToOriginalCasing.values()); - } - - /** - * Create directory watcher for include and exclude - * The watcher in tsserver doesn't support svelte file - * It won't add new created svelte file to root - */ - private setupWatchers() { - for (const directory in this.parsedCommandLine.wildcardDirectories) { - if ( - !Object.prototype.hasOwnProperty.call( - this.parsedCommandLine.wildcardDirectories, - directory - ) - ) { - continue; - } - - const watchDirectoryFlags = this.parsedCommandLine.wildcardDirectories[directory]; - const watcher = this.serverHost.watchDirectory( - directory, - this.watcherCallback.bind(this), - watchDirectoryFlags === this.typescript.WatchDirectoryFlags.Recursive, - this.parsedCommandLine.watchOptions - ); - - this.directoryWatchers.add(watcher); - } - } - - private watcherCallback(fileName: string) { - if (!isSvelteFilePath(fileName)) { - return; - } - - // We can't just add the file to the project directly, because - // - the casing of fileName is different - // - we don't know whether the file was added or deleted - this.updateProjectSvelteFiles(); - } - - private updateProjectSvelteFiles() { - const fileNamesAfter = this.readProjectSvelteFilesFromFs().map((file) => ({ - originalCasing: file, - canonicalFileName: this.project.projectService.toCanonicalFileName(file) - })); - - const removedFiles = new Set(this.projectFileToOriginalCasing.keys()); - const newFiles: typeof fileNamesAfter = []; - - for (const file of fileNamesAfter) { - const existingFile = this.projectFileToOriginalCasing.get(file.canonicalFileName); - if (!existingFile) { - newFiles.push(file); - continue; - } - - removedFiles.delete(file.canonicalFileName); - if (existingFile !== file.originalCasing) { - this.projectFileToOriginalCasing.set(file.canonicalFileName, file.originalCasing); - } - } - - for (const newFile of newFiles) { - this.addFileToProject(newFile.originalCasing); - this.projectFileToOriginalCasing.set(newFile.canonicalFileName, newFile.originalCasing); - } - for (const removedFile of removedFiles) { - this.removeFileFromProject(removedFile, false); - this.projectFileToOriginalCasing.delete(removedFile); - } - } - - private addFileToProject(newFile: string) { - this.snapshotManager.create(newFile); - const snapshot = this.project.projectService.getScriptInfo(newFile); - - if (!snapshot) { - return; - } - - if (this.project.isRoot(snapshot)) { - this.logger.debug(`File ${newFile} is already in root`); - return; - } - - this.project.addRoot(snapshot); - } - - private readProjectSvelteFilesFromFs() { - const fileSpec: TsFilesSpec = this.parsedCommandLine.raw; - const { include, exclude } = fileSpec; - - if (include?.length === 0) { - return []; - } - - return this.typescript.sys - .readDirectory( - this.project.getCurrentDirectory() || process.cwd(), - ['.svelte'], - exclude, - include - ) - .map(this.typescript.server.toNormalizedPath); - } - - private onConfigChanged(config: Configuration) { - this.disposeWatchers(); - this.clearProjectFile(); - - if (config.enable) { - this.setupWatchers(); - this.updateProjectSvelteFiles(); - } - } - - private removeFileFromProject(file: string, exists = true) { - const info = this.project.getScriptInfo(file); - - if (info) { - this.project.removeFile(info, exists, true); - } - } - - private disposeWatchers() { - this.directoryWatchers.forEach((watcher) => watcher.close()); - this.directoryWatchers.clear(); - } - - private clearProjectFile() { - this.projectFileToOriginalCasing.forEach((file) => this.removeFileFromProject(file)); - this.projectFileToOriginalCasing.clear(); - } - - dispose() { - this.disposeWatchers(); - - // Don't remove files from the project here - // because TypeScript already does that when the project is closed - // - and because the project is closed, `project.removeFile` will result in an error - this.projectFileToOriginalCasing.clear(); - - ProjectSvelteFilesManager.instances.delete(this.project.getProjectName()); - } -} diff --git a/packages/typescript-plugin/src/source-mapper.ts b/packages/typescript-plugin/src/source-mapper.ts deleted file mode 100644 index 9e8814f2e..000000000 --- a/packages/typescript-plugin/src/source-mapper.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { decode } from '@jridgewell/sourcemap-codec'; -import type ts from 'typescript/lib/tsserverlibrary'; - -type LineChar = ts.LineAndCharacter; - -type FileMapping = LineMapping[]; - -type LineMapping = CharacterMapping[]; // FileMapping[generated_line_index] = LineMapping - -type CharacterMapping = [ - number, // generated character - number, // original file - number, // original line - number // original index -]; - -type ReorderedChar = [ - original_character: number, - generated_line: number, - generated_character: number -]; - -interface ReorderedMap { - [original_line: number]: ReorderedChar[]; -} - -function binaryInsert(array: number[], value: number): void; -function binaryInsert | number[]>( - array: T[], - value: T, - key: keyof T -): void; -function binaryInsert> | number[]>( - array: A, - value: A[any], - key?: keyof (A[any] & object) -) { - if (0 === key) { - key = '0' as keyof A[any]; - } - const index = 1 + binarySearch(array, (key ? value[key] : value) as number, key); - let i = array.length; - while (index !== i--) { - array[1 + i] = array[i]; - } - array[index] = value; -} - -function binarySearch( - array: T[], - target: number, - key?: keyof (T & object) -) { - if (!array || 0 === array.length) { - return -1; - } - if (0 === key) { - key = '0' as keyof T; - } - let low = 0; - let high = array.length - 1; - while (low <= high) { - const i = low + ((high - low) >> 1); - const item = (undefined === key ? array[i] : array[i][key]) as number; - if (item === target) { - return i; - } - if (item < target) { - low = i + 1; - } else { - high = i - 1; - } - } - if ((low = ~low) < 0) { - low = ~low - 1; - } - return low; -} - -export class SourceMapper { - private mappings: FileMapping; - private reverseMappings?: ReorderedMap; - - constructor(mappings: FileMapping | string) { - if (typeof mappings === 'string') { - this.mappings = decode(mappings) as FileMapping; - } else { - this.mappings = mappings; - } - } - - getOriginalPosition(position: LineChar): LineChar { - const lineMap = this.mappings[position.line]; - if (!lineMap) { - return { line: -1, character: -1 }; - } - - const closestMatch = binarySearch(lineMap, position.character, 0); - const match = lineMap[closestMatch]; - if (!match) { - return { line: -1, character: -1 }; - } - - const { 2: line, 3: character } = match; - return { line, character }; - } - - getGeneratedPosition(position: LineChar): LineChar { - if (!this.reverseMappings) { - this.computeReversed(); - } - const lineMap = this.reverseMappings![position.line]; - if (!lineMap) { - return { line: -1, character: -1 }; - } - - const closestMatch = binarySearch(lineMap, position.character, 0); - const match = lineMap[closestMatch]; - if (!match) { - return { line: -1, character: -1 }; - } - - const { 1: line, 2: character } = match; - return { line, character }; - } - - private computeReversed() { - this.reverseMappings = {} as ReorderedMap; - for (let generated_line = 0; generated_line !== this.mappings.length; generated_line++) { - for (const { 0: generated_index, 2: original_line, 3: original_character_index } of this - .mappings[generated_line]) { - const reordered_char: ReorderedChar = [ - original_character_index, - generated_line, - generated_index - ]; - if (original_line in this.reverseMappings) { - binaryInsert(this.reverseMappings[original_line], reordered_char, 0); - } else { - this.reverseMappings[original_line] = [reordered_char]; - } - } - } - } -} diff --git a/packages/typescript-plugin/src/svelte-snapshots.ts b/packages/typescript-plugin/src/svelte-snapshots.ts deleted file mode 100644 index ec3ff8aeb..000000000 --- a/packages/typescript-plugin/src/svelte-snapshots.ts +++ /dev/null @@ -1,427 +0,0 @@ -import { svelte2tsx } from 'svelte2tsx'; -import type ts from 'typescript/lib/tsserverlibrary'; -import { ConfigManager } from './config-manager'; -import { Logger } from './logger'; -import { SourceMapper } from './source-mapper'; -import { isNoTextSpanInGeneratedCode, isSvelteFilePath } from './utils'; - -export class SvelteSnapshot { - private scriptInfo?: ts.server.ScriptInfo; - private lineOffsets?: number[]; - private convertInternalCodePositions = false; - - constructor( - private typescript: typeof ts, - private fileName: string, - private svelteCode: string, - private mapper: SourceMapper, - private logger: Logger, - public readonly isTsFile: boolean - ) {} - - update(svelteCode: string, mapper: SourceMapper) { - this.svelteCode = svelteCode; - this.mapper = mapper; - this.lineOffsets = undefined; - this.log('Updated Snapshot'); - } - - getOriginalTextSpan(textSpan: ts.TextSpan): ts.TextSpan | null { - if (!isNoTextSpanInGeneratedCode(this.getText(), textSpan)) { - return null; - } - - const start = this.getOriginalOffset(textSpan.start); - if (start === -1) { - return null; - } - - // Assumption: We don't change identifiers itself, so we don't change ranges. - return { - start, - length: textSpan.length - }; - } - - getOriginalOffset(generatedOffset: number) { - if (!this.scriptInfo) { - return generatedOffset; - } - - this.toggleMappingMode(true); - const lineOffset = this.scriptInfo.positionToLineOffset(generatedOffset); - this.debug('try convert offset', generatedOffset, '/', lineOffset); - const original = this.mapper.getOriginalPosition({ - line: lineOffset.line - 1, - character: lineOffset.offset - 1 - }); - this.toggleMappingMode(false); - if (original.line === -1) { - return -1; - } - - const originalOffset = this.scriptInfo.lineOffsetToPosition( - original.line + 1, - original.character + 1 - ); - this.debug('converted offset to', original, '/', originalOffset); - return originalOffset; - } - - getGeneratedTextSpan(textSpan: ts.TextSpan): ts.TextSpan | null { - const start = this.getGeneratedOffset(textSpan.start); - if (start === -1) { - return null; - } - - // Assumption: We don't change identifiers itself, so we don't change ranges. - return { - start, - length: textSpan.length - }; - } - - getGeneratedOffset(originalOffset: number) { - if (!this.scriptInfo) { - return originalOffset; - } - - const lineOffset = this.scriptInfo.positionToLineOffset(originalOffset); - const original = this.mapper.getGeneratedPosition({ - line: lineOffset.line - 1, - character: lineOffset.offset - 1 - }); - if (original.line === -1) { - return -1; - } - - this.toggleMappingMode(true); - const generatedOffset = this.scriptInfo.lineOffsetToPosition( - original.line + 1, - original.character + 1 - ); - this.toggleMappingMode(false); - this.debug('converted offset to', original, '/', generatedOffset); - return generatedOffset; - } - - setAndPatchScriptInfo(scriptInfo: ts.server.ScriptInfo) { - // @ts-expect-error - scriptInfo.scriptKind = this.typescript.ScriptKind.TS; - - const positionToLineOffset = scriptInfo.positionToLineOffset.bind(scriptInfo); - scriptInfo.positionToLineOffset = (position) => { - if (this.convertInternalCodePositions) { - const lineOffset = positionToLineOffset(position); - this.debug('positionToLineOffset for generated code', position, lineOffset); - return lineOffset; - } - - const lineOffset = this.positionAt(position); - this.debug('positionToLineOffset for original code', position, lineOffset); - return { line: lineOffset.line + 1, offset: lineOffset.character + 1 }; - }; - - const lineOffsetToPosition = scriptInfo.lineOffsetToPosition.bind(scriptInfo); - scriptInfo.lineOffsetToPosition = (line, offset) => { - if (this.convertInternalCodePositions) { - const position = lineOffsetToPosition(line, offset); - this.debug('lineOffsetToPosition for generated code', { line, offset }, position); - return position; - } - - const position = this.offsetAt({ line: line - 1, character: offset - 1 }); - this.debug('lineOffsetToPosition for original code', { line, offset }, position); - return position; - }; - - // TODO do we need to patch this? - // const lineToTextSpan = scriptInfo.lineToTextSpan.bind(scriptInfo); - // scriptInfo.lineToTextSpan = (line) => { - // if (this.convertInternalCodePositions) { - // const span = lineToTextSpan(line); - // this.debug('lineToTextSpan for generated code', line, span); - // return span; - // } - - // const lineOffset = this.getLineOffsets(); - // const start = lineOffset[line - 1]; - // const span: ts.TextSpan = { - // start, - // length: (lineOffset[line] || this.svelteCode.length) - start - // }; - // this.debug('lineToTextSpan for original code', line, span); - // return span; - // }; - - this.scriptInfo = scriptInfo; - this.log('patched scriptInfo'); - } - - /** - * Get the line and character based on the offset - * @param offset The index of the position - */ - positionAt(offset: number): ts.LineAndCharacter { - offset = this.clamp(offset, 0, this.svelteCode.length); - - const lineOffsets = this.getLineOffsets(); - let low = 0; - let high = lineOffsets.length; - if (high === 0) { - return { line: 0, character: offset }; - } - - while (low < high) { - const mid = Math.floor((low + high) / 2); - if (lineOffsets[mid] > offset) { - high = mid; - } else { - low = mid + 1; - } - } - - // low is the least x for which the line offset is larger than the current offset - // or array.length if no line offset is larger than the current offset - const line = low - 1; - - return { line, character: offset - lineOffsets[line] }; - } - - /** - * Get the index of the line and character position - * @param position Line and character position - */ - offsetAt(position: ts.LineAndCharacter): number { - const lineOffsets = this.getLineOffsets(); - - if (position.line >= lineOffsets.length) { - return this.svelteCode.length; - } else if (position.line < 0) { - return 0; - } - - const lineOffset = lineOffsets[position.line]; - const nextLineOffset = - position.line + 1 < lineOffsets.length - ? lineOffsets[position.line + 1] - : this.svelteCode.length; - - return this.clamp(nextLineOffset, lineOffset, lineOffset + position.character); - } - - private getLineOffsets() { - if (this.lineOffsets) { - return this.lineOffsets; - } - - const lineOffsets = []; - const text = this.svelteCode; - let isLineStart = true; - - for (let i = 0; i < text.length; i++) { - if (isLineStart) { - lineOffsets.push(i); - isLineStart = false; - } - const ch = text.charAt(i); - isLineStart = ch === '\r' || ch === '\n'; - if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') { - i++; - } - } - - if (isLineStart && text.length > 0) { - lineOffsets.push(text.length); - } - - this.lineOffsets = lineOffsets; - return lineOffsets; - } - - private clamp(num: number, min: number, max: number): number { - return Math.max(min, Math.min(max, num)); - } - - private log(...args: any[]) { - this.logger.log('SvelteSnapshot:', this.fileName, '-', ...args); - } - - private debug(...args: any[]) { - this.logger.debug('SvelteSnapshot:', this.fileName, '-', ...args); - } - - private toggleMappingMode(convertInternalCodePositions: boolean) { - this.convertInternalCodePositions = convertInternalCodePositions; - } - - getText() { - const snapshot = this.scriptInfo?.getSnapshot(); - if (!snapshot) { - return ''; - } - return snapshot.getText(0, snapshot.getLength()); - } - - getOriginalText() { - return this.svelteCode; - } -} - -export class SvelteSnapshotManager { - private snapshots = new Map(); - - constructor( - private typescript: typeof ts, - private projectService: ts.server.ProjectService, - private svelteOptions: { namespace: string }, - private logger: Logger, - private configManager: ConfigManager - ) { - this.patchProjectServiceReadFile(); - } - - get(fileName: string) { - return this.snapshots.get(this.projectService.toCanonicalFileName(fileName)); - } - - create(fileName: string): SvelteSnapshot | undefined { - const canonicalFilePath = this.projectService.toCanonicalFileName(fileName); - if (this.snapshots.has(canonicalFilePath)) { - return this.snapshots.get(canonicalFilePath)!; - } - - // This will trigger projectService.host.readFile which is patched below - const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath( - this.typescript.server.toNormalizedPath(fileName), - false - ); - if (!scriptInfo) { - this.logger.log('Was not able get snapshot for', fileName); - return; - } - - try { - scriptInfo.getSnapshot(); // needed to trigger readFile - } catch (e) { - this.logger.log('Loading Snapshot failed', fileName); - } - const snapshot = this.snapshots.get(this.projectService.toCanonicalFileName(fileName)); - if (!snapshot) { - this.logger.log( - 'Svelte snapshot was not found after trying to load script snapshot for', - fileName - ); - return; // should never get here - } - snapshot.setAndPatchScriptInfo(scriptInfo); - this.snapshots.set(canonicalFilePath, snapshot); - return snapshot; - } - - private patchProjectServiceReadFile() { - // @ts-ignore The projectService is shared across some instances, make sure we patch readFile only once - if (!this.projectService.host[onReadSvelteFile]) { - this.logger.log('patching projectService host readFile'); - - // @ts-ignore - this.projectService.host[onReadSvelteFile] = []; - - const readFile = this.projectService.host.readFile; - this.projectService.host.readFile = (path: string, encoding?: string | undefined) => { - if (!this.configManager.getConfig().enable) { - return readFile(path, encoding); - } - - // The following (very hacky) first two checks make sure that the ambient module definitions - // that tell TS "every import ending with .svelte is a valid module" are removed. - // They exist in svelte2tsx and svelte to make sure that people don't - // get errors in their TS files when importing Svelte files and not using our TS plugin. - // If someone wants to get back the behavior they can add an ambient module definition - // on their own. - const normalizedPath = path.replace(/\\/g, '/'); - if (normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) { - return ''; - } else if (normalizedPath.endsWith('svelte2tsx/svelte-jsx.d.ts')) { - // Remove the dom lib reference to not load these ambient types in case - // the user has a tsconfig.json with different lib settings like in - // https://github.com/sveltejs/language-tools/issues/1733 - const originalText = readFile(path) || ''; - const toReplace = '/// '; - return originalText.replace(toReplace, ' '.repeat(toReplace.length)); - } else if (normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts')) { - let originalText = readFile(path) || ''; - if (!originalText.includes('// -- start svelte-ls-remove --')) { - return originalText; // uses an older version of svelte2tsx or is already patched - } - const startIdx = originalText.indexOf('// -- start svelte-ls-remove --'); - const endIdx = originalText.indexOf('// -- end svelte-ls-remove --'); - originalText = - originalText.substring(0, startIdx) + - ' '.repeat(endIdx - startIdx) + - originalText.substring(endIdx); - return originalText; - } else if (isSvelteFilePath(path)) { - this.logger.debug('Read Svelte file:', path); - const svelteCode = readFile(path) || ''; - const isTsFile = true; // TODO check file contents? TS might be okay with importing ts into js. - let code: string; - let mapper: SourceMapper; - - try { - const result = svelte2tsx(svelteCode, { - filename: path.split('/').pop(), - isTsFile, - mode: 'ts', - typingsNamespace: this.svelteOptions.namespace - }); - code = result.code; - mapper = new SourceMapper(result.map.mappings); - this.logger.log('Successfully read Svelte file contents of', path); - } catch (e) { - this.logger.log('Error loading Svelte file:', path, ' Using fallback.'); - this.logger.debug('Error:', e); - // Return something either way, else "X is not a module" errors will appear - // in the TS files that use this file. - code = 'export default class extends Svelte2TsxComponent {}'; - mapper = new SourceMapper(''); - } - - // @ts-ignore - this.projectService.host[onReadSvelteFile].forEach((listener) => - listener(path, svelteCode, isTsFile, mapper) - ); - - return code; - } else { - return readFile(path, encoding); - } - }; - } - - // @ts-ignore - this.projectService.host[onReadSvelteFile].push( - (path: string, svelteCode: string, isTsFile: boolean, mapper: SourceMapper) => { - const canonicalFilePath = this.projectService.toCanonicalFileName(path); - const existingSnapshot = this.snapshots.get(canonicalFilePath); - if (existingSnapshot) { - existingSnapshot.update(svelteCode, mapper); - } else { - this.snapshots.set( - canonicalFilePath, - new SvelteSnapshot( - this.typescript, - path, - svelteCode, - mapper, - this.logger, - isTsFile - ) - ); - } - } - ); - } -} - -const onReadSvelteFile = Symbol('sveltePluginPatchSymbol'); diff --git a/packages/typescript-plugin/src/svelte-sys.ts b/packages/typescript-plugin/src/svelte-sys.ts deleted file mode 100644 index ca0f60ede..000000000 --- a/packages/typescript-plugin/src/svelte-sys.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type ts from 'typescript'; -import { Logger } from './logger'; -import { ensureRealSvelteFilePath, isVirtualSvelteFilePath, toRealSvelteFilePath } from './utils'; - -type _ts = typeof ts; - -/** - * This should only be accessed by TS svelte module resolution. - */ -export function createSvelteSys(ts: _ts, logger: Logger) { - const svelteSys: ts.System = { - ...ts.sys, - fileExists(path: string) { - return ts.sys.fileExists(ensureRealSvelteFilePath(path)); - }, - readDirectory(path, extensions, exclude, include, depth) { - const extensionsWithSvelte = (extensions ?? []).concat('.svelte'); - - return ts.sys.readDirectory(path, extensionsWithSvelte, exclude, include, depth); - }, - readFile(path, encoding) { - // imba typescript plugin patch this with Object.defineProperty - // and copying the property descriptor from a class that extends the original ts.sys - // so we explicitly define it here - return ts.sys.readFile(path, encoding); - } - }; - - if (ts.sys.realpath) { - const realpath = ts.sys.realpath; - svelteSys.realpath = function (path) { - if (isVirtualSvelteFilePath(path)) { - return realpath(toRealSvelteFilePath(path)) + '.ts'; - } - return realpath(path); - }; - } - - return svelteSys; -} diff --git a/packages/typescript-plugin/src/utils.ts b/packages/typescript-plugin/src/utils.ts deleted file mode 100644 index 3cdefee9b..000000000 --- a/packages/typescript-plugin/src/utils.ts +++ /dev/null @@ -1,239 +0,0 @@ -import type ts from 'typescript/lib/tsserverlibrary'; -import { SvelteSnapshot } from './svelte-snapshots'; -type _ts = typeof ts; - -export function isSvelteFilePath(filePath: string) { - return filePath.endsWith('.svelte'); -} - -export function isVirtualSvelteFilePath(filePath: string) { - return filePath.endsWith('.svelte.ts'); -} - -export function toRealSvelteFilePath(filePath: string) { - return filePath.slice(0, -'.ts'.length); -} - -export function ensureRealSvelteFilePath(filePath: string) { - return isVirtualSvelteFilePath(filePath) ? toRealSvelteFilePath(filePath) : filePath; -} - -export function isNotNullOrUndefined(val: T | undefined | null): val is T { - return val !== undefined && val !== null; -} - -/** - * Checks if this a section that should be completely ignored - * because it's purely generated. - */ -export function isInGeneratedCode(text: string, start: number, end: number) { - const lineStart = text.lastIndexOf('\n', start); - const lineEnd = text.indexOf('\n', end); - const lastStart = text.substring(lineStart, start).lastIndexOf('/*Ωignore_startΩ*/'); - const lastEnd = text.substring(lineStart, start).lastIndexOf('/*Ωignore_endΩ*/'); - return lastStart > lastEnd && text.substring(end, lineEnd).includes('/*Ωignore_endΩ*/'); -} - -/** - * Checks that this isn't a text span that should be completely ignored - * because it's purely generated. - */ -export function isNoTextSpanInGeneratedCode(text: string, span: ts.TextSpan) { - return !isInGeneratedCode(text, span.start, span.start + span.length); -} - -/** - * Replace all occurrences of a string within an object with another string, - */ -export function replaceDeep>( - obj: T, - searchStr: string | RegExp, - replacementStr: string -): T { - return _replaceDeep(obj); - - function _replaceDeep(_obj: any): any { - if (typeof _obj === 'string') { - return _obj.replace(searchStr, replacementStr); - } - if (Array.isArray(_obj)) { - return _obj.map((entry) => _replaceDeep(entry)); - } - if (typeof _obj === 'object') { - return Object.keys(_obj).reduce((_o, key) => { - _o[key] = _replaceDeep(_obj[key]); - return _o; - }, {} as any); - } - return _obj; - } -} - -export function getConfigPathForProject(project: ts.server.Project) { - return ( - (project as ts.server.ConfiguredProject).canonicalConfigFilePath ?? - (project.getCompilerOptions() as any).configFilePath - ); -} - -export function isStoreVariableIn$storeDeclaration(text: string, varStart: number) { - return ( - text.lastIndexOf('__sveltets_2_store_get(', varStart) === - varStart - '__sveltets_2_store_get('.length - ); -} - -export function get$storeOffsetOf$storeDeclaration(text: string, storePosition: number) { - return text.lastIndexOf(' =', storePosition) - 1; -} - -type NodePredicate = (node: ts.Node) => boolean; -type NodeTypePredicate = (node: ts.Node) => node is T; - -/** - * Finds node exactly matching span {start, length}. - */ -export function findNodeAtSpan( - node: ts.Node, - span: { start: number; length: number }, - predicate?: NodeTypePredicate -): T | void { - const { start, length } = span; - - const end = start + length; - - for (const child of node.getChildren()) { - const childStart = child.getStart(); - if (end <= childStart) { - return; - } - - const childEnd = child.getEnd(); - if (start >= childEnd) { - continue; - } - - if (start === childStart && end === childEnd) { - if (!predicate) { - return child as T; - } - if (predicate(child)) { - return child; - } - } - - const foundInChildren = findNodeAtSpan(child, span, predicate); - if (foundInChildren) { - return foundInChildren; - } - } -} - -/** - * Finds node somewhere at position. - */ -export function findNodeAtPosition( - node: ts.Node, - pos: number, - predicate?: NodeTypePredicate -): T | void { - for (const child of node.getChildren()) { - const childStart = child.getStart(); - if (pos < childStart) { - return; - } - - const childEnd = child.getEnd(); - if (pos > childEnd) { - continue; - } - - const foundInChildren = findNodeAtPosition(child, pos, predicate); - if (foundInChildren) { - return foundInChildren; - } - - if (!predicate) { - return child as T; - } - if (predicate(child)) { - return child; - } - } -} - -/** - * True if is `export const/let/function` - */ -export function isTopLevelExport(ts: _ts, node: ts.Node, source: ts.SourceFile) { - return ( - (ts.isVariableStatement(node) && source.statements.includes(node as any)) || - (ts.isIdentifier(node) && - node.parent && - ts.isVariableDeclaration(node.parent) && - source.statements.includes(node.parent?.parent?.parent as any)) || - (ts.isIdentifier(node) && - node.parent && - ts.isFunctionDeclaration(node.parent) && - source.statements.includes(node.parent as any)) - ); -} - -const COMPONENT_SUFFIX = '__SvelteComponent_'; - -export function isGeneratedSvelteComponentName(className: string) { - return className.endsWith(COMPONENT_SUFFIX); -} - -export function offsetOfGeneratedComponentExport(snapshot: SvelteSnapshot) { - return snapshot.getText().lastIndexOf(COMPONENT_SUFFIX); -} - -export function gatherDescendants( - node: ts.Node, - predicate: NodeTypePredicate, - dest: T[] = [] -) { - if (predicate(node)) { - dest.push(node); - } else { - for (const child of node.getChildren()) { - gatherDescendants(child, predicate, dest); - } - } - return dest; -} - -export function findIdentifier(ts: _ts, node: ts.Node): ts.Identifier | undefined { - if (ts.isIdentifier(node)) { - return node; - } - - if (ts.isFunctionDeclaration(node)) { - return node.name; - } - - while (node) { - if (ts.isIdentifier(node)) { - return node; - } - if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) { - return node.name; - } - - node = node.parent; - } -} - -export function hasNodeModule(compilerOptions: ts.CompilerOptions, module: string) { - try { - const hasModule = - typeof compilerOptions.configFilePath !== 'string' || - require.resolve(module, { paths: [compilerOptions.configFilePath] }); - return hasModule; - } catch (e) { - // If require.resolve fails, we end up here, which can be either because the package is not found, - // or (in case of things like SvelteKit) the package is found but the package.json is not exported. - return (e as any)?.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED'; - } -} diff --git a/packages/typescript-plugin/tsconfig.json b/packages/typescript-plugin/tsconfig.json index 2055054d9..9cbae2bd0 100644 --- a/packages/typescript-plugin/tsconfig.json +++ b/packages/typescript-plugin/tsconfig.json @@ -1,19 +1,10 @@ { - "compilerOptions": { - "lib": ["es2021"], - "target": "es2021", - "moduleResolution": "node", - "module": "CommonJS", - - "outDir": "dist", - "esModuleInterop": true, - "strict": true, - "declaration": true, - "sourceMap": true, - "composite": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["./src/**/*"], - "exclude": ["./node_modules"] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "out", + "rootDir": "src", + }, + "include": [ + "src", + ], } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1fb51a95..7e507cf58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,207 +24,88 @@ importers: packages/language-server: dependencies: - '@jridgewell/trace-mapping': - specifier: ^0.3.17 - version: 0.3.18 - '@vscode/emmet-helper': - specifier: 2.8.4 - version: 2.8.4 - chokidar: - specifier: ^3.4.1 - version: 3.5.3 - estree-walker: - specifier: ^2.0.1 - version: 2.0.2 - fast-glob: - specifier: ^3.2.7 - version: 3.2.12 - lodash: - specifier: ^4.17.21 - version: 4.17.21 - prettier: - specifier: ~3.1.0 - version: 3.1.0 - prettier-plugin-svelte: - specifier: ~3.1.1 - version: 3.1.1(prettier@3.1.0)(svelte@3.57.0) - svelte: - specifier: ^3.57.0 - version: 3.57.0 - svelte-preprocess: - specifier: ~5.1.0 - version: 5.1.0(svelte@3.57.0)(typescript@5.3.2) + '@jridgewell/sourcemap-codec': + specifier: ^1.4.15 + version: 1.4.15 + '@volar/language-core': + specifier: 2.0.0-alpha.13 + version: 2.0.0-alpha.13 + '@volar/language-server': + specifier: 2.0.0-alpha.13 + version: 2.0.0-alpha.13 svelte2tsx: - specifier: workspace:~ - version: link:../svelte2tsx - typescript: - specifier: ^5.3.2 - version: 5.3.2 - vscode-css-languageservice: - specifier: ~6.2.10 - version: 6.2.10 - vscode-html-languageservice: - specifier: ~5.1.1 - version: 5.1.1 - vscode-languageserver: - specifier: 8.0.2 - version: 8.0.2 - vscode-languageserver-protocol: - specifier: 3.17.2 - version: 3.17.2 - vscode-languageserver-types: - specifier: 3.17.2 - version: 3.17.2 + specifier: ^0.6.27 + version: 0.6.27(svelte@3.57.0)(typescript@5.3.2) + volar-service-css: + specifier: 0.0.25 + version: 0.0.25 + volar-service-html: + specifier: 0.0.25 + version: 0.0.25 + volar-service-typescript: + specifier: 0.0.25 + version: 0.0.25 + vscode-languageserver-textdocument: + specifier: ^1.0.11 + version: 1.0.11 vscode-uri: - specifier: ~3.0.0 + specifier: ^3.0.8 version: 3.0.8 - devDependencies: - '@types/estree': - specifier: ^0.0.42 - version: 0.0.42 - '@types/lodash': - specifier: ^4.14.116 - version: 4.14.194 - '@types/mocha': - specifier: ^9.1.0 - version: 9.1.1 - '@types/node': - specifier: ^16.0.0 - version: 16.18.32 - '@types/prettier': - specifier: ^2.2.3 - version: 2.7.2 - '@types/sinon': - specifier: ^7.5.2 - version: 7.5.2 - cross-env: - specifier: ^7.0.2 - version: 7.0.3 - mocha: - specifier: ^9.2.0 - version: 9.2.2 - sinon: - specifier: ^11.0.0 - version: 11.1.2 - ts-node: - specifier: ^10.0.0 - version: 10.9.1(@types/node@16.18.32)(typescript@5.3.2) packages/svelte-check: dependencies: - '@jridgewell/trace-mapping': - specifier: ^0.3.17 - version: 0.3.18 + '@volar/kit': + specifier: 2.0.0-alpha.13 + version: 2.0.0-alpha.13(typescript@5.3.2) chokidar: specifier: ^3.4.1 version: 3.5.3 fast-glob: - specifier: ^3.2.7 - version: 3.2.12 - import-fresh: - specifier: ^3.2.1 - version: 3.3.0 - picocolors: - specifier: ^1.0.0 - version: 1.0.0 - sade: - specifier: ^1.7.4 - version: 1.8.1 + specifier: ^3.3.2 + version: 3.3.2 + kleur: + specifier: ^4.1.5 + version: 4.1.5 svelte: specifier: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 version: 3.57.0 - svelte-preprocess: - specifier: ^5.1.0 - version: 5.1.0(svelte@3.57.0)(typescript@5.3.2) - typescript: - specifier: ^5.0.3 - version: 5.3.2 - devDependencies: - '@rollup/plugin-commonjs': - specifier: ^24.0.0 - version: 24.1.0(rollup@3.7.5) - '@rollup/plugin-json': - specifier: ^6.0.0 - version: 6.0.0(rollup@3.7.5) - '@rollup/plugin-node-resolve': - specifier: ^15.0.0 - version: 15.0.2(rollup@3.7.5) - '@rollup/plugin-replace': - specifier: 5.0.2 - version: 5.0.2(rollup@3.7.5) - '@rollup/plugin-typescript': - specifier: ^10.0.0 - version: 10.0.1(rollup@3.7.5)(tslib@2.5.2)(typescript@5.3.2) - '@types/sade': - specifier: ^1.7.2 - version: 1.7.4 - builtin-modules: - specifier: ^3.3.0 - version: 3.3.0 - rollup: - specifier: 3.7.5 - version: 3.7.5 - rollup-plugin-cleanup: - specifier: ^3.2.0 - version: 3.2.1(rollup@3.7.5) - rollup-plugin-copy: - specifier: ^3.4.0 - version: 3.4.0 svelte-language-server: - specifier: workspace:* + specifier: 0.0.0 version: link:../language-server - vscode-languageserver: - specifier: 8.0.2 - version: 8.0.2 - vscode-languageserver-protocol: - specifier: 3.17.2 - version: 3.17.2 - vscode-languageserver-types: - specifier: 3.17.2 - version: 3.17.2 - vscode-uri: - specifier: ~3.0.0 - version: 3.0.8 + volar-service-typescript: + specifier: 0.0.25 + version: 0.0.25 + yargs: + specifier: ^17.7.2 + version: 17.7.2 + devDependencies: + '@types/yargs': + specifier: ^17.0.32 + version: 17.0.32 packages/svelte-vscode: - dependencies: - lodash: - specifier: ^4.17.21 - version: 4.17.21 - svelte-language-server: - specifier: workspace:* - version: link:../language-server - typescript-svelte-plugin: - specifier: workspace:* - version: link:../typescript-plugin - vscode-languageclient: - specifier: ^8.0.0 - version: 8.1.0 - vscode-languageserver-protocol: - specifier: 3.17.2 - version: 3.17.2 devDependencies: - '@types/lodash': - specifier: ^4.14.116 - version: 4.14.194 - '@types/node': - specifier: ^16.0.0 - version: 16.18.32 '@types/vscode': - specifier: ^1.67 - version: 1.78.0 + specifier: ^1.82.0 + version: 1.85.0 + '@volar/language-server': + specifier: 2.0.0-alpha.13 + version: 2.0.0-alpha.13 + '@volar/vscode': + specifier: 2.0.0-alpha.13 + version: 2.0.0-alpha.13 js-yaml: specifier: ^3.14.0 version: 3.14.1 - tslib: - specifier: ^2.4.0 - version: 2.5.2 - typescript: - specifier: ^5.3.2 - version: 5.3.2 - vscode-tmgrammar-test: - specifier: ^0.0.11 - version: 0.0.11 + svelte-language-server: + specifier: 0.0.0 + version: link:../language-server + typescript-svelte-plugin: + specifier: 0.0.0 + version: link:../typescript-plugin + vsce: + specifier: latest + version: 2.15.0 packages/svelte2tsx: dependencies: @@ -307,19 +188,16 @@ importers: packages/typescript-plugin: dependencies: - '@jridgewell/sourcemap-codec': - specifier: ^1.4.14 - version: 1.4.15 - svelte2tsx: - specifier: workspace:~ - version: link:../svelte2tsx + '@volar/typescript': + specifier: 2.0.0-alpha.13 + version: 2.0.0-alpha.13 + svelte-language-server: + specifier: 0.0.0 + version: link:../language-server devDependencies: '@types/node': - specifier: ^16.0.0 - version: 16.18.32 - typescript: - specifier: ^5.3.2 - version: 5.3.2 + specifier: latest + version: 20.10.5 packages: @@ -330,25 +208,10 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true - /@emmetio/abbreviation@2.3.3: - resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} - dependencies: - '@emmetio/scanner': 1.0.4 - dev: false - - /@emmetio/css-abbreviation@2.1.8: - resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} - dependencies: - '@emmetio/scanner': 1.0.4 - dev: false - - /@emmetio/scanner@1.0.4: - resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} - dev: false - /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} + dev: true /@jridgewell/resolve-uri@3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} @@ -357,6 +220,7 @@ packages: /@jridgewell/sourcemap-codec@1.4.14: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} @@ -366,6 +230,7 @@ packages: dependencies: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + dev: true /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -441,20 +306,6 @@ packages: rollup: 3.7.5 dev: true - /@rollup/plugin-replace@5.0.2(rollup@3.7.5): - resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - '@rollup/pluginutils': 5.0.2(rollup@3.7.5) - magic-string: 0.27.0 - rollup: 3.7.5 - dev: true - /@rollup/plugin-typescript@10.0.1(rollup@3.7.5)(tslib@2.5.2)(typescript@5.3.2): resolution: {integrity: sha512-wBykxRLlX7EzL8BmUqMqk5zpx2onnmRMSw/l9M1sVfkJvdwfxogZQVNUM9gVMJbjRLDR5H6U0OMOrlDGmIV45A==} engines: {node: '>=14.0.0'} @@ -490,48 +341,6 @@ packages: rollup: 3.7.5 dev: true - /@sinonjs/commons@1.8.6: - resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} - dependencies: - type-detect: 4.0.8 - dev: true - - /@sinonjs/commons@2.0.0: - resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} - dependencies: - type-detect: 4.0.8 - dev: true - - /@sinonjs/commons@3.0.0: - resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} - dependencies: - type-detect: 4.0.8 - dev: true - - /@sinonjs/fake-timers@10.2.0: - resolution: {integrity: sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==} - dependencies: - '@sinonjs/commons': 3.0.0 - dev: true - - /@sinonjs/fake-timers@7.1.2: - resolution: {integrity: sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==} - dependencies: - '@sinonjs/commons': 1.8.6 - dev: true - - /@sinonjs/samsam@6.1.3: - resolution: {integrity: sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==} - dependencies: - '@sinonjs/commons': 1.8.6 - lodash.get: 4.4.2 - type-detect: 4.0.8 - dev: true - - /@sinonjs/text-encoding@0.7.2: - resolution: {integrity: sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==} - dev: true - /@tsconfig/node10@1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} dev: true @@ -556,12 +365,6 @@ packages: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: true - /@types/fs-extra@8.1.2: - resolution: {integrity: sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg==} - dependencies: - '@types/node': 16.18.32 - dev: true - /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: @@ -569,10 +372,6 @@ packages: '@types/node': 16.18.32 dev: true - /@types/lodash@4.14.194: - resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==} - dev: true - /@types/minimatch@5.1.2: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} dev: true @@ -581,36 +380,20 @@ packages: resolution: {integrity: sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==} dev: true - /@types/mri@1.1.1: - resolution: {integrity: sha512-nJOuiTlsvmClSr3+a/trTSx4DTuY/VURsWGKSf/eeavh0LRMqdsK60ti0TlwM5iHiGOK3/Ibkxsbr7i9rzGreA==} - dev: true - /@types/node@16.18.32: resolution: {integrity: sha512-zpnXe4dEz6PrWz9u7dqyRoq9VxwCvoXRPy/ewhmMa1CgEyVmtL1NJPQ2MX+4pf97vetquVKkpiMx0MwI8pjNOw==} dev: true - /@types/prettier@2.7.2: - resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} + /@types/node@20.10.5: + resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==} + dependencies: + undici-types: 5.26.5 dev: true - /@types/pug@2.0.6: - resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} - dev: false - /@types/resolve@1.20.2: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} dev: true - /@types/sade@1.7.4: - resolution: {integrity: sha512-6ys13kmtlY0aIOz4KtMdeBD9BHs6vSE3aRcj4vAZqXjypT2el8WZt6799CMjElVgh1cbOH/t3vrpQ4IpwytcPA==} - dependencies: - '@types/mri': 1.1.1 - dev: true - - /@types/sinon@7.5.2: - resolution: {integrity: sha512-T+m89VdXj/eidZyejvmoP9jivXgBDdkOSBVQjU9kF349NEx10QdPNGxHeZUaj1IlJ32/ewdyXJjnJxyxJroYwg==} - dev: true - /@types/unist@2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: true @@ -630,28 +413,93 @@ packages: '@types/vfile-message': 2.0.0 dev: true - /@types/vscode@1.78.0: - resolution: {integrity: sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==} + /@types/vscode@1.85.0: + resolution: {integrity: sha512-CF/RBon/GXwdfmnjZj0WTUMZN5H6YITOfBCP4iEZlOtVQXuzw6t7Le7+cR+7JzdMrnlm7Mfp49Oj2TuSXIWo3g==} + dev: true + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + dev: true + + /@types/yargs@17.0.32: + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + dependencies: + '@types/yargs-parser': 21.0.3 dev: true /@ungap/promise-all-settled@1.1.2: resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} dev: true - /@vscode/emmet-helper@2.8.4: - resolution: {integrity: sha512-lUki5QLS47bz/U8IlG9VQ+1lfxMtxMZENmU5nu4Z71eOD5j9FK0SmYGL5NiVJg9WBWeAU0VxRADMY2Qpq7BfVg==} + /@volar/kit@2.0.0-alpha.13(typescript@5.3.2): + resolution: {integrity: sha512-gjbGsugdX6xv2bsginqELgnivbDWXXqSkxWQE7t99tzm1qX8hPyT+4MAH+znVlCqbCQkmWlG5RSMzhlVjNO9YQ==} + peerDependencies: + typescript: '*' dependencies: - emmet: 2.4.4 - jsonc-parser: 2.3.1 + '@volar/language-service': 2.0.0-alpha.13 + '@volar/typescript': 2.0.0-alpha.13 + typesafe-path: 0.2.2 + typescript: 5.3.2 vscode-languageserver-textdocument: 1.0.11 - vscode-languageserver-types: 3.17.2 - vscode-nls: 5.2.0 - vscode-uri: 2.1.2 + vscode-uri: 3.0.8 dev: false + /@volar/language-core@2.0.0-alpha.13: + resolution: {integrity: sha512-idtaFl+Gehs8Na643ZGiCPl5XpfMTAnbwUbTlmIsUx3lh9u6tE/ED6uIxJ3bb3jmcrHpaULV8q9czKzgQ+U53A==} + dependencies: + '@volar/source-map': 2.0.0-alpha.13 + + /@volar/language-server@2.0.0-alpha.13: + resolution: {integrity: sha512-95Tc3s5hCEUPStoUK9uVRT9X+XDwjsdIWTLhEXWy++H+cM0DgAMMKYWSdpTrpFMJrQOXEl4iBtjSH+5AYvU/ig==} + dependencies: + '@volar/language-core': 2.0.0-alpha.13 + '@volar/language-service': 2.0.0-alpha.13 + '@volar/snapshot-document': 2.0.0-alpha.13 + '@volar/typescript': 2.0.0-alpha.13 + '@vscode/l10n': 0.0.16 + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + + /@volar/language-service@2.0.0-alpha.13: + resolution: {integrity: sha512-8Sv0YMUqvLfHQ0vrFQ9srlxmuyLD0g5VHcRFOrXnLKA482y0+HalIFEaKNo7syN8bkfjl+ZWHIFntM+579N+4g==} + dependencies: + '@volar/language-core': 2.0.0-alpha.13 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + + /@volar/snapshot-document@2.0.0-alpha.13: + resolution: {integrity: sha512-2iXJEEdImffqxDV6C3GMl2nw2XQAUl1wVWXxwWIysgstZ9QdLVlaN9PjJ7o8u+2pG8+JWaUgtaP43/oBOyPb7A==} + dependencies: + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.11 + + /@volar/source-map@2.0.0-alpha.13: + resolution: {integrity: sha512-irRsLATyKk9f0chUrzHUnJaBi5rw483NKFoMsQkjkJ1E5yyl7I/cCtYU03lICxkRDCz6DPqBMhooi1huMrXsxQ==} + dependencies: + muggle-string: 0.4.1 + + /@volar/typescript@2.0.0-alpha.13: + resolution: {integrity: sha512-w8BzSpqqek+Xu5IfWGRkXek1hxfRPuOZUOCL5/yyTQRY84XDn3+oyXTd157TXhtD0TpWKKPCVj6ETjS4qdmVBw==} + dependencies: + '@volar/language-core': 2.0.0-alpha.13 + path-browserify: 1.0.1 + + /@volar/vscode@2.0.0-alpha.13: + resolution: {integrity: sha512-RYREsWc0Id9aiJ+ssNecvQxLSmNO02N7ygMkeGGWxydFmZ2paPs7NwdiBEBJ3dcGCcuh5agxQvYjipFj0xHkVQ==} + dependencies: + '@volar/language-server': 2.0.0-alpha.13 + path-browserify: 1.0.1 + vscode-languageclient: 9.0.1 + vscode-nls: 5.2.0 + dev: true + /@vscode/l10n@0.0.16: resolution: {integrity: sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==} - dev: false /acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} @@ -680,7 +528,6 @@ packages: /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - dev: true /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} @@ -694,7 +541,6 @@ packages: engines: {node: '>=8'} dependencies: color-convert: 2.0.1 - dev: true /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} @@ -722,23 +568,49 @@ packages: engines: {node: '>=8'} dev: true + /azure-devops-node-api@11.2.0: + resolution: {integrity: sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==} + dependencies: + tunnel: 0.0.6 + typed-rest-client: 1.8.11 + dev: true + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 + dev: true /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: balanced-match: 1.0.2 + dev: true /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} @@ -752,21 +624,31 @@ packages: /buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - dev: false + dev: true /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + /builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} dev: true - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: false + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: true /camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} @@ -790,6 +672,30 @@ packages: supports-color: 7.2.0 dev: true + /cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + dev: true + + /cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + dev: true + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -804,6 +710,10 @@ packages: optionalDependencies: fsevents: 2.3.3 + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: true + /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -817,6 +727,15 @@ packages: wrap-ansi: 7.0.0 dev: true + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -828,7 +747,6 @@ packages: engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 - dev: true /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} @@ -836,14 +754,10 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true - - /colorette@1.4.0: - resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} - dev: true - /commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + /commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} dev: true /commondir@1.0.1: @@ -852,6 +766,7 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -874,6 +789,21 @@ packages: which: 2.0.2 dev: true + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: true + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + /debug@4.3.3(supports-color@8.1.1): resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} engines: {node: '>=6.0'} @@ -892,15 +822,36 @@ packages: engines: {node: '>=10'} dev: true + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: true + /dedent-js@1.0.1: resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} dev: false + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: true + /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} dev: true + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + /del@5.1.0: resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} engines: {node: '>=8'} @@ -915,10 +866,10 @@ packages: slash: 3.0.0 dev: true - /detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + /detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} engines: {node: '>=8'} - dev: false + dev: true /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} @@ -930,11 +881,6 @@ packages: engines: {node: '>=0.3.1'} dev: true - /diff@5.1.0: - resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} - engines: {node: '>=0.3.1'} - dev: true - /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -942,25 +888,54 @@ packages: path-type: 4.0.0 dev: true - /emmet@2.4.4: - resolution: {integrity: sha512-v8Mwpjym55CS3EjJgiCLWUB3J2HSR93jhzXW325720u8KvYxdI2voYLstW3pHBxFz54H6jFjayR9G4LfTG0q+g==} + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} dependencies: - '@emmetio/abbreviation': 2.3.3 - '@emmetio/css-abbreviation': 2.1.8 - dev: false + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 dev: true - /es6-promise@3.3.1: - resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - dev: false + /entities@2.1.0: + resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} + dev: true + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} - dev: true /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -978,15 +953,17 @@ packages: hasBin: true dev: true - /estree-walker@0.6.1: - resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} - dev: true - /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: true - /fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1000,6 +977,12 @@ packages: dependencies: reusify: 1.0.4 + /fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + dependencies: + pend: 1.2.0 + dev: true + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -1019,17 +1002,13 @@ packages: hasBin: true dev: true - /fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} dev: true /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -1042,9 +1021,25 @@ packages: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} dev: true + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: true + + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} dev: true /glob-parent@5.1.2: @@ -1073,6 +1068,7 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 + dev: true /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} @@ -1089,20 +1085,6 @@ packages: resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} dev: true - /globby@10.0.1: - resolution: {integrity: sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==} - engines: {node: '>=8'} - dependencies: - '@types/glob': 7.2.0 - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.2.12 - glob: 7.2.3 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 - dev: true - /globby@10.0.2: resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} engines: {node: '>=8'} @@ -1110,7 +1092,7 @@ packages: '@types/glob': 7.2.0 array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.2 glob: 7.2.3 ignore: 5.2.4 merge2: 1.4.1 @@ -1121,8 +1103,15 @@ packages: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: true + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true /growl@1.10.5: resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==} @@ -1139,6 +1128,22 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -1146,24 +1151,43 @@ packages: function-bind: 1.1.1 dev: true + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true dev: true + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} dev: true - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: false - /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -1174,9 +1198,15 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 + dev: true /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} @@ -1204,7 +1234,6 @@ packages: /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - dev: true /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -1235,11 +1264,6 @@ packages: engines: {node: '>=8'} dev: true - /is-plain-object@3.0.1: - resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==} - engines: {node: '>=0.10.0'} - dev: true - /is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} dependencies: @@ -1251,23 +1275,10 @@ packages: engines: {node: '>=10'} dev: true - /isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - dev: true - /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /js-cleanup@1.2.0: - resolution: {integrity: sha512-JeDD0yiiSt80fXzAVa/crrS0JDPQljyBG/RpOtaSbyDq03VHa9szJWMaWOYU/bcTn412uMN2MxApXq8v79cUiQ==} - engines: {node: ^10.14.2 || >=12.0.0} - dependencies: - magic-string: 0.25.9 - perf-regexes: 1.0.1 - skip-regex: 1.0.2 - dev: true - /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -1283,18 +1294,28 @@ packages: argparse: 2.0.1 dev: true - /jsonc-parser@2.3.1: - resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} + /keytar@7.9.0: + resolution: {integrity: sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==} + requiresBuild: true + dependencies: + node-addon-api: 4.3.0 + prebuild-install: 7.1.1 + dev: true + + /kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} dev: false - /jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - optionalDependencies: - graceful-fs: 4.2.11 + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} dev: true - /just-extend@4.2.1: - resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==} + /linkify-it@3.0.3: + resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} + dependencies: + uc.micro: 1.0.6 dev: true /locate-path@6.0.0: @@ -1304,14 +1325,6 @@ packages: p-locate: 5.0.0 dev: true - /lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - dev: true - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: false - /log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -1331,24 +1344,33 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: false - - /magic-string@0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} - dependencies: - sourcemap-codec: 1.4.8 - dev: true /magic-string@0.27.0: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + dev: true /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true + /markdown-it@12.3.2: + resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} + hasBin: true + dependencies: + argparse: 2.0.1 + entities: 2.1.0 + linkify-it: 3.0.3 + mdurl: 1.0.1 + uc.micro: 1.0.6 + dev: true + + /mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + dev: true + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1360,15 +1382,22 @@ packages: braces: 3.0.2 picomatch: 2.3.1 - /min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} - dev: false + hasBin: true + dev: true + + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: true /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 + dev: true /minimatch@4.2.1: resolution: {integrity: sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==} @@ -1382,17 +1411,15 @@ packages: engines: {node: '>=10'} dependencies: brace-expansion: 2.0.1 + dev: true /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: false + dev: true - /mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - dependencies: - minimist: 1.2.8 - dev: false + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: true /mocha@9.2.2: resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==} @@ -1425,11 +1452,6 @@ packages: yargs-unparser: 2.0.0 dev: true - /mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - dev: false - /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true @@ -1438,20 +1460,21 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true + /muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + /mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + dev: true + /nanoid@3.3.1: resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true dev: true - /nise@5.1.4: - resolution: {integrity: sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==} - dependencies: - '@sinonjs/commons': 2.0.0 - '@sinonjs/fake-timers': 10.2.0 - '@sinonjs/text-encoding': 0.7.2 - just-extend: 4.2.1 - path-to-regexp: 1.8.0 + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} dev: true /no-case@3.0.4: @@ -1461,14 +1484,36 @@ packages: tslib: 2.5.2 dev: false + /node-abi@3.52.0: + resolution: {integrity: sha512-JJ98b02z16ILv7859irtXn4oUaFWADtvkzy2c0IAatNVX2Mc9Yoh8z6hZInn3QwvMEYhHuQloYi+TTQy67SIdQ==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.1 + dev: true + + /node-addon-api@4.3.0: + resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + dev: true + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 + dev: true /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -1491,12 +1536,24 @@ packages: aggregate-error: 3.1.0 dev: true - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + /parse-semver@1.1.1: + resolution: {integrity: sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==} dependencies: - callsites: 3.1.0 - dev: false + semver: 5.7.2 + dev: true + + /parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + dev: true + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: true /pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} @@ -1505,6 +1562,9 @@ packages: tslib: 2.5.2 dev: false + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1513,6 +1573,7 @@ packages: /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + dev: true /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} @@ -1523,20 +1584,13 @@ packages: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true - /path-to-regexp@1.8.0: - resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} - dependencies: - isarray: 0.0.1 - dev: true - /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} dev: true - /perf-regexes@1.0.1: - resolution: {integrity: sha512-L7MXxUDtqr4PUaLFCDCXBfGV/6KLIuSEccizDI7JxT+c9x1G1v04BQ4+4oag84SHaCdrBgQAIs/Cqn+flwFPng==} - engines: {node: '>=6.14'} + /pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true /periscopic@2.0.3: @@ -1546,28 +1600,48 @@ packages: is-reference: 1.2.1 dev: true - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: false - /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - /prettier-plugin-svelte@3.1.1(prettier@3.1.0)(svelte@3.57.0): - resolution: {integrity: sha512-jLzaHfToav527/I5h2BMQfN3G5gylrJm54zFFyoXvUtfscI47877ftacUb+Eyse/3bXrhY+MtkyiuvruiHc+kg==} - peerDependencies: - prettier: ^3.0.0 - svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + /prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true dependencies: - prettier: 3.1.0 - svelte: 3.57.0 - dev: false + detect-libc: 2.0.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.52.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: true /prettier@3.1.0: resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} engines: {node: '>=14'} hasBin: true + dev: true + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + + /qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: true /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1578,21 +1652,44 @@ packages: safe-buffer: 5.2.1 dev: true + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: true + + /read@1.0.7: + resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==} + engines: {node: '>=0.8'} + dependencies: + mute-stream: 0.0.8 + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 + /request-light@0.7.0: + resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - dev: true - - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: false /resolve@1.22.2: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} @@ -1607,13 +1704,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - /rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: false - /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -1621,28 +1711,6 @@ packages: glob: 7.2.3 dev: true - /rollup-plugin-cleanup@3.2.1(rollup@3.7.5): - resolution: {integrity: sha512-zuv8EhoO3TpnrU8MX8W7YxSbO4gmOR0ny06Lm3nkFfq0IVKdBUtHwhVzY1OAJyNCIAdLiyPnOrU0KnO0Fri1GQ==} - engines: {node: ^10.14.2 || >=12.0.0} - peerDependencies: - rollup: '>=2.0' - dependencies: - js-cleanup: 1.2.0 - rollup: 3.7.5 - rollup-pluginutils: 2.8.2 - dev: true - - /rollup-plugin-copy@3.4.0: - resolution: {integrity: sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ==} - engines: {node: '>=8.3'} - dependencies: - '@types/fs-extra': 8.1.2 - colorette: 1.4.0 - fs-extra: 8.1.0 - globby: 10.0.1 - is-plain-object: 3.0.1 - dev: true - /rollup-plugin-delete@2.0.0: resolution: {integrity: sha512-/VpLMtDy+8wwRlDANuYmDa9ss/knGsAgrDhM+tEwB1npHwNu4DYNmDfUL55csse/GHs9Q+SMT/rw9uiaZ3pnzA==} engines: {node: '>=10'} @@ -1650,12 +1718,6 @@ packages: del: 5.1.0 dev: true - /rollup-pluginutils@2.8.2: - resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} - dependencies: - estree-walker: 0.6.1 - dev: true - /rollup@3.7.5: resolution: {integrity: sha512-z0ZbqHBtS/et2EEUKMrAl2CoSdwN7ZPzL17UMiKN9RjjqHShTlv7F9J6ZJZJNREYjBh3TvBrdfjkFDIXFNeuiQ==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -1669,30 +1731,31 @@ packages: dependencies: queue-microtask: 1.2.3 - /sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - dependencies: - mri: 1.2.0 - dev: false - /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: true - /sander@0.5.1: - resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} - dependencies: - es6-promise: 3.3.1 - graceful-fs: 4.2.11 - mkdirp: 0.5.6 - rimraf: 2.7.1 - dev: false + /sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + dev: true + + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + dev: true /semver@7.5.1: resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} engines: {node: '>=10'} hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true dependencies: lru-cache: 6.0.0 dev: false @@ -1703,6 +1766,16 @@ packages: randombytes: 2.1.0 dev: true + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1715,20 +1788,24 @@ packages: engines: {node: '>=8'} dev: true - /sinon@11.1.2: - resolution: {integrity: sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==} + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: - '@sinonjs/commons': 1.8.6 - '@sinonjs/fake-timers': 7.1.2 - '@sinonjs/samsam': 6.1.3 - diff: 5.1.0 - nise: 5.1.4 - supports-color: 7.2.0 + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 dev: true - /skip-regex@1.0.2: - resolution: {integrity: sha512-pEjMUbwJ5Pl/6Vn6FsamXHXItJXSRftcibixDmNCWbWhic0hzHrwkMZo0IZ7fMRH9KxcWDFSkzhccB4285PutA==} - engines: {node: '>=4.2'} + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: true + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 dev: true /slash@3.0.0: @@ -1736,16 +1813,6 @@ packages: engines: {node: '>=8'} dev: true - /sorcery@0.11.0: - resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==} - hasBin: true - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - buffer-crc32: 0.2.13 - minimist: 1.2.8 - sander: 0.5.1 - dev: false - /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: @@ -1758,11 +1825,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /sourcemap-codec@1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - deprecated: Please use @jridgewell/sourcemap-codec instead - dev: true - /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true @@ -1774,6 +1836,11 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 dev: true /strip-ansi@6.0.1: @@ -1781,14 +1848,11 @@ packages: engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - dev: true - /strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - dependencies: - min-indent: 1.0.1 - dev: false + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -1821,49 +1885,14 @@ packages: engines: {node: '>= 0.4'} dev: true - /svelte-preprocess@5.1.0(svelte@3.57.0)(typescript@5.3.2): - resolution: {integrity: sha512-EkErPiDzHAc0k2MF5m6vBNmRUh338h2myhinUw/xaqsLs7/ZvsgREiLGj03VrSzbY/TB5ZXgBOsKraFee5yceA==} - engines: {node: '>= 14.10.0'} - requiresBuild: true + /svelte2tsx@0.6.27(svelte@3.57.0)(typescript@5.3.2): + resolution: {integrity: sha512-E1uPW1o6VsbRz+nUk3fznZ2lSmCITAJoNu8AYefWSvIwE2pSB01i5sId4RMbWNzfcwCQl1DcgGShCPcldl4rvg==} peerDependencies: - '@babel/core': ^7.10.2 - coffeescript: ^2.5.1 - less: ^3.11.3 || ^4.0.0 - postcss: ^7 || ^8 - postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 - pug: ^3.0.0 - sass: ^1.26.8 - stylus: ^0.55.0 - sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 - svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 - typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' - peerDependenciesMeta: - '@babel/core': - optional: true - coffeescript: - optional: true - less: - optional: true - postcss: - optional: true - postcss-load-config: - optional: true - pug: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - typescript: - optional: true + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 dependencies: - '@types/pug': 2.0.6 - detect-indent: 6.1.0 - magic-string: 0.27.0 - sorcery: 0.11.0 - strip-indent: 3.0.0 + dedent-js: 1.0.1 + pascal-case: 3.1.2 svelte: 3.57.0 typescript: 5.3.2 dev: false @@ -1872,6 +1901,26 @@ packages: resolution: {integrity: sha512-WMXEvF+RtAaclw0t3bPDTUe19pplMlfyKDsixbHQYgCWi9+O9VN0kXU1OppzrB9gPAvz4NALuoca2LfW2bOjTQ==} engines: {node: '>= 8'} + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: true + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + /tiny-glob@0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} dependencies: @@ -1879,6 +1928,13 @@ packages: globrex: 0.1.2 dev: true + /tmp@0.2.1: + resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} + engines: {node: '>=8.17.0'} + dependencies: + rimraf: 3.0.2 + dev: true + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1919,25 +1975,64 @@ packages: /tslib@2.5.2: resolution: {integrity: sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==} - /type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + dev: true + + /typed-rest-client@1.8.11: + resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} + dependencies: + qs: 6.11.2 + tunnel: 0.0.6 + underscore: 1.13.6 dev: true + /typesafe-path@0.2.2: + resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} + dev: false + + /typescript-auto-import-cache@0.3.1: + resolution: {integrity: sha512-ujC5E2gT3Sf3Dzfg5QYgb8NkZNxFQI12W6rk5U/TbkDFXyvIb9YENic+hsNoVDmKEmlRTUjRRD8RCjLMIx1rxg==} + dependencies: + semver: 7.5.4 + dev: false + /typescript@5.3.2: resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} engines: {node: '>=14.17'} hasBin: true + /uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + dev: true + + /underscore@1.13.6: + resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + /unist-util-stringify-position@3.0.3: resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} dependencies: '@types/unist': 2.0.6 dev: true - /universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} + /url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true /v8-compile-cache-lib@3.0.1: @@ -1951,6 +2046,75 @@ packages: unist-util-stringify-position: 3.0.3 dev: true + /volar-service-css@0.0.25: + resolution: {integrity: sha512-Gk4DHGtoWTOxfXbOec8Orikr5viUrlEY6G7pz5wCaOVf4k6cDdadSl9fXZY3MFCZtZUo6RmfhTCyLZq9NeaFMg==} + peerDependencies: + '@volar/language-service': next + peerDependenciesMeta: + '@volar/language-service': + optional: true + dependencies: + vscode-css-languageservice: 6.2.10 + vscode-uri: 3.0.8 + dev: false + + /volar-service-html@0.0.25: + resolution: {integrity: sha512-ZosolIcq3ise5bz44gkFeUpTknbGGv8rG+FpnmcnAhqrgOktj0+XwLBbOOpeCfFVKw6N0FsUOAt8zGJs8Bpg9w==} + peerDependencies: + '@volar/language-service': next + peerDependenciesMeta: + '@volar/language-service': + optional: true + dependencies: + vscode-html-languageservice: 5.1.1 + vscode-uri: 3.0.8 + dev: false + + /volar-service-typescript@0.0.25: + resolution: {integrity: sha512-HapraB6P6S+v2o5nH+dzu4l5r4iIyY8EwYQjgbYTAhtFdpJEscIt9HtPOUJSY0d9xXbblFeLrhUFzAicNrSeaA==} + peerDependencies: + '@volar/language-service': next + '@volar/typescript': next + peerDependenciesMeta: + '@volar/language-service': + optional: true + dependencies: + path-browserify: 1.0.1 + semver: 7.5.4 + typescript-auto-import-cache: 0.3.1 + vscode-languageserver-textdocument: 1.0.11 + vscode-nls: 5.2.0 + vscode-uri: 3.0.8 + dev: false + + /vsce@2.15.0: + resolution: {integrity: sha512-P8E9LAZvBCQnoGoizw65JfGvyMqNGlHdlUXD1VAuxtvYAaHBKLBdKPnpy60XKVDAkQCfmMu53g+gq9FM+ydepw==} + engines: {node: '>= 14'} + deprecated: vsce has been renamed to @vscode/vsce. Install using @vscode/vsce instead. + hasBin: true + dependencies: + azure-devops-node-api: 11.2.0 + chalk: 2.4.2 + cheerio: 1.0.0-rc.12 + commander: 6.2.1 + glob: 7.2.3 + hosted-git-info: 4.1.0 + keytar: 7.9.0 + leven: 3.1.0 + markdown-it: 12.3.2 + mime: 1.6.0 + minimatch: 3.1.2 + parse-semver: 1.1.1 + read: 1.0.7 + semver: 5.7.2 + tmp: 0.2.1 + typed-rest-client: 1.8.11 + url-join: 4.0.1 + xml2js: 0.4.23 + yauzl: 2.10.0 + yazl: 2.5.1 + dev: true + /vscode-css-languageservice@6.2.10: resolution: {integrity: sha512-sYUZPku4mQ06AWGCbMyjv2tdR6juBW6hTbVPFwbJvNVzdtEfBioQOgkdXg7yMJNWnXkvWSU1FL2kb4Vxu5Cdyw==} dependencies: @@ -1969,85 +2133,39 @@ packages: vscode-uri: 3.0.8 dev: false - /vscode-jsonrpc@8.0.2: - resolution: {integrity: sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==} - engines: {node: '>=14.0.0'} - - /vscode-jsonrpc@8.1.0: - resolution: {integrity: sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==} + /vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} - dev: false - /vscode-languageclient@8.1.0: - resolution: {integrity: sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==} - engines: {vscode: ^1.67.0} + /vscode-languageclient@9.0.1: + resolution: {integrity: sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==} + engines: {vscode: ^1.82.0} dependencies: minimatch: 5.1.6 semver: 7.5.1 - vscode-languageserver-protocol: 3.17.3 - dev: false - - /vscode-languageserver-protocol@3.17.2: - resolution: {integrity: sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==} - dependencies: - vscode-jsonrpc: 8.0.2 - vscode-languageserver-types: 3.17.2 + vscode-languageserver-protocol: 3.17.5 + dev: true - /vscode-languageserver-protocol@3.17.3: - resolution: {integrity: sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==} + /vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} dependencies: - vscode-jsonrpc: 8.1.0 - vscode-languageserver-types: 3.17.3 - dev: false + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 /vscode-languageserver-textdocument@1.0.11: resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} - dev: false - - /vscode-languageserver-types@3.17.2: - resolution: {integrity: sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==} - - /vscode-languageserver-types@3.17.3: - resolution: {integrity: sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==} - dev: false /vscode-languageserver-types@3.17.5: resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - dev: false - /vscode-languageserver@8.0.2: - resolution: {integrity: sha512-bpEt2ggPxKzsAOZlXmCJ50bV7VrxwCS5BI4+egUmure/oI/t4OlFzi/YNtVvY24A2UDOZAgwFGgnZPwqSJubkA==} + /vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} hasBin: true dependencies: - vscode-languageserver-protocol: 3.17.2 + vscode-languageserver-protocol: 3.17.5 /vscode-nls@5.2.0: resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} - dev: false - - /vscode-oniguruma@1.7.0: - resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} - dev: true - - /vscode-textmate@5.5.0: - resolution: {integrity: sha512-jToQkPGMNKn0eyKyitYeINJF0NoD240aYyKPIWJv5W2jfPt++jIRg0OSergubtGhbw6SoefkvBYEpX7TsfoSUQ==} - dev: true - - /vscode-tmgrammar-test@0.0.11: - resolution: {integrity: sha512-Bd60x/OeBLAQnIxiR2GhUic1CQZOFfWM8Pd43HjdEUBf/0vcvYAlFQikOXvv+zkItHLznjKaDX7VWKPVYUF9ug==} - hasBin: true - dependencies: - chalk: 2.4.2 - commander: 2.20.3 - diff: 4.0.2 - glob: 7.2.3 - vscode-oniguruma: 1.7.0 - vscode-textmate: 5.5.0 - dev: true - - /vscode-uri@2.1.2: - resolution: {integrity: sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==} - dev: false /vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} @@ -2071,25 +2189,41 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /xml2js@0.4.23: + resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.3.0 + xmlbuilder: 11.0.1 + dev: true + + /xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: true /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - dev: true /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: false /yargs-parser@20.2.4: resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} engines: {node: '>=10'} dev: true + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: false + /yargs-unparser@2.0.0: resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} engines: {node: '>=10'} @@ -2113,6 +2247,32 @@ packages: yargs-parser: 20.2.4 dev: true + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + 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.8 + yargs-parser: 21.1.1 + dev: false + + /yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + dev: true + + /yazl@2.5.1: + resolution: {integrity: sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==} + dependencies: + buffer-crc32: 0.2.13 + dev: true + /yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 000000000..910f111a5 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2021", + "lib": [ + "ES2021", + ], + "module": "Node16", + "sourceMap": true, + "composite": true, + "declaration": true, + "strict": true, + "alwaysStrict": false, + "resolveJsonModule": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + }, +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 5a99332ab..4b1b78bb4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,13 @@ { - "references": [ - { "path": "./packages/language-server" }, - { "path": "./packages/svelte-vscode" } - ], - "files": [], - "include": [], - "exclude": ["**/node_modules"] -} + "extends": "./tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./packages/svelte-check/tsconfig.json" + }, + { + "path": "./packages/svelte-vscode/tsconfig.json" + }, + ] +} \ No newline at end of file From f92f9a11a899ad8265f6cf7e074f1f311c3587d8 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 27 Dec 2023 17:15:34 +0800 Subject: [PATCH 02/12] Update package.json --- packages/language-server/package.json | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 185ce52ba..cb7346161 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -1,19 +1,34 @@ { "name": "svelte-language-server", "version": "0.0.0", + "description": "A language server for Svelte", "main": "out/index.js", - "license": "MIT", "files": [ "out/**/*.js", "out/**/*.d.ts" ], "bin": { - "svelte-language-server": "./bin/server.js" + "svelteserver": "./bin/server.js" }, "repository": { "type": "git", - "url": "https://github.com/johnsoncodehk/volar.git", - "directory": "examples/svelte-language-server" + "url": "git+https://github.com/sveltejs/language-tools.git" + }, + "keywords": [ + "svelte", + "vscode", + "atom", + "editor", + "language-server" + ], + "author": "James Birtles and the Svelte Language Tools contributors", + "license": "MIT", + "bugs": { + "url": "https://github.com/sveltejs/language-tools/issues" + }, + "homepage": "https://github.com/sveltejs/language-tools#readme", + "engines": { + "node": ">= 16.0.0" }, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", From c741574f1898d73b9df20273715a638de60b2b1f Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 27 Dec 2023 17:17:16 +0800 Subject: [PATCH 03/12] Disable `structure` for virtual TS file --- packages/language-server/src/languagePlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-server/src/languagePlugin.ts b/packages/language-server/src/languagePlugin.ts index dcfe3f214..823082e0c 100644 --- a/packages/language-server/src/languagePlugin.ts +++ b/packages/language-server/src/languagePlugin.ts @@ -147,7 +147,7 @@ function getVirtualTsFile(fileName: string, text: string): VirtualFile | undefin completion: true, semantic: true, navigation: true, - structure: true, + structure: false, format: false, }, }); From df82ee3029d1697382ce0c4b321f28bf5f7d111e Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 27 Dec 2023 17:42:51 +0800 Subject: [PATCH 04/12] `module.exports = ` -> `export = ` --- packages/typescript-plugin/package.json | 3 --- packages/typescript-plugin/src/index.ts | 2 +- pnpm-lock.yaml | 14 -------------- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index b1e48a91e..71daa5e18 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -21,9 +21,6 @@ ], "author": "The Svelte Community", "license": "MIT", - "devDependencies": { - "@types/node": "latest" - }, "dependencies": { "@volar/typescript": "2.0.0-alpha.13", "svelte-language-server": "0.0.0" diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts index 538e70352..61a0ded93 100644 --- a/packages/typescript-plugin/src/index.ts +++ b/packages/typescript-plugin/src/index.ts @@ -1,7 +1,7 @@ import { createTSServerPlugin } from '@volar/typescript/lib/quickstart/createTSServerPlugin'; import { svelteLanguagePlugin } from 'svelte-language-server/out/languagePlugin'; -module.exports = createTSServerPlugin( +export = createTSServerPlugin( () => { return { languagePlugins: [svelteLanguagePlugin], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e507cf58..e2de4a027 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -194,10 +194,6 @@ importers: svelte-language-server: specifier: 0.0.0 version: link:../language-server - devDependencies: - '@types/node': - specifier: latest - version: 20.10.5 packages: @@ -384,12 +380,6 @@ packages: resolution: {integrity: sha512-zpnXe4dEz6PrWz9u7dqyRoq9VxwCvoXRPy/ewhmMa1CgEyVmtL1NJPQ2MX+4pf97vetquVKkpiMx0MwI8pjNOw==} dev: true - /@types/node@20.10.5: - resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==} - dependencies: - undici-types: 5.26.5 - dev: true - /@types/resolve@1.20.2: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} dev: true @@ -2017,10 +2007,6 @@ packages: resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} dev: true - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true - /unist-util-stringify-position@3.0.3: resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} dependencies: From 7bb3c4c9c4b5ca10712e9648ac5221f0bd69b859 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 23 Jan 2024 15:34:18 +0800 Subject: [PATCH 05/12] Upgrade to Volar 2.0 --- packages/language-server/package.json | 10 +- packages/language-server/src/index.ts | 4 +- .../language-server/src/languagePlugin.ts | 65 +++++----- packages/svelte-check/package.json | 4 +- packages/svelte-vscode/package.json | 4 +- packages/typescript-plugin/package.json | 2 +- packages/typescript-plugin/src/index.ts | 11 +- pnpm-lock.yaml | 116 +++++++++--------- 8 files changed, 104 insertions(+), 112 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index cb7346161..872ba79f1 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -32,12 +32,12 @@ }, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", - "@volar/language-core": "2.0.0-alpha.13", - "@volar/language-server": "2.0.0-alpha.13", + "@volar/language-core": "~2.0.0", + "@volar/language-server": "~2.0.0", "svelte2tsx": "^0.6.27", - "volar-service-css": "0.0.25", - "volar-service-html": "0.0.25", - "volar-service-typescript": "0.0.25", + "volar-service-css": "volar-2.0", + "volar-service-html": "volar-2.0", + "volar-service-typescript": "volar-2.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } diff --git a/packages/language-server/src/index.ts b/packages/language-server/src/index.ts index 4242fd165..3155e9ac9 100644 --- a/packages/language-server/src/index.ts +++ b/packages/language-server/src/index.ts @@ -1,11 +1,11 @@ -import { createConnection, createNodeServer, createTypeScriptProjectProvider } from '@volar/language-server/node'; +import { createConnection, createServer, createTypeScriptProjectProvider } from '@volar/language-server/node'; import { create as createCssScriptServicePlugin } from 'volar-service-css'; import { create as createHtmlServicePlugin } from 'volar-service-html'; import { create as createTypeScriptServicePlugin } from 'volar-service-typescript'; import { svelteLanguagePlugin } from './languagePlugin'; const connection = createConnection(); -const server = createNodeServer(connection); +const server = createServer(connection); connection.listen(); diff --git a/packages/language-server/src/languagePlugin.ts b/packages/language-server/src/languagePlugin.ts index 823082e0c..5c73aee89 100644 --- a/packages/language-server/src/languagePlugin.ts +++ b/packages/language-server/src/languagePlugin.ts @@ -1,32 +1,32 @@ import { decode } from '@jridgewell/sourcemap-codec'; -import type { CodeMapping, LanguagePlugin, VirtualFile } from '@volar/language-core'; +import { forEachEmbeddedCode, type CodeMapping, type LanguagePlugin, type VirtualCode } from '@volar/language-core'; import { svelte2tsx } from 'svelte2tsx'; import type * as ts from 'typescript'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { URI } from 'vscode-uri'; export const svelteLanguagePlugin: LanguagePlugin = { - createVirtualFile(fileName, languageId, snapshot) { + createVirtualCode(_fileId, languageId, snapshot) { if (languageId === 'svelte') { return { - fileName, + id: 'root', languageId, snapshot, - embeddedFiles: [ - ...getVirtualCssFiles(fileName, snapshot.getText(0, snapshot.getLength())), - getVirtualTsFile(fileName, snapshot.getText(0, snapshot.getLength())), - ].filter((v): v is VirtualFile => !!v), + embeddedCodes: [ + ...getVirtualCssFiles(snapshot.getText(0, snapshot.getLength())), + getVirtualTsFile(snapshot.getText(0, snapshot.getLength())), + ].filter((v): v is VirtualCode => !!v), mappings: [], codegenStacks: [] }; } }, - updateVirtualFile(svelteFile, snapshot) { - svelteFile.snapshot = snapshot; - svelteFile.embeddedFiles = [ - ...getVirtualCssFiles(svelteFile.fileName, snapshot.getText(0, snapshot.getLength())), - getVirtualTsFile(svelteFile.fileName, snapshot.getText(0, snapshot.getLength())), - ].filter((v): v is VirtualFile => !!v); + updateVirtualCode(_fileId, virtualCode, snapshot) { + virtualCode.snapshot = snapshot; + virtualCode.embeddedCodes = [ + ...getVirtualCssFiles(snapshot.getText(0, snapshot.getLength())), + getVirtualTsFile(snapshot.getText(0, snapshot.getLength())), + ].filter((v): v is VirtualCode => !!v); + return virtualCode; }, typescript: { extraFileExtensions: [{ @@ -34,20 +34,21 @@ export const svelteLanguagePlugin: LanguagePlugin = { isMixedContent: true, scriptKind: 7 satisfies ts.ScriptKind.Deferred, }], - resolveSourceFileName(tsFileName) { - if (tsFileName.endsWith('.svelte.ts')) { - return tsFileName.substring(0, tsFileName.length - '.ts'.length); - } - }, - resolveModuleName(moduleName, impliedNodeFormat) { - if (impliedNodeFormat === 99 satisfies ts.ModuleKind.ESNext && moduleName.endsWith('.svelte')) { - return `${moduleName}.js`; + getScript(rootVirtualCode) { + for (const code of forEachEmbeddedCode(rootVirtualCode)) { + if (code.id === 'ts') { + return { + code, + scriptKind: 3, + extension: '.ts', + }; + } } }, }, }; -function* getVirtualCssFiles(fileName: string, content: string): Generator { +function* getVirtualCssFiles(content: string): Generator { const styleBlocks = [...content.matchAll(/\([\s\S]*?)\<\/style\>/g)]; @@ -56,7 +57,7 @@ function* getVirtualCssFiles(fileName: string, content: string): Generator { - return { - languagePlugins: [svelteLanguagePlugin], - extensions: ['.svelte'], - } - } +export = createLanguageServicePlugin( + () => [svelteLanguagePlugin] ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2de4a027..3851adf24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,23 +28,23 @@ importers: specifier: ^1.4.15 version: 1.4.15 '@volar/language-core': - specifier: 2.0.0-alpha.13 - version: 2.0.0-alpha.13 + specifier: ~2.0.0 + version: 2.0.0 '@volar/language-server': - specifier: 2.0.0-alpha.13 - version: 2.0.0-alpha.13 + specifier: ~2.0.0 + version: 2.0.0 svelte2tsx: specifier: ^0.6.27 version: 0.6.27(svelte@3.57.0)(typescript@5.3.2) volar-service-css: - specifier: 0.0.25 - version: 0.0.25 + specifier: volar-2.0 + version: 0.0.27 volar-service-html: - specifier: 0.0.25 - version: 0.0.25 + specifier: volar-2.0 + version: 0.0.27 volar-service-typescript: - specifier: 0.0.25 - version: 0.0.25 + specifier: volar-2.0 + version: 0.0.27(@volar/typescript@2.0.0) vscode-languageserver-textdocument: specifier: ^1.0.11 version: 1.0.11 @@ -55,8 +55,8 @@ importers: packages/svelte-check: dependencies: '@volar/kit': - specifier: 2.0.0-alpha.13 - version: 2.0.0-alpha.13(typescript@5.3.2) + specifier: ~2.0.0 + version: 2.0.0(typescript@5.3.2) chokidar: specifier: ^3.4.1 version: 3.5.3 @@ -73,8 +73,8 @@ importers: specifier: 0.0.0 version: link:../language-server volar-service-typescript: - specifier: 0.0.25 - version: 0.0.25 + specifier: volar-2.0 + version: 0.0.27(@volar/typescript@2.0.0) yargs: specifier: ^17.7.2 version: 17.7.2 @@ -89,11 +89,11 @@ importers: specifier: ^1.82.0 version: 1.85.0 '@volar/language-server': - specifier: 2.0.0-alpha.13 - version: 2.0.0-alpha.13 + specifier: ~2.0.0 + version: 2.0.0 '@volar/vscode': - specifier: 2.0.0-alpha.13 - version: 2.0.0-alpha.13 + specifier: ~2.0.0 + version: 2.0.0 js-yaml: specifier: ^3.14.0 version: 3.14.1 @@ -189,8 +189,8 @@ importers: packages/typescript-plugin: dependencies: '@volar/typescript': - specifier: 2.0.0-alpha.13 - version: 2.0.0-alpha.13 + specifier: ~2.0.0 + version: 2.0.0 svelte-language-server: specifier: 0.0.0 version: link:../language-server @@ -421,31 +421,31 @@ packages: resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} dev: true - /@volar/kit@2.0.0-alpha.13(typescript@5.3.2): - resolution: {integrity: sha512-gjbGsugdX6xv2bsginqELgnivbDWXXqSkxWQE7t99tzm1qX8hPyT+4MAH+znVlCqbCQkmWlG5RSMzhlVjNO9YQ==} + /@volar/kit@2.0.0(typescript@5.3.2): + resolution: {integrity: sha512-1l6KjWjX1HT7Hf5kHT4hhDu1KEPNvGPoCr/ypwRbk5jFkBVNNv9eA5H2e8obXztlESLvQ1RDOQxyxv0PIXfg4g==} peerDependencies: typescript: '*' dependencies: - '@volar/language-service': 2.0.0-alpha.13 - '@volar/typescript': 2.0.0-alpha.13 + '@volar/language-service': 2.0.0 + '@volar/typescript': 2.0.0 typesafe-path: 0.2.2 typescript: 5.3.2 vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 dev: false - /@volar/language-core@2.0.0-alpha.13: - resolution: {integrity: sha512-idtaFl+Gehs8Na643ZGiCPl5XpfMTAnbwUbTlmIsUx3lh9u6tE/ED6uIxJ3bb3jmcrHpaULV8q9czKzgQ+U53A==} + /@volar/language-core@2.0.0: + resolution: {integrity: sha512-1L9GB7CwI4LKKwpKOxFSICJ54GdWGwjIUkI7UwAjJpnFIFm8+xteSf86nsEknFh0k7MHScm+Xb0x8eEx/8Qdrg==} dependencies: - '@volar/source-map': 2.0.0-alpha.13 + '@volar/source-map': 2.0.0 - /@volar/language-server@2.0.0-alpha.13: - resolution: {integrity: sha512-95Tc3s5hCEUPStoUK9uVRT9X+XDwjsdIWTLhEXWy++H+cM0DgAMMKYWSdpTrpFMJrQOXEl4iBtjSH+5AYvU/ig==} + /@volar/language-server@2.0.0: + resolution: {integrity: sha512-sfSXbCgVf7wiyKsGmeCt0y6qpFE4sma45LUd87Rs/Wv6I1UH/B8TP7UZ/6grAKh5YO5GF8WOv2zovn5Nmw7z3g==} dependencies: - '@volar/language-core': 2.0.0-alpha.13 - '@volar/language-service': 2.0.0-alpha.13 - '@volar/snapshot-document': 2.0.0-alpha.13 - '@volar/typescript': 2.0.0-alpha.13 + '@volar/language-core': 2.0.0 + '@volar/language-service': 2.0.0 + '@volar/snapshot-document': 2.0.0 + '@volar/typescript': 2.0.0 '@vscode/l10n': 0.0.16 path-browserify: 1.0.1 request-light: 0.7.0 @@ -454,35 +454,35 @@ packages: vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 - /@volar/language-service@2.0.0-alpha.13: - resolution: {integrity: sha512-8Sv0YMUqvLfHQ0vrFQ9srlxmuyLD0g5VHcRFOrXnLKA482y0+HalIFEaKNo7syN8bkfjl+ZWHIFntM+579N+4g==} + /@volar/language-service@2.0.0: + resolution: {integrity: sha512-aWalwQ54DAEvJ9dfEyJN/pr6ID5GWL7dBR46CMx6fr9JK/m9NkXaK7qJRcxKXaaSTYtlWA342dEhYESLB2dAYw==} dependencies: - '@volar/language-core': 2.0.0-alpha.13 + '@volar/language-core': 2.0.0 vscode-languageserver-protocol: 3.17.5 vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 - /@volar/snapshot-document@2.0.0-alpha.13: - resolution: {integrity: sha512-2iXJEEdImffqxDV6C3GMl2nw2XQAUl1wVWXxwWIysgstZ9QdLVlaN9PjJ7o8u+2pG8+JWaUgtaP43/oBOyPb7A==} + /@volar/snapshot-document@2.0.0: + resolution: {integrity: sha512-hf2meJIrJw848KaOr8dCQlCgGS0UWUVd/m4DkTnM4Rlo+fVmzd7CjZoYqjYMiuIqxeu3OxpypcP++fv4Rp28Ew==} dependencies: vscode-languageserver-protocol: 3.17.5 vscode-languageserver-textdocument: 1.0.11 - /@volar/source-map@2.0.0-alpha.13: - resolution: {integrity: sha512-irRsLATyKk9f0chUrzHUnJaBi5rw483NKFoMsQkjkJ1E5yyl7I/cCtYU03lICxkRDCz6DPqBMhooi1huMrXsxQ==} + /@volar/source-map@2.0.0: + resolution: {integrity: sha512-WZO1hZPqMGyCmWs9j4ZV090D7jfMjQUwALuz5CCbrMVSCGlvYxv5jhw0M5G39thJmiNV/ttgdgiavzvtOMe5/w==} dependencies: muggle-string: 0.4.1 - /@volar/typescript@2.0.0-alpha.13: - resolution: {integrity: sha512-w8BzSpqqek+Xu5IfWGRkXek1hxfRPuOZUOCL5/yyTQRY84XDn3+oyXTd157TXhtD0TpWKKPCVj6ETjS4qdmVBw==} + /@volar/typescript@2.0.0: + resolution: {integrity: sha512-6HGiCOMe612taDDRTX1N5938hY2ThjdLdW3TXHsIgcQz8Y+2H8o94BDwXnMOGf+XksektuXMZkDBalgLPFhBbg==} dependencies: - '@volar/language-core': 2.0.0-alpha.13 + '@volar/language-core': 2.0.0 path-browserify: 1.0.1 - /@volar/vscode@2.0.0-alpha.13: - resolution: {integrity: sha512-RYREsWc0Id9aiJ+ssNecvQxLSmNO02N7ygMkeGGWxydFmZ2paPs7NwdiBEBJ3dcGCcuh5agxQvYjipFj0xHkVQ==} + /@volar/vscode@2.0.0: + resolution: {integrity: sha512-FqNMA5MwAARbjP6vZQYJd2a6ydyzUfFO8XTV6+mJPQVzSlCKoxIvV5QbfgEa1P7AGT1DuuiSr/1WcfFEF5/s1A==} dependencies: - '@volar/language-server': 2.0.0-alpha.13 + '@volar/language-server': 2.0.0 path-browserify: 1.0.1 vscode-languageclient: 9.0.1 vscode-nls: 5.2.0 @@ -1748,7 +1748,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: false /serialize-javascript@6.0.0: resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} @@ -2032,10 +2031,10 @@ packages: unist-util-stringify-position: 3.0.3 dev: true - /volar-service-css@0.0.25: - resolution: {integrity: sha512-Gk4DHGtoWTOxfXbOec8Orikr5viUrlEY6G7pz5wCaOVf4k6cDdadSl9fXZY3MFCZtZUo6RmfhTCyLZq9NeaFMg==} + /volar-service-css@0.0.27: + resolution: {integrity: sha512-wPBj6nUqiLSNwbADRQ5hIX1LoKYROuVTxAKg4IiJGgw1A7MnLxvoOAOef+ruxtVDHW5GCgeN+3QSV68UOIxS/g==} peerDependencies: - '@volar/language-service': next + '@volar/language-service': ~2.0 peerDependenciesMeta: '@volar/language-service': optional: true @@ -2044,10 +2043,10 @@ packages: vscode-uri: 3.0.8 dev: false - /volar-service-html@0.0.25: - resolution: {integrity: sha512-ZosolIcq3ise5bz44gkFeUpTknbGGv8rG+FpnmcnAhqrgOktj0+XwLBbOOpeCfFVKw6N0FsUOAt8zGJs8Bpg9w==} + /volar-service-html@0.0.27: + resolution: {integrity: sha512-hyXgNaaiSJhj7f1et7L9BRpTYMzr+3rvlPhGvgZpqurJx+J00Y3wWmlU4adUWglKTXSAswgedKUvSNGn4BGAzw==} peerDependencies: - '@volar/language-service': next + '@volar/language-service': ~2.0 peerDependenciesMeta: '@volar/language-service': optional: true @@ -2056,15 +2055,16 @@ packages: vscode-uri: 3.0.8 dev: false - /volar-service-typescript@0.0.25: - resolution: {integrity: sha512-HapraB6P6S+v2o5nH+dzu4l5r4iIyY8EwYQjgbYTAhtFdpJEscIt9HtPOUJSY0d9xXbblFeLrhUFzAicNrSeaA==} + /volar-service-typescript@0.0.27(@volar/typescript@2.0.0): + resolution: {integrity: sha512-O8BEz5NFy4bYU36LKCXEQl08pl9mf6AOV/xpLo3VTiMK1CJiwax7XbPkE6SZKb1QfQe0gn5RbvHeRpzSCz6H5g==} peerDependencies: - '@volar/language-service': next - '@volar/typescript': next + '@volar/language-service': ~2.0 + '@volar/typescript': ~2.0 peerDependenciesMeta: '@volar/language-service': optional: true dependencies: + '@volar/typescript': 2.0.0 path-browserify: 1.0.1 semver: 7.5.4 typescript-auto-import-cache: 0.3.1 @@ -2128,7 +2128,7 @@ packages: engines: {vscode: ^1.82.0} dependencies: minimatch: 5.1.6 - semver: 7.5.1 + semver: 7.5.4 vscode-languageserver-protocol: 3.17.5 dev: true From 9338b13aa27109fb79d657f416430841cb5d6991 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 23 Jan 2024 15:42:35 +0800 Subject: [PATCH 06/12] Delete serviceCache.ts --- .../src/plugins/typescript/serviceCache.ts | 93 ------------------- 1 file changed, 93 deletions(-) delete mode 100644 packages/language-server/src/plugins/typescript/serviceCache.ts diff --git a/packages/language-server/src/plugins/typescript/serviceCache.ts b/packages/language-server/src/plugins/typescript/serviceCache.ts deleted file mode 100644 index 0dbb3bfe2..000000000 --- a/packages/language-server/src/plugins/typescript/serviceCache.ts +++ /dev/null @@ -1,93 +0,0 @@ -// abstracting the typescript-auto-import-cache package to support our use case - -import { - ProjectService, - createProjectService as createProjectService50 -} from 'typescript-auto-import-cache/out/5_0/projectService'; -import { createProject as createProject50 } from 'typescript-auto-import-cache/out/5_0/project'; -import { createProject as createProject53 } from 'typescript-auto-import-cache/out/5_3/project'; -import ts from 'typescript'; -import { ExportInfoMap } from 'typescript-auto-import-cache/out/5_0/exportInfoMap'; -import { ModuleSpecifierCache } from 'typescript-auto-import-cache/out/5_0/moduleSpecifierCache'; -import { SymlinkCache } from 'typescript-auto-import-cache/out/5_0/symlinkCache'; -import { ProjectPackageJsonInfo } from 'typescript-auto-import-cache/out/5_0/packageJsonCache'; - -export { ProjectService }; - -declare module 'typescript' { - interface LanguageServiceHost { - /** @internal */ getCachedExportInfoMap?(): ExportInfoMap; - /** @internal */ getModuleSpecifierCache?(): ModuleSpecifierCache; - /** @internal */ getGlobalTypingsCacheLocation?(): string | undefined; - /** @internal */ getSymlinkCache?(files: readonly ts.SourceFile[]): SymlinkCache; - /** @internal */ getPackageJsonsVisibleToFile?( - fileName: string, - rootDir?: string - ): readonly ProjectPackageJsonInfo[]; - /** @internal */ getPackageJsonAutoImportProvider?(): ts.Program | undefined; - /** @internal */ useSourceOfProjectReferenceRedirect?(): boolean; - } -} - -export function createProjectService( - system: ts.System, - hostConfiguration: { - preferences: ts.UserPreferences; - } -) { - const version = ts.version.split('.'); - const major = parseInt(version[0]); - - if (major < 5) { - return undefined; - } - - const projectService = createProjectService50( - ts, - system, - system.getCurrentDirectory(), - hostConfiguration, - ts.LanguageServiceMode.Semantic - ); - - return projectService; -} - -export function createProject( - host: ts.LanguageServiceHost, - createLanguageService: (host: ts.LanguageServiceHost) => ts.LanguageService, - options: { - projectService: ProjectService; - compilerOptions: ts.CompilerOptions; - currentDirectory: string; - } -) { - const version = ts.version.split('.'); - const major = parseInt(version[0]); - const minor = parseInt(version[1]); - - if (major < 5) { - return undefined; - } - - const factory = minor < 3 ? createProject50 : createProject53; - const project = factory(ts, host, createLanguageService, options); - - const proxyMethods: (keyof typeof project)[] = [ - 'getCachedExportInfoMap', - 'getModuleSpecifierCache', - 'getGlobalTypingsCacheLocation', - 'getSymlinkCache', - 'getPackageJsonsVisibleToFile', - 'getPackageJsonAutoImportProvider', - 'includePackageJsonAutoImports', - 'useSourceOfProjectReferenceRedirect' - ]; - proxyMethods.forEach((key) => ((host as any)[key] = project[key].bind(project))); - - if (host.log) { - project.log = host.log.bind(host); - } - - return project; -} From 5091488ce772fd3ce9a7ff65d2822d1ecb1206d8 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 23 Jan 2024 17:00:57 +0800 Subject: [PATCH 07/12] Use esbuild --- packages/svelte-vscode/.gitignore | 1 + packages/svelte-vscode/.vscodeignore | 8 +- packages/svelte-vscode/package.json | 8 +- packages/svelte-vscode/scripts/build.js | 40 ++++ packages/svelte-vscode/src/extension.ts | 2 +- pnpm-lock.yaml | 241 ++++++++++++++++++++++++ 6 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 packages/svelte-vscode/.gitignore create mode 100644 packages/svelte-vscode/scripts/build.js diff --git a/packages/svelte-vscode/.gitignore b/packages/svelte-vscode/.gitignore new file mode 100644 index 000000000..1521c8b76 --- /dev/null +++ b/packages/svelte-vscode/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/svelte-vscode/.vscodeignore b/packages/svelte-vscode/.vscodeignore index 362565c65..5eaf8f9f1 100644 --- a/packages/svelte-vscode/.vscodeignore +++ b/packages/svelte-vscode/.vscodeignore @@ -1,7 +1,9 @@ **/tsconfig.json -/src -/scripts -/.vscode +src +scripts +.vscode +out +test node_modules/@types diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index ed7e09b83..cb343ce47 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -2,12 +2,13 @@ "name": "svelte-vscode", "version": "0.5.0", "description": "Svelte language support for VS Code", - "main": "out/extension.js", + "main": "dist/client.js", "scripts": { "build:grammar": "npx js-yaml syntaxes/svelte.tmLanguage.src.yaml > syntaxes/svelte.tmLanguage.json && npx js-yaml syntaxes/postcss.src.yaml > syntaxes/postcss.json", "build:ts": "tsc -p ./", - "build": "npm run build:ts && npm run build:grammar", - "vscode:prepublish": "npm install && npm run build && npm prune --production", + "build:esbuild": "node scripts/build.js", + "build": "npm run build:ts && npm run build:esbuild && npm run build:grammar", + "vscode:prepublish": "npm run build", "watch": "npm run build:grammar && tsc -w -p ./", "test": "npm run build:grammar && node test/grammar/test.js" }, @@ -197,6 +198,7 @@ "@types/vscode": "^1.82.0", "@volar/language-server": "~2.0.0", "@volar/vscode": "~2.0.0", + "esbuild": "latest", "js-yaml": "^3.14.0", "svelte-language-server": "0.0.0", "typescript-svelte-plugin": "0.0.0", diff --git a/packages/svelte-vscode/scripts/build.js b/packages/svelte-vscode/scripts/build.js new file mode 100644 index 000000000..ba7499b65 --- /dev/null +++ b/packages/svelte-vscode/scripts/build.js @@ -0,0 +1,40 @@ +require("esbuild").context({ + entryPoints: { + client: './out/extension.js', + server: './node_modules/svelte-language-server/bin/server.js', + }, + bundle: true, + metafile: process.argv.includes('--metafile'), + outdir: './dist', + external: [ + 'vscode', + ], + format: 'cjs', + platform: 'node', + tsconfig: './tsconfig.json', + define: { 'process.env.NODE_ENV': '"production"' }, + minify: process.argv.includes('--minify'), + plugins: [ + { + name: 'umd2esm', + setup(build) { + build.onResolve({ filter: /^(vscode-.*-languageservice|jsonc-parser)/ }, args => { + const pathUmdMay = require.resolve(args.path, { paths: [args.resolveDir] }); + // Call twice the replace is to solve the problem of the path in Windows + const pathEsm = pathUmdMay.replace('/umd/', '/esm/').replace('\\umd\\', '\\esm\\'); + return { path: pathEsm }; + }); + }, + }, + ], +}).then(async ctx => { + console.log('building...'); + if (process.argv.includes('--watch')) { + await ctx.watch(); + console.log('watching...'); + } else { + await ctx.rebuild(); + await ctx.dispose(); + console.log('finished.'); + } +}); diff --git a/packages/svelte-vscode/src/extension.ts b/packages/svelte-vscode/src/extension.ts index c741077ad..09bf8a621 100644 --- a/packages/svelte-vscode/src/extension.ts +++ b/packages/svelte-vscode/src/extension.ts @@ -18,7 +18,7 @@ export async function activate(context: vscode.ExtensionContext) { ), }, }; - const serverModule = vscode.Uri.joinPath(context.extensionUri, 'node_modules', 'svelte-language-server', 'bin', 'server.js'); + const serverModule = vscode.Uri.joinPath(context.extensionUri, 'dist', 'server.js'); const runOptions = { execArgv: [] }; const debugOptions = { execArgv: ['--nolazy', '--inspect=' + 6009] }; const serverOptions: lsp.ServerOptions = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3851adf24..a1cad1445 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,6 +94,9 @@ importers: '@volar/vscode': specifier: ~2.0.0 version: 2.0.0 + esbuild: + specifier: latest + version: 0.19.11 js-yaml: specifier: ^3.14.0 version: 3.14.1 @@ -204,6 +207,213 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true + /@esbuild/aix-ppc64@0.19.11: + resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.19.11: + resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.11: + resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.11: + resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.11: + resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.11: + resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.11: + resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.11: + resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.11: + resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.11: + resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.11: + resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.11: + resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.11: + resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.11: + resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.11: + resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.11: + resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.11: + resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.11: + resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.11: + resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.11: + resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.11: + resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.11: + resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.11: + resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} @@ -923,6 +1133,37 @@ packages: engines: {node: '>=0.12'} dev: true + /esbuild@0.19.11: + resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.11 + '@esbuild/android-arm': 0.19.11 + '@esbuild/android-arm64': 0.19.11 + '@esbuild/android-x64': 0.19.11 + '@esbuild/darwin-arm64': 0.19.11 + '@esbuild/darwin-x64': 0.19.11 + '@esbuild/freebsd-arm64': 0.19.11 + '@esbuild/freebsd-x64': 0.19.11 + '@esbuild/linux-arm': 0.19.11 + '@esbuild/linux-arm64': 0.19.11 + '@esbuild/linux-ia32': 0.19.11 + '@esbuild/linux-loong64': 0.19.11 + '@esbuild/linux-mips64el': 0.19.11 + '@esbuild/linux-ppc64': 0.19.11 + '@esbuild/linux-riscv64': 0.19.11 + '@esbuild/linux-s390x': 0.19.11 + '@esbuild/linux-x64': 0.19.11 + '@esbuild/netbsd-x64': 0.19.11 + '@esbuild/openbsd-x64': 0.19.11 + '@esbuild/sunos-x64': 0.19.11 + '@esbuild/win32-arm64': 0.19.11 + '@esbuild/win32-ia32': 0.19.11 + '@esbuild/win32-x64': 0.19.11 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} From d3280c28473c7df95cf097546a8452c304dfbc5e Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 23 Jan 2024 17:25:08 +0800 Subject: [PATCH 08/12] Bundle TS plugin --- packages/svelte-vscode/package.json | 2 +- packages/svelte-vscode/scripts/build.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index cb343ce47..ea6857975 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -46,7 +46,7 @@ "contributes": { "typescriptServerPlugins": [ { - "name": "typescript-svelte-plugin", + "name": "typescript-svelte-plugin-bundled", "enableForWorkspaceTypeScriptVersions": true } ], diff --git a/packages/svelte-vscode/scripts/build.js b/packages/svelte-vscode/scripts/build.js index ba7499b65..4225103ee 100644 --- a/packages/svelte-vscode/scripts/build.js +++ b/packages/svelte-vscode/scripts/build.js @@ -38,3 +38,25 @@ require("esbuild").context({ console.log('finished.'); } }); + +require("esbuild").build({ + bundle: true, + entryPoints: ['./node_modules/typescript-svelte-plugin/out/index.js'], + outfile: './node_modules/typescript-svelte-plugin-bundled/index.js', + logLevel: 'info', + platform: 'node', + target: 'node16', + plugins: [ + { + name: 'alias', + setup({ onResolve, resolve }) { + onResolve({ filter: /^(jsonc-parser)$/ }, ({ path, ...options }) => + resolve(require.resolve(path).replace(/\/umd\//, '/esm/'), options) + ) + onResolve({ filter: /\/umd\// }, ({ path, ...options }) => + resolve(path.replace(/\/umd\//, '/esm/'), options) + ) + } + } + ] +}) From 17d44a3698ecab5bdf618729d7ee530cdbdde8c7 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 23 Jan 2024 17:33:23 +0800 Subject: [PATCH 09/12] Set version --- packages/svelte-vscode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index ea6857975..8ef283dc1 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -1,6 +1,6 @@ { "name": "svelte-vscode", - "version": "0.5.0", + "version": "0.0.0-volar-regular", "description": "Svelte language support for VS Code", "main": "dist/client.js", "scripts": { From 4eddf6fe5d993450f92830cbf8cfaae4a71c9e03 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 23 Jan 2024 17:33:41 +0800 Subject: [PATCH 10/12] Update package.json --- packages/svelte-vscode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index 8ef283dc1..067e19706 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -1,6 +1,6 @@ { "name": "svelte-vscode", - "version": "0.0.0-volar-regular", + "version": "0.0.0-volar-regular.1", "description": "Svelte language support for VS Code", "main": "dist/client.js", "scripts": { From b3e8b84fdb6f650c9a7b8ad0ed71b9836e7b0026 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 1 Jun 2024 06:48:23 +0800 Subject: [PATCH 11/12] Update pnpm-lock.yaml --- pnpm-lock.yaml | 56 ++++++++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1cad1445..2d1fb4091 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,18 +9,18 @@ importers: .: dependencies: typescript: - specifier: ^5.3.2 - version: 5.3.2 + specifier: ^5.4.5 + version: 5.4.5 devDependencies: cross-env: specifier: ^7.0.2 version: 7.0.3 prettier: - specifier: ~3.1.0 - version: 3.1.0 + specifier: ~3.2.5 + version: 3.2.5 ts-node: specifier: ^10.0.0 - version: 10.9.1(@types/node@16.18.32)(typescript@5.3.2) + version: 10.9.1(@types/node@16.18.32)(typescript@5.4.5) packages/language-server: dependencies: @@ -34,8 +34,8 @@ importers: specifier: ~2.0.0 version: 2.0.0 svelte2tsx: - specifier: ^0.6.27 - version: 0.6.27(svelte@3.57.0)(typescript@5.3.2) + specifier: workspace:~ + version: link:../svelte2tsx volar-service-css: specifier: volar-2.0 version: 0.0.27 @@ -56,12 +56,12 @@ importers: dependencies: '@volar/kit': specifier: ~2.0.0 - version: 2.0.0(typescript@5.3.2) + version: 2.0.0(typescript@5.4.5) chokidar: specifier: ^3.4.1 version: 3.5.3 fast-glob: - specifier: ^3.3.2 + specifier: ^3.2.7 version: 3.3.2 kleur: specifier: ^4.1.5 @@ -136,7 +136,7 @@ importers: version: 15.0.2(rollup@3.7.5) '@rollup/plugin-typescript': specifier: ^10.0.0 - version: 10.0.1(rollup@3.7.5)(tslib@2.5.2)(typescript@5.3.2) + version: 10.0.1(rollup@3.7.5)(tslib@2.5.2)(typescript@5.4.5) '@types/estree': specifier: ^0.0.42 version: 0.0.42 @@ -186,8 +186,8 @@ importers: specifier: ^2.4.0 version: 2.5.2 typescript: - specifier: ^5.3.2 - version: 5.3.2 + specifier: ^5.4.5 + version: 5.4.5 packages/typescript-plugin: dependencies: @@ -512,7 +512,7 @@ packages: rollup: 3.7.5 dev: true - /@rollup/plugin-typescript@10.0.1(rollup@3.7.5)(tslib@2.5.2)(typescript@5.3.2): + /@rollup/plugin-typescript@10.0.1(rollup@3.7.5)(tslib@2.5.2)(typescript@5.4.5): resolution: {integrity: sha512-wBykxRLlX7EzL8BmUqMqk5zpx2onnmRMSw/l9M1sVfkJvdwfxogZQVNUM9gVMJbjRLDR5H6U0OMOrlDGmIV45A==} engines: {node: '>=14.0.0'} peerDependencies: @@ -529,7 +529,7 @@ packages: resolve: 1.22.2 rollup: 3.7.5 tslib: 2.5.2 - typescript: 5.3.2 + typescript: 5.4.5 dev: true /@rollup/pluginutils@5.0.2(rollup@3.7.5): @@ -631,7 +631,7 @@ packages: resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} dev: true - /@volar/kit@2.0.0(typescript@5.3.2): + /@volar/kit@2.0.0(typescript@5.4.5): resolution: {integrity: sha512-1l6KjWjX1HT7Hf5kHT4hhDu1KEPNvGPoCr/ypwRbk5jFkBVNNv9eA5H2e8obXztlESLvQ1RDOQxyxv0PIXfg4g==} peerDependencies: typescript: '*' @@ -639,7 +639,7 @@ packages: '@volar/language-service': 2.0.0 '@volar/typescript': 2.0.0 typesafe-path: 0.2.2 - typescript: 5.3.2 + typescript: 5.4.5 vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 dev: false @@ -1854,8 +1854,8 @@ packages: tunnel-agent: 0.6.0 dev: true - /prettier@3.1.0: - resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} engines: {node: '>=14'} hasBin: true dev: true @@ -2115,18 +2115,6 @@ packages: engines: {node: '>= 0.4'} dev: true - /svelte2tsx@0.6.27(svelte@3.57.0)(typescript@5.3.2): - resolution: {integrity: sha512-E1uPW1o6VsbRz+nUk3fznZ2lSmCITAJoNu8AYefWSvIwE2pSB01i5sId4RMbWNzfcwCQl1DcgGShCPcldl4rvg==} - peerDependencies: - svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 - typescript: ^4.9.4 || ^5.0.0 - dependencies: - dedent-js: 1.0.1 - pascal-case: 3.1.2 - svelte: 3.57.0 - typescript: 5.3.2 - dev: false - /svelte@3.57.0: resolution: {integrity: sha512-WMXEvF+RtAaclw0t3bPDTUe19pplMlfyKDsixbHQYgCWi9+O9VN0kXU1OppzrB9gPAvz4NALuoca2LfW2bOjTQ==} engines: {node: '>= 8'} @@ -2171,7 +2159,7 @@ packages: dependencies: is-number: 7.0.0 - /ts-node@10.9.1(@types/node@16.18.32)(typescript@5.3.2): + /ts-node@10.9.1(@types/node@16.18.32)(typescript@5.4.5): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -2197,7 +2185,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.3.2 + typescript: 5.4.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -2234,8 +2222,8 @@ packages: semver: 7.5.4 dev: false - /typescript@5.3.2: - resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} + /typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} hasBin: true From f73d97423849ab039232daaee314ac8f60d2faaf Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 1 Jun 2024 07:03:37 +0800 Subject: [PATCH 12/12] Update Volar --- packages/language-server/package.json | 10 +- packages/language-server/src/index.ts | 32 +- .../language-server/src/languagePlugin.ts | 339 +++++++++--------- packages/svelte-check/package.json | 4 +- packages/svelte-check/src/check.ts | 14 +- packages/svelte-check/src/index.ts | 2 +- packages/svelte-vscode/package.json | 4 +- packages/svelte-vscode/src/extension.ts | 84 +++-- packages/typescript-plugin/package.json | 2 +- pnpm-lock.yaml | 141 ++++---- 10 files changed, 320 insertions(+), 312 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 12c80b1f6..1c3ba616c 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -32,12 +32,12 @@ }, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", - "@volar/language-core": "~2.0.0", - "@volar/language-server": "~2.0.0", + "@volar/language-core": "~2.3.0-alpha.4", + "@volar/language-server": "~2.3.0-alpha.4", "svelte2tsx": "workspace:~", - "volar-service-css": "volar-2.0", - "volar-service-html": "volar-2.0", - "volar-service-typescript": "volar-2.0", + "volar-service-css": "volar-2.3", + "volar-service-html": "volar-2.3", + "volar-service-typescript": "volar-2.3", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } diff --git a/packages/language-server/src/index.ts b/packages/language-server/src/index.ts index 3155e9ac9..753d16b81 100644 --- a/packages/language-server/src/index.ts +++ b/packages/language-server/src/index.ts @@ -1,7 +1,7 @@ -import { createConnection, createServer, createTypeScriptProjectProvider } from '@volar/language-server/node'; +import { createConnection, createServer, createTypeScriptProject, loadTsdkByPath } from '@volar/language-server/node'; import { create as createCssScriptServicePlugin } from 'volar-service-css'; import { create as createHtmlServicePlugin } from 'volar-service-html'; -import { create as createTypeScriptServicePlugin } from 'volar-service-typescript'; +import { create as createTypeScriptServicePlugins } from 'volar-service-typescript'; import { svelteLanguagePlugin } from './languagePlugin'; const connection = createConnection(); @@ -10,25 +10,23 @@ const server = createServer(connection); connection.listen(); connection.onInitialize(params => { - return server.initialize(params, createTypeScriptProjectProvider, { - watchFileExtensions: ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts', 'jsx', 'tsx', 'json', 'svelte'], - getServicePlugins() { - return [ - createCssScriptServicePlugin(), - createHtmlServicePlugin(), - createTypeScriptServicePlugin(server.modules.typescript!), - ] - }, - getLanguagePlugins() { - return [svelteLanguagePlugin]; - }, - }); + const tsdk = loadTsdkByPath(params.initializationOptions.typescript.tsdk, params.locale); + return server.initialize( + params, + [ + createCssScriptServicePlugin(), + createHtmlServicePlugin(), + ...createTypeScriptServicePlugins(tsdk.typescript, tsdk.diagnosticMessages), + ], + createTypeScriptProject(tsdk.typescript, undefined, () => [svelteLanguagePlugin]) + ); }); connection.onInitialized(() => { - server.initialized(); + server.initialized(); + server.watchFiles(['**/*.{js,cjs,mjs,ts,cts,mts,jsx,tsx,json,svelte}']) }); connection.onShutdown(() => { - server.shutdown(); + server.shutdown(); }); diff --git a/packages/language-server/src/languagePlugin.ts b/packages/language-server/src/languagePlugin.ts index 5c73aee89..6e46c4132 100644 --- a/packages/language-server/src/languagePlugin.ts +++ b/packages/language-server/src/languagePlugin.ts @@ -3,184 +3,191 @@ import { forEachEmbeddedCode, type CodeMapping, type LanguagePlugin, type Virtua import { svelte2tsx } from 'svelte2tsx'; import type * as ts from 'typescript'; import { TextDocument } from 'vscode-languageserver-textdocument'; +import { URI } from 'vscode-uri'; -export const svelteLanguagePlugin: LanguagePlugin = { - createVirtualCode(_fileId, languageId, snapshot) { - if (languageId === 'svelte') { - return { - id: 'root', - languageId, - snapshot, - embeddedCodes: [ - ...getVirtualCssFiles(snapshot.getText(0, snapshot.getLength())), - getVirtualTsFile(snapshot.getText(0, snapshot.getLength())), - ].filter((v): v is VirtualCode => !!v), - mappings: [], - codegenStacks: [] - }; - } - }, - updateVirtualCode(_fileId, virtualCode, snapshot) { - virtualCode.snapshot = snapshot; - virtualCode.embeddedCodes = [ - ...getVirtualCssFiles(snapshot.getText(0, snapshot.getLength())), - getVirtualTsFile(snapshot.getText(0, snapshot.getLength())), - ].filter((v): v is VirtualCode => !!v); - return virtualCode; - }, - typescript: { - extraFileExtensions: [{ - extension: 'svelte', - isMixedContent: true, - scriptKind: 7 satisfies ts.ScriptKind.Deferred, - }], - getScript(rootVirtualCode) { - for (const code of forEachEmbeddedCode(rootVirtualCode)) { - if (code.id === 'ts') { - return { - code, - scriptKind: 3, - extension: '.ts', - }; - } - } - }, - }, +export const svelteLanguagePlugin: LanguagePlugin = { + getLanguageId(fileNameOrUri) { + const path = typeof fileNameOrUri === 'string' ? fileNameOrUri : fileNameOrUri.path; + if (path.endsWith('.svelte')) { + return 'svelte'; + } + }, + createVirtualCode(_uri, languageId, snapshot) { + if (languageId === 'svelte') { + return { + id: 'root', + languageId, + snapshot, + embeddedCodes: [ + ...getVirtualCssFiles(snapshot.getText(0, snapshot.getLength())), + getVirtualTsFile(snapshot.getText(0, snapshot.getLength())), + ].filter((v): v is VirtualCode => !!v), + mappings: [], + codegenStacks: [] + }; + } + }, + updateVirtualCode(_uri, virtualCode, snapshot) { + virtualCode.snapshot = snapshot; + virtualCode.embeddedCodes = [ + ...getVirtualCssFiles(snapshot.getText(0, snapshot.getLength())), + getVirtualTsFile(snapshot.getText(0, snapshot.getLength())), + ].filter((v): v is VirtualCode => !!v); + return virtualCode; + }, + typescript: { + extraFileExtensions: [{ + extension: 'svelte', + isMixedContent: true, + scriptKind: 7 satisfies ts.ScriptKind.Deferred, + }], + getServiceScript(root) { + for (const code of forEachEmbeddedCode(root)) { + if (code.id === 'ts') { + return { + code, + scriptKind: 3, + extension: '.ts', + }; + } + } + }, + }, }; function* getVirtualCssFiles(content: string): Generator { - const styleBlocks = [...content.matchAll(/\([\s\S]*?)\<\/style\>/g)]; + const styleBlocks = [...content.matchAll(/\([\s\S]*?)\<\/style\>/g)]; - for (let i = 0; i < styleBlocks.length; i++) { - const styleBlock = styleBlocks[i]; - if (styleBlock.index !== undefined) { - const matchText = styleBlock[1]; - yield { - id: 'css_' + i, - languageId: 'css', - snapshot: { - getText(start, end) { - return matchText.substring(start, end); - }, - getLength() { - return matchText.length; - }, - getChangeRange() { - return undefined; - }, - }, - mappings: [ - { - sourceOffsets: [styleBlock.index + styleBlock[0].indexOf(matchText)], - generatedOffsets: [0], - lengths: [matchText.length], - data: { - verification: true, - completion: true, - semantic: true, - navigation: true, - structure: true, - format: false, - }, - } - ], - embeddedCodes: [], - } - } - } + for (let i = 0; i < styleBlocks.length; i++) { + const styleBlock = styleBlocks[i]; + if (styleBlock.index !== undefined) { + const matchText = styleBlock[1]; + yield { + id: 'css_' + i, + languageId: 'css', + snapshot: { + getText(start, end) { + return matchText.substring(start, end); + }, + getLength() { + return matchText.length; + }, + getChangeRange() { + return undefined; + }, + }, + mappings: [ + { + sourceOffsets: [styleBlock.index + styleBlock[0].indexOf(matchText)], + generatedOffsets: [0], + lengths: [matchText.length], + data: { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: false, + }, + } + ], + embeddedCodes: [], + } + } + } } function getVirtualTsFile(text: string): VirtualCode | undefined { - try { - const tsx = svelte2tsx(text, { - isTsFile: true, - mode: 'ts', - }); - const v3Mappings = decode(tsx.map.mappings); - const document = TextDocument.create('', 'svelte', 0, text); - const generateDocument = TextDocument.create('', 'typescript', 0, tsx.code); - const mappings: CodeMapping[] = []; + try { + const tsx = svelte2tsx(text, { + isTsFile: true, + mode: 'ts', + }); + const v3Mappings = decode(tsx.map.mappings); + const document = TextDocument.create('', 'svelte', 0, text); + const generateDocument = TextDocument.create('', 'typescript', 0, tsx.code); + const mappings: CodeMapping[] = []; - let current: { - genOffset: number, - sourceOffset: number, - } | undefined; + let current: { + genOffset: number, + sourceOffset: number, + } | undefined; - for (let genLine = 0; genLine < v3Mappings.length; genLine++) { - for (const segment of v3Mappings[genLine]) { - const genCharacter = segment[0]; - const genOffset = generateDocument.offsetAt({ line: genLine, character: genCharacter }); - if (current) { - let length = genOffset - current.genOffset; - const sourceText = text.substring(current.sourceOffset, current.sourceOffset + length); - const genText = tsx.code.substring(current.genOffset, current.genOffset + length); - if (sourceText !== genText) { - length = 0; - for (let i = 0; i < genOffset - current.genOffset; i++) { - if (sourceText[i] === genText[i]) { - length = i + 1; - } - else { - break; - } - } - } - if (length > 0) { - const lastMapping = mappings.length ? mappings[mappings.length - 1] : undefined; - if ( - lastMapping && - lastMapping.generatedOffsets[0] + lastMapping.lengths[0] === current.genOffset && - lastMapping.sourceOffsets[0] + lastMapping.lengths[0] === current.sourceOffset - ) { - lastMapping.lengths[0] += length; - } - else { - mappings.push({ - sourceOffsets: [current.sourceOffset], - generatedOffsets: [current.genOffset], - lengths: [length], - data: { - verification: true, - completion: true, - semantic: true, - navigation: true, - structure: false, - format: false, - }, - }); - } - } - current = undefined; - } - if (segment[2] !== undefined && segment[3] !== undefined) { - const sourceOffset = document.offsetAt({ line: segment[2], character: segment[3] }); - current = { - genOffset, - sourceOffset, - }; - } - } - } + for (let genLine = 0; genLine < v3Mappings.length; genLine++) { + for (const segment of v3Mappings[genLine]) { + const genCharacter = segment[0]; + const genOffset = generateDocument.offsetAt({ line: genLine, character: genCharacter }); + if (current) { + let length = genOffset - current.genOffset; + const sourceText = text.substring(current.sourceOffset, current.sourceOffset + length); + const genText = tsx.code.substring(current.genOffset, current.genOffset + length); + if (sourceText !== genText) { + length = 0; + for (let i = 0; i < genOffset - current.genOffset; i++) { + if (sourceText[i] === genText[i]) { + length = i + 1; + } + else { + break; + } + } + } + if (length > 0) { + const lastMapping = mappings.length ? mappings[mappings.length - 1] : undefined; + if ( + lastMapping && + lastMapping.generatedOffsets[0] + lastMapping.lengths[0] === current.genOffset && + lastMapping.sourceOffsets[0] + lastMapping.lengths[0] === current.sourceOffset + ) { + lastMapping.lengths[0] += length; + } + else { + mappings.push({ + sourceOffsets: [current.sourceOffset], + generatedOffsets: [current.genOffset], + lengths: [length], + data: { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: false, + format: false, + }, + }); + } + } + current = undefined; + } + if (segment[2] !== undefined && segment[3] !== undefined) { + const sourceOffset = document.offsetAt({ line: segment[2], character: segment[3] }); + current = { + genOffset, + sourceOffset, + }; + } + } + } - return { - id: 'ts', - languageId: 'typescript', - snapshot: { - getText(start, end) { - return tsx.code.substring(start, end); - }, - getLength() { - return tsx.code.length; - }, - getChangeRange() { - return undefined; - }, - }, - mappings: mappings, - embeddedCodes: [], - }; - } catch { } + return { + id: 'ts', + languageId: 'typescript', + snapshot: { + getText(start, end) { + return tsx.code.substring(start, end); + }, + getLength() { + return tsx.code.length; + }, + getChangeRange() { + return undefined; + }, + }, + mappings: mappings, + embeddedCodes: [], + }; + } catch { } } diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json index c38673e02..40035276e 100644 --- a/packages/svelte-check/package.json +++ b/packages/svelte-check/package.json @@ -27,8 +27,8 @@ "@types/yargs": "^17.0.32" }, "dependencies": { - "@volar/kit": "~2.0.0", - "volar-service-typescript": "volar-2.0", + "@volar/kit": "~2.3.0-alpha.4", + "volar-service-typescript": "volar-2.3", "svelte-language-server": "0.0.0", "chokidar": "^3.4.1", "kleur": "^4.1.5", diff --git a/packages/svelte-check/src/check.ts b/packages/svelte-check/src/check.ts index 5102d2b2d..71cd8ab17 100644 --- a/packages/svelte-check/src/check.ts +++ b/packages/svelte-check/src/check.ts @@ -4,7 +4,7 @@ import * as fg from 'fast-glob'; import { existsSync } from 'node:fs'; import { pathToFileURL } from 'node:url'; import { svelteLanguagePlugin } from 'svelte-language-server/out/languagePlugin.js'; -import { create as createTypeScriptServicePlugin } from 'volar-service-typescript'; +import { create as createTypeScriptServicePlugins } from 'volar-service-typescript'; // Export those for downstream consumers export { Diagnostic, DiagnosticSeverity }; @@ -55,7 +55,7 @@ export class SvelteCheck { | undefined; }): Promise { const files = - fileNames !== undefined ? fileNames : this.linter.languageHost.getScriptFileNames(); + fileNames !== undefined ? fileNames : this.linter.projectHost.getScriptFileNames(); const result: CheckResult = { status: undefined, @@ -92,7 +92,7 @@ export class SvelteCheck { console.info(errorText); } - const fileSnapshot = this.linter.languageHost.getScriptSnapshot(file); + const fileSnapshot = this.linter.projectHost.getScriptSnapshot(file); const fileContent = fileSnapshot?.getText(0, fileSnapshot.getLength()); result.fileResult.push({ @@ -124,13 +124,13 @@ export class SvelteCheck { this.ts = this.typescriptPath ? require(this.typescriptPath) : require('typescript'); const tsconfigPath = this.getTsconfig(); - const languages = [svelteLanguagePlugin]; - const services = [createTypeScriptServicePlugin(this.ts)]; + const languagePlugins = [svelteLanguagePlugin]; + const languageServicePlugins = createTypeScriptServicePlugins(this.ts); if (tsconfigPath) { - this.linter = kit.createTypeScriptChecker(languages, services, tsconfigPath); + this.linter = kit.createTypeScriptChecker(languagePlugins, languageServicePlugins, tsconfigPath); } else { - this.linter = kit.createTypeScriptInferredChecker(languages, services, () => { + this.linter = kit.createTypeScriptInferredChecker(languagePlugins, languageServicePlugins, () => { return fg.sync('**/*.svelte', { cwd: this.workspacePath, ignore: ['node_modules'], diff --git a/packages/svelte-check/src/index.ts b/packages/svelte-check/src/index.ts index de5825d7c..82ccb4529 100644 --- a/packages/svelte-check/src/index.ts +++ b/packages/svelte-check/src/index.ts @@ -44,7 +44,7 @@ export async function check(flags: Partial): Promise { // Dynamically get the list of extensions to watch from the files already included in the project const checkedExtensions = Array.from( new Set( - checker.linter.languageHost.getScriptFileNames().map((fileName) => path.extname(fileName)) + checker.linter.projectHost.getScriptFileNames().map((fileName) => path.extname(fileName)) ) ); createWatcher(workspaceRoot, checkedExtensions) diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index 2c965e7d5..ddfa76502 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -199,8 +199,8 @@ }, "devDependencies": { "@types/vscode": "^1.82.0", - "@volar/language-server": "~2.0.0", - "@volar/vscode": "~2.0.0", + "@volar/language-server": "~2.3.0-alpha.4", + "@volar/vscode": "~2.3.0-alpha.4", "esbuild": "latest", "js-yaml": "^3.14.0", "svelte-language-server": "0.0.0", diff --git a/packages/svelte-vscode/src/extension.ts b/packages/svelte-vscode/src/extension.ts index 09bf8a621..25eed4f91 100644 --- a/packages/svelte-vscode/src/extension.ts +++ b/packages/svelte-vscode/src/extension.ts @@ -1,4 +1,3 @@ -import type { InitializationOptions } from '@volar/language-server'; import * as protocol from '@volar/language-server/protocol'; import { BaseLanguageClient, createLabsInfo, activateTsConfigStatusItem, activateTsVersionStatusItem } from '@volar/vscode'; import * as lsp from '@volar/vscode/node'; @@ -9,52 +8,51 @@ let client: BaseLanguageClient; export async function activate(context: vscode.ExtensionContext) { - const documentSelector: lsp.DocumentSelector = [{ language: 'svelte' }]; - const initializationOptions: InitializationOptions = { - typescript: { - tsdk: path.join( - vscode.env.appRoot, - 'extensions/node_modules/typescript/lib', - ), - }, - }; - const serverModule = vscode.Uri.joinPath(context.extensionUri, 'dist', 'server.js'); - const runOptions = { execArgv: [] }; - const debugOptions = { execArgv: ['--nolazy', '--inspect=' + 6009] }; - const serverOptions: lsp.ServerOptions = { - run: { - module: serverModule.fsPath, - transport: lsp.TransportKind.ipc, - options: runOptions - }, - debug: { - module: serverModule.fsPath, - transport: lsp.TransportKind.ipc, - options: debugOptions - }, - }; - const clientOptions: lsp.LanguageClientOptions = { - documentSelector, - initializationOptions, - }; - client = new lsp.LanguageClient( - 'svelte-language-server', - 'Svelte Language Server', - serverOptions, - clientOptions, - ); - await client.start(); + const documentSelector: lsp.DocumentSelector = [{ language: 'svelte' }]; + const serverModule = vscode.Uri.joinPath(context.extensionUri, 'dist', 'server.js'); + const runOptions = { execArgv: [] }; + const debugOptions = { execArgv: ['--nolazy', '--inspect=' + 6009] }; + const serverOptions: lsp.ServerOptions = { + run: { + module: serverModule.fsPath, + transport: lsp.TransportKind.ipc, + options: runOptions + }, + debug: { + module: serverModule.fsPath, + transport: lsp.TransportKind.ipc, + options: debugOptions + }, + }; + const clientOptions: lsp.LanguageClientOptions = { + documentSelector, + initializationOptions: { + typescript: { + tsdk: path.join( + vscode.env.appRoot, + 'extensions/node_modules/typescript/lib', + ), + }, + }, + }; + client = new lsp.LanguageClient( + 'svelte-language-server', + 'Svelte Language Server', + serverOptions, + clientOptions, + ); + await client.start(); - activateTsConfigStatusItem('svelte', 'svelte.tsConfigStatus', client); - activateTsVersionStatusItem('svelte', 'svelte.tsVersionStatus', context, client, text => text); + activateTsConfigStatusItem('svelte', 'svelte.tsConfigStatus', client); + activateTsVersionStatusItem('svelte', 'svelte.tsVersionStatus', context, text => text); - const labsInfo = createLabsInfo(protocol); - labsInfo.addLanguageClient(client); + const labsInfo = createLabsInfo(protocol); + labsInfo.addLanguageClient(client); - // support https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volarjs-labs - return labsInfo.extensionExports; + // support https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volarjs-labs + return labsInfo.extensionExports; } export function deactivate(): Thenable | undefined { - return client?.stop(); + return client?.stop(); } diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index 33c632826..f357d9f64 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -22,7 +22,7 @@ "author": "The Svelte Community", "license": "MIT", "dependencies": { - "@volar/typescript": "~2.0.0", + "@volar/typescript": "~2.3.0-alpha.4", "svelte-language-server": "0.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d1fb4091..162e0fddc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,23 +28,23 @@ importers: specifier: ^1.4.15 version: 1.4.15 '@volar/language-core': - specifier: ~2.0.0 - version: 2.0.0 + specifier: ~2.3.0-alpha.4 + version: 2.3.0-alpha.4 '@volar/language-server': - specifier: ~2.0.0 - version: 2.0.0 + specifier: ~2.3.0-alpha.4 + version: 2.3.0-alpha.4 svelte2tsx: specifier: workspace:~ version: link:../svelte2tsx volar-service-css: - specifier: volar-2.0 - version: 0.0.27 + specifier: volar-2.3 + version: 0.0.49 volar-service-html: - specifier: volar-2.0 - version: 0.0.27 + specifier: volar-2.3 + version: 0.0.49 volar-service-typescript: - specifier: volar-2.0 - version: 0.0.27(@volar/typescript@2.0.0) + specifier: volar-2.3 + version: 0.0.49 vscode-languageserver-textdocument: specifier: ^1.0.11 version: 1.0.11 @@ -55,8 +55,8 @@ importers: packages/svelte-check: dependencies: '@volar/kit': - specifier: ~2.0.0 - version: 2.0.0(typescript@5.4.5) + specifier: ~2.3.0-alpha.4 + version: 2.3.0-alpha.4(typescript@5.4.5) chokidar: specifier: ^3.4.1 version: 3.5.3 @@ -73,8 +73,8 @@ importers: specifier: 0.0.0 version: link:../language-server volar-service-typescript: - specifier: volar-2.0 - version: 0.0.27(@volar/typescript@2.0.0) + specifier: volar-2.3 + version: 0.0.49 yargs: specifier: ^17.7.2 version: 17.7.2 @@ -89,11 +89,11 @@ importers: specifier: ^1.82.0 version: 1.85.0 '@volar/language-server': - specifier: ~2.0.0 - version: 2.0.0 + specifier: ~2.3.0-alpha.4 + version: 2.3.0-alpha.4 '@volar/vscode': - specifier: ~2.0.0 - version: 2.0.0 + specifier: ~2.3.0-alpha.4 + version: 2.3.0-alpha.4 esbuild: specifier: latest version: 0.19.11 @@ -192,8 +192,8 @@ importers: packages/typescript-plugin: dependencies: '@volar/typescript': - specifier: ~2.0.0 - version: 2.0.0 + specifier: ~2.3.0-alpha.4 + version: 2.3.0-alpha.4 svelte-language-server: specifier: 0.0.0 version: link:../language-server @@ -414,6 +414,15 @@ packages: dev: true optional: true + /@johnsoncodehk/vscode-html-languageservice@5.2.0-34a5462: + resolution: {integrity: sha512-etqLfpSJ5zaw76KUNF603be6d6QsiQPmaHr9FKEp4zhLZJzWCCMH6Icak7MtLUFLZLMpL761mZNImi/joBo1ZA==} + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.11 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + dev: false + /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} @@ -631,32 +640,31 @@ packages: resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} dev: true - /@volar/kit@2.0.0(typescript@5.4.5): - resolution: {integrity: sha512-1l6KjWjX1HT7Hf5kHT4hhDu1KEPNvGPoCr/ypwRbk5jFkBVNNv9eA5H2e8obXztlESLvQ1RDOQxyxv0PIXfg4g==} + /@volar/kit@2.3.0-alpha.4(typescript@5.4.5): + resolution: {integrity: sha512-LKOJQKECmEDwneYO3R+DDd4tIDb419bMcr6kM9fyNAFM7yHKpkJhbzHlFfIc0QNV6ek2ft38BtPv3JnBNNMfZw==} peerDependencies: typescript: '*' dependencies: - '@volar/language-service': 2.0.0 - '@volar/typescript': 2.0.0 + '@volar/language-service': 2.3.0-alpha.4 + '@volar/typescript': 2.3.0-alpha.4 typesafe-path: 0.2.2 typescript: 5.4.5 vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 dev: false - /@volar/language-core@2.0.0: - resolution: {integrity: sha512-1L9GB7CwI4LKKwpKOxFSICJ54GdWGwjIUkI7UwAjJpnFIFm8+xteSf86nsEknFh0k7MHScm+Xb0x8eEx/8Qdrg==} + /@volar/language-core@2.3.0-alpha.4: + resolution: {integrity: sha512-SYttwzcsJOkKtDDJBRkGqt+CwDpyZjk10r3ho4PwCLjVW2AhbGDgHffEEhlXKhGbD7XrG6ZxxDy0N/9tP+Vmag==} dependencies: - '@volar/source-map': 2.0.0 + '@volar/source-map': 2.3.0-alpha.4 - /@volar/language-server@2.0.0: - resolution: {integrity: sha512-sfSXbCgVf7wiyKsGmeCt0y6qpFE4sma45LUd87Rs/Wv6I1UH/B8TP7UZ/6grAKh5YO5GF8WOv2zovn5Nmw7z3g==} + /@volar/language-server@2.3.0-alpha.4: + resolution: {integrity: sha512-lwlQEEZfDT0QBN1tXOWPoukV96+VdudJLorSYGTcXWpfWAfTWYtusewAie5TR4vOtauA1ygqhVUOf5JD1E95Ng==} dependencies: - '@volar/language-core': 2.0.0 - '@volar/language-service': 2.0.0 - '@volar/snapshot-document': 2.0.0 - '@volar/typescript': 2.0.0 - '@vscode/l10n': 0.0.16 + '@volar/language-core': 2.3.0-alpha.4 + '@volar/language-service': 2.3.0-alpha.4 + '@volar/snapshot-document': 2.3.0-alpha.4 + '@volar/typescript': 2.3.0-alpha.4 path-browserify: 1.0.1 request-light: 0.7.0 vscode-languageserver: 9.0.1 @@ -664,35 +672,36 @@ packages: vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 - /@volar/language-service@2.0.0: - resolution: {integrity: sha512-aWalwQ54DAEvJ9dfEyJN/pr6ID5GWL7dBR46CMx6fr9JK/m9NkXaK7qJRcxKXaaSTYtlWA342dEhYESLB2dAYw==} + /@volar/language-service@2.3.0-alpha.4: + resolution: {integrity: sha512-6B40lwFLmjh8c+OfTmcfxJQPUlzmC3hi0zSTnNeg8fvLfo6pWielQU80Spzzx3pA9TExOqSa1UtJY9D/JQC6FA==} dependencies: - '@volar/language-core': 2.0.0 + '@volar/language-core': 2.3.0-alpha.4 vscode-languageserver-protocol: 3.17.5 vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 - /@volar/snapshot-document@2.0.0: - resolution: {integrity: sha512-hf2meJIrJw848KaOr8dCQlCgGS0UWUVd/m4DkTnM4Rlo+fVmzd7CjZoYqjYMiuIqxeu3OxpypcP++fv4Rp28Ew==} + /@volar/snapshot-document@2.3.0-alpha.4: + resolution: {integrity: sha512-IMwAbb9Q3H2Gzv/ywYuNEHnzvZBStw93EnnEnKbEtwVUTnfE5jZrUZhwoE1LGWJG2OtAecQziyyLdqx2fdbwuQ==} dependencies: vscode-languageserver-protocol: 3.17.5 vscode-languageserver-textdocument: 1.0.11 - /@volar/source-map@2.0.0: - resolution: {integrity: sha512-WZO1hZPqMGyCmWs9j4ZV090D7jfMjQUwALuz5CCbrMVSCGlvYxv5jhw0M5G39thJmiNV/ttgdgiavzvtOMe5/w==} + /@volar/source-map@2.3.0-alpha.4: + resolution: {integrity: sha512-cY73k8BZ6WqraWknof/PpA+MBiPq6fI6ShQ7FoQfPF89Vx56XKCu3fZp3tlaA/o0UwqQQkjVlqUsa6bmUafygQ==} dependencies: muggle-string: 0.4.1 - /@volar/typescript@2.0.0: - resolution: {integrity: sha512-6HGiCOMe612taDDRTX1N5938hY2ThjdLdW3TXHsIgcQz8Y+2H8o94BDwXnMOGf+XksektuXMZkDBalgLPFhBbg==} + /@volar/typescript@2.3.0-alpha.4: + resolution: {integrity: sha512-dQtdczowQCnGlJckYsfUXyieFgTpBNXYPY/EU+UzVmaA2QAmuTQPmJdx9R2CwUtbxDZAA1Zi5gNP0qscUPk/CA==} dependencies: - '@volar/language-core': 2.0.0 + '@volar/language-core': 2.3.0-alpha.4 path-browserify: 1.0.1 + vscode-uri: 3.0.8 - /@volar/vscode@2.0.0: - resolution: {integrity: sha512-FqNMA5MwAARbjP6vZQYJd2a6ydyzUfFO8XTV6+mJPQVzSlCKoxIvV5QbfgEa1P7AGT1DuuiSr/1WcfFEF5/s1A==} + /@volar/vscode@2.3.0-alpha.4: + resolution: {integrity: sha512-6kGXztFKnWPOvXjA4rkIsiljV8nu/MIxQ0y2ROrA2EghsNgLVMYBFS+cxdZaHPOVDMBO1Kz/p7C1fdHybJ22BQ==} dependencies: - '@volar/language-server': 2.0.0 + '@volar/language-server': 2.3.0-alpha.4 path-browserify: 1.0.1 vscode-languageclient: 9.0.1 vscode-nls: 5.2.0 @@ -700,6 +709,11 @@ packages: /@vscode/l10n@0.0.16: resolution: {integrity: sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==} + dev: false + + /@vscode/l10n@0.0.18: + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + dev: false /acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} @@ -2260,40 +2274,40 @@ packages: unist-util-stringify-position: 3.0.3 dev: true - /volar-service-css@0.0.27: - resolution: {integrity: sha512-wPBj6nUqiLSNwbADRQ5hIX1LoKYROuVTxAKg4IiJGgw1A7MnLxvoOAOef+ruxtVDHW5GCgeN+3QSV68UOIxS/g==} + /volar-service-css@0.0.49: + resolution: {integrity: sha512-uZNq8xOAdVZZ2eoC6HRVfsBDM3lZ7dybNTVmNrTHfscOMPuoei6HOYQxT6juYe0OTxHExs7aajTn+zodNpn8eg==} peerDependencies: - '@volar/language-service': ~2.0 + '@volar/language-service': ~2.3.0-alpha.1 peerDependenciesMeta: '@volar/language-service': optional: true dependencies: vscode-css-languageservice: 6.2.10 + vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 dev: false - /volar-service-html@0.0.27: - resolution: {integrity: sha512-hyXgNaaiSJhj7f1et7L9BRpTYMzr+3rvlPhGvgZpqurJx+J00Y3wWmlU4adUWglKTXSAswgedKUvSNGn4BGAzw==} + /volar-service-html@0.0.49: + resolution: {integrity: sha512-KjabMiBGLOB4J9IWP1Y3EtovcRjcJJEfvYEoslBkX7Y2kps+LKkutaJWjwW+smNRoCzlDArrT5w9wt71AU33Jw==} peerDependencies: - '@volar/language-service': ~2.0 + '@volar/language-service': ~2.3.0-alpha.1 peerDependenciesMeta: '@volar/language-service': optional: true dependencies: - vscode-html-languageservice: 5.1.1 + vscode-html-languageservice: /@johnsoncodehk/vscode-html-languageservice@5.2.0-34a5462 + vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 dev: false - /volar-service-typescript@0.0.27(@volar/typescript@2.0.0): - resolution: {integrity: sha512-O8BEz5NFy4bYU36LKCXEQl08pl9mf6AOV/xpLo3VTiMK1CJiwax7XbPkE6SZKb1QfQe0gn5RbvHeRpzSCz6H5g==} + /volar-service-typescript@0.0.49: + resolution: {integrity: sha512-gw96ukNPCoJJc38/z3VTtbo9vWRBGKYWHsOuwmvaOd/NIR8JIa/uFsI+Kmmjn6Cw/9T8ZcuHX9NUVFQLKLTaHg==} peerDependencies: - '@volar/language-service': ~2.0 - '@volar/typescript': ~2.0 + '@volar/language-service': ~2.3.0-alpha.1 peerDependenciesMeta: '@volar/language-service': optional: true dependencies: - '@volar/typescript': 2.0.0 path-browserify: 1.0.1 semver: 7.5.4 typescript-auto-import-cache: 0.3.1 @@ -2339,15 +2353,6 @@ packages: vscode-uri: 3.0.8 dev: false - /vscode-html-languageservice@5.1.1: - resolution: {integrity: sha512-JenrspIIG/Q+93R6G3L6HdK96itSisMynE0glURqHpQbL3dKAKzdm8L40lAHNkwJeBg+BBPpAshZKv/38onrTQ==} - dependencies: - '@vscode/l10n': 0.0.16 - vscode-languageserver-textdocument: 1.0.11 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.0.8 - dev: false - /vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'}