diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index c525746b88c..426f7fc6f42 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1473,6 +1473,21 @@ declare module 'vscode' { * @return Thenable indicating that the webview editor has been resolved. */ resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel): Thenable; + + /** + * TODO: discuss this at api sync. + * + * Handle when the underlying resource for a custom editor is renamed. + * + * This allows the webview for the editor be preserved throughout the rename. If this method is not implemented, + * VS Code will destory the previous custom editor and create a replacement one. + * + * @param newDocument New text document to use for the custom editor. + * @param existingWebviewPanel Webview panel for the custom editor. + * + * @return Thenable indicating that the webview editor has been moved. + */ + moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel): Thenable; } namespace window { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 8efa4f7e543..9730d689c30 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -576,6 +576,10 @@ export interface WebviewExtensionDescription { readonly location: UriComponents; } +export interface CustomTextEditorCapabilities { + readonly supportsMove?: boolean; +} + export interface MainThreadWebviewsShape extends IDisposable { $createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void; $disposeWebview(handle: WebviewPanelHandle): void; @@ -591,7 +595,7 @@ export interface MainThreadWebviewsShape extends IDisposable { $registerSerializer(viewType: string): void; $unregisterSerializer(viewType: string): void; - $registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void; + $registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void; $registerCustomEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void; $unregisterEditorProvider(viewType: string): void; @@ -627,6 +631,8 @@ export interface ExtHostWebviewsShape { $onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents): Promise; $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; + + $onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise; } export interface MainThreadUrlsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index f4dc637a2fe..81cba4cd4b6 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -488,7 +488,9 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { const disposables = new DisposableStore(); if ('resolveCustomTextEditor' in provider) { disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); - this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options); + this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options, { + supportsMove: !!provider.moveCustomTextEditor, + }); } else { disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider)); this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options); @@ -663,6 +665,26 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { document._disposeEdits(editIds); } + async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) { + throw new Error(`Provider does not implement move '${viewType}'`); + } + + const webview = this.getWebviewPanel(handle); + if (!webview) { + throw new Error(`No webview found`); + } + + const resource = URI.revive(newResourceComponents); + const document = this._extHostDocuments.getDocument(resource); + await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview); + } + async $undo(resourceComponents: UriComponents, viewType: string, editId: number): Promise { const document = this.getCustomDocument(viewType, resourceComponents); return document._undo(editId); diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index ee79d227e26..94b8ea8294a 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -186,7 +186,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { } } - const newEditorInput = customEditorService.createInput(targetResource, toggleView, activeGroup); + const newEditorInput = customEditorService.createInput(targetResource, toggleView, activeGroup.id); editorService.replaceEditors([{ editor: activeEditor, diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 8073a67f522..39730562c4c 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -161,6 +161,12 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { } move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + if (!this._moveHandler) { + return { + editor: this.customEditorService.createInput(newResource, this.viewType, group) + }; + } + this._moveHandler(newResource); const newEditor = this.tryMoveWebview(group, newResource); if (!newEditor) { return; @@ -171,12 +177,12 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { private tryMoveWebview(groupId: GroupIdentifier, uri: URI, options?: ITextEditorOptions): IEditorInput | undefined { const editorInfo = this.customEditorService.getCustomEditor(this.viewType); if (editorInfo?.matches(uri)) { - const webview = assertIsDefined(this.takeOwnershipOfWebview()); const newInput = this.instantiationService.createInstance(CustomEditorInput, uri, this.viewType, this.id, - new Lazy(() => webview)); + new Lazy(() => undefined!)); // this webview is replaced in the transfer call + this.transfer(newInput); newInput.updateGroup(groupId); return newInput; } @@ -193,4 +199,21 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { assertIsDefined(this._modelRef); this.undoRedoService.redo(this.resource); } + + private _moveHandler?: (newResource: URI) => void; + + public onMove(handler: (newResource: URI) => void): void { + // TODO: Move this to the service + this._moveHandler = handler; + } + + protected transfer(other: CustomEditorInput): CustomEditorInput | undefined { + if (!super.transfer(other)) { + return; + } + + other._moveHandler = this._moveHandler; + this._moveHandler = undefined; + return other; + } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 630f17dd7b7..f5399eb8fe5 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -21,7 +21,7 @@ import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { EditorInput, EditorOptions, IEditorInput, IEditorPane } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorInput, IEditorPane, GroupIdentifier } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { webviewEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/browser/extensionPoint'; import { CONTEXT_CUSTOM_EDITORS, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, CustomEditorSelector, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; @@ -238,14 +238,14 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return this.promptOpenWith(resource, options, group); } - const input = this.createInput(resource, viewType, group); + const input = this.createInput(resource, viewType, group?.id); return this.openEditorForResource(resource, input, options, group); } public createInput( resource: URI, viewType: string, - group: IEditorGroup | undefined, + group: GroupIdentifier | undefined, options?: { readonly customClasses: string; }, ): IEditorInput { if (viewType === defaultEditorId) { @@ -257,8 +257,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return this.webviewService.createWebviewOverlay(id, { customClasses: options?.customClasses }, {}); }); const input = this.instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview); - if (group) { - input.updateGroup(group.id); + if (typeof group !== 'undefined') { + input.updateGroup(group); } return input; } @@ -327,7 +327,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ } const moveResult = editor.move(group.id, newResource); - const replacement = moveResult ? moveResult.editor : this.createInput(newResource, editor.viewType, group); + const replacement = moveResult ? moveResult.editor : this.createInput(newResource, editor.viewType, group.id); this.editorService.replaceEditors([{ editor: editor, @@ -492,7 +492,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo return undefined; } - const input = this.customEditorService.createInput(resource, bestAvailableEditor.id, group, { customClasses }); + const input = this.customEditorService.createInput(resource, bestAvailableEditor.id, group.id, { customClasses }); if (input instanceof EditorInput) { return input; } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 5893a51979d..78369fec628 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -6,14 +6,14 @@ import { distinct, mergeSort } from 'vs/base/common/arrays'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; +import { IDisposable, IReference } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorPane, IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IEditorInput, IEditorPane, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IDisposable, IReference } from 'vs/base/common/lifecycle'; export const ICustomEditorService = createDecorator('customEditorService'); @@ -29,7 +29,7 @@ export interface ICustomEditorService { getContributedCustomEditors(resource: URI): CustomEditorInfoCollection; getUserConfiguredCustomEditors(resource: URI): CustomEditorInfoCollection; - createInput(resource: URI, viewType: string, group: IEditorGroup | undefined, options?: { readonly customClasses: string }): IEditorInput; + createInput(resource: URI, viewType: string, group: GroupIdentifier | undefined, options?: { readonly customClasses: string }): IEditorInput; openWith(resource: URI, customEditorViewType: string, options?: ITextEditorOptions, group?: IEditorGroup): Promise; promptOpenWith(resource: URI, options?: ITextEditorOptions, group?: IEditorGroup): Promise; diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts index 22dc38e88e9..dd29d133c52 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts @@ -109,7 +109,8 @@ export class WebviewEditor extends BaseEditor { return; } - if (this.webview) { + const alreadyOwnsWebview = input instanceof WebviewInput && input.webview === this.webview; + if (this.webview && !alreadyOwnsWebview) { this.webview.release(this); } @@ -125,7 +126,9 @@ export class WebviewEditor extends BaseEditor { input.updateGroup(this.group.id); } - this.claimWebview(input); + if (!alreadyOwnsWebview) { + this.claimWebview(input); + } if (this._dimension) { this.layout(this._dimension); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts index 98ca51637c8..3da701349a1 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts @@ -18,8 +18,9 @@ export class WebviewInput extends EditorInput { private _iconPath?: WebviewIcons; private _group?: GroupIdentifier; - private readonly _webview: Lazy; - private _didSomeoneTakeMyWebview = false; + private _webview: Lazy; + + private _hasTransfered = false; get resource() { return URI.from({ @@ -42,8 +43,8 @@ export class WebviewInput extends EditorInput { dispose() { if (!this.isDisposed()) { - if (!this._didSomeoneTakeMyWebview) { - this._webview?.rawValue?.dispose(); + if (!this._hasTransfered) { + this._webview.rawValue?.dispose(); } } super.dispose(); @@ -107,11 +108,12 @@ export class WebviewInput extends EditorInput { return false; } - protected takeOwnershipOfWebview(): WebviewOverlay | undefined { - if (this._didSomeoneTakeMyWebview) { + protected transfer(other: WebviewInput): WebviewInput | undefined { + if (this._hasTransfered) { return undefined; } - this._didSomeoneTakeMyWebview = true; - return this.webview; + this._hasTransfered = true; + other._webview = this._webview; + return other; } } diff --git a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts index 3ffe992a8e1..b32a09cb805 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts @@ -113,11 +113,25 @@ export class LazilyResolvedWebviewEditorInput extends WebviewInput { super(id, viewType, name, webview, webviewService); } + #resolved = false; + @memoize public async resolve() { - await this._webviewWorkbenchService.resolveWebview(this); + if (!this.#resolved) { + this.#resolved = true; + await this._webviewWorkbenchService.resolveWebview(this); + } return super.resolve(); } + + protected transfer(other: LazilyResolvedWebviewEditorInput): WebviewInput | undefined { + if (!super.transfer(other)) { + return; + } + + other.#resolved = this.#resolved; + return other; + } }