feat(language-service): add quick info for inline templates in ivy (#39060)
Adds implementation for `getQuickInfoAtPosition` to the Ivy Language Service, which now returns `ts.QuickInfo` for inline templates. PR Close #39060
This commit is contained in:
parent
4fe673d518
commit
904adb72d2
|
@ -15,6 +15,8 @@ ts_library(
|
||||||
"//packages/compiler-cli/src/ngtsc/shims",
|
"//packages/compiler-cli/src/ngtsc/shims",
|
||||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||||
|
# TODO(atscott): Pull functions/variables common to VE and Ivy into a new package
|
||||||
|
"//packages/language-service",
|
||||||
"@npm//typescript",
|
"@npm//typescript",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler';
|
||||||
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
|
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
|
||||||
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
||||||
|
|
||||||
|
import {isTemplateNode, isTemplateNodeWithKeyAndValue} from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the template AST node or expression AST node that most accurately
|
* Return the template AST node or expression AST node that most accurately
|
||||||
* represents the node at the specified cursor `position`.
|
* represents the node at the specified cursor `position`.
|
||||||
|
@ -165,24 +167,6 @@ class ExpressionVisitor extends e.RecursiveAstVisitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTemplateNode(node: t.Node|e.AST): node is t.Node {
|
|
||||||
// Template node implements the Node interface so we cannot use instanceof.
|
|
||||||
return node.sourceSpan instanceof ParseSourceSpan;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NodeWithKeyAndValue extends t.Node {
|
|
||||||
keySpan: ParseSourceSpan;
|
|
||||||
valueSpan?: ParseSourceSpan;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isTemplateNodeWithKeyAndValue(node: t.Node|e.AST): node is NodeWithKeyAndValue {
|
|
||||||
return isTemplateNode(node) && node.hasOwnProperty('keySpan');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isExpressionNode(node: t.Node|e.AST): node is e.AST {
|
|
||||||
return node instanceof e.AST;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSpanIncludingEndTag(ast: t.Node) {
|
function getSpanIncludingEndTag(ast: t.Node) {
|
||||||
const result = {
|
const result = {
|
||||||
start: ast.sourceSpan.start.offset,
|
start: ast.sourceSpan.start.offset,
|
||||||
|
|
|
@ -16,6 +16,8 @@ import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck'
|
||||||
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||||
|
|
||||||
|
import {QuickInfoBuilder} from './quick_info';
|
||||||
|
|
||||||
export class LanguageService {
|
export class LanguageService {
|
||||||
private options: CompilerOptions;
|
private options: CompilerOptions;
|
||||||
private lastKnownProgram: ts.Program|null = null;
|
private lastKnownProgram: ts.Program|null = null;
|
||||||
|
@ -45,6 +47,12 @@ export class LanguageService {
|
||||||
throw new Error('Ivy LS currently does not support external template');
|
throw new Error('Ivy LS currently does not support external template');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined {
|
||||||
|
const program = this.strategy.getProgram();
|
||||||
|
const compiler = this.createCompiler(program);
|
||||||
|
return new QuickInfoBuilder(this.tsLS, compiler).get(fileName, position);
|
||||||
|
}
|
||||||
|
|
||||||
private createCompiler(program: ts.Program): NgCompiler {
|
private createCompiler(program: ts.Program): NgCompiler {
|
||||||
return new NgCompiler(
|
return new NgCompiler(
|
||||||
this.adapter,
|
this.adapter,
|
||||||
|
|
|
@ -0,0 +1,237 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import {AST, BindingPipe, ImplicitReceiver, MethodCall, TmplAstBoundAttribute, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler';
|
||||||
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
||||||
|
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {createQuickInfo, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT} from '../src/hover';
|
||||||
|
|
||||||
|
import {findNodeAtPosition} from './hybrid_visitor';
|
||||||
|
import {filterAliasImports, getDirectiveMatches, getDirectiveMatchesForAttribute, getTemplateInfoAtPosition, getTextSpanOfNode} from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of Angular directive. Used for QuickInfo in template.
|
||||||
|
*/
|
||||||
|
export enum QuickInfoKind {
|
||||||
|
COMPONENT = 'component',
|
||||||
|
DIRECTIVE = 'directive',
|
||||||
|
EVENT = 'event',
|
||||||
|
REFERENCE = 'reference',
|
||||||
|
ELEMENT = 'element',
|
||||||
|
VARIABLE = 'variable',
|
||||||
|
PIPE = 'pipe',
|
||||||
|
PROPERTY = 'property',
|
||||||
|
METHOD = 'method',
|
||||||
|
TEMPLATE = 'template',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QuickInfoBuilder {
|
||||||
|
private readonly typeChecker = this.compiler.getNextProgram().getTypeChecker();
|
||||||
|
constructor(private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler) {}
|
||||||
|
|
||||||
|
get(fileName: string, position: number): ts.QuickInfo|undefined {
|
||||||
|
const templateInfo = getTemplateInfoAtPosition(fileName, position, this.compiler);
|
||||||
|
if (templateInfo === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const {template, component} = templateInfo;
|
||||||
|
|
||||||
|
const node = findNodeAtPosition(template, position);
|
||||||
|
if (node === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbol = this.compiler.getTemplateTypeChecker().getSymbolOfNode(node, component);
|
||||||
|
if (symbol === null) {
|
||||||
|
return isDollarAny(node) ? createDollarAnyQuickInfo(node) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getQuickInfoForSymbol(symbol, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQuickInfoForSymbol(symbol: Symbol, node: TmplAstNode|AST): ts.QuickInfo|undefined {
|
||||||
|
switch (symbol.kind) {
|
||||||
|
case SymbolKind.Input:
|
||||||
|
case SymbolKind.Output:
|
||||||
|
return this.getQuickInfoForBindingSymbol(symbol, node);
|
||||||
|
case SymbolKind.Template:
|
||||||
|
return createNgTemplateQuickInfo(node);
|
||||||
|
case SymbolKind.Element:
|
||||||
|
return this.getQuickInfoForElementSymbol(symbol);
|
||||||
|
case SymbolKind.Variable:
|
||||||
|
return this.getQuickInfoForVariableSymbol(symbol, node);
|
||||||
|
case SymbolKind.Reference:
|
||||||
|
return this.getQuickInfoForReferenceSymbol(symbol, node);
|
||||||
|
case SymbolKind.DomBinding:
|
||||||
|
return this.getQuickInfoForDomBinding(node, symbol);
|
||||||
|
case SymbolKind.Directive:
|
||||||
|
return this.getQuickInfoAtShimLocation(symbol.shimLocation, node);
|
||||||
|
case SymbolKind.Expression:
|
||||||
|
return node instanceof BindingPipe ?
|
||||||
|
this.getQuickInfoForPipeSymbol(symbol, node) :
|
||||||
|
this.getQuickInfoAtShimLocation(symbol.shimLocation, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQuickInfoForBindingSymbol(
|
||||||
|
symbol: InputBindingSymbol|OutputBindingSymbol, node: TmplAstNode|AST): ts.QuickInfo
|
||||||
|
|undefined {
|
||||||
|
if (symbol.bindings.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = symbol.kind === SymbolKind.Input ? QuickInfoKind.PROPERTY : QuickInfoKind.EVENT;
|
||||||
|
|
||||||
|
const quickInfo = this.getQuickInfoAtShimLocation(symbol.bindings[0].shimLocation, node);
|
||||||
|
return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQuickInfoForElementSymbol(symbol: ElementSymbol): ts.QuickInfo {
|
||||||
|
const {templateNode} = symbol;
|
||||||
|
const matches = getDirectiveMatches(symbol.directives, templateNode.name);
|
||||||
|
if (matches.size > 0) {
|
||||||
|
return this.getQuickInfoForDirectiveSymbol(matches.values().next().value, templateNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createQuickInfo(
|
||||||
|
templateNode.name, QuickInfoKind.ELEMENT, getTextSpanOfNode(templateNode),
|
||||||
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQuickInfoForVariableSymbol(symbol: VariableSymbol, node: TmplAstNode|AST):
|
||||||
|
ts.QuickInfo {
|
||||||
|
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.shimLocation);
|
||||||
|
return createQuickInfo(
|
||||||
|
symbol.declaration.name, QuickInfoKind.VARIABLE, getTextSpanOfNode(node),
|
||||||
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQuickInfoForReferenceSymbol(symbol: ReferenceSymbol, node: TmplAstNode|AST):
|
||||||
|
ts.QuickInfo {
|
||||||
|
const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.shimLocation);
|
||||||
|
return createQuickInfo(
|
||||||
|
symbol.declaration.name, QuickInfoKind.REFERENCE, getTextSpanOfNode(node),
|
||||||
|
undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQuickInfoForPipeSymbol(symbol: ExpressionSymbol, node: TmplAstNode|AST): ts.QuickInfo
|
||||||
|
|undefined {
|
||||||
|
const quickInfo = this.getQuickInfoAtShimLocation(symbol.shimLocation, node);
|
||||||
|
return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, QuickInfoKind.PIPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQuickInfoForDomBinding(node: TmplAstNode|AST, symbol: DomBindingSymbol) {
|
||||||
|
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const directives = getDirectiveMatchesForAttribute(
|
||||||
|
node.name, symbol.host.templateNode, symbol.host.directives);
|
||||||
|
if (directives.size === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getQuickInfoForDirectiveSymbol(directives.values().next().value, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQuickInfoForDirectiveSymbol(dir: DirectiveSymbol, node: TmplAstNode|AST):
|
||||||
|
ts.QuickInfo {
|
||||||
|
const kind = dir.isComponent ? QuickInfoKind.COMPONENT : QuickInfoKind.DIRECTIVE;
|
||||||
|
const documentation = this.getDocumentationFromTypeDefAtLocation(dir.shimLocation);
|
||||||
|
return createQuickInfo(
|
||||||
|
this.typeChecker.typeToString(dir.tsType), kind, getTextSpanOfNode(node),
|
||||||
|
undefined /* containerName */, undefined, documentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDocumentationFromTypeDefAtLocation(shimLocation: ShimLocation):
|
||||||
|
ts.SymbolDisplayPart[]|undefined {
|
||||||
|
const typeDefs = this.tsLS.getTypeDefinitionAtPosition(
|
||||||
|
shimLocation.shimPath, shimLocation.positionInShimFile);
|
||||||
|
if (typeDefs === undefined || typeDefs.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.tsLS.getQuickInfoAtPosition(typeDefs[0].fileName, typeDefs[0].textSpan.start)
|
||||||
|
?.documentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQuickInfoAtShimLocation(location: ShimLocation, node: TmplAstNode|AST): ts.QuickInfo
|
||||||
|
|undefined {
|
||||||
|
const quickInfo =
|
||||||
|
this.tsLS.getQuickInfoAtPosition(location.shimPath, location.positionInShimFile);
|
||||||
|
if (quickInfo === undefined || quickInfo.displayParts === undefined) {
|
||||||
|
return quickInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
quickInfo.displayParts = filterAliasImports(quickInfo.displayParts);
|
||||||
|
|
||||||
|
const textSpan = getTextSpanOfNode(node);
|
||||||
|
return {...quickInfo, textSpan};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQuickInfoKind(quickInfo: ts.QuickInfo, kind: QuickInfoKind): ts.QuickInfo {
|
||||||
|
if (quickInfo.displayParts === undefined) {
|
||||||
|
return quickInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startsWithKind = quickInfo.displayParts.length >= 3 &&
|
||||||
|
displayPartsEqual(quickInfo.displayParts[0], {text: '(', kind: SYMBOL_PUNC}) &&
|
||||||
|
quickInfo.displayParts[1].kind === SYMBOL_TEXT &&
|
||||||
|
displayPartsEqual(quickInfo.displayParts[2], {text: ')', kind: SYMBOL_PUNC});
|
||||||
|
if (startsWithKind) {
|
||||||
|
quickInfo.displayParts[1].text = kind;
|
||||||
|
} else {
|
||||||
|
quickInfo.displayParts = [
|
||||||
|
{text: '(', kind: SYMBOL_PUNC},
|
||||||
|
{text: kind, kind: SYMBOL_TEXT},
|
||||||
|
{text: ')', kind: SYMBOL_PUNC},
|
||||||
|
{text: ' ', kind: SYMBOL_SPACE},
|
||||||
|
...quickInfo.displayParts,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return quickInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayPartsEqual(a: {text: string, kind: string}, b: {text: string, kind: string}) {
|
||||||
|
return a.text === b.text && a.kind === b.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDollarAny(node: TmplAstNode|AST): node is MethodCall {
|
||||||
|
return node instanceof MethodCall && node.receiver instanceof ImplicitReceiver &&
|
||||||
|
node.name === '$any' && node.args.length === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDollarAnyQuickInfo(node: MethodCall): ts.QuickInfo {
|
||||||
|
return createQuickInfo(
|
||||||
|
'$any',
|
||||||
|
QuickInfoKind.METHOD,
|
||||||
|
getTextSpanOfNode(node),
|
||||||
|
/** containerName */ undefined,
|
||||||
|
'any',
|
||||||
|
[{
|
||||||
|
kind: SYMBOL_TEXT,
|
||||||
|
text: 'function to cast an expression to the `any` type',
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(atscott): Create special `ts.QuickInfo` for `ng-template` and `ng-container` as well.
|
||||||
|
function createNgTemplateQuickInfo(node: TmplAstNode|AST): ts.QuickInfo {
|
||||||
|
return createQuickInfo(
|
||||||
|
'ng-template',
|
||||||
|
QuickInfoKind.TEMPLATE,
|
||||||
|
getTextSpanOfNode(node),
|
||||||
|
/** containerName */ undefined,
|
||||||
|
/** type */ undefined,
|
||||||
|
[{
|
||||||
|
kind: SYMBOL_TEXT,
|
||||||
|
text:
|
||||||
|
'The `<ng-template>` is an Angular element for rendering HTML. It is never displayed directly.',
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,7 +10,8 @@ import {ParseError, parseTemplate} from '@angular/compiler';
|
||||||
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
|
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
|
||||||
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
||||||
|
|
||||||
import {findNodeAtPosition, isExpressionNode, isTemplateNode} from '../hybrid_visitor';
|
import {findNodeAtPosition} from '../hybrid_visitor';
|
||||||
|
import {isExpressionNode, isTemplateNode} from '../utils';
|
||||||
|
|
||||||
interface ParseResult {
|
interface ParseResult {
|
||||||
nodes: t.Node[];
|
nodes: t.Node[];
|
||||||
|
|
|
@ -17,7 +17,7 @@ describe('parseNgCompilerOptions', () => {
|
||||||
const options = parseNgCompilerOptions(project);
|
const options = parseNgCompilerOptions(project);
|
||||||
expect(options).toEqual(jasmine.objectContaining({
|
expect(options).toEqual(jasmine.objectContaining({
|
||||||
enableIvy: true, // default for ivy is true
|
enableIvy: true, // default for ivy is true
|
||||||
fullTemplateTypeCheck: true,
|
strictTemplates: true,
|
||||||
strictInjectionParameters: true,
|
strictInjectionParameters: true,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,393 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||||
|
|
||||||
|
import {LanguageService} from '../language_service';
|
||||||
|
|
||||||
|
import {APP_COMPONENT, setup, TEST_TEMPLATE} from './mock_host';
|
||||||
|
|
||||||
|
describe('quick info', () => {
|
||||||
|
const {project, service, tsLS} = setup();
|
||||||
|
const ngLS = new LanguageService(project, tsLS);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('elements', () => {
|
||||||
|
it('should work for native elements', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<butt¦on></button>`,
|
||||||
|
expectedSpanText: '<button></button>',
|
||||||
|
expectedDisplayString: '(element) button: HTMLButtonElement'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('templates', () => {
|
||||||
|
it('should return undefined for ng-templates', () => {
|
||||||
|
const {documentation} = expectQuickInfo({
|
||||||
|
templateOverride: `<ng-templ¦ate></ng-template>`,
|
||||||
|
expectedSpanText: '<ng-template></ng-template>',
|
||||||
|
expectedDisplayString: '(template) ng-template'
|
||||||
|
});
|
||||||
|
expect(toText(documentation))
|
||||||
|
.toContain('The `<ng-template>` is an Angular element for rendering HTML.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('directives', () => {
|
||||||
|
it('should work for directives', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div string-model¦></div>`,
|
||||||
|
expectedSpanText: 'string-model',
|
||||||
|
// TODO(atscott): Find a way to include the module
|
||||||
|
// expectedDisplayParts: '(directive) AppModule.StringModel'
|
||||||
|
expectedDisplayString: '(directive) StringModel'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for components', () => {
|
||||||
|
const {documentation} = expectQuickInfo({
|
||||||
|
templateOverride: `<t¦est-comp></test-comp>`,
|
||||||
|
expectedSpanText: '<test-comp></test-comp>',
|
||||||
|
// TODO(atscott): Find a way to include the module
|
||||||
|
// expectedDisplayParts: '(component) AppModule.TestComponent'
|
||||||
|
expectedDisplayString: '(component) TestComponent'
|
||||||
|
});
|
||||||
|
expect(toText(documentation)).toBe('This Component provides the `test-comp` selector.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for structural directives', () => {
|
||||||
|
const {documentation} = expectQuickInfo({
|
||||||
|
templateOverride: `<div *¦ngFor="let item of heroes"></div>`,
|
||||||
|
expectedSpanText: 'ngFor',
|
||||||
|
expectedDisplayString: '(directive) NgForOf<Hero, Array<Hero>>'
|
||||||
|
});
|
||||||
|
expect(toText(documentation))
|
||||||
|
.toContain('A [structural directive](guide/structural-directives) that renders');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for directives with compound selectors, some of which are bindings', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<ng-template ngF¦or let-hero [ngForOf]="heroes">{{item}}</ng-template>`,
|
||||||
|
expectedSpanText: 'ngFor',
|
||||||
|
expectedDisplayString: '(directive) NgForOf<Hero, Array<Hero>>'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for data-let- syntax', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride:
|
||||||
|
`<ng-template ngFor data-let-he¦ro [ngForOf]="heroes">{{item}}</ng-template>`,
|
||||||
|
expectedSpanText: 'hero',
|
||||||
|
expectedDisplayString: '(variable) hero: Hero'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bindings', () => {
|
||||||
|
describe('inputs', () => {
|
||||||
|
it('should work for input providers', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<test-comp [tcN¦ame]="name"></test-comp>`,
|
||||||
|
expectedSpanText: 'tcName',
|
||||||
|
expectedDisplayString: '(property) TestComponent.name: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for bind- syntax', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<test-comp bind-tcN¦ame="name"></test-comp>`,
|
||||||
|
expectedSpanText: 'tcName',
|
||||||
|
expectedDisplayString: '(property) TestComponent.name: string'
|
||||||
|
});
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<test-comp data-bind-tcN¦ame="name"></test-comp>`,
|
||||||
|
expectedSpanText: 'tcName',
|
||||||
|
expectedDisplayString: '(property) TestComponent.name: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for structural directive inputs ngForTrackBy', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div *ngFor="let item of heroes; tr¦ackBy: test;"></div>`,
|
||||||
|
expectedSpanText: 'trackBy',
|
||||||
|
expectedDisplayString:
|
||||||
|
'(property) NgForOf<Hero, Hero[]>.ngForTrackBy: TrackByFunction<Hero>'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for structural directive inputs ngForOf', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div *ngFor="let item o¦f heroes; trackBy: test;"></div>`,
|
||||||
|
expectedSpanText: 'of',
|
||||||
|
expectedDisplayString:
|
||||||
|
'(property) NgForOf<Hero, Hero[]>.ngForOf: Hero[] | (Hero[] & Iterable<Hero>) | null | undefined'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for two-way binding providers', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<test-comp string-model [(mo¦del)]="title"></test-comp>`,
|
||||||
|
expectedSpanText: 'model',
|
||||||
|
expectedDisplayString: '(property) StringModel.model: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('outputs', () => {
|
||||||
|
it('should work for event providers', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<test-comp (te¦st)="myClick($event)"></test-comp>`,
|
||||||
|
expectedSpanText: '(test)="myClick($event)"',
|
||||||
|
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter<any>'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for on- syntax binding', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<test-comp on-te¦st="myClick($event)"></test-comp>`,
|
||||||
|
expectedSpanText: 'on-test="myClick($event)"',
|
||||||
|
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter<any>'
|
||||||
|
});
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<test-comp data-on-te¦st="myClick($event)"></test-comp>`,
|
||||||
|
expectedSpanText: 'data-on-test="myClick($event)"',
|
||||||
|
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter<any>'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for $event from EventEmitter', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div string-model (modelChange)="myClick($e¦vent)"></div>`,
|
||||||
|
expectedSpanText: '$event',
|
||||||
|
expectedDisplayString: '(parameter) $event: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for $event from native element', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div (click)="myClick($e¦vent)"></div>`,
|
||||||
|
expectedSpanText: '$event',
|
||||||
|
expectedDisplayString: '(parameter) $event: MouseEvent'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('references', () => {
|
||||||
|
it('should work for element reference declarations', () => {
|
||||||
|
const {documentation} = expectQuickInfo({
|
||||||
|
templateOverride: `<div #¦chart></div>`,
|
||||||
|
expectedSpanText: '#chart',
|
||||||
|
expectedDisplayString: '(reference) chart: HTMLDivElement'
|
||||||
|
});
|
||||||
|
expect(toText(documentation))
|
||||||
|
.toEqual(
|
||||||
|
'Provides special properties (beyond the regular HTMLElement ' +
|
||||||
|
'interface it also has available to it by inheritance) for manipulating <div> elements.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for ref- syntax', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div ref-ch¦art></div>`,
|
||||||
|
expectedSpanText: 'ref-chart',
|
||||||
|
expectedDisplayString: '(reference) chart: HTMLDivElement'
|
||||||
|
});
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div data-ref-ch¦art></div>`,
|
||||||
|
expectedSpanText: 'data-ref-chart',
|
||||||
|
expectedDisplayString: '(reference) chart: HTMLDivElement'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('variables', () => {
|
||||||
|
it('should work for array members', () => {
|
||||||
|
const {documentation} = expectQuickInfo({
|
||||||
|
templateOverride: `<div *ngFor="let hero of heroes">{{her¦o}}</div>`,
|
||||||
|
expectedSpanText: 'hero',
|
||||||
|
expectedDisplayString: '(variable) hero: Hero'
|
||||||
|
});
|
||||||
|
expect(toText(documentation)).toEqual('The most heroic being.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for ReadonlyArray members (#36191)', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div *ngFor="let hero of readonlyHeroes">{{her¦o}}</div>`,
|
||||||
|
expectedSpanText: 'hero',
|
||||||
|
expectedDisplayString: '(variable) hero: Readonly<Hero>'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for const array members (#36191)', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div *ngFor="let name of constNames">{{na¦me}}</div>`,
|
||||||
|
expectedSpanText: 'name',
|
||||||
|
expectedDisplayString: '(variable) name: { readonly name: "name"; }'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pipes', () => {
|
||||||
|
it('should work for pipes', () => {
|
||||||
|
const templateOverride = `<p>The hero's birthday is {{birthday | da¦te: "MM/dd/yy"}}</p>`;
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride,
|
||||||
|
expectedSpanText: 'date',
|
||||||
|
expectedDisplayString:
|
||||||
|
'(pipe) DatePipe.transform(value: string | number | Date, format?: string | undefined, timezone?: ' +
|
||||||
|
'string | undefined, locale?: string | undefined): string | null (+2 overloads)'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('expressions', () => {
|
||||||
|
it('should find members in a text interpolation', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div>{{ tit¦le }}</div>`,
|
||||||
|
expectedSpanText: 'title',
|
||||||
|
expectedDisplayString: '(property) AppComponent.title: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for accessed property reads', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div>{{title.len¦gth}}</div>`,
|
||||||
|
expectedSpanText: 'length',
|
||||||
|
expectedDisplayString: '(property) String.length: number'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find members in an attribute interpolation', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div string-model model="{{tit¦le}}"></div>`,
|
||||||
|
expectedSpanText: 'title',
|
||||||
|
expectedDisplayString: '(property) AppComponent.title: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find members of input binding', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<test-comp [tcName]="ti¦tle"></test-comp>`,
|
||||||
|
expectedSpanText: 'title',
|
||||||
|
expectedDisplayString: '(property) AppComponent.title: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find input binding on text attribute', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<test-comp tcN¦ame="title"></test-comp>`,
|
||||||
|
expectedSpanText: 'tcName="title"',
|
||||||
|
expectedDisplayString: '(property) TestComponent.name: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find members of event binding', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<test-comp (test)="ti¦tle=$event"></test-comp>`,
|
||||||
|
expectedSpanText: 'title',
|
||||||
|
expectedDisplayString: '(property) AppComponent.title: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for method calls', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div (click)="setT¦itle('title')"></div>`,
|
||||||
|
expectedSpanText: 'setTitle',
|
||||||
|
expectedDisplayString: '(method) AppComponent.setTitle(newTitle: string): void'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for accessed properties in writes', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div (click)="hero.i¦d = 2"></div>`,
|
||||||
|
expectedSpanText: 'id',
|
||||||
|
expectedDisplayString: '(property) Hero.id: number'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for method call arguments', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div (click)="setTitle(hero.nam¦e)"></div>`,
|
||||||
|
expectedSpanText: 'name',
|
||||||
|
expectedDisplayString: '(property) Hero.name: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find members of two-way binding', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<input [(ngModel)]="ti¦tle" />`,
|
||||||
|
expectedSpanText: 'title',
|
||||||
|
expectedDisplayString: '(property) AppComponent.title: string'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find members in a structural directive', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div *ngIf="anyV¦alue"></div>`,
|
||||||
|
expectedSpanText: 'anyValue',
|
||||||
|
expectedDisplayString: '(property) AppComponent.anyValue: any'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for members in structural directives', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div *ngFor="let item of her¦oes; trackBy: test;"></div>`,
|
||||||
|
expectedSpanText: 'heroes',
|
||||||
|
expectedDisplayString: '(property) AppComponent.heroes: Hero[]'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for the $any() cast function', () => {
|
||||||
|
expectQuickInfo({
|
||||||
|
templateOverride: `<div>{{$an¦y(title)}}</div>`,
|
||||||
|
expectedSpanText: '$any',
|
||||||
|
expectedDisplayString: '(method) $any: any'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide documentation', () => {
|
||||||
|
const {position} = service.overwriteInlineTemplate(APP_COMPONENT, `<div>{{¦title}}</div>`);
|
||||||
|
const quickInfo = ngLS.getQuickInfoAtPosition(APP_COMPONENT, position);
|
||||||
|
const documentation = toText(quickInfo!.documentation);
|
||||||
|
expect(documentation).toBe('This is the title of the `AppComponent` Component.');
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(atscott): Enable once #39065 is merged
|
||||||
|
xit('works with external template', () => {
|
||||||
|
const {position, text} = service.overwrite(TEST_TEMPLATE, '<butt¦on></button>');
|
||||||
|
const quickInfo = ngLS.getQuickInfoAtPosition(TEST_TEMPLATE, position);
|
||||||
|
expect(quickInfo).toBeTruthy();
|
||||||
|
const {textSpan, displayParts} = quickInfo!;
|
||||||
|
expect(text.substring(textSpan.start, textSpan.start + textSpan.length))
|
||||||
|
.toEqual('<button></button>');
|
||||||
|
expect(toText(displayParts)).toEqual('(element) button: HTMLButtonElement');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function expectQuickInfo(
|
||||||
|
{templateOverride, expectedSpanText, expectedDisplayString}:
|
||||||
|
{templateOverride: string, expectedSpanText: string, expectedDisplayString: string}):
|
||||||
|
ts.QuickInfo {
|
||||||
|
const {position, text} = service.overwriteInlineTemplate(APP_COMPONENT, templateOverride);
|
||||||
|
const quickInfo = ngLS.getQuickInfoAtPosition(APP_COMPONENT, position);
|
||||||
|
expect(quickInfo).toBeTruthy();
|
||||||
|
const {textSpan, displayParts} = quickInfo!;
|
||||||
|
expect(text.substring(textSpan.start, textSpan.start + textSpan.length))
|
||||||
|
.toEqual(expectedSpanText);
|
||||||
|
expect(toText(displayParts)).toEqual(expectedDisplayString);
|
||||||
|
return quickInfo!;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toText(displayParts?: ts.SymbolDisplayPart[]): string {
|
||||||
|
return (displayParts || []).map(p => p.text).join('');
|
||||||
|
}
|
|
@ -28,9 +28,20 @@ export function create(info: ts.server.PluginCreateInfo): ts.LanguageService {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined {
|
||||||
|
if (angularOnly) {
|
||||||
|
return ngLS.getQuickInfoAtPosition(fileName, position);
|
||||||
|
} else {
|
||||||
|
// If TS could answer the query, then return that result. Otherwise, return from Angular LS.
|
||||||
|
return tsLS.getQuickInfoAtPosition(fileName, position) ??
|
||||||
|
ngLS.getQuickInfoAtPosition(fileName, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...tsLS,
|
...tsLS,
|
||||||
getSemanticDiagnostics,
|
getSemanticDiagnostics,
|
||||||
getTypeDefinitionAtPosition,
|
getTypeDefinitionAtPosition,
|
||||||
|
getQuickInfoAtPosition,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,228 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import {AbsoluteSourceSpan, CssSelector, ParseSourceSpan, SelectorMatcher} from '@angular/compiler';
|
||||||
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
||||||
|
import {DirectiveSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||||
|
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
|
||||||
|
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {ALIAS_NAME, SYMBOL_PUNC} from '../src/hover';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of directives and a text to use as a selector, returns the directives which match
|
||||||
|
* for the selector.
|
||||||
|
*/
|
||||||
|
export function getDirectiveMatches(
|
||||||
|
directives: DirectiveSymbol[], selector: string): Set<DirectiveSymbol> {
|
||||||
|
const selectorToMatch = CssSelector.parse(selector);
|
||||||
|
if (selectorToMatch.length === 0) {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
return new Set(directives.filter((dir: DirectiveSymbol) => {
|
||||||
|
if (dir.selector === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matcher = new SelectorMatcher();
|
||||||
|
matcher.addSelectables(CssSelector.parse(dir.selector));
|
||||||
|
|
||||||
|
return matcher.match(selectorToMatch[0], null);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTextSpanOfNode(node: t.Node|e.AST): ts.TextSpan {
|
||||||
|
if (isTemplateNodeWithKeyAndValue(node)) {
|
||||||
|
return toTextSpan(node.keySpan);
|
||||||
|
} else if (
|
||||||
|
node instanceof e.PropertyWrite || node instanceof e.MethodCall ||
|
||||||
|
node instanceof e.BindingPipe || node instanceof e.PropertyRead) {
|
||||||
|
// The `name` part of a `PropertyWrite`, `MethodCall`, and `BindingPipe` does not
|
||||||
|
// have its own AST so there is no way to retrieve a `Symbol` for just the `name` via a specific
|
||||||
|
// node.
|
||||||
|
return toTextSpan(node.nameSpan);
|
||||||
|
} else {
|
||||||
|
return toTextSpan(node.sourceSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toTextSpan(span: AbsoluteSourceSpan|ParseSourceSpan): ts.TextSpan {
|
||||||
|
let start: number, end: number;
|
||||||
|
if (span instanceof AbsoluteSourceSpan) {
|
||||||
|
start = span.start;
|
||||||
|
end = span.end;
|
||||||
|
} else {
|
||||||
|
start = span.start.offset;
|
||||||
|
end = span.end.offset;
|
||||||
|
}
|
||||||
|
return {start, length: end - start};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeWithKeyAndValue extends t.Node {
|
||||||
|
keySpan: ParseSourceSpan;
|
||||||
|
valueSpan?: ParseSourceSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTemplateNodeWithKeyAndValue(node: t.Node|e.AST): node is NodeWithKeyAndValue {
|
||||||
|
return isTemplateNode(node) && node.hasOwnProperty('keySpan');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTemplateNode(node: t.Node|e.AST): node is t.Node {
|
||||||
|
// Template node implements the Node interface so we cannot use instanceof.
|
||||||
|
return node.sourceSpan instanceof ParseSourceSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExpressionNode(node: t.Node|e.AST): node is e.AST {
|
||||||
|
return node instanceof e.AST;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateInfo {
|
||||||
|
template: t.Node[];
|
||||||
|
component: ts.ClassDeclaration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the `ts.ClassDeclaration` at a location along with its template nodes.
|
||||||
|
*/
|
||||||
|
export function getTemplateInfoAtPosition(
|
||||||
|
fileName: string, position: number, compiler: NgCompiler): TemplateInfo|undefined {
|
||||||
|
if (fileName.endsWith('.ts')) {
|
||||||
|
return getInlineTemplateInfoAtPosition(fileName, position, compiler);
|
||||||
|
} else {
|
||||||
|
return getFirstComponentForTemplateFile(fileName, compiler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First, attempt to sort component declarations by file name.
|
||||||
|
* If the files are the same, sort by start location of the declaration.
|
||||||
|
*/
|
||||||
|
function tsDeclarationSortComparator(a: ts.Declaration, b: ts.Declaration): number {
|
||||||
|
const aFile = a.getSourceFile().fileName;
|
||||||
|
const bFile = b.getSourceFile().fileName;
|
||||||
|
if (aFile < bFile) {
|
||||||
|
return -1;
|
||||||
|
} else if (aFile > bFile) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return b.getFullStart() - a.getFullStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFirstComponentForTemplateFile(fileName: string, compiler: NgCompiler): TemplateInfo|
|
||||||
|
undefined {
|
||||||
|
const templateTypeChecker = compiler.getTemplateTypeChecker();
|
||||||
|
const components = compiler.getComponentsWithTemplateFile(fileName);
|
||||||
|
const sortedComponents = Array.from(components).sort(tsDeclarationSortComparator);
|
||||||
|
for (const component of sortedComponents) {
|
||||||
|
if (!ts.isClassDeclaration(component)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const template = templateTypeChecker.getTemplate(component);
|
||||||
|
if (template === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return {template, component};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the `ts.ClassDeclaration` at a location along with its template nodes.
|
||||||
|
*/
|
||||||
|
function getInlineTemplateInfoAtPosition(
|
||||||
|
fileName: string, position: number, compiler: NgCompiler): TemplateInfo|undefined {
|
||||||
|
const sourceFile = compiler.getNextProgram().getSourceFile(fileName);
|
||||||
|
if (!sourceFile) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only support top level statements / class declarations
|
||||||
|
for (const statement of sourceFile.statements) {
|
||||||
|
if (!ts.isClassDeclaration(statement) || position < statement.pos || position > statement.end) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = compiler.getTemplateTypeChecker().getTemplate(statement);
|
||||||
|
if (template === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {template, component: statement};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an attribute name and the element or template the attribute appears on, determines which
|
||||||
|
* directives match because the attribute is present. That is, we find which directives are applied
|
||||||
|
* because of this attribute by elimination: compare the directive matches with the attribute
|
||||||
|
* present against the directive matches without it. The difference would be the directives which
|
||||||
|
* match because the attribute is present.
|
||||||
|
*
|
||||||
|
* @param attribute The attribute name to use for directive matching.
|
||||||
|
* @param hostNode The element or template node that the attribute is on.
|
||||||
|
* @param directives The list of directives to match against.
|
||||||
|
* @returns The list of directives matching the attribute via the strategy described above.
|
||||||
|
*/
|
||||||
|
export function getDirectiveMatchesForAttribute(
|
||||||
|
attribute: string, hostNode: t.Template|t.Element,
|
||||||
|
directives: DirectiveSymbol[]): Set<DirectiveSymbol> {
|
||||||
|
const attributes: Array<t.TextAttribute|t.BoundAttribute> =
|
||||||
|
[...hostNode.attributes, ...hostNode.inputs];
|
||||||
|
if (hostNode instanceof t.Template) {
|
||||||
|
attributes.push(...hostNode.templateAttrs);
|
||||||
|
}
|
||||||
|
function toAttributeString(a: t.TextAttribute|t.BoundAttribute) {
|
||||||
|
return `[${a.name}=${a.valueSpan?.toString() ?? ''}]`;
|
||||||
|
}
|
||||||
|
const attrs = attributes.map(toAttributeString);
|
||||||
|
const attrsOmit = attributes.map(a => a.name === attribute ? '' : toAttributeString(a));
|
||||||
|
|
||||||
|
const hostNodeName = hostNode instanceof t.Template ? hostNode.tagName : hostNode.name;
|
||||||
|
const directivesWithAttribute = getDirectiveMatches(directives, hostNodeName + attrs.join(''));
|
||||||
|
const directivesWithoutAttribute =
|
||||||
|
getDirectiveMatches(directives, hostNodeName + attrsOmit.join(''));
|
||||||
|
|
||||||
|
const result = new Set<DirectiveSymbol>();
|
||||||
|
for (const dir of directivesWithAttribute) {
|
||||||
|
if (!directivesWithoutAttribute.has(dir)) {
|
||||||
|
result.add(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new `ts.SymbolDisplayPart` array which has the alias imports from the tcb filtered
|
||||||
|
* out, i.e. `i0.NgForOf`.
|
||||||
|
*/
|
||||||
|
export function filterAliasImports(displayParts: ts.SymbolDisplayPart[]): ts.SymbolDisplayPart[] {
|
||||||
|
const tcbAliasImportRegex = /i\d+/;
|
||||||
|
function isImportAlias(part: {kind: string, text: string}) {
|
||||||
|
return part.kind === ALIAS_NAME && tcbAliasImportRegex.test(part.text);
|
||||||
|
}
|
||||||
|
function isDotPunctuation(part: {kind: string, text: string}) {
|
||||||
|
return part.kind === SYMBOL_PUNC && part.text === '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayParts.filter((part, i) => {
|
||||||
|
const previousPart = displayParts[i - 1];
|
||||||
|
const nextPart = displayParts[i + 1];
|
||||||
|
|
||||||
|
const aliasNameFollowedByDot =
|
||||||
|
isImportAlias(part) && nextPart !== undefined && isDotPunctuation(nextPart);
|
||||||
|
const dotPrecededByAlias =
|
||||||
|
isDotPunctuation(part) && previousPart !== undefined && isImportAlias(previousPart);
|
||||||
|
|
||||||
|
return !aliasNameFollowedByDot && !dotPrecededByAlias;
|
||||||
|
});
|
||||||
|
}
|
|
@ -13,10 +13,11 @@ import * as ng from './types';
|
||||||
import {inSpan} from './utils';
|
import {inSpan} from './utils';
|
||||||
|
|
||||||
// Reverse mappings of enum would generate strings
|
// Reverse mappings of enum would generate strings
|
||||||
const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space];
|
export const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space];
|
||||||
const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation];
|
export const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation];
|
||||||
const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.text];
|
export const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.text];
|
||||||
const SYMBOL_INTERFACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.interfaceName];
|
export const SYMBOL_INTERFACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.interfaceName];
|
||||||
|
export const ALIAS_NAME = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.aliasName];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traverse the template AST and look for the symbol located at `position`, then
|
* Traverse the template AST and look for the symbol located at `position`, then
|
||||||
|
@ -80,7 +81,7 @@ export function getTsHover(
|
||||||
* @param type user-friendly name of the type
|
* @param type user-friendly name of the type
|
||||||
* @param documentation docstring or comment
|
* @param documentation docstring or comment
|
||||||
*/
|
*/
|
||||||
function createQuickInfo(
|
export function createQuickInfo(
|
||||||
name: string, kind: string, textSpan: ts.TextSpan, containerName?: string, type?: string,
|
name: string, kind: string, textSpan: ts.TextSpan, containerName?: string, type?: string,
|
||||||
documentation?: ts.SymbolDisplayPart[]): ts.QuickInfo {
|
documentation?: ts.SymbolDisplayPart[]): ts.QuickInfo {
|
||||||
const containerDisplayParts = containerName ?
|
const containerDisplayParts = containerName ?
|
||||||
|
|
|
@ -13,6 +13,7 @@ ts_library(
|
||||||
srcs = [
|
srcs = [
|
||||||
"test_utils.ts",
|
"test_utils.ts",
|
||||||
],
|
],
|
||||||
|
visibility = ["//packages/language-service:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
"//packages/compiler-cli/test:test_utils",
|
"//packages/compiler-cli/test:test_utils",
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"fullTemplateTypeCheck": true,
|
"strictTemplates": true,
|
||||||
"strictInjectionParameters": true
|
"strictInjectionParameters": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue