Skip to content

Commit 5c927fb

Browse files
aparzithePunderWoman
authored andcommitted
fix(vscode-extension): provide folding ranges for templates with interpolated strings
Inline templates written as template literals with `${...}` substitutions are `TemplateExpression` nodes, not string literals, so they were not recognized when building the virtual HTML document. The HTML language service received a blank document and produced no folding ranges, breaking element folding for the whole template. Match template expressions in addition to string-literal-like nodes and blank out the `${...}` substitutions (preserving line breaks) so offsets in the virtual document stay aligned and the expression contents are not parsed as HTML. Fixes #65508 (cherry picked from commit 0ec19b5)
1 parent d0f7229 commit 5c927fb

3 files changed

Lines changed: 75 additions & 1 deletion

File tree

‎vscode-ng-language-service/integration/lsp/ivy_spec.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,39 @@ export class AppComponent {
251251
expect(response).toContain({startLine: 7, endLine: 8});
252252
});
253253

254+
it('provides folding ranges for inline templates with interpolated strings', async () => {
255+
openTextDocument(
256+
client,
257+
APP_COMPONENT,
258+
`
259+
import {Component, EventEmitter, Input, Output} from '@angular/core';
260+
261+
const suffix = '!';
262+
263+
@Component({
264+
selector: 'my-app',
265+
template: \`
266+
<div>
267+
<span>
268+
Hello \${suffix}
269+
</span>
270+
</div>\`,
271+
})
272+
export class AppComponent {
273+
name = 'Angular';
274+
}`,
275+
);
276+
const response = (await client.sendRequest(lsp.FoldingRangeRequest.type, {
277+
textDocument: {
278+
uri: APP_COMPONENT_URI,
279+
},
280+
})) as lsp.FoldingRange[];
281+
expect(Array.isArray(response)).toBe(true);
282+
expect(response.length).toEqual(2);
283+
expect(response).toContain({startLine: 8, endLine: 11});
284+
expect(response).toContain({startLine: 9, endLine: 10});
285+
});
286+
254287
it('provides folding ranges for control flow', async () => {
255288
openTextDocument(
256289
client,

‎vscode-ng-language-service/server/src/embedded_support.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ export function getHTMLVirtualContent(sf: ts.SourceFile): string {
3838
content.slice(0, region.getStart(sf) + 1) +
3939
documentText.slice(region.getStart(sf) + 1, region.getEnd() - 1) +
4040
content.slice(region.getEnd() - 1);
41+
42+
// Template literals may contain `${...}` substitutions, which are not part of the template
43+
// text. Blank them out (preserving line breaks) so the HTML language service only sees the
44+
// static parts of the template.
45+
if (ts.isTemplateExpression(region)) {
46+
let substitutionStart = region.head.end - 2; // position of the `${`
47+
for (const span of region.templateSpans) {
48+
const substitutionEnd = span.literal.getStart(sf) + 1; // position after the `}`
49+
content =
50+
content.slice(0, substitutionStart) +
51+
documentText.slice(substitutionStart, substitutionEnd).replace(/[^\n]/g, ' ') +
52+
content.slice(substitutionEnd);
53+
substitutionStart = span.literal.end - 2;
54+
}
55+
}
4156
}
4257
return content;
4358
}
@@ -95,7 +110,12 @@ function isAssignmentToPropertyWithName(node: ts.Node, propertyName: 'styles' |
95110
}
96111

97112
function isInlineTemplateNode(node: ts.Node) {
98-
return ts.isStringLiteralLike(node) ? isAssignmentToPropertyWithName(node, 'template') : false;
113+
// Unlike styles, templates are also matched when written as a template literal with
114+
// `${...}` substitutions. The substitutions themselves are blanked out when the virtual
115+
// document is constructed.
116+
return ts.isStringLiteralLike(node) || ts.isTemplateExpression(node)
117+
? isAssignmentToPropertyWithName(node, 'template')
118+
: false;
99119
}
100120

101121
/**

‎vscode-ng-language-service/server/src/tests/embedded_support_spec.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,27 @@ describe('server embedded support', () => {
3939
);
4040
});
4141

42+
it('handles template literals with substitutions', () => {
43+
assertEmbeddedHTMLContent(
44+
'@Component({template: `<p>${abc}</p>`}) export class MyCmp',
45+
' <p> </p> ',
46+
);
47+
});
48+
49+
it('handles template literals with multiple substitutions', () => {
50+
assertEmbeddedHTMLContent(
51+
'@Component({template: `<p>${a}b${c}</p>`})',
52+
' <p> b </p> ',
53+
);
54+
});
55+
56+
it('preserves line breaks inside template literal substitutions', () => {
57+
assertEmbeddedHTMLContent(
58+
'@Component({template: `<p>${a +\nb}</p>`})',
59+
' <p> \n </p> ',
60+
);
61+
});
62+
4263
it('works as expected for CRLF', () => {
4364
assertEmbeddedHTMLContent(
4465
`@Component({template: 'abc123'})\r\nexport class MyComponent {}`,

0 commit comments

Comments
 (0)