diff --git a/packages/language-services/src/features/do-complete.ts b/packages/language-services/src/features/do-complete.ts index 6270b828..8bda7286 100644 --- a/packages/language-services/src/features/do-complete.ts +++ b/packages/language-services/src/features/do-complete.ts @@ -321,7 +321,7 @@ export class DoComplete extends LanguageFeature { const items = await this.doMixinCompletion( document, currentDocument, - currentWord, + context, symbol, isPrivate, ); @@ -779,10 +779,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) { @@ -989,44 +988,25 @@ 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 snippetSupport = + this.clientCapabilities.textDocument?.completion?.completionItem + ?.snippetSupport; const 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 = { @@ -1039,90 +1019,207 @@ export class DoComplete extends LanguageFeature { } documentation.value += `\n____\nMixin declared in ${this.getFileName(currentDocument.uri)}`; - const getCompletionVariants = ( - insertText: string, - detail?: string, - ): CompletionItem[] => { - const variants: CompletionItem[] = []; - // 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.clientCapabilities.textDocument?.completion?.completionItem + ?.insertReplaceSupport + ) { + const base: CompletionItem = { + label, + documentation, + filterText, + sortText, + kind: CompletionItemKind.Method, + insertTextFormat: snippetSupport + ? InsertTextFormat.Snippet + : InsertTextFormat.PlainText, + tags: symbol.sassdoc?.deprecated ? [CompletionItemTag.Deprecated] : [], + }; + + let detail: string | undefined; + let insert = label; - if ( - this.configuration.completionSettings?.suggestionStyle !== "bracket" - ) { - variants.push({ - documentation, - filterText, - kind: CompletionItemKind.Method, - label, - labelDetails: detail ? { detail: `(${detail})` } : undefined, - insertText, - insertTextFormat: InsertTextFormat.Snippet, - sortText, - tags: symbol.sassdoc?.deprecated - ? [CompletionItemTag.Deprecated] - : [], - }); + const makeCompletionVariants = () => { + // 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" + ) { + items.push({ + ...base, + labelDetails: detail ? { detail: `(${detail})` } : undefined, + textEdit: TextEdit.replace( + this.getInsertRange(context, insert), + insert, + ), + }); + } + + if ( + snippetSupport && + this.configuration.completionSettings?.suggestionStyle !== + "nobracket" && + currentDocument.languageId === "scss" + ) { + // TODO: test if this works correctly with multiline + const insertSnippet = `${insert} {\n\t$0\n}`; + items.push({ + ...base, + labelDetails: { detail: detail ? `(${detail}) { }` : " { }" }, + textEdit: TextEdit.replace( + this.getInsertRange(context, insertSnippet), + insertSnippet, + ), + }); + } + }; + + // In the case of no required parameters, skip details. + 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(", "); + insert = label + `(${parametersSnippet})`; + + detail = requiredParameters + .map((p) => mapParameterSignature(p)) + .join(", "); + } + + // 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(", "); + insert = label + `(${parametersSnippet})`; + detail = parameters.map((p) => mapParameterSignature(p)).join(", "); + + makeCompletionVariants(); + } + } else { + makeCompletionVariants(); } + return items; + } else { + // This method is kept for backwards compatibility. + // It might be removed or simplified in a major version, placing + // the responsibility for correctly treating the word boundary + // on the editor. + // If editors are having trouble with the completion items made + // by this code, they should update to support completion items + // with text edits added in 3.16 of the LSP spec. + + 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 ( - this.configuration.completionSettings?.suggestionStyle !== - "nobracket" && - currentDocument.languageId === "scss" + namespace && + namespace !== "*" && + this.configuration.completionSettings?.afterModule && + this.configuration.completionSettings.afterModule.startsWith("{module}") ) { - variants.push({ + insertText = `${namespace}${insertText}`; + } + + const getCompletionVariants = ( + insertText: string, + detail?: string, + ): CompletionItem[] => { + const variants: CompletionItem[] = []; + const base: CompletionItem = { documentation, filterText, kind: CompletionItemKind.Method, label, - labelDetails: { detail: detail ? `(${detail}) { }` : " { }" }, - insertText: (insertText += " {\n\t$0\n}"), - insertTextFormat: InsertTextFormat.Snippet, + insertTextFormat: snippetSupport + ? InsertTextFormat.Snippet + : InsertTextFormat.PlainText, sortText, tags: symbol.sassdoc?.deprecated ? [CompletionItemTag.Deprecated] : [], - }); - } + }; - return variants; - }; + // 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({ + ...base, + labelDetails: detail ? { detail: `(${detail})` } : undefined, + insertText, + }); + } - // 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) { - const parameters = getParametersFromDetail(symbol.detail); - const requiredParameters = parameters.filter((p) => !p.defaultValue); - 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)); - } - if (requiredParameters.length !== parameters.length) { - const parametersSnippet = parameters - .map((p, i) => mapParameterSnippet(p, i, symbol.sassdoc)) - .join(", "); - const insert = insertText + `(${parametersSnippet})`; + if ( + snippetSupport && + this.configuration.completionSettings?.suggestionStyle !== + "nobracket" && + currentDocument.languageId === "scss" + ) { + variants.push({ + ...base, + labelDetails: { detail: detail ? `(${detail}) { }` : " { }" }, + insertText: (insertText += " {\n\t$0\n}"), + }); + } + + return variants; + }; - const detail = parameters - .map((p) => mapParameterSignature(p)) - .join(", "); + // 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 && snippetSupport) { + const parameters = getParametersFromDetail(symbol.detail); + const requiredParameters = parameters.filter((p) => !p.defaultValue); + 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)); + } + 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)); + items.push(...getCompletionVariants(insert, detail)); + } + } else { + items.push(...getCompletionVariants(insertText)); } - } else { - items.push(...getCompletionVariants(insertText)); + return items; } - return items; } private async doFunctionCompletion(