Files
vscode-cowriting-plugin/src/scmSurface.ts
T

121 lines
4.8 KiB
TypeScript

/**
* ScmSurfaceController — the native diff surface (spec §6.4/§5, INV-13).
* Owns: the "Cowriting" SourceControl + QuickDiffProvider (gutter bars), the
* cowriting-baseline: TextDocumentContentProvider, the "Review Changes" native
* diff command, and the "✦ Coediting · N changes" status-bar item. Gated on
* the CoeditingRegistry (INV-10): non-entered docs resolve no original.
*/
import * as path from "node:path";
import * as vscode from "vscode";
import type { CoeditingRegistry } from "./coeditingRegistry";
import type { DiffViewController } from "./diffViewController";
import { countLineHunks } from "./trackChangesModel";
export const BASELINE_SCHEME = "cowriting-baseline";
export function baselineUriFor(docUri: vscode.Uri): vscode.Uri {
const name = path.basename(docUri.path) || "untitled.md";
return vscode.Uri.from({ scheme: BASELINE_SCHEME, path: `/${name}`, query: encodeURIComponent(docUri.toString()) });
}
export class ScmSurfaceController implements vscode.Disposable {
private readonly disposables: vscode.Disposable[] = [];
private readonly statusItem: vscode.StatusBarItem;
private readonly counts = new Map<string, number>();
private readonly baselineEmitter = new vscode.EventEmitter<vscode.Uri>();
private readonly recountTimers = new Map<string, ReturnType<typeof setTimeout>>();
constructor(
private readonly registry: CoeditingRegistry,
private readonly diffView: DiffViewController,
) {
this.statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
this.statusItem.command = "cowriting.reviewChanges";
this.statusItem.tooltip = "Cowriting — click to review changes (baseline ⟷ live)";
this.disposables.push(this.statusItem, this.baselineEmitter);
this.disposables.push(
vscode.workspace.registerTextDocumentContentProvider(BASELINE_SCHEME, {
onDidChange: this.baselineEmitter.event,
provideTextDocumentContent: (uri) =>
this.diffView.getBaseline(decodeURIComponent(uri.query))?.text ?? "",
}),
);
const sc = vscode.scm.createSourceControl("cowriting", "✦ Cowriting");
sc.quickDiffProvider = {
provideOriginalResource: (uri: vscode.Uri) =>
this.registry.isCoediting(uri) && this.diffView.getBaseline(uri.toString())
? baselineUriFor(uri)
: null,
};
this.disposables.push(sc);
this.disposables.push(
vscode.commands.registerCommand("cowriting.reviewChanges", () => this.openReview()),
this.diffView.onDidChangeBaseline(({ uri }) => {
this.baselineEmitter.fire(baselineUriFor(vscode.Uri.parse(uri)));
this.scheduleRecount(uri);
}),
this.registry.onDidChange(({ uri, coediting }) => {
if (coediting) this.scheduleRecount(uri);
this.refreshStatus();
}),
vscode.workspace.onDidChangeTextDocument((e) => {
if (this.registry.isCoediting(e.document.uri)) this.scheduleRecount(e.document.uri.toString());
}),
vscode.window.onDidChangeActiveTextEditor(() => this.refreshStatus()),
);
this.refreshStatus();
}
changeCount(uriString: string): number {
return this.counts.get(uriString) ?? 0;
}
private async openReview(): Promise<void> {
const ed = vscode.window.activeTextEditor;
if (!ed || !this.registry.isCoediting(ed.document.uri)) {
void vscode.window.showWarningMessage("Cowriting: not coediting this document.");
return;
}
this.baselineEmitter.fire(baselineUriFor(ed.document.uri));
await vscode.commands.executeCommand(
"vscode.diff",
baselineUriFor(ed.document.uri),
ed.document.uri,
`${path.basename(ed.document.uri.path)} — Coediting (baseline ⟷ live)`,
{ preview: true },
);
}
private scheduleRecount(uriString: string): void {
const prev = this.recountTimers.get(uriString);
if (prev !== undefined) clearTimeout(prev);
this.recountTimers.set(uriString, setTimeout(() => {
this.recountTimers.delete(uriString);
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uriString);
const baseline = this.diffView.getBaseline(uriString);
if (!doc || !baseline) return;
this.counts.set(uriString, countLineHunks(baseline.text, doc.getText()));
this.refreshStatus();
}, 150));
}
private refreshStatus(): void {
const ed = vscode.window.activeTextEditor;
if (!ed || !this.registry.isCoediting(ed.document.uri)) {
this.statusItem.hide();
return;
}
const n = this.counts.get(ed.document.uri.toString()) ?? 0;
this.statusItem.text = `$(sparkle) Coediting · ${n} change${n === 1 ? "" : "s"}`;
this.statusItem.show();
}
dispose(): void {
for (const t of this.recountTimers.values()) clearTimeout(t);
for (const d of this.disposables) d.dispose();
}
}