You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

233 lines
6.1 KiB

'use strict';
import { TextDecoder } from 'util';
import {
commands,
ConfigurationChangeEvent,
ConfigurationTarget,
Disposable,
Uri,
ViewColumn,
Webview,
WebviewPanel,
WebviewPanelOnDidChangeViewStateEvent,
window,
workspace,
} from 'vscode';
import { configuration } from '../configuration';
import { Container } from '../container';
import { Logger } from '../logger';
import {
DidChangeConfigurationNotificationType,
IpcMessage,
IpcNotificationParamsOf,
IpcNotificationType,
onIpcCommand,
UpdateConfigurationCommandType,
} from './protocol';
import { Commands } from '../commands';
let ipcSequence = 0;
function nextIpcId() {
if (ipcSequence === Number.MAX_SAFE_INTEGER) {
ipcSequence = 1;
} else {
ipcSequence++;
}
return `host:${ipcSequence}`;
}
const emptyCommands: Disposable[] = [
{
dispose: function () {
/* noop */
},
},
];
export abstract class WebviewBase implements Disposable {
protected disposable: Disposable;
private _disposablePanel: Disposable | undefined;
private _panel: WebviewPanel | undefined;
constructor(showCommand: Commands, private readonly _column?: ViewColumn) {
this.disposable = Disposable.from(
configuration.onDidChange(this.onConfigurationChanged, this),
commands.registerCommand(showCommand, this.onShowCommand, this),
);
}
abstract get filename(): string;
abstract get id(): string;
abstract get title(): string;
registerCommands(): Disposable[] {
return emptyCommands;
}
renderHead?(): string | Promise<string>;
renderBody?(): string | Promise<string>;
renderEndOfBody?(): string | Promise<string>;
dispose() {
this.disposable.dispose();
this._disposablePanel?.dispose();
}
protected onShowCommand() {
void this.show(this._column);
}
private onConfigurationChanged(_e: ConfigurationChangeEvent) {
void this.notifyDidChangeConfiguration();
}
private onPanelDisposed() {
this._disposablePanel?.dispose();
this._panel = undefined;
}
private onViewStateChanged(e: WebviewPanelOnDidChangeViewStateEvent) {
Logger.log(
`Webview(${this.id}).onViewStateChanged`,
`active=${e.webviewPanel.active}, visible=${e.webviewPanel.visible}`,
);
// Anytime the webview becomes active, make sure it has the most up-to-date config
if (e.webviewPanel.active) {
void this.notifyDidChangeConfiguration();
}
}
protected onMessageReceived(e: IpcMessage) {
switch (e.method) {
case UpdateConfigurationCommandType.method:
onIpcCommand(UpdateConfigurationCommandType, e, async params => {
const target =
params.scope === 'workspace' ? ConfigurationTarget.Workspace : ConfigurationTarget.Global;
for (const key in params.changes) {
const inspect = configuration.inspect(key as any)!;
let value = params.changes[key];
if (value != null) {
if (params.scope === 'workspace') {
if (value === inspect.workspaceValue) continue;
} else {
if (value === inspect.globalValue && value !== inspect.defaultValue) continue;
if (value === inspect.defaultValue) {
value = undefined;
}
}
}
void (await configuration.update(key as any, value, target));
}
for (const key of params.removes) {
void (await configuration.update(key as any, undefined, target));
}
});
break;
default:
break;
}
}
private onMessageReceivedCore(e: IpcMessage) {
if (e == null) return;
Logger.log(`Webview(${this.id}).onMessageReceived: method=${e.method}, data=${JSON.stringify(e)}`);
this.onMessageReceived(e);
}
get visible() {
return this._panel?.visible ?? false;
}
hide() {
this._panel?.dispose();
}
setTitle(title: string) {
if (this._panel == null) return;
this._panel.title = title;
}
async show(column: ViewColumn = ViewColumn.Beside): Promise<void> {
if (this._panel == null) {
this._panel = window.createWebviewPanel(
this.id,
this.title,
{ viewColumn: column, preserveFocus: false },
{
retainContextWhenHidden: true,
enableFindWidget: true,
enableCommandUris: true,
enableScripts: true,
},
);
this._panel.iconPath = Uri.file(Container.context.asAbsolutePath('images/gitlens-icon.png'));
this._disposablePanel = Disposable.from(
this._panel,
this._panel.onDidDispose(this.onPanelDisposed, this),
this._panel.onDidChangeViewState(this.onViewStateChanged, this),
this._panel.webview.onDidReceiveMessage(this.onMessageReceivedCore, this),
...this.registerCommands(),
);
this._panel.webview.html = await this.getHtml(this._panel.webview);
} else {
const html = await this.getHtml(this._panel.webview);
// Reset the html to get the webview to reload
this._panel.webview.html = '';
this._panel.webview.html = html;
this._panel.reveal(this._panel.viewColumn ?? ViewColumn.Active, false);
}
}
private async getHtml(webview: Webview): Promise<string> {
const uri = Uri.joinPath(Container.context.extensionUri, 'dist', 'webviews', this.filename);
const content = new TextDecoder('utf8').decode(await workspace.fs.readFile(uri));
let html = content
.replace(/#{cspSource}/g, webview.cspSource)
.replace(/#{root}/g, webview.asWebviewUri(Container.context.extensionUri).toString());
if (this.renderHead != null) {
html = html.replace(/#{head}/i, await this.renderHead());
}
if (this.renderBody != null) {
html = html.replace(/#{body}/i, await this.renderBody());
}
if (this.renderEndOfBody != null) {
html = html.replace(/#{endOfBody}/i, await this.renderEndOfBody());
}
return html;
}
protected notify<NT extends IpcNotificationType>(type: NT, params: IpcNotificationParamsOf<NT>): Thenable<boolean> {
return this.postMessage({ id: nextIpcId(), method: type.method, params: params });
}
private notifyDidChangeConfiguration() {
// Make sure to get the raw config, not from the container which has the modes mixed in
return this.notify(DidChangeConfigurationNotificationType, { config: configuration.get() });
}
private postMessage(message: IpcMessage) {
if (this._panel == null) return Promise.resolve(false);
return this._panel.webview.postMessage(message);
}
}