Path: blob/main/extensions/media-preview/src/imagePreview/index.ts
4774 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as vscode from 'vscode';6import { BinarySizeStatusBarEntry } from '../binarySizeStatusBarEntry';7import { MediaPreview, PreviewState, reopenAsText } from '../mediaPreview';8import { escapeAttribute } from '../util/dom';9import { generateUuid } from '../util/uuid';10import { SizeStatusBarEntry } from './sizeStatusBarEntry';11import { Scale, ZoomStatusBarEntry } from './zoomStatusBarEntry';121314export class ImagePreviewManager implements vscode.CustomReadonlyEditorProvider {1516public static readonly viewType = 'imagePreview.previewEditor';1718private readonly _previews = new Set<ImagePreview>();19private _activePreview: ImagePreview | undefined;2021constructor(22private readonly extensionRoot: vscode.Uri,23private readonly sizeStatusBarEntry: SizeStatusBarEntry,24private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry,25private readonly zoomStatusBarEntry: ZoomStatusBarEntry,26) { }2728public async openCustomDocument(uri: vscode.Uri) {29return { uri, dispose: () => { } };30}3132public async resolveCustomEditor(33document: vscode.CustomDocument,34webviewEditor: vscode.WebviewPanel,35): Promise<void> {36const preview = new ImagePreview(this.extensionRoot, document.uri, webviewEditor, this.sizeStatusBarEntry, this.binarySizeStatusBarEntry, this.zoomStatusBarEntry);37this._previews.add(preview);38this.setActivePreview(preview);3940webviewEditor.onDidDispose(() => { this._previews.delete(preview); });4142webviewEditor.onDidChangeViewState(() => {43if (webviewEditor.active) {44this.setActivePreview(preview);45} else if (this._activePreview === preview && !webviewEditor.active) {46this.setActivePreview(undefined);47}48});49}5051public get activePreview() {52return this._activePreview;53}5455public getPreviewFor(resource: vscode.Uri, viewColumn?: vscode.ViewColumn): ImagePreview | undefined {56for (const preview of this._previews) {57if (preview.resource.toString() === resource.toString()) {58if (!viewColumn || preview.viewColumn === viewColumn) {59return preview;60}61}62}63return undefined;64}6566private setActivePreview(value: ImagePreview | undefined): void {67this._activePreview = value;68}69}707172class ImagePreview extends MediaPreview {7374private _imageSize: string | undefined;75private _imageZoom: Scale | undefined;7677private readonly emptyPngDataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg==';7879constructor(80private readonly extensionRoot: vscode.Uri,81resource: vscode.Uri,82webviewEditor: vscode.WebviewPanel,83private readonly sizeStatusBarEntry: SizeStatusBarEntry,84binarySizeStatusBarEntry: BinarySizeStatusBarEntry,85private readonly zoomStatusBarEntry: ZoomStatusBarEntry,86) {87super(extensionRoot, resource, webviewEditor, binarySizeStatusBarEntry);8889this._register(webviewEditor.webview.onDidReceiveMessage(message => {90switch (message.type) {91case 'size': {92this._imageSize = message.value;93this.updateState();94break;95}96case 'zoom': {97this._imageZoom = message.value;98this.updateState();99break;100}101case 'reopen-as-text': {102reopenAsText(resource, webviewEditor.viewColumn);103break;104}105}106}));107108this._register(zoomStatusBarEntry.onDidChangeScale(e => {109if (this.previewState === PreviewState.Active) {110this._webviewEditor.webview.postMessage({ type: 'setScale', scale: e.scale });111}112}));113114this._register(webviewEditor.onDidChangeViewState(() => {115this._webviewEditor.webview.postMessage({ type: 'setActive', value: this._webviewEditor.active });116}));117118this._register(webviewEditor.onDidDispose(() => {119if (this.previewState === PreviewState.Active) {120this.sizeStatusBarEntry.hide(this);121this.zoomStatusBarEntry.hide(this);122}123this.previewState = PreviewState.Disposed;124}));125126this.updateBinarySize();127this.render();128this.updateState();129}130131public override dispose(): void {132super.dispose();133this.sizeStatusBarEntry.hide(this);134this.zoomStatusBarEntry.hide(this);135}136137public get viewColumn() {138return this._webviewEditor.viewColumn;139}140141public zoomIn() {142if (this.previewState === PreviewState.Active) {143this._webviewEditor.webview.postMessage({ type: 'zoomIn' });144}145}146147public zoomOut() {148if (this.previewState === PreviewState.Active) {149this._webviewEditor.webview.postMessage({ type: 'zoomOut' });150}151}152153public copyImage() {154if (this.previewState === PreviewState.Active) {155this._webviewEditor.reveal();156this._webviewEditor.webview.postMessage({ type: 'copyImage' });157}158}159160protected override updateState() {161super.updateState();162163if (this.previewState === PreviewState.Disposed) {164return;165}166167if (this._webviewEditor.active) {168this.sizeStatusBarEntry.show(this, this._imageSize || '');169this.zoomStatusBarEntry.show(this, this._imageZoom || 'fit');170} else {171this.sizeStatusBarEntry.hide(this);172this.zoomStatusBarEntry.hide(this);173}174}175176protected override async render(): Promise<void> {177await super.render();178this._webviewEditor.webview.postMessage({ type: 'setActive', value: this._webviewEditor.active });179}180181protected override async getWebviewContents(): Promise<string> {182const version = Date.now().toString();183const settings = {184src: await this.getResourcePath(this._webviewEditor, this._resource, version),185};186187const nonce = generateUuid();188189const cspSource = this._webviewEditor.webview.cspSource;190return /* html */`<!DOCTYPE html>191<html lang="en">192<head>193<meta charset="UTF-8">194195<!-- Disable pinch zooming -->196<meta name="viewport"197content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">198199<title>Image Preview</title>200201<link rel="stylesheet" href="${escapeAttribute(this.extensionResource('media', 'imagePreview.css'))}" type="text/css" media="screen" nonce="${nonce}">202203<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data: ${cspSource}; connect-src ${cspSource}; script-src 'nonce-${nonce}'; style-src ${cspSource} 'nonce-${nonce}';">204<meta id="image-preview-settings" data-settings="${escapeAttribute(JSON.stringify(settings))}">205</head>206<body class="container image scale-to-fit loading" data-vscode-context='{ "preventDefaultContextMenuItems": true }'>207<div class="loading-indicator"></div>208<div class="image-load-error">209<p>${vscode.l10n.t("An error occurred while loading the image.")}</p>210<a href="#" class="open-file-link">${vscode.l10n.t("Open file using VS Code's standard text/binary editor?")}</a>211</div>212<script src="${escapeAttribute(this.extensionResource('media', 'imagePreview.js'))}" nonce="${nonce}"></script>213</body>214</html>`;215}216217private async getResourcePath(webviewEditor: vscode.WebviewPanel, resource: vscode.Uri, version: string): Promise<string> {218if (resource.scheme === 'git') {219const stat = await vscode.workspace.fs.stat(resource);220if (stat.size === 0) {221return this.emptyPngDataUri;222}223}224225// Avoid adding cache busting if there is already a query string226if (resource.query) {227return webviewEditor.webview.asWebviewUri(resource).toString();228}229return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString();230}231232private extensionResource(...parts: string[]) {233return this._webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts));234}235236public async reopenAsText() {237await vscode.commands.executeCommand('reopenActiveEditorWith', 'default');238this._webviewEditor.dispose();239}240}241242243export function registerImagePreviewSupport(context: vscode.ExtensionContext, binarySizeStatusBarEntry: BinarySizeStatusBarEntry): vscode.Disposable {244const disposables: vscode.Disposable[] = [];245246const sizeStatusBarEntry = new SizeStatusBarEntry();247disposables.push(sizeStatusBarEntry);248249const zoomStatusBarEntry = new ZoomStatusBarEntry();250disposables.push(zoomStatusBarEntry);251252const previewManager = new ImagePreviewManager(context.extensionUri, sizeStatusBarEntry, binarySizeStatusBarEntry, zoomStatusBarEntry);253254disposables.push(vscode.window.registerCustomEditorProvider(ImagePreviewManager.viewType, previewManager, {255supportsMultipleEditorsPerDocument: true,256}));257258disposables.push(vscode.commands.registerCommand('imagePreview.zoomIn', () => {259previewManager.activePreview?.zoomIn();260}));261262disposables.push(vscode.commands.registerCommand('imagePreview.zoomOut', () => {263previewManager.activePreview?.zoomOut();264}));265266disposables.push(vscode.commands.registerCommand('imagePreview.copyImage', () => {267previewManager.activePreview?.copyImage();268}));269270disposables.push(vscode.commands.registerCommand('imagePreview.reopenAsText', async () => {271return previewManager.activePreview?.reopenAsText();272}));273274disposables.push(vscode.commands.registerCommand('imagePreview.reopenAsPreview', async () => {275276await vscode.commands.executeCommand('reopenActiveEditorWith', ImagePreviewManager.viewType);277}));278279return vscode.Disposable.from(...disposables);280}281282283