Temporarily forking off a more focused discussion from #77131
The custom editor proposal in VS Code 1.41 defines a single provider method called resolveWebviewEditor that both lets extensions implement the custom editor view and create a model for that view. This means there is a 1:1 relationship between views and models, which is problematic when multiple views of the same resource are opened. In cases where that happens, triggering save ends up triggering save for each model that exists for the resources, resulting in multiple saves for the same resource
To fix this, we believe the best approach is to separate the model part of custom editors from the view part of custom editors. @bpasero, @eamodio, and I iterated on some possible approach to this at the custom editor sync, and later @kieferrm and I continued the discussion.
This issue covers two possible ways to accomplish the split
This design attempts to decouple the idea of a custom document (the model) from the custom editor that presents that document. Custom documents would be similar to TextDocument in the existing VS Code api.
One goal of this design is to allow multiple custom editor views to be registered for a given type of custom document. These custom editor views could even come from different extensions
Key points design 1:
CustomDocumentDelegate is used for all editors of a given typeCustomDocument objects would be created by VS Code. Extensions could access the original object returned by CustomDocumentDelegate using the .delegate propertyCustomDocument objects share many properties with TextDocumentAPI:
namespace window {
export function registerWebviewCustomEditorProvider(viewType: string, provider: WebviewCustomEditorProvider, options?: WebviewPanelOptions): Disposable;
export function registerCustomDocumentDelegate(documentType: string, delegate: CustomDocumentDelegate): Disposable;
}
interface CustomDocumentDelegate {
readonly editingDelegate?: CustomDocumentEditingDelegate;
}
interface CustomDocumentEditingDelegate {
save(document: CustomDocument<unknown>): Thenable<void>;
saveAs(document: CustomDocument<unknown>, targetResource: Uri): Thenable<void>;
readonly onEdit: Event<{ document: CustomDocument<unknown> }>;
applyEdits(document: CustomDocument<unknown>, edits: readonly any[]): Thenable<void>;
undoEdits(document: CustomDocument<unknown>, edits: readonly any[]): Thenable<void>;
}
export interface WebviewCustomEditorProvider {
resolveWebviewEditor(document: CustomDocument<unknown>, webview: WebviewPanel): Thenable<void>;
}
export interface BaseDocument {
readonly uri: Uri;
readonly fileName: string;
readonly isUntitled: boolean;
readonly isDirty: boolean;
readonly isClosed: boolean;
save(): Thenable<boolean>;
}
interface CustomDocument<T extends CustomDocumentDelegate> extends BaseDocument {
readonly delegate: T;
}
Contributions:
"webviewEditors": [
{
"viewType": "testWebviewEditor.abc",
"displayName": "Test ABC editor",
"documentType": "testWebviewEditorDocument.abc"
}
],
"customDocument": [
{
"documentType": "testWebviewEditorDocument.abc",
"selector": [
{
"filenamePattern": "*.abc"
}
]
}
]
At first, this design may appear identical to our existing api proposal. However this proposal actually evolved from Design 1
The main difference from design 1 is that here we tightly couple the custom document (model) to the custom editor (view). The reasoning for this is that while looser coupling is generally preferable, Design 1 may be too loose to actually be useful.
To understand why, consider a custom image editor. With Design 1, there are two ways the coupling between model and view could work:
The model only operates on binary data. The views manipulate the model's binary data.
This is super flexible but not performant. It is also not easy to implement views that do this.
The model expresses some set of image operations that it supports (crop, resize, ...). The views call these to operate on the image.
This however limits to views to the operations the model defines. If the model doesn't have a rotate method, a view could never add one.
Key points of Design 2:
WebviewCustomEditorProvider becomes the delegate. This means a single delegate object is used for all views of a given type. To support this, we pass the resource as the first argument to all delegate functionsCustomDocument. Instead we always use uris to identify which file we are talking aboutAPI:
namespace window {
export function registerWebviewCustomEditorProvider(viewType: string, provider: WebviewCustomEditorProvider, options?: WebviewPanelOptions): Disposable;
}
export interface WebviewCustomEditorProvider {
resolveWebviewEditor(resource: Uri, webview: WebviewPanel): Thenable<void>;
readonly editingDelegate?: CustomDocumentEditingDelegate;
}
interface CustomDocumentEditingDelegate {
save(resource: Uri): Thenable<void>;
saveAs(resource: Uri, targetResource: Uri): Thenable<void>;
readonly onEdit: Event<{ resource: Uri }>;
applyEdits(resource: Uri, edits: readonly any[]): Thenable<void>;
undoEdits(resource: Uri, edits: readonly any[]): Thenable<void>;
}
{
"viewType": "testWebviewEditor.abc",
"displayName": "Test ABC editor",
"selector": [
{
"filenamePattern": "*.abc"
}
]
}
/cc @rebornix
@eamodio Do you have any specific uses cases in mind for allowing any extension to provide webviews for another extension's custom documents?
My feeling is that while this sounds neat, it would be really difficult for extensions to actually implement this (especially for binary data) and I can't think of any killer use cases that would justify all this. That's the main reason I'm currently in favor of the simplified design 2
I've implemented the second design in this branch. Example editors using this draft api: https://github.com/mjbvz/vscode-experimental-webview-editor-extension/tree/86802-draft
I like the overall API design and found it fairly easy to work with.
I found the biggest challenge in implementing this API is tracking the lifecycle of the document models that the extension stores. The current proposal requires extension implement reference counting so that they can destroy their models when the last editor goes away. It me be worth investigating adding opened and closed methods on the delegate that would notify the extension when a model for a resource needs to be created and when the last editor for that resource goes away
@eamodio Do you have any specific uses cases in mind for allowing any extension to provide webviews for another extension's custom documents?
My feeling is that while this sounds neat, it would be really difficult for extensions to actually implement this (especially for binary data) and I can't think of any killer use cases that would justify all this. That's the main reason I'm currently in favor of the simplified design 2
you should look into Vega Viewer & Data Preview integration for a good example of 2 webviews scenario you mentioned: https://marketplace.visualstudio.com/items?itemName=RandomFractalsInc.vscode-vega-viewer
ideally, I'd like to have both extensions update their views on data file changes using your new webview editor api and document model hooks.
My simple real world use case scenario for the 2 extensions I've mentioned:
Does your design 2 support this real world use case scenario?
Granted, I'll switch both extensions to this new API for the undo/redo stack UI you expose for when Vega chart json spec or Data Preview filtering/sorting, etc. changes, if your final api can support those scenarios ...
@mjbvz I just really like the separation of concerns of dealing with a data model (document) and UI model (editor) separately, because it can lead to many unforeseen opportunities. The examples I can think of are:
VS Code (or maybe just an extension) provides a document for editing images, then other extensions can build upon that model and provide their own image editing UI -- from something simple which maybe comes built in, to something far more advanced.
VS Code provides TextDocuments today and if those could also be provided to an editor implementation, then you can have custom editors built for specific use-cases. Think UI over configuration (e.g. our own settings _could_ be implemented as a custom editor for our settings.json file. I think this custom editor support for TextDocuments can enable a lot of cool experiences.
Very similar to the above, but think CSV or raw textual data that can be visualized (and manipulated)
An extension could provide a document around remote data - say backed by Azure Storage or S3 or whatever and then that document could be re-mix/re-used by other extensions to provide richer UIs
Also IMO in these examples, editors don't _have_ to perform editing -- they could be just viewers as well -- as this provides a great hook point to register alternative viewers/editors for specific file types
I guess I will never get a follow up on my real world scenario?
You guys have fun with it!
@RandomFractals I know you said look at your extension鈥檚 source but I do not understand the issue you are trying to solve or what your concerns are. Please try to clearly and concisely explain these
@mjbvz Matt, I think you need to download and try both extensions. You don't have to look at my source code, as long as you can support my real-world scenario described above for the data/document charting, export and data preview flow.
Let me know if you have further questions after you try them & adjust your api to support that flow.
@RandomFractals I鈥檝e already taken a look and your use case seems like it would work just fine...
As I cannot read your mind, please explain in text explicitly why/how the current proposals don鈥檛 meet your needs
@mjbvz happy to hear you think you can support that charting and data view integration scenario.
I simply listed it for you to consider as you draft this api & to test it with extensions used in the field with different json data formats that are beyond silly .cat(s) demoes :)
I look forward to take it for a test drive when you think you are ready for it.
Thanks!
@eamodio Thanks for the input!
Just some of my thoughts on these points:
For an image editor for example, a simple binary model isn't going to work well since it's too low level and would not perform well. However a more complex model would have to encode the type of operations it supports: crop, scale, .... But that would mean that other extensions could only ever use these high level operations and not introduce new ones
With design 2, you can still use TextDocument behind the scenes to implement a custom editor. I agree that Design 1 is more explicit in this.
Same points as above. For model reuse with design 2, there are a few options:
openTextDocument that other extensions can invokeThis is sort of independent of the two designs. Neither one specifies where the data is coming from or where save write too. A file system provider may be a better fit for sharing like you are proposing
At our standup today, @kieferrm also raised an interesting meta point: introducing a CustomDocument into the VS Code API opens up a whole lot of other questions that we do not want to address right now. For example, can a workspace edit touch a CustomDocument? Can I get a list of all documents including custom documents? Do events like TextDocumentWillSave happen for custom documents? Why not?
It quickly gets complicated
I've merged a version of 2 to proposed with the understanding that we will continue discussing it this week. Design 2 takes us in a good general direction, and the concept of CustomDocuments could be added on top if we wanted. My current feeling though is that we should keep it simpler and not introduce CustomDocument
well, I think you should do that and at least describe in text how you see this new webview editor api to sync data sources from disk, local & in remote use cases, such as vscode online & with remote fs & for the webview editors depending on those data/document updates to update their views.
Also, I still have not seen the proposed UX for your edits stack ...
@RandomFractals You seem to have a different understanding of what the custom editor api is trying to accomplish. Custom editors are not about how data is synchronized, nor how data is saved to disk, nor how the views are updated: that's all intentionally left up to extensions.
The goal of the custom editors api is to provide a minimal set of hooks that allow extensions to implement webview based previews/editors that can integrate with VS Code's basic operations (such as undo and save). We also aim to keep this at least somewhat consistent with rest of VS Code (such as supporting autosave). Beyond that, it's up to you
@mjbvz I am starting to get it now. So, those undo/save actions in custom webview editor would just work with normal undo/save keyboard shortcuts when that webview panel is active? What I meant by UX for the edits stack.
also, this is maybe off topic again, but what if v3 of this api provided some standard api for custom webview editor toolbar UI?
Most preview and custom webview editors I've seen have a top or bottom panel toolbar for preview actions, such as zoom for image/svg/graph previews, add line in REPL webview editors, etc.
What if part of this or separate effort, vscode provided an API for such custom webview editors to define and wire custom toolbar UI and actions.
I think such effort could lead to even more standard webviews across the board and would make it easier for devs to get those custom extensions working with standard vscode-icons, themes and theme colors and uniform custom toolbars look and feel for all such extensions.
@mjbvz & @eamodio your thoughts on that idea?
And thanks for your hard work on fleshing out this api, sample repo and the rest.
I do appreciate you doing it in the open for the 3rd party devs to have a peek while you draft and design this new api standard. It's looking good so far!
@RandomFractals We do not have any plans to standardize UI inside of custom editors. That could be covered by a separate proposal delivered independently of the core custom editor experience
@mjbvz so ... basically, this new webivew editor api is just an edits stack. sounds like there is not much more to it for ext. devs to bite into or use in terms of reading/saving docs or wiring custom doc and toolbars.
Would this still work at least? & thanks for your update!
@mjbvz I am starting to get it now. So, those undo/save actions in custom webview editor would just work with normal undo/save keyboard shortcuts when that webview panel is active? What I meant by UX for the edits stack.
The api provides hooks into VS Code's save + undo/redo flows. So if you press cmd+z with a custom editor open, the extension's undoEdits is invoked. The general UX for undo/redo is the same as in a text editor (although obviously the effects depend on the custom editor implementation)
We are moving forward with the second design based on our discussions and the reasons outlined above. This has been merged into vscode.proposed.d.ts and the example extensions have been updated
Will post this update back to #77131