feat(language-service): autocompletion within expression contexts (#39727)
This commit adds support to the Language Service for autocompletion within expression contexts. Specifically, this is auto completion of property reads and method calls, both in normal and safe-navigational forms. PR Close #39727
This commit is contained in:
parent
269a775287
commit
93a83266f9
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AST, ParseError, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
import {AST, MethodCall, ParseError, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||||
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
@ -124,6 +124,15 @@ export interface TemplateTypeChecker {
|
||||||
getGlobalCompletions(context: TmplAstTemplate|null, component: ts.ClassDeclaration):
|
getGlobalCompletions(context: TmplAstTemplate|null, component: ts.ClassDeclaration):
|
||||||
GlobalCompletion|null;
|
GlobalCompletion|null;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For the given expression node, retrieve a `ShimLocation` that can be used to perform
|
||||||
|
* autocompletion at that point in the expression, if such a location exists.
|
||||||
|
*/
|
||||||
|
getExpressionCompletionLocation(
|
||||||
|
expr: PropertyRead|SafePropertyRead|MethodCall|SafeMethodCall,
|
||||||
|
component: ts.ClassDeclaration): ShimLocation|null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get basic metadata on the directives which are in scope for the given component.
|
* Get basic metadata on the directives which are in scope for the given component.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AST, ParseError, parseTemplate, TmplAstNode, TmplAstTemplate,} from '@angular/compiler';
|
import {AST, MethodCall, ParseError, parseTemplate, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
||||||
|
@ -285,6 +285,16 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
return engine.getGlobalCompletions(context);
|
return engine.getGlobalCompletions(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExpressionCompletionLocation(
|
||||||
|
ast: PropertyRead|SafePropertyRead|MethodCall|SafeMethodCall,
|
||||||
|
component: ts.ClassDeclaration): ShimLocation|null {
|
||||||
|
const engine = this.getOrCreateCompletionEngine(component);
|
||||||
|
if (engine === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return engine.getExpressionCompletionLocation(ast);
|
||||||
|
}
|
||||||
|
|
||||||
private getOrCreateCompletionEngine(component: ts.ClassDeclaration): CompletionEngine|null {
|
private getOrCreateCompletionEngine(component: ts.ClassDeclaration): CompletionEngine|null {
|
||||||
if (this.completionCache.has(component)) {
|
if (this.completionCache.has(component)) {
|
||||||
return this.completionCache.get(component)!;
|
return this.completionCache.get(component)!;
|
||||||
|
|
|
@ -7,10 +7,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TmplAstReference, TmplAstTemplate} from '@angular/compiler';
|
import {TmplAstReference, TmplAstTemplate} from '@angular/compiler';
|
||||||
|
import {MethodCall, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead} 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';
|
||||||
import {CompletionKind, GlobalCompletion, ReferenceCompletion, VariableCompletion} from '../api';
|
import {CompletionKind, GlobalCompletion, ReferenceCompletion, ShimLocation, VariableCompletion} from '../api';
|
||||||
|
|
||||||
import {ExpressionIdentifier, findFirstMatchingNode} from './comments';
|
import {ExpressionIdentifier, findFirstMatchingNode} from './comments';
|
||||||
import {TemplateData} from './context';
|
import {TemplateData} from './context';
|
||||||
|
@ -28,6 +29,9 @@ export class CompletionEngine {
|
||||||
*/
|
*/
|
||||||
private globalCompletionCache = new Map<TmplAstTemplate|null, GlobalCompletion>();
|
private globalCompletionCache = new Map<TmplAstTemplate|null, GlobalCompletion>();
|
||||||
|
|
||||||
|
private expressionCompletionCache =
|
||||||
|
new Map<PropertyRead|SafePropertyRead|MethodCall|SafeMethodCall, ShimLocation>();
|
||||||
|
|
||||||
constructor(private tcb: ts.Node, private data: TemplateData, private shimPath: AbsoluteFsPath) {}
|
constructor(private tcb: ts.Node, private data: TemplateData, private shimPath: AbsoluteFsPath) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,4 +83,52 @@ export class CompletionEngine {
|
||||||
this.globalCompletionCache.set(context, completion);
|
this.globalCompletionCache.set(context, completion);
|
||||||
return completion;
|
return completion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExpressionCompletionLocation(expr: PropertyRead|PropertyWrite|MethodCall|
|
||||||
|
SafeMethodCall): ShimLocation|null {
|
||||||
|
if (this.expressionCompletionCache.has(expr)) {
|
||||||
|
return this.expressionCompletionCache.get(expr)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Completion works inside property reads and method calls.
|
||||||
|
let tsExpr: ts.PropertyAccessExpression|null = null;
|
||||||
|
if (expr instanceof PropertyRead || expr instanceof MethodCall ||
|
||||||
|
expr instanceof PropertyWrite) {
|
||||||
|
// Non-safe navigation operations are trivial: `foo.bar` or `foo.bar()`
|
||||||
|
tsExpr = findFirstMatchingNode(this.tcb, {
|
||||||
|
filter: ts.isPropertyAccessExpression,
|
||||||
|
withSpan: expr.nameSpan,
|
||||||
|
});
|
||||||
|
} else if (expr instanceof SafePropertyRead || expr instanceof SafeMethodCall) {
|
||||||
|
// Safe navigation operations are a little more complex, and involve a ternary. Completion
|
||||||
|
// happens in the "true" case of the ternary.
|
||||||
|
const ternaryExpr = findFirstMatchingNode(this.tcb, {
|
||||||
|
filter: ts.isParenthesizedExpression,
|
||||||
|
withSpan: expr.sourceSpan,
|
||||||
|
});
|
||||||
|
if (ternaryExpr === null || !ts.isConditionalExpression(ternaryExpr.expression)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const whenTrue = ternaryExpr.expression.whenTrue;
|
||||||
|
|
||||||
|
if (expr instanceof SafePropertyRead && ts.isPropertyAccessExpression(whenTrue)) {
|
||||||
|
tsExpr = whenTrue;
|
||||||
|
} else if (
|
||||||
|
expr instanceof SafeMethodCall && ts.isCallExpression(whenTrue) &&
|
||||||
|
ts.isPropertyAccessExpression(whenTrue.expression)) {
|
||||||
|
tsExpr = whenTrue.expression;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tsExpr === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ShimLocation = {
|
||||||
|
shimPath: this.shimPath,
|
||||||
|
positionInShimFile: tsExpr.name.getEnd(),
|
||||||
|
};
|
||||||
|
this.expressionCompletionCache.set(expr, res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,16 +6,18 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AST, EmptyExpr, ImplicitReceiver, LiteralPrimitive, MethodCall, PropertyRead, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
|
import {AST, EmptyExpr, ImplicitReceiver, LiteralPrimitive, MethodCall, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
|
||||||
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
||||||
import {CompletionKind, TemplateDeclarationSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
import {CompletionKind, TemplateDeclarationSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||||
import {BoundEvent} from '@angular/compiler/src/render3/r3_ast';
|
import {BoundEvent} from '@angular/compiler/src/render3/r3_ast';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {DisplayInfoKind, getDisplayInfo, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
|
import {DisplayInfoKind, getDisplayInfo, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
|
||||||
|
import {filterAliasImports} from './utils';
|
||||||
|
|
||||||
type PropertyExpressionCompletionBuilder =
|
type PropertyExpressionCompletionBuilder =
|
||||||
CompletionBuilder<PropertyRead|MethodCall|EmptyExpr|LiteralPrimitive>;
|
CompletionBuilder<PropertyRead|PropertyWrite|MethodCall|EmptyExpr|SafePropertyRead|
|
||||||
|
SafeMethodCall>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs autocompletion operations on a given node in the template.
|
* Performs autocompletion operations on a given node in the template.
|
||||||
|
@ -84,7 +86,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
|
||||||
private isPropertyExpressionCompletion(this: CompletionBuilder<TmplAstNode|AST>):
|
private isPropertyExpressionCompletion(this: CompletionBuilder<TmplAstNode|AST>):
|
||||||
this is PropertyExpressionCompletionBuilder {
|
this is PropertyExpressionCompletionBuilder {
|
||||||
return this.node instanceof PropertyRead || this.node instanceof MethodCall ||
|
return this.node instanceof PropertyRead || this.node instanceof MethodCall ||
|
||||||
this.node instanceof EmptyExpr ||
|
this.node instanceof SafePropertyRead || this.node instanceof SafeMethodCall ||
|
||||||
|
this.node instanceof PropertyWrite || this.node instanceof EmptyExpr ||
|
||||||
isBrokenEmptyBoundEventExpression(this.node, this.nodeParent);
|
isBrokenEmptyBoundEventExpression(this.node, this.nodeParent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,9 +103,31 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
|
||||||
this.node.receiver instanceof ImplicitReceiver) {
|
this.node.receiver instanceof ImplicitReceiver) {
|
||||||
return this.getGlobalPropertyExpressionCompletion(options);
|
return this.getGlobalPropertyExpressionCompletion(options);
|
||||||
} else {
|
} else {
|
||||||
// TODO(alxhub): implement completion of non-global expressions.
|
const location = this.compiler.getTemplateTypeChecker().getExpressionCompletionLocation(
|
||||||
|
this.node, this.component);
|
||||||
|
if (location === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const tsResults = this.tsLS.getCompletionsAtPosition(
|
||||||
|
location.shimPath, location.positionInShimFile, options);
|
||||||
|
if (tsResults === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacementSpan = makeReplacementSpan(this.node);
|
||||||
|
|
||||||
|
let ngResults: ts.CompletionEntry[] = [];
|
||||||
|
for (const result of tsResults.entries) {
|
||||||
|
ngResults.push({
|
||||||
|
...result,
|
||||||
|
replacementSpan,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...tsResults,
|
||||||
|
entries: ngResults,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,15 +137,26 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
|
||||||
this: PropertyExpressionCompletionBuilder, entryName: string,
|
this: PropertyExpressionCompletionBuilder, entryName: string,
|
||||||
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 {
|
||||||
|
let details: ts.CompletionEntryDetails|undefined = undefined;
|
||||||
if (this.node instanceof EmptyExpr ||
|
if (this.node instanceof EmptyExpr ||
|
||||||
isBrokenEmptyBoundEventExpression(this.node, this.nodeParent) ||
|
isBrokenEmptyBoundEventExpression(this.node, this.nodeParent) ||
|
||||||
this.node.receiver instanceof ImplicitReceiver) {
|
this.node.receiver instanceof ImplicitReceiver) {
|
||||||
return this.getGlobalPropertyExpressionCompletionDetails(
|
details =
|
||||||
entryName, formatOptions, preferences);
|
this.getGlobalPropertyExpressionCompletionDetails(entryName, formatOptions, preferences);
|
||||||
} else {
|
} else {
|
||||||
// TODO(alxhub): implement completion of non-global expressions.
|
const location = this.compiler.getTemplateTypeChecker().getExpressionCompletionLocation(
|
||||||
|
this.node, this.component);
|
||||||
|
if (location === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
details = this.tsLS.getCompletionEntryDetails(
|
||||||
|
location.shimPath, location.positionInShimFile, entryName, formatOptions,
|
||||||
|
/* source */ undefined, preferences);
|
||||||
|
}
|
||||||
|
if (details !== undefined) {
|
||||||
|
details.displayParts = filterAliasImports(details.displayParts);
|
||||||
|
}
|
||||||
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,9 +168,14 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
|
||||||
this.node.receiver instanceof ImplicitReceiver) {
|
this.node.receiver instanceof ImplicitReceiver) {
|
||||||
return this.getGlobalPropertyExpressionCompletionSymbol(name);
|
return this.getGlobalPropertyExpressionCompletionSymbol(name);
|
||||||
} else {
|
} else {
|
||||||
// TODO(alxhub): implement completion of non-global expressions.
|
const location = this.compiler.getTemplateTypeChecker().getExpressionCompletionLocation(
|
||||||
|
this.node, this.component);
|
||||||
|
if (location === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
return this.tsLS.getCompletionEntrySymbol(
|
||||||
|
location.shimPath, location.positionInShimFile, name, /* source */ undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -154,10 +195,7 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
|
||||||
let replacementSpan: ts.TextSpan|undefined = undefined;
|
let replacementSpan: ts.TextSpan|undefined = undefined;
|
||||||
// Non-empty nodes get replaced with the completion.
|
// Non-empty nodes get replaced with the completion.
|
||||||
if (!(this.node instanceof EmptyExpr || this.node instanceof LiteralPrimitive)) {
|
if (!(this.node instanceof EmptyExpr || this.node instanceof LiteralPrimitive)) {
|
||||||
replacementSpan = {
|
replacementSpan = makeReplacementSpan(this.node);
|
||||||
start: this.node.nameSpan.start,
|
|
||||||
length: this.node.nameSpan.end - this.node.nameSpan.start,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge TS completion results with results from the template scope.
|
// Merge TS completion results with results from the template scope.
|
||||||
|
@ -285,3 +323,11 @@ function isBrokenEmptyBoundEventExpression(
|
||||||
return node instanceof LiteralPrimitive && parent !== null && parent instanceof BoundEvent &&
|
return node instanceof LiteralPrimitive && parent !== null && parent instanceof BoundEvent &&
|
||||||
node.value === 'ERROR';
|
node.value === 'ERROR';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeReplacementSpan(node: PropertyRead|PropertyWrite|MethodCall|SafePropertyRead|
|
||||||
|
SafeMethodCall): ts.TextSpan {
|
||||||
|
return {
|
||||||
|
start: node.nameSpan.start,
|
||||||
|
length: node.nameSpan.end - node.nameSpan.start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -106,6 +106,98 @@ describe('completions', () => {
|
||||||
expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title']);
|
expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('in an expression scope', () => {
|
||||||
|
it('should return completions in a property access expression', () => {
|
||||||
|
const {ngLS, fileName, cursor} =
|
||||||
|
setup(`{{name.f¦}}`, `name!: {first: string; last: string;};`);
|
||||||
|
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
|
||||||
|
expectAll(completions, {
|
||||||
|
first: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
last: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return completions in an empty property access expression', () => {
|
||||||
|
const {ngLS, fileName, cursor} =
|
||||||
|
setup(`{{name.¦}}`, `name!: {first: string; last: string;};`);
|
||||||
|
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
|
||||||
|
expectAll(completions, {
|
||||||
|
first: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
last: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return completions in a property write expression', () => {
|
||||||
|
const {ngLS, fileName, cursor} = setup(
|
||||||
|
`<button (click)="name.fi¦ = 'test"></button>`, `name!: {first: string; last: string;};`);
|
||||||
|
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
|
||||||
|
expectAll(completions, {
|
||||||
|
first: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
last: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return completions in a method call expression', () => {
|
||||||
|
const {ngLS, fileName, cursor} =
|
||||||
|
setup(`{{name.f¦()}}`, `name!: {first: string; full(): string;};`);
|
||||||
|
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
|
||||||
|
expectAll(completions, {
|
||||||
|
first: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
full: ts.ScriptElementKind.memberFunctionElement,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return completions in an empty method call expression', () => {
|
||||||
|
const {ngLS, fileName, cursor} =
|
||||||
|
setup(`{{name.¦()}}`, `name!: {first: string; full(): string;};`);
|
||||||
|
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
|
||||||
|
expectAll(completions, {
|
||||||
|
first: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
full: ts.ScriptElementKind.memberFunctionElement,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return completions in a safe property navigation context', () => {
|
||||||
|
const {ngLS, fileName, cursor} =
|
||||||
|
setup(`{{name?.f¦}}`, `name?: {first: string; last: string;};`);
|
||||||
|
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
|
||||||
|
expectAll(completions, {
|
||||||
|
first: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
last: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return completions in an empty safe property navigation context', () => {
|
||||||
|
const {ngLS, fileName, cursor} =
|
||||||
|
setup(`{{name?.¦}}`, `name?: {first: string; last: string;};`);
|
||||||
|
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
|
||||||
|
expectAll(completions, {
|
||||||
|
first: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
last: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return completions in a safe method call context', () => {
|
||||||
|
const {ngLS, fileName, cursor} =
|
||||||
|
setup(`{{name?.f¦()}}`, `name!: {first: string; full(): string;};`);
|
||||||
|
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
|
||||||
|
expectAll(completions, {
|
||||||
|
first: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
full: ts.ScriptElementKind.memberFunctionElement,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return completions in an empty safe method call context', () => {
|
||||||
|
const {ngLS, fileName, cursor} =
|
||||||
|
setup(`{{name?.¦()}}`, `name!: {first: string; full(): string;};`);
|
||||||
|
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
|
||||||
|
expectAll(completions, {
|
||||||
|
first: ts.ScriptElementKind.memberVariableElement,
|
||||||
|
full: ts.ScriptElementKind.memberFunctionElement,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function expectContain(
|
function expectContain(
|
||||||
|
@ -117,6 +209,16 @@ function expectContain(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectAll(
|
||||||
|
completions: ts.CompletionInfo|undefined,
|
||||||
|
contains: {[name: string]: ts.ScriptElementKind|DisplayInfoKind}): void {
|
||||||
|
expect(completions).toBeDefined();
|
||||||
|
for (const [name, kind] of Object.entries(contains)) {
|
||||||
|
expect(completions!.entries).toContain(jasmine.objectContaining({name, kind} as any));
|
||||||
|
}
|
||||||
|
expect(completions!.entries.length).toEqual(Object.keys(contains).length);
|
||||||
|
}
|
||||||
|
|
||||||
function toText(displayParts?: ts.SymbolDisplayPart[]): string {
|
function toText(displayParts?: ts.SymbolDisplayPart[]): string {
|
||||||
return (displayParts ?? []).map(p => p.text).join('');
|
return (displayParts ?? []).map(p => p.text).join('');
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue