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

WebviewContentProvider API investigation #45994

Closed
mjbvz opened this issue Mar 16, 2018 · 12 comments
Closed

WebviewContentProvider API investigation #45994

mjbvz opened this issue Mar 16, 2018 · 12 comments
Assignees
Labels

Comments

@mjbvz
Copy link
Collaborator

mjbvz commented Mar 16, 2018

From #43713

Problem
The currently proposed webview API is a push model (extensions call createWebview to create and push a webview to vscode). This model prevents us from implementing restoration of webviews when vscode is reloaded. It also does not feel consistent with the other APIs

Proposal
Based on a discussion with @jrieken and @kieferrm, move webview to more of a pull model similar to the TreeDataProviderProposal (vscode asks an extension to create a webview). This would also be very similar to the existing TextDocumentContentProvider api.

Here's a mock up:

	/**
	 * A webview is an editor with html content, like an iframe.
	 */
	export interface Webview {
		/**
		 * Unique identifer of the webview.
		 */
		readonly uri: Uri;

		/**
		 * Content settings for the webview.
		 */
		options: WebviewOptions;

		/**
		 * Title of the webview shown in UI.
		 */
		title: string;

		/**
		 * Contents of the webview.
		 *
		 * Should be a complete html document.
		 */
		html: string;

		/**
		 * The column in which the webview is showing.
		 */
		readonly viewColumn?: ViewColumn;

		/**
		 * Fired when the webview content posts a message.
		 */
		readonly onDidReceiveMessage: Event<any>;

		/**
		 * Fired when the webview is disposed.
		 */
		readonly onDidDispose: Event<void>;

		/**
		 * Event fired when webview's state changes
		 */
		readonly onDidChangeViewState: Event<{ isActive: boolean, viewColumn: ViewColumn }>;

		/**
		 * Post a message to the webview content.
		 *
		 * Messages are only develivered if the webview is visible.
		 *
		 * @param message Body of the message.
		 */
		postMessage(message: any): Thenable<boolean>;

		/**
		 * Shows the webview in a given column.
		 *
		 * A webview may only show in a single column at a time. If it is already showing, this
		 * command moves it to a new column.
		 */
		show(viewColumn: ViewColumn): void;

		/**
		 * Dispose of the the webview.
		 *
		 * This closes the webview if it showing and disposes of the resources owned by the webview.
		 * Webview are also disposed when the user closes the webview editor. Both cases fire `onDispose`
		 * event. Trying to use the webview after it has been disposed throws an exception.
		 */
		dispose(): any;
	}

	/**
	 * Provides webview editors based on uri.
	 *
	 * Webview Content providers are [registered](#workspace.registerWebviewContentProvider)
	 * for a [uri-scheme](#Uri.scheme). When a uri with that scheme is to
	 * be [loaded](#workspace.openTextDocument) the content provider is
	 * asked.
	 */
	export interface WebviewContentProvider {

		/**
		 * Initialize a new webview.
		 *
		 * @param uri Identifier of webview resource.
		 * @param webview Webview to initialize.
		 * @param token Cancellation token.
		 *
		 * @returns Did initilization succeed?
		 */
		initializeWebview(uri: Uri, webview: Webview, token: CancellationToken): ProviderResult<boolean>;
	}

	namespace workspace {
		/**
		 * Registers a new WebviewContentProvider for a given resource scheme.
		 *
		 * When a document of scheme is opened, the provider is asked to initialize the webview's content.
		 */
		export function registerWebviewContentProvider(scheme: string, provider: WebviewContentProvider): Disposable;
	}

The markdown extension for example would register a provider for the markdown-preview: scheme. API usage:

Create a new markdown preview

  • Markdown extension calls vscode.open('markdown-preview:preview1')
  • This invokes markdown's content provider

Restoring a markdown preview

  • Document with 'markdown-preview:preview1' uri is open
  • VS Code is reloaded.
  • VS Code restores the 'markdown-preview:preview1' document. This inits any extensions that activate on markdown-preview: and then invokes markdown's content provider

