Skip to content

Commit dd89a49

Browse files
committed
copy comments from string to template literal
1 parent 17f3861 commit dd89a49

8 files changed

+143
-25
lines changed

src/services/refactors/convertStringOrTemplateLiteral.ts

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,24 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
6464

6565
function getEditsForToTemplateLiteral(context: RefactorContext, node: Node) {
6666
const maybeBinary = getParentBinaryExpression(node);
67-
const arrayOfNodes = transformTreeToArray(maybeBinary);
68-
const templateLiteral = nodesToTemplate(arrayOfNodes);
69-
return textChanges.ChangeTracker.with(context, t => t.replaceNode(context.file, maybeBinary, templateLiteral));
67+
const file = context.file;
68+
69+
const templateLiteral = nodesToTemplate(treeToArray(maybeBinary), file);
70+
const trailingCommentRanges = getTrailingCommentRanges(file.text, maybeBinary.end);
71+
72+
if (trailingCommentRanges) {
73+
const lastComment = trailingCommentRanges[trailingCommentRanges.length - 1];
74+
const trailingRange = { pos: trailingCommentRanges[0].pos, end: lastComment.end };
75+
76+
return textChanges.ChangeTracker.with(context, t => {
77+
t.deleteRange(file, trailingRange);
78+
t.replaceNode(file, maybeBinary, templateLiteral);
79+
});
80+
}
81+
else {
82+
return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, maybeBinary, templateLiteral));
83+
}
84+
7085
}
7186

7287
const templateSpanToExpressions = (file: SourceFile) => (templateSpan: TemplateSpan): Expression[] => {
@@ -132,59 +147,97 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
132147
return containsString && areOperatorsValid;
133148
}
134149

135-
function transformTreeToArray(node: Node): ReadonlyArray<Expression> {
136-
return treeToArray(node).nodes;
137-
}
138-
139-
function treeToArray(node: Node): { nodes: ReadonlyArray<Expression>, containsString: boolean, areOperatorsValid: boolean} {
150+
function treeToArray(node: Node): { nodes: ReadonlyArray<Expression>, operators: Token<BinaryOperator>[], containsString: boolean, areOperatorsValid: boolean} {
140151
if (isBinaryExpression(node)) {
141-
const { nodes: leftNodes, containsString: leftHasString, areOperatorsValid: leftOperatorValid } = treeToArray(node.left);
152+
const { nodes: leftNodes, operators: leftOperator, containsString: leftHasString, areOperatorsValid: leftOperatorValid } = treeToArray(node.left);
142153
const { nodes: rightNodes, containsString: rightHasString, areOperatorsValid: rightOperatorValid } = treeToArray(node.right);
143154

144155
if (!leftHasString && !rightHasString) {
145-
return { nodes: [node], containsString: false, areOperatorsValid: true };
156+
return { nodes: [node], operators: [], containsString: false, areOperatorsValid: true };
146157
}
147158

148159
const currentOperatorValid = node.operatorToken.kind === SyntaxKind.PlusToken;
149160
const areOperatorsValid = leftOperatorValid && currentOperatorValid && rightOperatorValid;
161+
leftOperator.push(node.operatorToken);
150162

151-
return { nodes: leftNodes.concat(rightNodes), containsString: true, areOperatorsValid };
163+
return { nodes: leftNodes.concat(rightNodes), operators: leftOperator, containsString: true, areOperatorsValid };
152164
}
153165

154-
return { nodes: [node as Expression], containsString: isStringLiteral(node), areOperatorsValid: true };
166+
return { nodes: [node as Expression], operators: [], containsString: isStringLiteral(node), areOperatorsValid: true };
155167
}
156168

157-
function concatConsecutiveString(index: number, nodes: ReadonlyArray<Expression>): [number, string] {
169+
const copyTrailingOperatorComments = (operators: Token<BinaryOperator>[], file: SourceFile) => (index: number, targetNode: Node) => {
170+
if (index < operators.length) {
171+
copyTrailingComments(operators[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false);
172+
}
173+
};
174+
175+
const copyCommentFromMultiNode = (nodes: ReadonlyArray<Expression>, file: SourceFile, copyOperatorComments: (index: number, targetNode: Node) => void) =>
176+
(indexes: number[], targetNode: Node) => {
177+
while (indexes.length > 0) {
178+
const index = indexes.shift()!;
179+
copyTrailingComments(nodes[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false);
180+
copyOperatorComments(index, targetNode);
181+
}
182+
};
183+
184+
function concatConsecutiveString(index: number, nodes: ReadonlyArray<Expression>): [number, string, number[]] {
158185
let text = "";
186+
const indexes = [];
159187

160188
while (index < nodes.length && isStringLiteral(nodes[index])) {
161189
text = text + decodeRawString(nodes[index].getText());
190+
indexes.push(index);
162191
index++;
163192
}
164193

165194
text = escapeText(text);
166-
return [index, text];
195+
return [index, text, indexes];
167196
}
168197

169-
function nodesToTemplate(nodes: ReadonlyArray<Expression>) {
198+
function nodesToTemplate({nodes, operators}: {nodes: ReadonlyArray<Expression>, operators: Token<BinaryOperator>[]}, file: SourceFile) {
199+
const copyOperatorComments = copyTrailingOperatorComments(operators, file);
200+
const copyCommentFromStringLiterals = copyCommentFromMultiNode(nodes, file, copyOperatorComments);
201+
170202
const templateSpans: TemplateSpan[] = [];
171-
const [begin, headText] = concatConsecutiveString(0, nodes);
172-
const templateHead = createTemplateHead(headText);
203+
const [begin, headText, headIndexes] = concatConsecutiveString(0, nodes);
173204

174-
if (begin === nodes.length) return createNoSubstitutionTemplateLiteral(headText);
205+
if (begin === nodes.length) {
206+
const noSubstitutionTemplateLiteral = createNoSubstitutionTemplateLiteral(headText);
207+
copyCommentFromStringLiterals(headIndexes, noSubstitutionTemplateLiteral);
208+
return noSubstitutionTemplateLiteral;
209+
}
210+
211+
const templateHead = createTemplateHead(headText);
212+
copyCommentFromStringLiterals(headIndexes, templateHead);
175213

176214
for (let i = begin; i < nodes.length; i++) {
177-
const expression = isParenthesizedExpression(nodes[i]) ? (nodes[i] as ParenthesizedExpression).expression : nodes[i];
178-
const [newIndex, subsequentText] = concatConsecutiveString(i + 1, nodes);
215+
let currentNode = nodes[i];
216+
217+
if (isParenthesizedExpression(currentNode)) {
218+
copyCommentsWhenParenthesized(currentNode);
219+
currentNode = currentNode.expression;
220+
}
221+
222+
copyOperatorComments(i, currentNode);
223+
const [newIndex, subsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes);
179224
i = newIndex - 1;
180225

181226
const templatePart = i === nodes.length - 1 ? createTemplateTail(subsequentText) : createTemplateMiddle(subsequentText);
182-
templateSpans.push(createTemplateSpan(expression, templatePart));
227+
copyCommentFromStringLiterals(stringIndexes, templatePart);
228+
229+
templateSpans.push(createTemplateSpan(currentNode, templatePart));
183230
}
184231

185232
return createTemplateExpression(templateHead, templateSpans);
186233
}
187234

235+
function copyCommentsWhenParenthesized(node: ParenthesizedExpression) {
236+
const file = node.getSourceFile();
237+
copyTrailingComments(node, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false);
238+
copyTrailingAsLeadingComments(node.expression, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false);
239+
}
240+
188241
const hexToUnicode = (_match: string, grp: string) => String.fromCharCode(parseInt(grp, 16));
189242
const octalToUnicode = (_match: string, grp: string) => String.fromCharCode(parseInt(grp, 8));
190243

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/// <reference path='fourslash.ts' />
22

3-
//// const foo = `/*x*/H/*y*/EAD ${ /* C0 */ 42 /* C1 */} Span1 ${ /* C2 */ 43 /* C3 */} Span2 ${ /* C4 */ 44 /* C5 */} Span3`
3+
//// const foo = /* H */ `/*x*/H/*y*/EAD ${ /* C0 */ 42 /* C1 */} Span1 ${ /* C2 */ 43 /* C3 */} Span2 ${ /* C4 */ 44 /* C5 */} Span3` /* T */
44

55
goTo.select("x", "y");
66
edit.applyRefactor({
77
refactorName: "Convert string concatenation or template literal",
88
actionName: "Convert to string concatenation",
99
actionDescription: "Convert to string concatenation",
1010
newContent:
11-
`const foo = "HEAD " + /* C0 */ 42 /* C1 */ + " Span1 " + /* C2 */ 43 /* C3 */ + " Span2 " + /* C4 */ 44 /* C5 */ + " Span3"`,
11+
`const foo = /* H */ "HEAD " + /* C0 */ 42 /* C1 */ + " Span1 " + /* C2 */ 43 /* C3 */ + " Span2 " + /* C4 */ 44 /* C5 */ + " Span3" /* T */`,
1212
});
1313

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/// <reference path='fourslash.ts' />
22

3-
//// const foo = /* C0 */ /*x*/"/*y*/foobar" /* C1 */ + " is" /* C2 */ + 42 /* C3 */ + "years old" /* C4 */
3+
//// const foo = /* C0 */ /*x*/"/*y*/foo" /* C1 */ + " is" /* C2 */ + 42 /* C3 */ + " and bar" /* C4 */ + " is" /* C5 */ + 52/* C6 */
44

55
goTo.select("x", "y");
66
edit.applyRefactor({
77
refactorName: "Convert string concatenation or template literal",
88
actionName: "Convert to template literal",
99
actionDescription: "Convert to template literal",
1010
newContent:
11-
`const foo = /* C0 */ \`foobar is \${ /* C1 */ /* C2 */ 42 /* C3 */}years old\` /* C4 */`,
11+
"const foo = /* C0 */ `foo is\${ /* C1 */ /* C2 */42 /* C3 */} and bar is\${ /* C4 */ /* C5 */52 /* C6 */}`",
1212
});
1313

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const foo = /* C0 */ /*x*/"/*y*/foo" + /* C1 */ " is" + /* C2 */ 42 + /* C3 */ " and bar" + /* C4 */ " is" + /* C5 */ 52/* C6 */
4+
5+
goTo.select("x", "y");
6+
edit.applyRefactor({
7+
refactorName: "Convert string concatenation or template literal",
8+
actionName: "Convert to template literal",
9+
actionDescription: "Convert to template literal",
10+
newContent:
11+
"const foo = /* C0 */ `foo is\${ /* C1 */ /* C2 */42 /* C3 */} and bar is\${ /* C4 */ /* C5 */52 /* C6 */}`",
12+
});
13+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const foo = /* C0 */ /*x*/"/*y*/foo" /* C1 */ + " is"/* C2 */ /* C3 */
4+
5+
goTo.select("x", "y");
6+
edit.applyRefactor({
7+
refactorName: "Convert string concatenation or template literal",
8+
actionName: "Convert to template literal",
9+
actionDescription: "Convert to template literal",
10+
newContent:
11+
"const foo = /* C0 */ `foo is` /* C1 */ /* C2 */ /* C3 */",
12+
});
13+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const foo = /*x*/"/*y*/foobar is" + ( /* C1 */ 42 ) /* C2 */ + /* C3 */ " years old"
4+
5+
goTo.select("x", "y");
6+
edit.applyRefactor({
7+
refactorName: "Convert string concatenation or template literal",
8+
actionName: "Convert to template literal",
9+
actionDescription: "Convert to template literal",
10+
newContent:
11+
"const foo = `foobar is\${/* C1 */ 42 /* C2 */ /* C3 */} years old`",
12+
});
13+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const foo = /* C0 */ /*x*/"/*y*/foo" /* C1 */ + " is" /* C2 */ + 42/* C3 */
4+
5+
goTo.select("x", "y");
6+
edit.applyRefactor({
7+
refactorName: "Convert string concatenation or template literal",
8+
actionName: "Convert to template literal",
9+
actionDescription: "Convert to template literal",
10+
newContent:
11+
"const foo = /* C0 */ `foo is\${ /* C1 */ /* C2 */42 /* C3 */}`",
12+
});
13+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const foo = /* C0 */ /*x*/"/*y*/foo" /* C1 */ + " is" /* C2 */ + 42 + " years old"/* C3 */
4+
5+
goTo.select("x", "y");
6+
edit.applyRefactor({
7+
refactorName: "Convert string concatenation or template literal",
8+
actionName: "Convert to template literal",
9+
actionDescription: "Convert to template literal",
10+
newContent:
11+
"const foo = /* C0 */ `foo is\${ /* C1 */ /* C2 */42} years old` /* C3 */",
12+
});
13+

0 commit comments

Comments
 (0)