-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Expose important pieces of vs folder on API #5283
Comments
Good idea for common things like |
Looking through the list of addons, these would benefit from exposing event/emitter stuff on the API:
|
@Tyriar Did some further digging around Event/Emitter and Disposable:
Given the list of addons using
|
@jerch if you don't extend the typical thing is to use DisposableStore as a property instead. That's less convenient sometimes but if it'll save a bunch we can use that approach exclusively in addons instead of extends? Example: Did you want to take a stab at a PR for this or want me to, since I introduced the problem? 😉 |
Oh ic, yeah that might be the easier pattern then. Although I found a pattern with runtime /////////////////
// before
// in AddonXY.ts
import { Disposable } from 'vs/base/common/lifecycle';
export class AddonXY extends Disposable { ... }
/////////////////
// with runtime extends
// in core terminal (quickly hacked into core)
private helperCls = class extends Disposable {}
public disposableCtor(): {new(): Disposable} {
return this.helperCls;
}
// in AddonXY.ts
import type { Disposable } from 'vs/base/common/lifecycle';
export function AddonXY(terminal: Terminal, ...) {
const DisposableBase = (terminal as any)._core.disposableCtor() as {new(): Disposable};
const AddonXY = class extends DisposableBase { ... };
return new AddonXY(...);
} It works basically the same as before, but now needs a terminal arg at the fake ctor already to derive the Disposable impl from there. Its not nicely abstracted yet, the
Up to you. I can try to formalize that idea, though I am not 100% sure yet, if this works properly in all circumstances. At the moment this is a full drop-in replacement with only the Edit: To get |
Runtime extends looks pretty confusing to me. The composition approach isn't quite as convenient but it's super clear how it works. We could also have a special // shared/
export class AddonDisposable {
constructor(storeCtor: DisposableStoreCtor) {
// init protected readonly _store
}
dispose() {
this._store.dispose();
}
} // addon
export class WebglAddon extends AddonDisposable {
constructor() {
super(xterm.DisposableStore);
}
} That way only the class in shared/ will be pulled into the addon bundle |
Yepp, +1 to composition. It also needs less awkward APi additions. |
@Tyriar Would we need a separate xterm-shared for external addon creators, or will installing the xterm package with extended API be enough in the end to build an external addon? To me it seems quite involved if the full xterm source would be needed just to build a tiny addon xy. |
@jerch I don't think so, it'll be a very small wrapper and they'll only be gaining things by having new stuff exposed on the API. |
It is kinda obvious, that things here might lead to a bigger API change. So its prolly better to discuss a few things to not get on the wrong track. current situation xterm base package:
addons (plugins):
non-monolithic / non-single-API bundling for the rescue? Whats the actual issue? ideas for a solution Current addon invocation and relation/lifecycle relative to xterm:
Beside that there are no further restrictions. The last point is an important aspect of the addon instance's lifecycling: const xy = new AddonXY(customArgs);
/* Now instance xy is alive but not yet attached to a terminal instance.
For most addons this means, it cannot do anything yet
(most addon have no explicit or only tiny stub ctors).
*/
term.loadAddon(xy);
/* Thats the crucial call for the addon registering it to a terminal instance.
`loadAddon` does some bookkeeping of the instance and
takes the ownership for final disposal (in case the whole terminal
got disposed, so ppl dont have to cleanup addon instances themselves)
Finally `loadAddon` will call into `activate`...
*/
--> xy.activate(term);
/* The second or true initializer of the addon is finally called with
the terminal instance as argument. Most addons will postpone their real init code
to that method, as they have no means for any action without a terminal instance.
*/
...
// later on addon disposal happens either by:
xy.dispose() // or
term.dispose() So we basically have a 2-stage construction pattern for addons with the ctor doing its memory object creation thing and But why a 2-stage construction? So maybe I've overlooked something crucial in that 2-stage construction setup, if so plz tell me. The only thing I can imagine is a strong need of independence between stage 1 and stage 2 (between ctor and Proposal: class AddonXY implements ... {
constructor(term: Terminal, ...someOtherNeededArgs: any[]) {
// pre-registering setup goes here (prev code in ctor)
onChange = new term.shared.Emitter<Wotever>();
...
term.loadAddon(this);
// post-registering setup goes here (prev code in activate)
...
}
} This might need some different handling at taking the disposal ownership in the addon manager, but should be doable. The real benefit of such a simplified addon construction is the fact, that we can expose the needed goodies on the terminal instance itself, so the addon ctor can use them normally without getting the Finally I came back to the issue at hand, ewww. It is much much easier to solve with a single step addon construction than it is now with the ctor/activate separation. If we stick to the composition pattern and reduce the addon construction to a single step we should be able to avoid most of the doubled/tripled code issues from addons. @Tyriar Sorry for the wall of text, I admit that I got lost myself a bit in the middle weighing the pros/cons. I still think the big arc was needed to transport the whole picture. |
If I can remember right the reason I went with 2 stage is so that the interface is very simple in xterm.d.ts: Lines 1314 to 1322 in d81b25c
Nothing dynamic here, just a simple constructor(terminal: Terminal, ...args: unknown); Then we also need to either assume the addon is correctly implemented and actually called constructor(term: Terminal, ...someOtherNeededArgs: any[]) {
...
term.loadAddon(this); So we're leaving more up to addons doing the right thing. While having things be optional in addons is a little nuisance sometimes, large addons can follow a pattern similar to xterm.js/addons/addon-webgl/src/WebglAddon.ts Lines 18 to 20 in d81b25c
Another benefit of this 2 stage approach that's obvious here is that we can initialize listeners of events before we activate the addon. Otherwise we would miss the events here if they fire during activate: xterm.js/addons/addon-webgl/src/WebglAddon.ts Lines 22 to 29 in d81b25c
I think
So given the above I think we must actually keep the 2 stage due to the event listening. I actually wrote this up before reading through this issue and I think it's still relevant (your suggestion was kind of a hybrid of 1 and 2): There are 4 main options I'm considering here:
I'm still leaning towards 3 which is a breaking change for addons, but we're already coming up on v6.0.0 anyway. The breaking change will be obvious when someone updates as the addon ctors will not compile. Something else that comes to mind is maybe we want the addon to pull in a minimal set of things too. So instead of this: constructor(xtermApi: XtermApi, opts...) We do this: constructor(disposableCtor: typeof DisposableStore, opts...) Or this: constructor(xtermApi: Pick<XtermApi, 'DisposableStore'>, opts...) I'm trying to wrap my head around it but if we don't do this there's probably going to be a lot of people running into compile errors when the xterm.js version is even slightly out of sync with the addon version. I'm sure in practice these versions are never so strictly aligned (outside of VS Code where versions are always the most up to date |
To sum up, after reading everything and writing that last comment, I think these are the 3 options we have:
We can kind of just avoid all this if we bring back the old simple
What do you think about just going this route and avoiding the breaking change all together? |
Out of your list (3) would be my favorite currently, as it does the least "damage" to current API and addon setup. 3rd party addons not needing the goodies will just keep working as before. Still for the sake of uniformity I think we should apply the new argument to all first party addon ctors and just announce that as a major change of our addons. About (4) - yes, that actually might be needed in conjunction with (3) in the long run, if the vs impls + types will shift alot. Exposing their ctors directly on the API kinda reintroduces that building-blocks-issue I ranted about above, but I see no reason, why we cannot hide bigger internal shifts by just wrapping things into more stable API bridging code and minimalistic types (similar to what we do for terminal internals). My current PR indeed just forwards Edit: |
Coming from #5251 (comment)
Currently using Event, Disposable etc. from the
vs/*
path in addons adds ~50kB on addons. We should investigate, if and how we can save some binary size on addons by exposing crucial parts on the API, so the impls dont have to be copied over for every single addon.The text was updated successfully, but these errors were encountered: