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:
parent
d6bbc4d76d
commit
5a562d8a0a
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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[];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 || {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(', ')}`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue