refactor(compiler-cli): add a global autocompletion API (#39048)
This commit introduces a new API for the `TemplateTypeChecker` which allows for autocompletion in a global expression context (for example, in a new interpolation expression such as `{{|}}`). This API returns instances of the type `GlobalCompletion`, which can represent either a completion result from the template's component context or a declaration such as a local reference or template variable. The Language Service will use this API to implement autocompletion within templates. PR Close #39048
This commit is contained in:
parent
72755eadd2
commit
f2fca6d58e
|
@ -6,9 +6,10 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AST, ParseError, TmplAstNode,} from '@angular/compiler';
|
import {AST, ParseError, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {GlobalCompletion} from './completion';
|
||||||
import {Symbol} from './symbols';
|
import {Symbol} from './symbols';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,6 +89,18 @@ export interface TemplateTypeChecker {
|
||||||
* @see Symbol
|
* @see Symbol
|
||||||
*/
|
*/
|
||||||
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null;
|
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get "global" `Completion`s in the given context.
|
||||||
|
*
|
||||||
|
* Global completions are completions in the global context, as opposed to completions within an
|
||||||
|
* existing expression. For example, completing inside a new interpolation expression (`{{|}}`) or
|
||||||
|
* inside a new property binding `[input]="|" should retrieve global completions, which will
|
||||||
|
* include completions from the template's context component, as well as any local references or
|
||||||
|
* template variables which are in scope for that expression.
|
||||||
|
*/
|
||||||
|
getGlobalCompletions(context: TmplAstTemplate|null, component: ts.ClassDeclaration):
|
||||||
|
GlobalCompletion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {TmplAstReference, TmplAstVariable} from '@angular/compiler';
|
||||||
|
|
||||||
|
import {ShimLocation} from './symbols';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An autocompletion source of any kind.
|
||||||
|
*/
|
||||||
|
export type Completion = CompletionContextComponent|CompletionReference|CompletionVariable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An autocompletion source that drives completion in a global context.
|
||||||
|
*/
|
||||||
|
export type GlobalCompletion = CompletionContextComponent|CompletionReference|CompletionVariable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discriminant of an autocompletion source (a `Completion`).
|
||||||
|
*/
|
||||||
|
export enum CompletionKind {
|
||||||
|
ContextComponent,
|
||||||
|
Reference,
|
||||||
|
Variable,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An autocompletion source backed by a shim file position where TS APIs can be used to retrieve
|
||||||
|
* completions for the context component of a template.
|
||||||
|
*/
|
||||||
|
export interface CompletionContextComponent extends ShimLocation {
|
||||||
|
kind: CompletionKind.ContextComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An autocompletion result representing a local reference declared in the template.
|
||||||
|
*/
|
||||||
|
export interface CompletionReference {
|
||||||
|
kind: CompletionKind.Reference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `TmplAstReference` from the template which should be available as a completion.
|
||||||
|
*/
|
||||||
|
node: TmplAstReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An autocompletion result representing a variable declared in the template.
|
||||||
|
*/
|
||||||
|
export interface CompletionVariable {
|
||||||
|
kind: CompletionKind.Variable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `TmplAstVariable` from the template which should be available as a completion.
|
||||||
|
*/
|
||||||
|
node: TmplAstVariable;
|
||||||
|
}
|
|
@ -8,5 +8,6 @@
|
||||||
|
|
||||||
export * from './api';
|
export * from './api';
|
||||||
export * from './checker';
|
export * from './checker';
|
||||||
|
export * from './completion';
|
||||||
export * from './context';
|
export * from './context';
|
||||||
export * from './symbols';
|
export * from './symbols';
|
||||||
|
|
|
@ -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} from '@angular/compiler';
|
import {AST, ParseError, parseTemplate, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
||||||
|
@ -16,9 +16,10 @@ import {ReflectionHost} from '../../reflection';
|
||||||
import {ComponentScopeReader} from '../../scope';
|
import {ComponentScopeReader} from '../../scope';
|
||||||
import {isShim} from '../../shims';
|
import {isShim} from '../../shims';
|
||||||
import {getSourceFileOrNull} from '../../util/src/typescript';
|
import {getSourceFileOrNull} from '../../util/src/typescript';
|
||||||
import {OptimizeFor, ProgramTypeCheckAdapter, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
import {CompletionKind, GlobalCompletion, OptimizeFor, ProgramTypeCheckAdapter, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
||||||
import {TemplateDiagnostic} from '../diagnostics';
|
import {TemplateDiagnostic} from '../diagnostics';
|
||||||
|
|
||||||
|
import {ExpressionIdentifier, findFirstMatchingNode} from './comments';
|
||||||
import {InliningMode, ShimTypeCheckingData, TemplateData, TypeCheckContextImpl, TypeCheckingHost} from './context';
|
import {InliningMode, ShimTypeCheckingData, TemplateData, TypeCheckContextImpl, TypeCheckingHost} from './context';
|
||||||
import {findTypeCheckBlock, shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics';
|
import {findTypeCheckBlock, shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics';
|
||||||
import {TemplateSourceManager} from './source';
|
import {TemplateSourceManager} from './source';
|
||||||
|
@ -53,14 +54,15 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null {
|
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null {
|
||||||
const templateData = this.getTemplateData(component);
|
const {data} = this.getLatestComponentState(component);
|
||||||
if (templateData === null) {
|
if (data === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return templateData.template;
|
return data.template;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTemplateData(component: ts.ClassDeclaration): TemplateData|null {
|
private getLatestComponentState(component: ts.ClassDeclaration):
|
||||||
|
{data: TemplateData|null, tcb: ts.Node|null, shimPath: AbsoluteFsPath} {
|
||||||
this.ensureShimForComponent(component);
|
this.ensureShimForComponent(component);
|
||||||
|
|
||||||
const sf = component.getSourceFile();
|
const sf = component.getSourceFile();
|
||||||
|
@ -70,17 +72,34 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
const fileRecord = this.getFileData(sfPath);
|
const fileRecord = this.getFileData(sfPath);
|
||||||
|
|
||||||
if (!fileRecord.shimData.has(shimPath)) {
|
if (!fileRecord.shimData.has(shimPath)) {
|
||||||
return null;
|
return {data: null, tcb: null, shimPath};
|
||||||
}
|
}
|
||||||
|
|
||||||
const templateId = fileRecord.sourceManager.getTemplateId(component);
|
const templateId = fileRecord.sourceManager.getTemplateId(component);
|
||||||
const shimRecord = fileRecord.shimData.get(shimPath)!;
|
const shimRecord = fileRecord.shimData.get(shimPath)!;
|
||||||
|
const id = fileRecord.sourceManager.getTemplateId(component);
|
||||||
|
|
||||||
if (!shimRecord.templates.has(templateId)) {
|
const program = this.typeCheckingStrategy.getProgram();
|
||||||
return null;
|
const shimSf = getSourceFileOrNull(program, shimPath);
|
||||||
|
|
||||||
|
if (shimSf === null || !fileRecord.shimData.has(shimPath)) {
|
||||||
|
throw new Error(`Error: no shim file in program: ${shimPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return shimRecord.templates.get(templateId)!;
|
let tcb: ts.Node|null = findTypeCheckBlock(shimSf, id);
|
||||||
|
|
||||||
|
if (tcb === null) {
|
||||||
|
// Try for an inline block.
|
||||||
|
const inlineSf = getSourceFileOrError(program, sfPath);
|
||||||
|
tcb = findTypeCheckBlock(inlineSf, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: TemplateData|null = null;
|
||||||
|
if (shimRecord.templates.has(templateId)) {
|
||||||
|
data = shimRecord.templates.get(templateId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {data, tcb, shimPath};
|
||||||
}
|
}
|
||||||
|
|
||||||
overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
|
overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
|
||||||
|
@ -186,31 +205,55 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null {
|
getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null {
|
||||||
this.ensureAllShimsForOneFile(component.getSourceFile());
|
return this.getLatestComponentState(component).tcb;
|
||||||
|
|
||||||
const program = this.typeCheckingStrategy.getProgram();
|
|
||||||
const filePath = absoluteFromSourceFile(component.getSourceFile());
|
|
||||||
const shimPath = this.typeCheckingStrategy.shimPathForComponent(component);
|
|
||||||
|
|
||||||
if (!this.state.has(filePath)) {
|
|
||||||
throw new Error(`Error: no data for source file: ${filePath}`);
|
|
||||||
}
|
|
||||||
const fileRecord = this.state.get(filePath)!;
|
|
||||||
const id = fileRecord.sourceManager.getTemplateId(component);
|
|
||||||
|
|
||||||
const shimSf = getSourceFileOrNull(program, shimPath);
|
|
||||||
if (shimSf === null || !fileRecord.shimData.has(shimPath)) {
|
|
||||||
throw new Error(`Error: no shim file in program: ${shimPath}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let node: ts.Node|null = findTypeCheckBlock(shimSf, id);
|
getGlobalCompletions(context: TmplAstTemplate|null, component: ts.ClassDeclaration):
|
||||||
if (node === null) {
|
GlobalCompletion[] {
|
||||||
// Try for an inline block.
|
const {tcb, data, shimPath} = this.getLatestComponentState(component);
|
||||||
const inlineSf = getSourceFileOrError(program, filePath);
|
if (tcb === null || data === null) {
|
||||||
node = findTypeCheckBlock(inlineSf, id);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return node;
|
const {boundTarget} = data;
|
||||||
|
|
||||||
|
// Global completions are the union of two separate pieces: a `ContextComponentCompletion` which
|
||||||
|
// is created from an expression within the TCB, and a list of named entities (variables and
|
||||||
|
// references) which are visible within the given `context` template.
|
||||||
|
const completions: GlobalCompletion[] = [];
|
||||||
|
|
||||||
|
const globalRead = findFirstMatchingNode(tcb, {
|
||||||
|
filter: ts.isPropertyAccessExpression,
|
||||||
|
withExpressionIdentifier: ExpressionIdentifier.COMPONENT_COMPLETION
|
||||||
|
});
|
||||||
|
|
||||||
|
if (globalRead === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
completions.push({
|
||||||
|
kind: CompletionKind.ContextComponent,
|
||||||
|
shimPath,
|
||||||
|
positionInShimFile: globalRead.name.getStart(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add completions for each entity in the template scope. Since each entity is uniquely named,
|
||||||
|
// there is no special ordering applied here.
|
||||||
|
for (const node of boundTarget.getEntitiesInTemplateScope(context)) {
|
||||||
|
if (node instanceof TmplAstReference) {
|
||||||
|
completions.push({
|
||||||
|
kind: CompletionKind.Reference,
|
||||||
|
node: node,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
completions.push({
|
||||||
|
kind: CompletionKind.Variable,
|
||||||
|
node: node,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private maybeAdoptPriorResultsForFile(sf: ts.SourceFile): void {
|
private maybeAdoptPriorResultsForFile(sf: ts.SourceFile): void {
|
||||||
|
@ -362,17 +405,12 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null {
|
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null {
|
||||||
const tcb = this.getTypeCheckBlock(component);
|
const {tcb, data, shimPath} = this.getLatestComponentState(component);
|
||||||
if (tcb === null) {
|
if (tcb === null || data === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeChecker = this.typeCheckingStrategy.getProgram().getTypeChecker();
|
const typeChecker = this.typeCheckingStrategy.getProgram().getTypeChecker();
|
||||||
const shimPath = this.typeCheckingStrategy.shimPathForComponent(component);
|
|
||||||
const data = this.getTemplateData(component);
|
|
||||||
if (data === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SymbolBuilder(typeChecker, shimPath, tcb, data, this.componentScopeReader)
|
return new SymbolBuilder(typeChecker, shimPath, tcb, data, this.componentScopeReader)
|
||||||
.getSymbol(node);
|
.getSymbol(node);
|
||||||
|
|
|
@ -42,6 +42,7 @@ export enum CommentTriviaType {
|
||||||
/** Identifies what the TCB expression is for (for example, a directive declaration). */
|
/** Identifies what the TCB expression is for (for example, a directive declaration). */
|
||||||
export enum ExpressionIdentifier {
|
export enum ExpressionIdentifier {
|
||||||
DIRECTIVE = 'DIR',
|
DIRECTIVE = 'DIR',
|
||||||
|
COMPONENT_COMPLETION = 'COMPCOMP',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tags the node with the given expression identifier. */
|
/** Tags the node with the given expression identifier. */
|
||||||
|
@ -85,6 +86,7 @@ function makeRecursiveVisitor<T extends ts.Node>(visitor: (node: ts.Node) => T |
|
||||||
|
|
||||||
export interface FindOptions<T extends ts.Node> {
|
export interface FindOptions<T extends ts.Node> {
|
||||||
filter: (node: ts.Node) => node is T;
|
filter: (node: ts.Node) => node is T;
|
||||||
|
withExpressionIdentifier?: ExpressionIdentifier;
|
||||||
withSpan?: AbsoluteSourceSpan|ParseSourceSpan;
|
withSpan?: AbsoluteSourceSpan|ParseSourceSpan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +111,7 @@ function getSpanFromOptions(opts: FindOptions<ts.Node>) {
|
||||||
export function findFirstMatchingNode<T extends ts.Node>(tcb: ts.Node, opts: FindOptions<T>): T|
|
export function findFirstMatchingNode<T extends ts.Node>(tcb: ts.Node, opts: FindOptions<T>): T|
|
||||||
null {
|
null {
|
||||||
const withSpan = getSpanFromOptions(opts);
|
const withSpan = getSpanFromOptions(opts);
|
||||||
|
const withExpressionIdentifier = opts.withExpressionIdentifier;
|
||||||
const sf = tcb.getSourceFile();
|
const sf = tcb.getSourceFile();
|
||||||
const visitor = makeRecursiveVisitor<T>(node => {
|
const visitor = makeRecursiveVisitor<T>(node => {
|
||||||
if (!opts.filter(node)) {
|
if (!opts.filter(node)) {
|
||||||
|
@ -120,6 +123,10 @@ export function findFirstMatchingNode<T extends ts.Node>(tcb: ts.Node, opts: Fin
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (withExpressionIdentifier !== undefined &&
|
||||||
|
!hasExpressionIdentifier(sf, node, withExpressionIdentifier)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
return tcb.forEachChild(visitor) ?? null;
|
return tcb.forEachChild(visitor) ?? null;
|
||||||
|
@ -135,6 +142,7 @@ export function findFirstMatchingNode<T extends ts.Node>(tcb: ts.Node, opts: Fin
|
||||||
*/
|
*/
|
||||||
export function findAllMatchingNodes<T extends ts.Node>(tcb: ts.Node, opts: FindOptions<T>): T[] {
|
export function findAllMatchingNodes<T extends ts.Node>(tcb: ts.Node, opts: FindOptions<T>): T[] {
|
||||||
const withSpan = getSpanFromOptions(opts);
|
const withSpan = getSpanFromOptions(opts);
|
||||||
|
const withExpressionIdentifier = opts.withExpressionIdentifier;
|
||||||
const results: T[] = [];
|
const results: T[] = [];
|
||||||
const stack: ts.Node[] = [tcb];
|
const stack: ts.Node[] = [tcb];
|
||||||
const sf = tcb.getSourceFile();
|
const sf = tcb.getSourceFile();
|
||||||
|
@ -153,6 +161,10 @@ export function findAllMatchingNodes<T extends ts.Node>(tcb: ts.Node, opts: Find
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (withExpressionIdentifier !== undefined &&
|
||||||
|
!hasExpressionIdentifier(sf, node, withExpressionIdentifier)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
results.push(node);
|
results.push(node);
|
||||||
}
|
}
|
||||||
|
|
|
@ -962,6 +962,30 @@ class TcbUnclaimedOutputsOp extends TcbOp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `TcbOp` which generates a completion point for the component context.
|
||||||
|
*
|
||||||
|
* This completion point looks like `ctx. ;` in the TCB output, and does not produce diagnostics.
|
||||||
|
* TypeScript autocompletion APIs can be used at this completion point (after the '.') to produce
|
||||||
|
* autocompletion results of properties and methods from the template's component context.
|
||||||
|
*/
|
||||||
|
class TcbComponentContextCompletionOp extends TcbOp {
|
||||||
|
constructor(private scope: Scope) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly optional = false;
|
||||||
|
|
||||||
|
execute(): null {
|
||||||
|
const ctx = ts.createIdentifier('ctx');
|
||||||
|
const ctxDot = ts.createPropertyAccess(ctx, '');
|
||||||
|
markIgnoreDiagnostics(ctxDot);
|
||||||
|
addExpressionIdentifier(ctxDot, ExpressionIdentifier.COMPONENT_COMPLETION);
|
||||||
|
this.scope.addStatement(ts.createExpressionStatement(ctxDot));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value used to break a circular reference between `TcbOp`s.
|
* Value used to break a circular reference between `TcbOp`s.
|
||||||
*
|
*
|
||||||
|
@ -1089,6 +1113,11 @@ class Scope {
|
||||||
guard: ts.Expression|null): Scope {
|
guard: ts.Expression|null): Scope {
|
||||||
const scope = new Scope(tcb, parent, guard);
|
const scope = new Scope(tcb, parent, guard);
|
||||||
|
|
||||||
|
if (parent === null && tcb.env.config.enableTemplateTypeChecker) {
|
||||||
|
// Add an autocompletion point for the component context.
|
||||||
|
scope.opQueue.push(new TcbComponentContextCompletionOp(scope));
|
||||||
|
}
|
||||||
|
|
||||||
let children: TmplAstNode[];
|
let children: TmplAstNode[];
|
||||||
|
|
||||||
// If given an actual `TmplAstTemplate` instance, then process any additional information it
|
// If given an actual `TmplAstTemplate` instance, then process any additional information it
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {TmplAstTemplate} from '@angular/compiler';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
|
||||||
|
import {runInEachFileSystem} from '../../file_system/testing';
|
||||||
|
import {getTokenAtPosition} from '../../util/src/typescript';
|
||||||
|
import {CompletionKind, TypeCheckingConfig} from '../api';
|
||||||
|
|
||||||
|
import {getClass, setup as baseTestSetup, TypeCheckingTarget} from './test_utils';
|
||||||
|
|
||||||
|
runInEachFileSystem(() => {
|
||||||
|
describe('TemplateTypeChecker.getGlobalCompletions()', () => {
|
||||||
|
it('should return a completion point in the TCB for the component context', () => {
|
||||||
|
const MAIN_TS = absoluteFrom('/main.ts');
|
||||||
|
const {templateTypeChecker, programStrategy} = setup([
|
||||||
|
{
|
||||||
|
fileName: MAIN_TS,
|
||||||
|
templates: {'SomeCmp': `No special template needed`},
|
||||||
|
source: `
|
||||||
|
export class SomeCmp {}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(programStrategy.getProgram(), MAIN_TS);
|
||||||
|
const SomeCmp = getClass(sf, 'SomeCmp');
|
||||||
|
|
||||||
|
const [global, ...rest] =
|
||||||
|
templateTypeChecker.getGlobalCompletions(/* root template */ null, SomeCmp);
|
||||||
|
expect(rest.length).toBe(0);
|
||||||
|
if (global.kind !== CompletionKind.ContextComponent) {
|
||||||
|
return fail(`Expected a ContextComponent completion`);
|
||||||
|
}
|
||||||
|
const tcbSf =
|
||||||
|
getSourceFileOrError(programStrategy.getProgram(), absoluteFrom(global.shimPath));
|
||||||
|
const node = getTokenAtPosition(tcbSf, global.positionInShimFile).parent;
|
||||||
|
if (!ts.isExpressionStatement(node)) {
|
||||||
|
return fail(`Expected a ts.ExpressionStatement`);
|
||||||
|
}
|
||||||
|
expect(node.expression.getText()).toEqual('ctx.');
|
||||||
|
// The position should be between the '.' and a following space.
|
||||||
|
expect(tcbSf.text.substr(global.positionInShimFile - 1, 2)).toEqual('. ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return additional completions for references and variables when available', () => {
|
||||||
|
const MAIN_TS = absoluteFrom('/main.ts');
|
||||||
|
const {templateTypeChecker, programStrategy} = setup([
|
||||||
|
{
|
||||||
|
fileName: MAIN_TS,
|
||||||
|
templates: {
|
||||||
|
'SomeCmp': `
|
||||||
|
<div *ngFor="let user of users">
|
||||||
|
<div #innerRef></div>
|
||||||
|
<div *ngIf="user">
|
||||||
|
<div #notInScope></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div #topLevelRef></div>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
source: `
|
||||||
|
export class SomeCmp {
|
||||||
|
users: string[];
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(programStrategy.getProgram(), MAIN_TS);
|
||||||
|
const SomeCmp = getClass(sf, 'SomeCmp');
|
||||||
|
|
||||||
|
const tmpl = templateTypeChecker.getTemplate(SomeCmp)!;
|
||||||
|
const ngForTemplate = tmpl[0] as TmplAstTemplate;
|
||||||
|
|
||||||
|
const [contextCmp, ...rest] =
|
||||||
|
templateTypeChecker.getGlobalCompletions(ngForTemplate, SomeCmp);
|
||||||
|
if (contextCmp.kind !== CompletionKind.ContextComponent) {
|
||||||
|
return fail(`Expected first completion to be a ContextComponent`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const completionKeys: string[] = [];
|
||||||
|
for (const completion of rest) {
|
||||||
|
if (completion.kind !== CompletionKind.Reference &&
|
||||||
|
completion.kind !== CompletionKind.Variable) {
|
||||||
|
return fail(`Unexpected CompletionKind, expected a Reference or Variable`);
|
||||||
|
}
|
||||||
|
completionKeys.push(completion.node.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(new Set(completionKeys)).toEqual(new Set(['innerRef', 'user', 'topLevelRef']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support shadowing between outer and inner templates ', () => {
|
||||||
|
const MAIN_TS = absoluteFrom('/main.ts');
|
||||||
|
const {templateTypeChecker, programStrategy} = setup([
|
||||||
|
{
|
||||||
|
fileName: MAIN_TS,
|
||||||
|
templates: {
|
||||||
|
'SomeCmp': `
|
||||||
|
<div *ngFor="let user of users">
|
||||||
|
Within this template, 'user' should be a variable, not a reference.
|
||||||
|
</div>
|
||||||
|
<div #user>Out here, 'user' is the reference.</div>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
source: `
|
||||||
|
export class SomeCmp {
|
||||||
|
users: string[];
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const sf = getSourceFileOrError(programStrategy.getProgram(), MAIN_TS);
|
||||||
|
const SomeCmp = getClass(sf, 'SomeCmp');
|
||||||
|
|
||||||
|
const tmpl = templateTypeChecker.getTemplate(SomeCmp)!;
|
||||||
|
const ngForTemplate = tmpl[0] as TmplAstTemplate;
|
||||||
|
|
||||||
|
const [_a, userAtTopLevel] =
|
||||||
|
templateTypeChecker.getGlobalCompletions(/* root template */ null, SomeCmp);
|
||||||
|
const [_b, userInNgFor] = templateTypeChecker.getGlobalCompletions(ngForTemplate, SomeCmp);
|
||||||
|
|
||||||
|
expect(userAtTopLevel.kind).toBe(CompletionKind.Reference);
|
||||||
|
expect(userInNgFor.kind).toBe(CompletionKind.Variable);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setup(targets: TypeCheckingTarget[], config?: Partial<TypeCheckingConfig>) {
|
||||||
|
return baseTestSetup(
|
||||||
|
targets, {inlining: false, config: {...config, enableTemplateTypeChecker: true}});
|
||||||
|
}
|
Loading…
Reference in New Issue