diff --git a/README.md b/README.md index 73cccb5..1a4b979 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,10 @@ A clean, **zero-annotation editor** on the left; the rendered preview on the right as the **single interactive review surface**. The editor carries no attribution tint, no in-editor proposal threads, and no diff — all review lives in the preview, toggled by the **Annotations** switch in its header (on by -default). `Ctrl+Alt+R` opens **"Open Review Preview"**. +default). Open it via `Ctrl+Alt+R`, the editor title-bar button, or +**right-click a markdown file in the Explorer / its editor tab → +"Open Cowriting Review Panel"** (#41) — the right-click acts on the clicked +document, opening it first if needed. In the on-state the preview shows **green = human / blue = Claude / strikethrough = deleted**, and surfaces each of Claude's pending F4 proposals as diff --git a/package.json b/package.json index d845587..969c54a 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ }, { "command": "cowriting.showTrackChangesPreview", - "title": "Cowriting: Open Review Preview", + "title": "Open Cowriting Review Panel", "category": "Cowriting" }, { @@ -119,6 +119,20 @@ "group": "navigation@9" } ], + "editor/title/context": [ + { + "command": "cowriting.showTrackChangesPreview", + "when": "resourceLangId == markdown", + "group": "1_cowriting" + } + ], + "explorer/context": [ + { + "command": "cowriting.showTrackChangesPreview", + "when": "resourceLangId == markdown", + "group": "navigation@9" + } + ], "editor/context": [ { "command": "cowriting.editSelection", diff --git a/src/trackChangesPreview.ts b/src/trackChangesPreview.ts index 85afc8e..680b490 100644 --- a/src/trackChangesPreview.ts +++ b/src/trackChangesPreview.ts @@ -71,9 +71,16 @@ export class TrackChangesPreviewController implements vscode.Disposable { this.disposables.push( // F11 (SLICE-5): the editor/title gateway passes the tab's resource Uri; // the palette / keybinding pass nothing → fall back to the active editor. - vscode.commands.registerCommand("cowriting.showTrackChangesPreview", (uri?: vscode.Uri) => { - const byUri = uri && vscode.workspace.textDocuments.find((d) => d.uri.toString() === uri.toString()); - this.show(byUri || vscode.window.activeTextEditor?.document); + // #41: the explorer/tab right-click also passes the clicked Uri, which may + // not be an open document yet — open it so we preview the clicked file, not + // whatever happens to be the active editor. + vscode.commands.registerCommand("cowriting.showTrackChangesPreview", async (uri?: vscode.Uri) => { + if (uri) { + const open = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uri.toString()); + this.show(open ?? (await vscode.workspace.openTextDocument(uri))); + return; + } + this.show(vscode.window.activeTextEditor?.document); }), // F11: document-scoped Ask-Claude (also reused by #42's gateway). Edits the // active markdown doc; the rewrite is diffed into per-hunk F4 proposals. diff --git a/test/e2e/suite/reviewPanelMenu.test.ts b/test/e2e/suite/reviewPanelMenu.test.ts new file mode 100644 index 0000000..41c0d5d --- /dev/null +++ b/test/e2e/suite/reviewPanelMenu.test.ts @@ -0,0 +1,116 @@ +import * as assert from "assert"; +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; +import type { CowritingApi } from "../../../src/extension"; + +const WS = process.env.E2E_WORKSPACE!; +const settle = () => new Promise((r) => setTimeout(r, 400)); + +async function getApi(): Promise { + const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!; + const api = (await ext.activate()) as CowritingApi; + assert.ok(api?.trackChangesPreviewController, "exports preview controller"); + return api; +} + +/** Write a markdown file under WS and return its uri (without opening it). */ +function writeFile(rel: string, body: string): vscode.Uri { + const abs = path.join(WS, rel); + fs.mkdirSync(path.dirname(abs), { recursive: true }); + fs.writeFileSync(abs, body, "utf8"); + return vscode.Uri.file(abs); +} + +function pkg(): any { + return JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8")); +} + +// #41 (story): "Open Cowriting Review Panel" reachable from the markdown +// file/tab right-click menu. The review surface (showTrackChangesPreview) is the +// plugin's central affordance; this adds explorer/context + editor/title/context +// entry points and makes the command open the *clicked* document — not merely +// the active editor — so the explorer right-click works even when the file is +// not already open. +suite("#41 review-panel right-click entry (host E2E — menu wiring + clicked-doc resolution)", () => { + test("command with a Uri previews the CLICKED doc, not the active editor", async () => { + // Two markdown docs; A is the active editor, B is the right-clicked target. + const aUri = writeFile("docs/menu-active.md", "# Active\n\nThe active editor's document.\n"); + const bUri = writeFile("docs/menu-clicked.md", "# Clicked\n\nThe right-clicked document.\n"); + const aDoc = await vscode.workspace.openTextDocument(aUri); + await vscode.window.showTextDocument(aDoc); + const bDoc = await vscode.workspace.openTextDocument(bUri); // open but NOT active + await settle(); + const api = await getApi(); + + await vscode.commands.executeCommand("cowriting.showTrackChangesPreview", bUri); + await settle(); + + assert.strictEqual(api.trackChangesPreviewController.isOpen(bUri.toString()), true, "preview opened for the clicked doc"); + assert.strictEqual( + api.trackChangesPreviewController.isOpen(aUri.toString()), + false, + "the active editor's doc did NOT get a preview", + ); + assert.ok(bDoc, "clicked doc handle held"); + }); + + test("command with a Uri for a not-yet-open file opens it and previews (explorer case)", async () => { + // Write a file but do not openTextDocument it — mimics an Explorer right-click. + const uri = writeFile("docs/menu-unopened.md", "# Unopened\n\nNever opened before the right-click.\n"); + const key = uri.toString(); + const api = await getApi(); + assert.ok( + !vscode.workspace.textDocuments.some((d) => d.uri.toString() === key), + "precondition: the file is not an open document", + ); + + await vscode.commands.executeCommand("cowriting.showTrackChangesPreview", uri); + await settle(); + + assert.strictEqual(api.trackChangesPreviewController.isOpen(key), true, "the clicked-but-unopened file got a preview"); + }); + + test("no-arg invocation still falls back to the active editor (palette / keybinding unchanged)", async () => { + const uri = writeFile("docs/menu-noarg.md", "# No-arg\n\nActive editor for the no-arg path.\n"); + const doc = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(doc); + await settle(); + const api = await getApi(); + + await vscode.commands.executeCommand("cowriting.showTrackChangesPreview"); + await settle(); + + assert.strictEqual(api.trackChangesPreviewController.isOpen(uri.toString()), true, "no-arg previews the active editor"); + }); + + test("explorer/context contributes the review-panel item for markdown only", () => { + const items = (pkg().contributes.menus["explorer/context"] ?? []) as Array<{ command: string; when?: string }>; + const entry = items.find((m) => m.command === "cowriting.showTrackChangesPreview"); + assert.ok(entry, "explorer/context has a showTrackChangesPreview entry"); + assert.match(entry!.when ?? "", /resourceLangId == markdown/, "gated on markdown resources"); + }); + + test("editor/title/context (tab right-click) contributes the review-panel item for markdown only", () => { + const items = (pkg().contributes.menus["editor/title/context"] ?? []) as Array<{ command: string; when?: string }>; + const entry = items.find((m) => m.command === "cowriting.showTrackChangesPreview"); + assert.ok(entry, "editor/title/context has a showTrackChangesPreview entry"); + assert.match(entry!.when ?? "", /resourceLangId == markdown/, "gated on markdown tabs"); + }); + + test("the command title reads 'Open Cowriting Review Panel'", () => { + const cmd = (pkg().contributes.commands as Array<{ command: string; title: string }>).find( + (c) => c.command === "cowriting.showTrackChangesPreview", + ); + assert.ok(cmd, "command is contributed"); + assert.strictEqual(cmd!.title, "Open Cowriting Review Panel", "menus render this title"); + }); + + test("the ctrl+alt+r keybinding still targets the command (unchanged)", () => { + const kb = (pkg().contributes.keybindings as Array<{ command: string; key: string; when?: string }>).find( + (k) => k.command === "cowriting.showTrackChangesPreview", + ); + assert.ok(kb, "keybinding still present"); + assert.strictEqual(kb!.key, "ctrl+alt+r", "key unchanged"); + }); +});