89 lines
3.7 KiB
TypeScript
89 lines
3.7 KiB
TypeScript
/**
|
|
* workspacePath — pure, vscode-free helpers for the "is this document one
|
|
* Cowriting can anchor to?" decision (F2/F3/F4 require a SAVED file under the
|
|
* workspace folder, because threads/attribution/proposals persist to a
|
|
* `.threads/` sidecar beside that file).
|
|
*
|
|
* Split out so the membership test and the per-condition warning are
|
|
* deterministic and unit-testable with no editor — see `test/workspacePath.test.ts`.
|
|
*/
|
|
import * as path from "node:path";
|
|
|
|
/**
|
|
* Is `fsPath` the workspace root or a path strictly inside it?
|
|
*
|
|
* Uses a path-separator boundary, NOT a bare `startsWith(root)` — otherwise a
|
|
* sibling whose name merely begins with the root's name would falsely match
|
|
* (e.g. `.../vscode-cowriting-plugin-content/x` vs root `.../vscode-cowriting-plugin`).
|
|
*/
|
|
export function isUnderRoot(fsPath: string, root: string): boolean {
|
|
return fsPath === root || fsPath.startsWith(root + path.sep);
|
|
}
|
|
|
|
/**
|
|
* Documents Cowriting can author on: a saved file OR an unsaved buffer (F8 §6.2).
|
|
* F8 widened membership from "saved file under the workspace folder" to any
|
|
* `file:`/`untitled:` doc — the SidecarRouter then routes where its artifact is
|
|
* stored (repo `.threads/` in-workspace, global storage otherwise). `isUnderRoot`
|
|
* (above) is retained as that ROUTING input, no longer an authoring gate.
|
|
*/
|
|
export function isAuthorable(scheme: string): boolean {
|
|
return scheme === "file" || scheme === "untitled";
|
|
}
|
|
|
|
export interface SelectionContext {
|
|
/** Is there an active text editor at all? */
|
|
hasEditor: boolean;
|
|
/** Is the active editor's selection empty (no highlight)? */
|
|
selectionEmpty: boolean;
|
|
/** The active document's URI scheme (`file`, `untitled`, …). */
|
|
scheme: string;
|
|
}
|
|
|
|
/**
|
|
* Why a selection can't be sent to Claude — or `null` if it can. F8 widened the
|
|
* membership: any `file:` or `untitled:` document is authorable (in-workspace
|
|
* files persist to the repo `.threads/` sidecar, out-of-workspace/untitled to
|
|
* global storage — the router decides). Only a non-{file,untitled} scheme (a
|
|
* read-only `git:`/`output:` view), a missing editor, or an empty selection is
|
|
* refused — each with its OWN message (#24's per-condition messaging).
|
|
*/
|
|
export function selectionRejection(ctx: SelectionContext): string | null {
|
|
if (!ctx.hasEditor) {
|
|
return "Cowriting: focus a text editor with a selection first.";
|
|
}
|
|
if (ctx.selectionEmpty) {
|
|
return "Cowriting: select some text to send to Claude first.";
|
|
}
|
|
if (!isAuthorable(ctx.scheme)) {
|
|
return "Cowriting: this kind of document can't be edited — Cowriting authors on a file or an untitled buffer, not a read-only view.";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export interface EditRouteContext {
|
|
/** Was the command invoked on a specific resource (a tab right-click)? */
|
|
hasUri: boolean;
|
|
/** Does that resource match the focused editor's document? */
|
|
uriMatchesActiveEditor: boolean;
|
|
/** Is there a focused text editor at all? */
|
|
hasActiveEditor: boolean;
|
|
/** Is the focused editor's selection empty (no highlight)? */
|
|
selectionEmpty: boolean;
|
|
}
|
|
|
|
/**
|
|
* Route the single "Ask Claude to Edit" gesture (`cowriting.edit`) to the
|
|
* selection or whole-document flow. One conceptual command, two destinations:
|
|
*
|
|
* - A tab right-click on a document that ISN'T the focused editor has no
|
|
* selection to act on → edit the whole document.
|
|
* - Otherwise a non-empty selection in the focused editor → edit the selection;
|
|
* an empty selection (or no editor) → edit the whole document.
|
|
*/
|
|
export function routeEdit(ctx: EditRouteContext): "selection" | "document" {
|
|
if (ctx.hasUri && !ctx.uriMatchesActiveEditor) return "document";
|
|
if (ctx.hasActiveEditor && !ctx.selectionEmpty) return "selection";
|
|
return "document";
|
|
}
|