Skip to content

Commit d4ce714

Browse files
committed
New custom editor API proposal
For microsoft#77131 Fixes microsoft#93963 Fixes microsoft#94515 Fixes microsoft#94517 Fixes microsoft#94527 Fixes microsoft#94509 Fixes microsoft#94514 Fixes microsoft#93996 Fixes microsoft#93913 This removes explicit edits from the API and reshapes the API to more closely match VS Code's internal API. The change also tries to better express the lifecycle of backups
1 parent 1ed9dcd commit d4ce714

10 files changed

Lines changed: 254 additions & 401 deletions

File tree

extensions/image-preview/src/preview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class PreviewManager implements vscode.CustomEditorProvider {
2828
) { }
2929

3030
public async openCustomDocument(uri: vscode.Uri) {
31-
return new vscode.CustomDocument(uri);
31+
return { uri, dispose: () => { } };
3232
}
3333

3434
public async resolveCustomEditor(

extensions/markdown-language-features/src/features/previewManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
151151
}
152152

153153
public async openCustomDocument(uri: vscode.Uri) {
154-
return new vscode.CustomDocument(uri);
154+
return { uri, dispose: () => { } };
155155
}
156156

157157
public async resolveCustomTextEditor(

src/vs/vscode.proposed.d.ts

Lines changed: 93 additions & 183 deletions
Large diffs are not rendered by default.

src/vs/workbench/api/browser/mainThreadWebview.ts

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/
3535
import { WebviewExtensionDescription, WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview';
3636
import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
3737
import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService';
38-
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
38+
import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
3939
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
4040
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
4141
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
@@ -133,6 +133,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
133133
@ITelemetryService private readonly _telemetryService: ITelemetryService,
134134
@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
135135
@IInstantiationService private readonly _instantiationService: IInstantiationService,
136+
@IBackupFileService private readonly _backupService: IBackupFileService,
136137
) {
137138
super();
138139

@@ -410,7 +411,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
410411
: MainThreadCustomEditorModel.create(this._instantiationService, this._proxy, viewType, resource, () => {
411412
return Array.from(this._webviewInputs)
412413
.filter(editor => editor instanceof CustomEditorInput && isEqual(editor.resource, resource)) as CustomEditorInput[];
413-
}, cancellation);
414+
}, cancellation, this._backupService);
414415

415416
return this._customEditorService.models.add(resource, viewType, model);
416417
}
@@ -577,7 +578,7 @@ namespace HotExitState {
577578
readonly type = Type.Pending;
578579

579580
constructor(
580-
public readonly operation: CancelablePromise<void>,
581+
public readonly operation: CancelablePromise<string>,
581582
) { }
582583
}
583584

@@ -600,58 +601,58 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
600601
resource: URI,
601602
getEditors: () => CustomEditorInput[],
602603
cancellation: CancellationToken,
604+
backupFileService: IBackupFileService,
603605
) {
604-
const { editable } = await proxy.$createWebviewCustomEditorDocument(resource, viewType, cancellation);
605-
const model = instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, editable, getEditors);
606-
await model.init();
607-
return model;
606+
const backup = await backupFileService.resolve<CustomDocumentBackupData>(MainThreadCustomEditorModel.toWorkingCopyResource(viewType, resource));
607+
const { editable } = await proxy.$createCustomDocument(resource, viewType, backup?.meta?.backupId, cancellation);
608+
return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, backup, editable, getEditors);
608609
}
609610

610611
constructor(
611612
private readonly _proxy: extHostProtocol.ExtHostWebviewsShape,
612613
private readonly _viewType: string,
613614
private readonly _editorResource: URI,
615+
backup: IResolvedBackup<CustomDocumentBackupData> | undefined,
614616
private readonly _editable: boolean,
615617
private readonly _getEditors: () => CustomEditorInput[],
616618
@IWorkingCopyService workingCopyService: IWorkingCopyService,
617619
@ILabelService private readonly _labelService: ILabelService,
618620
@IFileService private readonly _fileService: IFileService,
619621
@IUndoRedoService private readonly _undoService: IUndoRedoService,
620-
@IBackupFileService private readonly _backupFileService: IBackupFileService,
621622
) {
622623
super();
623624

624625
if (_editable) {
625626
this._register(workingCopyService.registerWorkingCopy(this));
626627
}
628+
this._fromBackup = !!backup;
627629
}
628630

629631
get editorResource() {
630632
return this._editorResource;
631633
}
632634

633-
async init(): Promise<void> {
634-
const backup = await this._backupFileService.resolve<CustomDocumentBackupData>(this.resource);
635-
this._fromBackup = !!backup;
636-
}
637-
638635
dispose() {
639636
if (this._editable) {
640637
this._undoService.removeElements(this._editorResource);
641638
}
642-
this._proxy.$disposeWebviewCustomEditorDocument(this._editorResource, this._viewType);
639+
this._proxy.$disposeCustomDocument(this._editorResource, this._viewType);
643640
super.dispose();
644641
}
645642

