fix(language-service): Make Definition and QuickInfo compatible with TS LS (#31972)

Now that the Angular LS is a proper tsserver plugin, it does not make
sense for it to maintain its own language service API.

This is part one of the effort to remove our custom LanguageService
interface.
This interface is cumbersome because we have to do two transformations:
  ng def -> ts def -> lsp definition

The TS LS interface is more comprehensive, so this allows the Angular LS
to return more information.

PR Close #31972
This commit is contained in:
Keen Yee Liau
2019-08-01 13:07:32 -07:00
committed by Alex Rickabaugh
parent e906a4f0d8
commit a8e2ee1343
10 changed files with 442 additions and 260 deletions

View File

@ -6,28 +6,50 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as tss from 'typescript/lib/tsserverlibrary';
import * as ts from 'typescript'; // used as value and is provided at runtime
import {TemplateInfo} from './common';
import {locateSymbol} from './locate_symbol';
import {Location} from './types';
import {Span} from './types';
export function getDefinition(info: TemplateInfo): Location[]|undefined {
const result = locateSymbol(info);
return result && result.symbol.definition;
}
export function ngLocationToTsDefinitionInfo(loc: Location): tss.DefinitionInfo {
/**
* Convert Angular Span to TypeScript TextSpan. Angular Span has 'start' and
* 'end' whereas TS TextSpan has 'start' and 'length'.
* @param span Angular Span
*/
function ngSpanToTsTextSpan(span: Span): ts.TextSpan {
return {
fileName: loc.fileName,
textSpan: {
start: loc.span.start,
length: loc.span.end - loc.span.start,
},
// TODO(kyliau): Provide more useful info for name, kind and containerKind
name: '', // should be name of symbol but we don't have enough information here.
kind: tss.ScriptElementKind.unknown,
containerName: loc.fileName,
containerKind: tss.ScriptElementKind.unknown,
start: span.start,
length: span.end - span.start,
};
}
export function getDefinitionAndBoundSpan(info: TemplateInfo): ts.DefinitionInfoAndBoundSpan|
undefined {
const symbolInfo = locateSymbol(info);
if (!symbolInfo) {
return;
}
const textSpan = ngSpanToTsTextSpan(symbolInfo.span);
const {symbol} = symbolInfo;
const {container, definition: locations} = symbol;
if (!locations || !locations.length) {
// symbol.definition is really the locations of the symbol. There could be
// more than one. No meaningful info could be provided without any location.
return {textSpan};
}
const containerKind = container ? container.kind : ts.ScriptElementKind.unknown;
const containerName = container ? container.name : '';
const definitions = locations.map((location) => {
return {
kind: symbol.kind as ts.ScriptElementKind,
name: symbol.name,
containerKind: containerKind as ts.ScriptElementKind,
containerName: containerName,
textSpan: ngSpanToTsTextSpan(location.span),
fileName: location.fileName,
};
});
return {
definitions, textSpan,
};
}

View File

@ -6,23 +6,42 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {TemplateInfo} from './common';
import {locateSymbol} from './locate_symbol';
import {Hover, HoverTextSection, Symbol} from './types';
export function getHover(info: TemplateInfo): Hover|undefined {
const result = locateSymbol(info);
if (result) {
return {text: hoverTextOf(result.symbol), span: result.span};
// Reverse mappings of enum would generate strings
const SYMBOL_SPACE = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.space];
const SYMBOL_PUNC = ts.SymbolDisplayPartKind[ts.SymbolDisplayPartKind.punctuation];
export function getHover(info: TemplateInfo): ts.QuickInfo|undefined {
const symbolInfo = locateSymbol(info);
if (!symbolInfo) {
return;
}
const {symbol, span} = symbolInfo;
const containerDisplayParts: ts.SymbolDisplayPart[] = symbol.container ?
[
{text: symbol.container.name, kind: symbol.container.kind},
{text: '.', kind: SYMBOL_PUNC},
] :
[];
return {
kind: symbol.kind as ts.ScriptElementKind,
kindModifiers: '', // kindModifier info not available on 'ng.Symbol'
textSpan: {
start: span.start,
length: span.end - span.start,
},
// 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.
displayParts: [
{text: '(', kind: SYMBOL_PUNC}, {text: symbol.kind, kind: symbol.kind},
{text: ')', kind: SYMBOL_PUNC}, {text: ' ', kind: SYMBOL_SPACE}, ...containerDisplayParts,
{text: symbol.name, kind: symbol.kind},
// TODO: Append type info as well, but Symbol doesn't expose that!
// Ideally hover text should be like '(property) ClassX.propY: string'
],
};
}
function hoverTextOf(symbol: Symbol): HoverTextSection[] {
const result: HoverTextSection[] =
[{text: symbol.kind}, {text: ' '}, {text: symbol.name, language: symbol.language}];
const container = symbol.container;
if (container) {
result.push({text: ' of '}, {text: container.name, language: container.language});
}
return result;
}

View File

@ -8,9 +8,9 @@
import {CompileMetadataResolver, CompilePipeSummary} from '@angular/compiler';
import {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics} from '@angular/compiler-cli/src/language_services';
import * as tss from 'typescript/lib/tsserverlibrary';
import {getTemplateCompletions} from './completions';
import {getDefinition} from './definitions';
import {getDefinitionAndBoundSpan} from './definitions';
import {getDeclarationDiagnostics} from './diagnostics';
import {getHover} from './hover';
import {Completion, Diagnostic, DiagnosticKind, Diagnostics, Hover, LanguageService, LanguageServiceHost, Location, Span, TemplateSource} from './types';
@ -30,8 +30,6 @@ export function createLanguageService(host: LanguageServiceHost): LanguageServic
class LanguageServiceImpl implements LanguageService {
constructor(private host: LanguageServiceHost) {}
private get metadataResolver(): CompileMetadataResolver { return this.host.resolver; }
getTemplateReferences(): string[] { return this.host.getTemplateReferences(); }
getDiagnostics(fileName: string): Diagnostic[] {
@ -65,14 +63,14 @@ class LanguageServiceImpl implements LanguageService {
}
}
getDefinitionAt(fileName: string, position: number): Location[]|undefined {
getDefinitionAt(fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined {
let templateInfo = this.host.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return getDefinition(templateInfo);
return getDefinitionAndBoundSpan(templateInfo);
}
}
getHoverAt(fileName: string, position: number): Hover|undefined {
getHoverAt(fileName: string, position: number): tss.QuickInfo|undefined {
let templateInfo = this.host.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return getHover(templateInfo);

View File

@ -9,9 +9,8 @@
import * as ts from 'typescript'; // used as value, passed in by tsserver at runtime
import * as tss from 'typescript/lib/tsserverlibrary'; // used as type only
import {ngLocationToTsDefinitionInfo} from './definitions';
import {createLanguageService} from './language_service';
import {Completion, Diagnostic, DiagnosticMessageChain, Location} from './types';
import {Completion, Diagnostic, DiagnosticMessageChain} from './types';
import {TypeScriptServiceHost} from './typescript_host';
const projectHostMap = new WeakMap<tss.server.Project, TypeScriptServiceHost>();
@ -76,13 +75,13 @@ export function create(info: tss.server.PluginCreateInfo): tss.LanguageService {
// This effectively disables native TS features and is meant for internal
// use only.
const angularOnly = config ? config.angularOnly === true : false;
const proxy: tss.LanguageService = Object.assign({}, tsLS);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
const ngLS = createLanguageService(ngLSHost);
projectHostMap.set(project, ngLSHost);
proxy.getCompletionsAtPosition = function(
fileName: string, position: number, options: tss.GetCompletionsAtPositionOptions|undefined) {
function getCompletionsAtPosition(
fileName: string, position: number,
options: tss.GetCompletionsAtPositionOptions | undefined) {
if (!angularOnly) {
const results = tsLS.getCompletionsAtPosition(fileName, position, options);
if (results && results.entries.length) {
@ -100,39 +99,20 @@ export function create(info: tss.server.PluginCreateInfo): tss.LanguageService {
isNewIdentifierLocation: false,
entries: results.map(completionToEntry),
};
};
}
proxy.getQuickInfoAtPosition = function(fileName: string, position: number): tss.QuickInfo |
undefined {
if (!angularOnly) {
const result = tsLS.getQuickInfoAtPosition(fileName, position);
if (result) {
// If TS could answer the query, then return results immediately.
return result;
}
}
const result = ngLS.getHoverAt(fileName, position);
if (!result) {
return;
}
return {
// TODO(kyliau): Provide more useful info for kind and kindModifiers
kind: ts.ScriptElementKind.unknown,
kindModifiers: ts.ScriptElementKindModifier.none,
textSpan: {
start: result.span.start,
length: result.span.end - result.span.start,
},
displayParts: result.text.map((part) => {
return {
text: part.text,
kind: part.language || 'angular',
};
}),
};
};
function getQuickInfoAtPosition(fileName: string, position: number): tss.QuickInfo|undefined {
if (!angularOnly) {
const result = tsLS.getQuickInfoAtPosition(fileName, position);
if (result) {
// If TS could answer the query, then return results immediately.
return result;
}
}
return ngLS.getHoverAt(fileName, position);
}
proxy.getSemanticDiagnostics = function(fileName: string): tss.Diagnostic[] {
function getSemanticDiagnostics(fileName: string): tss.Diagnostic[] {
const results: tss.Diagnostic[] = [];
if (!angularOnly) {
const tsResults = tsLS.getSemanticDiagnostics(fileName);
@ -146,48 +126,43 @@ export function create(info: tss.server.PluginCreateInfo): tss.LanguageService {
const sourceFile = fileName.endsWith('.ts') ? ngLSHost.getSourceFile(fileName) : undefined;
results.push(...ngResults.map(d => diagnosticToDiagnostic(d, sourceFile)));
return results;
};
}
proxy.getDefinitionAtPosition = function(fileName: string, position: number):
ReadonlyArray<tss.DefinitionInfo>|
undefined {
if (!angularOnly) {
const results = tsLS.getDefinitionAtPosition(fileName, position);
if (results) {
// If TS could answer the query, then return results immediately.
return results;
}
}
const results = ngLS.getDefinitionAt(fileName, position);
if (!results) {
return;
}
return results.map(ngLocationToTsDefinitionInfo);
};
function getDefinitionAtPosition(
fileName: string, position: number): ReadonlyArray<tss.DefinitionInfo>|undefined {
if (!angularOnly) {
const results = tsLS.getDefinitionAtPosition(fileName, position);
if (results) {
// If TS could answer the query, then return results immediately.
return results;
}
}
const result = ngLS.getDefinitionAt(fileName, position);
if (!result || !result.definitions || !result.definitions.length) {
return;
}
return result.definitions;
}
proxy.getDefinitionAndBoundSpan = function(fileName: string, position: number):
tss.DefinitionInfoAndBoundSpan |
undefined {
if (!angularOnly) {
const result = tsLS.getDefinitionAndBoundSpan(fileName, position);
if (result) {
// If TS could answer the query, then return results immediately.
return result;
}
}
const results = ngLS.getDefinitionAt(fileName, position);
if (!results || !results.length) {
return;
}
const {span} = results[0];
return {
definitions: results.map(ngLocationToTsDefinitionInfo),
textSpan: {
start: span.start,
length: span.end - span.start,
},
};
};
function getDefinitionAndBoundSpan(
fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined {
if (!angularOnly) {
const result = tsLS.getDefinitionAndBoundSpan(fileName, position);
if (result) {
// If TS could answer the query, then return results immediately.
return result;
}
}
return ngLS.getDefinitionAt(fileName, position);
}
const proxy: tss.LanguageService = Object.assign(
// First clone the original TS language service
{}, tsLS,
// Then override the methods supported by Angular language service
{
getCompletionsAtPosition, getQuickInfoAtPosition, getSemanticDiagnostics,
getDefinitionAtPosition, getDefinitionAndBoundSpan,
});
return proxy;
}

View File

@ -8,6 +8,8 @@
import {CompileDirectiveMetadata, CompileMetadataResolver, CompilePipeSummary, NgAnalyzedModules, StaticSymbol} from '@angular/compiler';
import {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from '@angular/compiler-cli/src/language_services';
import * as tss from 'typescript/lib/tsserverlibrary';
import {AstResult, TemplateInfo} from './common';
export {
@ -394,12 +396,12 @@ export interface LanguageService {
/**
* Return the definition location for the symbol at position.
*/
getDefinitionAt(fileName: string, position: number): Location[]|undefined;
getDefinitionAt(fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined;
/**
* Return the hover information for the symbol at position.
*/
getHoverAt(fileName: string, position: number): Hover|undefined;
getHoverAt(fileName: string, position: number): tss.QuickInfo|undefined;
/**
* Return the pipes that are available at the given position.