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}>`), + 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\n

extract 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\n

extract 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 `` - // Or matches `-->` - // Or closing curly brace - // - // eslint-disable-next-line no-useless-escape - decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/ - }, - // Matches a number or word that either: - // - Is a number with an optional negative sign and optional full number - // with numbers following the decimal point. e.g `-1.1px`, `.5`, `-.42rem`, etc - // - Is a sequence of characters without spaces and not containing - // any of the following: `~!@$^&*()=+[{]}\|;:'",.<>/ - // - // eslint-disable-next-line max-len, no-useless-escape - wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\#\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g, - onEnterRules: [ - { - // Matches an opening tag that: - // - Isn't an empty element - // - Is possibly namespaced - // - Isn't a void element - // - Isn't followed by another tag on the same line - // - // eslint-disable-next-line no-useless-escape - beforeText: new RegExp( - `<(?!(?:${EMPTY_ELEMENTS.join( - '|' - )}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, - 'i' - ), - // Matches a closing tag that: - // - Is possibly namespaced - // - Possibly has excess whitespace following tagname - afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i, - action: { indentAction: IndentAction.IndentOutdent } - }, - { - // Matches an opening tag that: - // - Isn't an empty element - // - Isn't namespaced - // - Isn't a void element - // - Isn't followed by another tag on the same line - // - // eslint-disable-next-line no-useless-escape - beforeText: new RegExp( - `<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, - 'i' - ), - action: { indentAction: IndentAction.Indent } - } - ] - }); - - // This API is considered private and only exposed for experimenting. - // Interface may change at any time. Use at your own risk! - return { - /** - * As a function, because restarting the server - * will result in another instance. - */ - getLanguageServer: getLS - }; + warnIfOldExtensionInstalled(); + + const runtimeConfig = workspace.getConfiguration('svelte.language-server'); + + const { workspaceFolders } = workspace; + const rootPath = Array.isArray(workspaceFolders) ? workspaceFolders[0].uri.fsPath : undefined; + + const tempLsPath = runtimeConfig.get('ls-path'); + // Returns undefined if path is empty string + // Return absolute path if not already + const lsPath = + tempLsPath && tempLsPath.trim() !== '' + ? path.isAbsolute(tempLsPath) + ? tempLsPath + : path.join(rootPath as string, tempLsPath) + : undefined; + + const serverModule = require.resolve(lsPath || 'svelte-language-server/bin/server.js'); + console.log('Loading server from ', serverModule); + + // Add --experimental-modules flag for people using node 12 < version < 12.17 + // Remove this in mid 2022 and bump vs code minimum required version to 1.55 + const runExecArgv: string[] = ['--experimental-modules']; + let port = runtimeConfig.get('port') ?? -1; + if (port < 0) { + port = 6009; + } else { + console.log('setting port to', port); + runExecArgv.push(`--inspect=${port}`); + } + const debugOptions = { execArgv: ['--nolazy', '--experimental-modules', `--inspect=${port}`] }; + + const serverOptions: ServerOptions = { + run: { + module: serverModule, + transport: TransportKind.ipc, + options: { execArgv: runExecArgv } + }, + debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } + }; + + const serverRuntime = runtimeConfig.get('runtime'); + if (serverRuntime) { + serverOptions.run.runtime = serverRuntime; + serverOptions.debug.runtime = serverRuntime; + console.log('setting server runtime to', serverRuntime); + } + + const clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: 'file', language: 'svelte' }], + revealOutputChannelOn: RevealOutputChannelOn.Never, + synchronize: { + configurationSection: ['svelte', 'javascript', 'typescript', 'prettier'], + fileEvents: workspace.createFileSystemWatcher('{**/*.js,**/*.ts}', false, false, false) + }, + initializationOptions: { + configuration: { + svelte: workspace.getConfiguration('svelte'), + prettier: workspace.getConfiguration('prettier'), + emmet: workspace.getConfiguration('emmet'), + typescript: workspace.getConfiguration('typescript'), + javascript: workspace.getConfiguration('javascript') + }, + dontFilterIncompleteCompletions: true // VSCode filters client side and is smarter at it than us + } + }; + + let ls = createLanguageServer(serverOptions, clientOptions); + context.subscriptions.push(ls.start()); + + ls.onReady().then(() => { + const tagRequestor = (document: TextDocument, position: Position) => { + const param = ls.code2ProtocolConverter.asTextDocumentPositionParams( + document, + position + ); + return ls.sendRequest(TagCloseRequest.type, param); + }; + const disposable = activateTagClosing( + tagRequestor, + { svelte: true }, + 'html.autoClosingTags' + ); + context.subscriptions.push(disposable); + }); + + workspace.onDidSaveTextDocument(async (doc) => { + const parts = doc.uri.toString(true).split(/\/|\\/); + if ( + [ + 'tsconfig.json', + 'jsconfig.json', + 'svelte.config.js', + 'svelte.config.cjs', + 'svelte.config.mjs' + ].includes(parts[parts.length - 1]) + ) { + await restartLS(false); + } + }); + + context.subscriptions.push( + commands.registerCommand('svelte.restartLanguageServer', async () => { + await restartLS(true); + }) + ); + + async function restartLS(showNotification: boolean) { + await ls.stop(); + ls = createLanguageServer(serverOptions, clientOptions); + context.subscriptions.push(ls.start()); + await ls.onReady(); + if (showNotification) { + window.showInformationMessage('Svelte language server restarted.'); + } + } + + function getLS() { + return ls; + } + + addDidChangeTextDocumentListener(getLS); + + addRenameFileListener(getLS); + + addCompilePreviewCommand(getLS, context); + + addExtracComponentCommand(getLS, context); + + languages.setLanguageConfiguration('svelte', { + indentationRules: { + // Matches a valid opening tag that is: + // - Not a doctype + // - Not a void element + // - Not a closing tag + // - Not followed by a closing tag of the same element + // Or matches `)|\{[^}"']*$/, + // Matches a closing tag that: + // - Follows optional whitespace + // - Is not `` + // Or matches `-->` + // Or closing curly brace + // + // eslint-disable-next-line no-useless-escape + decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/ + }, + // Matches a number or word that either: + // - Is a number with an optional negative sign and optional full number + // with numbers following the decimal point. e.g `-1.1px`, `.5`, `-.42rem`, etc + // - Is a sequence of characters without spaces and not containing + // any of the following: `~!@$^&*()=+[{]}\|;:'",.<>/ + // + // eslint-disable-next-line max-len, no-useless-escape + wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\#\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g, + onEnterRules: [ + { + // Matches an opening tag that: + // - Isn't an empty element + // - Is possibly namespaced + // - Isn't a void element + // - Isn't followed by another tag on the same line + // + // eslint-disable-next-line no-useless-escape + beforeText: new RegExp( + `<(?!(?:${EMPTY_ELEMENTS.join( + '|' + )}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, + 'i' + ), + // Matches a closing tag that: + // - Is possibly namespaced + // - Possibly has excess whitespace following tagname + afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i, + action: { indentAction: IndentAction.IndentOutdent } + }, + { + // Matches an opening tag that: + // - Isn't an empty element + // - Isn't namespaced + // - Isn't a void element + // - Isn't followed by another tag on the same line + // + // eslint-disable-next-line no-useless-escape + beforeText: new RegExp( + `<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, + 'i' + ), + action: { indentAction: IndentAction.Indent } + } + ] + }); + + // This API is considered private and only exposed for experimenting. + // Interface may change at any time. Use at your own risk! + return { + /** + * As a function, because restarting the server + * will result in another instance. + */ + getLanguageServer: getLS + }; } function addDidChangeTextDocumentListener(getLS: () => LanguageClient) { - // Only Svelte file changes are automatically notified through the inbuilt LSP - // because the extension says it's only responsible for Svelte files. - // Therefore we need to set this up for TS/JS files manually. - workspace.onDidChangeTextDocument((evt) => { - if (evt.document.languageId === 'typescript' || evt.document.languageId === 'javascript') { - getLS().sendNotification('$/onDidChangeTsOrJsFile', { - uri: evt.document.uri.toString(true), - changes: evt.contentChanges.map((c) => ({ - range: { - start: { line: c.range.start.line, character: c.range.start.character }, - end: { line: c.range.end.line, character: c.range.end.character } - }, - text: c.text - })) - }); - } - }); + // Only Svelte file changes are automatically notified through the inbuilt LSP + // because the extension says it's only responsible for Svelte files. + // Therefore we need to set this up for TS/JS files manually. + workspace.onDidChangeTextDocument((evt) => { + if (evt.document.languageId === 'typescript' || evt.document.languageId === 'javascript') { + getLS().sendNotification('$/onDidChangeTsOrJsFile', { + uri: evt.document.uri.toString(true), + changes: evt.contentChanges.map((c) => ({ + range: { + start: { line: c.range.start.line, character: c.range.start.character }, + end: { line: c.range.end.line, character: c.range.end.character } + }, + text: c.text + })) + }); + } + }); } function addRenameFileListener(getLS: () => LanguageClient) { - workspace.onDidRenameFiles(async (evt) => { - const oldUri = evt.files[0].oldUri.toString(true); - const parts = oldUri.split(/\/|\\/); - const lastPart = parts[parts.length - 1]; - // If user moves/renames a folder, the URI only contains the parts up to that folder, - // and not files. So in case the URI does not contain a '.', check for imports to update. - if ( - lastPart.includes('.') && - !['.ts', '.js', '.json', '.svelte'].some((ending) => lastPart.endsWith(ending)) - ) { - return; - } - - window.withProgress( - { location: ProgressLocation.Window, title: 'Updating Imports..' }, - async () => { - const editsForFileRename = await getLS().sendRequest( - '$/getEditsForFileRename', - // Right now files is always an array with a single entry. - // The signature was only designed that way to - maybe, in the future - - // have the possibility to change that. If that ever does, update this. - // In the meantime, just assume it's a single entry and simplify the - // rest of the logic that way. - { - oldUri, - newUri: evt.files[0].newUri.toString(true) - } - ); - if (!editsForFileRename) { - return; - } - - const workspaceEdit = new WorkspaceEdit(); - // Renaming a file should only result in edits of existing files - editsForFileRename.documentChanges?.filter(TextDocumentEdit.is).forEach((change) => - change.edits.forEach((edit) => { - workspaceEdit.replace( - Uri.parse(change.textDocument.uri), - new Range( - new Position(edit.range.start.line, edit.range.start.character), - new Position(edit.range.end.line, edit.range.end.character) - ), - edit.newText - ); - }) - ); - workspace.applyEdit(workspaceEdit); - } - ); - }); + workspace.onDidRenameFiles(async (evt) => { + const oldUri = evt.files[0].oldUri.toString(true); + const parts = oldUri.split(/\/|\\/); + const lastPart = parts[parts.length - 1]; + // If user moves/renames a folder, the URI only contains the parts up to that folder, + // and not files. So in case the URI does not contain a '.', check for imports to update. + if ( + lastPart.includes('.') && + !['.ts', '.js', '.json', '.svelte'].some((ending) => lastPart.endsWith(ending)) + ) { + return; + } + + window.withProgress( + { location: ProgressLocation.Window, title: 'Updating Imports..' }, + async () => { + const editsForFileRename = await getLS().sendRequest( + '$/getEditsForFileRename', + // Right now files is always an array with a single entry. + // The signature was only designed that way to - maybe, in the future - + // have the possibility to change that. If that ever does, update this. + // In the meantime, just assume it's a single entry and simplify the + // rest of the logic that way. + { + oldUri, + newUri: evt.files[0].newUri.toString(true) + } + ); + if (!editsForFileRename) { + return; + } + + const workspaceEdit = new WorkspaceEdit(); + // Renaming a file should only result in edits of existing files + editsForFileRename.documentChanges?.filter(TextDocumentEdit.is).forEach((change) => + change.edits.forEach((edit) => { + workspaceEdit.replace( + Uri.parse(change.textDocument.uri), + new Range( + new Position(edit.range.start.line, edit.range.start.character), + new Position(edit.range.end.line, edit.range.end.character) + ), + edit.newText + ); + }) + ); + workspace.applyEdit(workspaceEdit); + } + ); + }); } function addCompilePreviewCommand(getLS: () => LanguageClient, context: ExtensionContext) { - const compiledCodeContentProvider = new CompiledCodeContentProvider(getLS); - - context.subscriptions.push( - workspace.registerTextDocumentContentProvider( - CompiledCodeContentProvider.scheme, - compiledCodeContentProvider - ), - compiledCodeContentProvider - ); - - context.subscriptions.push( - commands.registerTextEditorCommand('svelte.showCompiledCodeToSide', async (editor) => { - if (editor?.document?.languageId !== 'svelte') { - return; - } - - const uri = editor.document.uri; - const svelteUri = CompiledCodeContentProvider.toSvelteSchemeUri(uri); - window.withProgress( - { location: ProgressLocation.Window, title: 'Compiling..' }, - async () => { - return await window.showTextDocument(svelteUri, { - preview: true, - viewColumn: ViewColumn.Beside - }); - } - ); - }) - ); + const compiledCodeContentProvider = new CompiledCodeContentProvider(getLS); + + context.subscriptions.push( + workspace.registerTextDocumentContentProvider( + CompiledCodeContentProvider.scheme, + compiledCodeContentProvider + ), + compiledCodeContentProvider + ); + + context.subscriptions.push( + commands.registerTextEditorCommand('svelte.showCompiledCodeToSide', async (editor) => { + if (editor?.document?.languageId !== 'svelte') { + return; + } + + const uri = editor.document.uri; + const svelteUri = CompiledCodeContentProvider.toSvelteSchemeUri(uri); + window.withProgress( + { location: ProgressLocation.Window, title: 'Compiling..' }, + async () => { + return await window.showTextDocument(svelteUri, { + preview: true, + viewColumn: ViewColumn.Beside + }); + } + ); + }) + ); } function addExtracComponentCommand(getLS: () => LanguageClient, context: ExtensionContext) { - context.subscriptions.push( - commands.registerTextEditorCommand('svelte.extractComponent', async (editor) => { - if (editor?.document?.languageId !== 'svelte') { - return; - } - - // Prompt for new component name - const options = { - prompt: 'Component Name: ', - placeHolder: 'NewComponent' - }; - - window.showInputBox(options).then(async (filePath) => { - if (!filePath) { - return window.showErrorMessage('No component name'); - } - - const uri = editor.document.uri.toString(); - const range = editor.selection; - getLS().sendRequest(ExecuteCommandRequest.type, { - command: 'extract_to_svelte_component', - arguments: [uri, { uri, range, filePath }] - }); - }); - }) - ); + context.subscriptions.push( + commands.registerTextEditorCommand('svelte.extractComponent', async (editor) => { + if (editor?.document?.languageId !== 'svelte') { + return; + } + + // Prompt for new component name + const options = { + prompt: 'Component Name: ', + placeHolder: 'NewComponent' + }; + + window.showInputBox(options).then(async (filePath) => { + if (!filePath) { + return window.showErrorMessage('No component name'); + } + + const uri = editor.document.uri.toString(); + const range = editor.selection; + getLS().sendRequest(ExecuteCommandRequest.type, { + command: 'extract_to_svelte_component', + arguments: [uri, { uri, range, filePath }] + }); + }); + }) + ); } function createLanguageServer(serverOptions: ServerOptions, clientOptions: LanguageClientOptions) { - return new LanguageClient('svelte', 'Svelte', serverOptions, clientOptions); + return new LanguageClient('svelte', 'Svelte', serverOptions, clientOptions); } function warnIfOldExtensionInstalled() { - if (extensions.getExtension('JamesBirtles.svelte-vscode')) { - window.showWarningMessage( - 'It seems you have the old and deprecated extension named "Svelte" installed. Please remove it. ' + - 'Through the UI: You can find it when searching for "@installed" in the extensions window (searching "Svelte" won\'t work). ' + - 'Command line: "code --uninstall-extension JamesBirtles.svelte-vscode"' - ); - } + if (extensions.getExtension('JamesBirtles.svelte-vscode')) { + window.showWarningMessage( + 'It seems you have the old and deprecated extension named "Svelte" installed. Please remove it. ' + + 'Through the UI: You can find it when searching for "@installed" in the extensions window (searching "Svelte" won\'t work). ' + + 'Command line: "code --uninstall-extension JamesBirtles.svelte-vscode"' + ); + } } diff --git a/packages/svelte-vscode/src/html/autoClose.ts b/packages/svelte-vscode/src/html/autoClose.ts index 1dc71b2b3..4e4e827a8 100644 --- a/packages/svelte-vscode/src/html/autoClose.ts +++ b/packages/svelte-vscode/src/html/autoClose.ts @@ -11,95 +11,95 @@ import { window, workspace, Disposable, TextDocument, Position, SnippetString } import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; export function activateTagClosing( - tagProvider: (document: TextDocument, position: Position) => Thenable, - supportedLanguages: { [id: string]: boolean }, - configName: string + tagProvider: (document: TextDocument, position: Position) => Thenable, + supportedLanguages: { [id: string]: boolean }, + configName: string ): Disposable { - const disposables: Disposable[] = []; - workspace.onDidChangeTextDocument( - (event) => onDidChangeTextDocument(event.document, event.contentChanges), - null, - disposables - ); + const disposables: Disposable[] = []; + workspace.onDidChangeTextDocument( + (event) => onDidChangeTextDocument(event.document, event.contentChanges), + null, + disposables + ); - let isEnabled = false; - updateEnabledState(); - window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables); + let isEnabled = false; + updateEnabledState(); + window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables); - let timeout: NodeJS.Timer | undefined = void 0; + let timeout: NodeJS.Timer | undefined = void 0; - function updateEnabledState() { - isEnabled = false; - const editor = window.activeTextEditor; - if (!editor) { - return; - } - const document = editor.document; - if (!supportedLanguages[document.languageId]) { - return; - } - if (!workspace.getConfiguration(void 0, document.uri).get(configName)) { - return; - } - isEnabled = true; - } + function updateEnabledState() { + isEnabled = false; + const editor = window.activeTextEditor; + if (!editor) { + return; + } + const document = editor.document; + if (!supportedLanguages[document.languageId]) { + return; + } + if (!workspace.getConfiguration(void 0, document.uri).get(configName)) { + return; + } + isEnabled = true; + } - function onDidChangeTextDocument( - document: TextDocument, - changes: readonly TextDocumentContentChangeEvent[] - ) { - if (!isEnabled) { - return; - } - const activeDocument = window.activeTextEditor && window.activeTextEditor.document; - if (document !== activeDocument || changes.length === 0) { - return; - } - if (typeof timeout !== 'undefined') { - clearTimeout(timeout); - } - const lastChange = changes[changes.length - 1]; - const lastCharacter = lastChange.text[lastChange.text.length - 1]; - if ( - ('range' in lastChange && (lastChange.rangeLength ?? 0) > 0) || - (lastCharacter !== '>' && lastCharacter !== '/') - ) { - return; - } - const rangeStart = - 'range' in lastChange - ? lastChange.range.start - : new Position(0, document.getText().length); - const version = document.version; - timeout = setTimeout(() => { - const position = new Position( - rangeStart.line, - rangeStart.character + lastChange.text.length - ); - tagProvider(document, position).then((text) => { - if (text && isEnabled) { - const activeEditor = window.activeTextEditor; - if (activeEditor) { - const activeDocument = activeEditor.document; - if (document === activeDocument && activeDocument.version === version) { - const selections = activeEditor.selections; - if ( - selections.length && - selections.some((s) => s.active.isEqual(position)) - ) { - activeEditor.insertSnippet( - new SnippetString(text), - selections.map((s) => s.active) - ); - } else { - activeEditor.insertSnippet(new SnippetString(text), position); - } - } - } - } - }); - timeout = void 0; - }, 100); - } - return Disposable.from(...disposables); + function onDidChangeTextDocument( + document: TextDocument, + changes: readonly TextDocumentContentChangeEvent[] + ) { + if (!isEnabled) { + return; + } + const activeDocument = window.activeTextEditor && window.activeTextEditor.document; + if (document !== activeDocument || changes.length === 0) { + return; + } + if (typeof timeout !== 'undefined') { + clearTimeout(timeout); + } + const lastChange = changes[changes.length - 1]; + const lastCharacter = lastChange.text[lastChange.text.length - 1]; + if ( + ('range' in lastChange && (lastChange.rangeLength ?? 0) > 0) || + (lastCharacter !== '>' && lastCharacter !== '/') + ) { + return; + } + const rangeStart = + 'range' in lastChange + ? lastChange.range.start + : new Position(0, document.getText().length); + const version = document.version; + timeout = setTimeout(() => { + const position = new Position( + rangeStart.line, + rangeStart.character + lastChange.text.length + ); + tagProvider(document, position).then((text) => { + if (text && isEnabled) { + const activeEditor = window.activeTextEditor; + if (activeEditor) { + const activeDocument = activeEditor.document; + if (document === activeDocument && activeDocument.version === version) { + const selections = activeEditor.selections; + if ( + selections.length && + selections.some((s) => s.active.isEqual(position)) + ) { + activeEditor.insertSnippet( + new SnippetString(text), + selections.map((s) => s.active) + ); + } else { + activeEditor.insertSnippet(new SnippetString(text), position); + } + } + } + } + }); + timeout = void 0; + }, 100); + } + return Disposable.from(...disposables); } diff --git a/packages/svelte-vscode/src/html/htmlEmptyTagsShared.ts b/packages/svelte-vscode/src/html/htmlEmptyTagsShared.ts index 99e6df7f7..84dd1f217 100644 --- a/packages/svelte-vscode/src/html/htmlEmptyTagsShared.ts +++ b/packages/svelte-vscode/src/html/htmlEmptyTagsShared.ts @@ -6,20 +6,20 @@ *--------------------------------------------------------------------------------------------*/ export const EMPTY_ELEMENTS: string[] = [ - 'area', - 'base', - 'br', - 'col', - 'embed', - 'hr', - 'img', - 'input', - 'keygen', - 'link', - 'menuitem', - 'meta', - 'param', - 'source', - 'track', - 'wbr' + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'menuitem', + 'meta', + 'param', + 'source', + 'track', + 'wbr' ]; diff --git a/packages/svelte-vscode/src/utils.ts b/packages/svelte-vscode/src/utils.ts index 867bf5154..ea078c5a1 100644 --- a/packages/svelte-vscode/src/utils.ts +++ b/packages/svelte-vscode/src/utils.ts @@ -1,9 +1,9 @@ export function atob(encoded: string) { - const buffer = Buffer.from(encoded, 'base64'); - return buffer.toString('utf8'); + const buffer = Buffer.from(encoded, 'base64'); + return buffer.toString('utf8'); } export function btoa(decoded: string) { - const buffer = Buffer.from(decoded, 'utf8'); - return buffer.toString('base64'); + const buffer = Buffer.from(decoded, 'utf8'); + return buffer.toString('base64'); } diff --git a/packages/svelte2tsx/src/estree.d.ts b/packages/svelte2tsx/src/estree.d.ts index fda53c16f..92e9ac897 100644 --- a/packages/svelte2tsx/src/estree.d.ts +++ b/packages/svelte2tsx/src/estree.d.ts @@ -6,17 +6,17 @@ import { BaseNode } from 'estree'; // to both estree and estree-walker. declare module 'estree-walker' { - export interface Node extends BaseNode { - start: number; - end: number; - [propName: string]: any; - } + export interface Node extends BaseNode { + start: number; + end: number; + [propName: string]: any; + } } declare module 'estree' { - export interface BaseNode { - start: number; - end: number; - [propName: string]: any; - } + export interface BaseNode { + start: number; + end: number; + [propName: string]: any; + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/index.ts b/packages/svelte2tsx/src/htmlxtojsx/index.ts index f650a8f71..648db942b 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/index.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/index.ts @@ -30,11 +30,11 @@ import { usesLet } from './utils/node-utils'; type Walker = (node: TemplateNode, parent: BaseNode, prop: string, index: number) => void; function stripDoctype(str: MagicString): void { - const regex = /(\n)?/i; - const result = regex.exec(str.original); - if (result) { - str.remove(result.index, result.index + result[0].length); - } + const regex = /(\n)?/i; + const result = regex.exec(str.original); + if (result) { + str.remove(result.index, result.index + result[0].length); + } } /** @@ -42,200 +42,200 @@ function stripDoctype(str: MagicString): void { * and converts it to JSX */ export function convertHtmlxToJsx( - str: MagicString, - ast: TemplateNode, - onWalk: Walker = null, - onLeave: Walker = null, - options: { preserveAttributeCase?: boolean } = {} + str: MagicString, + ast: TemplateNode, + onWalk: Walker = null, + onLeave: Walker = null, + options: { preserveAttributeCase?: boolean } = {} ): void { - const htmlx = str.original; - stripDoctype(str); - str.prepend('<>'); - str.append(''); + const htmlx = str.original; + stripDoctype(str); + str.prepend('<>'); + str.append(''); - const templateScopeManager = new TemplateScopeManager(); + const templateScopeManager = new TemplateScopeManager(); - let ifScope = new IfScope(templateScopeManager); + let ifScope = new IfScope(templateScopeManager); - walk(ast, { - enter: (node: TemplateNode, parent: BaseNode, prop: string, index: number) => { - try { - switch (node.type) { - case 'IfBlock': - handleIf(htmlx, str, node, ifScope); - if (!node.elseif) { - ifScope = ifScope.getChild(); - } - break; - case 'EachBlock': - templateScopeManager.eachEnter(node); - handleEach(htmlx, str, node, ifScope); - break; - case 'ElseBlock': - templateScopeManager.elseEnter(parent); - handleElse(htmlx, str, node, parent, ifScope); - break; - case 'AwaitBlock': - handleAwait(htmlx, str, node, ifScope, templateScopeManager); - break; - case 'PendingBlock': - templateScopeManager.awaitPendingEnter(node, parent); - handleAwaitPending(parent, htmlx, str, ifScope); - break; - case 'ThenBlock': - templateScopeManager.awaitThenEnter(node, parent); - handleAwaitThen(parent, htmlx, str, ifScope); - break; - case 'CatchBlock': - templateScopeManager.awaitCatchEnter(node, parent); - handleAwaitCatch(parent, htmlx, str, ifScope); - break; - case 'KeyBlock': - handleKey(htmlx, str, node); - break; - case 'RawMustacheTag': - handleRawHtml(htmlx, str, node); - break; - case 'DebugTag': - handleDebug(htmlx, str, node); - break; - case 'InlineComponent': - templateScopeManager.componentOrSlotTemplateOrElementEnter(node); - handleComponent( - htmlx, - str, - node, - parent, - ifScope, - templateScopeManager.value - ); - break; - case 'Element': - templateScopeManager.componentOrSlotTemplateOrElementEnter(node); - handleElement( - htmlx, - str, - node, - parent, - ifScope, - templateScopeManager.value - ); - break; - case 'Comment': - handleComment(str, node); - break; - case 'Binding': - handleBinding(htmlx, str, node as BaseDirective, parent); - break; - case 'Class': - handleClassDirective(str, node as BaseDirective); - break; - case 'Action': - handleActionDirective(htmlx, str, node as BaseDirective, parent); - break; - case 'Transition': - handleTransitionDirective(htmlx, str, node as BaseDirective, parent); - break; - case 'Animation': - handleAnimateDirective(htmlx, str, node as BaseDirective, parent); - break; - case 'Attribute': - handleAttribute( - htmlx, - str, - node as Attribute, - parent, - options.preserveAttributeCase - ); - break; - case 'EventHandler': - handleEventHandler(htmlx, str, node as BaseDirective, parent); - break; - case 'Options': - handleSvelteTag(htmlx, str, node); - break; - case 'Window': - handleSvelteTag(htmlx, str, node); - break; - case 'Head': - handleSvelteTag(htmlx, str, node); - break; - case 'Body': - handleSvelteTag(htmlx, str, node); - break; - case 'SlotTemplate': - handleSvelteTag(htmlx, str, node); - templateScopeManager.componentOrSlotTemplateOrElementEnter(node); - if (usesLet(node)) { - handleSlot( - htmlx, - str, - node, - parent, - getSlotName(node) || 'default', - ifScope, - templateScopeManager.value - ); - } - break; - case 'Text': - handleText(str, node as Text); - break; - } - if (onWalk) { - onWalk(node, parent, prop, index); - } - } catch (e) { - console.error('Error walking node ', node); - throw e; - } - }, + walk(ast, { + enter: (node: TemplateNode, parent: BaseNode, prop: string, index: number) => { + try { + switch (node.type) { + case 'IfBlock': + handleIf(htmlx, str, node, ifScope); + if (!node.elseif) { + ifScope = ifScope.getChild(); + } + break; + case 'EachBlock': + templateScopeManager.eachEnter(node); + handleEach(htmlx, str, node, ifScope); + break; + case 'ElseBlock': + templateScopeManager.elseEnter(parent); + handleElse(htmlx, str, node, parent, ifScope); + break; + case 'AwaitBlock': + handleAwait(htmlx, str, node, ifScope, templateScopeManager); + break; + case 'PendingBlock': + templateScopeManager.awaitPendingEnter(node, parent); + handleAwaitPending(parent, htmlx, str, ifScope); + break; + case 'ThenBlock': + templateScopeManager.awaitThenEnter(node, parent); + handleAwaitThen(parent, htmlx, str, ifScope); + break; + case 'CatchBlock': + templateScopeManager.awaitCatchEnter(node, parent); + handleAwaitCatch(parent, htmlx, str, ifScope); + break; + case 'KeyBlock': + handleKey(htmlx, str, node); + break; + case 'RawMustacheTag': + handleRawHtml(htmlx, str, node); + break; + case 'DebugTag': + handleDebug(htmlx, str, node); + break; + case 'InlineComponent': + templateScopeManager.componentOrSlotTemplateOrElementEnter(node); + handleComponent( + htmlx, + str, + node, + parent, + ifScope, + templateScopeManager.value + ); + break; + case 'Element': + templateScopeManager.componentOrSlotTemplateOrElementEnter(node); + handleElement( + htmlx, + str, + node, + parent, + ifScope, + templateScopeManager.value + ); + break; + case 'Comment': + handleComment(str, node); + break; + case 'Binding': + handleBinding(htmlx, str, node as BaseDirective, parent); + break; + case 'Class': + handleClassDirective(str, node as BaseDirective); + break; + case 'Action': + handleActionDirective(htmlx, str, node as BaseDirective, parent); + break; + case 'Transition': + handleTransitionDirective(htmlx, str, node as BaseDirective, parent); + break; + case 'Animation': + handleAnimateDirective(htmlx, str, node as BaseDirective, parent); + break; + case 'Attribute': + handleAttribute( + htmlx, + str, + node as Attribute, + parent, + options.preserveAttributeCase + ); + break; + case 'EventHandler': + handleEventHandler(htmlx, str, node as BaseDirective, parent); + break; + case 'Options': + handleSvelteTag(htmlx, str, node); + break; + case 'Window': + handleSvelteTag(htmlx, str, node); + break; + case 'Head': + handleSvelteTag(htmlx, str, node); + break; + case 'Body': + handleSvelteTag(htmlx, str, node); + break; + case 'SlotTemplate': + handleSvelteTag(htmlx, str, node); + templateScopeManager.componentOrSlotTemplateOrElementEnter(node); + if (usesLet(node)) { + handleSlot( + htmlx, + str, + node, + parent, + getSlotName(node) || 'default', + ifScope, + templateScopeManager.value + ); + } + break; + case 'Text': + handleText(str, node as Text); + break; + } + if (onWalk) { + onWalk(node, parent, prop, index); + } + } catch (e) { + console.error('Error walking node ', node); + throw e; + } + }, - leave: (node: TemplateNode, parent: BaseNode, prop: string, index: number) => { - try { - switch (node.type) { - case 'IfBlock': - if (!node.elseif) { - ifScope = ifScope.getParent(); - } - break; - case 'EachBlock': - templateScopeManager.eachLeave(node); - break; - case 'AwaitBlock': - templateScopeManager.awaitLeave(); - break; - case 'InlineComponent': - case 'Element': - case 'SlotTemplate': - templateScopeManager.componentOrSlotTemplateOrElementLeave(node); - break; - } - if (onLeave) { - onLeave(node, parent, prop, index); - } - } catch (e) { - console.error('Error leaving node ', node); - throw e; - } - } - }); + leave: (node: TemplateNode, parent: BaseNode, prop: string, index: number) => { + try { + switch (node.type) { + case 'IfBlock': + if (!node.elseif) { + ifScope = ifScope.getParent(); + } + break; + case 'EachBlock': + templateScopeManager.eachLeave(node); + break; + case 'AwaitBlock': + templateScopeManager.awaitLeave(); + break; + case 'InlineComponent': + case 'Element': + case 'SlotTemplate': + templateScopeManager.componentOrSlotTemplateOrElementLeave(node); + break; + } + if (onLeave) { + onLeave(node, parent, prop, index); + } + } catch (e) { + console.error('Error leaving node ', node); + throw e; + } + } + }); } /** * @internal For testing only */ export function htmlx2jsx( - htmlx: string, - options?: { emitOnTemplateError?: boolean; preserveAttributeCase: boolean } + htmlx: string, + options?: { emitOnTemplateError?: boolean; preserveAttributeCase: boolean } ) { - const ast = parseHtmlx(htmlx, options); - const str = new MagicString(htmlx); + const ast = parseHtmlx(htmlx, options); + const str = new MagicString(htmlx); - convertHtmlxToJsx(str, ast, null, null, options); + convertHtmlxToJsx(str, ast, null, null, options); - return { - map: str.generateMap({ hires: true }), - code: str.toString() - }; + return { + map: str.generateMap({ hires: true }), + code: str.toString() + }; } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/action-directive.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/action-directive.ts index 2081d5191..d94fee51a 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/action-directive.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/action-directive.ts @@ -6,26 +6,26 @@ import { BaseDirective, BaseNode } from '../../interfaces'; * use:xxx={params} ---> {...__sveltets_ensureAction(xxx(__sveltets_mapElementTag('ParentNodeName'),(params)))} */ export function handleActionDirective( - htmlx: string, - str: MagicString, - attr: BaseDirective, - parent: BaseNode + htmlx: string, + str: MagicString, + attr: BaseDirective, + parent: BaseNode ): void { - str.overwrite(attr.start, attr.start + 'use:'.length, '{...__sveltets_ensureAction('); + str.overwrite(attr.start, attr.start + 'use:'.length, '{...__sveltets_ensureAction('); - if (!attr.expression) { - str.appendLeft(attr.end, `(__sveltets_mapElementTag('${parent.name}')))}`); - return; - } + if (!attr.expression) { + str.appendLeft(attr.end, `(__sveltets_mapElementTag('${parent.name}')))}`); + return; + } - str.overwrite( - attr.start + `use:${attr.name}`.length, - attr.expression.start, - `(__sveltets_mapElementTag('${parent.name}'),(` - ); - str.appendLeft(attr.expression.end, ')))'); - const lastChar = htmlx[attr.end - 1]; - if (isQuote(lastChar)) { - str.remove(attr.end - 1, attr.end); - } + str.overwrite( + attr.start + `use:${attr.name}`.length, + attr.expression.start, + `(__sveltets_mapElementTag('${parent.name}'),(` + ); + str.appendLeft(attr.expression.end, ')))'); + const lastChar = htmlx[attr.end - 1]; + if (isQuote(lastChar)) { + str.remove(attr.end - 1, attr.end); + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/animation-directive.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/animation-directive.ts index 882ede55c..1519b8b17 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/animation-directive.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/animation-directive.ts @@ -6,30 +6,30 @@ import { BaseDirective, BaseNode } from '../../interfaces'; * animate:xxx(yyy) ---> {...__sveltets_ensureAnimation(xxx(__sveltets_mapElementTag('..'),__sveltets_AnimationMove,(yyy)))} */ export function handleAnimateDirective( - htmlx: string, - str: MagicString, - attr: BaseDirective, - parent: BaseNode + htmlx: string, + str: MagicString, + attr: BaseDirective, + parent: BaseNode ): void { - str.overwrite( - attr.start, - htmlx.indexOf(':', attr.start) + 1, - '{...__sveltets_ensureAnimation(' - ); + str.overwrite( + attr.start, + htmlx.indexOf(':', attr.start) + 1, + '{...__sveltets_ensureAnimation(' + ); - const nodeType = `__sveltets_mapElementTag('${parent.name}')`; + const nodeType = `__sveltets_mapElementTag('${parent.name}')`; - if (!attr.expression) { - str.appendLeft(attr.end, `(${nodeType},__sveltets_AnimationMove,{}))}`); - return; - } - str.overwrite( - htmlx.indexOf(':', attr.start) + 1 + `${attr.name}`.length, - attr.expression.start, - `(${nodeType},__sveltets_AnimationMove,(` - ); - str.appendLeft(attr.expression.end, ')))'); - if (isQuote(htmlx[attr.end - 1])) { - str.remove(attr.end - 1, attr.end); - } + if (!attr.expression) { + str.appendLeft(attr.end, `(${nodeType},__sveltets_AnimationMove,{}))}`); + return; + } + str.overwrite( + htmlx.indexOf(':', attr.start) + 1 + `${attr.name}`.length, + attr.expression.start, + `(${nodeType},__sveltets_AnimationMove,(` + ); + str.appendLeft(attr.expression.end, ')))'); + if (isQuote(htmlx[attr.end - 1])) { + str.remove(attr.end - 1, attr.end); + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts index f2801b277..b63ea4635 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts @@ -7,25 +7,25 @@ import { Attribute, BaseNode } from '../../interfaces'; * List taken from `svelte-jsx.d.ts` by searching for all attributes of type number */ const numberOnlyAttributes = new Set([ - 'cols', - 'colspan', - 'currenttime', - 'defaultplaybackrate', - 'high', - 'low', - 'marginheight', - 'marginwidth', - 'minlength', - 'maxlength', - 'optimum', - 'rows', - 'rowspan', - 'size', - 'span', - 'start', - 'tabindex', - 'results', - 'volume' + 'cols', + 'colspan', + 'currenttime', + 'defaultplaybackrate', + 'high', + 'low', + 'marginheight', + 'marginwidth', + 'minlength', + 'maxlength', + 'optimum', + 'rows', + 'rowspan', + 'size', + 'span', + 'start', + 'tabindex', + 'results', + 'volume' ]); /** @@ -36,162 +36,162 @@ const numberOnlyAttributes = new Set([ * - multi-value handling */ export function handleAttribute( - htmlx: string, - str: MagicString, - attr: Attribute, - parent: BaseNode, - preserveCase: boolean + htmlx: string, + str: MagicString, + attr: Attribute, + parent: BaseNode, + preserveCase: boolean ): void { - let transformedFromDirectiveOrNamespace = false; - - const transformAttributeCase = (name: string) => { - if (!preserveCase && !svgAttributes.find((x) => x == name)) { - return name.toLowerCase(); - } else { - return name; - } - }; - - //if we are on an "element" we are case insensitive, lowercase to match our JSX - if (parent.type == 'Element') { - const sapperLinkActions = ['sapper:prefetch', 'sapper:noscroll']; - const sveltekitLinkActions = ['sveltekit:prefetch', 'sveltekit:noscroll']; - // skip Attribute shorthand, that is handled below - if ( - (attr.value !== true && - !( - attr.value.length && - attr.value.length == 1 && - attr.value[0].type == 'AttributeShorthand' - )) || - sapperLinkActions.includes(attr.name) || - sveltekitLinkActions.includes(attr.name) - ) { - let name = transformAttributeCase(attr.name); - - //strip ":" from out attribute name and uppercase the next letter to convert to jsx attribute - const colonIndex = name.indexOf(':'); - if (colonIndex >= 0) { - const parts = name.split(':'); - name = parts[0] + parts[1][0].toUpperCase() + parts[1].substring(1); - } - - str.overwrite(attr.start, attr.start + attr.name.length, name); - - transformedFromDirectiveOrNamespace = true; - } - } - - //we are a bare attribute - if (attr.value === true) { - if ( - parent.type === 'Element' && - !transformedFromDirectiveOrNamespace && - parent.name !== '!DOCTYPE' - ) { - str.overwrite(attr.start, attr.end, transformAttributeCase(attr.name)); - } - return; - } - - if (attr.value.length == 0) return; //wut? - //handle single value - if (attr.value.length == 1) { - const attrVal = attr.value[0]; - - if (attr.name == 'slot') { - str.remove(attr.start, attr.end); - return; - } - - if (attrVal.type == 'AttributeShorthand') { - let attrName = attrVal.expression.name; - if (parent.type == 'Element') { - attrName = transformAttributeCase(attrName); - } - - str.appendRight(attr.start, `${attrName}=`); - return; - } - - const equals = htmlx.lastIndexOf('=', attrVal.start); - - const sanitizedName = sanitizeLeadingChars(attr.name); - if (sanitizedName !== attr.name) { - str.overwrite(attr.start, equals, sanitizedName); - } - - if (attrVal.type == 'Text') { - const endsWithQuote = - htmlx.lastIndexOf('"', attrVal.end) === attrVal.end - 1 || - htmlx.lastIndexOf("'", attrVal.end) === attrVal.end - 1; - const needsQuotes = attrVal.end == attr.end && !endsWithQuote; - - const hasBrackets = - htmlx.lastIndexOf('}', attrVal.end) === attrVal.end - 1 || - htmlx.lastIndexOf('}"', attrVal.end) === attrVal.end - 1 || - htmlx.lastIndexOf("}'", attrVal.end) === attrVal.end - 1; - const needsNumberConversion = - !hasBrackets && - parent.type === 'Element' && - numberOnlyAttributes.has(attr.name.toLowerCase()) && - !isNaN(attrVal.data); - - if (needsNumberConversion) { - if (needsQuotes) { - str.prependRight(equals + 1, '{'); - str.appendLeft(attr.end, '}'); - } else { - str.overwrite(equals + 1, equals + 2, '{'); - str.overwrite(attr.end - 1, attr.end, '}'); - } - } else if (needsQuotes) { - str.prependRight(equals + 1, '"'); - str.appendLeft(attr.end, '"'); - } - return; - } - - if (attrVal.type == 'MustacheTag') { - //if the end doesn't line up, we are wrapped in quotes - if (attrVal.end != attr.end) { - str.remove(attrVal.start - 1, attrVal.start); - str.remove(attr.end - 1, attr.end); - } - return; - } - return; - } - - // we have multiple attribute values, so we build a string out of them. - // technically the user can do something funky like attr="text "{value} or even attr=text{value} - // so instead of trying to maintain a nice sourcemap with prepends etc, we just overwrite the whole thing - - const equals = htmlx.lastIndexOf('=', attr.value[0].start); - str.overwrite(equals, attr.value[0].start, '={`'); - - for (const n of attr.value) { - if (n.type == 'MustacheTag') { - str.appendRight(n.start, '$'); - } - } - - if (isQuote(htmlx[attr.end - 1])) { - str.overwrite(attr.end - 1, attr.end, '`}'); - } else { - str.appendLeft(attr.end, '`}'); - } + let transformedFromDirectiveOrNamespace = false; + + const transformAttributeCase = (name: string) => { + if (!preserveCase && !svgAttributes.find((x) => x == name)) { + return name.toLowerCase(); + } else { + return name; + } + }; + + //if we are on an "element" we are case insensitive, lowercase to match our JSX + if (parent.type == 'Element') { + const sapperLinkActions = ['sapper:prefetch', 'sapper:noscroll']; + const sveltekitLinkActions = ['sveltekit:prefetch', 'sveltekit:noscroll']; + // skip Attribute shorthand, that is handled below + if ( + (attr.value !== true && + !( + attr.value.length && + attr.value.length == 1 && + attr.value[0].type == 'AttributeShorthand' + )) || + sapperLinkActions.includes(attr.name) || + sveltekitLinkActions.includes(attr.name) + ) { + let name = transformAttributeCase(attr.name); + + //strip ":" from out attribute name and uppercase the next letter to convert to jsx attribute + const colonIndex = name.indexOf(':'); + if (colonIndex >= 0) { + const parts = name.split(':'); + name = parts[0] + parts[1][0].toUpperCase() + parts[1].substring(1); + } + + str.overwrite(attr.start, attr.start + attr.name.length, name); + + transformedFromDirectiveOrNamespace = true; + } + } + + //we are a bare attribute + if (attr.value === true) { + if ( + parent.type === 'Element' && + !transformedFromDirectiveOrNamespace && + parent.name !== '!DOCTYPE' + ) { + str.overwrite(attr.start, attr.end, transformAttributeCase(attr.name)); + } + return; + } + + if (attr.value.length == 0) return; //wut? + //handle single value + if (attr.value.length == 1) { + const attrVal = attr.value[0]; + + if (attr.name == 'slot') { + str.remove(attr.start, attr.end); + return; + } + + if (attrVal.type == 'AttributeShorthand') { + let attrName = attrVal.expression.name; + if (parent.type == 'Element') { + attrName = transformAttributeCase(attrName); + } + + str.appendRight(attr.start, `${attrName}=`); + return; + } + + const equals = htmlx.lastIndexOf('=', attrVal.start); + + const sanitizedName = sanitizeLeadingChars(attr.name); + if (sanitizedName !== attr.name) { + str.overwrite(attr.start, equals, sanitizedName); + } + + if (attrVal.type == 'Text') { + const endsWithQuote = + htmlx.lastIndexOf('"', attrVal.end) === attrVal.end - 1 || + htmlx.lastIndexOf("'", attrVal.end) === attrVal.end - 1; + const needsQuotes = attrVal.end == attr.end && !endsWithQuote; + + const hasBrackets = + htmlx.lastIndexOf('}', attrVal.end) === attrVal.end - 1 || + htmlx.lastIndexOf('}"', attrVal.end) === attrVal.end - 1 || + htmlx.lastIndexOf("}'", attrVal.end) === attrVal.end - 1; + const needsNumberConversion = + !hasBrackets && + parent.type === 'Element' && + numberOnlyAttributes.has(attr.name.toLowerCase()) && + !isNaN(attrVal.data); + + if (needsNumberConversion) { + if (needsQuotes) { + str.prependRight(equals + 1, '{'); + str.appendLeft(attr.end, '}'); + } else { + str.overwrite(equals + 1, equals + 2, '{'); + str.overwrite(attr.end - 1, attr.end, '}'); + } + } else if (needsQuotes) { + str.prependRight(equals + 1, '"'); + str.appendLeft(attr.end, '"'); + } + return; + } + + if (attrVal.type == 'MustacheTag') { + //if the end doesn't line up, we are wrapped in quotes + if (attrVal.end != attr.end) { + str.remove(attrVal.start - 1, attrVal.start); + str.remove(attr.end - 1, attr.end); + } + return; + } + return; + } + + // we have multiple attribute values, so we build a string out of them. + // technically the user can do something funky like attr="text "{value} or even attr=text{value} + // so instead of trying to maintain a nice sourcemap with prepends etc, we just overwrite the whole thing + + const equals = htmlx.lastIndexOf('=', attr.value[0].start); + str.overwrite(equals, attr.value[0].start, '={`'); + + for (const n of attr.value) { + if (n.type == 'MustacheTag') { + str.appendRight(n.start, '$'); + } + } + + if (isQuote(htmlx[attr.end - 1])) { + str.overwrite(attr.end - 1, attr.end, '`}'); + } else { + str.appendLeft(attr.end, '`}'); + } } function sanitizeLeadingChars(attrName: string): string { - let sanitizedName = ''; - for (let i = 0; i < attrName.length; i++) { - if (/[A-Za-z$_]/.test(attrName[i])) { - sanitizedName += attrName.substr(i); - return sanitizedName; - } else { - sanitizedName += '_'; - } - } - return sanitizedName; + let sanitizedName = ''; + for (let i = 0; i < attrName.length; i++) { + if (/[A-Za-z$_]/.test(attrName[i])) { + sanitizedName += attrName.substr(i); + return sanitizedName; + } else { + sanitizedName += '_'; + } + } + return sanitizedName; } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts index d3e5a7748..660929ed9 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts @@ -8,120 +8,120 @@ import { BaseNode } from '../../interfaces'; * Transform {#await ...} into something JSX understands */ export function handleAwait( - htmlx: string, - str: MagicString, - awaitBlock: BaseNode, - ifScope: IfScope, - templateScopeManager: TemplateScopeManager + htmlx: string, + str: MagicString, + awaitBlock: BaseNode, + ifScope: IfScope, + templateScopeManager: TemplateScopeManager ): void { - // {#await somePromise then value} -> - // {() => {let _$$p = (somePromise); - let ifCondition = ifScope.getFullCondition(); - ifCondition = ifCondition ? surroundWithIgnoreComments(`if(${ifCondition}) {`) : ''; - templateScopeManager.awaitEnter(awaitBlock); - const constRedeclares = ifScope.getConstsToRedeclare(); - str.overwrite( - awaitBlock.start, - awaitBlock.expression.start, - `{() => {${constRedeclares}${ifCondition}let _$$p = (` - ); + // {#await somePromise then value} -> + // {() => {let _$$p = (somePromise); + let ifCondition = ifScope.getFullCondition(); + ifCondition = ifCondition ? surroundWithIgnoreComments(`if(${ifCondition}) {`) : ''; + templateScopeManager.awaitEnter(awaitBlock); + const constRedeclares = ifScope.getConstsToRedeclare(); + str.overwrite( + awaitBlock.start, + awaitBlock.expression.start, + `{() => {${constRedeclares}${ifCondition}let _$$p = (` + ); - // {/await} -> - // <>})} - const awaitEndStart = htmlx.lastIndexOf('{', awaitBlock.end - 1); - str.overwrite(awaitEndStart, awaitBlock.end, '})}}' + (ifCondition ? '}' : '')); + // {/await} -> + // <>})} + const awaitEndStart = htmlx.lastIndexOf('{', awaitBlock.end - 1); + str.overwrite(awaitEndStart, awaitBlock.end, '})}}' + (ifCondition ? '}' : '')); } export function handleAwaitPending( - awaitBlock: BaseNode, - htmlx: string, - str: MagicString, - ifScope: IfScope + awaitBlock: BaseNode, + htmlx: string, + str: MagicString, + ifScope: IfScope ): void { - if (awaitBlock.pending.skip) { - return; - } + if (awaitBlock.pending.skip) { + return; + } - // {await aPromise} ... -> aPromise); (possibleIfCondition &&)<> ... - const pendingStart = htmlx.indexOf('}', awaitBlock.expression.end); - const pendingEnd = !awaitBlock.then.skip - ? awaitBlock.then.start - : !awaitBlock.catch.skip - ? awaitBlock.catch.start - : htmlx.lastIndexOf('{', awaitBlock.end); - str.overwrite(awaitBlock.expression.end, pendingStart + 1, ');'); - str.appendRight(pendingStart + 1, ` ${ifScope.addPossibleIfCondition()}<>`); - str.appendLeft(pendingEnd, '; '); + // {await aPromise} ... -> aPromise); (possibleIfCondition &&)<> ... + const pendingStart = htmlx.indexOf('}', awaitBlock.expression.end); + const pendingEnd = !awaitBlock.then.skip + ? awaitBlock.then.start + : !awaitBlock.catch.skip + ? awaitBlock.catch.start + : htmlx.lastIndexOf('{', awaitBlock.end); + str.overwrite(awaitBlock.expression.end, pendingStart + 1, ');'); + str.appendRight(pendingStart + 1, ` ${ifScope.addPossibleIfCondition()}<>`); + str.appendLeft(pendingEnd, '; '); - if (!awaitBlock.then.skip) { - return; - } - // no need to prepend ifcondition here as we know the then block is empty - str.appendLeft(pendingEnd, '__sveltets_awaitThen(_$$p, () => {<>'); + if (!awaitBlock.then.skip) { + return; + } + // no need to prepend ifcondition here as we know the then block is empty + str.appendLeft(pendingEnd, '__sveltets_awaitThen(_$$p, () => {<>'); } export function handleAwaitThen( - awaitBlock: BaseNode, - htmlx: string, - str: MagicString, - ifScope: IfScope + awaitBlock: BaseNode, + htmlx: string, + str: MagicString, + ifScope: IfScope ): void { - if (awaitBlock.then.skip) { - return; - } + if (awaitBlock.then.skip) { + return; + } - // then value } | {:then value} | {await ..} .. {/await} -> - // __sveltets_awaitThen(_$$p, (value) => {(possibleIfCondition && )<> - let thenStart: number; - let thenEnd: number; - // then value } | {:then value} - if (!awaitBlock.pending.skip) { - // {await ...} ... {:then ...} - // thenBlock includes the {:then} - thenStart = awaitBlock.then.start; - if (awaitBlock.value) { - thenEnd = htmlx.indexOf('}', awaitBlock.value.end) + 1; - } else { - thenEnd = htmlx.indexOf('}', awaitBlock.then.start) + 1; - } - } else { - // {await ... then ...} - thenStart = htmlx.indexOf('then', awaitBlock.expression.end); - thenEnd = htmlx.lastIndexOf('}', awaitBlock.then.start) + 1; - // somePromise then -> somePromise); then - str.overwrite(awaitBlock.expression.end, thenStart, '); '); - } + // then value } | {:then value} | {await ..} .. {/await} -> + // __sveltets_awaitThen(_$$p, (value) => {(possibleIfCondition && )<> + let thenStart: number; + let thenEnd: number; + // then value } | {:then value} + if (!awaitBlock.pending.skip) { + // {await ...} ... {:then ...} + // thenBlock includes the {:then} + thenStart = awaitBlock.then.start; + if (awaitBlock.value) { + thenEnd = htmlx.indexOf('}', awaitBlock.value.end) + 1; + } else { + thenEnd = htmlx.indexOf('}', awaitBlock.then.start) + 1; + } + } else { + // {await ... then ...} + thenStart = htmlx.indexOf('then', awaitBlock.expression.end); + thenEnd = htmlx.lastIndexOf('}', awaitBlock.then.start) + 1; + // somePromise then -> somePromise); then + str.overwrite(awaitBlock.expression.end, thenStart, '); '); + } - if (awaitBlock.value) { - str.overwrite(thenStart, awaitBlock.value.start, '__sveltets_awaitThen(_$$p, ('); - str.overwrite(awaitBlock.value.end, thenEnd, `) => {${ifScope.addPossibleIfCondition()}<>`); - } else { - const awaitThenFn = `__sveltets_awaitThen(_$$p, () => {${ifScope.addPossibleIfCondition()}<>`; // eslint-disable-line - if (thenStart === thenEnd) { - str.appendLeft(thenStart, awaitThenFn); - } else { - str.overwrite(thenStart, thenEnd, awaitThenFn); - } - } + if (awaitBlock.value) { + str.overwrite(thenStart, awaitBlock.value.start, '__sveltets_awaitThen(_$$p, ('); + str.overwrite(awaitBlock.value.end, thenEnd, `) => {${ifScope.addPossibleIfCondition()}<>`); + } else { + const awaitThenFn = `__sveltets_awaitThen(_$$p, () => {${ifScope.addPossibleIfCondition()}<>`; // eslint-disable-line + if (thenStart === thenEnd) { + str.appendLeft(thenStart, awaitThenFn); + } else { + str.overwrite(thenStart, thenEnd, awaitThenFn); + } + } } export function handleAwaitCatch( - awaitBlock: BaseNode, - htmlx: string, - str: MagicString, - ifScope: IfScope + awaitBlock: BaseNode, + htmlx: string, + str: MagicString, + ifScope: IfScope ): void { - //{:catch error} -> - //}, (error) => {<> - if (!awaitBlock.catch.skip) { - //catch block includes the {:catch} - const catchStart = awaitBlock.catch.start; - const catchSymbolEnd = htmlx.indexOf(':catch', catchStart) + ':catch'.length; + //{:catch error} -> + //}, (error) => {<> + if (!awaitBlock.catch.skip) { + //catch block includes the {:catch} + const catchStart = awaitBlock.catch.start; + const catchSymbolEnd = htmlx.indexOf(':catch', catchStart) + ':catch'.length; - const errorStart = awaitBlock.error ? awaitBlock.error.start : catchSymbolEnd; - const errorEnd = awaitBlock.error ? awaitBlock.error.end : errorStart; - const catchEnd = htmlx.indexOf('}', errorEnd) + 1; - str.overwrite(catchStart, errorStart, '}, ('); - str.overwrite(errorEnd, catchEnd, `) => {${ifScope.addPossibleIfCondition()}<>`); - } + const errorStart = awaitBlock.error ? awaitBlock.error.start : catchSymbolEnd; + const errorEnd = awaitBlock.error ? awaitBlock.error.end : errorStart; + const catchEnd = htmlx.indexOf('}', errorEnd) + 1; + str.overwrite(catchStart, errorStart, '}, ('); + str.overwrite(errorEnd, catchEnd, `) => {${ifScope.addPossibleIfCondition()}<>`); + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts index 70570be17..c26253ae4 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts @@ -3,91 +3,91 @@ import { isShortHandAttribute, getThisType, isQuote } from '../utils/node-utils' import { BaseDirective, BaseNode } from '../../interfaces'; const oneWayBindingAttributes: Map = new Map( - ['clientWidth', 'clientHeight', 'offsetWidth', 'offsetHeight'] - .map((e) => [e, 'HTMLDivElement'] as [string, string]) - .concat( - ['duration', 'buffered', 'seekable', 'seeking', 'played', 'ended'].map((e) => [ - e, - 'HTMLMediaElement' - ]) - ) + ['clientWidth', 'clientHeight', 'offsetWidth', 'offsetHeight'] + .map((e) => [e, 'HTMLDivElement'] as [string, string]) + .concat( + ['duration', 'buffered', 'seekable', 'seeking', 'played', 'ended'].map((e) => [ + e, + 'HTMLMediaElement' + ]) + ) ); /** * Transform bind:xxx into something that conforms to JSX */ export function handleBinding( - htmlx: string, - str: MagicString, - attr: BaseDirective, - el: BaseNode + htmlx: string, + str: MagicString, + attr: BaseDirective, + el: BaseNode ): void { - //bind group on input - if (attr.name == 'group' && el.name == 'input') { - str.remove(attr.start, attr.expression.start); - str.appendLeft(attr.expression.start, '{...__sveltets_empty('); + //bind group on input + if (attr.name == 'group' && el.name == 'input') { + str.remove(attr.start, attr.expression.start); + str.appendLeft(attr.expression.start, '{...__sveltets_empty('); - const endBrackets = ')}'; - if (isShortHandAttribute(attr)) { - str.prependRight(attr.end, endBrackets); - } else { - str.overwrite(attr.expression.end, attr.end, endBrackets); - } - return; - } + const endBrackets = ')}'; + if (isShortHandAttribute(attr)) { + str.prependRight(attr.end, endBrackets); + } else { + str.overwrite(attr.expression.end, attr.end, endBrackets); + } + return; + } - const supportsBindThis = [ - 'InlineComponent', - 'Element', - 'Body', - 'Slot' // only valid for Web Components compile target - ]; + const supportsBindThis = [ + 'InlineComponent', + 'Element', + 'Body', + 'Slot' // only valid for Web Components compile target + ]; - //bind this - if (attr.name === 'this' && supportsBindThis.includes(el.type)) { - const thisType = getThisType(el); + //bind this + if (attr.name === 'this' && supportsBindThis.includes(el.type)) { + const thisType = getThisType(el); - if (thisType) { - str.remove(attr.start, attr.expression.start); - str.appendLeft(attr.expression.start, `{...__sveltets_ensureType(${thisType}, `); - str.overwrite(attr.expression.end, attr.end, ')}'); - return; - } - } + if (thisType) { + str.remove(attr.start, attr.expression.start); + str.appendLeft(attr.expression.start, `{...__sveltets_ensureType(${thisType}, `); + str.overwrite(attr.expression.end, attr.end, ')}'); + return; + } + } - //one way binding - if (oneWayBindingAttributes.has(attr.name) && el.type === 'Element') { - str.remove(attr.start, attr.expression.start); - str.appendLeft(attr.expression.start, '{...__sveltets_empty('); - if (isShortHandAttribute(attr)) { - // eslint-disable-next-line max-len - str.appendLeft( - attr.end, - `=__sveltets_instanceOf(${oneWayBindingAttributes.get(attr.name)}).${attr.name})}` - ); - } else { - // eslint-disable-next-line max-len - str.overwrite( - attr.expression.end, - attr.end, - `=__sveltets_instanceOf(${oneWayBindingAttributes.get(attr.name)}).${attr.name})}` - ); - } - return; - } + //one way binding + if (oneWayBindingAttributes.has(attr.name) && el.type === 'Element') { + str.remove(attr.start, attr.expression.start); + str.appendLeft(attr.expression.start, '{...__sveltets_empty('); + if (isShortHandAttribute(attr)) { + // eslint-disable-next-line max-len + str.appendLeft( + attr.end, + `=__sveltets_instanceOf(${oneWayBindingAttributes.get(attr.name)}).${attr.name})}` + ); + } else { + // eslint-disable-next-line max-len + str.overwrite( + attr.expression.end, + attr.end, + `=__sveltets_instanceOf(${oneWayBindingAttributes.get(attr.name)}).${attr.name})}` + ); + } + return; + } - str.remove(attr.start, attr.start + 'bind:'.length); - if (attr.expression.start === attr.start + 'bind:'.length) { - str.prependLeft(attr.expression.start, `${attr.name}={`); - str.appendLeft(attr.end, '}'); - return; - } + str.remove(attr.start, attr.start + 'bind:'.length); + if (attr.expression.start === attr.start + 'bind:'.length) { + str.prependLeft(attr.expression.start, `${attr.name}={`); + str.appendLeft(attr.end, '}'); + return; + } - //remove possible quotes - const lastChar = htmlx[attr.end - 1]; - if (isQuote(lastChar)) { - const firstQuote = htmlx.indexOf(lastChar, attr.start); - str.remove(firstQuote, firstQuote + 1); - str.remove(attr.end - 1, attr.end); - } + //remove possible quotes + const lastChar = htmlx[attr.end - 1]; + if (isQuote(lastChar)) { + const firstQuote = htmlx.indexOf(lastChar, attr.start); + str.remove(firstQuote, firstQuote + 1); + str.remove(attr.end - 1, attr.end); + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/class-directive.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/class-directive.ts index 28edc264a..171549c43 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/class-directive.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/class-directive.ts @@ -5,11 +5,11 @@ import { BaseDirective } from '../../interfaces'; * class:xx={yyy} ---> {...__sveltets_ensureType(Boolean, !!(yyy))} */ export function handleClassDirective(str: MagicString, attr: BaseDirective): void { - str.overwrite(attr.start, attr.expression.start, '{...__sveltets_ensureType(Boolean, !!('); - const endBrackets = '))}'; - if (attr.end !== attr.expression.end) { - str.overwrite(attr.expression.end, attr.end, endBrackets); - } else { - str.appendLeft(attr.end, endBrackets); - } + str.overwrite(attr.start, attr.expression.start, '{...__sveltets_ensureType(Boolean, !!('); + const endBrackets = '))}'; + if (attr.end !== attr.expression.end) { + str.overwrite(attr.expression.end, attr.end, endBrackets); + } else { + str.appendLeft(attr.end, endBrackets); + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/comment.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/comment.ts index 0c6ab2343..021af7c4b 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/comment.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/comment.ts @@ -5,5 +5,5 @@ import { BaseNode } from '../../interfaces'; * Removes comment */ export function handleComment(str: MagicString, node: BaseNode): void { - str.overwrite(node.start, node.end, '', { contentOnly: true }); + str.overwrite(node.start, node.end, '', { contentOnly: true }); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts index a3d9ba6cf..62fbfedb5 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts @@ -9,34 +9,34 @@ import { BaseNode } from '../../interfaces'; * Handle `` and slot-specific transformations. */ export function handleComponent( - htmlx: string, - str: MagicString, - el: BaseNode, - parent: BaseNode, - ifScope: IfScope, - templateScope: TemplateScope + htmlx: string, + str: MagicString, + el: BaseNode, + parent: BaseNode, + ifScope: IfScope, + templateScope: TemplateScope ): void { - //we need to remove : if it is a svelte component - if (el.name.startsWith('svelte:')) { - const colon = htmlx.indexOf(':', el.start); - str.remove(colon, colon + 1); + //we need to remove : if it is a svelte component + if (el.name.startsWith('svelte:')) { + const colon = htmlx.indexOf(':', el.start); + str.remove(colon, colon + 1); - const closeTag = htmlx.lastIndexOf('/' + el.name, el.end); - if (closeTag > el.start) { - const colon = htmlx.indexOf(':', closeTag); - str.remove(colon, colon + 1); - } - } + const closeTag = htmlx.lastIndexOf('/' + el.name, el.end); + if (closeTag > el.start) { + const colon = htmlx.indexOf(':', closeTag); + str.remove(colon, colon + 1); + } + } - // Handle possible slot - const slotName = getSlotName(el) || 'default'; - handleSlot( - htmlx, - str, - el, - slotName === 'default' ? el : parent, - slotName, - ifScope, - templateScope - ); + // Handle possible slot + const slotName = getSlotName(el) || 'default'; + handleSlot( + htmlx, + str, + el, + slotName === 'default' ? el : parent, + slotName, + ifScope, + templateScope + ); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/debug.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/debug.ts index 1a8dc4778..88ac459c7 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/debug.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/debug.ts @@ -7,12 +7,12 @@ import { BaseNode } from '../../interfaces'; * tsx won't accept commas, must split */ export function handleDebug(_htmlx: string, str: MagicString, debugBlock: BaseNode): void { - let cursor = debugBlock.start; - for (const identifier of debugBlock.identifiers as BaseNode[]) { - str.remove(cursor, identifier.start); - str.prependLeft(identifier.start, '{'); - str.prependLeft(identifier.end, '}'); - cursor = identifier.end; - } - str.remove(cursor, debugBlock.end); + let cursor = debugBlock.start; + for (const identifier of debugBlock.identifiers as BaseNode[]) { + str.remove(cursor, identifier.start); + str.prependLeft(identifier.start, '{'); + str.prependLeft(identifier.end, '}'); + cursor = identifier.end; + } + str.remove(cursor, debugBlock.end); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts index 174beca87..71f65bf29 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts @@ -6,47 +6,47 @@ import { IfScope } from './if-scope'; * Transform each block into something JSX can understand. */ export function handleEach( - htmlx: string, - str: MagicString, - eachBlock: Node, - ifScope: IfScope + htmlx: string, + str: MagicString, + eachBlock: Node, + ifScope: IfScope ): void { - // {#each items as item,i (key)} -> - // {__sveltets_each(items, (item,i) => (key) && (possible if expression &&) <> - const constRedeclares = ifScope.getConstsToRedeclare(); - const prefix = constRedeclares ? `{() => {${constRedeclares}() => ` : ''; - str.overwrite(eachBlock.start, eachBlock.expression.start, `${prefix}{__sveltets_each(`); - str.overwrite(eachBlock.expression.end, eachBlock.context.start, ', ('); + // {#each items as item,i (key)} -> + // {__sveltets_each(items, (item,i) => (key) && (possible if expression &&) <> + const constRedeclares = ifScope.getConstsToRedeclare(); + const prefix = constRedeclares ? `{() => {${constRedeclares}() => ` : ''; + str.overwrite(eachBlock.start, eachBlock.expression.start, `${prefix}{__sveltets_each(`); + str.overwrite(eachBlock.expression.end, eachBlock.context.start, ', ('); - // {#each true, items as item} - if (eachBlock.expression.type === 'SequenceExpression') { - str.appendRight(eachBlock.expression.start, '('); - str.appendLeft(eachBlock.expression.end, ')'); - } + // {#each true, items as item} + if (eachBlock.expression.type === 'SequenceExpression') { + str.appendRight(eachBlock.expression.start, '('); + str.appendLeft(eachBlock.expression.end, ')'); + } - let contextEnd = eachBlock.context.end; - if (eachBlock.index) { - const idxLoc = htmlx.indexOf(eachBlock.index, contextEnd); - contextEnd = idxLoc + eachBlock.index.length; - } - str.prependLeft(contextEnd, ') =>'); - if (eachBlock.key) { - const endEachStart = htmlx.indexOf('}', eachBlock.key.end); - str.overwrite(endEachStart, endEachStart + 1, ` && ${ifScope.addPossibleIfCondition()}<>`); - } else { - const endEachStart = htmlx.indexOf('}', contextEnd); - str.overwrite(endEachStart, endEachStart + 1, ` ${ifScope.addPossibleIfCondition()}<>`); - } + let contextEnd = eachBlock.context.end; + if (eachBlock.index) { + const idxLoc = htmlx.indexOf(eachBlock.index, contextEnd); + contextEnd = idxLoc + eachBlock.index.length; + } + str.prependLeft(contextEnd, ') =>'); + if (eachBlock.key) { + const endEachStart = htmlx.indexOf('}', eachBlock.key.end); + str.overwrite(endEachStart, endEachStart + 1, ` && ${ifScope.addPossibleIfCondition()}<>`); + } else { + const endEachStart = htmlx.indexOf('}', contextEnd); + str.overwrite(endEachStart, endEachStart + 1, ` ${ifScope.addPossibleIfCondition()}<>`); + } - const endEach = htmlx.lastIndexOf('{', eachBlock.end - 1); - const suffix = constRedeclares ? ')}}}' : ')}'; - // {/each} -> )} or {:else} -> )} - if (eachBlock.else) { - const elseEnd = htmlx.lastIndexOf('}', eachBlock.else.start); - const elseStart = htmlx.lastIndexOf('{', elseEnd); - str.overwrite(elseStart, elseEnd + 1, suffix); - str.remove(endEach, eachBlock.end); - } else { - str.overwrite(endEach, eachBlock.end, suffix); - } + const endEach = htmlx.lastIndexOf('{', eachBlock.end - 1); + const suffix = constRedeclares ? ')}}}' : ')}'; + // {/each} -> )} or {:else} -> )} + if (eachBlock.else) { + const elseEnd = htmlx.lastIndexOf('}', eachBlock.else.start); + const elseStart = htmlx.lastIndexOf('{', elseEnd); + str.overwrite(elseStart, elseEnd + 1, suffix); + str.remove(endEach, eachBlock.end); + } else { + str.overwrite(endEach, eachBlock.end, suffix); + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts index 803a178bf..22b783086 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts @@ -9,30 +9,30 @@ import { BaseNode } from '../../interfaces'; * Special treatment for self-closing / void tags to make them conform to JSX. */ export function handleElement( - htmlx: string, - str: MagicString, - node: BaseNode, - parent: BaseNode, - ifScope: IfScope, - templateScope: TemplateScope + htmlx: string, + str: MagicString, + node: BaseNode, + parent: BaseNode, + ifScope: IfScope, + templateScope: TemplateScope ): void { - const slotName = getSlotName(node); - if (slotName) { - handleSlot(htmlx, str, node, parent, slotName, ifScope, templateScope); - } + const slotName = getSlotName(node); + if (slotName) { + handleSlot(htmlx, str, node, parent, slotName, ifScope, templateScope); + } - //we just have to self close void tags since jsx always wants the /> - const voidTags = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'.split( - ',' - ); - if (voidTags.find((x) => x == node.name)) { - if (htmlx[node.end - 2] != '/') { - str.appendRight(node.end - 1, '/'); - } - } + //we just have to self close void tags since jsx always wants the /> + const voidTags = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'.split( + ',' + ); + if (voidTags.find((x) => x == node.name)) { + if (htmlx[node.end - 2] != '/') { + str.appendRight(node.end - 1, '/'); + } + } - //some tags auto close when they encounter certain elements, jsx doesn't support this - if (htmlx[node.end - 1] != '>') { - str.appendRight(node.end, ``); - } + //some tags auto close when they encounter certain elements, jsx doesn't support this + if (htmlx[node.end - 1] != '>') { + str.appendRight(node.end, ``); + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/event-handler.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/event-handler.ts index 522562bbe..6ff33e558 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/event-handler.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/event-handler.ts @@ -8,44 +8,44 @@ import { BaseDirective, BaseNode } from '../../interfaces'; * - For Svelte components/special elements: ---> {__sveltets_instanceOf(..ComponentType..).$on("xxx", yyy)} */ export function handleEventHandler( - htmlx: string, - str: MagicString, - attr: BaseDirective, - parent: BaseNode + htmlx: string, + str: MagicString, + attr: BaseDirective, + parent: BaseNode ): void { - const jsxEventName = attr.name; + const jsxEventName = attr.name; - if ( - ['Element', 'Window', 'Body'].includes( - parent.type - ) /*&& KnownEvents.indexOf('on'+jsxEventName) >= 0*/ - ) { - if (attr.expression) { - const endAttr = htmlx.indexOf('=', attr.start); - str.overwrite(attr.start + 'on:'.length - 1, endAttr, jsxEventName); - const lastChar = htmlx[attr.end - 1]; - if (isQuote(lastChar)) { - const firstQuote = htmlx.indexOf(lastChar, endAttr); - str.remove(firstQuote, firstQuote + 1); - str.remove(attr.end - 1, attr.end); - } - } else { - str.overwrite(attr.start + 'on:'.length - 1, attr.end, `${jsxEventName}={undefined}`); - } - } else { - if (attr.expression) { - const on = 'on'; - //for handler assignment, we change it to call to our __sveltets_ensureFunction - str.appendRight(attr.start, `{__sveltets_instanceOf(${getTypeForComponent(parent)}).$`); - const eventNameIndex = htmlx.indexOf(':', attr.start) + 1; - str.overwrite(htmlx.indexOf(on, attr.start) + on.length, eventNameIndex, "('"); - const eventEnd = htmlx.lastIndexOf('=', attr.expression.start); - str.overwrite(eventEnd, attr.expression.start, "', "); - str.overwrite(attr.expression.end, attr.end, ')}'); - str.move(attr.start, attr.end, parent.end); - } else { - //for passthrough handlers, we just remove - str.remove(attr.start, attr.end); - } - } + if ( + ['Element', 'Window', 'Body'].includes( + parent.type + ) /*&& KnownEvents.indexOf('on'+jsxEventName) >= 0*/ + ) { + if (attr.expression) { + const endAttr = htmlx.indexOf('=', attr.start); + str.overwrite(attr.start + 'on:'.length - 1, endAttr, jsxEventName); + const lastChar = htmlx[attr.end - 1]; + if (isQuote(lastChar)) { + const firstQuote = htmlx.indexOf(lastChar, endAttr); + str.remove(firstQuote, firstQuote + 1); + str.remove(attr.end - 1, attr.end); + } + } else { + str.overwrite(attr.start + 'on:'.length - 1, attr.end, `${jsxEventName}={undefined}`); + } + } else { + if (attr.expression) { + const on = 'on'; + //for handler assignment, we change it to call to our __sveltets_ensureFunction + str.appendRight(attr.start, `{__sveltets_instanceOf(${getTypeForComponent(parent)}).$`); + const eventNameIndex = htmlx.indexOf(':', attr.start) + 1; + str.overwrite(htmlx.indexOf(on, attr.start) + on.length, eventNameIndex, "('"); + const eventEnd = htmlx.lastIndexOf('=', attr.expression.start); + str.overwrite(eventEnd, attr.expression.start, "', "); + str.overwrite(attr.expression.end, attr.end, ')}'); + str.move(attr.start, attr.end, parent.end); + } else { + //for passthrough handlers, we just remove + str.remove(attr.start, attr.end); + } + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts index 2099e905d..5e49bcf44 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts @@ -6,64 +6,64 @@ import { BaseNode } from '../../interfaces'; * {# if ...}...{/if} ---> {() => {if(...){<>...}}} */ export function handleIf( - htmlx: string, - str: MagicString, - ifBlock: BaseNode, - ifScope: IfScope + htmlx: string, + str: MagicString, + ifBlock: BaseNode, + ifScope: IfScope ): void { - const endIf = htmlx.lastIndexOf('{', ifBlock.end - 1); + const endIf = htmlx.lastIndexOf('{', ifBlock.end - 1); - if (ifBlock.elseif) { - // {:else if expr} -> : (expr) ? <> - const elseIfStart = htmlx.lastIndexOf('{', ifBlock.expression.start); - const elseIfConditionEnd = htmlx.indexOf('}', ifBlock.expression.end) + 1; - str.overwrite(elseIfStart, ifBlock.expression.start, ' : (', { contentOnly: true }); - str.overwrite(ifBlock.expression.end, elseIfConditionEnd, ') ? <>'); + if (ifBlock.elseif) { + // {:else if expr} -> : (expr) ? <> + const elseIfStart = htmlx.lastIndexOf('{', ifBlock.expression.start); + const elseIfConditionEnd = htmlx.indexOf('}', ifBlock.expression.end) + 1; + str.overwrite(elseIfStart, ifBlock.expression.start, ' : (', { contentOnly: true }); + str.overwrite(ifBlock.expression.end, elseIfConditionEnd, ') ? <>'); - ifScope.addElseIf(ifBlock.expression, str); + ifScope.addElseIf(ifBlock.expression, str); - if (!ifBlock.else) { - str.appendLeft(endIf, ' : <>'); - } - return; - } + if (!ifBlock.else) { + str.appendLeft(endIf, ' : <>'); + } + return; + } - // {#if expr} -> {(expr) ? <> - str.overwrite(ifBlock.start, ifBlock.expression.start, '{(', { contentOnly: true }); - const end = htmlx.indexOf('}', ifBlock.expression.end); - str.overwrite(ifBlock.expression.end, end + 1, ') ? <>', { contentOnly: true }); + // {#if expr} -> {(expr) ? <> + str.overwrite(ifBlock.start, ifBlock.expression.start, '{(', { contentOnly: true }); + const end = htmlx.indexOf('}', ifBlock.expression.end); + str.overwrite(ifBlock.expression.end, end + 1, ') ? <>', { contentOnly: true }); - ifScope.addNestedIf(ifBlock.expression, str); + ifScope.addNestedIf(ifBlock.expression, str); - if (ifBlock.else) { - // {/if} -> } - str.overwrite(endIf, ifBlock.end, ' }', { contentOnly: true }); - } else { - // {/if} -> : <>} - str.overwrite(endIf, ifBlock.end, ' : <>}', { contentOnly: true }); - } + if (ifBlock.else) { + // {/if} -> } + str.overwrite(endIf, ifBlock.end, ' }', { contentOnly: true }); + } else { + // {/if} -> : <>} + str.overwrite(endIf, ifBlock.end, ' : <>}', { contentOnly: true }); + } } /** * {:else} ---> : <> */ export function handleElse( - htmlx: string, - str: MagicString, - elseBlock: BaseNode, - parent: BaseNode, - ifScope: IfScope + htmlx: string, + str: MagicString, + elseBlock: BaseNode, + parent: BaseNode, + ifScope: IfScope ): void { - if ( - parent.type !== 'IfBlock' || - (elseBlock.children[0]?.type === 'IfBlock' && elseBlock.children[0]?.elseif) - ) { - return; - } - const elseEnd = htmlx.lastIndexOf('}', elseBlock.start); - const elseword = htmlx.lastIndexOf(':else', elseEnd); - const elseStart = htmlx.lastIndexOf('{', elseword); - str.overwrite(elseStart, elseEnd + 1, ' : <>'); + if ( + parent.type !== 'IfBlock' || + (elseBlock.children[0]?.type === 'IfBlock' && elseBlock.children[0]?.elseif) + ) { + return; + } + const elseEnd = htmlx.lastIndexOf('}', elseBlock.start); + const elseword = htmlx.lastIndexOf(':else', elseEnd); + const elseStart = htmlx.lastIndexOf('{', elseword); + str.overwrite(elseStart, elseEnd + 1, ' : <>'); - ifScope.addElse(); + ifScope.addElse(); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/if-scope.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/if-scope.ts index 9fdd39aa5..d9b8f3a57 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/if-scope.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/if-scope.ts @@ -5,9 +5,9 @@ import { TemplateScope } from '../nodes/template-scope'; import { surroundWithIgnoreComments } from '../../utils/ignore'; enum IfType { - If, - ElseIf, - Else + If, + ElseIf, + Else } /** @@ -15,33 +15,33 @@ enum IfType { * used within that condition. */ interface ConditionInfo { - identifiers: Map>; - text: string; + identifiers: Map>; + text: string; } /** * See `Condition` for an explanation of the structure. */ interface IfCondition { - type: IfType.If; - condition: ConditionInfo; + type: IfType.If; + condition: ConditionInfo; } /** * See `Condition` for an explanation of the structure. */ interface ElseIfCondition { - type: IfType.ElseIf; - condition: ConditionInfo; - parent: IfCondition | ElseIfCondition; + type: IfType.ElseIf; + condition: ConditionInfo; + parent: IfCondition | ElseIfCondition; } /** * See `Condition` for an explanation of the structure. */ interface ElseCondition { - type: IfType.Else; - parent: IfCondition | ElseIfCondition; + type: IfType.Else; + parent: IfCondition | ElseIfCondition; } /** @@ -85,14 +85,14 @@ type Condition = IfCondition | ElseIfCondition | ElseCondition; * See `Condition` for an explanation of the structure. */ function addElseIfCondition( - existingCondition: IfCondition | ElseIfCondition, - newCondition: ConditionInfo + existingCondition: IfCondition | ElseIfCondition, + newCondition: ConditionInfo ): ElseIfCondition { - return { - parent: existingCondition, - condition: newCondition, - type: IfType.ElseIf - }; + return { + parent: existingCondition, + condition: newCondition, + type: IfType.ElseIf + }; } /** @@ -101,10 +101,10 @@ function addElseIfCondition( * See `Condition` for an explanation of the structure. */ function addElseCondition(existingCondition: IfCondition | ElseIfCondition): ElseCondition { - return { - parent: existingCondition, - type: IfType.Else - }; + return { + parent: existingCondition, + type: IfType.Else + }; } const REPLACEMENT_PREFIX = '\u03A9'; @@ -114,45 +114,45 @@ const REPLACEMENT_PREFIX = '\u03A9'; * get replaced if they were redeclared. */ function getFullCondition( - condition: Condition, - replacedNames: string[], - replacementPrefix: string + condition: Condition, + replacedNames: string[], + replacementPrefix: string ): string { - switch (condition.type) { - case IfType.If: - return _getFullCondition(condition, false, replacedNames, replacementPrefix); - case IfType.ElseIf: - return _getFullCondition(condition, false, replacedNames, replacementPrefix); - case IfType.Else: - return _getFullCondition(condition, false, replacedNames, replacementPrefix); - } + switch (condition.type) { + case IfType.If: + return _getFullCondition(condition, false, replacedNames, replacementPrefix); + case IfType.ElseIf: + return _getFullCondition(condition, false, replacedNames, replacementPrefix); + case IfType.Else: + return _getFullCondition(condition, false, replacedNames, replacementPrefix); + } } function _getFullCondition( - condition: Condition, - negate: boolean, - replacedNames: string[], - replacementPrefix: string + condition: Condition, + negate: boolean, + replacedNames: string[], + replacementPrefix: string ): string { - switch (condition.type) { - case IfType.If: - return negate - ? `!(${getConditionString(condition.condition, replacedNames, replacementPrefix)})` - : `(${getConditionString(condition.condition, replacedNames, replacementPrefix)})`; - case IfType.ElseIf: - return `${_getFullCondition( - condition.parent, - true, - replacedNames, - replacementPrefix - )} && ${negate ? '!' : ''}(${getConditionString( - condition.condition, - replacedNames, - replacementPrefix - )})`; - case IfType.Else: - return `${_getFullCondition(condition.parent, true, replacedNames, replacementPrefix)}`; - } + switch (condition.type) { + case IfType.If: + return negate + ? `!(${getConditionString(condition.condition, replacedNames, replacementPrefix)})` + : `(${getConditionString(condition.condition, replacedNames, replacementPrefix)})`; + case IfType.ElseIf: + return `${_getFullCondition( + condition.parent, + true, + replacedNames, + replacementPrefix + )} && ${negate ? '!' : ''}(${getConditionString( + condition.condition, + replacedNames, + replacementPrefix + )})`; + case IfType.Else: + return `${_getFullCondition(condition.parent, true, replacedNames, replacementPrefix)}`; + } } /** @@ -160,56 +160,56 @@ function _getFullCondition( * are replaced accordingly. */ function getConditionString( - condition: ConditionInfo, - replacedNames: string[], - replacementPrefix: string + condition: ConditionInfo, + replacedNames: string[], + replacementPrefix: string ): string { - const replacements: Array<{ name: string; start: number; end: number }> = []; - for (const name of replacedNames) { - const occurences = condition.identifiers.get(name); - if (occurences) { - for (const occurence of occurences) { - replacements.push({ ...occurence, name }); - } - } - } - - if (!replacements.length) { - return condition.text; - } - - replacements.sort((r1, r2) => r1.start - r2.start); - return ( - condition.text.substring(0, replacements[0].start) + - replacements - .map( - (replacement, idx) => - replacementPrefix + - replacement.name + - condition.text.substring(replacement.end, replacements[idx + 1]?.start) - ) - .join('') - ); + const replacements: Array<{ name: string; start: number; end: number }> = []; + for (const name of replacedNames) { + const occurences = condition.identifiers.get(name); + if (occurences) { + for (const occurence of occurences) { + replacements.push({ ...occurence, name }); + } + } + } + + if (!replacements.length) { + return condition.text; + } + + replacements.sort((r1, r2) => r1.start - r2.start); + return ( + condition.text.substring(0, replacements[0].start) + + replacements + .map( + (replacement, idx) => + replacementPrefix + + replacement.name + + condition.text.substring(replacement.end, replacements[idx + 1]?.start) + ) + .join('') + ); } /** * Returns a set of all identifiers that were used in this condition */ function collectReferencedIdentifiers(condition: Condition | undefined): Set { - const identifiers = new Set(); - let current = condition; - while (current) { - if (current.type === IfType.ElseIf || current.type === IfType.If) { - for (const identifier of current.condition.identifiers.keys()) { - identifiers.add(identifier); - } - } - current = - current.type === IfType.ElseIf || current.type === IfType.Else - ? current.parent - : undefined; - } - return identifiers; + const identifiers = new Set(); + let current = condition; + while (current) { + if (current.type === IfType.ElseIf || current.type === IfType.If) { + for (const identifier of current.condition.identifiers.keys()) { + identifiers.add(identifier); + } + } + current = + current.type === IfType.ElseIf || current.type === IfType.Else + ? current.parent + : undefined; + } + return identifiers; } /** @@ -234,170 +234,170 @@ function collectReferencedIdentifiers(condition: Condition | undefined): Set { - const current = collectReferencedIdentifiers(this.current); - const parent = this.parent?.collectReferencedIdentifiers(); - if (parent) { - for (const identifier of parent) { - current.add(identifier); - } - } - return current; - } - - /** - * Should be invoked when a new template scope which resets control flow (await, each, slot) is created. - * The returned string contains a list of `const` declarations which redeclares the identifiers - * in the conditions which would be overwritten by the scope - * (because they declare a variable with the same name, therefore shadowing the outer variable). - */ - getConstsToRedeclare(): string { - const replacements = this.getNamesToRedeclare() - .map((identifier) => `${this.replacementPrefix + identifier}=${identifier}`) - .join(','); - return replacements ? surroundWithIgnoreComments(`const ${replacements};`) : ''; - } - - /** - * Returns true if given identifier is referenced in this IfScope or a parent scope. - */ - referencesIdentifier(name: string): boolean { - const current = collectReferencedIdentifiers(this.current); - if (current.has(name)) { - return true; - } - if (!this.parent || this.ownScope.inits.has(name)) { - return false; - } - return this.parent.referencesIdentifier(name); - } - - private getConditionInfo(str: MagicString, expression: Node): ConditionInfo { - const identifiers = getIdentifiersInIfExpression(expression); - const text = str.original.substring(expression.start, expression.end); - return { identifiers, text }; - } - - /** - * Contains a list of identifiers which would be overwritten by the child template scope. - */ - private getNamesToRedeclare() { - return [...this.scope.value.inits.keys()].filter((init) => { - let parent = this.scope.value.parent; - while (parent && parent !== this.ownScope) { - if (parent.inits.has(init)) { - return false; - } - parent = parent.parent; - } - return this.referencesIdentifier(init); - }); - } - - /** - * Return all identifiers that were redeclared and therefore need replacement. - */ - private getNamesThatNeedReplacement() { - const referencedIdentifiers = this.collectReferencedIdentifiers(); - return [...referencedIdentifiers].filter((identifier) => - this.someChildScopeHasRedeclaredVariable(identifier) - ); - } - - /** - * Returns true if given identifier name is redeclared in a child template scope - * and is therefore shadowed within that scope. - */ - private someChildScopeHasRedeclaredVariable(name: string) { - let scope = this.scope.value; - while (scope && scope !== this.ownScope) { - if (scope.inits.has(name)) { - return true; - } - scope = scope.parent; - } - return false; - } - - private computeDepth() { - let idx = 1; - let parent = this.ownScope.parent; - while (parent) { - idx++; - parent = parent.parent; - } - return idx; - } + private child?: IfScope; + private ownScope = this.scope.value; + private replacementPrefix = REPLACEMENT_PREFIX.repeat(this.computeDepth()); + + constructor( + private scope: { value: TemplateScope }, + private current?: Condition, + private parent?: IfScope + ) {} + + /** + * Returns the full currently known condition, prepended with the conditions + * of its parents. Identifiers in the condition get replaced if they were redeclared. + */ + getFullCondition(): string { + if (!this.current) { + return ''; + } + + const parentCondition = this.parent?.getFullCondition(); + const condition = `(${getFullCondition( + this.current, + this.getNamesThatNeedReplacement(), + this.replacementPrefix + )})`; + return parentCondition ? `(${parentCondition}) && ${condition}` : condition; + } + + /** + * Convenience method which invokes `getFullCondition` and adds a `&&` at the end + * for easy chaining. + */ + addPossibleIfCondition(): string { + const condition = this.getFullCondition(); + return condition ? surroundWithIgnoreComments(`${condition} && `) : ''; + } + + /** + * Adds a new child IfScope. + */ + addNestedIf(expression: Node, str: MagicString): void { + const condition = this.getConditionInfo(str, expression); + const ifScope = new IfScope(this.scope, { condition, type: IfType.If }, this); + this.child = ifScope; + } + + /** + * Adds a `else if` branch to the scope and enhances the condition accordingly. + */ + addElseIf(expression: Node, str: MagicString): void { + const condition = this.getConditionInfo(str, expression); + this.current = addElseIfCondition(this.current as IfCondition | ElseIfCondition, condition); + } + + /** + * Adds a `else` branch to the scope and enhances the condition accordingly. + */ + addElse(): void { + this.current = addElseCondition(this.current as IfCondition | ElseIfCondition); + } + + getChild(): IfScope { + return this.child || this; + } + + getParent(): IfScope { + return this.parent || this; + } + + /** + * Returns a set of all identifiers that were used in this IfScope and its parent scopes. + */ + collectReferencedIdentifiers(): Set { + const current = collectReferencedIdentifiers(this.current); + const parent = this.parent?.collectReferencedIdentifiers(); + if (parent) { + for (const identifier of parent) { + current.add(identifier); + } + } + return current; + } + + /** + * Should be invoked when a new template scope which resets control flow (await, each, slot) is created. + * The returned string contains a list of `const` declarations which redeclares the identifiers + * in the conditions which would be overwritten by the scope + * (because they declare a variable with the same name, therefore shadowing the outer variable). + */ + getConstsToRedeclare(): string { + const replacements = this.getNamesToRedeclare() + .map((identifier) => `${this.replacementPrefix + identifier}=${identifier}`) + .join(','); + return replacements ? surroundWithIgnoreComments(`const ${replacements};`) : ''; + } + + /** + * Returns true if given identifier is referenced in this IfScope or a parent scope. + */ + referencesIdentifier(name: string): boolean { + const current = collectReferencedIdentifiers(this.current); + if (current.has(name)) { + return true; + } + if (!this.parent || this.ownScope.inits.has(name)) { + return false; + } + return this.parent.referencesIdentifier(name); + } + + private getConditionInfo(str: MagicString, expression: Node): ConditionInfo { + const identifiers = getIdentifiersInIfExpression(expression); + const text = str.original.substring(expression.start, expression.end); + return { identifiers, text }; + } + + /** + * Contains a list of identifiers which would be overwritten by the child template scope. + */ + private getNamesToRedeclare() { + return [...this.scope.value.inits.keys()].filter((init) => { + let parent = this.scope.value.parent; + while (parent && parent !== this.ownScope) { + if (parent.inits.has(init)) { + return false; + } + parent = parent.parent; + } + return this.referencesIdentifier(init); + }); + } + + /** + * Return all identifiers that were redeclared and therefore need replacement. + */ + private getNamesThatNeedReplacement() { + const referencedIdentifiers = this.collectReferencedIdentifiers(); + return [...referencedIdentifiers].filter((identifier) => + this.someChildScopeHasRedeclaredVariable(identifier) + ); + } + + /** + * Returns true if given identifier name is redeclared in a child template scope + * and is therefore shadowed within that scope. + */ + private someChildScopeHasRedeclaredVariable(name: string) { + let scope = this.scope.value; + while (scope && scope !== this.ownScope) { + if (scope.inits.has(name)) { + return true; + } + scope = scope.parent; + } + return false; + } + + private computeDepth() { + let idx = 1; + let parent = this.ownScope.parent; + while (parent) { + idx++; + parent = parent.parent; + } + return idx; + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/key.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/key.ts index 0a126a418..cb1337de7 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/key.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/key.ts @@ -5,12 +5,12 @@ import { BaseNode } from '../../interfaces'; * {#key expr}content{/key} ---> {expr} content */ export function handleKey(htmlx: string, str: MagicString, keyBlock: BaseNode): void { - // {#key expr} -> {expr} - str.overwrite(keyBlock.start, keyBlock.expression.start, '{'); - const end = htmlx.indexOf('}', keyBlock.expression.end); - str.overwrite(keyBlock.expression.end, end + 1, '} '); + // {#key expr} -> {expr} + str.overwrite(keyBlock.start, keyBlock.expression.start, '{'); + const end = htmlx.indexOf('}', keyBlock.expression.end); + str.overwrite(keyBlock.expression.end, end + 1, '} '); - // {/key} -> - const endKey = htmlx.lastIndexOf('{', keyBlock.end - 1); - str.remove(endKey, keyBlock.end); + // {/key} -> + const endKey = htmlx.lastIndexOf('{', keyBlock.end - 1); + str.remove(endKey, keyBlock.end); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/raw-html.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/raw-html.ts index 55b9ccb71..ea0a99fe5 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/raw-html.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/raw-html.ts @@ -5,6 +5,6 @@ import { BaseNode } from '../../interfaces'; * {@html ...} ---> {...} */ export function handleRawHtml(htmlx: string, str: MagicString, rawBlock: BaseNode): void { - const tokenStart = htmlx.indexOf('@html', rawBlock.start); - str.remove(tokenStart, tokenStart + '@html'.length); + const tokenStart = htmlx.indexOf('@html', rawBlock.start); + str.remove(tokenStart, tokenStart + '@html'.length); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts index 5f4c5a7e0..1348f4965 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts @@ -6,67 +6,67 @@ import { TemplateScope } from '../nodes/template-scope'; import { BaseNode } from '../../interfaces'; export function handleSlot( - htmlx: string, - str: MagicString, - slotEl: BaseNode, - component: BaseNode, - slotName: string, - ifScope: IfScope, - templateScope: TemplateScope + htmlx: string, + str: MagicString, + slotEl: BaseNode, + component: BaseNode, + slotName: string, + ifScope: IfScope, + templateScope: TemplateScope ): void { - //collect "let" definitions - const slotElIsComponent = slotEl === component; - let hasMoved = false; - let slotDefInsertionPoint: number; - for (const attr of slotEl.attributes) { - if (attr.type != 'Let') { - continue; - } + //collect "let" definitions + const slotElIsComponent = slotEl === component; + let hasMoved = false; + let slotDefInsertionPoint: number; + for (const attr of slotEl.attributes) { + if (attr.type != 'Let') { + continue; + } - if (slotElIsComponent && slotEl.children.length == 0) { - //no children anyway, just wipe out the attribute - str.remove(attr.start, attr.end); - continue; - } + if (slotElIsComponent && slotEl.children.length == 0) { + //no children anyway, just wipe out the attribute + str.remove(attr.start, attr.end); + continue; + } - slotDefInsertionPoint = - slotDefInsertionPoint || - (slotElIsComponent - ? htmlx.lastIndexOf('>', slotEl.children[0].start) + 1 - : slotEl.start); + slotDefInsertionPoint = + slotDefInsertionPoint || + (slotElIsComponent + ? htmlx.lastIndexOf('>', slotEl.children[0].start) + 1 + : slotEl.start); - str.move(attr.start, attr.end, slotDefInsertionPoint); + str.move(attr.start, attr.end, slotDefInsertionPoint); - //remove let: - if (hasMoved) { - str.overwrite(attr.start, attr.start + 'let:'.length, ', '); - } else { - str.remove(attr.start, attr.start + 'let:'.length); - } - templateScope.inits.add(attr.expression?.name || attr.name); - hasMoved = true; - if (attr.expression) { - //overwrite the = as a : - const equalSign = htmlx.lastIndexOf('=', attr.expression.start); - const curly = htmlx.lastIndexOf('{', beforeStart(attr.expression.start)); - str.overwrite(equalSign, curly + 1, ':'); - str.remove(attr.expression.end, attr.end); - } - } - if (!hasMoved) { - return; - } + //remove let: + if (hasMoved) { + str.overwrite(attr.start, attr.start + 'let:'.length, ', '); + } else { + str.remove(attr.start, attr.start + 'let:'.length); + } + templateScope.inits.add(attr.expression?.name || attr.name); + hasMoved = true; + if (attr.expression) { + //overwrite the = as a : + const equalSign = htmlx.lastIndexOf('=', attr.expression.start); + const curly = htmlx.lastIndexOf('{', beforeStart(attr.expression.start)); + str.overwrite(equalSign, curly + 1, ':'); + str.remove(attr.expression.end, attr.end); + } + } + if (!hasMoved) { + return; + } - const constRedeclares = ifScope.getConstsToRedeclare(); - const prefix = constRedeclares ? `() => {${constRedeclares}` : ''; - str.appendLeft(slotDefInsertionPoint, `{${prefix}() => { let {`); - str.appendRight( - slotDefInsertionPoint, - `} = ${getSingleSlotDef(component, slotName)}` + `;${ifScope.addPossibleIfCondition()}<>` - ); + const constRedeclares = ifScope.getConstsToRedeclare(); + const prefix = constRedeclares ? `() => {${constRedeclares}` : ''; + str.appendLeft(slotDefInsertionPoint, `{${prefix}() => { let {`); + str.appendRight( + slotDefInsertionPoint, + `} = ${getSingleSlotDef(component, slotName)}` + `;${ifScope.addPossibleIfCondition()}<>` + ); - const closeSlotDefInsertionPoint = slotElIsComponent - ? htmlx.lastIndexOf('<', slotEl.end - 1) - : slotEl.end; - str.appendLeft(closeSlotDefInsertionPoint, `}}${constRedeclares ? '}' : ''}`); + const closeSlotDefInsertionPoint = slotElIsComponent + ? htmlx.lastIndexOf('<', slotEl.end - 1) + : slotEl.end; + str.appendLeft(closeSlotDefInsertionPoint, `}}${constRedeclares ? '}' : ''}`); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts index 44a64db07..e17be911b 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts @@ -6,12 +6,12 @@ import { BaseNode } from '../../interfaces'; * (same for :head, :body, :options, :fragment) */ export function handleSvelteTag(htmlx: string, str: MagicString, node: BaseNode): void { - const colon = htmlx.indexOf(':', node.start); - str.remove(colon, colon + 1); + const colon = htmlx.indexOf(':', node.start); + str.remove(colon, colon + 1); - const closeTag = htmlx.lastIndexOf('/' + node.name, node.end); - if (closeTag > node.start) { - const colon = htmlx.indexOf(':', closeTag); - str.remove(colon, colon + 1); - } + const closeTag = htmlx.lastIndexOf('/' + node.name, node.end); + if (closeTag > node.start) { + const colon = htmlx.indexOf(':', closeTag); + str.remove(colon, colon + 1); + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/template-scope.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/template-scope.ts index 64b8917de..ba56e8df2 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/template-scope.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/template-scope.ts @@ -4,110 +4,110 @@ import { isDestructuringPatterns, isIdentifier } from '../../utils/svelteAst'; import { usesLet } from '../utils/node-utils'; export class TemplateScope { - inits = new Set(); - parent?: TemplateScope; + inits = new Set(); + parent?: TemplateScope; - constructor(parent?: TemplateScope) { - this.parent = parent; - } + constructor(parent?: TemplateScope) { + this.parent = parent; + } - child() { - const child = new TemplateScope(this); - return child; - } + child() { + const child = new TemplateScope(this); + return child; + } } export class TemplateScopeManager { - value = new TemplateScope(); + value = new TemplateScope(); - eachEnter(node: BaseNode) { - this.value = this.value.child(); - if (node.context) { - this.handleScope(node.context); - } - if (node.index) { - this.value.inits.add(node.index); - } - } + eachEnter(node: BaseNode) { + this.value = this.value.child(); + if (node.context) { + this.handleScope(node.context); + } + if (node.index) { + this.value.inits.add(node.index); + } + } - eachLeave(node: BaseNode) { - if (!node.else) { - this.value = this.value.parent; - } - } + eachLeave(node: BaseNode) { + if (!node.else) { + this.value = this.value.parent; + } + } - awaitEnter(node: BaseNode) { - this.value = this.value.child(); - if (node.value) { - this.handleScope(node.value); - } - if (node.error) { - this.handleScope(node.error); - } - } + awaitEnter(node: BaseNode) { + this.value = this.value.child(); + if (node.value) { + this.handleScope(node.value); + } + if (node.error) { + this.handleScope(node.error); + } + } - awaitPendingEnter(node: BaseNode, parent: BaseNode) { - if (node.skip || parent.type !== 'AwaitBlock') { - return; - } - // Reset inits, as pending can have no inits - this.value.inits.clear(); - } + awaitPendingEnter(node: BaseNode, parent: BaseNode) { + if (node.skip || parent.type !== 'AwaitBlock') { + return; + } + // Reset inits, as pending can have no inits + this.value.inits.clear(); + } - awaitThenEnter(node: BaseNode, parent: BaseNode) { - if (node.skip || parent.type !== 'AwaitBlock') { - return; - } - // Reset inits, this time only taking the then - // scope into account. - this.value.inits.clear(); - if (parent.value) { - this.handleScope(parent.value); - } - } + awaitThenEnter(node: BaseNode, parent: BaseNode) { + if (node.skip || parent.type !== 'AwaitBlock') { + return; + } + // Reset inits, this time only taking the then + // scope into account. + this.value.inits.clear(); + if (parent.value) { + this.handleScope(parent.value); + } + } - awaitCatchEnter(node: BaseNode, parent: BaseNode) { - if (node.skip || parent.type !== 'AwaitBlock') { - return; - } - // Reset inits, this time only taking the error - // scope into account. - this.value.inits.clear(); - if (parent.error) { - this.handleScope(parent.error); - } - } + awaitCatchEnter(node: BaseNode, parent: BaseNode) { + if (node.skip || parent.type !== 'AwaitBlock') { + return; + } + // Reset inits, this time only taking the error + // scope into account. + this.value.inits.clear(); + if (parent.error) { + this.handleScope(parent.error); + } + } - awaitLeave() { - this.value = this.value.parent; - } + awaitLeave() { + this.value = this.value.parent; + } - elseEnter(parent: BaseNode) { - if (parent.type === 'EachBlock') { - this.value = this.value.parent; - } - } + elseEnter(parent: BaseNode) { + if (parent.type === 'EachBlock') { + this.value = this.value.parent; + } + } - componentOrSlotTemplateOrElementEnter(node: BaseNode) { - if (usesLet(node)) { - this.value = this.value.child(); - } - } + componentOrSlotTemplateOrElementEnter(node: BaseNode) { + if (usesLet(node)) { + this.value = this.value.child(); + } + } - componentOrSlotTemplateOrElementLeave(node: BaseNode) { - if (usesLet(node)) { - this.value = this.value.parent; - } - } + componentOrSlotTemplateOrElementLeave(node: BaseNode) { + if (usesLet(node)) { + this.value = this.value.parent; + } + } - private handleScope(identifierDef: BaseNode) { - if (isIdentifier(identifierDef)) { - this.value.inits.add(identifierDef.name); - } - if (isDestructuringPatterns(identifierDef)) { - // the node object is returned as-it with no mutation - const identifiers = extract_identifiers(identifierDef) as SvelteIdentifier[]; - identifiers.forEach((id) => this.value.inits.add(id.name)); - } - } + private handleScope(identifierDef: BaseNode) { + if (isIdentifier(identifierDef)) { + this.value.inits.add(identifierDef.name); + } + if (isDestructuringPatterns(identifierDef)) { + // the node object is returned as-it with no mutation + const identifiers = extract_identifiers(identifierDef) as SvelteIdentifier[]; + identifiers.forEach((id) => this.value.inits.add(id.name)); + } + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/text.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/text.ts index 29369de30..840c19b12 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/text.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/text.ts @@ -2,16 +2,16 @@ import MagicString from 'magic-string'; import { Text } from 'svelte/types/compiler/interfaces'; export function handleText(str: MagicString, node: Text) { - if (!node.data) { - return; - } - const needsRemoves = ['}', '>'] as const; + if (!node.data) { + return; + } + const needsRemoves = ['}', '>'] as const; - for (const token of needsRemoves) { - let index = node.data.indexOf(token); - while (index >= 0) { - str.remove(index + node.start, index + node.start + 1); - index = node.data.indexOf(token, index + 1); - } - } + for (const token of needsRemoves) { + let index = node.data.indexOf(token); + while (index >= 0) { + str.remove(index + node.start, index + node.start + 1); + index = node.data.indexOf(token, index + 1); + } + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/transition-directive.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/transition-directive.ts index 3ca29e3b7..d082b2371 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/transition-directive.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/transition-directive.ts @@ -6,36 +6,36 @@ import { BaseDirective, BaseNode } from '../../interfaces'; * transition:xxx(yyy) ---> {...__sveltets_ensureTransition(xxx(__sveltets_mapElementTag('..'),(yyy)))} */ export function handleTransitionDirective( - htmlx: string, - str: MagicString, - attr: BaseDirective, - parent: BaseNode + htmlx: string, + str: MagicString, + attr: BaseDirective, + parent: BaseNode ): void { - str.overwrite( - attr.start, - htmlx.indexOf(':', attr.start) + 1, - '{...__sveltets_ensureTransition(' - ); + str.overwrite( + attr.start, + htmlx.indexOf(':', attr.start) + 1, + '{...__sveltets_ensureTransition(' + ); - if (attr.modifiers.length) { - const local = htmlx.indexOf('|', attr.start); - str.remove(local, attr.expression ? attr.expression.start : attr.end); - } + if (attr.modifiers.length) { + const local = htmlx.indexOf('|', attr.start); + str.remove(local, attr.expression ? attr.expression.start : attr.end); + } - const nodeType = `__sveltets_mapElementTag('${parent.name}')`; + const nodeType = `__sveltets_mapElementTag('${parent.name}')`; - if (!attr.expression) { - str.appendLeft(attr.end, `(${nodeType},{}))}`); - return; - } + if (!attr.expression) { + str.appendLeft(attr.end, `(${nodeType},{}))}`); + return; + } - str.overwrite( - htmlx.indexOf(':', attr.start) + 1 + `${attr.name}`.length, - attr.expression.start, - `(${nodeType},(` - ); - str.appendLeft(attr.expression.end, ')))'); - if (isQuote(htmlx[attr.end - 1])) { - str.remove(attr.end - 1, attr.end); - } + str.overwrite( + htmlx.indexOf(':', attr.start) + 1 + `${attr.name}`.length, + attr.expression.start, + `(${nodeType},(` + ); + str.appendLeft(attr.expression.end, ')))'); + if (isQuote(htmlx[attr.end - 1])) { + str.remove(attr.end - 1, attr.end); + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx/svgattributes.ts b/packages/svelte2tsx/src/htmlxtojsx/svgattributes.ts index ebd4f1055..7a5f5cb60 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/svgattributes.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/svgattributes.ts @@ -1,3 +1,3 @@ export default 'accent-height accumulate additive alignment-baseline allowReorder alphabetic amplitude arabic-form ascent attributeName attributeType autoReverse azimuth baseFrequency baseline-shift baseProfile bbox begin bias by calcMode cap-height class clip clipPathUnits clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor cx cy d decelerate descent diffuseConstant direction display divisor dominant-baseline dur dx dy edgeMode elevation enable-background end exponent externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format from fr fx fy g1 g2 glyph-name glyph-orientation-horizontal glyph-orientation-vertical glyphRef gradientTransform gradientUnits hanging height href horiz-adv-x horiz-origin-x id ideographic image-rendering in in2 intercept k k1 k2 k3 k4 kernelMatrix kernelUnitLength kerning keyPoints keySplines keyTimes lang lengthAdjust letter-spacing lighting-color limitingConeAngle local marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask maskContentUnits maskUnits mathematical max media method min mode name numOctaves offset onabort onactivate onbegin onclick onend onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onrepeat onresize onscroll onunload opacity operator order orient orientation origin overflow overline-position overline-thickness panose-1 paint-order pathLength patternContentUnits patternTransform patternUnits pointer-events points pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits r radius refX refY rendering-intent repeatCount repeatDur requiredExtensions requiredFeatures restart result rotate rx ry scale seed shape-rendering slope spacing specularConstant specularExponent speed spreadMethod startOffset stdDeviation stemh stemv stitchTiles stop-color stop-opacity strikethrough-position strikethrough-thickness string stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale systemLanguage tabindex tableValues target targetX targetY text-anchor text-decoration text-rendering textLength to transform type u1 u2 underline-position underline-thickness unicode unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical values version vert-adv-y vert-origin-x vert-origin-y viewBox viewTarget visibility width widths word-spacing writing-mode x x-height x1 x2 xChannelSelector xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y y1 y2 yChannelSelector z zoomAndPan'.split( - ' ' + ' ' ); diff --git a/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts b/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts index b7115f3c1..0d187ba25 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts @@ -2,69 +2,69 @@ import { Node, walk } from 'estree-walker'; import { BaseNode } from '../../interfaces'; export function getTypeForComponent(node: Node): string { - if (node.name === 'svelte:component' || node.name === 'svelte:self') { - return '__sveltets_componentType()'; - } else { - return node.name; - } + if (node.name === 'svelte:component' || node.name === 'svelte:self') { + return '__sveltets_componentType()'; + } else { + return node.name; + } } export function getThisType(node: Node): string | undefined { - switch (node.type) { - case 'InlineComponent': - return getTypeForComponent(node); - case 'Element': - return `__sveltets_ctorOf(__sveltets_mapElementTag('${node.name}'))`; - case 'Body': - return 'HTMLBodyElement'; - case 'Slot': // Web Components only - return 'HTMLSlotElement'; - } + switch (node.type) { + case 'InlineComponent': + return getTypeForComponent(node); + case 'Element': + return `__sveltets_ctorOf(__sveltets_mapElementTag('${node.name}'))`; + case 'Body': + return 'HTMLBodyElement'; + case 'Slot': // Web Components only + return 'HTMLSlotElement'; + } } export function beforeStart(start: number): number { - return start - 1; + return start - 1; } export function isShortHandAttribute(attr: Node): boolean { - return attr.expression.end === attr.end; + return attr.expression.end === attr.end; } export function isQuote(str: string): boolean { - return str === '"' || str === "'"; + return str === '"' || str === "'"; } export function getIdentifiersInIfExpression( - expression: Node + expression: Node ): Map> { - const offset = expression.start; - const identifiers = new Map>(); - walk(expression, { - enter: (node, parent) => { - switch (node.type) { - case 'Identifier': - // parent.property === node => node is "prop" in "obj.prop" - // parent.callee === node => node is "fun" in "fun(..)" - if (parent?.property !== node && parent?.callee !== node) { - add(node); - } - break; - } - } - }); + const offset = expression.start; + const identifiers = new Map>(); + walk(expression, { + enter: (node, parent) => { + switch (node.type) { + case 'Identifier': + // parent.property === node => node is "prop" in "obj.prop" + // parent.callee === node => node is "fun" in "fun(..)" + if (parent?.property !== node && parent?.callee !== node) { + add(node); + } + break; + } + } + }); - function add(node: Node) { - let entry = identifiers.get(node.name); - if (!entry) { - entry = []; - } - entry.push({ start: node.start - offset, end: node.end - offset }); - identifiers.set(node.name, entry); - } + function add(node: Node) { + let entry = identifiers.get(node.name); + if (!entry) { + entry = []; + } + entry.push({ start: node.start - offset, end: node.end - offset }); + identifiers.set(node.name, entry); + } - return identifiers; + return identifiers; } export function usesLet(node: BaseNode): boolean { - return node.attributes?.some((attr) => attr.type === 'Let'); + return node.attributes?.some((attr) => attr.type === 'Let'); } diff --git a/packages/svelte2tsx/src/interfaces.ts b/packages/svelte2tsx/src/interfaces.ts index 3fd4a9d82..1d9ec95f2 100644 --- a/packages/svelte2tsx/src/interfaces.ts +++ b/packages/svelte2tsx/src/interfaces.ts @@ -1,15 +1,15 @@ import { ArrayPattern, ObjectPattern, Identifier } from 'estree'; import { - Directive, - TemplateNode, - Transition, - MustacheTag, - Text + Directive, + TemplateNode, + Transition, + MustacheTag, + Text } from 'svelte/types/compiler/interfaces'; export interface NodeRange { - start: number; - end: number; + start: number; + end: number; } export interface SvelteIdentifier extends Identifier, NodeRange {} @@ -19,8 +19,8 @@ export interface SvelteArrayPattern extends ArrayPattern, NodeRange {} export interface SvelteObjectPattern extends ObjectPattern, NodeRange {} export interface WithName { - type: string; - name: string; + type: string; + name: string; } export type BaseNode = Exclude; @@ -28,5 +28,5 @@ export type BaseNode = Exclude; export interface Attribute extends BaseNode { - value: BaseNode[] | true; + value: BaseNode[] | true; } diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index 0f97fed54..d3b2d54df 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -11,8 +11,8 @@ import { ExportedNames } from './nodes/ExportedNames'; import { createClassGetters, createRenderFunctionGetterStr } from './nodes/exportgetters'; import { createClassAccessors } from './nodes/exportaccessors'; import { - handleScopeAndResolveForSlot, - handleScopeAndResolveLetVarForSlot + handleScopeAndResolveForSlot, + handleScopeAndResolveLetVarForSlot } from './nodes/handleScopeAndResolveForSlot'; import { ImplicitStoreValues } from './nodes/ImplicitStoreValues'; import { Scripts } from './nodes/Scripts'; @@ -20,50 +20,50 @@ import { SlotHandler } from './nodes/slot'; import { Stores } from './nodes/Stores'; import TemplateScope from './nodes/TemplateScope'; import { - InstanceScriptProcessResult, - processInstanceScriptContent + InstanceScriptProcessResult, + processInstanceScriptContent } from './processInstanceScriptContent'; import { processModuleScriptTag } from './processModuleScriptTag'; import { ScopeStack } from './utils/Scope'; interface CreateRenderFunctionPara extends InstanceScriptProcessResult { - str: MagicString; - scriptTag: Node; - scriptDestination: number; - slots: Map>; - events: ComponentEvents; - isTsFile: boolean; + str: MagicString; + scriptTag: Node; + scriptDestination: number; + slots: Map>; + events: ComponentEvents; + isTsFile: boolean; } interface AddComponentExportPara { - str: MagicString; - uses$$propsOr$$restProps: boolean; - strictMode: boolean; - /** - * If true, not fallback to `any` - * -> all unknown events will throw a type error - * */ - strictEvents: boolean; - isTsFile: boolean; - getters: Set; - usesAccessors: boolean; - exportedNames: ExportedNames; - fileName?: string; - componentDocumentation: ComponentDocumentation; + str: MagicString; + uses$$propsOr$$restProps: boolean; + strictMode: boolean; + /** + * If true, not fallback to `any` + * -> all unknown events will throw a type error + * */ + strictEvents: boolean; + isTsFile: boolean; + getters: Set; + usesAccessors: boolean; + exportedNames: ExportedNames; + fileName?: string; + componentDocumentation: ComponentDocumentation; } type TemplateProcessResult = { - uses$$props: boolean; - uses$$restProps: boolean; - uses$$slots: boolean; - slots: Map>; - scriptTag: Node; - moduleScriptTag: Node; - /** To be added later as a comment on the default class export */ - componentDocumentation: ComponentDocumentation; - events: ComponentEvents; - resolvedStores: string[]; - usesAccessors: boolean; + uses$$props: boolean; + uses$$restProps: boolean; + uses$$slots: boolean; + slots: Map>; + scriptTag: Node; + moduleScriptTag: Node; + /** To be added later as a comment on the default class export */ + componentDocumentation: ComponentDocumentation; + events: ComponentEvents; + resolvedStores: string[]; + usesAccessors: boolean; }; /** @@ -73,282 +73,282 @@ type TemplateProcessResult = { const COMPONENT_SUFFIX = '__SvelteComponent_'; function processSvelteTemplate( - str: MagicString, - options?: { emitOnTemplateError?: boolean; namespace?: string } + str: MagicString, + options?: { emitOnTemplateError?: boolean; namespace?: string } ): TemplateProcessResult { - const htmlxAst = parseHtmlx(str.original, options); - - let uses$$props = false; - let uses$$restProps = false; - let uses$$slots = false; - let usesAccessors = false; - - const componentDocumentation = new ComponentDocumentation(); - - //track if we are in a declaration scope - const isDeclaration = { value: false }; - - //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes - //which prevents us just changing all instances of Identity that start with $ - - const scopeStack = new ScopeStack(); - const stores = new Stores(scopeStack, str, isDeclaration); - const scripts = new Scripts(htmlxAst); - - const handleSvelteOptions = (node: Node) => { - for (let i = 0; i < node.attributes.length; i++) { - const optionName = node.attributes[i].name; - const optionValue = node.attributes[i].value; - - switch (optionName) { - case 'accessors': - if (Array.isArray(optionValue)) { - if (optionValue[0].type === 'MustacheTag') { - usesAccessors = optionValue[0].expression.value; - } - } else { - usesAccessors = true; - } - break; - } - } - }; - - const handleIdentifier = (node: Node) => { - if (node.name === '$$props') { - uses$$props = true; - return; - } - if (node.name === '$$restProps') { - uses$$restProps = true; - return; - } - - if (node.name === '$$slots') { - uses$$slots = true; - return; - } - }; - - const handleStyleTag = (node: Node) => { - str.remove(node.start, node.end); - }; - - const slotHandler = new SlotHandler(str.original); - let templateScope = new TemplateScope(); - - const handleEach = (node: Node) => { - templateScope = templateScope.child(); - - if (node.context) { - handleScopeAndResolveForSlotInner(node.context, node.expression, node); - } - }; - - const handleAwait = (node: Node) => { - templateScope = templateScope.child(); - if (node.value) { - handleScopeAndResolveForSlotInner(node.value, node.expression, node.then); - } - if (node.error) { - handleScopeAndResolveForSlotInner(node.error, node.expression, node.catch); - } - }; - - const handleComponentLet = (component: Node) => { - templateScope = templateScope.child(); - const lets = slotHandler.getSlotConsumerOfComponent(component); - - for (const { letNode, slotName } of lets) { - handleScopeAndResolveLetVarForSlot({ - letNode, - slotName, - slotHandler, - templateScope, - component - }); - } - }; - - const handleScopeAndResolveForSlotInner = ( - identifierDef: Node, - initExpression: Node, - owner: Node - ) => { - handleScopeAndResolveForSlot({ - identifierDef, - initExpression, - slotHandler, - templateScope, - owner - }); - }; - - const eventHandler = new EventHandler(); - - const onHtmlxWalk = (node: Node, parent: Node, prop: string) => { - if ( - prop == 'params' && - (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression') - ) { - isDeclaration.value = true; - } - if (prop == 'id' && parent.type == 'VariableDeclarator') { - isDeclaration.value = true; - } - - switch (node.type) { - case 'Comment': - componentDocumentation.handleComment(node); - break; - case 'Options': - handleSvelteOptions(node); - break; - case 'Identifier': - handleIdentifier(node); - stores.handleIdentifier(node, parent, prop); - eventHandler.handleIdentifier(node, parent, prop); - break; - case 'Transition': - case 'Action': - case 'Animation': - stores.handleDirective(node, str); - break; - case 'Slot': - slotHandler.handleSlot(node, templateScope); - break; - case 'Style': - handleStyleTag(node); - break; - case 'Element': - scripts.handleScriptTag(node, parent); - break; - case 'BlockStatement': - scopeStack.push(); - break; - case 'FunctionDeclaration': - scopeStack.push(); - break; - case 'ArrowFunctionExpression': - scopeStack.push(); - break; - case 'EventHandler': - eventHandler.handleEventHandler(node, parent); - break; - case 'VariableDeclarator': - isDeclaration.value = true; - break; - case 'EachBlock': - handleEach(node); - break; - case 'AwaitBlock': - handleAwait(node); - break; - case 'InlineComponent': - handleComponentLet(node); - break; - } - }; - - const onHtmlxLeave = (node: Node, parent: Node, prop: string, _index: number) => { - if ( - prop == 'params' && - (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression') - ) { - isDeclaration.value = false; - } - - if (prop == 'id' && parent.type == 'VariableDeclarator') { - isDeclaration.value = false; - } - const onTemplateScopeLeave = () => { - templateScope = templateScope.parent; - }; - - switch (node.type) { - case 'BlockStatement': - scopeStack.pop(); - break; - case 'FunctionDeclaration': - scopeStack.pop(); - break; - case 'ArrowFunctionExpression': - scopeStack.pop(); - break; - case 'EachBlock': - onTemplateScopeLeave(); - break; - case 'AwaitBlock': - onTemplateScopeLeave(); - break; - case 'InlineComponent': - onTemplateScopeLeave(); - break; - } - }; - - convertHtmlxToJsx(str, htmlxAst, onHtmlxWalk, onHtmlxLeave, { - preserveAttributeCase: options?.namespace == 'foreign' - }); - - // resolve scripts - const { scriptTag, moduleScriptTag } = scripts.getTopLevelScriptTags(); - scripts.blankOtherScriptTags(str); - - //resolve stores - const resolvedStores = stores.resolveStores(); - - return { - moduleScriptTag, - scriptTag, - slots: slotHandler.getSlotDef(), - events: new ComponentEvents(eventHandler), - uses$$props, - uses$$restProps, - uses$$slots, - componentDocumentation, - resolvedStores, - usesAccessors - }; + const htmlxAst = parseHtmlx(str.original, options); + + let uses$$props = false; + let uses$$restProps = false; + let uses$$slots = false; + let usesAccessors = false; + + const componentDocumentation = new ComponentDocumentation(); + + //track if we are in a declaration scope + const isDeclaration = { value: false }; + + //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes + //which prevents us just changing all instances of Identity that start with $ + + const scopeStack = new ScopeStack(); + const stores = new Stores(scopeStack, str, isDeclaration); + const scripts = new Scripts(htmlxAst); + + const handleSvelteOptions = (node: Node) => { + for (let i = 0; i < node.attributes.length; i++) { + const optionName = node.attributes[i].name; + const optionValue = node.attributes[i].value; + + switch (optionName) { + case 'accessors': + if (Array.isArray(optionValue)) { + if (optionValue[0].type === 'MustacheTag') { + usesAccessors = optionValue[0].expression.value; + } + } else { + usesAccessors = true; + } + break; + } + } + }; + + const handleIdentifier = (node: Node) => { + if (node.name === '$$props') { + uses$$props = true; + return; + } + if (node.name === '$$restProps') { + uses$$restProps = true; + return; + } + + if (node.name === '$$slots') { + uses$$slots = true; + return; + } + }; + + const handleStyleTag = (node: Node) => { + str.remove(node.start, node.end); + }; + + const slotHandler = new SlotHandler(str.original); + let templateScope = new TemplateScope(); + + const handleEach = (node: Node) => { + templateScope = templateScope.child(); + + if (node.context) { + handleScopeAndResolveForSlotInner(node.context, node.expression, node); + } + }; + + const handleAwait = (node: Node) => { + templateScope = templateScope.child(); + if (node.value) { + handleScopeAndResolveForSlotInner(node.value, node.expression, node.then); + } + if (node.error) { + handleScopeAndResolveForSlotInner(node.error, node.expression, node.catch); + } + }; + + const handleComponentLet = (component: Node) => { + templateScope = templateScope.child(); + const lets = slotHandler.getSlotConsumerOfComponent(component); + + for (const { letNode, slotName } of lets) { + handleScopeAndResolveLetVarForSlot({ + letNode, + slotName, + slotHandler, + templateScope, + component + }); + } + }; + + const handleScopeAndResolveForSlotInner = ( + identifierDef: Node, + initExpression: Node, + owner: Node + ) => { + handleScopeAndResolveForSlot({ + identifierDef, + initExpression, + slotHandler, + templateScope, + owner + }); + }; + + const eventHandler = new EventHandler(); + + const onHtmlxWalk = (node: Node, parent: Node, prop: string) => { + if ( + prop == 'params' && + (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression') + ) { + isDeclaration.value = true; + } + if (prop == 'id' && parent.type == 'VariableDeclarator') { + isDeclaration.value = true; + } + + switch (node.type) { + case 'Comment': + componentDocumentation.handleComment(node); + break; + case 'Options': + handleSvelteOptions(node); + break; + case 'Identifier': + handleIdentifier(node); + stores.handleIdentifier(node, parent, prop); + eventHandler.handleIdentifier(node, parent, prop); + break; + case 'Transition': + case 'Action': + case 'Animation': + stores.handleDirective(node, str); + break; + case 'Slot': + slotHandler.handleSlot(node, templateScope); + break; + case 'Style': + handleStyleTag(node); + break; + case 'Element': + scripts.handleScriptTag(node, parent); + break; + case 'BlockStatement': + scopeStack.push(); + break; + case 'FunctionDeclaration': + scopeStack.push(); + break; + case 'ArrowFunctionExpression': + scopeStack.push(); + break; + case 'EventHandler': + eventHandler.handleEventHandler(node, parent); + break; + case 'VariableDeclarator': + isDeclaration.value = true; + break; + case 'EachBlock': + handleEach(node); + break; + case 'AwaitBlock': + handleAwait(node); + break; + case 'InlineComponent': + handleComponentLet(node); + break; + } + }; + + const onHtmlxLeave = (node: Node, parent: Node, prop: string, _index: number) => { + if ( + prop == 'params' && + (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression') + ) { + isDeclaration.value = false; + } + + if (prop == 'id' && parent.type == 'VariableDeclarator') { + isDeclaration.value = false; + } + const onTemplateScopeLeave = () => { + templateScope = templateScope.parent; + }; + + switch (node.type) { + case 'BlockStatement': + scopeStack.pop(); + break; + case 'FunctionDeclaration': + scopeStack.pop(); + break; + case 'ArrowFunctionExpression': + scopeStack.pop(); + break; + case 'EachBlock': + onTemplateScopeLeave(); + break; + case 'AwaitBlock': + onTemplateScopeLeave(); + break; + case 'InlineComponent': + onTemplateScopeLeave(); + break; + } + }; + + convertHtmlxToJsx(str, htmlxAst, onHtmlxWalk, onHtmlxLeave, { + preserveAttributeCase: options?.namespace == 'foreign' + }); + + // resolve scripts + const { scriptTag, moduleScriptTag } = scripts.getTopLevelScriptTags(); + scripts.blankOtherScriptTags(str); + + //resolve stores + const resolvedStores = stores.resolveStores(); + + return { + moduleScriptTag, + scriptTag, + slots: slotHandler.getSlotDef(), + events: new ComponentEvents(eventHandler), + uses$$props, + uses$$restProps, + uses$$slots, + componentDocumentation, + resolvedStores, + usesAccessors + }; } function addComponentExport({ - str, - uses$$propsOr$$restProps, - strictMode, - strictEvents, - isTsFile, - getters, - usesAccessors, - exportedNames, - fileName, - componentDocumentation + str, + uses$$propsOr$$restProps, + strictMode, + strictEvents, + isTsFile, + getters, + usesAccessors, + exportedNames, + fileName, + componentDocumentation }: AddComponentExportPara) { - const eventsDef = strictEvents ? 'render' : '__sveltets_with_any_event(render)'; - const propDef = - // Omit partial-wrapper only if both strict mode and ts file, because - // in a js file the user has no way of telling the language that - // the prop is optional - strictMode && isTsFile - ? uses$$propsOr$$restProps - ? `__sveltets_with_any(${eventsDef})` - : eventsDef - : `__sveltets_partial${isTsFile ? '_ts' : ''}${ - uses$$propsOr$$restProps ? '_with_any' : '' - }(${eventsDef})`; - - const doc = componentDocumentation.getFormatted(); - const className = fileName && classNameFromFilename(fileName); - - const statement = - `\n\n${doc}export default class${ - className ? ` ${className}` : '' - } extends createSvelte2TsxComponent(${propDef}) {` + - createClassGetters(getters) + - (usesAccessors ? createClassAccessors(getters, exportedNames) : '') + - '\n}'; - - str.append(statement); + const eventsDef = strictEvents ? 'render' : '__sveltets_with_any_event(render)'; + const propDef = + // Omit partial-wrapper only if both strict mode and ts file, because + // in a js file the user has no way of telling the language that + // the prop is optional + strictMode && isTsFile + ? uses$$propsOr$$restProps + ? `__sveltets_with_any(${eventsDef})` + : eventsDef + : `__sveltets_partial${isTsFile ? '_ts' : ''}${ + uses$$propsOr$$restProps ? '_with_any' : '' + }(${eventsDef})`; + + const doc = componentDocumentation.getFormatted(); + const className = fileName && classNameFromFilename(fileName); + + const statement = + `\n\n${doc}export default class${ + className ? ` ${className}` : '' + } extends createSvelte2TsxComponent(${propDef}) {` + + createClassGetters(getters) + + (usesAccessors ? createClassAccessors(getters, exportedNames) : '') + + '\n}'; + + str.append(statement); } /** @@ -358,206 +358,206 @@ function addComponentExport({ * https://svelte.dev/docs#Tags */ function classNameFromFilename(filename: string): string | undefined { - try { - const withoutExtensions = path.parse(filename).name?.split('.')[0]; - const withoutInvalidCharacters = withoutExtensions - .split('') - // Although "-" is invalid, we leave it in, pascal-case-handling will throw it out later - .filter((char) => /[A-Za-z$_\d-]/.test(char)) - .join(''); - const firstValidCharIdx = withoutInvalidCharacters - .split('') - // Although _ and $ are valid first characters for classes, they are invalid first characters - // for tag names. For a better import autocompletion experience, we therefore throw them out. - .findIndex((char) => /[A-Za-z]/.test(char)); - const withoutLeadingInvalidCharacters = withoutInvalidCharacters.substr(firstValidCharIdx); - const inPascalCase = pascalCase(withoutLeadingInvalidCharacters); - const finalName = firstValidCharIdx === -1 ? `A${inPascalCase}` : inPascalCase; - return `${finalName}${COMPONENT_SUFFIX}`; - } catch (error) { - console.warn(`Failed to create a name for the component class from filename ${filename}`); - return undefined; - } + try { + const withoutExtensions = path.parse(filename).name?.split('.')[0]; + const withoutInvalidCharacters = withoutExtensions + .split('') + // Although "-" is invalid, we leave it in, pascal-case-handling will throw it out later + .filter((char) => /[A-Za-z$_\d-]/.test(char)) + .join(''); + const firstValidCharIdx = withoutInvalidCharacters + .split('') + // Although _ and $ are valid first characters for classes, they are invalid first characters + // for tag names. For a better import autocompletion experience, we therefore throw them out. + .findIndex((char) => /[A-Za-z]/.test(char)); + const withoutLeadingInvalidCharacters = withoutInvalidCharacters.substr(firstValidCharIdx); + const inPascalCase = pascalCase(withoutLeadingInvalidCharacters); + const finalName = firstValidCharIdx === -1 ? `A${inPascalCase}` : inPascalCase; + return `${finalName}${COMPONENT_SUFFIX}`; + } catch (error) { + console.warn(`Failed to create a name for the component class from filename ${filename}`); + return undefined; + } } function createRenderFunction({ - str, - scriptTag, - scriptDestination, - slots, - getters, - events, - exportedNames, - isTsFile, - uses$$props, - uses$$restProps, - uses$$slots + str, + scriptTag, + scriptDestination, + slots, + getters, + events, + exportedNames, + isTsFile, + uses$$props, + uses$$restProps, + uses$$slots }: CreateRenderFunctionPara) { - const htmlx = str.original; - let propsDecl = ''; - - if (uses$$props) { - propsDecl += ' let $$props = __sveltets_allPropsType();'; - } - if (uses$$restProps) { - propsDecl += ' let $$restProps = __sveltets_restPropsType();'; - } - - if (uses$$slots) { - propsDecl += - ' let $$slots = __sveltets_slotsType({' + - Array.from(slots.keys()) - .map((name) => `'${name}': ''`) - .join(', ') + - '});'; - } - - if (scriptTag) { - //I couldn't get magicstring to let me put the script before the <> we prepend during conversion of the template to jsx, so we just close it instead - const scriptTagEnd = htmlx.lastIndexOf('>', scriptTag.content.start) + 1; - str.overwrite(scriptTag.start, scriptTag.start + 1, ';'); - str.overwrite(scriptTag.start + 1, scriptTagEnd, `function render() {${propsDecl}\n`); - - const scriptEndTagStart = htmlx.lastIndexOf('<', scriptTag.end - 1); - // wrap template with callback - str.overwrite(scriptEndTagStart, scriptTag.end, ';\n() => (<>', { - contentOnly: true - }); - } else { - str.prependRight(scriptDestination, `;function render() {${propsDecl}\n<>`); - } - - const slotsAsDef = - '{' + - Array.from(slots.entries()) - .map(([name, attrs]) => { - const attrsAsString = Array.from(attrs.entries()) - .map(([exportName, expr]) => `${exportName}:${expr}`) - .join(', '); - return `'${name}': {${attrsAsString}}`; - }) - .join(', ') + - '}'; - - const returnString = - `\nreturn { props: ${exportedNames.createPropsStr(isTsFile)}` + - `, slots: ${slotsAsDef}` + - `, getters: ${createRenderFunctionGetterStr(getters)}` + - `, events: ${events.toDefString()} }}`; - - // wrap template with callback - if (scriptTag) { - str.append(');'); - } - - str.append(returnString); + const htmlx = str.original; + let propsDecl = ''; + + if (uses$$props) { + propsDecl += ' let $$props = __sveltets_allPropsType();'; + } + if (uses$$restProps) { + propsDecl += ' let $$restProps = __sveltets_restPropsType();'; + } + + if (uses$$slots) { + propsDecl += + ' let $$slots = __sveltets_slotsType({' + + Array.from(slots.keys()) + .map((name) => `'${name}': ''`) + .join(', ') + + '});'; + } + + if (scriptTag) { + //I couldn't get magicstring to let me put the script before the <> we prepend during conversion of the template to jsx, so we just close it instead + const scriptTagEnd = htmlx.lastIndexOf('>', scriptTag.content.start) + 1; + str.overwrite(scriptTag.start, scriptTag.start + 1, ';'); + str.overwrite(scriptTag.start + 1, scriptTagEnd, `function render() {${propsDecl}\n`); + + const scriptEndTagStart = htmlx.lastIndexOf('<', scriptTag.end - 1); + // wrap template with callback + str.overwrite(scriptEndTagStart, scriptTag.end, ';\n() => (<>', { + contentOnly: true + }); + } else { + str.prependRight(scriptDestination, `;function render() {${propsDecl}\n<>`); + } + + const slotsAsDef = + '{' + + Array.from(slots.entries()) + .map(([name, attrs]) => { + const attrsAsString = Array.from(attrs.entries()) + .map(([exportName, expr]) => `${exportName}:${expr}`) + .join(', '); + return `'${name}': {${attrsAsString}}`; + }) + .join(', ') + + '}'; + + const returnString = + `\nreturn { props: ${exportedNames.createPropsStr(isTsFile)}` + + `, slots: ${slotsAsDef}` + + `, getters: ${createRenderFunctionGetterStr(getters)}` + + `, events: ${events.toDefString()} }}`; + + // wrap template with callback + if (scriptTag) { + str.append(');'); + } + + str.append(returnString); } export function svelte2tsx( - svelte: string, - options?: { - filename?: string; - strictMode?: boolean; - isTsFile?: boolean; - emitOnTemplateError?: boolean; - namespace?: string; - } + svelte: string, + options?: { + filename?: string; + strictMode?: boolean; + isTsFile?: boolean; + emitOnTemplateError?: boolean; + namespace?: string; + } ) { - const str = new MagicString(svelte); - // process the htmlx as a svelte template - let { - moduleScriptTag, - scriptTag, - slots, - uses$$props, - uses$$slots, - uses$$restProps, - events, - componentDocumentation, - resolvedStores, - usesAccessors - } = processSvelteTemplate(str, options); - - /* Rearrange the script tags so that module is first, and instance second followed finally by the template - * This is a bit convoluted due to some trouble I had with magic string. A simple str.move(start,end,0) for each script wasn't enough - * since if the module script was already at 0, it wouldn't move (which is fine) but would mean the order would be swapped when the script tag tried to move to 0 - * In this case we instead have to move it to moduleScriptTag.end. We track the location for the script move in the MoveInstanceScriptTarget var - */ - let instanceScriptTarget = 0; - - if (moduleScriptTag) { - if (moduleScriptTag.start != 0) { - //move our module tag to the top - str.move(moduleScriptTag.start, moduleScriptTag.end, 0); - } else { - //since our module script was already at position 0, we need to move our instance script tag to the end of it. - instanceScriptTarget = moduleScriptTag.end; - } - } - - const renderFunctionStart = scriptTag - ? str.original.lastIndexOf('>', scriptTag.content.start) + 1 - : instanceScriptTarget; - const implicitStoreValues = new ImplicitStoreValues(resolvedStores, renderFunctionStart); - //move the instance script and process the content - let exportedNames = new ExportedNames(); - let getters = new Set(); - if (scriptTag) { - //ensure it is between the module script and the rest of the template (the variables need to be declared before the jsx template) - if (scriptTag.start != instanceScriptTarget) { - str.move(scriptTag.start, scriptTag.end, instanceScriptTarget); - } - const res = processInstanceScriptContent(str, scriptTag, events, implicitStoreValues); - uses$$props = uses$$props || res.uses$$props; - uses$$restProps = uses$$restProps || res.uses$$restProps; - uses$$slots = uses$$slots || res.uses$$slots; - - ({ exportedNames, events, getters } = res); - } - - //wrap the script tag and template content in a function returning the slot and exports - createRenderFunction({ - str, - scriptTag, - scriptDestination: instanceScriptTarget, - slots, - events, - getters, - exportedNames, - isTsFile: options?.isTsFile, - uses$$props, - uses$$restProps, - uses$$slots - }); - - // we need to process the module script after the instance script has moved otherwise we get warnings about moving edited items - if (moduleScriptTag) { - processModuleScriptTag( - str, - moduleScriptTag, - new ImplicitStoreValues(implicitStoreValues.getAccessedStores(), renderFunctionStart) - ); - } - - addComponentExport({ - str, - uses$$propsOr$$restProps: uses$$props || uses$$restProps, - strictMode: !!options?.strictMode, - strictEvents: events.hasInterface(), - isTsFile: options?.isTsFile, - getters, - exportedNames, - usesAccessors, - fileName: options?.filename, - componentDocumentation - }); - - str.prepend('///\n'); - - return { - code: str.toString(), - map: str.generateMap({ hires: true, source: options?.filename }), - exportedNames, - events: events.createAPI() - }; + const str = new MagicString(svelte); + // process the htmlx as a svelte template + let { + moduleScriptTag, + scriptTag, + slots, + uses$$props, + uses$$slots, + uses$$restProps, + events, + componentDocumentation, + resolvedStores, + usesAccessors + } = processSvelteTemplate(str, options); + + /* Rearrange the script tags so that module is first, and instance second followed finally by the template + * This is a bit convoluted due to some trouble I had with magic string. A simple str.move(start,end,0) for each script wasn't enough + * since if the module script was already at 0, it wouldn't move (which is fine) but would mean the order would be swapped when the script tag tried to move to 0 + * In this case we instead have to move it to moduleScriptTag.end. We track the location for the script move in the MoveInstanceScriptTarget var + */ + let instanceScriptTarget = 0; + + if (moduleScriptTag) { + if (moduleScriptTag.start != 0) { + //move our module tag to the top + str.move(moduleScriptTag.start, moduleScriptTag.end, 0); + } else { + //since our module script was already at position 0, we need to move our instance script tag to the end of it. + instanceScriptTarget = moduleScriptTag.end; + } + } + + const renderFunctionStart = scriptTag + ? str.original.lastIndexOf('>', scriptTag.content.start) + 1 + : instanceScriptTarget; + const implicitStoreValues = new ImplicitStoreValues(resolvedStores, renderFunctionStart); + //move the instance script and process the content + let exportedNames = new ExportedNames(); + let getters = new Set(); + if (scriptTag) { + //ensure it is between the module script and the rest of the template (the variables need to be declared before the jsx template) + if (scriptTag.start != instanceScriptTarget) { + str.move(scriptTag.start, scriptTag.end, instanceScriptTarget); + } + const res = processInstanceScriptContent(str, scriptTag, events, implicitStoreValues); + uses$$props = uses$$props || res.uses$$props; + uses$$restProps = uses$$restProps || res.uses$$restProps; + uses$$slots = uses$$slots || res.uses$$slots; + + ({ exportedNames, events, getters } = res); + } + + //wrap the script tag and template content in a function returning the slot and exports + createRenderFunction({ + str, + scriptTag, + scriptDestination: instanceScriptTarget, + slots, + events, + getters, + exportedNames, + isTsFile: options?.isTsFile, + uses$$props, + uses$$restProps, + uses$$slots + }); + + // we need to process the module script after the instance script has moved otherwise we get warnings about moving edited items + if (moduleScriptTag) { + processModuleScriptTag( + str, + moduleScriptTag, + new ImplicitStoreValues(implicitStoreValues.getAccessedStores(), renderFunctionStart) + ); + } + + addComponentExport({ + str, + uses$$propsOr$$restProps: uses$$props || uses$$restProps, + strictMode: !!options?.strictMode, + strictEvents: events.hasInterface(), + isTsFile: options?.isTsFile, + getters, + exportedNames, + usesAccessors, + fileName: options?.filename, + componentDocumentation + }); + + str.prepend('///\n'); + + return { + code: str.toString(), + map: str.generateMap({ hires: true, source: options?.filename }), + exportedNames, + events: events.createAPI() + }; } diff --git a/packages/svelte2tsx/src/svelte2tsx/knownevents.ts b/packages/svelte2tsx/src/svelte2tsx/knownevents.ts index eaa6e5ed7..8427a3f82 100644 --- a/packages/svelte2tsx/src/svelte2tsx/knownevents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/knownevents.ts @@ -1,118 +1,118 @@ export default [ - 'oncopy', - 'oncut', - 'onpaste', + 'oncopy', + 'oncut', + 'onpaste', - // composition events - 'oncompositionend', - 'oncompositionstart', - 'oncompositionupdate', + // composition events + 'oncompositionend', + 'oncompositionstart', + 'oncompositionupdate', - // focus events - 'onfocus', - 'onblur', + // focus events + 'onfocus', + 'onblur', - // form events - 'onchange', - 'oninput', - 'onreset', - 'onsubmit', - 'oninvalid', + // form events + 'onchange', + 'oninput', + 'onreset', + 'onsubmit', + 'oninvalid', - // image events - 'onload', - 'onerror', + // image events + 'onload', + 'onerror', - // keyboard events - 'onkeydown', - 'onkeypress', - 'onkeyup', + // keyboard events + 'onkeydown', + 'onkeypress', + 'onkeyup', - // media events - 'onabort', - 'oncanplay', - 'oncanplaythrough', - 'oncuechange', - 'ondurationchange', - 'onemptied', - 'onencrypted', - 'onended', - 'onloadeddata', - 'onloadedmetadata', - 'onloadstart', - 'onpause', - 'onplay', - 'onplaying', - 'onprogress', - 'onratechange', - 'onseeked', - 'onseeking', - 'onstalled', - 'onsuspend', - 'ontimeupdate', - 'onvolumechange', - 'onwaiting', + // media events + 'onabort', + 'oncanplay', + 'oncanplaythrough', + 'oncuechange', + 'ondurationchange', + 'onemptied', + 'onencrypted', + 'onended', + 'onloadeddata', + 'onloadedmetadata', + 'onloadstart', + 'onpause', + 'onplay', + 'onplaying', + 'onprogress', + 'onratechange', + 'onseeked', + 'onseeking', + 'onstalled', + 'onsuspend', + 'ontimeupdate', + 'onvolumechange', + 'onwaiting', - // mouseevents - 'onclick', - 'onauxclick', - 'oncontextmenu', - 'ondblclick', - 'ondrag', - 'ondragend', - 'ondragenter', - 'ondragexit', - 'ondragleave', - 'ondragover', - 'ondragstart', - 'ondrop', - 'onmousedown', - 'onmouseenter', - 'onmouseleave', - 'onmousemove', - 'onmouseout', - 'onmouseover', - 'onmouseup', + // mouseevents + 'onclick', + 'onauxclick', + 'oncontextmenu', + 'ondblclick', + 'ondrag', + 'ondragend', + 'ondragenter', + 'ondragexit', + 'ondragleave', + 'ondragover', + 'ondragstart', + 'ondrop', + 'onmousedown', + 'onmouseenter', + 'onmouseleave', + 'onmousemove', + 'onmouseout', + 'onmouseover', + 'onmouseup', - // selection events - 'onselect', - 'onselectionchange', - 'onselectstart', + // selection events + 'onselect', + 'onselectionchange', + 'onselectstart', - // touch events - 'ontouchcancel', - 'ontouchend', - 'ontouchmove', - 'ontouchstart', + // touch events + 'ontouchcancel', + 'ontouchend', + 'ontouchmove', + 'ontouchstart', - // pointer events - 'ongotpointercapture', - 'onpointercancel', - 'onpointerdown', - 'onpointerenter', - 'onpointerleave', - 'onpointermove', - 'onpointerout', - 'onpointerover', - 'onpointerup', - 'onlostpointercapture', + // pointer events + 'ongotpointercapture', + 'onpointercancel', + 'onpointerdown', + 'onpointerenter', + 'onpointerleave', + 'onpointermove', + 'onpointerout', + 'onpointerover', + 'onpointerup', + 'onlostpointercapture', - // ui events - 'onscroll', + // ui events + 'onscroll', - // wheel events - 'onwheel', + // wheel events + 'onwheel', - // animation events - 'onanimationstart', - 'onanimationend', - 'onanimationiteration', + // animation events + 'onanimationstart', + 'onanimationend', + 'onanimationiteration', - // transition events - 'ontransitionend', + // transition events + 'ontransitionend', - // global events - 'oncancel', - 'onmessage', - 'onmessageerror' + // global events + 'oncancel', + 'onmessage', + 'onmessageerror' ]; diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentDocumentation.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentDocumentation.ts index 43eb98074..9157f5b08 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentDocumentation.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentDocumentation.ts @@ -8,33 +8,33 @@ import dedent from 'dedent-js'; const COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG = '@component'; export class ComponentDocumentation { - private componentDocumentation = ''; + private componentDocumentation = ''; - handleComment = (node: Node) => { - if ( - 'data' in node && - typeof node.data === 'string' && - node.data.includes(COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG) - ) { - this.componentDocumentation = node.data - .replace(COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG, '') - .trim(); - } - }; + handleComment = (node: Node) => { + if ( + 'data' in node && + typeof node.data === 'string' && + node.data.includes(COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG) + ) { + this.componentDocumentation = node.data + .replace(COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG, '') + .trim(); + } + }; - getFormatted() { - if (!this.componentDocumentation) { - return ''; - } - if (!this.componentDocumentation.includes('\n')) { - return `/** ${this.componentDocumentation} */\n`; - } + getFormatted() { + if (!this.componentDocumentation) { + return ''; + } + if (!this.componentDocumentation.includes('\n')) { + return `/** ${this.componentDocumentation} */\n`; + } - const lines = dedent(this.componentDocumentation) - .split('\n') - .map((line) => ` *${line ? ` ${line}` : ''}`) - .join('\n'); + const lines = dedent(this.componentDocumentation) + .split('\n') + .map((line) => ` *${line ? ` ${line}` : ''}`) + .join('\n'); - return `/**\n${lines}\n */\n`; - } + return `/**\n${lines}\n */\n`; + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts index 09f1fff59..a3b87a85b 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts @@ -19,293 +19,293 @@ import { getVariableAtTopLevel, getLastLeadingDoc } from '../utils/tsAst'; * - If no, track all invocations of it to get the event names */ export class ComponentEvents { - private componentEventsInterface?: ComponentEventsFromInterface; - private componentEventsFromEventsMap: ComponentEventsFromEventsMap; - - private get eventsClass() { - return this.componentEventsInterface || this.componentEventsFromEventsMap; - } - - constructor(eventHandler: EventHandler) { - this.componentEventsFromEventsMap = new ComponentEventsFromEventsMap(eventHandler); - } - - /** - * Collect state and create the API which will be part - * of the return object of the `svelte2tsx` function. - */ - createAPI() { - const entries: Array<{ name: string; type: string; doc?: string }> = []; - - const iterableEntries = this.eventsClass.events.entries(); - for (const entry of iterableEntries) { - entries.push({ name: entry[0], ...entry[1] }); - } - - return { - getAll(): Array<{ name: string; type?: string; doc?: string }> { - return entries; - } - }; - } - - setComponentEventsInterface(node: ts.InterfaceDeclaration): void { - this.componentEventsInterface = new ComponentEventsFromInterface(node); - } - - hasInterface(): boolean { - return !!this.componentEventsInterface; - } - - checkIfImportIsEventDispatcher(node: ts.ImportDeclaration): void { - this.componentEventsFromEventsMap.checkIfImportIsEventDispatcher(node); - } - - checkIfIsStringLiteralDeclaration(node: ts.VariableDeclaration): void { - this.componentEventsFromEventsMap.checkIfIsStringLiteralDeclaration(node); - } - - checkIfDeclarationInstantiatedEventDispatcher(node: ts.VariableDeclaration): void { - this.componentEventsFromEventsMap.checkIfDeclarationInstantiatedEventDispatcher(node); - } - - checkIfCallExpressionIsDispatch(node: ts.CallExpression): void { - this.componentEventsFromEventsMap.checkIfCallExpressionIsDispatch(node); - } - - toDefString(): string { - return this.eventsClass.toDefString(); - } + private componentEventsInterface?: ComponentEventsFromInterface; + private componentEventsFromEventsMap: ComponentEventsFromEventsMap; + + private get eventsClass() { + return this.componentEventsInterface || this.componentEventsFromEventsMap; + } + + constructor(eventHandler: EventHandler) { + this.componentEventsFromEventsMap = new ComponentEventsFromEventsMap(eventHandler); + } + + /** + * Collect state and create the API which will be part + * of the return object of the `svelte2tsx` function. + */ + createAPI() { + const entries: Array<{ name: string; type: string; doc?: string }> = []; + + const iterableEntries = this.eventsClass.events.entries(); + for (const entry of iterableEntries) { + entries.push({ name: entry[0], ...entry[1] }); + } + + return { + getAll(): Array<{ name: string; type?: string; doc?: string }> { + return entries; + } + }; + } + + setComponentEventsInterface(node: ts.InterfaceDeclaration): void { + this.componentEventsInterface = new ComponentEventsFromInterface(node); + } + + hasInterface(): boolean { + return !!this.componentEventsInterface; + } + + checkIfImportIsEventDispatcher(node: ts.ImportDeclaration): void { + this.componentEventsFromEventsMap.checkIfImportIsEventDispatcher(node); + } + + checkIfIsStringLiteralDeclaration(node: ts.VariableDeclaration): void { + this.componentEventsFromEventsMap.checkIfIsStringLiteralDeclaration(node); + } + + checkIfDeclarationInstantiatedEventDispatcher(node: ts.VariableDeclaration): void { + this.componentEventsFromEventsMap.checkIfDeclarationInstantiatedEventDispatcher(node); + } + + checkIfCallExpressionIsDispatch(node: ts.CallExpression): void { + this.componentEventsFromEventsMap.checkIfCallExpressionIsDispatch(node); + } + + toDefString(): string { + return this.eventsClass.toDefString(); + } } class ComponentEventsFromInterface { - events = new Map(); + events = new Map(); - constructor(node: ts.InterfaceDeclaration) { - this.events = this.extractEvents(node); - } + constructor(node: ts.InterfaceDeclaration) { + this.events = this.extractEvents(node); + } - toDefString() { - return '{} as unknown as ComponentEvents'; - } + toDefString() { + return '{} as unknown as ComponentEvents'; + } - private extractEvents(node: ts.InterfaceDeclaration) { - const map = new Map(); + private extractEvents(node: ts.InterfaceDeclaration) { + const map = new Map(); - node.members.filter(ts.isPropertySignature).forEach((member) => { - map.set(getName(member.name), { - type: member.type?.getText() || 'Event', - doc: getDoc(member) - }); - }); + node.members.filter(ts.isPropertySignature).forEach((member) => { + map.set(getName(member.name), { + type: member.type?.getText() || 'Event', + doc: getDoc(member) + }); + }); - return map; - } + return map; + } } class ComponentEventsFromEventsMap { - events = new Map(); - private dispatchedEvents = new Set(); - private stringVars = new Map(); - private eventDispatcherImport = ''; - private eventDispatchers: Array<{ name: string; typing?: string }> = []; - - constructor(private eventHandler: EventHandler) { - this.events = this.extractEvents(eventHandler); - } - - checkIfImportIsEventDispatcher(node: ts.ImportDeclaration) { - if (this.eventDispatcherImport) { - return; - } - if (ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text !== 'svelte') { - return; - } - - const namedImports = node.importClause?.namedBindings; - if (ts.isNamedImports(namedImports)) { - const eventDispatcherImport = namedImports.elements.find( - // If it's an aliased import, propertyName is set - (el) => (el.propertyName || el.name).text === 'createEventDispatcher' - ); - if (eventDispatcherImport) { - this.eventDispatcherImport = eventDispatcherImport.name.text; - } - } - } - - checkIfIsStringLiteralDeclaration(node: ts.VariableDeclaration) { - if ( - ts.isIdentifier(node.name) && - node.initializer && - ts.isStringLiteral(node.initializer) - ) { - this.stringVars.set(node.name.text, node.initializer.text); - } - } - - checkIfDeclarationInstantiatedEventDispatcher(node: ts.VariableDeclaration) { - if (!ts.isIdentifier(node.name) || !node.initializer) { - return; - } - - if ( - ts.isCallExpression(node.initializer) && - ts.isIdentifier(node.initializer.expression) && - node.initializer.expression.text === this.eventDispatcherImport - ) { - const dispatcherName = node.name.text; - const dispatcherTyping = node.initializer.typeArguments?.[0]; - - if (dispatcherTyping && ts.isTypeLiteralNode(dispatcherTyping)) { - this.eventDispatchers.push({ - name: dispatcherName, - typing: dispatcherTyping.getText() - }); - dispatcherTyping.members.filter(ts.isPropertySignature).forEach((member) => { - this.addToEvents(getName(member.name), { - type: `CustomEvent<${member.type?.getText() || 'any'}>`, - doc: getDoc(member) - }); - }); - } else { - this.eventDispatchers.push({ name: dispatcherName }); - this.eventHandler - .getDispatchedEventsForIdentifier(dispatcherName) - .forEach((evtName) => { - this.addToEvents(evtName); - this.dispatchedEvents.add(evtName); - }); - } - } - } - - checkIfCallExpressionIsDispatch(node: ts.CallExpression) { - if ( - this.eventDispatchers.some( - (dispatcher) => - !dispatcher.typing && - ts.isIdentifier(node.expression) && - node.expression.text === dispatcher.name - ) - ) { - const firstArg = node.arguments[0]; - if (ts.isStringLiteral(firstArg)) { - this.addToEvents(firstArg.text); - this.dispatchedEvents.add(firstArg.text); - } else if (ts.isIdentifier(firstArg)) { - const str = this.stringVars.get(firstArg.text); - if (str) { - this.addToEvents(str); - this.dispatchedEvents.add(str); - } - } - } - } - - private addToEvents( - eventName: string, - info: { type: string; doc?: string } = { type: 'CustomEvent' } - ) { - if (this.events.has(eventName)) { - // If there are multiple definitions, merge them by falling back to any-typing - this.events.set(eventName, { type: 'CustomEvent' }); - this.dispatchedEvents.add(eventName); - } else { - this.events.set(eventName, info); - } - } - - toDefString() { - return ( - '{' + - [ - ...this.eventDispatchers - .map( - (dispatcher) => - dispatcher.typing && - `...__sveltets_toEventTypings<${dispatcher.typing}>()` - ) - .filter((str) => !!str), - ...this.eventHandler.bubbledEventsAsStrings(), - ...[...this.dispatchedEvents.keys()].map((e) => `'${e}': __sveltets_customEvent`) - ].join(', ') + - '}' - ); - } - - private extractEvents(eventHandler: EventHandler) { - const map = new Map(); - for (const name of eventHandler.getBubbledEvents().keys()) { - map.set(name, { type: 'Event' }); - } - return map; - } + events = new Map(); + private dispatchedEvents = new Set(); + private stringVars = new Map(); + private eventDispatcherImport = ''; + private eventDispatchers: Array<{ name: string; typing?: string }> = []; + + constructor(private eventHandler: EventHandler) { + this.events = this.extractEvents(eventHandler); + } + + checkIfImportIsEventDispatcher(node: ts.ImportDeclaration) { + if (this.eventDispatcherImport) { + return; + } + if (ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text !== 'svelte') { + return; + } + + const namedImports = node.importClause?.namedBindings; + if (ts.isNamedImports(namedImports)) { + const eventDispatcherImport = namedImports.elements.find( + // If it's an aliased import, propertyName is set + (el) => (el.propertyName || el.name).text === 'createEventDispatcher' + ); + if (eventDispatcherImport) { + this.eventDispatcherImport = eventDispatcherImport.name.text; + } + } + } + + checkIfIsStringLiteralDeclaration(node: ts.VariableDeclaration) { + if ( + ts.isIdentifier(node.name) && + node.initializer && + ts.isStringLiteral(node.initializer) + ) { + this.stringVars.set(node.name.text, node.initializer.text); + } + } + + checkIfDeclarationInstantiatedEventDispatcher(node: ts.VariableDeclaration) { + if (!ts.isIdentifier(node.name) || !node.initializer) { + return; + } + + if ( + ts.isCallExpression(node.initializer) && + ts.isIdentifier(node.initializer.expression) && + node.initializer.expression.text === this.eventDispatcherImport + ) { + const dispatcherName = node.name.text; + const dispatcherTyping = node.initializer.typeArguments?.[0]; + + if (dispatcherTyping && ts.isTypeLiteralNode(dispatcherTyping)) { + this.eventDispatchers.push({ + name: dispatcherName, + typing: dispatcherTyping.getText() + }); + dispatcherTyping.members.filter(ts.isPropertySignature).forEach((member) => { + this.addToEvents(getName(member.name), { + type: `CustomEvent<${member.type?.getText() || 'any'}>`, + doc: getDoc(member) + }); + }); + } else { + this.eventDispatchers.push({ name: dispatcherName }); + this.eventHandler + .getDispatchedEventsForIdentifier(dispatcherName) + .forEach((evtName) => { + this.addToEvents(evtName); + this.dispatchedEvents.add(evtName); + }); + } + } + } + + checkIfCallExpressionIsDispatch(node: ts.CallExpression) { + if ( + this.eventDispatchers.some( + (dispatcher) => + !dispatcher.typing && + ts.isIdentifier(node.expression) && + node.expression.text === dispatcher.name + ) + ) { + const firstArg = node.arguments[0]; + if (ts.isStringLiteral(firstArg)) { + this.addToEvents(firstArg.text); + this.dispatchedEvents.add(firstArg.text); + } else if (ts.isIdentifier(firstArg)) { + const str = this.stringVars.get(firstArg.text); + if (str) { + this.addToEvents(str); + this.dispatchedEvents.add(str); + } + } + } + } + + private addToEvents( + eventName: string, + info: { type: string; doc?: string } = { type: 'CustomEvent' } + ) { + if (this.events.has(eventName)) { + // If there are multiple definitions, merge them by falling back to any-typing + this.events.set(eventName, { type: 'CustomEvent' }); + this.dispatchedEvents.add(eventName); + } else { + this.events.set(eventName, info); + } + } + + toDefString() { + return ( + '{' + + [ + ...this.eventDispatchers + .map( + (dispatcher) => + dispatcher.typing && + `...__sveltets_toEventTypings<${dispatcher.typing}>()` + ) + .filter((str) => !!str), + ...this.eventHandler.bubbledEventsAsStrings(), + ...[...this.dispatchedEvents.keys()].map((e) => `'${e}': __sveltets_customEvent`) + ].join(', ') + + '}' + ); + } + + private extractEvents(eventHandler: EventHandler) { + const map = new Map(); + for (const name of eventHandler.getBubbledEvents().keys()) { + map.set(name, { type: 'Event' }); + } + return map; + } } function getName(prop: ts.PropertyName) { - if (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) { - return prop.text; - } - - if (ts.isComputedPropertyName(prop)) { - if (ts.isIdentifier(prop.expression)) { - const identifierName = prop.expression.text; - const identifierValue = getIdentifierValue(prop, identifierName); - if (!identifierValue) { - throwError(prop); - } - return identifierValue; - } - } - - throwError(prop); + if (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) { + return prop.text; + } + + if (ts.isComputedPropertyName(prop)) { + if (ts.isIdentifier(prop.expression)) { + const identifierName = prop.expression.text; + const identifierValue = getIdentifierValue(prop, identifierName); + if (!identifierValue) { + throwError(prop); + } + return identifierValue; + } + } + + throwError(prop); } function getIdentifierValue(prop: ts.ComputedPropertyName, identifierName: string) { - const variable = getVariableAtTopLevel(prop.getSourceFile(), identifierName); - if (variable && ts.isStringLiteral(variable.initializer)) { - return variable.initializer.text; - } + const variable = getVariableAtTopLevel(prop.getSourceFile(), identifierName); + if (variable && ts.isStringLiteral(variable.initializer)) { + return variable.initializer.text; + } } function throwError(prop: ts.PropertyName) { - const error: any = new Error( - 'The ComponentEvents interface can only have properties of type ' + - 'Identifier, StringLiteral or ComputedPropertyName. ' + - 'In case of ComputedPropertyName, ' + - 'it must be a const declared within the component and initialized with a string.' - ); - error.start = toLineColumn(prop.getStart()); - error.end = toLineColumn(prop.getEnd()); - throw error; - - function toLineColumn(pos: number) { - const lineChar = prop.getSourceFile().getLineAndCharacterOfPosition(pos); - return { - line: lineChar.line + 1, - column: lineChar.character - }; - } + const error: any = new Error( + 'The ComponentEvents interface can only have properties of type ' + + 'Identifier, StringLiteral or ComputedPropertyName. ' + + 'In case of ComputedPropertyName, ' + + 'it must be a const declared within the component and initialized with a string.' + ); + error.start = toLineColumn(prop.getStart()); + error.end = toLineColumn(prop.getEnd()); + throw error; + + function toLineColumn(pos: number) { + const lineChar = prop.getSourceFile().getLineAndCharacterOfPosition(pos); + return { + line: lineChar.line + 1, + column: lineChar.character + }; + } } function getDoc(member: ts.PropertySignature) { - let doc = undefined; - const comment = getLastLeadingDoc(member); - - if (comment) { - doc = comment - .split('\n') - .map((line) => - // Remove /** */ - line - .replace(/\s*\/\*\*/, '') - .replace(/\s*\*\//, '') - .replace(/\s*\*/, '') - .trim() - ) - .join('\n'); - } - - return doc; + let doc = undefined; + const comment = getLastLeadingDoc(member); + + if (comment) { + doc = comment + .split('\n') + .map((line) => + // Remove /** */ + line + .replace(/\s*\/\*\*/, '') + .replace(/\s*\*\//, '') + .replace(/\s*\*/, '') + .trim() + ) + .join('\n'); + } + + return doc; } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts index c29778f03..a245d30f3 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts @@ -2,96 +2,96 @@ import ts from 'typescript'; import { getLastLeadingDoc } from '../utils/tsAst'; export interface IExportedNames { - has(name: string): boolean; + has(name: string): boolean; } export class ExportedNames - extends Map< - string, - { - type?: string; - identifierText?: string; - required?: boolean; - doc?: string; - } - > - implements IExportedNames { - /** - * Adds export to map - */ - addExport( - name: ts.BindingName, - target: ts.BindingName = null, - type: ts.TypeNode = null, - required = false - ): void { - if (name.kind != ts.SyntaxKind.Identifier) { - throw Error('export source kind not supported ' + name); - } - if (target && target.kind != ts.SyntaxKind.Identifier) { - throw Error('export target kind not supported ' + target); - } + extends Map< + string, + { + type?: string; + identifierText?: string; + required?: boolean; + doc?: string; + } + > + implements IExportedNames { + /** + * Adds export to map + */ + addExport( + name: ts.BindingName, + target: ts.BindingName = null, + type: ts.TypeNode = null, + required = false + ): void { + if (name.kind != ts.SyntaxKind.Identifier) { + throw Error('export source kind not supported ' + name); + } + if (target && target.kind != ts.SyntaxKind.Identifier) { + throw Error('export target kind not supported ' + target); + } - if (target) { - this.set(name.text, { - type: type?.getText(), - identifierText: (target as ts.Identifier).text, - required, - doc: this.getDoc(target) - }); - } else { - this.set(name.text, {}); - } - } + if (target) { + this.set(name.text, { + type: type?.getText(), + identifierText: (target as ts.Identifier).text, + required, + doc: this.getDoc(target) + }); + } else { + this.set(name.text, {}); + } + } - private getDoc(target: ts.BindingName) { - let doc = undefined; - // Traverse `a` up to `export let a` - const exportExpr = target?.parent?.parent?.parent; + private getDoc(target: ts.BindingName) { + let doc = undefined; + // Traverse `a` up to `export let a` + const exportExpr = target?.parent?.parent?.parent; - if (exportExpr) { - doc = getLastLeadingDoc(exportExpr); - } + if (exportExpr) { + doc = getLastLeadingDoc(exportExpr); + } - return doc; - } + return doc; + } - /** - * Creates a string from the collected props - * - * @param isTsFile Whether this is a TypeScript file or not. - */ - createPropsStr(isTsFile: boolean) { - const names = Array.from(this.entries()); - const dontAddTypeDef = - !isTsFile || - names.length === 0 || - names.every(([_, value]) => !value.type && value.required); + /** + * Creates a string from the collected props + * + * @param isTsFile Whether this is a TypeScript file or not. + */ + createPropsStr(isTsFile: boolean) { + const names = Array.from(this.entries()); + const dontAddTypeDef = + !isTsFile || + names.length === 0 || + names.every(([_, value]) => !value.type && value.required); - const returnElements = names.map(([key, value]) => { - // Important to not use shorthand props for rename functionality - return `${dontAddTypeDef && value.doc ? `\n${value.doc}` : ''}${ - value.identifierText || key - }: ${key}`; - }); + const returnElements = names.map(([key, value]) => { + // Important to not use shorthand props for rename functionality + return `${dontAddTypeDef && value.doc ? `\n${value.doc}` : ''}${ + value.identifierText || key + }: ${key}`; + }); - if (dontAddTypeDef) { - // No exports or only `typeof` exports -> omit the `as {...}` completely. - // If not TS, omit the types to not have a "cannot use types in jsx" error. - return `{${returnElements.join(' , ')}}`; - } + if (dontAddTypeDef) { + // No exports or only `typeof` exports -> omit the `as {...}` completely. + // If not TS, omit the types to not have a "cannot use types in jsx" error. + return `{${returnElements.join(' , ')}}`; + } - const returnElementsType = names.map(([key, value]) => { - const identifier = `${value.doc ? `\n${value.doc}` : ''}${value.identifierText || key}${ - value.required ? '' : '?' - }`; - if (!value.type) { - return `${identifier}: typeof ${key}`; - } + const returnElementsType = names.map(([key, value]) => { + const identifier = `${value.doc ? `\n${value.doc}` : ''}${value.identifierText || key}${ + value.required ? '' : '?' + }`; + if (!value.type) { + return `${identifier}: typeof ${key}`; + } - return `${identifier}: ${value.type}`; - }); + return `${identifier}: ${value.type}`; + }); - return `{${returnElements.join(' , ')}} as {${returnElementsType.join(', ')}}`; - } + return `{${returnElements.join(' , ')}} as {${returnElementsType.join(', ')}}`; + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ImplicitStoreValues.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ImplicitStoreValues.ts index cd6d0d36f..0493433ab 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ImplicitStoreValues.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ImplicitStoreValues.ts @@ -10,107 +10,107 @@ import { extractIdentifiers, getNamesFromLabeledStatement } from '../utils/tsAst * were used as stores are appended with `let $xx = __sveltets_store_get(xx)` to create the store variables. */ export class ImplicitStoreValues { - private accessedStores = new Set(); - private variableDeclarations: ts.VariableDeclaration[] = []; - private reactiveDeclarations: ts.LabeledStatement[] = []; - private importStatements: Array = []; - - public addStoreAcess = this.accessedStores.add.bind(this.accessedStores); - public addVariableDeclaration = this.variableDeclarations.push.bind(this.variableDeclarations); - public addReactiveDeclaration = this.reactiveDeclarations.push.bind(this.reactiveDeclarations); - public addImportStatement = this.importStatements.push.bind(this.importStatements); - - constructor(storesResolvedInTemplate: string[] = [], private renderFunctionStart: number) { - storesResolvedInTemplate.forEach(this.addStoreAcess); - } - - /** - * All variable declartaions and imports which - * were used as stores are appended with `let $xx = __sveltets_store_get(xx)` to create the store variables. - */ - public modifyCode(astOffset: number, str: MagicString) { - this.variableDeclarations.forEach((node) => - this.attachStoreValueDeclarationToDecl(node, astOffset, str) - ); - - this.reactiveDeclarations.forEach((node) => - this.attachStoreValueDeclarationToReactiveAssignment(node, astOffset, str) - ); - - this.attachStoreValueDeclarationOfImportsToRenderFn(str); - } - - public getAccessedStores(): string[] { - return [...this.accessedStores.keys()]; - } - - private attachStoreValueDeclarationToDecl( - node: ts.VariableDeclaration, - astOffset: number, - str: MagicString - ) { - const storeNames = extractIdentifiers(node.name) - .map((id) => id.text) - .filter((name) => this.accessedStores.has(name)); - if (!storeNames.length) { - return; - } - - const storeDeclarations = surroundWithIgnoreComments( - this.createStoreDeclarations(storeNames) - ); - const nodeEnd = - ts.isVariableDeclarationList(node.parent) && node.parent.declarations.length > 1 - ? node.parent.declarations[node.parent.declarations.length - 1].getEnd() - : node.getEnd(); - - str.appendRight(nodeEnd + astOffset, storeDeclarations); - } - - private attachStoreValueDeclarationToReactiveAssignment( - node: ts.LabeledStatement, - astOffset: number, - str: MagicString - ) { - const storeNames = getNamesFromLabeledStatement(node).filter((name) => - this.accessedStores.has(name) - ); - if (!storeNames.length) { - return; - } - - const storeDeclarations = surroundWithIgnoreComments( - this.createStoreDeclarations(storeNames) - ); - const endPos = node.getEnd() + astOffset; - - str.appendRight(endPos, storeDeclarations); - } - - private attachStoreValueDeclarationOfImportsToRenderFn(str: MagicString) { - const storeNames = this.importStatements - .filter(({ name }) => name && this.accessedStores.has(name.getText())) - .map(({ name }) => name.getText()); - if (!storeNames.length) { - return; - } - - const storeDeclarations = surroundWithIgnoreComments( - this.createStoreDeclarations(storeNames) - ); - - str.appendRight(this.renderFunctionStart, storeDeclarations); - } - - private createStoreDeclarations(storeNames: string[]): string { - let declarations = ''; - for (let i = 0; i < storeNames.length; i++) { - declarations += this.createStoreDeclaration(storeNames[i]); - } - return declarations; - } - - private createStoreDeclaration(storeName: string): string { - return `;let $${storeName} = __sveltets_store_get(${storeName});`; - } + private accessedStores = new Set(); + private variableDeclarations: ts.VariableDeclaration[] = []; + private reactiveDeclarations: ts.LabeledStatement[] = []; + private importStatements: Array = []; + + public addStoreAcess = this.accessedStores.add.bind(this.accessedStores); + public addVariableDeclaration = this.variableDeclarations.push.bind(this.variableDeclarations); + public addReactiveDeclaration = this.reactiveDeclarations.push.bind(this.reactiveDeclarations); + public addImportStatement = this.importStatements.push.bind(this.importStatements); + + constructor(storesResolvedInTemplate: string[] = [], private renderFunctionStart: number) { + storesResolvedInTemplate.forEach(this.addStoreAcess); + } + + /** + * All variable declartaions and imports which + * were used as stores are appended with `let $xx = __sveltets_store_get(xx)` to create the store variables. + */ + public modifyCode(astOffset: number, str: MagicString) { + this.variableDeclarations.forEach((node) => + this.attachStoreValueDeclarationToDecl(node, astOffset, str) + ); + + this.reactiveDeclarations.forEach((node) => + this.attachStoreValueDeclarationToReactiveAssignment(node, astOffset, str) + ); + + this.attachStoreValueDeclarationOfImportsToRenderFn(str); + } + + public getAccessedStores(): string[] { + return [...this.accessedStores.keys()]; + } + + private attachStoreValueDeclarationToDecl( + node: ts.VariableDeclaration, + astOffset: number, + str: MagicString + ) { + const storeNames = extractIdentifiers(node.name) + .map((id) => id.text) + .filter((name) => this.accessedStores.has(name)); + if (!storeNames.length) { + return; + } + + const storeDeclarations = surroundWithIgnoreComments( + this.createStoreDeclarations(storeNames) + ); + const nodeEnd = + ts.isVariableDeclarationList(node.parent) && node.parent.declarations.length > 1 + ? node.parent.declarations[node.parent.declarations.length - 1].getEnd() + : node.getEnd(); + + str.appendRight(nodeEnd + astOffset, storeDeclarations); + } + + private attachStoreValueDeclarationToReactiveAssignment( + node: ts.LabeledStatement, + astOffset: number, + str: MagicString + ) { + const storeNames = getNamesFromLabeledStatement(node).filter((name) => + this.accessedStores.has(name) + ); + if (!storeNames.length) { + return; + } + + const storeDeclarations = surroundWithIgnoreComments( + this.createStoreDeclarations(storeNames) + ); + const endPos = node.getEnd() + astOffset; + + str.appendRight(endPos, storeDeclarations); + } + + private attachStoreValueDeclarationOfImportsToRenderFn(str: MagicString) { + const storeNames = this.importStatements + .filter(({ name }) => name && this.accessedStores.has(name.getText())) + .map(({ name }) => name.getText()); + if (!storeNames.length) { + return; + } + + const storeDeclarations = surroundWithIgnoreComments( + this.createStoreDeclarations(storeNames) + ); + + str.appendRight(this.renderFunctionStart, storeDeclarations); + } + + private createStoreDeclarations(storeNames: string[]): string { + let declarations = ''; + for (let i = 0; i < storeNames.length; i++) { + declarations += this.createStoreDeclaration(storeNames[i]); + } + return declarations; + } + + private createStoreDeclaration(storeName: string): string { + return `;let $${storeName} = __sveltets_store_get(${storeName});`; + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ImplicitTopLevelNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ImplicitTopLevelNames.ts index 280d31ec5..2ed3fc548 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ImplicitTopLevelNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ImplicitTopLevelNames.ts @@ -1,61 +1,61 @@ import ts from 'typescript'; import MagicString from 'magic-string'; import { - isParenthesizedObjectOrArrayLiteralExpression, - getNamesFromLabeledStatement + isParenthesizedObjectOrArrayLiteralExpression, + getNamesFromLabeledStatement } from '../utils/tsAst'; export class ImplicitTopLevelNames { - private map = new Set(); - - add(node: ts.LabeledStatement) { - this.map.add(node); - } - - modifyCode(rootVariables: Set, astOffset: number, str: MagicString) { - for (const node of this.map.values()) { - const names = getNamesFromLabeledStatement(node); - if (names.length === 0) { - continue; - } - - const implicitTopLevelNames = names.filter((name) => !rootVariables.has(name)); - const pos = node.label.getStart(); - - if (this.hasOnlyImplicitTopLevelNames(names, implicitTopLevelNames)) { - // remove '$:' label - str.remove(pos + astOffset, pos + astOffset + 2); - str.prependRight(pos + astOffset, 'let '); - - this.removeBracesFromParenthizedExpression(node, astOffset, str); - } else { - implicitTopLevelNames.forEach((name) => { - str.prependRight(pos + astOffset, `let ${name};\n`); - }); - } - } - } - - private hasOnlyImplicitTopLevelNames(names: string[], implicitTopLevelNames: string[]) { - return names.length === implicitTopLevelNames.length; - } - - private removeBracesFromParenthizedExpression( - node: ts.LabeledStatement, - astOffset: number, - str: MagicString - ) { - // If expression is of type `$: ({a} = b);`, - // remove the surrounding braces so that the transformation - // to `let {a} = b;` produces valid code. - if ( - ts.isExpressionStatement(node.statement) && - isParenthesizedObjectOrArrayLiteralExpression(node.statement.expression) - ) { - const start = node.statement.expression.getStart() + astOffset; - str.overwrite(start, start + 1, '', { contentOnly: true }); - const end = node.statement.expression.getEnd() + astOffset - 1; - str.overwrite(end, end + 1, '', { contentOnly: true }); - } - } + private map = new Set(); + + add(node: ts.LabeledStatement) { + this.map.add(node); + } + + modifyCode(rootVariables: Set, astOffset: number, str: MagicString) { + for (const node of this.map.values()) { + const names = getNamesFromLabeledStatement(node); + if (names.length === 0) { + continue; + } + + const implicitTopLevelNames = names.filter((name) => !rootVariables.has(name)); + const pos = node.label.getStart(); + + if (this.hasOnlyImplicitTopLevelNames(names, implicitTopLevelNames)) { + // remove '$:' label + str.remove(pos + astOffset, pos + astOffset + 2); + str.prependRight(pos + astOffset, 'let '); + + this.removeBracesFromParenthizedExpression(node, astOffset, str); + } else { + implicitTopLevelNames.forEach((name) => { + str.prependRight(pos + astOffset, `let ${name};\n`); + }); + } + } + } + + private hasOnlyImplicitTopLevelNames(names: string[], implicitTopLevelNames: string[]) { + return names.length === implicitTopLevelNames.length; + } + + private removeBracesFromParenthizedExpression( + node: ts.LabeledStatement, + astOffset: number, + str: MagicString + ) { + // If expression is of type `$: ({a} = b);`, + // remove the surrounding braces so that the transformation + // to `let {a} = b;` produces valid code. + if ( + ts.isExpressionStatement(node.statement) && + isParenthesizedObjectOrArrayLiteralExpression(node.statement.expression) + ) { + const start = node.statement.expression.getStart() + astOffset; + str.overwrite(start, start + 1, '', { contentOnly: true }); + const end = node.statement.expression.getEnd() + astOffset - 1; + str.overwrite(end, end + 1, '', { contentOnly: true }); + } + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/Scripts.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/Scripts.ts index 2db2ce1b4..ed72c1348 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/Scripts.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/Scripts.ts @@ -2,48 +2,48 @@ import { Node } from 'estree-walker'; import MagicString from 'magic-string'; export class Scripts { - // All script tags, no matter at what level, are listed within the root children. - // To get the top level scripts, filter out all those that are part of children's children. - // Those have another type ('Element' with name 'script'). - private scriptTags = (this.htmlxAst.children as Node[]).filter( - (child) => child.type === 'Script' - ); - private topLevelScripts = this.scriptTags; + // All script tags, no matter at what level, are listed within the root children. + // To get the top level scripts, filter out all those that are part of children's children. + // Those have another type ('Element' with name 'script'). + private scriptTags = (this.htmlxAst.children as Node[]).filter( + (child) => child.type === 'Script' + ); + private topLevelScripts = this.scriptTags; - constructor(private htmlxAst: Node) {} + constructor(private htmlxAst: Node) {} - handleScriptTag = (node: Node, parent: Node) => { - if (parent !== this.htmlxAst && node.name === 'script') { - this.topLevelScripts = this.topLevelScripts.filter( - (tag) => tag.start !== node.start || tag.end !== node.end - ); - } - }; + handleScriptTag = (node: Node, parent: Node) => { + if (parent !== this.htmlxAst && node.name === 'script') { + this.topLevelScripts = this.topLevelScripts.filter( + (tag) => tag.start !== node.start || tag.end !== node.end + ); + } + }; - getTopLevelScriptTags(): { scriptTag: Node; moduleScriptTag: Node } { - let scriptTag: Node = null; - let moduleScriptTag: Node = null; - // should be 2 at most, one each, so using forEach is safe - this.topLevelScripts.forEach((tag) => { - if ( - tag.attributes && - tag.attributes.find( - (a) => a.name == 'context' && a.value.length == 1 && a.value[0].raw == 'module' - ) - ) { - moduleScriptTag = tag; - } else { - scriptTag = tag; - } - }); - return { scriptTag, moduleScriptTag }; - } + getTopLevelScriptTags(): { scriptTag: Node; moduleScriptTag: Node } { + let scriptTag: Node = null; + let moduleScriptTag: Node = null; + // should be 2 at most, one each, so using forEach is safe + this.topLevelScripts.forEach((tag) => { + if ( + tag.attributes && + tag.attributes.find( + (a) => a.name == 'context' && a.value.length == 1 && a.value[0].raw == 'module' + ) + ) { + moduleScriptTag = tag; + } else { + scriptTag = tag; + } + }); + return { scriptTag, moduleScriptTag }; + } - blankOtherScriptTags(str: MagicString): void { - this.scriptTags - .filter((tag) => !this.topLevelScripts.includes(tag)) - .forEach((tag) => { - str.remove(tag.start, tag.end); - }); - } + blankOtherScriptTags(str: MagicString): void { + this.scriptTags + .filter((tag) => !this.topLevelScripts.includes(tag)) + .forEach((tag) => { + str.remove(tag.start, tag.end); + }); + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts index a4a1a3fac..c5a7d3348 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts @@ -4,132 +4,132 @@ import { ScopeStack, Scope } from '../utils/Scope'; import { isObjectKey, isMember } from '../../utils/svelteAst'; export function handleStore(node: Node, parent: Node, str: MagicString): void { - const storename = node.name.slice(1); - //handle assign to - if (parent.type == 'AssignmentExpression' && parent.left == node && parent.operator == '=') { - const dollar = str.original.indexOf('$', node.start); - str.remove(dollar, dollar + 1); - str.overwrite(node.end, str.original.indexOf('=', node.end) + 1, '.set('); - str.appendLeft(parent.end, ')'); - return; - } - // handle Assignment operators ($store +=, -=, *=, /=, %=, **=, etc.) - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Assignment - const operators = ['+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '>>>=', '&=', '^=', '|=']; - if ( - parent.type == 'AssignmentExpression' && - parent.left == node && - operators.includes(parent.operator) - ) { - const storename = node.name.slice(1); // drop the $ - const operator = parent.operator.substring(0, parent.operator.length - 1); // drop the = sign - str.overwrite( - parent.start, - str.original.indexOf('=', node.end) + 1, - `${storename}.set( $${storename} ${operator}` - ); - str.appendLeft(parent.end, ')'); - return; - } - // handle $store++, $store--, ++$store, --$store - if (parent.type == 'UpdateExpression') { - let simpleOperator; - if (parent.operator === '++') simpleOperator = '+'; - if (parent.operator === '--') simpleOperator = '-'; - if (simpleOperator) { - const storename = node.name.slice(1); // drop the $ - str.overwrite( - parent.start, - parent.end, - `${storename}.set( $${storename} ${simpleOperator} 1)` - ); - } else { - console.warn( - `Warning - unrecognized UpdateExpression operator ${parent.operator}! + const storename = node.name.slice(1); + //handle assign to + if (parent.type == 'AssignmentExpression' && parent.left == node && parent.operator == '=') { + const dollar = str.original.indexOf('$', node.start); + str.remove(dollar, dollar + 1); + str.overwrite(node.end, str.original.indexOf('=', node.end) + 1, '.set('); + str.appendLeft(parent.end, ')'); + return; + } + // handle Assignment operators ($store +=, -=, *=, /=, %=, **=, etc.) + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Assignment + const operators = ['+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '>>>=', '&=', '^=', '|=']; + if ( + parent.type == 'AssignmentExpression' && + parent.left == node && + operators.includes(parent.operator) + ) { + const storename = node.name.slice(1); // drop the $ + const operator = parent.operator.substring(0, parent.operator.length - 1); // drop the = sign + str.overwrite( + parent.start, + str.original.indexOf('=', node.end) + 1, + `${storename}.set( $${storename} ${operator}` + ); + str.appendLeft(parent.end, ')'); + return; + } + // handle $store++, $store--, ++$store, --$store + if (parent.type == 'UpdateExpression') { + let simpleOperator; + if (parent.operator === '++') simpleOperator = '+'; + if (parent.operator === '--') simpleOperator = '-'; + if (simpleOperator) { + const storename = node.name.slice(1); // drop the $ + str.overwrite( + parent.start, + parent.end, + `${storename}.set( $${storename} ${simpleOperator} 1)` + ); + } else { + console.warn( + `Warning - unrecognized UpdateExpression operator ${parent.operator}! This is an edge case unaccounted for in svelte2tsx, please file an issue: https://github.com/sveltejs/language-tools/issues/new/choose `, - str.original.slice(parent.start, parent.end) - ); - } - return; - } + str.original.slice(parent.start, parent.end) + ); + } + return; + } - // we change "$store" references into "(__sveltets_store_get(store), $store)" - // - in order to get ts errors if store is not assignable to SvelteStore - // - use $store variable defined above to get ts flow control - const dollar = str.original.indexOf('$', node.start); - str.overwrite(dollar, dollar + 1, '(__sveltets_store_get('); - str.prependLeft(node.end, `), $${storename})`); + // we change "$store" references into "(__sveltets_store_get(store), $store)" + // - in order to get ts errors if store is not assignable to SvelteStore + // - use $store variable defined above to get ts flow control + const dollar = str.original.indexOf('$', node.start); + str.overwrite(dollar, dollar + 1, '(__sveltets_store_get('); + str.prependLeft(node.end, `), $${storename})`); } type PendingStoreResolution = { - node: T; - parent: T; - scope: Scope; + node: T; + parent: T; + scope: Scope; }; const reservedNames = new Set(['$$props', '$$restProps', '$$slots']); export class Stores { - pendingStoreResolutions: Array> = []; + pendingStoreResolutions: Array> = []; - constructor( - private scope: ScopeStack, - private str: MagicString, - private isDeclaration: { value: boolean } - ) {} + constructor( + private scope: ScopeStack, + private str: MagicString, + private isDeclaration: { value: boolean } + ) {} - handleDirective(node: Node, str: MagicString): void { - if (this.notAStore(node.name) || this.isDeclaration.value) { - return; - } + handleDirective(node: Node, str: MagicString): void { + if (this.notAStore(node.name) || this.isDeclaration.value) { + return; + } - const start = str.original.indexOf('$', node.start); - const end = start + node.name.length; - this.pendingStoreResolutions.push({ - node: { type: 'Identifier', start, end, name: node.name }, - parent: { start: 0, end: 0, type: '' }, - scope: this.scope.current - }); - } + const start = str.original.indexOf('$', node.start); + const end = start + node.name.length; + this.pendingStoreResolutions.push({ + node: { type: 'Identifier', start, end, name: node.name }, + parent: { start: 0, end: 0, type: '' }, + scope: this.scope.current + }); + } - handleIdentifier(node: Node, parent: Node, prop: string): void { - if (this.notAStore(node.name)) { - return; - } + handleIdentifier(node: Node, parent: Node, prop: string): void { + if (this.notAStore(node.name)) { + return; + } - //handle potential store - if (this.isDeclaration.value) { - if (isObjectKey(parent, prop)) { - return; - } - this.scope.current.declared.add(node.name); - } else { - if (isMember(parent, prop) && !parent.computed) { - return; - } - if (isObjectKey(parent, prop)) { - return; - } - this.pendingStoreResolutions.push({ node, parent, scope: this.scope.current }); - } - } + //handle potential store + if (this.isDeclaration.value) { + if (isObjectKey(parent, prop)) { + return; + } + this.scope.current.declared.add(node.name); + } else { + if (isMember(parent, prop) && !parent.computed) { + return; + } + if (isObjectKey(parent, prop)) { + return; + } + this.pendingStoreResolutions.push({ node, parent, scope: this.scope.current }); + } + } - resolveStores(): string[] { - const unresolvedStores = this.pendingStoreResolutions.filter(({ node, scope }) => { - const name = node.name; - // if variable starting with '$' was manually declared by the user, - // this isn't a store access. - return !scope.hasDefined(name); - }); + resolveStores(): string[] { + const unresolvedStores = this.pendingStoreResolutions.filter(({ node, scope }) => { + const name = node.name; + // if variable starting with '$' was manually declared by the user, + // this isn't a store access. + return !scope.hasDefined(name); + }); - unresolvedStores.forEach(({ node, parent }) => handleStore(node, parent, this.str)); + unresolvedStores.forEach(({ node, parent }) => handleStore(node, parent, this.str)); - return unresolvedStores.map(({ node }) => node.name.slice(1)); - } + return unresolvedStores.map(({ node }) => node.name.slice(1)); + } - private notAStore(name: string): boolean { - return name[0] !== '$' || reservedNames.has(name); - } + private notAStore(name: string): boolean { + return name[0] !== '$' || reservedNames.has(name); + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/TemplateScope.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/TemplateScope.ts index 2e926e428..cb565f630 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/TemplateScope.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/TemplateScope.ts @@ -5,44 +5,44 @@ import { WithName } from '../../interfaces'; * adopted from https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/nodes/shared/TemplateScope.ts */ export default class TemplateScope { - names: Set; - owners: Map = new Map(); - inits: Map = new Map(); - parent?: TemplateScope; - - constructor(parent?: TemplateScope) { - this.parent = parent; - this.names = new Set(parent ? parent.names : []); - } - - addMany(inits: WithName[], owner: Node) { - inits.forEach((item) => this.add(item, owner)); - return this; - } - - add(init: WithName, owner: Node) { - const { name } = init; - this.names.add(name); - this.inits.set(name, init); - this.owners.set(name, owner); - return this; - } - - child() { - const child = new TemplateScope(this); - return child; - } - - getOwner(name: string): Node { - return this.owners.get(name) || this.parent?.getOwner(name); - } - - getInit(name: string): WithName { - return this.inits.get(name) || this.parent?.getInit(name); - } - - isLet(name: string) { - const owner = this.getOwner(name); - return owner && (owner.type === 'Element' || owner.type === 'InlineComponent'); - } + names: Set; + owners: Map = new Map(); + inits: Map = new Map(); + parent?: TemplateScope; + + constructor(parent?: TemplateScope) { + this.parent = parent; + this.names = new Set(parent ? parent.names : []); + } + + addMany(inits: WithName[], owner: Node) { + inits.forEach((item) => this.add(item, owner)); + return this; + } + + add(init: WithName, owner: Node) { + const { name } = init; + this.names.add(name); + this.inits.set(name, init); + this.owners.set(name, owner); + return this; + } + + child() { + const child = new TemplateScope(this); + return child; + } + + getOwner(name: string): Node { + return this.owners.get(name) || this.parent?.getOwner(name); + } + + getInit(name: string): WithName { + return this.inits.get(name) || this.parent?.getInit(name); + } + + isLet(name: string) { + const owner = this.getOwner(name); + return owner && (owner.type === 'Element' || owner.type === 'InlineComponent'); + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts index e0e776baa..5dcb6c5c6 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts @@ -1,78 +1,78 @@ import { Node } from 'estree-walker'; export class EventHandler { - private bubbledEvents = new Map(); - private callees: Array<{ name: string; parent: Node }> = []; + private bubbledEvents = new Map(); + private callees: Array<{ name: string; parent: Node }> = []; - handleEventHandler(node: Node, parent: Node): void { - const eventName = node.name; + handleEventHandler(node: Node, parent: Node): void { + const eventName = node.name; - // pass-through/ bubble - if (!node.expression) { - if (parent.type === 'InlineComponent') { - this.handleEventHandlerBubble(parent, eventName); - } else { - this.bubbledEvents.set( - eventName, - getEventDefExpressionForNonCompoent(eventName, parent) - ); - } - } - } + // pass-through/ bubble + if (!node.expression) { + if (parent.type === 'InlineComponent') { + this.handleEventHandlerBubble(parent, eventName); + } else { + this.bubbledEvents.set( + eventName, + getEventDefExpressionForNonCompoent(eventName, parent) + ); + } + } + } - handleIdentifier(node: Node, parent: Node, prop: string): void { - if (prop === 'callee') { - this.callees.push({ name: node.name, parent }); - } - } + handleIdentifier(node: Node, parent: Node, prop: string): void { + if (prop === 'callee') { + this.callees.push({ name: node.name, parent }); + } + } - getBubbledEvents() { - return this.bubbledEvents; - } + getBubbledEvents() { + return this.bubbledEvents; + } - getDispatchedEventsForIdentifier(name: string) { - const eventNames = new Set(); + getDispatchedEventsForIdentifier(name: string) { + const eventNames = new Set(); - this.callees.forEach((callee) => { - if (callee.name === name) { - const [name] = callee.parent.arguments; + this.callees.forEach((callee) => { + if (callee.name === name) { + const [name] = callee.parent.arguments; - if (name.value !== undefined) { - eventNames.add(name.value); - } - } - }); + if (name.value !== undefined) { + eventNames.add(name.value); + } + } + }); - return eventNames; - } + return eventNames; + } - bubbledEventsAsStrings() { - return Array.from(this.bubbledEvents.entries()).map(eventMapEntryToString); - } + bubbledEventsAsStrings() { + return Array.from(this.bubbledEvents.entries()).map(eventMapEntryToString); + } - private handleEventHandlerBubble(parent: Node, eventName: string): void { - const componentEventDef = `__sveltets_instanceOf(${parent.name})`; - const exp = `__sveltets_bubbleEventDef(${componentEventDef}.$$events_def, '${eventName}')`; - const exist = this.bubbledEvents.get(eventName); - this.bubbledEvents.set(eventName, exist ? [].concat(exist, exp) : exp); - } + private handleEventHandlerBubble(parent: Node, eventName: string): void { + const componentEventDef = `__sveltets_instanceOf(${parent.name})`; + const exp = `__sveltets_bubbleEventDef(${componentEventDef}.$$events_def, '${eventName}')`; + const exist = this.bubbledEvents.get(eventName); + this.bubbledEvents.set(eventName, exist ? [].concat(exist, exp) : exp); + } } function getEventDefExpressionForNonCompoent(eventName: string, ele: Node) { - switch (ele.type) { - case 'Element': - return `__sveltets_mapElementEvent('${eventName}')`; - case 'Body': - return `__sveltets_mapBodyEvent('${eventName}')`; - case 'Window': - return `__sveltets_mapWindowEvent('${eventName}')`; - default: - break; - } + switch (ele.type) { + case 'Element': + return `__sveltets_mapElementEvent('${eventName}')`; + case 'Body': + return `__sveltets_mapBodyEvent('${eventName}')`; + case 'Window': + return `__sveltets_mapWindowEvent('${eventName}')`; + default: + break; + } } function eventMapEntryToString([eventName, expression]: [string, string | string[]]) { - return `'${eventName}':${ - Array.isArray(expression) ? `__sveltets_unionType(${expression.join(',')})` : expression - }`; + return `'${eventName}':${ + Array.isArray(expression) ? `__sveltets_unionType(${expression.join(',')})` : expression + }`; } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/exportaccessors.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/exportaccessors.ts index a827cbacd..d0ceb345a 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/exportaccessors.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/exportaccessors.ts @@ -1,18 +1,18 @@ import { ExportedNames } from './ExportedNames'; const createClassAccessor = (name: string) => - `\n get ${name}() { return render().props.${name} }` + - `\n /**accessor*/\n set ${name}(_) {}`; + `\n get ${name}() { return render().props.${name} }` + + `\n /**accessor*/\n set ${name}(_) {}`; export const createClassAccessors = (getters: Set, exportedNames: ExportedNames) => { - const accessors: string[] = []; - for (const value of exportedNames.values()) { - if (getters.has(value.identifierText)) { - continue; - } + const accessors: string[] = []; + for (const value of exportedNames.values()) { + if (getters.has(value.identifierText)) { + continue; + } - accessors.push(value.identifierText); - } + accessors.push(value.identifierText); + } - return accessors.map(createClassAccessor).join(''); + return accessors.map(createClassAccessor).join(''); }; diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/exportgetters.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/exportgetters.ts index 0d3176075..37c965511 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/exportgetters.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/exportgetters.ts @@ -1,11 +1,11 @@ const createClassGetter = (name: string) => - `\n get ${name}() { return render().getters.${name} }`; + `\n get ${name}() { return render().getters.${name} }`; export const createClassGetters = (names: Set) => { - return Array.from(names).map(createClassGetter).join(''); + return Array.from(names).map(createClassGetter).join(''); }; export function createRenderFunctionGetterStr(getters: Set) { - const properties = Array.from(getters).map((name) => `${name}: ${name}`); - return `{${properties.join(', ')}}`; + const properties = Array.from(getters).map((name) => `${name}: ${name}`); + return `{${properties.join(', ')}}`; } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts index d7a7b0a33..5642fd4b4 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts @@ -7,79 +7,79 @@ import { extract_identifiers as extractIdentifiers } from 'periscopic'; import { Directive } from 'svelte/types/compiler/interfaces'; export function handleScopeAndResolveForSlot({ - identifierDef, - initExpression, - owner, - slotHandler, - templateScope + identifierDef, + initExpression, + owner, + slotHandler, + templateScope }: { - identifierDef: Node; - initExpression: Node; - owner: Node; - slotHandler: SlotHandler; - templateScope: TemplateScope; + identifierDef: Node; + initExpression: Node; + owner: Node; + slotHandler: SlotHandler; + templateScope: TemplateScope; }) { - if (isIdentifier(identifierDef)) { - templateScope.add(identifierDef, owner); + if (isIdentifier(identifierDef)) { + templateScope.add(identifierDef, owner); - slotHandler.resolve(identifierDef, initExpression, templateScope); - } - if (isDestructuringPatterns(identifierDef)) { - // the node object is returned as-it with no mutation - const identifiers = extractIdentifiers(identifierDef) as SvelteIdentifier[]; - templateScope.addMany(identifiers, owner); + slotHandler.resolve(identifierDef, initExpression, templateScope); + } + if (isDestructuringPatterns(identifierDef)) { + // the node object is returned as-it with no mutation + const identifiers = extractIdentifiers(identifierDef) as SvelteIdentifier[]; + templateScope.addMany(identifiers, owner); - slotHandler.resolveDestructuringAssignment( - identifierDef, - identifiers, - initExpression, - templateScope - ); - } + slotHandler.resolveDestructuringAssignment( + identifierDef, + identifiers, + initExpression, + templateScope + ); + } } export function handleScopeAndResolveLetVarForSlot({ - letNode, - component, - slotName, - templateScope, - slotHandler + letNode, + component, + slotName, + templateScope, + slotHandler }: { - letNode: Directive; - slotName: string; - component: Node; - templateScope: TemplateScope; - slotHandler: SlotHandler; + letNode: Directive; + slotName: string; + component: Node; + templateScope: TemplateScope; + slotHandler: SlotHandler; }) { - const { expression } = letNode; - // - if (!expression) { - templateScope.add(letNode, component); - slotHandler.resolveLet(letNode, letNode, component, slotName); - } else { - if (isIdentifier(expression)) { - templateScope.add(expression, component); - slotHandler.resolveLet(letNode, expression, component, slotName); - } - const expForExtract = { ...expression }; + const { expression } = letNode; + // + if (!expression) { + templateScope.add(letNode, component); + slotHandler.resolveLet(letNode, letNode, component, slotName); + } else { + if (isIdentifier(expression)) { + templateScope.add(expression, component); + slotHandler.resolveLet(letNode, expression, component, slotName); + } + const expForExtract = { ...expression }; - // https://github.com/sveltejs/svelte/blob/3a37de364bfbe75202d8e9fcef9e76b9ce6faaa2/src/compiler/compile/nodes/Let.ts#L37 - if (expression.type === 'ArrayExpression') { - expForExtract.type = 'ArrayPattern'; - } else if (expression.type === 'ObjectExpression') { - expForExtract.type = 'ObjectPattern'; - } - if (isDestructuringPatterns(expForExtract)) { - const identifiers = extractIdentifiers(expForExtract) as SvelteIdentifier[]; - templateScope.addMany(identifiers, component); + // https://github.com/sveltejs/svelte/blob/3a37de364bfbe75202d8e9fcef9e76b9ce6faaa2/src/compiler/compile/nodes/Let.ts#L37 + if (expression.type === 'ArrayExpression') { + expForExtract.type = 'ArrayPattern'; + } else if (expression.type === 'ObjectExpression') { + expForExtract.type = 'ObjectPattern'; + } + if (isDestructuringPatterns(expForExtract)) { + const identifiers = extractIdentifiers(expForExtract) as SvelteIdentifier[]; + templateScope.addMany(identifiers, component); - slotHandler.resolveDestructuringAssignmentForLet( - expForExtract, - identifiers, - letNode, - component, - slotName - ); - } - } + slotHandler.resolveDestructuringAssignmentForLet( + expForExtract, + identifiers, + letNode, + component, + slotName + ); + } + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/handleTypeAssertion.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/handleTypeAssertion.ts index 3b43f9faa..b183783aa 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/handleTypeAssertion.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/handleTypeAssertion.ts @@ -5,22 +5,22 @@ import ts from 'typescript'; * Transform type assertion to as expression: a => a as Type */ export function handleTypeAssertion( - str: MagicString, - assertion: ts.TypeAssertion, - astOffset: number + str: MagicString, + assertion: ts.TypeAssertion, + astOffset: number ) { - const { expression, type } = assertion; - const assertionStart = assertion.getStart() + astOffset; - const typeStart = type.getStart() + astOffset; - const typeEnd = type.getEnd() + astOffset; - const expressionStart = expression.getStart() + astOffset; - const expressionEnd = expression.getEnd() + astOffset; + const { expression, type } = assertion; + const assertionStart = assertion.getStart() + astOffset; + const typeStart = type.getStart() + astOffset; + const typeEnd = type.getEnd() + astOffset; + const expressionStart = expression.getStart() + astOffset; + const expressionEnd = expression.getEnd() + astOffset; - str.appendLeft(expressionEnd, ' as '); - // move 'HTMLElement' to the end of expression - str.move(assertionStart, typeEnd, expressionEnd); - str.remove(assertionStart, typeStart); + str.appendLeft(expressionEnd, ' as '); + // move 'HTMLElement' to the end of expression + str.move(assertionStart, typeEnd, expressionEnd); + str.remove(assertionStart, typeStart); - // remove '>' - str.remove(typeEnd, expressionStart); + // remove '>' + str.remove(typeEnd, expressionStart); } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts index 703e8d8ce..b38d45aa2 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts @@ -1,12 +1,12 @@ import { Node, walk } from 'estree-walker'; import MagicString from 'magic-string'; import { - attributeValueIsString, - isMember, - isObjectKey, - isObjectValueShortHand, - isObjectValue, - getSlotName + attributeValueIsString, + isMember, + isObjectKey, + isObjectValueShortHand, + isObjectValue, + getSlotName } from '../../utils/svelteAst'; import TemplateScope from './TemplateScope'; import { SvelteIdentifier, WithName } from '../../interfaces'; @@ -14,249 +14,249 @@ import { getTypeForComponent } from '../../htmlxtojsx/utils/node-utils'; import { Directive } from 'svelte/types/compiler/interfaces'; function attributeStrValueAsJsExpression(attr: Node): string { - if (attr.value.length == 0) return "''"; //wut? + if (attr.value.length == 0) return "''"; //wut? - //handle single value - if (attr.value.length == 1) { - const attrVal = attr.value[0]; + //handle single value + if (attr.value.length == 1) { + const attrVal = attr.value[0]; - if (attrVal.type == 'Text') { - return '"' + attrVal.raw + '"'; - } - } + if (attrVal.type == 'Text') { + return '"' + attrVal.raw + '"'; + } + } - // we have multiple attribute values, so we know we are building a string out of them. - // so return a dummy string, it will typecheck the same :) - return '"__svelte_ts_string"'; + // we have multiple attribute values, so we know we are building a string out of them. + // so return a dummy string, it will typecheck the same :) + return '"__svelte_ts_string"'; } export class SlotHandler { - constructor(private readonly htmlx: string) {} - - slots = new Map>(); - resolved = new Map(); - resolvedExpression = new Map(); - - resolve(identifierDef: SvelteIdentifier, initExpression: Node, scope: TemplateScope) { - let resolved = this.resolved.get(identifierDef); - if (resolved) { - return resolved; - } - - resolved = this.getResolveExpressionStr(identifierDef, scope, initExpression); - if (resolved) { - this.resolved.set(identifierDef, resolved); - } - - return resolved; - } - - private getResolveExpressionStr( - identifierDef: SvelteIdentifier, - scope: TemplateScope, - initExpression: Node - ) { - const { name } = identifierDef; - - const owner = scope.getOwner(name); - - if (owner?.type === 'CatchBlock') { - return '__sveltets_any({})'; - } - - // list.map(list => list.someProperty) - // initExpression's scope should the parent scope of identifier scope - else if (owner?.type === 'ThenBlock') { - const resolvedExpression = this.resolveExpression(initExpression, scope.parent); - - return `__sveltets_unwrapPromiseLike(${resolvedExpression})`; - } else if (owner?.type === 'EachBlock') { - const resolvedExpression = this.resolveExpression(initExpression, scope.parent); - - return `__sveltets_unwrapArr(${resolvedExpression})`; - } - return null; - } - - resolveDestructuringAssignment( - destructuringNode: Node, - identifiers: SvelteIdentifier[], - initExpression: Node, - scope: TemplateScope - ) { - const destructuring = this.htmlx.slice(destructuringNode.start, destructuringNode.end); - identifiers.forEach((identifier) => { - const resolved = this.getResolveExpressionStr(identifier, scope, initExpression); - if (resolved) { - this.resolved.set( - identifier, - `((${destructuring}) => ${identifier.name})(${resolved})` - ); - } - }); - } - - resolveDestructuringAssignmentForLet( - destructuringNode: Node, - identifiers: SvelteIdentifier[], - letNode: Directive, - component: Node, - slotName: string - ) { - const destructuring = this.htmlx.slice(destructuringNode.start, destructuringNode.end); - identifiers.forEach((identifier) => { - const resolved = this.getResolveExpressionStrForLet(letNode, component, slotName); - this.resolved.set( - identifier, - `((${destructuring}) => ${identifier.name})(${resolved})` - ); - }); - } - - private getResolveExpressionStrForLet(letNode: Directive, component: Node, slotName: string) { - return `${getSingleSlotDef(component, slotName)}.${letNode.name}`; - } - - resolveLet(letNode: Directive, identifierDef: WithName, component: Node, slotName: string) { - let resolved = this.resolved.get(identifierDef); - if (resolved) { - return resolved; - } - - resolved = this.getResolveExpressionStrForLet(letNode, component, slotName); - - this.resolved.set(identifierDef, resolved); - - return resolved; - } - - getSlotConsumerOfComponent(component: Node) { - let result = this.getLetNodes(component, 'default') ?? []; - for (const child of component.children) { - const slotName = getSlotName(child); - - if (slotName) { - const letNodes = this.getLetNodes(child, slotName); - - if (letNodes?.length) { - result = result.concat(letNodes); - } - } - } - - return result; - } - - private getLetNodes(child: Node, slotName: string) { - const letNodes = ((child?.attributes as Node[]) ?? []).filter( - (attr) => attr.type === 'Let' - ) as Directive[]; - - return letNodes?.map((letNode) => ({ - letNode, - slotName - })); - } - - private resolveExpression(expression: Node, scope: TemplateScope) { - let resolved = this.resolvedExpression.get(expression); - if (resolved) { - return resolved; - } - - const strForExpression = new MagicString(this.htmlx); - - const identifiers: Node[] = []; - const objectShortHands: Node[] = []; - walk(expression, { - enter(node, parent, prop) { - if (node.type === 'Identifier') { - if (parent) { - if (isMember(parent, prop)) return; - if (isObjectKey(parent, prop)) { - return; - } - if (isObjectValue(parent, prop)) { - // { value } - if (isObjectValueShortHand(parent)) { - this.skip(); - objectShortHands.push(node); - return; - } - } - } - - this.skip(); - identifiers.push(node); - } - } - }); - - const getOverwrite = (name: string) => { - const init = scope.getInit(name); - return init ? this.resolved.get(init) : name; - }; - for (const identifier of objectShortHands) { - const { end, name } = identifier; - const value = getOverwrite(name); - strForExpression.appendLeft(end, `:${value}`); - } - for (const identifier of identifiers) { - const { start, end, name } = identifier; - const value = getOverwrite(name); - strForExpression.overwrite(start, end, value); - } - - resolved = strForExpression.slice(expression.start, expression.end); - this.resolvedExpression.set(expression, resolved); - - return resolved; - } - - handleSlot(node: Node, scope: TemplateScope) { - const nameAttr = node.attributes.find((a: Node) => a.name == 'name'); - const slotName = nameAttr ? nameAttr.value[0].raw : 'default'; - //collect attributes - const attributes = new Map(); - for (const attr of node.attributes) { - if (attr.name == 'name') continue; - if (!attr.value?.length) continue; - - if (attributeValueIsString(attr)) { - attributes.set(attr.name, attributeStrValueAsJsExpression(attr)); - continue; - } - attributes.set(attr.name, this.resolveAttr(attr, scope)); - } - this.slots.set(slotName, attributes); - } - - getSlotDef() { - return this.slots; - } - - resolveAttr(attr: Node, scope: TemplateScope): string { - const attrVal = attr.value[0]; - if (!attrVal) { - return null; - } - - if (attrVal.type == 'AttributeShorthand') { - const { name } = attrVal.expression; - const init = scope.getInit(name); - const resolved = this.resolved.get(init); - - return resolved ?? name; - } - - if (attrVal.type == 'MustacheTag') { - return this.resolveExpression(attrVal.expression, scope); - } - throw Error('Unknown attribute value type:' + attrVal.type); - } + constructor(private readonly htmlx: string) {} + + slots = new Map>(); + resolved = new Map(); + resolvedExpression = new Map(); + + resolve(identifierDef: SvelteIdentifier, initExpression: Node, scope: TemplateScope) { + let resolved = this.resolved.get(identifierDef); + if (resolved) { + return resolved; + } + + resolved = this.getResolveExpressionStr(identifierDef, scope, initExpression); + if (resolved) { + this.resolved.set(identifierDef, resolved); + } + + return resolved; + } + + private getResolveExpressionStr( + identifierDef: SvelteIdentifier, + scope: TemplateScope, + initExpression: Node + ) { + const { name } = identifierDef; + + const owner = scope.getOwner(name); + + if (owner?.type === 'CatchBlock') { + return '__sveltets_any({})'; + } + + // list.map(list => list.someProperty) + // initExpression's scope should the parent scope of identifier scope + else if (owner?.type === 'ThenBlock') { + const resolvedExpression = this.resolveExpression(initExpression, scope.parent); + + return `__sveltets_unwrapPromiseLike(${resolvedExpression})`; + } else if (owner?.type === 'EachBlock') { + const resolvedExpression = this.resolveExpression(initExpression, scope.parent); + + return `__sveltets_unwrapArr(${resolvedExpression})`; + } + return null; + } + + resolveDestructuringAssignment( + destructuringNode: Node, + identifiers: SvelteIdentifier[], + initExpression: Node, + scope: TemplateScope + ) { + const destructuring = this.htmlx.slice(destructuringNode.start, destructuringNode.end); + identifiers.forEach((identifier) => { + const resolved = this.getResolveExpressionStr(identifier, scope, initExpression); + if (resolved) { + this.resolved.set( + identifier, + `((${destructuring}) => ${identifier.name})(${resolved})` + ); + } + }); + } + + resolveDestructuringAssignmentForLet( + destructuringNode: Node, + identifiers: SvelteIdentifier[], + letNode: Directive, + component: Node, + slotName: string + ) { + const destructuring = this.htmlx.slice(destructuringNode.start, destructuringNode.end); + identifiers.forEach((identifier) => { + const resolved = this.getResolveExpressionStrForLet(letNode, component, slotName); + this.resolved.set( + identifier, + `((${destructuring}) => ${identifier.name})(${resolved})` + ); + }); + } + + private getResolveExpressionStrForLet(letNode: Directive, component: Node, slotName: string) { + return `${getSingleSlotDef(component, slotName)}.${letNode.name}`; + } + + resolveLet(letNode: Directive, identifierDef: WithName, component: Node, slotName: string) { + let resolved = this.resolved.get(identifierDef); + if (resolved) { + return resolved; + } + + resolved = this.getResolveExpressionStrForLet(letNode, component, slotName); + + this.resolved.set(identifierDef, resolved); + + return resolved; + } + + getSlotConsumerOfComponent(component: Node) { + let result = this.getLetNodes(component, 'default') ?? []; + for (const child of component.children) { + const slotName = getSlotName(child); + + if (slotName) { + const letNodes = this.getLetNodes(child, slotName); + + if (letNodes?.length) { + result = result.concat(letNodes); + } + } + } + + return result; + } + + private getLetNodes(child: Node, slotName: string) { + const letNodes = ((child?.attributes as Node[]) ?? []).filter( + (attr) => attr.type === 'Let' + ) as Directive[]; + + return letNodes?.map((letNode) => ({ + letNode, + slotName + })); + } + + private resolveExpression(expression: Node, scope: TemplateScope) { + let resolved = this.resolvedExpression.get(expression); + if (resolved) { + return resolved; + } + + const strForExpression = new MagicString(this.htmlx); + + const identifiers: Node[] = []; + const objectShortHands: Node[] = []; + walk(expression, { + enter(node, parent, prop) { + if (node.type === 'Identifier') { + if (parent) { + if (isMember(parent, prop)) return; + if (isObjectKey(parent, prop)) { + return; + } + if (isObjectValue(parent, prop)) { + // { value } + if (isObjectValueShortHand(parent)) { + this.skip(); + objectShortHands.push(node); + return; + } + } + } + + this.skip(); + identifiers.push(node); + } + } + }); + + const getOverwrite = (name: string) => { + const init = scope.getInit(name); + return init ? this.resolved.get(init) : name; + }; + for (const identifier of objectShortHands) { + const { end, name } = identifier; + const value = getOverwrite(name); + strForExpression.appendLeft(end, `:${value}`); + } + for (const identifier of identifiers) { + const { start, end, name } = identifier; + const value = getOverwrite(name); + strForExpression.overwrite(start, end, value); + } + + resolved = strForExpression.slice(expression.start, expression.end); + this.resolvedExpression.set(expression, resolved); + + return resolved; + } + + handleSlot(node: Node, scope: TemplateScope) { + const nameAttr = node.attributes.find((a: Node) => a.name == 'name'); + const slotName = nameAttr ? nameAttr.value[0].raw : 'default'; + //collect attributes + const attributes = new Map(); + for (const attr of node.attributes) { + if (attr.name == 'name') continue; + if (!attr.value?.length) continue; + + if (attributeValueIsString(attr)) { + attributes.set(attr.name, attributeStrValueAsJsExpression(attr)); + continue; + } + attributes.set(attr.name, this.resolveAttr(attr, scope)); + } + this.slots.set(slotName, attributes); + } + + getSlotDef() { + return this.slots; + } + + resolveAttr(attr: Node, scope: TemplateScope): string { + const attrVal = attr.value[0]; + if (!attrVal) { + return null; + } + + if (attrVal.type == 'AttributeShorthand') { + const { name } = attrVal.expression; + const init = scope.getInit(name); + const resolved = this.resolved.get(init); + + return resolved ?? name; + } + + if (attrVal.type == 'MustacheTag') { + return this.resolveExpression(attrVal.expression, scope); + } + throw Error('Unknown attribute value type:' + attrVal.type); + } } export function getSingleSlotDef(componentNode: Node, slotName: string) { - const componentType = getTypeForComponent(componentNode); - return `__sveltets_instanceOf(${componentType}).$$slot_def['${slotName}']`; + const componentType = getTypeForComponent(componentNode); + return `__sveltets_instanceOf(${componentType}).$$slot_def['${slotName}']`; } diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts index eb50c0872..f694ed6e0 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts @@ -2,10 +2,10 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; import * as ts from 'typescript'; import { - findExportKeyword, - getBinaryAssignmentExpr, - isFirstInAnExpressionStatement, - isNotPropertyNameOfImport + findExportKeyword, + getBinaryAssignmentExpr, + isFirstInAnExpressionStatement, + isNotPropertyNameOfImport } from './utils/tsAst'; import { ExportedNames } from './nodes/ExportedNames'; import { ImplicitTopLevelNames } from './nodes/ImplicitTopLevelNames'; @@ -15,495 +15,495 @@ import { handleTypeAssertion } from './nodes/handleTypeAssertion'; import { ImplicitStoreValues } from './nodes/ImplicitStoreValues'; export interface InstanceScriptProcessResult { - exportedNames: ExportedNames; - events: ComponentEvents; - uses$$props: boolean; - uses$$restProps: boolean; - uses$$slots: boolean; - getters: Set; + exportedNames: ExportedNames; + events: ComponentEvents; + uses$$props: boolean; + uses$$restProps: boolean; + uses$$slots: boolean; + getters: Set; } interface PendingStoreResolution { - node: ts.Identifier; - parent: ts.Node; - scope: Scope; + node: ts.Identifier; + parent: ts.Node; + scope: Scope; } export function processInstanceScriptContent( - str: MagicString, - script: Node, - events: ComponentEvents, - implicitStoreValues: ImplicitStoreValues + str: MagicString, + script: Node, + events: ComponentEvents, + implicitStoreValues: ImplicitStoreValues ): InstanceScriptProcessResult { - const htmlx = str.original; - const scriptContent = htmlx.substring(script.content.start, script.content.end); - const tsAst = ts.createSourceFile( - 'component.ts.svelte', - scriptContent, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TS - ); - const astOffset = script.content.start; - const exportedNames = new ExportedNames(); - const getters = new Set(); - - const implicitTopLevelNames = new ImplicitTopLevelNames(); - let uses$$props = false; - let uses$$restProps = false; - let uses$$slots = false; - - //track if we are in a declaration scope - let isDeclaration = false; - - //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes - //which prevents us just changing all instances of Identity that start with $ - const pendingStoreResolutions: PendingStoreResolution[] = []; - - let scope = new Scope(); - const rootScope = scope; - - const pushScope = () => (scope = new Scope(scope)); - const popScope = () => (scope = scope.parent); - - const addGetter = (node: ts.Identifier) => { - if (!node) { - return; - } - getters.add(node.text); - }; - - const removeExport = (start: number, end: number) => { - const exportStart = str.original.indexOf('export', start + astOffset); - const exportEnd = exportStart + (end - start); - str.remove(exportStart, exportEnd); - }; - - const propTypeAssertToUserDefined = (node: ts.VariableDeclarationList) => { - const hasInitializers = node.declarations.filter((declaration) => declaration.initializer); - const handleTypeAssertion = (declaration: ts.VariableDeclaration) => { - const identifier = declaration.name; - const tsType = declaration.type; - const jsDocType = ts.getJSDocType(declaration); - const type = tsType || jsDocType; - - if ( - !ts.isIdentifier(identifier) || - (!type && - // Edge case: TS infers `export let bla = false` to type `false`. - // prevent that by adding the any-wrap in this case, too. - ![ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.TrueKeyword].includes( - declaration.initializer?.kind - )) - ) { - return; - } - const name = identifier.getText(); - const end = declaration.end + astOffset; - - str.appendLeft(end, `;${name} = __sveltets_any(${name});`); - }; - - const findComma = (target: ts.Node) => - target.getChildren().filter((child) => child.kind === ts.SyntaxKind.CommaToken); - const splitDeclaration = () => { - const commas = node - .getChildren() - .filter((child) => child.kind === ts.SyntaxKind.SyntaxList) - .map(findComma) - .reduce((current, previous) => [...current, ...previous], []); - - commas.forEach((comma) => { - const start = comma.getStart() + astOffset; - const end = comma.getEnd() + astOffset; - str.overwrite(start, end, ';let ', { contentOnly: true }); - }); - }; - splitDeclaration(); - - for (const declaration of hasInitializers) { - handleTypeAssertion(declaration); - } - }; - - const handleExportFunctionOrClass = (node: ts.ClassDeclaration | ts.FunctionDeclaration) => { - const exportModifier = findExportKeyword(node); - if (!exportModifier) { - return; - } - - removeExport(exportModifier.getStart(), exportModifier.end); - addGetter(node.name); - - // Can't export default here - if (node.name) { - exportedNames.addExport(node.name); - } - }; - - const handleStore = (ident: ts.Identifier, parent: ts.Node) => { - // ignore "typeof $store" - if (parent && parent.kind === ts.SyntaxKind.TypeQuery) { - return; - } - // ignore break - if (parent && parent.kind === ts.SyntaxKind.BreakStatement) { - return; - } - - const storename = ident.getText().slice(1); // drop the $ - // handle assign to - if ( - parent && - ts.isBinaryExpression(parent) && - parent.operatorToken.kind == ts.SyntaxKind.EqualsToken && - parent.left == ident - ) { - //remove $ - const dollar = str.original.indexOf('$', ident.getStart() + astOffset); - str.remove(dollar, dollar + 1); - // replace = with .set( - str.overwrite(ident.end + astOffset, parent.operatorToken.end + astOffset, '.set('); - // append ) - str.appendLeft(parent.end + astOffset, ')'); - return; - } - - // handle Assignment operators ($store +=, -=, *=, /=, %=, **=, etc.) - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Assignment - const operators = { - [ts.SyntaxKind.PlusEqualsToken]: '+', - [ts.SyntaxKind.MinusEqualsToken]: '-', - [ts.SyntaxKind.AsteriskEqualsToken]: '*', - [ts.SyntaxKind.SlashEqualsToken]: '/', - [ts.SyntaxKind.PercentEqualsToken]: '%', - [ts.SyntaxKind.AsteriskAsteriskEqualsToken]: '**', - [ts.SyntaxKind.LessThanLessThanEqualsToken]: '<<', - [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken]: '>>', - [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>', - [ts.SyntaxKind.AmpersandEqualsToken]: '&', - [ts.SyntaxKind.CaretEqualsToken]: '^', - [ts.SyntaxKind.BarEqualsToken]: '|' - }; - if ( - ts.isBinaryExpression(parent) && - parent.left == ident && - Object.keys(operators).find((x) => x === String(parent.operatorToken.kind)) - ) { - const operator = operators[parent.operatorToken.kind]; - str.overwrite( - parent.getStart() + astOffset, - str.original.indexOf('=', ident.end + astOffset) + 1, - `${storename}.set( $${storename} ${operator}` - ); - str.appendLeft(parent.end + astOffset, ')'); - return; - } - // handle $store++, $store--, ++$store, --$store - if ( - (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) && - parent.operator !== - ts.SyntaxKind.ExclamationToken /* `!$store` does not need processing */ - ) { - let simpleOperator: string; - if (parent.operator === ts.SyntaxKind.PlusPlusToken) { - simpleOperator = '+'; - } - if (parent.operator === ts.SyntaxKind.MinusMinusToken) { - simpleOperator = '-'; - } - - if (simpleOperator) { - str.overwrite( - parent.getStart() + astOffset, - parent.end + astOffset, - `${storename}.set( $${storename} ${simpleOperator} 1)` - ); - return; - } else { - console.warn( - `Warning - unrecognized UnaryExpression operator ${parent.operator}! + const htmlx = str.original; + const scriptContent = htmlx.substring(script.content.start, script.content.end); + const tsAst = ts.createSourceFile( + 'component.ts.svelte', + scriptContent, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS + ); + const astOffset = script.content.start; + const exportedNames = new ExportedNames(); + const getters = new Set(); + + const implicitTopLevelNames = new ImplicitTopLevelNames(); + let uses$$props = false; + let uses$$restProps = false; + let uses$$slots = false; + + //track if we are in a declaration scope + let isDeclaration = false; + + //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes + //which prevents us just changing all instances of Identity that start with $ + const pendingStoreResolutions: PendingStoreResolution[] = []; + + let scope = new Scope(); + const rootScope = scope; + + const pushScope = () => (scope = new Scope(scope)); + const popScope = () => (scope = scope.parent); + + const addGetter = (node: ts.Identifier) => { + if (!node) { + return; + } + getters.add(node.text); + }; + + const removeExport = (start: number, end: number) => { + const exportStart = str.original.indexOf('export', start + astOffset); + const exportEnd = exportStart + (end - start); + str.remove(exportStart, exportEnd); + }; + + const propTypeAssertToUserDefined = (node: ts.VariableDeclarationList) => { + const hasInitializers = node.declarations.filter((declaration) => declaration.initializer); + const handleTypeAssertion = (declaration: ts.VariableDeclaration) => { + const identifier = declaration.name; + const tsType = declaration.type; + const jsDocType = ts.getJSDocType(declaration); + const type = tsType || jsDocType; + + if ( + !ts.isIdentifier(identifier) || + (!type && + // Edge case: TS infers `export let bla = false` to type `false`. + // prevent that by adding the any-wrap in this case, too. + ![ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.TrueKeyword].includes( + declaration.initializer?.kind + )) + ) { + return; + } + const name = identifier.getText(); + const end = declaration.end + astOffset; + + str.appendLeft(end, `;${name} = __sveltets_any(${name});`); + }; + + const findComma = (target: ts.Node) => + target.getChildren().filter((child) => child.kind === ts.SyntaxKind.CommaToken); + const splitDeclaration = () => { + const commas = node + .getChildren() + .filter((child) => child.kind === ts.SyntaxKind.SyntaxList) + .map(findComma) + .reduce((current, previous) => [...current, ...previous], []); + + commas.forEach((comma) => { + const start = comma.getStart() + astOffset; + const end = comma.getEnd() + astOffset; + str.overwrite(start, end, ';let ', { contentOnly: true }); + }); + }; + splitDeclaration(); + + for (const declaration of hasInitializers) { + handleTypeAssertion(declaration); + } + }; + + const handleExportFunctionOrClass = (node: ts.ClassDeclaration | ts.FunctionDeclaration) => { + const exportModifier = findExportKeyword(node); + if (!exportModifier) { + return; + } + + removeExport(exportModifier.getStart(), exportModifier.end); + addGetter(node.name); + + // Can't export default here + if (node.name) { + exportedNames.addExport(node.name); + } + }; + + const handleStore = (ident: ts.Identifier, parent: ts.Node) => { + // ignore "typeof $store" + if (parent && parent.kind === ts.SyntaxKind.TypeQuery) { + return; + } + // ignore break + if (parent && parent.kind === ts.SyntaxKind.BreakStatement) { + return; + } + + const storename = ident.getText().slice(1); // drop the $ + // handle assign to + if ( + parent && + ts.isBinaryExpression(parent) && + parent.operatorToken.kind == ts.SyntaxKind.EqualsToken && + parent.left == ident + ) { + //remove $ + const dollar = str.original.indexOf('$', ident.getStart() + astOffset); + str.remove(dollar, dollar + 1); + // replace = with .set( + str.overwrite(ident.end + astOffset, parent.operatorToken.end + astOffset, '.set('); + // append ) + str.appendLeft(parent.end + astOffset, ')'); + return; + } + + // handle Assignment operators ($store +=, -=, *=, /=, %=, **=, etc.) + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Assignment + const operators = { + [ts.SyntaxKind.PlusEqualsToken]: '+', + [ts.SyntaxKind.MinusEqualsToken]: '-', + [ts.SyntaxKind.AsteriskEqualsToken]: '*', + [ts.SyntaxKind.SlashEqualsToken]: '/', + [ts.SyntaxKind.PercentEqualsToken]: '%', + [ts.SyntaxKind.AsteriskAsteriskEqualsToken]: '**', + [ts.SyntaxKind.LessThanLessThanEqualsToken]: '<<', + [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken]: '>>', + [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>', + [ts.SyntaxKind.AmpersandEqualsToken]: '&', + [ts.SyntaxKind.CaretEqualsToken]: '^', + [ts.SyntaxKind.BarEqualsToken]: '|' + }; + if ( + ts.isBinaryExpression(parent) && + parent.left == ident && + Object.keys(operators).find((x) => x === String(parent.operatorToken.kind)) + ) { + const operator = operators[parent.operatorToken.kind]; + str.overwrite( + parent.getStart() + astOffset, + str.original.indexOf('=', ident.end + astOffset) + 1, + `${storename}.set( $${storename} ${operator}` + ); + str.appendLeft(parent.end + astOffset, ')'); + return; + } + // handle $store++, $store--, ++$store, --$store + if ( + (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) && + parent.operator !== + ts.SyntaxKind.ExclamationToken /* `!$store` does not need processing */ + ) { + let simpleOperator: string; + if (parent.operator === ts.SyntaxKind.PlusPlusToken) { + simpleOperator = '+'; + } + if (parent.operator === ts.SyntaxKind.MinusMinusToken) { + simpleOperator = '-'; + } + + if (simpleOperator) { + str.overwrite( + parent.getStart() + astOffset, + parent.end + astOffset, + `${storename}.set( $${storename} ${simpleOperator} 1)` + ); + return; + } else { + console.warn( + `Warning - unrecognized UnaryExpression operator ${parent.operator}! This is an edge case unaccounted for in svelte2tsx, please file an issue: https://github.com/sveltejs/language-tools/issues/new/choose `, - parent.getText() - ); - } - } - - // we change "$store" references into "(__sveltets_store_get(store), $store)" - // - in order to get ts errors if store is not assignable to SvelteStore - // - use $store variable defined above to get ts flow control - const dollar = str.original.indexOf('$', ident.getStart() + astOffset); - const getPrefix = isFirstInAnExpressionStatement(ident) ? ';' : ''; - str.overwrite(dollar, dollar + 1, getPrefix + '(__sveltets_store_get('); - str.prependLeft(ident.end + astOffset, `), $${storename})`); - }; - - const resolveStore = (pending: PendingStoreResolution) => { - let { node, parent, scope } = pending; - const name = (node as ts.Identifier).text; - while (scope) { - if (scope.declared.has(name)) { - //we were manually declared, this isn't a store access. - return; - } - scope = scope.parent; - } - //We haven't been resolved, we must be a store read/write, handle it. - handleStore(node, parent); - const storename = node.getText().slice(1); - implicitStoreValues.addStoreAcess(storename); - }; - - const handleIdentifier = (ident: ts.Identifier, parent: ts.Node) => { - if (ident.text === '$$props') { - uses$$props = true; - return; - } - if (ident.text === '$$restProps') { - uses$$restProps = true; - return; - } - if (ident.text === '$$slots') { - uses$$slots = true; - return; - } - - if (ts.isLabeledStatement(parent) && parent.label == ident) { - return; - } - - if (isDeclaration || ts.isParameter(parent)) { - if ( - isNotPropertyNameOfImport(ident) && - (!ts.isBindingElement(ident.parent) || ident.parent.name == ident) - ) { - // we are a key, not a name, so don't care - if (ident.text.startsWith('$') || scope == rootScope) { - // track all top level declared identifiers and all $ prefixed identifiers - scope.declared.add(ident.text); - } - } - } else { - //track potential store usage to be resolved - if (ident.text.startsWith('$')) { - if ( - (!ts.isPropertyAccessExpression(parent) || parent.expression == ident) && - (!ts.isPropertyAssignment(parent) || parent.initializer == ident) && - !ts.isPropertySignature(parent) && - !ts.isPropertyDeclaration(parent) - ) { - pendingStoreResolutions.push({ node: ident, parent, scope }); - } - } - } - }; - - const handleExportedVariableDeclarationList = (list: ts.VariableDeclarationList) => { - ts.forEachChild(list, (node) => { - if (ts.isVariableDeclaration(node)) { - if (ts.isIdentifier(node.name)) { - exportedNames.addExport(node.name, node.name, node.type, !node.initializer); - } else if ( - ts.isObjectBindingPattern(node.name) || - ts.isArrayBindingPattern(node.name) - ) { - ts.forEachChild(node.name, (element) => { - if (ts.isBindingElement(element)) { - exportedNames.addExport(element.name); - } - }); - } - } - }); - }; - - const wrapExpressionWithInvalidate = (expression: ts.Expression | undefined) => { - if (!expression) { - return; - } - - const start = expression.getStart() + astOffset; - const end = expression.getEnd() + astOffset; - - // () => ({}) - if (ts.isObjectLiteralExpression(expression)) { - str.appendLeft(start, '('); - str.appendRight(end, ')'); - } - - str.prependLeft(start, '__sveltets_invalidate(() => '); - str.appendRight(end, ')'); - // Not adding ';' at the end because right now this function is only invoked - // in situations where there is a line break of ; guaranteed to be present (else the code is invalid) - }; - - const walk = (node: ts.Node, parent: ts.Node) => { - type onLeaveCallback = () => void; - const onLeaveCallbacks: onLeaveCallback[] = []; - - if (ts.isInterfaceDeclaration(node) && node.name.text === 'ComponentEvents') { - events.setComponentEventsInterface(node); - } - - if (ts.isVariableStatement(node)) { - const exportModifier = findExportKeyword(node); - if (exportModifier) { - const isLet = node.declarationList.flags === ts.NodeFlags.Let; - const isConst = node.declarationList.flags === ts.NodeFlags.Const; - - handleExportedVariableDeclarationList(node.declarationList); - if (isLet) { - propTypeAssertToUserDefined(node.declarationList); - } else if (isConst) { - node.declarationList.forEachChild((n) => { - if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) { - addGetter(n.name); - } - }); - } - removeExport(exportModifier.getStart(), exportModifier.end); - } - } - - if (ts.isFunctionDeclaration(node)) { - handleExportFunctionOrClass(node); - - pushScope(); - onLeaveCallbacks.push(() => popScope()); - } - - if (ts.isClassDeclaration(node)) { - handleExportFunctionOrClass(node); - } - - if (ts.isBlock(node)) { - pushScope(); - onLeaveCallbacks.push(() => popScope()); - } - - if (ts.isArrowFunction(node)) { - pushScope(); - onLeaveCallbacks.push(() => popScope()); - } - - if (ts.isExportDeclaration(node)) { - const { exportClause } = node; - if (ts.isNamedExports(exportClause)) { - for (const ne of exportClause.elements) { - if (ne.propertyName) { - exportedNames.addExport(ne.propertyName, ne.name); - } else { - exportedNames.addExport(ne.name); - } - } - //we can remove entire statement - removeExport(node.getStart(), node.end); - } - } - - if (ts.isImportDeclaration(node)) { - //move imports to top of script so they appear outside our render function - str.move(node.getStart() + astOffset, node.end + astOffset, script.start + 1); - //add in a \n - const originalEndChar = str.original[node.end + astOffset - 1]; - str.overwrite(node.end + astOffset - 1, node.end + astOffset, originalEndChar + '\n'); - // Check if import is the event dispatcher - events.checkIfImportIsEventDispatcher(node); - } - - if (ts.isVariableDeclaration(node)) { - events.checkIfIsStringLiteralDeclaration(node); - events.checkIfDeclarationInstantiatedEventDispatcher(node); - implicitStoreValues.addVariableDeclaration(node); - } - - if (ts.isCallExpression(node)) { - events.checkIfCallExpressionIsDispatch(node); - } - - if (ts.isVariableDeclaration(parent) && parent.name == node) { - isDeclaration = true; - onLeaveCallbacks.push(() => (isDeclaration = false)); - } - - if (ts.isBindingElement(parent) && parent.name == node) { - isDeclaration = true; - onLeaveCallbacks.push(() => (isDeclaration = false)); - } - - if (ts.isImportClause(node)) { - isDeclaration = true; - onLeaveCallbacks.push(() => (isDeclaration = false)); - implicitStoreValues.addImportStatement(node); - } - - if (ts.isImportSpecifier(node)) { - implicitStoreValues.addImportStatement(node); - } - - //handle stores etc - if (ts.isIdentifier(node)) { - handleIdentifier(node, parent); - } - - //track implicit declarations in reactive blocks at the top level - if ( - ts.isLabeledStatement(node) && - parent == tsAst && //top level - node.label.text == '$' && - node.statement - ) { - const binaryExpression = getBinaryAssignmentExpr(node); - if (binaryExpression) { - implicitTopLevelNames.add(node); - implicitStoreValues.addReactiveDeclaration(node); - wrapExpressionWithInvalidate(binaryExpression.right); - } else { - const start = node.getStart() + astOffset; - const end = node.getEnd() + astOffset; - - str.prependLeft(start, ';() => {'); - str.appendRight(end, '}'); - } - } - - // Defensively call function (checking for undefined) because it got added only recently (TS 4.0) - // and therefore might break people using older TS versions - if (ts.isTypeAssertionExpression?.(node)) { - handleTypeAssertion(str, node, astOffset); - } - - //to save a bunch of condition checks on each node, we recurse into processChild which skips all the checks for top level items - ts.forEachChild(node, (n) => walk(n, node)); - //fire off the on leave callbacks - onLeaveCallbacks.map((c) => c()); - }; - - //walk the ast and convert to tsx as we go - tsAst.forEachChild((n) => walk(n, tsAst)); - - //resolve stores - pendingStoreResolutions.map(resolveStore); - - // declare implicit reactive variables we found in the script - implicitTopLevelNames.modifyCode(rootScope.declared, astOffset, str); - implicitStoreValues.modifyCode(astOffset, str); - - const firstImport = tsAst.statements - .filter(ts.isImportDeclaration) - .sort((a, b) => a.end - b.end)[0]; - if (firstImport) { - str.appendRight(firstImport.getStart() + astOffset, '\n'); - } - - return { - exportedNames, - events, - uses$$props, - uses$$restProps, - uses$$slots, - getters - }; + parent.getText() + ); + } + } + + // we change "$store" references into "(__sveltets_store_get(store), $store)" + // - in order to get ts errors if store is not assignable to SvelteStore + // - use $store variable defined above to get ts flow control + const dollar = str.original.indexOf('$', ident.getStart() + astOffset); + const getPrefix = isFirstInAnExpressionStatement(ident) ? ';' : ''; + str.overwrite(dollar, dollar + 1, getPrefix + '(__sveltets_store_get('); + str.prependLeft(ident.end + astOffset, `), $${storename})`); + }; + + const resolveStore = (pending: PendingStoreResolution) => { + let { node, parent, scope } = pending; + const name = (node as ts.Identifier).text; + while (scope) { + if (scope.declared.has(name)) { + //we were manually declared, this isn't a store access. + return; + } + scope = scope.parent; + } + //We haven't been resolved, we must be a store read/write, handle it. + handleStore(node, parent); + const storename = node.getText().slice(1); + implicitStoreValues.addStoreAcess(storename); + }; + + const handleIdentifier = (ident: ts.Identifier, parent: ts.Node) => { + if (ident.text === '$$props') { + uses$$props = true; + return; + } + if (ident.text === '$$restProps') { + uses$$restProps = true; + return; + } + if (ident.text === '$$slots') { + uses$$slots = true; + return; + } + + if (ts.isLabeledStatement(parent) && parent.label == ident) { + return; + } + + if (isDeclaration || ts.isParameter(parent)) { + if ( + isNotPropertyNameOfImport(ident) && + (!ts.isBindingElement(ident.parent) || ident.parent.name == ident) + ) { + // we are a key, not a name, so don't care + if (ident.text.startsWith('$') || scope == rootScope) { + // track all top level declared identifiers and all $ prefixed identifiers + scope.declared.add(ident.text); + } + } + } else { + //track potential store usage to be resolved + if (ident.text.startsWith('$')) { + if ( + (!ts.isPropertyAccessExpression(parent) || parent.expression == ident) && + (!ts.isPropertyAssignment(parent) || parent.initializer == ident) && + !ts.isPropertySignature(parent) && + !ts.isPropertyDeclaration(parent) + ) { + pendingStoreResolutions.push({ node: ident, parent, scope }); + } + } + } + }; + + const handleExportedVariableDeclarationList = (list: ts.VariableDeclarationList) => { + ts.forEachChild(list, (node) => { + if (ts.isVariableDeclaration(node)) { + if (ts.isIdentifier(node.name)) { + exportedNames.addExport(node.name, node.name, node.type, !node.initializer); + } else if ( + ts.isObjectBindingPattern(node.name) || + ts.isArrayBindingPattern(node.name) + ) { + ts.forEachChild(node.name, (element) => { + if (ts.isBindingElement(element)) { + exportedNames.addExport(element.name); + } + }); + } + } + }); + }; + + const wrapExpressionWithInvalidate = (expression: ts.Expression | undefined) => { + if (!expression) { + return; + } + + const start = expression.getStart() + astOffset; + const end = expression.getEnd() + astOffset; + + // () => ({}) + if (ts.isObjectLiteralExpression(expression)) { + str.appendLeft(start, '('); + str.appendRight(end, ')'); + } + + str.prependLeft(start, '__sveltets_invalidate(() => '); + str.appendRight(end, ')'); + // Not adding ';' at the end because right now this function is only invoked + // in situations where there is a line break of ; guaranteed to be present (else the code is invalid) + }; + + const walk = (node: ts.Node, parent: ts.Node) => { + type onLeaveCallback = () => void; + const onLeaveCallbacks: onLeaveCallback[] = []; + + if (ts.isInterfaceDeclaration(node) && node.name.text === 'ComponentEvents') { + events.setComponentEventsInterface(node); + } + + if (ts.isVariableStatement(node)) { + const exportModifier = findExportKeyword(node); + if (exportModifier) { + const isLet = node.declarationList.flags === ts.NodeFlags.Let; + const isConst = node.declarationList.flags === ts.NodeFlags.Const; + + handleExportedVariableDeclarationList(node.declarationList); + if (isLet) { + propTypeAssertToUserDefined(node.declarationList); + } else if (isConst) { + node.declarationList.forEachChild((n) => { + if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) { + addGetter(n.name); + } + }); + } + removeExport(exportModifier.getStart(), exportModifier.end); + } + } + + if (ts.isFunctionDeclaration(node)) { + handleExportFunctionOrClass(node); + + pushScope(); + onLeaveCallbacks.push(() => popScope()); + } + + if (ts.isClassDeclaration(node)) { + handleExportFunctionOrClass(node); + } + + if (ts.isBlock(node)) { + pushScope(); + onLeaveCallbacks.push(() => popScope()); + } + + if (ts.isArrowFunction(node)) { + pushScope(); + onLeaveCallbacks.push(() => popScope()); + } + + if (ts.isExportDeclaration(node)) { + const { exportClause } = node; + if (ts.isNamedExports(exportClause)) { + for (const ne of exportClause.elements) { + if (ne.propertyName) { + exportedNames.addExport(ne.propertyName, ne.name); + } else { + exportedNames.addExport(ne.name); + } + } + //we can remove entire statement + removeExport(node.getStart(), node.end); + } + } + + if (ts.isImportDeclaration(node)) { + //move imports to top of script so they appear outside our render function + str.move(node.getStart() + astOffset, node.end + astOffset, script.start + 1); + //add in a \n + const originalEndChar = str.original[node.end + astOffset - 1]; + str.overwrite(node.end + astOffset - 1, node.end + astOffset, originalEndChar + '\n'); + // Check if import is the event dispatcher + events.checkIfImportIsEventDispatcher(node); + } + + if (ts.isVariableDeclaration(node)) { + events.checkIfIsStringLiteralDeclaration(node); + events.checkIfDeclarationInstantiatedEventDispatcher(node); + implicitStoreValues.addVariableDeclaration(node); + } + + if (ts.isCallExpression(node)) { + events.checkIfCallExpressionIsDispatch(node); + } + + if (ts.isVariableDeclaration(parent) && parent.name == node) { + isDeclaration = true; + onLeaveCallbacks.push(() => (isDeclaration = false)); + } + + if (ts.isBindingElement(parent) && parent.name == node) { + isDeclaration = true; + onLeaveCallbacks.push(() => (isDeclaration = false)); + } + + if (ts.isImportClause(node)) { + isDeclaration = true; + onLeaveCallbacks.push(() => (isDeclaration = false)); + implicitStoreValues.addImportStatement(node); + } + + if (ts.isImportSpecifier(node)) { + implicitStoreValues.addImportStatement(node); + } + + //handle stores etc + if (ts.isIdentifier(node)) { + handleIdentifier(node, parent); + } + + //track implicit declarations in reactive blocks at the top level + if ( + ts.isLabeledStatement(node) && + parent == tsAst && //top level + node.label.text == '$' && + node.statement + ) { + const binaryExpression = getBinaryAssignmentExpr(node); + if (binaryExpression) { + implicitTopLevelNames.add(node); + implicitStoreValues.addReactiveDeclaration(node); + wrapExpressionWithInvalidate(binaryExpression.right); + } else { + const start = node.getStart() + astOffset; + const end = node.getEnd() + astOffset; + + str.prependLeft(start, ';() => {'); + str.appendRight(end, '}'); + } + } + + // Defensively call function (checking for undefined) because it got added only recently (TS 4.0) + // and therefore might break people using older TS versions + if (ts.isTypeAssertionExpression?.(node)) { + handleTypeAssertion(str, node, astOffset); + } + + //to save a bunch of condition checks on each node, we recurse into processChild which skips all the checks for top level items + ts.forEachChild(node, (n) => walk(n, node)); + //fire off the on leave callbacks + onLeaveCallbacks.map((c) => c()); + }; + + //walk the ast and convert to tsx as we go + tsAst.forEachChild((n) => walk(n, tsAst)); + + //resolve stores + pendingStoreResolutions.map(resolveStore); + + // declare implicit reactive variables we found in the script + implicitTopLevelNames.modifyCode(rootScope.declared, astOffset, str); + implicitStoreValues.modifyCode(astOffset, str); + + const firstImport = tsAst.statements + .filter(ts.isImportDeclaration) + .sort((a, b) => a.end - b.end)[0]; + if (firstImport) { + str.appendRight(firstImport.getStart() + astOffset, '\n'); + } + + return { + exportedNames, + events, + uses$$props, + uses$$restProps, + uses$$slots, + getters + }; } diff --git a/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts b/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts index bfd9e1417..bfe6db227 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts @@ -4,55 +4,55 @@ import ts from 'typescript'; import { ImplicitStoreValues } from './nodes/ImplicitStoreValues'; export function processModuleScriptTag( - str: MagicString, - script: Node, - implicitStoreValues: ImplicitStoreValues + str: MagicString, + script: Node, + implicitStoreValues: ImplicitStoreValues ) { - const htmlx = str.original; + const htmlx = str.original; - resolveImplicitStores(htmlx, script, implicitStoreValues, str); + resolveImplicitStores(htmlx, script, implicitStoreValues, str); - const scriptStartTagEnd = htmlx.indexOf('>', script.start) + 1; - const scriptEndTagStart = htmlx.lastIndexOf('<', script.end - 1); + const scriptStartTagEnd = htmlx.indexOf('>', script.start) + 1; + const scriptEndTagStart = htmlx.lastIndexOf('<', script.end - 1); - str.overwrite(script.start, scriptStartTagEnd, ';'); - str.overwrite(scriptEndTagStart, script.end, ';<>'); + str.overwrite(script.start, scriptStartTagEnd, ';'); + str.overwrite(scriptEndTagStart, script.end, ';<>'); } function resolveImplicitStores( - htmlx: string, - script: Node, - implicitStoreValues: ImplicitStoreValues, - str: MagicString + htmlx: string, + script: Node, + implicitStoreValues: ImplicitStoreValues, + str: MagicString ) { - const scriptContent = htmlx.substring(script.content.start, script.content.end); - const tsAst = ts.createSourceFile( - 'component.module.ts.svelte', - scriptContent, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TS - ); - const astOffset = script.content.start; - - const walk = (node: ts.Node) => { - if (ts.isVariableDeclaration(node)) { - implicitStoreValues.addVariableDeclaration(node); - } - - if (ts.isImportClause(node)) { - implicitStoreValues.addImportStatement(node); - } - - if (ts.isImportSpecifier(node)) { - implicitStoreValues.addImportStatement(node); - } - - ts.forEachChild(node, (n) => walk(n)); - }; - - //walk the ast and convert to tsx as we go - tsAst.forEachChild((n) => walk(n)); - - // declare store declarations we found in the script - implicitStoreValues.modifyCode(astOffset, str); + const scriptContent = htmlx.substring(script.content.start, script.content.end); + const tsAst = ts.createSourceFile( + 'component.module.ts.svelte', + scriptContent, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS + ); + const astOffset = script.content.start; + + const walk = (node: ts.Node) => { + if (ts.isVariableDeclaration(node)) { + implicitStoreValues.addVariableDeclaration(node); + } + + if (ts.isImportClause(node)) { + implicitStoreValues.addImportStatement(node); + } + + if (ts.isImportSpecifier(node)) { + implicitStoreValues.addImportStatement(node); + } + + ts.forEachChild(node, (n) => walk(n)); + }; + + //walk the ast and convert to tsx as we go + tsAst.forEachChild((n) => walk(n)); + + // declare store declarations we found in the script + implicitStoreValues.modifyCode(astOffset, str); } diff --git a/packages/svelte2tsx/src/svelte2tsx/utils/Scope.ts b/packages/svelte2tsx/src/svelte2tsx/utils/Scope.ts index 75e780a47..f110679d4 100644 --- a/packages/svelte2tsx/src/svelte2tsx/utils/Scope.ts +++ b/packages/svelte2tsx/src/svelte2tsx/utils/Scope.ts @@ -1,24 +1,24 @@ export class Scope { - declared: Set = new Set(); - parent: Scope; + declared: Set = new Set(); + parent: Scope; - constructor(parent?: Scope) { - this.parent = parent; - } + constructor(parent?: Scope) { + this.parent = parent; + } - hasDefined(name: string) { - return this.declared.has(name) || (!!this.parent && this.parent.hasDefined(name)); - } + hasDefined(name: string) { + return this.declared.has(name) || (!!this.parent && this.parent.hasDefined(name)); + } } export class ScopeStack { - current = new Scope(); + current = new Scope(); - push() { - this.current = new Scope(this.current); - } + push() { + this.current = new Scope(this.current); + } - pop() { - this.current = this.current.parent; - } + pop() { + this.current = this.current.parent; + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts index 5f36a4270..a71b4433a 100644 --- a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts +++ b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts @@ -1,53 +1,53 @@ import ts from 'typescript'; export function findExportKeyword(node: ts.Node) { - return node.modifiers?.find((x) => x.kind == ts.SyntaxKind.ExportKeyword); + return node.modifiers?.find((x) => x.kind == ts.SyntaxKind.ExportKeyword); } /** * Node is like `bla = ...` or `{bla} = ...` or `[bla] = ...` */ function isAssignmentBinaryExpr(node: ts.Expression): node is ts.BinaryExpression { - return ( - ts.isBinaryExpression(node) && - node.operatorToken.kind == ts.SyntaxKind.EqualsToken && - (ts.isIdentifier(node.left) || - ts.isObjectLiteralExpression(node.left) || - ts.isArrayLiteralExpression(node.left)) - ); + return ( + ts.isBinaryExpression(node) && + node.operatorToken.kind == ts.SyntaxKind.EqualsToken && + (ts.isIdentifier(node.left) || + ts.isObjectLiteralExpression(node.left) || + ts.isArrayLiteralExpression(node.left)) + ); } /** * Returns if node is like `$: bla = ...` or `$: ({bla} = ...)` or `$: [bla] = ...=` */ export function getBinaryAssignmentExpr( - node: ts.LabeledStatement + node: ts.LabeledStatement ): ts.BinaryExpression | undefined { - if (ts.isExpressionStatement(node.statement)) { - if (isAssignmentBinaryExpr(node.statement.expression)) { - return node.statement.expression; - } - if ( - ts.isParenthesizedExpression(node.statement.expression) && - isAssignmentBinaryExpr(node.statement.expression.expression) - ) { - return node.statement.expression.expression; - } - } + if (ts.isExpressionStatement(node.statement)) { + if (isAssignmentBinaryExpr(node.statement.expression)) { + return node.statement.expression; + } + if ( + ts.isParenthesizedExpression(node.statement.expression) && + isAssignmentBinaryExpr(node.statement.expression.expression) + ) { + return node.statement.expression.expression; + } + } } /** * Returns true if node is like `({bla} ..)` or `([bla] ...)` */ export function isParenthesizedObjectOrArrayLiteralExpression( - node: ts.Expression + node: ts.Expression ): node is ts.ParenthesizedExpression { - return ( - ts.isParenthesizedExpression(node) && - ts.isBinaryExpression(node.expression) && - (ts.isObjectLiteralExpression(node.expression.left) || - ts.isArrayLiteralExpression(node.expression.left)) - ); + return ( + ts.isParenthesizedExpression(node) && + ts.isBinaryExpression(node.expression) && + (ts.isObjectLiteralExpression(node.expression.left) || + ts.isArrayLiteralExpression(node.expression.left)) + ); } /** @@ -55,51 +55,51 @@ export function isParenthesizedObjectOrArrayLiteralExpression( * Adapted from https://github.com/Rich-Harris/periscopic/blob/d7a820b04e1f88b452313ab3e54771b352f0defb/src/index.ts#L150 */ export function extractIdentifiers( - node: ts.Node, - identifiers: ts.Identifier[] = [] + node: ts.Node, + identifiers: ts.Identifier[] = [] ): ts.Identifier[] { - if (ts.isIdentifier(node)) { - identifiers.push(node); - } else if (ts.isBindingElement(node)) { - extractIdentifiers(node.name, identifiers); - } else if (isMember(node)) { - let object: ts.Node = node; - while (isMember(object)) { - object = object.expression; - } - if (ts.isIdentifier(object)) { - identifiers.push(object); - } - } else if (ts.isArrayBindingPattern(node) || ts.isObjectBindingPattern(node)) { - node.elements.forEach((element) => { - extractIdentifiers(element, identifiers); - }); - } else if (ts.isObjectLiteralExpression(node)) { - node.properties.forEach((child) => { - if (ts.isSpreadAssignment(child)) { - extractIdentifiers(child.expression, identifiers); - } else if (ts.isShorthandPropertyAssignment(child)) { - // in ts Ast { a = 1 } and { a } are both ShorthandPropertyAssignment - extractIdentifiers(child.name, identifiers); - } - }); - } else if (ts.isArrayLiteralExpression(node)) { - node.elements.forEach((element) => { - if (ts.isSpreadElement(element)) { - extractIdentifiers(element, identifiers); - } else { - extractIdentifiers(element, identifiers); - } - }); - } - - return identifiers; + if (ts.isIdentifier(node)) { + identifiers.push(node); + } else if (ts.isBindingElement(node)) { + extractIdentifiers(node.name, identifiers); + } else if (isMember(node)) { + let object: ts.Node = node; + while (isMember(object)) { + object = object.expression; + } + if (ts.isIdentifier(object)) { + identifiers.push(object); + } + } else if (ts.isArrayBindingPattern(node) || ts.isObjectBindingPattern(node)) { + node.elements.forEach((element) => { + extractIdentifiers(element, identifiers); + }); + } else if (ts.isObjectLiteralExpression(node)) { + node.properties.forEach((child) => { + if (ts.isSpreadAssignment(child)) { + extractIdentifiers(child.expression, identifiers); + } else if (ts.isShorthandPropertyAssignment(child)) { + // in ts Ast { a = 1 } and { a } are both ShorthandPropertyAssignment + extractIdentifiers(child.name, identifiers); + } + }); + } else if (ts.isArrayLiteralExpression(node)) { + node.elements.forEach((element) => { + if (ts.isSpreadElement(element)) { + extractIdentifiers(element, identifiers); + } else { + extractIdentifiers(element, identifiers); + } + }); + } + + return identifiers; } export function isMember( - node: ts.Node + node: ts.Node ): node is ts.ElementAccessExpression | ts.PropertyAccessExpression { - return ts.isElementAccessExpression(node) || ts.isPropertyAccessExpression(node); + return ts.isElementAccessExpression(node) || ts.isPropertyAccessExpression(node); } /** @@ -107,45 +107,45 @@ export function isMember( * if it is a variable declaration in the form of `const/let a = ..` */ export function getVariableAtTopLevel( - node: ts.SourceFile, - identifierName: string + node: ts.SourceFile, + identifierName: string ): ts.VariableDeclaration | undefined { - for (const child of node.statements) { - if (ts.isVariableStatement(child)) { - const variable = child.declarationList.declarations.find( - (declaration) => - ts.isIdentifier(declaration.name) && declaration.name.text === identifierName - ); - if (variable) { - return variable; - } - } - } + for (const child of node.statements) { + if (ts.isVariableStatement(child)) { + const variable = child.declarationList.declarations.find( + (declaration) => + ts.isIdentifier(declaration.name) && declaration.name.text === identifierName + ); + if (variable) { + return variable; + } + } + } } /** * Get the leading multiline trivia doc of the node. */ export function getLastLeadingDoc(node: ts.Node): string | undefined { - const nodeText = node.getFullText(); - const comments = ts - .getLeadingCommentRanges(nodeText, 0) - ?.filter((c) => c.kind === ts.SyntaxKind.MultiLineCommentTrivia); - const comment = comments?.[comments?.length - 1]; - - if (comment) { - let commentText = nodeText.substring(comment.pos, comment.end); - - const typedefTags = ts.getAllJSDocTagsOfKind(node, ts.SyntaxKind.JSDocTypedefTag); - typedefTags - .filter((tag) => tag.pos >= comment.pos) - .map((tag) => nodeText.substring(tag.pos, tag.end)) - .forEach((comment) => { - commentText = commentText.replace(comment, ''); - }); - - return commentText; - } + const nodeText = node.getFullText(); + const comments = ts + .getLeadingCommentRanges(nodeText, 0) + ?.filter((c) => c.kind === ts.SyntaxKind.MultiLineCommentTrivia); + const comment = comments?.[comments?.length - 1]; + + if (comment) { + let commentText = nodeText.substring(comment.pos, comment.end); + + const typedefTags = ts.getAllJSDocTagsOfKind(node, ts.SyntaxKind.JSDocTypedefTag); + typedefTags + .filter((tag) => tag.pos >= comment.pos) + .map((tag) => nodeText.substring(tag.pos, tag.end)) + .forEach((comment) => { + commentText = commentText.replace(comment, ''); + }); + + return commentText; + } } /** @@ -153,35 +153,35 @@ export function getLastLeadingDoc(node: ts.Node): string | undefined { * In other words: It is not `a` in `import {a as b} from ..` */ export function isNotPropertyNameOfImport(identifier: ts.Identifier): boolean { - return ( - !ts.isImportSpecifier(identifier.parent) || identifier.parent.propertyName !== identifier - ); + return ( + !ts.isImportSpecifier(identifier.parent) || identifier.parent.propertyName !== identifier + ); } /** * Extract the variable names that are assigned to out of a labeled statement. */ export function getNamesFromLabeledStatement(node: ts.LabeledStatement): string[] { - const leftHandSide = getBinaryAssignmentExpr(node)?.left; - if (!leftHandSide) { - return []; - } - - return ( - extractIdentifiers(leftHandSide) - .map((id) => id.text) - // svelte won't let you create a variable with $ prefix (reserved for stores) - .filter((name) => !name.startsWith('$')) - ); + const leftHandSide = getBinaryAssignmentExpr(node)?.left; + if (!leftHandSide) { + return []; + } + + return ( + extractIdentifiers(leftHandSide) + .map((id) => id.text) + // svelte won't let you create a variable with $ prefix (reserved for stores) + .filter((name) => !name.startsWith('$')) + ); } export function isFirstInAnExpressionStatement(node: ts.Identifier): boolean { - let parent = node.parent; - while (parent && !ts.isExpressionStatement(parent)) { - parent = parent.parent; - } - if (!parent) { - return false; - } - return parent.getStart() === node.getStart(); + let parent = node.parent; + while (parent && !ts.isExpressionStatement(parent)) { + parent = parent.parent; + } + if (!parent) { + return false; + } + return parent.getStart() === node.getStart(); } diff --git a/packages/svelte2tsx/src/utils/htmlxparser.ts b/packages/svelte2tsx/src/utils/htmlxparser.ts index a9a0631fd..eb566ae84 100644 --- a/packages/svelte2tsx/src/utils/htmlxparser.ts +++ b/packages/svelte2tsx/src/utils/htmlxparser.ts @@ -2,166 +2,166 @@ import { parse } from 'svelte/compiler'; import { Node } from 'estree-walker'; function parseAttributeValue(value: string): string { - return /^['"]/.test(value) ? value.slice(1, -1) : value; + return /^['"]/.test(value) ? value.slice(1, -1) : value; } function parseAttributes(str: string, start: number) { - const attrs: Node[] = []; - str.split(/\s+/) - .filter(Boolean) - .forEach((attr) => { - const attrStart = start + str.indexOf(attr); - const [name, value] = attr.split('='); - attrs[name] = value ? parseAttributeValue(value) : name; - attrs.push({ - type: 'Attribute', - name, - value: !value || [ - { - type: 'Text', - start: attrStart + attr.indexOf('=') + 1, - end: attrStart + attr.length, - raw: parseAttributeValue(value) - } - ], - start: attrStart, - end: attrStart + attr.length - }); - }); - - return attrs; + const attrs: Node[] = []; + str.split(/\s+/) + .filter(Boolean) + .forEach((attr) => { + const attrStart = start + str.indexOf(attr); + const [name, value] = attr.split('='); + attrs[name] = value ? parseAttributeValue(value) : name; + attrs.push({ + type: 'Attribute', + name, + value: !value || [ + { + type: 'Text', + start: attrStart + attr.indexOf('=') + 1, + end: attrStart + attr.length, + raw: parseAttributeValue(value) + } + ], + start: attrStart, + end: attrStart + attr.length + }); + }); + + return attrs; } function extractTag(htmlx: string, tag: 'script' | 'style') { - const exp = new RegExp(`()|(<${tag}([\\S\\s]*?)>)([\\S\\s]*?)<\\/${tag}>`, 'g'); - const matches: Node[] = []; - - let match: RegExpExecArray | null = null; - while ((match = exp.exec(htmlx)) != null) { - if (match[0].startsWith(')|(<${tag}([\\S\\s]*?)>)([\\S\\s]*?)<\\/${tag}>`, 'g'); + const matches: Node[] = []; + + let match: RegExpExecArray | null = null; + while ((match = exp.exec(htmlx)) != null) { + if (match[0].startsWith(' \ No newline at end of file +--> diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index e80bc46c4..6d1b1fd96 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -21,4 +21,4 @@ Add any other context or screenshots about the feature request here. \ No newline at end of file +--> diff --git a/.github/ISSUE_TEMPLATE/something-else.md b/.github/ISSUE_TEMPLATE/something-else.md index 65f6e38a8..196e33d4a 100644 --- a/.github/ISSUE_TEMPLATE/something-else.md +++ b/.github/ISSUE_TEMPLATE/something-else.md @@ -6,5 +6,3 @@ labels: '' assignees: '' --- - - diff --git a/README.md b/README.md index b3b4732e5..09692969f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- Cybernetically enhanced web apps: Svelte + Cybernetically enhanced web apps: Svelte diff --git a/docs/README.md b/docs/README.md index 4f42d85ac..67bf4f72d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,7 +47,7 @@ LSP-compatible editors, you can use an HTML comment with the `@component` tag: The VS Code extension comes with its own syntax highlighting grammar which defines special scopes. If your syntax highlighting seems to be not working for Svelte components or you feel that some colors are wrong, you can add something like the following to your `settings.json`: -``` +```json { "editor.tokenColorCustomizations": { "[]": { @@ -80,7 +80,7 @@ You need to save the file to see the changes. If the problem persists after savi ```json "files.watcherExclude": { - "**/*": true, + "**/*": true, } ``` @@ -94,13 +94,13 @@ If you have the `Babel Javascript` plugin installed, this may be the cause. Disa Your default formatter for Svelte files may be wrong. -- Mabye it's set to the old Svelte extension, if so, remove the setting +- Maybe it's set to the old Svelte extension, if so, remove the setting - Maybe you set all files to be formatted by the prettier extension. Then you have two options: Either install `prettier-plugin-svelte` from npm, or tell VSCode to format the code with the `Svelte for VSCode extension`: ```json - "[svelte]": { +"[svelte]": { "editor.defaultFormatter": "svelte.svelte-vscode" - }, +}, ``` ## Internals diff --git a/docs/preprocessors/typescript.md b/docs/preprocessors/typescript.md index aecbd95c1..52f4d7015 100644 --- a/docs/preprocessors/typescript.md +++ b/docs/preprocessors/typescript.md @@ -108,10 +108,9 @@ At the moment, you cannot. Only `script`/`style` tags are preprocessed/transpile You may need to set `baseUrl` in `tsconfig.json` at the project root to include (restart the language server to see this take effect): -``` +```json "compilerOptions": { "baseUrl": "." - } } ``` @@ -140,7 +139,7 @@ Then make sure that `d.ts` file is referenced in your `tsconfig.json`. If it rea You are most likely extending from Svelte's `@tsconfig/svelte` base config in your `tsconfig.json`, or you did set `"types": [..]` in your `tsconfig`. In both cases, a `"types": [..]` property is present. This makes TypeScript prevent all ambient types which are not listed in that `types`-array from getting picked up. The solution is to enhance/add a `types` section to your `tsconfig.json`: -``` +```json { "compilerOptions": { // .. diff --git a/packages/language-server/README.md b/packages/language-server/README.md index b851da0fc..ce36c3fb0 100644 --- a/packages/language-server/README.md +++ b/packages/language-server/README.md @@ -93,7 +93,6 @@ Update: javascript: { /* .. */ }, prettierConfig: { /* .. */ }, // ... - } } ``` diff --git a/packages/svelte-vscode/README.md b/packages/svelte-vscode/README.md index 9a6efbd66..3cc796c05 100644 --- a/packages/svelte-vscode/README.md +++ b/packages/svelte-vscode/README.md @@ -13,10 +13,10 @@ If you have previously installed the old "Svelte" extension by James Birtles, un This extension comes bundled with a formatter for Svelte files. To let this extension format Svelte files, adjust your VS Code settings: -``` - "[svelte]": { - "editor.defaultFormatter": "svelte.svelte-vscode" - }, +```json +"[svelte]": { + "editor.defaultFormatter": "svelte.svelte-vscode" +}, ``` The formatter is a [Prettier](https://prettier.io/) [plugin](https://prettier.io/docs/en/plugins.html), which means some formatting options of Prettier apply. There are also Svelte specific settings like the sort order of scripts, markup, styles. More info about them and how to configure it can be found [here](https://github.com/sveltejs/prettier-plugin-svelte). From be3687b6b7e3d2aa4fd3a555fdc14a6475e09f36 Mon Sep 17 00:00:00 2001 From: Ignatius Bagus Date: Fri, 16 Apr 2021 21:38:34 +0700 Subject: [PATCH 8/9] change code block lang to svelte only where it's appropriate --- README.md | 2 +- docs/internal/overview.md | 4 ++-- docs/preprocessors/in-general.md | 2 +- docs/preprocessors/scss-less.md | 2 +- docs/preprocessors/typescript.md | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 09692969f..8da33c8fb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Svelte Language Tools contains a library implementing the Language Server Protoc A `.svelte` file would look something like this: -```html +```svelte @@ -64,7 +64,7 @@ This example shows how our `language-server` uses `svelte2tsx`: 1. Svelte file comes in -```html +```svelte diff --git a/docs/preprocessors/in-general.md b/docs/preprocessors/in-general.md index 496908688..299e15183 100644 --- a/docs/preprocessors/in-general.md +++ b/docs/preprocessors/in-general.md @@ -17,7 +17,7 @@ module.exports = { It's also necessary to add a `type="text/language-name"` or `lang="language-name"` to your `style` and `script` tags, which defines how that code should be interpreted by the extension. -```html +```svelte

diff --git a/docs/preprocessors/scss-less.md b/docs/preprocessors/scss-less.md index c185032d7..f292f00fc 100644 --- a/docs/preprocessors/scss-less.md +++ b/docs/preprocessors/scss-less.md @@ -35,7 +35,7 @@ module.exports = { To gain syntax highlighing for your SCSS code and to make us understand the language you are using, add a `type` or `lang` attribute to your style tags like so: -```html +```svelte