Close a markdown preview
Either:

  • User action
  • Extension calls .dispose() on the webview
  • Or command closes the document for ``'markdown-preview:preview1'` resource

/cc @sandy081

@mjbvz mjbvz self-assigned this Mar 16, 2018
@sandy081
Copy link
Member

@jrieken Is vscode.open API is the entry point to get access to the view (TreeView / WebView)?

@jrieken
Copy link
Member

jrieken commented Mar 19, 2018

A few more ideas after discussions during and after our standup:

1) have explicit state and a factory

  • using Uri is confusing, we should have a state object and another identity string
  • Factories ftw, so lets call it WebviewFactory and lets make it return a Webview (created via createWebview)
  • lets have an event that's fired before a webview is being closed
export interface WillCloseWebviewEvent {
  storeState(state: any): void;
}

export Webview {
  onWillClose: Event<WillCodeWebviewEvent>
}

export interface WebviewFactory {
  export function createWebview(id:string, state:any, token: CancellationToken): Webview;
}

export namespace window {
  export function createWebview(id:string, options): Webview;
  export function registerWebviewFactory(id:string, ...): Disposable; 
}

The markdown flow would be

  • The preview-command creates and populates a webview via createWebview
  • Before closing the onWillCose-event fires and allows the extension to persist state
  • After re-open the workbench sends an activation event onView:webview (maybe onView:webview/<id-prefix>?)
  • extensions register their webview factories
  • the workbench finds the right webview factory (based on the id), retrieves the last state, and invokes the factory
  • the factory uses createWebview and the provided state-object

2) have a webview serialiser

We can make the (web)view-state something that is first-class and ask extensions to (optionally) provide a state serialiser/deserialiser for restoring webviews and ideally tree views

export interface WebviewSerializer {
  captureWebview(webview: Webview):any;
  restoreWebview(state:any): Webview;
}

export namespace window {
  export function registerWebviewSerializer(id: string, serializer: WebviewSerializer): Disposable;

  export function createWebview(id:string, options...): Webview;
}

Again, the markdown flow

  • The preview-command creates and populates a webview via createWebview
  • Before closing the workbench looks up webview serialisers by id and call them with the respective webviews
  • After re-open the workbench sends an activation event onView:webview (maybe onView:webview/<id-prefix>?)
  • extensions register their webview serializers
  • the workbench finds the right webview serializer (based on the id), retrieves the last state, and invokes the serializer
  • the serializer uses createWebview and the provided state-object

@DanTup
Copy link
Contributor

DanTup commented Mar 19, 2018

#30288 referenced this, but the changes above look very webView specific. Will this apply to trees too? (#30288 is about being able to get access to a tree and select a node).

@sandy081
Copy link
Member

sandy081 commented Mar 19, 2018

@DanTup This is referenced because both of them are views and we want the API to be similar in exposing them.

@jrieken It seems we are going with create(Web|Tree)View approach that return (Web|Tree)View which has to be disposable?

mjbvz added a commit that referenced this issue Mar 20, 2018
As discussed in #45994, move from using a uri to using a viewType. The view type is shared among all webviews of a given type, such as all markdown previews

Fixes #44575
@mjbvz
Copy link
Collaborator Author

mjbvz commented Mar 20, 2018

Ok, I like option 2 but am concerned about needing to call out to extensions on shutdown to get the state. My proposal is that we make the state something that extensions continuously write. Something like:

interface Webview {
    ...,

    state: any;
}

// TODO: better name
interface WebviewRehydrator {
  restoreWebview(webview: Webview): void
}

export namespace window {
  export function registerWebviewRehydrator(id: string, rehydrator: WebviewRehydrator): Disposable;
}

That way, on shutdown we can just write out the view state without having to call into extensions

@jrieken Any concerns with this approach?

@mjbvz mjbvz added the webview Webview issues label Mar 20, 2018
@jrieken
Copy link
Member

jrieken commented Mar 21, 2018

Any concerns with this approach?

Yeah, we discussed the continuous state writing approach and I initially liked it but I now think that reality is more complex, esp. since most state is managed inside the webview-process and not the extension. Asking extensions to continuously update means heavy ipc-traffic and it's also hard to enforce, e.g some extensions might continuously update their state and some might never do it. That's why I preferred the explicit pull model in the end.

2 but am concerned about needing to call out to extensions on shutdown to get the state.

Sharing but also not sharing the concern. For once we already call into extensions on shutdown have a little of a grace/waiting period. Then, the API doesn't talk about shutdown and the workbench is free to call the captureState-function when it wants, e.g. after every n-th IPC message. That gives us more control and flexibility.

@mjbvz
Copy link
Collaborator Author

mjbvz commented Mar 21, 2018

Ok, I'm just struggling to fit pull serialization into our editor model. Leveraging IEditorInputFactory would make the most sense but it is all sync.

Realistically the extension host or the extension itself could throttle the state updates. These state objects should also be small. The markdown preview's state for example would be something like:

{
     "resource": "/path/to/file.md",
     "locked": false,
     "topMostVisibleRow": 0
}

Sorry for bringing it up again, but a few tweaks on the .state based designs:

  • Make state a uuid and completely delegate to the extension for writing and restoring this state based on the uuid

  • Make the contents of webview write the state. That would avoid polluting the extension ipc channel. However it wouldn't handle the case where the webview's state should change but it is not visible (such as when I have a markdown preview in the background and then change the active markdown file)

@jrieken
Copy link
Member

jrieken commented Mar 23, 2018

Ok, I'm just struggling to fit pull serialization into our editor model. Leveraging IEditorInputFactory would make the most sense but it is all sync.

I understand but that shouldn't have an influence in how we design the API. @bpasero might be able to help out and explore how editor input persistence can be made async

@bpasero
Copy link
Member

bpasero commented Mar 23, 2018

It all starts from here for the editor input factory to persist. We do allow long running operations from the shutdown, so we could make this async. But as usual, doing long running things in the shutdown phase is something we should stay away from. In the worst case it can mean that the OS flags VS Code as preventing a shutdown if it takes too long to quit.

@mjbvz
Copy link
Collaborator Author

mjbvz commented Mar 23, 2018

Thanks @bpasero! That seems like exactly what we need here.

I'll work to get a prototype of this API for this iteration. There are a few other things to investigate as part of restoration that will be pushed off for this iteration:

  • We should show a loading indicator when restoring the webview.

  • What happens when we restore a webview but the extension that provided it is now disabled or uninstalled? Probably should show a message in the preview itself

  • Can an extension veto restoration? (They could call webview.dispose() to do this)

  • The markdown preview is designed to only allow a single preview in a given editor group. With lazy preview restoration however, we can't enforce this:

    1. Open markdown preview
    2. Move preview to background tab
    3. Restart vscode
    4. Open another markdown preview in the same editor group as the old preview
    5. Bug: two markdown previews open in the same editor group now (one live one and one waiting to be revived)

@bpasero
Copy link
Member

bpasero commented Mar 24, 2018

@mjbvz I believe we already have code in place that gracefully deactivates extensions on shutdown by giving them a little bit of time (not sure where that is implemented and if the time is just hardcoded). We would need to add this logic close to that place to not have a race condition between deactivating extensions and persisting the editor view state.

We should show a loading indicator when restoring the webview.

I think that will happen automatically if the call to openEditor is long running. e.g we already show a progress bar when opening a slow loading file or large file.

mjbvz added a commit that referenced this issue Apr 4, 2018
From #45994

Adds an experimental API that allows extensions to persist webviews and restore them even after vscode restarts. Uses this api to restore markdown previews. This is done by:

- Adding a new `state` field on webviews. This is a json serializable blob of data
- Adds a new `WebviewReviver` interface (name will probably change). This binds a specific viewType to a provider that can restore a webview's contents from its `state`
- In VS Code core, persist webview editors. When we restart and need to show a webview, activate all extensions that may have reviviers for it using the `onView:viewType` activation event.

Current implementation is sort of a mess. Will try to clean things up
@mjbvz
Copy link
Collaborator Author

mjbvz commented Apr 4, 2018

Fixed by #46380. Will open new issues to track the individual issues identified

@mjbvz mjbvz closed this as completed Apr 4, 2018
@vscodebot vscodebot bot locked and limited conversation to collaborators May 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

5 participants