646643
//#region IWorkingCopy
647644

648645
public get resource() {
649646
// Make sure each custom editor has a unique resource for backup and edits
647+
return MainThreadCustomEditorModel.toWorkingCopyResource(this._viewType, this._editorResource);
648+
}
649+
650+
private static toWorkingCopyResource(viewType: string, resource: URI) {
650651
return URI.from({
651652
scheme: Schemas.vscodeCustomEditor,
652-
authority: this._viewType,
653-
path: this._editorResource.path,
654-
query: JSON.stringify(this._editorResource.toJSON()),
653+
authority: viewType,
654+
path: resource.path,
655+
query: JSON.stringify(resource.toJSON()),
655656
});
656657
}
657658

@@ -719,15 +720,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
719720
this.change(() => {
720721
--this._currentEditIndex;
721722
});
722-
await this._proxy.$undo(this._editorResource, this.viewType, undoneEdit, this.getEditState());
723-
}
724-
725-
private getEditState(): extHostProtocol.CustomDocumentEditState {
726-
return {
727-
allEdits: this._edits,
728-
currentIndex: this._currentEditIndex,
729-
saveIndex: this._savePoint,
730-
};
723+
await this._proxy.$undo(this._editorResource, this.viewType, undoneEdit, this.isDirty());
731724
}
732725

733726
private async redo(): Promise<void> {
@@ -744,7 +737,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
744737
this.change(() => {
745738
++this._currentEditIndex;
746739
});
747-
await this._proxy.$redo(this._editorResource, this.viewType, redoneEdit, this.getEditState());
740+
await this._proxy.$redo(this._editorResource, this.viewType, redoneEdit, this.isDirty());
748741
}
749742

750743
private spliceEdits(editToInsert?: number) {
@@ -779,16 +772,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
779772
return;
780773
}
781774

