From 7b98e751a918da1a8118a60f17976f90357bd8f7 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Wed, 30 Oct 2019 10:44:30 +0000 Subject: [PATCH] [webview] fix #5647: restore webviews Signed-off-by: Anton Kosyakov --- .../src/hosted/browser/hosted-plugin.ts | 76 ++++++++++++++++ .../src/main/browser/webview/webview.ts | 13 ++- .../src/main/browser/webviews-main.ts | 90 +++++++------------ 3 files changed, 117 insertions(+), 62 deletions(-) diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index f9f4a25634f8d..b352b025643e3 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -54,6 +54,8 @@ import { FrontendApplicationStateService } from '@theia/core/lib/browser/fronten import { PluginViewRegistry } from '../../main/browser/view/plugin-view-registry'; import { TaskProviderRegistry, TaskResolverRegistry } from '@theia/task/lib/browser/task-contribution'; import { WebviewEnvironment } from '../../main/browser/webview/webview-environment'; +import { WebviewWidget } from '../../main/browser/webview/webview'; +import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; export type PluginHost = 'frontend' | string; export type DebugActivationEvent = 'onDebugResolve' | 'onDebugInitialConfigurations' | 'onDebugAdapterProtocolTracker'; @@ -131,6 +133,9 @@ export class HostedPluginSupport { @inject(WebviewEnvironment) protected readonly webviewEnvironment: WebviewEnvironment; + @inject(WidgetManager) + protected readonly widgets: WidgetManager; + private theiaReadyPromise: Promise; protected readonly managers = new Map(); @@ -158,6 +163,15 @@ export class HostedPluginSupport { this.viewRegistry.onDidExpandView(id => this.activateByView(id)); this.taskProviderRegistry.onWillProvideTaskProvider(event => this.ensureTaskActivation(event)); this.taskResolverRegistry.onWillProvideTaskResolver(event => this.ensureTaskActivation(event)); + this.widgets.onDidCreateWidget(({ factoryId, widget }) => { + if (factoryId === WebviewWidget.FACTORY_ID && widget instanceof WebviewWidget) { + const restoreState = widget.restoreState.bind(widget); + widget.restoreState = oldState => { + restoreState(oldState); + this.preserveWebview(widget); + }; + } + }); } get plugins(): PluginMetadata[] { @@ -185,6 +199,7 @@ export class HostedPluginSupport { protected async doLoad(): Promise { const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ })); + toDisconnect.push(Disposable.create(() => this.preserveWebviews())); this.server.onDidCloseConnection(() => toDisconnect.dispose()); // process empty plugins as well in order to properly remove stale plugin widgets @@ -211,6 +226,7 @@ export class HostedPluginSupport { return; } await this.startPlugins(contributionsByHost, toDisconnect); + this.restoreWebviews(); } /** @@ -562,6 +578,66 @@ export class HostedPluginSupport { console.log(`[${this.clientId}] ${prefix} of ${pluginCount} took: ${measurement()} ms`); } + protected readonly webviewsToRestore = new Set(); + protected readonly webviewRevivers = new Map Promise>(); + + registerWebviewReviver(viewType: string, reviver: (webview: WebviewWidget) => Promise): void { + if (this.webviewRevivers.has(viewType)) { + throw new Error(`Reviver for ${viewType} already registered`); + } + this.webviewRevivers.set(viewType, reviver); + } + + unregisterWebviewReviver(viewType: string): void { + this.webviewRevivers.delete(viewType); + } + + protected preserveWebviews(): void { + for (const webview of this.widgets.getWidgets(WebviewWidget.FACTORY_ID)) { + this.preserveWebview(webview as WebviewWidget); + } + } + + protected preserveWebview(webview: WebviewWidget): void { + if (!this.webviewsToRestore.has(webview)) { + this.webviewsToRestore.add(webview); + webview.disposed.connect(() => this.webviewsToRestore.delete(webview)); + } + } + + protected restoreWebviews(): void { + for (const webview of this.webviewsToRestore) { + this.restoreWebview(webview); + } + this.webviewsToRestore.clear(); + } + + protected async restoreWebview(webview: WebviewWidget): Promise { + await this.activateByEvent(`onWebviewPanel:${webview.viewType}`); + const restore = this.webviewRevivers.get(webview.viewType); + if (!restore) { + webview.setHTML(this.getDeserializationFailedContents(webview.viewType)); + return; + } + try { + await restore(webview); + } catch (e) { + webview.setHTML(this.getDeserializationFailedContents(webview.viewType)); + console.error('Failed to restore the webview', e); + } + } + + protected getDeserializationFailedContents(viewType: string): string { + return ` + + + + + + An error occurred while restoring view:${viewType} + `; + } + } export class PluginContributions extends DisposableCollection { diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index ada914b003e93..241656e03c17d 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -16,7 +16,7 @@ import { injectable, inject, postConstruct } from 'inversify'; import { ArrayExt } from '@phosphor/algorithm/lib/array'; -import { WebviewPanelOptions, WebviewPortMapping, Uri } from '@theia/plugin'; +import { WebviewPanelOptions, WebviewPortMapping } from '@theia/plugin'; import { BaseWidget, Message } from '@theia/core/lib/browser/widgets/widget'; import { Disposable } from '@theia/core/lib/common/disposable'; // TODO: get rid of dependencies to the mini browser @@ -32,13 +32,15 @@ import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; // tslint:disable:no-any export const enum WebviewMessageChannels { + doUpdateState = 'do-update-state', + doReload = 'do-reload', loadResource = 'load-resource', webviewReady = 'webview-ready' } export interface WebviewContentOptions { readonly allowScripts?: boolean; - readonly localResourceRoots?: ReadonlyArray; + readonly localResourceRoots?: ReadonlyArray; readonly portMapping?: ReadonlyArray; readonly enableCommandUris?: boolean; } @@ -132,6 +134,10 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { this.ready.resolve(); }); this.toDispose.push(subscription); + this.toDispose.push(this.on(WebviewMessageChannels.doUpdateState, (state: any) => { + this.state = state; + })); + this.toDispose.push(this.on(WebviewMessageChannels.doReload, () => this.reload())); this.toDispose.push(this.on(WebviewMessageChannels.loadResource, (entry: any) => { const rawPath = entry.path; const normalizedPath = decodeURIComponent(rawPath); @@ -303,7 +309,6 @@ export namespace WebviewWidget { viewType: string title: string options: WebviewPanelOptions - // TODO serialize/revive URIs contentOptions: WebviewContentOptions state: any // TODO: preserve icon class @@ -311,7 +316,7 @@ export namespace WebviewWidget { export function compareWebviewContentOptions(a: WebviewContentOptions, b: WebviewContentOptions): boolean { return a.enableCommandUris === b.enableCommandUris && a.allowScripts === b.allowScripts && - ArrayExt.shallowEqual(a.localResourceRoots || [], b.localResourceRoots || [], (uri, uri2) => uri.toString() === uri2.toString()) && + ArrayExt.shallowEqual(a.localResourceRoots || [], b.localResourceRoots || [], (uri, uri2) => uri === uri2) && ArrayExt.shallowEqual(a.portMapping || [], b.portMapping || [], (m, m2) => m.extensionHostPort === m2.extensionHostPort && m.webviewPort === m2.webviewPort ); diff --git a/packages/plugin-ext/src/main/browser/webviews-main.ts b/packages/plugin-ext/src/main/browser/webviews-main.ts index 37bf98fe17fd3..d810db6c747b1 100644 --- a/packages/plugin-ext/src/main/browser/webviews-main.ts +++ b/packages/plugin-ext/src/main/browser/webviews-main.ts @@ -27,16 +27,16 @@ import { ThemeRulesService } from './webview/theme-rules-service'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { ViewColumnService } from './view-column-service'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; -import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; import { JSONExt } from '@phosphor/coreutils/lib/json'; import { Mutable } from '@theia/core/lib/common/types'; +import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; export class WebviewsMainImpl implements WebviewsMain, Disposable { - private readonly revivers = new Set(); private readonly proxy: WebviewsExt; protected readonly shell: ApplicationShell; protected readonly widgets: WidgetManager; + protected readonly pluginService: HostedPluginSupport; protected readonly viewColumnService: ViewColumnService; protected readonly keybindingRegistry: KeybindingRegistry; protected readonly themeService = ThemeService.get(); @@ -49,20 +49,10 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { this.keybindingRegistry = container.get(KeybindingRegistry); this.viewColumnService = container.get(ViewColumnService); this.widgets = container.get(WidgetManager); - const pluginService = container.get(HostedPluginSupport); + this.pluginService = container.get(HostedPluginSupport); this.toDispose.push(this.shell.onDidChangeActiveWidget(() => this.updateViewStates())); this.toDispose.push(this.shell.onDidChangeCurrentWidget(() => this.updateViewStates())); this.toDispose.push(this.viewColumnService.onViewColumnChanged(() => this.updateViewStates())); - this.toDispose.push(this.widgets.onDidCreateWidget(({ factoryId, widget }) => { - if (factoryId === WebviewWidget.FACTORY_ID && widget instanceof WebviewWidget) { - const restoreState = widget.restoreState.bind(widget); - widget.restoreState = async oldState => { - restoreState(oldState); - await pluginService.activateByEvent(`onWebviewPanel:${widget.viewType}`); - this.restoreWidget(widget); - }; - } - })); } dispose(): void { @@ -80,9 +70,13 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { this.hookWebview(view); view.viewType = viewType; view.title.label = title; - const { enableFindWidget, retainContextWhenHidden, enableScripts, ...contentOptions } = options; + const { enableFindWidget, retainContextWhenHidden, enableScripts, localResourceRoots, ...contentOptions } = options; view.options = { enableFindWidget, retainContextWhenHidden }; - view.setContentOptions({ allowScripts: enableScripts, ...contentOptions }); + view.setContentOptions({ + allowScripts: enableScripts, + localResourceRoots: localResourceRoots && localResourceRoots.map(root => root.toString()), + ...contentOptions + }); this.addOrReattachWidget(panelId, showOptions); } @@ -90,7 +84,9 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { const handle = view.identifier.id; const toDisposeOnClose = new DisposableCollection(); const toDisposeOnLoad = new DisposableCollection(); + view.eventDelegate = { + // TODO review callbacks onMessage: m => { this.proxy.$onMessage(handle, m); }, @@ -121,12 +117,14 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { })); } }; + this.toDispose.push(Disposable.create(() => view.eventDelegate = {})); + view.disposed.connect(() => { toDisposeOnClose.dispose(); - this.proxy.$onDidDisposeWebviewPanel(handle); + if (!this.toDispose.disposed) { + this.proxy.$onDidDisposeWebviewPanel(handle); + } }); - - this.toDispose.push(view); toDisposeOnClose.push(Disposable.create(() => this.themeRulesService.setIconPath(handle, undefined))); } @@ -191,11 +189,7 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { this.addOrReattachWidget(widget.identifier.id, showOptions); return; } - } else if (!widget.options.retainContextWhenHidden) { - // reload content when revealing - widget.reload(); } - if (showOptions.preserveFocus) { this.shell.revealWidget(widget.id); } else { @@ -221,8 +215,12 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { async $setOptions(handle: string, options: WebviewOptions): Promise { const webview = await this.getWebview(handle); - const { enableScripts, ...contentOptions } = options; - webview.setContentOptions({ allowScripts: enableScripts, ...contentOptions }); + const { enableScripts, localResourceRoots, ...contentOptions } = options; + webview.setContentOptions({ + allowScripts: enableScripts, + localResourceRoots: localResourceRoots && localResourceRoots.map(root => root.toString()), + ...contentOptions + }); } // tslint:disable-next-line:no-any @@ -233,47 +231,23 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { } $registerSerializer(viewType: string): void { - if (this.revivers.has(viewType)) { - throw new Error(`Reviver for ${viewType} already registered`); - } - this.revivers.add(viewType); + this.pluginService.registerWebviewReviver(viewType, widget => this.restoreWidget(widget)); this.toDispose.push(Disposable.create(() => this.$unregisterSerializer(viewType))); } $unregisterSerializer(viewType: string): void { - this.revivers.delete(viewType); + this.pluginService.unregisterWebviewReviver(viewType); } protected async restoreWidget(widget: WebviewWidget): Promise { - const viewType = widget.viewType; - if (!this.revivers.has(viewType)) { - widget.setHTML(this.getDeserializationFailedContents(viewType)); - return; - } - try { - this.hookWebview(widget); - const handle = widget.identifier.id; - const title = widget.title.label; - const state = widget.state; - const options = widget.options; - this.viewColumnService.updateViewColumns(); - const position = this.viewColumnService.getViewColumn(widget.id) || 0; - await this.proxy.$deserializeWebviewPanel(handle, viewType, title, state, position, options); - } catch (e) { - widget.setHTML(this.getDeserializationFailedContents(viewType)); - console.error('Failed to restore the webview', e); - } - } - - protected getDeserializationFailedContents(viewType: string): string { - return ` - - - - - - An error occurred while restoring view:${viewType} - `; + this.hookWebview(widget); + const handle = widget.identifier.id; + const title = widget.title.label; + const state = widget.state; + const options = widget.options; + this.viewColumnService.updateViewColumns(); + const position = this.viewColumnService.getViewColumn(widget.id) || 0; + await this.proxy.$deserializeWebviewPanel(handle, widget.viewType, title, state, position, options); } protected readonly updateViewStates = debounce(() => {