Skip to content

Commit d8fbc56

Browse files
committed
Restore variant analysis view on restart of VSCode
This implements persistence for the variant analysis webview, allowing the webview panel to be restored when VSCode is restarted. It's probably easier to add this now than to try to add it later. The basic idea is that there are no real differences when opening the webview for the first time. However, when VSCode is restarted it will use the `VariantAnalysisViewSerializer` to restore the webview panel. In our case this means recreating the `VariantAnalysisView`. To fully test this, I've also added a mock variant analysis ID as the state of the webview. This value is now randomly generated when calling the `codeQL.mockVariantAnalysisView` command. This allows us to test opening multiple webviews and that the webviews are restored with the correct state. See: https://code.visualstudio.com/api/extension-guides/webview#persistence
1 parent cf3ba32 commit d8fbc56

File tree

10 files changed

+521
-199
lines changed

10 files changed

+521
-199
lines changed

extensions/ql-vscode/.storybook/preview.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ export const parameters = {
3333
};
3434

3535
(window as any).acquireVsCodeApi = () => ({
36-
postMessage: action('post-vscode-message')
36+
postMessage: action('post-vscode-message'),
37+
setState: action('set-vscode-state'),
3738
});

extensions/ql-vscode/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"onCommand:codeQL.quickQuery",
6464
"onCommand:codeQL.restartQueryServer",
6565
"onWebviewPanel:resultsView",
66+
"onWebviewPanel:codeQL.variantAnalysis",
6667
"onFileSystem:codeql-zip-archive"
6768
],
6869
"main": "./out/extension",

extensions/ql-vscode/src/abstract-webview.ts

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export abstract class AbstractWebview<ToMessage extends WebviewMessage, FromMess
3333
super();
3434
}
3535

36+
public async restoreView(panel: WebviewPanel): Promise<void> {
37+
this.panel = panel;
38+
this.setupPanel(panel);
39+
}
40+
3641
protected get isShowingPanel() {
3742
return !!this.panel;
3843
}
@@ -59,37 +64,43 @@ export abstract class AbstractWebview<ToMessage extends WebviewMessage, FromMess
5964
],
6065
}
6166
);
62-
this.push(
63-
this.panel.onDidDispose(
64-
() => {
65-
this.panel = undefined;
66-
this.panelLoaded = false;
67-
this.onPanelDispose();
68-
},
69-
null,
70-
ctx.subscriptions
71-
)
72-
);
73-
74-
this.panel.webview.html = getHtmlForWebview(
75-
ctx,
76-
this.panel.webview,
77-
config.view,
78-
{
79-
allowInlineStyles: true,
80-
}
81-
);
82-
this.push(
83-
this.panel.webview.onDidReceiveMessage(
84-
async (e) => this.onMessage(e),
85-
undefined,
86-
ctx.subscriptions
87-
)
88-
);
67+
this.setupPanel(this.panel);
8968
}
9069
return this.panel;
9170
}
9271

72+
protected setupPanel(panel: WebviewPanel): void {
73+
const config = this.getPanelConfig();
74+
75+
this.push(
76+
panel.onDidDispose(
77+
() => {
78+
this.panel = undefined;
79+
this.panelLoaded = false;
80+
this.onPanelDispose();
81+
},
82+
null,
83+
this.ctx.subscriptions
84+
)
85+
);
86+
87+
panel.webview.html = getHtmlForWebview(
88+
this.ctx,
89+
panel.webview,
90+
config.view,
91+
{
92+
allowInlineStyles: true,
93+
}
94+
);
95+
this.push(
96+
panel.webview.onDidReceiveMessage(
97+
async (e) => this.onMessage(e),
98+
undefined,
99+
this.ctx.subscriptions
100+
)
101+
);
102+
}
103+
93104
protected abstract getPanelConfig(): WebviewPanelConfig;
94105

95106
protected abstract onPanelDispose(): void;

extensions/ql-vscode/src/extension.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import { createInitialQueryInfo } from './run-queries-shared';
105105
import { LegacyQueryRunner } from './legacy-query-server/legacyRunner';
106106
import { QueryRunner } from './queryRunner';
107107
import { VariantAnalysisView } from './remote-queries/variant-analysis-view';
108+
import { VariantAnalysisViewSerializer } from './remote-queries/variant-analysis-view-serializer';
108109

