2020-10-13 14:14:13 -04:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2020-11-18 20:30:52 -05:00
|
|
|
import {AST, EmptyExpr, ImplicitReceiver, LiteralPrimitive, MethodCall, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
|
2020-10-13 14:14:13 -04:00
|
|
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
2020-11-18 20:30:52 -05:00
|
|
|
import {CompletionKind, DirectiveInScope, TemplateDeclarationSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
2020-10-13 14:14:13 -04:00
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
2020-11-18 20:30:52 -05:00
|
|
|
import {DisplayInfoKind, getDirectiveDisplayInfo, getSymbolDisplayInfo, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
|
2020-11-17 17:43:40 -05:00
|
|
|
import {filterAliasImports} from './utils';
|
2020-10-13 14:14:13 -04:00
|
|
|
|
|
|
|
type PropertyExpressionCompletionBuilder =
|
2020-11-17 17:43:40 -05:00
|
|
|
CompletionBuilder<PropertyRead|PropertyWrite|MethodCall|EmptyExpr|SafePropertyRead|
|
|
|
|
SafeMethodCall>;
|
2020-10-13 14:14:13 -04:00
|
|
|
|
2020-11-18 20:30:52 -05:00
|
|
|
|
|
|
|
export enum CompletionNodeContext {
|
|
|
|
None,
|
|
|
|
ElementTag,
|
|
|
|
ElementAttributeKey,
|
|
|
|
ElementAttributeValue,
|
|
|
|
}
|
|
|
|
|
2020-10-13 14:14:13 -04:00
|
|
|
/**
|
|
|
|
* Performs autocompletion operations on a given node in the template.
|
|
|
|
*
|
|
|
|
* This class acts as a closure around all of the context required to perform the 3 autocompletion
|
|
|
|
* operations (completions, get details, and get symbol) at a specific node.
|
|
|
|
*
|
|
|
|
* The generic `N` type for the template node is narrowed internally for certain operations, as the
|
|
|
|
* compiler operations required to implement completion may be different for different node types.
|
|
|
|
*
|
|
|
|
* @param N type of the template node in question, narrowed accordingly.
|
|
|
|
*/
|
|
|
|
export class CompletionBuilder<N extends TmplAstNode|AST> {
|
|
|
|
private readonly typeChecker = this.compiler.getNextProgram().getTypeChecker();
|
|
|
|
private readonly templateTypeChecker = this.compiler.getTemplateTypeChecker();
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler,
|
|
|
|
private readonly component: ts.ClassDeclaration, private readonly node: N,
|
2020-11-18 20:30:52 -05:00
|
|
|
private readonly nodeContext: CompletionNodeContext,
|
2020-10-13 14:14:13 -04:00
|
|
|
private readonly nodeParent: TmplAstNode|AST|null,
|
2020-11-18 17:07:30 -05:00
|
|
|
private readonly template: TmplAstTemplate|null) {}
|
2020-10-13 14:14:13 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Analogue for `ts.LanguageService.getCompletionsAtPosition`.
|
|
|
|
*/
|
|
|
|
getCompletionsAtPosition(options: ts.GetCompletionsAtPositionOptions|
|
|
|
|
undefined): ts.WithMetadata<ts.CompletionInfo>|undefined {
|
|
|
|
if (this.isPropertyExpressionCompletion()) {
|
|
|
|
return this.getPropertyExpressionCompletion(options);
|
2020-11-18 20:30:52 -05:00
|
|
|
} else if (this.isElementTagCompletion()) {
|
|
|
|
return this.getElementTagCompletion();
|
2020-10-13 14:14:13 -04:00
|
|
|
} else {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Analogue for `ts.LanguageService.getCompletionEntryDetails`.
|
|
|
|
*/
|
|
|
|
getCompletionEntryDetails(
|
|
|
|
entryName: string, formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined,
|
|
|
|
preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined {
|
|
|
|
if (this.isPropertyExpressionCompletion()) {
|
|
|
|
return this.getPropertyExpressionCompletionDetails(entryName, formatOptions, preferences);
|
2020-11-18 20:30:52 -05:00
|
|
|
} else if (this.isElementTagCompletion()) {
|
|
|
|
return this.getElementTagCompletionDetails(entryName);
|
2020-10-13 14:14:13 -04:00
|
|
|
} else {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Analogue for `ts.LanguageService.getCompletionEntrySymbol`.
|
|
|
|
*/
|
|
|
|
getCompletionEntrySymbol(name: string): ts.Symbol|undefined {
|
|
|
|
if (this.isPropertyExpressionCompletion()) {
|
|
|
|
return this.getPropertyExpressionCompletionSymbol(name);
|
2020-11-18 20:30:52 -05:00
|
|
|
} else if (this.isElementTagCompletion()) {
|
|
|
|
return this.getElementTagCompletionSymbol(name);
|
2020-10-13 14:14:13 -04:00
|
|
|
} else {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine if the current node is the completion of a property expression, and narrow the type
|
|
|
|
* of `this.node` if so.
|
|
|
|
*
|
|
|
|
* This narrowing gives access to additional methods related to completion of property
|
|
|
|
* expressions.
|
|
|
|
*/
|
|
|
|
private isPropertyExpressionCompletion(this: CompletionBuilder<TmplAstNode|AST>):
|
|
|
|
this is PropertyExpressionCompletionBuilder {
|
|
|
|
return this.node instanceof PropertyRead || this.node instanceof MethodCall ||
|
2020-11-17 17:43:40 -05:00
|
|
|
this.node instanceof SafePropertyRead || this.node instanceof SafeMethodCall ||
|
|
|
|
this.node instanceof PropertyWrite || this.node instanceof EmptyExpr ||
|
2020-10-13 14:14:13 -04:00
|
|
|
isBrokenEmptyBoundEventExpression(this.node, this.nodeParent);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get completions for property expressions.
|
|
|
|
*/
|
|
|
|
private getPropertyExpressionCompletion(
|
|
|
|
this: PropertyExpressionCompletionBuilder,
|
|
|
|
options: ts.GetCompletionsAtPositionOptions|
|
|
|
|
undefined): ts.WithMetadata<ts.CompletionInfo>|undefined {
|
|
|
|
if (this.node instanceof EmptyExpr ||
|
|
|
|
isBrokenEmptyBoundEventExpression(this.node, this.nodeParent) ||
|
|
|
|
this.node.receiver instanceof ImplicitReceiver) {
|
|
|
|
return this.getGlobalPropertyExpressionCompletion(options);
|
|
|
|
} else {
|
2020-11-17 17:43:40 -05:00
|
|
|
const location = this.compiler.getTemplateTypeChecker().getExpressionCompletionLocation(
|
|
|
|
this.node, this.component);
|
|
|
|
if (location === null) {
|
|
|
|
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,
|
|
|
|
};
|
2020-10-13 14:14:13 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the details of a specific completion for a property expression.
|
|
|
|
*/
|
|
|
|
private getPropertyExpressionCompletionDetails(
|
|
|
|
this: PropertyExpressionCompletionBuilder, entryName: string,
|
|
|
|
formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined,
|
|
|
|
preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined {
|
2020-11-17 17:43:40 -05:00
|
|
|
let details: ts.CompletionEntryDetails|undefined = undefined;
|
2020-10-13 14:14:13 -04:00
|
|
|
if (this.node instanceof EmptyExpr ||
|
|
|
|
isBrokenEmptyBoundEventExpression(this.node, this.nodeParent) ||
|
|
|
|
this.node.receiver instanceof ImplicitReceiver) {
|
2020-11-17 17:43:40 -05:00
|
|
|
details =
|
|
|
|
this.getGlobalPropertyExpressionCompletionDetails(entryName, formatOptions, preferences);
|
2020-10-13 14:14:13 -04:00
|
|
|
} else {
|
2020-11-17 17:43:40 -05:00
|
|
|
const location = this.compiler.getTemplateTypeChecker().getExpressionCompletionLocation(
|
|
|
|
this.node, this.component);
|
|
|
|
if (location === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
details = this.tsLS.getCompletionEntryDetails(
|
|
|
|
location.shimPath, location.positionInShimFile, entryName, formatOptions,
|
|
|
|
/* source */ undefined, preferences);
|
|
|
|
}
|
|
|
|
if (details !== undefined) {
|
|
|
|
details.displayParts = filterAliasImports(details.displayParts);
|
2020-10-13 14:14:13 -04:00
|
|
|
}
|
2020-11-17 17:43:40 -05:00
|
|
|
return details;
|
2020-10-13 14:14:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the `ts.Symbol` for a specific completion for a property expression.
|
|
|
|
*/
|
|
|
|
private getPropertyExpressionCompletionSymbol(
|
|
|
|
this: PropertyExpressionCompletionBuilder, name: string): ts.Symbol|undefined {
|
|
|
|
if (this.node instanceof EmptyExpr || this.node instanceof LiteralPrimitive ||
|
|
|
|
this.node.receiver instanceof ImplicitReceiver) {
|
|
|
|
return this.getGlobalPropertyExpressionCompletionSymbol(name);
|
|
|
|
} else {
|
2020-11-17 17:43:40 -05:00
|
|
|
const location = this.compiler.getTemplateTypeChecker().getExpressionCompletionLocation(
|
|
|
|
this.node, this.component);
|
|
|
|
if (location === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return this.tsLS.getCompletionEntrySymbol(
|
|
|
|
location.shimPath, location.positionInShimFile, name, /* source */ undefined);
|
2020-10-13 14:14:13 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get completions for a property expression in a global context (e.g. `{{y|}}`).
|
|
|
|
*/
|
|
|
|
private getGlobalPropertyExpressionCompletion(
|
|
|
|
this: PropertyExpressionCompletionBuilder,
|
|
|
|
options: ts.GetCompletionsAtPositionOptions|
|
|
|
|
undefined): ts.WithMetadata<ts.CompletionInfo>|undefined {
|
2020-11-18 17:07:30 -05:00
|
|
|
const completions =
|
|
|
|
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
|
2020-10-13 14:14:13 -04:00
|
|
|
if (completions === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const {componentContext, templateContext} = completions;
|
|
|
|
|
|
|
|
let replacementSpan: ts.TextSpan|undefined = undefined;
|
|
|
|
// Non-empty nodes get replaced with the completion.
|
|
|
|
if (!(this.node instanceof EmptyExpr || this.node instanceof LiteralPrimitive)) {
|
2020-11-17 17:43:40 -05:00
|
|
|
replacementSpan = makeReplacementSpan(this.node);
|
2020-10-13 14:14:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Merge TS completion results with results from the template scope.
|
|
|
|
let entries: ts.CompletionEntry[] = [];
|
|
|
|
const tsLsCompletions = this.tsLS.getCompletionsAtPosition(
|
|
|
|
componentContext.shimPath, componentContext.positionInShimFile, options);
|
|
|
|
if (tsLsCompletions !== undefined) {
|
|
|
|
for (const tsCompletion of tsLsCompletions.entries) {
|
|
|
|
// Skip completions that are shadowed by a template entity definition.
|
|
|
|
if (templateContext.has(tsCompletion.name)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
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) {
|
|
|
|
entries.push({
|
|
|
|
name,
|
|
|
|
sortText: name,
|
|
|
|
replacementSpan,
|
|
|
|
kindModifiers: ts.ScriptElementKindModifier.none,
|
|
|
|
kind: unsafeCastDisplayInfoKindToScriptElementKind(
|
|
|
|
entity.kind === CompletionKind.Reference ? DisplayInfoKind.REFERENCE :
|
|
|
|
DisplayInfoKind.VARIABLE),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
entries,
|
|
|
|
// Although this completion is "global" in the sense of an Angular expression (there is no
|
|
|
|
// explicit receiver), it is not "global" in a TypeScript sense since Angular expressions have
|
|
|
|
// the component as an implicit receiver.
|
|
|
|
isGlobalCompletion: false,
|
|
|
|
isMemberCompletion: true,
|
|
|
|
isNewIdentifierLocation: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the details of a specific completion for a property expression in a global context (e.g.
|
|
|
|
* `{{y|}}`).
|
|
|
|
*/
|
|
|
|
private getGlobalPropertyExpressionCompletionDetails(
|
|
|
|
this: PropertyExpressionCompletionBuilder, entryName: string,
|
|
|
|
formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined,
|
|
|
|
preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined {
|
2020-11-18 17:07:30 -05:00
|
|
|
const completions =
|
|
|
|
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
|
2020-10-13 14:14:13 -04:00
|
|
|
if (completions === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const {componentContext, templateContext} = completions;
|
|
|
|
|
|
|
|
if (templateContext.has(entryName)) {
|
|
|
|
const entry = templateContext.get(entryName)!;
|
|
|
|
// Entries that reference a symbol in the template context refer either to local references or
|
|
|
|
// variables.
|
|
|
|
const symbol = this.templateTypeChecker.getSymbolOfNode(entry.node, this.component) as
|
|
|
|
TemplateDeclarationSymbol |
|
|
|
|
null;
|
|
|
|
if (symbol === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const {kind, displayParts, documentation} =
|
2020-11-18 20:30:52 -05:00
|
|
|
getSymbolDisplayInfo(this.tsLS, this.typeChecker, symbol);
|
2020-10-13 14:14:13 -04:00
|
|
|
return {
|
|
|
|
kind: unsafeCastDisplayInfoKindToScriptElementKind(kind),
|
|
|
|
name: entryName,
|
|
|
|
kindModifiers: ts.ScriptElementKindModifier.none,
|
|
|
|
displayParts,
|
|
|
|
documentation,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return this.tsLS.getCompletionEntryDetails(
|
|
|
|
componentContext.shimPath, componentContext.positionInShimFile, entryName, formatOptions,
|
|
|
|
/* source */ undefined, preferences);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the `ts.Symbol` of a specific completion for a property expression in a global context
|
|
|
|
* (e.g.
|
|
|
|
* `{{y|}}`).
|
|
|
|
*/
|
|
|
|
private getGlobalPropertyExpressionCompletionSymbol(
|
|
|
|
this: PropertyExpressionCompletionBuilder, entryName: string): ts.Symbol|undefined {
|
2020-11-18 17:07:30 -05:00
|
|
|
const completions =
|
|
|
|
this.templateTypeChecker.getGlobalCompletions(this.template, this.component);
|
2020-10-13 14:14:13 -04:00
|
|
|
if (completions === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const {componentContext, templateContext} = completions;
|
|
|
|
if (templateContext.has(entryName)) {
|
|
|
|
const node: TmplAstReference|TmplAstVariable = templateContext.get(entryName)!.node;
|
|
|
|
const symbol = this.templateTypeChecker.getSymbolOfNode(node, this.component) as
|
|
|
|
TemplateDeclarationSymbol |
|
|
|
|
null;
|
|
|
|
if (symbol === null || symbol.tsSymbol === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return symbol.tsSymbol;
|
|
|
|
} else {
|
|
|
|
return this.tsLS.getCompletionEntrySymbol(
|
|
|
|
componentContext.shimPath, componentContext.positionInShimFile, entryName,
|
|
|
|
/* source */ undefined);
|
|
|
|
}
|
|
|
|
}
|
2020-11-18 20:30:52 -05:00
|
|
|
|
|
|
|
private isElementTagCompletion(): this is CompletionBuilder<TmplAstElement> {
|
|
|
|
return this.node instanceof TmplAstElement &&
|
|
|
|
this.nodeContext === CompletionNodeContext.ElementTag;
|
|
|
|
}
|
|
|
|
|
|
|
|
private getElementTagCompletion(this: CompletionBuilder<TmplAstElement>):
|
|
|
|
ts.WithMetadata<ts.CompletionInfo>|undefined {
|
|
|
|
const templateTypeChecker = this.compiler.getTemplateTypeChecker();
|
|
|
|
|
|
|
|
// The replacementSpan is the tag name.
|
|
|
|
const replacementSpan: ts.TextSpan = {
|
|
|
|
start: this.node.sourceSpan.start.offset + 1, // account for leading '<'
|
|
|
|
length: this.node.name.length,
|
|
|
|
};
|
|
|
|
|
|
|
|
const entries: ts.CompletionEntry[] =
|
|
|
|
Array.from(templateTypeChecker.getPotentialElementTags(this.component))
|
|
|
|
.map(([tag, directive]) => ({
|
|
|
|
kind: tagCompletionKind(directive),
|
|
|
|
name: tag,
|
|
|
|
sortText: tag,
|
|
|
|
replacementSpan,
|
|
|
|
}));
|
|
|
|
|
|
|
|
return {
|
|
|
|
entries,
|
|
|
|
isGlobalCompletion: false,
|
|
|
|
isMemberCompletion: false,
|
|
|
|
isNewIdentifierLocation: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private getElementTagCompletionDetails(
|
|
|
|
this: CompletionBuilder<TmplAstElement>, entryName: string): ts.CompletionEntryDetails
|
|
|
|
|undefined {
|
|
|
|
const templateTypeChecker = this.compiler.getTemplateTypeChecker();
|
|
|
|
|
|
|
|
const tagMap = templateTypeChecker.getPotentialElementTags(this.component);
|
|
|
|
if (!tagMap.has(entryName)) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const directive = tagMap.get(entryName)!;
|
|
|
|
let displayParts: ts.SymbolDisplayPart[];
|
|
|
|
let documentation: ts.SymbolDisplayPart[]|undefined = undefined;
|
|
|
|
if (directive === null) {
|
|
|
|
displayParts = [];
|
|
|
|
} else {
|
|
|
|
const displayInfo = getDirectiveDisplayInfo(this.tsLS, directive);
|
|
|
|
displayParts = displayInfo.displayParts;
|
|
|
|
documentation = displayInfo.documentation;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
kind: tagCompletionKind(directive),
|
|
|
|
name: entryName,
|
|
|
|
kindModifiers: ts.ScriptElementKindModifier.none,
|
|
|
|
displayParts,
|
|
|
|
documentation,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private getElementTagCompletionSymbol(this: CompletionBuilder<TmplAstElement>, entryName: string):
|
|
|
|
ts.Symbol|undefined {
|
|
|
|
const templateTypeChecker = this.compiler.getTemplateTypeChecker();
|
|
|
|
|
|
|
|
const tagMap = templateTypeChecker.getPotentialElementTags(this.component);
|
|
|
|
if (!tagMap.has(entryName)) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const directive = tagMap.get(entryName)!;
|
|
|
|
return directive?.tsSymbol;
|
|
|
|
}
|
|
|
|
|
|
|
|
// private getElementAttributeCompletions(this: CompletionBuilder<TmplAstElement>):
|
|
|
|
// ts.WithMetadata<ts.CompletionInfo> {}
|
2020-10-13 14:14:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether the given `node` is (most likely) a synthetic node created by the template parser
|
|
|
|
* for an empty event binding `(event)=""`.
|
|
|
|
*
|
|
|
|
* When parsing such an expression, a synthetic `LiteralPrimitive` node is generated for the
|
|
|
|
* `BoundEvent`'s handler with the literal text value 'ERROR'. Detecting this case is crucial to
|
|
|
|
* supporting completions within empty event bindings.
|
|
|
|
*/
|
|
|
|
function isBrokenEmptyBoundEventExpression(
|
|
|
|
node: TmplAstNode|AST, parent: TmplAstNode|AST|null): node is LiteralPrimitive {
|
2020-11-18 20:30:52 -05:00
|
|
|
return node instanceof LiteralPrimitive && parent !== null &&
|
|
|
|
parent instanceof TmplAstBoundEvent && node.value === 'ERROR';
|
2020-10-13 14:14:13 -04:00
|
|
|
}
|
2020-11-17 17:43:40 -05:00
|
|
|
|
|
|
|
function makeReplacementSpan(node: PropertyRead|PropertyWrite|MethodCall|SafePropertyRead|
|
|
|
|
SafeMethodCall): ts.TextSpan {
|
|
|
|
return {
|
|
|
|
start: node.nameSpan.start,
|
|
|
|
length: node.nameSpan.end - node.nameSpan.start,
|
|
|
|
};
|
|
|
|
}
|
2020-11-18 20:30:52 -05:00
|
|
|
|
|
|
|
function tagCompletionKind(directive: DirectiveInScope|null): ts.ScriptElementKind {
|
|
|
|
let kind: DisplayInfoKind;
|
|
|
|
if (directive === null) {
|
|
|
|
kind = DisplayInfoKind.ELEMENT;
|
|
|
|
} else if (directive.isComponent) {
|
|
|
|
kind = DisplayInfoKind.COMPONENT;
|
|
|
|
} else {
|
|
|
|
kind = DisplayInfoKind.DIRECTIVE;
|
|
|
|
}
|
|
|
|
return unsafeCastDisplayInfoKindToScriptElementKind(kind);
|
|
|
|
}
|