fix(compiler-cli): autocomplete literal types in templates. (#41456)
This adds string literals, number literals, `true`, `false`, `null` and `undefined` to autocomplete results in templates. For example, when completing an input of union type. Component: `@Input('input') input!: 'a'|'b'|null;` Template: `[input]="|"` Provide `'a'`, `'b'`, and `null` as autocompletion entries. Previously we did not include literal types because we only included results from the component context (`ctx.`) and the template scope. PR Close #41456
This commit is contained in:
parent
ba84fa6f42
commit
1d12c50f63
|
@ -105,8 +105,9 @@ export interface TemplateTypeChecker {
|
||||||
* include completions from the template's context component, as well as any local references or
|
* include completions from the template's context component, as well as any local references or
|
||||||
* template variables which are in scope for that expression.
|
* template variables which are in scope for that expression.
|
||||||
*/
|
*/
|
||||||
getGlobalCompletions(context: TmplAstTemplate|null, component: ts.ClassDeclaration):
|
getGlobalCompletions(
|
||||||
GlobalCompletion|null;
|
context: TmplAstTemplate|null, component: ts.ClassDeclaration,
|
||||||
|
node: AST|TmplAstNode): GlobalCompletion|null;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -71,4 +71,10 @@ export interface GlobalCompletion {
|
||||||
* the same name (from the `componentContext` completions).
|
* the same name (from the `componentContext` completions).
|
||||||
*/
|
*/
|
||||||
templateContext: Map<string, ReferenceCompletion|VariableCompletion>;
|
templateContext: Map<string, ReferenceCompletion|VariableCompletion>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A location within the type-checking shim where TypeScript's completion APIs can be used to
|
||||||
|
* access completions for the AST node of the cursor position (primitive constants).
|
||||||
|
*/
|
||||||
|
nodeContext: ShimLocation|null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -257,14 +257,15 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
return this.getLatestComponentState(component).tcb;
|
return this.getLatestComponentState(component).tcb;
|
||||||
}
|
}
|
||||||
|
|
||||||
getGlobalCompletions(context: TmplAstTemplate|null, component: ts.ClassDeclaration):
|
getGlobalCompletions(
|
||||||
GlobalCompletion|null {
|
context: TmplAstTemplate|null, component: ts.ClassDeclaration,
|
||||||
|
node: AST|TmplAstNode): GlobalCompletion|null {
|
||||||
const engine = this.getOrCreateCompletionEngine(component);
|
const engine = this.getOrCreateCompletionEngine(component);
|
||||||
if (engine === null) {
|
if (engine === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return this.perf.inPhase(
|
return this.perf.inPhase(
|
||||||
PerfPhase.TtcAutocompletion, () => engine.getGlobalCompletions(context));
|
PerfPhase.TtcAutocompletion, () => engine.getGlobalCompletions(context, node));
|
||||||
}
|
}
|
||||||
|
|
||||||
getExpressionCompletionLocation(
|
getExpressionCompletionLocation(
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TmplAstReference, TmplAstTemplate} from '@angular/compiler';
|
import {TmplAstReference, TmplAstTemplate} from '@angular/compiler';
|
||||||
import {MethodCall, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead} from '@angular/compiler/src/compiler';
|
import {AST, EmptyExpr, MethodCall, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstNode} from '@angular/compiler/src/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {AbsoluteFsPath} from '../../file_system';
|
import {AbsoluteFsPath} from '../../file_system';
|
||||||
|
@ -23,65 +23,77 @@ import {TemplateData} from './context';
|
||||||
* surrounding TS program have changed.
|
* surrounding TS program have changed.
|
||||||
*/
|
*/
|
||||||
export class CompletionEngine {
|
export class CompletionEngine {
|
||||||
|
private componentContext: ShimLocation|null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache of `GlobalCompletion`s for various levels of the template, including the root template
|
* Cache of completions for various levels of the template, including the root template (`null`).
|
||||||
* (`null`).
|
* Memoizes `getTemplateContextCompletions`.
|
||||||
*/
|
*/
|
||||||
private globalCompletionCache = new Map<TmplAstTemplate|null, GlobalCompletion>();
|
private templateContextCache =
|
||||||
|
new Map<TmplAstTemplate|null, Map<string, ReferenceCompletion|VariableCompletion>>();
|
||||||
|
|
||||||
private expressionCompletionCache =
|
private expressionCompletionCache =
|
||||||
new Map<PropertyRead|SafePropertyRead|MethodCall|SafeMethodCall, ShimLocation>();
|
new Map<PropertyRead|SafePropertyRead|MethodCall|SafeMethodCall, ShimLocation>();
|
||||||
|
|
||||||
constructor(private tcb: ts.Node, private data: TemplateData, private shimPath: AbsoluteFsPath) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get global completions within the given template context - either a `TmplAstTemplate` embedded
|
|
||||||
* view, or `null` for the root template context.
|
|
||||||
*/
|
|
||||||
getGlobalCompletions(context: TmplAstTemplate|null): GlobalCompletion|null {
|
|
||||||
if (this.globalCompletionCache.has(context)) {
|
|
||||||
return this.globalCompletionCache.get(context)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
constructor(private tcb: ts.Node, private data: TemplateData, private shimPath: AbsoluteFsPath) {
|
||||||
// Find the component completion expression within the TCB. This looks like: `ctx. /* ... */;`
|
// Find the component completion expression within the TCB. This looks like: `ctx. /* ... */;`
|
||||||
const globalRead = findFirstMatchingNode(this.tcb, {
|
const globalRead = findFirstMatchingNode(this.tcb, {
|
||||||
filter: ts.isPropertyAccessExpression,
|
filter: ts.isPropertyAccessExpression,
|
||||||
withExpressionIdentifier: ExpressionIdentifier.COMPONENT_COMPLETION
|
withExpressionIdentifier: ExpressionIdentifier.COMPONENT_COMPLETION
|
||||||
});
|
});
|
||||||
|
|
||||||
if (globalRead === null) {
|
if (globalRead !== null) {
|
||||||
return null;
|
this.componentContext = {
|
||||||
}
|
|
||||||
|
|
||||||
const completion: GlobalCompletion = {
|
|
||||||
componentContext: {
|
|
||||||
shimPath: this.shimPath,
|
shimPath: this.shimPath,
|
||||||
// `globalRead.name` is an empty `ts.Identifier`, so its start position immediately follows
|
// `globalRead.name` is an empty `ts.Identifier`, so its start position immediately follows
|
||||||
// the `.` in `ctx.`. TS autocompletion APIs can then be used to access completion results
|
// the `.` in `ctx.`. TS autocompletion APIs can then be used to access completion results
|
||||||
// for the component context.
|
// for the component context.
|
||||||
positionInShimFile: globalRead.name.getStart(),
|
positionInShimFile: globalRead.name.getStart(),
|
||||||
},
|
|
||||||
templateContext: new Map<string, ReferenceCompletion|VariableCompletion>(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The bound template already has details about the references and variables in scope in the
|
|
||||||
// `context` template - they just need to be converted to `Completion`s.
|
|
||||||
for (const node of this.data.boundTarget.getEntitiesInTemplateScope(context)) {
|
|
||||||
if (node instanceof TmplAstReference) {
|
|
||||||
completion.templateContext.set(node.name, {
|
|
||||||
kind: CompletionKind.Reference,
|
|
||||||
node,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
completion.templateContext.set(node.name, {
|
this.componentContext = null;
|
||||||
kind: CompletionKind.Variable,
|
|
||||||
node,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.globalCompletionCache.set(context, completion);
|
/**
|
||||||
return completion;
|
* Get global completions within the given template context and AST node.
|
||||||
|
*
|
||||||
|
* @param context the given template context - either a `TmplAstTemplate` embedded view, or `null`
|
||||||
|
* for the root
|
||||||
|
* template context.
|
||||||
|
* @param node the given AST node
|
||||||
|
*/
|
||||||
|
getGlobalCompletions(context: TmplAstTemplate|null, node: AST|TmplAstNode): GlobalCompletion
|
||||||
|
|null {
|
||||||
|
if (this.componentContext === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateContext = this.getTemplateContextCompletions(context);
|
||||||
|
if (templateContext === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodeContext: ShimLocation|null = null;
|
||||||
|
if (node instanceof EmptyExpr) {
|
||||||
|
const nodeLocation = findFirstMatchingNode(this.tcb, {
|
||||||
|
filter: ts.isIdentifier,
|
||||||
|
withSpan: node.sourceSpan,
|
||||||
|
});
|
||||||
|
if (nodeLocation !== null) {
|
||||||
|
nodeContext = {
|
||||||
|
shimPath: this.shimPath,
|
||||||
|
positionInShimFile: nodeLocation.getStart(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
componentContext: this.componentContext,
|
||||||
|
templateContext,
|
||||||
|
nodeContext,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getExpressionCompletionLocation(expr: PropertyRead|PropertyWrite|MethodCall|
|
getExpressionCompletionLocation(expr: PropertyRead|PropertyWrite|MethodCall|
|
||||||
|
@ -131,4 +143,36 @@ export class CompletionEngine {
|
||||||
this.expressionCompletionCache.set(expr, res);
|
this.expressionCompletionCache.set(expr, res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get global completions within the given template context - either a `TmplAstTemplate` embedded
|
||||||
|
* view, or `null` for the root context.
|
||||||
|
*/
|
||||||
|
private getTemplateContextCompletions(context: TmplAstTemplate|null):
|
||||||
|
Map<string, ReferenceCompletion|VariableCompletion>|null {
|
||||||
|
if (this.templateContextCache.has(context)) {
|
||||||
|
return this.templateContextCache.get(context)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateContext = new Map<string, ReferenceCompletion|VariableCompletion>();
|
||||||
|
|
||||||
|
// The bound template already has details about the references and variables in scope in the
|
||||||
|
// `context` template - they just need to be converted to `Completion`s.
|
||||||
|
for (const node of this.data.boundTarget.getEntitiesInTemplateScope(context)) {
|
||||||
|
if (node instanceof TmplAstReference) {
|
||||||
|
templateContext.set(node.name, {
|
||||||
|
kind: CompletionKind.Reference,
|
||||||
|
node,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
templateContext.set(node.name, {
|
||||||
|
kind: CompletionKind.Variable,
|
||||||
|
node,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.templateContextCache.set(context, templateContext);
|
||||||
|
return templateContext;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,7 +141,7 @@ function setupCompletions(
|
||||||
context = tmpl;
|
context = tmpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const completions = templateTypeChecker.getGlobalCompletions(context, SomeCmp)!;
|
const completions = templateTypeChecker.getGlobalCompletions(context, SomeCmp, null!)!;
|
||||||
expect(completions).toBeDefined();
|
expect(completions).toBeDefined();
|
||||||
return {
|
return {
|
||||||
completions,
|
completions,
|
||||||
|
|
|
@ -216,21 +216,21 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
|
||||||
options: ts.GetCompletionsAtPositionOptions|
|
options: ts.GetCompletionsAtPositionOptions|
|
||||||
undefined): ts.WithMetadata<ts.CompletionInfo>|undefined {
|
undefined): ts.WithMetadata<ts.CompletionInfo>|undefined {
|
||||||
const completions =
|
const completions =
|
||||||
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
|
this.templateTypeChecker.getGlobalCompletions(this.template, this.component, this.node);
|
||||||
if (completions === null) {
|
if (completions === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {componentContext, templateContext} = completions;
|
const {componentContext, templateContext, nodeContext: astContext} = completions;
|
||||||
|
|
||||||
const replacementSpan = makeReplacementSpanFromAst(this.node);
|
const replacementSpan = makeReplacementSpanFromAst(this.node);
|
||||||
|
|
||||||
// Merge TS completion results with results from the template scope.
|
// Merge TS completion results with results from the template scope.
|
||||||
let entries: ts.CompletionEntry[] = [];
|
let entries: ts.CompletionEntry[] = [];
|
||||||
const tsLsCompletions = this.tsLS.getCompletionsAtPosition(
|
const componentCompletions = this.tsLS.getCompletionsAtPosition(
|
||||||
componentContext.shimPath, componentContext.positionInShimFile, options);
|
componentContext.shimPath, componentContext.positionInShimFile, options);
|
||||||
if (tsLsCompletions !== undefined) {
|
if (componentCompletions !== undefined) {
|
||||||
for (const tsCompletion of tsLsCompletions.entries) {
|
for (const tsCompletion of componentCompletions.entries) {
|
||||||
// Skip completions that are shadowed by a template entity definition.
|
// Skip completions that are shadowed by a template entity definition.
|
||||||
if (templateContext.has(tsCompletion.name)) {
|
if (templateContext.has(tsCompletion.name)) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -244,6 +244,24 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge TS completion results with results from the ast context.
|
||||||
|
if (astContext !== null) {
|
||||||
|
const nodeCompletions = this.tsLS.getCompletionsAtPosition(
|
||||||
|
astContext.shimPath, astContext.positionInShimFile, options);
|
||||||
|
if (nodeCompletions !== undefined) {
|
||||||
|
for (const tsCompletion of nodeCompletions.entries) {
|
||||||
|
if (this.isValidNodeContextCompletion(tsCompletion)) {
|
||||||
|
entries.push({
|
||||||
|
...tsCompletion,
|
||||||
|
// Substitute the TS completion's `replacementSpan` (which uses offsets within the
|
||||||
|
// TCB) with the `replacementSpan` within the template source.
|
||||||
|
replacementSpan,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [name, entity] of templateContext) {
|
for (const [name, entity] of templateContext) {
|
||||||
entries.push({
|
entries.push({
|
||||||
name,
|
name,
|
||||||
|
@ -276,7 +294,7 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
|
||||||
formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined,
|
formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined,
|
||||||
preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined {
|
preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined {
|
||||||
const completions =
|
const completions =
|
||||||
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
|
this.templateTypeChecker.getGlobalCompletions(this.template, this.component, this.node);
|
||||||
if (completions === null) {
|
if (completions === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -317,7 +335,7 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
|
||||||
private getGlobalPropertyExpressionCompletionSymbol(
|
private getGlobalPropertyExpressionCompletionSymbol(
|
||||||
this: PropertyExpressionCompletionBuilder, entryName: string): ts.Symbol|undefined {
|
this: PropertyExpressionCompletionBuilder, entryName: string): ts.Symbol|undefined {
|
||||||
const completions =
|
const completions =
|
||||||
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
|
this.templateTypeChecker.getGlobalCompletions(this.template, this.component, this.node);
|
||||||
if (completions === null) {
|
if (completions === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -638,6 +656,25 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
|
||||||
isNewIdentifierLocation: false,
|
isNewIdentifierLocation: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From the AST node of the cursor position, include completion of string literals, number
|
||||||
|
* literals, `true`, `false`, `null`, and `undefined`.
|
||||||
|
*/
|
||||||
|
private isValidNodeContextCompletion(completion: ts.CompletionEntry): boolean {
|
||||||
|
if (completion.kind === ts.ScriptElementKind.string) {
|
||||||
|
// 'string' kind includes both string literals and number literals
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (completion.kind === ts.ScriptElementKind.keyword) {
|
||||||
|
return completion.name === 'true' || completion.name === 'false' ||
|
||||||
|
completion.name === 'null';
|
||||||
|
}
|
||||||
|
if (completion.kind === ts.ScriptElementKind.variableElement) {
|
||||||
|
return completion.name === 'undefined';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeReplacementSpanFromParseSourceSpan(span: ParseSourceSpan): ts.TextSpan {
|
function makeReplacementSpanFromParseSourceSpan(span: ParseSourceSpan): ts.TextSpan {
|
||||||
|
|
|
@ -24,6 +24,18 @@ const DIR_WITH_INPUT = {
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DIR_WITH_UNION_TYPE_INPUT = {
|
||||||
|
'Dir': `
|
||||||
|
@Directive({
|
||||||
|
selector: '[dir]',
|
||||||
|
inputs: ['myInput']
|
||||||
|
})
|
||||||
|
export class Dir {
|
||||||
|
myInput!: 'foo'|42|null|undefined
|
||||||
|
}
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
const DIR_WITH_OUTPUT = {
|
const DIR_WITH_OUTPUT = {
|
||||||
'Dir': `
|
'Dir': `
|
||||||
@Directive({
|
@Directive({
|
||||||
|
@ -203,6 +215,18 @@ describe('completions', () => {
|
||||||
const completions = templateFile.getCompletionsAtPosition();
|
const completions = templateFile.getCompletionsAtPosition();
|
||||||
expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title']);
|
expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return completions of string literals, number literals, `true`, `false`, `null` and `undefined`',
|
||||||
|
() => {
|
||||||
|
const {templateFile} = setup(`<input dir [myInput]="">`, '', DIR_WITH_UNION_TYPE_INPUT);
|
||||||
|
templateFile.moveCursorToText('dir [myInput]="¦">');
|
||||||
|
|
||||||
|
const completions = templateFile.getCompletionsAtPosition();
|
||||||
|
expectContain(completions, ts.ScriptElementKind.string, [`'foo'`, '42']);
|
||||||
|
expectContain(completions, ts.ScriptElementKind.keyword, ['null']);
|
||||||
|
expectContain(completions, ts.ScriptElementKind.variableElement, ['undefined']);
|
||||||
|
expectDoesNotContain(completions, ts.ScriptElementKind.parameterElement, ['ctx']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('in an expression scope', () => {
|
describe('in an expression scope', () => {
|
||||||
|
|
Loading…
Reference in New Issue