diff --git a/packages/language-service/ivy/attribute_completions.ts b/packages/language-service/ivy/attribute_completions.ts
index a6f469b55ac..631d6854eaf 100644
--- a/packages/language-service/ivy/attribute_completions.ts
+++ b/packages/language-service/ivy/attribute_completions.ts
@@ -370,6 +370,10 @@ export function buildAttributeCompletionTable(
return table;
}
+function buildSnippet(insertSnippet: true|undefined, text: string): string|undefined {
+ return insertSnippet ? `${text}="$1"` : undefined;
+}
+
/**
* Given an `AttributeCompletion`, add any available completions to a `ts.CompletionEntry` array of
* results.
@@ -381,10 +385,15 @@ export function buildAttributeCompletionTable(
* is generated. Note that this completion does not have the `[]` property binding sugar as its
* implicitly present in a property binding context (we're already completing within an `[attr|]`
* expression).
+ *
+ * If the `insertSnippet` is `true`, the completion entries should includes the property or event
+ * binding sugar in some case. For Example `
`, the `replacementSpan` is `(my)`, and the
+ * `insertText` is `(myOutput)="$0"`.
*/
export function addAttributeCompletionEntries(
entries: ts.CompletionEntry[], completion: AttributeCompletion, isAttributeContext: boolean,
- isElementContext: boolean, replacementSpan: ts.TextSpan|undefined): void {
+ isElementContext: boolean, replacementSpan: ts.TextSpan|undefined,
+ insertSnippet: true|undefined): void {
switch (completion.kind) {
case AttributeCompletionKind.DirectiveAttribute: {
entries.push({
@@ -403,17 +412,21 @@ export function addAttributeCompletionEntries(
entries.push({
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.DIRECTIVE),
name: prefix + completion.attribute,
+ insertText: buildSnippet(insertSnippet, prefix + completion.attribute),
+ isSnippet: insertSnippet,
sortText: prefix + completion.attribute,
replacementSpan,
});
break;
}
case AttributeCompletionKind.DirectiveInput: {
- if (isAttributeContext) {
+ if (isAttributeContext || insertSnippet) {
// Offer a completion of a property binding.
entries.push({
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PROPERTY),
name: `[${completion.propertyName}]`,
+ insertText: buildSnippet(insertSnippet, `[${completion.propertyName}]`),
+ isSnippet: insertSnippet,
sortText: completion.propertyName,
replacementSpan,
});
@@ -422,6 +435,8 @@ export function addAttributeCompletionEntries(
entries.push({
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PROPERTY),
name: `[(${completion.propertyName})]`,
+ insertText: buildSnippet(insertSnippet, `[(${completion.propertyName})]`),
+ isSnippet: insertSnippet,
// This completion should sort after the property binding.
sortText: completion.propertyName + '_1',
replacementSpan,
@@ -431,6 +446,8 @@ export function addAttributeCompletionEntries(
entries.push({
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.ATTRIBUTE),
name: completion.propertyName,
+ insertText: buildSnippet(insertSnippet, completion.propertyName),
+ isSnippet: insertSnippet,
// This completion should sort after both property binding options (one-way and two-way).
sortText: completion.propertyName + '_2',
replacementSpan,
@@ -439,6 +456,8 @@ export function addAttributeCompletionEntries(
entries.push({
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PROPERTY),
name: completion.propertyName,
+ insertText: buildSnippet(insertSnippet, completion.propertyName),
+ isSnippet: insertSnippet,
sortText: completion.propertyName,
replacementSpan,
});
@@ -446,10 +465,12 @@ export function addAttributeCompletionEntries(
break;
}
case AttributeCompletionKind.DirectiveOutput: {
- if (isAttributeContext) {
+ if (isAttributeContext || insertSnippet) {
entries.push({
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT),
name: `(${completion.eventName})`,
+ insertText: buildSnippet(insertSnippet, `(${completion.eventName})`),
+ isSnippet: insertSnippet,
sortText: completion.eventName,
replacementSpan,
});
@@ -457,6 +478,8 @@ export function addAttributeCompletionEntries(
entries.push({
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT),
name: completion.eventName,
+ insertText: buildSnippet(insertSnippet, completion.eventName),
+ isSnippet: insertSnippet,
sortText: completion.eventName,
replacementSpan,
});
@@ -464,11 +487,13 @@ export function addAttributeCompletionEntries(
break;
}
case AttributeCompletionKind.DomAttribute: {
- if (isAttributeContext && completion.isAlsoProperty) {
+ if ((isAttributeContext || insertSnippet) && completion.isAlsoProperty) {
// Offer a completion of a property binding to the DOM property.
entries.push({
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PROPERTY),
name: `[${completion.attribute}]`,
+ insertText: buildSnippet(insertSnippet, `[${completion.attribute}]`),
+ isSnippet: insertSnippet,
// In the case of DOM attributes, the property binding should sort after the attribute
// binding.
sortText: completion.attribute + '_1',
@@ -482,6 +507,8 @@ export function addAttributeCompletionEntries(
entries.push({
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PROPERTY),
name: completion.property,
+ insertText: buildSnippet(insertSnippet, completion.property),
+ isSnippet: insertSnippet,
sortText: completion.property,
replacementSpan,
});
@@ -492,6 +519,8 @@ export function addAttributeCompletionEntries(
entries.push({
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT),
name: `(${completion.eventName})`,
+ insertText: buildSnippet(insertSnippet, `(${completion.eventName})`),
+ isSnippet: insertSnippet,
sortText: completion.eventName,
replacementSpan,
});
diff --git a/packages/language-service/ivy/completions.ts b/packages/language-service/ivy/completions.ts
index 333fcb07205..6bf6cee9bfd 100644
--- a/packages/language-service/ivy/completions.ts
+++ b/packages/language-service/ivy/completions.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {AST, BindingPipe, Call, EmptyExpr, ImplicitReceiver, LiteralPrimitive, ParseSourceSpan, PropertyRead, PropertyWrite, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstText, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
+import {AST, ASTWithSource, BindingPipe, Call, EmptyExpr, ImplicitReceiver, LiteralPrimitive, ParseSourceSpan, PropertyRead, PropertyWrite, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstText, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {CompletionKind, DirectiveInScope, SymbolKind, TemplateDeclarationSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import {BoundEvent, TextAttribute} from '@angular/compiler/src/render3/r3_ast';
@@ -15,7 +15,7 @@ import ts from 'typescript';
import {addAttributeCompletionEntries, AttributeCompletionKind, buildAttributeCompletionTable, getAttributeCompletionSymbol} from './attribute_completions';
import {DisplayInfo, DisplayInfoKind, getDirectiveDisplayInfo, getSymbolDisplayInfo, getTsSymbolDisplayInfo, unsafeCastDisplayInfoKindToScriptElementKind} from './display_parts';
import {TargetContext, TargetNodeKind, TemplateTarget} from './template_target';
-import {filterAliasImports} from './utils';
+import {filterAliasImports, isBoundEventWithSyntheticHandler} from './utils';
type PropertyExpressionCompletionBuilder =
CompletionBuilder;
@@ -70,7 +70,7 @@ export class CompletionBuilder {
} else if (this.isElementTagCompletion()) {
return this.getElementTagCompletion();
} else if (this.isElementAttributeCompletion()) {
- return this.getElementAttributeCompletions();
+ return this.getElementAttributeCompletions(options);
} else if (this.isPipeCompletion()) {
return this.getPipeCompletions();
} else if (this.isLiteralCompletion()) {
@@ -538,8 +538,10 @@ export class CompletionBuilder {
this.node instanceof TmplAstTextAttribute || this.node instanceof TmplAstBoundEvent);
}
- private getElementAttributeCompletions(this: ElementAttributeCompletionBuilder):
- ts.WithMetadata|undefined {
+ private getElementAttributeCompletions(
+ this: ElementAttributeCompletionBuilder,
+ options: ts.GetCompletionsAtPositionOptions|
+ undefined): ts.WithMetadata|undefined {
let element: TmplAstElement|TmplAstTemplate;
if (this.node instanceof TmplAstElement) {
element = this.node;
@@ -558,6 +560,45 @@ export class CompletionBuilder {
replacementSpan = makeReplacementSpanFromParseSourceSpan(this.node.keySpan);
}
+ let insertSnippet: true|undefined;
+ if (options?.includeCompletionsWithSnippetText && options.includeCompletionsWithInsertText) {
+ if (this.node instanceof TmplAstBoundEvent && isBoundEventWithSyntheticHandler(this.node)) {
+ replacementSpan = makeReplacementSpanFromParseSourceSpan(this.node.sourceSpan);
+ insertSnippet = true;
+ }
+
+ const isBoundAttributeValueEmpty = this.node instanceof TmplAstBoundAttribute &&
+ (this.node.valueSpan === undefined ||
+ (this.node.value instanceof ASTWithSource && this.node.value.ast instanceof EmptyExpr));
+ if (isBoundAttributeValueEmpty) {
+ replacementSpan = makeReplacementSpanFromParseSourceSpan(this.node.sourceSpan);
+ insertSnippet = true;
+ }
+
+ if (this.node instanceof TmplAstTextAttribute && this.node.keySpan !== undefined) {
+ // The `sourceSpan` only includes `ngFor` and the `valueSpan` is always empty even if there
+ // is something there because we split this up into the desugared AST, `ngFor ngForOf=""`.
+ const nodeStart = this.node.keySpan.start.getContext(1, 1);
+ if (nodeStart?.before[0] === '*') {
+ const nodeEnd = this.node.keySpan.end.getContext(1, 1);
+ if (nodeEnd?.after[0] !== '=') {
+ // *ngFor -> *ngFor="¦"
+ insertSnippet = true;
+ }
+ } else {
+ if (this.node.value === '') {
+ replacementSpan = makeReplacementSpanFromParseSourceSpan(this.node.sourceSpan);
+ insertSnippet = true;
+ }
+ }
+ }
+
+ if (this.node instanceof TmplAstElement) {
+ //
+ insertSnippet = true;
+ }
+ }
+
const attrTable = buildAttributeCompletionTable(
this.component, element, this.compiler.getTemplateTypeChecker());
@@ -607,8 +648,10 @@ export class CompletionBuilder {
// Is the completion for an element (not an )?
const isElementContext =
this.node instanceof TmplAstElement || this.nodeParent instanceof TmplAstElement;
+
addAttributeCompletionEntries(
- entries, completion, isAttributeContext, isElementContext, replacementSpan);
+ entries, completion, isAttributeContext, isElementContext, replacementSpan,
+ insertSnippet);
}
return {
diff --git a/packages/language-service/ivy/template_target.ts b/packages/language-service/ivy/template_target.ts
index 12847cc1d2f..d5e3f556417 100644
--- a/packages/language-service/ivy/template_target.ts
+++ b/packages/language-service/ivy/template_target.ts
@@ -10,7 +10,7 @@ import {ParseSpan, TmplAstBoundEvent} from '@angular/compiler';
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
-import {isTemplateNodeWithKeyAndValue, isWithin, isWithinKeyValue} from './utils';
+import {isBoundEventWithSyntheticHandler, isTemplateNodeWithKeyAndValue, isWithin, isWithinKeyValue} from './utils';
/**
* Contextual information for a target position within the template.
@@ -375,22 +375,10 @@ class TemplateTargetVisitor implements t.Visitor {
}
visitBoundEvent(event: t.BoundEvent) {
- // An event binding with no value (e.g. `(event|)`) parses to a `BoundEvent` with a
- // `LiteralPrimitive` handler with value `'ERROR'`, as opposed to a property binding with no
- // value which has an `EmptyExpr` as its value. This is a synthetic node created by the binding
- // parser, and is not suitable to use for Language Service analysis. Skip it.
- //
- // TODO(alxhub): modify the parser to generate an `EmptyExpr` instead.
- let handler: e.AST = event.handler;
- if (handler instanceof e.ASTWithSource) {
- handler = handler.ast;
+ if (!isBoundEventWithSyntheticHandler(event)) {
+ const visitor = new ExpressionVisitor(this.position);
+ visitor.visit(event.handler, this.path);
}
- if (handler instanceof e.LiteralPrimitive && handler.value === 'ERROR') {
- return;
- }
-
- const visitor = new ExpressionVisitor(this.position);
- visitor.visit(event.handler, this.path);
}
visitText(text: t.Text) {
diff --git a/packages/language-service/ivy/test/completions_spec.ts b/packages/language-service/ivy/test/completions_spec.ts
index 41870024bba..cf5128ad27c 100644
--- a/packages/language-service/ivy/test/completions_spec.ts
+++ b/packages/language-service/ivy/test/completions_spec.ts
@@ -48,6 +48,18 @@ const DIR_WITH_OUTPUT = {
`
};
+const CUSTOM_BUTTON = {
+ 'Button': `
+ @Directive({
+ selector: 'button[mat-button]',
+ inputs: ['color']
+ })
+ export class Button {
+ color!: any;
+ }
+ `
+};
+
const DIR_WITH_TWO_WAY_BINDING = {
'Dir': `
@Directive({
@@ -875,6 +887,219 @@ describe('completions', () => {
expectReplacementText(completions, templateFile.contents, 'title');
});
});
+
+ describe('insert snippet text', () => {
+ it('should be able to complete for an empty attribute', () => {
+ const {templateFile} = setup(``, '', DIR_WITH_OUTPUT);
+ templateFile.moveCursorToText('¦>');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT),
+ ['(myOutput)="$1"']);
+ });
+
+ it('should be able to complete for a partial attribute', () => {
+ const {templateFile} = setup(``, '', DIR_WITH_OUTPUT);
+ templateFile.moveCursorToText('¦>');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT),
+ ['(myOutput)="$1"']);
+ expectReplacementText(completions, templateFile.contents, 'my');
+ });
+
+ it('should be able to complete for the event binding with the value is empty', () => {
+ const {templateFile} = setup(``, '', DIR_WITH_OUTPUT);
+ templateFile.moveCursorToText('(¦)="">');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT),
+ ['(myOutput)="$1"']);
+ expectReplacementText(completions, templateFile.contents, '()=""');
+ });
+
+ it('should be able to complete for the event binding', () => {
+ const {templateFile} = setup(``, '', DIR_WITH_OUTPUT);
+ templateFile.moveCursorToText('(¦)>');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT),
+ ['(myOutput)="$1"']);
+ expectReplacementText(completions, templateFile.contents, '()');
+ });
+
+ it('should be able to complete for the dom event binding', () => {
+ const {templateFile} = setup(``, '', DIR_WITH_OUTPUT);
+ templateFile.moveCursorToText('(cli¦)>');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT),
+ ['(click)="$1"']);
+ expectReplacementText(completions, templateFile.contents, '(cli)');
+ });
+
+ it('should be able to complete for the property binding with the value is empty', () => {
+ const {templateFile} = setup(``, '', DIR_WITH_SELECTED_INPUT);
+ templateFile.moveCursorToText('[my¦]=""');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PROPERTY),
+ ['[myInput]="$1"']);
+ expectReplacementText(completions, templateFile.contents, '[my]=""');
+ });
+
+ it('should be able to complete for the property binding', () => {
+ const {templateFile} = setup(``, '', DIR_WITH_SELECTED_INPUT);
+ templateFile.moveCursorToText('[my¦]');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PROPERTY),
+ ['[myInput]="$1"']);
+ expectReplacementText(completions, templateFile.contents, '[my]');
+ });
+
+ it('should be able to complete for the dom property binding', () => {
+ const {templateFile} = setup(``, '', DIR_WITH_SELECTED_INPUT);
+ templateFile.moveCursorToText('[val¦]');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PROPERTY),
+ ['[value]="$1"']);
+ expectReplacementText(completions, templateFile.contents, '[val]');
+ });
+
+ it('should be able to complete for the two way binding with the value is empty', () => {
+ const {templateFile} = setup(``, ``, DIR_WITH_TWO_WAY_BINDING);
+ templateFile.moveCursorToText('[(mod¦)]=""');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PROPERTY),
+ ['[(model)]="$1"']);
+ expectReplacementText(completions, templateFile.contents, '[(mod)]=""');
+ });
+
+ it('should be able to complete for the two way binding via', () => {
+ const {templateFile} = setup(``, ``, DIR_WITH_TWO_WAY_BINDING);
+ templateFile.moveCursorToText('[(mod¦)]');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PROPERTY),
+ ['[(model)]="$1"']);
+ expectReplacementText(completions, templateFile.contents, '[(mod)]');
+ });
+
+ it('should be able to complete for the structural directive with the value is empty', () => {
+ const {templateFile} = setup(``, '', NG_FOR_DIR);
+ templateFile.moveCursorToText('ngFor¦="">');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ // Now here is using the `=` to check the value of the structural directive. If the
+ // sourceSpan/valueSpan made more sense, it should behave like this `ngFor¦="" -> ngFor="¦"`,
+ // and enable comments below.
+ //
+ // expectContainInsertTextWithSnippet(
+ // completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.DIRECTIVE),
+ // ['ngFor="$1"']);
+ // expectReplacementText(completions, templateFile.contents, 'ngFor=""');
+ expectContain(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.DIRECTIVE),
+ ['ngFor']);
+ expect(completions?.entries[0]).toBeDefined();
+ expect(completions?.entries[0].isSnippet).toBeUndefined();
+ expectReplacementText(completions, templateFile.contents, 'ngFor');
+ });
+
+ it('should be able to complete for the structural directive', () => {
+ const {templateFile} = setup(``, '', NG_FOR_DIR);
+ templateFile.moveCursorToText('ngFor¦>');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.DIRECTIVE),
+ ['ngFor="$1"']);
+ expectReplacementText(completions, templateFile.contents, 'ngFor');
+ });
+
+ it('should not be included in the completion for an attribute with a value', () => {
+ const {templateFile} = setup(``, '', DIR_WITH_SELECTED_INPUT);
+ templateFile.moveCursorToText('myInput¦="1">');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContain(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.ATTRIBUTE),
+ ['myInput']);
+ expectDoesNotContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.ATTRIBUTE),
+ ['myInput="$1"']);
+ expectReplacementText(completions, templateFile.contents, 'myInput');
+ });
+
+ it('should not be included in the completion for a directive attribute without input', () => {
+ const {templateFile} = setup(``, '', CUSTOM_BUTTON);
+ templateFile.moveCursorToText('mat-¦>');
+
+ const completions = templateFile.getCompletionsAtPosition({
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ });
+ expectContain(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.DIRECTIVE),
+ ['mat-button']);
+ expectDoesNotContainInsertTextWithSnippet(
+ completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.DIRECTIVE),
+ ['mat-button="$1"']);
+ expectReplacementText(completions, templateFile.contents, 'mat-');
+ });
+ });
});
function expectContainInsertText(
@@ -886,6 +1111,26 @@ function expectContainInsertText(
}
}
+function expectContainInsertTextWithSnippet(
+ completions: ts.CompletionInfo|undefined, kind: ts.ScriptElementKind|DisplayInfoKind,
+ insertTexts: string[]) {
+ expect(completions).toBeDefined();
+ for (const insertText of insertTexts) {
+ expect(completions!.entries)
+ .toContain(jasmine.objectContaining({insertText, kind, isSnippet: true} as any));
+ }
+}
+
+function expectDoesNotContainInsertTextWithSnippet(
+ completions: ts.CompletionInfo|undefined, kind: ts.ScriptElementKind|DisplayInfoKind,
+ insertTexts: string[]) {
+ expect(completions).toBeDefined();
+ for (const insertText of insertTexts) {
+ expect(completions!.entries)
+ .not.toContain(jasmine.objectContaining({insertText, kind, isSnippet: true} as any));
+ }
+}
+
function expectContain(
completions: ts.CompletionInfo|undefined, kind: ts.ScriptElementKind|DisplayInfoKind,
names: string[]) {
diff --git a/packages/language-service/ivy/utils.ts b/packages/language-service/ivy/utils.ts
index 3e44d1ae92c..2da2af621c5 100644
--- a/packages/language-service/ivy/utils.ts
+++ b/packages/language-service/ivy/utils.ts
@@ -374,3 +374,20 @@ export function getTemplateLocationFromShimLocation(
}
return {templateUrl, span};
}
+
+export function isBoundEventWithSyntheticHandler(event: t.BoundEvent): boolean {
+ // An event binding with no value (e.g. `(event|)`) parses to a `BoundEvent` with a
+ // `LiteralPrimitive` handler with value `'ERROR'`, as opposed to a property binding with no
+ // value which has an `EmptyExpr` as its value. This is a synthetic node created by the binding
+ // parser, and is not suitable to use for Language Service analysis. Skip it.
+ //
+ // TODO(alxhub): modify the parser to generate an `EmptyExpr` instead.
+ let handler: e.AST = event.handler;
+ if (handler instanceof e.ASTWithSource) {
+ handler = handler.ast;
+ }
+ if (handler instanceof e.LiteralPrimitive && handler.value === 'ERROR') {
+ return true;
+ }
+ return false;
+}