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:
parent
ebb7ac5979
commit
7d74853a1d
|
@ -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) {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue