3520397e41
F10 (#29) made the rendered preview the single review surface and hid the F6 two-pane vscode.diff view (command + ctrl+alt+d set when:false). This removes that now-unreachable view code: - DiffViewController: drop toggle/findDiffTab/epochLabel/isDiffOpen, the `cowriting-baseline:` TextDocumentContentProvider + BASELINE_SCHEME + baselineUri + the content-provider change emitter, and the toggleDiffView command. The baseline DATA layer is fully intact — ensureBaseline/advance/pin/capture, getBaseline, baselineFilePath, onDidChangeBaseline, persistence (INV-19), and the machine-landing auto-advance (INV-18) that F7/F10 consume. - package.json: remove the toggleDiffView command, its commandPalette entry, and the ctrl+alt+d keybinding. - E2E: diffView suite keeps the baseline-data-layer tests, drops the two-pane view tests; the F10 + no-workspace suites assert toggleDiffView is now absent (was: declared-but-hidden). Deliberate deviation from the issue's literal acceptance: pinDiffBaseline is KEPT. The canonical Solution Design (coauthoring-interactive-review.md §6.7) scopes the removal to the two-pane VIEW only ("keep the controller + baseline store"); pin() lives in the baseline lifecycle (§6.4), never touches vscode.diff, and is exercised by live F7 baseline-reset tests. Where the P3 capture draft and the approved spec conflict, the spec wins (documentation-leads-automation). 194 unit + 49 E2E green; typecheck + build clean. No F7/F10 behavior change. Closes #34 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
147 lines
6.3 KiB
TypeScript
147 lines
6.3 KiB
TypeScript
/**
|
|
* DiffViewController — F6 baseline data layer (spec §6.4/§6.7). Owns the baseline
|
|
* lifecycle (initialize at first sight / advance at every machine landing / pin
|
|
* on demand) and serves it to F7/F10 via `getBaseline` + the additive
|
|
* `onDidChangeBaseline` event. A pure data layer: never mutates the document,
|
|
* sidecar, or attribution state (INV-19).
|
|
*
|
|
* The F6 two-pane `vscode.diff` *view* (toggle UI + the `cowriting-baseline:`
|
|
* virtual document) was removed in #34 once F10 made the rendered preview the
|
|
* single review surface; only the baseline store survives here (spec §6.7).
|
|
*
|
|
* The baseline works on ANY text document, not just workspace files — it needs no
|
|
* `.threads/` sidecar, only a stable doc identity + a storage home. So it has
|
|
* its OWN diffable predicate (any `file:` or `untitled:` doc), decoupled from
|
|
* F2's workspace `isTracked`. Persistable docs (`file:`) keep their baseline in
|
|
* VS Code's per-extension GLOBAL storage keyed by a hash of the document URI;
|
|
* untitled buffers have no durable identity, so their baseline is in-memory
|
|
* only (lost on reload) — the same degrade the storage-unavailable path uses.
|
|
*/
|
|
import { createHash } from "node:crypto";
|
|
import * as vscode from "vscode";
|
|
import { BaselineStore, type Baseline, type BaselineReason } from "./baselineStore";
|
|
|
|
export class DiffViewController implements vscode.Disposable {
|
|
private readonly disposables: vscode.Disposable[] = [];
|
|
/** Source of truth for the baseline, keyed by `document.uri.toString()`. */
|
|
private readonly baselines = new Map<string, Baseline>();
|
|
/** F7 (additive): fires on every baseline capture (open / advance / pin) so
|
|
* the track-changes preview refreshes without polling. Carries the real
|
|
* document URI. */
|
|
private readonly onDidChangeBaselineEmitter = new vscode.EventEmitter<{ uri: string }>();
|
|
readonly onDidChangeBaseline = this.onDidChangeBaselineEmitter.event;
|
|
private storageWarned = false;
|
|
|
|
constructor(private readonly store: BaselineStore | null) {
|
|
this.disposables.push(
|
|
this.onDidChangeBaselineEmitter,
|
|
vscode.commands.registerCommand("cowriting.pinDiffBaseline", () =>
|
|
this.pinCommand(vscode.window.activeTextEditor),
|
|
),
|
|
// F6 captures a baseline for any diffable doc the moment it is first seen,
|
|
// independent of the workspace gate (so "opened" is the open-time text).
|
|
vscode.workspace.onDidOpenTextDocument((d) => this.ensureBaseline(d)),
|
|
);
|
|
for (const d of vscode.workspace.textDocuments) this.ensureBaseline(d);
|
|
}
|
|
|
|
// ---- diffability / identity --------------------------------------------------------
|
|
|
|
/** Any text document F6 tracks a baseline for: a saved file OR an unsaved buffer. */
|
|
private isDiffable(document: vscode.TextDocument): boolean {
|
|
return document.uri.scheme === "file" || document.uri.scheme === "untitled";
|
|
}
|
|
/** Only `file:` docs have a durable identity to persist a baseline against. */
|
|
private isPersistable(document: vscode.TextDocument): boolean {
|
|
return document.uri.scheme === "file";
|
|
}
|
|
/** In-memory map key — the full document URI. */
|
|
private uriKey(document: vscode.TextDocument): string {
|
|
return document.uri.toString();
|
|
}
|
|
/** Filesystem-safe storage key for a persistable doc: sha256 of its URI. */
|
|
private storageKey(uriKey: string): string {
|
|
return createHash("sha256").update(uriKey).digest("hex");
|
|
}
|
|
|
|
// ---- baseline lifecycle (§6.4) -----------------------------------------------------
|
|
|
|
/** First sight of a diffable doc: load the stored baseline, else capture `opened`. */
|
|
ensureBaseline(document: vscode.TextDocument): void {
|
|
if (!this.isDiffable(document)) return;
|
|
const key = this.uriKey(document);
|
|
if (this.baselines.has(key)) return;
|
|
if (this.store && this.isPersistable(document)) {
|
|
try {
|
|
const stored = this.store.load(this.storageKey(key));
|
|
if (stored) {
|
|
this.baselines.set(key, stored);
|
|
return;
|
|
}
|
|
} catch {
|
|
this.warnStorageOnce();
|
|
}
|
|
}
|
|
this.capture(document, "opened");
|
|
}
|
|
|
|
/** Machine landing (INV-18): re-capture so landed text never shows as a change. */
|
|
advance(document: vscode.TextDocument): void {
|
|
if (!this.isDiffable(document)) return;
|
|
this.capture(document, "machine-landing");
|
|
}
|
|
|
|
/** Human pin: baseline := now; the preview's change-marks empty (left = right). */
|
|
pin(document: vscode.TextDocument): void {
|
|
if (!this.isDiffable(document)) return;
|
|
this.capture(document, "pinned");
|
|
}
|
|
|
|
/** Capture buffer text at this epoch, persist (if persistable), notify F7/F10. */
|
|
private capture(document: vscode.TextDocument, reason: BaselineReason): void {
|
|
const key = this.uriKey(document);
|
|
const baseline: Baseline = { uri: key, text: document.getText(), capturedAt: new Date().toISOString(), reason };
|
|
this.baselines.set(key, baseline);
|
|
if (this.store && this.isPersistable(document)) {
|
|
try {
|
|
this.store.save(this.storageKey(key), baseline);
|
|
} catch {
|
|
this.warnStorageOnce();
|
|
}
|
|
}
|
|
this.onDidChangeBaselineEmitter.fire({ uri: key });
|
|
}
|
|
|
|
private warnStorageOnce(): void {
|
|
if (this.storageWarned) return;
|
|
this.storageWarned = true;
|
|
void vscode.window.showWarningMessage(
|
|
"Cowriting: baseline storage is unavailable — baselines are kept in memory only and won't survive a reload.",
|
|
);
|
|
}
|
|
|
|
private pinCommand(editor: vscode.TextEditor | undefined): void {
|
|
if (!editor || !this.isDiffable(editor.document)) {
|
|
void vscode.window.showWarningMessage("Cowriting: focus a text editor to pin its review baseline.");
|
|
return;
|
|
}
|
|
this.pin(editor.document);
|
|
}
|
|
|
|
// ---- test-facing surface (§6.4) ----------------------------------------------------
|
|
|
|
getBaseline(uriString: string): { text: string; reason: BaselineReason; capturedAt: string } | undefined {
|
|
const b = this.baselines.get(uriString);
|
|
return b ? { text: b.text, reason: b.reason, capturedAt: b.capturedAt } : undefined;
|
|
}
|
|
/** Absolute on-disk path of this doc's persisted baseline, or undefined (untitled/in-memory). */
|
|
baselineFilePath(uriString: string): string | undefined {
|
|
if (!this.store || vscode.Uri.parse(uriString).scheme !== "file") return undefined;
|
|
return this.store.baselinePath(this.storageKey(uriString));
|
|
}
|
|
|
|
dispose(): void {
|
|
for (const d of this.disposables) d.dispose();
|
|
}
|
|
}
|