feat(language-service): Show documentation on hover (#34506)
This commit adds dpcumentation to the hover tooltip. PR closes https://github.com/angular/vscode-ng-language-service/issues/321 PR Close #34506
This commit is contained in:
parent
ba2fd31e62
commit
c079f38cbb
@ -219,6 +219,7 @@ export class AstType implements AstVisitor {
|
|||||||
nullable: false,
|
nullable: false,
|
||||||
public: true,
|
public: true,
|
||||||
definition: undefined,
|
definition: undefined,
|
||||||
|
documentation: [],
|
||||||
members(): SymbolTable{return _this.scope;},
|
members(): SymbolTable{return _this.scope;},
|
||||||
signatures(): Signature[]{return [];},
|
signatures(): Signature[]{return [];},
|
||||||
selectSignature(types): Signature | undefined{return undefined;},
|
selectSignature(types): Signature | undefined{return undefined;},
|
||||||
|
@ -43,6 +43,10 @@ export const createGlobalSymbolTable: (query: ng.SymbolQuery) => ng.SymbolTable
|
|||||||
callable: true,
|
callable: true,
|
||||||
definition: undefined,
|
definition: undefined,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
documentation: [{
|
||||||
|
kind: 'text',
|
||||||
|
text: 'function to cast an expression to the `any` type',
|
||||||
|
}],
|
||||||
members: () => EMPTY_SYMBOL_TABLE,
|
members: () => EMPTY_SYMBOL_TABLE,
|
||||||
signatures: () => [],
|
signatures: () => [],
|
||||||
selectSignature(args: ng.Symbol[]) {
|
selectSignature(args: ng.Symbol[]) {
|
||||||
|
@ -8,11 +8,14 @@
|
|||||||
|
|
||||||
import {CompileSummaryKind, StaticSymbol} from '@angular/compiler';
|
import {CompileSummaryKind, StaticSymbol} from '@angular/compiler';
|
||||||
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 * as ng from './types';
|
||||||
import {TypeScriptServiceHost} from './typescript_host';
|
import {TypeScriptServiceHost} from './typescript_host';
|
||||||
import {findTightestNode} from './utils';
|
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];
|
||||||
@ -37,7 +40,7 @@ export function getHover(info: AstResult, position: number, host: Readonly<TypeS
|
|||||||
const textSpan = {start: span.start, length: span.end - span.start};
|
const textSpan = {start: span.start, length: span.end - span.start};
|
||||||
|
|
||||||
if (compileTypeSummary && compileTypeSummary.summaryKind === CompileSummaryKind.Directive) {
|
if (compileTypeSummary && compileTypeSummary.summaryKind === CompileSummaryKind.Directive) {
|
||||||
return getDirectiveModule(compileTypeSummary.type.reference, textSpan, host);
|
return getDirectiveModule(compileTypeSummary.type.reference, textSpan, host, symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerDisplayParts: ts.SymbolDisplayPart[] = symbol.container ?
|
const containerDisplayParts: ts.SymbolDisplayPart[] = symbol.container ?
|
||||||
@ -57,6 +60,7 @@ export function getHover(info: AstResult, position: number, host: Readonly<TypeS
|
|||||||
kind: symbol.kind as ts.ScriptElementKind,
|
kind: symbol.kind as ts.ScriptElementKind,
|
||||||
kindModifiers: '', // kindModifier info not available on 'ng.Symbol'
|
kindModifiers: '', // kindModifier info not available on 'ng.Symbol'
|
||||||
textSpan,
|
textSpan,
|
||||||
|
documentation: symbol.documentation,
|
||||||
// this would generate a string like '(property) ClassX.propY: type'
|
// this would generate a string like '(property) ClassX.propY: type'
|
||||||
// 'kind' in displayParts does not really matter because it's dropped when
|
// 'kind' in displayParts does not really matter because it's dropped when
|
||||||
// displayParts get converted to string.
|
// displayParts get converted to string.
|
||||||
@ -105,11 +109,13 @@ export function getTsHover(
|
|||||||
/**
|
/**
|
||||||
* Attempts to get quick info for the NgModule a Directive is declared in.
|
* Attempts to get quick info for the NgModule a Directive is declared in.
|
||||||
* @param directive identifier on a potential Directive class declaration
|
* @param directive identifier on a potential Directive class declaration
|
||||||
|
* @param textSpan span of the symbol
|
||||||
* @param host Language Service host to query
|
* @param host Language Service host to query
|
||||||
|
* @param symbol the internal symbol that represents the directive
|
||||||
*/
|
*/
|
||||||
function getDirectiveModule(
|
function getDirectiveModule(
|
||||||
directive: StaticSymbol, textSpan: ts.TextSpan,
|
directive: StaticSymbol, textSpan: ts.TextSpan, host: Readonly<TypeScriptServiceHost>,
|
||||||
host: Readonly<TypeScriptServiceHost>): ts.QuickInfo|undefined {
|
symbol?: ng.Symbol): ts.QuickInfo|undefined {
|
||||||
const analyzedModules = host.getAnalyzedModules(false);
|
const analyzedModules = host.getAnalyzedModules(false);
|
||||||
const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(directive);
|
const ngModule = analyzedModules.ngModuleByPipeOrDirective.get(directive);
|
||||||
if (!ngModule) return;
|
if (!ngModule) return;
|
||||||
@ -124,6 +130,7 @@ function getDirectiveModule(
|
|||||||
kindModifiers:
|
kindModifiers:
|
||||||
ts.ScriptElementKindModifier.none, // kindModifier info not available on 'ng.Symbol'
|
ts.ScriptElementKindModifier.none, // kindModifier info not available on 'ng.Symbol'
|
||||||
textSpan,
|
textSpan,
|
||||||
|
documentation: symbol ? symbol.documentation : undefined,
|
||||||
// This generates a string like '(directive) NgModule.Directive: class'
|
// This generates a string like '(directive) NgModule.Directive: class'
|
||||||
// 'kind' in displayParts does not really matter because it's dropped when
|
// 'kind' in displayParts does not really matter because it's dropped when
|
||||||
// displayParts get converted to string.
|
// displayParts get converted to string.
|
||||||
|
@ -214,6 +214,8 @@ class OverrideKindSymbol implements Symbol {
|
|||||||
|
|
||||||
get definition(): Definition { return this.sym.definition; }
|
get definition(): Definition { return this.sym.definition; }
|
||||||
|
|
||||||
|
get documentation(): ts.SymbolDisplayPart[] { return this.sym.documentation; }
|
||||||
|
|
||||||
members() { return this.sym.members(); }
|
members() { return this.sym.members(); }
|
||||||
|
|
||||||
signatures() { return this.sym.signatures(); }
|
signatures() { return this.sym.signatures(); }
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {StaticSymbol} from '@angular/compiler';
|
import {StaticSymbol} from '@angular/compiler';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The range of a span of text in a source file.
|
* The range of a span of text in a source file.
|
||||||
@ -95,6 +97,11 @@ export interface Symbol {
|
|||||||
*/
|
*/
|
||||||
readonly nullable: boolean;
|
readonly nullable: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Documentation comment on the Symbol, if any.
|
||||||
|
*/
|
||||||
|
readonly documentation: ts.SymbolDisplayPart[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A table of the members of the symbol; that is, the members that can appear
|
* A table of the members of the symbol; that is, the members that can appear
|
||||||
* after a `.` in an Angular expression.
|
* after a `.` in an Angular expression.
|
||||||
|
@ -262,6 +262,14 @@ class TypeWrapper implements Symbol {
|
|||||||
return this.context.checker.getNonNullableType(this.tsType) != this.tsType;
|
return this.context.checker.getNonNullableType(this.tsType) != this.tsType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentation(): ts.SymbolDisplayPart[] {
|
||||||
|
const symbol = this.tsType.getSymbol();
|
||||||
|
if (!symbol) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return symbol.getDocumentationComment(this.context.checker);
|
||||||
|
}
|
||||||
|
|
||||||
get definition(): Definition|undefined {
|
get definition(): Definition|undefined {
|
||||||
const symbol = this.tsType.getSymbol();
|
const symbol = this.tsType.getSymbol();
|
||||||
return symbol ? definitionFromTsSymbol(symbol) : undefined;
|
return symbol ? definitionFromTsSymbol(symbol) : undefined;
|
||||||
@ -347,6 +355,10 @@ class SymbolWrapper implements Symbol {
|
|||||||
|
|
||||||
get definition(): Definition { return definitionFromTsSymbol(this.symbol); }
|
get definition(): Definition { return definitionFromTsSymbol(this.symbol); }
|
||||||
|
|
||||||
|
get documentation(): ts.SymbolDisplayPart[] {
|
||||||
|
return this.symbol.getDocumentationComment(this.context.checker);
|
||||||
|
}
|
||||||
|
|
||||||
members(): SymbolTable {
|
members(): SymbolTable {
|
||||||
if (!this._members) {
|
if (!this._members) {
|
||||||
if ((this.symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) != 0) {
|
if ((this.symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)) != 0) {
|
||||||
@ -397,9 +409,10 @@ class DeclaredSymbol implements Symbol {
|
|||||||
|
|
||||||
get callable(): boolean { return this.declaration.type.callable; }
|
get callable(): boolean { return this.declaration.type.callable; }
|
||||||
|
|
||||||
|
|
||||||
get definition(): Definition { return this.declaration.definition; }
|
get definition(): Definition { return this.declaration.definition; }
|
||||||
|
|
||||||
|
get documentation(): ts.SymbolDisplayPart[] { return this.declaration.type.documentation; }
|
||||||
|
|
||||||
members(): SymbolTable { return this.declaration.type.members(); }
|
members(): SymbolTable { return this.declaration.type.members(); }
|
||||||
|
|
||||||
signatures(): Signature[] { return this.declaration.type.signatures(); }
|
signatures(): Signature[] { return this.declaration.type.signatures(); }
|
||||||
@ -596,6 +609,14 @@ class PipeSymbol implements Symbol {
|
|||||||
return symbol ? definitionFromTsSymbol(symbol) : undefined;
|
return symbol ? definitionFromTsSymbol(symbol) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentation(): ts.SymbolDisplayPart[] {
|
||||||
|
const symbol = this.tsType.getSymbol();
|
||||||
|
if (!symbol) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return symbol.getDocumentationComment(this.context.checker);
|
||||||
|
}
|
||||||
|
|
||||||
members(): SymbolTable { return EmptyTable.instance; }
|
members(): SymbolTable { return EmptyTable.instance; }
|
||||||
|
|
||||||
signatures(): Signature[] { return signaturesOf(this.tsType, this.context); }
|
signatures(): Signature[] { return signaturesOf(this.tsType, this.context); }
|
||||||
|
@ -205,6 +205,24 @@ describe('hover', () => {
|
|||||||
});
|
});
|
||||||
expect(toText(displayParts)).toBe('(method) $any: $any');
|
expect(toText(displayParts)).toBe('(method) $any: $any');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should provide documentation for a property', () => {
|
||||||
|
mockHost.override(TEST_TEMPLATE, `<div>{{~{cursor}title}}</div>`);
|
||||||
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
||||||
|
const quickInfo = ngLS.getHoverAt(TEST_TEMPLATE, marker.start);
|
||||||
|
expect(quickInfo).toBeDefined();
|
||||||
|
const documentation = toText(quickInfo !.documentation);
|
||||||
|
expect(documentation).toBe('This is the title of the `TemplateReference` Component.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide documentation for a selector', () => {
|
||||||
|
mockHost.override(TEST_TEMPLATE, `<~{cursor}test-comp></test-comp>`);
|
||||||
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
||||||
|
const quickInfo = ngLS.getHoverAt(TEST_TEMPLATE, marker.start);
|
||||||
|
expect(quickInfo).toBeDefined();
|
||||||
|
const documentation = toText(quickInfo !.documentation);
|
||||||
|
expect(documentation).toBe('This Component provides the `test-comp` selector.');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function toText(displayParts?: ts.SymbolDisplayPart[]): string {
|
function toText(displayParts?: ts.SymbolDisplayPart[]): string {
|
||||||
|
@ -132,6 +132,9 @@ export class AsyncForUsingComponent {
|
|||||||
export class References {
|
export class References {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Component provides the `test-comp` selector.
|
||||||
|
*/
|
||||||
/*BeginTestComponent*/ @Component({
|
/*BeginTestComponent*/ @Component({
|
||||||
selector: 'test-comp',
|
selector: 'test-comp',
|
||||||
template: '<div>Testing: {{name}}</div>',
|
template: '<div>Testing: {{name}}</div>',
|
||||||
@ -145,6 +148,9 @@ export class TestComponent {
|
|||||||
templateUrl: 'test.ng',
|
templateUrl: 'test.ng',
|
||||||
})
|
})
|
||||||
export class TemplateReference {
|
export class TemplateReference {
|
||||||
|
/**
|
||||||
|
* This is the title of the `TemplateReference` Component.
|
||||||
|
*/
|
||||||
title = 'Some title';
|
title = 'Some title';
|
||||||
hero: Hero = {id: 1, name: 'Windstorm'};
|
hero: Hero = {id: 1, name: 'Windstorm'};
|
||||||
heroes: Hero[] = [this.hero];
|
heroes: Hero[] = [this.hero];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user