Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostCustomEditors.ts
3296 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, document: vscode.CustomDocument) {
108
const key = this.key(viewType, document.uri);
109
this._documents.delete(key);
110
}
111
112
private key(viewType: string, resource: vscode.Uri): string {
113
return `${viewType}@@@${resource}`;
114
}
115
}
116
117
const enum CustomEditorType {
118
Text,
119
Custom
120
}
121
122
type ProviderEntry = {
123
readonly extension: IExtensionDescription;
124
readonly type: CustomEditorType.Text;
125
readonly provider: vscode.CustomTextEditorProvider;
126
} | {
127
readonly extension: IExtensionDescription;
128
readonly type: CustomEditorType.Custom;
129
readonly provider: vscode.CustomReadonlyEditorProvider;
130
};
131
132
class EditorProviderStore {
133
private readonly _providers = new Map<string, ProviderEntry>();
134
135
public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable {
136
return this.add(viewType, { type: CustomEditorType.Text, extension, provider });
137
}
138
139
public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable {
140
return this.add(viewType, { type: CustomEditorType.Custom, extension, provider });
141
}
142
143
public get(viewType: string): ProviderEntry | undefined {
144
return this._providers.get(viewType);
145
}
146
147
private add(viewType: string, entry: ProviderEntry): vscode.Disposable {
148
if (this._providers.has(viewType)) {
149
throw new Error(`Provider for viewType:${viewType} already registered`);
150
}
151
this._providers.set(viewType, entry);
152
return new extHostTypes.Disposable(() => this._providers.delete(viewType));
153
}
154
}
155
156
export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditorsShape {
157
158
private readonly _proxy: extHostProtocol.MainThreadCustomEditorsShape;
159
160
private readonly _editorProviders = new EditorProviderStore();
161
162
private readonly _documents = new CustomDocumentStore();
163
164
constructor(
165
mainContext: extHostProtocol.IMainContext,
166
private readonly _extHostDocuments: ExtHostDocuments,
167
private readonly _extensionStoragePaths: IExtensionStoragePaths | undefined,
168
private readonly _extHostWebview: ExtHostWebviews,
169
private readonly _extHostWebviewPanels: ExtHostWebviewPanels,
170
) {
171
this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCustomEditors);
172
}
173
174
public registerCustomEditorProvider(
175
extension: IExtensionDescription,
176
viewType: string,
177
provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider,
178
options: { webviewOptions?: vscode.WebviewPanelOptions; supportsMultipleEditorsPerDocument?: boolean },
179
): vscode.Disposable {
180
const disposables = new DisposableStore();
181
if (isCustomTextEditorProvider(provider)) {
182
disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider));
183
this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, {
184
supportsMove: !!provider.moveCustomTextEditor,
185
}, shouldSerializeBuffersForPostMessage(extension));
186
} else {
187
disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider));
188
189
if (isCustomEditorProviderWithEditingCapability(provider)) {
190
disposables.add(provider.onDidChangeCustomDocument(e => {
191
const entry = this.getCustomDocumentEntry(viewType, e.document.uri);
192
if (isEditEvent(e)) {
193
const editId = entry.addEdit(e);
194
this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label);
195
} else {
196
this._proxy.$onContentChange(e.document.uri, viewType);
197
}
198
}));
199
}
200
201
this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument, shouldSerializeBuffersForPostMessage(extension));
202
}
203
204
return extHostTypes.Disposable.from(
205
disposables,
206
new extHostTypes.Disposable(() => {
207
this._proxy.$unregisterEditorProvider(viewType);
208
}));
209
}
210
211
async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, untitledDocumentData: VSBuffer | undefined, cancellation: CancellationToken) {
212
const entry = this._editorProviders.get(viewType);
213
if (!entry) {
214
throw new Error(`No provider found for '${viewType}'`);
215
}
216
217
if (entry.type !== CustomEditorType.Custom) {
218
throw new Error(`Invalid provide type for '${viewType}'`);
219
}
220
221
const revivedResource = URI.revive(resource);
222
const document = await entry.provider.openCustomDocument(revivedResource, { backupId, untitledDocumentData: untitledDocumentData?.buffer }, cancellation);
223
224
let storageRoot: URI | undefined;
225
if (isCustomEditorProviderWithEditingCapability(entry.provider) && this._extensionStoragePaths) {
226
storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension);
227
}
228
this._documents.add(viewType, document, storageRoot);
229
230
return { editable: isCustomEditorProviderWithEditingCapability(entry.provider) };
231
}
232
233
async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise<void> {
234
const entry = this._editorProviders.get(viewType);
235
if (!entry) {
236
throw new Error(`No provider found for '${viewType}'`);
237
}
238
239
if (entry.type !== CustomEditorType.Custom) {
240
throw new Error(`Invalid provider type for '${viewType}'`);
241
}
242
243
const revivedResource = URI.revive(resource);
244
const { document } = this.getCustomDocumentEntry(viewType, revivedResource);
245
this._documents.delete(viewType, document);
246
document.dispose();
247
}
248
249
async $resolveCustomEditor(
250
resource: UriComponents,
251
handle: extHostProtocol.WebviewHandle,
252
viewType: string,
253
initData: {
254
title: string;
255
contentOptions: extHostProtocol.IWebviewContentOptions;
256
options: extHostProtocol.IWebviewPanelOptions;
257
active: boolean;
258
},
259
position: EditorGroupColumn,
260
cancellation: CancellationToken,
261
): Promise<void> {
262
const entry = this._editorProviders.get(viewType);
263
if (!entry) {
264
throw new Error(`No provider found for '${viewType}'`);
265
}
266
267
const viewColumn = typeConverters.ViewColumn.to(position);
268
269
const webview = this._extHostWebview.createNewWebview(handle, initData.contentOptions, entry.extension);
270
const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, initData.title, viewColumn, initData.options, webview, initData.active);
271
272
const revivedResource = URI.revive(resource);
273
274
switch (entry.type) {
275
case CustomEditorType.Custom: {
276
const { document } = this.getCustomDocumentEntry(viewType, revivedResource);
277
return entry.provider.resolveCustomEditor(document, panel, cancellation);
278
}
279
case CustomEditorType.Text: {
280
const document = this._extHostDocuments.getDocument(revivedResource);
281
return entry.provider.resolveCustomTextEditor(document, panel, cancellation);
282
}
283
default: {
284
throw new Error('Unknown webview provider type');
285
}
286
}
287
}
288
289
$disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void {
290
const document = this.getCustomDocumentEntry(viewType, resourceComponents);
291
document.disposeEdits(editIds);
292
}
293
294
async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise<void> {
295
const entry = this._editorProviders.get(viewType);
296
if (!entry) {
297
throw new Error(`No provider found for '${viewType}'`);
298
}
299
300
if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) {
301
throw new Error(`Provider does not implement move '${viewType}'`);
302
}
303
304
const webview = this._extHostWebviewPanels.getWebviewPanel(handle);
305
if (!webview) {
306
throw new Error(`No webview found`);
307
}
308
309
const resource = URI.revive(newResourceComponents);
310
const document = this._extHostDocuments.getDocument(resource);
311
await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview, CancellationToken.None);
312
}
313
314
async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {
315
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
316
return entry.undo(editId, isDirty);
317
}
318
319
async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {
320
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
321
return entry.redo(editId, isDirty);
322
}
323
324
async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
325
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
326
const provider = this.getCustomEditorProvider(viewType);
327
await provider.revertCustomDocument(entry.document, cancellation);
328
entry.disposeBackup();
329
}
330
331
async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
332
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
333
const provider = this.getCustomEditorProvider(viewType);
334
await provider.saveCustomDocument(entry.document, cancellation);
335
entry.disposeBackup();
336
}
337
338
async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise<void> {
339
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
340
const provider = this.getCustomEditorProvider(viewType);
341
return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation);
342
}
343
344
async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string> {
345
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
346
const provider = this.getCustomEditorProvider(viewType);
347
348
const backup = await provider.backupCustomDocument(entry.document, {
349
destination: entry.getNewBackupUri(),
350
}, cancellation);
351
entry.updateBackup(backup);
352
return backup.id;
353
}
354
355
private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry {
356
const entry = this._documents.get(viewType, URI.revive(resource));
357
if (!entry) {
358
throw new Error('No custom document found');
359
}
360
return entry;
361
}
362
363
private getCustomEditorProvider(viewType: string): vscode.CustomEditorProvider {
364
const entry = this._editorProviders.get(viewType);
365
const provider = entry?.provider;
366
if (!provider || !isCustomEditorProviderWithEditingCapability(provider)) {
367
throw new Error('Custom document is not editable');
368
}
369
return provider;
370
}
371
}
372
373
function isCustomEditorProviderWithEditingCapability(provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider): provider is vscode.CustomEditorProvider {
374
return !!(provider as vscode.CustomEditorProvider).onDidChangeCustomDocument;
375
}
376
377
function isCustomTextEditorProvider(provider: vscode.CustomReadonlyEditorProvider<vscode.CustomDocument> | vscode.CustomTextEditorProvider): provider is vscode.CustomTextEditorProvider {
378
return typeof (provider as vscode.CustomTextEditorProvider).resolveCustomTextEditor === 'function';
379
}
380
381
function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent {
382
return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function'
383
&& typeof (e as vscode.CustomDocumentEditEvent).redo === 'function';
384
}
385
386
function hashPath(resource: URI): string {
387
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
388
return hash(str) + '';
389
}
390
391