New custom editor API proposal

For #77131

Fixes #93963
Fixes #94515
Fixes #94517
Fixes #94527
Fixes #94509
Fixes #94514
Fixes #93996
Fixes #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
This commit is contained in:
Matt Bierner
2020-04-08 17:53:28 -07:00
parent 1ed9dcda26
commit d4ce7148dd
10 changed files with 255 additions and 402 deletions

View File

@@ -18,6 +18,7 @@ import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor';
import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview';
import type * as vscode from 'vscode';
import { Cache } from './cache';
import * as extHostProtocol from './extHost.protocol';
import * as extHostTypes from './extHostTypes';
@@ -262,22 +263,78 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa
}
}
class WebviewDocumentStore {
private readonly _documents = new Map<string, extHostTypes.CustomDocument>();
class CustomDocumentStoreEntry {
public get(viewType: string, resource: vscode.Uri): extHostTypes.CustomDocument | undefined {
constructor(
public readonly document: vscode.CustomDocument,
) { }
private readonly _edits = new Cache<vscode.CustomDocumentEditEvent>('custom documents');
private _backup?: vscode.CustomDocumentBackup;
addEdit(item: vscode.CustomDocumentEditEvent): number {
return this._edits.add([item]);
}
async undo(editId: number, isDirty: boolean): Promise<void> {
await this.getEdit(editId).undo();
if (!isDirty) {
this.disposeBackup();
}
}
async redo(editId: number, isDirty: boolean): Promise<void> {
await this.getEdit(editId).redo();
if (!isDirty) {
this.disposeBackup();
}
}
disposeEdits(editIds: number[]): void {
for (const id of editIds) {
this._edits.delete(id);
}
}
updateBackup(backup: vscode.CustomDocumentBackup): void {
this._backup?.dispose();
this._backup = backup;
}
disposeBackup(): void {
this._backup?.dispose();
this._backup = undefined;
}
private getEdit(editId: number): vscode.CustomDocumentEditEvent {
const edit = this._edits.get(editId, 0);
if (!edit) {
throw new Error('No edit found');
}
return edit;
}
}
class CustomDocumentStore {
private readonly _documents = new Map<string, CustomDocumentStoreEntry>();
public get(viewType: string, resource: vscode.Uri): CustomDocumentStoreEntry | undefined {
return this._documents.get(this.key(viewType, resource));
}
public add(viewType: string, document: extHostTypes.CustomDocument) {
public add(viewType: string, document: vscode.CustomDocument): CustomDocumentStoreEntry {
const key = this.key(viewType, document.uri);
if (this._documents.has(key)) {
throw new Error(`Document already exists for viewType:${viewType} resource:${document.uri}`);
}
this._documents.set(key, document);
const entry = new CustomDocumentStoreEntry(document);
this._documents.set(key, entry);
return entry;
}
public delete(viewType: string, document: extHostTypes.CustomDocument) {
public delete(viewType: string, document: vscode.CustomDocument) {
const key = this.key(viewType, document.uri);
this._documents.delete(key);
}
@@ -285,6 +342,7 @@ class WebviewDocumentStore {
private key(viewType: string, resource: vscode.Uri): string {
return `${viewType}@@@${resource}`;
}
}
const enum WebviewEditorType {
@@ -342,7 +400,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
private readonly _editorProviders = new EditorProviderStore();
private readonly _documents = new WebviewDocumentStore();
private readonly _documents = new CustomDocumentStore();
constructor(
mainContext: extHostProtocol.IMainContext,
@@ -410,13 +468,6 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
} else {
disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider));
this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options);
if (provider.editingDelegate) {
disposables.add(provider.editingDelegate.onDidEdit(e => {
const document = e.document;
const editId = (document as extHostTypes.CustomDocument)._addEdit(e.edit);
this._proxy.$onDidEdit(document.uri, viewType, editId, e.label);
}));
}
}
return extHostTypes.Disposable.from(
@@ -504,7 +555,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
await serializer.deserializeWebviewPanel(revivedPanel, state);
}
async $createWebviewCustomEditorDocument(resource: UriComponents, viewType: string, cancellation: CancellationToken) {
async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) {
const entry = this._editorProviders.get(viewType);
if (!entry) {
throw new Error(`No provider found for '${viewType}'`);
@@ -515,14 +566,20 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
}
const revivedResource = URI.revive(resource);
const document = await entry.provider.openCustomDocument(revivedResource, cancellation);
this._documents.add(viewType, document as extHostTypes.CustomDocument);
return {
editable: !!entry.provider.editingDelegate,
};
const document = await entry.provider.openCustomDocument(revivedResource, { backupId }, cancellation);
const documentEntry = this._documents.add(viewType, document);
if (this.isEditable(document)) {
document.onDidEdit(e => {
const editId = documentEntry.addEdit(e);
this._proxy.$onDidEdit(document.uri, viewType, editId, e.label);
});
}
return { editable: this.isEditable(document) };
}
async $disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise<void> {
async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise<void> {
const entry = this._editorProviders.get(viewType);
if (!entry) {
throw new Error(`No provider found for '${viewType}'`);
@@ -533,9 +590,9 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
}
const revivedResource = URI.revive(resource);
const document = this.getCustomDocument(viewType, revivedResource);
const { document } = this.getCustomDocumentEntry(viewType, revivedResource);
this._documents.delete(viewType, document);
document._dispose();
document.dispose();
}
async $resolveWebviewEditor(
@@ -561,7 +618,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
switch (entry.type) {
case WebviewEditorType.Custom:
{
const document = this.getCustomDocument(viewType, revivedResource);
const { document } = this.getCustomDocumentEntry(viewType, revivedResource);
return entry.provider.resolveCustomEditor(document, revivedPanel, cancellation);
}
case WebviewEditorType.Text:
@@ -577,8 +634,8 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
}
$disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void {
const document = this.getCustomDocument(viewType, resourceComponents);
document._disposeEdits(editIds);
const document = this.getCustomDocumentEntry(viewType, resourceComponents);
document.disposeEdits(editIds);
}
async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise<void> {
@@ -601,69 +658,65 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview, CancellationToken.None);
}
async $undo(resourceComponents: UriComponents, viewType: string, editId: number, state: extHostProtocol.CustomDocumentEditState): Promise<void> {
const delegate = this.getEditingDelegate(viewType);
const document = this.getCustomDocument(viewType, resourceComponents);
document._updateEditState(state);
return delegate.undoEdits(document, [document._getEdit(editId)]);
async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
return entry.undo(editId, isDirty);
}
async $redo(resourceComponents: UriComponents, viewType: string, editId: number, state: extHostProtocol.CustomDocumentEditState): Promise<void> {
const delegate = this.getEditingDelegate(viewType);
const document = this.getCustomDocument(viewType, resourceComponents);
document._updateEditState(state);
return delegate.applyEdits(document, [document._getEdit(editId)]);
async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void> {
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
return entry.redo(editId, isDirty);
}
async $revert(resourceComponents: UriComponents, viewType: string, changes: { undoneEdits: number[], redoneEdits: number[] }, state: extHostProtocol.CustomDocumentEditState): Promise<void> {
const delegate = this.getEditingDelegate(viewType);
const document = this.getCustomDocument(viewType, resourceComponents);
const undoneEdits = changes.undoneEdits.map(id => document._getEdit(id));
const appliedEdits = changes.redoneEdits.map(id => document._getEdit(id));
document._updateEditState(state);
return delegate.revert(document, { undoneEdits, appliedEdits });
async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
const document = this.getEditableCustomDocument(viewType, resourceComponents);
await document.revert(cancellation);
entry.disposeBackup();
}
async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
const delegate = this.getEditingDelegate(viewType);
const document = this.getCustomDocument(viewType, resourceComponents);
return delegate.save(document, cancellation);
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
const document = this.getEditableCustomDocument(viewType, resourceComponents);
await document.save(cancellation);
entry.disposeBackup();
}
async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise<void> {
const delegate = this.getEditingDelegate(viewType);
const document = this.getCustomDocument(viewType, resourceComponents);
return delegate.saveAs(document, URI.revive(targetResource), cancellation);
const document = this.getEditableCustomDocument(viewType, resourceComponents);
return document.saveAs(URI.revive(targetResource), cancellation);
}
async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
const delegate = this.getEditingDelegate(viewType);
const document = this.getCustomDocument(viewType, resourceComponents);
return delegate.backup(document, cancellation);
async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string> {
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
const document = this.getEditableCustomDocument(viewType, resourceComponents);
const backup = await document.backup(cancellation);
entry.updateBackup(backup);
return backup.backupId;
}
private getWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): ExtHostWebviewEditor | undefined {
return this._webviewPanels.get(handle);
}
private getCustomDocument(viewType: string, resource: UriComponents): extHostTypes.CustomDocument {
const document = this._documents.get(viewType, URI.revive(resource));
if (!document) {
throw new Error('No webview editor custom document found');
private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry {
const entry = this._documents.get(viewType, URI.revive(resource));
if (!entry) {
throw new Error('No custom document found');
}
return document;
return entry;
}
private getEditingDelegate(viewType: string): vscode.CustomEditorEditingDelegate {
const entry = this._editorProviders.get(viewType);
if (!entry) {
throw new Error(`No provider found for '${viewType}'`);
private isEditable(document: vscode.CustomDocument): document is vscode.EditableCustomDocument {
return !!(document as vscode.EditableCustomDocument).onDidEdit;
}
private getEditableCustomDocument(viewType: string, resource: UriComponents): vscode.EditableCustomDocument {
const { document } = this.getCustomDocumentEntry(viewType, resource);
if (!this.isEditable(document)) {
throw new Error('Custom document is not editable');
}
const delegate = (entry.provider as vscode.CustomEditorProvider).editingDelegate;
if (!delegate) {
throw new Error(`Provider for ${viewType}' does not support editing`);
}
return delegate;
return document;
}
}