Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostCustomEditors.ts
5239 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 { VSBuffer } from '../../../base/common/buffer.js';
7
import { CancellationToken } from '../../../base/common/cancellation.js';
8
import { hash } from '../../../base/common/hash.js';
9
import { DisposableStore } from '../../../base/common/lifecycle.js';
10
import { Schemas } from '../../../base/common/network.js';
11
import { joinPath } from '../../../base/common/resources.js';
12
import { URI, UriComponents } from '../../../base/common/uri.js';
13
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
14
import { ExtHostDocuments } from './extHostDocuments.js';
15
import { IExtensionStoragePaths } from './extHostStoragePaths.js';
16
import * as typeConverters from './extHostTypeConverters.js';
17
import { ExtHostWebviews, shouldSerializeBuffersForPostMessage, toExtensionData } from './extHostWebview.js';
18
import { ExtHostWebviewPanels } from './extHostWebviewPanels.js';
19
import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js';
20
import type * as vscode from 'vscode';
21
import { Cache } from './cache.js';
22
import * as extHostProtocol from './extHost.protocol.js';
23
import * as extHostTypes from './extHostTypes.js';
24
25
26
class CustomDocumentStoreEntry {
27
28
private _backupCounter = 1;
29
30
constructor(
31
public readonly document: vscode.CustomDocument,
32
private readonly _storagePath: URI | undefined,
33
) { }
34
35
private readonly _edits = new Cache<vscode.CustomDocumentEditEvent>('custom documents');
36
37
private _backup?: vscode.CustomDocumentBackup;
38
39
addEdit(item: vscode.CustomDocumentEditEvent): number {
40
return this._edits.add([item]);
41
}
42
43
async undo(editId: number, isDirty: boolean): Promise<void> {
44
await this.getEdit(editId).undo();
45
if (!isDirty) {
46
this.disposeBackup();
47
}
48
}
49
50
async redo(editId: number, isDirty: boolean): Promise<void> {
51
await this.getEdit(editId).redo();
52
if (!isDirty) {
53
this.disposeBackup();
54
}
55
}
56
57
disposeEdits(editIds: number[]): void {
58
for (const id of editIds) {
59
this._edits.delete(id);
60
}
61
}
62
63
getNewBackupUri(): URI {
64
if (!this._storagePath) {
65
throw new Error('Backup requires a valid storage path');
66
}
67
const fileName = hashPath(this.document.uri) + (this._backupCounter++);
68
return joinPath(this._storagePath, fileName);
69
}
70
71
updateBackup(backup: vscode.CustomDocumentBackup): void {
72
this._backup?.delete();
73
this._backup = backup;
74
}
75
76
disposeBackup(): void {
77
this._backup?.delete();
78
this._backup = undefined;
79
}
80
81
private getEdit(editId: number): vscode.CustomDocumentEditEvent {
82
const edit = this._edits.get(editId, 0);
83
if (!edit) {
84
throw new Error('No edit found');
85
}
86
return edit;
87
}
88
}
89
90
class CustomDocumentStore {
91
private readonly _documents = new Map<string, CustomDocumentStoreEntry>();
92
93
public get(viewType: string, resource: vscode.Uri): CustomDocumentStoreEntry | undefined {
94
return this._documents.get(this.key(viewType, resource));
95
}
96
97
public add(viewType: string, document: vscode.CustomDocument, storagePath: URI | undefined): CustomDocumentStoreEntry {
98
const key = this.key(viewType, document.uri);
99
if (this._documents.has(key)) {
100
throw new Error(`Document already exists for viewType:${viewType} resource:${document.uri}`);
101
}
102
const entry = new CustomDocumentStoreEntry(document, storagePath);
103
this._documents.set(key, entry);
104
return entry;
105
}
106
107
public delete(viewType: string, resource: vscode.Uri) {
108
// Use the resource parameter directly instead of document.uri, because the document's
109
// URI may have changed (e.g., after SaveAs from untitled to a file path).
110
const key = this.key(viewType, resource);
111
this._documents.delete(key);
112
}
113
114
private key(viewType: string, resource: vscode.Uri): string {
115
return `${viewType}@@@${resource}`;
116
}
117
}
118
119
const enum CustomEditorType {
120
Text,
121
Custom
122
}
123
124
type ProviderEntry = {
125
readonly extension: IExtensionDescription;
126
readonly type: CustomEditorType.Text;
127
readonly provider: vscode.CustomTextEditorProvider;
128
} | {
129
readonly extension: IExtensionDescription;
130
readonly type: CustomEditorType.Custom;
131
readonly provider: vscode.CustomReadonlyEditorProvider;
132
};
133
134
class EditorProviderStore {
135
private readonly _providers = new Map<string, ProviderEntry>();
136
137
public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable {
138
return this.add(viewType, { type: CustomEditorType.Text, extension, provider });
139
}
140
141
public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable {
142
return this.add(viewType, { type: CustomEditorType.Custom, extension, provider });
143
}
144
145
public get(viewType: string): ProviderEntry | undefined {
146
return this._providers.get(viewType);
147
}
148
149
private add(viewType: string, entry: ProviderEntry): vscode.Disposable {
150
if (this._providers.has(viewType)) {
151
throw new Error(`Provider for viewType:${viewType} already registered`);
152
}
153
this._providers.set(viewType, entry);
154
return new extHostTypes.Disposable(() => this._providers.delete(viewType));
155
}
156
}
157
158
export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditorsShape {
159
160
private readonly _proxy: extHostProtocol.MainThreadCustomEditorsShape;
161
162
private readonly _editorProviders = new EditorProviderStore();
163
164
private readonly _documents = new CustomDocumentStore();
165
166
constructor(
167
mainContext: extHostProtocol.IMainContext,
168
private readonly _extHostDocuments: ExtHostDocuments,
169
private readonly _extensionStoragePaths: IExtensionStoragePaths | undefined,
170
private readonly _extHostWebview: ExtHostWebviews,
171
private readonly _extHostWebviewPanels: ExtHostWebviewPanels,
172
) {
173
this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCustomEditors);
174
}
175
176
public registerCustomEditorProvider(
177
extension: IExtensionDescription,
178
viewType: string,
179
provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider,
180
options: { webviewOptions?: vscode.WebviewPanelOptions; supportsMultipleEditorsPerDocument?: boolean },
181
): vscode.Disposable {
182
const disposables = new DisposableStore();
183
if (isCustomTextEditorProvider(provider)) {
184
disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider));
185
this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, {
186
supportsMove: !!provider.moveCustomTextEditor,
187
}, shouldSerializeBuffersForPostMessage(extension));
188
} else {
189
disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider));
190
191
if (isCustomEditorProviderWithEditingCapability(provider)) {
192
disposables.add(provider.onDidChangeCustomDocument(e => {
193
const entry = this.getCustomDocumentEntry(viewType, e.document.uri);
194
if (isEditEvent(e)) {
195
const editId = entry.addEdit(e);
196
this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label);
197
} else {
198
this._proxy.$onContentChange(e.document.uri, viewType);
199
}
200
}));
201
}
202
203
this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument, shouldSerializeBuffersForPostMessage(extension));
204
}
205
206
return extHostTypes.Disposable.from(
207
disposables,
208
new extHostTypes.Disposable(() => {
209
this._proxy.$unregisterEditorProvider(viewType);
210
}));
211
}
212
213
async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, cancellation: CancellationToken) {
214
const entry = this._editorProviders.get(viewType);
215
if (!entry) {
216
throw new Error(`No provider found for '${viewType}'`);
217
}
218
219
if (entry.type !== CustomEditorType.Custom) {
220
throw new Error(`Invalid provide type for '${viewType}'`);
221
}
222
223
const revivedResource = URI.revive(resource);
224
const document = await entry.provider.openCustomDocument(revivedResource, { backupId, untitledDocumentData: untitledDocumentData?.buffer }, cancellation);
225
226
let storageRoot: URI | undefined;
227
if (isCustomEditorProviderWithEditingCapability(entry.provider) && this._extensionStoragePaths) {
228
storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension);
229
}
230
this._documents.add(viewType, document, storageRoot);
231
232
return { editable: isCustomEditorProviderWithEditingCapability(entry.provider) };
233
}
234
235
async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise<void> {
236
const entry = this._editorProviders.get(viewType);
237
if (!entry) {
238
throw new Error(`No provider found for '${viewType}'`);
239
}
240
241
if (entry.type !== CustomEditorType.Custom) {
242
throw new Error(`Invalid provider type for '${viewType}'`);
243
}
244
245
const revivedResource = URI.revive(resource);
246
const { document } = this.getCustomDocumentEntry(viewType, revivedResource);
247
// Pass the resource we used to look up the document, not document.uri,
248
// because the document's URI may have changed (e.g., after SaveAs).
249
this._documents.delete(viewType, revivedResource);
250
document.dispose();
251
}
252
253
async $resolveCustomEditor(
254
resource: UriComponents,
255
handle: extHostProtocol.WebviewHandle,
256
viewType: string,
257
initData: {
258
title: string;
259
contentOptions: extHostProtocol.IWebviewContentOptions;
260
options: extHostProtocol.IWebviewPanelOptions;
261
active: boolean;
262
},
263
position: EditorGroupColumn,
264
cancellation: CancellationToken,
265
): Promise<void> {
266
const entry = this._editorProviders.get(viewType);
267
if (!entry) {
268
throw new Error(`No provider found for '${viewType}'`);
269
}
270
271
const viewColumn = typeConverters.ViewColumn.to(position);
272
273
const webview = this._extHostWebview.createNewWebview(handle, initData.contentOptions, entry.extension);
274
const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, initData.title, viewColumn, initData.options, webview, initData.active);
275
276
const revivedResource = URI.revive(resource);
277
278
switch (entry.type) {
279
case CustomEditorType.Custom: {
280
const { document } = this.getCustomDocumentEntry(viewType, revivedResource);
281
return entry.provider.resolveCustomEditor(document, panel, cancellation);
282
}
283
case CustomEditorType.Text: {
284
const document = this._extHostDocuments.getDocument(revivedResource);
285
return entry.provider.resolveCustomTextEditor(document, panel, cancellation);
286
}
287
default: {
288
throw new Error('Unknown webview provider type');
289
}
290
}
291
}
292
293
$disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void {
294
const document = this.getCustomDocumentEntry(viewType, resourceComponents);
295
document.disposeEdits(editIds);
296
}
297
298
async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise<void> {
299
const entry = this._editorProviders.get(viewType);
300
if (!entry) {
301
throw new Error(`No provider found for '${viewType}'`);
302
}
303
304
if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) {
305
throw new Error(`Provider does not implement move '${viewType}'`);
306
}
307
308
const webview = this._extHostWebviewPanels.getWebviewPanel(handle);
309
if (!webview) {
310
throw new Error(`No webview found`);
311
}
312
313
const resource = URI.revive(newResourceComponents);
314
const document = this._extHostDocuments.getDocument(resource);
315
await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview, CancellationToken.None);
316
}
317
318
async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {
319
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
320
return entry.undo(editId, isDirty);
321
}
322
323
async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {
324
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
325
return entry.redo(editId, isDirty);
326
}
327
328
async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
329
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
330
const provider = this.getCustomEditorProvider(viewType);
331
await provider.revertCustomDocument(entry.document, cancellation);
332
entry.disposeBackup();
333
}
334
335
async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
336
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
337
const provider = this.getCustomEditorProvider(viewType);
338
await provider.saveCustomDocument(entry.document, cancellation);
339
entry.disposeBackup();
340
}
341
342
async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise<void> {
343
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
344
const provider = this.getCustomEditorProvider(viewType);
345
return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation);
346
}
347
348
async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string> {
349
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
350
const provider = this.getCustomEditorProvider(viewType);
351
352
const backup = await provider.backupCustomDocument(entry.document, {
353
destination: entry.getNewBackupUri(),
354
}, cancellation);
355
entry.updateBackup(backup);
356
return backup.id;
357
}
358
359
private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry {
360
const entry = this._documents.get(viewType, URI.revive(resource));
361
if (!entry) {
362
throw new Error('No custom document found');
363
}
364
return entry;
365
}
366
367
private getCustomEditorProvider(viewType: string): vscode.CustomEditorProvider {
368
const entry = this._editorProviders.get(viewType);
369
const provider = entry?.provider;
370
if (!provider || !isCustomEditorProviderWithEditingCapability(provider)) {
371
throw new Error('Custom document is not editable');
372
}
373
return provider;
374
}
375
}
376
377
function isCustomEditorProviderWithEditingCapability(provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider): provider is vscode.CustomEditorProvider {
378
return !!(provider as vscode.CustomEditorProvider).onDidChangeCustomDocument;
379
}
380
381
function isCustomTextEditorProvider(provider: vscode.CustomReadonlyEditorProvider<vscode.CustomDocument> | vscode.CustomTextEditorProvider): provider is vscode.CustomTextEditorProvider {
382
return typeof (provider as vscode.CustomTextEditorProvider).resolveCustomTextEditor === 'function';
383
}
384
385
function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent {
386
return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function'
387
&& typeof (e as vscode.CustomDocumentEditEvent).redo === 'function';
388
}
389
390
function hashPath(resource: URI): string {
391
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
392
return hash(str) + '';
393
}
394
395