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
This commit is contained in:
ayazhafiz 2019-09-16 21:07:43 -05:00 committed by atscott
parent 3de59d48b5
commit 0d186dda35
3 changed files with 96 additions and 1 deletions

View File

@ -9,10 +9,14 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AstResult} from './common'; import {AstResult} from './common';
import {locateSymbol} from './locate_symbol'; import {locateSymbol} from './locate_symbol';
import {TypeScriptServiceHost} from './typescript_host';
import {findTightestNode} from './utils';
// Reverse mappings of enum would generate strings // Reverse mappings of enum would generate strings
const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space]; const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space];
const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation]; 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 * 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<TypeScriptServiceHost>): 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<TypeScriptServiceHost>): 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},
],
};
}

View File

@ -12,7 +12,7 @@ import {isAstResult} from './common';
import {getTemplateCompletions} from './completions'; import {getTemplateCompletions} from './completions';
import {getDefinitionAndBoundSpan, getTsDefinitionAndBoundSpan} from './definitions'; import {getDefinitionAndBoundSpan, getTsDefinitionAndBoundSpan} from './definitions';
import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic, uniqueBySpan} from './diagnostics'; import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic, uniqueBySpan} from './diagnostics';
import {getHover} from './hover'; import {getHover, getTsHover} from './hover';
import {Diagnostic, LanguageService} from './types'; import {Diagnostic, LanguageService} from './types';
import {TypeScriptServiceHost} from './typescript_host'; import {TypeScriptServiceHost} from './typescript_host';
@ -97,5 +97,14 @@ class LanguageServiceImpl implements LanguageService {
if (templateInfo) { if (templateInfo) {
return getHover(templateInfo, position); 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);
}
}
} }
} }

View File

@ -148,6 +148,25 @@ describe('hover', () => {
const quickInfo = ngLS.getHoverAt(fileName, marker.start); const quickInfo = ngLS.getHoverAt(fileName, marker.start);
expect(quickInfo).toBeUndefined(); 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: '<div></div>'
})
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 { function toText(displayParts?: ts.SymbolDisplayPart[]): string {