782-
let editsToUndo: number[] = [];
783-
let editsToRedo: number[] = [];
784-
785-
if (this._currentEditIndex >= this._savePoint) {
786-
editsToUndo = this._edits.slice(this._savePoint, this._currentEditIndex).reverse();
787-
} else if (this._currentEditIndex < this._savePoint) {
788-
editsToRedo = this._edits.slice(this._currentEditIndex, this._savePoint);
789-
}
790-
791-
this._proxy.$revert(this._editorResource, this.viewType, { undoneEdits: editsToUndo, redoneEdits: editsToRedo }, this.getEditState());
775+
this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None);
792776
this.change(() => {
793777
this._currentEditIndex = this._savePoint;
794778
this.spliceEdits();
@@ -838,6 +822,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
838822
meta: {
839823
viewType: this.viewType,
840824
editorResource: this._editorResource,
825+
backupId: '',
841826
extension: primaryEditor.extension ? {
842827
id: primaryEditor.extension.id.value,
843828
location: primaryEditor.extension.location,
@@ -864,10 +849,11 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
864849
this._hotExitState = pendingState;
865850

866851
try {
867-
await pendingState.operation;
852+
const backupId = await pendingState.operation;
868853
// Make sure state has not changed in the meantime
869854
if (this._hotExitState === pendingState) {
870855
this._hotExitState = HotExitState.Allowed;
856+
backupData.meta!.backupId = backupId;
871857
}
872858
} catch (e) {
873859
// Make sure state has not changed in the meantime

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
10391039
TimelineItem: extHostTypes.TimelineItem,
10401040
CellKind: extHostTypes.CellKind,
10411041
CellOutputKind: extHostTypes.CellOutputKind,
1042-
CustomDocument: extHostTypes.CustomDocument,
10431042
};
10441043
};
10451044
}

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -624,12 +624,6 @@ export interface WebviewPanelViewStateData {
624624
};
625625
}
626626

627-
export interface CustomDocumentEditState {
628-
readonly allEdits: readonly number[];
629-
readonly currentIndex: number;
630-
readonly saveIndex: number;
631-
}
632-
633627
export interface ExtHostWebviewsShape {
634628
$onMessage(handle: WebviewPanelHandle, message: any): void;
635629
$onMissingCsp(handle: WebviewPanelHandle, extensionId: string): void;
@@ -639,18 +633,18 @@ export interface ExtHostWebviewsShape {
639633
$deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise<void>;
640634

641635
$resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, cancellation: CancellationToken): Promise<void>;
642-
$createWebviewCustomEditorDocument(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<{ editable: boolean }>;
643-
$disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise<void>;
636+
$createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken): Promise<{ editable: boolean }>;
637+
$disposeCustomDocument(resource: UriComponents, viewType: string): Promise<void>;
644638

645-
$undo(resource: UriComponents, viewType: string, editId: number, state: CustomDocumentEditState): Promise<void>;
646-
$redo(resource: UriComponents, viewType: string, editId: number, state: CustomDocumentEditState): Promise<void>;
647-
$revert(resource: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }, state: CustomDocumentEditState): Promise<void>;
639+
$undo(resource: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void>;
640+
$redo(resource: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void>;
641+
$revert(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void>;
648642
$disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void;
649643

650644
$onSave(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void>;
651645
$onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise<void>;
652646

653-
$backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void>;
647+
$backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string>;
654648

655649
$onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise<void>;
656650
}

src/vs/workbench/api/common/extHostTypes.ts

Lines changed: 1 addition & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,15 @@
66
import { coalesce, equals } from 'vs/base/common/arrays';
77
import { escapeCodicons } from 'vs/base/common/codicons';
88
import { illegalArgument } from 'vs/base/common/errors';
9-
import { Emitter } from 'vs/base/common/event';
109
import { IRelativePattern } from 'vs/base/common/glob';
1110
import { isMarkdownString } from 'vs/base/common/htmlContent';
1211
import { startsWith } from 'vs/base/common/strings';
12+
import { isStringArray } from 'vs/base/common/types';
1313
import { URI } from 'vs/base/common/uri';
1414
import { generateUuid } from 'vs/base/common/uuid';
1515
import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files';
1616
import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
1717
import type * as vscode from 'vscode';
18-
import { Cache } from './cache';
19-
import { assertIsDefined, isStringArray } from 'vs/base/common/types';
20-
import { Schemas } from 'vs/base/common/network';
2118

2219
function es5ClassCompat(target: Function): any {
2320
///@ts-expect-error
@@ -2726,94 +2723,3 @@ export class TimelineItem implements vscode.TimelineItem {
27262723
}
27272724

27282725
//#endregion Timeline
2729-
2730-
//#region Custom Editors
2731-
2732-
interface EditState {
2733-
readonly allEdits: readonly number[];
2734-
readonly currentIndex: number;
2735-
readonly saveIndex: number;
2736-
}
2737-
2738-
export class CustomDocument<EditType = unknown> implements vscode.CustomDocument<EditType> {
2739-
2740-
readonly #edits = new Cache<EditType>('edits');
2741-
2742-
readonly #uri: vscode.Uri;
2743-
2744-
#editState: EditState = {
2745-
allEdits: [],
2746-
currentIndex: -1,
2747-
saveIndex: -1,
2748-
};
2749-
#isDisposed = false;
2750-
#version = 1;
2751-
2752-
constructor(uri: vscode.Uri) {
2753-
this.#uri = uri;
2754-
}
2755-
2756-
//#region Public API
2757-
2758-
public get uri(): vscode.Uri { return this.#uri; }
2759-
2760-
public get fileName(): string { return this.uri.fsPath; }
2761-
2762-
public get isUntitled() { return this.uri.scheme === Schemas.untitled; }
2763-
2764-
#onDidDispose = new Emitter<void>();
2765-
public readonly onDidDispose = this.#onDidDispose.event;
2766-
2767-
public get isClosed() { return this.#isDisposed; }
2768-
2769-
public get version() { return this.#version; }
2770-
2771-
public get isDirty() {
2772-
return this.#editState.currentIndex !== this.#editState.saveIndex;
2773-
}
2774-
2775-
public get appliedEdits() {
2776-
return this.#editState.allEdits.slice(0, this.#editState.currentIndex + 1)
2777-
.map(id => this._getEdit(id));
2778-
}
2779-
2780-
public get savedEdits() {
2781-
return this.#editState.allEdits.slice(0, this.#editState.saveIndex + 1)
2782-
.map(id => this._getEdit(id));
2783-
}
2784-
2785-
//#endregion
2786-
2787-
/** @internal */ _dispose(): void {
2788-
this.#isDisposed = true;
2789-
this.#onDidDispose.fire();
2790-
this.#onDidDispose.dispose();
2791-
}
2792-
2793-
/** @internal */ _updateEditState(state: EditState) {
2794-
++this.#version;
2795-
this.#editState = state;
2796-
}
2797-
2798-
/** @internal*/ _getEdit(editId: number): EditType {
2799-
return assertIsDefined(this.#edits.get(editId, 0));
2800-
}
2801-
2802-
/** @internal*/ _disposeEdits(editIds: number[]) {
2803-
for (const editId of editIds) {
2804-
this.#edits.delete(editId);
2805-
}
2806-
}
2807-
2808-
/** @internal*/ _addEdit(edit: EditType): number {
2809-
const id = this.#edits.add([edit]);
2810-
this._updateEditState({
2811-
allEdits: [...this.#editState.allEdits.slice(0, this.#editState.currentIndex + 1), id],
2812-
currentIndex: this.#editState.currentIndex + 1,
2813-
saveIndex: this.#editState.saveIndex,
2814-
});
2815-
return id;
2816-
}
2817-
}
2818-
2819-
// #endregion

0 commit comments

Comments
 (0)