From a74fbf647fd27403d4b80c7cceed30a4a59d1a90 Mon Sep 17 00:00:00 2001
From: Ignatius Bagus
Date: Fri, 16 Apr 2021 20:58:42 +0700
Subject: [PATCH 1/9] adjust and use overrides in prettier config
---
.prettierrc | 27 +++++++++++++++++++++------
1 file changed, 21 insertions(+), 6 deletions(-)
diff --git a/.prettierrc b/.prettierrc
index 5a219d934..b8797bfb7 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,8 +1,23 @@
{
- "useTabs": false,
- "printWidth": 100,
- "tabWidth": 4,
- "semi": true,
- "trailingComma": "none",
- "singleQuote": true
+ "arrowParens": "always",
+ "useTabs": true,
+ "printWidth": 100,
+ "tabWidth": 4,
+ "semi": true,
+ "trailingComma": "none",
+ "singleQuote": true,
+ "overrides": [
+ {
+ "files": ["package.json"],
+ "options": {
+ "tabWidth": 2
+ }
+ },
+ {
+ "files": ["*.md"],
+ "options": {
+ "useTabs": false
+ }
+ }
+ ]
}
From 2316f7fdb81e20c769939ef73afa6bd9e4a30fbd Mon Sep 17 00:00:00 2001
From: Ignatius Bagus
Date: Fri, 16 Apr 2021 21:07:38 +0700
Subject: [PATCH 2/9] add formatted package.json's
---
package.json | 58 +-
packages/language-server/package.json | 136 +--
packages/svelte-check/package.json | 110 +--
packages/svelte-vscode/package.json | 1032 +++++++++++------------
packages/svelte2tsx/package.json | 128 +--
packages/typescript-plugin/package.json | 42 +-
6 files changed, 753 insertions(+), 753 deletions(-)
diff --git a/package.json b/package.json
index d09836956..237731b35 100644
--- a/package.json
+++ b/package.json
@@ -1,31 +1,31 @@
{
- "name": "@svelte/language-tools",
- "version": "1.0.0",
- "author": "Svelte Contributors",
- "license": "MIT",
- "private": true,
- "workspaces": [
- "packages/*"
- ],
- "scripts": {
- "bootstrap": "yarn workspace svelte2tsx build && yarn workspace svelte-vscode build:grammar",
- "build": "tsc -b",
- "test": "cross-env CI=true yarn workspaces run test",
- "watch": "tsc -b -watch",
- "format": "prettier --write .",
- "lint": "prettier --check . && eslint \"packages/**/*.{ts,js}\""
- },
- "dependencies": {
- "typescript": "^4.2.2"
- },
- "devDependencies": {
- "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.2.0",
- "@typescript-eslint/eslint-plugin": "^4.3.0",
- "@typescript-eslint/parser": "^4.3.0",
- "eslint": "^7.7.0",
- "eslint-plugin-import": "^2.22.1",
- "eslint-plugin-svelte3": "^2.7.3",
- "prettier": "2.2.1",
- "cross-env": "^7.0.2"
- }
+ "name": "@svelte/language-tools",
+ "version": "1.0.0",
+ "author": "Svelte Contributors",
+ "license": "MIT",
+ "private": true,
+ "workspaces": [
+ "packages/*"
+ ],
+ "scripts": {
+ "bootstrap": "yarn workspace svelte2tsx build && yarn workspace svelte-vscode build:grammar",
+ "build": "tsc -b",
+ "test": "cross-env CI=true yarn workspaces run test",
+ "watch": "tsc -b -watch",
+ "format": "prettier --write .",
+ "lint": "prettier --check . && eslint \"packages/**/*.{ts,js}\""
+ },
+ "dependencies": {
+ "typescript": "^4.2.2"
+ },
+ "devDependencies": {
+ "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.2.0",
+ "@typescript-eslint/eslint-plugin": "^4.3.0",
+ "@typescript-eslint/parser": "^4.3.0",
+ "eslint": "^7.7.0",
+ "eslint-plugin-import": "^2.22.1",
+ "eslint-plugin-svelte3": "^2.7.3",
+ "prettier": "2.2.1",
+ "cross-env": "^7.0.2"
+ }
}
diff --git a/packages/language-server/package.json b/packages/language-server/package.json
index a1fb7e8c8..14a480b29 100644
--- a/packages/language-server/package.json
+++ b/packages/language-server/package.json
@@ -1,70 +1,70 @@
{
- "name": "svelte-language-server",
- "version": "0.13.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\"",
- "build": "tsc",
- "prepublishOnly": "npm run build",
- "watch": "tsc -w"
- },
- "bin": {
- "svelteserver": "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": {
- "@tsconfig/node12": "^1.0.0",
- "@types/estree": "^0.0.42",
- "@types/glob": "^7.1.1",
- "@types/lodash": "^4.14.116",
- "@types/mocha": "^7.0.2",
- "@types/node": "^13.9.0",
- "@types/prettier": "^1.13.2",
- "@types/sinon": "^7.5.2",
- "@types/source-map": "^0.5.7",
- "cross-env": "^7.0.2",
- "mocha": "^7.1.0",
- "sinon": "^9.0.0",
- "ts-node": "^8.6.2"
- },
- "dependencies": {
- "chokidar": "^3.4.1",
- "estree-walker": "^2.0.1",
- "glob": "^7.1.6",
- "lodash": "^4.17.19",
- "prettier": "2.2.1",
- "prettier-plugin-svelte": "~2.2.0",
- "source-map": "^0.7.3",
- "svelte": "~3.35.0",
- "svelte-preprocess": "~4.6.1",
- "svelte2tsx": "*",
- "typescript": "*",
- "vscode-css-languageservice": "5.0.0",
- "vscode-emmet-helper": "2.1.2",
- "vscode-html-languageservice": "4.0.0",
- "vscode-languageserver": "7.0.0",
- "vscode-languageserver-types": "3.16.0",
- "vscode-uri": "2.1.2"
- }
+ "name": "svelte-language-server",
+ "version": "0.13.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\"",
+ "build": "tsc",
+ "prepublishOnly": "npm run build",
+ "watch": "tsc -w"
+ },
+ "bin": {
+ "svelteserver": "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": {
+ "@tsconfig/node12": "^1.0.0",
+ "@types/estree": "^0.0.42",
+ "@types/glob": "^7.1.1",
+ "@types/lodash": "^4.14.116",
+ "@types/mocha": "^7.0.2",
+ "@types/node": "^13.9.0",
+ "@types/prettier": "^1.13.2",
+ "@types/sinon": "^7.5.2",
+ "@types/source-map": "^0.5.7",
+ "cross-env": "^7.0.2",
+ "mocha": "^7.1.0",
+ "sinon": "^9.0.0",
+ "ts-node": "^8.6.2"
+ },
+ "dependencies": {
+ "chokidar": "^3.4.1",
+ "estree-walker": "^2.0.1",
+ "glob": "^7.1.6",
+ "lodash": "^4.17.19",
+ "prettier": "2.2.1",
+ "prettier-plugin-svelte": "~2.2.0",
+ "source-map": "^0.7.3",
+ "svelte": "~3.35.0",
+ "svelte-preprocess": "~4.6.1",
+ "svelte2tsx": "*",
+ "typescript": "*",
+ "vscode-css-languageservice": "5.0.0",
+ "vscode-emmet-helper": "2.1.2",
+ "vscode-html-languageservice": "4.0.0",
+ "vscode-languageserver": "7.0.0",
+ "vscode-languageserver-types": "3.16.0",
+ "vscode-uri": "2.1.2"
+ }
}
diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json
index 5ac335bd0..e5cd6a0e5 100644
--- a/packages/svelte-check/package.json
+++ b/packages/svelte-check/package.json
@@ -1,57 +1,57 @@
{
- "name": "svelte-check",
- "description": "Svelte Code Checker Terminal Interface",
- "version": "1.1.0",
- "main": "./dist/src/index.js",
- "bin": "./bin/svelte-check",
- "author": "The Svelte Community",
- "license": "MIT",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/sveltejs/language-tools.git"
- },
- "keywords": [
- "svelte",
- "cli"
- ],
- "bugs": {
- "url": "https://github.com/sveltejs/language-tools/issues"
- },
- "homepage": "https://github.com/sveltejs/language-tools#readme",
- "dependencies": {
- "chalk": "^4.0.0",
- "chokidar": "^3.4.1",
- "glob": "^7.1.6",
- "import-fresh": "^3.2.1",
- "minimist": "^1.2.5",
- "source-map": "^0.7.3",
- "svelte-preprocess": "^4.0.0",
- "typescript": "*"
- },
- "peerDependencies": {
- "svelte": "^3.24.0"
- },
- "scripts": {
- "build": "rollup -c",
- "prepublishOnly": "npm run build",
- "test": "echo 'NOOP'"
- },
- "devDependencies": {
- "@rollup/plugin-typescript": "^6.0.0",
- "@rollup/plugin-commonjs": "^15.0.0",
- "@rollup/plugin-json": "^4.0.0",
- "@rollup/plugin-node-resolve": "^9.0.0",
- "@rollup/plugin-replace": "2.3.3",
- "@tsconfig/node12": "^1.0.0",
- "@types/glob": "^7.1.1",
- "@types/minimist": "^1.2.0",
- "rollup": "^2.28.0",
- "rollup-plugin-cleanup": "^3.0.0",
- "rollup-plugin-copy": "^3.0.0",
- "svelte-language-server": "*",
- "vscode-languageserver": "7.0.0",
- "vscode-languageserver-protocol": "3.16.0",
- "vscode-languageserver-types": "3.16.0",
- "vscode-uri": "2.1.2"
- }
+ "name": "svelte-check",
+ "description": "Svelte Code Checker Terminal Interface",
+ "version": "1.1.0",
+ "main": "./dist/src/index.js",
+ "bin": "./bin/svelte-check",
+ "author": "The Svelte Community",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/sveltejs/language-tools.git"
+ },
+ "keywords": [
+ "svelte",
+ "cli"
+ ],
+ "bugs": {
+ "url": "https://github.com/sveltejs/language-tools/issues"
+ },
+ "homepage": "https://github.com/sveltejs/language-tools#readme",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "chokidar": "^3.4.1",
+ "glob": "^7.1.6",
+ "import-fresh": "^3.2.1",
+ "minimist": "^1.2.5",
+ "source-map": "^0.7.3",
+ "svelte-preprocess": "^4.0.0",
+ "typescript": "*"
+ },
+ "peerDependencies": {
+ "svelte": "^3.24.0"
+ },
+ "scripts": {
+ "build": "rollup -c",
+ "prepublishOnly": "npm run build",
+ "test": "echo 'NOOP'"
+ },
+ "devDependencies": {
+ "@rollup/plugin-typescript": "^6.0.0",
+ "@rollup/plugin-commonjs": "^15.0.0",
+ "@rollup/plugin-json": "^4.0.0",
+ "@rollup/plugin-node-resolve": "^9.0.0",
+ "@rollup/plugin-replace": "2.3.3",
+ "@tsconfig/node12": "^1.0.0",
+ "@types/glob": "^7.1.1",
+ "@types/minimist": "^1.2.0",
+ "rollup": "^2.28.0",
+ "rollup-plugin-cleanup": "^3.0.0",
+ "rollup-plugin-copy": "^3.0.0",
+ "svelte-language-server": "*",
+ "vscode-languageserver": "7.0.0",
+ "vscode-languageserver-protocol": "3.16.0",
+ "vscode-languageserver-types": "3.16.0",
+ "vscode-uri": "2.1.2"
+ }
}
diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json
index f4b002ad2..fef8180e3 100644
--- a/packages/svelte-vscode/package.json
+++ b/packages/svelte-vscode/package.json
@@ -1,518 +1,518 @@
{
- "name": "svelte-vscode",
- "version": "0.5.0",
- "description": "Svelte language support for VS Code",
- "main": "dist/src/extension.js",
- "scripts": {
- "build:grammar": "npx js-yaml syntaxes/svelte.tmLanguage.src.yaml > syntaxes/svelte.tmLanguage.json",
- "build:ts": "tsc -p ./",
- "build": "npm run build:ts && npm run build:grammar",
- "vscode:prepublish": "npm run build && npm prune --production",
- "watch": "npm run build:grammar && tsc -w -p ./",
- "test": "echo 'NOOP'"
- },
- "repository": {
- "type": "git",
- "url": "https://github.com/sveltejs/language-tools.git"
- },
- "keywords": [
- "svelte",
- "vscode"
- ],
- "author": "James Birtles & the Svelte Core Team",
- "license": "MIT",
- "bugs": {
- "url": "https://github.com/sveltejs/language-tools/issues"
- },
- "homepage": "https://github.com/sveltejs/language-tools#readme",
- "displayName": "Svelte for VS Code",
- "publisher": "svelte",
- "icon": "icons/logo.png",
- "galleryBanner": {
- "color": "#FF3E00",
- "theme": "dark"
- },
- "categories": [
- "Programming Languages",
- "Formatters"
- ],
- "engines": {
- "vscode": "^1.52.0"
- },
- "activationEvents": [
- "onLanguage:svelte",
- "onCommand:svelte.restartLanguageServer"
- ],
- "contributes": {
- "configuration": {
- "type": "object",
- "title": "Svelte",
- "properties": {
- "svelte.language-server.runtime": {
- "scope": "application",
- "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": {
- "scope": "application",
- "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 setting can only be changed in user settings for security reasons."
- },
- "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.trace.server": {
- "type": "string",
- "enum": [
- "off",
- "messages",
- "verbose"
- ],
- "default": "off",
- "description": "Traces the communication between VS Code and the Svelte Language Server."
- },
- "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.findReferences.enable": {
- "type": "boolean",
- "default": true,
- "title": "TypeScript: Find References",
- "description": "Enable find-references for TypeScript"
- },
- "svelte.plugin.typescript.definitions.enable": {
- "type": "boolean",
- "default": true,
- "title": "TypeScript: Go to Definition",
- "description": "Enable go to definition 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.rename.enable": {
- "type": "boolean",
- "default": true,
- "title": "TypeScript: Rename",
- "description": "Enable rename functionality for JS/TS variables inside Svelte files"
- },
- "svelte.plugin.typescript.semanticTokens.enable": {
- "type": "boolean",
- "default": true,
- "title": "TypeScript: Semantic Tokens",
- "description": "Enable semantic tokens (semantic highlight) for TypeScript. Doesn't apply to JavaScript"
- },
- "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.html.renameTags.enable": {
- "type": "boolean",
- "default": true,
- "title": "HTML: Rename tags",
- "description": "Enable rename for the opening/closing tag pairs in 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 ``
- );
- }
+ 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}>${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 '..'`
@@ -142,21 +142,21 @@ const scriptRelativeImportRegex = /import\s+{[^}]*}.*['"`](((\.\/)|(\.\.\/)).*?)
const styleRelativeImportRege = /@import\s+['"`](((\.\/)|(\.\.\/)).*?)['"`]/g;
function updateRelativeImports(
- svelteDoc: SvelteDocument,
- tagText: string,
- newComponentRelativePath: string,
- isStyleTag: boolean
+ 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;
+ 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
index da5953c51..d090e9899 100644
--- a/packages/language-server/src/plugins/svelte/features/getCodeActions/index.ts
+++ b/packages/language-server/src/plugins/svelte/features/getCodeActions/index.ts
@@ -1,34 +1,34 @@
import {
- CodeAction,
- CodeActionContext,
- CodeActionKind,
- Range,
- WorkspaceEdit
+ 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
+ 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);
- }
+ const svelteDiagnostics = context.diagnostics.filter(isIgnorableSvelteDiagnostic);
+ if (
+ svelteDiagnostics.length &&
+ (!context.only || context.only.includes(CodeActionKind.QuickFix))
+ ) {
+ return await getQuickfixActions(svelteDoc, svelteDiagnostics);
+ }
- return [];
+ return [];
}
export async function executeCommand(
- svelteDoc: SvelteDocument,
- command: string,
- args?: any[]
+ svelteDoc: SvelteDocument,
+ command: string,
+ args?: any[]
): Promise {
- return await executeRefactoringCommand(svelteDoc, command, args);
+ 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
index 8b16a17b2..b9fe084ae 100644
--- a/packages/language-server/src/plugins/svelte/features/getCompletions.ts
+++ b/packages/language-server/src/plugins/svelte/features/getCompletions.ts
@@ -1,11 +1,11 @@
import { EOL } from 'os';
import { SvelteDocument } from '../SvelteDocument';
import {
- Position,
- CompletionList,
- CompletionItemKind,
- CompletionItem,
- InsertTextFormat
+ Position,
+ CompletionList,
+ CompletionItemKind,
+ CompletionItem,
+ InsertTextFormat
} from 'vscode-languageserver';
import { SvelteTag, documentation, getLatestOpeningTag } from './SvelteTags';
import { isInTag } from '../../../lib/documents';
@@ -13,204 +13,204 @@ import { isInTag } from '../../../lib/documents';
const HTML_COMMENT_START = ' find out which one
- return getLatestOpeningTag(svelteDoc, offset);
+ if (!foundTag) {
+ return null;
+ }
+ if (foundTag.tag !== ':else') {
+ return foundTag.tag;
+ }
+ // ':else can be part of one of each, await, if --> find out which one
+ return getLatestOpeningTag(svelteDoc, offset);
}
function isAroundOffset(
- charactersOffset: number,
- charactersAroundOffset: string,
- toFind: string,
- offset: number
+ 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 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'] },
- // @
- { tag: 'html' as const, values: ['@html'] },
- { tag: 'debug' as const, values: ['@debug'] },
- // this tag has multiple possibilities
- { tag: ':else' as const, values: [':else'] }
+ { 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'] },
+ // @
+ { tag: 'html' as const, values: ['@html'] },
+ { tag: 'debug' as const, values: ['@debug'] },
+ // 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|})`
+ `[\\s\\S]*{\\s*(${flatten(tagPossibilities.map((p) => p.values)).join('|')})(\\s|})`
);
diff --git a/packages/language-server/src/plugins/svelte/features/getSelectionRanges.ts b/packages/language-server/src/plugins/svelte/features/getSelectionRanges.ts
index c9cbdadc5..49955e281 100644
--- a/packages/language-server/src/plugins/svelte/features/getSelectionRanges.ts
+++ b/packages/language-server/src/plugins/svelte/features/getSelectionRanges.ts
@@ -8,61 +8,61 @@ import { SvelteDocument } from '../SvelteDocument';
type Node = any;
type OffsetRange = {
- start: number;
- end: number;
+ 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 { 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;
- }
- }
+ 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;
+ let nearest: OffsetRange = html;
+ let result: SelectionRange | undefined;
- walk(html, {
- enter(node: Node, parent: Node) {
- if (!parent) {
- // keep looking
- return;
- }
+ walk(html, {
+ enter(node: Node, parent: Node) {
+ if (!parent) {
+ // keep looking
+ return;
+ }
- if (!('start' in node && 'end' in node)) {
- this.skip();
- return;
- }
+ if (!('start' in node && 'end' in node)) {
+ this.skip();
+ return;
+ }
- const { start, end } = node;
- const isWithin = start <= offset && end >= offset;
+ const { start, end } = node;
+ const isWithin = start <= offset && end >= offset;
- if (!isWithin) {
- this.skip();
- return;
- }
+ if (!isWithin) {
+ this.skip();
+ return;
+ }
- if (nearest === parent) {
- nearest = node;
- result = createSelectionRange(node, result);
- }
- }
- });
+ if (nearest === parent) {
+ nearest = node;
+ result = createSelectionRange(node, result);
+ }
+ }
+ });
- return result ? mapSelectionRangeToParent(transpiled, result) : null;
+ 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);
- }
+ 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/typescript/DocumentMapper.ts b/packages/language-server/src/plugins/typescript/DocumentMapper.ts
index 6f15c0299..d468b7274 100644
--- a/packages/language-server/src/plugins/typescript/DocumentMapper.ts
+++ b/packages/language-server/src/plugins/typescript/DocumentMapper.ts
@@ -3,27 +3,27 @@ import { SourceMapConsumer } from 'source-map';
import { SourceMapDocumentMapper } from '../../lib/documents';
export class ConsumerDocumentMapper extends SourceMapDocumentMapper {
- constructor(consumer: SourceMapConsumer, sourceUri: string, private nrPrependesLines: number) {
- super(consumer, sourceUri);
- }
+ constructor(consumer: SourceMapConsumer, sourceUri: string, private nrPrependesLines: number) {
+ super(consumer, sourceUri);
+ }
- getOriginalPosition(generatedPosition: Position): Position {
- return super.getOriginalPosition(
- Position.create(
- generatedPosition.line - this.nrPrependesLines,
- generatedPosition.character
- )
- );
- }
+ 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;
- }
+ 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;
- }
+ 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
index c02f8b826..07661d222 100644
--- a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
+++ b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
@@ -3,31 +3,31 @@ import svelte2tsx, { IExportedNames, ComponentEvents } from 'svelte2tsx';
import ts from 'typescript';
import { Position, Range, TextDocumentContentChangeEvent } from 'vscode-languageserver';
import {
- Document,
- DocumentMapper,
- FragmentMapper,
- IdentityMapper,
- offsetAt,
- positionAt,
- TagInformation,
- isInTag
+ Document,
+ DocumentMapper,
+ FragmentMapper,
+ IdentityMapper,
+ offsetAt,
+ positionAt,
+ TagInformation,
+ isInTag
} from '../../lib/documents';
import { pathToUrl } from '../../utils';
import { ConsumerDocumentMapper } from './DocumentMapper';
import {
- getScriptKindFromAttributes,
- getScriptKindFromFileName,
- isSvelteFilePath,
- getTsCheckComment
+ getScriptKindFromAttributes,
+ getScriptKindFromFileName,
+ isSvelteFilePath,
+ getTsCheckComment
} from './utils';
/**
* An error which occured while trying to parse/preprocess the svelte file contents.
*/
export interface ParserError {
- message: string;
- range: Range;
- code: number;
+ message: string;
+ range: Range;
+ code: number;
}
/**
@@ -40,280 +40,280 @@ export const INITIAL_VERSION = 0;
* Can be a svelte or ts/js file.
*/
export interface DocumentSnapshot extends ts.IScriptSnapshot {
- version: number;
- filePath: string;
- scriptKind: ts.ScriptKind;
- positionAt(offset: number): Position;
- /**
- * Instantiates a source mapper.
- * `destroyFragment` needs to be called when
- * it's no longer needed / the class should be cleaned up
- * in order to prevent memory leaks.
- */
- getFragment(): Promise;
- /**
- * Needs to be called when source mapper
- * is no longer needed / the class should be cleaned up
- * in order to prevent memory leaks.
- */
- destroyFragment(): void;
- /**
- * Convenience function for getText(0, getLength())
- */
- getFullText(): string;
+ version: number;
+ filePath: string;
+ scriptKind: ts.ScriptKind;
+ positionAt(offset: number): Position;
+ /**
+ * Instantiates a source mapper.
+ * `destroyFragment` needs to be called when
+ * it's no longer needed / the class should be cleaned up
+ * in order to prevent memory leaks.
+ */
+ getFragment(): Promise;
+ /**
+ * Needs to be called when source mapper
+ * is no longer needed / the class should be cleaned up
+ * in order to prevent memory leaks.
+ */
+ destroyFragment(): void;
+ /**
+ * Convenience function for getText(0, getLength())
+ */
+ getFullText(): string;
}
/**
* The mapper to get from original snapshot positions to generated and vice versa.
*/
export interface SnapshotFragment extends DocumentMapper {
- scriptInfo: TagInformation | null;
- positionAt(offset: number): Position;
- offsetAt(position: Position): number;
+ scriptInfo: TagInformation | null;
+ positionAt(offset: number): Position;
+ offsetAt(position: Position): number;
}
/**
* Options that apply to svelte files.
*/
export interface SvelteSnapshotOptions {
- strictMode: boolean;
- transformOnTemplateError: boolean;
+ strictMode: boolean;
+ transformOnTemplateError: boolean;
}
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,
- text,
- exportedNames,
- componentEvents,
- parserError,
- nrPrependedLines,
- scriptKind
- } = preprocessSvelteFile(document, options);
-
- return new SvelteDocumentSnapshot(
- document,
- parserError,
- scriptKind,
- text,
- nrPrependedLines,
- exportedNames,
- componentEvents,
- tsxMap
- );
- }
-
- /**
- * 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
- ) {
- if (isSvelteFilePath(filePath)) {
- return DocumentSnapshot.fromSvelteFilePath(filePath, createDocument, options);
- } else {
- return DocumentSnapshot.fromNonSvelteFilePath(filePath);
- }
- }
-
- /**
- * 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) {
- const originalText = ts.sys.readFile(filePath) ?? '';
- 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);
- }
+ /**
+ * 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,
+ text,
+ exportedNames,
+ componentEvents,
+ parserError,
+ nrPrependedLines,
+ scriptKind
+ } = preprocessSvelteFile(document, options);
+
+ return new SvelteDocumentSnapshot(
+ document,
+ parserError,
+ scriptKind,
+ text,
+ nrPrependedLines,
+ exportedNames,
+ componentEvents,
+ tsxMap
+ );
+ }
+
+ /**
+ * 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
+ ) {
+ if (isSvelteFilePath(filePath)) {
+ return DocumentSnapshot.fromSvelteFilePath(filePath, createDocument, options);
+ } else {
+ return DocumentSnapshot.fromNonSvelteFilePath(filePath);
+ }
+ }
+
+ /**
+ * 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) {
+ const originalText = ts.sys.readFile(filePath) ?? '';
+ 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: RawSourceMap | undefined;
- let parserError: ParserError | null = null;
- let nrPrependedLines = 0;
- let text = document.getText();
- let exportedNames: IExportedNames = { has: () => false };
- let componentEvents: ComponentEvents | undefined = undefined;
-
- const scriptKind = [
- getScriptKindFromAttributes(document.scriptInfo?.attributes ?? {}),
- getScriptKindFromAttributes(document.moduleScriptInfo?.attributes ?? {})
- ].includes(ts.ScriptKind.TSX)
- ? ts.ScriptKind.TSX
- : ts.ScriptKind.JSX;
-
- try {
- const tsx = svelte2tsx(text, {
- strictMode: options.strictMode,
- filename: document.getFilePath() ?? undefined,
- isTsFile: scriptKind === ts.ScriptKind.TSX,
- emitOnTemplateError: options.transformOnTemplateError,
- namespace: document.config?.compilerOptions?.namespace
- });
- text = tsx.code;
- tsxMap = tsx.map;
- exportedNames = tsx.exportedNames;
- componentEvents = tsx.events;
- 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) {
- // 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 ?? 0,
- 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,
- componentEvents,
- parserError,
- nrPrependedLines,
- scriptKind
- };
+ let tsxMap: RawSourceMap | undefined;
+ let parserError: ParserError | null = null;
+ let nrPrependedLines = 0;
+ let text = document.getText();
+ let exportedNames: IExportedNames = { has: () => false };
+ let componentEvents: ComponentEvents | undefined = undefined;
+
+ const scriptKind = [
+ getScriptKindFromAttributes(document.scriptInfo?.attributes ?? {}),
+ getScriptKindFromAttributes(document.moduleScriptInfo?.attributes ?? {})
+ ].includes(ts.ScriptKind.TSX)
+ ? ts.ScriptKind.TSX
+ : ts.ScriptKind.JSX;
+
+ try {
+ const tsx = svelte2tsx(text, {
+ strictMode: options.strictMode,
+ filename: document.getFilePath() ?? undefined,
+ isTsFile: scriptKind === ts.ScriptKind.TSX,
+ emitOnTemplateError: options.transformOnTemplateError,
+ namespace: document.config?.compilerOptions?.namespace
+ });
+ text = tsx.code;
+ tsxMap = tsx.map;
+ exportedNames = tsx.exportedNames;
+ componentEvents = tsx.events;
+ 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) {
+ // 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 ?? 0,
+ 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,
+ componentEvents,
+ parserError,
+ nrPrependedLines,
+ scriptKind
+ };
}
/**
* A svelte document snapshot suitable for the ts language service and the plugin.
*/
export class SvelteDocumentSnapshot implements DocumentSnapshot {
- private fragment?: SvelteSnapshotFragment;
-
- version = this.parent.version;
-
- constructor(
- private 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 componentEvents?: ComponentEvents,
- private readonly tsxMap?: RawSourceMap
- ) {}
-
- get filePath() {
- return this.parent.getFilePath() || '';
- }
-
- 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);
- }
-
- 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);
- }
-
- getEvents() {
- return this.componentEvents?.getAll() || [];
- }
-
- async getFragment() {
- if (!this.fragment) {
- const uri = pathToUrl(this.filePath);
- this.fragment = new SvelteSnapshotFragment(
- await this.getMapper(uri),
- this.text,
- this.parent,
- uri
- );
- }
- return this.fragment;
- }
-
- destroyFragment() {
- if (this.fragment) {
- this.fragment.destroy();
- this.fragment = undefined;
- }
- }
-
- private async getMapper(uri: string) {
- const scriptInfo = this.parent.scriptInfo || this.parent.moduleScriptInfo;
-
- if (!scriptInfo) {
- return new IdentityMapper(uri);
- }
- if (!this.tsxMap) {
- return new FragmentMapper(this.parent.getText(), scriptInfo, uri);
- }
- return new ConsumerDocumentMapper(
- await new SourceMapConsumer(this.tsxMap),
- uri,
- this.nrPrependedLines
- );
- }
+ private fragment?: SvelteSnapshotFragment;
+
+ version = this.parent.version;
+
+ constructor(
+ private 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 componentEvents?: ComponentEvents,
+ private readonly tsxMap?: RawSourceMap
+ ) {}
+
+ get filePath() {
+ return this.parent.getFilePath() || '';
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+
+ getEvents() {
+ return this.componentEvents?.getAll() || [];
+ }
+
+ async getFragment() {
+ if (!this.fragment) {
+ const uri = pathToUrl(this.filePath);
+ this.fragment = new SvelteSnapshotFragment(
+ await this.getMapper(uri),
+ this.text,
+ this.parent,
+ uri
+ );
+ }
+ return this.fragment;
+ }
+
+ destroyFragment() {
+ if (this.fragment) {
+ this.fragment.destroy();
+ this.fragment = undefined;
+ }
+ }
+
+ private async getMapper(uri: string) {
+ const scriptInfo = this.parent.scriptInfo || this.parent.moduleScriptInfo;
+
+ if (!scriptInfo) {
+ return new IdentityMapper(uri);
+ }
+ if (!this.tsxMap) {
+ return new FragmentMapper(this.parent.getText(), scriptInfo, uri);
+ }
+ return new ConsumerDocumentMapper(
+ await new SourceMapConsumer(this.tsxMap),
+ uri,
+ this.nrPrependedLines
+ );
+ }
}
/**
@@ -321,63 +321,63 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
* Since no mapping has to be done here, it also implements the mapper interface.
*/
export class JSOrTSDocumentSnapshot
- extends IdentityMapper
- implements DocumentSnapshot, SnapshotFragment {
- scriptKind = getScriptKindFromFileName(this.filePath);
- scriptInfo = null;
-
- constructor(public version: number, public readonly filePath: string, private text: string) {
- super(pathToUrl(filePath));
- }
-
- 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);
- }
-
- offsetAt(position: Position): number {
- return offsetAt(position, this.text);
- }
-
- async getFragment() {
- return this;
- }
-
- destroyFragment() {
- // nothing to clean up
- }
-
- update(changes: TextDocumentContentChangeEvent[]): void {
- for (const change of changes) {
- let start = 0;
- let end = 0;
- if ('range' in change) {
- start = this.offsetAt(change.range.start);
- end = this.offsetAt(change.range.end);
- } else {
- end = this.getLength();
- }
-
- this.text = this.text.slice(0, start) + change.text + this.text.slice(end);
- }
-
- this.version++;
- }
+ extends IdentityMapper
+ implements DocumentSnapshot, SnapshotFragment {
+ scriptKind = getScriptKindFromFileName(this.filePath);
+ scriptInfo = null;
+
+ constructor(public version: number, public readonly filePath: string, private text: string) {
+ super(pathToUrl(filePath));
+ }
+
+ 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);
+ }
+
+ offsetAt(position: Position): number {
+ return offsetAt(position, this.text);
+ }
+
+ async getFragment() {
+ return this;
+ }
+
+ destroyFragment() {
+ // nothing to clean up
+ }
+
+ update(changes: TextDocumentContentChangeEvent[]): void {
+ for (const change of changes) {
+ let start = 0;
+ let end = 0;
+ if ('range' in change) {
+ start = this.offsetAt(change.range.start);
+ end = this.offsetAt(change.range.end);
+ } else {
+ end = this.getLength();
+ }
+
+ this.text = this.text.slice(0, start) + change.text + this.text.slice(end);
+ }
+
+ this.version++;
+ }
}
/**
@@ -385,47 +385,47 @@ export class JSOrTSDocumentSnapshot
* to generated snapshot positions and vice versa.
*/
export class SvelteSnapshotFragment implements SnapshotFragment {
- constructor(
- private readonly mapper: DocumentMapper,
- public readonly text: string,
- private readonly parent: Document,
- private readonly url: string
- ) {}
-
- get scriptInfo() {
- return this.parent.scriptInfo || this.parent.moduleScriptInfo;
- }
-
- getOriginalPosition(pos: Position): Position {
- return this.mapper.getOriginalPosition(pos);
- }
-
- getGeneratedPosition(pos: Position): Position {
- return this.mapper.getGeneratedPosition(pos);
- }
-
- isInGenerated(pos: Position): boolean {
- return !isInTag(pos, this.parent.styleInfo);
- }
-
- getURL(): string {
- return this.url;
- }
-
- positionAt(offset: number) {
- return positionAt(offset, this.text);
- }
-
- offsetAt(position: Position) {
- return offsetAt(position, this.text);
- }
-
- /**
- * Needs to be called when source mapper is no longer needed in order to prevent memory leaks.
- */
- destroy() {
- if (this.mapper.destroy) {
- this.mapper.destroy();
- }
- }
+ constructor(
+ private readonly mapper: DocumentMapper,
+ public readonly text: string,
+ private readonly parent: Document,
+ private readonly url: string
+ ) {}
+
+ get scriptInfo() {
+ return this.parent.scriptInfo || this.parent.moduleScriptInfo;
+ }
+
+ getOriginalPosition(pos: Position): Position {
+ return this.mapper.getOriginalPosition(pos);
+ }
+
+ getGeneratedPosition(pos: Position): Position {
+ return this.mapper.getGeneratedPosition(pos);
+ }
+
+ isInGenerated(pos: Position): boolean {
+ return !isInTag(pos, this.parent.styleInfo);
+ }
+
+ getURL(): string {
+ return this.url;
+ }
+
+ positionAt(offset: number) {
+ return positionAt(offset, this.text);
+ }
+
+ offsetAt(position: Position) {
+ return offsetAt(position, this.text);
+ }
+
+ /**
+ * Needs to be called when source mapper is no longer needed in order to prevent memory leaks.
+ */
+ destroy() {
+ if (this.mapper.destroy) {
+ this.mapper.destroy();
+ }
+ }
}
diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts
index 6772a95c7..9d66c4e30 100644
--- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts
+++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts
@@ -4,115 +4,115 @@ import { LSConfigManager } from '../../ls-config';
import { debounceSameArg, pathToUrl } from '../../utils';
import { DocumentSnapshot, SvelteDocumentSnapshot } from './DocumentSnapshot';
import {
- getLanguageServiceForDocument,
- getLanguageServiceForPath,
- getService,
- LanguageServiceContainer,
- LanguageServiceDocumentContext
+ getLanguageServiceForDocument,
+ getLanguageServiceForPath,
+ getService,
+ LanguageServiceContainer,
+ LanguageServiceDocumentContext
} from './service';
import { SnapshotManager } from './SnapshotManager';
export class LSAndTSDocResolver {
- constructor(
- private readonly docManager: DocumentManager,
- private readonly workspaceUris: string[],
- private readonly configManager: LSConfigManager,
- private readonly transformOnTemplateError = true
- ) {
- const handleDocumentChange = (document: Document) => {
- // This refreshes the document in the ts language service
- this.getLSAndTSDoc(document);
- };
- docManager.on(
- 'documentChange',
- debounceSameArg(
- handleDocumentChange,
- (newDoc, prevDoc) => newDoc.uri === prevDoc?.uri,
- 1000
- )
- );
+ constructor(
+ private readonly docManager: DocumentManager,
+ private readonly workspaceUris: string[],
+ private readonly configManager: LSConfigManager,
+ private readonly transformOnTemplateError = true
+ ) {
+ const handleDocumentChange = (document: Document) => {
+ // This refreshes the document in the ts language service
+ this.getLSAndTSDoc(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', handleDocumentChange);
- }
+ // 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', handleDocumentChange);
+ }
- /**
- * 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
- });
- this.docManager.lockDocument(uri);
- return document;
- };
+ /**
+ * 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
+ });
+ this.docManager.lockDocument(uri);
+ return document;
+ };
- private get lsDocumentContext(): LanguageServiceDocumentContext {
- return {
- createDocument: this.createDocument,
- transformOnTemplateError: this.transformOnTemplateError
- };
- }
+ private get lsDocumentContext(): LanguageServiceDocumentContext {
+ return {
+ createDocument: this.createDocument,
+ transformOnTemplateError: this.transformOnTemplateError
+ };
+ }
- async getLSForPath(path: string) {
- return getLanguageServiceForPath(path, this.workspaceUris, this.lsDocumentContext);
- }
+ async getLSForPath(path: string) {
+ return getLanguageServiceForPath(path, this.workspaceUris, this.lsDocumentContext);
+ }
- async getLSAndTSDoc(
- document: Document
- ): Promise<{
- tsDoc: SvelteDocumentSnapshot;
- lang: ts.LanguageService;
- userPreferences: ts.UserPreferences;
- }> {
- const lang = await getLanguageServiceForDocument(
- document,
- this.workspaceUris,
- this.lsDocumentContext
- );
- const tsDoc = await this.getSnapshot(document);
- const userPreferences = this.getUserPreferences(tsDoc.scriptKind);
+ async getLSAndTSDoc(
+ document: Document
+ ): Promise<{
+ tsDoc: SvelteDocumentSnapshot;
+ lang: ts.LanguageService;
+ userPreferences: ts.UserPreferences;
+ }> {
+ const lang = await getLanguageServiceForDocument(
+ document,
+ this.workspaceUris,
+ this.lsDocumentContext
+ );
+ const tsDoc = await this.getSnapshot(document);
+ const userPreferences = this.getUserPreferences(tsDoc.scriptKind);
- return { tsDoc, lang, userPreferences };
- }
+ return { tsDoc, lang, userPreferences };
+ }
- 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.updateDocument(pathOrDoc);
- }
+ 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.updateDocument(pathOrDoc);
+ }
- async updateSnapshotPath(oldPath: string, newPath: string): Promise {
- await this.deleteSnapshot(oldPath);
- return this.getSnapshot(newPath);
- }
+ async updateSnapshotPath(oldPath: string, newPath: string): Promise {
+ await this.deleteSnapshot(oldPath);
+ return this.getSnapshot(newPath);
+ }
- async deleteSnapshot(filePath: string) {
- (await this.getTSService(filePath)).deleteDocument(filePath);
- this.docManager.releaseDocument(pathToUrl(filePath));
- }
+ async deleteSnapshot(filePath: string) {
+ (await this.getTSService(filePath)).deleteDocument(filePath);
+ this.docManager.releaseDocument(pathToUrl(filePath));
+ }
- async getSnapshotManager(filePath: string): Promise {
- return (await this.getTSService(filePath)).snapshotManager;
- }
+ async getSnapshotManager(filePath: string): Promise {
+ return (await this.getTSService(filePath)).snapshotManager;
+ }
- private getTSService(filePath: string): Promise {
- return getService(filePath, this.workspaceUris, this.lsDocumentContext);
- }
+ private getTSService(filePath: string): Promise {
+ return getService(filePath, this.workspaceUris, this.lsDocumentContext);
+ }
- private getUserPreferences(scriptKind: ts.ScriptKind): ts.UserPreferences {
- const configLang =
- scriptKind === ts.ScriptKind.TS || scriptKind === ts.ScriptKind.TSX
- ? 'typescript'
- : 'javascript';
+ private getUserPreferences(scriptKind: ts.ScriptKind): ts.UserPreferences {
+ const configLang =
+ scriptKind === ts.ScriptKind.TS || scriptKind === ts.ScriptKind.TSX
+ ? 'typescript'
+ : 'javascript';
- return this.configManager.getTsUserPreferences(configLang);
- }
+ return this.configManager.getTsUserPreferences(configLang);
+ }
}
diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts
index 2a06b89dc..2ffdfa3e5 100644
--- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts
+++ b/packages/language-server/src/plugins/typescript/SnapshotManager.ts
@@ -4,121 +4,121 @@ import { Logger } from '../../logger';
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
export interface TsFilesSpec {
- include?: readonly string[];
- exclude?: readonly string[];
+ include?: readonly string[];
+ exclude?: readonly string[];
}
export class SnapshotManager {
- private documents: Map = new Map();
- private lastLogged = new Date(new Date().getTime() - 60_001);
-
- private readonly watchExtensions = [
- ts.Extension.Dts,
- ts.Extension.Js,
- ts.Extension.Jsx,
- ts.Extension.Ts,
- ts.Extension.Tsx,
- ts.Extension.Json
- ];
-
- constructor(
- private projectFiles: string[],
- private fileSpec: TsFilesSpec,
- private workspaceRoot: string
- ) {}
-
- updateProjectFiles() {
- 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
- );
-
- this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles]));
- }
-
- updateTsOrJsFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void {
- const previousSnapshot = this.get(fileName);
-
- if (changes) {
- if (!(previousSnapshot instanceof JSOrTSDocumentSnapshot)) {
- return;
- }
- previousSnapshot.update(changes);
- } else {
- const newSnapshot = DocumentSnapshot.fromNonSvelteFilePath(fileName);
-
- 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);
- }
- }
-
- has(fileName: string) {
- return this.projectFiles.includes(fileName) || this.getFileNames().includes(fileName);
- }
-
- set(fileName: string, snapshot: DocumentSnapshot) {
- const prev = this.get(fileName);
- if (prev) {
- prev.destroyFragment();
- }
-
- this.logStatistics();
-
- return this.documents.set(fileName, snapshot);
- }
-
- get(fileName: string) {
- return this.documents.get(fileName);
- }
-
- delete(fileName: string) {
- this.projectFiles = this.projectFiles.filter((s) => s !== fileName);
- return this.documents.delete(fileName);
- }
-
- getFileNames() {
- return Array.from(this.documents.keys());
- }
-
- getProjectFileNames() {
- return [...this.projectFiles];
- }
-
- 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 projectFiles = this.getProjectFileNames();
- const allFiles = Array.from(new Set([...projectFiles, ...this.getFileNames()]));
- Logger.log(
- 'SnapshotManager File Statistics:\n' +
- `Project files: ${projectFiles.length}\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}`
- );
- }
- }
+ private documents: Map = new Map();
+ private lastLogged = new Date(new Date().getTime() - 60_001);
+
+ private readonly watchExtensions = [
+ ts.Extension.Dts,
+ ts.Extension.Js,
+ ts.Extension.Jsx,
+ ts.Extension.Ts,
+ ts.Extension.Tsx,
+ ts.Extension.Json
+ ];
+
+ constructor(
+ private projectFiles: string[],
+ private fileSpec: TsFilesSpec,
+ private workspaceRoot: string
+ ) {}
+
+ updateProjectFiles() {
+ 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
+ );
+
+ this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles]));
+ }
+
+ updateTsOrJsFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void {
+ const previousSnapshot = this.get(fileName);
+
+ if (changes) {
+ if (!(previousSnapshot instanceof JSOrTSDocumentSnapshot)) {
+ return;
+ }
+ previousSnapshot.update(changes);
+ } else {
+ const newSnapshot = DocumentSnapshot.fromNonSvelteFilePath(fileName);
+
+ 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);
+ }
+ }
+
+ has(fileName: string) {
+ return this.projectFiles.includes(fileName) || this.getFileNames().includes(fileName);
+ }
+
+ set(fileName: string, snapshot: DocumentSnapshot) {
+ const prev = this.get(fileName);
+ if (prev) {
+ prev.destroyFragment();
+ }
+
+ this.logStatistics();
+
+ return this.documents.set(fileName, snapshot);
+ }
+
+ get(fileName: string) {
+ return this.documents.get(fileName);
+ }
+
+ delete(fileName: string) {
+ this.projectFiles = this.projectFiles.filter((s) => s !== fileName);
+ return this.documents.delete(fileName);
+ }
+
+ getFileNames() {
+ return Array.from(this.documents.keys());
+ }
+
+ getProjectFileNames() {
+ return [...this.projectFiles];
+ }
+
+ 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 projectFiles = this.getProjectFileNames();
+ const allFiles = Array.from(new Set([...projectFiles, ...this.getFileNames()]));
+ Logger.log(
+ 'SnapshotManager File Statistics:\n' +
+ `Project files: ${projectFiles.length}\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}`
+ );
+ }
+ }
}
diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts
index a0d56d669..1f57555c5 100644
--- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts
+++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts
@@ -1,58 +1,58 @@
import ts, { NavigationTree } from 'typescript';
import {
- CodeAction,
- CodeActionContext,
- CompletionContext,
- DefinitionLink,
- Diagnostic,
- FileChangeType,
- Hover,
- Location,
- LocationLink,
- Position,
- Range,
- ReferenceContext,
- SymbolInformation,
- WorkspaceEdit,
- CompletionList,
- SelectionRange,
- SignatureHelp,
- SignatureHelpContext,
- SemanticTokens,
- TextDocumentContentChangeEvent
+ CodeAction,
+ CodeActionContext,
+ CompletionContext,
+ DefinitionLink,
+ Diagnostic,
+ FileChangeType,
+ Hover,
+ Location,
+ LocationLink,
+ Position,
+ Range,
+ ReferenceContext,
+ SymbolInformation,
+ WorkspaceEdit,
+ CompletionList,
+ SelectionRange,
+ SignatureHelp,
+ SignatureHelpContext,
+ SemanticTokens,
+ TextDocumentContentChangeEvent
} from 'vscode-languageserver';
import {
- Document,
- DocumentManager,
- mapSymbolInformationToOriginal,
- getTextInRange
+ Document,
+ DocumentManager,
+ mapSymbolInformationToOriginal,
+ getTextInRange
} from '../../lib/documents';
import { LSConfigManager, LSTypescriptConfig } from '../../ls-config';
import { isNotNullOrUndefined, pathToUrl } from '../../utils';
import {
- AppCompletionItem,
- AppCompletionList,
- CodeActionsProvider,
- CompletionsProvider,
- DefinitionsProvider,
- DiagnosticsProvider,
- DocumentSymbolsProvider,
- FileRename,
- FindReferencesProvider,
- HoverProvider,
- OnWatchFileChanges,
- RenameProvider,
- SelectionRangeProvider,
- SignatureHelpProvider,
- UpdateImportsProvider,
- OnWatchFileChangesPara,
- SemanticTokensProvider,
- UpdateTsOrJsFile
+ AppCompletionItem,
+ AppCompletionList,
+ CodeActionsProvider,
+ CompletionsProvider,
+ DefinitionsProvider,
+ DiagnosticsProvider,
+ DocumentSymbolsProvider,
+ FileRename,
+ FindReferencesProvider,
+ HoverProvider,
+ OnWatchFileChanges,
+ RenameProvider,
+ SelectionRangeProvider,
+ SignatureHelpProvider,
+ UpdateImportsProvider,
+ OnWatchFileChangesPara,
+ SemanticTokensProvider,
+ UpdateTsOrJsFile
} from '../interfaces';
import { CodeActionsProviderImpl } from './features/CodeActionsProvider';
import {
- CompletionEntryWithIdentifer,
- CompletionsProviderImpl
+ CompletionEntryWithIdentifer,
+ CompletionsProviderImpl
} from './features/CompletionProvider';
import { DiagnosticsProviderImpl } from './features/DiagnosticsProvider';
import { HoverProviderImpl } from './features/HoverProvider';
@@ -69,383 +69,383 @@ import { SemanticTokensProviderImpl } from './features/SemanticTokensProvider';
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';
export class TypeScriptPlugin
- implements
- DiagnosticsProvider,
- HoverProvider,
- DocumentSymbolsProvider,
- DefinitionsProvider,
- CodeActionsProvider,
- UpdateImportsProvider,
- RenameProvider,
- FindReferencesProvider,
- SelectionRangeProvider,
- SignatureHelpProvider,
- SemanticTokensProvider,
- OnWatchFileChanges,
- CompletionsProvider,
- UpdateTsOrJsFile {
- 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 selectionRangeProvider: SelectionRangeProviderImpl;
- private readonly signatureHelpProvider: SignatureHelpProviderImpl;
- private readonly semanticTokensProvider: SemanticTokensProviderImpl;
-
- constructor(
- docManager: DocumentManager,
- configManager: LSConfigManager,
- workspaceUris: string[],
- isEditor = true
- ) {
- this.configManager = configManager;
- this.lsAndTsDocResolver = new LSAndTSDocResolver(
- docManager,
- workspaceUris,
- configManager,
- /**transformOnTemplateError */ isEditor
- );
- this.completionProvider = new CompletionsProviderImpl(this.lsAndTsDocResolver);
- this.codeActionsProvider = new CodeActionsProviderImpl(
- this.lsAndTsDocResolver,
- this.completionProvider
- );
- this.updateImportsProvider = new UpdateImportsProviderImpl(this.lsAndTsDocResolver);
- this.diagnosticsProvider = new DiagnosticsProviderImpl(this.lsAndTsDocResolver);
- this.renameProvider = new RenameProviderImpl(this.lsAndTsDocResolver);
- this.hoverProvider = new HoverProviderImpl(this.lsAndTsDocResolver);
- this.findReferencesProvider = new FindReferencesProviderImpl(this.lsAndTsDocResolver);
- this.selectionRangeProvider = new SelectionRangeProviderImpl(this.lsAndTsDocResolver);
- this.signatureHelpProvider = new SignatureHelpProviderImpl(this.lsAndTsDocResolver);
- this.semanticTokensProvider = new SemanticTokensProviderImpl(this.lsAndTsDocResolver);
- }
-
- async getDiagnostics(document: Document): Promise {
- if (!this.featureEnabled('diagnostics')) {
- return [];
- }
-
- return this.diagnosticsProvider.getDiagnostics(document);
- }
-
- async doHover(document: Document, position: Position): Promise {
- if (!this.featureEnabled('hover')) {
- return null;
- }
-
- return this.hoverProvider.doHover(document, position);
- }
-
- async getDocumentSymbols(document: Document): Promise {
- if (!this.featureEnabled('documentSymbols')) {
- return [];
- }
-
- const { lang, tsDoc } = await this.getLSAndTSDoc(document);
- const fragment = await tsDoc.getFragment();
- const navTree = lang.getNavigationTree(tsDoc.filePath);
-
- const symbols: SymbolInformation[] = [];
- collectSymbols(navTree, undefined, (symbol) => symbols.push(symbol));
-
- const topContainerName = symbols[0].name;
- return (
- symbols
- .slice(1)
- .map((symbol) => {
- if (symbol.containerName === topContainerName) {
- return { ...symbol, containerName: 'script' };
- }
-
- return symbol;
- })
- .map((symbol) => mapSymbolInformationToOriginal(fragment, symbol))
- // Due to svelte2tsx, there will also be some symbols that are unmapped.
- // Filter those out to keep the lsp from throwing errors.
- // Also filter out transformation artifacts
- .filter(
- (symbol) =>
- symbol.location.range.start.line >= 0 &&
- symbol.location.range.end.line >= 0 &&
- !symbol.name.startsWith('__sveltets_')
- )
- .map((symbol) => {
- if (symbol.name !== '') {
- return symbol;
- }
-
- let name = getTextInRange(symbol.location.range, document.getText()).trimLeft();
- if (name.length > 50) {
- name = name.substring(0, 50) + '...';
- }
- return {
- ...symbol,
- name
- };
- })
- );
-
- 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(
- fragment.positionAt(start.start),
- fragment.positionAt(end.start + end.length)
- ),
- fragment.getURL(),
- container
- )
- );
- }
- if (tree.childItems) {
- for (const child of tree.childItems) {
- collectSymbols(child, tree.text, cb);
- }
- }
- }
- }
-
- async getCompletions(
- document: Document,
- position: Position,
- completionContext?: CompletionContext
- ): Promise | null> {
- if (!this.featureEnabled('completions')) {
- return null;
- }
-
- const tsDirectiveCommentCompletions = getDirectiveCommentCompletions(
- position,
- document,
- completionContext
- );
-
- const completions = await this.completionProvider.getCompletions(
- document,
- position,
- completionContext
- );
-
- if (completions && tsDirectiveCommentCompletions) {
- return CompletionList.create(
- completions.items.concat(tsDirectiveCommentCompletions.items),
- completions.isIncomplete
- );
- }
-
- return completions ?? tsDirectiveCommentCompletions;
- }
-
- async resolveCompletion(
- document: Document,
- completionItem: AppCompletionItem
- ): Promise> {
- return this.completionProvider.resolveCompletion(document, completionItem);
- }
-
- async getDefinitions(document: Document, position: Position): Promise {
- if (!this.featureEnabled('definitions')) {
- return [];
- }
-
- const { lang, tsDoc } = await this.getLSAndTSDoc(document);
- const mainFragment = await tsDoc.getFragment();
-
- const defs = lang.getDefinitionAndBoundSpan(
- tsDoc.filePath,
- mainFragment.offsetAt(mainFragment.getGeneratedPosition(position))
- );
-
- if (!defs || !defs.definitions) {
- return [];
- }
-
- const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
- docs.set(tsDoc.filePath, { fragment: mainFragment, snapshot: tsDoc });
-
- const result = await Promise.all(
- defs.definitions.map(async (def) => {
- const { fragment, snapshot } = await docs.retrieve(def.fileName);
-
- if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) {
- return LocationLink.create(
- pathToUrl(def.fileName),
- convertToLocationRange(fragment, def.textSpan),
- convertToLocationRange(fragment, def.textSpan),
- convertToLocationRange(mainFragment, defs.textSpan)
- );
- }
- })
- );
- return result.filter(isNotNullOrUndefined);
- }
-
- async prepareRename(document: Document, position: Position): Promise {
- if (!this.featureEnabled('rename')) {
- return null;
- }
-
- return this.renameProvider.prepareRename(document, position);
- }
-
- async rename(
- document: Document,
- position: Position,
- newName: string
- ): Promise {
- if (!this.featureEnabled('rename')) {
- return null;
- }
-
- return this.renameProvider.rename(document, position, newName);
- }
-
- async getCodeActions(
- document: Document,
- range: Range,
- context: CodeActionContext
- ): Promise {
- if (!this.featureEnabled('codeActions')) {
- return [];
- }
-
- return this.codeActionsProvider.getCodeActions(document, range, context);
- }
-
- 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 {
- if (!this.featureEnabled('findReferences')) {
- return null;
- }
-
- return this.findReferencesProvider.findReferences(document, position, context);
- }
-
- async onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): Promise {
- const doneUpdateProjectFiles = new Set();
-
- for (const { fileName, changeType } of onWatchFileChangesParas) {
- const scriptKind = getScriptKindFromFileName(fileName);
-
- if (scriptKind === ts.ScriptKind.Unknown) {
- // We don't deal with svelte files here
- continue;
- }
-
- const snapshotManager = await this.getSnapshotManager(fileName);
- if (changeType === FileChangeType.Created) {
- if (!doneUpdateProjectFiles.has(snapshotManager)) {
- snapshotManager.updateProjectFiles();
- doneUpdateProjectFiles.add(snapshotManager);
- }
- } else if (changeType === FileChangeType.Deleted) {
- snapshotManager.delete(fileName);
- return;
- }
-
- snapshotManager.updateTsOrJsFile(fileName);
- }
- }
-
- async updateTsOrJsFile(
- fileName: string,
- changes: TextDocumentContentChangeEvent[]
- ): Promise {
- const snapshotManager = await this.getSnapshotManager(fileName);
- snapshotManager.updateTsOrJsFile(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
- ): Promise {
- if (!this.featureEnabled('signatureHelp')) {
- return null;
- }
-
- return this.signatureHelpProvider.getSignatureHelp(document, position, context);
- }
-
- async getSemanticTokens(textDocument: Document, range?: Range): Promise {
- if (!this.featureEnabled('semanticTokens')) {
- return {
- data: []
- };
- }
-
- return this.semanticTokensProvider.getSemanticTokens(textDocument, range);
- }
-
- private async getLSAndTSDoc(document: Document) {
- return this.lsAndTsDocResolver.getLSAndTSDoc(document);
- }
-
- /**
- *
- * @internal
- */
- 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`)
- );
- }
+ implements
+ DiagnosticsProvider,
+ HoverProvider,
+ DocumentSymbolsProvider,
+ DefinitionsProvider,
+ CodeActionsProvider,
+ UpdateImportsProvider,
+ RenameProvider,
+ FindReferencesProvider,
+ SelectionRangeProvider,
+ SignatureHelpProvider,
+ SemanticTokensProvider,
+ OnWatchFileChanges,
+ CompletionsProvider,
+ UpdateTsOrJsFile {
+ 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 selectionRangeProvider: SelectionRangeProviderImpl;
+ private readonly signatureHelpProvider: SignatureHelpProviderImpl;
+ private readonly semanticTokensProvider: SemanticTokensProviderImpl;
+
+ constructor(
+ docManager: DocumentManager,
+ configManager: LSConfigManager,
+ workspaceUris: string[],
+ isEditor = true
+ ) {
+ this.configManager = configManager;
+ this.lsAndTsDocResolver = new LSAndTSDocResolver(
+ docManager,
+ workspaceUris,
+ configManager,
+ /**transformOnTemplateError */ isEditor
+ );
+ this.completionProvider = new CompletionsProviderImpl(this.lsAndTsDocResolver);
+ this.codeActionsProvider = new CodeActionsProviderImpl(
+ this.lsAndTsDocResolver,
+ this.completionProvider
+ );
+ this.updateImportsProvider = new UpdateImportsProviderImpl(this.lsAndTsDocResolver);
+ this.diagnosticsProvider = new DiagnosticsProviderImpl(this.lsAndTsDocResolver);
+ this.renameProvider = new RenameProviderImpl(this.lsAndTsDocResolver);
+ this.hoverProvider = new HoverProviderImpl(this.lsAndTsDocResolver);
+ this.findReferencesProvider = new FindReferencesProviderImpl(this.lsAndTsDocResolver);
+ this.selectionRangeProvider = new SelectionRangeProviderImpl(this.lsAndTsDocResolver);
+ this.signatureHelpProvider = new SignatureHelpProviderImpl(this.lsAndTsDocResolver);
+ this.semanticTokensProvider = new SemanticTokensProviderImpl(this.lsAndTsDocResolver);
+ }
+
+ async getDiagnostics(document: Document): Promise {
+ if (!this.featureEnabled('diagnostics')) {
+ return [];
+ }
+
+ return this.diagnosticsProvider.getDiagnostics(document);
+ }
+
+ async doHover(document: Document, position: Position): Promise {
+ if (!this.featureEnabled('hover')) {
+ return null;
+ }
+
+ return this.hoverProvider.doHover(document, position);
+ }
+
+ async getDocumentSymbols(document: Document): Promise {
+ if (!this.featureEnabled('documentSymbols')) {
+ return [];
+ }
+
+ const { lang, tsDoc } = await this.getLSAndTSDoc(document);
+ const fragment = await tsDoc.getFragment();
+ const navTree = lang.getNavigationTree(tsDoc.filePath);
+
+ const symbols: SymbolInformation[] = [];
+ collectSymbols(navTree, undefined, (symbol) => symbols.push(symbol));
+
+ const topContainerName = symbols[0].name;
+ return (
+ symbols
+ .slice(1)
+ .map((symbol) => {
+ if (symbol.containerName === topContainerName) {
+ return { ...symbol, containerName: 'script' };
+ }
+
+ return symbol;
+ })
+ .map((symbol) => mapSymbolInformationToOriginal(fragment, symbol))
+ // Due to svelte2tsx, there will also be some symbols that are unmapped.
+ // Filter those out to keep the lsp from throwing errors.
+ // Also filter out transformation artifacts
+ .filter(
+ (symbol) =>
+ symbol.location.range.start.line >= 0 &&
+ symbol.location.range.end.line >= 0 &&
+ !symbol.name.startsWith('__sveltets_')
+ )
+ .map((symbol) => {
+ if (symbol.name !== '') {
+ return symbol;
+ }
+
+ let name = getTextInRange(symbol.location.range, document.getText()).trimLeft();
+ if (name.length > 50) {
+ name = name.substring(0, 50) + '...';
+ }
+ return {
+ ...symbol,
+ name
+ };
+ })
+ );
+
+ 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(
+ fragment.positionAt(start.start),
+ fragment.positionAt(end.start + end.length)
+ ),
+ fragment.getURL(),
+ container
+ )
+ );
+ }
+ if (tree.childItems) {
+ for (const child of tree.childItems) {
+ collectSymbols(child, tree.text, cb);
+ }
+ }
+ }
+ }
+
+ async getCompletions(
+ document: Document,
+ position: Position,
+ completionContext?: CompletionContext
+ ): Promise | null> {
+ if (!this.featureEnabled('completions')) {
+ return null;
+ }
+
+ const tsDirectiveCommentCompletions = getDirectiveCommentCompletions(
+ position,
+ document,
+ completionContext
+ );
+
+ const completions = await this.completionProvider.getCompletions(
+ document,
+ position,
+ completionContext
+ );
+
+ if (completions && tsDirectiveCommentCompletions) {
+ return CompletionList.create(
+ completions.items.concat(tsDirectiveCommentCompletions.items),
+ completions.isIncomplete
+ );
+ }
+
+ return completions ?? tsDirectiveCommentCompletions;
+ }
+
+ async resolveCompletion(
+ document: Document,
+ completionItem: AppCompletionItem
+ ): Promise> {
+ return this.completionProvider.resolveCompletion(document, completionItem);
+ }
+
+ async getDefinitions(document: Document, position: Position): Promise {
+ if (!this.featureEnabled('definitions')) {
+ return [];
+ }
+
+ const { lang, tsDoc } = await this.getLSAndTSDoc(document);
+ const mainFragment = await tsDoc.getFragment();
+
+ const defs = lang.getDefinitionAndBoundSpan(
+ tsDoc.filePath,
+ mainFragment.offsetAt(mainFragment.getGeneratedPosition(position))
+ );
+
+ if (!defs || !defs.definitions) {
+ return [];
+ }
+
+ const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
+ docs.set(tsDoc.filePath, { fragment: mainFragment, snapshot: tsDoc });
+
+ const result = await Promise.all(
+ defs.definitions.map(async (def) => {
+ const { fragment, snapshot } = await docs.retrieve(def.fileName);
+
+ if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) {
+ return LocationLink.create(
+ pathToUrl(def.fileName),
+ convertToLocationRange(fragment, def.textSpan),
+ convertToLocationRange(fragment, def.textSpan),
+ convertToLocationRange(mainFragment, defs.textSpan)
+ );
+ }
+ })
+ );
+ return result.filter(isNotNullOrUndefined);
+ }
+
+ async prepareRename(document: Document, position: Position): Promise {
+ if (!this.featureEnabled('rename')) {
+ return null;
+ }
+
+ return this.renameProvider.prepareRename(document, position);
+ }
+
+ async rename(
+ document: Document,
+ position: Position,
+ newName: string
+ ): Promise {
+ if (!this.featureEnabled('rename')) {
+ return null;
+ }
+
+ return this.renameProvider.rename(document, position, newName);
+ }
+
+ async getCodeActions(
+ document: Document,
+ range: Range,
+ context: CodeActionContext
+ ): Promise {
+ if (!this.featureEnabled('codeActions')) {
+ return [];
+ }
+
+ return this.codeActionsProvider.getCodeActions(document, range, context);
+ }
+
+ 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 {
+ if (!this.featureEnabled('findReferences')) {
+ return null;
+ }
+
+ return this.findReferencesProvider.findReferences(document, position, context);
+ }
+
+ async onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): Promise {
+ const doneUpdateProjectFiles = new Set();
+
+ for (const { fileName, changeType } of onWatchFileChangesParas) {
+ const scriptKind = getScriptKindFromFileName(fileName);
+
+ if (scriptKind === ts.ScriptKind.Unknown) {
+ // We don't deal with svelte files here
+ continue;
+ }
+
+ const snapshotManager = await this.getSnapshotManager(fileName);
+ if (changeType === FileChangeType.Created) {
+ if (!doneUpdateProjectFiles.has(snapshotManager)) {
+ snapshotManager.updateProjectFiles();
+ doneUpdateProjectFiles.add(snapshotManager);
+ }
+ } else if (changeType === FileChangeType.Deleted) {
+ snapshotManager.delete(fileName);
+ return;
+ }
+
+ snapshotManager.updateTsOrJsFile(fileName);
+ }
+ }
+
+ async updateTsOrJsFile(
+ fileName: string,
+ changes: TextDocumentContentChangeEvent[]
+ ): Promise {
+ const snapshotManager = await this.getSnapshotManager(fileName);
+ snapshotManager.updateTsOrJsFile(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
+ ): Promise {
+ if (!this.featureEnabled('signatureHelp')) {
+ return null;
+ }
+
+ return this.signatureHelpProvider.getSignatureHelp(document, position, context);
+ }
+
+ async getSemanticTokens(textDocument: Document, range?: Range): Promise {
+ if (!this.featureEnabled('semanticTokens')) {
+ return {
+ data: []
+ };
+ }
+
+ return this.semanticTokensProvider.getSemanticTokens(textDocument, range);
+ }
+
+ private async getLSAndTSDoc(document: Document) {
+ return this.lsAndTsDocResolver.getLSAndTSDoc(document);
+ }
+
+ /**
+ *
+ * @internal
+ */
+ 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/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts
index 3e9b5513d..f44113347 100644
--- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts
@@ -1,19 +1,19 @@
import {
- CodeAction,
- CodeActionContext,
- CodeActionKind,
- OptionalVersionedTextDocumentIdentifier,
- Range,
- TextDocumentEdit,
- TextEdit,
- WorkspaceEdit
+ CodeAction,
+ CodeActionContext,
+ CodeActionKind,
+ OptionalVersionedTextDocumentIdentifier,
+ Range,
+ TextDocumentEdit,
+ TextEdit,
+ WorkspaceEdit
} from 'vscode-languageserver';
import {
- Document,
- mapRangeToOriginal,
- isRangeInTag,
- isInTag,
- getLineAtPosition
+ Document,
+ mapRangeToOriginal,
+ isRangeInTag,
+ isInTag,
+ getLineAtPosition
} from '../../../lib/documents';
import { pathToUrl, flatten, isNotNullOrUndefined, modifyLines } from '../../../utils';
import { CodeActionsProvider } from '../../interfaces';
@@ -26,379 +26,379 @@ import { CompletionsProviderImpl } from './CompletionProvider';
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils';
interface RefactorArgs {
- type: 'refactor';
- refactorName: string;
- textRange: ts.TextRange;
- originalRange: Range;
+ type: 'refactor';
+ refactorName: string;
+ textRange: ts.TextRange;
+ originalRange: Range;
}
export class CodeActionsProviderImpl implements CodeActionsProvider {
- constructor(
- private readonly lsAndTsDocResolver: LSAndTSDocResolver,
- private readonly completionProvider: CompletionsProviderImpl
- ) {}
-
- async getCodeActions(
- document: Document,
- range: Range,
- context: CodeActionContext
- ): Promise {
- if (context.only?.[0] === CodeActionKind.SourceOrganizeImports) {
- return await this.organizeImports(document);
- }
-
- if (
- context.diagnostics.length &&
- (!context.only || context.only.includes(CodeActionKind.QuickFix))
- ) {
- return await this.applyQuickfix(document, range, context);
- }
-
- if (!context.only || context.only.includes(CodeActionKind.Refactor)) {
- return await this.getApplicableRefactors(document, range);
- }
-
- return [];
- }
-
- private async organizeImports(document: Document): Promise {
- if (!document.scriptInfo && !document.moduleScriptInfo) {
- return [];
- }
-
- const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
- const fragment = await tsDoc.getFragment();
-
- const changes = lang.organizeImports(
- {
- fileName: tsDoc.filePath,
- type: 'file'
- },
- {},
- 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,
- fragment,
- mapRangeToOriginal(fragment, convertRange(fragment, edit.span))
- );
-
- return TextEdit.replace(
- range,
- this.fixIndentationOfImports(edit.newText, range, document)
- );
- })
- );
- })
- );
-
- return [
- CodeAction.create(
- 'Organize Imports',
- { documentChanges },
- CodeActionKind.SourceOrganizeImports
- )
- ];
- }
-
- private fixIndentationOfImports(edit: string, range: Range, document: Document): string {
- // "Organize Imports" will have edits that delete all imports by return empty edits
- // and one edit which contains all the organized imports. Fix indentation
- // of that one by prepending all lines with the indentation of the first line.
- if (!edit || 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;
- }
- return modifyLines(edit, (line, idx) => (idx === 0 || !line ? line : leadingChars + line));
- }
-
- private checkRemoveImportCodeActionRange(
- edit: ts.TextChange,
- fragment: SnapshotFragment,
- 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(fragment, convertRange(fragment, edit.span));
- range.end.character += 1;
- }
-
- return range;
- }
-
- private async applyQuickfix(document: Document, range: Range, context: CodeActionContext) {
- const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
- const fragment = await tsDoc.getFragment();
-
- const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
- const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
- const errorCodes: number[] = context.diagnostics.map((diag) => Number(diag.code));
- const codeFixes = lang.getCodeFixesAtPosition(
- tsDoc.filePath,
- start,
- end,
- errorCodes,
- {},
- userPreferences
- );
-
- const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
- docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc });
-
- return await Promise.all(
- codeFixes.map(async (fix) => {
- const documentChanges = await Promise.all(
- fix.changes.map(async (change) => {
- const { snapshot, fragment } = await docs.retrieve(change.fileName);
- return TextDocumentEdit.create(
- OptionalVersionedTextDocumentIdentifier.create(
- pathToUrl(change.fileName),
- null
- ),
- change.textChanges
- .map((edit) => {
- if (
- fix.fixName === 'import' &&
- fragment instanceof SvelteSnapshotFragment
- ) {
- return this.completionProvider.codeActionChangeToTextEdit(
- document,
- fragment,
- edit,
- true,
- isInTag(range.start, document.scriptInfo) ||
- isInTag(range.start, document.moduleScriptInfo)
- );
- }
-
- if (
- !isNoTextSpanInGeneratedCode(
- snapshot.getFullText(),
- edit.span
- )
- ) {
- return undefined;
- }
-
- let originalRange = mapRangeToOriginal(
- fragment,
- convertRange(fragment, edit.span)
- );
-
- if (fix.fixName === 'unusedIdentifier') {
- originalRange = this.checkRemoveImportCodeActionRange(
- edit,
- fragment,
- originalRange
- );
- }
-
- if (fix.fixName === 'fixMissingFunctionDeclaration') {
- originalRange = this.checkEndOfFileCodeInsert(
- originalRange,
- range,
- document
- );
- }
-
- return TextEdit.replace(originalRange, edit.newText);
- })
- .filter(isNotNullOrUndefined)
- );
- })
- );
- return CodeAction.create(
- fix.description,
- {
- documentChanges
- },
- CodeActionKind.QuickFix
- );
- })
- );
- }
-
- private async getApplicableRefactors(document: Document, range: Range): 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);
- const fragment = await tsDoc.getFragment();
- const textRange = {
- pos: fragment.offsetAt(fragment.getGeneratedPosition(range.start)),
- end: fragment.offsetAt(fragment.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')
- )
- // 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 fragment = await tsDoc.getFragment();
- 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(fragment, convertRange(fragment, 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, document: Document) {
- if (resultRange.start.line < 0 || resultRange.end.line < 0) {
- if (isRangeInTag(targetRange, document.scriptInfo)) {
- resultRange = Range.create(document.scriptInfo.endPos, document.scriptInfo.endPos);
- } else if (isRangeInTag(targetRange, document.moduleScriptInfo)) {
- resultRange = Range.create(
- document.moduleScriptInfo.endPos,
- document.moduleScriptInfo.endPos
- );
- }
- }
- return resultRange;
- }
-
- private async getLSAndTSDoc(document: Document) {
- return this.lsAndTsDocResolver.getLSAndTSDoc(document);
- }
+ constructor(
+ private readonly lsAndTsDocResolver: LSAndTSDocResolver,
+ private readonly completionProvider: CompletionsProviderImpl
+ ) {}
+
+ async getCodeActions(
+ document: Document,
+ range: Range,
+ context: CodeActionContext
+ ): Promise {
+ if (context.only?.[0] === CodeActionKind.SourceOrganizeImports) {
+ return await this.organizeImports(document);
+ }
+
+ if (
+ context.diagnostics.length &&
+ (!context.only || context.only.includes(CodeActionKind.QuickFix))
+ ) {
+ return await this.applyQuickfix(document, range, context);
+ }
+
+ if (!context.only || context.only.includes(CodeActionKind.Refactor)) {
+ return await this.getApplicableRefactors(document, range);
+ }
+
+ return [];
+ }
+
+ private async organizeImports(document: Document): Promise {
+ if (!document.scriptInfo && !document.moduleScriptInfo) {
+ return [];
+ }
+
+ const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
+ const fragment = await tsDoc.getFragment();
+
+ const changes = lang.organizeImports(
+ {
+ fileName: tsDoc.filePath,
+ type: 'file'
+ },
+ {},
+ 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,
+ fragment,
+ mapRangeToOriginal(fragment, convertRange(fragment, edit.span))
+ );
+
+ return TextEdit.replace(
+ range,
+ this.fixIndentationOfImports(edit.newText, range, document)
+ );
+ })
+ );
+ })
+ );
+
+ return [
+ CodeAction.create(
+ 'Organize Imports',
+ { documentChanges },
+ CodeActionKind.SourceOrganizeImports
+ )
+ ];
+ }
+
+ private fixIndentationOfImports(edit: string, range: Range, document: Document): string {
+ // "Organize Imports" will have edits that delete all imports by return empty edits
+ // and one edit which contains all the organized imports. Fix indentation
+ // of that one by prepending all lines with the indentation of the first line.
+ if (!edit || 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;
+ }
+ return modifyLines(edit, (line, idx) => (idx === 0 || !line ? line : leadingChars + line));
+ }
+
+ private checkRemoveImportCodeActionRange(
+ edit: ts.TextChange,
+ fragment: SnapshotFragment,
+ 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(fragment, convertRange(fragment, edit.span));
+ range.end.character += 1;
+ }
+
+ return range;
+ }
+
+ private async applyQuickfix(document: Document, range: Range, context: CodeActionContext) {
+ const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
+ const fragment = await tsDoc.getFragment();
+
+ const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
+ const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
+ const errorCodes: number[] = context.diagnostics.map((diag) => Number(diag.code));
+ const codeFixes = lang.getCodeFixesAtPosition(
+ tsDoc.filePath,
+ start,
+ end,
+ errorCodes,
+ {},
+ userPreferences
+ );
+
+ const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
+ docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc });
+
+ return await Promise.all(
+ codeFixes.map(async (fix) => {
+ const documentChanges = await Promise.all(
+ fix.changes.map(async (change) => {
+ const { snapshot, fragment } = await docs.retrieve(change.fileName);
+ return TextDocumentEdit.create(
+ OptionalVersionedTextDocumentIdentifier.create(
+ pathToUrl(change.fileName),
+ null
+ ),
+ change.textChanges
+ .map((edit) => {
+ if (
+ fix.fixName === 'import' &&
+ fragment instanceof SvelteSnapshotFragment
+ ) {
+ return this.completionProvider.codeActionChangeToTextEdit(
+ document,
+ fragment,
+ edit,
+ true,
+ isInTag(range.start, document.scriptInfo) ||
+ isInTag(range.start, document.moduleScriptInfo)
+ );
+ }
+
+ if (
+ !isNoTextSpanInGeneratedCode(
+ snapshot.getFullText(),
+ edit.span
+ )
+ ) {
+ return undefined;
+ }
+
+ let originalRange = mapRangeToOriginal(
+ fragment,
+ convertRange(fragment, edit.span)
+ );
+
+ if (fix.fixName === 'unusedIdentifier') {
+ originalRange = this.checkRemoveImportCodeActionRange(
+ edit,
+ fragment,
+ originalRange
+ );
+ }
+
+ if (fix.fixName === 'fixMissingFunctionDeclaration') {
+ originalRange = this.checkEndOfFileCodeInsert(
+ originalRange,
+ range,
+ document
+ );
+ }
+
+ return TextEdit.replace(originalRange, edit.newText);
+ })
+ .filter(isNotNullOrUndefined)
+ );
+ })
+ );
+ return CodeAction.create(
+ fix.description,
+ {
+ documentChanges
+ },
+ CodeActionKind.QuickFix
+ );
+ })
+ );
+ }
+
+ private async getApplicableRefactors(document: Document, range: Range): 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);
+ const fragment = await tsDoc.getFragment();
+ const textRange = {
+ pos: fragment.offsetAt(fragment.getGeneratedPosition(range.start)),
+ end: fragment.offsetAt(fragment.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')
+ )
+ // 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 fragment = await tsDoc.getFragment();
+ 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(fragment, convertRange(fragment, 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, document: Document) {
+ if (resultRange.start.line < 0 || resultRange.end.line < 0) {
+ if (isRangeInTag(targetRange, document.scriptInfo)) {
+ resultRange = Range.create(document.scriptInfo.endPos, document.scriptInfo.endPos);
+ } else if (isRangeInTag(targetRange, document.moduleScriptInfo)) {
+ resultRange = Range.create(
+ document.moduleScriptInfo.endPos,
+ document.moduleScriptInfo.endPos
+ );
+ }
+ }
+ return resultRange;
+ }
+
+ 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
index 4d8663048..ddde3dae2 100644
--- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts
@@ -1,23 +1,23 @@
import ts from 'typescript';
import {
- CompletionContext,
- CompletionList,
- CompletionTriggerKind,
- MarkupContent,
- MarkupKind,
- Position,
- Range,
- TextDocumentIdentifier,
- TextEdit
+ CompletionContext,
+ CompletionList,
+ CompletionTriggerKind,
+ MarkupContent,
+ MarkupKind,
+ Position,
+ Range,
+ TextDocumentIdentifier,
+ TextEdit
} from 'vscode-languageserver';
import {
- Document,
- getNodeIfIsInHTMLStartTag,
- getWordRangeAt,
- isInTag,
- mapCompletionItemToOriginal,
- mapRangeToOriginal,
- toRange
+ Document,
+ getNodeIfIsInHTMLStartTag,
+ getWordRangeAt,
+ isInTag,
+ mapCompletionItemToOriginal,
+ mapRangeToOriginal,
+ toRange
} from '../../../lib/documents';
import { flatten, getRegExpMatches, isNotNullOrUndefined, pathToUrl } from '../../../utils';
import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces';
@@ -25,439 +25,439 @@ import { SvelteDocumentSnapshot, SvelteSnapshotFragment } from '../DocumentSnaps
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { getMarkdownDocumentation } from '../previewer';
import {
- convertRange,
- getCommitCharactersForScriptElement,
- scriptElementKindToCompletionItemKind
+ convertRange,
+ getCommitCharactersForScriptElement,
+ scriptElementKindToCompletionItemKind
} from '../utils';
import { getJsDocTemplateCompletion } from './getJsDocTemplateCompletion';
import { getComponentAtPosition } from './utils';
export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDocumentIdentifier {
- position: Position;
+ position: Position;
}
type validTriggerCharacter = '.' | '"' | "'" | '`' | '/' | '@' | '<' | '#';
export class CompletionsProviderImpl implements CompletionsProvider {
- constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
-
- /**
- * 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 isValidTriggerCharacter(
- character: string | undefined
- ): character is validTriggerCharacter {
- return this.validTriggerCharacters.includes(character as validTriggerCharacter);
- }
-
- async getCompletions(
- document: Document,
- position: Position,
- completionContext?: CompletionContext
- ): Promise | null> {
- if (isInTag(position, document.styleInfo)) {
- return null;
- }
-
- const { lang, tsDoc, userPreferences } = await this.lsAndTsDocResolver.getLSAndTSDoc(
- 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 isEventTriggerCharacter = triggerCharacter === ':';
-
- // ignore any custom trigger character specified in server capabilities
- // and is not allow by ts
- if (
- isCustomTriggerCharacter &&
- !validTriggerCharacter &&
- !isJsDocTriggerCharacter &&
- !isEventTriggerCharacter
- ) {
- return null;
- }
-
- const fragment = await tsDoc.getFragment();
- if (!fragment.isInGenerated(position)) {
- return null;
- }
-
- const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
-
- if (isJsDocTriggerCharacter) {
- return getJsDocTemplateCompletion(fragment, lang, filePath, offset);
- }
-
- const eventCompletions = await this.getEventCompletions(
- lang,
- document,
- tsDoc,
- fragment,
- position
- );
-
- if (isEventTriggerCharacter) {
- return CompletionList.create(eventCompletions, !!tsDoc.parserError);
- }
-
- const completions =
- lang.getCompletionsAtPosition(filePath, offset, {
- ...userPreferences,
- triggerCharacter: validTriggerCharacter
- })?.entries || [];
-
- if (completions.length === 0 && eventCompletions.length === 0) {
- return tsDoc.parserError ? CompletionList.create([], true) : null;
- }
-
- const existingImports = this.getExistingImports(document);
- const completionItems = completions
- .filter(isValidCompletion(document, position))
- .map((comp) =>
- this.toCompletionItem(
- fragment,
- comp,
- pathToUrl(tsDoc.filePath),
- position,
- existingImports
- )
- )
- .filter(isNotNullOrUndefined)
- .map((comp) => mapCompletionItemToOriginal(fragment, comp))
- .concat(eventCompletions);
-
- return CompletionList.create(completionItems, !!tsDoc.parserError);
- }
-
- 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 async getEventCompletions(
- lang: ts.LanguageService,
- doc: Document,
- tsDoc: SvelteDocumentSnapshot,
- fragment: SvelteSnapshotFragment,
- originalPosition: Position
- ): Promise>> {
- const snapshot = await getComponentAtPosition(
- this.lsAndTsDocResolver,
- lang,
- doc,
- tsDoc,
- fragment,
- originalPosition
- );
- if (!snapshot) {
- return [];
- }
-
- const offset = doc.offsetAt(originalPosition);
- const { start, end } = getWordRangeAt(doc.getText(), offset, {
- left: /\S+$/,
- right: /[^\w$:]/
- });
-
- return snapshot.getEvents().map((event) => {
- const eventName = 'on:' + event.name;
- return {
- label: eventName,
- sortText: '-1',
- detail: event.name + ': ' + event.type,
- documentation: event.doc && { kind: MarkupKind.Markdown, value: event.doc },
- textEdit:
- start !== end
- ? TextEdit.replace(toRange(doc.getText(), start, end), eventName)
- : undefined
- };
- });
- }
-
- private toCompletionItem(
- fragment: SvelteSnapshotFragment,
- comp: ts.CompletionEntry,
- uri: string,
- position: Position,
- existingImports: Set
- ): AppCompletionItem | null {
- const completionLabelAndInsert = this.getCompletionLabelAndInsert(fragment, comp);
- if (!completionLabelAndInsert) {
- return null;
- }
-
- const { label, insertText, isSvelteComp } = 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;
- }
-
- return {
- label,
- insertText,
- kind: scriptElementKindToCompletionItemKind(comp.kind),
- commitCharacters: getCommitCharactersForScriptElement(comp.kind),
- // Make sure svelte component takes precedence
- sortText: isSvelteComp ? '-1' : comp.sortText,
- preselect: isSvelteComp ? true : comp.isRecommended,
- // pass essential data for resolving completion
- data: {
- ...comp,
- uri,
- position
- }
- };
- }
-
- private getCompletionLabelAndInsert(
- fragment: SvelteSnapshotFragment,
- comp: ts.CompletionEntry
- ) {
- let { kind, kindModifiers, name, source } = comp;
- const isScriptElement = kind === ts.ScriptElementKind.scriptElement;
- const hasModifier = Boolean(comp.kindModifiers);
- const isSvelteComp = this.isSvelteComponentImport(name);
- if (isSvelteComp) {
- name = this.changeSvelteComponentName(name);
-
- if (this.isExistingSvelteComponentImport(fragment, name, source)) {
- return null;
- }
- }
-
- if (isScriptElement && hasModifier) {
- return {
- insertText: name,
- label: name + kindModifiers,
- isSvelteComp
- };
- }
-
- return {
- label: name,
- isSvelteComp
- };
- }
-
- private isExistingSvelteComponentImport(
- fragment: SvelteSnapshotFragment,
- name: string,
- source?: string
- ): boolean {
- const importStatement = new RegExp(`import ${name} from ["'\`][\\s\\S]+\\.svelte["'\`]`);
- return !!source && !!fragment.text.match(importStatement);
- }
-
- async resolveCompletion(
- document: Document,
- completionItem: AppCompletionItem
- ): Promise> {
- const { data: comp } = completionItem;
- const { tsDoc, lang, userPreferences } = await this.lsAndTsDocResolver.getLSAndTSDoc(
- document
- );
-
- const filePath = tsDoc.filePath;
-
- if (!comp || !filePath) {
- return completionItem;
- }
-
- const fragment = await tsDoc.getFragment();
- const detail = lang.getCompletionEntryDetails(
- filePath,
- fragment.offsetAt(fragment.getGeneratedPosition(comp.position)),
- comp.name,
- {},
- comp.source,
- userPreferences
- );
-
- if (detail) {
- const {
- detail: itemDetail,
- documentation: itemDocumentation
- } = this.getCompletionDocument(detail);
-
- completionItem.detail = itemDetail;
- completionItem.documentation = itemDocumentation;
- }
-
- const actions = detail?.codeActions;
- const isImport = !!detail?.source;
-
- if (actions) {
- const edit: TextEdit[] = [];
-
- for (const action of actions) {
- for (const change of action.changes) {
- edit.push(
- ...this.codeActionChangesToTextEdit(
- document,
- fragment,
- change,
- isImport,
- isInTag(comp.position, document.scriptInfo) ||
- isInTag(comp.position, document.moduleScriptInfo)
- )
- );
- }
- }
-
- completionItem.additionalTextEdits = edit;
- }
-
- return completionItem;
- }
-
- private getCompletionDocument(compDetail: ts.CompletionEntryDetails) {
- const { source, documentation: tsDocumentation, displayParts, tags } = compDetail;
- let detail: string = this.changeSvelteComponentName(ts.displayPartsToString(displayParts));
-
- if (source) {
- const importPath = ts.displayPartsToString(source);
- detail = `Auto import from ${importPath}\n${detail}`;
- }
-
- const markdownDoc = getMarkdownDocumentation(tsDocumentation, tags);
- const documentation: MarkupContent | undefined = markdownDoc
- ? { value: markdownDoc, kind: MarkupKind.Markdown }
- : undefined;
-
- return {
- documentation,
- detail
- };
- }
-
- private codeActionChangesToTextEdit(
- doc: Document,
- fragment: SvelteSnapshotFragment,
- changes: ts.FileTextChanges,
- isImport: boolean,
- actionTriggeredInScript: boolean
- ): TextEdit[] {
- return changes.textChanges.map((change) =>
- this.codeActionChangeToTextEdit(
- doc,
- fragment,
- change,
- isImport,
- actionTriggeredInScript
- )
- );
- }
-
- codeActionChangeToTextEdit(
- doc: Document,
- fragment: SvelteSnapshotFragment,
- change: ts.TextChange,
- isImport: boolean,
- actionTriggeredInScript: boolean
- ): TextEdit {
- change.newText = this.changeComponentImport(change.newText, actionTriggeredInScript);
-
- const scriptTagInfo = fragment.scriptInfo;
- if (!scriptTagInfo) {
- // no script tag defined yet, add it.
- return TextEdit.replace(
- beginOfDocumentRange,
- `${ts.sys.newLine}`
- );
- }
-
- const { span } = change;
-
- const virtualRange = convertRange(fragment, 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(fragment, virtualRange);
- } else {
- range = mapRangeToOriginal(fragment, 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) ||
- !isInTag(range.start, scriptTagInfo)
- ) {
- range = convertRange(doc, {
- start: scriptTagInfo.start,
- length: span.length
- });
- }
- // prevent newText from being placed like this: ${ts.sys.newLine}`
+ );
+ }
+
+ const { span } = change;
+
+ const virtualRange = convertRange(fragment, 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(fragment, virtualRange);
+ } else {
+ range = mapRangeToOriginal(fragment, 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) ||
+ !isInTag(range.start, scriptTagInfo)
+ ) {
+ range = convertRange(doc, {
+ start: scriptTagInfo.start,
+ length: span.length
+ });
+ }
+ // prevent newText from being placed like this: '
- };
- }
-
- return diagnostic;
+ if (diagnostic.code === 2786) {
+ return {
+ ...diagnostic,
+ message:
+ 'Type definitions are missing for this Svelte Component. ' +
+ // eslint-disable-next-line max-len
+ "It needs a class definition with at least the property '$$prop_def' which should contain a map of input property definitions.\n" +
+ 'Example:\n' +
+ ' class ComponentName { $$prop_def: { propertyName: string; } }\n' +
+ 'If you are using Svelte 3.31+, use SvelteComponentTyped:\n' +
+ ' import type { SvelteComponentTyped } from "svelte";\n' +
+ ' class ComponentName extends SvelteComponentTyped<{propertyName: string;}> {}\n\n' +
+ 'Underlying error:\n' +
+ diagnostic.message
+ };
+ }
+
+ if (diagnostic.code === 2607) {
+ return {
+ ...diagnostic,
+ message:
+ 'Element does not support attributes because ' +
+ 'type definitions are missing for this Svelte Component or element cannot be used as such.\n\n' +
+ 'Underlying error:\n' +
+ diagnostic.message
+ };
+ }
+
+ if (diagnostic.code === 1184) {
+ return {
+ ...diagnostic,
+ message:
+ diagnostic.message +
+ '\nIf this is a declare statement, move it into '
+ };
+ }
+
+ return diagnostic;
}
/**
* Due to source mapping, some ranges may be swapped: Start is end. Swap back in this case.
*/
function swapRangeStartEndIfNecessary(diag: Diagnostic): Diagnostic {
- if (
- diag.range.end.line < diag.range.start.line ||
- (diag.range.end.line === diag.range.start.line &&
- diag.range.end.character < diag.range.start.character)
- ) {
- const start = diag.range.start;
- diag.range.start = diag.range.end;
- diag.range.end = start;
- }
- return diag;
+ if (
+ diag.range.end.line < diag.range.start.line ||
+ (diag.range.end.line === diag.range.start.line &&
+ diag.range.end.character < diag.range.start.character)
+ ) {
+ const start = diag.range.start;
+ diag.range.start = diag.range.end;
+ diag.range.end = start;
+ }
+ return diag;
}
/**
@@ -192,10 +192,10 @@ function swapRangeStartEndIfNecessary(diag: Diagnostic): Diagnostic {
* 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);
- };
+ return (diagnostic: ts.Diagnostic) => {
+ if (diagnostic.start === undefined || diagnostic.length === undefined) {
+ return true;
+ }
+ return !isInGeneratedCode(text, diagnostic.start, diagnostic.start + diagnostic.length);
+ };
}
diff --git a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts
index 8957d5403..ffc394e91 100644
--- a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts
@@ -8,49 +8,49 @@ import { convertToLocationRange } from '../utils';
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils';
export class FindReferencesProviderImpl implements FindReferencesProvider {
- constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
-
- async findReferences(
- document: Document,
- position: Position,
- context: ReferenceContext
- ): Promise {
- const { lang, tsDoc } = await this.getLSAndTSDoc(document);
- const fragment = await tsDoc.getFragment();
-
- const references = lang.getReferencesAtPosition(
- tsDoc.filePath,
- fragment.offsetAt(fragment.getGeneratedPosition(position))
- );
- if (!references) {
- return null;
- }
-
- const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
- docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc });
-
- return await Promise.all(
- references
- .filter((ref) => context.includeDeclaration || !ref.isDefinition)
- .filter(notInGeneratedCode(tsDoc.getFullText()))
- .map(async (ref) => {
- const defDoc = await docs.retrieveFragment(ref.fileName);
-
- return Location.create(
- pathToUrl(ref.fileName),
- convertToLocationRange(defDoc, ref.textSpan)
- );
- })
- );
- }
-
- private async getLSAndTSDoc(document: Document) {
- return this.lsAndTsDocResolver.getLSAndTSDoc(document);
- }
+ constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
+
+ async findReferences(
+ document: Document,
+ position: Position,
+ context: ReferenceContext
+ ): Promise {
+ const { lang, tsDoc } = await this.getLSAndTSDoc(document);
+ const fragment = await tsDoc.getFragment();
+
+ const references = lang.getReferencesAtPosition(
+ tsDoc.filePath,
+ fragment.offsetAt(fragment.getGeneratedPosition(position))
+ );
+ if (!references) {
+ return null;
+ }
+
+ const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
+ docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc });
+
+ return await Promise.all(
+ references
+ .filter((ref) => context.includeDeclaration || !ref.isDefinition)
+ .filter(notInGeneratedCode(tsDoc.getFullText()))
+ .map(async (ref) => {
+ const defDoc = await docs.retrieveFragment(ref.fileName);
+
+ return Location.create(
+ pathToUrl(ref.fileName),
+ convertToLocationRange(defDoc, ref.textSpan)
+ );
+ })
+ );
+ }
+
+ private async getLSAndTSDoc(document: Document) {
+ return this.lsAndTsDocResolver.getLSAndTSDoc(document);
+ }
}
function notInGeneratedCode(text: string) {
- return (ref: ts.ReferenceEntry) => {
- return isNoTextSpanInGeneratedCode(text, ref.textSpan);
- };
+ return (ref: ts.ReferenceEntry) => {
+ return isNoTextSpanInGeneratedCode(text, ref.textSpan);
+ };
}
diff --git a/packages/language-server/src/plugins/typescript/features/HoverProvider.ts b/packages/language-server/src/plugins/typescript/features/HoverProvider.ts
index 5ddc4e6a7..70f7b05a9 100644
--- a/packages/language-server/src/plugins/typescript/features/HoverProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/HoverProvider.ts
@@ -9,103 +9,103 @@ import { convertRange } from '../utils';
import { getComponentAtPosition } from './utils';
export class HoverProviderImpl implements HoverProvider {
- constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
+ constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
- async doHover(document: Document, position: Position): Promise {
- const { lang, tsDoc } = await this.getLSAndTSDoc(document);
- const fragment = await tsDoc.getFragment();
+ async doHover(document: Document, position: Position): Promise {
+ const { lang, tsDoc } = await this.getLSAndTSDoc(document);
+ const fragment = await tsDoc.getFragment();
- const eventHoverInfo = await this.getEventHoverInfo(
- lang,
- document,
- tsDoc,
- fragment,
- position
- );
- if (eventHoverInfo) {
- return eventHoverInfo;
- }
+ const eventHoverInfo = await this.getEventHoverInfo(
+ lang,
+ document,
+ tsDoc,
+ fragment,
+ position
+ );
+ if (eventHoverInfo) {
+ return eventHoverInfo;
+ }
- const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
- let info = lang.getQuickInfoAtPosition(tsDoc.filePath, offset);
- if (!info) {
- return null;
- }
+ const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
+ let info = lang.getQuickInfoAtPosition(tsDoc.filePath, offset);
+ if (!info) {
+ return null;
+ }
- const textSpan = info.textSpan;
+ const textSpan = info.textSpan;
- // show docs of $store instead of store if necessary
- const is$store = fragment.text
- .substring(0, info.textSpan.start)
- .endsWith('(__sveltets_store_get(');
- if (is$store) {
- const infoFor$store = lang.getQuickInfoAtPosition(
- tsDoc.filePath,
- textSpan.start + textSpan.length + 3
- );
- if (infoFor$store) {
- info = infoFor$store;
- }
- }
+ // show docs of $store instead of store if necessary
+ const is$store = fragment.text
+ .substring(0, info.textSpan.start)
+ .endsWith('(__sveltets_store_get(');
+ if (is$store) {
+ const infoFor$store = lang.getQuickInfoAtPosition(
+ tsDoc.filePath,
+ textSpan.start + textSpan.length + 3
+ );
+ if (infoFor$store) {
+ info = infoFor$store;
+ }
+ }
- const declaration = ts.displayPartsToString(info.displayParts);
- const documentation = getMarkdownDocumentation(info.documentation, info.tags);
+ 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');
+ // https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
+ const contents = ['```typescript', declaration, '```']
+ .concat(documentation ? ['---', documentation] : [])
+ .join('\n');
- return mapObjWithRangeToOriginal(fragment, {
- range: convertRange(fragment, textSpan),
- contents
- });
- }
+ return mapObjWithRangeToOriginal(fragment, {
+ range: convertRange(fragment, textSpan),
+ contents
+ });
+ }
- private async getEventHoverInfo(
- lang: ts.LanguageService,
- doc: Document,
- tsDoc: SvelteDocumentSnapshot,
- fragment: SvelteSnapshotFragment,
- originalPosition: Position
- ): Promise {
- const possibleEventName = getWordAt(doc.getText(), doc.offsetAt(originalPosition), {
- left: /\S+$/,
- right: /[\s=]/
- });
- if (!possibleEventName.startsWith('on:')) {
- return null;
- }
+ private async getEventHoverInfo(
+ lang: ts.LanguageService,
+ doc: Document,
+ tsDoc: SvelteDocumentSnapshot,
+ fragment: SvelteSnapshotFragment,
+ originalPosition: Position
+ ): Promise {
+ const possibleEventName = getWordAt(doc.getText(), doc.offsetAt(originalPosition), {
+ left: /\S+$/,
+ right: /[\s=]/
+ });
+ if (!possibleEventName.startsWith('on:')) {
+ return null;
+ }
- const component = await getComponentAtPosition(
- this.lsAndTsDocResolver,
- lang,
- doc,
- tsDoc,
- fragment,
- originalPosition
- );
- if (!component) {
- return null;
- }
+ const component = await getComponentAtPosition(
+ this.lsAndTsDocResolver,
+ lang,
+ doc,
+ tsDoc,
+ fragment,
+ originalPosition
+ );
+ if (!component) {
+ return null;
+ }
- const eventName = possibleEventName.substr('on:'.length);
- const event = component.getEvents().find((event) => event.name === eventName);
- if (!event) {
- 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')
- };
- }
+ return {
+ contents: [
+ '```typescript',
+ `${event.name}: ${event.type}`,
+ '```',
+ event.doc || ''
+ ].join('\n')
+ };
+ }
- private async getLSAndTSDoc(document: Document) {
- return this.lsAndTsDocResolver.getLSAndTSDoc(document);
- }
+ private async getLSAndTSDoc(document: Document) {
+ return this.lsAndTsDocResolver.getLSAndTSDoc(document);
+ }
}
diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts
index 4b263cc16..850925332 100644
--- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts
@@ -1,17 +1,17 @@
import { Position, WorkspaceEdit, Range } from 'vscode-languageserver';
import {
- Document,
- mapRangeToOriginal,
- positionAt,
- offsetAt,
- getLineAtPosition
+ Document,
+ mapRangeToOriginal,
+ positionAt,
+ offsetAt,
+ getLineAtPosition
} from '../../../lib/documents';
import { filterAsync, isNotNullOrUndefined, pathToUrl } from '../../../utils';
import { RenameProvider } from '../../interfaces';
import {
- SnapshotFragment,
- SvelteSnapshotFragment,
- SvelteDocumentSnapshot
+ SnapshotFragment,
+ SvelteSnapshotFragment,
+ SvelteDocumentSnapshot
} from '../DocumentSnapshot';
import { convertRange } from '../utils';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
@@ -20,365 +20,365 @@ import { uniqWith, isEqual } from 'lodash';
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './utils';
export class RenameProviderImpl implements RenameProvider {
- constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
+ constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
- // TODO props written as `export {x as y}` are not supported yet.
+ // 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 fragment = await tsDoc.getFragment();
+ async prepareRename(document: Document, position: Position): Promise {
+ const { lang, tsDoc } = await this.getLSAndTSDoc(document);
+ const fragment = await tsDoc.getFragment();
- const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
- const renameInfo = this.getRenameInfo(lang, tsDoc, offset);
- if (!renameInfo) {
- return null;
- }
+ const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
+ const renameInfo = this.getRenameInfo(lang, tsDoc, offset);
+ if (!renameInfo) {
+ return null;
+ }
- return this.mapRangeToOriginal(fragment, renameInfo.triggerSpan);
- }
+ return this.mapRangeToOriginal(fragment, renameInfo.triggerSpan);
+ }
- async rename(
- document: Document,
- position: Position,
- newName: string
- ): Promise {
- const { lang, tsDoc } = await this.getLSAndTSDoc(document);
- const fragment = await tsDoc.getFragment();
+ async rename(
+ document: Document,
+ position: Position,
+ newName: string
+ ): Promise {
+ const { lang, tsDoc } = await this.getLSAndTSDoc(document);
+ const fragment = await tsDoc.getFragment();
- const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
+ const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
- if (!this.getRenameInfo(lang, tsDoc, offset)) {
- return null;
- }
+ if (!this.getRenameInfo(lang, tsDoc, offset)) {
+ return null;
+ }
- const renameLocations = lang.findRenameLocations(
- tsDoc.filePath,
- offset,
- false,
- false,
- true
- );
- if (!renameLocations) {
- return null;
- }
+ const renameLocations = lang.findRenameLocations(
+ tsDoc.filePath,
+ offset,
+ false,
+ false,
+ true
+ );
+ if (!renameLocations) {
+ return null;
+ }
- const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
- docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc });
- let convertedRenameLocations: Array<
- ts.RenameLocation & {
- range: Range;
- }
- > = await this.mapAndFilterRenameLocations(renameLocations, docs);
- // eslint-disable-next-line max-len
- const additionalRenameForPropRenameInsideComponentWithProp = await this.getAdditionLocationsForRenameOfPropInsideComponentWithProp(
- document,
- tsDoc,
- fragment,
- position,
- convertedRenameLocations,
- docs,
- lang
- );
- const additionalRenamesForPropRenameOutsideComponentWithProp =
- // This is an either-or-situation, don't do both
- additionalRenameForPropRenameInsideComponentWithProp.length > 0
- ? []
- : await this.getAdditionalLocationsForRenameOfPropInsideOtherComponent(
- convertedRenameLocations,
- docs,
- lang
- );
- convertedRenameLocations = [
- ...convertedRenameLocations,
- ...additionalRenameForPropRenameInsideComponentWithProp,
- ...additionalRenamesForPropRenameOutsideComponentWithProp
- ];
+ const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
+ docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc });
+ let convertedRenameLocations: Array<
+ ts.RenameLocation & {
+ range: Range;
+ }
+ > = await this.mapAndFilterRenameLocations(renameLocations, docs);
+ // eslint-disable-next-line max-len
+ const additionalRenameForPropRenameInsideComponentWithProp = await this.getAdditionLocationsForRenameOfPropInsideComponentWithProp(
+ document,
+ tsDoc,
+ fragment,
+ position,
+ convertedRenameLocations,
+ docs,
+ lang
+ );
+ const additionalRenamesForPropRenameOutsideComponentWithProp =
+ // This is an either-or-situation, don't do both
+ additionalRenameForPropRenameInsideComponentWithProp.length > 0
+ ? []
+ : await this.getAdditionalLocationsForRenameOfPropInsideOtherComponent(
+ convertedRenameLocations,
+ docs,
+ lang
+ );
+ 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 || '') + newName + (loc.suffixText || ''),
- range: loc.range
- });
- return acc;
- },
- >>{ changes: {} }
- );
- }
+ 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 || '') + newName + (loc.suffixText || ''),
+ range: loc.range
+ });
+ return acc;
+ },
+ >>{ changes: {} }
+ );
+ }
- private getRenameInfo(
- lang: ts.LanguageService,
- tsDoc: SvelteDocumentSnapshot,
- offset: number
- ): {
- canRename: true;
- kind: ts.ScriptElementKind;
- displayName: string;
- fullDisplayName: string;
- triggerSpan: { start: number; length: number };
- } | 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: any = lang.getRenameInfo(tsDoc.filePath, offset, {
- allowRenameOfImportPath: false
- });
- // TODO this will also forbid renames of svelte component properties
- // in another component because the ScriptElementKind is a JSXAttribute.
- // To fix this we would need to enhance svelte2tsx with info methods like
- // "what props does this file have?"
- if (
- !renameInfo.canRename ||
- renameInfo.kind === ts.ScriptElementKind.jsxAttribute ||
- renameInfo.fullDisplayName?.includes('JSX.IntrinsicElements')
- ) {
- return null;
- }
- return renameInfo;
- }
+ private getRenameInfo(
+ lang: ts.LanguageService,
+ tsDoc: SvelteDocumentSnapshot,
+ offset: number
+ ): {
+ canRename: true;
+ kind: ts.ScriptElementKind;
+ displayName: string;
+ fullDisplayName: string;
+ triggerSpan: { start: number; length: number };
+ } | 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: any = lang.getRenameInfo(tsDoc.filePath, offset, {
+ allowRenameOfImportPath: false
+ });
+ // TODO this will also forbid renames of svelte component properties
+ // in another component because the ScriptElementKind is a JSXAttribute.
+ // To fix this we would need to enhance svelte2tsx with info methods like
+ // "what props does this file have?"
+ if (
+ !renameInfo.canRename ||
+ renameInfo.kind === ts.ScriptElementKind.jsxAttribute ||
+ renameInfo.fullDisplayName?.includes('JSX.IntrinsicElements')
+ ) {
+ return null;
+ }
+ return renameInfo;
+ }
- /**
- * 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,
- fragment: SvelteSnapshotFragment,
- position: Position,
- convertedRenameLocations: Array,
- fragments: SnapshotFragmentMap,
- lang: ts.LanguageService
- ) {
- // 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.
- const regex = new RegExp(
- `export\\s+let\\s+${this.getVariableAtPosition(
- tsDoc,
- fragment,
- lang,
- position
- )}($|\\s|;|:)` // ':' for typescript's type operator (`export let bla: boolean`)
- );
- 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,
- fragments
- );
- if (!updatePropLocation) {
- return [];
- }
- // Typescript does a rename of `oldPropName: newPropName` -> find oldPropName and rename that, too.
- const idxOfOldPropName = fragment.text.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) ||
- []
- ).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(fragment, rename)
- );
- return await this.mapAndFilterRenameLocations(replacementsForProp, fragments);
- }
+ /**
+ * 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,
+ fragment: SvelteSnapshotFragment,
+ position: Position,
+ convertedRenameLocations: Array,
+ fragments: SnapshotFragmentMap,
+ lang: ts.LanguageService
+ ) {
+ // 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.
+ const regex = new RegExp(
+ `export\\s+let\\s+${this.getVariableAtPosition(
+ tsDoc,
+ fragment,
+ lang,
+ position
+ )}($|\\s|;|:)` // ':' for typescript's type operator (`export let bla: boolean`)
+ );
+ 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,
+ fragments
+ );
+ if (!updatePropLocation) {
+ return [];
+ }
+ // Typescript does a rename of `oldPropName: newPropName` -> find oldPropName and rename that, too.
+ const idxOfOldPropName = fragment.text.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) ||
+ []
+ ).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(fragment, rename)
+ );
+ return await this.mapAndFilterRenameLocations(replacementsForProp, fragments);
+ }
- /**
- * 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: Array,
- fragments: SnapshotFragmentMap,
- lang: ts.LanguageService
- ) {
- // Check if it's a prop rename
- const updatePropLocation = this.findLocationWhichWantsToUpdatePropName(
- convertedRenameLocations,
- fragments
- );
- if (!updatePropLocation) {
- return [];
- }
- // Find generated `export let`
- const doc = fragments.getFragment(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 await this.mapAndFilterRenameLocations(replacementsForProp, fragments);
- }
+ /**
+ * 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: Array,
+ fragments: SnapshotFragmentMap,
+ lang: ts.LanguageService
+ ) {
+ // Check if it's a prop rename
+ const updatePropLocation = this.findLocationWhichWantsToUpdatePropName(
+ convertedRenameLocations,
+ fragments
+ );
+ if (!updatePropLocation) {
+ return [];
+ }
+ // Find generated `export let`
+ const doc = fragments.getFragment(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 await this.mapAndFilterRenameLocations(replacementsForProp, fragments);
+ }
- // --------> svelte2tsx?
- private matchGeneratedExportLet(
- fragment: SvelteSnapshotFragment,
- updatePropLocation: ts.RenameLocation
- ) {
- const regex = new RegExp(
- // no 'export let', only 'let', because that's what it's translated to in svelte2tsx
- `\\s+let\\s+(${fragment.text.substr(
- updatePropLocation.textSpan.start,
- updatePropLocation.textSpan.length
- )})($|\\s|;|:)`
- );
- const match = fragment.text.match(regex);
- return match;
- }
+ // --------> svelte2tsx?
+ private matchGeneratedExportLet(
+ fragment: SvelteSnapshotFragment,
+ updatePropLocation: ts.RenameLocation
+ ) {
+ const regex = new RegExp(
+ // no 'export let', only 'let', because that's what it's translated to in svelte2tsx
+ `\\s+let\\s+(${fragment.text.substr(
+ updatePropLocation.textSpan.start,
+ updatePropLocation.textSpan.length
+ )})($|\\s|;|:)`
+ );
+ const match = fragment.text.match(regex);
+ return match;
+ }
- private findLocationWhichWantsToUpdatePropName(
- convertedRenameLocations: Array,
- fragments: SnapshotFragmentMap
- ) {
- return convertedRenameLocations.find((loc) => {
- // Props are not in mapped range
- if (loc.range.start.line >= 0 && loc.range.end.line >= 0) {
- return;
- }
+ private findLocationWhichWantsToUpdatePropName(
+ convertedRenameLocations: Array,
+ fragments: SnapshotFragmentMap
+ ) {
+ return convertedRenameLocations.find((loc) => {
+ // Props are not in mapped range
+ if (loc.range.start.line >= 0 && loc.range.end.line >= 0) {
+ return;
+ }
- const fragment = fragments.getFragment(loc.fileName);
- // Props are in svelte snapshots only
- if (!(fragment instanceof SvelteSnapshotFragment)) {
- return false;
- }
+ const fragment = fragments.getFragment(loc.fileName);
+ // Props are in svelte snapshots only
+ if (!(fragment instanceof SvelteSnapshotFragment)) {
+ return false;
+ }
- return this.isInSvelte2TsxPropLine(fragment, loc);
- });
- }
+ return this.isInSvelte2TsxPropLine(fragment, loc);
+ });
+ }
- // --------> svelte2tsx?
- private isInSvelte2TsxPropLine(fragment: SvelteSnapshotFragment, loc: ts.RenameLocation) {
- const pos = positionAt(loc.textSpan.start, fragment.text);
- const textInLine = fragment.text.substring(
- offsetAt({ ...pos, character: 0 }, fragment.text),
- loc.textSpan.start
- );
- // This is how svelte2tsx writes out the props
- if (textInLine.includes('return { props: {')) {
- return true;
- }
- }
+ // --------> svelte2tsx?
+ private isInSvelte2TsxPropLine(fragment: SvelteSnapshotFragment, loc: ts.RenameLocation) {
+ const pos = positionAt(loc.textSpan.start, fragment.text);
+ const textInLine = fragment.text.substring(
+ offsetAt({ ...pos, character: 0 }, fragment.text),
+ loc.textSpan.start
+ );
+ // This is how svelte2tsx writes out the props
+ if (textInLine.includes('return { props: {')) {
+ return true;
+ }
+ }
- /**
- * 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[],
- fragments: SnapshotFragmentMap
- ): Promise> {
- const mappedLocations = await Promise.all(
- renameLocations.map(async (loc) => {
- const { fragment, snapshot } = await fragments.retrieve(loc.fileName);
+ /**
+ * 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[],
+ fragments: SnapshotFragmentMap
+ ): Promise> {
+ const mappedLocations = await Promise.all(
+ renameLocations.map(async (loc) => {
+ const { fragment, snapshot } = await fragments.retrieve(loc.fileName);
- if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), loc.textSpan)) {
- return {
- ...loc,
- range: this.mapRangeToOriginal(fragment, loc.textSpan)
- };
- }
- })
- );
- return this.filterWrongRenameLocations(mappedLocations.filter(isNotNullOrUndefined));
- }
+ if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), loc.textSpan)) {
+ return {
+ ...loc,
+ range: this.mapRangeToOriginal(fragment, loc.textSpan)
+ };
+ }
+ })
+ );
+ return this.filterWrongRenameLocations(mappedLocations.filter(isNotNullOrUndefined));
+ }
- private filterWrongRenameLocations(
- mappedLocations: Array
- ): Promise> {
- return filterAsync(mappedLocations, async (loc) => {
- const snapshot = await this.getSnapshot(loc.fileName);
- if (!(snapshot instanceof SvelteDocumentSnapshot)) {
- return true;
- }
+ private filterWrongRenameLocations(
+ mappedLocations: Array
+ ): 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_instanceOf(TheComponentToRename)` or
- // `__sveltets_ensureType(TheComponentToRename,..`. Prevent that.
- // Additionally, we cannot rename the hidden variable containing the store value
- return (
- notPrecededBy('__sveltets_instanceOf(') &&
- notPrecededBy('__sveltets_ensureType(') &&
- notPrecededBy('= __sveltets_store_get(')
- );
+ const content = snapshot.getText(0, snapshot.getLength());
+ // When the user renames a Svelte component, ts will also want to rename
+ // `__sveltets_instanceOf(TheComponentToRename)` or
+ // `__sveltets_ensureType(TheComponentToRename,..`. Prevent that.
+ // Additionally, we cannot rename the hidden variable containing the store value
+ return (
+ notPrecededBy('__sveltets_instanceOf(') &&
+ notPrecededBy('__sveltets_ensureType(') &&
+ notPrecededBy('= __sveltets_store_get(')
+ );
- function notPrecededBy(str: string) {
- return (
- content.lastIndexOf(str, loc.textSpan.start) !== loc.textSpan.start - str.length
- );
- }
- });
- }
+ function notPrecededBy(str: string) {
+ return (
+ content.lastIndexOf(str, loc.textSpan.start) !== loc.textSpan.start - str.length
+ );
+ }
+ });
+ }
- private mapRangeToOriginal(doc: SnapshotFragment, 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(doc, convertRange(doc, textSpan));
- if (range.end.character - range.start.character < textSpan.length) {
- range.end.character++;
- }
- return range;
- }
+ private mapRangeToOriginal(doc: SnapshotFragment, 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(doc, convertRange(doc, textSpan));
+ if (range.end.character - range.start.character < textSpan.length) {
+ range.end.character++;
+ }
+ return range;
+ }
- private getVariableAtPosition(
- tsDoc: SvelteDocumentSnapshot,
- fragment: SvelteSnapshotFragment,
- lang: ts.LanguageService,
- position: Position
- ) {
- const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
- const { start, length } = lang.getSmartSelectionRange(tsDoc.filePath, offset).textSpan;
- return tsDoc.getText(start, start + length);
- }
+ private getVariableAtPosition(
+ tsDoc: SvelteDocumentSnapshot,
+ fragment: SvelteSnapshotFragment,
+ lang: ts.LanguageService,
+ position: Position
+ ) {
+ const offset = fragment.offsetAt(fragment.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 async getLSAndTSDoc(document: Document) {
+ return this.lsAndTsDocResolver.getLSAndTSDoc(document);
+ }
- private getSnapshot(filePath: string) {
- return this.lsAndTsDocResolver.getSnapshot(filePath);
- }
+ private getSnapshot(filePath: string) {
+ return this.lsAndTsDocResolver.getSnapshot(filePath);
+ }
}
function unique(array: T[]): T[] {
- return uniqWith(array, isEqual);
+ return uniqWith(array, isEqual);
}
diff --git a/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts b/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts
index 355abc4a6..4519c5ba1 100644
--- a/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts
@@ -7,65 +7,65 @@ import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertRange } from '../utils';
export class SelectionRangeProviderImpl implements SelectionRangeProvider {
- constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
+ constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
- async getSelectionRange(
- document: Document,
- position: Position
- ): Promise {
- const { tsDoc, lang } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
- const fragment = await tsDoc.getFragment();
+ async getSelectionRange(
+ document: Document,
+ position: Position
+ ): Promise {
+ const { tsDoc, lang } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
+ const fragment = await tsDoc.getFragment();
- const tsSelectionRange = lang.getSmartSelectionRange(
- tsDoc.filePath,
- fragment.offsetAt(fragment.getGeneratedPosition(position))
- );
- const selectionRange = this.toSelectionRange(fragment, tsSelectionRange);
- const mappedRange = mapSelectionRangeToParent(fragment, selectionRange);
+ const tsSelectionRange = lang.getSmartSelectionRange(
+ tsDoc.filePath,
+ fragment.offsetAt(fragment.getGeneratedPosition(position))
+ );
+ const selectionRange = this.toSelectionRange(fragment, tsSelectionRange);
+ const mappedRange = mapSelectionRangeToParent(fragment, selectionRange);
- return this.filterOutUnmappedRange(mappedRange);
- }
+ return this.filterOutUnmappedRange(mappedRange);
+ }
- private toSelectionRange(
- fragment: SvelteSnapshotFragment,
- { textSpan, parent }: ts.SelectionRange
- ): SelectionRange {
- return {
- range: convertRange(fragment, textSpan),
- parent: parent && this.toSelectionRange(fragment, parent)
- };
- }
+ private toSelectionRange(
+ fragment: SvelteSnapshotFragment,
+ { textSpan, parent }: ts.SelectionRange
+ ): SelectionRange {
+ return {
+ range: convertRange(fragment, textSpan),
+ parent: parent && this.toSelectionRange(fragment, 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;
- }
+ 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;
+ let result: SelectionRange | undefined;
- for (const selectionRange of filtered) {
- result = SelectionRange.create(selectionRange, result);
- }
+ for (const selectionRange of filtered) {
+ result = SelectionRange.create(selectionRange, result);
+ }
- return result ?? null;
- }
+ 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;
+ /**
+ * 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;
- }
+ while (current.parent) {
+ result.unshift(current.range);
+ current = current.parent;
+ }
- return result;
- }
+ return result;
+ }
}
diff --git a/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts b/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts
index d2e3e7061..49906856c 100644
--- a/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts
@@ -9,117 +9,117 @@ import { convertToTextSpan } from '../utils';
const CONTENT_LENGTH_LIMIT = 50000;
export class SemanticTokensProviderImpl implements SemanticTokensProvider {
- constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
-
- async getSemanticTokens(textDocument: Document, range?: Range): Promise {
- const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(textDocument);
- const fragment = await tsDoc.getFragment();
-
- // for better performance, don't do full-file semantic tokens when the file is too big
- if (!range && fragment.text.length > CONTENT_LENGTH_LIMIT) {
- return null;
- }
-
- const textSpan = range
- ? convertToTextSpan(range, fragment)
- : {
- start: 0,
- length: tsDoc.parserError
- ? fragment.text.length
- : // This is appended by svelte2tsx, there's nothing mappable afterwards
- fragment.text.lastIndexOf('return { props:') || fragment.text.length
- };
-
- 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,
- fragment,
- generatedOffset,
- generatedLength
- );
- 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,
- fragment: SnapshotFragment,
- generatedOffset: number,
- generatedLength: number
- ): [line: number, character: number, length: number] | undefined {
- const range = {
- start: fragment.positionAt(generatedOffset),
- end: fragment.positionAt(generatedOffset + generatedLength)
- };
- const { start: startPosition, end: endPosition } = mapRangeToOriginal(fragment, 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];
- }
-
- /**
- * TSClassification = (TokenType + 1) << TokenEncodingConsts.typeOffset + TokenModifier
- */
- private getTokenTypeFromClassification(tsClassification: number): number {
- return (tsClassification >> TokenEncodingConsts.typeOffset) - 1;
- }
-
- private getTokenModifierFromClassification(tsClassification: number) {
- return tsClassification & TokenEncodingConsts.modifierMask;
- }
+ constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
+
+ async getSemanticTokens(textDocument: Document, range?: Range): Promise {
+ const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(textDocument);
+ const fragment = await tsDoc.getFragment();
+
+ // for better performance, don't do full-file semantic tokens when the file is too big
+ if (!range && fragment.text.length > CONTENT_LENGTH_LIMIT) {
+ return null;
+ }
+
+ const textSpan = range
+ ? convertToTextSpan(range, fragment)
+ : {
+ start: 0,
+ length: tsDoc.parserError
+ ? fragment.text.length
+ : // This is appended by svelte2tsx, there's nothing mappable afterwards
+ fragment.text.lastIndexOf('return { props:') || fragment.text.length
+ };
+
+ 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,
+ fragment,
+ generatedOffset,
+ generatedLength
+ );
+ 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,
+ fragment: SnapshotFragment,
+ generatedOffset: number,
+ generatedLength: number
+ ): [line: number, character: number, length: number] | undefined {
+ const range = {
+ start: fragment.positionAt(generatedOffset),
+ end: fragment.positionAt(generatedOffset + generatedLength)
+ };
+ const { start: startPosition, end: endPosition } = mapRangeToOriginal(fragment, 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];
+ }
+
+ /**
+ * 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
+ 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
index 82d900c77..b77bae498 100644
--- a/packages/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts
@@ -1,12 +1,12 @@
import ts from 'typescript';
import {
- Position,
- SignatureHelpContext,
- SignatureHelp,
- SignatureHelpTriggerKind,
- SignatureInformation,
- ParameterInformation,
- MarkupKind
+ Position,
+ SignatureHelpContext,
+ SignatureHelp,
+ SignatureHelpTriggerKind,
+ SignatureInformation,
+ ParameterInformation,
+ MarkupKind
} from 'vscode-languageserver';
import { SignatureHelpProvider } from '../..';
import { Document } from '../../../lib/documents';
@@ -14,138 +14,138 @@ 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
- ): Promise {
- const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
- const fragment = await tsDoc.getFragment();
-
- const offset = fragment.offsetAt(fragment.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')
- );
- }
+ constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
+
+ private static readonly triggerCharacters = ['(', ',', '<'];
+ private static readonly retriggerCharacters = [')'];
+
+ async getSignatureHelp(
+ document: Document,
+ position: Position,
+ context: SignatureHelpContext | undefined
+ ): Promise {
+ const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
+ const fragment = await tsDoc.getFragment();
+
+ const offset = fragment.offsetAt(fragment.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/UpdateImportsProvider.ts b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts
index 7349a450e..6f7d21ad9 100644
--- a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts
@@ -1,8 +1,8 @@
import {
- OptionalVersionedTextDocumentIdentifier,
- TextDocumentEdit,
- TextEdit,
- WorkspaceEdit
+ OptionalVersionedTextDocumentIdentifier,
+ TextDocumentEdit,
+ TextEdit,
+ WorkspaceEdit
} from 'vscode-languageserver';
import { mapRangeToOriginal } from '../../../lib/documents';
import { urlToPath } from '../../../utils';
@@ -12,53 +12,53 @@ import { convertRange } from '../utils';
import { SnapshotFragmentMap } from './utils';
export class UpdateImportsProviderImpl implements UpdateImportsProvider {
- constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
+ constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
- async updateImports(fileRename: FileRename): Promise {
- const oldPath = urlToPath(fileRename.oldUri);
- const newPath = urlToPath(fileRename.newUri);
- if (!oldPath || !newPath) {
- return null;
- }
+ async updateImports(fileRename: FileRename): Promise {
+ const oldPath = urlToPath(fileRename.oldUri);
+ const newPath = urlToPath(fileRename.newUri);
+ if (!oldPath || !newPath) {
+ return null;
+ }
- const ls = await this.getLSForPath(newPath);
- // `getEditsForFileRename` might take a while
- const fileChanges = ls.getEditsForFileRename(oldPath, newPath, {}, {});
+ const ls = await this.getLSForPath(newPath);
+ // `getEditsForFileRename` might take a while
+ const fileChanges = ls.getEditsForFileRename(oldPath, newPath, {}, {});
- await this.lsAndTsDocResolver.updateSnapshotPath(oldPath, newPath);
- const updateImportsChanges = fileChanges
- // 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 === oldPath)
- // 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 :)
- .map((change) => {
- change.fileName = change.fileName.replace(oldPath, newPath);
- return change;
- });
+ await this.lsAndTsDocResolver.updateSnapshotPath(oldPath, newPath);
+ const updateImportsChanges = fileChanges
+ // 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 === oldPath)
+ // 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 :)
+ .map((change) => {
+ change.fileName = change.fileName.replace(oldPath, newPath);
+ return change;
+ });
- const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
- const documentChanges = await Promise.all(
- updateImportsChanges.map(async (change) => {
- const fragment = await docs.retrieveFragment(change.fileName);
+ const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver);
+ const documentChanges = await Promise.all(
+ updateImportsChanges.map(async (change) => {
+ const fragment = await docs.retrieveFragment(change.fileName);
- return TextDocumentEdit.create(
- OptionalVersionedTextDocumentIdentifier.create(fragment.getURL(), null),
- change.textChanges.map((edit) => {
- const range = mapRangeToOriginal(
- fragment,
- convertRange(fragment, edit.span)
- );
- return TextEdit.replace(range, edit.newText);
- })
- );
- })
- );
+ return TextDocumentEdit.create(
+ OptionalVersionedTextDocumentIdentifier.create(fragment.getURL(), null),
+ change.textChanges.map((edit) => {
+ const range = mapRangeToOriginal(
+ fragment,
+ convertRange(fragment, edit.span)
+ );
+ return TextEdit.replace(range, edit.newText);
+ })
+ );
+ })
+ );
- return { documentChanges };
- }
+ return { documentChanges };
+ }
- private async getLSForPath(path: string) {
- return this.lsAndTsDocResolver.getLSForPath(path);
- }
+ 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
index 825a9e4d0..c0ed1f53c 100644
--- a/packages/language-server/src/plugins/typescript/features/getDirectiveCommentCompletions.ts
+++ b/packages/language-server/src/plugins/typescript/features/getDirectiveCommentCompletions.ts
@@ -1,77 +1,77 @@
import { Document, isInTag } from '../../../lib/documents';
import {
- Position,
- CompletionItemKind,
- CompletionItem,
- TextEdit,
- Range,
- CompletionList,
- CompletionContext
+ 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.'
- }
+ {
+ 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
+ position: Position,
+ document: Document,
+ completionContext: CompletionContext | undefined
) {
- // don't trigger until // @
- if (completionContext?.triggerCharacter === '/') {
- return null;
- }
+ // 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 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-]*)?$/);
+ 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);
+ 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
- )
- }));
+ 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);
+ 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
index 8a54d7025..71bf04ae0 100644
--- a/packages/language-server/src/plugins/typescript/features/getJsDocTemplateCompletion.ts
+++ b/packages/language-server/src/plugins/typescript/features/getJsDocTemplateCompletion.ts
@@ -1,11 +1,11 @@
import ts from 'typescript';
import {
- CompletionItem,
- CompletionItemKind,
- CompletionList,
- InsertTextFormat,
- Range,
- TextEdit
+ CompletionItem,
+ CompletionItemKind,
+ CompletionList,
+ InsertTextFormat,
+ Range,
+ TextEdit
} from 'vscode-languageserver';
import { mapRangeToOriginal } from '../../../lib/documents';
import { SvelteSnapshotFragment } from '../DocumentSnapshot';
@@ -13,50 +13,50 @@ import { SvelteSnapshotFragment } from '../DocumentSnapshot';
const DEFAULT_SNIPPET = `/**${ts.sys.newLine} * $0${ts.sys.newLine} */`;
export function getJsDocTemplateCompletion(
- fragment: SvelteSnapshotFragment,
- lang: ts.LanguageService,
- filePath: string,
- offset: number
+ fragment: SvelteSnapshotFragment,
+ lang: ts.LanguageService,
+ filePath: string,
+ offset: number
): CompletionList | null {
- const template = lang.getDocCommentTemplateAtPosition(filePath, offset);
+ const template = lang.getDocCommentTemplateAtPosition(filePath, offset);
- if (!template) {
- return null;
- }
- const { text } = fragment;
- const lineStart = text.lastIndexOf('\n', offset);
- const lineEnd = text.indexOf('\n', offset);
- const isLastLine = lineEnd === -1;
+ if (!template) {
+ return null;
+ }
+ const { text } = fragment;
+ 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 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(
- fragment,
- Range.create(
- fragment.positionAt(start),
- fragment.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 start = line.lastIndexOf('/**', character) + lineStart;
+ const suffix = line.slice(character).match(/^\s*\**\//);
+ const textEditRange = mapRangeToOriginal(
+ fragment,
+ Range.create(
+ fragment.positionAt(start),
+ fragment.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
- };
+ const item: CompletionItem = {
+ label: '/** */',
+ detail: 'JSDoc comment',
+ sortText: '\0',
+ kind: CompletionItemKind.Snippet,
+ textEdit: TextEdit.replace(textEditRange, snippet),
+ insertTextFormat: InsertTextFormat.Snippet
+ };
- return CompletionList.create([item]);
+ return CompletionList.create([item]);
}
/**
@@ -66,14 +66,14 @@ export function getJsDocTemplateCompletion(
* 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')
- );
+ 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
index d96ba6ab7..8a4f214eb 100644
--- a/packages/language-server/src/plugins/typescript/features/utils.ts
+++ b/packages/language-server/src/plugins/typescript/features/utils.ts
@@ -2,10 +2,10 @@ import ts from 'typescript';
import { Position } from 'vscode-languageserver';
import { Document, getNodeIfIsInComponentStartTag, isInTag } from '../../../lib/documents';
import {
- DocumentSnapshot,
- SnapshotFragment,
- SvelteDocumentSnapshot,
- SvelteSnapshotFragment
+ DocumentSnapshot,
+ SnapshotFragment,
+ SvelteDocumentSnapshot,
+ SvelteSnapshotFragment
} from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
@@ -14,44 +14,44 @@ import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
* return the snapshot of that component.
*/
export async function getComponentAtPosition(
- lsAndTsDocResovler: LSAndTSDocResolver,
- lang: ts.LanguageService,
- doc: Document,
- tsDoc: SvelteDocumentSnapshot,
- fragment: SvelteSnapshotFragment,
- originalPosition: Position
+ lsAndTsDocResovler: LSAndTSDocResolver,
+ lang: ts.LanguageService,
+ doc: Document,
+ tsDoc: SvelteDocumentSnapshot,
+ fragment: SvelteSnapshotFragment,
+ originalPosition: Position
): Promise {
- if (tsDoc.parserError) {
- return null;
- }
+ if (tsDoc.parserError) {
+ return null;
+ }
- if (
- isInTag(originalPosition, doc.scriptInfo) ||
- isInTag(originalPosition, doc.moduleScriptInfo)
- ) {
- // Inside script tags -> not a component
- 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 node = getNodeIfIsInComponentStartTag(doc.html, doc.offsetAt(originalPosition));
+ if (!node) {
+ return null;
+ }
- const generatedPosition = fragment.getGeneratedPosition(doc.positionAt(node.start + 1));
- const def = lang.getDefinitionAtPosition(
- tsDoc.filePath,
- fragment.offsetAt(generatedPosition)
- )?.[0];
- if (!def) {
- return null;
- }
+ const generatedPosition = fragment.getGeneratedPosition(doc.positionAt(node.start + 1));
+ const def = lang.getDefinitionAtPosition(
+ tsDoc.filePath,
+ fragment.offsetAt(generatedPosition)
+ )?.[0];
+ if (!def) {
+ return null;
+ }
- const snapshot = await lsAndTsDocResovler.getSnapshot(def.fileName);
- if (!(snapshot instanceof SvelteDocumentSnapshot)) {
- return null;
- }
- return snapshot;
+ const snapshot = await lsAndTsDocResovler.getSnapshot(def.fileName);
+ if (!(snapshot instanceof SvelteDocumentSnapshot)) {
+ return null;
+ }
+ return snapshot;
}
/**
@@ -59,12 +59,12 @@ export async function getComponentAtPosition(
* 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);
- return (
- text.substring(lineStart, start).includes('/*Ωignore_startΩ*/') &&
- text.substring(end, lineEnd).includes('/*Ωignore_endΩ*/')
- );
+ const lineStart = text.lastIndexOf('\n', start);
+ const lineEnd = text.indexOf('\n', end);
+ return (
+ text.substring(lineStart, start).includes('/*Ωignore_startΩ*/') &&
+ text.substring(end, lineEnd).includes('/*Ωignore_endΩ*/')
+ );
}
/**
@@ -72,37 +72,37 @@ export function isInGeneratedCode(text: string, start: number, end: number) {
* because it's purely generated.
*/
export function isNoTextSpanInGeneratedCode(text: string, span: ts.TextSpan) {
- return !isInGeneratedCode(text, span.start, span.start + span.length);
+ return !isInGeneratedCode(text, span.start, span.start + span.length);
}
export class SnapshotFragmentMap {
- private map = new Map();
- constructor(private resolver: LSAndTSDocResolver) {}
+ private map = new Map();
+ constructor(private resolver: LSAndTSDocResolver) {}
- set(fileName: string, content: { fragment: SnapshotFragment; snapshot: DocumentSnapshot }) {
- this.map.set(fileName, content);
- }
+ set(fileName: string, content: { fragment: SnapshotFragment; snapshot: DocumentSnapshot }) {
+ this.map.set(fileName, content);
+ }
- get(fileName: string) {
- return this.map.get(fileName);
- }
+ get(fileName: string) {
+ return this.map.get(fileName);
+ }
- getFragment(fileName: string) {
- return this.map.get(fileName)?.fragment;
- }
+ getFragment(fileName: string) {
+ return this.map.get(fileName)?.fragment;
+ }
- async retrieve(fileName: string) {
- let snapshotFragment = this.get(fileName);
- if (!snapshotFragment) {
- const snapshot = await this.resolver.getSnapshot(fileName);
- const fragment = await snapshot.getFragment();
- snapshotFragment = { fragment, snapshot };
- this.set(fileName, snapshotFragment);
- }
- return snapshotFragment;
- }
+ async retrieve(fileName: string) {
+ let snapshotFragment = this.get(fileName);
+ if (!snapshotFragment) {
+ const snapshot = await this.resolver.getSnapshot(fileName);
+ const fragment = await snapshot.getFragment();
+ snapshotFragment = { fragment, snapshot };
+ this.set(fileName, snapshotFragment);
+ }
+ return snapshotFragment;
+ }
- async retrieveFragment(fileName: string) {
- return (await this.retrieve(fileName)).fragment;
- }
+ async retrieveFragment(fileName: string) {
+ return (await this.retrieve(fileName)).fragment;
+ }
}
diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts
index 6b19e78c1..113be7940 100644
--- a/packages/language-server/src/plugins/typescript/module-loader.ts
+++ b/packages/language-server/src/plugins/typescript/module-loader.ts
@@ -1,8 +1,8 @@
import ts from 'typescript';
import {
- isVirtualSvelteFilePath,
- ensureRealSvelteFilePath,
- getExtensionFromScriptKind
+ isVirtualSvelteFilePath,
+ ensureRealSvelteFilePath,
+ getExtensionFromScriptKind
} from './utils';
import { DocumentSnapshot } from './DocumentSnapshot';
import { createSvelteSys } from './svelte-sys';
@@ -11,40 +11,40 @@ import { createSvelteSys } from './svelte-sys';
* Caches resolved modules.
*/
class ModuleResolutionCache {
- private cache = new Map();
+ private cache = new Map();
- /**
- * Tries to get a cached module.
- */
- get(moduleName: string, containingFile: string): ts.ResolvedModule | undefined {
- return this.cache.get(this.getKey(moduleName, containingFile));
- }
+ /**
+ * Tries to get a cached module.
+ */
+ get(moduleName: string, containingFile: string): ts.ResolvedModule | undefined {
+ return this.cache.get(this.getKey(moduleName, containingFile));
+ }
- /**
- * Caches resolved module, if it is not undefined.
- */
- set(moduleName: string, containingFile: string, resolvedModule: ts.ResolvedModule | undefined) {
- if (!resolvedModule) {
- return;
- }
- this.cache.set(this.getKey(moduleName, containingFile), resolvedModule);
- }
+ /**
+ * Caches resolved module, if it is not undefined.
+ */
+ set(moduleName: string, containingFile: string, resolvedModule: ts.ResolvedModule | 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 {
- this.cache.forEach((val, key) => {
- if (val.resolvedFileName === resolvedModuleName) {
- this.cache.delete(key);
- }
- });
- }
+ /**
+ * Deletes module from cache. Call this if a file was deleted.
+ * @param resolvedModuleName full path of the module
+ */
+ delete(resolvedModuleName: string): void {
+ this.cache.forEach((val, key) => {
+ if (val.resolvedFileName === resolvedModuleName) {
+ this.cache.delete(key);
+ }
+ });
+ }
- private getKey(moduleName: string, containingFile: string) {
- return containingFile + ':::' + ensureRealSvelteFilePath(moduleName);
- }
+ private getKey(moduleName: string, containingFile: string) {
+ return containingFile + ':::' + ensureRealSvelteFilePath(moduleName);
+ }
}
/**
@@ -60,69 +60,69 @@ class ModuleResolutionCache {
* @param compilerOptions The typescript compiler options
*/
export function createSvelteModuleLoader(
- getSnapshot: (fileName: string) => DocumentSnapshot,
- compilerOptions: ts.CompilerOptions
+ getSnapshot: (fileName: string) => DocumentSnapshot,
+ compilerOptions: ts.CompilerOptions
) {
- const svelteSys = createSvelteSys(getSnapshot);
- const moduleCache = new ModuleResolutionCache();
+ const svelteSys = createSvelteSys(getSnapshot);
+ const moduleCache = new ModuleResolutionCache();
- return {
- fileExists: svelteSys.fileExists,
- readFile: svelteSys.readFile,
- readDirectory: svelteSys.readDirectory,
- deleteFromModuleCache: (path: string) => moduleCache.delete(path),
- resolveModuleNames
- };
+ return {
+ fileExists: svelteSys.fileExists,
+ readFile: svelteSys.readFile,
+ readDirectory: svelteSys.readDirectory,
+ deleteFromModuleCache: (path: string) => moduleCache.delete(path),
+ resolveModuleNames
+ };
- function resolveModuleNames(
- moduleNames: string[],
- containingFile: string
- ): Array {
- return moduleNames.map((moduleName) => {
- const cachedModule = moduleCache.get(moduleName, containingFile);
- if (cachedModule) {
- return cachedModule;
- }
+ function resolveModuleNames(
+ moduleNames: string[],
+ containingFile: string
+ ): Array {
+ return moduleNames.map((moduleName) => {
+ const cachedModule = moduleCache.get(moduleName, containingFile);
+ if (cachedModule) {
+ return cachedModule;
+ }
- const resolvedModule = resolveModuleName(moduleName, containingFile);
- moduleCache.set(moduleName, containingFile, resolvedModule);
- return resolvedModule;
- });
- }
+ const resolvedModule = resolveModuleName(moduleName, containingFile);
+ moduleCache.set(moduleName, containingFile, resolvedModule);
+ return resolvedModule;
+ });
+ }
- function resolveModuleName(
- name: string,
- containingFile: string
- ): ts.ResolvedModule | undefined {
- // 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 tsResolvedModule = ts.resolveModuleName(name, containingFile, compilerOptions, ts.sys)
- .resolvedModule;
- if (tsResolvedModule && !isVirtualSvelteFilePath(tsResolvedModule.resolvedFileName)) {
- return tsResolvedModule;
- }
+ function resolveModuleName(
+ name: string,
+ containingFile: string
+ ): ts.ResolvedModule | undefined {
+ // 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 tsResolvedModule = ts.resolveModuleName(name, containingFile, compilerOptions, ts.sys)
+ .resolvedModule;
+ if (tsResolvedModule && !isVirtualSvelteFilePath(tsResolvedModule.resolvedFileName)) {
+ return tsResolvedModule;
+ }
- const svelteResolvedModule = ts.resolveModuleName(
- name,
- containingFile,
- compilerOptions,
- svelteSys
- ).resolvedModule;
- if (
- !svelteResolvedModule ||
- !isVirtualSvelteFilePath(svelteResolvedModule.resolvedFileName)
- ) {
- return svelteResolvedModule;
- }
+ const svelteResolvedModule = ts.resolveModuleName(
+ name,
+ containingFile,
+ compilerOptions,
+ svelteSys
+ ).resolvedModule;
+ if (
+ !svelteResolvedModule ||
+ !isVirtualSvelteFilePath(svelteResolvedModule.resolvedFileName)
+ ) {
+ return svelteResolvedModule;
+ }
- const resolvedFileName = ensureRealSvelteFilePath(svelteResolvedModule.resolvedFileName);
- const snapshot = getSnapshot(resolvedFileName);
+ const resolvedFileName = ensureRealSvelteFilePath(svelteResolvedModule.resolvedFileName);
+ const snapshot = getSnapshot(resolvedFileName);
- const resolvedSvelteModule: ts.ResolvedModuleFull = {
- extension: getExtensionFromScriptKind(snapshot && snapshot.scriptKind),
- resolvedFileName
- };
- return resolvedSvelteModule;
- }
+ const resolvedSvelteModule: ts.ResolvedModuleFull = {
+ extension: getExtensionFromScriptKind(snapshot && snapshot.scriptKind),
+ resolvedFileName
+ };
+ return resolvedSvelteModule;
+ }
}
diff --git a/packages/language-server/src/plugins/typescript/previewer.ts b/packages/language-server/src/plugins/typescript/previewer.ts
index 0f80b1e18..192814d1f 100644
--- a/packages/language-server/src/plugins/typescript/previewer.ts
+++ b/packages/language-server/src/plugins/typescript/previewer.ts
@@ -11,130 +11,130 @@ 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})`;
- }
- }
- )
- );
+ 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);
+ 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(tag.text);
- case 'author':
- return makeEmailTag(tag.text);
- case 'default':
- return makeCodeblock(tag.text);
- }
-
- return processInlineTags(tag.text);
+ 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(tag.text);
+ case 'author':
+ return makeEmailTag(tag.text);
+ case 'default':
+ return makeCodeblock(tag.text);
+ }
+
+ return processInlineTags(tag.text);
}
export function getTagDocumentation(tag: ts.JSDocTagInfo): string | undefined {
- function getWithType() {
- const body = (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}`);
+ function getWithType() {
+ const body = (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));
+ return processInlineTags(typeof parts === 'string' ? parts : ts.displayPartsToString(parts));
}
export function getMarkdownDocumentation(
- documentation: ts.SymbolDisplayPart[] | undefined,
- tags: ts.JSDocTagInfo[] | undefined
+ documentation: ts.SymbolDisplayPart[] | undefined,
+ tags: ts.JSDocTagInfo[] | undefined
) {
- let result: Array = [];
- if (documentation) {
- result.push(plain(documentation));
- }
+ let result: Array = [];
+ if (documentation) {
+ result.push(plain(documentation));
+ }
- if (tags) {
- result = result.concat(tags.map(getTagDocumentation));
- }
+ if (tags) {
+ result = result.concat(tags.map(getTagDocumentation));
+ }
- return result.filter(isNotNullOrUndefined).join('\n\n');
+ 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
index 1a44727c6..f204882d5 100644
--- a/packages/language-server/src/plugins/typescript/service.ts
+++ b/packages/language-server/src/plugins/typescript/service.ts
@@ -10,265 +10,265 @@ import { ensureRealSvelteFilePath, findTsConfigPath } from './utils';
import { configLoader } from '../../lib/documents/configLoader';
export interface LanguageServiceContainer {
- readonly tsconfigPath: string;
- readonly compilerOptions: ts.CompilerOptions;
- readonly snapshotManager: SnapshotManager;
- getService(): ts.LanguageService;
- updateDocument(documentOrFilePath: Document | string): DocumentSnapshot;
- deleteDocument(filePath: string): void;
+ readonly tsconfigPath: string;
+ readonly compilerOptions: ts.CompilerOptions;
+ readonly snapshotManager: SnapshotManager;
+ getService(): ts.LanguageService;
+ updateDocument(documentOrFilePath: Document | string): DocumentSnapshot;
+ deleteDocument(filePath: string): void;
}
const services = new Map>();
export interface LanguageServiceDocumentContext {
- transformOnTemplateError: boolean;
- createDocument: (fileName: string, content: string) => Document;
+ transformOnTemplateError: boolean;
+ createDocument: (fileName: string, content: string) => Document;
}
export async function getLanguageServiceForPath(
- path: string,
- workspaceUris: string[],
- docContext: LanguageServiceDocumentContext
+ path: string,
+ workspaceUris: string[],
+ docContext: LanguageServiceDocumentContext
): Promise {
- return (await getService(path, workspaceUris, docContext)).getService();
+ return (await getService(path, workspaceUris, docContext)).getService();
}
export async function getLanguageServiceForDocument(
- document: Document,
- workspaceUris: string[],
- docContext: LanguageServiceDocumentContext
+ document: Document,
+ workspaceUris: string[],
+ docContext: LanguageServiceDocumentContext
): Promise {
- return getLanguageServiceForPath(document.getFilePath() || '', workspaceUris, docContext);
+ return getLanguageServiceForPath(document.getFilePath() || '', workspaceUris, docContext);
}
export async function getService(
- path: string,
- workspaceUris: string[],
- docContext: LanguageServiceDocumentContext
+ path: string,
+ workspaceUris: string[],
+ docContext: LanguageServiceDocumentContext
) {
- const tsconfigPath = findTsConfigPath(path, workspaceUris);
-
- let service: LanguageServiceContainer;
- if (services.has(tsconfigPath)) {
- service = await services.get(tsconfigPath)!;
- } else {
- Logger.log('Initialize new ts service at ', tsconfigPath);
- const newService = createLanguageService(tsconfigPath, docContext);
- services.set(tsconfigPath, newService);
- service = await newService;
- }
-
- return service;
+ const tsconfigPath = findTsConfigPath(path, workspaceUris);
+
+ let service: LanguageServiceContainer;
+ if (services.has(tsconfigPath)) {
+ service = await services.get(tsconfigPath)!;
+ } else {
+ Logger.log('Initialize new ts service at ', tsconfigPath);
+ const newService = createLanguageService(tsconfigPath, docContext);
+ services.set(tsconfigPath, newService);
+ service = await newService;
+ }
+
+ return service;
}
async function createLanguageService(
- tsconfigPath: string,
- docContext: LanguageServiceDocumentContext
+ tsconfigPath: string,
+ docContext: LanguageServiceDocumentContext
): Promise {
- const workspacePath = tsconfigPath ? dirname(tsconfigPath) : '';
-
- const { options: compilerOptions, fileNames: files, raw } = 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(files, raw, workspacePath || process.cwd());
-
- // 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 to determine
- // the default language.
- await configLoader.loadConfigs(workspacePath);
-
- const svelteModuleLoader = createSvelteModuleLoader(getSnapshot, compilerOptions);
-
- let svelteTsPath: string;
- try {
- // For when svelte2tsx is part of node_modules, for example VS Code extension
- svelteTsPath = dirname(require.resolve('svelte2tsx'));
- } catch (e) {
- // Fall back to dirname, for example for svelte-check
- svelteTsPath = __dirname;
- }
- const svelteTsxFiles = [
- './svelte-shims.d.ts',
- './svelte-jsx.d.ts',
- './svelte-native-jsx.d.ts'
- ].map((f) => ts.sys.resolvePath(resolve(svelteTsPath, f)));
-
- const host: ts.LanguageServiceHost = {
- getCompilationSettings: () => compilerOptions,
- getScriptFileNames: () =>
- Array.from(
- new Set([
- ...snapshotManager.getProjectFileNames(),
- ...snapshotManager.getFileNames(),
- ...svelteTsxFiles
- ])
- ),
- getScriptVersion: (fileName: string) => getSnapshot(fileName).version.toString(),
- getScriptSnapshot: getSnapshot,
- getCurrentDirectory: () => workspacePath,
- getDefaultLibFileName: ts.getDefaultLibFilePath,
- fileExists: svelteModuleLoader.fileExists,
- readFile: svelteModuleLoader.readFile,
- resolveModuleNames: svelteModuleLoader.resolveModuleNames,
- readDirectory: svelteModuleLoader.readDirectory,
- getDirectories: ts.sys.getDirectories,
- useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
- getScriptKind: (fileName: string) => getSnapshot(fileName).scriptKind
- };
- let languageService = ts.createLanguageService(host);
- const transformationConfig = {
- strictMode: !!compilerOptions.strict,
- transformOnTemplateError: docContext.transformOnTemplateError
- };
-
- return {
- tsconfigPath,
- compilerOptions,
- getService: () => languageService,
- updateDocument,
- deleteDocument,
- snapshotManager
- };
-
- function deleteDocument(filePath: string): void {
- svelteModuleLoader.deleteFromModuleCache(filePath);
- snapshotManager.delete(filePath);
- }
-
- function updateDocument(documentOrFilePath: Document | string): DocumentSnapshot {
- const filePath =
- typeof documentOrFilePath === 'string'
- ? documentOrFilePath
- : documentOrFilePath.getFilePath() || '';
- const document = typeof documentOrFilePath === 'string' ? undefined : documentOrFilePath;
- const prevSnapshot = snapshotManager.get(filePath);
-
- // Don't reinitialize document if no update needed.
- if (document && prevSnapshot?.version === document.version) {
- return prevSnapshot;
- }
-
- const newSnapshot = document
- ? DocumentSnapshot.fromDocument(document, transformationConfig)
- : DocumentSnapshot.fromFilePath(
- filePath,
- docContext.createDocument,
- transformationConfig
- );
- snapshotManager.set(filePath, newSnapshot);
- if (prevSnapshot && prevSnapshot.scriptKind !== newSnapshot.scriptKind) {
- // Restart language service as it doesn't handle script kind changes.
- languageService.dispose();
- languageService = ts.createLanguageService(host);
- }
-
- return newSnapshot;
- }
-
- function getSnapshot(fileName: string): DocumentSnapshot {
- fileName = ensureRealSvelteFilePath(fileName);
-
- let doc = snapshotManager.get(fileName);
- if (doc) {
- return doc;
- }
-
- doc = DocumentSnapshot.fromFilePath(
- fileName,
- docContext.createDocument,
- transformationConfig
- );
- snapshotManager.set(fileName, doc);
- return doc;
- }
-
- function getParsedConfig() {
- const forcedCompilerOptions: ts.CompilerOptions = {
- allowNonTsExtensions: true,
- target: ts.ScriptTarget.Latest,
- module: ts.ModuleKind.ESNext,
- moduleResolution: ts.ModuleResolutionKind.NodeJs,
- allowJs: true,
- noEmit: true,
- declaration: false,
- skipLibCheck: true,
- // these are needed to handle the results of svelte2tsx preprocessing:
- jsx: ts.JsxEmit.Preserve
- };
-
- // always let ts parse config to get default compilerOption
- let configJson =
- (tsconfigPath && ts.readConfigFile(tsconfigPath, ts.sys.readFile).config) ||
- getDefaultJsConfig();
-
- // Only default exclude when no extends for now
- if (!configJson.extends) {
- configJson = Object.assign(
- {
- exclude: getDefaultExclude()
- },
- configJson
- );
- }
-
- const parsedConfig = ts.parseJsonConfigFileContent(
- configJson,
- ts.sys,
- workspacePath,
- forcedCompilerOptions,
- tsconfigPath,
- undefined,
- [{ extension: 'svelte', isMixedContent: false, scriptKind: ts.ScriptKind.TSX }]
- );
-
- const compilerOptions: ts.CompilerOptions = {
- ...parsedConfig.options,
- ...forcedCompilerOptions
- };
-
- // detect which JSX namespace to use (svelte | svelteNative) if not specified or not compatible
- if (!compilerOptions.jsxFactory || !compilerOptions.jsxFactory.startsWith('svelte')) {
- //default to regular svelte, this causes the usage of the "svelte.JSX" namespace
- compilerOptions.jsxFactory = 'svelte.createElement';
-
- //override if we detect svelte-native
- if (workspacePath) {
- try {
- const svelteNativePkgInfo = getPackageInfo('svelte-native', workspacePath);
- if (svelteNativePkgInfo.path) {
- compilerOptions.jsxFactory = 'svelteNative.createElement';
- }
- } catch (e) {
- //we stay regular svelte
- }
- }
- }
-
- return {
- ...parsedConfig,
- options: compilerOptions
- };
- }
-
- /**
- * 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 ['__sapper__', 'node_modules'];
- }
+ const workspacePath = tsconfigPath ? dirname(tsconfigPath) : '';
+
+ const { options: compilerOptions, fileNames: files, raw } = 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(files, raw, workspacePath || process.cwd());
+
+ // 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 to determine
+ // the default language.
+ await configLoader.loadConfigs(workspacePath);
+
+ const svelteModuleLoader = createSvelteModuleLoader(getSnapshot, compilerOptions);
+
+ let svelteTsPath: string;
+ try {
+ // For when svelte2tsx is part of node_modules, for example VS Code extension
+ svelteTsPath = dirname(require.resolve('svelte2tsx'));
+ } catch (e) {
+ // Fall back to dirname, for example for svelte-check
+ svelteTsPath = __dirname;
+ }
+ const svelteTsxFiles = [
+ './svelte-shims.d.ts',
+ './svelte-jsx.d.ts',
+ './svelte-native-jsx.d.ts'
+ ].map((f) => ts.sys.resolvePath(resolve(svelteTsPath, f)));
+
+ const host: ts.LanguageServiceHost = {
+ getCompilationSettings: () => compilerOptions,
+ getScriptFileNames: () =>
+ Array.from(
+ new Set([
+ ...snapshotManager.getProjectFileNames(),
+ ...snapshotManager.getFileNames(),
+ ...svelteTsxFiles
+ ])
+ ),
+ getScriptVersion: (fileName: string) => getSnapshot(fileName).version.toString(),
+ getScriptSnapshot: getSnapshot,
+ getCurrentDirectory: () => workspacePath,
+ getDefaultLibFileName: ts.getDefaultLibFilePath,
+ fileExists: svelteModuleLoader.fileExists,
+ readFile: svelteModuleLoader.readFile,
+ resolveModuleNames: svelteModuleLoader.resolveModuleNames,
+ readDirectory: svelteModuleLoader.readDirectory,
+ getDirectories: ts.sys.getDirectories,
+ useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
+ getScriptKind: (fileName: string) => getSnapshot(fileName).scriptKind
+ };
+ let languageService = ts.createLanguageService(host);
+ const transformationConfig = {
+ strictMode: !!compilerOptions.strict,
+ transformOnTemplateError: docContext.transformOnTemplateError
+ };
+
+ return {
+ tsconfigPath,
+ compilerOptions,
+ getService: () => languageService,
+ updateDocument,
+ deleteDocument,
+ snapshotManager
+ };
+
+ function deleteDocument(filePath: string): void {
+ svelteModuleLoader.deleteFromModuleCache(filePath);
+ snapshotManager.delete(filePath);
+ }
+
+ function updateDocument(documentOrFilePath: Document | string): DocumentSnapshot {
+ const filePath =
+ typeof documentOrFilePath === 'string'
+ ? documentOrFilePath
+ : documentOrFilePath.getFilePath() || '';
+ const document = typeof documentOrFilePath === 'string' ? undefined : documentOrFilePath;
+ const prevSnapshot = snapshotManager.get(filePath);
+
+ // Don't reinitialize document if no update needed.
+ if (document && prevSnapshot?.version === document.version) {
+ return prevSnapshot;
+ }
+
+ const newSnapshot = document
+ ? DocumentSnapshot.fromDocument(document, transformationConfig)
+ : DocumentSnapshot.fromFilePath(
+ filePath,
+ docContext.createDocument,
+ transformationConfig
+ );
+ snapshotManager.set(filePath, newSnapshot);
+ if (prevSnapshot && prevSnapshot.scriptKind !== newSnapshot.scriptKind) {
+ // Restart language service as it doesn't handle script kind changes.
+ languageService.dispose();
+ languageService = ts.createLanguageService(host);
+ }
+
+ return newSnapshot;
+ }
+
+ function getSnapshot(fileName: string): DocumentSnapshot {
+ fileName = ensureRealSvelteFilePath(fileName);
+
+ let doc = snapshotManager.get(fileName);
+ if (doc) {
+ return doc;
+ }
+
+ doc = DocumentSnapshot.fromFilePath(
+ fileName,
+ docContext.createDocument,
+ transformationConfig
+ );
+ snapshotManager.set(fileName, doc);
+ return doc;
+ }
+
+ function getParsedConfig() {
+ const forcedCompilerOptions: ts.CompilerOptions = {
+ allowNonTsExtensions: true,
+ target: ts.ScriptTarget.Latest,
+ module: ts.ModuleKind.ESNext,
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
+ allowJs: true,
+ noEmit: true,
+ declaration: false,
+ skipLibCheck: true,
+ // these are needed to handle the results of svelte2tsx preprocessing:
+ jsx: ts.JsxEmit.Preserve
+ };
+
+ // always let ts parse config to get default compilerOption
+ let configJson =
+ (tsconfigPath && ts.readConfigFile(tsconfigPath, ts.sys.readFile).config) ||
+ getDefaultJsConfig();
+
+ // Only default exclude when no extends for now
+ if (!configJson.extends) {
+ configJson = Object.assign(
+ {
+ exclude: getDefaultExclude()
+ },
+ configJson
+ );
+ }
+
+ const parsedConfig = ts.parseJsonConfigFileContent(
+ configJson,
+ ts.sys,
+ workspacePath,
+ forcedCompilerOptions,
+ tsconfigPath,
+ undefined,
+ [{ extension: 'svelte', isMixedContent: false, scriptKind: ts.ScriptKind.TSX }]
+ );
+
+ const compilerOptions: ts.CompilerOptions = {
+ ...parsedConfig.options,
+ ...forcedCompilerOptions
+ };
+
+ // detect which JSX namespace to use (svelte | svelteNative) if not specified or not compatible
+ if (!compilerOptions.jsxFactory || !compilerOptions.jsxFactory.startsWith('svelte')) {
+ //default to regular svelte, this causes the usage of the "svelte.JSX" namespace
+ compilerOptions.jsxFactory = 'svelte.createElement';
+
+ //override if we detect svelte-native
+ if (workspacePath) {
+ try {
+ const svelteNativePkgInfo = getPackageInfo('svelte-native', workspacePath);
+ if (svelteNativePkgInfo.path) {
+ compilerOptions.jsxFactory = 'svelteNative.createElement';
+ }
+ } catch (e) {
+ //we stay regular svelte
+ }
+ }
+ }
+
+ return {
+ ...parsedConfig,
+ options: compilerOptions
+ };
+ }
+
+ /**
+ * 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 ['__sapper__', 'node_modules'];
+ }
}
diff --git a/packages/language-server/src/plugins/typescript/svelte-sys.ts b/packages/language-server/src/plugins/typescript/svelte-sys.ts
index 6393c0c30..414e00eb6 100644
--- a/packages/language-server/src/plugins/typescript/svelte-sys.ts
+++ b/packages/language-server/src/plugins/typescript/svelte-sys.ts
@@ -6,31 +6,31 @@ import { ensureRealSvelteFilePath, isVirtualSvelteFilePath, toRealSvelteFilePath
* This should only be accessed by TS svelte module resolution.
*/
export function createSvelteSys(getSnapshot: (fileName: string) => DocumentSnapshot) {
- const svelteSys: ts.System = {
- ...ts.sys,
- fileExists(path: string) {
- return ts.sys.fileExists(ensureRealSvelteFilePath(path));
- },
- readFile(path: string) {
- const snapshot = getSnapshot(path);
- return snapshot.getText(0, snapshot.getLength());
- },
- readDirectory(path, extensions, exclude, include, depth) {
- const extensionsWithSvelte = (extensions ?? []).concat('.svelte');
+ const svelteSys: ts.System = {
+ ...ts.sys,
+ fileExists(path: string) {
+ return ts.sys.fileExists(ensureRealSvelteFilePath(path));
+ },
+ readFile(path: string) {
+ const snapshot = getSnapshot(path);
+ return snapshot.getText(0, snapshot.getLength());
+ },
+ readDirectory(path, extensions, exclude, include, depth) {
+ const extensionsWithSvelte = (extensions ?? []).concat('.svelte');
- return ts.sys.readDirectory(path, extensionsWithSvelte, exclude, include, depth);
- }
- };
+ return ts.sys.readDirectory(path, extensionsWithSvelte, exclude, include, depth);
+ }
+ };
- if (ts.sys.realpath) {
- const realpath = ts.sys.realpath;
- svelteSys.realpath = function (path) {
- if (isVirtualSvelteFilePath(path)) {
- return realpath(toRealSvelteFilePath(path)) + '.ts';
- }
- return realpath(path);
- };
- }
+ 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;
+ return svelteSys;
}
diff --git a/packages/language-server/src/plugins/typescript/utils.ts b/packages/language-server/src/plugins/typescript/utils.ts
index 112adc6cc..231a1a8b5 100644
--- a/packages/language-server/src/plugins/typescript/utils.ts
+++ b/packages/language-server/src/plugins/typescript/utils.ts
@@ -1,264 +1,264 @@
import { dirname } from 'path';
import ts from 'typescript';
import {
- CompletionItemKind,
- DiagnosticSeverity,
- Position,
- Range,
- SymbolKind
+ CompletionItemKind,
+ DiagnosticSeverity,
+ Position,
+ Range,
+ SymbolKind
} from 'vscode-languageserver';
import { mapRangeToOriginal } from '../../lib/documents';
import { pathToUrl } from '../../utils';
import { SnapshotFragment } 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;
- }
+ 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;
- }
+ 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
+ attrs: Record
): ts.ScriptKind.TSX | ts.ScriptKind.JSX {
- const type = attrs.lang || attrs.type;
+ 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;
- }
+ 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');
+ return filePath.endsWith('.svelte');
}
export function isVirtualSvelteFilePath(filePath: string) {
- return filePath.endsWith('.svelte.ts');
+ return filePath.endsWith('.svelte.ts');
}
export function toRealSvelteFilePath(filePath: string) {
- return filePath.slice(0, -'.ts'.length);
+ return filePath.slice(0, -'.ts'.length);
}
export function ensureRealSvelteFilePath(filePath: string) {
- return isVirtualSvelteFilePath(filePath) ? toRealSvelteFilePath(filePath) : filePath;
+ return isVirtualSvelteFilePath(filePath) ? toRealSvelteFilePath(filePath) : filePath;
}
export function convertRange(
- document: { positionAt: (offset: number) => Position },
- range: { start?: number; length?: number }
+ 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))
- );
+ return Range.create(
+ document.positionAt(range.start || 0),
+ document.positionAt((range.start || 0) + (range.length || 0))
+ );
}
export function convertToLocationRange(defDoc: SnapshotFragment, textSpan: ts.TextSpan): Range {
- const range = mapRangeToOriginal(defDoc, convertRange(defDoc, textSpan));
- // Some definition like the svelte component class definition don't exist in the original, so we map to 0,1
- if (range.start.line < 0) {
- range.start.line = 0;
- range.start.character = 1;
- }
- if (range.end.line < 0) {
- range.end = range.start;
- }
+ const range = mapRangeToOriginal(defDoc, convertRange(defDoc, textSpan));
+ // Some definition like the svelte component class definition don't exist in the original, so we map to 0,1
+ if (range.start.line < 0) {
+ range.start.line = 0;
+ range.start.character = 1;
+ }
+ if (range.end.line < 0) {
+ range.end = range.start;
+ }
- return range;
+ return range;
}
export function findTsConfigPath(fileName: string, rootUris: string[]) {
- const searchDir = dirname(fileName);
+ const searchDir = dirname(fileName);
- const path =
- ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json') ||
- ts.findConfigFile(searchDir, ts.sys.fileExists, 'jsconfig.json') ||
- '';
- // Don't return config files that exceed the current workspace context.
- return !!path && rootUris.some((rootUri) => isSubPath(rootUri, path)) ? path : '';
+ const path =
+ ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json') ||
+ ts.findConfigFile(searchDir, ts.sys.fileExists, 'jsconfig.json') ||
+ '';
+ // Don't return config files that exceed the current workspace context.
+ return !!path && rootUris.some((rootUri) => isSubPath(rootUri, path)) ? path : '';
}
export function isSubPath(uri: string, possibleSubPath: string): boolean {
- return pathToUrl(possibleSubPath).startsWith(uri);
+ return pathToUrl(possibleSubPath).startsWith(uri);
}
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;
- }
+ 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
+ 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;
+ 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 getCommitCharactersForScriptElement(
- kind: ts.ScriptElementKind
+ kind: ts.ScriptElementKind
): string[] | undefined {
- const commitCharacters: string[] = [];
- switch (kind) {
- case ts.ScriptElementKind.memberGetAccessorElement:
- case ts.ScriptElementKind.memberSetAccessorElement:
- case ts.ScriptElementKind.constructSignatureElement:
- case ts.ScriptElementKind.callSignatureElement:
- case ts.ScriptElementKind.indexSignatureElement:
- case ts.ScriptElementKind.enumElement:
- case ts.ScriptElementKind.interfaceElement:
- commitCharacters.push('.');
- break;
+ const commitCharacters: string[] = [];
+ switch (kind) {
+ case ts.ScriptElementKind.memberGetAccessorElement:
+ case ts.ScriptElementKind.memberSetAccessorElement:
+ case ts.ScriptElementKind.constructSignatureElement:
+ case ts.ScriptElementKind.callSignatureElement:
+ case ts.ScriptElementKind.indexSignatureElement:
+ case ts.ScriptElementKind.enumElement:
+ case ts.ScriptElementKind.interfaceElement:
+ commitCharacters.push('.');
+ break;
- case ts.ScriptElementKind.moduleElement:
- case ts.ScriptElementKind.alias:
- case ts.ScriptElementKind.constElement:
- case ts.ScriptElementKind.letElement:
- case ts.ScriptElementKind.variableElement:
- case ts.ScriptElementKind.localVariableElement:
- case ts.ScriptElementKind.memberVariableElement:
- case ts.ScriptElementKind.classElement:
- case ts.ScriptElementKind.functionElement:
- case ts.ScriptElementKind.memberFunctionElement:
- commitCharacters.push('.', ',');
- commitCharacters.push('(');
- break;
- }
+ case ts.ScriptElementKind.moduleElement:
+ case ts.ScriptElementKind.alias:
+ case ts.ScriptElementKind.constElement:
+ case ts.ScriptElementKind.letElement:
+ case ts.ScriptElementKind.variableElement:
+ case ts.ScriptElementKind.localVariableElement:
+ case ts.ScriptElementKind.memberVariableElement:
+ case ts.ScriptElementKind.classElement:
+ case ts.ScriptElementKind.functionElement:
+ case ts.ScriptElementKind.memberFunctionElement:
+ commitCharacters.push('.', ',');
+ commitCharacters.push('(');
+ break;
+ }
- return commitCharacters.length === 0 ? undefined : commitCharacters;
+ return commitCharacters.length === 0 ? undefined : commitCharacters;
}
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;
- }
+ 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;
+ return DiagnosticSeverity.Error;
}
// Matches comments that come before any non-comment content
@@ -277,22 +277,22 @@ const tsCheckRegex = /\/\/[ \t\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\
* 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}`;
- }
- }
+ 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, fragment: SnapshotFragment): ts.TextSpan {
- const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
- const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
+ const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
+ const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
- return {
- start,
- length: end - start
- };
+ return {
+ start,
+ length: end - start
+ };
}
diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts
index 41a798a9f..65206dfe4 100644
--- a/packages/language-server/src/server.ts
+++ b/packages/language-server/src/server.ts
@@ -1,22 +1,22 @@
import _ from 'lodash';
import {
- ApplyWorkspaceEditParams,
- ApplyWorkspaceEditRequest,
- CodeActionKind,
- DocumentUri,
- Connection,
- MessageType,
- RenameFile,
- RequestType,
- ShowMessageNotification,
- TextDocumentIdentifier,
- TextDocumentPositionParams,
- TextDocumentSyncKind,
- WorkspaceEdit,
- SemanticTokensRequest,
- SemanticTokensRangeRequest,
- DidChangeWatchedFilesParams,
- LinkedEditingRangeRequest
+ ApplyWorkspaceEditParams,
+ ApplyWorkspaceEditRequest,
+ CodeActionKind,
+ DocumentUri,
+ Connection,
+ MessageType,
+ RenameFile,
+ RequestType,
+ ShowMessageNotification,
+ TextDocumentIdentifier,
+ TextDocumentPositionParams,
+ TextDocumentSyncKind,
+ WorkspaceEdit,
+ SemanticTokensRequest,
+ SemanticTokensRangeRequest,
+ DidChangeWatchedFilesParams,
+ LinkedEditingRangeRequest
} from 'vscode-languageserver';
import { IPCMessageReader, IPCMessageWriter, createConnection } from 'vscode-languageserver/node';
import { DiagnosticsManager } from './lib/DiagnosticsManager';
@@ -25,36 +25,36 @@ import { getSemanticTokenLegends } from './lib/semanticToken/semanticTokenLegend
import { Logger } from './logger';
import { LSConfigManager } from './ls-config';
import {
- AppCompletionItem,
- CSSPlugin,
- HTMLPlugin,
- PluginHost,
- SveltePlugin,
- TypeScriptPlugin,
- OnWatchFileChangesPara
+ AppCompletionItem,
+ CSSPlugin,
+ HTMLPlugin,
+ PluginHost,
+ SveltePlugin,
+ TypeScriptPlugin,
+ OnWatchFileChangesPara
} from './plugins';
import { isNotNullOrUndefined, urlToPath } from './utils';
import { FallbackWatcher } from './lib/FallbackWatcher';
namespace TagCloseRequest {
- export const type: RequestType<
- TextDocumentPositionParams,
- string | null,
- any
- > = new RequestType('html/tag');
+ export const type: RequestType<
+ TextDocumentPositionParams,
+ string | null,
+ any
+ > = 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;
+ /**
+ * 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;
}
/**
@@ -63,325 +63,325 @@ export interface LSOptions {
* @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);
- }
-
- // 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.updateEmmetConfig(
- evt.initializationOptions?.configuration?.emmet ||
- evt.initializationOptions?.emmetConfig ||
- {}
- );
- configManager.updatePrettierConfig(
- evt.initializationOptions?.configuration?.prettier ||
- evt.initializationOptions?.prettierConfig ||
- {}
- );
-
- pluginHost.initialize({
- filterIncompleteCompletions: !evt.initializationOptions
- ?.dontFilterIncompleteCompletions,
- definitionLinkSupport: !!evt.capabilities.textDocument?.definition?.linkSupport
- });
- pluginHost.register((sveltePlugin = new SveltePlugin(configManager)));
- pluginHost.register(new HTMLPlugin(docManager, configManager));
- pluginHost.register(new CSSPlugin(docManager, configManager));
- pluginHost.register(new TypeScriptPlugin(docManager, configManager, workspaceUris));
-
- const clientSupportApplyEditCommand = !!evt.capabilities.workspace?.applyEdit;
-
- 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
- ':'
- ]
- },
- documentFormattingProvider: true,
- colorProvider: true,
- documentSymbolProvider: true,
- definitionProvider: true,
- codeActionProvider: evt.capabilities.textDocument?.codeAction
- ?.codeActionLiteralSupport
- ? {
- codeActionKinds: [
- CodeActionKind.QuickFix,
- CodeActionKind.SourceOrganizeImports,
- ...(clientSupportApplyEditCommand ? [CodeActionKind.Refactor] : [])
- ]
- }
- : 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'
- ]
- }
- : 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
- }
- };
- });
-
- 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.updateEmmetConfig(settings.emmet);
- configManager.updatePrettierConfig(settings.prettier);
- });
-
- connection.onDidOpenTextDocument((evt) => {
- docManager.openDocument(evt.textDocument);
- docManager.markAsOpenedInClient(evt.textDocument.uri);
- });
-
- connection.onDidCloseTextDocument((evt) => docManager.closeDocument(evt.textDocument.uri));
- connection.onDidChangeTextDocument((evt) =>
- docManager.updateDocument(evt.textDocument, evt.contentChanges)
- );
- connection.onHover((evt) => pluginHost.doHover(evt.textDocument, evt.position));
- connection.onCompletion((evt) =>
- pluginHost.getCompletions(evt.textDocument, evt.position, evt.context)
- );
- 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) => pluginHost.getDocumentSymbols(evt.textDocument));
- connection.onDefinition((evt) => pluginHost.getDefinitions(evt.textDocument, evt.position));
- connection.onReferences((evt) =>
- pluginHost.findReferences(evt.textDocument, evt.position, evt.context)
- );
-
- connection.onCodeAction((evt) =>
- pluginHost.getCodeActions(evt.textDocument, evt.range, evt.context)
- );
- 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.onCompletionResolve((completionItem) => {
- const data = (completionItem as AppCompletionItem).data as TextDocumentIdentifier;
-
- if (!data) {
- return completionItem;
- }
-
- return pluginHost.resolveCompletion(data, completionItem);
- });
-
- connection.onSignatureHelp((evt) =>
- pluginHost.getSignatureHelp(evt.textDocument, evt.position, evt.context)
- );
-
- connection.onSelectionRanges((evt) =>
- pluginHost.getSelectionRanges(evt.textDocument, evt.positions)
- );
-
- const diagnosticsManager = new DiagnosticsManager(
- connection.sendDiagnostics,
- docManager,
- pluginHost.getDiagnostics.bind(pluginHost)
- );
-
- const updateAllDiagnostics = _.debounce(() => diagnosticsManager.updateAll(), 1000);
-
- 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);
-
- updateAllDiagnostics();
- }
-
- connection.onDidSaveTextDocument(updateAllDiagnostics);
- connection.onNotification('$/onDidChangeTsOrJsFile', async (e: any) => {
- const path = urlToPath(e.uri);
- if (path) {
- pluginHost.updateTsOrJsFile(path, e.changes);
- }
- updateAllDiagnostics();
- });
-
- connection.onRequest(SemanticTokensRequest.type, (evt) =>
- pluginHost.getSemanticTokens(evt.textDocument)
- );
- connection.onRequest(SemanticTokensRangeRequest.type, (evt) =>
- pluginHost.getSemanticTokens(evt.textDocument, evt.range)
- );
-
- connection.onRequest(
- LinkedEditingRangeRequest.type,
- async (evt) => await pluginHost.getLinkedEditingRanges(evt.textDocument, evt.position)
- );
-
- docManager.on(
- 'documentChange',
- _.debounce(async (document: Document) => diagnosticsManager.update(document), 500)
- );
- 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('$/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();
+ 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);
+ }
+
+ // 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.updateEmmetConfig(
+ evt.initializationOptions?.configuration?.emmet ||
+ evt.initializationOptions?.emmetConfig ||
+ {}
+ );
+ configManager.updatePrettierConfig(
+ evt.initializationOptions?.configuration?.prettier ||
+ evt.initializationOptions?.prettierConfig ||
+ {}
+ );
+
+ pluginHost.initialize({
+ filterIncompleteCompletions: !evt.initializationOptions
+ ?.dontFilterIncompleteCompletions,
+ definitionLinkSupport: !!evt.capabilities.textDocument?.definition?.linkSupport
+ });
+ pluginHost.register((sveltePlugin = new SveltePlugin(configManager)));
+ pluginHost.register(new HTMLPlugin(docManager, configManager));
+ pluginHost.register(new CSSPlugin(docManager, configManager));
+ pluginHost.register(new TypeScriptPlugin(docManager, configManager, workspaceUris));
+
+ const clientSupportApplyEditCommand = !!evt.capabilities.workspace?.applyEdit;
+
+ 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
+ ':'
+ ]
+ },
+ documentFormattingProvider: true,
+ colorProvider: true,
+ documentSymbolProvider: true,
+ definitionProvider: true,
+ codeActionProvider: evt.capabilities.textDocument?.codeAction
+ ?.codeActionLiteralSupport
+ ? {
+ codeActionKinds: [
+ CodeActionKind.QuickFix,
+ CodeActionKind.SourceOrganizeImports,
+ ...(clientSupportApplyEditCommand ? [CodeActionKind.Refactor] : [])
+ ]
+ }
+ : 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'
+ ]
+ }
+ : 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
+ }
+ };
+ });
+
+ 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.updateEmmetConfig(settings.emmet);
+ configManager.updatePrettierConfig(settings.prettier);
+ });
+
+ connection.onDidOpenTextDocument((evt) => {
+ docManager.openDocument(evt.textDocument);
+ docManager.markAsOpenedInClient(evt.textDocument.uri);
+ });
+
+ connection.onDidCloseTextDocument((evt) => docManager.closeDocument(evt.textDocument.uri));
+ connection.onDidChangeTextDocument((evt) =>
+ docManager.updateDocument(evt.textDocument, evt.contentChanges)
+ );
+ connection.onHover((evt) => pluginHost.doHover(evt.textDocument, evt.position));
+ connection.onCompletion((evt) =>
+ pluginHost.getCompletions(evt.textDocument, evt.position, evt.context)
+ );
+ 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) => pluginHost.getDocumentSymbols(evt.textDocument));
+ connection.onDefinition((evt) => pluginHost.getDefinitions(evt.textDocument, evt.position));
+ connection.onReferences((evt) =>
+ pluginHost.findReferences(evt.textDocument, evt.position, evt.context)
+ );
+
+ connection.onCodeAction((evt) =>
+ pluginHost.getCodeActions(evt.textDocument, evt.range, evt.context)
+ );
+ 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.onCompletionResolve((completionItem) => {
+ const data = (completionItem as AppCompletionItem).data as TextDocumentIdentifier;
+
+ if (!data) {
+ return completionItem;
+ }
+
+ return pluginHost.resolveCompletion(data, completionItem);
+ });
+
+ connection.onSignatureHelp((evt) =>
+ pluginHost.getSignatureHelp(evt.textDocument, evt.position, evt.context)
+ );
+
+ connection.onSelectionRanges((evt) =>
+ pluginHost.getSelectionRanges(evt.textDocument, evt.positions)
+ );
+
+ const diagnosticsManager = new DiagnosticsManager(
+ connection.sendDiagnostics,
+ docManager,
+ pluginHost.getDiagnostics.bind(pluginHost)
+ );
+
+ const updateAllDiagnostics = _.debounce(() => diagnosticsManager.updateAll(), 1000);
+
+ 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);
+
+ updateAllDiagnostics();
+ }
+
+ connection.onDidSaveTextDocument(updateAllDiagnostics);
+ connection.onNotification('$/onDidChangeTsOrJsFile', async (e: any) => {
+ const path = urlToPath(e.uri);
+ if (path) {
+ pluginHost.updateTsOrJsFile(path, e.changes);
+ }
+ updateAllDiagnostics();
+ });
+
+ connection.onRequest(SemanticTokensRequest.type, (evt) =>
+ pluginHost.getSemanticTokens(evt.textDocument)
+ );
+ connection.onRequest(SemanticTokensRangeRequest.type, (evt) =>
+ pluginHost.getSemanticTokens(evt.textDocument, evt.range)
+ );
+
+ connection.onRequest(
+ LinkedEditingRangeRequest.type,
+ async (evt) => await pluginHost.getLinkedEditingRanges(evt.textDocument, evt.position)
+ );
+
+ docManager.on(
+ 'documentChange',
+ _.debounce(async (document: Document) => diagnosticsManager.update(document), 500)
+ );
+ 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('$/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
index 13eb2089c..067ae8521 100644
--- a/packages/language-server/src/svelte-check.ts
+++ b/packages/language-server/src/svelte-check.ts
@@ -8,8 +8,8 @@ import { urlToPath, pathToUrl } from './utils';
export type SvelteCheckDiagnosticSource = 'js' | 'css' | 'svelte';
export interface SvelteCheckOptions {
- compilerWarnings?: Record;
- diagnosticSources?: SvelteCheckDiagnosticSource[];
+ compilerWarnings?: Record;
+ diagnosticSources?: SvelteCheckDiagnosticSource[];
}
/**
@@ -17,97 +17,97 @@ export interface SvelteCheckOptions {
* 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 docManager = new DocumentManager(
+ (textDocument) => new Document(textDocument.uri, textDocument.text)
+ );
+ private configManager = new LSConfigManager();
+ private pluginHost = new PluginHost(this.docManager);
- constructor(workspacePath: string, options: SvelteCheckOptions = {}) {
- Logger.setLogErrorsOnly(true);
- this.initialize(workspacePath, options);
- }
+ constructor(workspacePath: string, options: SvelteCheckOptions = {}) {
+ Logger.setLogErrorsOnly(true);
+ this.initialize(workspacePath, options);
+ }
- private initialize(workspacePath: string, options: SvelteCheckOptions) {
- 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')) {
- this.pluginHost.register(new CSSPlugin(this.docManager, this.configManager));
- }
- if (shouldRegister('js')) {
- this.pluginHost.register(
- new TypeScriptPlugin(
- this.docManager,
- this.configManager,
- [pathToUrl(workspacePath)],
- /**isEditor */ false
- )
- );
- }
+ private initialize(workspacePath: string, options: SvelteCheckOptions) {
+ 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')) {
+ this.pluginHost.register(new CSSPlugin(this.docManager, this.configManager));
+ }
+ if (shouldRegister('js')) {
+ this.pluginHost.register(
+ new TypeScriptPlugin(
+ this.docManager,
+ this.configManager,
+ [pathToUrl(workspacePath)],
+ /**isEditor */ false
+ )
+ );
+ }
- function shouldRegister(source: SvelteCheckDiagnosticSource) {
- return !options.diagnosticSources || options.diagnosticSources.includes(source);
- }
- }
+ function shouldRegister(source: SvelteCheckDiagnosticSource) {
+ return !options.diagnosticSources || options.diagnosticSources.includes(source);
+ }
+ }
- /**
- * Creates/updates given document
- *
- * @param doc Text and Uri of the document
- */
- upsertDocument(doc: { text: string; uri: string }) {
- if (doc.uri.endsWith('.ts') || doc.uri.endsWith('.js')) {
- this.pluginHost.updateTsOrJsFile(urlToPath(doc.uri) || '', [
- {
- range: Range.create(
- Position.create(0, 0),
- Position.create(Number.MAX_VALUE, Number.MAX_VALUE)
- ),
- text: doc.text
- }
- ]);
- } else {
- this.docManager.openDocument({
- text: doc.text,
- uri: doc.uri
- });
- this.docManager.markAsOpenedInClient(doc.uri);
- }
- }
+ /**
+ * Creates/updates given document
+ *
+ * @param doc Text and Uri of the document
+ */
+ upsertDocument(doc: { text: string; uri: string }) {
+ if (doc.uri.endsWith('.ts') || doc.uri.endsWith('.js')) {
+ this.pluginHost.updateTsOrJsFile(urlToPath(doc.uri) || '', [
+ {
+ range: Range.create(
+ Position.create(0, 0),
+ Position.create(Number.MAX_VALUE, Number.MAX_VALUE)
+ ),
+ text: doc.text
+ }
+ ]);
+ } else {
+ this.docManager.openDocument({
+ text: doc.text,
+ uri: doc.uri
+ });
+ this.docManager.markAsOpenedInClient(doc.uri);
+ }
+ }
- /**
- * Removes/closes document
- *
- * @param uri Uri of the document
- */
- removeDocument(uri: string) {
- this.docManager.closeDocument(uri);
- this.docManager.releaseDocument(uri);
- }
+ /**
+ * Removes/closes document
+ *
+ * @param uri Uri of the document
+ */
+ removeDocument(uri: string) {
+ this.docManager.closeDocument(uri);
+ this.docManager.releaseDocument(uri);
+ }
- /**
- * Gets the diagnostics for all currently open files.
- */
- async getDiagnostics(): Promise<
- Array<{ filePath: string; text: string; diagnostics: Diagnostic[] }>
- > {
- return await Promise.all(
- this.docManager.getAllOpenedByClient().map(async (doc) => {
- const uri = doc[1].uri;
- const diagnostics = await this.pluginHost.getDiagnostics({ uri });
- return {
- filePath: urlToPath(uri) || '',
- text: this.docManager.get(uri)?.getText() || '',
- diagnostics
- };
- })
- );
- }
+ /**
+ * Gets the diagnostics for all currently open files.
+ */
+ async getDiagnostics(): Promise<
+ Array<{ filePath: string; text: string; diagnostics: Diagnostic[] }>
+ > {
+ return await Promise.all(
+ this.docManager.getAllOpenedByClient().map(async (doc) => {
+ const uri = doc[1].uri;
+ const diagnostics = await this.pluginHost.getDiagnostics({ uri });
+ return {
+ filePath: urlToPath(uri) || '',
+ text: this.docManager.get(uri)?.getText() || '',
+ diagnostics
+ };
+ })
+ );
+ }
}
diff --git a/packages/language-server/src/utils.ts b/packages/language-server/src/utils.ts
index ef0ebbebd..d5951a550 100644
--- a/packages/language-server/src/utils.ts
+++ b/packages/language-server/src/utils.ts
@@ -2,19 +2,19 @@ import { URI } from 'vscode-uri';
import { Position, Range } from 'vscode-languageserver';
export function clamp(num: number, min: number, max: number): number {
- return Math.max(min, Math.min(max, num));
+ 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, '/');
+ 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();
+ return URI.file(path).toString();
}
/**
@@ -23,29 +23,29 @@ export function pathToUrl(path: string) {
* This normalizes them to be the same as the internally generated ones.
*/
export function normalizeUri(uri: string): string {
- return URI.parse(uri).toString();
+ return URI.parse(uri).toString();
}
export function flatten(arr: T[][]): T[] {
- return arr.reduce((all, item) => [...all, ...item], []);
+ return arr.reduce((all, item) => [...all, ...item], []);
}
export function isInRange(range: Range, positionToTest: Position): boolean {
- return (
- isBeforeOrEqualToPosition(range.end, positionToTest) &&
- isBeforeOrEqualToPosition(positionToTest, range.start)
- );
+ return (
+ isBeforeOrEqualToPosition(range.end, positionToTest) &&
+ isBeforeOrEqualToPosition(positionToTest, range.start)
+ );
}
export function isBeforeOrEqualToPosition(position: Position, positionToTest: Position): boolean {
- return (
- positionToTest.line < position.line ||
- (positionToTest.line === position.line && positionToTest.character <= position.character)
- );
+ return (
+ positionToTest.line < position.line ||
+ (positionToTest.line === position.line && positionToTest.character <= position.character)
+ );
}
export function isNotNullOrUndefined(val: T | undefined | null): val is T {
- return val !== undefined && val !== null;
+ return val !== undefined && val !== null;
}
/**
@@ -57,87 +57,87 @@ export function isNotNullOrUndefined(val: T | undefined | null): val is T {
* @param miliseconds Number of miliseconds to debounce
*/
export function debounceSameArg(
- fn: (arg: T) => void,
- shouldCancelPrevious: (newArg: T, prevArg?: T) => boolean,
- miliseconds: number
+ 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);
- };
+ 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);
+ };
}
/**
* 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;
+ 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;
}
/**
* 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;
+ 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
+ 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');
+ 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
+ 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[];
+ const fail = Symbol();
+ return (
+ await Promise.all(
+ array.map(async (item, idx) => ((await predicate(item, idx)) ? item : fail))
+ )
+ ).filter((i) => i !== fail) as T[];
}
diff --git a/packages/language-server/test/lib/documents/Document.test.ts b/packages/language-server/test/lib/documents/Document.test.ts
index cedc2e6e6..396317bce 100644
--- a/packages/language-server/test/lib/documents/Document.test.ts
+++ b/packages/language-server/test/lib/documents/Document.test.ts
@@ -3,134 +3,134 @@ import { Document } from '../../../src/lib/documents';
import { Position } from 'vscode-languageserver';
describe('Document', () => {
- it('gets the correct text', () => {
- const document = new Document('file:///hello.svelte', 'Hello, world!
');
- assert.strictEqual(document.getText(), 'Hello, world!
');
- });
-
- it('sets the text', () => {
- const document = new Document('file:///hello.svelte', 'Hello, world!
');
- document.setText('Hello, svelte!
');
- assert.strictEqual(document.getText(), 'Hello, svelte!
');
- });
-
- it('increments the version on edits', () => {
- const document = new Document('file:///hello.svelte', 'hello');
- assert.strictEqual(document.version, 0);
-
- document.setText('Hello, world!');
- assert.strictEqual(document.version, 1);
- document.update('svelte', 7, 12);
- assert.strictEqual(document.version, 2);
- });
-
- it('recalculates the tag infos on edits', () => {
- const document = new Document('file:///hello.svelte', '');
- assert.deepEqual(document.scriptInfo, {
- content: 'a',
- attributes: {
- lang: 'javascript'
- },
- start: 8,
- end: 9,
- startPos: Position.create(0, 8),
- endPos: Position.create(0, 9),
- container: { start: 0, end: 18 }
- });
- assert.deepEqual(document.styleInfo, {
- content: 'b',
- attributes: {
- lang: 'css'
- },
- start: 25,
- end: 26,
- startPos: Position.create(0, 25),
- endPos: Position.create(0, 26),
- container: { start: 18, end: 34 }
- });
-
- document.setText('');
- assert.deepEqual(document.scriptInfo, {
- content: 'b',
- attributes: {
- lang: 'javascript'
- },
- start: 8,
- end: 9,
- startPos: Position.create(0, 8),
- endPos: Position.create(0, 9),
- container: { start: 0, end: 18 }
- });
- assert.strictEqual(document.styleInfo, null);
- });
-
- it('returns the correct file path', () => {
- const document = new Document('file:///hello.svelte', 'hello');
-
- assert.strictEqual(document.getFilePath(), '/hello.svelte');
- });
-
- it('returns null for non file urls', () => {
- const document = new Document('ftp:///hello.svelte', 'hello');
-
- assert.strictEqual(document.getFilePath(), null);
- });
-
- it('gets the text length', () => {
- const document = new Document('file:///hello.svelte', 'Hello, world!');
- assert.strictEqual(document.getTextLength(), 13);
- });
-
- it('updates the text range', () => {
- const document = new Document('file:///hello.svelte', 'Hello, world!');
- document.update('svelte', 7, 12);
- assert.strictEqual(document.getText(), 'Hello, svelte!');
- });
-
- it('gets the correct position from offset', () => {
- const document = new Document('file:///hello.svelte', 'Hello\nworld\n');
- assert.deepStrictEqual(document.positionAt(1), { line: 0, character: 1 });
- assert.deepStrictEqual(document.positionAt(9), { line: 1, character: 3 });
- assert.deepStrictEqual(document.positionAt(12), { line: 2, character: 0 });
- });
-
- it('gets the correct offset from position', () => {
- const document = new Document('file:///hello.svelte', 'Hello\nworld\n');
- assert.strictEqual(document.offsetAt({ line: 0, character: 1 }), 1);
- assert.strictEqual(document.offsetAt({ line: 1, character: 3 }), 9);
- assert.strictEqual(document.offsetAt({ line: 2, character: 0 }), 12);
- });
-
- it('gets the correct position from offset with CRLF', () => {
- const document = new Document('file:///hello.svelte', 'Hello\r\nworld\r\n');
- assert.deepStrictEqual(document.positionAt(1), { line: 0, character: 1 });
- assert.deepStrictEqual(document.positionAt(10), { line: 1, character: 3 });
- assert.deepStrictEqual(document.positionAt(14), { line: 2, character: 0 });
- });
-
- it('gets the correct offset from position with CRLF', () => {
- const document = new Document('file:///hello.svelte', 'Hello\r\nworld\r\n');
- assert.strictEqual(document.offsetAt({ line: 0, character: 1 }), 1);
- assert.strictEqual(document.offsetAt({ line: 1, character: 3 }), 10);
- assert.strictEqual(document.offsetAt({ line: 2, character: 0 }), 14);
- });
-
- it('limits the position when offset is out of bounds', () => {
- const document = new Document('file:///hello.svelte', 'Hello\nworld\n');
- assert.deepStrictEqual(document.positionAt(20), { line: 2, character: 0 });
- assert.deepStrictEqual(document.positionAt(-1), { line: 0, character: 0 });
- });
-
- it('limits the offset when position is out of bounds', () => {
- const document = new Document('file:///hello.svelte', 'Hello\nworld\n');
- assert.strictEqual(document.offsetAt({ line: 5, character: 0 }), 12);
- assert.strictEqual(document.offsetAt({ line: 1, character: 20 }), 12);
- assert.strictEqual(document.offsetAt({ line: -1, character: 0 }), 0);
- });
-
- it('supports empty contents', () => {
- const document = new Document('file:///hello.svelte', '');
- assert.strictEqual(document.offsetAt({ line: 0, character: 0 }), 0);
- assert.deepStrictEqual(document.positionAt(0), { line: 0, character: 0 });
- });
+ it('gets the correct text', () => {
+ const document = new Document('file:///hello.svelte', 'Hello, world!
');
+ assert.strictEqual(document.getText(), 'Hello, world!
');
+ });
+
+ it('sets the text', () => {
+ const document = new Document('file:///hello.svelte', 'Hello, world!
');
+ document.setText('Hello, svelte!
');
+ assert.strictEqual(document.getText(), 'Hello, svelte!
');
+ });
+
+ it('increments the version on edits', () => {
+ const document = new Document('file:///hello.svelte', 'hello');
+ assert.strictEqual(document.version, 0);
+
+ document.setText('Hello, world!');
+ assert.strictEqual(document.version, 1);
+ document.update('svelte', 7, 12);
+ assert.strictEqual(document.version, 2);
+ });
+
+ it('recalculates the tag infos on edits', () => {
+ const document = new Document('file:///hello.svelte', '');
+ assert.deepEqual(document.scriptInfo, {
+ content: 'a',
+ attributes: {
+ lang: 'javascript'
+ },
+ start: 8,
+ end: 9,
+ startPos: Position.create(0, 8),
+ endPos: Position.create(0, 9),
+ container: { start: 0, end: 18 }
+ });
+ assert.deepEqual(document.styleInfo, {
+ content: 'b',
+ attributes: {
+ lang: 'css'
+ },
+ start: 25,
+ end: 26,
+ startPos: Position.create(0, 25),
+ endPos: Position.create(0, 26),
+ container: { start: 18, end: 34 }
+ });
+
+ document.setText('');
+ assert.deepEqual(document.scriptInfo, {
+ content: 'b',
+ attributes: {
+ lang: 'javascript'
+ },
+ start: 8,
+ end: 9,
+ startPos: Position.create(0, 8),
+ endPos: Position.create(0, 9),
+ container: { start: 0, end: 18 }
+ });
+ assert.strictEqual(document.styleInfo, null);
+ });
+
+ it('returns the correct file path', () => {
+ const document = new Document('file:///hello.svelte', 'hello');
+
+ assert.strictEqual(document.getFilePath(), '/hello.svelte');
+ });
+
+ it('returns null for non file urls', () => {
+ const document = new Document('ftp:///hello.svelte', 'hello');
+
+ assert.strictEqual(document.getFilePath(), null);
+ });
+
+ it('gets the text length', () => {
+ const document = new Document('file:///hello.svelte', 'Hello, world!');
+ assert.strictEqual(document.getTextLength(), 13);
+ });
+
+ it('updates the text range', () => {
+ const document = new Document('file:///hello.svelte', 'Hello, world!');
+ document.update('svelte', 7, 12);
+ assert.strictEqual(document.getText(), 'Hello, svelte!');
+ });
+
+ it('gets the correct position from offset', () => {
+ const document = new Document('file:///hello.svelte', 'Hello\nworld\n');
+ assert.deepStrictEqual(document.positionAt(1), { line: 0, character: 1 });
+ assert.deepStrictEqual(document.positionAt(9), { line: 1, character: 3 });
+ assert.deepStrictEqual(document.positionAt(12), { line: 2, character: 0 });
+ });
+
+ it('gets the correct offset from position', () => {
+ const document = new Document('file:///hello.svelte', 'Hello\nworld\n');
+ assert.strictEqual(document.offsetAt({ line: 0, character: 1 }), 1);
+ assert.strictEqual(document.offsetAt({ line: 1, character: 3 }), 9);
+ assert.strictEqual(document.offsetAt({ line: 2, character: 0 }), 12);
+ });
+
+ it('gets the correct position from offset with CRLF', () => {
+ const document = new Document('file:///hello.svelte', 'Hello\r\nworld\r\n');
+ assert.deepStrictEqual(document.positionAt(1), { line: 0, character: 1 });
+ assert.deepStrictEqual(document.positionAt(10), { line: 1, character: 3 });
+ assert.deepStrictEqual(document.positionAt(14), { line: 2, character: 0 });
+ });
+
+ it('gets the correct offset from position with CRLF', () => {
+ const document = new Document('file:///hello.svelte', 'Hello\r\nworld\r\n');
+ assert.strictEqual(document.offsetAt({ line: 0, character: 1 }), 1);
+ assert.strictEqual(document.offsetAt({ line: 1, character: 3 }), 10);
+ assert.strictEqual(document.offsetAt({ line: 2, character: 0 }), 14);
+ });
+
+ it('limits the position when offset is out of bounds', () => {
+ const document = new Document('file:///hello.svelte', 'Hello\nworld\n');
+ assert.deepStrictEqual(document.positionAt(20), { line: 2, character: 0 });
+ assert.deepStrictEqual(document.positionAt(-1), { line: 0, character: 0 });
+ });
+
+ it('limits the offset when position is out of bounds', () => {
+ const document = new Document('file:///hello.svelte', 'Hello\nworld\n');
+ assert.strictEqual(document.offsetAt({ line: 5, character: 0 }), 12);
+ assert.strictEqual(document.offsetAt({ line: 1, character: 20 }), 12);
+ assert.strictEqual(document.offsetAt({ line: -1, character: 0 }), 0);
+ });
+
+ it('supports empty contents', () => {
+ const document = new Document('file:///hello.svelte', '');
+ assert.strictEqual(document.offsetAt({ line: 0, character: 0 }), 0);
+ assert.deepStrictEqual(document.positionAt(0), { line: 0, character: 0 });
+ });
});
diff --git a/packages/language-server/test/lib/documents/DocumentManager.test.ts b/packages/language-server/test/lib/documents/DocumentManager.test.ts
index 05c737b9c..7ff332a58 100644
--- a/packages/language-server/test/lib/documents/DocumentManager.test.ts
+++ b/packages/language-server/test/lib/documents/DocumentManager.test.ts
@@ -4,80 +4,80 @@ import { TextDocumentItem, Range } from 'vscode-languageserver-types';
import { DocumentManager, Document } from '../../../src/lib/documents';
describe('Document Manager', () => {
- const textDocument: TextDocumentItem = {
- uri: 'file:///hello.svelte',
- version: 0,
- languageId: 'svelte',
- text: 'Hello, world!'
- };
-
- const createTextDocument = (textDocument: Pick) =>
- new Document(textDocument.uri, textDocument.text);
-
- it('opens documents', () => {
- const createDocument = sinon.spy();
- const manager = new DocumentManager(createDocument);
-
- manager.openDocument(textDocument);
-
- sinon.assert.calledOnce(createDocument);
- sinon.assert.calledWith(createDocument.firstCall, textDocument);
- });
-
- it('updates the whole document', () => {
- const document = createTextDocument(textDocument);
- const update = sinon.spy(document, 'update');
- const createDocument = sinon.stub().returns(document);
- const manager = new DocumentManager(createDocument);
-
- manager.openDocument(textDocument);
- manager.updateDocument(textDocument, [{ text: 'New content' }]);
-
- sinon.assert.calledOnce(update);
- sinon.assert.calledWith(update.firstCall, 'New content', 0, textDocument.text.length);
- });
-
- it('updates the parts of the document', () => {
- const document = createTextDocument(textDocument);
- const update = sinon.spy(document, 'update');
- const createDocument = sinon.stub().returns(document);
- const manager = new DocumentManager(createDocument);
-
- manager.openDocument(textDocument);
- manager.updateDocument(textDocument, [
- {
- text: 'svelte',
- range: Range.create(0, 7, 0, 12),
- rangeLength: 5
- },
- {
- text: 'Greetings',
- range: Range.create(0, 0, 0, 5),
- rangeLength: 5
- }
- ]);
-
- sinon.assert.calledTwice(update);
- sinon.assert.calledWith(update.firstCall, 'svelte', 7, 12);
- sinon.assert.calledWith(update.secondCall, 'Greetings', 0, 5);
- });
-
- it("fails to update if document isn't open", () => {
- const manager = new DocumentManager(createTextDocument);
-
- assert.throws(() => manager.updateDocument(textDocument, []));
- });
-
- it('emits a document change event on open and update', () => {
- const manager = new DocumentManager(createTextDocument);
- const cb = sinon.spy();
-
- manager.on('documentChange', cb);
-
- manager.openDocument(textDocument);
- sinon.assert.calledOnce(cb);
-
- manager.updateDocument(textDocument, []);
- sinon.assert.calledTwice(cb);
- });
+ const textDocument: TextDocumentItem = {
+ uri: 'file:///hello.svelte',
+ version: 0,
+ languageId: 'svelte',
+ text: 'Hello, world!'
+ };
+
+ const createTextDocument = (textDocument: Pick) =>
+ new Document(textDocument.uri, textDocument.text);
+
+ it('opens documents', () => {
+ const createDocument = sinon.spy();
+ const manager = new DocumentManager(createDocument);
+
+ manager.openDocument(textDocument);
+
+ sinon.assert.calledOnce(createDocument);
+ sinon.assert.calledWith(createDocument.firstCall, textDocument);
+ });
+
+ it('updates the whole document', () => {
+ const document = createTextDocument(textDocument);
+ const update = sinon.spy(document, 'update');
+ const createDocument = sinon.stub().returns(document);
+ const manager = new DocumentManager(createDocument);
+
+ manager.openDocument(textDocument);
+ manager.updateDocument(textDocument, [{ text: 'New content' }]);
+
+ sinon.assert.calledOnce(update);
+ sinon.assert.calledWith(update.firstCall, 'New content', 0, textDocument.text.length);
+ });
+
+ it('updates the parts of the document', () => {
+ const document = createTextDocument(textDocument);
+ const update = sinon.spy(document, 'update');
+ const createDocument = sinon.stub().returns(document);
+ const manager = new DocumentManager(createDocument);
+
+ manager.openDocument(textDocument);
+ manager.updateDocument(textDocument, [
+ {
+ text: 'svelte',
+ range: Range.create(0, 7, 0, 12),
+ rangeLength: 5
+ },
+ {
+ text: 'Greetings',
+ range: Range.create(0, 0, 0, 5),
+ rangeLength: 5
+ }
+ ]);
+
+ sinon.assert.calledTwice(update);
+ sinon.assert.calledWith(update.firstCall, 'svelte', 7, 12);
+ sinon.assert.calledWith(update.secondCall, 'Greetings', 0, 5);
+ });
+
+ it("fails to update if document isn't open", () => {
+ const manager = new DocumentManager(createTextDocument);
+
+ assert.throws(() => manager.updateDocument(textDocument, []));
+ });
+
+ it('emits a document change event on open and update', () => {
+ const manager = new DocumentManager(createTextDocument);
+ const cb = sinon.spy();
+
+ manager.on('documentChange', cb);
+
+ manager.openDocument(textDocument);
+ sinon.assert.calledOnce(cb);
+
+ manager.updateDocument(textDocument, []);
+ sinon.assert.calledTwice(cb);
+ });
});
diff --git a/packages/language-server/test/lib/documents/DocumentMapper.test.ts b/packages/language-server/test/lib/documents/DocumentMapper.test.ts
index 3c21edb38..a84d5d6df 100644
--- a/packages/language-server/test/lib/documents/DocumentMapper.test.ts
+++ b/packages/language-server/test/lib/documents/DocumentMapper.test.ts
@@ -2,45 +2,45 @@ import * as assert from 'assert';
import { FragmentMapper, positionAt } from '../../../src/lib/documents';
describe('DocumentMapper', () => {
- describe('FragmentMapper', () => {
- function setup(content: string, start: number, end: number) {
- return new FragmentMapper(
- content,
- {
- start,
- end,
- endPos: positionAt(end, content),
- content: content.substring(start, end)
- },
- 'file:///hello.svelte'
- );
- }
+ describe('FragmentMapper', () => {
+ function setup(content: string, start: number, end: number) {
+ return new FragmentMapper(
+ content,
+ {
+ start,
+ end,
+ endPos: positionAt(end, content),
+ content: content.substring(start, end)
+ },
+ 'file:///hello.svelte'
+ );
+ }
- it('isInGenerated works', () => {
- const fragment = setup('Hello, \nworld!', 8, 13);
+ it('isInGenerated works', () => {
+ const fragment = setup('Hello, \nworld!', 8, 13);
- assert.strictEqual(fragment.isInGenerated({ line: 0, character: 0 }), false);
- assert.strictEqual(fragment.isInGenerated({ line: 1, character: 0 }), true);
- assert.strictEqual(fragment.isInGenerated({ line: 1, character: 5 }), true);
- assert.strictEqual(fragment.isInGenerated({ line: 1, character: 6 }), false);
- });
+ assert.strictEqual(fragment.isInGenerated({ line: 0, character: 0 }), false);
+ assert.strictEqual(fragment.isInGenerated({ line: 1, character: 0 }), true);
+ assert.strictEqual(fragment.isInGenerated({ line: 1, character: 5 }), true);
+ assert.strictEqual(fragment.isInGenerated({ line: 1, character: 6 }), false);
+ });
- it('calculates the position in parent', () => {
- const fragment = setup('Hello, \nworld!', 8, 13);
+ it('calculates the position in parent', () => {
+ const fragment = setup('Hello, \nworld!', 8, 13);
- assert.deepStrictEqual(fragment.getOriginalPosition({ line: 0, character: 2 }), {
- line: 1,
- character: 2
- });
- });
+ assert.deepStrictEqual(fragment.getOriginalPosition({ line: 0, character: 2 }), {
+ line: 1,
+ character: 2
+ });
+ });
- it('calculates the position in fragment', () => {
- const fragment = setup('Hello, \nworld!', 8, 13);
+ it('calculates the position in fragment', () => {
+ const fragment = setup('Hello, \nworld!', 8, 13);
- assert.deepStrictEqual(fragment.getGeneratedPosition({ line: 1, character: 2 }), {
- line: 0,
- character: 2
- });
- });
- });
+ assert.deepStrictEqual(fragment.getGeneratedPosition({ line: 1, character: 2 }), {
+ line: 0,
+ character: 2
+ });
+ });
+ });
});
diff --git a/packages/language-server/test/lib/documents/configLoader.test.ts b/packages/language-server/test/lib/documents/configLoader.test.ts
index 676b999ec..288cee74f 100644
--- a/packages/language-server/test/lib/documents/configLoader.test.ts
+++ b/packages/language-server/test/lib/documents/configLoader.test.ts
@@ -4,146 +4,146 @@ import { pathToFileURL, URL } from 'url';
import assert from 'assert';
describe('ConfigLoader', () => {
- function configFrom(path: string) {
- return {
- compilerOptions: {
- dev: true,
- generate: false
- },
- preprocess: pathToFileURL(path).toString()
- };
- }
+ function configFrom(path: string) {
+ return {
+ compilerOptions: {
+ dev: true,
+ generate: false
+ },
+ preprocess: pathToFileURL(path).toString()
+ };
+ }
- async function assertFindsConfig(
- configLoader: ConfigLoader,
- filePath: string,
- configPath: string
- ) {
- filePath = path.join(...filePath.split('/'));
- configPath = path.join(...configPath.split('/'));
- assert.deepStrictEqual(configLoader.getConfig(filePath), configFrom(configPath));
- assert.deepStrictEqual(await configLoader.awaitConfig(filePath), configFrom(configPath));
- }
+ async function assertFindsConfig(
+ configLoader: ConfigLoader,
+ filePath: string,
+ configPath: string
+ ) {
+ filePath = path.join(...filePath.split('/'));
+ configPath = path.join(...configPath.split('/'));
+ assert.deepStrictEqual(configLoader.getConfig(filePath), configFrom(configPath));
+ assert.deepStrictEqual(await configLoader.awaitConfig(filePath), configFrom(configPath));
+ }
- it('should load all config files below and the one inside/above given directory', async () => {
- const configLoader = new ConfigLoader(
- () => ['svelte.config.js', 'below/svelte.config.js'],
- { existsSync: () => true },
- path,
- (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } })
- );
- await configLoader.loadConfigs('/some/path');
+ it('should load all config files below and the one inside/above given directory', async () => {
+ const configLoader = new ConfigLoader(
+ () => ['svelte.config.js', 'below/svelte.config.js'],
+ { existsSync: () => true },
+ path,
+ (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } })
+ );
+ await configLoader.loadConfigs('/some/path');
- assertFindsConfig(configLoader, '/some/path/comp.svelte', '/some/path/svelte.config.js');
- assertFindsConfig(
- configLoader,
- '/some/path/aside/comp.svelte',
- '/some/path/svelte.config.js'
- );
- assertFindsConfig(
- configLoader,
- '/some/path/below/comp.svelte',
- '/some/path/below/svelte.config.js'
- );
- assertFindsConfig(
- configLoader,
- '/some/path/below/further/comp.svelte',
- '/some/path/below/svelte.config.js'
- );
- });
+ assertFindsConfig(configLoader, '/some/path/comp.svelte', '/some/path/svelte.config.js');
+ assertFindsConfig(
+ configLoader,
+ '/some/path/aside/comp.svelte',
+ '/some/path/svelte.config.js'
+ );
+ assertFindsConfig(
+ configLoader,
+ '/some/path/below/comp.svelte',
+ '/some/path/below/svelte.config.js'
+ );
+ assertFindsConfig(
+ configLoader,
+ '/some/path/below/further/comp.svelte',
+ '/some/path/below/svelte.config.js'
+ );
+ });
- it('finds first above if none found inside/below directory', async () => {
- const configLoader = new ConfigLoader(
- () => [],
- {
- existsSync: (p) =>
- typeof p === 'string' && p.endsWith(path.join('some', 'svelte.config.js'))
- },
- path,
- (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } })
- );
- await configLoader.loadConfigs('/some/path');
+ it('finds first above if none found inside/below directory', async () => {
+ const configLoader = new ConfigLoader(
+ () => [],
+ {
+ existsSync: (p) =>
+ typeof p === 'string' && p.endsWith(path.join('some', 'svelte.config.js'))
+ },
+ path,
+ (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } })
+ );
+ await configLoader.loadConfigs('/some/path');
- assertFindsConfig(configLoader, '/some/path/comp.svelte', '/some/svelte.config.js');
- });
+ assertFindsConfig(configLoader, '/some/path/comp.svelte', '/some/svelte.config.js');
+ });
- it('adds fallback if no config found', async () => {
- const configLoader = new ConfigLoader(
- () => [],
- { existsSync: () => false },
- path,
- (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } })
- );
- await configLoader.loadConfigs('/some/path');
+ it('adds fallback if no config found', async () => {
+ const configLoader = new ConfigLoader(
+ () => [],
+ { existsSync: () => false },
+ path,
+ (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } })
+ );
+ await configLoader.loadConfigs('/some/path');
- assert.deepStrictEqual(
- // Can't do the equal-check directly, instead check if it's the expected object props
- // of svelte-preprocess
- Object.keys(configLoader.getConfig('/some/path/comp.svelte')?.preprocess || {}).sort(),
- ['defaultLanguages', 'markup', 'script', 'style'].sort()
- );
- });
+ assert.deepStrictEqual(
+ // Can't do the equal-check directly, instead check if it's the expected object props
+ // of svelte-preprocess
+ Object.keys(configLoader.getConfig('/some/path/comp.svelte')?.preprocess || {}).sort(),
+ ['defaultLanguages', 'markup', 'script', 'style'].sort()
+ );
+ });
- it('will not load config multiple times if config loading started in parallel', async () => {
- let firstGlobCall = true;
- let nrImportCalls = 0;
- const configLoader = new ConfigLoader(
- () => {
- if (firstGlobCall) {
- firstGlobCall = false;
- return ['svelte.config.js'];
- } else {
- return [];
- }
- },
- {
- existsSync: (p) =>
- typeof p === 'string' &&
- p.endsWith(path.join('some', 'path', 'svelte.config.js'))
- },
- path,
- (module: URL) => {
- nrImportCalls++;
- return new Promise((resolve) => {
- setTimeout(() => resolve({ default: { preprocess: module.toString() } }), 500);
- });
- }
- );
- await Promise.all([
- configLoader.loadConfigs('/some/path'),
- configLoader.loadConfigs('/some/path/sub'),
- configLoader.awaitConfig('/some/path/file.svelte')
- ]);
+ it('will not load config multiple times if config loading started in parallel', async () => {
+ let firstGlobCall = true;
+ let nrImportCalls = 0;
+ const configLoader = new ConfigLoader(
+ () => {
+ if (firstGlobCall) {
+ firstGlobCall = false;
+ return ['svelte.config.js'];
+ } else {
+ return [];
+ }
+ },
+ {
+ existsSync: (p) =>
+ typeof p === 'string' &&
+ p.endsWith(path.join('some', 'path', 'svelte.config.js'))
+ },
+ path,
+ (module: URL) => {
+ nrImportCalls++;
+ return new Promise((resolve) => {
+ setTimeout(() => resolve({ default: { preprocess: module.toString() } }), 500);
+ });
+ }
+ );
+ await Promise.all([
+ configLoader.loadConfigs('/some/path'),
+ configLoader.loadConfigs('/some/path/sub'),
+ configLoader.awaitConfig('/some/path/file.svelte')
+ ]);
- assertFindsConfig(configLoader, '/some/path/comp.svelte', '/some/path/svelte.config.js');
- assertFindsConfig(
- configLoader,
- '/some/path/sub/comp.svelte',
- '/some/path/svelte.config.js'
- );
- assert.deepStrictEqual(nrImportCalls, 1);
- });
+ assertFindsConfig(configLoader, '/some/path/comp.svelte', '/some/path/svelte.config.js');
+ assertFindsConfig(
+ configLoader,
+ '/some/path/sub/comp.svelte',
+ '/some/path/svelte.config.js'
+ );
+ assert.deepStrictEqual(nrImportCalls, 1);
+ });
- it('can deal with missing config', () => {
- const configLoader = new ConfigLoader(
- () => [],
- { existsSync: () => false },
- path,
- () => Promise.resolve('unimportant')
- );
- assert.deepStrictEqual(configLoader.getConfig('/some/file.svelte'), undefined);
- });
+ it('can deal with missing config', () => {
+ const configLoader = new ConfigLoader(
+ () => [],
+ { existsSync: () => false },
+ path,
+ () => Promise.resolve('unimportant')
+ );
+ assert.deepStrictEqual(configLoader.getConfig('/some/file.svelte'), undefined);
+ });
- it('should await config', async () => {
- const configLoader = new ConfigLoader(
- () => [],
- { existsSync: () => true },
- path,
- (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } })
- );
- assert.deepStrictEqual(
- await configLoader.awaitConfig(path.join('some', 'file.svelte')),
- configFrom(path.join('some', 'svelte.config.js'))
- );
- });
+ it('should await config', async () => {
+ const configLoader = new ConfigLoader(
+ () => [],
+ { existsSync: () => true },
+ path,
+ (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } })
+ );
+ assert.deepStrictEqual(
+ await configLoader.awaitConfig(path.join('some', 'file.svelte')),
+ configFrom(path.join('some', 'svelte.config.js'))
+ );
+ });
});
diff --git a/packages/language-server/test/lib/documents/parseHtml.test.ts b/packages/language-server/test/lib/documents/parseHtml.test.ts
index 0d6d814a5..32d192801 100644
--- a/packages/language-server/test/lib/documents/parseHtml.test.ts
+++ b/packages/language-server/test/lib/documents/parseHtml.test.ts
@@ -3,74 +3,74 @@ import { HTMLDocument } from 'vscode-html-languageservice';
import { parseHtml } from '../../../src/lib/documents/parseHtml';
describe('parseHtml', () => {
- const testRootElements = (document: HTMLDocument) => {
- assert.deepStrictEqual(
- document.roots.map((r) => r.tag),
- ['Foo', 'style']
- );
- };
+ const testRootElements = (document: HTMLDocument) => {
+ assert.deepStrictEqual(
+ document.roots.map((r) => r.tag),
+ ['Foo', 'style']
+ );
+ };
- it('ignore arrow inside moustache', () => {
- testRootElements(
- parseHtml(
- ` console.log('ya!!!')} />
+ it('ignore arrow inside moustache', () => {
+ testRootElements(
+ parseHtml(
+ ` console.log('ya!!!')} />
`
- )
- );
- });
+ )
+ );
+ });
- it('ignore greater than operator inside moustache', () => {
- testRootElements(
- parseHtml(
- ` 1} />
+ it('ignore greater than operator inside moustache', () => {
+ testRootElements(
+ parseHtml(
+ ` 1} />
`
- )
- );
- });
+ )
+ );
+ });
- it('ignore less than operator inside moustache', () => {
- testRootElements(
- parseHtml(
- `
+ it('ignore less than operator inside moustache', () => {
+ testRootElements(
+ parseHtml(
+ `
`
- )
- );
- });
+ )
+ );
+ });
- it('ignore less than operator inside moustache with tag not self closed', () => {
- testRootElements(
- parseHtml(
- `
+ it('ignore less than operator inside moustache with tag not self closed', () => {
+ testRootElements(
+ parseHtml(
+ `
`
- )
- );
- });
+ )
+ );
+ });
- it('parse baseline html', () => {
- testRootElements(
- parseHtml(
- `
+ it('parse baseline html', () => {
+ testRootElements(
+ parseHtml(
+ `
`
- )
- );
- });
+ )
+ );
+ });
- it('parse baseline html with moustache', () => {
- testRootElements(
- parseHtml(
- `
+ it('parse baseline html with moustache', () => {
+ testRootElements(
+ parseHtml(
+ `
`
- )
- );
- });
+ )
+ );
+ });
- it('parse baseline html with possibly un-closed start tag', () => {
- testRootElements(
- parseHtml(
- ` {
+ testRootElements(
+ parseHtml(
+ ``
- )
- );
- });
+ )
+ );
+ });
});
diff --git a/packages/language-server/test/lib/documents/utils.test.ts b/packages/language-server/test/lib/documents/utils.test.ts
index a76fd97eb..d90eb161c 100644
--- a/packages/language-server/test/lib/documents/utils.test.ts
+++ b/packages/language-server/test/lib/documents/utils.test.ts
@@ -1,167 +1,167 @@
import * as assert from 'assert';
import {
- getLineAtPosition,
- extractStyleTag,
- extractScriptTags,
- updateRelativeImport,
- getWordAt
+ getLineAtPosition,
+ extractStyleTag,
+ extractScriptTags,
+ updateRelativeImport,
+ getWordAt
} from '../../../src/lib/documents/utils';
import { Position } from 'vscode-languageserver';
describe('document/utils', () => {
- describe('extractTag', () => {
- it('supports boolean attributes', () => {
- const extracted = extractStyleTag('');
- assert.deepStrictEqual(extracted?.attributes, { test: 'test' });
- });
+ describe('extractTag', () => {
+ it('supports boolean attributes', () => {
+ const extracted = extractStyleTag('');
+ assert.deepStrictEqual(extracted?.attributes, { test: 'test' });
+ });
- it('supports unquoted attributes', () => {
- const extracted = extractStyleTag('');
- assert.deepStrictEqual(extracted?.attributes, {
- type: 'text/css'
- });
- });
+ it('supports unquoted attributes', () => {
+ const extracted = extractStyleTag('');
+ assert.deepStrictEqual(extracted?.attributes, {
+ type: 'text/css'
+ });
+ });
- it('does not extract style tag inside comment', () => {
- const text = `
+ it('does not extract style tag inside comment', () => {
+ const text = `
bla
`;
- assert.deepStrictEqual(extractStyleTag(text), {
- content: 'p{ color: blue; }',
- attributes: {},
- start: 108,
- end: 125,
- startPos: Position.create(3, 23),
- endPos: Position.create(3, 40),
- container: { start: 101, end: 133 }
- });
- });
+ assert.deepStrictEqual(extractStyleTag(text), {
+ content: 'p{ color: blue; }',
+ attributes: {},
+ start: 108,
+ end: 125,
+ startPos: Position.create(3, 23),
+ endPos: Position.create(3, 40),
+ container: { start: 101, end: 133 }
+ });
+ });
- it('does not extract tags starting with style/script', () => {
- // https://github.com/sveltejs/language-tools/issues/43
- // this would previously match .... due to misconfigured attribute matching regex
- const text = `
+ it('does not extract tags starting with style/script', () => {
+ // https://github.com/sveltejs/language-tools/issues/43
+ // this would previously match .... due to misconfigured attribute matching regex
+ const text = `
p{ color: blue; }
bla
>
`;
- assert.deepStrictEqual(extractStyleTag(text), null);
- });
+ assert.deepStrictEqual(extractStyleTag(text), null);
+ });
- it('is canse sensitive to style/script', () => {
- const text = `
+ it('is canse sensitive to style/script', () => {
+ const text = `
`;
- assert.deepStrictEqual(extractStyleTag(text), null);
- assert.deepStrictEqual(extractScriptTags(text), null);
- });
+ assert.deepStrictEqual(extractStyleTag(text), null);
+ assert.deepStrictEqual(extractScriptTags(text), null);
+ });
- it('only extract attribute until tag ends', () => {
- const text = `
+ it('only extract attribute until tag ends', () => {
+ const text = `
`;
- const extracted = extractScriptTags(text);
- const attributes = extracted?.script?.attributes;
- assert.deepStrictEqual(attributes, { type: 'typescript' });
- });
+ const extracted = extractScriptTags(text);
+ const attributes = extracted?.script?.attributes;
+ assert.deepStrictEqual(attributes, { type: 'typescript' });
+ });
- it('can extract with self-closing component before it', () => {
- const extracted = extractStyleTag('');
- assert.deepStrictEqual(extracted, {
- start: 22,
- end: 22,
- startPos: {
- character: 22,
- line: 0
- },
- endPos: {
- character: 22,
- line: 0
- },
- attributes: {},
- content: '',
- container: {
- end: 30,
- start: 15
- }
- });
- });
+ it('can extract with self-closing component before it', () => {
+ const extracted = extractStyleTag('');
+ assert.deepStrictEqual(extracted, {
+ start: 22,
+ end: 22,
+ startPos: {
+ character: 22,
+ line: 0
+ },
+ endPos: {
+ character: 22,
+ line: 0
+ },
+ attributes: {},
+ content: '',
+ container: {
+ end: 30,
+ start: 15
+ }
+ });
+ });
- it('can extract with unclosed component after it', () => {
- const extracted = extractStyleTag('asd
{/if}');
- assert.deepStrictEqual(extracted, {
- start: 7,
- end: 7,
- startPos: {
- character: 7,
- line: 0
- },
- endPos: {
- character: 7,
- line: 0
- },
- attributes: {},
- content: '',
- container: {
- start: 0,
- end: 15
- }
- });
- });
+ it('can extract with unclosed component after it', () => {
+ const extracted = extractStyleTag('asd{/if}');
+ assert.deepStrictEqual(extracted, {
+ start: 7,
+ end: 7,
+ startPos: {
+ character: 7,
+ line: 0
+ },
+ endPos: {
+ character: 7,
+ line: 0
+ },
+ attributes: {},
+ content: '',
+ container: {
+ start: 0,
+ end: 15
+ }
+ });
+ });
- it('extracts style tag', () => {
- const text = `
+ it('extracts style tag', () => {
+ const text = `
bla
`;
- assert.deepStrictEqual(extractStyleTag(text), {
- content: 'p{ color: blue; }',
- attributes: {},
- start: 51,
- end: 68,
- startPos: Position.create(2, 23),
- endPos: Position.create(2, 40),
- container: { start: 44, end: 76 }
- });
- });
+ assert.deepStrictEqual(extractStyleTag(text), {
+ content: 'p{ color: blue; }',
+ attributes: {},
+ start: 51,
+ end: 68,
+ startPos: Position.create(2, 23),
+ endPos: Position.create(2, 40),
+ container: { start: 44, end: 76 }
+ });
+ });
- it('extracts style tag with attributes', () => {
- const text = `
+ it('extracts style tag with attributes', () => {
+ const text = `
`;
- assert.deepStrictEqual(extractStyleTag(text), {
- content: 'p{ color: blue; }',
- attributes: { lang: 'scss' },
- start: 36,
- end: 53,
- startPos: Position.create(1, 35),
- endPos: Position.create(1, 52),
- container: { start: 17, end: 61 }
- });
- });
+ assert.deepStrictEqual(extractStyleTag(text), {
+ content: 'p{ color: blue; }',
+ attributes: { lang: 'scss' },
+ start: 36,
+ end: 53,
+ startPos: Position.create(1, 35),
+ endPos: Position.create(1, 52),
+ container: { start: 17, end: 61 }
+ });
+ });
- it('extracts style tag with attributes and extra whitespace', () => {
- const text = `
+ it('extracts style tag with attributes and extra whitespace', () => {
+ const text = `
`;
- assert.deepStrictEqual(extractStyleTag(text), {
- content: ' p{ color: blue; } ',
- attributes: { lang: 'scss' },
- start: 44,
- end: 65,
- startPos: Position.create(1, 43),
- endPos: Position.create(1, 64),
- container: { start: 17, end: 73 }
- });
- });
+ assert.deepStrictEqual(extractStyleTag(text), {
+ content: ' p{ color: blue; } ',
+ attributes: { lang: 'scss' },
+ start: 44,
+ end: 65,
+ startPos: Position.create(1, 43),
+ endPos: Position.create(1, 64),
+ container: { start: 17, end: 73 }
+ });
+ });
- it('extracts top level script tag only', () => {
- const text = `
+ it('extracts top level script tag only', () => {
+ const text = `
{#if name}
`;
- assert.deepStrictEqual(extractScriptTags(text)?.script, {
- content: 'top level script',
- attributes: {},
- start: 1243,
- end: 1259,
- startPos: Position.create(34, 24),
- endPos: Position.create(34, 40),
- container: { start: 1235, end: 1268 }
- });
- });
+ assert.deepStrictEqual(extractScriptTags(text)?.script, {
+ content: 'top level script',
+ attributes: {},
+ start: 1243,
+ end: 1259,
+ startPos: Position.create(34, 24),
+ endPos: Position.create(34, 40),
+ container: { start: 1235, end: 1268 }
+ });
+ });
- it('ignores script tag in svelte:head', () => {
- // https://github.com/sveltejs/language-tools/issues/143#issuecomment-636422045
- const text = `
+ it('ignores script tag in svelte:head', () => {
+ // https://github.com/sveltejs/language-tools/issues/143#issuecomment-636422045
+ const text = `
`;
- assert.deepStrictEqual(extractScriptTags(text), {
- moduleScript: {
- attributes: {
- context: 'module'
- },
- container: {
- end: 48,
- start: 13
- },
- content: 'a',
- start: 38,
- end: 39,
- startPos: {
- character: 37,
- line: 1
- },
- endPos: {
- character: 38,
- line: 1
- }
- },
- script: {
- attributes: {},
- container: {
- end: 79,
- start: 61
- },
- content: 'b',
- start: 69,
- end: 70,
- startPos: {
- character: 20,
- line: 2
- },
- endPos: {
- character: 21,
- line: 2
- }
- }
- });
- });
+ assert.deepStrictEqual(extractScriptTags(text), {
+ moduleScript: {
+ attributes: {
+ context: 'module'
+ },
+ container: {
+ end: 48,
+ start: 13
+ },
+ content: 'a',
+ start: 38,
+ end: 39,
+ startPos: {
+ character: 37,
+ line: 1
+ },
+ endPos: {
+ character: 38,
+ line: 1
+ }
+ },
+ script: {
+ attributes: {},
+ container: {
+ end: 79,
+ start: 61
+ },
+ content: 'b',
+ start: 69,
+ end: 70,
+ startPos: {
+ character: 20,
+ line: 2
+ },
+ endPos: {
+ character: 21,
+ line: 2
+ }
+ }
+ });
+ });
- it('extract tag correctly with #if and < operator', () => {
- const text = `
+ it('extract tag correctly with #if and < operator', () => {
+ const text = `
{#if value < 3}
bla
@@ -298,98 +298,98 @@ describe('document/utils', () => {
{:else if value < 4}
{/if}
`;
- assert.deepStrictEqual(extractScriptTags(text)?.script, {
- content: 'let value = 2',
- attributes: {},
- start: 159,
- end: 172,
- startPos: Position.create(7, 18),
- endPos: Position.create(7, 31),
- container: { start: 151, end: 181 }
- });
- });
- });
+ assert.deepStrictEqual(extractScriptTags(text)?.script, {
+ content: 'let value = 2',
+ attributes: {},
+ start: 159,
+ end: 172,
+ startPos: Position.create(7, 18),
+ endPos: Position.create(7, 31),
+ container: { start: 151, end: 181 }
+ });
+ });
+ });
- describe('#getLineAtPosition', () => {
- it('should return line at position (only one line)', () => {
- assert.deepStrictEqual(getLineAtPosition(Position.create(0, 1), 'ABC'), 'ABC');
- });
+ describe('#getLineAtPosition', () => {
+ it('should return line at position (only one line)', () => {
+ assert.deepStrictEqual(getLineAtPosition(Position.create(0, 1), 'ABC'), 'ABC');
+ });
- it('should return line at position (multiple lines)', () => {
- assert.deepStrictEqual(
- getLineAtPosition(Position.create(1, 1), 'ABC\nDEF\nGHI'),
- 'DEF\n'
- );
- });
- });
+ it('should return line at position (multiple lines)', () => {
+ assert.deepStrictEqual(
+ getLineAtPosition(Position.create(1, 1), 'ABC\nDEF\nGHI'),
+ 'DEF\n'
+ );
+ });
+ });
- describe('#updateRelativeImport', () => {
- it('should update path of component with ending', () => {
- const newPath = updateRelativeImport(
- 'C:/absolute/path/oldPath',
- 'C:/absolute/newPath',
- './Component.svelte'
- );
- assert.deepStrictEqual(newPath, '../path/oldPath/Component.svelte');
- });
+ describe('#updateRelativeImport', () => {
+ it('should update path of component with ending', () => {
+ const newPath = updateRelativeImport(
+ 'C:/absolute/path/oldPath',
+ 'C:/absolute/newPath',
+ './Component.svelte'
+ );
+ assert.deepStrictEqual(newPath, '../path/oldPath/Component.svelte');
+ });
- it('should update path of file without ending', () => {
- const newPath = updateRelativeImport(
- 'C:/absolute/path/oldPath',
- 'C:/absolute/newPath',
- './someTsFile'
- );
- assert.deepStrictEqual(newPath, '../path/oldPath/someTsFile');
- });
+ it('should update path of file without ending', () => {
+ const newPath = updateRelativeImport(
+ 'C:/absolute/path/oldPath',
+ 'C:/absolute/newPath',
+ './someTsFile'
+ );
+ assert.deepStrictEqual(newPath, '../path/oldPath/someTsFile');
+ });
- it('should update path of file going one up', () => {
- const newPath = updateRelativeImport(
- 'C:/absolute/path/oldPath',
- 'C:/absolute/path',
- './someTsFile'
- );
- assert.deepStrictEqual(newPath, './oldPath/someTsFile');
- });
- });
+ it('should update path of file going one up', () => {
+ const newPath = updateRelativeImport(
+ 'C:/absolute/path/oldPath',
+ 'C:/absolute/path',
+ './someTsFile'
+ );
+ assert.deepStrictEqual(newPath, './oldPath/someTsFile');
+ });
+ });
- describe('#getWordAt', () => {
- it('returns word between whitespaces', () => {
- assert.equal(getWordAt('qwd asd qwd', 5), 'asd');
- });
+ describe('#getWordAt', () => {
+ it('returns word between whitespaces', () => {
+ assert.equal(getWordAt('qwd asd qwd', 5), 'asd');
+ });
- it('returns word between whitespace and end of string', () => {
- assert.equal(getWordAt('qwd asd', 5), 'asd');
- });
+ it('returns word between whitespace and end of string', () => {
+ assert.equal(getWordAt('qwd asd', 5), 'asd');
+ });
- it('returns word between start of string and whitespace', () => {
- assert.equal(getWordAt('asd qwd', 2), 'asd');
- });
+ it('returns word between start of string and whitespace', () => {
+ assert.equal(getWordAt('asd qwd', 2), 'asd');
+ });
- it('returns word between start of string and end of string', () => {
- assert.equal(getWordAt('asd', 2), 'asd');
- });
+ it('returns word between start of string and end of string', () => {
+ assert.equal(getWordAt('asd', 2), 'asd');
+ });
- it('returns word with custom delimiters', () => {
- assert.equal(
- getWordAt('asd on:asd-qwd="asd" ', 10, { left: /\S+$/, right: /[\s=]/ }),
- 'on:asd-qwd'
- );
- });
+ it('returns word with custom delimiters', () => {
+ assert.equal(
+ getWordAt('asd on:asd-qwd="asd" ', 10, { left: /\S+$/, right: /[\s=]/ }),
+ 'on:asd-qwd'
+ );
+ });
- function testEvent(str: string, pos: number, expected: string) {
- assert.equal(getWordAt(str, pos, { left: /\S+$/, right: /[^\w$:]/ }), expected);
- }
+ function testEvent(str: string, pos: number, expected: string) {
+ assert.equal(getWordAt(str, pos, { left: /\S+$/, right: /[^\w$:]/ }), expected);
+ }
- it('returns event #1', () => {
- testEvent('', 8, 'on:');
- });
+ it('returns event #1', () => {
+ testEvent('
', 8, 'on:');
+ });
- it('returns event #2', () => {
- testEvent('
', 8, 'on:');
- });
+ it('returns event #2', () => {
+ testEvent('
', 8, 'on:');
+ });
- it('returns empty string when only whitespace', () => {
- assert.equal(getWordAt('a a', 2), '');
- });
- });
+ it('returns empty string when only whitespace', () => {
+ assert.equal(getWordAt('a a', 2), '');
+ });
+ });
});
diff --git a/packages/language-server/test/plugins/PluginHost.test.ts b/packages/language-server/test/plugins/PluginHost.test.ts
index bfe733a35..8c0fd5c3e 100644
--- a/packages/language-server/test/plugins/PluginHost.test.ts
+++ b/packages/language-server/test/plugins/PluginHost.test.ts
@@ -1,11 +1,11 @@
import sinon from 'sinon';
import {
- CompletionItem,
- Location,
- LocationLink,
- Position,
- Range,
- TextDocumentItem
+ CompletionItem,
+ Location,
+ LocationLink,
+ Position,
+ Range,
+ TextDocumentItem
} from 'vscode-languageserver-types';
import { DocumentManager, Document } from '../../src/lib/documents';
import { LSPProviderConfig, PluginHost } from '../../src/plugins';
@@ -13,171 +13,171 @@ import { CompletionTriggerKind } from 'vscode-languageserver';
import assert from 'assert';
describe('PluginHost', () => {
- const textDocument: TextDocumentItem = {
- uri: 'file:///hello.svelte',
- version: 0,
- languageId: 'svelte',
- text: 'Hello, world!'
- };
-
- function setup
(
- pluginProviderStubs: T,
- config: LSPProviderConfig = {
- definitionLinkSupport: true,
- filterIncompleteCompletions: false
- }
- ) {
- const docManager = new DocumentManager(
- (textDocument) => new Document(textDocument.uri, textDocument.text)
- );
-
- const pluginHost = new PluginHost(docManager);
- const plugin = {
- ...pluginProviderStubs
- };
-
- pluginHost.initialize(config);
- pluginHost.register(plugin);
-
- return { docManager, pluginHost, plugin };
- }
-
- it('executes getDiagnostics on plugins', async () => {
- const { docManager, pluginHost, plugin } = setup({
- getDiagnostics: sinon.stub().returns([])
- });
- const document = docManager.openDocument(textDocument);
-
- await pluginHost.getDiagnostics(textDocument);
-
- sinon.assert.calledOnce(plugin.getDiagnostics);
- sinon.assert.calledWithExactly(plugin.getDiagnostics, document);
- });
-
- it('executes doHover on plugins', async () => {
- const { docManager, pluginHost, plugin } = setup({
- doHover: sinon.stub().returns(null)
- });
- const document = docManager.openDocument(textDocument);
- const pos = Position.create(0, 0);
-
- await pluginHost.doHover(textDocument, pos);
-
- sinon.assert.calledOnce(plugin.doHover);
- sinon.assert.calledWithExactly(plugin.doHover, document, pos);
- });
-
- it('executes getCompletions on plugins', async () => {
- const { docManager, pluginHost, plugin } = setup({
- getCompletions: sinon.stub().returns({ items: [] })
- });
- const document = docManager.openDocument(textDocument);
- const pos = Position.create(0, 0);
-
- await pluginHost.getCompletions(textDocument, pos, {
- triggerKind: CompletionTriggerKind.TriggerCharacter,
- triggerCharacter: '.'
- });
-
- sinon.assert.calledOnce(plugin.getCompletions);
- sinon.assert.calledWithExactly(plugin.getCompletions, document, pos, {
- triggerKind: CompletionTriggerKind.TriggerCharacter,
- triggerCharacter: '.'
- });
- });
-
- describe('getCompletions (incomplete)', () => {
- function setupGetIncompleteCompletions(filterServerSide: boolean) {
- const { docManager, pluginHost } = setup(
- {
- getCompletions: sinon.stub().returns({
- isIncomplete: true,
- items: [{ label: 'Hello' }, { label: 'foo' }]
- })
- },
- { definitionLinkSupport: true, filterIncompleteCompletions: filterServerSide }
- );
- docManager.openDocument(textDocument);
- return pluginHost;
- }
-
- it('filters client side', async () => {
- const pluginHost = setupGetIncompleteCompletions(false);
- const completions = await pluginHost.getCompletions(
- textDocument,
- Position.create(0, 2)
- );
-
- assert.deepStrictEqual(completions.items, [
- { label: 'Hello' },
- { label: 'foo' }
- ]);
- });
-
- it('filters server side', async () => {
- const pluginHost = setupGetIncompleteCompletions(true);
- const completions = await pluginHost.getCompletions(
- textDocument,
- Position.create(0, 2)
- );
-
- assert.deepStrictEqual(completions.items, [{ label: 'Hello' }]);
- });
- });
-
- describe('getDefinitions', () => {
- function setupGetDefinitions(linkSupport: boolean) {
- const { pluginHost, docManager } = setup(
- {
- getDefinitions: sinon.stub().returns([
- {
- targetRange: Range.create(Position.create(0, 0), Position.create(0, 2)),
- targetSelectionRange: Range.create(
- Position.create(0, 0),
- Position.create(0, 1)
- ),
- targetUri: 'uri'
- }
- ])
- },
- { definitionLinkSupport: linkSupport, filterIncompleteCompletions: false }
- );
- docManager.openDocument(textDocument);
- return pluginHost;
- }
-
- it('uses LocationLink', async () => {
- const pluginHost = setupGetDefinitions(true);
- const definitions = await pluginHost.getDefinitions(
- textDocument,
- Position.create(0, 0)
- );
-
- assert.deepStrictEqual(definitions, [
- {
- targetRange: Range.create(Position.create(0, 0), Position.create(0, 2)),
- targetSelectionRange: Range.create(
- Position.create(0, 0),
- Position.create(0, 1)
- ),
- targetUri: 'uri'
- }
- ]);
- });
-
- it('uses Location', async () => {
- const pluginHost = setupGetDefinitions(false);
- const definitions = await pluginHost.getDefinitions(
- textDocument,
- Position.create(0, 0)
- );
-
- assert.deepStrictEqual(definitions, [
- {
- range: Range.create(Position.create(0, 0), Position.create(0, 1)),
- uri: 'uri'
- }
- ]);
- });
- });
+ const textDocument: TextDocumentItem = {
+ uri: 'file:///hello.svelte',
+ version: 0,
+ languageId: 'svelte',
+ text: 'Hello, world!'
+ };
+
+ function setup(
+ pluginProviderStubs: T,
+ config: LSPProviderConfig = {
+ definitionLinkSupport: true,
+ filterIncompleteCompletions: false
+ }
+ ) {
+ const docManager = new DocumentManager(
+ (textDocument) => new Document(textDocument.uri, textDocument.text)
+ );
+
+ const pluginHost = new PluginHost(docManager);
+ const plugin = {
+ ...pluginProviderStubs
+ };
+
+ pluginHost.initialize(config);
+ pluginHost.register(plugin);
+
+ return { docManager, pluginHost, plugin };
+ }
+
+ it('executes getDiagnostics on plugins', async () => {
+ const { docManager, pluginHost, plugin } = setup({
+ getDiagnostics: sinon.stub().returns([])
+ });
+ const document = docManager.openDocument(textDocument);
+
+ await pluginHost.getDiagnostics(textDocument);
+
+ sinon.assert.calledOnce(plugin.getDiagnostics);
+ sinon.assert.calledWithExactly(plugin.getDiagnostics, document);
+ });
+
+ it('executes doHover on plugins', async () => {
+ const { docManager, pluginHost, plugin } = setup({
+ doHover: sinon.stub().returns(null)
+ });
+ const document = docManager.openDocument(textDocument);
+ const pos = Position.create(0, 0);
+
+ await pluginHost.doHover(textDocument, pos);
+
+ sinon.assert.calledOnce(plugin.doHover);
+ sinon.assert.calledWithExactly(plugin.doHover, document, pos);
+ });
+
+ it('executes getCompletions on plugins', async () => {
+ const { docManager, pluginHost, plugin } = setup({
+ getCompletions: sinon.stub().returns({ items: [] })
+ });
+ const document = docManager.openDocument(textDocument);
+ const pos = Position.create(0, 0);
+
+ await pluginHost.getCompletions(textDocument, pos, {
+ triggerKind: CompletionTriggerKind.TriggerCharacter,
+ triggerCharacter: '.'
+ });
+
+ sinon.assert.calledOnce(plugin.getCompletions);
+ sinon.assert.calledWithExactly(plugin.getCompletions, document, pos, {
+ triggerKind: CompletionTriggerKind.TriggerCharacter,
+ triggerCharacter: '.'
+ });
+ });
+
+ describe('getCompletions (incomplete)', () => {
+ function setupGetIncompleteCompletions(filterServerSide: boolean) {
+ const { docManager, pluginHost } = setup(
+ {
+ getCompletions: sinon.stub().returns({
+ isIncomplete: true,
+ items: [{ label: 'Hello' }, { label: 'foo' }]
+ })
+ },
+ { definitionLinkSupport: true, filterIncompleteCompletions: filterServerSide }
+ );
+ docManager.openDocument(textDocument);
+ return pluginHost;
+ }
+
+ it('filters client side', async () => {
+ const pluginHost = setupGetIncompleteCompletions(false);
+ const completions = await pluginHost.getCompletions(
+ textDocument,
+ Position.create(0, 2)
+ );
+
+ assert.deepStrictEqual(completions.items, [
+ { label: 'Hello' },
+ { label: 'foo' }
+ ]);
+ });
+
+ it('filters server side', async () => {
+ const pluginHost = setupGetIncompleteCompletions(true);
+ const completions = await pluginHost.getCompletions(
+ textDocument,
+ Position.create(0, 2)
+ );
+
+ assert.deepStrictEqual(completions.items, [{ label: 'Hello' }]);
+ });
+ });
+
+ describe('getDefinitions', () => {
+ function setupGetDefinitions(linkSupport: boolean) {
+ const { pluginHost, docManager } = setup(
+ {
+ getDefinitions: sinon.stub().returns([
+ {
+ targetRange: Range.create(Position.create(0, 0), Position.create(0, 2)),
+ targetSelectionRange: Range.create(
+ Position.create(0, 0),
+ Position.create(0, 1)
+ ),
+ targetUri: 'uri'
+ }
+ ])
+ },
+ { definitionLinkSupport: linkSupport, filterIncompleteCompletions: false }
+ );
+ docManager.openDocument(textDocument);
+ return pluginHost;
+ }
+
+ it('uses LocationLink', async () => {
+ const pluginHost = setupGetDefinitions(true);
+ const definitions = await pluginHost.getDefinitions(
+ textDocument,
+ Position.create(0, 0)
+ );
+
+ assert.deepStrictEqual(definitions, [
+ {
+ targetRange: Range.create(Position.create(0, 0), Position.create(0, 2)),
+ targetSelectionRange: Range.create(
+ Position.create(0, 0),
+ Position.create(0, 1)
+ ),
+ targetUri: 'uri'
+ }
+ ]);
+ });
+
+ it('uses Location', async () => {
+ const pluginHost = setupGetDefinitions(false);
+ const definitions = await pluginHost.getDefinitions(
+ textDocument,
+ Position.create(0, 0)
+ );
+
+ assert.deepStrictEqual(definitions, [
+ {
+ range: Range.create(Position.create(0, 0), Position.create(0, 1)),
+ uri: 'uri'
+ }
+ ]);
+ });
+ });
});
diff --git a/packages/language-server/test/plugins/css/CSSPlugin.test.ts b/packages/language-server/test/plugins/css/CSSPlugin.test.ts
index c29cc1695..759ea2a0f 100644
--- a/packages/language-server/test/plugins/css/CSSPlugin.test.ts
+++ b/packages/language-server/test/plugins/css/CSSPlugin.test.ts
@@ -1,411 +1,411 @@
import * as assert from 'assert';
import {
- Range,
- Position,
- Hover,
- CompletionItem,
- CompletionItemKind,
- TextEdit,
- CompletionContext,
- SelectionRange,
- CompletionTriggerKind
+ Range,
+ Position,
+ Hover,
+ CompletionItem,
+ CompletionItemKind,
+ TextEdit,
+ CompletionContext,
+ SelectionRange,
+ CompletionTriggerKind
} from 'vscode-languageserver';
import { DocumentManager, Document } from '../../../src/lib/documents';
import { CSSPlugin } from '../../../src/plugins';
import { LSConfigManager } from '../../../src/ls-config';
describe('CSS Plugin', () => {
- function setup(content: string) {
- const document = new Document('file:///hello.svelte', content);
- const docManager = new DocumentManager(() => document);
- const pluginManager = new LSConfigManager();
- const plugin = new CSSPlugin(docManager, pluginManager);
- docManager.openDocument('some doc');
- return { plugin, document };
- }
-
- describe('provides hover info', () => {
- it('for normal css', () => {
- const { plugin, document } = setup('');
-
- assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 8)), {
- contents: [
- { language: 'html', value: '' },
- '[Selector Specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity): (0, 0, 1)'
- ],
- range: Range.create(0, 7, 0, 9)
- });
-
- assert.strictEqual(plugin.doHover(document, Position.create(0, 10)), null);
- });
-
- it('not for SASS', () => {
- const { plugin, document } = setup('');
- assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 20)), null);
- });
-
- it('not for stylus', () => {
- const { plugin, document } = setup('');
- assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 22)), null);
- });
-
- it('for style attribute', () => {
- const { plugin, document } = setup('
');
- assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 13)), {
- contents: {
- kind: 'markdown',
- value:
- 'Specifies the height of the content area,' +
- " padding area or border area \\(depending on 'box\\-sizing'\\)" +
- ' of certain boxes\\.\n' +
- '\nSyntax: <viewport\\-length>\\{1,2\\}\n\n' +
- '[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/height)'
- },
- range: Range.create(0, 12, 0, 24)
- });
- });
-
- it('not for style attribute with interpolation', () => {
- const { plugin, document } = setup('');
- assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 13)), null);
- });
- });
-
- describe('provides completions', () => {
- it('for normal css', () => {
- const { plugin, document } = setup('');
-
- const completions = plugin.getCompletions(document, Position.create(0, 7), {
- triggerCharacter: '.'
- } as CompletionContext);
- assert.ok(
- Array.isArray(completions && completions.items),
- 'Expected completion items to be an array'
- );
- assert.ok(completions!.items.length > 0, 'Expected completions to have length');
-
- assert.deepStrictEqual(completions!.items[0], {
- label: '@charset',
- kind: CompletionItemKind.Keyword,
- documentation: {
- kind: 'markdown',
- value:
- 'Defines character set of the document\\.\n\n[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@charset)'
- },
- textEdit: TextEdit.insert(Position.create(0, 7), '@charset'),
- tags: []
- });
- });
-
- it('for :global modifier', () => {
- const { plugin, document } = setup('');
-
- const completions = plugin.getCompletions(document, Position.create(0, 9), {
- triggerCharacter: ':'
- } as CompletionContext);
- const globalCompletion = completions?.items.find((item) => item.label === ':global()');
- assert.ok(globalCompletion);
- });
-
- it('not for stylus', () => {
- const { plugin, document } = setup('');
- const completions = plugin.getCompletions(document, Position.create(0, 21), {
- triggerCharacter: '.'
- } as CompletionContext);
- assert.deepStrictEqual(completions, null);
- });
-
- it('for style attribute', () => {
- const { plugin, document } = setup('');
- const completions = plugin.getCompletions(document, Position.create(0, 22), {
- triggerKind: CompletionTriggerKind.Invoked
- } as CompletionContext);
- assert.deepStrictEqual(
- completions?.items.find((item) => item.label === 'none'),
- {
- insertTextFormat: undefined,
- kind: 12,
- label: 'none',
- documentation: {
- kind: 'markdown',
- value: 'The element and its descendants generates no boxes\\.'
- },
- sortText: ' ',
- tags: [],
- textEdit: {
- newText: 'none',
- range: {
- start: {
- line: 0,
- character: 21
- },
- end: {
- line: 0,
- character: 22
- }
- }
- }
- }
- );
- });
-
- it('not for style attribute with interpolation', () => {
- const { plugin, document } = setup('');
- assert.deepStrictEqual(plugin.getCompletions(document, Position.create(0, 21)), null);
- });
- });
-
- describe('provides diagnostics', () => {
- it('- everything ok', () => {
- const { plugin, document } = setup('');
-
- const diagnostics = plugin.getDiagnostics(document);
-
- assert.deepStrictEqual(diagnostics, []);
- });
-
- it('- has error', () => {
- const { plugin, document } = setup('');
-
- const diagnostics = plugin.getDiagnostics(document);
-
- assert.deepStrictEqual(diagnostics, [
- {
- code: 'unknownProperties',
- message: "Unknown property: 'iDunnoDisProperty'",
- range: {
- end: {
- character: 28,
- line: 0
- },
- start: {
- character: 11,
- line: 0
- }
- },
- severity: 2,
- source: 'css'
- }
- ]);
- });
-
- it('- no diagnostics for sass', () => {
- const { plugin, document } = setup(
- `');
+
+ assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 8)), {
+ contents: [
+ { language: 'html', value: '' },
+ '[Selector Specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity): (0, 0, 1)'
+ ],
+ range: Range.create(0, 7, 0, 9)
+ });
+
+ assert.strictEqual(plugin.doHover(document, Position.create(0, 10)), null);
+ });
+
+ it('not for SASS', () => {
+ const { plugin, document } = setup('');
+ assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 20)), null);
+ });
+
+ it('not for stylus', () => {
+ const { plugin, document } = setup('');
+ assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 22)), null);
+ });
+
+ it('for style attribute', () => {
+ const { plugin, document } = setup('
');
+ assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 13)), {
+ contents: {
+ kind: 'markdown',
+ value:
+ 'Specifies the height of the content area,' +
+ " padding area or border area \\(depending on 'box\\-sizing'\\)" +
+ ' of certain boxes\\.\n' +
+ '\nSyntax: <viewport\\-length>\\{1,2\\}\n\n' +
+ '[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/height)'
+ },
+ range: Range.create(0, 12, 0, 24)
+ });
+ });
+
+ it('not for style attribute with interpolation', () => {
+ const { plugin, document } = setup('');
+ assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 13)), null);
+ });
+ });
+
+ describe('provides completions', () => {
+ it('for normal css', () => {
+ const { plugin, document } = setup('');
+
+ const completions = plugin.getCompletions(document, Position.create(0, 7), {
+ triggerCharacter: '.'
+ } as CompletionContext);
+ assert.ok(
+ Array.isArray(completions && completions.items),
+ 'Expected completion items to be an array'
+ );
+ assert.ok(completions!.items.length > 0, 'Expected completions to have length');
+
+ assert.deepStrictEqual(completions!.items[0], {
+ label: '@charset',
+ kind: CompletionItemKind.Keyword,
+ documentation: {
+ kind: 'markdown',
+ value:
+ 'Defines character set of the document\\.\n\n[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@charset)'
+ },
+ textEdit: TextEdit.insert(Position.create(0, 7), '@charset'),
+ tags: []
+ });
+ });
+
+ it('for :global modifier', () => {
+ const { plugin, document } = setup('');
+
+ const completions = plugin.getCompletions(document, Position.create(0, 9), {
+ triggerCharacter: ':'
+ } as CompletionContext);
+ const globalCompletion = completions?.items.find((item) => item.label === ':global()');
+ assert.ok(globalCompletion);
+ });
+
+ it('not for stylus', () => {
+ const { plugin, document } = setup('');
+ const completions = plugin.getCompletions(document, Position.create(0, 21), {
+ triggerCharacter: '.'
+ } as CompletionContext);
+ assert.deepStrictEqual(completions, null);
+ });
+
+ it('for style attribute', () => {
+ const { plugin, document } = setup('');
+ const completions = plugin.getCompletions(document, Position.create(0, 22), {
+ triggerKind: CompletionTriggerKind.Invoked
+ } as CompletionContext);
+ assert.deepStrictEqual(
+ completions?.items.find((item) => item.label === 'none'),
+ {
+ insertTextFormat: undefined,
+ kind: 12,
+ label: 'none',
+ documentation: {
+ kind: 'markdown',
+ value: 'The element and its descendants generates no boxes\\.'
+ },
+ sortText: ' ',
+ tags: [],
+ textEdit: {
+ newText: 'none',
+ range: {
+ start: {
+ line: 0,
+ character: 21
+ },
+ end: {
+ line: 0,
+ character: 22
+ }
+ }
+ }
+ }
+ );
+ });
+
+ it('not for style attribute with interpolation', () => {
+ const { plugin, document } = setup('');
+ assert.deepStrictEqual(plugin.getCompletions(document, Position.create(0, 21)), null);
+ });
+ });
+
+ describe('provides diagnostics', () => {
+ it('- everything ok', () => {
+ const { plugin, document } = setup('');
+
+ const diagnostics = plugin.getDiagnostics(document);
+
+ assert.deepStrictEqual(diagnostics, []);
+ });
+
+ it('- has error', () => {
+ const { plugin, document } = setup('');
+
+ const diagnostics = plugin.getDiagnostics(document);
+
+ assert.deepStrictEqual(diagnostics, [
+ {
+ code: 'unknownProperties',
+ message: "Unknown property: 'iDunnoDisProperty'",
+ range: {
+ end: {
+ character: 28,
+ line: 0
+ },
+ start: {
+ character: 11,
+ line: 0
+ }
+ },
+ severity: 2,
+ source: 'css'
+ }
+ ]);
+ });
+
+ it('- no diagnostics for sass', () => {
+ const { plugin, document } = setup(
+ ``
- );
- const diagnostics = plugin.getDiagnostics(document);
- assert.deepStrictEqual(diagnostics, []);
- });
-
- it('- no diagnostics for stylus', () => {
- const { plugin, document } = setup(
- ``
- );
- const diagnostics = plugin.getDiagnostics(document);
- assert.deepStrictEqual(diagnostics, []);
- });
- });
-
- describe('provides document colors', () => {
- it('for normal css', () => {
- const { plugin, document } = setup('');
-
- const colors = plugin.getColorPresentations(
- document,
- {
- start: { line: 0, character: 17 },
- end: { line: 0, character: 21 }
- },
- { alpha: 1, blue: 255, green: 0, red: 0 }
- );
-
- assert.deepStrictEqual(colors, [
- {
- label: 'rgb(0, 0, 65025)',
- textEdit: {
- range: {
- end: {
- character: 21,
- line: 0
- },
- start: {
- character: 17,
- line: 0
- }
- },
- newText: 'rgb(0, 0, 65025)'
- }
- },
- {
- label: '#00000fe01',
- textEdit: {
- range: {
- end: {
- character: 21,
- line: 0
- },
- start: {
- character: 17,
- line: 0
- }
- },
- newText: '#00000fe01'
- }
- },
- {
- label: 'hsl(240, -101%, 12750%)',
- textEdit: {
- range: {
- end: {
- character: 21,
- line: 0
- },
- start: {
- character: 17,
- line: 0
- }
- },
- newText: 'hsl(240, -101%, 12750%)'
- }
- }
- ]);
- });
-
- it('not for SASS', () => {
- const { plugin, document } = setup(`');
+
+ const colors = plugin.getColorPresentations(
+ document,
+ {
+ start: { line: 0, character: 17 },
+ end: { line: 0, character: 21 }
+ },
+ { alpha: 1, blue: 255, green: 0, red: 0 }
+ );
+
+ assert.deepStrictEqual(colors, [
+ {
+ label: 'rgb(0, 0, 65025)',
+ textEdit: {
+ range: {
+ end: {
+ character: 21,
+ line: 0
+ },
+ start: {
+ character: 17,
+ line: 0
+ }
+ },
+ newText: 'rgb(0, 0, 65025)'
+ }
+ },
+ {
+ label: '#00000fe01',
+ textEdit: {
+ range: {
+ end: {
+ character: 21,
+ line: 0
+ },
+ start: {
+ character: 17,
+ line: 0
+ }
+ },
+ newText: '#00000fe01'
+ }
+ },
+ {
+ label: 'hsl(240, -101%, 12750%)',
+ textEdit: {
+ range: {
+ end: {
+ character: 21,
+ line: 0
+ },
+ start: {
+ character: 17,
+ line: 0
+ }
+ },
+ newText: 'hsl(240, -101%, 12750%)'
+ }
+ }
+ ]);
+ });
+
+ it('not for SASS', () => {
+ const { plugin, document } = setup(``);
- assert.deepStrictEqual(
- plugin.getColorPresentations(
- document,
- {
- start: { line: 2, character: 22 },
- end: { line: 2, character: 26 }
- },
- { alpha: 1, blue: 255, green: 0, red: 0 }
- ),
- []
- );
- assert.deepStrictEqual(plugin.getDocumentColors(document), []);
- });
-
- it('not for stylus', () => {
- const { plugin, document } = setup(``);
- assert.deepStrictEqual(
- plugin.getColorPresentations(
- document,
- {
- start: { line: 2, character: 22 },
- end: { line: 2, character: 26 }
- },
- { alpha: 1, blue: 255, green: 0, red: 0 }
- ),
- []
- );
- assert.deepStrictEqual(plugin.getDocumentColors(document), []);
- });
- });
-
- describe('provides document symbols', () => {
- it('for normal css', () => {
- const { plugin, document } = setup('');
-
- const symbols = plugin.getDocumentSymbols(document);
-
- assert.deepStrictEqual(symbols, [
- {
- containerName: 'style',
- kind: 5,
- location: {
- range: {
- end: {
- character: 23,
- line: 0
- },
- start: {
- character: 7,
- line: 0
- }
- },
- uri: 'file:///hello.svelte'
- },
- name: 'h1'
- }
- ]);
- });
-
- it('not for SASS', () => {
- const { plugin, document } = setup('');
- assert.deepStrictEqual(plugin.getDocumentSymbols(document), []);
- });
-
- it('not for stylus', () => {
- const { plugin, document } = setup('');
- assert.deepStrictEqual(plugin.getDocumentSymbols(document), []);
- });
- });
-
- it('provides selection range', () => {
- const { plugin, document } = setup('');
-
- const selectionRange = plugin.getSelectionRange(document, Position.create(0, 11));
-
- assert.deepStrictEqual(selectionRange, {
- parent: {
- parent: {
- parent: undefined,
- range: {
- end: {
- character: 12,
- line: 0
- },
- start: {
- character: 7,
- line: 0
- }
- }
- },
- range: {
- end: {
- character: 12,
- line: 0
- },
- start: {
- character: 10,
- line: 0
- }
- }
- },
- range: {
- end: {
- character: 11,
- line: 0
- },
- start: {
- character: 11,
- line: 0
- }
- }
- });
- });
-
- it('return null for selection range when not in style', () => {
- const { plugin, document } = setup('');
-
- const selectionRange = plugin.getSelectionRange(document, Position.create(0, 10));
-
- assert.equal(selectionRange, null);
- });
+ assert.deepStrictEqual(
+ plugin.getColorPresentations(
+ document,
+ {
+ start: { line: 2, character: 22 },
+ end: { line: 2, character: 26 }
+ },
+ { alpha: 1, blue: 255, green: 0, red: 0 }
+ ),
+ []
+ );
+ assert.deepStrictEqual(plugin.getDocumentColors(document), []);
+ });
+ });
+
+ describe('provides document symbols', () => {
+ it('for normal css', () => {
+ const { plugin, document } = setup('');
+
+ const symbols = plugin.getDocumentSymbols(document);
+
+ assert.deepStrictEqual(symbols, [
+ {
+ containerName: 'style',
+ kind: 5,
+ location: {
+ range: {
+ end: {
+ character: 23,
+ line: 0
+ },
+ start: {
+ character: 7,
+ line: 0
+ }
+ },
+ uri: 'file:///hello.svelte'
+ },
+ name: 'h1'
+ }
+ ]);
+ });
+
+ it('not for SASS', () => {
+ const { plugin, document } = setup('');
+ assert.deepStrictEqual(plugin.getDocumentSymbols(document), []);
+ });
+
+ it('not for stylus', () => {
+ const { plugin, document } = setup('');
+ assert.deepStrictEqual(plugin.getDocumentSymbols(document), []);
+ });
+ });
+
+ it('provides selection range', () => {
+ const { plugin, document } = setup('');
+
+ const selectionRange = plugin.getSelectionRange(document, Position.create(0, 11));
+
+ assert.deepStrictEqual(selectionRange, {
+ parent: {
+ parent: {
+ parent: undefined,
+ range: {
+ end: {
+ character: 12,
+ line: 0
+ },
+ start: {
+ character: 7,
+ line: 0
+ }
+ }
+ },
+ range: {
+ end: {
+ character: 12,
+ line: 0
+ },
+ start: {
+ character: 10,
+ line: 0
+ }
+ }
+ },
+ range: {
+ end: {
+ character: 11,
+ line: 0
+ },
+ start: {
+ character: 11,
+ line: 0
+ }
+ }
+ });
+ });
+
+ it('return null for selection range when not in style', () => {
+ const { plugin, document } = setup('');
+
+ const selectionRange = plugin.getSelectionRange(document, Position.create(0, 10));
+
+ assert.equal(selectionRange, null);
+ });
});
diff --git a/packages/language-server/test/plugins/css/features/getIdClassCompletion.test.ts b/packages/language-server/test/plugins/css/features/getIdClassCompletion.test.ts
index 6d3321246..1630c10eb 100644
--- a/packages/language-server/test/plugins/css/features/getIdClassCompletion.test.ts
+++ b/packages/language-server/test/plugins/css/features/getIdClassCompletion.test.ts
@@ -5,74 +5,74 @@ import { LSConfigManager } from '../../../../src/ls-config';
import { CSSPlugin } from '../../../../src/plugins';
import { CSSDocument } from '../../../../src/plugins/css/CSSDocument';
import {
- collectSelectors,
- NodeType,
- CSSNode
+ collectSelectors,
+ NodeType,
+ CSSNode
} from '../../../../src/plugins/css/features/getIdClassCompletion';
describe('getIdClassCompletion', () => {
- function createDocument(content: string) {
- return new Document('file:///hello.svelte', content);
- }
+ function createDocument(content: string) {
+ return new Document('file:///hello.svelte', content);
+ }
- function createCSSDocument(content: string) {
- return new CSSDocument(createDocument(content));
- }
+ function createCSSDocument(content: string) {
+ return new CSSDocument(createDocument(content));
+ }
- function testSelectors(items: CompletionItem[], expectedSelectors: string[]) {
- assert.deepStrictEqual(
- items.map((item) => item.label),
- expectedSelectors,
- 'vscode-language-services might have changed the NodeType enum. Check if we need to update it'
- );
- }
+ function testSelectors(items: CompletionItem[], expectedSelectors: string[]) {
+ assert.deepStrictEqual(
+ items.map((item) => item.label),
+ expectedSelectors,
+ 'vscode-language-services might have changed the NodeType enum. Check if we need to update it'
+ );
+ }
- it('collect css classes', () => {
- const actual = collectSelectors(
- createCSSDocument('').stylesheet as CSSNode,
- NodeType.ClassSelector
- );
- testSelectors(actual, ['abc']);
- });
+ it('collect css classes', () => {
+ const actual = collectSelectors(
+ createCSSDocument('').stylesheet as CSSNode,
+ NodeType.ClassSelector
+ );
+ testSelectors(actual, ['abc']);
+ });
- it('collect css ids', () => {
- const actual = collectSelectors(
- createCSSDocument('').stylesheet as CSSNode,
- NodeType.IdentifierSelector
- );
- testSelectors(actual, ['abc']);
- });
+ it('collect css ids', () => {
+ const actual = collectSelectors(
+ createCSSDocument('').stylesheet as CSSNode,
+ NodeType.IdentifierSelector
+ );
+ testSelectors(actual, ['abc']);
+ });
- function setup(content: string) {
- const document = createDocument(content);
- const docManager = new DocumentManager(() => document);
- const pluginManager = new LSConfigManager();
- const plugin = new CSSPlugin(docManager, pluginManager);
- docManager.openDocument('some doc');
- return { plugin, document };
- }
+ function setup(content: string) {
+ const document = createDocument(content);
+ const docManager = new DocumentManager(() => document);
+ const pluginManager = new LSConfigManager();
+ const plugin = new CSSPlugin(docManager, pluginManager);
+ docManager.openDocument('some doc');
+ return { plugin, document };
+ }
- it('provides css classes completion for class attribute', () => {
- const { plugin, document } = setup('');
- assert.deepStrictEqual(plugin.getCompletions(document, { line: 0, character: 11 }), {
- isIncomplete: false,
- items: [{ label: 'abc', kind: CompletionItemKind.Keyword }]
- } as CompletionList);
- });
+ it('provides css classes completion for class attribute', () => {
+ const { plugin, document } = setup('');
+ assert.deepStrictEqual(plugin.getCompletions(document, { line: 0, character: 11 }), {
+ isIncomplete: false,
+ items: [{ label: 'abc', kind: CompletionItemKind.Keyword }]
+ } as CompletionList);
+ });
- it('provides css classes completion for class directive', () => {
- const { plugin, document } = setup('');
- assert.deepStrictEqual(plugin.getCompletions(document, { line: 0, character: 11 }), {
- isIncomplete: false,
- items: [{ label: 'abc', kind: CompletionItemKind.Keyword }]
- } as CompletionList);
- });
+ it('provides css classes completion for class directive', () => {
+ const { plugin, document } = setup('');
+ assert.deepStrictEqual(plugin.getCompletions(document, { line: 0, character: 11 }), {
+ isIncomplete: false,
+ items: [{ label: 'abc', kind: CompletionItemKind.Keyword }]
+ } as CompletionList);
+ });
- it('provides css id completion for id attribute', () => {
- const { plugin, document } = setup('');
- assert.deepStrictEqual(plugin.getCompletions(document, { line: 0, character: 8 }), {
- isIncomplete: false,
- items: [{ label: 'abc', kind: CompletionItemKind.Keyword }]
- } as CompletionList);
- });
+ it('provides css id completion for id attribute', () => {
+ const { plugin, document } = setup('');
+ assert.deepStrictEqual(plugin.getCompletions(document, { line: 0, character: 8 }), {
+ isIncomplete: false,
+ items: [{ label: 'abc', kind: CompletionItemKind.Keyword }]
+ } as CompletionList);
+ });
});
diff --git a/packages/language-server/test/plugins/html/HTMLPlugin.test.ts b/packages/language-server/test/plugins/html/HTMLPlugin.test.ts
index f1301c421..439efe96e 100644
--- a/packages/language-server/test/plugins/html/HTMLPlugin.test.ts
+++ b/packages/language-server/test/plugins/html/HTMLPlugin.test.ts
@@ -1,192 +1,192 @@
import * as assert from 'assert';
import {
- Range,
- Position,
- Hover,
- CompletionItem,
- TextEdit,
- CompletionItemKind,
- InsertTextFormat
+ Range,
+ Position,
+ Hover,
+ CompletionItem,
+ TextEdit,
+ CompletionItemKind,
+ InsertTextFormat
} from 'vscode-languageserver';
import { HTMLPlugin } from '../../../src/plugins';
import { DocumentManager, Document } from '../../../src/lib/documents';
import { LSConfigManager } from '../../../src/ls-config';
describe('HTML Plugin', () => {
- function setup(content: string) {
- const document = new Document('file:///hello.svelte', content);
- const docManager = new DocumentManager(() => document);
- const pluginManager = new LSConfigManager();
- const plugin = new HTMLPlugin(docManager, pluginManager);
- docManager.openDocument('some doc');
- return { plugin, document };
- }
-
- it('provides hover info', async () => {
- const { plugin, document } = setup('Hello, world!
');
-
- assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 2)), {
- contents: {
- kind: 'markdown',
- value:
- 'The h1 element represents a section heading.\n\n[MDN Reference](https://developer.mozilla.org/docs/Web/HTML/Element/Heading_Elements)'
- },
-
- range: Range.create(0, 1, 0, 3)
- });
-
- assert.strictEqual(plugin.doHover(document, Position.create(0, 10)), null);
- });
-
- it('does not provide hover info for component having the same name as a html element but being uppercase', async () => {
- const { plugin, document } = setup('');
-
- assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 2)), null);
- });
-
- it('provides completions', async () => {
- const { plugin, document } = setup('<');
-
- const completions = plugin.getCompletions(document, Position.create(0, 1));
- assert.ok(Array.isArray(completions && completions.items));
- assert.ok(completions!.items.length > 0);
-
- assert.deepStrictEqual(completions!.items[0], {
- label: '!DOCTYPE',
- kind: CompletionItemKind.Property,
- documentation: 'A preamble for an HTML document.',
- textEdit: TextEdit.insert(Position.create(0, 1), '!DOCTYPE html>'),
- insertTextFormat: InsertTextFormat.PlainText
- });
- });
-
- it('does not provide completions inside of moustache tag', async () => {
- const { plugin, document } = setup('');
-
- const completions = plugin.getCompletions(document, Position.create(0, 20));
- assert.strictEqual(completions, null);
-
- const tagCompletion = plugin.doTagComplete(document, Position.create(0, 20));
- assert.strictEqual(tagCompletion, null);
- });
-
- it('does provide completions outside of moustache tag', async () => {
- const { plugin, document } = setup('
');
-
- const completions = plugin.getCompletions(document, Position.create(0, 21));
- assert.deepEqual(completions?.items[0], {
- filterText: '
',
- insertTextFormat: 2,
- kind: 10,
- label: '
',
- textEdit: {
- newText: '$0',
- range: {
- end: {
- character: 21,
- line: 0
- },
- start: {
- character: 21,
- line: 0
- }
- }
- }
- });
-
- const tagCompletion = plugin.doTagComplete(document, Position.create(0, 21));
- assert.strictEqual(tagCompletion, '$0
');
- });
-
- it('does provide lang in completions', async () => {
- const { plugin, document } = setup('
item.label === 'style (lang="less")'));
- });
-
- it('does not provide lang in completions for attributes', async () => {
- const { plugin, document } = setup(' item.label === 'style (lang="less")'),
- undefined
- );
- });
-
- it('does not provide rename for element being uppercase', async () => {
- const { plugin, document } = setup('
');
-
- assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 2)), null);
- assert.deepStrictEqual(plugin.rename(document, Position.create(0, 2), 'p'), null);
- });
-
- it('does not provide rename for valid element but incorrect position', () => {
- const { plugin, document } = setup('
ab}>asd
');
- const newName = 'p';
-
- assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 16)), null);
- assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 5)), null);
- assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 26)), null);
-
- assert.deepStrictEqual(plugin.rename(document, Position.create(0, 16), newName), null);
- assert.deepStrictEqual(plugin.rename(document, Position.create(0, 5), newName), null);
- assert.deepStrictEqual(plugin.rename(document, Position.create(0, 26), newName), null);
- });
-
- it('provides rename for element', () => {
- const { plugin, document } = setup('
{}}>
');
- const newName = 'p';
-
- const pepareRenameInfo = Range.create(Position.create(0, 1), Position.create(0, 4));
- assert.deepStrictEqual(
- plugin.prepareRename(document, Position.create(0, 2)),
- pepareRenameInfo
- );
- assert.deepStrictEqual(
- plugin.prepareRename(document, Position.create(0, 28)),
- pepareRenameInfo
- );
-
- const renameInfo = {
- changes: {
- [document.uri]: [
- {
- newText: 'p',
- range: {
- start: { line: 0, character: 1 },
- end: { line: 0, character: 4 }
- }
- },
- {
- newText: 'p',
- range: {
- start: { line: 0, character: 27 },
- end: { line: 0, character: 30 }
- }
- }
- ]
- }
- };
- assert.deepStrictEqual(plugin.rename(document, Position.create(0, 2), newName), renameInfo);
- assert.deepStrictEqual(
- plugin.rename(document, Position.create(0, 28), newName),
- renameInfo
- );
- });
-
- it('provides linked editing ranges', async () => {
- const { plugin, document } = setup('
');
-
- const ranges = plugin.getLinkedEditingRanges(document, Position.create(0, 3));
- assert.deepStrictEqual(ranges, {
- ranges: [
- { start: { line: 0, character: 1 }, end: { line: 0, character: 4 } },
- { start: { line: 0, character: 7 }, end: { line: 0, character: 10 } }
- ]
- });
- });
+ function setup(content: string) {
+ const document = new Document('file:///hello.svelte', content);
+ const docManager = new DocumentManager(() => document);
+ const pluginManager = new LSConfigManager();
+ const plugin = new HTMLPlugin(docManager, pluginManager);
+ docManager.openDocument(
'some doc');
+ return { plugin, document };
+ }
+
+ it('provides hover info', async () => {
+ const { plugin, document } = setup('Hello, world!
');
+
+ assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 2)), {
+ contents: {
+ kind: 'markdown',
+ value:
+ 'The h1 element represents a section heading.\n\n[MDN Reference](https://developer.mozilla.org/docs/Web/HTML/Element/Heading_Elements)'
+ },
+
+ range: Range.create(0, 1, 0, 3)
+ });
+
+ assert.strictEqual(plugin.doHover(document, Position.create(0, 10)), null);
+ });
+
+ it('does not provide hover info for component having the same name as a html element but being uppercase', async () => {
+ const { plugin, document } = setup('');
+
+ assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 2)), null);
+ });
+
+ it('provides completions', async () => {
+ const { plugin, document } = setup('<');
+
+ const completions = plugin.getCompletions(document, Position.create(0, 1));
+ assert.ok(Array.isArray(completions && completions.items));
+ assert.ok(completions!.items.length > 0);
+
+ assert.deepStrictEqual(completions!.items[0], {
+ label: '!DOCTYPE',
+ kind: CompletionItemKind.Property,
+ documentation: 'A preamble for an HTML document.',
+ textEdit: TextEdit.insert(Position.create(0, 1), '!DOCTYPE html>'),
+ insertTextFormat: InsertTextFormat.PlainText
+ });
+ });
+
+ it('does not provide completions inside of moustache tag', async () => {
+ const { plugin, document } = setup('');
+
+ const completions = plugin.getCompletions(document, Position.create(0, 20));
+ assert.strictEqual(completions, null);
+
+ const tagCompletion = plugin.doTagComplete(document, Position.create(0, 20));
+ assert.strictEqual(tagCompletion, null);
+ });
+
+ it('does provide completions outside of moustache tag', async () => {
+ const { plugin, document } = setup('
');
+
+ const completions = plugin.getCompletions(document, Position.create(0, 21));
+ assert.deepEqual(completions?.items[0], {
+ filterText: '
',
+ insertTextFormat: 2,
+ kind: 10,
+ label: '
',
+ textEdit: {
+ newText: '$0 ',
+ range: {
+ end: {
+ character: 21,
+ line: 0
+ },
+ start: {
+ character: 21,
+ line: 0
+ }
+ }
+ }
+ });
+
+ const tagCompletion = plugin.doTagComplete(document, Position.create(0, 21));
+ assert.strictEqual(tagCompletion, '$0');
+ });
+
+ it('does provide lang in completions', async () => {
+ const { plugin, document } = setup('
item.label === 'style (lang="less")'));
+ });
+
+ it('does not provide lang in completions for attributes', async () => {
+ const { plugin, document } = setup(' item.label === 'style (lang="less")'),
+ undefined
+ );
+ });
+
+ it('does not provide rename for element being uppercase', async () => {
+ const { plugin, document } = setup('
');
+
+ assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 2)), null);
+ assert.deepStrictEqual(plugin.rename(document, Position.create(0, 2), 'p'), null);
+ });
+
+ it('does not provide rename for valid element but incorrect position', () => {
+ const { plugin, document } = setup('
ab}>asd
');
+ const newName = 'p';
+
+ assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 16)), null);
+ assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 5)), null);
+ assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 26)), null);
+
+ assert.deepStrictEqual(plugin.rename(document, Position.create(0, 16), newName), null);
+ assert.deepStrictEqual(plugin.rename(document, Position.create(0, 5), newName), null);
+ assert.deepStrictEqual(plugin.rename(document, Position.create(0, 26), newName), null);
+ });
+
+ it('provides rename for element', () => {
+ const { plugin, document } = setup('
{}}>
');
+ const newName = 'p';
+
+ const pepareRenameInfo = Range.create(Position.create(0, 1), Position.create(0, 4));
+ assert.deepStrictEqual(
+ plugin.prepareRename(document, Position.create(0, 2)),
+ pepareRenameInfo
+ );
+ assert.deepStrictEqual(
+ plugin.prepareRename(document, Position.create(0, 28)),
+ pepareRenameInfo
+ );
+
+ const renameInfo = {
+ changes: {
+ [document.uri]: [
+ {
+ newText: 'p',
+ range: {
+ start: { line: 0, character: 1 },
+ end: { line: 0, character: 4 }
+ }
+ },
+ {
+ newText: 'p',
+ range: {
+ start: { line: 0, character: 27 },
+ end: { line: 0, character: 30 }
+ }
+ }
+ ]
+ }
+ };
+ assert.deepStrictEqual(plugin.rename(document, Position.create(0, 2), newName), renameInfo);
+ assert.deepStrictEqual(
+ plugin.rename(document, Position.create(0, 28), newName),
+ renameInfo
+ );
+ });
+
+ it('provides linked editing ranges', async () => {
+ const { plugin, document } = setup('
');
+
+ const ranges = plugin.getLinkedEditingRanges(document, Position.create(0, 3));
+ assert.deepStrictEqual(ranges, {
+ ranges: [
+ { start: { line: 0, character: 1 }, end: { line: 0, character: 4 } },
+ { start: { line: 0, character: 7 }, end: { line: 0, character: 10 } }
+ ]
+ });
+ });
});
diff --git a/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts b/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts
index 02d24cc7d..c44dcdb57 100644
--- a/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts
+++ b/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts
@@ -4,126 +4,126 @@ import { Position } from 'vscode-languageserver';
import { Document } from '../../../src/lib/documents';
import * as importPackage from '../../../src/importPackage';
import {
- SvelteDocument,
- TranspiledSvelteDocument
+ SvelteDocument,
+ TranspiledSvelteDocument
} from '../../../src/plugins/svelte/SvelteDocument';
import { configLoader, SvelteConfig } from '../../../src/lib/documents/configLoader';
describe('Svelte Document', () => {
- function getSourceCode(transpiled: boolean): string {
- return `
+ function getSourceCode(transpiled: boolean): string {
+ return `
jo
Hello, world!
`;
- }
-
- function setup(config: SvelteConfig = {}) {
- sinon.stub(configLoader, 'getConfig').returns(config);
- const parent = new Document('file:///hello.svelte', getSourceCode(false));
- sinon.restore();
- const svelteDoc = new SvelteDocument(parent);
- return { parent, svelteDoc };
- }
-
- it('gets the parents text', () => {
- const { parent, svelteDoc } = setup();
- assert.strictEqual(svelteDoc.getText(), parent.getText());
- });
-
- describe('#transpiled', () => {
- async function setupTranspiled() {
- const { parent, svelteDoc } = setup({
- preprocess: {
- script: () => ({
- code: '',
- map: JSON.stringify({
- version: 3,
- file: '',
- names: [],
- sources: [],
- sourceRoot: '',
- mappings: ''
- })
- })
- }
- });
-
- // stub svelte preprocess and getOriginalPosition
- // to fake a source mapping process
- sinon.stub(importPackage, 'importSvelte').returns({
- preprocess: (text, preprocessor) => {
- preprocessor = Array.isArray(preprocessor) ? preprocessor : [preprocessor];
- preprocessor.forEach((p) => p.script?.(
{}));
- return Promise.resolve({
- code: getSourceCode(true),
- dependencies: [],
- toString: () => getSourceCode(true),
- map: null
- });
- },
- walk: null,
- VERSION: '',
- compile: null,
- parse: null
- });
- const transpiled = await svelteDoc.getTranspiled();
- const scriptSourceMapper = (transpiled.scriptMapper).sourceMapper;
- // hacky reset of method because mocking the SourceMap constructor is an impossible task
- scriptSourceMapper.getOriginalPosition = ({ line, character }: Position) => ({
- line: line - 1,
- character
- });
- scriptSourceMapper.getGeneratedPosition = ({ line, character }: Position) => ({
- line: line + 1,
- character
- });
- sinon.restore();
-
- return { parent, svelteDoc, transpiled };
- }
-
- function assertCanMapBackAndForth(
- transpiled: TranspiledSvelteDocument,
- generatedPosition: Position,
- originalPosition: Position
- ) {
- assert.deepStrictEqual(
- transpiled.getOriginalPosition(generatedPosition),
- originalPosition,
- 'error mapping to original position'
- );
-
- assert.deepStrictEqual(
- transpiled.getGeneratedPosition(originalPosition),
- generatedPosition,
- 'error mapping to generated position'
- );
- }
-
- it('should map correctly within sourcemapped script', async () => {
- const { transpiled } = await setupTranspiled();
-
- assertCanMapBackAndForth(transpiled, Position.create(3, 2), Position.create(2, 18));
- });
-
- it('should map correctly in template before script', async () => {
- const { transpiled } = await setupTranspiled();
-
- assertCanMapBackAndForth(transpiled, Position.create(1, 1), Position.create(1, 1));
- });
-
- it('should map correctly in template after script', async () => {
- const { transpiled } = await setupTranspiled();
-
- assertCanMapBackAndForth(transpiled, Position.create(4, 1), Position.create(3, 1));
- });
-
- it('should map correctly in style', async () => {
- const { transpiled } = await setupTranspiled();
-
- assertCanMapBackAndForth(transpiled, Position.create(5, 18), Position.create(4, 18));
- });
- });
+ }
+
+ function setup(config: SvelteConfig = {}) {
+ sinon.stub(configLoader, 'getConfig').returns(config);
+ const parent = new Document('file:///hello.svelte', getSourceCode(false));
+ sinon.restore();
+ const svelteDoc = new SvelteDocument(parent);
+ return { parent, svelteDoc };
+ }
+
+ it('gets the parents text', () => {
+ const { parent, svelteDoc } = setup();
+ assert.strictEqual(svelteDoc.getText(), parent.getText());
+ });
+
+ describe('#transpiled', () => {
+ async function setupTranspiled() {
+ const { parent, svelteDoc } = setup({
+ preprocess: {
+ script: () => ({
+ code: '',
+ map: JSON.stringify({
+ version: 3,
+ file: '',
+ names: [],
+ sources: [],
+ sourceRoot: '',
+ mappings: ''
+ })
+ })
+ }
+ });
+
+ // stub svelte preprocess and getOriginalPosition
+ // to fake a source mapping process
+ sinon.stub(importPackage, 'importSvelte').returns({
+ preprocess: (text, preprocessor) => {
+ preprocessor = Array.isArray(preprocessor) ? preprocessor : [preprocessor];
+ preprocessor.forEach((p) => p.script?.({}));
+ return Promise.resolve({
+ code: getSourceCode(true),
+ dependencies: [],
+ toString: () => getSourceCode(true),
+ map: null
+ });
+ },
+ walk: null,
+ VERSION: '',
+ compile: null,
+ parse: null
+ });
+ const transpiled = await svelteDoc.getTranspiled();
+ const scriptSourceMapper = (transpiled.scriptMapper).sourceMapper;
+ // hacky reset of method because mocking the SourceMap constructor is an impossible task
+ scriptSourceMapper.getOriginalPosition = ({ line, character }: Position) => ({
+ line: line - 1,
+ character
+ });
+ scriptSourceMapper.getGeneratedPosition = ({ line, character }: Position) => ({
+ line: line + 1,
+ character
+ });
+ sinon.restore();
+
+ return { parent, svelteDoc, transpiled };
+ }
+
+ function assertCanMapBackAndForth(
+ transpiled: TranspiledSvelteDocument,
+ generatedPosition: Position,
+ originalPosition: Position
+ ) {
+ assert.deepStrictEqual(
+ transpiled.getOriginalPosition(generatedPosition),
+ originalPosition,
+ 'error mapping to original position'
+ );
+
+ assert.deepStrictEqual(
+ transpiled.getGeneratedPosition(originalPosition),
+ generatedPosition,
+ 'error mapping to generated position'
+ );
+ }
+
+ it('should map correctly within sourcemapped script', async () => {
+ const { transpiled } = await setupTranspiled();
+
+ assertCanMapBackAndForth(transpiled, Position.create(3, 2), Position.create(2, 18));
+ });
+
+ it('should map correctly in template before script', async () => {
+ const { transpiled } = await setupTranspiled();
+
+ assertCanMapBackAndForth(transpiled, Position.create(1, 1), Position.create(1, 1));
+ });
+
+ it('should map correctly in template after script', async () => {
+ const { transpiled } = await setupTranspiled();
+
+ assertCanMapBackAndForth(transpiled, Position.create(4, 1), Position.create(3, 1));
+ });
+
+ it('should map correctly in style', async () => {
+ const { transpiled } = await setupTranspiled();
+
+ assertCanMapBackAndForth(transpiled, Position.create(5, 18), Position.create(4, 18));
+ });
+ });
});
diff --git a/packages/language-server/test/plugins/svelte/SveltePlugin.test.ts b/packages/language-server/test/plugins/svelte/SveltePlugin.test.ts
index 78646827c..a82df4252 100644
--- a/packages/language-server/test/plugins/svelte/SveltePlugin.test.ts
+++ b/packages/language-server/test/plugins/svelte/SveltePlugin.test.ts
@@ -7,140 +7,140 @@ import * as importPackage from '../../../src/importPackage';
import sinon from 'sinon';
describe('Svelte Plugin', () => {
- function setup(content: string, prettierConfig?: any) {
- const document = new Document('file:///hello.svelte', content);
- const docManager = new DocumentManager(() => document);
- const pluginManager = new LSConfigManager();
- pluginManager.updatePrettierConfig(prettierConfig);
- const plugin = new SveltePlugin(pluginManager);
- docManager.openDocument('some doc');
- return { plugin, document };
- }
-
- it('provides diagnostic warnings', async () => {
- const { plugin, document } = setup('Hello, world!
\n');
-
- const diagnostics = await plugin.getDiagnostics(document);
- const diagnostic = Diagnostic.create(
- Range.create(1, 0, 1, 21),
- 'A11y: element should have an alt attribute',
- DiagnosticSeverity.Warning,
- 'a11y-missing-attribute',
- 'svelte'
- );
-
- assert.deepStrictEqual(diagnostics, [diagnostic]);
- });
-
- it('provides diagnostic errors', async () => {
- const { plugin, document } = setup('');
-
- const diagnostics = await plugin.getDiagnostics(document);
- const diagnostic = Diagnostic.create(
- Range.create(0, 10, 0, 18),
- 'whatever is not declared',
- DiagnosticSeverity.Error,
- 'binding-undeclared',
- 'svelte'
- );
-
- assert.deepStrictEqual(diagnostics, [diagnostic]);
- });
-
- describe('#formatDocument', () => {
- function stubPrettier(config: any) {
- const formatStub = sinon.stub().returns('formatted');
-
- sinon.stub(importPackage, 'importPrettier').returns({
- resolveConfig: () => Promise.resolve(config),
- getFileInfo: () => ({ ignored: false }),
- format: formatStub,
- getSupportInfo: () => ({ languages: [{ name: 'svelte' }] })
- });
-
- return formatStub;
- }
-
- async function testFormat(config: any, fallbackPrettierConfig: any) {
- const { plugin, document } = setup('unformatted', fallbackPrettierConfig);
- const formatStub = stubPrettier(config);
-
- const formatted = await plugin.formatDocument(document, {
- insertSpaces: true,
- tabSize: 4
- });
- assert.deepStrictEqual(formatted, [
- {
- newText: 'formatted',
- range: {
- end: {
- character: 11,
- line: 0
- },
- start: {
- character: 0,
- line: 0
- }
- }
- }
- ]);
-
- return formatStub;
- }
-
- afterEach(() => {
- sinon.restore();
- });
-
- it('should use config for formatting', async () => {
- const formatStub = await testFormat({ fromConfig: true }, { fallbackConfig: true });
- sinon.assert.calledOnceWithExactly(formatStub, 'unformatted', {
- fromConfig: true,
- plugins: [],
- parser: 'svelte'
- });
- });
-
- const defaultSettings = {
- svelteSortOrder: 'options-scripts-markup-styles',
- svelteStrictMode: false,
- svelteAllowShorthand: true,
- svelteBracketNewLine: true,
- svelteIndentScriptAndStyle: true,
- printWidth: 80,
- singleQuote: false
- };
-
- it('should use prettier fallback config for formatting', async () => {
- const formatStub = await testFormat(undefined, { fallbackConfig: true });
- sinon.assert.calledOnceWithExactly(formatStub, 'unformatted', {
- fallbackConfig: true,
- plugins: [],
- parser: 'svelte',
- ...defaultSettings
- });
- });
-
- it('should use FormattingOptions for formatting', async () => {
- const formatStub = await testFormat(undefined, undefined);
- sinon.assert.calledOnceWithExactly(formatStub, 'unformatted', {
- tabWidth: 4,
- useTabs: false,
- plugins: [],
- parser: 'svelte',
- ...defaultSettings
- });
- });
-
- it('should use FormattingOptions for formatting when configs are empty objects', async () => {
- const formatStub = await testFormat({}, {});
- sinon.assert.calledOnceWithExactly(formatStub, 'unformatted', {
- tabWidth: 4,
- useTabs: false,
- plugins: [],
- parser: 'svelte',
- ...defaultSettings
- });
- });
- });
+ function setup(content: string, prettierConfig?: any) {
+ const document = new Document('file:///hello.svelte', content);
+ const docManager = new DocumentManager(() => document);
+ const pluginManager = new LSConfigManager();
+ pluginManager.updatePrettierConfig(prettierConfig);
+ const plugin = new SveltePlugin(pluginManager);
+ docManager.openDocument('some doc');
+ return { plugin, document };
+ }
+
+ it('provides diagnostic warnings', async () => {
+ const { plugin, document } = setup('Hello, world!
\n');
+
+ const diagnostics = await plugin.getDiagnostics(document);
+ const diagnostic = Diagnostic.create(
+ Range.create(1, 0, 1, 21),
+ 'A11y: element should have an alt attribute',
+ DiagnosticSeverity.Warning,
+ 'a11y-missing-attribute',
+ 'svelte'
+ );
+
+ assert.deepStrictEqual(diagnostics, [diagnostic]);
+ });
+
+ it('provides diagnostic errors', async () => {
+ const { plugin, document } = setup('');
+
+ const diagnostics = await plugin.getDiagnostics(document);
+ const diagnostic = Diagnostic.create(
+ Range.create(0, 10, 0, 18),
+ 'whatever is not declared',
+ DiagnosticSeverity.Error,
+ 'binding-undeclared',
+ 'svelte'
+ );
+
+ assert.deepStrictEqual(diagnostics, [diagnostic]);
+ });
+
+ describe('#formatDocument', () => {
+ function stubPrettier(config: any) {
+ const formatStub = sinon.stub().returns('formatted');
+
+ sinon.stub(importPackage, 'importPrettier').returns({
+ resolveConfig: () => Promise.resolve(config),
+ getFileInfo: () => ({ ignored: false }),
+ format: formatStub,
+ getSupportInfo: () => ({ languages: [{ name: 'svelte' }] })
+ });
+
+ return formatStub;
+ }
+
+ async function testFormat(config: any, fallbackPrettierConfig: any) {
+ const { plugin, document } = setup('unformatted', fallbackPrettierConfig);
+ const formatStub = stubPrettier(config);
+
+ const formatted = await plugin.formatDocument(document, {
+ insertSpaces: true,
+ tabSize: 4
+ });
+ assert.deepStrictEqual(formatted, [
+ {
+ newText: 'formatted',
+ range: {
+ end: {
+ character: 11,
+ line: 0
+ },
+ start: {
+ character: 0,
+ line: 0
+ }
+ }
+ }
+ ]);
+
+ return formatStub;
+ }
+
+ afterEach(() => {
+ sinon.restore();
+ });
+
+ it('should use config for formatting', async () => {
+ const formatStub = await testFormat({ fromConfig: true }, { fallbackConfig: true });
+ sinon.assert.calledOnceWithExactly(formatStub, 'unformatted', {
+ fromConfig: true,
+ plugins: [],
+ parser: 'svelte'
+ });
+ });
+
+ const defaultSettings = {
+ svelteSortOrder: 'options-scripts-markup-styles',
+ svelteStrictMode: false,
+ svelteAllowShorthand: true,
+ svelteBracketNewLine: true,
+ svelteIndentScriptAndStyle: true,
+ printWidth: 80,
+ singleQuote: false
+ };
+
+ it('should use prettier fallback config for formatting', async () => {
+ const formatStub = await testFormat(undefined, { fallbackConfig: true });
+ sinon.assert.calledOnceWithExactly(formatStub, 'unformatted', {
+ fallbackConfig: true,
+ plugins: [],
+ parser: 'svelte',
+ ...defaultSettings
+ });
+ });
+
+ it('should use FormattingOptions for formatting', async () => {
+ const formatStub = await testFormat(undefined, undefined);
+ sinon.assert.calledOnceWithExactly(formatStub, 'unformatted', {
+ tabWidth: 4,
+ useTabs: false,
+ plugins: [],
+ parser: 'svelte',
+ ...defaultSettings
+ });
+ });
+
+ it('should use FormattingOptions for formatting when configs are empty objects', async () => {
+ const formatStub = await testFormat({}, {});
+ sinon.assert.calledOnceWithExactly(formatStub, 'unformatted', {
+ tabWidth: 4,
+ useTabs: false,
+ plugins: [],
+ parser: 'svelte',
+ ...defaultSettings
+ });
+ });
+ });
});
diff --git a/packages/language-server/test/plugins/svelte/features/getCodeAction.test.ts b/packages/language-server/test/plugins/svelte/features/getCodeAction.test.ts
index 6f014a87d..432baa959 100644
--- a/packages/language-server/test/plugins/svelte/features/getCodeAction.test.ts
+++ b/packages/language-server/test/plugins/svelte/features/getCodeAction.test.ts
@@ -3,343 +3,343 @@ import * as fs from 'fs';
import { EOL } from 'os';
import * as path from 'path';
import {
- CodeAction,
- CodeActionContext,
- CreateFile,
- DiagnosticSeverity,
- Position,
- Range,
- TextDocumentEdit,
- TextEdit,
- OptionalVersionedTextDocumentIdentifier,
- WorkspaceEdit
+ CodeAction,
+ CodeActionContext,
+ CreateFile,
+ DiagnosticSeverity,
+ Position,
+ Range,
+ TextDocumentEdit,
+ TextEdit,
+ OptionalVersionedTextDocumentIdentifier,
+ WorkspaceEdit
} from 'vscode-languageserver';
import { Document } from '../../../../src/lib/documents';
import { getCodeActions } from '../../../../src/plugins/svelte/features/getCodeActions';
import {
- executeRefactoringCommand,
- ExtractComponentArgs,
- extractComponentCommand
+ executeRefactoringCommand,
+ ExtractComponentArgs,
+ extractComponentCommand
} from '../../../../src/plugins/svelte/features/getCodeActions/getRefactorings';
import { SvelteDocument } from '../../../../src/plugins/svelte/SvelteDocument';
import { pathToUrl } from '../../../../src/utils';
describe('SveltePlugin#getCodeAction', () => {
- const testDir = path.join(__dirname, '..', 'testfiles');
+ const testDir = path.join(__dirname, '..', 'testfiles');
- function getFullPath(filename: string) {
- return path.join(testDir, filename);
- }
+ function getFullPath(filename: string) {
+ return path.join(testDir, filename);
+ }
- function getUri(filename: string) {
- return pathToUrl(getFullPath(filename));
- }
+ function getUri(filename: string) {
+ return pathToUrl(getFullPath(filename));
+ }
- async function expectCodeActionFor(filename: string, context: CodeActionContext) {
- const filePath = path.join(testDir, filename);
- const document = new Document(
- pathToUrl(filePath),
- filename ? fs.readFileSync(filePath)?.toString() : ''
- );
- const svelteDoc = new SvelteDocument(document);
- const codeAction = await getCodeActions(
- svelteDoc,
- Range.create(Position.create(0, 0), Position.create(0, 0)),
- context
- );
- return {
- toEqual: (expected: CodeAction[]) => assert.deepStrictEqual(codeAction, expected)
- };
- }
+ async function expectCodeActionFor(filename: string, context: CodeActionContext) {
+ const filePath = path.join(testDir, filename);
+ const document = new Document(
+ pathToUrl(filePath),
+ filename ? fs.readFileSync(filePath)?.toString() : ''
+ );
+ const svelteDoc = new SvelteDocument(document);
+ const codeAction = await getCodeActions(
+ svelteDoc,
+ Range.create(Position.create(0, 0), Position.create(0, 0)),
+ context
+ );
+ return {
+ toEqual: (expected: CodeAction[]) => assert.deepStrictEqual(codeAction, expected)
+ };
+ }
- describe('It should not provide svelte ignore code actions', () => {
- const startRange: Range = Range.create(
- { line: 0, character: 0 },
- { line: 0, character: 1 }
- );
- it('if no svelte diagnostic', async () => {
- (
- await expectCodeActionFor('', {
- diagnostics: [
- {
- code: 'whatever',
- source: 'eslint',
- range: startRange,
- message: ''
- }
- ]
- })
- ).toEqual([]);
- });
+ describe('It should not provide svelte ignore code actions', () => {
+ const startRange: Range = Range.create(
+ { line: 0, character: 0 },
+ { line: 0, character: 1 }
+ );
+ it('if no svelte diagnostic', async () => {
+ (
+ await expectCodeActionFor('', {
+ diagnostics: [
+ {
+ code: 'whatever',
+ source: 'eslint',
+ range: startRange,
+ message: ''
+ }
+ ]
+ })
+ ).toEqual([]);
+ });
- it('if no diagnostic code', async () => {
- (
- await expectCodeActionFor('', {
- diagnostics: [
- {
- source: 'svelte',
- range: startRange,
- message: ''
- }
- ]
- })
- ).toEqual([]);
- });
+ it('if no diagnostic code', async () => {
+ (
+ await expectCodeActionFor('', {
+ diagnostics: [
+ {
+ source: 'svelte',
+ range: startRange,
+ message: ''
+ }
+ ]
+ })
+ ).toEqual([]);
+ });
- it('if diagnostic is error', async () => {
- (
- await expectCodeActionFor('', {
- diagnostics: [
- {
- source: 'svelte',
- range: startRange,
- message: '',
- severity: DiagnosticSeverity.Error
- }
- ]
- })
- ).toEqual([]);
- });
- });
+ it('if diagnostic is error', async () => {
+ (
+ await expectCodeActionFor('', {
+ diagnostics: [
+ {
+ source: 'svelte',
+ range: startRange,
+ message: '',
+ severity: DiagnosticSeverity.Error
+ }
+ ]
+ })
+ ).toEqual([]);
+ });
+ });
- describe('It should provide svelte ignore code actions ', () => {
- const svelteIgnoreCodeAction = 'svelte-ignore-code-action.svelte';
+ describe('It should provide svelte ignore code actions ', () => {
+ const svelteIgnoreCodeAction = 'svelte-ignore-code-action.svelte';
- it('should provide ignore comment', async () => {
- (
- await expectCodeActionFor(svelteIgnoreCodeAction, {
- diagnostics: [
- {
- severity: DiagnosticSeverity.Warning,
- code: 'a11y-missing-attribute',
- range: Range.create(
- { line: 0, character: 0 },
- { line: 0, character: 6 }
- ),
- message: '',
- source: 'svelte'
- }
- ]
- })
- ).toEqual([
- {
- edit: {
- documentChanges: [
- {
- edits: [
- {
- // eslint-disable-next-line max-len
- newText: `${EOL}`,
- range: {
- end: {
- character: 0,
- line: 0
- },
- start: {
- character: 0,
- line: 0
- }
- }
- }
- ],
- textDocument: {
- uri: getUri(svelteIgnoreCodeAction),
- version: null
- }
- }
- ]
- },
- title: '(svelte) Disable a11y-missing-attribute for this line',
- kind: 'quickfix'
- }
- ]);
- });
+ it('should provide ignore comment', async () => {
+ (
+ await expectCodeActionFor(svelteIgnoreCodeAction, {
+ diagnostics: [
+ {
+ severity: DiagnosticSeverity.Warning,
+ code: 'a11y-missing-attribute',
+ range: Range.create(
+ { line: 0, character: 0 },
+ { line: 0, character: 6 }
+ ),
+ message: '',
+ source: 'svelte'
+ }
+ ]
+ })
+ ).toEqual([
+ {
+ edit: {
+ documentChanges: [
+ {
+ edits: [
+ {
+ // eslint-disable-next-line max-len
+ newText: `${EOL}`,
+ range: {
+ end: {
+ character: 0,
+ line: 0
+ },
+ start: {
+ character: 0,
+ line: 0
+ }
+ }
+ }
+ ],
+ textDocument: {
+ uri: getUri(svelteIgnoreCodeAction),
+ version: null
+ }
+ }
+ ]
+ },
+ title: '(svelte) Disable a11y-missing-attribute for this line',
+ kind: 'quickfix'
+ }
+ ]);
+ });
- it('should provide ignore comment with indent', async () => {
- (
- await expectCodeActionFor(svelteIgnoreCodeAction, {
- diagnostics: [
- {
- severity: DiagnosticSeverity.Warning,
- code: 'a11y-missing-attribute',
- range: Range.create(
- { line: 3, character: 4 },
- { line: 3, character: 11 }
- ),
- message: '',
- source: 'svelte'
- }
- ]
- })
- ).toEqual([
- {
- edit: {
- documentChanges: [
- {
- edits: [
- {
- newText: `${' '.repeat(
- 4
- )}${EOL}`,
- range: {
- end: {
- character: 0,
- line: 3
- },
- start: {
- character: 0,
- line: 3
- }
- }
- }
- ],
- textDocument: {
- uri: getUri(svelteIgnoreCodeAction),
- version: null
- }
- }
- ]
- },
- title: '(svelte) Disable a11y-missing-attribute for this line',
- kind: 'quickfix'
- }
- ]);
- });
+ it('should provide ignore comment with indent', async () => {
+ (
+ await expectCodeActionFor(svelteIgnoreCodeAction, {
+ diagnostics: [
+ {
+ severity: DiagnosticSeverity.Warning,
+ code: 'a11y-missing-attribute',
+ range: Range.create(
+ { line: 3, character: 4 },
+ { line: 3, character: 11 }
+ ),
+ message: '',
+ source: 'svelte'
+ }
+ ]
+ })
+ ).toEqual([
+ {
+ edit: {
+ documentChanges: [
+ {
+ edits: [
+ {
+ newText: `${' '.repeat(
+ 4
+ )}${EOL}`,
+ range: {
+ end: {
+ character: 0,
+ line: 3
+ },
+ start: {
+ character: 0,
+ line: 3
+ }
+ }
+ }
+ ],
+ textDocument: {
+ uri: getUri(svelteIgnoreCodeAction),
+ version: null
+ }
+ }
+ ]
+ },
+ title: '(svelte) Disable a11y-missing-attribute for this line',
+ kind: 'quickfix'
+ }
+ ]);
+ });
- it('should provide ignore comment with indent of parent tag', async () => {
- (
- await expectCodeActionFor(svelteIgnoreCodeAction, {
- diagnostics: [
- {
- severity: DiagnosticSeverity.Warning,
- code: 'a11y-invalid-attribute',
- range: Range.create(
- { line: 6, character: 8 },
- { line: 6, character: 15 }
- ),
- message: '',
- source: 'svelte'
- }
- ]
- })
- ).toEqual([
- {
- edit: {
- documentChanges: [
- {
- edits: [
- {
- newText: `${' '.repeat(
- 4
- )}${EOL}`,
- range: {
- end: {
- character: 0,
- line: 5
- },
- start: {
- character: 0,
- line: 5
- }
- }
- }
- ],
- textDocument: {
- uri: getUri(svelteIgnoreCodeAction),
- version: null
- }
- }
- ]
- },
- title: '(svelte) Disable a11y-invalid-attribute for this line',
- kind: 'quickfix'
- }
- ]);
- });
- });
+ it('should provide ignore comment with indent of parent tag', async () => {
+ (
+ await expectCodeActionFor(svelteIgnoreCodeAction, {
+ diagnostics: [
+ {
+ severity: DiagnosticSeverity.Warning,
+ code: 'a11y-invalid-attribute',
+ range: Range.create(
+ { line: 6, character: 8 },
+ { line: 6, character: 15 }
+ ),
+ message: '',
+ source: 'svelte'
+ }
+ ]
+ })
+ ).toEqual([
+ {
+ edit: {
+ documentChanges: [
+ {
+ edits: [
+ {
+ newText: `${' '.repeat(
+ 4
+ )}${EOL}`,
+ range: {
+ end: {
+ character: 0,
+ line: 5
+ },
+ start: {
+ character: 0,
+ line: 5
+ }
+ }
+ }
+ ],
+ textDocument: {
+ uri: getUri(svelteIgnoreCodeAction),
+ version: null
+ }
+ }
+ ]
+ },
+ title: '(svelte) Disable a11y-invalid-attribute for this line',
+ kind: 'quickfix'
+ }
+ ]);
+ });
+ });
- describe('#extractComponent', async () => {
- const scriptContent = ``;
- const styleContent = '';
- const content = `
+ const styleContent = '';
+ const content = `
${scriptContent}
something else
extract me
${styleContent}`;
- const doc = new SvelteDocument(new Document('someUrl', content));
+ const doc = new SvelteDocument(new Document('someUrl', content));
- async function extractComponent(filePath: string, range: Range) {
- return executeRefactoringCommand(doc, extractComponentCommand, [
- '',
- {
- filePath,
- range,
- uri: ''
- }
- ]);
- }
+ async function extractComponent(filePath: string, range: Range) {
+ return executeRefactoringCommand(doc, extractComponentCommand, [
+ '',
+ {
+ filePath,
+ range,
+ uri: ''
+ }
+ ]);
+ }
- async function shouldExtractComponent(
- path: 'NewComp' | 'NewComp.svelte' | './NewComp' | './NewComp.svelte'
- ) {
- const range = Range.create(Position.create(5, 8), Position.create(5, 25));
- const result = await extractComponent(path, range);
- assert.deepStrictEqual(result, {
- documentChanges: [
- TextDocumentEdit.create(
- OptionalVersionedTextDocumentIdentifier.create('someUrl', null),
- [
- TextEdit.replace(range, ''),
- TextEdit.insert(
- doc.script?.startPos || Position.create(0, 0),
- "\n import NewComp from './NewComp.svelte';\n"
- )
- ]
- ),
- CreateFile.create('file:///NewComp.svelte', { overwrite: true }),
- TextDocumentEdit.create(
- OptionalVersionedTextDocumentIdentifier.create(
- 'file:///NewComp.svelte',
- null
- ),
- [
- TextEdit.insert(
- Position.create(0, 0),
- `${scriptContent}\n\nextract me
\n\n${styleContent}\n\n`
- )
- ]
- )
- ]
- });
- }
+ async function shouldExtractComponent(
+ path: 'NewComp' | 'NewComp.svelte' | './NewComp' | './NewComp.svelte'
+ ) {
+ const range = Range.create(Position.create(5, 8), Position.create(5, 25));
+ const result = await extractComponent(path, range);
+ assert.deepStrictEqual(result, {
+ documentChanges: [
+ TextDocumentEdit.create(
+ OptionalVersionedTextDocumentIdentifier.create('someUrl', null),
+ [
+ TextEdit.replace(range, ''),
+ TextEdit.insert(
+ doc.script?.startPos || Position.create(0, 0),
+ "\n import NewComp from './NewComp.svelte';\n"
+ )
+ ]
+ ),
+ CreateFile.create('file:///NewComp.svelte', { overwrite: true }),
+ TextDocumentEdit.create(
+ OptionalVersionedTextDocumentIdentifier.create(
+ 'file:///NewComp.svelte',
+ null
+ ),
+ [
+ TextEdit.insert(
+ Position.create(0, 0),
+ `${scriptContent}\n\nextract me
\n\n${styleContent}\n\n`
+ )
+ ]
+ )
+ ]
+ });
+ }
- it('should extract component (no .svelte at the end)', async () => {
- await shouldExtractComponent('./NewComp');
- });
+ it('should extract component (no .svelte at the end)', async () => {
+ await shouldExtractComponent('./NewComp');
+ });
- it('should extract component (no .svelte at the end, no relative path)', async () => {
- await shouldExtractComponent('NewComp');
- });
+ it('should extract component (no .svelte at the end, no relative path)', async () => {
+ await shouldExtractComponent('NewComp');
+ });
- it('should extract component (.svelte at the end, no relative path', async () => {
- await shouldExtractComponent('NewComp.svelte');
- });
+ it('should extract component (.svelte at the end, no relative path', async () => {
+ await shouldExtractComponent('NewComp.svelte');
+ });
- it('should extract component (.svelte at the end, relative path)', async () => {
- await shouldExtractComponent('./NewComp.svelte');
- });
+ it('should extract component (.svelte at the end, relative path)', async () => {
+ await shouldExtractComponent('./NewComp.svelte');
+ });
- it('should return "Invalid selection range"', async () => {
- const range = Range.create(Position.create(6, 8), Position.create(6, 25));
- const result = await extractComponent('Bla', range);
- assert.deepStrictEqual(result, 'Invalid selection range');
- });
+ it('should return "Invalid selection range"', async () => {
+ const range = Range.create(Position.create(6, 8), Position.create(6, 25));
+ const result = await extractComponent('Bla', range);
+ assert.deepStrictEqual(result, 'Invalid selection range');
+ });
- it('should update relative imports', async () => {
- const content = `
@@ -347,48 +347,48 @@ describe('SveltePlugin#getCodeAction', () => {
`;
- const existingFileUri = pathToUrl('C:/path/File.svelte');
- const doc = new SvelteDocument(new Document(existingFileUri, content));
- const range = Range.create(Position.create(4, 12), Position.create(4, 21));
- const result = await executeRefactoringCommand(doc, extractComponentCommand, [
- '',
- {
- filePath: '../NewComp',
- range,
- uri: ''
- }
- ]);
+ const existingFileUri = pathToUrl('C:/path/File.svelte');
+ const doc = new SvelteDocument(new Document(existingFileUri, content));
+ const range = Range.create(Position.create(4, 12), Position.create(4, 21));
+ const result = await executeRefactoringCommand(doc, extractComponentCommand, [
+ '',
+ {
+ filePath: '../NewComp',
+ range,
+ uri: ''
+ }
+ ]);
- const newFileUri = pathToUrl('C:/NewComp.svelte');
- assert.deepStrictEqual(result, {
- documentChanges: [
- TextDocumentEdit.create(
- OptionalVersionedTextDocumentIdentifier.create(existingFileUri, null),
- [
- TextEdit.replace(range, ''),
- TextEdit.insert(
- doc.script?.startPos || Position.create(0, 0),
- "\n import NewComp from '../NewComp.svelte';\n"
- )
- ]
- ),
- CreateFile.create(newFileUri, { overwrite: true }),
- TextDocumentEdit.create(
- OptionalVersionedTextDocumentIdentifier.create(newFileUri, null),
- [
- TextEdit.insert(
- Position.create(0, 0),
- `\n\ntoExtract\n\n\n\n`
- )
- ]
- )
- ]
- });
- });
- });
+ )
+ ]
+ )
+ ]
+ });
+ });
+ });
});
diff --git a/packages/language-server/test/plugins/svelte/features/getCompletions.test.ts b/packages/language-server/test/plugins/svelte/features/getCompletions.test.ts
index f7a26230f..9f282256d 100644
--- a/packages/language-server/test/plugins/svelte/features/getCompletions.test.ts
+++ b/packages/language-server/test/plugins/svelte/features/getCompletions.test.ts
@@ -6,125 +6,125 @@ import { SvelteDocument } from '../../../../src/plugins/svelte/SvelteDocument';
import { Document } from '../../../../src/lib/documents';
describe('SveltePlugin#getCompletions', () => {
- function expectCompletionsFor(
- content: string,
- position: Position = Position.create(0, content.length)
- ) {
- const svelteDoc = new SvelteDocument(new Document('url', content));
- const completions = getCompletions(svelteDoc, position);
- return {
- toEqual: (expectedLabels: string[] | null) =>
- assert.deepStrictEqual(
- completions?.items.map((item) => item.label) ?? null,
- expectedLabels
- )
- };
- }
-
- describe('should return null', () => {
- it('if position inside style', () => {
- expectCompletionsFor(
- 'test
',
- Position.create(0, 10)
- ).toEqual(null);
- });
-
- it('if position inside script', () => {
- expectCompletionsFor(
- 'test
',
- Position.create(0, 10)
- ).toEqual(null);
- });
-
- it('if not preceeded by valid content #1', () => {
- expectCompletionsFor('{nope').toEqual(null);
- });
-
- it('if not preceeded by valid content #2', () => {
- expectCompletionsFor('not really').toEqual(null);
- });
-
- it('if not preceeded by valid content #3', () => {
- expectCompletionsFor('{#awa.').toEqual(null);
- });
- });
-
- it('should return completions for #', () => {
- expectCompletionsFor('{#').toEqual(['if', 'each', 'await :then', 'await then', 'key']);
- });
-
- it('should return completions for @', () => {
- expectCompletionsFor('{@').toEqual(['html', 'debug']);
- });
-
- describe('should return no completions for :', () => {
- it(' when no open tag before that', () => {
- expectCompletionsFor('{:').toEqual(null);
- });
-
- it(' when only completed tag before that', () => {
- expectCompletionsFor('{#if}{/if}{:').toEqual(null);
- });
- });
-
- describe('should return no completions for /', () => {
- it('when no open tag before that', () => {
- expectCompletionsFor('{/').toEqual(null);
- });
-
- it('when only completed tag before that', () => {
- expectCompletionsFor('{#if}{/if}{/').toEqual(null);
- });
-
- it('when the only completed tag before it has white space before close symbol', () => {
- expectCompletionsFor('{#if}{ /if}{/').toEqual(null);
- });
- });
-
- describe('should return completion for :', () => {
- it('for if', () => {
- expectCompletionsFor('{#if}{:').toEqual(['else', 'else if']);
- });
-
- it('for each', () => {
- expectCompletionsFor('{#each}{:').toEqual(['else']);
- });
-
- it('for await', () => {
- expectCompletionsFor('{#await}{:').toEqual(['then', 'catch']);
- });
-
- it('for last open tag', () => {
- expectCompletionsFor('{#if}{/if}{#if}{#await}{:').toEqual(['then', 'catch']);
- });
- });
-
- describe('should return completion for /', () => {
- it('for if', () => {
- expectCompletionsFor('{#if}{/').toEqual(['if']);
- });
-
- it('for each', () => {
- expectCompletionsFor('{#each}{/').toEqual(['each']);
- });
-
- it('for await', () => {
- expectCompletionsFor('{#await}{/').toEqual(['await']);
- });
-
- it('for key', () => {
- expectCompletionsFor('{#key}{/').toEqual(['key']);
- });
-
- it('for last open tag', () => {
- expectCompletionsFor('{#if}{/if}{#if}{#await}{/').toEqual(['await']);
- });
- });
-
- it('should return completion for component documentation comment', () => {
- const content = ')|\{[^}"']*$/,
- // Matches a closing tag that:
- // - Follows optional whitespace
- // - Is not `