Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@
"onDebug",
"onDebugDynamicConfigurations:brightscript",
"onTaskType:brightscript",
"onWebviewPanel:rceVideoView"
"onWebviewPanel:rceVideoView",
"onWebviewPanel:sceneGraphInspectorView"
],
"contributes": {
"taskDefinitions": [
Expand Down
1 change: 1 addition & 0 deletions src/mockVscode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export let vscode = {
},
window: {
registerCustomEditorProvider: () => { },
registerWebviewPanelSerializer: () => { },
withProgress: (options, action) => {
return action();
},
Expand Down
92 changes: 77 additions & 15 deletions src/viewProviders/BaseWebviewViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,21 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi
}

protected postMessage(message) {
this.view?.webview.postMessage(message).then(null, (reason) => {
console.log('postMessage failed: ', reason);
});
// resolve `.webview` lazily inside the guard: accessing it on a DISPOSED view or
// panel throws synchronously ("Webview is disposed") — e.g. after the pop-out
// editor is closed, or while the sidebar view is hidden behind the panel.
this.tryPostMessage(() => this.view?.webview, message);
this.tryPostMessage(() => this.panel?.webview, message);
}

this.panel?.webview.postMessage(message).then(null, (reason) => {
private tryPostMessage(resolveWebview: () => vscode.Webview | undefined, message) {
let webview: vscode.Webview | undefined;
try {
webview = resolveWebview();
} catch {
return; // the view/panel was disposed
}
webview?.postMessage(message).then(null, (reason) => {
console.log('postMessage failed: ', reason);
});
}
Expand Down Expand Up @@ -207,7 +217,7 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi
return [];
}

protected async getHtmlForWebview() {
protected async getHtmlForWebview(webviewContext: 'sidebar' | 'panel' = 'sidebar') {
try {
let watcher;
try {
Expand All @@ -224,22 +234,30 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi
if (
events.find(x => (x.type === 'create' || x.type === 'update') && x.path?.toLowerCase()?.endsWith('index.html'))
) {
this.view.webview.html = '';
this.view.webview.html = this.getIndexHtml();
const webview = (this.view ?? this.panel)?.webview;
if (webview) {
webview.html = '';
// the sidebar view takes precedence in (this.view ?? this.panel),
// so reload with the matching context
webview.html = this.getIndexHtml(this.view ? 'sidebar' : 'panel');
}
}
});
}
} catch (e) {
console.error(e);
}
return this.getIndexHtml();
return this.getIndexHtml(webviewContext);
}

private getIndexHtml() {
private getIndexHtml(webviewContext: 'sidebar' | 'panel' = 'sidebar') {
return buildWebviewIndexHtml({
webview: this.view?.webview ?? this.panel?.webview,
webviewBasePath: this.webviewBasePath,
viewName: this.id,
//lets a view persist/behave differently in the sidebar vs the popped-out
//editor panel (the same view component runs in both)
webviewContext: webviewContext,
additionalScriptContents: this.additionalScriptContents()
});
}
Expand Down Expand Up @@ -277,7 +295,7 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi
vscode.Uri.file(this.webviewBasePath)
]
};
webview.html = await this.getHtmlForWebview();
webview.html = await this.getHtmlForWebview('sidebar');
}

protected async createOrRevealWebviewPanel() {
Expand All @@ -297,24 +315,68 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi
}

if (createPanel) {
this.panel = vscode.window.createWebviewPanel(
const panel = vscode.window.createWebviewPanel(
this.id,
await this.getViewNameById(this.id),
vscode.ViewColumn.Active,
{
// Enable javascript in the webview
enableScripts: true,
retainContextWhenHidden: this.retainPanelContextWhenHidden,
localResourceRoots: [
vscode.Uri.file(this.webviewBasePath)
]
}
);
await this.attachPanel(panel);
}
}

/** Subclasses may set this to keep the panel's webview state alive while its tab
* is in the background (more memory, but no reset on every tab switch). */
protected retainPanelContextWhenHidden = false;

/** Adopt an editor panel (newly created, or restored after a window reload):
* wire messaging, (re-)assert webview options, and set the html. */
private async attachPanel(panel: vscode.WebviewPanel) {
this.panel = panel;
// when the pop-out editor is closed, drop the reference so postMessage (and the
// dev-watcher reload) stop targeting the disposed panel
panel.onDidDispose?.(() => {
if (this.panel === panel) {
this.panel = undefined;
}
});
this.setupViewMessageObserver(panel.webview);
panel.webview.options = {
enableScripts: true,
localResourceRoots: [
vscode.Uri.file(this.webviewBasePath)
]
};
panel.webview.html = await this.getHtmlForWebview('panel');
this.onPanelAttached(panel);
}

this.setupViewMessageObserver(this.panel.webview);
/** Called exactly once per editor panel, when it's created or restored after a
* window reload (not on reveal) — e.g. to track the panel's lifetime. */
protected onPanelAttached(panel: vscode.WebviewPanel) { }

const html = await this.getHtmlForWebview();
this.panel.webview.html = html;
}
/**
* Restore this provider's editor panel across window reloads — without a
* registered serializer VS Code destroys the panel (the tab is forgotten).
* Call from the subclass constructor (i.e. during activation) and pair it with
* an `onWebviewPanel:<id>` activation event in package.json so the extension
* wakes up to revive the panel.
*/
protected enablePanelRestore() {
this.extensionContext.subscriptions.push(
vscode.window.registerWebviewPanelSerializer(this.id, {
deserializeWebviewPanel: async (panel: vscode.WebviewPanel) => {
await this.attachPanel(panel);
}
})
);
}

private async getViewNameById(viewId) {
Expand Down
3 changes: 3 additions & 0 deletions src/viewProviders/SceneGraphInspectorViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ export class SceneGraphInspectorViewProvider extends BaseRdbViewProvider {
this.registerCommand(VscodeCommand.openSceneGraphInspectorInPanel, async () => {
await this.createOrRevealWebviewPanel();
});
// bring the panel back after a window reload (paired with the
// onWebviewPanel:sceneGraphInspectorView activation event)
this.enablePanelRestore();
}
}
3 changes: 3 additions & 0 deletions src/viewProviders/webviewHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function buildWebviewIndexHtml(options: BuildWebviewIndexHtmlOptions): st
//the data that will be replaced in the index.html
const data = {
viewName: options.viewName,
webviewContext: options.webviewContext ?? 'sidebar',
baseHref: `${baseUri}/`,
additionalScriptContents: (options.additionalScriptContents ?? []).join('\n ')
};
Expand Down Expand Up @@ -50,6 +51,8 @@ export interface BuildWebviewIndexHtmlOptions {
webviewBasePath: string;
/** The client-side view name the bundle should boot (a key in webviews/src/main.ts's views map) */
viewName: string;
/** Where the view is rendering, so it can persist/behave differently in the sidebar vs a popped-out editor panel */
webviewContext?: 'sidebar' | 'panel';
/** Extra script lines injected into the page */
additionalScriptContents?: string[];
}
3 changes: 3 additions & 0 deletions webviews/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script>
viewName = '{{viewName}}';
// 'sidebar' or 'panel' — the same view component runs in both; views that care
// (e.g. Solid Devtools, for per-context layout) read window.webviewContext
webviewContext = '{{webviewContext}}';
// This gets replaced with the actual contents in BaseWebviewViewProvider
//{{additionalScriptContents}}
</script>
Expand Down
Loading