Vscode: Custom editor / custom document discussion

Created on 12 Dec 2019  路  20Comments  路  Source: microsoft/vscode

Temporarily forking off a more focused discussion from #77131

Background

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

Design one 鈥斅燜ully separate custom documents and custom webview editors

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:

  • Two providers + two extension points, one for custom documents and one for custom editors
  • A single CustomDocumentDelegate is used for all editors of a given type
  • The CustomDocument objects would be created by VS Code. Extensions could access the original object returned by CustomDocumentDelegate using the .delegate property
  • CustomDocument objects share many properties with TextDocument

API:

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"
            }
        ]
    }
]

Design 2 鈥斅燬ingle provider that acts as both delegate and resolver

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:

  • The 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 functions
  • There is no more need for CustomDocument. Instead we always use uris to identify which file we are talking about

API:

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

custom-editors under-discussion

All 20 comments

@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:

  • Developer creates a Vega chart vg.json spec using population.json data.
  • You can preview it in Vega Viewer and save it as SVG or PNG document.
  • You can view the data source for that chart webview in Data Preview (try it!)
  • Developer changes population.json data file. Both Vega Viewer and Data Preview webviews get updated.

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:

  1. 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.

  2. 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.

  3. Very similar to the above, but think CSV or raw textual data that can be visualized (and manipulated)

  4. 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:

  1. This gets back to the point I made while introducing the rational behind design 2: It is seems difficult to define a custom document that is generic enough to be useful for multiple view types while not so generic as to be meaningless

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

  1. 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.

  2. Same points as above. For model reuse with design 2, there are a few options:

    • If a single extension is implementing all this, just share model code
    • Otherwise, have the extension expose an API similar to openTextDocument that other extensions can invoke
  3. This 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

Was this page helpful?
0 / 5 - 0 ratings