From 7d74853a1d4f4a46057ae3b55e9d5a3fdbe44cc6 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Thu, 17 Dec 2020 14:44:27 -0800 Subject: [PATCH] fix(language-service): Support completions of two-way bindings (#40185) This commit adds special handling to the completion builder by detecting a two way binding context and ensuring that we filter out any `Input`s that do not support two way binding. PR Close #40185 --- packages/language-service/ivy/completions.ts | 8 +++- .../language-service/ivy/language_service.ts | 4 ++ .../ivy/test/completions_spec.ts | 37 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/language-service/ivy/completions.ts b/packages/language-service/ivy/completions.ts index 88b29999ed..0dace86de3 100644 --- a/packages/language-service/ivy/completions.ts +++ b/packages/language-service/ivy/completions.ts @@ -31,6 +31,7 @@ export enum CompletionNodeContext { ElementAttributeKey, ElementAttributeValue, EventValue, + TwoWayBinding, } /** @@ -415,7 +416,8 @@ export class CompletionBuilder { } private isElementAttributeCompletion(): this is ElementAttributeCompletionBuilder { - return this.nodeContext === CompletionNodeContext.ElementAttributeKey && + return (this.nodeContext === CompletionNodeContext.ElementAttributeKey || + this.nodeContext === CompletionNodeContext.TwoWayBinding) && (this.node instanceof TmplAstElement || this.node instanceof TmplAstBoundAttribute || this.node instanceof TmplAstTextAttribute || this.node instanceof TmplAstBoundEvent); } @@ -460,6 +462,10 @@ export class CompletionBuilder { if (this.node instanceof TmplAstBoundEvent) { continue; } + if (!completion.twoWayBindingSupported && + this.nodeContext === CompletionNodeContext.TwoWayBinding) { + continue; + } break; case AttributeCompletionKind.DirectiveOutput: if (this.node instanceof TmplAstBoundAttribute) { diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts index 3771192c4f..a735833a34 100644 --- a/packages/language-service/ivy/language_service.ts +++ b/packages/language-service/ivy/language_service.ts @@ -133,6 +133,8 @@ export class LanguageService { return null; } + // For two-way bindings, we actually only need to be concerned with the bound attribute because + // the bindings in the template are written with the attribute name, not the event name. const node = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ? positionDetails.context.nodes[0] : positionDetails.context.node; @@ -272,6 +274,8 @@ function nodeContextFromTarget(target: TargetContext): CompletionNodeContext { case TargetNodeKind.ElementInBodyContext: // Completions in element bodies are for new attributes. return CompletionNodeContext.ElementAttributeKey; + case TargetNodeKind.TwoWayBindingContext: + return CompletionNodeContext.TwoWayBinding; case TargetNodeKind.AttributeInKeyContext: return CompletionNodeContext.ElementAttributeKey; case TargetNodeKind.AttributeInValueContext: diff --git a/packages/language-service/ivy/test/completions_spec.ts b/packages/language-service/ivy/test/completions_spec.ts index 32a09e9b19..3ab849be76 100644 --- a/packages/language-service/ivy/test/completions_spec.ts +++ b/packages/language-service/ivy/test/completions_spec.ts @@ -39,6 +39,22 @@ const DIR_WITH_OUTPUT = { ` }; +const DIR_WITH_TWO_WAY_BINDING = { + 'Dir': ` + @Directive({ + selector: '[dir]', + inputs: ['model', 'otherInput'], + outputs: ['modelChange', 'otherOutput'], + }) + export class Dir { + model!: any; + modelChange!: any; + otherInput!: any; + otherOutput!: any; + } + ` +}; + const NG_FOR_DIR = { 'NgFor': ` @Directive({ @@ -519,6 +535,27 @@ describe('completions', () => { ['myOutput']); expectReplacementText(completions, text, 'my'); }); + + it('should return completions inside an LHS of a partially complete two-way binding', () => { + const {ngLS, fileName, cursor, text} = + setup(`

`, ``, DIR_WITH_TWO_WAY_BINDING); + const completions = + ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined); + expectReplacementText(completions, text, 'mod'); + + expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['model']); + + // The completions should not include the events (because the 'Change' suffix is not used in + // the two way binding) or inputs that do not have a corresponding name+'Change' output. + expectDoesNotContain( + completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT), + ['modelChange']); + expectDoesNotContain( + completions, ts.ScriptElementKind.memberVariableElement, ['otherInput']); + expectDoesNotContain( + completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT), + ['otherOutput']); + }); }); });