Skip to content

Commit c079f38

Browse files
kyliaualxhub
authored andcommitted
feat(language-service): Show documentation on hover (angular#34506)
This commit adds dpcumentation to the hover tooltip. PR closes angular/vscode-ng-language-service#321 PR Close angular#34506
1 parent ba2fd31 commit c079f38

8 files changed

Lines changed: 70 additions & 4 deletions

File tree

packages/language-service/src/expression_type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export class AstType implements AstVisitor {
219219
nullable: false,
220220
public: true,
221221
definition: undefined,
222+
documentation: [],
222223
members(): SymbolTable{return _this.scope;},
223224
signatures(): Signature[]{return [];},
224225
selectSignature(types): Signature | undefined{return undefined;},

packages/language-service/src/global_symbols.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export const createGlobalSymbolTable: (query: ng.SymbolQuery) => ng.SymbolTable
4343
callable: true,
4444
definition: undefined,
4545
nullable: false,
46+
documentation: [{
47+
kind: 'text',
48+
text: 'function to cast an expression to the `any` type',
49+
}],
4650
members: () => EMPTY_SYMBOL_TABLE,
4751
signatures: () => [],
4852
selectSignature(args: ng.Symbol[]) {

packages/language-service/src/hover.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88

99
import {CompileSummaryKind, StaticSymbol} from '@angular/compiler';
1010
import * as ts from 'typescript';
11+
1112
import {AstResult} from './common';
1213
import {locateSymbol} from './locate_symbol';
14+
import * as ng from './types';
1315
import {TypeScriptServiceHost} from './typescript_host';
1416
import {findTightestNode} from './utils';
1517

18+
1619
// Reverse mappings of enum would generate strings
1720
const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space];
1821
const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation];
@@ -37,7 +40,7 @@ export function getHover(info: AstResult, position: number, host: Readonly<TypeS
3740
const textSpan = {start: span.start, length: span.end - span.start};
3841

3942
if (compileTypeSummary && compileTypeSummary.summaryKind === CompileSummaryKind.Directive) {
40-
return getDirectiveModule(compileTypeSummary.type.reference, textSpan, host);
43+
return getDirectiveModule(compileTypeSummary.type.reference, textSpan, host, symbol);
4144
}
4245

4346
const containerDisplayParts: ts.SymbolDisplayPart[] = symbol.container ?
@@ -57,6 +60,7 @@ export function getHover(info: AstResult, position: number, host: Readonly<TypeS
5760
kind: symbol.kind as ts.ScriptElementKind,
5861
kindModifiers: '', // kindModifier info not available on 'ng.Symbol'
5962
textSpan,
63+
documentation: symbol.documentation,
6064
// this would generate a string like '(property) ClassX.propY: type'
6165
// 'kind' in displayParts does not really matter because it's dropped when
6266
// displayParts get converted to string.
@@ -105,11 +109,13 @@ export function getTsHover(
105109
/**
106110
* Attempts to get quick info for the NgModule a Directive is declared in.
107111
* @param directive identifier on a potential Directive class declaration
112+
* @param textSpan span of the symbol
108113
* @param host Language Service host to query
114+
* @param symbol the internal symbol that represents the directive
109115
*/
110116
function getDirectiveModule(
111-
directive: StaticSymbol, textSpan: ts.TextSpan,
112-
host: Readonly<TypeScriptServiceHost>): ts.QuickInfo|undefined {
117+
directive: StaticSymbol, textSpan: ts.TextSpan, host: Readonly<TypeScriptServiceHost>,
118+
symbol?: ng.Symbol): ts.QuickInfo|undefined {
113119
const analyzedModules = host.getAnalyzedModules(false);
114120
const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(directive);
115121
if (!ngModule) return;
@@ -124,6 +130,7 @@ function getDirectiveModule(
124130
kindModifiers:
125131
ts.ScriptElementKindModifier.none, // kindModifier info not available on 'ng.Symbol'
126132
textSpan,
133+
documentation: symbol ? symbol.documentation : undefined,
127134
// This generates a string like '(directive) NgModule.Directive: class'
128135
// 'kind' in displayParts does not really matter because it's dropped when
129136
// displayParts get converted to string.

packages/language-service/src/locate_symbol.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ class OverrideKindSymbol implements Symbol {
214214

215215
get definition(): Definition { return this.sym.definition; }
216216

217+
get documentation(): ts.SymbolDisplayPart[] { return this.sym.documentation; }
218+
217219
members() { return this.sym.members(); }
218220

219221
signatures() { return this.sym.signatures(); }

packages/language-service/src/symbols.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*/
88

99
import {StaticSymbol} from '@angular/compiler';
10+
import * as ts from 'typescript';
11+
1012

1113
/**
1214
* The range of a span of text in a source file.
@@ -95,6 +97,11 @@ export interface Symbol {
9597
*/
9698
readonly nullable: boolean;
9799

100+
/**
101+
* Documentation comment on the Symbol, if any.
102+
*/
103+
readonly documentation: ts.SymbolDisplayPart[];
104+
98105
/**
99106
* A table of the members of the symbol; that is, the members that can appear
100107
* after a `.` in an Angular expression.

packages/language-service/src/typescript_symbols.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,14 @@ class TypeWrapper implements Symbol {
262262
return this.context.checker.getNonNullableType(this.tsType) != this.tsType;
263263
}
264264

265+
get documentation(): ts.SymbolDisplayPart[] {
266+
const symbol = this.tsType.getSymbol();
267+
if (!symbol) {
268+
return [];
269+
}
270+
return symbol.getDocumentationComment(this.context.checker);
271+
}
272+
265273
get definition(): Definition|undefined {
266274
const symbol = this.tsType.getSymbol();
267275
return symbol ? definitionFromTsSymbol(symbol) : undefined;
@@ -347,6 +355,10 @@ class SymbolWrapper implements Symbol {
347355

348356
get definition(): Definition { return definitionFromTsSymbol(this.symbol); }
349357

358+
get documentation(): ts.SymbolDisplayPart[] {
359+
return this.symbol.getDocumentationComment(this.context.checker);
360+
}
361+
350362
members(): SymbolTable {
351363
if (!this._members) {
352364
if ((this.symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) != 0) {
@@ -397,9 +409,10 @@ class DeclaredSymbol implements Symbol {
397409

398410
get callable(): boolean { return this.declaration.type.callable; }
399411

400-
401412
get definition(): Definition { return this.declaration.definition; }
402413

414+
get documentation(): ts.SymbolDisplayPart[] { return this.declaration.type.documentation; }
415+
403416
members(): SymbolTable { return this.declaration.type.members(); }
404417

405418
signatures(): Signature[] { return this.declaration.type.signatures(); }
@@ -596,6 +609,14 @@ class PipeSymbol implements Symbol {
596609
return symbol ? definitionFromTsSymbol(symbol) : undefined;
597610
}
598611

612+
get documentation(): ts.SymbolDisplayPart[] {
613+
const symbol = this.tsType.getSymbol();
614+
if (!symbol) {
615+
return [];
616+
}
617+
return symbol.getDocumentationComment(this.context.checker);
618+
}
619+
599620
members(): SymbolTable { return EmptyTable.instance; }
600621

601622
signatures(): Signature[] { return signaturesOf(this.tsType, this.context); }

packages/language-service/test/hover_spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,24 @@ describe('hover', () => {
205205
});
206206
expect(toText(displayParts)).toBe('(method) $any: $any');
207207
});
208+
209+
it('should provide documentation for a property', () => {
210+
mockHost.override(TEST_TEMPLATE, `<div>{{~{cursor}title}}</div>`);
211+
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
212+
const quickInfo = ngLS.getHoverAt(TEST_TEMPLATE, marker.start);
213+
expect(quickInfo).toBeDefined();
214+
const documentation = toText(quickInfo !.documentation);
215+
expect(documentation).toBe('This is the title of the `TemplateReference` Component.');
216+
});
217+
218+
it('should provide documentation for a selector', () => {
219+
mockHost.override(TEST_TEMPLATE, `<~{cursor}test-comp></test-comp>`);
220+
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
221+
const quickInfo = ngLS.getHoverAt(TEST_TEMPLATE, marker.start);
222+
expect(quickInfo).toBeDefined();
223+
const documentation = toText(quickInfo !.documentation);
224+
expect(documentation).toBe('This Component provides the `test-comp` selector.');
225+
});
208226
});
209227

210228
function toText(displayParts?: ts.SymbolDisplayPart[]): string {

packages/language-service/test/project/app/parsing-cases.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ export class AsyncForUsingComponent {
132132
export class References {
133133
}
134134

135+
/**
136+
* This Component provides the `test-comp` selector.
137+
*/
135138
/*BeginTestComponent*/ @Component({
136139
selector: 'test-comp',
137140
template: '<div>Testing: {{name}}</div>',
@@ -145,6 +148,9 @@ export class TestComponent {
145148
templateUrl: 'test.ng',
146149
})
147150
export class TemplateReference {
151+
/**
152+
* This is the title of the `TemplateReference` Component.
153+
*/
148154
title = 'Some title';
149155
hero: Hero = {id: 1, name: 'Windstorm'};
150156
heroes: Hero[] = [this.hero];

0 commit comments

Comments
 (0)