109110
/**
110111
* extension.ts
@@ -381,14 +382,21 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
381382
allowAutoUpdating: true
382383
})));
383384

384-
return await installOrUpdateThenTryActivate({
385+
const variantAnalysisViewSerializer = new VariantAnalysisViewSerializer(ctx);
386+
Window.registerWebviewPanelSerializer(VariantAnalysisView.viewType, variantAnalysisViewSerializer);
387+
388+
const codeQlExtension = await installOrUpdateThenTryActivate({
385389
isUserInitiated: !!ctx.globalState.get(shouldUpdateOnNextActivationKey),
386390
shouldDisplayMessageWhenNoUpdates: false,
387391

388392
// only auto update on startup if the user has previously requested an update
389393
// otherwise, ask user to accept the update
390394
allowAutoUpdating: !!ctx.globalState.get(shouldUpdateOnNextActivationKey)
391395
});
396+
397+
variantAnalysisViewSerializer.onExtensionLoaded();
398+
399+
return codeQlExtension;
392400
}
393401

394402
async function activateWithInstalledDistribution(
@@ -909,8 +917,11 @@ async function activateWithInstalledDistribution(
909917

910918
ctx.subscriptions.push(
911919
commandRunner('codeQL.mockVariantAnalysisView', async () => {
912-
const variantAnalysisView = new VariantAnalysisView(ctx);
913-
variantAnalysisView.openView();
920+
// Generate a random variant analysis ID for testing
921+
const variantAnalysisId: number = Math.floor(Math.random() * 1000000);
922+
923+
const variantAnalysisView = new VariantAnalysisView(ctx, variantAnalysisId);
924+
void variantAnalysisView.openView();
914925
})
915926
);
916927

extensions/ql-vscode/src/pure/interface-types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as sarif from 'sarif';
22
import { AnalysisResults } from '../remote-queries/shared/analysis-result';
33
import { AnalysisSummary, RemoteQueryResult } from '../remote-queries/shared/remote-query-result';
44
import { RawResultSet, ResultRow, ResultSetSchema, Column, ResolvableLocationValue } from './bqrs-cli-types';
5+
import { VariantAnalysis } from '../remote-queries/shared/variant-analysis';
56

67
/**
78
* This module contains types and code that are shared between
@@ -429,3 +430,24 @@ export interface CopyRepoListMessage {
429430
t: 'copyRepoList';
430431
queryId: string;
431432
}
433+
434+
export interface SetVariantAnalysisMessage {
435+
t: 'setVariantAnalysis';
436+
variantAnalysis: VariantAnalysis;
437+
}
438+
439+
export type ToVariantAnalysisMessage =
440+
| SetVariantAnalysisMessage;
441+
442+
export type StopVariantAnalysisMessage = {
443+
t: 'stopVariantAnalysis';
444+
variantAnalysisId: number;
445+
}
446+
447+
export type FromVariantAnalysisMessage =
448+
| ViewLoadedMsg
449+
| StopVariantAnalysisMessage;
450+
451+
export type VariantAnalysisState = {
452+
variantAnalysisId: number;
453+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ExtensionContext, WebviewPanel, WebviewPanelSerializer } from 'vscode';
2+
import { VariantAnalysisView } from './variant-analysis-view';
3+
import { VariantAnalysisState } from '../pure/interface-types';
4+
5+
export class VariantAnalysisViewSerializer implements WebviewPanelSerializer {
6+
private extensionLoaded = false;
7+
private readonly resolvePromises: (() => void)[] = [];
8+
9+
public constructor(
10+
private readonly ctx: ExtensionContext
11+
) { }
12+
13+
onExtensionLoaded(): void {
14+
this.extensionLoaded = true;
15+
this.resolvePromises.forEach((resolve) => resolve());
16+
}
17+
18+
async deserializeWebviewPanel(webviewPanel: WebviewPanel, state: unknown): Promise<void> {
19+
if (!state || typeof state !== 'object') {
20+
return;
21+
}
22+
23+
if (!('variantAnalysisId' in state)) {
24+
return;
25+
}
26+
27+
const variantAnalysisState: VariantAnalysisState = state as VariantAnalysisState;
28+
29+
await this.waitForExtensionFullyLoaded();
30+
31+
const view = new VariantAnalysisView(this.ctx, variantAnalysisState.variantAnalysisId);
32+
await view.restoreView(webviewPanel);
33+
}
34+
35+
private waitForExtensionFullyLoaded(): Promise<void> {
36+
if (this.extensionLoaded) {
37+
return Promise.resolve();
38+
}
39+
40+
return new Promise((resolve) => {
41+
this.resolvePromises.push(resolve);
42+
});
43+
}
44+
}

0 commit comments

Comments
 (0)