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): 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(); + // Extract url path specified by the url node, which is relative to the TypeScript source file + // the url node is defined in. + const url = path.join(path.dirname(sf.fileName), urlNode.text); + + // If the file does not exist, bail. It is possible that the TypeScript language service host + // does not have a `fileExists` method, in which case optimistically assume the file exists. + if (tsLsHost.fileExists && !tsLsHost.fileExists(url)) 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. + 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, + }, + }; +} /** * Traverse the template AST and look for the symbol located at `position`, then @@ -80,3 +145,21 @@ export function getDefinitionAndBoundSpan( textSpan: symbols[0].span, }; } + +/** + * Gets an Angular-specific definition in a TypeScript source file. + */ +export function getTsDefinitionAndBoundSpan( + sf: ts.SourceFile, position: number, + tsLsHost: Readonly): 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, tsLsHost); + default: + return undefined; + } +} diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts index 1848156516..adbf824a04 100644 --- a/packages/language-service/src/diagnostics.ts +++ b/packages/language-service/src/diagnostics.ts @@ -10,11 +10,9 @@ import {NgAnalyzedModules} from '@angular/compiler'; import * as path from 'path'; import * as ts from 'typescript'; -import {findTightestNode} from '../common/ts_utils'; - import {createDiagnostic, Diagnostic} from './diagnostic_messages'; import {getTemplateExpressionDiagnostics} from './expression_diagnostics'; -import {findPropertyValueOfType} from './ts_utils'; +import {findPropertyValueOfType, findTightestNode} from './ts_utils'; import * as ng from './types'; import {TypeScriptServiceHost} from './typescript_host'; import {offsetSpan, spanOf} from './utils'; diff --git a/packages/language-service/src/hover.ts b/packages/language-service/src/hover.ts index 9bc98a3f86..3130318a80 100644 --- a/packages/language-service/src/hover.ts +++ b/packages/language-service/src/hover.ts @@ -9,8 +9,6 @@ import {NgAnalyzedModules} from '@angular/compiler'; import * as ts from 'typescript'; -import {createQuickInfo} from '../common/quick_info'; - import {locateSymbols} from './locate_symbol'; import * as ng from './types'; import {inSpan} from './utils'; @@ -67,3 +65,56 @@ export function getTsHover( } } } + + + +// Reverse mappings of enum would generate strings +const ALIAS_NAME = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.aliasName]; +const SYMBOL_INTERFACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.interfaceName]; +const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation]; +const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space]; +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 + */ +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/src/language_service.ts b/packages/language-service/src/language_service.ts index e9eeac68fe..177a73f0d6 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -9,10 +9,8 @@ import * as path from 'path'; import * as tss from 'typescript/lib/tsserverlibrary'; -import {getTsDefinitionAndBoundSpan, ResourceResolver} from '../common/definitions'; - import {getTemplateCompletions} from './completions'; -import {getDefinitionAndBoundSpan} from './definitions'; +import {getDefinitionAndBoundSpan, getTsDefinitionAndBoundSpan} from './definitions'; import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic} from './diagnostics'; import {getTemplateHover, getTsHover} from './hover'; import * as ng from './types'; @@ -83,8 +81,7 @@ class LanguageServiceImpl implements ng.LanguageService { if (fileName.endsWith('.ts')) { const sf = this.host.getSourceFile(fileName); if (sf) { - return getTsDefinitionAndBoundSpan( - sf, position, new ViewEngineLSResourceResolver(this.host.tsLsHost)); + return getTsDefinitionAndBoundSpan(sf, position, this.host.tsLsHost); } } } @@ -115,20 +112,3 @@ class LanguageServiceImpl implements ng.LanguageService { return this.host.tsLS.getReferencesAtPosition(tsDef.fileName, tsDef.textSpan.start); } } - -class ViewEngineLSResourceResolver implements ResourceResolver { - constructor(private host: ts.LanguageServiceHost) {} - - resolve(file: string, basePath: string): string { - // Extract url path specified by the url node, which is relative to the TypeScript source file - // the url node is defined in. - const url = path.join(path.dirname(basePath), file); - - // If the file does not exist, bail. It is possible that the TypeScript language service host - // does not have a `fileExists` method, in which case optimistically assume the file exists. - if (this.host.fileExists && !this.host.fileExists(url)) { - throw new Error(`ResourceResolver: could not resolve ${url} in context of ${basePath})`); - } - return url; - } -} diff --git a/packages/language-service/src/ts_utils.ts b/packages/language-service/src/ts_utils.ts index 11c756007b..ca740e43e6 100644 --- a/packages/language-service/src/ts_utils.ts +++ b/packages/language-service/src/ts_utils.ts @@ -68,3 +68,81 @@ export function findPropertyValueOfType( } return startNode.forEachChild(c => findPropertyValueOfType(c, propName, predicate)); } + +/** + * 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 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); +} + +/** + * 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; +} diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index 0921f50400..1ce6b56f8b 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -10,12 +10,10 @@ import {analyzeNgModules, AotSummaryResolver, CompileDirectiveSummary, CompileMe import {SchemaMetadata, ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import * as tss from 'typescript/lib/tsserverlibrary'; -import {findTightestNode, getClassDeclOfInlineTemplateNode} from '../common/ts_utils'; - import {createLanguageService} from './language_service'; import {ReflectorHost} from './reflector_host'; import {ExternalTemplate, InlineTemplate} from './template'; -import {getDirectiveClassLike} from './ts_utils'; +import {findTightestNode, getClassDeclOfInlineTemplateNode, getDirectiveClassLike} from './ts_utils'; import {AstResult, Declaration, DeclarationError, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types'; /** diff --git a/packages/language-service/src/utils.ts b/packages/language-service/src/utils.ts index 7a1332030c..9cbb0432fb 100644 --- a/packages/language-service/src/utils.ts +++ b/packages/language-service/src/utils.ts @@ -7,6 +7,7 @@ */ import {AstPath, BoundEventAst, CompileDirectiveSummary, CompileTypeMetadata, CssSelector, DirectiveAst, ElementAst, EmbeddedTemplateAst, HtmlAstPath, identifierName, Identifiers, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, RecursiveVisitor, TemplateAst, TemplateAstPath, templateVisitAll, visitAll} from '@angular/compiler'; +import {getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils'; import {AstResult, DiagnosticTemplateInfo, SelectorInfo, Span, Symbol, SymbolQuery} from './types'; interface SpanHolder { diff --git a/packages/language-service/test/BUILD.bazel b/packages/language-service/test/BUILD.bazel index a3d512a6c3..36bd6f7771 100644 --- a/packages/language-service/test/BUILD.bazel +++ b/packages/language-service/test/BUILD.bazel @@ -59,7 +59,6 @@ ts_library( "//packages/compiler", "//packages/language-service", "//packages/language-service:ts_utils", - "//packages/language-service/common", "@npm//typescript", ], ) diff --git a/packages/language-service/test/utils_spec.ts b/packages/language-service/test/utils_spec.ts index 24001eafff..b29b7cccb0 100644 --- a/packages/language-service/test/utils_spec.ts +++ b/packages/language-service/test/utils_spec.ts @@ -9,8 +9,7 @@ import * as ng from '@angular/compiler'; import * as ts from 'typescript'; -import {getClassDeclFromDecoratorProp} from '../common/ts_utils'; -import {getDirectiveClassLike} from '../src/ts_utils'; +import {getClassDeclFromDecoratorProp, getDirectiveClassLike} from '../src/ts_utils'; import {getPathToNodeAtPosition} from '../src/utils'; import {MockTypescriptHost} from './test_utils';