diff --git a/extensions/image-preview/src/extension.ts b/extensions/image-preview/src/extension.ts index cd68edf2e5c..81e2451e35b 100644 --- a/extensions/image-preview/src/extension.ts +++ b/extensions/image-preview/src/extension.ts @@ -23,8 +23,7 @@ export function activate(context: vscode.ExtensionContext) { PreviewManager.viewType, { async resolveWebviewEditor({ resource }, editor: vscode.WebviewPanel): Promise { - previewManager.resolve(resource, editor); - return {}; + return previewManager.resolve(resource, editor); } })); diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index 1b9c8d3f322..310043c4d37 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -28,7 +28,7 @@ export class PreviewManager { public resolve( resource: vscode.Uri, webviewEditor: vscode.WebviewPanel, - ) { + ): vscode.WebviewEditorCapabilities { const preview = new Preview(this.extensionRoot, resource, webviewEditor, this.sizeStatusBarEntry, this.zoomStatusBarEntry); this._previews.add(preview); this.setActivePreview(preview); @@ -42,6 +42,17 @@ export class PreviewManager { this.setActivePreview(undefined); } }); + + const onEdit = new vscode.EventEmitter<{ now: number }>(); + return { + editingCapability: { + onEdit: onEdit.event, + save: async () => { }, + hotExit: async () => { }, + applyEdits: async () => { }, + undoEdits: async (edits) => { console.log('undo', edits); }, + } + }; } public get activePreview() { return this._activePreview; } diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 907f258a0bc..5056c3feb61 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -27,6 +27,7 @@ import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { extHostNamedCustomer } from '../common/extHostCustomers'; +import { CustomEditorModel } from 'vs/workbench/contrib/customEditor/browser/customEditorModel'; /** * Bi-directional map between webview handles and inputs. @@ -94,6 +95,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private readonly _webviewInputs = new WebviewInputStore(); private readonly _revivers = new Map(); private readonly _editorProviders = new Map(); + private readonly _models = new Map(); constructor( context: extHostProtocol.IExtHostContext, @@ -261,7 +263,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma canResolve: (webviewInput) => { return webviewInput instanceof CustomFileEditorInput && webviewInput.viewType === viewType; }, - resolveWebview: async (webviewInput) => { + resolveWebview: async (webviewInput: CustomFileEditorInput) => { const handle = webviewInput.id; this._webviewInputs.add(handle, webviewInput); this.hookupWebviewEventDelegate(handle, webviewInput); @@ -269,6 +271,18 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma webviewInput.webview.options = options; webviewInput.webview.extension = extension; + const model = new CustomEditorModel(); + webviewInput.setModel(model); + this._models.set(handle, model); + + webviewInput.onDispose(() => { + this._models.delete(handle); + }); + + model.onUndo(edit => { + this._proxy.$undoEdits(handle, [edit]); + }); + try { await this._proxy.$resolveWebviewEditor( webviewInput.getResource(), @@ -296,11 +310,18 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._editorProviders.delete(viewType); } - public $onEdit(handle: extHostProtocol.WebviewPanelHandle, editJson: string): void { + public $onEdit(handle: extHostProtocol.WebviewPanelHandle, editData: string): void { const webview = this.getWebviewInput(handle); if (!(webview instanceof CustomFileEditorInput)) { - throw new Error(`Webview is not a webview editor`); + throw new Error('Webview is not a webview editor'); } + + const model = this._models.get(handle); + if (!model) { + throw new Error('Could not find model for webview editor'); + } + + model.makeEdit(editData); } private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0d286d7cc52..9bc433a1727 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -589,6 +589,7 @@ export interface ExtHostWebviewsShape { $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise; $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; + $undoEdits(handle: WebviewPanelHandle, edits: string[]): void; } export interface MainThreadUrlsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index b35c93e194d..7da97013142 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as modes from 'vs/editor/common/modes'; @@ -93,6 +93,7 @@ export class ExtHostWebview implements vscode.Webview { } export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPanel { + private readonly _handle: WebviewPanelHandle; private readonly _proxy: MainThreadWebviewsShape; private readonly _viewType: string; @@ -112,6 +113,7 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa readonly _onDidChangeViewStateEmitter = this._register(new Emitter()); public readonly onDidChangeViewState: Event = this._onDidChangeViewStateEmitter.event; + _capabilities: vscode.WebviewEditorCapabilities; constructor( handle: WebviewPanelHandle, @@ -233,8 +235,17 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa }); } - _addDisposable(disposable: IDisposable) { - this._register(disposable); + _setCapabilities(capabilities: vscode.WebviewEditorCapabilities) { + this._capabilities = capabilities; + if (capabilities.editingCapability) { + this._register(capabilities.editingCapability.onEdit(edit => { + this._proxy.$onEdit(this._handle, JSON.stringify(edit)); + })); + } + } + + _undoEdits(edits: string[]): void { + this._capabilities.editingCapability?.undoEdits(edits); } private assertNotDisposed() { @@ -402,10 +413,6 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { await serializer.deserializeWebviewPanel(revivedPanel, state); } - private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined { - return this._webviewPanels.get(handle); - } - async $resolveWebviewEditor( resource: UriComponents, handle: WebviewPanelHandle, @@ -424,19 +431,19 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { const revivedPanel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); this._webviewPanels.set(handle, revivedPanel); const capabilities = await provider.resolveWebviewEditor({ resource: URI.revive(resource) }, revivedPanel); - revivedPanel._addDisposable(this.hookupCapabilities(handle, capabilities)); + revivedPanel._setCapabilities(capabilities); } - private hookupCapabilities(handle: WebviewPanelHandle, capabilities: vscode.WebviewEditorCapabilities): IDisposable { - const disposables = new DisposableStore(); - - if (capabilities.editingCapability) { - disposables.add(capabilities.editingCapability.onEdit(edit => { - this._proxy.$onEdit(handle, JSON.stringify(edit)); - })); + $undoEdits(handle: WebviewPanelHandle, edits: string[]): void { + const panel = this.getWebviewPanel(handle); + if (!panel) { + return; } + panel._undoEdits(edits); + } - return disposables; + private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined { + return this._webviewPanels.get(handle); } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 24a42571e89..d89fcb9b26f 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -15,12 +15,14 @@ import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IEditorInput, Verbosity } from 'vs/workbench/common/editor'; import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; +import { CustomEditorModel } from './customEditorModel'; export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { public static typeId = 'workbench.editors.webviewEditor'; private readonly _editorResource: URI; + private _model?: CustomEditorModel; constructor( resource: URI, @@ -105,4 +107,16 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return this.longTitle; } } + + public setModel(model: CustomEditorModel) { + if (this._model) { + throw new Error('Model is already set'); + } + this._model = model; + this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + } + + public isDirty(): boolean { + return this._model ? this._model.isDirty() : false; + } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorModel.ts new file mode 100644 index 00000000000..003630bf19d --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorModel.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; + +type Edit = string; + +export class CustomEditorModel extends Disposable { + + private _currentEditIndex: number = 0; + private _savePoint: number = -1; + private _edits: Array = []; + + protected readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); + readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; + + protected readonly _onUndo: Emitter = this._register(new Emitter()); + readonly onUndo: Event = this._onUndo.event; + + public makeEdit(data: string): void { + this._edits.splice(this._currentEditIndex, this._edits.length - this._currentEditIndex, data); + this._currentEditIndex = this._edits.length - 1; + this.updateDirty(); + } + + public isDirty(): boolean { + return this._edits.length > 0 && this._savePoint !== this._edits.length; + } + + private updateDirty() { + this._onDidChangeDirty.fire(); + } + + public save() { + this._savePoint = this._edits.length; + this.updateDirty(); + } + + public undo() { + if (this._currentEditIndex >= 0) { + const undoneEdit = this._edits[this._currentEditIndex]; + --this._currentEditIndex; + this._onUndo.fire(undoneEdit); + } + this.updateDirty(); + } +}