From 53ee45f63e8fac1afb5fbe99bc3638c7f93f2f3f Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 7 Jan 2026 10:21:58 +0100 Subject: [PATCH 1/2] Fix a set of issues around transfer and save of custom editor documents --- .../workbench/api/browser/mainThreadCustomEditors.ts | 11 +++++++++++ src/vs/workbench/api/common/extHostCustomEditors.ts | 10 +++++++--- .../customEditor/browser/customEditorInput.ts | 9 ++++++++- .../untitled/common/untitledTextEditorModel.ts | 12 ++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 74cacd27427..95ab653c010 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -176,6 +176,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc const disposeSub = webviewInput.webview.onDidDispose(() => { disposeSub.dispose(); + inputDisposeSub.dispose(); // If the model is still dirty, make sure we have time to save it if (modelRef.object.isDirty()) { @@ -191,6 +192,14 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc modelRef.dispose(); }); + // Also listen for when the input is disposed (e.g., during SaveAs when the webview is transferred to a new editor). + // In this case, webview.onDidDispose won't fire because the webview is reused. + const inputDisposeSub = webviewInput.onWillDispose(() => { + inputDisposeSub.dispose(); + disposeSub.dispose(); + modelRef.dispose(); + }); + if (capabilities.supportsMove) { webviewInput.onMove(async (newResource: URI) => { const oldModel = modelRef; @@ -647,7 +656,9 @@ class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustom // TODO: handle cancellation await createCancelablePromise(token => this._proxy.$onSaveAs(this._editorResource, this.viewType, targetResource, token)); this.change(() => { + this._isDirtyFromContentChange = false; this._savePoint = this._currentEditIndex; + this._fromBackup = false; }); return true; } else { diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts index 458795180b0..80dcb8f675f 100644 --- a/src/vs/workbench/api/common/extHostCustomEditors.ts +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -104,8 +104,10 @@ class CustomDocumentStore { return entry; } - public delete(viewType: string, document: vscode.CustomDocument) { - const key = this.key(viewType, document.uri); + public delete(viewType: string, resource: vscode.Uri) { + // Use the resource parameter directly instead of document.uri, because the document's + // URI may have changed (e.g., after SaveAs from untitled to a file path). + const key = this.key(viewType, resource); this._documents.delete(key); } @@ -242,7 +244,9 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor const revivedResource = URI.revive(resource); const { document } = this.getCustomDocumentEntry(viewType, revivedResource); - this._documents.delete(viewType, document); + // Pass the resource we used to look up the document, not document.uri, + // because the document's URI may have changed (e.g., after SaveAs). + this._documents.delete(viewType, revivedResource); document.dispose(); } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 29382bf5aa9..47baf0e3df9 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -52,8 +52,15 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { ): EditorInput { return instantiationService.invokeFunction(accessor => { // If it's an untitled file we must populate the untitledDocumentData - const untitledString = accessor.get(IUntitledTextEditorService).getValue(init.resource); + const untitledTextEditorService = accessor.get(IUntitledTextEditorService); + const untitledTextModel = untitledTextEditorService.get(init.resource); + const untitledString = untitledTextModel?.textEditorModel?.getValue(); const untitledDocumentData = untitledString ? VSBuffer.fromString(untitledString) : undefined; + + // If we're taking over an untitled text editor, mark its content as transferred + // so it's no longer tracked as a dirty working copy (fixes #125293). + untitledTextModel?.markContentTransferred(); + const webview = accessor.get(IWebviewService).createWebviewOverlay({ providedViewType: init.viewType, title: init.webviewTitle, diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index d167c5dc4bd..19af22b6388 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -61,6 +61,12 @@ export interface IUntitledTextEditorModel extends ITextEditorModel, ILanguageSup */ setEncoding(encoding: string): Promise; + /** + * Marks the content as transferred to another editor (e.g. custom editor). + * This clears the dirty state without triggering revert behavior. + */ + markContentTransferred(): void; + /** * Resolves the untitled model. */ @@ -268,6 +274,12 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this._onDidChangeDirty.fire(); } + markContentTransferred(): void { + // Clear dirty state without triggering revert. + // Used when content is transferred to another editor (e.g. custom editor). + this.setDirty(false); + } + //#endregion //#region Save / Revert / Backup From 09bdc4bbedb072c3f591f21651b9914804e414f8 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 7 Jan 2026 11:50:43 +0100 Subject: [PATCH 2/2] PR feedback --- .../customEditor/browser/customEditorInput.ts | 6 +++--- .../untitled/common/untitledTextEditorModel.ts | 12 ------------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 47baf0e3df9..cef8490460b 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -57,9 +57,9 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { const untitledString = untitledTextModel?.textEditorModel?.getValue(); const untitledDocumentData = untitledString ? VSBuffer.fromString(untitledString) : undefined; - // If we're taking over an untitled text editor, mark its content as transferred - // so it's no longer tracked as a dirty working copy (fixes #125293). - untitledTextModel?.markContentTransferred(); + // If we're taking over an untitled text editor, revert it so it's no longer + // tracked as a dirty working copy (fixes #125293). + untitledTextModel?.revert(); const webview = accessor.get(IWebviewService).createWebviewOverlay({ providedViewType: init.viewType, diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 19af22b6388..d167c5dc4bd 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -61,12 +61,6 @@ export interface IUntitledTextEditorModel extends ITextEditorModel, ILanguageSup */ setEncoding(encoding: string): Promise; - /** - * Marks the content as transferred to another editor (e.g. custom editor). - * This clears the dirty state without triggering revert behavior. - */ - markContentTransferred(): void; - /** * Resolves the untitled model. */ @@ -274,12 +268,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this._onDidChangeDirty.fire(); } - markContentTransferred(): void { - // Clear dirty state without triggering revert. - // Used when content is transferred to another editor (e.g. custom editor). - this.setDirty(false); - } - //#endregion //#region Save / Revert / Backup