Skip to content

Commit 904adb7

Browse files
atscottjosephperrott
authored andcommitted
feat(language-service): add quick info for inline templates in ivy (angular#39060)
Adds implementation for `getQuickInfoAtPosition` to the Ivy Language Service, which now returns `ts.QuickInfo` for inline templates. PR Close angular#39060
1 parent 4fe673d commit 904adb7

File tree

12 files changed

+892
-26
lines changed

12 files changed

+892
-26
lines changed

packages/language-service/ivy/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ ts_library(
1515
"//packages/compiler-cli/src/ngtsc/shims",
1616
"//packages/compiler-cli/src/ngtsc/typecheck",
1717
"//packages/compiler-cli/src/ngtsc/typecheck/api",
18+
# TODO(atscott): Pull functions/variables common to VE and Ivy into a new package
19+
"//packages/language-service",
1820
"@npm//typescript",
1921
],
2022
)

packages/language-service/ivy/hybrid_visitor.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler';
1010
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
1111
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
1212

13+
import {isTemplateNode, isTemplateNodeWithKeyAndValue} from './utils';
14+
1315
/**
1416
* Return the template AST node or expression AST node that most accurately
1517
* represents the node at the specified cursor `position`.
@@ -165,24 +167,6 @@ class ExpressionVisitor extends e.RecursiveAstVisitor {
165167
}
166168
}
167169

168-
export function isTemplateNode(node: t.Node|e.AST): node is t.Node {
169-
// Template node implements the Node interface so we cannot use instanceof.
170-
return node.sourceSpan instanceof ParseSourceSpan;
171-
}
172-
173-
interface NodeWithKeyAndValue extends t.Node {
174-
keySpan: ParseSourceSpan;
175-
valueSpan?: ParseSourceSpan;
176-
}
177-
178-
export function isTemplateNodeWithKeyAndValue(node: t.Node|e.AST): node is NodeWithKeyAndValue {
179-
return isTemplateNode(node) && node.hasOwnProperty('keySpan');
180-
}
181-
182-
export function isExpressionNode(node: t.Node|e.AST): node is e.AST {
183-
return node instanceof e.AST;
184-
}
185-
186170
function getSpanIncludingEndTag(ast: t.Node) {
187171
const result = {
188172
start: ast.sourceSpan.start.offset,

packages/language-service/ivy/language_service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck'
1616
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
1717
import * as ts from 'typescript/lib/tsserverlibrary';
1818

19+
import {QuickInfoBuilder} from './quick_info';
20+
1921
export class LanguageService {
2022
private options: CompilerOptions;
2123
private lastKnownProgram: ts.Program|null = null;
@@ -45,6 +47,12 @@ export class LanguageService {
4547
throw new Error('Ivy LS currently does not support external template');
4648
}
4749

50+
getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined {
51+
const program = this.strategy.getProgram();
52+
const compiler = this.createCompiler(program);
53+
return new QuickInfoBuilder(this.tsLS, compiler).get(fileName, position);
54+
}
55+
4856
private createCompiler(program: ts.Program): NgCompiler {
4957
return new NgCompiler(
5058
this.adapter,
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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.io/license
7+
*/
8+
import {AST, BindingPipe, ImplicitReceiver, MethodCall, TmplAstBoundAttribute, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler';
9+
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
10+
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
11+
import * as ts from 'typescript';
12+
13+
import {createQuickInfo, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT} from '../src/hover';
14+
15+
import {findNodeAtPosition} from './hybrid_visitor';
16+
import {filterAliasImports, getDirectiveMatches, getDirectiveMatchesForAttribute, getTemplateInfoAtPosition, getTextSpanOfNode} from './utils';
17+
18+
/**
19+
* The type of Angular directive. Used for QuickInfo in template.
20+
*/
21+
export enum QuickInfoKind {
22+
COMPONENT = 'component',
23+
DIRECTIVE = 'directive',
24+
EVENT = 'event',
25+
REFERENCE = 'reference',
26+
ELEMENT = 'element',
27+
VARIABLE = 'variable',
28+
PIPE = 'pipe',
29+
PROPERTY = 'property',
30+
METHOD = 'method',
31+
TEMPLATE = 'template',
32+
}
33+
34+
export class QuickInfoBuilder {
35+
private readonly typeChecker = this.compiler.getNextProgram().getTypeChecker();
36+
constructor(private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler) {}
37+
38+
get(fileName: string, position: number): ts.QuickInfo|undefined {
39+
const templateInfo = getTemplateInfoAtPosition(fileName, position, this.compiler);
40+
if (templateInfo === undefined) {
41+
return undefined;
42+
}
43+
const {template, component} = templateInfo;
44+
45+
const node = findNodeAtPosition(template, position);
46+
if (node === undefined) {
47+
return undefined;
48+
}
49+
50+
const symbol = this.compiler.getTemplateTypeChecker().getSymbolOfNode(node, component);
51+
if (symbol === null) {
52+
return isDollarAny(node) ? createDollarAnyQuickInfo(node) : undefined;
53+
}
54+
55+
return this.getQuickInfoForSymbol(symbol, node);
56+
}
57+
58+
private getQuickInfoForSymbol(symbol: Symbol, node: TmplAstNode|AST): ts.QuickInfo|undefined {
59+
switch (symbol.kind) {
60+
case SymbolKind.Input:
61+
case SymbolKind.Output:
62+
return this.getQuickInfoForBindingSymbol(symbol, node);
63+
case SymbolKind.Template:
64+
return createNgTemplateQuickInfo(node);
65+
case SymbolKind.Element:
66+
return this.getQuickInfoForElementSymbol(symbol);
67+
case SymbolKind.Variable:
68+
return this.getQuickInfoForVariableSymbol(symbol, node);
69+
case SymbolKind.Reference:
70+
return this.getQuickInfoForReferenceSymbol(symbol, node);
71+
case SymbolKind.DomBinding:
72+
return this.getQuickInfoForDomBinding(node, symbol);
73+
case SymbolKind.Directive:
74+
return this.getQuickInfoAtShimLocation(symbol.shimLocation, node);
75+
case SymbolKind.Expression:
76+
return node instanceof BindingPipe ?
77+
this.getQuickInfoForPipeSymbol(symbol, node) :
78+
this.getQuickInfoAtShimLocation(symbol.shimLocation, node);
79+
}
80+
}
81+
82+
private getQuickInfoForBindingSymbol(
83+
symbol: InputBindingSymbol|OutputBindingSymbol, node: TmplAstNode|AST): ts.QuickInfo
84+
|undefined {
85+
if (symbol.bindings.length === 0) {
86+
return undefined;
87+
}
88+
89+
const kind = symbol.kind === SymbolKind.Input ? QuickInfoKind.PROPERTY : QuickInfoKind.EVENT;
90+
91+
const quickInfo = this.getQuickInfoAtShimLocation(symbol.bindings[0].shimLocation, node);
92+
return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, kind);
93+
}
94+
95+
private getQuickInfoForElementSymbol(symbol: ElementSymbol): ts.QuickInfo {
96+
const {templateNode} = symbol;
97+
const matches = getDirectiveMatches(symbol.directives, templateNode.name);
98+
if (matches.size > 0) {
99+
return this.getQuickInfoForDirectiveSymbol(matches.values().next().value, templateNode);
100+
}
101+
102+
return createQuickInfo(
103+
templateNode.name, QuickInfoKind.ELEMENT, getTextSpanOfNode(templateNode),
104+
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType));
105+
}
106+
107+
private getQuickInfoForVariableSymbol(symbol: VariableSymbol, node: TmplAstNode|AST):
108+
ts.QuickInfo {
109+
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.shimLocation);
110+
return createQuickInfo(
111+
symbol.declaration.name, QuickInfoKind.VARIABLE, getTextSpanOfNode(node),
112+
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
113+
}
114+
115+
private getQuickInfoForReferenceSymbol(symbol: ReferenceSymbol, node: TmplAstNode|AST):
116+
ts.QuickInfo {
117+
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.shimLocation);
118+
return createQuickInfo(
119+
symbol.declaration.name, QuickInfoKind.REFERENCE, getTextSpanOfNode(node),
120+
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
121+
}
122+
123+
private getQuickInfoForPipeSymbol(symbol: ExpressionSymbol, node: TmplAstNode|AST): ts.QuickInfo
124+
|undefined {
125+
const quickInfo = this.getQuickInfoAtShimLocation(symbol.shimLocation, node);
126+
return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, QuickInfoKind.PIPE);
127+
}
128+
129+
private getQuickInfoForDomBinding(node: TmplAstNode|AST, symbol: DomBindingSymbol) {
130+
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
131+
return undefined;
132+
}
133+
const directives = getDirectiveMatchesForAttribute(
134+
node.name, symbol.host.templateNode, symbol.host.directives);
135+
if (directives.size === 0) {
136+
return undefined;
137+
}
138+
139+
return this.getQuickInfoForDirectiveSymbol(directives.values().next().value, node);
140+
}
141+
142+
private getQuickInfoForDirectiveSymbol(dir: DirectiveSymbol, node: TmplAstNode|AST):
143+
ts.QuickInfo {
144+
const kind = dir.isComponent ? QuickInfoKind.COMPONENT : QuickInfoKind.DIRECTIVE;
145+
const documentation = this.getDocumentationFromTypeDefAtLocation(dir.shimLocation);
146+
return createQuickInfo(
147+
this.typeChecker.typeToString(dir.tsType), kind, getTextSpanOfNode(node),
148+
undefined /* containerName */, undefined, documentation);
149+
}
150+
151+
private getDocumentationFromTypeDefAtLocation(shimLocation: ShimLocation):
152+
ts.SymbolDisplayPart[]|undefined {
153+
const typeDefs = this.tsLS.getTypeDefinitionAtPosition(
154+
shimLocation.shimPath, shimLocation.positionInShimFile);
155+
if (typeDefs === undefined || typeDefs.length === 0) {
156+
return undefined;
157+
}
158+
return this.tsLS.getQuickInfoAtPosition(typeDefs[0].fileName, typeDefs[0].textSpan.start)
159+
?.documentation;
160+
}
161+
162+
private getQuickInfoAtShimLocation(location: ShimLocation, node: TmplAstNode|AST): ts.QuickInfo
163+
|undefined {
164+
const quickInfo =
165+
this.tsLS.getQuickInfoAtPosition(location.shimPath, location.positionInShimFile);
166+
if (quickInfo === undefined || quickInfo.displayParts === undefined) {
167+
return quickInfo;
168+
}
169+
170+
quickInfo.displayParts = filterAliasImports(quickInfo.displayParts);
171+
172+
const textSpan = getTextSpanOfNode(node);
173+
return {...quickInfo, textSpan};
174+
}
175+
}
176+
177+
function updateQuickInfoKind(quickInfo: ts.QuickInfo, kind: QuickInfoKind): ts.QuickInfo {
178+
if (quickInfo.displayParts === undefined) {
179+
return quickInfo;
180+
}
181+
182+
const startsWithKind = quickInfo.displayParts.length >= 3 &&
183+
displayPartsEqual(quickInfo.displayParts[0], {text: '(', kind: SYMBOL_PUNC}) &&
184+
quickInfo.displayParts[1].kind === SYMBOL_TEXT &&
185+
displayPartsEqual(quickInfo.displayParts[2], {text: ')', kind: SYMBOL_PUNC});
186+
if (startsWithKind) {
187+
quickInfo.displayParts[1].text = kind;
188+
} else {
189+
quickInfo.displayParts = [
190+
{text: '(', kind: SYMBOL_PUNC},
191+
{text: kind, kind: SYMBOL_TEXT},
192+
{text: ')', kind: SYMBOL_PUNC},
193+
{text: ' ', kind: SYMBOL_SPACE},
194+
...quickInfo.displayParts,
195+
];
196+
}
197+
return quickInfo;
198+
}
199+
200+
function displayPartsEqual(a: {text: string, kind: string}, b: {text: string, kind: string}) {
201+
return a.text === b.text && a.kind === b.kind;
202+
}
203+
204+
function isDollarAny(node: TmplAstNode|AST): node is MethodCall {
205+
return node instanceof MethodCall && node.receiver instanceof ImplicitReceiver &&
206+
node.name === '$any' && node.args.length === 1;
207+
}
208+
209+
function createDollarAnyQuickInfo(node: MethodCall): ts.QuickInfo {
210+
return createQuickInfo(
211+
'$any',
212+
QuickInfoKind.METHOD,
213+
getTextSpanOfNode(node),
214+
/** containerName */ undefined,
215+
'any',
216+
[{
217+
kind: SYMBOL_TEXT,
218+
text: 'function to cast an expression to the `any` type',
219+
}],
220+
);
221+
}
222+
223+
// TODO(atscott): Create special `ts.QuickInfo` for `ng-template` and `ng-container` as well.
224+
function createNgTemplateQuickInfo(node: TmplAstNode|AST): ts.QuickInfo {
225+
return createQuickInfo(
226+
'ng-template',
227+
QuickInfoKind.TEMPLATE,
228+
getTextSpanOfNode(node),
229+
/** containerName */ undefined,
230+
/** type */ undefined,
231+
[{
232+
kind: SYMBOL_TEXT,
233+
text:
234+
'The `<ng-template>` is an Angular element for rendering HTML. It is never displayed directly.',
235+
}],
236+
);
237+
}

packages/language-service/ivy/test/hybrid_visitor_spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {ParseError, parseTemplate} from '@angular/compiler';
1010
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
1111
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
1212

13-
import {findNodeAtPosition, isExpressionNode, isTemplateNode} from '../hybrid_visitor';
13+
import {findNodeAtPosition} from '../hybrid_visitor';
14+
import {isExpressionNode, isTemplateNode} from '../utils';
1415

1516
interface ParseResult {
1617
nodes: t.Node[];

packages/language-service/ivy/test/language_service_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('parseNgCompilerOptions', () => {
1717
const options = parseNgCompilerOptions(project);
1818
expect(options).toEqual(jasmine.objectContaining({
1919
enableIvy: true, // default for ivy is true
20-
fullTemplateTypeCheck: true,
20+
strictTemplates: true,
2121
strictInjectionParameters: true,
2222
}));
2323
});

0 commit comments

Comments
 (0)