From a04a9a31ab2df58a4969ac3a0ac29ec6c2645aa8 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Fri, 13 Sep 2024 21:17:57 +0200 Subject: [PATCH 1/3] feat: allow finer-grained control of insertText See the documentation for server-only settings for the details. https://wkillerud.github.io/some-sass/language-server/configure-a-client.html#server-only-settings --- .../src/language-server/configure-a-client.md | 28 ++ docs/src/language-server/existing-clients.md | 1 - package-lock.json | 274 +----------------- package.json | 2 - packages/language-server/src/server.ts | 42 ++- packages/language-server/src/settings.ts | 30 ++ .../language-server/src/utils/embedded.ts | 2 +- .../__tests__/do-complete-modules.test.ts | 8 +- .../src/features/do-complete.ts | 231 +++++++-------- .../language-services/src/language-feature.ts | 48 ++- .../src/language-services-types.ts | 63 ++-- vscode-extension/package.json | 5 +- 12 files changed, 291 insertions(+), 443 deletions(-) diff --git a/docs/src/language-server/configure-a-client.md b/docs/src/language-server/configure-a-client.md index 9e399e50..d92c62d3 100644 --- a/docs/src/language-server/configure-a-client.md +++ b/docs/src/language-server/configure-a-client.md @@ -47,6 +47,34 @@ For example: } ``` +### Language-specific configuration + +For the completion settings above you can tweak them per supported language. + +The setting without a specified syntax applies to all of them. +If both are specified, the one for the individual syntax takes precedence. + +```json +{ + "settings": { + "somesass": { + "completion": { + "afterModule": "{module}", + }, + "vue": { + "completion": { + "afterModule": "{module}." + } + } + } + } +} +``` + +In the example above, `somesass.completion.afterModule` with the value `{module}` applies to all languages except Vue, which uses `{module}.`. + + + ## Existing clients This list of [language client implementations][languageclients] may be a helpful starting point. You may also want to look at [existing clients](./existing-clients.md). diff --git a/docs/src/language-server/existing-clients.md b/docs/src/language-server/existing-clients.md index dd2e5b8d..a9d1cbde 100644 --- a/docs/src/language-server/existing-clients.md +++ b/docs/src/language-server/existing-clients.md @@ -6,4 +6,3 @@ these are editors with ready-configured clients, maintained by the community. - [Helix](./helix.md) - [Neovim](./neovim.md) - diff --git a/package-lock.json b/package-lock.json index 17a372a0..33fc7912 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "@somesass/project", "version": "1.0.0", - "hasInstallScript": true, "workspaces": [ "packages/*", "vscode-extension" @@ -37,7 +36,6 @@ "npm-run-all2": "6.2.2", "nx": "19.6.5", "ovsx": "0.9.2", - "patch-package": "8.0.0", "prettier": "3.3.3", "shx": "0.3.4", "ts-loader": "9.5.1", @@ -11433,16 +11431,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-yarn-workspace-root": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "micromatch": "^4.0.2" - } - }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -13298,13 +13286,6 @@ "node": ">=8" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -13737,25 +13718,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stable-stringify": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz", - "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "isarray": "^2.0.5", - "jsonify": "^0.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -13796,16 +13758,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", - "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", - "dev": true, - "license": "Public Domain", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -13998,16 +13950,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11" - } - }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -16794,16 +16736,6 @@ "dev": true, "license": "ISC" }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ovsx": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/ovsx/-/ovsx-0.9.2.tgz", @@ -17211,206 +17143,6 @@ "node": ">= 0.8" } }, - "node_modules/patch-package": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", - "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^4.1.2", - "ci-info": "^3.7.0", - "cross-spawn": "^7.0.3", - "find-yarn-workspace-root": "^2.0.0", - "fs-extra": "^9.0.0", - "json-stable-stringify": "^1.0.2", - "klaw-sync": "^6.0.0", - "minimist": "^1.2.6", - "open": "^7.4.2", - "rimraf": "^2.6.3", - "semver": "^7.5.3", - "slash": "^2.0.0", - "tmp": "^0.0.33", - "yaml": "^2.2.2" - }, - "bin": { - "patch-package": "index.js" - }, - "engines": { - "node": ">=14", - "npm": ">5" - } - }, - "node_modules/patch-package/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/patch-package/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/patch-package/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/patch-package/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/patch-package/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/patch-package/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/patch-package/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/patch-package/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/patch-package/node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/patch-package/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/patch-package/node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/patch-package/node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -22981,7 +22713,7 @@ "some-sass-language-server": "1.8.0", "vscode-css-languageservice": "6.3.1", "vscode-languageclient": "9.0.1", - "vscode-uri": "3.0.7" + "vscode-uri": "3.0.8" }, "devDependencies": { "@types/mocha": "10.0.7", @@ -23019,10 +22751,6 @@ "engines": { "node": ">= 6" } - }, - "vscode-extension/node_modules/vscode-uri": { - "version": "3.0.7", - "license": "MIT" } } } diff --git a/package.json b/package.json index 854448a9..8dd9fd4f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ ], "scripts": { "prepare": "husky", - "postinstall": "patch-package", "build": "nx run-many -t build", "preclean": "nx reset", "clean": "nx run-many -t clean", @@ -58,7 +57,6 @@ "npm-run-all2": "6.2.2", "nx": "19.6.5", "ovsx": "0.9.2", - "patch-package": "8.0.0", "prettier": "3.3.3", "shx": "0.3.4", "ts-loader": "9.5.1", diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 34ea2d0a..f8bd9f1d 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -146,8 +146,46 @@ export class SomeSassServer { suggestionStyle: settings.suggestionStyle, suggestFunctionsInStringContextAfterSymbols: settings.suggestFunctionsInStringContextAfterSymbols, - afterModule: settings.completion?.afterModule, - beforeVariable: settings.completion?.beforeVariable, + scss: { + afterModule: + settings?.scss?.completion?.afterModule || + settings.completion?.afterModule, + beforeVariable: + settings?.scss?.completion?.beforeVariable || + settings.completion?.beforeVariable, + }, + sass: { + afterModule: + settings.sass?.completion?.afterModule || + settings.completion?.afterModule, + beforeVariable: + settings.sass?.completion?.beforeVariable || + settings.completion?.afterModule, + }, + astro: { + afterModule: + settings.astro?.completion?.afterModule || + settings.completion?.afterModule, + beforeVariable: + settings.astro?.completion?.beforeVariable || + settings.completion?.afterModule, + }, + vue: { + afterModule: + settings.vue?.completion?.afterModule || + settings.completion?.afterModule, + beforeVariable: + settings.vue?.completion?.beforeVariable || + settings.completion?.afterModule, + }, + svelte: { + afterModule: + settings.svelte?.completion?.afterModule || + settings.completion?.afterModule, + beforeVariable: + settings.svelte?.completion?.beforeVariable || + settings.completion?.afterModule, + }, }, }); } diff --git a/packages/language-server/src/settings.ts b/packages/language-server/src/settings.ts index f57fa3dc..8546774c 100644 --- a/packages/language-server/src/settings.ts +++ b/packages/language-server/src/settings.ts @@ -12,6 +12,36 @@ export interface ISettings { afterModule?: string; beforeVariable?: string; }; + readonly scss?: { + completion?: { + afterModule?: string; + beforeVariable?: string; + }; + }; + readonly sass?: { + completion?: { + afterModule?: string; + beforeVariable?: string; + }; + }; + readonly astro?: { + completion?: { + afterModule?: string; + beforeVariable?: string; + }; + }; + readonly vue?: { + completion?: { + afterModule?: string; + beforeVariable?: string; + }; + }; + readonly svelte?: { + completion?: { + afterModule?: string; + beforeVariable?: string; + }; + }; } export interface IEditorSettings { diff --git a/packages/language-server/src/utils/embedded.ts b/packages/language-server/src/utils/embedded.ts index bd0bfc88..0dbf74df 100644 --- a/packages/language-server/src/utils/embedded.ts +++ b/packages/language-server/src/utils/embedded.ts @@ -89,7 +89,7 @@ export function getSassRegionsDocument( return TextDocument.create( uri, - regions[0].type, + document.languageId, version, getSassContent(text, regions), ); diff --git a/packages/language-services/src/features/__tests__/do-complete-modules.test.ts b/packages/language-services/src/features/__tests__/do-complete-modules.test.ts index 5b8be8cf..5de5ac59 100644 --- a/packages/language-services/src/features/__tests__/do-complete-modules.test.ts +++ b/packages/language-services/src/features/__tests__/do-complete-modules.test.ts @@ -1060,8 +1060,8 @@ test("should suggest all symbols as legacy @import may be in use", async () => { { commitCharacters: [";", ","], documentation: "limegreen\n____\nVariable declared in one.scss", - filterText: undefined, - insertText: undefined, + filterText: "$primary", + insertText: "$primary", kind: CompletionItemKind.Color, label: "$primary", sortText: undefined, @@ -1127,8 +1127,8 @@ test("should suggest symbol from a different document via @use with wildcard ali { commitCharacters: [";", ","], documentation: "limegreen\n____\nVariable declared in one.scss", - filterText: undefined, - insertText: undefined, + filterText: "$primary", + insertText: "$primary", kind: CompletionItemKind.Color, label: "$primary", sortText: undefined, diff --git a/packages/language-services/src/features/do-complete.ts b/packages/language-services/src/features/do-complete.ts index 3a2718d8..0de99bcb 100644 --- a/packages/language-services/src/features/do-complete.ts +++ b/packages/language-services/src/features/do-complete.ts @@ -33,6 +33,7 @@ import { TextDocument, URI, Utils, + LanguageSpecificCompletionSettings, } from "../language-services-types"; import { asDollarlessVariable } from "../utils/sass"; import { applySassDoc } from "../utils/sassdoc"; @@ -276,7 +277,6 @@ export class DoComplete extends LanguageFeature { // Legacy @import style suggestions if (!this.configuration.completionSettings?.suggestFromUseOnly) { - const currentWord = context.currentWord; const documents = this.cache.documents(); for (const currentDocument of documents) { if ( @@ -300,7 +300,7 @@ export class DoComplete extends LanguageFeature { const items = await this.doVariableCompletion( document, currentDocument, - currentWord, + context, symbol, isPrivate, ); @@ -315,7 +315,7 @@ export class DoComplete extends LanguageFeature { const items = await this.doMixinCompletion( document, currentDocument, - currentWord, + context, symbol, isPrivate, ); @@ -330,7 +330,7 @@ export class DoComplete extends LanguageFeature { const items = await this.doFunctionCompletion( document, currentDocument, - currentWord, + context, symbol, isPrivate, ); @@ -722,10 +722,9 @@ export class DoComplete extends LanguageFeature { const vars = await this.doVariableCompletion( document, currentDocument, - context.currentWord, + context, symbol, isPrivate, - context.namespace, prefix, ); if (vars.length > 0) { @@ -739,10 +738,9 @@ export class DoComplete extends LanguageFeature { const mixs = await this.doMixinCompletion( document, currentDocument, - context.currentWord, + context, symbol, isPrivate, - context.namespace, prefix, ); if (mixs.length > 0) { @@ -756,10 +754,9 @@ export class DoComplete extends LanguageFeature { const funcs = await this.doFunctionCompletion( document, currentDocument, - context.currentWord, + context, symbol, isPrivate, - context.namespace, prefix, ); if (funcs.length > 0) { @@ -802,13 +799,79 @@ export class DoComplete extends LanguageFeature { return result; } + getInsertText( + document: TextDocument, + context: CompletionContext, + label: string, + symbolKind: SymbolKind, + ): string { + let settings: Required; + + // We don't use languageId here since it gets set to scss or sass in vue, svelte and astro files + // when we extract only the Sass syntax parts of the document. + const dotExt = document.uri.slice( + Math.max(0, document.uri.lastIndexOf(".")), + ); + + switch (dotExt) { + case ".vue": + settings = this.configuration.completionSettings + ?.vue as Required; + break; + case ".svelte": + settings = this.configuration.completionSettings + ?.svelte as Required; + break; + case ".astro": + settings = this.configuration.completionSettings + ?.astro as Required; + break; + case ".sass": + settings = this.configuration.completionSettings + ?.sass as Required; + break; + case ".scss": + default: + settings = this.configuration.completionSettings + ?.scss as Required; + break; + } + + const namespace = context.namespace; + if (!namespace || namespace === "*") { + if (settings.beforeVariable === "") { + return asDollarlessVariable(label); + } + return label; + } + + let insertText = label; + const noDot = !settings.afterModule.includes("."); + + if (symbolKind === SymbolKind.Variable) { + const noDollar = !settings.afterModule.includes("$"); + if (context.currentWord.endsWith(".")) { + insertText = `${noDot ? "" : "."}${label}`; + } else if (noDollar) { + insertText = asDollarlessVariable(label); + } + } else { + insertText = `${noDot ? "" : "."}${label}`; + } + + if (settings.afterModule.startsWith("{module}")) { + insertText = `${namespace}${insertText}`; + } + + return insertText; + } + private async doVariableCompletion( initialDocument: TextDocument, currentDocument: TextDocument, - currentWord: string, + context: CompletionContext, symbol: SassDocumentSymbol, isPrivate: boolean, - namespace = "", prefix = "", ): Promise { // Avoid ending up with namespace.prefix-$variable @@ -839,46 +902,15 @@ export class DoComplete extends LanguageFeature { documentation += `\n____\nVariable declared in ${this.getFileName(currentDocument.uri)}`; const sortText = isPrivate ? label.replace(/^$[_]/, "") : undefined; - - const dotExt = initialDocument.uri.slice( - Math.max(0, initialDocument.uri.lastIndexOf(".")), + const insertText = this.getInsertText( + initialDocument, + context, + label, + SymbolKind.Variable, ); - const isEmbedded = !dotExt.match(reSassDotExt); - let insertText: string | undefined; - let filterText: string | undefined; - - if (namespace && namespace !== "*") { - const noDot = - isEmbedded || - dotExt === ".sass" || - this.configuration.completionSettings?.afterModule === ""; - - const noDollar = isEmbedded; - - insertText = currentWord.endsWith(".") - ? `${noDot ? "" : "."}${label}` - : noDollar - ? asDollarlessVariable(label) - : label; - - if ( - this.configuration.completionSettings?.afterModule && - this.configuration.completionSettings.afterModule.startsWith("{module}") - ) { - insertText = `${namespace}${insertText}`; - } - - filterText = currentWord.endsWith(".") ? `${namespace}.${label}` : label; - } else if ( - dotExt === ".vue" || - dotExt === ".astro" || - dotExt === ".sass" || - this.configuration.completionSettings?.beforeVariable === "" - ) { - // In these languages the $ does not get replaced by the suggestion, - // so exclude it from the insertText. - insertText = asDollarlessVariable(label); - } + const filterText = context.currentWord.endsWith(".") + ? `${context.namespace}.${label}` + : label; const item: CompletionItem = { commitCharacters: [";", ","], @@ -910,14 +942,14 @@ export class DoComplete extends LanguageFeature { private async doMixinCompletion( initialDocument: TextDocument, currentDocument: TextDocument, - currentWord: string, + context: CompletionContext, symbol: SassDocumentSymbol, isPrivate: boolean, - namespace = "", prefix = "", ): Promise { const items: CompletionItem[] = []; + const namespace = context.namespace; const label = `${prefix}${symbol.name}`; const filterText = namespace ? namespace !== "*" @@ -925,29 +957,12 @@ export class DoComplete extends LanguageFeature { : `${prefix}${symbol.name}` : symbol.name; - const isEmbedded = this.isEmbedded(initialDocument); - - const noDot = - namespace === "*" || - isEmbedded || - initialDocument.languageId === "sass" || - this.configuration.completionSettings?.afterModule === ""; - - let insertText = namespace - ? noDot - ? `${prefix}${symbol.name}` - : `.${prefix}${symbol.name}` - : symbol.name; - - if ( - namespace && - namespace !== "*" && - this.configuration.completionSettings?.afterModule && - this.configuration.completionSettings.afterModule.startsWith("{module}") - ) { - insertText = `${namespace}${insertText}`; - } - + const insertText = this.getInsertText( + initialDocument, + context, + label, + SymbolKind.Method, + ); const sortText = isPrivate ? label.replace(/^$[_]/, "") : undefined; const documentation = { @@ -1049,14 +1064,14 @@ export class DoComplete extends LanguageFeature { private async doFunctionCompletion( initialDocument: TextDocument, currentDocument: TextDocument, - currentWord: string, + context: CompletionContext, symbol: SassDocumentSymbol, isPrivate: boolean, - namespace = "", prefix = "", ): Promise { const items: CompletionItem[] = []; + const namespace = context.namespace; const label = `${prefix}${symbol.name}`; let filterText = symbol.name; if (namespace) { @@ -1067,29 +1082,12 @@ export class DoComplete extends LanguageFeature { } } - const isEmbedded = this.isEmbedded(initialDocument); - - const noDot = - namespace === "*" || - isEmbedded || - initialDocument.languageId === "sass" || - this.configuration.completionSettings?.afterModule === ""; - - let insertText = namespace - ? noDot - ? `${prefix}${symbol.name}` - : `.${prefix}${symbol.name}` - : symbol.name; - - if ( - namespace && - namespace !== "*" && - this.configuration.completionSettings?.afterModule && - this.configuration.completionSettings.afterModule.startsWith("{module}") - ) { - insertText = `${namespace}${insertText}`; - } - + const insertText = this.getInsertText( + initialDocument, + context, + label, + SymbolKind.Function, + ); const sortText = isPrivate ? label.replace(/^$[_]/, "") : undefined; const documentation = { @@ -1172,29 +1170,12 @@ export class DoComplete extends LanguageFeature { // Client needs the namespace as part of the text that is matched, const filterText = `${context.namespace}.${label}`; - - // Inserted text needs to include the `.` which will otherwise - // be replaced (except when we're embedded in Vue, Svelte or Astro). - // Example result: .floor(${1:number}) - const isEmbedded = this.isEmbedded(document); - - const noDot = - isEmbedded || - document.languageId === "sass" || - this.configuration.completionSettings?.afterModule === ""; - - let insertText = context.currentWord.includes(".") - ? `${noDot ? "" : "."}${label}${ - signature ? `(${parameterSnippet})` : "" - }` - : label; - - if ( - this.configuration.completionSettings?.afterModule && - this.configuration.completionSettings.afterModule.startsWith("{module}") - ) { - insertText = `${context.namespace}${insertText}`; - } + const insertText = this.getInsertText( + document, + context, + label, + signature ? SymbolKind.Function : SymbolKind.Variable, + ); items.push({ documentation: { @@ -1202,7 +1183,7 @@ export class DoComplete extends LanguageFeature { value: `${description}\n\n[Sass documentation](${moduleDocs.reference}#${name})`, }, filterText, - insertText, + insertText: `${insertText}${signature ? `(${parameterSnippet})` : ""}`, insertTextFormat: parameterSnippet ? InsertTextFormat.Snippet : InsertTextFormat.PlainText, diff --git a/packages/language-services/src/language-feature.ts b/packages/language-services/src/language-feature.ts index 5f6c543b..1a9e1acb 100644 --- a/packages/language-services/src/language-feature.ts +++ b/packages/language-services/src/language-feature.ts @@ -45,8 +45,26 @@ const defaultConfiguration: LanguageServiceConfiguration = { suggestFunctionsInStringContextAfterSymbols: " (+-*%", suggestionStyle: "all", triggerPropertyValueCompletion: true, - afterModule: ".", - beforeVariable: "$", + scss: { + afterModule: ".$", + beforeVariable: "$", + }, + sass: { + afterModule: ".$", + beforeVariable: "", + }, + vue: { + afterModule: "", + beforeVariable: "", + }, + astro: { + afterModule: "", + beforeVariable: "", + }, + svelte: { + afterModule: "", + beforeVariable: "$", + }, }, }; @@ -57,7 +75,7 @@ const defaultConfiguration: LanguageServiceConfiguration = { export abstract class LanguageFeature { protected ls; protected options; - protected configuration: LanguageServiceConfiguration = {}; + protected configuration: LanguageServiceConfiguration = defaultConfiguration; private _internal: LanguageFeatureInternal; @@ -81,10 +99,30 @@ export abstract class LanguageFeature { ...configuration, completionSettings: { ...defaultConfiguration.completionSettings, - ...(configuration.completionSettings || {}), + ...configuration.completionSettings, + scss: { + ...defaultConfiguration.completionSettings?.scss, + ...configuration.completionSettings?.scss, + }, + sass: { + ...defaultConfiguration.completionSettings?.sass, + ...configuration.completionSettings?.sass, + }, + astro: { + ...defaultConfiguration.completionSettings?.astro, + ...configuration.completionSettings?.astro, + }, + vue: { + ...defaultConfiguration.completionSettings?.vue, + ...configuration.completionSettings?.vue, + }, + svelte: { + ...defaultConfiguration.completionSettings?.svelte, + ...configuration.completionSettings?.svelte, + }, }, }; - this._internal.sassLs.configure(configuration); + this._internal.sassLs.configure(this.configuration); } protected getUpstreamLanguageServer(): VSCodeLanguageService { diff --git a/packages/language-services/src/language-services-types.ts b/packages/language-services/src/language-services-types.ts index f86d97ca..94d173c4 100644 --- a/packages/language-services/src/language-services-types.ts +++ b/packages/language-services/src/language-services-types.ts @@ -175,6 +175,36 @@ export type Rename = | { range: Range; placeholder: string } | { defaultBehavior: boolean }; +export type LanguageSpecificCompletionSettings = { + /** + * If you end up with an extra `.` after accepting a suggestion, set this to the empty string. + * If your module disappears, set it to "{module}" or "{module}." depending on your situation. + * + * @example + * ```scss + * .foo { + * // set this setting to the empty string "" to fix this bug, + * // which varies depending on your editor's grammar for Sass. + * color: module..$variable; + * } + * ``` + */ + afterModule?: string; + /** + * If you end up with an extra `&` after accepting a suggestion, set this to the empty string. + * + * @example + * ```scss + * .foo { + * // set this setting to the empty string "" to fix this bug, + * // which varies depending on your editor's grammar for Sass. + * color: $$variable; + * } + * ``` + */ + beforeVariable?: string; +}; + export interface LanguageServiceConfiguration { /** * Pass in [load paths](https://sass-lang.com/documentation/cli/dart-sass/#load-path) that will be used in addition to `node_modules`. @@ -224,34 +254,11 @@ export interface LanguageServiceConfiguration { * @default true */ triggerPropertyValueCompletion?: boolean; - includePrefixDot?: boolean; - /** - * If you end up with an extra `.` after accepting a suggestion, set this to the empty string. - * If your module disappears, set it to "{module}" or "{module}." depending on your situation. - * - * @example - * ```scss - * .foo { - * // set this setting to the empty string "" to fix this bug, - * // which varies depending on your editor's grammar for Sass. - * color: module..$variable; - * } - * ``` - */ - afterModule?: string; - /** - * If you end up with an extra `&` after accepting a suggestion, set this to the empty string. - * - * @example - * ```scss - * .foo { - * // set this setting to the empty string "" to fix this bug, - * // which varies depending on your editor's grammar for Sass. - * color: $$variable; - * } - * ``` - */ - beforeVariable?: string; + scss?: LanguageSpecificCompletionSettings; + sass?: LanguageSpecificCompletionSettings; + vue?: LanguageSpecificCompletionSettings; + svelte?: LanguageSpecificCompletionSettings; + astro?: LanguageSpecificCompletionSettings; }; editorSettings?: EditorSettings; workspaceRoot?: URI; diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 2f1eeba0..d30f97d1 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -142,7 +142,7 @@ "some-sass-language-server": "1.8.0", "vscode-css-languageservice": "6.3.1", "vscode-languageclient": "9.0.1", - "vscode-uri": "3.0.7" + "vscode-uri": "3.0.8" }, "devDependencies": { "@types/mocha": "10.0.7", @@ -161,6 +161,7 @@ "build:browser": "rspack --config ./rspack.browser.config.js", "build:development:node": "npm run build:node -- --mode=development", "build:development:browser": "npm run build:browser -- --mode=development", + "build:production": "run-s build:production:*", "build:production:node": "npm run build:node -- --mode=production", "build:production:browser": "npm run build:browser -- --mode=production", "start:web": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=.", @@ -176,4 +177,4 @@ "publisherId": "02638283-c13a-4acf-9f26-24bdcfdfce24", "isPreReleaseVersion": false } -} \ No newline at end of file +} From b1b99fa375d1758f929bc77e9989517a26f4e841 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Fri, 13 Sep 2024 22:37:36 +0200 Subject: [PATCH 2/3] refactor: use lodash.merge to not clobber defaults with undefined --- package-lock.json | 20 ++++++++++++- packages/language-server/src/server.ts | 4 +-- .../language-server/src/utils/embedded.ts | 2 +- packages/language-services/package.json | 2 ++ .../language-services/src/language-feature.ts | 30 ++----------------- packages/language-services/tsconfig.json | 1 + 6 files changed, 27 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33fc7912..9dad7760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6077,6 +6077,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.merge": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", + "integrity": "sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -14848,7 +14865,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, "node_modules/lodash.mergewith": { @@ -22664,9 +22680,11 @@ "dependencies": { "@somesass/vscode-css-languageservice": "1.6.0", "colorjs.io": "0.5.2", + "lodash.merge": "4.6.2", "sassdoc-parser": "3.4.0" }, "devDependencies": { + "@types/lodash.merge": "4.6.9", "@vitest/coverage-v8": "2.0.5", "shx": "0.3.4", "typescript": "5.5.4" diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index f8bd9f1d..3d915a30 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -148,10 +148,10 @@ export class SomeSassServer { settings.suggestFunctionsInStringContextAfterSymbols, scss: { afterModule: - settings?.scss?.completion?.afterModule || + settings.scss?.completion?.afterModule || settings.completion?.afterModule, beforeVariable: - settings?.scss?.completion?.beforeVariable || + settings.scss?.completion?.beforeVariable || settings.completion?.beforeVariable, }, sass: { diff --git a/packages/language-server/src/utils/embedded.ts b/packages/language-server/src/utils/embedded.ts index 0dbf74df..bd0bfc88 100644 --- a/packages/language-server/src/utils/embedded.ts +++ b/packages/language-server/src/utils/embedded.ts @@ -89,7 +89,7 @@ export function getSassRegionsDocument( return TextDocument.create( uri, - document.languageId, + regions[0].type, version, getSassContent(text, regions), ); diff --git a/packages/language-services/package.json b/packages/language-services/package.json index 82114686..bcf3437f 100644 --- a/packages/language-services/package.json +++ b/packages/language-services/package.json @@ -50,9 +50,11 @@ "dependencies": { "@somesass/vscode-css-languageservice": "1.6.0", "colorjs.io": "0.5.2", + "lodash.merge": "4.6.2", "sassdoc-parser": "3.4.0" }, "devDependencies": { + "@types/lodash.merge": "4.6.9", "@vitest/coverage-v8": "2.0.5", "shx": "0.3.4", "typescript": "5.5.4" diff --git a/packages/language-services/src/language-feature.ts b/packages/language-services/src/language-feature.ts index 1a9e1acb..a18009f6 100644 --- a/packages/language-services/src/language-feature.ts +++ b/packages/language-services/src/language-feature.ts @@ -5,6 +5,7 @@ import { Scanner, SassScanner, } from "@somesass/vscode-css-languageservice"; +import merge from "lodash.merge"; import { LanguageModelCache } from "./language-model-cache"; import { LanguageServiceOptions, @@ -94,34 +95,7 @@ export abstract class LanguageFeature { } configure(configuration: LanguageServiceConfiguration): void { - this.configuration = { - ...defaultConfiguration, - ...configuration, - completionSettings: { - ...defaultConfiguration.completionSettings, - ...configuration.completionSettings, - scss: { - ...defaultConfiguration.completionSettings?.scss, - ...configuration.completionSettings?.scss, - }, - sass: { - ...defaultConfiguration.completionSettings?.sass, - ...configuration.completionSettings?.sass, - }, - astro: { - ...defaultConfiguration.completionSettings?.astro, - ...configuration.completionSettings?.astro, - }, - vue: { - ...defaultConfiguration.completionSettings?.vue, - ...configuration.completionSettings?.vue, - }, - svelte: { - ...defaultConfiguration.completionSettings?.svelte, - ...configuration.completionSettings?.svelte, - }, - }, - }; + this.configuration = merge(defaultConfiguration, configuration); this._internal.sassLs.configure(this.configuration); } diff --git a/packages/language-services/tsconfig.json b/packages/language-services/tsconfig.json index 2b0655a9..946c1b3e 100644 --- a/packages/language-services/tsconfig.json +++ b/packages/language-services/tsconfig.json @@ -6,6 +6,7 @@ "module": "commonjs", "moduleResolution": "node", "declaration": true, + "esModuleInterop": true, "rootDir": "src", "outDir": "dist", "strict": true From 61e4f8bb895bce96902bbcc501c785a8eb5b287e Mon Sep 17 00:00:00 2001 From: William Killerud Date: Fri, 13 Sep 2024 22:43:21 +0200 Subject: [PATCH 3/3] refactor: allow empty string setting --- packages/language-server/src/server.ts | 50 +++++++++++++++----------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 3d915a30..6cf61fdb 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -148,43 +148,53 @@ export class SomeSassServer { settings.suggestFunctionsInStringContextAfterSymbols, scss: { afterModule: - settings.scss?.completion?.afterModule || - settings.completion?.afterModule, + typeof settings.scss?.completion?.afterModule === "undefined" + ? settings.completion?.afterModule + : settings.scss?.completion?.afterModule, beforeVariable: - settings.scss?.completion?.beforeVariable || - settings.completion?.beforeVariable, + typeof settings.scss?.completion?.beforeVariable === "undefined" + ? settings.completion?.beforeVariable + : settings.scss?.completion?.beforeVariable, }, sass: { afterModule: - settings.sass?.completion?.afterModule || - settings.completion?.afterModule, + typeof settings.sass?.completion?.afterModule === "undefined" + ? settings.completion?.afterModule + : settings.sass?.completion?.afterModule, beforeVariable: - settings.sass?.completion?.beforeVariable || - settings.completion?.afterModule, + typeof settings.sass?.completion?.beforeVariable === "undefined" + ? settings.completion?.beforeVariable + : settings.sass?.completion?.beforeVariable, }, astro: { afterModule: - settings.astro?.completion?.afterModule || - settings.completion?.afterModule, + typeof settings.astro?.completion?.afterModule === "undefined" + ? settings.completion?.afterModule + : settings.astro?.completion?.afterModule, beforeVariable: - settings.astro?.completion?.beforeVariable || - settings.completion?.afterModule, + typeof settings.astro?.completion?.beforeVariable === "undefined" + ? settings.completion?.beforeVariable + : settings.astro?.completion?.beforeVariable, }, vue: { afterModule: - settings.vue?.completion?.afterModule || - settings.completion?.afterModule, + typeof settings.vue?.completion?.afterModule === "undefined" + ? settings.completion?.afterModule + : settings.vue?.completion?.afterModule, beforeVariable: - settings.vue?.completion?.beforeVariable || - settings.completion?.afterModule, + typeof settings.vue?.completion?.beforeVariable === "undefined" + ? settings.completion?.beforeVariable + : settings.vue?.completion?.beforeVariable, }, svelte: { afterModule: - settings.svelte?.completion?.afterModule || - settings.completion?.afterModule, + typeof settings.svelte?.completion?.afterModule === "undefined" + ? settings.completion?.afterModule + : settings.svelte?.completion?.afterModule, beforeVariable: - settings.svelte?.completion?.beforeVariable || - settings.completion?.afterModule, + typeof settings.svelte?.completion?.beforeVariable === "undefined" + ? settings.completion?.beforeVariable + : settings.svelte?.completion?.beforeVariable, }, }, });