diff --git a/src/compiler/factory/nodeFactory.ts b/src/compiler/factory/nodeFactory.ts index b5151fd3d6122..86380ded4403e 100644 --- a/src/compiler/factory/nodeFactory.ts +++ b/src/compiler/factory/nodeFactory.ts @@ -6148,7 +6148,7 @@ namespace ts { } let token = rawTextScanner.scan(); - if (token === SyntaxKind.CloseBracketToken) { + if (token === SyntaxKind.CloseBraceToken) { token = rawTextScanner.reScanTemplateToken(/*isTaggedTemplate*/ false); } diff --git a/src/services/refactors/convertStringOrTemplateLiteral.ts b/src/services/refactors/convertStringOrTemplateLiteral.ts index 2c09eb7d1d530..69b8f5273db0a 100644 --- a/src/services/refactors/convertStringOrTemplateLiteral.ts +++ b/src/services/refactors/convertStringOrTemplateLiteral.ts @@ -147,61 +147,85 @@ namespace ts.refactor.convertStringOrTemplateLiteral { } }; - function concatConsecutiveString(index: number, nodes: readonly Expression[]): [number, string, number[]] { + function escapeStringForTemplate(s: string) { + // Escaping for $s in strings that are to be used in template strings + // Naive implementation: replace \x by itself and otherwise $ by \$. + // But to complicate it a bit, this should work for raw strings too. + // And another bit: escape the $ in the replacement for JS's .replace(). + return s.replace(/\\.|\$/g, m => m === "$" ? "\\\$" : m); + // Finally, a less-backslash-happy version can work too, doing only ${ instead of all $s: + // s.replace(/\\.|\${/g, m => m === "${" ? "\\\${" : m); + // but `\$${foo}` is likely more clear than the more-confusing-but-still-working `$${foo}`. + } + + function getRawTextOfTemplate(node: TemplateHead | TemplateMiddle | TemplateTail) { + // in these cases the right side is ${ + const rightShaving = isTemplateHead(node) || isTemplateMiddle(node) ? -2 : -1; + return getTextOfNode(node).slice(1, rightShaving); + } + + function concatConsecutiveString(index: number, nodes: readonly Expression[]): [nextIndex: number, text: string, rawText: string, usedIndexes: number[]] { const indexes = []; - let text = ""; + let text = "", rawText = ""; while (index < nodes.length) { const node = nodes[index]; if (isStringLiteralLike(node)) { // includes isNoSubstitutionTemplateLiteral(node) - text = text + node.text; + text += escapeStringForTemplate(node.text); + rawText += escapeStringForTemplate(getTextOfNode(node).slice(1, -1)); indexes.push(index); index++; } else if (isTemplateExpression(node)) { - text = text + node.head.text; + text += node.head.text; + rawText += getRawTextOfTemplate(node.head); break; } else { break; } } - return [index, text, indexes]; + return [index, text, rawText, indexes]; } function nodesToTemplate({ nodes, operators }: { nodes: readonly Expression[], operators: Token[] }, file: SourceFile) { const copyOperatorComments = copyTrailingOperatorComments(operators, file); const copyCommentFromStringLiterals = copyCommentFromMultiNode(nodes, file, copyOperatorComments); - const [begin, headText, headIndexes] = concatConsecutiveString(0, nodes); + const [begin, headText, rawHeadText, headIndexes] = concatConsecutiveString(0, nodes); if (begin === nodes.length) { - const noSubstitutionTemplateLiteral = factory.createNoSubstitutionTemplateLiteral(headText); + const noSubstitutionTemplateLiteral = factory.createNoSubstitutionTemplateLiteral(headText, rawHeadText); copyCommentFromStringLiterals(headIndexes, noSubstitutionTemplateLiteral); return noSubstitutionTemplateLiteral; } const templateSpans: TemplateSpan[] = []; - const templateHead = factory.createTemplateHead(headText); + const templateHead = factory.createTemplateHead(headText, rawHeadText); copyCommentFromStringLiterals(headIndexes, templateHead); for (let i = begin; i < nodes.length; i++) { const currentNode = getExpressionFromParenthesesOrExpression(nodes[i]); copyOperatorComments(i, currentNode); - const [newIndex, subsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes); + const [newIndex, subsequentText, rawSubsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes); i = newIndex - 1; const isLast = i === nodes.length - 1; if (isTemplateExpression(currentNode)) { const spans = map(currentNode.templateSpans, (span, index) => { copyExpressionComments(span); - const nextSpan = currentNode.templateSpans[index + 1]; - const text = span.literal.text + (nextSpan ? "" : subsequentText); - return factory.createTemplateSpan(span.expression, isLast ? factory.createTemplateTail(text) : factory.createTemplateMiddle(text)); + const isLastSpan = index === currentNode.templateSpans.length - 1; + const text = span.literal.text + (isLastSpan ? subsequentText : ""); + const rawText = getRawTextOfTemplate(span.literal) + (isLastSpan ? rawSubsequentText : ""); + return factory.createTemplateSpan(span.expression, isLast + ? factory.createTemplateTail(text, rawText) + : factory.createTemplateMiddle(text, rawText)); }); templateSpans.push(...spans); } else { - const templatePart = isLast ? factory.createTemplateTail(subsequentText) : factory.createTemplateMiddle(subsequentText); + const templatePart = isLast + ? factory.createTemplateTail(subsequentText, rawSubsequentText) + : factory.createTemplateMiddle(subsequentText, rawSubsequentText); copyCommentFromStringLiterals(stringIndexes, templatePart); templateSpans.push(factory.createTemplateSpan(currentNode, templatePart)); } diff --git a/tests/cases/fourslash/refactorConvertStringOrTemplateLiteral_escapeSequences.ts b/tests/cases/fourslash/refactorConvertStringOrTemplateLiteral_escapeSequences.ts new file mode 100644 index 0000000000000..aacb4461f946f --- /dev/null +++ b/tests/cases/fourslash/refactorConvertStringOrTemplateLiteral_escapeSequences.ts @@ -0,0 +1,96 @@ +/// + +// @Filename: /a.ts +////let s = /*a1*/"\0\b\f\t\r\n" + text + "\n"/*a2*/; + +goTo.select("a1", "a2"); +edit.applyRefactor({ + refactorName: "Convert to template string", + actionName: "Convert to template string", + actionDescription: ts.Diagnostics.Convert_to_template_string.message, + newContent: 'let s = `\\0\\b\\f\\t\\r\\n${text}\\n`;' +}); + +// @Filename: /b.ts +////let s = /*b1*/'"' + text + "'"/*b2*/; + +goTo.select("b1", "b2"); +edit.applyRefactor({ + refactorName: "Convert to template string", + actionName: "Convert to template string", + actionDescription: ts.Diagnostics.Convert_to_template_string.message, + // newContent is: let s = `"${text}'`; + newContent: 'let s = `"${text}\'`;' +}); + +// @Filename: /c.ts +////let s = /*c1*/'$' + text + "\\"/*c2*/; + +goTo.select("c1", "c2"); +edit.applyRefactor({ + refactorName: "Convert to template string", + actionName: "Convert to template string", + actionDescription: ts.Diagnostics.Convert_to_template_string.message, + // newContent is: let s = `\$${text}\\`; + newContent: 'let s = `\\$${text}\\\\`;' +}); + +// @Filename: /d.ts +////let s = /*d1*/`$` + text + `\\`/*d2*/; + +goTo.select("d1", "d2"); +edit.applyRefactor({ + refactorName: "Convert to template string", + actionName: "Convert to template string", + actionDescription: ts.Diagnostics.Convert_to_template_string.message, + // newContent is: let s = `\$${text}\\`; + newContent: 'let s = `\\$${text}\\\\`;' +}); + +// @Filename: /e.ts +////let s = /*e1*/'${' + text + "}"/*e2*/; + +goTo.select("e1", "e2"); +edit.applyRefactor({ + refactorName: "Convert to template string", + actionName: "Convert to template string", + actionDescription: ts.Diagnostics.Convert_to_template_string.message, + // newContent is: let s = `\${${text}}`; + newContent: 'let s = `\\${${text}}`;' +}); + +// @Filename: /f.ts +////let s = /*f1*/`\${` + text + `}`/*f2*/; + +goTo.select("f1", "f2"); +edit.applyRefactor({ + refactorName: "Convert to template string", + actionName: "Convert to template string", + actionDescription: ts.Diagnostics.Convert_to_template_string.message, + // newContent is: let s = `\${${text}}`; + newContent: 'let s = `\\${${text}}`;' +}); + +// @Filename: /g.ts +////let s = /*g1*/'\\$' + text + "\\"/*g2*/; + +goTo.select("g1", "g2"); +edit.applyRefactor({ + refactorName: "Convert to template string", + actionName: "Convert to template string", + actionDescription: ts.Diagnostics.Convert_to_template_string.message, + // newContent is: let s = `\\\$${text}\\`; + newContent: 'let s = `\\\\\\$${text}\\\\`;' +}); + +// @Filename: /h.ts +////let s = /*h1*/"\u0041\u0061" + text + "\0\u0000"/*h2*/; + +goTo.select("h1", "h2"); +edit.applyRefactor({ + refactorName: "Convert to template string", + actionName: "Convert to template string", + actionDescription: ts.Diagnostics.Convert_to_template_string.message, + // newContent is: let s = `\u0041\u0061${text}\0\u0000`; + newContent: 'let s = `\\u0041\\u0061${text}\\0\\u0000`;' +});