Skip to content

Commit

Permalink
Fix #4267 by avoiding to define postMessage function through a timeou…
Browse files Browse the repository at this point in the history
…t (now function is defined earlier)

Also:
- keep scrolling of webview when returning back to the webview
- handling keep state when moving webviews
- fix initial webview column
- handle theia-resource and vscode-resource in postMessage content
- handle localRoots
- handle retainContextWhenHidden
- do not let enter messages until the webview is not ready to receive them
- ...

Change-Id: Ie35c911c0c62ebf8260c2449299e7fe91433255b
Signed-off-by: Florent Benoit <fbenoit@redhat.com>
  • Loading branch information
benoitf committed Apr 29, 2019
1 parent 3f30361 commit 61cf13d
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 16 deletions.
8 changes: 8 additions & 0 deletions packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> => {
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;
};

Expand Down
65 changes: 56 additions & 9 deletions packages/plugin-ext/src/main/browser/webview/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -55,7 +58,9 @@ export class WebviewWidget extends BaseWidget {
}
}

postMessage(message: any) {
async postMessage(message: any): Promise<void> {
// wait message can be delivered
await this.waitReadyToReceiveMessage();
this.iframe.contentWindow!.postMessage(message, '*');
}

Expand All @@ -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) {
Expand All @@ -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'];
Expand All @@ -110,12 +121,6 @@ export class WebviewWidget extends BaseWidget {
this.eventDelegate.onLoad(<Document>contentDocument);
}
}
if (newFrame && newFrame.contentDocument === contentDocument) {
(<any>contentWindow).postMessageExt = (e: any) => {
this.handleMessage(e);
};
newFrame.style.visibility = 'visible';
}
};

clearTimeout(this.loadTimeout);
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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<boolean> {
if (this.readyToReceiveMessage) {
return true;
}
return new Promise<boolean>((resolve, reject) => {
setTimeout(this.waitReceiveMessage, 100, this, resolve);
});
}

}

export namespace WebviewWidget {
Expand Down
22 changes: 18 additions & 4 deletions packages/plugin-ext/src/main/browser/webviews-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ export class WebviewsMainImpl implements WebviewsMain {
protected readonly updateViewOptions: () => void;

private readonly views = new Map<string, WebviewWidget>();
private readonly viewsOptions = new Map<string, { panelOptions: WebviewPanelShowOptions; panelId: string; active: boolean; visible: boolean; }>();
private readonly viewsOptions = new Map<string, {
panelOptions: WebviewPanelShowOptions;
options: (WebviewPanelOptions & WebviewOptions) | undefined;
panelId: string;
active: boolean;
visible: boolean;
}>();

constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WEBVIEWS_EXT);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 {
Expand Down
34 changes: 31 additions & 3 deletions packages/plugin-ext/src/plugin/webviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,30 @@ export class WebviewImpl implements theia.Webview {
// tslint:disable-next-line:no-any
postMessage(message: any): PromiseLike<boolean> {
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<theia.Uri>) {
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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit 61cf13d

Please sign in to comment.