Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(language-service): two-way binding completion should not remove t… #45582

Conversation

ivanwonder
Copy link
Contributor

@ivanwonder ivanwonder commented Apr 10, 2022

…he trailing quote

For two-way binding syntax, the Angular compiler will append the =$event to the expression of
BoundEvent.

`${name}Change`, `${expression} =$event`, /* isAssignmentEvent */ true, sourceSpan,

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

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.io application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

@ivanwonder ivanwonder force-pushed the two-way-binding-removes-trailing-quote branch 4 times, most recently from 56720da to bbfce2d Compare April 10, 2022 15:29
@ivanwonder ivanwonder marked this pull request as ready for review April 10, 2022 15:53
@pullapprove pullapprove bot requested a review from atscott April 10, 2022 15:53
@jessicajaniuk jessicajaniuk added the area: language-service Issues related to Angular's VS Code language service label Apr 11, 2022
@ngbot ngbot bot added this to the Backlog milestone Apr 11, 2022
@atscott
Copy link
Contributor

atscott commented Apr 11, 2022

This is a regression caused by db6cf7e which is creating an inaccurate source span for the synthetic bound event used for the completion.

@JoostK Can we change the implementation to just remove the trailing ! if the expression ends with it? Doing that would simplify the implementation, since the LHS wouldn't have the ! at the end. I know this would still not have accurate source spans, but that wouldn't be as much of a problem here since there wouldn't be completions for something that just ends with a !.

i.e.

   if (expression.endsWith('!')) {
      expression = expression.slice(0, -1);
    }
    this.bindingParser.parseEvent(
        `${name}Change`, `${expression}=$event`, /* isAssignmentEvent (no longer needed) */ true, sourceSpan,
        valueSpan || sourceSpan, targetMatchableAttrs, events, keySpan);

@atscott atscott requested a review from JoostK April 11, 2022 17:34
@atscott atscott added area: compiler Issues related to `ngc`, Angular's template compiler regression Indicates than the issue relates to something that worked in a previous version target: patch This PR is targeted for the next patch release action: review The PR is still awaiting reviews from at least one requested reviewer labels Apr 11, 2022
@JoostK
Copy link
Member

JoostK commented Apr 11, 2022

Oh, interesting; that is definitely an unexpected side-effect of db6cf7e!

Can we change the implementation to just remove the trailing ! if the expression ends with it?

I am a bit wary of doing that, as it would again be susceptible to unexpected parses in combination with the = sign. The only valid expression that I can think of that would cause trouble is when using double (or repeated really) non-null assertions; [(ngModel)]="value!!", which admittedly is easy to account for with your suggestion as well. Then there's things like [(ngModel)]="input>" which results in a valid parse tree input >= $event for the output binding, but this is caught as an error for the input binding so unlikely to become a real problem.

Overall the current parsing approach is awful and would benefit from an overhaul, also to fix the source span issues.

@atscott
Copy link
Contributor

atscott commented Apr 11, 2022

In that case, I would propose updating the template visitor logic to not create this situation in the first place rather than filtering things out after the fact. This bit of code could be added to the visitElementOrTemplate after this.visitAll(element.inputs);.

    // We allow the path to contain both the `t.BoundAttribute` and `t.BoundEvent` for two-way
    // bindings but do not want the path to contain both the `t.BoundAttribute` with its
    // children when the position is in the value span because we would then logically create a path
    // that also contains the `PropertyWrite` from the `t.BoundEvent`. This early return condition
    // ensures we target just `t.BoundAttribute` for this case and exclude `t.BoundEvent` children.
    if (this.path[this.path.length - 1] !== element &&
        !(this.path[this.path.length - 1] instanceof t.BoundAttribute)) {
      return;
    }

There should also be a test in the template_target_spec for this, not just in the completions_spec.ts.

@JoostK Had some feelings about changing the implementation of the visitor to "extend the return type of TemplateTargetVisitor.visitTemplate to be the path to BoundAttribute, together with a (nullable) pointer to a BoundEvent" so we aren't breaking the mental model of the path by having the BoundEvent look like a child of the BoundAttribute when there's a two way binding. This is a bit larger of a refactor though.

