From e409ed0eabb1331b64dac5cacf0f06cd6b2f41a0 Mon Sep 17 00:00:00 2001 From: ayazhafiz Date: Fri, 11 Oct 2019 19:15:07 -0500 Subject: [PATCH] feat(language-service): hover information for component NgModules (#33118) Enables providing information about the NgModule a component is in when its selector is hovered on in a template. Also enables differentiation of a component and a directive when a directive class name is hovered over in a TypeScript file. Next step is to enable hover information for directives. Part of #32565. PR Close #33118 --- packages/language-service/src/hover.ts | 53 ++++++++++++------- .../language-service/src/language_service.ts | 2 +- .../language-service/src/locate_symbol.ts | 15 ++++-- packages/language-service/test/hover_spec.ts | 23 ++++++-- 4 files changed, 64 insertions(+), 29 deletions(-) diff --git a/packages/language-service/src/hover.ts b/packages/language-service/src/hover.ts index 899d9c0e65..9860f7eb3f 100644 --- a/packages/language-service/src/hover.ts +++ b/packages/language-service/src/hover.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {CompileSummaryKind, StaticSymbol} from '@angular/compiler'; import * as ts from 'typescript'; import {AstResult} from './common'; import {locateSymbol} from './locate_symbol'; @@ -23,13 +24,21 @@ const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.text]; * return the corresponding quick info. * @param info template AST * @param position location of the symbol + * @param host Language Service host to query */ -export function getHover(info: AstResult, position: number): ts.QuickInfo|undefined { +export function getHover(info: AstResult, position: number, host: Readonly): + ts.QuickInfo|undefined { const symbolInfo = locateSymbol(info, position); if (!symbolInfo) { return; } - const {symbol, span} = symbolInfo; + const {symbol, span, compileTypeSummary} = symbolInfo; + const textSpan = {start: span.start, length: span.end - span.start}; + + if (compileTypeSummary && compileTypeSummary.summaryKind === CompileSummaryKind.Directive) { + return getDirectiveModule(compileTypeSummary.type.reference, textSpan, host); + } + const containerDisplayParts: ts.SymbolDisplayPart[] = symbol.container ? [ {text: symbol.container.name, kind: symbol.container.kind}, @@ -39,10 +48,7 @@ export function getHover(info: AstResult, position: number): ts.QuickInfo|undefi return { kind: symbol.kind as ts.ScriptElementKind, kindModifiers: '', // kindModifier info not available on 'ng.Symbol' - textSpan: { - start: span.start, - length: span.end - span.start, - }, + textSpan, // this would generate a string like '(property) ClassX.propY' // 'kind' in displayParts does not really matter because it's dropped when // displayParts get converted to string. @@ -69,7 +75,17 @@ export function getTsHover( if (!node) return; switch (node.kind) { case ts.SyntaxKind.Identifier: - return getDirectiveModule(node as ts.Identifier, host); + const directiveId = node as ts.Identifier; + if (ts.isClassDeclaration(directiveId.parent)) { + const directiveName = directiveId.text; + const directiveSymbol = host.getStaticSymbol(node.getSourceFile().fileName, directiveName); + if (!directiveSymbol) return; + return getDirectiveModule( + directiveSymbol, + {start: directiveId.getStart(), length: directiveId.end - directiveId.getStart()}, + host); + } + break; default: break; } @@ -82,36 +98,33 @@ export function getTsHover( * @param host Language Service host to query */ function getDirectiveModule( - directive: ts.Identifier, host: Readonly): ts.QuickInfo|undefined { - if (!ts.isClassDeclaration(directive.parent)) return; - const directiveName = directive.text; - const directiveSymbol = host.getStaticSymbol(directive.getSourceFile().fileName, directiveName); - if (!directiveSymbol) return; - + directive: StaticSymbol, textSpan: ts.TextSpan, + host: Readonly): ts.QuickInfo|undefined { const analyzedModules = host.getAnalyzedModules(false); - const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(directiveSymbol); + const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(directive); if (!ngModule) return; + const isComponent = + host.getDeclarations(directive.filePath) + .find(decl => decl.type === directive && decl.metadata && decl.metadata.isComponent); + const moduleName = ngModule.type.reference.name; return { kind: ts.ScriptElementKind.classElement, kindModifiers: ts.ScriptElementKindModifier.none, // kindModifier info not available on 'ng.Symbol' - textSpan: { - start: directive.getStart(), - length: directive.end - directive.getStart(), - }, + textSpan, // This generates a string like '(directive) NgModule.Directive: class' // 'kind' in displayParts does not really matter because it's dropped when // displayParts get converted to string. displayParts: [ {text: '(', kind: SYMBOL_PUNC}, - {text: 'directive', kind: SYMBOL_TEXT}, + {text: isComponent ? 'component' : 'directive', kind: SYMBOL_TEXT}, {text: ')', kind: SYMBOL_PUNC}, {text: ' ', kind: SYMBOL_SPACE}, {text: moduleName, kind: SYMBOL_CLASS}, {text: '.', kind: SYMBOL_PUNC}, - {text: directiveName, kind: SYMBOL_CLASS}, + {text: directive.name, kind: SYMBOL_CLASS}, {text: ':', kind: SYMBOL_PUNC}, {text: ' ', kind: SYMBOL_SPACE}, {text: ts.ScriptElementKind.classElement, kind: SYMBOL_TEXT}, diff --git a/packages/language-service/src/language_service.ts b/packages/language-service/src/language_service.ts index 537882bfc9..476506db24 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -95,7 +95,7 @@ class LanguageServiceImpl implements LanguageService { this.host.getAnalyzedModules(); // same role as 'synchronizeHostData' const templateInfo = this.host.getTemplateAstAtPosition(fileName, position); if (templateInfo) { - return getHover(templateInfo, position); + return getHover(templateInfo, position, this.host); } // Attempt to get Angular-specific hover information in a TypeScript file, the NgModule a diff --git a/packages/language-service/src/locate_symbol.ts b/packages/language-service/src/locate_symbol.ts index 827bc136c6..8a2fa325cc 100644 --- a/packages/language-service/src/locate_symbol.ts +++ b/packages/language-service/src/locate_symbol.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, ElementAst, TemplateAstPath, findNode, tokenReference} from '@angular/compiler'; +import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, CompileTypeSummary, ElementAst, TemplateAstPath, findNode, tokenReference} from '@angular/compiler'; import {getExpressionScope} from '@angular/compiler-cli/src/language_services'; import {AstResult} from './common'; @@ -17,6 +17,7 @@ import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, inSpan, offsetSpan, s export interface SymbolInfo { symbol: Symbol; span: Span; + compileTypeSummary: CompileTypeSummary|undefined; } /** @@ -27,6 +28,7 @@ export interface SymbolInfo { export function locateSymbol(info: AstResult, position: number): SymbolInfo|undefined { const templatePosition = position - info.template.span.start; const path = findTemplateAstAt(info.templateAst, templatePosition); + let compileTypeSummary: CompileTypeSummary|undefined = undefined; if (path.tail) { let symbol: Symbol|undefined = undefined; let span: Span|undefined = undefined; @@ -57,7 +59,8 @@ export function locateSymbol(info: AstResult, position: number): SymbolInfo|unde visitElement(ast) { const component = ast.directives.find(d => d.directive.isComponent); if (component) { - symbol = info.template.query.getTypeSymbol(component.directive.type.reference); + compileTypeSummary = component.directive; + symbol = info.template.query.getTypeSymbol(compileTypeSummary.type.reference); symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.COMPONENT); span = spanOf(ast); } else { @@ -65,7 +68,8 @@ export function locateSymbol(info: AstResult, position: number): SymbolInfo|unde const directive = ast.directives.find( d => d.directive.selector != null && d.directive.selector.indexOf(ast.name) >= 0); if (directive) { - symbol = info.template.query.getTypeSymbol(directive.directive.type.reference); + compileTypeSummary = directive.directive; + symbol = info.template.query.getTypeSymbol(compileTypeSummary.type.reference); symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.DIRECTIVE); span = spanOf(ast); } @@ -100,7 +104,8 @@ export function locateSymbol(info: AstResult, position: number): SymbolInfo|unde }, visitText(ast) {}, visitDirective(ast) { - symbol = info.template.query.getTypeSymbol(ast.directive.type.reference); + compileTypeSummary = ast.directive; + symbol = info.template.query.getTypeSymbol(compileTypeSummary.type.reference); span = spanOf(ast); }, visitDirectiveProperty(ast) { @@ -112,7 +117,7 @@ export function locateSymbol(info: AstResult, position: number): SymbolInfo|unde }, null); if (symbol && span) { - return {symbol, span: offsetSpan(span, info.template.span.start)}; + return {symbol, span: offsetSpan(span, info.template.span.start), compileTypeSummary}; } } } diff --git a/packages/language-service/test/hover_spec.ts b/packages/language-service/test/hover_spec.ts index 3d3a69e9b9..07ab3193e1 100644 --- a/packages/language-service/test/hover_spec.ts +++ b/packages/language-service/test/hover_spec.ts @@ -103,7 +103,7 @@ describe('hover', () => { expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo !; expect(textSpan).toEqual(marker); - expect(toText(displayParts)).toBe('(component) TestComponent'); + expect(toText(displayParts)).toBe('(component) AppModule.TestComponent: class'); }); it('should be able to find an event provider', () => { @@ -149,7 +149,7 @@ describe('hover', () => { expect(quickInfo).toBeUndefined(); }); - it('should be able to find a directive module', () => { + it('should be able to find the NgModule of a component', () => { const fileName = '/app/app.component.ts'; mockHost.override(fileName, ` import {Component} from '@angular/core'; @@ -165,7 +165,24 @@ describe('hover', () => { expect(quickInfo).toBeTruthy(); const {textSpan, displayParts} = quickInfo !; expect(textSpan).toEqual(marker); - expect(toText(displayParts)).toBe('(directive) AppModule.AppComponent: class'); + expect(toText(displayParts)).toBe('(component) AppModule.AppComponent: class'); + }); + + it('should be able to find the NgModule of a directive', () => { + const fileName = '/app/parsing-cases.ts'; + mockHost.override(fileName, ` + import {Directive} from '@angular/core'; + + @Directive({ + selector: '[string-model]', + }) + export class «StringModel» {}`); + const marker = mockHost.getReferenceMarkerFor(fileName, 'StringModel'); + const quickInfo = ngLS.getHoverAt(fileName, marker.start); + expect(quickInfo).toBeTruthy(); + const {textSpan, displayParts} = quickInfo !; + expect(textSpan).toEqual(marker); + expect(toText(displayParts)).toBe('(directive) AppModule.StringModel: class'); }); });