diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts index ea77f0317f461..e019af079030f 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts @@ -60,6 +60,14 @@ export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIF } }); + // override postMessage method to replace vscode-resource: + const originalPostMessage = panel.webview.postMessage; + panel.webview.postMessage = (message: any): PromiseLike => { + const decoded = JSON.stringify(message); + const newMessage = decoded.replace(new RegExp('vscode-resource:/', 'g'), '/webview/'); + return originalPostMessage.call(panel.webview, JSON.parse(newMessage)); + }; + return panel; }; diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index 8610fe00507f7..6aa0ed3775811 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -35,6 +35,8 @@ export class WebviewWidget extends BaseWidget { private iframe: HTMLIFrameElement; private state: { [key: string]: any } | undefined = undefined; private loadTimeout: number | undefined; + private scrollY: number; + private readyToReceiveMessage: boolean = false; constructor(title: string, private options: WebviewWidgetOptions, private eventDelegate: WebviewEvents) { super(); @@ -43,6 +45,7 @@ export class WebviewWidget extends BaseWidget { this.title.closable = true; this.title.label = title; this.addClass(WebviewWidget.Styles.WEBVIEW); + this.scrollY = 0; } protected handleMessage(message: any) { @@ -55,7 +58,9 @@ export class WebviewWidget extends BaseWidget { } } - postMessage(message: any) { + async postMessage(message: any): Promise { + // wait message can be delivered + await this.waitReadyToReceiveMessage(); this.iframe.contentWindow!.postMessage(message, '*'); } @@ -82,6 +87,9 @@ export class WebviewWidget extends BaseWidget { a.title = a.href; } }); + (window as any)[`postMessageExt${this.id}`] = (e: any) => { + this.handleMessage(e); + }; this.updateApiScript(newDocument); const previousPendingFrame = this.iframe; if (previousPendingFrame) { @@ -97,6 +105,9 @@ export class WebviewWidget extends BaseWidget { newFrame.contentDocument!.open('text/html', 'replace'); const onLoad = (contentDocument: any, contentWindow: any) => { + if (newFrame && newFrame.contentDocument === contentDocument) { + newFrame.style.visibility = 'visible'; + } if (contentDocument.body) { if (this.eventDelegate && this.eventDelegate.onKeyboardEvent) { const eventNames = ['keydown', 'keypress', 'click']; @@ -110,12 +121,6 @@ export class WebviewWidget extends BaseWidget { this.eventDelegate.onLoad(contentDocument); } } - if (newFrame && newFrame.contentDocument === contentDocument) { - (contentWindow).postMessageExt = (e: any) => { - this.handleMessage(e); - }; - newFrame.style.visibility = 'visible'; - } }; clearTimeout(this.loadTimeout); @@ -141,10 +146,29 @@ export class WebviewWidget extends BaseWidget { protected onActivateRequest(msg: Message): void { super.onActivateRequest(msg); + // restore scrolling if there was one + if (this.scrollY > 0) { + this.iframe.contentWindow!.scrollTo({ top: this.scrollY }); + } this.node.focus(); + // unblock messages + this.readyToReceiveMessage = true; + } + + // block messages + protected onBeforeShow(msg: Message): void { + this.readyToReceiveMessage = false; } - private reloadFrame() { + protected onBeforeHide(msg: Message): void { + // persist scrolling + if (this.iframe.contentWindow) { + this.scrollY = this.iframe.contentWindow.scrollY; + } + super.onBeforeHide(msg); + } + + public reloadFrame() { if (!this.iframe || !this.iframe.contentDocument || !this.iframe.contentDocument.documentElement) { return; } @@ -177,7 +201,8 @@ export class WebviewWidget extends BaseWidget { const codeApiScript = contentDocument.createElement('script'); codeApiScript.id = scriptId; codeApiScript.textContent = ` - const acquireVsCodeApi = (function() { + window.postMessageExt = window.parent['postMessageExt${this.id}']; + const acquireVsCodeApi = (function() { let acquired = false; let state = ${this.state ? `JSON.parse(${JSON.stringify(this.state)})` : undefined}; return () => { @@ -234,6 +259,28 @@ export class WebviewWidget extends BaseWidget { parent.appendChild(codeApiScript); } } + + /** + * Check if given object is ready to receive message and if it is ready, resolve promise + */ + waitReceiveMessage(object: WebviewWidget, resolve: any) { + if (object.readyToReceiveMessage) { + resolve(true); + } + } + + /** + * Block until we're able to receive message + */ + public async waitReadyToReceiveMessage(): Promise { + if (this.readyToReceiveMessage) { + return true; + } + return new Promise((resolve, reject) => { + setTimeout(this.waitReceiveMessage, 100, this, resolve); + }); + } + } export namespace WebviewWidget { diff --git a/packages/plugin-ext/src/main/browser/webviews-main.ts b/packages/plugin-ext/src/main/browser/webviews-main.ts index d613d1a3382a0..b6424adf4424c 100644 --- a/packages/plugin-ext/src/main/browser/webviews-main.ts +++ b/packages/plugin-ext/src/main/browser/webviews-main.ts @@ -40,7 +40,13 @@ export class WebviewsMainImpl implements WebviewsMain { protected readonly updateViewOptions: () => void; private readonly views = new Map(); - private readonly viewsOptions = new Map(); + private readonly viewsOptions = new Map(); constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WEBVIEWS_EXT); @@ -102,7 +108,7 @@ export class WebviewsMainImpl implements WebviewsMain { }); this.views.set(panelId, view); - this.viewsOptions.set(view.id, { panelOptions: showOptions, panelId, visible: false, active: false }); + this.viewsOptions.set(view.id, { panelOptions: showOptions, options: options, panelId, visible: false, active: false }); this.addOrReattachWidget(panelId, showOptions); } private addOrReattachWidget(handler: string, showOptions: WebviewPanelShowOptions) { @@ -165,9 +171,13 @@ export class WebviewsMainImpl implements WebviewsMain { if (view.isDisposed) { return; } - if (showOptions.viewColumn !== undefined || showOptions.area !== undefined) { + const options = this.viewsOptions.get(view.id); + let retain = false; + if (options && options.options && options.options.retainContextWhenHidden) { + retain = options.options.retainContextWhenHidden; + } + if ((showOptions.viewColumn !== undefined && showOptions.viewColumn !== options!.panelOptions.viewColumn) || showOptions.area !== undefined) { this.viewColumnService.updateViewColumns(); - const options = this.viewsOptions.get(view.id); if (!options) { return; } @@ -179,7 +189,11 @@ export class WebviewsMainImpl implements WebviewsMain { this.updateViewOptions(); return; } + } else if (!retain) { + // reload content when revealing + view.reloadFrame(); } + if (showOptions.preserveFocus) { this.shell.revealWidget(view.id); } else { diff --git a/packages/plugin-ext/src/plugin/webviews.ts b/packages/plugin-ext/src/plugin/webviews.ts index f45094733c6bb..c9e707c41237b 100644 --- a/packages/plugin-ext/src/plugin/webviews.ts +++ b/packages/plugin-ext/src/plugin/webviews.ts @@ -148,7 +148,30 @@ export class WebviewImpl implements theia.Webview { // tslint:disable-next-line:no-any postMessage(message: any): PromiseLike { this.checkIsDisposed(); - return this.proxy.$postMessage(this.viewId, message); + // replace theia-resource: content in the given message + const decoded = JSON.stringify(message); + let newMessage = decoded.replace(new RegExp('theia-resource:/', 'g'), '/webview/'); + if (this._options && this._options.localResourceRoots) { + newMessage = this.filterLocalRoots(newMessage, this._options.localResourceRoots); + } + return this.proxy.$postMessage(this.viewId, JSON.parse(newMessage)); + } + + protected filterLocalRoots(content: string, localResourceRoots: ReadonlyArray) { + const webViewsRegExp = /"(\/webview\/.*?)\"/g; + let m; + while ((m = webViewsRegExp.exec(content)) !== null) { + if (m.index === webViewsRegExp.lastIndex) { + webViewsRegExp.lastIndex++; + } + // take group 1 which is webview URL + const url = m[1]; + const isIncluded = localResourceRoots.some((uri): boolean => url.substring('/webview'.length).startsWith(uri.fsPath)); + if (!isIncluded) { + content = content.replace(url, url.replace('/webview', '/webview-disallowed-localroot')); + } + } + return content; } get options(): theia.WebviewOptions { @@ -168,7 +191,11 @@ export class WebviewImpl implements theia.Webview { } set html(html: string) { - const newHtml = html.replace(new RegExp('theia-resource:/', 'g'), '/webview/'); + let newHtml = html.replace(new RegExp('theia-resource:/', 'g'), '/webview/'); + if (this._options && this._options.localResourceRoots) { + newHtml = this.filterLocalRoots(newHtml, this._options.localResourceRoots); + } + this.checkIsDisposed(); if (this._html !== newHtml) { this._html = newHtml; @@ -205,6 +232,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { private readonly _webview: WebviewImpl ) { this._showOptions = typeof showOptions === 'object' ? showOptions : { viewColumn: showOptions as theia.ViewColumn }; + this.setViewColumn(undefined); } dispose() { @@ -267,7 +295,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { return this._showOptions.viewColumn; } - setViewColumn(value: theia.ViewColumn) { + setViewColumn(value: theia.ViewColumn | undefined) { this.checkIsDisposed(); this._showOptions.viewColumn = value; }