From 6af2f8d3f2f236093c06000b265fe2b59e857600 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sat, 14 Sep 2024 13:59:36 +0200 Subject: [PATCH 1/4] fix: use textEdit for completion items The edge cases in different editors with regards to word boundaries has been too high (even for syntaxes in the same editor). --- .../__tests__/do-complete-modules.test.ts | 4 - .../src/features/do-complete.ts | 484 ++++++++---------- .../language-services/src/language-feature.ts | 3 + .../src/language-services-types.ts | 17 +- .../src/utils/test-helpers.ts | 5 +- .../test/e2e/defaults-scss/completion.test.js | 23 +- 6 files changed, 244 insertions(+), 292 deletions(-) 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..1e53e394 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,6 @@ 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, kind: CompletionItemKind.Color, label: "$primary", sortText: undefined, @@ -1127,8 +1125,6 @@ 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, 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..3d7fdc7a 100644 --- a/packages/language-services/src/features/do-complete.ts +++ b/packages/language-services/src/features/do-complete.ts @@ -1,4 +1,8 @@ -import { getNodeAtOffset } from "@somesass/vscode-css-languageservice"; +import { + getNodeAtOffset, + Range, + TextEdit, +} from "@somesass/vscode-css-languageservice"; import ColorDotJS from "colorjs.io"; import { ParseResult } from "sassdoc-parser"; import { SassBuiltInModule, sassBuiltInModules } from "../facts/sass"; @@ -67,7 +71,9 @@ const rePlaceholderDeclaration = /^\s*%/; const rePartialModuleAtRule = /@(?:use|forward|import) ["']/; type CompletionContext = { + position: Position; currentWord: string; + lineBeforePosition: string; namespace?: string; isMixinContext?: boolean; isFunctionContext?: boolean; @@ -127,14 +133,20 @@ export class DoComplete extends LanguageFeature { const isMixin = keyword === "@mixin"; if (isFunction || isMixin) { if (prevToken && prevToken.text.match(reNewSassdocBlock)) { - const node = getNodeAtOffset(stylesheet, token.offset); + // This makes heavy use of the snippet syntax if ( - node && - (node instanceof MixinDeclaration || - node instanceof FunctionDeclaration) + this.clientCapabilities.textDocument?.completion?.completionItem + ?.snippetSupport ) { - const item = this.doSassdocBlockCompletion(document, node); - result.items.push(item); + const node = getNodeAtOffset(stylesheet, token.offset); + if ( + node && + (node instanceof MixinDeclaration || + node instanceof FunctionDeclaration) + ) { + const item = this.doSassdocBlockCompletion(document, node); + result.items.push(item); + } } } } @@ -194,7 +206,7 @@ export class DoComplete extends LanguageFeature { } if (context.isPlaceholderDeclarationContext) { - const items = await this.doPlaceholderDeclarationCompletion(); + const items = await this.doPlaceholderDeclarationCompletion(context); if (items.length > 0) { result.items.push(...items); } @@ -202,7 +214,7 @@ export class DoComplete extends LanguageFeature { } if (context.isPlaceholderContext) { - const items = await this.doPlaceholderUsageCompletion(document); + const items = await this.doPlaceholderUsageCompletion(document, context); if (items.length > 0) { result.items.push(...items); } @@ -276,7 +288,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 ( @@ -298,9 +309,8 @@ export class DoComplete extends LanguageFeature { if (!context.isVariableContext) break; const items = await this.doVariableCompletion( - document, currentDocument, - currentWord, + context, symbol, isPrivate, ); @@ -313,9 +323,8 @@ export class DoComplete extends LanguageFeature { if (!context.isMixinContext) break; const items = await this.doMixinCompletion( - document, currentDocument, - currentWord, + context, symbol, isPrivate, ); @@ -328,9 +337,8 @@ export class DoComplete extends LanguageFeature { if (!context.isFunctionContext) break; const items = await this.doFunctionCompletion( - document, currentDocument, - currentWord, + context, symbol, isPrivate, ); @@ -378,48 +386,40 @@ export class DoComplete extends LanguageFeature { } const currentWord = text.substring(i + 1, offset); + const context: CompletionContext = { + position, + currentWord, + lineBeforePosition, + }; + if (rePartialModuleAtRule.test(lineBeforePosition)) { - return { - currentWord, - isImportContext: true, - }; + context.isImportContext = true; + return context; } if (reSassDoc.test(lineBeforePosition)) { - return { - currentWord, - isSassdocContext: true, - }; + context.isSassdocContext = true; + return context; } if (reComment.test(lineBeforePosition)) { - return { - currentWord, - isCommentContext: true, - }; + context.isCommentContext = true; + return context; } if (rePlaceholder.test(lineBeforePosition)) { - return { - currentWord, - isPlaceholderContext: true, - }; + context.isPlaceholderContext = true; + return context; } if (rePlaceholderDeclaration.test(lineBeforePosition)) { - return { - currentWord, - isPlaceholderDeclarationContext: true, - }; + context.isPlaceholderDeclarationContext = true; + return context; } const isInterpolation = currentWord.includes("#{") || lineBeforePosition.includes("#{"); - const context: CompletionContext = { - currentWord, - }; - if (isInterpolation) { context.isFunctionContext = true; context.isVariableContext = true; @@ -516,6 +516,7 @@ export class DoComplete extends LanguageFeature { async doPlaceholderUsageCompletion( initialDocument: TextDocument, + context: CompletionContext, ): Promise { const visited = new Set(); const items: CompletionItem[] = []; @@ -528,7 +529,11 @@ export class DoComplete extends LanguageFeature { const items: CompletionItem[] = []; for (const symbol of symbols) { if (symbol.kind === SymbolKind.Class && symbol.name.startsWith("%")) { - const item: CompletionItem = this.toCompletionItem(document, symbol); + const item: CompletionItem = this.toPlaceholderCompletionItem( + document, + context, + symbol, + ); items.push(item); } } @@ -549,7 +554,11 @@ export class DoComplete extends LanguageFeature { const symbols = this.ls.findDocumentSymbols(current); for (const symbol of symbols) { if (symbol.kind === SymbolKind.Class && symbol.name.startsWith("%")) { - const item: CompletionItem = this.toCompletionItem(current, symbol); + const item: CompletionItem = this.toPlaceholderCompletionItem( + current, + context, + symbol, + ); items.push(item); } } @@ -559,24 +568,26 @@ export class DoComplete extends LanguageFeature { return items; } - private toCompletionItem(document: TextDocument, symbol: SassDocumentSymbol) { - const filterText = symbol.name.substring(1); - + private toPlaceholderCompletionItem( + document: TextDocument, + context: CompletionContext, + symbol: SassDocumentSymbol, + ) { let documentation = symbol.name; const sassdoc = applySassDoc(symbol); if (sassdoc) { documentation += `\n____\n${sassdoc}`; } - const detail = `Placeholder declared in ${this.getFileName(document.uri)}`; + const filterText = symbol.name.substring(1); const item: CompletionItem = { detail, documentation, filterText, - insertText: filterText, - insertTextFormat: InsertTextFormat.PlainText, + textEdit: TextEdit.replace(this.getReplaceRange(context), symbol.name), kind: CompletionItemKind.Class, + insertTextFormat: InsertTextFormat.PlainText, label: symbol.name, tags: symbol.sassdoc?.deprecated ? [CompletionItemTag.Deprecated] @@ -592,7 +603,9 @@ export class DoComplete extends LanguageFeature { * * @see https://github.com/wkillerud/some-sass/issues/49 */ - async doPlaceholderDeclarationCompletion(): Promise { + async doPlaceholderDeclarationCompletion( + context: CompletionContext, + ): Promise { const items: CompletionItem[] = []; const documents = this.cache.documents(); for (const currentDocument of documents) { @@ -606,13 +619,19 @@ export class DoComplete extends LanguageFeature { for (const child of symbol.children) { if (child.kind === SymbolKind.Class && child.name.startsWith("%")) { const filterText = child.name.substring(1); - items.push({ + + const item: CompletionItem = { + textEdit: TextEdit.replace( + this.getReplaceRange(context), + child.name, + ), filterText, - insertText: filterText, insertTextFormat: InsertTextFormat.PlainText, kind: CompletionItemKind.Class, label: child.name, - }); + }; + + items.push(item); } } } @@ -720,12 +739,10 @@ export class DoComplete extends LanguageFeature { if (!context.isVariableContext) break; const vars = await this.doVariableCompletion( - document, currentDocument, - context.currentWord, + context, symbol, isPrivate, - context.namespace, prefix, ); if (vars.length > 0) { @@ -737,12 +754,10 @@ export class DoComplete extends LanguageFeature { if (!context.isMixinContext) break; const mixs = await this.doMixinCompletion( - document, currentDocument, - context.currentWord, + context, symbol, isPrivate, - context.namespace, prefix, ); if (mixs.length > 0) { @@ -754,12 +769,10 @@ export class DoComplete extends LanguageFeature { if (!context.isFunctionContext) break; const funcs = await this.doFunctionCompletion( - document, currentDocument, - context.currentWord, + context, symbol, isPrivate, - context.namespace, prefix, ); if (funcs.length > 0) { @@ -803,27 +816,23 @@ export class DoComplete extends LanguageFeature { } private async doVariableCompletion( - initialDocument: TextDocument, - currentDocument: TextDocument, - currentWord: string, + document: TextDocument, + context: CompletionContext, symbol: SassDocumentSymbol, isPrivate: boolean, - namespace = "", prefix = "", ): Promise { - // Avoid ending up with namespace.prefix-$variable - const label = `$${prefix}${asDollarlessVariable(symbol.name)}`; - const rawValue = this.getVariableValue(currentDocument, symbol); - let value = await this.findValue( - currentDocument, - symbol.selectionRange.start, - ); + const rawValue = this.getVariableValue(document, symbol); + let value = await this.findValue(document, symbol.selectionRange.start); value = value || rawValue; + const color = value ? getColorValue(value) : null; const completionKind = color ? CompletionItemKind.Color : CompletionItemKind.Variable; + // Avoid ending up with namespace.prefix-$variable + const label = `$${prefix}${asDollarlessVariable(symbol.name)}`; let documentation = color || [ @@ -836,50 +845,10 @@ export class DoComplete extends LanguageFeature { if (sassdoc) { documentation += `\n____\n${sassdoc}`; } - documentation += `\n____\nVariable declared in ${this.getFileName(currentDocument.uri)}`; + documentation += `\n____\nVariable declared in ${this.getFileName(document.uri)}`; const sortText = isPrivate ? label.replace(/^$[_]/, "") : undefined; - const dotExt = initialDocument.uri.slice( - Math.max(0, initialDocument.uri.lastIndexOf(".")), - ); - 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 item: CompletionItem = { commitCharacters: [";", ","], documentation: @@ -889,16 +858,45 @@ export class DoComplete extends LanguageFeature { kind: MarkupKind.Markdown, value: documentation, }, - filterText, kind: completionKind, label, - insertText, sortText, tags: symbol.sassdoc?.deprecated ? [CompletionItemTag.Deprecated] : [], }; + + let insertText: string = label; + if (context.namespace && context.namespace !== "*") { + insertText = `${context.namespace}.${label}`; + item.filterText = `${context.namespace}.${label}`; + } + + const range = this.getReplaceRange(context); + item.textEdit = TextEdit.replace(range, insertText); + return [item]; } + getReplaceRange(context: CompletionContext): Range { + const { position, currentWord } = context; + const start = Position.create( + position.line, + position.character - currentWord.length, + ); + + const end = Position.create( + position.line, + start.character + currentWord.length, + ); + + const interpolation = currentWord.indexOf("#{"); + if (interpolation !== -1) { + // don't replace the interpolation syntax (or what may be before it) + start.character = start.character + interpolation + 2; + } + + return Range.create(start, end); + } + private isEmbedded(initialDocument: TextDocument) { const dotExt = initialDocument.uri.slice( Math.max(0, initialDocument.uri.lastIndexOf(".")), @@ -908,46 +906,26 @@ export class DoComplete extends LanguageFeature { } private async doMixinCompletion( - initialDocument: TextDocument, - currentDocument: TextDocument, - currentWord: string, + document: TextDocument, + context: CompletionContext, symbol: SassDocumentSymbol, isPrivate: boolean, - namespace = "", prefix = "", ): Promise { const items: CompletionItem[] = []; + const snippetSupport = + this.clientCapabilities.textDocument?.completion?.completionItem + ?.snippetSupport; - const label = `${prefix}${symbol.name}`; + let label = `${prefix}${symbol.name}`; + + const { namespace } = context; const filterText = namespace ? namespace !== "*" ? `${namespace}.${prefix}${symbol.name}` : `${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 sortText = isPrivate ? label.replace(/^$[_]/, "") : undefined; const documentation = { @@ -958,106 +936,110 @@ export class DoComplete extends LanguageFeature { if (sassdoc) { documentation.value += `\n____\n${sassdoc}`; } - documentation.value += `\n____\nMixin declared in ${this.getFileName(currentDocument.uri)}`; + documentation.value += `\n____\nMixin declared in ${this.getFileName(document.uri)}`; - const getCompletionVariants = ( - insertText: string, - detail?: string, - ): CompletionItem[] => { - const variants: CompletionItem[] = []; + const base: CompletionItem = { + label, + documentation, + filterText, + sortText, + kind: CompletionItemKind.Method, + insertTextFormat: snippetSupport + ? InsertTextFormat.Snippet + : InsertTextFormat.PlainText, + tags: symbol.sassdoc?.deprecated ? [CompletionItemTag.Deprecated] : [], + }; + + let insert = label; + if (context.namespace && context.namespace !== "*") { + insert = `${context.namespace}.${label}`; + base.filterText = `${context.namespace}.${label}`; + } + + const makeCompletionVariants = (insert: string, detail?: string) => { // Not all mixins have @content, but when they do, be smart about adding brackets // and move the cursor to be ready to add said contents. // Include as separate suggestion since content may not always be needed or wanted. - if ( this.configuration.completionSettings?.suggestionStyle !== "bracket" ) { - variants.push({ - documentation, - filterText, - kind: CompletionItemKind.Method, - label, + items.push({ + ...base, labelDetails: detail ? { detail: `(${detail})` } : undefined, - insertText, - insertTextFormat: InsertTextFormat.Snippet, - sortText, - tags: symbol.sassdoc?.deprecated - ? [CompletionItemTag.Deprecated] - : [], + textEdit: TextEdit.replace(this.getReplaceRange(context), insert), }); } if ( + snippetSupport && this.configuration.completionSettings?.suggestionStyle !== "nobracket" && - currentDocument.languageId === "scss" + document.languageId === "scss" ) { - variants.push({ - documentation, - filterText, - kind: CompletionItemKind.Method, - label, + // TODO: test if this works correctly with multiline, I think so from the spec text + const insertSnippet = `${insert} {\n\t$0\n}`; + items.push({ + ...base, labelDetails: { detail: detail ? `(${detail}) { }` : " { }" }, - insertText: (insertText += " {\n\t$0\n}"), - insertTextFormat: InsertTextFormat.Snippet, - sortText, - tags: symbol.sassdoc?.deprecated - ? [CompletionItemTag.Deprecated] - : [], + textEdit: TextEdit.replace( + this.getReplaceRange(context), + insertSnippet, + ), }); } - - return variants; }; // In the case of no required parameters, skip details. - // If there are required parameters, add a suggestion with only them. - // If there are optional parameters, add a suggestion with all parameters. - if (symbol.detail) { + if (symbol.detail && snippetSupport) { const parameters = getParametersFromDetail(symbol.detail); const requiredParameters = parameters.filter((p) => !p.defaultValue); + + // If there are required parameters, add a suggestion with only them. if (requiredParameters.length > 0) { const parametersSnippet = requiredParameters .map((p, i) => mapParameterSnippet(p, i, symbol.sassdoc)) .join(", "); - const insert = insertText + `(${parametersSnippet})`; - const detail = requiredParameters .map((p) => mapParameterSignature(p)) .join(", "); - items.push(...getCompletionVariants(insert, detail)); + makeCompletionVariants(`${insert}(${parametersSnippet})`, detail); } + + // If there are optional parameters, add a suggestion with all parameters. if (requiredParameters.length !== parameters.length) { const parametersSnippet = parameters .map((p, i) => mapParameterSnippet(p, i, symbol.sassdoc)) .join(", "); - const insert = insertText + `(${parametersSnippet})`; const detail = parameters .map((p) => mapParameterSignature(p)) .join(", "); - items.push(...getCompletionVariants(insert, detail)); + makeCompletionVariants(`${insert}(${parametersSnippet})`, detail); } } else { - items.push(...getCompletionVariants(insertText)); + makeCompletionVariants(insert); } + return items; } private async doFunctionCompletion( - initialDocument: TextDocument, - currentDocument: TextDocument, - currentWord: string, + document: TextDocument, + context: CompletionContext, symbol: SassDocumentSymbol, isPrivate: boolean, - namespace = "", prefix = "", ): Promise { const items: CompletionItem[] = []; + const snippetSupport = + this.clientCapabilities.textDocument?.completion?.completionItem + ?.snippetSupport; const label = `${prefix}${symbol.name}`; + + const { namespace } = context; let filterText = symbol.name; if (namespace) { if (namespace === "*") { @@ -1067,29 +1049,6 @@ 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 sortText = isPrivate ? label.replace(/^$[_]/, "") : undefined; const documentation = { @@ -1100,7 +1059,7 @@ export class DoComplete extends LanguageFeature { if (sassdoc) { documentation.value += `\n____\n${sassdoc}`; } - documentation.value += `\n____\nFunction declared in ${this.getFileName(currentDocument.uri)}`; + documentation.value += `\n____\nFunction declared in ${this.getFileName(document.uri)}`; // If there are required parameters, add a suggestion with only them. // If there are optional parameters, add a suggestion with all parameters. @@ -1113,37 +1072,55 @@ export class DoComplete extends LanguageFeature { .map((p) => mapParameterSignature(p)) .join(", "); - const item: CompletionItem = { + const base: CompletionItem = { documentation, filterText, - kind: CompletionItemKind.Function, label, labelDetails: { detail: `(${detail})` }, - insertText: `${insertText}(${parametersSnippet})`, - insertTextFormat: InsertTextFormat.Snippet, sortText, + insertTextFormat: InsertTextFormat.Snippet, + kind: CompletionItemKind.Function, tags: symbol.sassdoc?.deprecated ? [CompletionItemTag.Deprecated] : [], }; - items.push(item); - if (requiredParameters.length !== parameters.length) { - const parametersSnippet = parameters - .map((p, i) => mapParameterSnippet(p, i, symbol.sassdoc)) - .join(", "); - const detail = parameters.map((p) => mapParameterSignature(p)).join(", "); + let insert = label; + if (context.namespace && context.namespace !== "*") { + insert = `${context.namespace}.${label}`; + base.filterText = `${context.namespace}.${label}`; + } + if (!snippetSupport) { + const insertText = `${insert}()`; const item: CompletionItem = { - documentation, - filterText, - kind: CompletionItemKind.Function, - label, - labelDetails: { detail: `(${detail})` }, - insertText: `${insertText}(${parametersSnippet})`, - insertTextFormat: InsertTextFormat.Snippet, - sortText, - tags: symbol.sassdoc?.deprecated ? [CompletionItemTag.Deprecated] : [], + ...base, + textEdit: TextEdit.replace(this.getReplaceRange(context), insertText), + insertTextFormat: InsertTextFormat.PlainText, + }; + items.push(item); + } else { + const insertText = `${insert}(${parametersSnippet})`; + const item: CompletionItem = { + ...base, + textEdit: TextEdit.replace(this.getReplaceRange(context), insertText), }; items.push(item); + + if (requiredParameters.length !== parameters.length) { + const parametersSnippet = parameters + .map((p, i) => mapParameterSnippet(p, i, symbol.sassdoc)) + .join(", "); + const detail = parameters + .map((p) => mapParameterSignature(p)) + .join(", "); + + const insertText = `${insert}(${parametersSnippet})`; + const item: CompletionItem = { + ...base, + labelDetails: { detail: `(${detail})` }, + textEdit: TextEdit.replace(this.getReplaceRange(context), insertText), + }; + items.push(item); + } } return items; @@ -1156,6 +1133,10 @@ export class DoComplete extends LanguageFeature { prefix: string = "", ): CompletionItem[] { const items: CompletionItem[] = []; + const snippetSupport = + this.clientCapabilities.textDocument?.completion?.completionItem + ?.snippetSupport; + for (const [name, docs] of Object.entries(moduleDocs.exports)) { const { description, signature, parameterSnippet, returns } = docs; const kind = signature @@ -1173,39 +1154,16 @@ 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}`; - } - - items.push({ + const base: CompletionItem = { documentation: { kind: MarkupKind.Markdown, value: `${description}\n\n[Sass documentation](${moduleDocs.reference}#${name})`, }, filterText, - insertText, - insertTextFormat: parameterSnippet - ? InsertTextFormat.Snippet - : InsertTextFormat.PlainText, + insertTextFormat: + parameterSnippet && snippetSupport + ? InsertTextFormat.Snippet + : InsertTextFormat.PlainText, kind: signature ? CompletionItemKind.Function : CompletionItemKind.Variable, @@ -1214,6 +1172,14 @@ export class DoComplete extends LanguageFeature { detail: signature && returns ? `${signature} => ${returns}` : signature, }, + }; + + const insert = `${context.namespace}.${label}${ + signature ? `(${snippetSupport ? parameterSnippet : ""})` : "" + }`; + items.push({ + ...base, + textEdit: TextEdit.replace(this.getReplaceRange(context), insert), }); } diff --git a/packages/language-services/src/language-feature.ts b/packages/language-services/src/language-feature.ts index 5f6c543b..7234faa1 100644 --- a/packages/language-services/src/language-feature.ts +++ b/packages/language-services/src/language-feature.ts @@ -20,6 +20,7 @@ import { VariableDeclaration, URI, Utils, + ClientCapabilities, } from "./language-services-types"; import { asDollarlessVariable } from "./utils/sass"; @@ -57,6 +58,7 @@ const defaultConfiguration: LanguageServiceConfiguration = { export abstract class LanguageFeature { protected ls; protected options; + protected clientCapabilities: ClientCapabilities; protected configuration: LanguageServiceConfiguration = {}; private _internal: LanguageFeatureInternal; @@ -72,6 +74,7 @@ export abstract class LanguageFeature { ) { this.ls = ls; this.options = options; + this.clientCapabilities = options.clientCapabilities; this._internal = _internal; } diff --git a/packages/language-services/src/language-services-types.ts b/packages/language-services/src/language-services-types.ts index f86d97ca..85da7f85 100644 --- a/packages/language-services/src/language-services-types.ts +++ b/packages/language-services/src/language-services-types.ts @@ -286,6 +286,8 @@ export interface ClientCapabilities { completion?: { completionItem?: { documentationFormat?: MarkupKind[]; + insertReplaceSupport?: boolean; + snippetSupport?: boolean; }; }; hover?: { @@ -294,21 +296,6 @@ export interface ClientCapabilities { }; } -export namespace ClientCapabilities { - export const LATEST: ClientCapabilities = { - textDocument: { - completion: { - completionItem: { - documentationFormat: [MarkupKind.Markdown, MarkupKind.PlainText], - }, - }, - hover: { - contentFormat: [MarkupKind.Markdown, MarkupKind.PlainText], - }, - }, - }; -} - export interface LanguageServiceOptions { clientCapabilities: ClientCapabilities; /** diff --git a/packages/language-services/src/utils/test-helpers.ts b/packages/language-services/src/utils/test-helpers.ts index 792624cb..b45671e9 100644 --- a/packages/language-services/src/utils/test-helpers.ts +++ b/packages/language-services/src/utils/test-helpers.ts @@ -126,7 +126,10 @@ export function getOptions(): LanguageServiceOptions & { clientCapabilities: { textDocument: { completion: { - completionItem: { documentationFormat: ["markdown", "plaintext"] }, + completionItem: { + snippetSupport: true, + documentationFormat: ["markdown", "plaintext"], + }, }, hover: { contentFormat: ["markdown", "plaintext"], diff --git a/vscode-extension/test/e2e/defaults-scss/completion.test.js b/vscode-extension/test/e2e/defaults-scss/completion.test.js index ce1a066f..0b6066cc 100644 --- a/vscode-extension/test/e2e/defaults-scss/completion.test.js +++ b/vscode-extension/test/e2e/defaults-scss/completion.test.js @@ -72,12 +72,11 @@ test("Offers namespaces completions including prefixes", async () => { let expectedCompletions = [ { label: "$var-var-variable", - insertText: '".$var-var-variable"', - filterText: '"ns.$var-var-variable"', + insertText: '"ns.$var-var-variable"', }, { label: "fun-fun-function", - insertText: '".fun-fun-function()"', + insertText: '"ns.fun-fun-function()"', }, ]; @@ -86,7 +85,7 @@ test("Offers namespaces completions including prefixes", async () => { expectedCompletions = [ { label: "mix-mix-mixin", - insertText: '".mix-mix-mixin"', + insertText: '"ns.mix-mix-mixin"', }, ]; @@ -116,12 +115,11 @@ test("Offers namespace completion inside string interpolation", async () => { let expectedCompletions = [ { label: "$var-var-variable", - insertText: '".$var-var-variable"', - filterText: '"ns.$var-var-variable"', + insertText: '"ns.$var-var-variable"', }, { label: "fun-fun-function", - insertText: '".fun-fun-function()"', + insertText: '"ns.fun-fun-function()"', }, ]; @@ -132,7 +130,7 @@ test("Offers completions for Sass built-ins", async () => { let expectedCompletions = [ { label: "floor", - insertText: '".floor(${1:number})"', + insertText: '"math.floor(${1:number})"', filterText: '"math.floor"', }, ]; @@ -144,12 +142,12 @@ test("Offers namespace completion inside string interpolation with preceeding no const expectedCompletions = [ { label: "$var-var-variable", - insertText: '".$var-var-variable"', + insertText: '"ns.$var-var-variable"', filterText: '"ns.$var-var-variable"', }, { label: "fun-fun-function", - insertText: '".fun-fun-function()"', + insertText: '"ns.fun-fun-function()"', }, ]; @@ -160,12 +158,11 @@ test("Offers namespace completion as part of return statement", async () => { const expectedCompletions = [ { label: "$var-var-variable", - insertText: '".$var-var-variable"', - filterText: '"ns.$var-var-variable"', + insertText: '"ns.$var-var-variable"', }, { label: "fun-fun-function", - insertText: '".fun-fun-function()"', + insertText: '"ns.fun-fun-function()"', }, ]; From 82255f03d024fd43cc3faf10e722e326dbca4f11 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 17 Sep 2024 19:42:40 +0200 Subject: [PATCH 2/4] test: update test assertions with textEdit --- .../__tests__/do-complete-embedded.test.ts | 16 +- .../__tests__/do-complete-modules.test.ts | 343 ++++++++++++++---- .../do-complete-placeholders.test.ts | 14 +- .../__tests__/do-complete-sassdoc.test.ts | 15 +- .../test/web/suite/completion.test.js | 20 +- 5 files changed, 315 insertions(+), 93 deletions(-) diff --git a/packages/language-services/src/features/__tests__/do-complete-embedded.test.ts b/packages/language-services/src/features/__tests__/do-complete-embedded.test.ts index d66834d9..2a2c70ca 100644 --- a/packages/language-services/src/features/__tests__/do-complete-embedded.test.ts +++ b/packages/language-services/src/features/__tests__/do-complete-embedded.test.ts @@ -96,7 +96,7 @@ test("should suggest symbol from a different document via @use", async () => { '", ], @@ -124,11 +124,23 @@ test("should suggest symbol from a different document via @use", async () => { commitCharacters: [";", ","], documentation: "limegreen\n____\nVariable declared in one.scss", filterText: "ns.$primary", - insertText: "$primary", kind: CompletionItemKind.Color, label: "$primary", sortText: undefined, tags: [], + textEdit: { + newText: "ns.$primary", + range: { + end: { + character: 11, + line: 9, + }, + start: { + character: 8, + line: 9, + }, + }, + }, }, ); }); 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 1e53e394..0dffe5b3 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 @@ -41,13 +41,25 @@ test("suggests built-in sass modules", async () => { "The value of the mathematical constant **π**.\n\n[Sass documentation](https://sass-lang.com/documentation/modules/math#$pi)", }, filterText: "math.$pi", - insertText: ".$pi", insertTextFormat: InsertTextFormat.PlainText, kind: CompletionItemKind.Variable, label: "$pi", labelDetails: { detail: undefined, }, + textEdit: { + newText: "math.$pi", + range: { + end: { + character: 11, + line: 1, + }, + start: { + character: 6, + line: 1, + }, + }, + }, }, ); }); @@ -82,13 +94,25 @@ test("suggest sass built-ins that are forwarded by the stylesheet that is @used" "The value of the mathematical constant **π**.\n\n[Sass documentation](https://sass-lang.com/documentation/modules/math#$pi)", }, filterText: "test.$pi", - insertText: ".$pi", insertTextFormat: InsertTextFormat.PlainText, kind: CompletionItemKind.Variable, label: "$pi", labelDetails: { detail: undefined, }, + textEdit: { + newText: "test.$pi", + range: { + end: { + character: 11, + line: 1, + }, + start: { + character: 6, + line: 1, + }, + }, + }, }, ); }); @@ -102,7 +126,7 @@ test("suggest sass built-ins that are forwarded with a prefix", async () => { ); const two = fileSystemProvider.createDocument([ '@use "./test";', - "$var: test.", + "$var: test.;", ]); // emulate scanner of language service which adds workspace documents to the cache @@ -126,13 +150,25 @@ test("suggest sass built-ins that are forwarded with a prefix", async () => { "The value of the mathematical constant **π**.\n\n[Sass documentation](https://sass-lang.com/documentation/modules/math#$pi)", }, filterText: "test.$math-pi", - insertText: ".$math-pi", insertTextFormat: InsertTextFormat.PlainText, kind: CompletionItemKind.Variable, label: "$math-pi", labelDetails: { detail: undefined, }, + textEdit: { + newText: "test.$math-pi", + range: { + end: { + character: 11, + line: 1, + }, + start: { + character: 6, + line: 1, + }, + }, + }, }, ); assert.ok(items.find((a) => a.label === "math-clamp")); @@ -165,11 +201,23 @@ test("should suggest symbol from a different document via @use", async () => { commitCharacters: [";", ","], documentation: "limegreen\n____\nVariable declared in one.scss", filterText: "one.$primary", - insertText: ".$primary", kind: CompletionItemKind.Color, label: "$primary", sortText: undefined, tags: [], + textEdit: { + newText: "one.$primary", + range: { + end: { + character: 16, + line: 1, + }, + start: { + character: 12, + line: 1, + }, + }, + }, }, ); }); @@ -208,11 +256,23 @@ test("should suggest symbols from the document we use when it also forwards anot commitCharacters: [";", ","], documentation: "red\n____\nVariable declared in two.scss", filterText: "two.$secondary", - insertText: ".$secondary", kind: CompletionItemKind.Color, label: "$secondary", sortText: undefined, tags: [], + textEdit: { + newText: "two.$secondary", + range: { + end: { + character: 16, + line: 1, + }, + start: { + character: 12, + line: 1, + }, + }, + }, }, ); assert.deepStrictEqual( @@ -221,11 +281,23 @@ test("should suggest symbols from the document we use when it also forwards anot commitCharacters: [";", ","], documentation: "limegreen\n____\nVariable declared in one.scss", filterText: "two.$primary", - insertText: ".$primary", kind: CompletionItemKind.Color, label: "$primary", sortText: undefined, tags: [], + textEdit: { + newText: "two.$primary", + range: { + end: { + character: 16, + line: 1, + }, + start: { + character: 12, + line: 1, + }, + }, + }, }, ); }); @@ -459,7 +531,7 @@ test("should suggest prefixed symbol from a different document via @use and @for uri: "two.scss", }); const three = fileSystemProvider.createDocument( - ['@use "./two";', ".a { color: two."], + ['@use "./two";', ".a { color: two.; }"], { uri: "three.scss", }, @@ -482,11 +554,23 @@ test("should suggest prefixed symbol from a different document via @use and @for commitCharacters: [";", ","], documentation: "limegreen\n____\nVariable declared in one.scss", filterText: "two.$foo-primary", - insertText: ".$foo-primary", kind: CompletionItemKind.Color, label: "$foo-primary", sortText: undefined, tags: [], + textEdit: { + newText: "two.$foo-primary", + range: { + end: { + character: 16, + line: 1, + }, + start: { + character: 12, + line: 1, + }, + }, + }, }, ); }); @@ -634,13 +718,25 @@ test("should suggest mixin with no parameter", async () => { "```scss\n@mixin primary()\n```\n____\nMixin declared in one.scss", }, filterText: "one.primary", - insertText: ".primary", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Method, label: "primary", labelDetails: undefined, sortText: undefined, tags: [], + textEdit: { + newText: "one.primary", + range: { + end: { + character: 18, + line: 1, + }, + start: { + character: 14, + line: 1, + }, + }, + }, }, ); }); @@ -649,6 +745,7 @@ test("should suggest mixin with optional parameter", async () => { ls.configure({ completionSettings: { suggestFromUseOnly: true, + suggestionStyle: "nobracket", }, }); @@ -676,28 +773,25 @@ test("should suggest mixin with optional parameter", async () => { "```scss\n@mixin primary($color: red)\n```\n____\nMixin declared in one.scss", }, filterText: "one.primary", - insertText: ".primary(${1:color})", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Method, label: "primary", labelDetails: { detail: "($color: red)" }, sortText: undefined, tags: [], - }, - { - documentation: { - kind: "markdown", - value: - "```scss\n@mixin primary($color: red)\n```\n____\nMixin declared in one.scss", + textEdit: { + newText: "one.primary(${1:color})", + range: { + end: { + character: 18, + line: 1, + }, + start: { + character: 14, + line: 1, + }, + }, }, - filterText: "one.primary", - insertText: ".primary(${1:color}) {\n\t$0\n}", - insertTextFormat: InsertTextFormat.Snippet, - kind: CompletionItemKind.Method, - label: "primary", - labelDetails: { detail: "($color: red) { }" }, - sortText: undefined, - tags: [], }, ], ); @@ -707,6 +801,7 @@ test("should suggest mixin with required parameter", async () => { ls.configure({ completionSettings: { suggestFromUseOnly: true, + suggestionStyle: "bracket", }, }); @@ -734,28 +829,25 @@ test("should suggest mixin with required parameter", async () => { "```scss\n@mixin primary($color)\n```\n____\nMixin declared in one.scss", }, filterText: "one.primary", - insertText: ".primary(${1:color})", - insertTextFormat: InsertTextFormat.Snippet, - kind: CompletionItemKind.Method, - label: "primary", - labelDetails: { detail: "($color)" }, - sortText: undefined, - tags: [], - }, - { - documentation: { - kind: "markdown", - value: - "```scss\n@mixin primary($color)\n```\n____\nMixin declared in one.scss", - }, - filterText: "one.primary", - insertText: ".primary(${1:color}) {\n\t$0\n}", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Method, label: "primary", labelDetails: { detail: "($color) { }" }, sortText: undefined, tags: [], + textEdit: { + newText: "one.primary(${1:color}) {\n\t$0\n}", + range: { + end: { + character: 18, + line: 1, + }, + start: { + character: 14, + line: 1, + }, + }, + }, }, ], ); @@ -765,6 +857,7 @@ test("given both required and optional parameters should suggest two variants of ls.configure({ completionSettings: { suggestFromUseOnly: true, + suggestionStyle: "nobracket", }, }); @@ -776,7 +869,7 @@ test("given both required and optional parameters should suggest two variants of ); const two = fileSystemProvider.createDocument([ '@use "./one";', - ".a { @include one.", + ".a { @include one.; }", ]); // emulate scanner of language service which adds workspace documents to the cache @@ -794,28 +887,25 @@ test("given both required and optional parameters should suggest two variants of "```scss\n@mixin primary($background, $color: red)\n```\n____\nMixin declared in one.scss", }, filterText: "one.primary", - insertText: ".primary(${1:background})", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Method, label: "primary", labelDetails: { detail: "($background)" }, sortText: undefined, tags: [], - }, - { - documentation: { - kind: "markdown", - value: - "```scss\n@mixin primary($background, $color: red)\n```\n____\nMixin declared in one.scss", + textEdit: { + newText: "one.primary(${1:background})", + range: { + end: { + character: 18, + line: 1, + }, + start: { + character: 14, + line: 1, + }, + }, }, - filterText: "one.primary", - insertText: ".primary(${1:background}) {\n\t$0\n}", - insertTextFormat: InsertTextFormat.Snippet, - kind: CompletionItemKind.Method, - label: "primary", - labelDetails: { detail: "($background) { }" }, - sortText: undefined, - tags: [], }, { documentation: { @@ -824,28 +914,25 @@ test("given both required and optional parameters should suggest two variants of "```scss\n@mixin primary($background, $color: red)\n```\n____\nMixin declared in one.scss", }, filterText: "one.primary", - insertText: ".primary(${1:background}, ${2:color})", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Method, label: "primary", labelDetails: { detail: "($background, $color: red)" }, sortText: undefined, tags: [], - }, - { - documentation: { - kind: "markdown", - value: - "```scss\n@mixin primary($background, $color: red)\n```\n____\nMixin declared in one.scss", + textEdit: { + newText: "one.primary(${1:background}, ${2:color})", + range: { + end: { + character: 18, + line: 1, + }, + start: { + character: 14, + line: 1, + }, + }, }, - filterText: "one.primary", - insertText: ".primary(${1:background}, ${2:color}) {\n\t$0\n}", - insertTextFormat: InsertTextFormat.Snippet, - kind: CompletionItemKind.Method, - label: "primary", - labelDetails: { detail: "($background, $color: red) { }" }, - sortText: undefined, - tags: [], }, ], ); @@ -881,13 +968,25 @@ test("should suggest function with no parameter", async () => { "```scss\n@function primary()\n```\n____\nFunction declared in one.scss", }, filterText: "one.primary", - insertText: ".primary()", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Function, label: "primary", labelDetails: { detail: "()" }, sortText: undefined, tags: [], + textEdit: { + newText: "one.primary()", + range: { + end: { + character: 18, + line: 1, + }, + start: { + character: 14, + line: 1, + }, + }, + }, }, ); }); @@ -922,13 +1021,25 @@ test("should suggest function with optional parameter", async () => { "```scss\n@function primary($color: red)\n```\n____\nFunction declared in one.scss", }, filterText: "one.primary", - insertText: ".primary()", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Function, label: "primary", labelDetails: { detail: "()" }, sortText: undefined, tags: [], + textEdit: { + newText: "one.primary()", + range: { + end: { + character: 18, + line: 1, + }, + start: { + character: 14, + line: 1, + }, + }, + }, }, ); }); @@ -963,13 +1074,25 @@ test("should suggest function with required parameter", async () => { "```scss\n@function primary($color)\n```\n____\nFunction declared in one.scss", }, filterText: "one.primary", - insertText: ".primary(${1:color})", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Function, label: "primary", labelDetails: { detail: "($color)" }, sortText: undefined, tags: [], + textEdit: { + newText: "one.primary(${1:color})", + range: { + end: { + character: 18, + line: 1, + }, + start: { + character: 14, + line: 1, + }, + }, + }, }, ); }); @@ -1005,13 +1128,25 @@ test("given both required and optional parameters should suggest two variants of "```scss\n@function primary($a, $b: 1)\n```\n____\nFunction declared in one.scss", }, filterText: "one.primary", - insertText: ".primary(${1:a})", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Function, label: "primary", labelDetails: { detail: "($a)" }, sortText: undefined, tags: [], + textEdit: { + newText: "one.primary(${1:a})", + range: { + end: { + character: 18, + line: 1, + }, + start: { + character: 14, + line: 1, + }, + }, + }, }, { documentation: { @@ -1020,13 +1155,25 @@ test("given both required and optional parameters should suggest two variants of "```scss\n@function primary($a, $b: 1)\n```\n____\nFunction declared in one.scss", }, filterText: "one.primary", - insertText: ".primary(${1:a}, ${2:b})", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Function, label: "primary", labelDetails: { detail: "($a, $b: 1)" }, sortText: undefined, tags: [], + textEdit: { + newText: "one.primary(${1:a}, ${2:b})", + range: { + end: { + character: 18, + line: 1, + }, + start: { + character: 14, + line: 1, + }, + }, + }, }, ], ); @@ -1064,6 +1211,19 @@ test("should suggest all symbols as legacy @import may be in use", async () => { label: "$primary", sortText: undefined, tags: [], + textEdit: { + newText: "$primary", + range: { + end: { + character: 12, + line: 0, + }, + start: { + character: 12, + line: 0, + }, + }, + }, }, ); }); @@ -1108,7 +1268,7 @@ test("should suggest symbol from a different document via @use with wildcard ali }, ); const two = fileSystemProvider.createDocument( - ['@use "./one" as *;', ".a { color: "], + ['@use "./one" as *;', ".a { color: }"], { uri: "two.scss", }, @@ -1129,6 +1289,19 @@ test("should suggest symbol from a different document via @use with wildcard ali label: "$primary", sortText: undefined, tags: [], + textEdit: { + newText: "$primary", + range: { + end: { + character: 12, + line: 1, + }, + start: { + character: 12, + line: 1, + }, + }, + }, }, ); assert.deepStrictEqual( @@ -1140,7 +1313,6 @@ test("should suggest symbol from a different document via @use with wildcard ali "```scss\n@function one()\n```\n____\nFunction declared in one.scss", }, filterText: "one", - insertText: "one()", insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Function, label: "one", @@ -1149,6 +1321,19 @@ test("should suggest symbol from a different document via @use with wildcard ali }, sortText: undefined, tags: [], + textEdit: { + newText: "one()", + range: { + end: { + character: 12, + line: 1, + }, + start: { + character: 12, + line: 1, + }, + }, + }, }, ); }); diff --git a/packages/language-services/src/features/__tests__/do-complete-placeholders.test.ts b/packages/language-services/src/features/__tests__/do-complete-placeholders.test.ts index fa9da064..0e79025e 100644 --- a/packages/language-services/src/features/__tests__/do-complete-placeholders.test.ts +++ b/packages/language-services/src/features/__tests__/do-complete-placeholders.test.ts @@ -29,9 +29,21 @@ test("when declaring a placeholder selector, suggest placeholders that have an @ const { items } = await ls.doComplete(two, Position.create(0, 1)); assert.deepStrictEqual(items[0], { filterText: "main", - insertText: "main", insertTextFormat: InsertTextFormat.PlainText, kind: CompletionItemKind.Class, label: "%main", + textEdit: { + newText: "%main", + range: { + end: { + character: 1, + line: 0, + }, + start: { + character: 0, + line: 0, + }, + }, + }, }); }); diff --git a/packages/language-services/src/features/__tests__/do-complete-sassdoc.test.ts b/packages/language-services/src/features/__tests__/do-complete-sassdoc.test.ts index 571066c3..cc44c5dc 100644 --- a/packages/language-services/src/features/__tests__/do-complete-sassdoc.test.ts +++ b/packages/language-services/src/features/__tests__/do-complete-sassdoc.test.ts @@ -285,7 +285,7 @@ ____ Function declared in timing.scss`, }, filterText: "t.timing", - insertText: '.timing(${1|"sonic","link","homer","snorlax"|})', + insertTextFormat: 2, kind: 3, label: "timing", @@ -294,6 +294,19 @@ Function declared in timing.scss`, }, sortText: undefined, tags: [], + textEdit: { + newText: 't.timing(${1|"sonic","link","homer","snorlax"|})', + range: { + end: { + character: 28, + line: 2, + }, + start: { + character: 22, + line: 2, + }, + }, + }, }, ], }); diff --git a/vscode-extension/test/web/suite/completion.test.js b/vscode-extension/test/web/suite/completion.test.js index bb7480e2..c3e8f59b 100644 --- a/vscode-extension/test/web/suite/completion.test.js +++ b/vscode-extension/test/web/suite/completion.test.js @@ -19,12 +19,12 @@ test("for namespaces including prefixes", async () => { let expectedCompletions = [ { label: "$var-var-variable", - insertText: '".$var-var-variable"', + insertText: '"ns.$var-var-variable"', filterText: '"ns.$var-var-variable"', }, { label: "fun-fun-function", - insertText: '".fun-fun-function()"', + insertText: '"ns.fun-fun-function()"', }, ]; @@ -33,7 +33,7 @@ test("for namespaces including prefixes", async () => { expectedCompletions = [ { label: "mix-mix-mixin", - insertText: '".mix-mix-mixin"', + insertText: '"ns.mix-mix-mixin"', }, ]; @@ -44,12 +44,12 @@ test("inside string interpolation", async () => { const expectedCompletions = [ { label: "$var-var-variable", - insertText: '".$var-var-variable"', + insertText: '"ns.$var-var-variable"', filterText: '"ns.$var-var-variable"', }, { label: "fun-fun-function", - insertText: '".fun-fun-function()"', + insertText: '"ns.fun-fun-function()"', }, ]; @@ -60,7 +60,7 @@ test("for Sass built-ins", async () => { const expectedCompletions = [ { label: "floor", - insertText: '".floor(${1:number})"', + insertText: '"math.floor(${1:number})"', filterText: '"math.floor"', }, ]; @@ -72,12 +72,12 @@ test("inside string interpolation with preceeding non-space character", async () const expectedCompletions = [ { label: "$var-var-variable", - insertText: '".$var-var-variable"', + insertText: '"ns.$var-var-variable"', filterText: '"ns.$var-var-variable"', }, { label: "fun-fun-function", - insertText: '".fun-fun-function()"', + insertText: '"ns.fun-fun-function()"', }, ]; @@ -88,12 +88,12 @@ test("as part of return statement", async () => { const expectedCompletions = [ { label: "$var-var-variable", - insertText: '".$var-var-variable"', + insertText: '"ns.$var-var-variable"', filterText: '"ns.$var-var-variable"', }, { label: "fun-fun-function", - insertText: '".fun-fun-function()"', + insertText: '"ns.fun-fun-function()"', }, ]; From bb68ba65d2d7129ef1174614d50ead9b81da8a87 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 17 Sep 2024 20:20:07 +0200 Subject: [PATCH 3/4] fix: skip upstream diagnostics for svelte --- packages/language-services/src/features/do-diagnostics.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/language-services/src/features/do-diagnostics.ts b/packages/language-services/src/features/do-diagnostics.ts index abf2b5ba..b7ea6edb 100644 --- a/packages/language-services/src/features/do-diagnostics.ts +++ b/packages/language-services/src/features/do-diagnostics.ts @@ -66,9 +66,9 @@ export class DoDiagnostics extends LanguageFeature { private async doUpstreamDiagnostics(document: TextDocument) { if ( - document.languageId === "vue" || - document.languageId === "astro" || - document.languageId === "svelte" + document.uri.endsWith(".vue") || + document.uri.endsWith(".astro") || + document.uri.endsWith(".svelte") ) { return []; } From 1e57d399c2186e930ef466d2055f54916215e13a Mon Sep 17 00:00:00 2001 From: William Killerud Date: Tue, 17 Sep 2024 20:32:01 +0200 Subject: [PATCH 4/4] docs: remove now obsolete settings from docs --- .../src/language-server/configure-a-client.md | 23 ---------------- docs/src/language-server/helix.md | 2 +- packages/language-server/README.md | 4 +-- packages/language-server/src/server.ts | 2 -- packages/language-server/src/settings.ts | 4 --- .../language-services/src/language-feature.ts | 2 -- .../src/language-services-types.ts | 27 ------------------- 7 files changed, 2 insertions(+), 62 deletions(-) diff --git a/docs/src/language-server/configure-a-client.md b/docs/src/language-server/configure-a-client.md index 9e399e50..32e4aa8a 100644 --- a/docs/src/language-server/configure-a-client.md +++ b/docs/src/language-server/configure-a-client.md @@ -24,29 +24,6 @@ For example, while we may document `"somesass.loadPaths": []` (and write it this } ``` -### Server-only settings - -In addition to [the user settings](../user-guide/settings.md), language clients may want to configure these server-only settings to tweak how certain features interact with your specific editor. - -| Key | Description | -| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `somesass.completion.afterModule` | Set this to the empty string if you end up with `module..$variable` after accepting a code suggestion item. If `module.` or `module` disappears, you can set it to `"{module}."` or `"{module}"` respectively. That is a "magic string" that will be replaced with the actual module name. | -| `somesass.completion.beforeVariable` | Set this to the empty string if you end up with `$$variable` after accepting a code suggestion item. | - -For example: - -```json -{ - "settings": { - "somesass": { - "completion": { - "afterModule": "{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/helix.md b/docs/src/language-server/helix.md index 2c498046..306218c3 100644 --- a/docs/src/language-server/helix.md +++ b/docs/src/language-server/helix.md @@ -8,7 +8,7 @@ You can configure new language servers in [`.config/helix/languages.toml`](https [language-server.some-sass-language-server] command = "some-sass-language-server" args = ["--stdio"] -config = { somesass = { completion = { afterModule = "", beforeVariable = "" } } } +config = { somesass = { loadPaths = [] } } [[language]] name = "scss" diff --git a/packages/language-server/README.md b/packages/language-server/README.md index 1c369d55..81ca4928 100644 --- a/packages/language-server/README.md +++ b/packages/language-server/README.md @@ -35,9 +35,7 @@ some-sass-language-server --stdio ### Workspace configuration -The language server requests [settings](../user-guide/settings.md) via the [`workspace/configuration` message](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration), on the `somesass` key. All fields are optional. - -See the [documentation for the available settings](https://wkillerud.github.io/some-sass/user-guide/settings.html). +See [how to configure a client](https://wkillerud.github.io/some-sass/language-server/configure-a-client.html) and the [documentation for the available settings](https://wkillerud.github.io/some-sass/user-guide/settings.html). ## Capabilities diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 34ea2d0a..79b755b9 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -146,8 +146,6 @@ export class SomeSassServer { suggestionStyle: settings.suggestionStyle, suggestFunctionsInStringContextAfterSymbols: settings.suggestFunctionsInStringContextAfterSymbols, - afterModule: settings.completion?.afterModule, - beforeVariable: settings.completion?.beforeVariable, }, }); } diff --git a/packages/language-server/src/settings.ts b/packages/language-server/src/settings.ts index f57fa3dc..acf0d3f3 100644 --- a/packages/language-server/src/settings.ts +++ b/packages/language-server/src/settings.ts @@ -8,10 +8,6 @@ export interface ISettings { readonly suggestFromUseOnly: boolean; readonly suggestFunctionsInStringContextAfterSymbols: " (+-*%"; readonly triggerPropertyValueCompletion: boolean; - readonly completion?: { - afterModule?: string; - beforeVariable?: string; - }; } export interface IEditorSettings { diff --git a/packages/language-services/src/language-feature.ts b/packages/language-services/src/language-feature.ts index 7234faa1..a9187e4e 100644 --- a/packages/language-services/src/language-feature.ts +++ b/packages/language-services/src/language-feature.ts @@ -46,8 +46,6 @@ const defaultConfiguration: LanguageServiceConfiguration = { suggestFunctionsInStringContextAfterSymbols: " (+-*%", suggestionStyle: "all", triggerPropertyValueCompletion: true, - afterModule: ".", - beforeVariable: "$", }, }; diff --git a/packages/language-services/src/language-services-types.ts b/packages/language-services/src/language-services-types.ts index 85da7f85..c1cc3a2b 100644 --- a/packages/language-services/src/language-services-types.ts +++ b/packages/language-services/src/language-services-types.ts @@ -225,33 +225,6 @@ export interface LanguageServiceConfiguration { */ 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; }; editorSettings?: EditorSettings; workspaceRoot?: URI;