Skip to content

Commit

Permalink
fix(language-service): two-way binding completion should not remove t…
Browse files Browse the repository at this point in the history
…he trailing quote

For two-way binding syntax, the Angular compiler will append the ` =$event` to the expression of
`BoundEvent`.
https://github.com/angular/angular/blob/e0ac61412137144a43dc5a2134b4cd620bb4c30f/packages/compiler/src/render3/r3_template_transform.ts#L493
For example, the `[(model)]="title.¦"`, the expression of `BoundEvent` will be converted to
`title.¦ =$event`.
        ^------ this blank will be included in the `replacementSpan` of completion item.
When the user selects an item, the trailing quote will be removed.

Now the paths include `BoundAttribute` and `BoundEvent` for the two-way binding syntax. So the
`BoundEvent` should be removed from the paths and use the `BoundAttribute` instead.

Fixes angular/vscode-ng-language-service#1626
  • Loading branch information
ivanwonder committed Apr 10, 2022
1 parent e0ac614 commit bbfce2d
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 2 deletions.
43 changes: 41 additions & 2 deletions packages/language-service/src/template_target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,16 @@ export function getTargetAtPosition(template: t.Node[], position: number): Templ
node: candidate,
};
} else if (candidate instanceof e.AST) {
const parents = path.filter((value: e.AST|t.Node): value is e.AST => value instanceof e.AST);
const newPaths = removeBoundEventInTwoWayBindingPath(path);
const parents =
newPaths.filter((value: e.AST|t.Node): value is e.AST => value instanceof e.AST);
const newCandidate = parents[parents.length - 1];
// Remove the current node from the parents list.
parents.pop();

nodeInContext = {
kind: TargetNodeKind.RawExpression,
node: candidate,
node: newCandidate,
parents,
};
} else if (candidate instanceof t.Element) {
Expand Down Expand Up @@ -454,3 +457,39 @@ function getSpanIncludingEndTag(ast: t.Node) {
}
return result;
}

/**
* For two-way binding syntax, the Angular compiler will append the ` =$event` to the expression of
* `BoundEvent`.
* https://github.com/angular/angular/blob/e0ac61412137144a43dc5a2134b4cd620bb4c30f/packages/compiler/src/render3/r3_template_transform.ts#L493
* For example, the `[(model)]="title.¦"`, the expression of `BoundEvent` will be converted to
* `title.¦ =$event`.
* ^------ this blank will be included in the `replacementSpan` of completion item.
* When the user selects an item, the trailing quote will be removed.
*
* Now the paths include `BoundAttribute` and `BoundEvent` for the two-way binding syntax. So the
* `BoundEvent` should be removed from the paths and use the `BoundAttribute` instead.
*/
function removeBoundEventInTwoWayBindingPath(paths: Array<t.Node|e.AST>): Array<t.Node|e.AST> {
const attributeOrEventNode: Array<t.BoundAttribute|t.BoundEvent> = paths.filter(
(path): path is t.BoundAttribute|t.BoundEvent =>
path instanceof t.BoundAttribute || path instanceof t.BoundEvent);

if (attributeOrEventNode.length === 2 && attributeOrEventNode[0] instanceof t.BoundAttribute &&
attributeOrEventNode[1] instanceof t.BoundEvent &&
attributeOrEventNode[1].name === (attributeOrEventNode[0].name + 'Change')) {
const newPaths: Array<t.Node|e.AST> = [];
let index = 0;
while (index < paths.length) {
if (paths[index] === attributeOrEventNode[1]) {
break;
}
newPaths.push(paths[index]);
index++;
}

return newPaths;
}

return paths;
}
8 changes: 8 additions & 0 deletions packages/language-service/test/completions_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,14 @@ describe('completions', () => {
expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title']);
});

it('should not include the trailing quote inside the RHS of a two-way binding', () => {
const {templateFile} = setup(`<h1 [(model)]="title."></h1>`, `title!: string;`);
templateFile.moveCursorToText('[(model)]="title.¦"');
const completions = templateFile.getCompletionsAtPosition();
expectContain(completions, ts.ScriptElementKind.memberFunctionElement, ['charAt']);
expectReplacementText(completions, templateFile.contents, '');
});

it('should return completions inside an empty RHS of a two-way binding', () => {
const {templateFile} = setup(`<h1 [(model)]=""></h1>`, `title!: string;`);
templateFile.moveCursorToText('[(model)]="¦"');
Expand Down

0 comments on commit bbfce2d

Please sign in to comment.