Skip to content
Permalink
Browse files

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
  • Loading branch information...
kyliau authored and alxhub committed Aug 1, 2019
1 parent e906a4f commit a8e2ee134303e2c2f9650e4ce4e61af4629aeea8
@@ -20,12 +20,12 @@
],
"textSpan": {
"start": {
"line": 7,
"offset": 30
"line": 5,
"offset": 26
},
"end": {
"line": 7,
"offset": 47
"line": 5,
"offset": 30
}
}
}
@@ -5,7 +5,7 @@
"request_seq": 2,
"success": true,
"body": {
"kind": "",
"kind": "property",
"kindModifiers": "",
"start": {
"line": 5,
@@ -15,7 +15,7 @@
"line": 5,
"offset": 30
},
"displayString": "property name of AppComponent",
"displayString": "(property) AppComponent.name",
"documentation": "",
"tags": []
}
@@ -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;
/**
* 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 {
start: span.start,
length: span.end - span.start,
};
}

export function ngLocationToTsDefinitionInfo(loc: Location): tss.DefinitionInfo {
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 {
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,
definitions, textSpan,
};
}
@@ -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];

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});
export function getHover(info: TemplateInfo): ts.QuickInfo|undefined {
const symbolInfo = locateSymbol(info);
if (!symbolInfo) {
return;
}
return result;
}
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'
],
};
}
@@ -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);
@@ -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;
}
@@ -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.

0 comments on commit a8e2ee1

Please sign in to comment.
You can’t perform that action at this time.