@ivanwonder
Copy link
Contributor Author

In that case, I would propose updating the template visitor logic to not create this situation in the first place rather than filtering things out after the fact. This bit of code could be added to the visitElementOrTemplate after this.visitAll(element.inputs);.

The BoundEvent is still needed in the path.

private getDefinitionMetaAtPosition({template, component}: TemplateInfo, position: number):
DefinitionMeta[]|undefined {
const target = getTargetAtPosition(template, position);
if (target === null) {
return undefined;
}
const {context, parent} = target;
const nodes =
context.kind === TargetNodeKind.TwoWayBindingContext ? context.nodes : [context.node];
const definitionMetas: DefinitionMeta[] = [];
for (const node of nodes) {
const symbol = this.compiler.getTemplateTypeChecker().getSymbolOfNode(node, component);
if (symbol === null) {
continue;
}
definitionMetas.push({node, parent, symbol});
}
return definitionMetas.length > 0 ? definitionMetas : undefined;
}
}

@atscott
Copy link
Contributor

atscott commented Apr 13, 2022

The BoundEvent is still needed in the path.

Only for the case where the previous value in the path is a BoundAttribute. Otherwise it's not identified as a two way binding anyways.

@ivanwonder
Copy link
Contributor Author

Oh, I got it, but I think this is easy to understand for me

    const length = this.path.length;
    this.visitAll(element.inputs);
    const noMatchedInputs = this.path.length === length;
    const matchedBoundAttribute =
        this.path.length - length === 1 && this.path[length] instanceof t.BoundAttribute;
    if (noMatchedInputs || matchedBoundAttribute) {
      this.visitAll(element.outputs);
    }

visit the event only when there are no matched inputs or there only has one BoundAttribute.

@atscott
Copy link
Contributor

atscott commented Apr 14, 2022

@ivanwonder Right, that's pretty close to the same thing. The issue I have is that it really can be an early exit from the rest of the visit calls. If you visit the inputs and match anything, the rest of the visit logic doesn't need to happen (aside from possibly the BoundEvent for two way binding). There's nothing possibly more specific. So it shouldn't be just if (condition) visitOutputs but if (condition) early exit because we know we have the most specific match possible

Edit: I think the distinction might really not matter too much in the end. The rest of the visitor logic would just exit immediately since the position would be outside the spans. But I do think it's easier to understand if (we know we're done) return; instead of if (something) do more work.

…he trailing quote

We allow the path to contain both the `t.BoundAttribute` and `t.BoundEvent` for two-way
bindings but do not want the path to contain both the `t.BoundAttribute` with its
children when the position is in the value span because we would then logically create a path
that also contains the `PropertyWrite` from the `t.BoundEvent`. This early return condition
ensures we target just `t.BoundAttribute` for this case and exclude `t.BoundEvent` children.

Fixes angular/vscode-ng-language-service#1626
@ivanwonder ivanwonder force-pushed the two-way-binding-removes-trailing-quote branch from bbfce2d to af58ff8 Compare April 15, 2022 11:28
@atscott atscott removed the request for review from JoostK April 15, 2022 19:17
@atscott atscott added action: merge The PR is ready for merge by the caretaker merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Apr 15, 2022
@ngbot
Copy link

ngbot bot commented Apr 15, 2022

I see that you just added the action: merge label, but the following checks are still failing:
    failure status "ci/circleci: legacy-unit-tests-saucelabs" is failing

If you want your PR to be merged, it has to pass all the CI checks.

If you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help.

@atscott
Copy link
Contributor

atscott commented Apr 15, 2022

merge assistance: the failing test is unrelated

@dylhunn
Copy link
Contributor

dylhunn commented Apr 15, 2022

This PR was merged into the repository by commit f57e46c.

