diff --git a/src/services/completions.ts b/src/services/completions.ts index 1f1007f2f6150..42821f5dcccb5 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1199,18 +1199,19 @@ namespace ts.Completions { function createSnippetPrinter( printerOptions: PrinterOptions, ) { + let escapes: TextChange[] | undefined; const baseWriter = textChanges.createWriter(getNewLineCharacter(printerOptions)); const printer = createPrinter(printerOptions, baseWriter); const writer: EmitTextWriter = { ...baseWriter, - write: s => baseWriter.write(escapeSnippetText(s)), + write: s => escapingWrite(s, () => baseWriter.write(s)), nonEscapingWrite: baseWriter.write, - writeLiteral: s => baseWriter.writeLiteral(escapeSnippetText(s)), - writeStringLiteral: s => baseWriter.writeStringLiteral(escapeSnippetText(s)), - writeSymbol: (s, symbol) => baseWriter.writeSymbol(escapeSnippetText(s), symbol), - writeParameter: s => baseWriter.writeParameter(escapeSnippetText(s)), - writeComment: s => baseWriter.writeComment(escapeSnippetText(s)), - writeProperty: s => baseWriter.writeProperty(escapeSnippetText(s)), + writeLiteral: s => escapingWrite(s, () => baseWriter.writeLiteral(s)), + writeStringLiteral: s => escapingWrite(s, () => baseWriter.writeStringLiteral(s)), + writeSymbol: (s, symbol) => escapingWrite(s, () => baseWriter.writeSymbol(s, symbol)), + writeParameter: s => escapingWrite(s, () => baseWriter.writeParameter(s)), + writeComment: s => escapingWrite(s, () => baseWriter.writeComment(s)), + writeProperty: s => escapingWrite(s, () => baseWriter.writeProperty(s)), }; return { @@ -1218,12 +1219,39 @@ namespace ts.Completions { printAndFormatSnippetList, }; + // The formatter/scanner will have issues with snippet-escaped text, + // so instead of writing the escaped text directly to the writer, + // generate a set of changes that can be applied to the unescaped text + // to escape it post-formatting. + function escapingWrite(s: string, write: () => void) { + const escaped = escapeSnippetText(s); + if (escaped !== s) { + const start = baseWriter.getTextPos(); + write(); + const end = baseWriter.getTextPos(); + escapes = append(escapes ||= [], { newText: escaped, span: { start, length: end - start } }); + } + else { + write(); + } + } + /* Snippet-escaping version of `printer.printList`. */ function printSnippetList( format: ListFormat, list: NodeArray, sourceFile: SourceFile | undefined, ): string { + const unescaped = printUnescapedSnippetList(format, list, sourceFile); + return escapes ? textChanges.applyChanges(unescaped, escapes) : unescaped; + } + + function printUnescapedSnippetList( + format: ListFormat, + list: NodeArray, + sourceFile: SourceFile | undefined, + ): string { + escapes = undefined; writer.clear(); printer.writeList(format, list, sourceFile, writer); return writer.getText(); @@ -1236,7 +1264,7 @@ namespace ts.Completions { formatContext: formatting.FormatContext, ): string { const syntheticFile = { - text: printSnippetList( + text: printUnescapedSnippetList( format, list, sourceFile), @@ -1256,7 +1284,11 @@ namespace ts.Completions { /* delta */ 0, { ...formatContext, options: formatOptions }); }); - return textChanges.applyChanges(syntheticFile.text, changes); + + const allChanges = escapes + ? stableSort(concatenate(changes, escapes), (a, b) => compareTextSpans(a.span, b.span)) + : changes; + return textChanges.applyChanges(syntheticFile.text, allChanges); } } diff --git a/tests/cases/fourslash/completionsOverridingMethod2.ts b/tests/cases/fourslash/completionsOverridingMethod2.ts index 4abdbfa2707c3..4fc36685405ed 100644 --- a/tests/cases/fourslash/completionsOverridingMethod2.ts +++ b/tests/cases/fourslash/completionsOverridingMethod2.ts @@ -5,6 +5,9 @@ // Case: Snippet text needs escaping ////interface DollarSign { //// "$usd"(a: number): number; +//// $cad(b: number): number; +//// cla$$y(c: number): number; +//// isDollarAmountString(s: string): s is `$${number}` ////} ////class USD implements DollarSign { //// /*a*/ @@ -25,6 +28,24 @@ verify.completions({ sortText: completion.SortText.ClassMemberSnippets, isSnippet: true, insertText: "\"\\$usd\"(a: number): number {\n $0\n}", - } + }, + { + name: "$cad", + sortText: completion.SortText.ClassMemberSnippets, + isSnippet: true, + insertText: "\\$cad(b: number): number {\n $0\n}", + }, + { + name: "cla$$y", + sortText: completion.SortText.ClassMemberSnippets, + isSnippet: true, + insertText: "cla\\$\\$y(c: number): number {\n $0\n}", + }, + { + name: "isDollarAmountString", + sortText: completion.SortText.ClassMemberSnippets, + isSnippet: true, + insertText: "isDollarAmountString(s: string): s is `\\$\\${number}` {\n $0\n}" + }, ], }); \ No newline at end of file