/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { Disposable } from './dispose'; import { SizeStatusBarEntry } from './sizeStatusBarEntry'; import { Scale, ZoomStatusBarEntry } from './zoomStatusBarEntry'; import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; const localize = nls.loadMessageBundle(); export class PreviewManager implements vscode.CustomReadonlyEditorProvider { public static readonly viewType = 'imagePreview.previewEditor'; private readonly _previews = new Set(); private _activePreview: Preview | undefined; constructor( private readonly extensionRoot: vscode.Uri, private readonly sizeStatusBarEntry: SizeStatusBarEntry, private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry, private readonly zoomStatusBarEntry: ZoomStatusBarEntry, ) { } public async openCustomDocument(uri: vscode.Uri) { return { uri, dispose: () => { } }; } public async resolveCustomEditor( document: vscode.CustomDocument, webviewEditor: vscode.WebviewPanel, ): Promise { const preview = new Preview(this.extensionRoot, document.uri, webviewEditor, this.sizeStatusBarEntry, this.binarySizeStatusBarEntry, this.zoomStatusBarEntry); this._previews.add(preview); this.setActivePreview(preview); webviewEditor.onDidDispose(() => { this._previews.delete(preview); }); webviewEditor.onDidChangeViewState(() => { if (webviewEditor.active) { this.setActivePreview(preview); } else if (this._activePreview === preview && !webviewEditor.active) { this.setActivePreview(undefined); } }); } public get activePreview() { return this._activePreview; } private setActivePreview(value: Preview | undefined): void { this._activePreview = value; this.setPreviewActiveContext(!!value); } private setPreviewActiveContext(value: boolean) { vscode.commands.executeCommand('setContext', 'imagePreviewFocus', value); } } const enum PreviewState { Disposed, Visible, Active, } class Preview extends Disposable { private readonly id: string = `${Date.now()}-${Math.random().toString()}`; private _previewState = PreviewState.Visible; private _imageSize: string | undefined; private _imageBinarySize: number | undefined; private _imageZoom: Scale | undefined; private readonly emptyPngDataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg=='; constructor( private readonly extensionRoot: vscode.Uri, private readonly resource: vscode.Uri, private readonly webviewEditor: vscode.WebviewPanel, private readonly sizeStatusBarEntry: SizeStatusBarEntry, private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry, private readonly zoomStatusBarEntry: ZoomStatusBarEntry, ) { super(); const resourceRoot = resource.with({ path: resource.path.replace(/\/[^\/]+?\.\w+$/, '/'), }); webviewEditor.webview.options = { enableScripts: true, localResourceRoots: [ resourceRoot, extensionRoot, ] }; this._register(webviewEditor.webview.onDidReceiveMessage(message => { switch (message.type) { case 'size': { this._imageSize = message.value; this.update(); break; } case 'zoom': { this._imageZoom = message.value; this.update(); break; } case 'reopen-as-text': { vscode.commands.executeCommand('vscode.openWith', resource, 'default', webviewEditor.viewColumn); break; } } })); this._register(zoomStatusBarEntry.onDidChangeScale(e => { if (this._previewState === PreviewState.Active) { this.webviewEditor.webview.postMessage({ type: 'setScale', scale: e.scale }); } })); this._register(webviewEditor.onDidChangeViewState(() => { this.update(); this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active }); })); this._register(webviewEditor.onDidDispose(() => { if (this._previewState === PreviewState.Active) { this.sizeStatusBarEntry.hide(this.id); this.binarySizeStatusBarEntry.hide(this.id); this.zoomStatusBarEntry.hide(this.id); } this._previewState = PreviewState.Disposed; })); const watcher = this._register(vscode.workspace.createFileSystemWatcher(resource.fsPath)); this._register(watcher.onDidChange(e => { if (e.toString() === this.resource.toString()) { this.render(); } })); this._register(watcher.onDidDelete(e => { if (e.toString() === this.resource.toString()) { this.webviewEditor.dispose(); } })); vscode.workspace.fs.stat(resource).then(({ size }) => { this._imageBinarySize = size; this.update(); }); this.render(); this.update(); this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active }); } public zoomIn() { if (this._previewState === PreviewState.Active) { this.webviewEditor.webview.postMessage({ type: 'zoomIn' }); } } public zoomOut() { if (this._previewState === PreviewState.Active) { this.webviewEditor.webview.postMessage({ type: 'zoomOut' }); } } private async render() { if (this._previewState !== PreviewState.Disposed) { this.webviewEditor.webview.html = await this.getWebviewContents(); } } private update() { if (this._previewState === PreviewState.Disposed) { return; } if (this.webviewEditor.active) { this._previewState = PreviewState.Active; this.sizeStatusBarEntry.show(this.id, this._imageSize || ''); this.binarySizeStatusBarEntry.show(this.id, this._imageBinarySize); this.zoomStatusBarEntry.show(this.id, this._imageZoom || 'fit'); } else { if (this._previewState === PreviewState.Active) { this.sizeStatusBarEntry.hide(this.id); this.binarySizeStatusBarEntry.hide(this.id); this.zoomStatusBarEntry.hide(this.id); } this._previewState = PreviewState.Visible; } } private async getWebviewContents(): Promise { const version = Date.now().toString(); const settings = { isMac: process.platform === 'darwin', src: await this.getResourcePath(this.webviewEditor, this.resource, version), }; const nonce = Date.now().toString(); return /* html */` Image Preview

${localize('preview.imageLoadError', "An error occurred while loading the image.")}

${localize('preview.imageLoadErrorLink', "Open file using VS Code's standard text/binary editor?")}
`; } private async getResourcePath(webviewEditor: vscode.WebviewPanel, resource: vscode.Uri, version: string): Promise { if (resource.scheme === 'git') { const stat = await vscode.workspace.fs.stat(resource); if (stat.size === 0) { return this.emptyPngDataUri; } } // Avoid adding cache busting if there is already a query string if (resource.query) { return webviewEditor.webview.asWebviewUri(resource).toString(); } return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString(); } private extensionResource(path: string) { return this.webviewEditor.webview.asWebviewUri(this.extensionRoot.with({ path: this.extensionRoot.path + path })); } } function escapeAttribute(value: string | vscode.Uri): string { return value.toString().replace(/"/g, '"'); }