diff --git a/packages/language-service/BUILD.bazel b/packages/language-service/BUILD.bazel index 7869219b68..320cc23995 100644 --- a/packages/language-service/BUILD.bazel +++ b/packages/language-service/BUILD.bazel @@ -19,7 +19,6 @@ ts_library( "//packages/compiler", "//packages/compiler-cli", "//packages/core", - "//packages/language-service/common", "@npm//@types/node", "@npm//typescript", ], diff --git a/packages/language-service/common/BUILD.bazel b/packages/language-service/common/BUILD.bazel deleted file mode 100644 index afe8f39b56..0000000000 --- a/packages/language-service/common/BUILD.bazel +++ /dev/null @@ -1,11 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -package(default_visibility = ["//packages/language-service:__subpackages__"]) - -ts_library( - name = "common", - srcs = glob(["*.ts"]), - deps = [ - "@npm//typescript", - ], -) diff --git a/packages/language-service/common/definitions.ts b/packages/language-service/common/definitions.ts deleted file mode 100644 index 7e38cd8f8f..0000000000 --- a/packages/language-service/common/definitions.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * @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'; - -import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils'; - -export interface ResourceResolver { - /** - * Resolve the url of a resource relative to the file that contains the reference to it. - * - * @param file The, possibly relative, url of the resource. - * @param basePath The path to the file that contains the URL of the resource. - * @returns A resolved url of resource. - * @throws An error if the resource cannot be resolved. - */ - resolve(file: string, basePath: string): string; -} - -/** - * Gets an Angular-specific definition in a TypeScript source file. - */ -export function getTsDefinitionAndBoundSpan( - sf: ts.SourceFile, position: number, - resourceResolver: ResourceResolver): ts.DefinitionInfoAndBoundSpan|undefined { - const node = findTightestNode(sf, position); - if (!node) return; - switch (node.kind) { - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - // Attempt to extract definition of a URL in a property assignment. - return getUrlFromProperty(node as ts.StringLiteralLike, resourceResolver); - default: - return undefined; - } -} - -/** - * Attempts to get the definition of a file whose URL is specified in a property assignment in a - * directive decorator. - * Currently applies to `templateUrl` and `styleUrls` properties. - */ -function getUrlFromProperty(urlNode: ts.StringLiteralLike, resourceResolver: ResourceResolver): - ts.DefinitionInfoAndBoundSpan|undefined { - // Get the property assignment node corresponding to the `templateUrl` or `styleUrls` assignment. - // These assignments are specified differently; `templateUrl` is a string, and `styleUrls` is - // an array of strings: - // { - // templateUrl: './template.ng.html', - // styleUrls: ['./style.css', './other-style.css'] - // } - // `templateUrl`'s property assignment can be found from the string literal node; - // `styleUrls`'s property assignment can be found from the array (parent) node. - // - // First search for `templateUrl`. - let asgn = getPropertyAssignmentFromValue(urlNode, 'templateUrl'); - if (!asgn) { - // `templateUrl` assignment not found; search for `styleUrls` array assignment. - asgn = getPropertyAssignmentFromValue(urlNode.parent, 'styleUrls'); - if (!asgn) { - // Nothing found, bail. - return; - } - } - - // If the property assignment is not a property of a class decorator, don't generate definitions - // for it. - if (!getClassDeclFromDecoratorProp(asgn)) { - return; - } - - const sf = urlNode.getSourceFile(); - let url: string; - try { - url = resourceResolver.resolve(urlNode.text, sf.fileName); - } catch { - // If the file does not exist, bail. - return; - } - - const templateDefinitions: ts.DefinitionInfo[] = [{ - kind: ts.ScriptElementKind.externalModuleName, - name: url, - containerKind: ts.ScriptElementKind.unknown, - containerName: '', - // Reading the template is expensive, so don't provide a preview. - // TODO(ayazhafiz): Consider providing an actual span: - // 1. We're likely to read the template anyway - // 2. We could show just the first 100 chars or so - textSpan: {start: 0, length: 0}, - fileName: url, - }]; - - return { - definitions: templateDefinitions, - textSpan: { - // Exclude opening and closing quotes in the url span. - start: urlNode.getStart() + 1, - length: urlNode.getWidth() - 2, - }, - }; -} diff --git a/packages/language-service/common/quick_info.ts b/packages/language-service/common/quick_info.ts deleted file mode 100644 index 9f1076e98a..0000000000 --- a/packages/language-service/common/quick_info.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @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'; - -// Reverse mappings of enum would generate strings -export const ALIAS_NAME = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.aliasName]; -export const SYMBOL_INTERFACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.interfaceName]; -export const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation]; -export const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space]; -export const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.text]; - -/** - * Construct a QuickInfo object taking into account its container and type. - * @param name Name of the QuickInfo target - * @param kind component, directive, pipe, etc. - * @param textSpan span of the target - * @param containerName either the Symbol's container or the NgModule that contains the directive - * @param type user-friendly name of the type - * @param documentation docstring or comment - */ -export function createQuickInfo( - name: string, kind: string, textSpan: ts.TextSpan, containerName?: string, type?: string, - documentation?: ts.SymbolDisplayPart[]): ts.QuickInfo { - const containerDisplayParts = containerName ? - [ - {text: containerName, kind: SYMBOL_INTERFACE}, - {text: '.', kind: SYMBOL_PUNC}, - ] : - []; - - const typeDisplayParts = type ? - [ - {text: ':', kind: SYMBOL_PUNC}, - {text: ' ', kind: SYMBOL_SPACE}, - {text: type, kind: SYMBOL_INTERFACE}, - ] : - []; - - return { - kind: kind as ts.ScriptElementKind, - kindModifiers: ts.ScriptElementKindModifier.none, - textSpan: textSpan, - displayParts: [ - {text: '(', kind: SYMBOL_PUNC}, - {text: kind, kind: SYMBOL_TEXT}, - {text: ')', kind: SYMBOL_PUNC}, - {text: ' ', kind: SYMBOL_SPACE}, - ...containerDisplayParts, - {text: name, kind: SYMBOL_INTERFACE}, - ...typeDisplayParts, - ], - documentation, - }; -} diff --git a/packages/language-service/common/ts_utils.ts b/packages/language-service/common/ts_utils.ts deleted file mode 100644 index 3ff50242e0..0000000000 --- a/packages/language-service/common/ts_utils.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @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'; - -/** - * Return the node that most tightly encompass the specified `position`. - * @param node - * @param position - */ -export function findTightestNode(node: ts.Node, position: number): ts.Node|undefined { - if (node.getStart() <= position && position < node.getEnd()) { - return node.forEachChild(c => findTightestNode(c, position)) || node; - } -} - -/** - * Returns a property assignment from the assignment value if the property name - * matches the specified `key`, or `undefined` if there is no match. - */ -export function getPropertyAssignmentFromValue(value: ts.Node, key: string): ts.PropertyAssignment| - undefined { - const propAssignment = value.parent; - if (!propAssignment || !ts.isPropertyAssignment(propAssignment) || - propAssignment.name.getText() !== key) { - return; - } - return propAssignment; -} - -/** - * Given a decorator property assignment, return the ClassDeclaration node that corresponds to the - * directive class the property applies to. - * If the property assignment is not on a class decorator, no declaration is returned. - * - * For example, - * - * @Component({ - * template: '
' - * ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment - * }) - * class AppComponent {} - * ^---- class declaration node - * - * @param propAsgnNode property assignment - */ -export function getClassDeclFromDecoratorProp(propAsgnNode: ts.PropertyAssignment): - ts.ClassDeclaration|undefined { - if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) { - return; - } - const objLitExprNode = propAsgnNode.parent; - if (!objLitExprNode.parent || !ts.isCallExpression(objLitExprNode.parent)) { - return; - } - const callExprNode = objLitExprNode.parent; - if (!callExprNode.parent || !ts.isDecorator(callExprNode.parent)) { - return; - } - const decorator = callExprNode.parent; - if (!decorator.parent || !ts.isClassDeclaration(decorator.parent)) { - return; - } - const classDeclNode = decorator.parent; - return classDeclNode; -} - -/** - * Given the node which is the string of the inline template for a component, returns the - * `ts.ClassDeclaration` for the component. - */ -export function getClassDeclOfInlineTemplateNode(templateStringNode: ts.Node): ts.ClassDeclaration| - undefined { - if (!ts.isStringLiteralLike(templateStringNode)) { - return; - } - const tmplAsgn = getPropertyAssignmentFromValue(templateStringNode, 'template'); - if (!tmplAsgn) { - return; - } - return getClassDeclFromDecoratorProp(tmplAsgn); -} diff --git a/packages/language-service/ivy/BUILD.bazel b/packages/language-service/ivy/BUILD.bazel index c8df602d71..6a48cdcde5 100644 --- a/packages/language-service/ivy/BUILD.bazel +++ b/packages/language-service/ivy/BUILD.bazel @@ -17,7 +17,6 @@ ts_library( "//packages/compiler-cli/src/ngtsc/shims", "//packages/compiler-cli/src/ngtsc/typecheck", "//packages/compiler-cli/src/ngtsc/typecheck/api", - "//packages/language-service/common", "@npm//typescript", ], ) diff --git a/packages/language-service/ivy/definitions.ts b/packages/language-service/ivy/definitions.ts index 48fb9d9715..94f2feeb73 100644 --- a/packages/language-service/ivy/definitions.ts +++ b/packages/language-service/ivy/definitions.ts @@ -11,10 +11,106 @@ import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ShimLocation, Symbol, SymbolKind, TemplateSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import * as ts from 'typescript'; -import {getTsDefinitionAndBoundSpan, ResourceResolver} from '../common/definitions'; - import {getPathToNodeAtPosition} from './hybrid_visitor'; -import {flatMap, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTextSpanOfNode, isDollarEvent, isTypeScriptFile, TemplateInfo, toTextSpan} from './utils'; +import {findTightestNode, flatMap, getClassDeclFromDecoratorProp, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getPropertyAssignmentFromValue, getTemplateInfoAtPosition, getTextSpanOfNode, isDollarEvent, isTypeScriptFile, TemplateInfo, toTextSpan} from './utils'; + + +export interface ResourceResolver { + /** + * Resolve the url of a resource relative to the file that contains the reference to it. + * + * @param file The, possibly relative, url of the resource. + * @param basePath The path to the file that contains the URL of the resource. + * @returns A resolved url of resource. + * @throws An error if the resource cannot be resolved. + */ + resolve(file: string, basePath: string): string; +} + +/** + * Gets an Angular-specific definition in a TypeScript source file. + */ +export function getTsDefinitionAndBoundSpan( + sf: ts.SourceFile, position: number, + resourceResolver: ResourceResolver): ts.DefinitionInfoAndBoundSpan|undefined { + const node = findTightestNode(sf, position); + if (!node) return; + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + // Attempt to extract definition of a URL in a property assignment. + return getUrlFromProperty(node as ts.StringLiteralLike, resourceResolver); + default: + return undefined; + } +} + +/** + * Attempts to get the definition of a file whose URL is specified in a property assignment in a + * directive decorator. + * Currently applies to `templateUrl` and `styleUrls` properties. + */ +function getUrlFromProperty(urlNode: ts.StringLiteralLike, resourceResolver: ResourceResolver): + ts.DefinitionInfoAndBoundSpan|undefined { + // Get the property assignment node corresponding to the `templateUrl` or `styleUrls` assignment. + // These assignments are specified differently; `templateUrl` is a string, and `styleUrls` is + // an array of strings: + // { + // templateUrl: './template.ng.html', + // styleUrls: ['./style.css', './other-style.css'] + // } + // `templateUrl`'s property assignment can be found from the string literal node; + // `styleUrls`'s property assignment can be found from the array (parent) node. + // + // First search for `templateUrl`. + let asgn = getPropertyAssignmentFromValue(urlNode, 'templateUrl'); + if (!asgn) { + // `templateUrl` assignment not found; search for `styleUrls` array assignment. + asgn = getPropertyAssignmentFromValue(urlNode.parent, 'styleUrls'); + if (!asgn) { + // Nothing found, bail. + return; + } + } + + // If the property assignment is not a property of a class decorator, don't generate definitions + // for it. + if (!getClassDeclFromDecoratorProp(asgn)) { + return; + } + + const sf = urlNode.getSourceFile(); + let url: string; + try { + url = resourceResolver.resolve(urlNode.text, sf.fileName); + } catch { + // If the file does not exist, bail. + return; + } + + const templateDefinitions: ts.DefinitionInfo[] = [{ + kind: ts.ScriptElementKind.externalModuleName, + name: url, + containerKind: ts.ScriptElementKind.unknown, + containerName: '', + // Reading the template is expensive, so don't provide a preview. + // TODO(ayazhafiz): Consider providing an actual span: + // 1. We're likely to read the template anyway + // 2. We could show just the first 100 chars or so + textSpan: {start: 0, length: 0}, + fileName: url, + }]; + + return { + definitions: templateDefinitions, + textSpan: { + // Exclude opening and closing quotes in the url span. + start: urlNode.getStart() + 1, + length: urlNode.getWidth() - 2, + }, + }; +} + interface DefinitionMeta { node: AST|TmplAstNode; diff --git a/packages/language-service/ivy/display_parts.ts b/packages/language-service/ivy/display_parts.ts new file mode 100644 index 0000000000..aff731fbf6 --- /dev/null +++ b/packages/language-service/ivy/display_parts.ts @@ -0,0 +1,126 @@ +/** + * @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 {ReferenceSymbol, ShimLocation, Symbol, SymbolKind, VariableSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; +import * as ts from 'typescript'; + + +// Reverse mappings of enum would generate strings +export const ALIAS_NAME = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.aliasName]; +export const SYMBOL_INTERFACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.interfaceName]; +export const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation]; +export const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space]; +export const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.text]; + + +/** + * Label for various kinds of Angular entities for TS display info. + */ +export enum DisplayInfoKind { + COMPONENT = 'component', + DIRECTIVE = 'directive', + EVENT = 'event', + REFERENCE = 'reference', + ELEMENT = 'element', + VARIABLE = 'variable', + PIPE = 'pipe', + PROPERTY = 'property', + METHOD = 'method', + TEMPLATE = 'template', +} + +export interface DisplayInfo { + kind: DisplayInfoKind; + displayParts: ts.SymbolDisplayPart[]; + documentation: ts.SymbolDisplayPart[]|undefined; +} + +export function getDisplayInfo( + tsLS: ts.LanguageService, typeChecker: ts.TypeChecker, + symbol: ReferenceSymbol|VariableSymbol): DisplayInfo { + let kind: DisplayInfoKind; + if (symbol.kind === SymbolKind.Reference) { + kind = DisplayInfoKind.REFERENCE; + } else if (symbol.kind === SymbolKind.Variable) { + kind = DisplayInfoKind.VARIABLE; + } else { + throw new Error( + `AssertionError: unexpected symbol kind ${SymbolKind[(symbol as Symbol).kind]}`); + } + + const displayParts = createDisplayParts( + symbol.declaration.name, kind, /* containerName */ undefined, + typeChecker.typeToString(symbol.tsType)); + const documentation = getDocumentationFromTypeDefAtLocation(tsLS, symbol.shimLocation); + return { + kind, + displayParts, + documentation, + }; +} + +/** + * Construct a compound `ts.SymbolDisplayPart[]` which incorporates the container and type of a + * target declaration. + * @param name Name of the target + * @param kind component, directive, pipe, etc. + * @param containerName either the Symbol's container or the NgModule that contains the directive + * @param type user-friendly name of the type + * @param documentation docstring or comment + */ +export function createDisplayParts( + name: string, kind: DisplayInfoKind, containerName: string|undefined, + type: string|undefined): ts.SymbolDisplayPart[] { + const containerDisplayParts = containerName !== undefined ? + [ + {text: containerName, kind: SYMBOL_INTERFACE}, + {text: '.', kind: SYMBOL_PUNC}, + ] : + []; + + const typeDisplayParts = type !== undefined ? + [ + {text: ':', kind: SYMBOL_PUNC}, + {text: ' ', kind: SYMBOL_SPACE}, + {text: type, kind: SYMBOL_INTERFACE}, + ] : + []; + return [ + {text: '(', kind: SYMBOL_PUNC}, + {text: kind, kind: SYMBOL_TEXT}, + {text: ')', kind: SYMBOL_PUNC}, + {text: ' ', kind: SYMBOL_SPACE}, + ...containerDisplayParts, + {text: name, kind: SYMBOL_INTERFACE}, + ...typeDisplayParts, + ]; +} + +/** + * Convert a `SymbolDisplayInfoKind` to a `ts.ScriptElementKind` type, allowing it to pass through + * TypeScript APIs. + * + * In practice, this is an "illegal" type cast. Since `ts.ScriptElementKind` is a string, this is + * safe to do if TypeScript only uses the value in a string context. Consumers of this conversion + * function are responsible for ensuring this is the case. + */ +export function unsafeCastDisplayInfoKindToScriptElementKind(kind: DisplayInfoKind): + ts.ScriptElementKind { + return kind as string as ts.ScriptElementKind; +} + +function getDocumentationFromTypeDefAtLocation( + tsLS: ts.LanguageService, shimLocation: ShimLocation): ts.SymbolDisplayPart[]|undefined { + const typeDefs = + tsLS.getTypeDefinitionAtPosition(shimLocation.shimPath, shimLocation.positionInShimFile); + if (typeDefs === undefined || typeDefs.length === 0) { + return undefined; + } + return tsLS.getQuickInfoAtPosition(typeDefs[0].fileName, typeDefs[0].textSpan.start) + ?.documentation; +} diff --git a/packages/language-service/ivy/language_service_adapter.ts b/packages/language-service/ivy/language_service_adapter.ts index 7b2cc77395..377eea2779 100644 --- a/packages/language-service/ivy/language_service_adapter.ts +++ b/packages/language-service/ivy/language_service_adapter.ts @@ -11,8 +11,7 @@ import {absoluteFrom, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file import {AdapterResourceLoader} from '@angular/compiler-cli/src/ngtsc/resource'; import {isShim} from '@angular/compiler-cli/src/ngtsc/shims'; import * as ts from 'typescript/lib/tsserverlibrary'; - -import {ResourceResolver} from '../common/definitions'; +import {ResourceResolver} from './definitions'; import {isTypeScriptFile} from './utils'; diff --git a/packages/language-service/ivy/quick_info.ts b/packages/language-service/ivy/quick_info.ts index d1e9123e5a..312c23fddd 100644 --- a/packages/language-service/ivy/quick_info.ts +++ b/packages/language-service/ivy/quick_info.ts @@ -10,27 +10,10 @@ 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 '../common/quick_info'; - +import {createDisplayParts, DisplayInfoKind, SYMBOL_PUNC, SYMBOL_SPACE, SYMBOL_TEXT, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts'; import {findNodeAtPosition} from './hybrid_visitor'; import {filterAliasImports, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, 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) {} @@ -86,7 +69,8 @@ export class QuickInfoBuilder { return undefined; } - const kind = symbol.kind === SymbolKind.Input ? QuickInfoKind.PROPERTY : QuickInfoKind.EVENT; + const kind = + symbol.kind === SymbolKind.Input ? DisplayInfoKind.PROPERTY : DisplayInfoKind.EVENT; const quickInfo = this.getQuickInfoAtShimLocation(symbol.bindings[0].shimLocation, node); return quickInfo === undefined ? undefined : updateQuickInfoKind(quickInfo, kind); @@ -100,7 +84,7 @@ export class QuickInfoBuilder { } return createQuickInfo( - templateNode.name, QuickInfoKind.ELEMENT, getTextSpanOfNode(templateNode), + templateNode.name, DisplayInfoKind.ELEMENT, getTextSpanOfNode(templateNode), undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType)); } @@ -108,7 +92,7 @@ export class QuickInfoBuilder { ts.QuickInfo { const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.shimLocation); return createQuickInfo( - symbol.declaration.name, QuickInfoKind.VARIABLE, getTextSpanOfNode(node), + symbol.declaration.name, DisplayInfoKind.VARIABLE, getTextSpanOfNode(node), undefined /* containerName */, this.typeChecker.typeToString(symbol.tsType), documentation); } @@ -116,14 +100,15 @@ export class QuickInfoBuilder { ts.QuickInfo { const documentation = this.getDocumentationFromTypeDefAtLocation(symbol.shimLocation); return createQuickInfo( - symbol.declaration.name, QuickInfoKind.REFERENCE, getTextSpanOfNode(node), + symbol.declaration.name, DisplayInfoKind.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); + return quickInfo === undefined ? undefined : + updateQuickInfoKind(quickInfo, DisplayInfoKind.PIPE); } private getQuickInfoForDomBinding(node: TmplAstNode|AST, symbol: DomBindingSymbol) { @@ -141,7 +126,7 @@ export class QuickInfoBuilder { private getQuickInfoForDirectiveSymbol(dir: DirectiveSymbol, node: TmplAstNode|AST): ts.QuickInfo { - const kind = dir.isComponent ? QuickInfoKind.COMPONENT : QuickInfoKind.DIRECTIVE; + const kind = dir.isComponent ? DisplayInfoKind.COMPONENT : DisplayInfoKind.DIRECTIVE; const documentation = this.getDocumentationFromTypeDefAtLocation(dir.shimLocation); let containerName: string|undefined; if (ts.isClassDeclaration(dir.tsSymbol.valueDeclaration) && dir.ngModule !== null) { @@ -179,7 +164,7 @@ export class QuickInfoBuilder { } } -function updateQuickInfoKind(quickInfo: ts.QuickInfo, kind: QuickInfoKind): ts.QuickInfo { +function updateQuickInfoKind(quickInfo: ts.QuickInfo, kind: DisplayInfoKind): ts.QuickInfo { if (quickInfo.displayParts === undefined) { return quickInfo; } @@ -214,7 +199,7 @@ function isDollarAny(node: TmplAstNode|AST): node is MethodCall { function createDollarAnyQuickInfo(node: MethodCall): ts.QuickInfo { return createQuickInfo( '$any', - QuickInfoKind.METHOD, + DisplayInfoKind.METHOD, getTextSpanOfNode(node), /** containerName */ undefined, 'any', @@ -229,7 +214,7 @@ function createDollarAnyQuickInfo(node: MethodCall): ts.QuickInfo { function createNgTemplateQuickInfo(node: TmplAstNode|AST): ts.QuickInfo { return createQuickInfo( 'ng-template', - QuickInfoKind.TEMPLATE, + DisplayInfoKind.TEMPLATE, getTextSpanOfNode(node), /** containerName */ undefined, /** type */ undefined, @@ -240,3 +225,26 @@ function createNgTemplateQuickInfo(node: TmplAstNode|AST): ts.QuickInfo { }], ); } + +/** + * Construct a QuickInfo object taking into account its container and type. + * @param name Name of the QuickInfo target + * @param kind component, directive, pipe, etc. + * @param textSpan span of the target + * @param containerName either the Symbol's container or the NgModule that contains the directive + * @param type user-friendly name of the type + * @param documentation docstring or comment + */ +export function createQuickInfo( + name: string, kind: DisplayInfoKind, textSpan: ts.TextSpan, containerName?: string, + type?: string, documentation?: ts.SymbolDisplayPart[]): ts.QuickInfo { + const displayParts = createDisplayParts(name, kind, containerName, type); + + return { + kind: unsafeCastDisplayInfoKindToScriptElementKind(kind), + kindModifiers: ts.ScriptElementKindModifier.none, + textSpan: textSpan, + displayParts, + documentation, + }; +} \ No newline at end of file diff --git a/packages/language-service/ivy/utils.ts b/packages/language-service/ivy/utils.ts index e2481dde42..266c43f1f4 100644 --- a/packages/language-service/ivy/utils.ts +++ b/packages/language-service/ivy/utils.ts @@ -13,8 +13,88 @@ import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expr 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 '../common/quick_info'; -import {findTightestNode, getClassDeclOfInlineTemplateNode} from '../common/ts_utils'; +import {ALIAS_NAME, SYMBOL_PUNC} from './display_parts'; + + + +/** + * Return the node that most tightly encompass the specified `position`. + * @param node + * @param position + */ +export function findTightestNode(node: ts.Node, position: number): ts.Node|undefined { + if (node.getStart() <= position && position < node.getEnd()) { + return node.forEachChild(c => findTightestNode(c, position)) || node; + } +} + +/** + * Returns a property assignment from the assignment value if the property name + * matches the specified `key`, or `undefined` if there is no match. + */ +export function getPropertyAssignmentFromValue(value: ts.Node, key: string): ts.PropertyAssignment| + undefined { + const propAssignment = value.parent; + if (!propAssignment || !ts.isPropertyAssignment(propAssignment) || + propAssignment.name.getText() !== key) { + return; + } + return propAssignment; +} + +/** + * Given a decorator property assignment, return the ClassDeclaration node that corresponds to the + * directive class the property applies to. + * If the property assignment is not on a class decorator, no declaration is returned. + * + * For example, + * + * @Component({ + * template: '' + * ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment + * }) + * class AppComponent {} + * ^---- class declaration node + * + * @param propAsgnNode property assignment + */ +export function getClassDeclFromDecoratorProp(propAsgnNode: ts.PropertyAssignment): + ts.ClassDeclaration|undefined { + if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) { + return; + } + const objLitExprNode = propAsgnNode.parent; + if (!objLitExprNode.parent || !ts.isCallExpression(objLitExprNode.parent)) { + return; + } + const callExprNode = objLitExprNode.parent; + if (!callExprNode.parent || !ts.isDecorator(callExprNode.parent)) { + return; + } + const decorator = callExprNode.parent; + if (!decorator.parent || !ts.isClassDeclaration(decorator.parent)) { + return; + } + const classDeclNode = decorator.parent; + return classDeclNode; +} + +/** + * Given the node which is the string of the inline template for a component, returns the + * `ts.ClassDeclaration` for the component. + */ +export function getClassDeclOfInlineTemplateNode(templateStringNode: ts.Node): ts.ClassDeclaration| + undefined { + if (!ts.isStringLiteralLike(templateStringNode)) { + return; + } + const tmplAsgn = getPropertyAssignmentFromValue(templateStringNode, 'template'); + if (!tmplAsgn) { + return; + } + return getClassDeclFromDecoratorProp(tmplAsgn); +} + export function getTextSpanOfNode(node: t.Node|e.AST): ts.TextSpan { if (isTemplateNodeWithKeyAndValue(node)) { diff --git a/packages/language-service/src/definitions.ts b/packages/language-service/src/definitions.ts index 6539895d68..620e954e5e 100644 --- a/packages/language-service/src/definitions.ts +++ b/packages/language-service/src/definitions.ts @@ -6,9 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import * as path from 'path'; import * as ts from 'typescript'; // used as value and is provided at runtime import {locateSymbols} from './locate_symbol'; +import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils'; import {AstResult, Span} from './types'; /** @@ -22,6 +24,69 @@ function ngSpanToTsTextSpan(span: Span): ts.TextSpan { length: span.end - span.start, }; } +/** + * Attempts to get the definition of a file whose URL is specified in a property assignment in a + * directive decorator. + * Currently applies to `templateUrl` and `styleUrls` properties. + */ +function getUrlFromProperty( + urlNode: ts.StringLiteralLike, + tsLsHost: Readonly