refactor(language-service): Create ng.CompletionEntry to replace ts.CompletionEntry (#33379)
It is messy to keep casting `CompletionEntry.kind` from `ng.CompletionKind` to `ts.ScriptElementKind`. Instead, create a new type `ng.CompletionEntry` that is exactly the same as `ts.CompletionEntry`, but with the `kind` type overridden to `ng.CompletionKind`. This way, we only have to cast it once, and can do so in a safe manner. PR Close #33379
This commit is contained in:
parent
b00189bb9a
commit
73530a9e25
@ -12,7 +12,7 @@ import {getExpressionScope} from '@angular/compiler-cli/src/language_services';
|
|||||||
import {AstResult, AttrInfo} from './common';
|
import {AstResult, AttrInfo} from './common';
|
||||||
import {getExpressionCompletions} from './expressions';
|
import {getExpressionCompletions} from './expressions';
|
||||||
import {attributeNames, elementNames, eventNames, propertyNames} from './html_info';
|
import {attributeNames, elementNames, eventNames, propertyNames} from './html_info';
|
||||||
import {CompletionKind, Span, Symbol, SymbolTable, TemplateSource} from './types';
|
import * as ng from './types';
|
||||||
import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, getSelectors, hasTemplateReference, inSpan, spanOf} from './utils';
|
import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, getSelectors, hasTemplateReference, inSpan, spanOf} from './utils';
|
||||||
|
|
||||||
const TEMPLATE_ATTR_PREFIX = '*';
|
const TEMPLATE_ATTR_PREFIX = '*';
|
||||||
@ -31,8 +31,8 @@ const hiddenHtmlElements = {
|
|||||||
const ANGULAR_ELEMENTS: ReadonlyArray<string> = ['ng-container', 'ng-content', 'ng-template'];
|
const ANGULAR_ELEMENTS: ReadonlyArray<string> = ['ng-container', 'ng-content', 'ng-template'];
|
||||||
|
|
||||||
export function getTemplateCompletions(
|
export function getTemplateCompletions(
|
||||||
templateInfo: AstResult, position: number): ts.CompletionEntry[] {
|
templateInfo: AstResult, position: number): ng.CompletionEntry[] {
|
||||||
let result: ts.CompletionEntry[] = [];
|
let result: ng.CompletionEntry[] = [];
|
||||||
const {htmlAst, template} = templateInfo;
|
const {htmlAst, template} = templateInfo;
|
||||||
// The templateNode starts at the delimiter character so we add 1 to skip it.
|
// The templateNode starts at the delimiter character so we add 1 to skip it.
|
||||||
const templatePosition = position - template.span.start;
|
const templatePosition = position - template.span.start;
|
||||||
@ -98,7 +98,7 @@ export function getTemplateCompletions(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function attributeCompletions(info: AstResult, path: AstPath<HtmlAst>): ts.CompletionEntry[] {
|
function attributeCompletions(info: AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
|
||||||
const item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail);
|
const item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail);
|
||||||
if (item instanceof Element) {
|
if (item instanceof Element) {
|
||||||
return attributeCompletionsForElement(info, item.name);
|
return attributeCompletionsForElement(info, item.name);
|
||||||
@ -107,14 +107,14 @@ function attributeCompletions(info: AstResult, path: AstPath<HtmlAst>): ts.Compl
|
|||||||
}
|
}
|
||||||
|
|
||||||
function attributeCompletionsForElement(
|
function attributeCompletionsForElement(
|
||||||
info: AstResult, elementName: string): ts.CompletionEntry[] {
|
info: AstResult, elementName: string): ng.CompletionEntry[] {
|
||||||
const results: ts.CompletionEntry[] = [];
|
const results: ng.CompletionEntry[] = [];
|
||||||
|
|
||||||
// Add html attributes
|
// Add html attributes
|
||||||
for (const name of attributeNames(elementName)) {
|
for (const name of attributeNames(elementName)) {
|
||||||
results.push({
|
results.push({
|
||||||
name,
|
name,
|
||||||
kind: CompletionKind.HTML_ATTRIBUTE as unknown as ts.ScriptElementKind,
|
kind: ng.CompletionKind.HTML_ATTRIBUTE,
|
||||||
sortText: name,
|
sortText: name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -123,7 +123,7 @@ function attributeCompletionsForElement(
|
|||||||
for (const name of propertyNames(elementName)) {
|
for (const name of propertyNames(elementName)) {
|
||||||
results.push({
|
results.push({
|
||||||
name: `[${name}]`,
|
name: `[${name}]`,
|
||||||
kind: CompletionKind.ATTRIBUTE as unknown as ts.ScriptElementKind,
|
kind: ng.CompletionKind.ATTRIBUTE,
|
||||||
sortText: name,
|
sortText: name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -132,7 +132,7 @@ function attributeCompletionsForElement(
|
|||||||
for (const name of eventNames(elementName)) {
|
for (const name of eventNames(elementName)) {
|
||||||
results.push({
|
results.push({
|
||||||
name: `(${name})`,
|
name: `(${name})`,
|
||||||
kind: CompletionKind.ATTRIBUTE as unknown as ts.ScriptElementKind,
|
kind: ng.CompletionKind.ATTRIBUTE,
|
||||||
sortText: name,
|
sortText: name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -144,7 +144,7 @@ function attributeCompletionsForElement(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function attributeValueCompletions(
|
function attributeValueCompletions(
|
||||||
info: AstResult, position: number, attr: Attribute): ts.CompletionEntry[] {
|
info: AstResult, position: number, attr: Attribute): ng.CompletionEntry[] {
|
||||||
const path = findTemplateAstAt(info.templateAst, position);
|
const path = findTemplateAstAt(info.templateAst, position);
|
||||||
if (!path.tail) {
|
if (!path.tail) {
|
||||||
return [];
|
return [];
|
||||||
@ -166,7 +166,7 @@ function attributeValueCompletions(
|
|||||||
return visitor.result || [];
|
return visitor.result || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function elementCompletions(info: AstResult, path: AstPath<HtmlAst>): ts.CompletionEntry[] {
|
function elementCompletions(info: AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
|
||||||
const htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements));
|
const htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements));
|
||||||
|
|
||||||
// Collect the elements referenced by the selectors
|
// Collect the elements referenced by the selectors
|
||||||
@ -177,27 +177,21 @@ function elementCompletions(info: AstResult, path: AstPath<HtmlAst>): ts.Complet
|
|||||||
const components = directiveElements.map(name => {
|
const components = directiveElements.map(name => {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
// Need to cast to unknown because Angular's CompletionKind includes HTML
|
kind: ng.CompletionKind.COMPONENT,
|
||||||
// entites.
|
|
||||||
kind: CompletionKind.COMPONENT as unknown as ts.ScriptElementKind,
|
|
||||||
sortText: name,
|
sortText: name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const htmlElements = htmlNames.map(name => {
|
const htmlElements = htmlNames.map(name => {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
// Need to cast to unknown because Angular's CompletionKind includes HTML
|
kind: ng.CompletionKind.ELEMENT,
|
||||||
// entites.
|
|
||||||
kind: CompletionKind.ELEMENT as unknown as ts.ScriptElementKind,
|
|
||||||
sortText: name,
|
sortText: name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const angularElements = ANGULAR_ELEMENTS.map(name => {
|
const angularElements = ANGULAR_ELEMENTS.map(name => {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
// Need to cast to unknown because Angular's CompletionKind includes HTML
|
kind: ng.CompletionKind.ANGULAR_ELEMENT,
|
||||||
// entites.
|
|
||||||
kind: CompletionKind.ANGULAR_ELEMENT as unknown as ts.ScriptElementKind,
|
|
||||||
sortText: name,
|
sortText: name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -210,7 +204,7 @@ function elementCompletions(info: AstResult, path: AstPath<HtmlAst>): ts.Complet
|
|||||||
* Filter the specified `entries` by unique name.
|
* Filter the specified `entries` by unique name.
|
||||||
* @param entries Completion Entries
|
* @param entries Completion Entries
|
||||||
*/
|
*/
|
||||||
function uniqueByName(entries: ts.CompletionEntry[]) {
|
function uniqueByName(entries: ng.CompletionEntry[]) {
|
||||||
const results = [];
|
const results = [];
|
||||||
const set = new Set();
|
const set = new Set();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@ -222,20 +216,18 @@ function uniqueByName(entries: ts.CompletionEntry[]) {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
function entityCompletions(value: string, position: number): ts.CompletionEntry[] {
|
function entityCompletions(value: string, position: number): ng.CompletionEntry[] {
|
||||||
// Look for entity completions
|
// Look for entity completions
|
||||||
const re = /&[A-Za-z]*;?(?!\d)/g;
|
const re = /&[A-Za-z]*;?(?!\d)/g;
|
||||||
let found: RegExpExecArray|null;
|
let found: RegExpExecArray|null;
|
||||||
let result: ts.CompletionEntry[] = [];
|
let result: ng.CompletionEntry[] = [];
|
||||||
while (found = re.exec(value)) {
|
while (found = re.exec(value)) {
|
||||||
let len = found[0].length;
|
let len = found[0].length;
|
||||||
if (position >= found.index && position < (found.index + len)) {
|
if (position >= found.index && position < (found.index + len)) {
|
||||||
result = Object.keys(NAMED_ENTITIES).map(name => {
|
result = Object.keys(NAMED_ENTITIES).map(name => {
|
||||||
return {
|
return {
|
||||||
name: `&${name};`,
|
name: `&${name};`,
|
||||||
// Need to cast to unknown because Angular's CompletionKind includes
|
kind: ng.CompletionKind.ENTITY,
|
||||||
// HTML entites.
|
|
||||||
kind: CompletionKind.ENTITY as unknown as ts.ScriptElementKind,
|
|
||||||
sortText: name,
|
sortText: name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -245,7 +237,7 @@ function entityCompletions(value: string, position: number): ts.CompletionEntry[
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function interpolationCompletions(info: AstResult, position: number): ts.CompletionEntry[] {
|
function interpolationCompletions(info: AstResult, position: number): ng.CompletionEntry[] {
|
||||||
// Look for an interpolation in at the position.
|
// Look for an interpolation in at the position.
|
||||||
const templatePath = findTemplateAstAt(info.templateAst, position);
|
const templatePath = findTemplateAstAt(info.templateAst, position);
|
||||||
if (!templatePath.tail) {
|
if (!templatePath.tail) {
|
||||||
@ -265,7 +257,7 @@ function interpolationCompletions(info: AstResult, position: number): ts.Complet
|
|||||||
// code checks for this case and returns element completions if it is detected or undefined
|
// code checks for this case and returns element completions if it is detected or undefined
|
||||||
// if it is not.
|
// if it is not.
|
||||||
function voidElementAttributeCompletions(
|
function voidElementAttributeCompletions(
|
||||||
info: AstResult, path: AstPath<HtmlAst>): ts.CompletionEntry[] {
|
info: AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
|
||||||
const tail = path.tail;
|
const tail = path.tail;
|
||||||
if (tail instanceof Text) {
|
if (tail instanceof Text) {
|
||||||
const match = tail.value.match(/<(\w(\w|\d|-)*:)?(\w(\w|\d|-)*)\s/);
|
const match = tail.value.match(/<(\w(\w|\d|-)*:)?(\w(\w|\d|-)*)\s/);
|
||||||
@ -280,12 +272,12 @@ function voidElementAttributeCompletions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ExpressionVisitor extends NullTemplateVisitor {
|
class ExpressionVisitor extends NullTemplateVisitor {
|
||||||
private getExpressionScope: () => SymbolTable;
|
private getExpressionScope: () => ng.SymbolTable;
|
||||||
result: ts.CompletionEntry[]|undefined;
|
result: ng.CompletionEntry[]|undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private info: AstResult, private position: number, private attr?: Attribute,
|
private info: AstResult, private position: number, private attr?: Attribute,
|
||||||
getExpressionScope?: () => SymbolTable) {
|
getExpressionScope?: () => ng.SymbolTable) {
|
||||||
super();
|
super();
|
||||||
this.getExpressionScope = getExpressionScope || (() => info.template.members);
|
this.getExpressionScope = getExpressionScope || (() => info.template.members);
|
||||||
}
|
}
|
||||||
@ -337,9 +329,7 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
|||||||
this.result = keys.map(key => {
|
this.result = keys.map(key => {
|
||||||
return {
|
return {
|
||||||
name: key,
|
name: key,
|
||||||
// Need to cast to unknown because Angular's CompletionKind includes
|
kind: ng.CompletionKind.KEY,
|
||||||
// HTML entites.
|
|
||||||
kind: CompletionKind.KEY as unknown as ts.ScriptElementKind,
|
|
||||||
sortText: key,
|
sortText: key,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -408,11 +398,11 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private symbolsToCompletions(symbols: Symbol[]): ts.CompletionEntry[] {
|
private symbolsToCompletions(symbols: ng.Symbol[]): ng.CompletionEntry[] {
|
||||||
return symbols.filter(s => !s.name.startsWith('__') && s.public).map(symbol => {
|
return symbols.filter(s => !s.name.startsWith('__') && s.public).map(symbol => {
|
||||||
return {
|
return {
|
||||||
name: symbol.name,
|
name: symbol.name,
|
||||||
kind: symbol.kind as ts.ScriptElementKind,
|
kind: symbol.kind as ng.CompletionKind,
|
||||||
sortText: symbol.name,
|
sortText: symbol.name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -426,7 +416,7 @@ class ExpressionVisitor extends NullTemplateVisitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSourceText(template: TemplateSource, span: Span): string {
|
function getSourceText(template: ng.TemplateSource, span: ng.Span): string {
|
||||||
return template.source.substring(span.start, span.end);
|
return template.source.substring(span.start, span.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -454,7 +444,7 @@ function lowerName(name: string): string {
|
|||||||
return name && (name[0].toLowerCase() + name.substr(1));
|
return name && (name[0].toLowerCase() + name.substr(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
function angularAttributes(info: AstResult, elementName: string): ts.CompletionEntry[] {
|
function angularAttributes(info: AstResult, elementName: string): ng.CompletionEntry[] {
|
||||||
const {selectors, map: selectorMap} = getSelectors(info);
|
const {selectors, map: selectorMap} = getSelectors(info);
|
||||||
const templateRefs = new Set<string>();
|
const templateRefs = new Set<string>();
|
||||||
const inputs = new Set<string>();
|
const inputs = new Set<string>();
|
||||||
@ -482,18 +472,18 @@ function angularAttributes(info: AstResult, elementName: string): ts.CompletionE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: ts.CompletionEntry[] = [];
|
const results: ng.CompletionEntry[] = [];
|
||||||
for (const name of templateRefs) {
|
for (const name of templateRefs) {
|
||||||
results.push({
|
results.push({
|
||||||
name: `*${name}`,
|
name: `*${name}`,
|
||||||
kind: CompletionKind.ATTRIBUTE as unknown as ts.ScriptElementKind,
|
kind: ng.CompletionKind.ATTRIBUTE,
|
||||||
sortText: name,
|
sortText: name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const name of inputs) {
|
for (const name of inputs) {
|
||||||
results.push({
|
results.push({
|
||||||
name: `[${name}]`,
|
name: `[${name}]`,
|
||||||
kind: CompletionKind.ATTRIBUTE as unknown as ts.ScriptElementKind,
|
kind: ng.CompletionKind.ATTRIBUTE,
|
||||||
sortText: name,
|
sortText: name,
|
||||||
});
|
});
|
||||||
// Add banana-in-a-box syntax
|
// Add banana-in-a-box syntax
|
||||||
@ -501,7 +491,7 @@ function angularAttributes(info: AstResult, elementName: string): ts.CompletionE
|
|||||||
if (outputs.has(`${name}Change`)) {
|
if (outputs.has(`${name}Change`)) {
|
||||||
results.push({
|
results.push({
|
||||||
name: `[(${name})]`,
|
name: `[(${name})]`,
|
||||||
kind: CompletionKind.ATTRIBUTE as unknown as ts.ScriptElementKind,
|
kind: ng.CompletionKind.ATTRIBUTE,
|
||||||
sortText: name,
|
sortText: name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -509,14 +499,14 @@ function angularAttributes(info: AstResult, elementName: string): ts.CompletionE
|
|||||||
for (const name of outputs) {
|
for (const name of outputs) {
|
||||||
results.push({
|
results.push({
|
||||||
name: `(${name})`,
|
name: `(${name})`,
|
||||||
kind: CompletionKind.ATTRIBUTE as unknown as ts.ScriptElementKind,
|
kind: ng.CompletionKind.ATTRIBUTE,
|
||||||
sortText: name,
|
sortText: name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const name of others) {
|
for (const name of others) {
|
||||||
results.push({
|
results.push({
|
||||||
name,
|
name,
|
||||||
kind: CompletionKind.ATTRIBUTE as unknown as ts.ScriptElementKind,
|
kind: ng.CompletionKind.ATTRIBUTE,
|
||||||
sortText: name,
|
sortText: name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,8 @@ class LanguageServiceImpl implements LanguageService {
|
|||||||
isGlobalCompletion: false,
|
isGlobalCompletion: false,
|
||||||
isMemberCompletion: false,
|
isMemberCompletion: false,
|
||||||
isNewIdentifierLocation: false,
|
isNewIdentifierLocation: false,
|
||||||
entries: results,
|
// Cast CompletionEntry.kind from ng.CompletionKind to ts.ScriptElementKind
|
||||||
|
entries: results as unknown as ts.CompletionEntry[],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,6 +277,10 @@ export enum CompletionKind {
|
|||||||
VARIABLE = 'variable',
|
VARIABLE = 'variable',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CompletionEntry = Omit<ts.CompletionEntry, 'kind'>& {
|
||||||
|
kind: CompletionKind,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A template diagnostics message chain. This is similar to the TypeScript
|
* A template diagnostics message chain. This is similar to the TypeScript
|
||||||
* DiagnosticMessageChain. The messages are intended to be formatted as separate
|
* DiagnosticMessageChain. The messages are intended to be formatted as separate
|
||||||
|
Loading…
x
Reference in New Issue
Block a user