volarjs / volar.js

💙🌊
https://volarjs.dev/
MIT License
963 stars 47 forks source link

feat(source-map): add API to support generated code with different length than original code #183

Closed piotrtomiak closed 3 months ago

piotrtomiak commented 3 months ago

I am working on support for type checking of Angular templates in WebStorm using Volar in plug-in mode. In the Angular template transpilation logic some variables in expressions are renamed in the output, so the generated range length does not always match the source range length. This PR adds support for an optional generatedLength property on Mapping interface and adds support for it across the codebase.

There is also a fix for findOverlapCodeRange present. mappedEnd was incorrectly calculated.

johnsoncodehk commented 3 months ago

Thank you for your contribution. This method has been deprecated in Volar v2, as we found a more reliable way is to split the code with varying lengths into multiple segments.

For example, the v-load directive in the Vue template is generated into vLoad of different length, and the mapping input is:

{
    sourceOffsets: [0 /* v */, 2 /* load */],
    generatedOffsets: [0 /* v */, 1 /* Load */],
    lengths: [1, 4],
}

This helps to align each character.

https://github.com/volarjs/volar.js/assets/16279759/7ee7f9ab-c935-40aa-bd52-03bf959076dd

Before merging the PR, I want to confirm whether you have considered the above method.

piotrtomiak commented 3 months ago

That's a good point. I wanted to do something like that, but the problem I am facing is as follows:

This is the template:

<main *ngIf="title != 'foo' as bar" [foo]="{a: outletRef}">
  <div [foo]="{a: outletRef}" [foo2]="12 | thePipe">
    <router-outlet #outletRef="outlet"></router-outlet>
  </div>
</main>

<span i18n>Updated {minutes, plural, =0 {just now} =1 {one minute ago} other {{{minutes}} minutes ago}}</span>

and this is the generated code:

function _tcb_0<T extends mytype>(this: AppComponent<T>) {
  var _pipe1: MyPipe = null!;
  const _ctor1: <T = any>(init: Pick<TestIf<T>, "ngIf" | "ngIfThen">) => TestIf<T> = null!;
  const _ctor2: <T extends number = any>(init: Pick<TestFoo<T>, "foo" | "foo2">) => TestFoo<T> = null!;
  var _t1 = _ctor1({ "ngIf": this.title != 'foo', "ngIfThen": null as any });
  _t1.ngIf = this.title != 'foo';
  var _t2: any = null!;
  if (TestIf.ngTemplateContextGuard(_t1, _t2) && this.title != 'foo') {
    var _t3 = _t2.ngIf;
    var _t5: RouterOutlet = null!;
    var _t4 = _t5;
    var _t6 = _ctor2({ "foo": {
        "a": _t4
      }, "foo2": null as any });
    _t6.foo = {
      "a": _t4
    };
    var _t7 = _ctor2({ "foo": {
        "a": _t4
      }, "foo2": _pipe1.transform(12) });
    _t7.foo = ({
      "a": _t4
    });
    _t7.foo2 = _pipe1.transform(12);
  }
  "" + this.minutes;
  "" + this.minutes;
}

A good example here is pipe call: 12 | thePipe, which is translated to _pipe1.transform(12) in the output. The thePipe needs to map to transform.

Another one is {a: outletRef}, which is translated to {"a": _t4}. Here outletRef variable is replaced with generated name _t4, so all of the references are replaced as well.

The transpilation code comes from Agnular compiler, and I cannot change the output.

johnsoncodehk commented 3 months ago

Thanks for the explanation, I can see this being a problem that cannot be solved without replacing/changing the Angular compiler.

Not sure if that helps you, in https://github.com/volarjs/angular-language-tools (based on a rather outdated version of Volar) we avoid this by replacing the Angular compiler with @angular-eslint/bundled-angular-compiler and implementing template code transpilation ourselves.

I'll merge it so it doesn't block your implementation, but if one day you solve this problem and no longer need generatedLengths please let me know.

piotrtomiak commented 3 months ago

Thank you!