From 0d186dda35ec236fb7f02ccb90720c0585cd0301 Mon Sep 17 00:00:00 2001 From: ayazhafiz Date: Mon, 16 Sep 2019 21:07:43 -0500 Subject: [PATCH] feat(language-service): module definitions on directive hover (#32763) Adds information about the NgModule a Directive is declared in when the Directive class name is hovered over, in the form ``` (directive) NgModule.Directive: class ``` Closes #32565 PR Close #32763 --- packages/language-service/src/hover.ts | 67 +++++++++++++++++++ .../language-service/src/language_service.ts | 11 ++- packages/language-service/test/hover_spec.ts | 19 ++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/language-service/src/hover.ts b/packages/language-service/src/hover.ts index 79543a21de..899d9c0e65 100644 --- a/packages/language-service/src/hover.ts +++ b/packages/language-service/src/hover.ts @@ -9,10 +9,14 @@ import * as ts from 'typescript'; import {AstResult} from './common'; import {locateSymbol} from './locate_symbol'; +import {TypeScriptServiceHost} from './typescript_host'; +import {findTightestNode} from './utils'; // Reverse mappings of enum would generate strings const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space]; const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation]; +const SYMBOL_CLASS = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.className]; +const SYMBOL_TEXT = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.text]; /** * Traverse the template AST and look for the symbol located at `position`, then @@ -51,3 +55,66 @@ export function getHover(info: AstResult, position: number): ts.QuickInfo|undefi ], }; } + +/** + * Get quick info for Angular semantic entities in TypeScript files, like Directives. + * @param sf TypeScript source file an Angular symbol is in + * @param position location of the symbol in the source file + * @param host Language Service host to query + */ +export function getTsHover( + sf: ts.SourceFile, position: number, host: Readonly): ts.QuickInfo| + undefined { + const node = findTightestNode(sf, position); + if (!node) return; + switch (node.kind) { + case ts.SyntaxKind.Identifier: + return getDirectiveModule(node as ts.Identifier, host); + default: + break; + } + return undefined; +} + +/** + * Attempts to get quick info for the NgModule a Directive is declared in. + * @param directive identifier on a potential Directive class declaration + * @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; + + const analyzedModules = host.getAnalyzedModules(false); + const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(directiveSymbol); + if (!ngModule) return; + + 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(), + }, + // 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: ')', kind: SYMBOL_PUNC}, + {text: ' ', kind: SYMBOL_SPACE}, + {text: moduleName, kind: SYMBOL_CLASS}, + {text: '.', kind: SYMBOL_PUNC}, + {text: directiveName, 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 37df7d4d69..537882bfc9 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -12,7 +12,7 @@ import {isAstResult} from './common'; import {getTemplateCompletions} from './completions'; import {getDefinitionAndBoundSpan, getTsDefinitionAndBoundSpan} from './definitions'; import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic, uniqueBySpan} from './diagnostics'; -import {getHover} from './hover'; +import {getHover, getTsHover} from './hover'; import {Diagnostic, LanguageService} from './types'; import {TypeScriptServiceHost} from './typescript_host'; @@ -97,5 +97,14 @@ class LanguageServiceImpl implements LanguageService { if (templateInfo) { return getHover(templateInfo, position); } + + // Attempt to get Angular-specific hover information in a TypeScript file, the NgModule a + // directive belongs to. + if (fileName.endsWith('.ts')) { + const sf = this.host.getSourceFile(fileName); + if (sf) { + return getTsHover(sf, position, this.host); + } + } } } diff --git a/packages/language-service/test/hover_spec.ts b/packages/language-service/test/hover_spec.ts index 17309044e0..3d3a69e9b9 100644 --- a/packages/language-service/test/hover_spec.ts +++ b/packages/language-service/test/hover_spec.ts @@ -148,6 +148,25 @@ describe('hover', () => { const quickInfo = ngLS.getHoverAt(fileName, marker.start); expect(quickInfo).toBeUndefined(); }); + + it('should be able to find a directive module', () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import {Component} from '@angular/core'; + + @Component({ + template: '
' + }) + export class «AppComponent» { + name: string; + }`); + const marker = mockHost.getReferenceMarkerFor(fileName, 'AppComponent'); + const quickInfo = ngLS.getHoverAt(fileName, marker.start); + expect(quickInfo).toBeTruthy(); + const {textSpan, displayParts} = quickInfo !; + expect(textSpan).toEqual(marker); + expect(toText(displayParts)).toBe('(directive) AppModule.AppComponent: class'); + }); }); function toText(displayParts?: ts.SymbolDisplayPart[]): string {