From 69ee9fc0690e120171465145b7cf8d2454b7a056 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 12 Oct 2021 13:58:22 -0700 Subject: [PATCH] support escaping in snippets --- src/compiler/emitter.ts | 105 +++++++++++++----- src/compiler/types.ts | 1 + src/services/completions.ts | 3 +- .../fourslash/completionsOverridingMethod2.ts | 38 +++++++ 4 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 tests/cases/fourslash/completionsOverridingMethod2.ts diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index cde27013087..b0e4d9d08bf 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -867,6 +867,27 @@ namespace ts { const newLine = getNewLineCharacter(printerOptions); const moduleKind = getEmitModuleKind(printerOptions); const bundledHelpers = new Map(); + const hasSnippet = !!printerOptions.hasSnippet; + + // Writers that must handle snippet text escaping, if enabled + let writeLiteral = noEscapeWriteLiteral; + let writeStringLiteral = noEscapeWriteStringLiteral; + let writeBase = noEscapeWriteBase; + let writeSymbol = noEscapeWriteSymbol; + let writeParameter = noEscapeWriteParameter; + let writeComment = noEscapeWriteComment; + let writeProperty = noEscapeWriteProperty; + + if (hasSnippet) { + writeLiteral = escapeWriteLiteral; + writeStringLiteral = escapeWriteStringLiteral; + writeBase = escapeWriteBase; + writeSymbol = escapeWriteSymbol; + writeParameter = escapeWriteParameter; + writeComment = escapeWriteComment; + writeProperty = escapeWriteProperty; + } + let currentSourceFile: SourceFile | undefined; let nodeIdToGeneratedName: string[]; // Map of generated names for specific nodes. @@ -911,9 +932,6 @@ namespace ts { const parenthesizer = factory.parenthesizer; const emitBinaryExpression = createEmitBinaryExpression(); - // Snippets - let inSnippet = false; - reset(); return { // public API @@ -1286,14 +1304,16 @@ namespace ts { currentParenthesizerRule = undefined; } - // >> TODO: remove allowSnippets - function pipelineEmitWithHintWorker(hint: EmitHint, node: Node, _allowSnippets = true): void { - if (!inSnippet) { + function pipelineEmitWithHintWorker(hint: EmitHint, node: Node, allowSnippets = true): void { + if (allowSnippets) { const snippet = getSnippetElement(node); if (snippet) { return emitSnippetNode(hint, node, snippet); } } + // else { + // Debug.assert(!getSnippetElement(node), "A snippet cannot exist inside another snippet."); + // } if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile)); if (hint === EmitHint.IdentifierName) return emitIdentifier(cast(node, isIdentifier)); if (hint === EmitHint.JsxAttributeValue) return emitLiteral(cast(node, isStringLiteral), /*jsxAttributeEscape*/ true); @@ -1881,19 +1901,14 @@ namespace ts { const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape, jsxAttributeEscape); if ((printerOptions.sourceMap || printerOptions.inlineSourceMap) && (node.kind === SyntaxKind.StringLiteral || isTemplateLiteralKind(node.kind))) { - writeLiteral(inSnippet ? escapeSnippetText(text) : text); + writeLiteral(text); } else { // Quick info expects all literals to be called with writeStringLiteral, as there's no specific type for numberLiterals - writeStringLiteral(inSnippet ? escapeSnippetText(text) : text); + writeStringLiteral(text); } } - // TODO: move this - function escapeSnippetText(text: string): string { - return text.replace(/\$/gm, "\\$"); - } - // SyntaxKind.UnparsedSource // SyntaxKind.UnparsedPrepend function emitUnparsedSourceOrPrepend(unparsed: UnparsedSource | UnparsedPrepend) { @@ -1944,7 +1959,6 @@ namespace ts { // function emitSnippetNode(hint: EmitHint, node: Node, snippet: SnippetElement) { - inSnippet = true; switch (snippet.kind) { case SnippetKind.Placeholder: emitPlaceholder(hint, node, snippet); @@ -1953,18 +1967,17 @@ namespace ts { emitTabStop(snippet); break; } - inSnippet = false; } function emitPlaceholder(hint: EmitHint, node: Node, snippet: Placeholder) { - write(`\$\{${snippet.order}:`); // `${2:` + noEscapeWrite(`\$\{${snippet.order}:`); // `${2:` pipelineEmitWithHintWorker(hint, node, /*allowSnippets*/ false); // `...` - write(`\}`); // `}` + noEscapeWrite(`\}`); // `}` // `${2:...}` } function emitTabStop(snippet: TabStop) { - write(`\$${snippet.order}`); + noEscapeWrite(`\$${snippet.order}`); } // @@ -1973,8 +1986,7 @@ namespace ts { function emitIdentifier(node: Identifier) { const writeText = node.symbol ? writeSymbol : write; - const text = getTextOfNode(node, /*includeTrivia*/ false); - writeText(inSnippet ? escapeSnippetText(text) : text, node.symbol); + writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol); emitList(node, node.typeArguments, ListFormat.TypeParameters); // Call emitList directly since it could be an array of TypeParameterDeclarations _or_ type arguments } @@ -1984,8 +1996,7 @@ namespace ts { function emitPrivateIdentifier(node: PrivateIdentifier) { const writeText = node.symbol ? writeSymbol : write; - const text = getTextOfNode(node, /*includeTrivia*/ false); - writeText(inSnippet ? escapeSnippetText(text) : text, node.symbol); + writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol); } @@ -4454,22 +4465,38 @@ namespace ts { // Writers - function writeLiteral(s: string) { + function noEscapeWriteLiteral(s: string) { writer.writeLiteral(s); } - function writeStringLiteral(s: string) { + function escapeWriteLiteral(s: string) { + writer.writeLiteral(escapeSnippetText(s)); + } + + function noEscapeWriteStringLiteral(s: string) { writer.writeStringLiteral(s); } - function writeBase(s: string) { + function escapeWriteStringLiteral(s: string) { + writer.writeStringLiteral(escapeSnippetText(s)); + } + + function noEscapeWriteBase(s: string) { writer.write(s); } - function writeSymbol(s: string, sym: Symbol) { + function escapeWriteBase(s: string) { + writer.write(escapeSnippetText(s)); + } + + function noEscapeWriteSymbol(s: string, sym: Symbol) { writer.writeSymbol(s, sym); } + function escapeWriteSymbol(s: string, sym: Symbol) { + writer.writeSymbol(escapeSnippetText(s), sym); + } + function writePunctuation(s: string) { writer.writePunctuation(s); } @@ -4486,28 +4513,48 @@ namespace ts { writer.writeOperator(s); } - function writeParameter(s: string) { + function noEscapeWriteParameter(s: string) { writer.writeParameter(s); } - function writeComment(s: string) { + function escapeWriteParameter(s: string) { + writer.writeParameter(escapeSnippetText(s)); + } + + function noEscapeWriteComment(s: string) { writer.writeComment(s); } + function escapeWriteComment(s: string) { + writer.writeComment(escapeSnippetText(s)); + } + function writeSpace() { writer.writeSpace(" "); } - function writeProperty(s: string) { + function noEscapeWriteProperty(s: string) { writer.writeProperty(s); } + function escapeWriteProperty(s: string) { + writer.writeProperty(escapeSnippetText(s)); + } + function writeLine(count = 1) { for (let i = 0; i < count; i++) { writer.writeLine(i > 0); } } + function noEscapeWrite(s: string) { // >> update + writer.write(s); + } + + function escapeSnippetText(text: string): string { + return text.replace(/\$/gm, "\\$"); + } + function increaseIndent() { writer.increaseIndent(); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8c488241f98..c29cc3645bf 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -8201,6 +8201,7 @@ namespace ts { /*@internal*/ preserveSourceNewlines?: boolean; /*@internal*/ terminateUnterminatedLiterals?: boolean; /*@internal*/ relativeToBuildInfo?: (path: string) => string; + /*@internal*/ hasSnippet?: boolean; } /* @internal */ diff --git a/src/services/completions.ts b/src/services/completions.ts index a1c3b1cae47..2d69bc30ae7 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -807,6 +807,7 @@ namespace ts.Completions { module: options.module, target: options.target, omitTrailingSemicolon: true, + hasSnippet: true, }); const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host); @@ -908,7 +909,7 @@ namespace ts.Completions { } } - // TODO: move to snippets file? + // TODO: move to services/utilities? function escapeSnippetText(text: string): string { return text.replace(/\$/gm, "\\$"); } diff --git a/tests/cases/fourslash/completionsOverridingMethod2.ts b/tests/cases/fourslash/completionsOverridingMethod2.ts new file mode 100644 index 00000000000..5f833a2ffa5 --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingMethod2.ts @@ -0,0 +1,38 @@ +/// + +// @Filename: a.ts +// Case: Snippet text needs escaping +////interface DollarSign { +//// "$usd"(a: number): number; +////} +////class USD implements DollarSign { +//// /*a*/ +////} + +// format.setFormatOptions({ +// newLineCharacter: "\n", +// }); +// format.setOption("newline", "\n"); + +verify.completions({ + marker: "a", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: true, + }, + includes: [ + { + name: "$usd", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + isSnippet: true, + insertText: +"$usd(a: number): number {\r\n $0\r\n}\r\n", + } + ], +}); \ No newline at end of file