Plan for #138525
Problem
Today, custom editor providers (CustomEditorProvider, CustomTextEditorProvider) can define how files are rendered and edited in a single editor. When a diff is opened for a file type handled by a custom editor, VS Code shows two side-by-side custom editor webviews wrapped in a standard DiffEditorInput. This works but has limitations:
- No custom diff rendering — the extension cannot provide a unified diff experience (e.g., overlaid image diffs, side-by-side binary comparisons with highlights, or structured document diffs)
- Two separate webviews — wasteful for cases where a single view could render a better diff
- No access to both documents simultaneously — each webview only sees one side
Proposal
Extend the existing custom editor provider interfaces with an optional resolveCustomDiffEditor method. When present, VS Code opens a single webview for diffs, passing both documents to the extension. The extension renders the diff however it sees fit.
This reuses the existing customEditors contribution point and CustomDocument model — no new contribution point needed.
Proposed API
// For CustomDocument-based providers
interface CustomReadonlyEditorProvider<T extends CustomDocument = CustomDocument> {
// existing
openCustomDocument(uri: Uri, openContext: CustomDocumentOpenContext, token: CancellationToken): Thenable<T> | T;
resolveCustomEditor(document: T, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void> | void;
// NEW — optional
resolveCustomDiffEditor?(originalDocument: T, modifiedDocument: T, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void> | void;
}
// Editing support inherited from CustomEditorProvider<T> — save/revert/undo/redo apply to the modified document
// For TextDocument-based providers
interface CustomTextEditorProvider {
// existing
resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void> | void;
// NEW — optional
resolveCustomTextDiffEditor?(originalDocument: TextDocument, modifiedDocument: TextDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void> | void;
}
How it works
- Extension registers a
CustomEditorProvider with resolveCustomDiffEditor implemented
- User opens a diff for a file matching the editor's selector
- Framework calls
openCustomDocument(uri) for both the original and modified URIs — same function, same document model, ref-counted as usual via CustomEditorModelManager
- Framework creates a single webview and calls
resolveCustomDiffEditor(originalDoc, modifiedDoc, webviewPanel)
- Extension renders a unified diff view in the webview
- Save/revert/undo/redo on the modified document work through the existing
CustomDocument infrastructure (onDidChangeCustomDocument, saveCustomDocument, etc.)
- Extensions that do not implement
resolveCustomDiffEditor fall back to the current behavior (two side-by-side custom editor webviews)
Key design decisions
- No new contribution point — the existing
customEditors selector already matches the file type for diffs
- Reuses
openCustomDocument — both sides get documents from the same openCustomDocument call; if the document is already open in a regular editor, the ref-counted model is shared
- Single webview — the diff editor gets one
WebviewPanel; the extension controls the entire diff rendering surface
- Editing on modified side — dirty state, save, revert, undo/redo all work through the existing
CustomDocument model on the modified document; the original side is read-only by convention
- Proposed API — behind
customDiffEditorProvider enablement flag
Implementation Plan
Phase 1: Proposed API Surface
- Create
src/vscode-dts/vscode.proposed.customDiffEditorProvider.d.ts with the interfaces above
- Regenerate
extensionsApiProposals.ts (run compile-api-proposal-names gulp task)
Phase 2: Protocol Layer
- Extend
ExtHostCustomEditorsShape in extHost.protocol.ts — add:
$resolveCustomDiffEditor(
originalResource: UriComponents,
modifiedResource: UriComponents,
newWebviewHandle: WebviewHandle,
viewType: string,
initData: { title: string; contentOptions: IWebviewContentOptions; options: IWebviewPanelOptions; active: boolean },
position: EditorGroupColumn,
cancellation: CancellationToken
): Promise<void>;
- Extend
$registerCustomEditorProvider / $registerTextEditorProvider to pass supportsDiffEditor: boolean via capabilities
- Implement
$resolveCustomDiffEditor in extHostCustomEditors.ts — create one webview panel, retrieve both documents, call the provider's resolveCustomDiffEditor
- Detect diff-capable providers during registration — check for
resolveCustomDiffEditor method on provider, pass supportsDiffEditor flag to main thread
Phase 3: Custom Diff Editor Input & Pane
Following the NotebookDiffEditorInput pattern:
- Create
CustomDiffEditorInput in src/vs/workbench/contrib/customEditor/browser/customDiffEditorInput.ts
- Extends
DiffEditorInput
- Holds
original: CustomEditorInput, modified: CustomEditorInput, plus a single IOverlayWebview
editorId returns this.modified.viewType (routes to the correct pane)
resolve() acquires models for both sides, resolves the webview
- Create
CustomDiffEditor pane in src/vs/workbench/contrib/customEditor/browser/customDiffEditor.ts
- Extends
EditorPane, renders the single webview
setInput() calls input.resolve(), claims the webview, manages layout
- Register pane in
customEditor.contribution.ts:
EditorPaneDescriptor.create(CustomDiffEditor, ...) bound to CustomDiffEditorInput
- Register serializer for
CustomDiffEditorInput
Phase 4: Main Thread Wiring
- Extend
CustomEditorCapabilities in customEditor.ts with supportsDiffEditor?: boolean
- Modify
createDiffEditorInput factory in customEditors.ts:
- If
supportsDiffEditor is true → create CustomDiffEditorInput (single webview)
- If false → existing behavior (two
CustomEditorInputs in standard DiffEditorInput)
- Extend
registerEditorProvider in mainThreadCustomEditors.ts:
- Register a webview resolver matching
CustomDiffEditorInput
resolveWebview: create models for both sides, call $resolveCustomDiffEditor on ext host
Phase 5: API Gate
- In
extHost.api.impl.ts, call checkProposedApiEnabled(extension, 'customDiffEditorProvider') when a diff-capable provider is detected
Files to change
| File |
Action |
src/vscode-dts/vscode.proposed.customDiffEditorProvider.d.ts |
NEW — proposed API types |
src/vs/platform/extensions/common/extensionsApiProposals.ts |
Auto-regenerated |
src/vs/workbench/api/common/extHost.protocol.ts |
Extend ExtHostCustomEditorsShape and MainThreadCustomEditorsShape |
src/vs/workbench/api/common/extHostCustomEditors.ts |
Implement $resolveCustomDiffEditor, detect diff providers |
src/vs/workbench/api/common/extHost.api.impl.ts |
Proposed API gate |
src/vs/workbench/api/browser/mainThreadCustomEditors.ts |
Diff webview resolution |
src/vs/workbench/contrib/customEditor/common/customEditor.ts |
Extend CustomEditorCapabilities |
src/vs/workbench/contrib/customEditor/browser/customEditors.ts |
Modify diff factory |
src/vs/workbench/contrib/customEditor/browser/customDiffEditorInput.ts |
NEW — diff input class |
src/vs/workbench/contrib/customEditor/browser/customDiffEditor.ts |
NEW — diff editor pane |
src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts |
Register pane + serializer |
src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts |
Add diff serializer |
Verification
- Compilation: Zero TypeScript errors via
VS Code - Build task
- Layering:
npm run valid-layers-check passes
- Existing tests:
src/vs/workbench/contrib/customEditor/test/ — no regressions
- Manual test: Extension registering
resolveCustomDiffEditor → single webview diff opens, save/revert works, omitting the method falls back to side-by-side
Plan for #138525
Problem
Today, custom editor providers (
CustomEditorProvider,CustomTextEditorProvider) can define how files are rendered and edited in a single editor. When a diff is opened for a file type handled by a custom editor, VS Code shows two side-by-side custom editor webviews wrapped in a standardDiffEditorInput. This works but has limitations:Proposal
Extend the existing custom editor provider interfaces with an optional
resolveCustomDiffEditormethod. When present, VS Code opens a single webview for diffs, passing both documents to the extension. The extension renders the diff however it sees fit.This reuses the existing
customEditorscontribution point andCustomDocumentmodel — no new contribution point needed.Proposed API
How it works
CustomEditorProviderwithresolveCustomDiffEditorimplementedopenCustomDocument(uri)for both the original and modified URIs — same function, same document model, ref-counted as usual viaCustomEditorModelManagerresolveCustomDiffEditor(originalDoc, modifiedDoc, webviewPanel)CustomDocumentinfrastructure (onDidChangeCustomDocument,saveCustomDocument, etc.)resolveCustomDiffEditorfall back to the current behavior (two side-by-side custom editor webviews)Key design decisions
customEditorsselector already matches the file type for diffsopenCustomDocument— both sides get documents from the sameopenCustomDocumentcall; if the document is already open in a regular editor, the ref-counted model is sharedWebviewPanel; the extension controls the entire diff rendering surfaceCustomDocumentmodel on the modified document; the original side is read-only by conventioncustomDiffEditorProviderenablement flagImplementation Plan
Phase 1: Proposed API Surface
src/vscode-dts/vscode.proposed.customDiffEditorProvider.d.tswith the interfaces aboveextensionsApiProposals.ts(runcompile-api-proposal-namesgulp task)Phase 2: Protocol Layer
ExtHostCustomEditorsShapeinextHost.protocol.ts— add:$registerCustomEditorProvider/$registerTextEditorProviderto passsupportsDiffEditor: booleanvia capabilities$resolveCustomDiffEditorinextHostCustomEditors.ts— create one webview panel, retrieve both documents, call the provider'sresolveCustomDiffEditorresolveCustomDiffEditormethod on provider, passsupportsDiffEditorflag to main threadPhase 3: Custom Diff Editor Input & Pane
Following the
NotebookDiffEditorInputpattern:CustomDiffEditorInputinsrc/vs/workbench/contrib/customEditor/browser/customDiffEditorInput.tsDiffEditorInputoriginal: CustomEditorInput,modified: CustomEditorInput, plus a singleIOverlayWebvieweditorIdreturnsthis.modified.viewType(routes to the correct pane)resolve()acquires models for both sides, resolves the webviewCustomDiffEditorpane insrc/vs/workbench/contrib/customEditor/browser/customDiffEditor.tsEditorPane, renders the single webviewsetInput()callsinput.resolve(), claims the webview, manages layoutcustomEditor.contribution.ts:EditorPaneDescriptor.create(CustomDiffEditor, ...)bound toCustomDiffEditorInputCustomDiffEditorInputPhase 4: Main Thread Wiring
CustomEditorCapabilitiesincustomEditor.tswithsupportsDiffEditor?: booleancreateDiffEditorInputfactory incustomEditors.ts:supportsDiffEditoris true → createCustomDiffEditorInput(single webview)CustomEditorInputs in standardDiffEditorInput)registerEditorProviderinmainThreadCustomEditors.ts:CustomDiffEditorInputresolveWebview: create models for both sides, call$resolveCustomDiffEditoron ext hostPhase 5: API Gate
extHost.api.impl.ts, callcheckProposedApiEnabled(extension, 'customDiffEditorProvider')when a diff-capable provider is detectedFiles to change
src/vscode-dts/vscode.proposed.customDiffEditorProvider.d.tssrc/vs/platform/extensions/common/extensionsApiProposals.tssrc/vs/workbench/api/common/extHost.protocol.tsExtHostCustomEditorsShapeandMainThreadCustomEditorsShapesrc/vs/workbench/api/common/extHostCustomEditors.ts$resolveCustomDiffEditor, detect diff providerssrc/vs/workbench/api/common/extHost.api.impl.tssrc/vs/workbench/api/browser/mainThreadCustomEditors.tssrc/vs/workbench/contrib/customEditor/common/customEditor.tsCustomEditorCapabilitiessrc/vs/workbench/contrib/customEditor/browser/customEditors.tssrc/vs/workbench/contrib/customEditor/browser/customDiffEditorInput.tssrc/vs/workbench/contrib/customEditor/browser/customDiffEditor.tssrc/vs/workbench/contrib/customEditor/browser/customEditor.contribution.tssrc/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.tsVerification
VS Code - Buildtasknpm run valid-layers-checkpassessrc/vs/workbench/contrib/customEditor/test/— no regressionsresolveCustomDiffEditor→ single webview diff opens, save/revert works, omitting the method falls back to side-by-side