Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/media-preview/src/imagePreview/index.ts
4774 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as vscode from 'vscode';
7
import { BinarySizeStatusBarEntry } from '../binarySizeStatusBarEntry';
8
import { MediaPreview, PreviewState, reopenAsText } from '../mediaPreview';
9
import { escapeAttribute } from '../util/dom';
10
import { generateUuid } from '../util/uuid';
11
import { SizeStatusBarEntry } from './sizeStatusBarEntry';
12
import { Scale, ZoomStatusBarEntry } from './zoomStatusBarEntry';
13
14
15
export class ImagePreviewManager implements vscode.CustomReadonlyEditorProvider {
16
17
public static readonly viewType = 'imagePreview.previewEditor';
18
19
private readonly _previews = new Set<ImagePreview>();
20
private _activePreview: ImagePreview | undefined;
21
22
constructor(
23
private readonly extensionRoot: vscode.Uri,
24
private readonly sizeStatusBarEntry: SizeStatusBarEntry,
25
private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry,
26
private readonly zoomStatusBarEntry: ZoomStatusBarEntry,
27
) { }
28
29
public async openCustomDocument(uri: vscode.Uri) {
30
return { uri, dispose: () => { } };
31
}
32
33
public async resolveCustomEditor(
34
document: vscode.CustomDocument,
35
webviewEditor: vscode.WebviewPanel,
36
): Promise<void> {
37
const preview = new ImagePreview(this.extensionRoot, document.uri, webviewEditor, this.sizeStatusBarEntry, this.binarySizeStatusBarEntry, this.zoomStatusBarEntry);
38
this._previews.add(preview);
39
this.setActivePreview(preview);
40
41
webviewEditor.onDidDispose(() => { this._previews.delete(preview); });
42
43
webviewEditor.onDidChangeViewState(() => {
44
if (webviewEditor.active) {
45
this.setActivePreview(preview);
46
} else if (this._activePreview === preview && !webviewEditor.active) {
47
this.setActivePreview(undefined);
48
}
49
});
50
}
51
52
public get activePreview() {
53
return this._activePreview;
54
}
55
56
public getPreviewFor(resource: vscode.Uri, viewColumn?: vscode.ViewColumn): ImagePreview | undefined {
57
for (const preview of this._previews) {
58
if (preview.resource.toString() === resource.toString()) {
59
if (!viewColumn || preview.viewColumn === viewColumn) {
60
return preview;
61
}
62
}
63
}
64
return undefined;
65
}
66
67
private setActivePreview(value: ImagePreview | undefined): void {
68
this._activePreview = value;
69
}
70
}
71
72
73
class ImagePreview extends MediaPreview {
74
75
private _imageSize: string | undefined;
76
private _imageZoom: Scale | undefined;
77
78
private readonly emptyPngDataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg==';
79
80
constructor(
81
private readonly extensionRoot: vscode.Uri,
82
resource: vscode.Uri,
83
webviewEditor: vscode.WebviewPanel,
84
private readonly sizeStatusBarEntry: SizeStatusBarEntry,
85
binarySizeStatusBarEntry: BinarySizeStatusBarEntry,
86
private readonly zoomStatusBarEntry: ZoomStatusBarEntry,
87
) {
88
super(extensionRoot, resource, webviewEditor, binarySizeStatusBarEntry);
89
90
this._register(webviewEditor.webview.onDidReceiveMessage(message => {
91
switch (message.type) {
92
case 'size': {
93
this._imageSize = message.value;
94
this.updateState();
95
break;
96
}
97
case 'zoom': {
98
this._imageZoom = message.value;
99
this.updateState();
100
break;
101
}
102
case 'reopen-as-text': {
103
reopenAsText(resource, webviewEditor.viewColumn);
104
break;
105
}
106
}
107
}));
108
109
this._register(zoomStatusBarEntry.onDidChangeScale(e => {
110
if (this.previewState === PreviewState.Active) {
111
this._webviewEditor.webview.postMessage({ type: 'setScale', scale: e.scale });
112
}
113
}));
114
115
this._register(webviewEditor.onDidChangeViewState(() => {
116
this._webviewEditor.webview.postMessage({ type: 'setActive', value: this._webviewEditor.active });
117
}));
118
119
this._register(webviewEditor.onDidDispose(() => {
120
if (this.previewState === PreviewState.Active) {
121
this.sizeStatusBarEntry.hide(this);
122
this.zoomStatusBarEntry.hide(this);
123
}
124
this.previewState = PreviewState.Disposed;
125
}));
126
127
this.updateBinarySize();
128
this.render();
129
this.updateState();
130
}
131
132
public override dispose(): void {
133
super.dispose();
134
this.sizeStatusBarEntry.hide(this);
135
this.zoomStatusBarEntry.hide(this);
136
}
137
138
public get viewColumn() {
139
return this._webviewEditor.viewColumn;
140
}
141
142
public zoomIn() {
143
if (this.previewState === PreviewState.Active) {
144
this._webviewEditor.webview.postMessage({ type: 'zoomIn' });
145
}
146
}
147
148
public zoomOut() {
149
if (this.previewState === PreviewState.Active) {
150
this._webviewEditor.webview.postMessage({ type: 'zoomOut' });
151
}
152
}
153
154
public copyImage() {
155
if (this.previewState === PreviewState.Active) {
156
this._webviewEditor.reveal();
157
this._webviewEditor.webview.postMessage({ type: 'copyImage' });
158
}
159
}
160
161
protected override updateState() {
162
super.updateState();
163
164
if (this.previewState === PreviewState.Disposed) {
165
return;
166
}
167
168
if (this._webviewEditor.active) {
169
this.sizeStatusBarEntry.show(this, this._imageSize || '');
170
this.zoomStatusBarEntry.show(this, this._imageZoom || 'fit');
171
} else {
172
this.sizeStatusBarEntry.hide(this);
173
this.zoomStatusBarEntry.hide(this);
174
}
175
}
176
177
protected override async render(): Promise<void> {
178
await super.render();
179
this._webviewEditor.webview.postMessage({ type: 'setActive', value: this._webviewEditor.active });
180
}
181
182
protected override async getWebviewContents(): Promise<string> {
183
const version = Date.now().toString();
184
const settings = {
185
src: await this.getResourcePath(this._webviewEditor, this._resource, version),
186
};
187
188
const nonce = generateUuid();
189
190
const cspSource = this._webviewEditor.webview.cspSource;
191
return /* html */`<!DOCTYPE html>
192
<html lang="en">
193
<head>
194
<meta charset="UTF-8">
195
196
<!-- Disable pinch zooming -->
197
<meta name="viewport"
198
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
199
200
<title>Image Preview</title>
201
202
<link rel="stylesheet" href="${escapeAttribute(this.extensionResource('media', 'imagePreview.css'))}" type="text/css" media="screen" nonce="${nonce}">
203
204
<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}';">
205
<meta id="image-preview-settings" data-settings="${escapeAttribute(JSON.stringify(settings))}">
206
</head>
207
<body class="container image scale-to-fit loading" data-vscode-context='{ "preventDefaultContextMenuItems": true }'>
208
<div class="loading-indicator"></div>
209
<div class="image-load-error">
210
<p>${vscode.l10n.t("An error occurred while loading the image.")}</p>
211
<a href="#" class="open-file-link">${vscode.l10n.t("Open file using VS Code's standard text/binary editor?")}</a>
212
</div>
213
<script src="${escapeAttribute(this.extensionResource('media', 'imagePreview.js'))}" nonce="${nonce}"></script>
214
</body>
215
</html>`;
216
}
217
218
private async getResourcePath(webviewEditor: vscode.WebviewPanel, resource: vscode.Uri, version: string): Promise<string> {
219
if (resource.scheme === 'git') {
220
const stat = await vscode.workspace.fs.stat(resource);
221
if (stat.size === 0) {
222
return this.emptyPngDataUri;
223
}
224
}
225
226
// Avoid adding cache busting if there is already a query string
227
if (resource.query) {
228
return webviewEditor.webview.asWebviewUri(resource).toString();
229
}
230
return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString();
231
}
232
233
private extensionResource(...parts: string[]) {
234
return this._webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts));
235
}
236
237
public async reopenAsText() {
238
await vscode.commands.executeCommand('reopenActiveEditorWith', 'default');
239
this._webviewEditor.dispose();
240
}
241
}
242
243
244
export function registerImagePreviewSupport(context: vscode.ExtensionContext, binarySizeStatusBarEntry: BinarySizeStatusBarEntry): vscode.Disposable {
245
const disposables: vscode.Disposable[] = [];
246
247
const sizeStatusBarEntry = new SizeStatusBarEntry();
248
disposables.push(sizeStatusBarEntry);
249
250
const zoomStatusBarEntry = new ZoomStatusBarEntry();
251
disposables.push(zoomStatusBarEntry);
252
253
const previewManager = new ImagePreviewManager(context.extensionUri, sizeStatusBarEntry, binarySizeStatusBarEntry, zoomStatusBarEntry);
254
255
disposables.push(vscode.window.registerCustomEditorProvider(ImagePreviewManager.viewType, previewManager, {
256
supportsMultipleEditorsPerDocument: true,
257
}));
258
259
disposables.push(vscode.commands.registerCommand('imagePreview.zoomIn', () => {
260
previewManager.activePreview?.zoomIn();
261
}));
262
263
disposables.push(vscode.commands.registerCommand('imagePreview.zoomOut', () => {
264
previewManager.activePreview?.zoomOut();
265
}));
266
267
disposables.push(vscode.commands.registerCommand('imagePreview.copyImage', () => {
268
previewManager.activePreview?.copyImage();
269
}));
270
271
disposables.push(vscode.commands.registerCommand('imagePreview.reopenAsText', async () => {
272
return previewManager.activePreview?.reopenAsText();
273
}));
274
275
disposables.push(vscode.commands.registerCommand('imagePreview.reopenAsPreview', async () => {
276
277
await vscode.commands.executeCommand('reopenActiveEditorWith', ImagePreviewManager.viewType);
278
}));
279
280
return vscode.Disposable.from(...disposables);
281
}
282
283