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
This commit is contained in:
Andrew Scott 2020-12-17 14:44:27 -08:00 committed by atscott
parent ebb7ac5979
commit 7d74853a1d
3 changed files with 48 additions and 1 deletions

View File

@ -31,6 +31,7 @@ export enum CompletionNodeContext {
ElementAttributeKey, ElementAttributeKey,
ElementAttributeValue, ElementAttributeValue,
EventValue, EventValue,
TwoWayBinding,
} }
/** /**
@ -415,7 +416,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
} }
private isElementAttributeCompletion(): this is ElementAttributeCompletionBuilder { 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 TmplAstElement || this.node instanceof TmplAstBoundAttribute ||
this.node instanceof TmplAstTextAttribute || this.node instanceof TmplAstBoundEvent); this.node instanceof TmplAstTextAttribute || this.node instanceof TmplAstBoundEvent);
} }
@ -460,6 +462,10 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
if (this.node instanceof TmplAstBoundEvent) { if (this.node instanceof TmplAstBoundEvent) {
continue; continue;
} }
if (!completion.twoWayBindingSupported &&
this.nodeContext === CompletionNodeContext.TwoWayBinding) {
continue;
}
break; break;
case AttributeCompletionKind.DirectiveOutput: case AttributeCompletionKind.DirectiveOutput:
if (this.node instanceof TmplAstBoundAttribute) { if (this.node instanceof TmplAstBoundAttribute) {

View File

@ -133,6 +133,8 @@ export class LanguageService {
return null; 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 ? const node = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
positionDetails.context.nodes[0] : positionDetails.context.nodes[0] :
positionDetails.context.node; positionDetails.context.node;
@ -272,6 +274,8 @@ function nodeContextFromTarget(target: TargetContext): CompletionNodeContext {
case TargetNodeKind.ElementInBodyContext: case TargetNodeKind.ElementInBodyContext:
// Completions in element bodies are for new attributes. // Completions in element bodies are for new attributes.
return CompletionNodeContext.ElementAttributeKey; return CompletionNodeContext.ElementAttributeKey;
case TargetNodeKind.TwoWayBindingContext:
return CompletionNodeContext.TwoWayBinding;
case TargetNodeKind.AttributeInKeyContext: case TargetNodeKind.AttributeInKeyContext:
return CompletionNodeContext.ElementAttributeKey; return CompletionNodeContext.ElementAttributeKey;
case TargetNodeKind.AttributeInValueContext: case TargetNodeKind.AttributeInValueContext:

View File

@ -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 = { const NG_FOR_DIR = {
'NgFor': ` 'NgFor': `
@Directive({ @Directive({
@ -519,6 +535,27 @@ describe('completions', () => {
['myOutput']); ['myOutput']);
expectReplacementText(completions, text, 'my'); expectReplacementText(completions, text, 'my');
}); });
it('should return completions inside an LHS of a partially complete two-way binding', () => {
const {ngLS, fileName, cursor, text} =
setup(`<h1 dir [(mod¦)]></h1>`, ``, 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']);
});
}); });
}); });