From a0e4d78cf7c8377162af315c7aa8211542e26ed8 Mon Sep 17 00:00:00 2001 From: Wolfram Arnold Date: Thu, 23 Jul 2026 17:54:07 +0200 Subject: [PATCH] Offer the rendered diff in the editor context menu for committed diffs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After #60 the title-bar button renders a committed revision diff, but right-clicking inside that diff editor still did not offer "Show Rich Markdown Diff" — the button uses the built-in `isInDiffEditor` context, while the context menu is gated on `rich-markdown-diff.canShowRenderedDiff`, and the two disagreed. The context key is computed by `updateRenderedDiffContext`, which — like the original #59 bug — rebuilds the active editor's `git:` URI as a plain `file:` URI, dropping the commit ref. The comparison then resolves as HEAD-vs-working-tree, a committed blob is not dirty, and `isActionableSingleFileComparison` returns false, so the key stayed off. - Add `getActiveRevisionComparison`, reusing #60's `getActiveDiffTabUriPair` and `getRevisionComparison`, to report the revision diff the active tab is showing. - In `updateRenderedDiffContext`, short-circuit to canShow = true when the active tab is a markdown revision diff, before the ref-dropping fallback runs. The markdown-path check stops a non-markdown diff in another editor group from enabling the key. - Factor the repeated "set the context key only if it changed, and only if this update has not been superseded" logic into `setCanShowRenderedDiff`, and use it at the existing call sites. Fixes #61 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/commandTarget.ts | 21 +++++++++ src/extension.ts | 64 +++++++++++++++++++++------- src/test/suite/commandTarget.test.ts | 57 +++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 16 deletions(-) diff --git a/src/commandTarget.ts b/src/commandTarget.ts index 247b72c..8811100 100644 --- a/src/commandTarget.ts +++ b/src/commandTarget.ts @@ -330,6 +330,27 @@ export function getActiveDiffTabUriPair(): ComparisonUriPair | undefined { return undefined; } +/** + * Reports the revision comparison shown by the diff editor that currently has + * focus, if it is one. + * + * This is what lets the editor context menu offer the rendered diff while a + * committed revision diff is open: unlike the working tree and index, a commit + * cannot be recovered from the file on disk, so it has to be read from the tab. + * + * @returns The two sides when the active tab is a revision diff, otherwise + * undefined. + */ +export function getActiveRevisionComparison(): ComparisonUriPair | undefined { + const pair = getActiveDiffTabUriPair(); + + if (!pair) { + return undefined; + } + + return getRevisionComparison(pair.originalUri, pair.modifiedUri); +} + export const __test__ = { extractComparisonUris, inferComparisonHint, diff --git a/src/extension.ts b/src/extension.ts index dabb3cb..af1516b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -35,6 +35,7 @@ import { CommandTarget, ComparisonUriPair, getActiveDiffTabUriPair, + getActiveRevisionComparison, getCommandTarget, getFileUriFromCommandArg, getRevisionComparison, @@ -472,6 +473,34 @@ function isActionableSingleFileComparison( return comparison.kind !== "cleanHeadToWorkingTree" || isDirty; } +/** + * Sets the `canShowRenderedDiff` context key, but only when the value actually + * changed and this update has not been superseded by a newer one. + * + * @param canShow - Whether the rendered diff command should be offered. + * @param generation - The generation this update belongs to; a mismatch means a + * later update already ran, so this one is dropped. + */ +async function setCanShowRenderedDiff( + canShow: boolean, + generation: number, +): Promise { + if (generation !== contextUpdateGeneration) { + return; + } + + if (lastCanShowRenderedDiff === canShow) { + return; + } + + lastCanShowRenderedDiff = canShow; + await vscode.commands.executeCommand( + "setContext", + "rich-markdown-diff.canShowRenderedDiff", + canShow, + ); +} + async function updateRenderedDiffContext( editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor, ) { @@ -497,14 +526,7 @@ async function updateRenderedDiffContext( if (!hasVisibleMarkdownEditor) { activeEditorRepositorySubscription?.dispose(); activeEditorRepositorySubscription = undefined; - if (lastCanShowRenderedDiff !== false) { - lastCanShowRenderedDiff = false; - await vscode.commands.executeCommand( - "setContext", - "rich-markdown-diff.canShowRenderedDiff", - false, - ); - } + await setCanShowRenderedDiff(false, myGen); } else if (!lastCanShowRenderedDiff) { // A markdown editor is visible but context was previously disabled or // uninitialized — re-evaluate by finding the visible markdown editor. @@ -520,6 +542,23 @@ async function updateRenderedDiffContext( return; } + // A committed revision diff (opened from the Source Control Graph, say) is + // renderable, but the comparison resolved below only sees the working tree + // and index and would classify the file as unchanged. Detect it from the + // active diff tab so the editor context menu offers the command too, matching + // the title bar button that the diff editor already shows. The markdown check + // stops a non-markdown diff in another editor group from enabling the key. + const activeRevision = getActiveRevisionComparison(); + if ( + activeRevision && + isMarkdownPath(toFileBackedUri(activeRevision.modifiedUri).fsPath) + ) { + activeEditorRepositorySubscription?.dispose(); + activeEditorRepositorySubscription = undefined; + await setCanShowRenderedDiff(true, myGen); + return; + } + // Normalize git: / vscode-userdata: URIs to file: URIs so the git API // can locate the repository (e.g. when the built-in diff editor is focused). const editorUri = editor.document.uri; @@ -545,14 +584,7 @@ async function updateRenderedDiffContext( comparison, editor.document.isDirty, ); - if (lastCanShowRenderedDiff !== canShow) { - lastCanShowRenderedDiff = canShow; - await vscode.commands.executeCommand( - "setContext", - "rich-markdown-diff.canShowRenderedDiff", - canShow, - ); - } + await setCanShowRenderedDiff(canShow, myGen); if (myGen !== contextUpdateGeneration) { return; diff --git a/src/test/suite/commandTarget.test.ts b/src/test/suite/commandTarget.test.ts index 6f41c7a..1eb12a4 100644 --- a/src/test/suite/commandTarget.test.ts +++ b/src/test/suite/commandTarget.test.ts @@ -30,6 +30,7 @@ import * as vscode from "vscode"; import { __test__, getActiveDiffTabUriPair, + getActiveRevisionComparison, getCommandTarget, getRevisionComparison, refersToSameFile, @@ -312,3 +313,59 @@ describe("Same File Detection", () => { ); }); }); + +describe("Active Revision Comparison", () => { + const fileUri = vscode.Uri.file( + path.join(os.tmpdir(), "rmd-active-rev", "doc.md"), + ); + const parentSha = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678"; + const commitSha = "c02e3e4a1b2c3d4e5f60718293a4b5c6d7e8f901"; + + const gitUri = (ref: string) => + fileUri.with({ + scheme: "git", + query: JSON.stringify({ path: fileUri.fsPath, ref }), + }); + + afterEach(async () => { + await vscode.commands.executeCommand("workbench.action.closeAllEditors"); + }); + + it("detects a commit-to-commit diff tab", async () => { + await vscode.commands.executeCommand( + "vscode.diff", + gitUri(parentSha), + gitUri(commitSha), + "doc.md (commit)", + ); + + const revision = getActiveRevisionComparison(); + + assert.ok(revision, "Expected a revision comparison"); + assert.strictEqual( + revision?.modifiedUri.toString(), + gitUri(commitSha).toString(), + ); + }); + + it("ignores a HEAD-to-working-tree diff tab", async () => { + await vscode.commands.executeCommand( + "vscode.diff", + gitUri("HEAD"), + fileUri, + "doc.md (changes)", + ); + + assert.strictEqual(getActiveRevisionComparison(), undefined); + }); + + it("ignores a plain markdown editor", async () => { + const document = await vscode.workspace.openTextDocument({ + language: "markdown", + content: "# Heading\n", + }); + await vscode.window.showTextDocument(document); + + assert.strictEqual(getActiveRevisionComparison(), undefined); + }); +});