Skip to content

Commit

Permalink
Format completion snippet text before escaping (#48793)
Browse files Browse the repository at this point in the history
* Format snippet text before escaping

* Reset `escapes` before printing so printer can be reused
  • Loading branch information
andrewbranch authored Apr 21, 2022
1 parent ab2523b commit 7abdb9e
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 10 deletions.
50 changes: 41 additions & 9 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1199,31 +1199,59 @@ 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 {
printSnippetList,
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<Node>,
sourceFile: SourceFile | undefined,
): string {
const unescaped = printUnescapedSnippetList(format, list, sourceFile);
return escapes ? textChanges.applyChanges(unescaped, escapes) : unescaped;
}

function printUnescapedSnippetList(
format: ListFormat,
list: NodeArray<Node>,
sourceFile: SourceFile | undefined,
): string {
escapes = undefined;
writer.clear();
printer.writeList(format, list, sourceFile, writer);
return writer.getText();
Expand All @@ -1236,7 +1264,7 @@ namespace ts.Completions {
formatContext: formatting.FormatContext,
): string {
const syntheticFile = {
text: printSnippetList(
text: printUnescapedSnippetList(
format,
list,
sourceFile),
Expand All @@ -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);
}
}

Expand Down
23 changes: 22 additions & 1 deletion tests/cases/fourslash/completionsOverridingMethod2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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*/
Expand All @@ -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}"
},
],
});

0 comments on commit 7abdb9e

Please sign in to comment.