@dylhunn dylhunn closed this in f57e46c Apr 15, 2022
dylhunn pushed a commit that referenced this pull request Apr 15, 2022
…he trailing quote (#45582)

We allow the path to contain both the `t.BoundAttribute` and `t.BoundEvent` for two-way
bindings but do not want the path to contain both the `t.BoundAttribute` with its
children when the position is in the value span because we would then logically create a path
that also contains the `PropertyWrite` from the `t.BoundEvent`. This early return condition
ensures we target just `t.BoundAttribute` for this case and exclude `t.BoundEvent` children.

Fixes angular/vscode-ng-language-service#1626

PR Close #45582
@ivanwonder ivanwonder deleted the two-way-binding-removes-trailing-quote branch April 16, 2022 02:03
crapStone pushed a commit to Calciumdibromid/CaBr2 that referenced this pull request Apr 25, 2022
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@angular/animations](https://github.com/angular/angular) | dependencies | patch | [`13.3.3` -> `13.3.4`](https://renovatebot.com/diffs/npm/@angular%2fanimations/13.3.3/13.3.4) |
| [@angular/common](https://github.com/angular/angular) | dependencies | patch | [`13.3.3` -> `13.3.4`](https://renovatebot.com/diffs/npm/@angular%2fcommon/13.3.3/13.3.4) |
| [@angular/compiler](https://github.com/angular/angular) | dependencies | patch | [`13.3.3` -> `13.3.4`](https://renovatebot.com/diffs/npm/@angular%2fcompiler/13.3.3/13.3.4) |
| [@angular/compiler-cli](https://github.com/angular/angular) | devDependencies | patch | [`13.3.3` -> `13.3.4`](https://renovatebot.com/diffs/npm/@angular%2fcompiler-cli/13.3.3/13.3.4) |
| [@angular/core](https://github.com/angular/angular) | dependencies | patch | [`13.3.3` -> `13.3.4`](https://renovatebot.com/diffs/npm/@angular%2fcore/13.3.3/13.3.4) |
| [@angular/forms](https://github.com/angular/angular) | dependencies | patch | [`13.3.3` -> `13.3.4`](https://renovatebot.com/diffs/npm/@angular%2fforms/13.3.3/13.3.4) |
| [@angular/platform-browser](https://github.com/angular/angular) | dependencies | patch | [`13.3.3` -> `13.3.4`](https://renovatebot.com/diffs/npm/@angular%2fplatform-browser/13.3.3/13.3.4) |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular) | dependencies | patch | [`13.3.3` -> `13.3.4`](https://renovatebot.com/diffs/npm/@angular%2fplatform-browser-dynamic/13.3.3/13.3.4) |

---

### Release Notes

<details>
<summary>angular/angular</summary>

### [`v13.3.4`](https://github.com/angular/angular/blob/HEAD/CHANGELOG.md#&#8203;1334-2022-04-20)

[Compare Source](angular/angular@13.3.3...13.3.4)

##### core

| Commit | Type | Description |
| -- | -- | -- |
| [9317f513d5](angular/angular@9317f51) | fix | better error message when directive extends a component ([#&#8203;45658](angular/angular#45658)) |
| [4766817f02](angular/angular@4766817) | fix | improve multiple components match error ([#&#8203;45645](angular/angular#45645)) |

##### language-service

| Commit | Type | Description |
| -- | -- | -- |
| [d68333e508](angular/angular@d68333e) | fix | two-way binding completion should not remove the trailing quote ([#&#8203;45582](angular/angular#45582)) |

#### Special Thanks

Andrew Kushnir, Andrew Scott, George Kalpakas, Ilya Marchik, Jeremy Elbourn, Kristiyan Kostadinov, Louis Gombert, Mangalraj, Marko Kaznovac, Paul Gschwendtner, Saurabh Kamble, dario-piotrowicz and ivanwonder

<!-- CHANGELOG SPLIT MARKER -->

</details>

---

### Configuration

📅 **Schedule**: At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, click this checkbox.

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

Co-authored-by: cabr2-bot <cabr2.help@gmail.com>
Reviewed-on: https://codeberg.org/Calciumdibromid/CaBr2/pulls/1316
Reviewed-by: Epsilon_02 <epsilon_02@noreply.codeberg.org>
Co-authored-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org>
Co-committed-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org>
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators May 17, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: compiler Issues related to `ngc`, Angular's template compiler area: language-service Issues related to Angular's VS Code language service merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note regression Indicates than the issue relates to something that worked in a previous version target: patch This PR is targeted for the next patch release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Angular template code completion removes trailing quote
5 participants