d6b3c6fa5f
Operator feedback on the Ask-Claude ergonomics (relates to #42/#43/#60):
- One user-facing command `cowriting.edit` ("Ask Claude to Edit") routes to the
selection or whole-document flow at runtime via the pure `routeEdit` helper
(selection → editSelection; none / tab right-click → editDocument). The two
underlying commands stay registered for the seams + E2E but are hidden from the
palette, and the split menu pairs collapse to one entry.
- Keybindings with `mac` variants (the missing variant is why ⌃⌥R "didn't work"
on macOS — Option combos are unreliable there): `cmd+alt+e`/`ctrl+alt+e` for
Ask-Claude-to-Edit, `cmd+alt+r`/`ctrl+alt+r` for the review panel.
- The instruction prompt now renders INLINE at the selection/cursor via a
dedicated Comments-API controller (`InlineAskController`, `cowriting.askClaude`)
instead of the top-center QuickInput — VS Code's Inline-Chat-style placement.
Both Ask-Claude entry points (editSelection + the preview's askClaude) share it.
Tests: routeEdit unit tests; E2E menu/palette assertions updated for the unified
command; E2E drives the inline prompt by stubbing `api.inlineAsk.prompt`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
3.7 KiB
TypeScript
85 lines
3.7 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { isAuthorable, isUnderRoot, routeEdit, selectionRejection } from "../src/workspacePath";
|
|
|
|
describe("isUnderRoot", () => {
|
|
const root = "/a/vscode-cowriting-plugin/sandbox";
|
|
|
|
it("accepts the root itself and files inside it", () => {
|
|
expect(isUnderRoot(root, root)).toBe(true);
|
|
expect(isUnderRoot(`${root}/docs/x.md`, root)).toBe(true);
|
|
});
|
|
|
|
it("rejects files outside the root", () => {
|
|
expect(isUnderRoot("/a/other/x.md", root)).toBe(false);
|
|
});
|
|
|
|
it("rejects a SIBLING whose path is a string-prefix of the root's parent (the prefix-collision bug)", () => {
|
|
// The reported case: EDH root is .../vscode-cowriting-plugin/sandbox, the file
|
|
// is in the sibling content repo. Plain startsWith on the plugin-repo prefix
|
|
// would falsely match; the separator boundary must reject it.
|
|
const pluginRoot = "/a/vscode-cowriting-plugin";
|
|
const contentFile = "/a/vscode-cowriting-plugin-content/issues/x.md";
|
|
expect(contentFile.startsWith(pluginRoot)).toBe(true); // the latent bug
|
|
expect(isUnderRoot(contentFile, pluginRoot)).toBe(false); // fixed
|
|
});
|
|
});
|
|
|
|
describe("isAuthorable", () => {
|
|
it("accepts file: and untitled: schemes", () => {
|
|
expect(isAuthorable("file")).toBe(true);
|
|
expect(isAuthorable("untitled")).toBe(true);
|
|
});
|
|
it("rejects read-only / virtual schemes", () => {
|
|
expect(isAuthorable("git")).toBe(false);
|
|
expect(isAuthorable("output")).toBe(false);
|
|
expect(isAuthorable("cowriting-baseline")).toBe(false);
|
|
expect(isAuthorable("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("selectionRejection — F8 widened (accepts out-of-folder + untitled)", () => {
|
|
const ok = { hasEditor: true, selectionEmpty: false, scheme: "file" };
|
|
|
|
it("accepts an in-folder / out-of-folder file (file: scheme)", () => {
|
|
expect(selectionRejection({ ...ok, scheme: "file" })).toBeNull();
|
|
});
|
|
it("accepts an untitled buffer (no longer rejected)", () => {
|
|
expect(selectionRejection({ ...ok, scheme: "untitled" })).toBeNull();
|
|
});
|
|
it("names the missing editor", () => {
|
|
expect(selectionRejection({ ...ok, hasEditor: false })).toMatch(/focus a text editor/i);
|
|
});
|
|
it("names the empty selection (only when there IS an editor)", () => {
|
|
expect(selectionRejection({ ...ok, selectionEmpty: true })).toMatch(/select some text/i);
|
|
});
|
|
it("rejects a non-{file,untitled} scheme with its own message (not 'select some text')", () => {
|
|
const msg = selectionRejection({ ...ok, scheme: "git" });
|
|
expect(msg).toMatch(/can.?t be edited|read-only|not a file/i);
|
|
expect(msg).not.toMatch(/select some text/i);
|
|
});
|
|
});
|
|
|
|
describe("routeEdit (single Ask-Claude-to-Edit gesture)", () => {
|
|
const focused = { hasUri: false, uriMatchesActiveEditor: false, hasActiveEditor: true, selectionEmpty: false };
|
|
|
|
it("non-empty selection in the focused editor → selection", () => {
|
|
expect(routeEdit(focused)).toBe("selection");
|
|
});
|
|
it("empty selection in the focused editor → document", () => {
|
|
expect(routeEdit({ ...focused, selectionEmpty: true })).toBe("document");
|
|
});
|
|
it("no active editor → document", () => {
|
|
expect(routeEdit({ ...focused, hasActiveEditor: false, selectionEmpty: true })).toBe("document");
|
|
});
|
|
it("tab right-click on a doc that ISN'T the focused editor → document (no selection to act on)", () => {
|
|
expect(routeEdit({ hasUri: true, uriMatchesActiveEditor: false, hasActiveEditor: true, selectionEmpty: false })).toBe(
|
|
"document",
|
|
);
|
|
});
|
|
it("tab right-click on the focused editor with a selection → selection", () => {
|
|
expect(routeEdit({ hasUri: true, uriMatchesActiveEditor: true, hasActiveEditor: true, selectionEmpty: false })).toBe(
|
|
"selection",
|
|
);
|
|
});
|
|
});
|