refactor(language-service): Return ts.CompletionInfo for getCompletionsAt() (#32116)

Part 3/3 of language-service refactoring:
Change all language service APIs to return TS value since Angular LS
will be a proper tsserver plugin. This reduces the need to transform
results among Angular <--> TS <--> LSP.

PR Close #32116
This commit is contained in:
Keen Yee Liau 2019-08-12 16:54:36 -07:00 committed by Andrew Kushnir
parent d6bbc4d76d
commit 5a562d8a0a
7 changed files with 124 additions and 143 deletions

View File

@ -498,3 +498,12 @@ function expandedAttr(attr: AttrInfo): AttrInfo[] {
function lowerName(name: string): string { function lowerName(name: string): string {
return name && (name[0].toLowerCase() + name.substr(1)); return name && (name[0].toLowerCase() + name.substr(1));
} }
export function ngCompletionToTsCompletionEntry(completion: Completion): ts.CompletionEntry {
return {
name: completion.name,
kind: completion.kind as ts.ScriptElementKind,
kindModifiers: '',
sortText: completion.sort,
};
}

View File

@ -130,3 +130,21 @@ export function ngDiagnosticToTsDiagnostic(
source: 'ng', source: 'ng',
}; };
} }
export function uniqueBySpan<T extends{span: Span}>(elements: T[]): T[] {
const result: T[] = [];
const map = new Map<number, Set<number>>();
for (const element of elements) {
const {span} = element;
let set = map.get(span.start);
if (!set) {
set = new Set();
map.set(span.start, set);
}
if (!set.has(span.end)) {
set.add(span.end);
result.push(element);
}
}
return result;
}

View File

@ -6,17 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {CompilePipeSummary} from '@angular/compiler';
import * as tss from 'typescript/lib/tsserverlibrary'; import * as tss from 'typescript/lib/tsserverlibrary';
import {getTemplateCompletions, ngCompletionToTsCompletionEntry} from './completions';
import {getTemplateCompletions} from './completions';
import {getDefinitionAndBoundSpan} from './definitions'; import {getDefinitionAndBoundSpan} from './definitions';
import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic} from './diagnostics'; import {getDeclarationDiagnostics, getTemplateDiagnostics, ngDiagnosticToTsDiagnostic, uniqueBySpan} from './diagnostics';
import {getHover} from './hover'; import {getHover} from './hover';
import {Completion, Diagnostic, LanguageService, Span} from './types'; import {Diagnostic, LanguageService} from './types';
import {TypeScriptServiceHost} from './typescript_host'; import {TypeScriptServiceHost} from './typescript_host';
/** /**
* Create an instance of an Angular `LanguageService`. * Create an instance of an Angular `LanguageService`.
* *
@ -53,21 +50,22 @@ class LanguageServiceImpl implements LanguageService {
return uniqueBySpan(results).map(d => ngDiagnosticToTsDiagnostic(d, sourceFile)); return uniqueBySpan(results).map(d => ngDiagnosticToTsDiagnostic(d, sourceFile));
} }
getPipesAt(fileName: string, position: number): CompilePipeSummary[] { getCompletionsAt(fileName: string, position: number): tss.CompletionInfo|undefined {
this.host.getAnalyzedModules(); // same role as 'synchronizeHostData' this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
const templateInfo = this.host.getTemplateAstAtPosition(fileName, position); const templateInfo = this.host.getTemplateAstAtPosition(fileName, position);
if (templateInfo) { if (!templateInfo) {
return templateInfo.pipes; return;
} }
return []; const results = getTemplateCompletions(templateInfo);
} if (!results || !results.length) {
return;
getCompletionsAt(fileName: string, position: number): Completion[]|undefined {
this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
const templateInfo = this.host.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return getTemplateCompletions(templateInfo);
} }
return {
isGlobalCompletion: false,
isMemberCompletion: false,
isNewIdentifierLocation: false,
entries: results.map(ngCompletionToTsCompletionEntry),
};
} }
getDefinitionAt(fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined { getDefinitionAt(fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined {
@ -86,21 +84,3 @@ class LanguageServiceImpl implements LanguageService {
} }
} }
} }
function uniqueBySpan<T extends{span: Span}>(elements: T[]): T[] {
const result: T[] = [];
const map = new Map<number, Set<number>>();
for (const element of elements) {
const {span} = element;
let set = map.get(span.start);
if (!set) {
set = new Set();
map.set(span.start, set);
}
if (!set.has(span.end)) {
set.add(span.end);
result.push(element);
}
}
return result;
}

View File

@ -6,11 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
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 * as tss from 'typescript/lib/tsserverlibrary'; // used as type only
import {createLanguageService} from './language_service'; import {createLanguageService} from './language_service';
import {Completion} from './types';
import {TypeScriptServiceHost} from './typescript_host'; import {TypeScriptServiceHost} from './typescript_host';
const projectHostMap = new WeakMap<tss.server.Project, TypeScriptServiceHost>(); const projectHostMap = new WeakMap<tss.server.Project, TypeScriptServiceHost>();
@ -24,16 +22,6 @@ export function getExternalFiles(project: tss.server.Project): string[]|undefine
} }
} }
function completionToEntry(c: Completion): tss.CompletionEntry {
return {
// TODO: remove any and fix type error.
kind: c.kind as any,
name: c.name,
sortText: c.sort,
kindModifiers: ''
};
}
export function create(info: tss.server.PluginCreateInfo): tss.LanguageService { export function create(info: tss.server.PluginCreateInfo): tss.LanguageService {
const {project, languageService: tsLS, languageServiceHost: tsLSHost, config} = info; const {project, languageService: tsLS, languageServiceHost: tsLSHost, config} = info;
// This plugin could operate under two different modes: // This plugin could operate under two different modes:
@ -60,16 +48,7 @@ export function create(info: tss.server.PluginCreateInfo): tss.LanguageService {
return results; return results;
} }
} }
const results = ngLS.getCompletionsAt(fileName, position); return ngLS.getCompletionsAt(fileName, position);
if (!results || !results.length) {
return;
}
return {
isGlobalCompletion: false,
isMemberCompletion: false,
isNewIdentifierLocation: false,
entries: results.map(completionToEntry),
};
} }
function getQuickInfoAtPosition(fileName: string, position: number): tss.QuickInfo|undefined { function getQuickInfoAtPosition(fileName: string, position: number): tss.QuickInfo|undefined {

View File

@ -376,7 +376,7 @@ export interface LanguageService {
/** /**
* Returns a list of all the external templates referenced by the project. * Returns a list of all the external templates referenced by the project.
*/ */
getTemplateReferences(): string[]|undefined; getTemplateReferences(): string[];
/** /**
* Returns a list of all error for all templates in the given file. * Returns a list of all error for all templates in the given file.
@ -386,7 +386,7 @@ export interface LanguageService {
/** /**
* Return the completions at the given position. * Return the completions at the given position.
*/ */
getCompletionsAt(fileName: string, position: number): Completion[]|undefined; getCompletionsAt(fileName: string, position: number): tss.CompletionInfo|undefined;
/** /**
* Return the definition location for the symbol at position. * Return the definition location for the symbol at position.
@ -397,9 +397,4 @@ export interface LanguageService {
* Return the hover information for the symbol at position. * Return the hover information for the symbol at position.
*/ */
getHoverAt(fileName: string, position: number): tss.QuickInfo|undefined; getHoverAt(fileName: string, position: number): tss.QuickInfo|undefined;
/**
* Return the pipes that are available at the given position.
*/
getPipesAt(fileName: string, position: number): CompilePipeSummary[];
} }

View File

@ -219,8 +219,8 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
const result: Declarations = []; const result: Declarations = [];
const sourceFile = this.getSourceFile(fileName); const sourceFile = this.getSourceFile(fileName);
if (sourceFile) { if (sourceFile) {
let visit = (child: ts.Node) => { const visit = (child: ts.Node) => {
let declaration = this.getDeclarationFromNode(sourceFile, child); const declaration = this.getDeclarationFromNode(sourceFile, child);
if (declaration) { if (declaration) {
result.push(declaration); result.push(declaration);
} else { } else {
@ -383,13 +383,14 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
} }
private get reflector(): StaticReflector { private get reflector(): StaticReflector {
let result = this._reflector; if (!this._reflector) {
if (!result) { this._reflector = new StaticReflector(
const ssr = this.staticSymbolResolver; this.summaryResolver, this.staticSymbolResolver,
result = this._reflector = new StaticReflector( [], // knownMetadataClasses
this.summaryResolver, ssr, [], [], (e, filePath) => this.collectError(e, filePath !)); [], // knownMetadataFunctions
(e, filePath) => this.collectError(e, filePath !));
} }
return result; return this._reflector;
} }
private getTemplateClassFromStaticSymbol(type: StaticSymbol): ts.ClassDeclaration|undefined { private getTemplateClassFromStaticSymbol(type: StaticSymbol): ts.ClassDeclaration|undefined {
@ -442,12 +443,12 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
} }
const callTarget = (<ts.CallExpression>parentNode).expression; const callTarget = (<ts.CallExpression>parentNode).expression;
let decorator = parentNode.parent; // Decorator const decorator = parentNode.parent; // Decorator
if (!decorator || decorator.kind !== ts.SyntaxKind.Decorator) { if (!decorator || decorator.kind !== ts.SyntaxKind.Decorator) {
return TypeScriptServiceHost.missingTemplate; return TypeScriptServiceHost.missingTemplate;
} }
let declaration = <ts.ClassDeclaration>decorator.parent; // ClassDeclaration const declaration = <ts.ClassDeclaration>decorator.parent; // ClassDeclaration
if (!declaration || declaration.kind !== ts.SyntaxKind.ClassDeclaration) { if (!declaration || declaration.kind !== ts.SyntaxKind.ClassDeclaration) {
return TypeScriptServiceHost.missingTemplate; return TypeScriptServiceHost.missingTemplate;
} }
@ -531,73 +532,73 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
} }
getTemplateAstAtPosition(fileName: string, position: number): TemplateInfo|undefined { getTemplateAstAtPosition(fileName: string, position: number): TemplateInfo|undefined {
let template = this.getTemplateAt(fileName, position); const template = this.getTemplateAt(fileName, position);
if (template) { if (!template) {
let astResult = this.getTemplateAst(template, fileName); return;
if (astResult && astResult.htmlAst && astResult.templateAst && astResult.directive && }
astResult.directives && astResult.pipes && astResult.expressionParser) const astResult = this.getTemplateAst(template, fileName);
return { if (astResult && astResult.htmlAst && astResult.templateAst && astResult.directive &&
position, astResult.directives && astResult.pipes && astResult.expressionParser) {
fileName, return {
template, position,
htmlAst: astResult.htmlAst, fileName,
directive: astResult.directive, template,
directives: astResult.directives, htmlAst: astResult.htmlAst,
pipes: astResult.pipes, directive: astResult.directive,
templateAst: astResult.templateAst, directives: astResult.directives,
expressionParser: astResult.expressionParser pipes: astResult.pipes,
}; templateAst: astResult.templateAst,
expressionParser: astResult.expressionParser
};
} }
return undefined;
} }
getTemplateAst(template: TemplateSource, contextFile: string): AstResult { getTemplateAst(template: TemplateSource, contextFile: string): AstResult {
let result: AstResult|undefined = undefined;
try { try {
const resolvedMetadata = const resolvedMetadata = this.resolver.getNonNormalizedDirectiveMetadata(template.type);
this.resolver.getNonNormalizedDirectiveMetadata(template.type as any);
const metadata = resolvedMetadata && resolvedMetadata.metadata; const metadata = resolvedMetadata && resolvedMetadata.metadata;
if (metadata) { if (!metadata) {
const rawHtmlParser = new HtmlParser(); return {};
const htmlParser = new I18NHtmlParser(rawHtmlParser); }
const expressionParser = new Parser(new Lexer()); const rawHtmlParser = new HtmlParser();
const config = new CompilerConfig(); const htmlParser = new I18NHtmlParser(rawHtmlParser);
const parser = new TemplateParser( const expressionParser = new Parser(new Lexer());
config, this.resolver.getReflector(), expressionParser, new DomElementSchemaRegistry(), const config = new CompilerConfig();
htmlParser, null !, []); const parser = new TemplateParser(
const htmlResult = htmlParser.parse(template.source, '', {tokenizeExpansionForms: true}); config, this.resolver.getReflector(), expressionParser, new DomElementSchemaRegistry(),
let errors: Diagnostic[]|undefined = undefined; htmlParser, null !, []);
let ngModule = this.analyzedModules.ngModuleByPipeOrDirective.get(template.type); const htmlResult = htmlParser.parse(template.source, '', {tokenizeExpansionForms: true});
if (!ngModule) { const errors: Diagnostic[]|undefined = undefined;
const ngModule = this.analyzedModules.ngModuleByPipeOrDirective.get(template.type) ||
// Reported by the the declaration diagnostics. // Reported by the the declaration diagnostics.
ngModule = findSuitableDefaultModule(this.analyzedModules); findSuitableDefaultModule(this.analyzedModules);
} if (!ngModule) {
if (ngModule) { return {};
const directives =
ngModule.transitiveModule.directives
.map(d => this.resolver.getNonNormalizedDirectiveMetadata(d.reference))
.filter(d => d)
.map(d => d !.metadata.toSummary());
const pipes = ngModule.transitiveModule.pipes.map(
p => this.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
const schemas = ngModule.schemas;
const parseResult = parser.tryParseHtml(htmlResult, metadata, directives, pipes, schemas);
result = {
htmlAst: htmlResult.rootNodes,
templateAst: parseResult.templateAst,
directive: metadata, directives, pipes,
parseErrors: parseResult.errors, expressionParser, errors
};
}
} }
const directives = ngModule.transitiveModule.directives
.map(d => this.resolver.getNonNormalizedDirectiveMetadata(d.reference))
.filter(d => d)
.map(d => d !.metadata.toSummary());
const pipes = ngModule.transitiveModule.pipes.map(
p => this.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
const schemas = ngModule.schemas;
const parseResult = parser.tryParseHtml(htmlResult, metadata, directives, pipes, schemas);
return {
htmlAst: htmlResult.rootNodes,
templateAst: parseResult.templateAst,
directive: metadata, directives, pipes,
parseErrors: parseResult.errors, expressionParser, errors
};
} catch (e) { } catch (e) {
let span = template.span; const span =
if (e.fileName == contextFile) { e.fileName === contextFile && template.query.getSpanAt(e.line, e.column) || template.span;
span = template.query.getSpanAt(e.line, e.column) || span; return {
} errors: [{
result = {errors: [{kind: DiagnosticKind.Error, message: e.message, span}]}; kind: DiagnosticKind.Error,
message: e.message, span,
}],
};
} }
return result || {};
} }
} }

View File

@ -229,24 +229,23 @@ export class MyComponent {
function expectEntries( function expectEntries(
locationMarker: string, completions: Completion[] | undefined, ...names: string[]) { locationMarker: string, completion: ts.CompletionInfo | undefined, ...names: string[]) {
let entries: {[name: string]: boolean} = {}; let entries: {[name: string]: boolean} = {};
if (!completions) { if (!completion) {
throw new Error( throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`); `Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
} }
if (!completions.length) { if (!completion.entries.length) {
throw new Error( throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} an empty result provided`); `Expected result from ${locationMarker} to include ${names.join(', ')} an empty result provided`);
} else { }
for (let entry of completions) { for (const entry of completion.entries) {
entries[entry.name] = true; entries[entry.name] = true;
} }
let missing = names.filter(name => !entries[name]); let missing = names.filter(name => !entries[name]);
if (missing.length) { if (missing.length) {
throw new Error( throw new Error(
`Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${completions.map(entry => entry.name).join(', ')}`); `Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${completion.entries.map(entry => entry.name).join(', ')}`);
}
} }
} }