8000 feat(language-service): add semantic tokens for templates (#60260) · angular/angular@20c1f99 · GitHub
[go: up one dir, main page]

Skip to content

Commit 20c1f99

Browse files
JannikLassahnthePunderWoman
authored andcommitted
feat(language-service): add semantic tokens for templates (#60260)
Adds support for `getEncodedSemanticClassifications` to the language service. The service now classifies components in a template as the `class` type. PR Close #60260
1 parent 54f3571 commit 20c1f99

File tree

6 files changed

+714
-6
lines changed

6 files changed

+714
-6
lines changed

packages/compiler-cli/src/ngtsc/perf/src/api.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,6 @@ export enum PerfPhase {
154154
*/
155155
OutliningSpans,
156156

157-
/**
158-
* Tracks the number of `PerfPhase`s, and must appear at the end of the list.
159-
*/
160-
LAST,
161-
162157
/**
163158
* Time spent by the Angular Language Service calculating code fixes.
164159
*/
@@ -178,6 +173,16 @@ export enum PerfPhase {
178173
* Time spent computing changes for applying a given refactoring.
179174
*/
180175
LSApplyRefactoring,
176+
177+
/**
178+
* Time spent by the Angular Language Service calculating semantic classifications.
179+
*/
180+
LSSemanticClassification,
181+
182+
/**
183+
* Tracks the number of `PerfPhase`s, and must appear at the end of the list.
184+
*/
185+
LAST,
181186
}
182187

183188
/**

packages/language-service/src/language_service.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ import {
4747
getParentClassDeclaration,
4848
getPropertyAssignmentFromValue,
4949
} from './utils/ts_utils';
50-
import {getTypeCheckInfoAtPosition, isTypeScriptFile} from './utils';
50+
import {getTypeCheckInfoAtPosition, isTypeScriptFile, TypeCheckInfo} from './utils';
5151
import {ActiveRefactoring, allRefactorings} from './refactorings/refactoring';
52+
import {getClassificationsForTemplate} from './semantic_tokens';
53+
import {isExternalResource} from '@angular/compiler-cli/src/ngtsc/metadata';
5254

5355
type LanguageServiceConfig = Omit<PluginConfig, 'angularOnly'>;
5456

@@ -290,6 +292,77 @@ export class LanguageService {
290292
);
291293
}
292294

295+
getEncodedSemanticClassifications(
296+
fileName: string,
297+
span: ts.TextSpan,
298+
format: ts.SemanticClassificationFormat | undefined,
299+
): ts.Classifications {
300+
return this.withCompilerAndPerfTracing(PerfPhase.LSSemanticClassification, (compiler) => {
301+
return this.getEncodedSemanticClassificationsImpl(fileName, span, format, compiler);
302+
});
303+
}
304+
305+
private getEncodedSemanticClassificationsImpl(
306+
fileName: string,
307+
span: ts.TextSpan,
308+
format: ts.SemanticClassificationFormat | undefined,
309+
compiler: NgCompiler,
310+
): ts.Classifications {
311+
if (format == ts.SemanticClassificationFormat.Original) {
312+
return {spans: [], endOfLineState: ts.EndOfLineState.None};
313+
}
314+
315+
if (isTypeScriptFile(fileName)) {
316+
const sf = compiler.getCurrentProgram().getSourceFile(fileName);
317+
if (sf === undefined) {
318+
return {spans: [], endOfLineState: ts.EndOfLineState.None};
319+
}
320+
321+
const classDeclarations: ts.ClassDeclaration[] = [];
322+
sf.forEachChild((node) => {
323+
if (ts.isClassDeclaration(node)) {
324+
classDeclarations.push(node);
325+
}
326+
});
327+
328+
const hasInlineTemplate = (classDecl: ts.ClassDeclaration) => {
329+
const resources = compiler.getDirectiveResources(classDecl);
330+
return resources && resources.template && !isExternalResource(resources.template);
331+
};
332+
333+
const typeCheckInfos: TypeCheckInfo[] = [];
334+
const templateChecker = compiler.getTemplateTypeChecker();
335+
336+
for (const classDecl of classDeclarations) {
337+
if (!hasInlineTemplate(classDecl)) {
338+
continue;
339+
}
340+
const template = templateChecker.getTemplate(classDecl);
341+
if (template !== null) {
342+
typeCheckInfos.push({
343+
nodes: template,
344+
declaration: classDecl,
345+
});
346+
}
347+
}
348+
349+
const spans = [];
350+
for (const templInfo of typeCheckInfos) {
351+
const classifications = getClassificationsForTemplate(compiler, templInfo, span);
352+
spans.push(...classifications.spans);
353+
}
354+
355+
return {spans, endOfLineState: ts.EndOfLineState.None};
356+
} else {
357+
const typeCheckInfo = getTypeCheckInfoAtPosition(fileName, span.start, compiler);
358+
if (typeCheckInfo === undefined) {
359+
return {spans: [], endOfLineState: ts.EndOfLineState.None};
360+
}
361+
362+
return getClassificationsForTemplate(compiler, typeCheckInfo, span);
363+
}
364+
}
365+
293366
getCompletionsAtPosition(
294367
fileName: string,
295368
position: number,
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
TmplAstElement,
11+
TmplAstNode,
12+
TmplAstTemplate,
13+
TmplAstVisitor,
14+
TmplAstBoundAttribute,
15+
TmplAstBoundEvent,
16+
TmplAstBoundText,
17+
TmplAstContent,
18+
TmplAstDeferredBlock,
19+
TmplAstDeferredBlockError,
20+
TmplAstDeferredBlockLoading,
21+
TmplAstDeferredBlockPlaceholder,
22+
TmplAstDeferredTrigger,
23+
TmplAstForLoopBlock,
24+
TmplAstForLoopBlockEmpty,
25+
TmplAstIcu,
26+
TmplAstIfBlock,
27+
TmplAstIfBlockBranch,
28+
TmplAstLetDeclaration,
29+
TmplAstReference,
30+
TmplAstSwitchBlock,
31+
TmplAstSwitchBlockCase,
32+
TmplAstText,
33+
TmplAstTextAttribute,
34+
TmplAstUnknownBlock,
35+
TmplAstVariable,
36+
TmplAstComponent,
37+
TmplAstDirective,
38+
ParseSourceSpan,
39+
} from '@angular/compiler';
40+
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
41+
import {PotentialDirective} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
42+
import ts from 'typescript';
43+
import {TypeCheckInfo} from './utils';
44+
45+
/**
46+
* see https://github.com/microsoft/TypeScript/blob/c85e626d8e17427a6865521737b45ccbbe9c78ef/src/services/classifier2020.ts#L49
47+
*/
48+
export const enum TokenEncodingConsts {
49+
typeOffset = 8,
50+
modifierMask = (1 << typeOffset) - 1,
51+
}
52+
53+
/**
54+
* Token types extended from TypeScript
55+
* see https://github.com/microsoft/TypeScript/blob/c85e626d8e17427a6865521737b45ccbbe9c78ef/src/services/classifier2020.ts#L55
56+
*/
57+
export const enum TokenType {
58+
class,
59+
enum,
60+
interface,
61+
namespace,
62+
typeParameter,
63+
type,
64+
parameter,
65+
variable,
66+
enumMember,
67+
property,
68+
function,
69+
member,
70+
}
71+
72+
/**
73+
* Token modifiers extended from TypeScript
74+
* see https://github.com/microsoft/TypeScript/blob/c85e626d8e17427a6865521737b45ccbbe9c78ef/src/services/classifier2020.ts#L71
75+
*/
76+
export const enum TokenModifier {
77+
declaration,
78+
static,
79+
async,
80+
readonly,
81+
defaultLibrary,
82+
local,
83+
}
84+
85+
export function getClassificationsForTemplate(
86+
compiler: NgCompiler,
87+
typeCheckInfo: TypeCheckInfo,
88+
range: ts.TextSpan,
89+
): ts.Classifications {
90+
const templateTypeChecker = compiler.getTemplateTypeChecker();
91+
const potentialTags = templateTypeChecker.getElementsInFileScope(typeCheckInfo.declaration);
92+
93+
const visitor = new ClassificationVisitor(potentialTags, range);
94+
visitor.visitAll(typeCheckInfo.nodes);
95+
96+
return {
97+
spans: visitor.getSpans(),
98+
endOfLineState: ts.EndOfLineState.None,
99+
};
100+
}
101+
102+
class ClassificationVisitor implements TmplAstVisitor {
103+
private spans: number[] = [];
104+
constructor(
105+
private tags: Map<string, PotentialDirective | null>,
106+
private range: ts.TextSpan,
107+
) {}
108+
109+
getSpans(): number[] {
110+
return this.spans;
111+
}
112+
113+
visit(node: TmplAstNode | null) {
114+
if (node && this.rangeIntersectsWith(node.sourceSpan)) {
115+
node.visit(this);
116+
}
117+
}
118+
119+
visitElement(element: TmplAstElement) {
120+
const tag = element.name;
121+
const potentialDirective = this.tags.get(tag);
122+
// prevent classification of non-component directives that would be applied
123+
// to this element due to a matching selector
124+
const isComponent = potentialDirective && potentialDirective.isComponent;
125+
const classification = this.classifyAs(TokenType.class);
126+
127+
if (isComponent && this.rangeIntersectsWith(element.startSourceSpan)) {
128+
this.spans.push(element.startSourceSpan.start.offset + 1, tag.length, classification);
129+
}
130+
131+
this.visitAll(element.children);
132+
133+
if (isComponent && !element.isSelfClosing && this.rangeIntersectsWith(element.endSourceSpan!)) {
134+
this.spans.push(element.endSourceSpan!.start.offset + 2, tag.length, classification);
135+
}
136+
}
137+
138+
visitContent(content: TmplAstContent) {
139+
this.visitAll(content.children);
140+
}
141+
142+
visitVariable(variable: TmplAstVariable) {}
143+
visitReference(reference: TmplAstReference) {}
144+
visitTextAttribute(attribute: TmplAstTextAttribute) {}
145+
visitBoundAttribute(attribute: TmplAstBoundAttribute) {}
146+
visitBoundEvent(attribute: TmplAstBoundEvent) {}
147+
visitText(text: TmplAstText) {}
148+
visitBoundText(text: TmplAstBoundText) {}
149+
visitIcu(icu: TmplAstIcu) {}
150+
151+
visitDeferredBlock(deferred: TmplAstDeferredBlock) {
152+
this.visitAll(deferred.children);
153+
this.visit(deferred.error);
154+
this.visit(deferred.loading);
155+
this.visit(deferred.placeholder);
156+
}
157+
158+
visitDeferredBlockPlaceholder(block: TmplAstDeferredBlockPlaceholder) {
159+
this.visitAll(block.children);
160+
}
161+
162+
visitDeferredBlockError(block: TmplAstDeferredBlockError) {
163+
this.visitAll(block.children);
164+
}
165+
166+
visitDeferredBlockLoading(block: TmplAstDeferredBlockLoading) {
167+
this.visitAll(block.children);
168+
}
169+
170+
visitDeferredTrigger(trigger: TmplAstDeferredTrigger) {}
171+
172+
visitSwitchBlock(block: TmplAstSwitchBlock) {
173+
this.visitAll(block.cases);
174+
}
175+
176+
visitSwitchBlockCase(block: TmplAstSwitchBlockCase) {
177+
this.visitAll(block.children);
178+
}
179+
180+
visitForLoopBlock(block: TmplAstForLoopBlock) {
181+
this.visitAll(block.children);
182+
this.visit(block.empty);
183+
}
184+
185+
visitForLoopBlockEmpty(block: TmplAstForLoopBlockEmpty) {
186+
this.visitAll(block.children);
187+
}
188+
189+
visitIfBlock(block: TmplAstIfBlock) {
190+
this.visitAll(block.branches);
191+
}
192+
193+
visitIfBlockBranch(block: TmplAstIfBlockBranch) {
194+
this.visitAll(block.children);
195+
}
196+
197+
visitTemplate(template: TmplAstTemplate) {
198+
this.visitAll(template.children);
199+
}
200+
201+
visitUnknownBlock(block: TmplAstUnknownBlock) {}
202+
visitLetDeclaration(decl: TmplAstLetDeclaration) {}
203+
204+
visitComponent(component: TmplAstComponent) {}
205+
visitDirective(directive: TmplAstDirective) {}
206+
207+
visitAll(children: TmplAstNode[]) {
208+
for (const child of children) {
209+
this.visit(child);
210+
}
211+
}
212+
213+
private rangeIntersectsWith(span: ParseSourceSpan) {
214+
const start = span.start.offset;
215+
const length = span.end.offset - start;
216+
return ts.textSpanIntersectsWith(this.range, start, length);
217+
}
218+
219+
private classifyAs(type: TokenType, modifiers: number = 0) {
220+
return ((type + 1) << TokenEncodingConsts.typeOffset) + modifiers;
221+
}
222+
}

packages/language-service/src/ts_plugin.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,24 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService {
131131
return ngLS.getRenameInfo(fileName, position);
132132
}
133133

134+
function getEncodedSemanticClassifications(
135+
fileName: string,
136+
span: ts.TextSpan,
137+
format?: ts.SemanticClassificationFormat,
138+
): ts.Classifications {
139+
if (angularOnly || !isTypeScriptFile(fileName)) {
140+
return ngLS.getEncodedSemanticClassifications(fileName, span, format);
141+
} else {
142+
const ngClassifications = ngLS.getEncodedSemanticClassifications(fileName, span, format);
143+
const tsClassifications = tsLS.getEncodedSemanticClassifications(fileName, span, format);
144+
const spans = [...ngClassifications.spans, ...tsClassifications.spans];
145+
return {
146+
spans,
147+
endOfLineState: tsClassifications.endOfLineState,
148+
};
149+
}
150+
}
151+
134152
function getCompletionsAtPosition(
135153
fileName: string,
136154
position: number,
@@ -352,6 +370,7 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService {
352370
getReferencesAtPosition,
353371
findRenameLocations,
354372
getRenameInfo,
373+
getEncodedSemanticClassifications,
355374
getCompletionsAtPosition,
356375
getCompletionEntryDetails,
357376
getCompletionEntrySymbol,

0 commit comments

Comments
 (0)
0