173 lines
5.9 KiB
TypeScript
173 lines
5.9 KiB
TypeScript
import * as vscode from "vscode";
|
|
import { ActorSnapshot, formatSnapshot } from "./protocol";
|
|
import { canEditActor, parseSnapshotEdit, SnapshotEdit } from "./snapshotEdits";
|
|
|
|
interface DocumentSnapshot {
|
|
session: number;
|
|
endpoint: string;
|
|
actor: ActorSnapshot;
|
|
capturedAt: string;
|
|
connected: boolean;
|
|
removed: boolean;
|
|
mtime: number;
|
|
}
|
|
|
|
export class SnapshotDocuments implements vscode.FileSystemProvider {
|
|
private readonly changes = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
|
|
readonly onDidChangeFile = this.changes.event;
|
|
private readonly documents = new Map<string, DocumentSnapshot>();
|
|
private readonly baselines = new Map<string, string>();
|
|
|
|
constructor(
|
|
private readonly save: (session: number, actor: string, edit: SnapshotEdit) => Promise<void>,
|
|
private readonly warn: (message: string) => void,
|
|
) {}
|
|
|
|
set(session: number, endpoint: string, actor: ActorSnapshot): vscode.Uri {
|
|
const uri = vscode.Uri.from({
|
|
scheme: "runa",
|
|
authority: String(session),
|
|
path: `/actors/${actor.actor_ref}.json`,
|
|
});
|
|
const previous = this.documents.get(uri.toString());
|
|
this.documents.set(uri.toString(), {
|
|
session,
|
|
endpoint,
|
|
actor,
|
|
capturedAt: new Date().toISOString(),
|
|
connected: true,
|
|
removed: false,
|
|
mtime: Math.max(Date.now(), (previous?.mtime ?? 0) + 1),
|
|
});
|
|
this.changed(uri);
|
|
return uri;
|
|
}
|
|
|
|
refresh(session: number, endpoint: string, actors: ActorSnapshot[]): void {
|
|
const byRef = new Map(actors.map((actor) => [actor.actor_ref, actor]));
|
|
for (const [key, snapshot] of this.documents) {
|
|
const actor = byRef.get(snapshot.actor.actor_ref);
|
|
if (snapshot.session !== session) continue;
|
|
if (actor) this.set(session, endpoint, actor);
|
|
else if (!snapshot.removed) {
|
|
snapshot.removed = true;
|
|
snapshot.mtime++;
|
|
this.changed(vscode.Uri.parse(key));
|
|
}
|
|
}
|
|
}
|
|
|
|
disconnect(session: number): void {
|
|
for (const [key, snapshot] of this.documents) {
|
|
if (snapshot.session === session) {
|
|
snapshot.connected = false;
|
|
snapshot.mtime++;
|
|
this.changed(vscode.Uri.parse(key));
|
|
}
|
|
}
|
|
}
|
|
|
|
private changed(uri: vscode.Uri): void {
|
|
this.changes.fire([{ type: vscode.FileChangeType.Changed, uri }]);
|
|
}
|
|
|
|
private snapshot(uri: vscode.Uri): DocumentSnapshot {
|
|
const snapshot = this.documents.get(uri.toString());
|
|
if (!snapshot) throw vscode.FileSystemError.FileNotFound(uri);
|
|
return snapshot;
|
|
}
|
|
|
|
private text(snapshot: DocumentSnapshot): string {
|
|
return formatSnapshot(
|
|
snapshot.actor,
|
|
snapshot.endpoint,
|
|
snapshot.capturedAt,
|
|
snapshot.connected,
|
|
snapshot.removed,
|
|
);
|
|
}
|
|
|
|
stat(uri: vscode.Uri): vscode.FileStat {
|
|
if (this.isDirectory(uri))
|
|
return { type: vscode.FileType.Directory, ctime: 0, mtime: 0, size: 0 };
|
|
const snapshot = this.snapshot(uri);
|
|
return {
|
|
type: vscode.FileType.File,
|
|
ctime: 0,
|
|
mtime: snapshot.mtime,
|
|
size: Buffer.byteLength(this.text(snapshot)),
|
|
permissions:
|
|
snapshot.connected && !snapshot.removed && canEditActor(snapshot.actor)
|
|
? undefined
|
|
: vscode.FilePermission.Readonly,
|
|
};
|
|
}
|
|
|
|
readFile(uri: vscode.Uri): Uint8Array {
|
|
const text = this.text(this.snapshot(uri));
|
|
const dirty = vscode.workspace.textDocuments.some(
|
|
(document) => document.uri.toString() === uri.toString() && document.isDirty,
|
|
);
|
|
if (!dirty) this.baselines.set(uri.toString(), text);
|
|
return Buffer.from(text);
|
|
}
|
|
|
|
async writeFile(uri: vscode.Uri, content: Uint8Array): Promise<void> {
|
|
const snapshot = this.snapshot(uri);
|
|
if (!snapshot.connected || snapshot.removed || !canEditActor(snapshot.actor))
|
|
throw vscode.FileSystemError.NoPermissions(
|
|
"Actor must be paused or failed with no active invocation, and not removed or disconnected",
|
|
);
|
|
const baseline = this.baselines.get(uri.toString());
|
|
if (baseline === undefined)
|
|
throw vscode.FileSystemError.Unavailable("Inspect the actor before editing");
|
|
const edit = parseSnapshotEdit(Buffer.from(content).toString("utf8"), baseline);
|
|
await this.save(snapshot.session, snapshot.actor.actor_ref, edit);
|
|
if (edit.discarded.length)
|
|
this.warn(`Discarded read-only inspection fields: ${edit.discarded.join(", ")}`);
|
|
this.changed(uri);
|
|
}
|
|
|
|
didSave(document: vscode.TextDocument): void {
|
|
if (document.uri.scheme !== "runa" || document.isDirty) return;
|
|
const snapshot = this.snapshot(document.uri);
|
|
// Notify after the save completes so VS Code reloads the now-clean editor.
|
|
snapshot.mtime = Math.max(Date.now(), snapshot.mtime + 1);
|
|
this.changed(document.uri);
|
|
}
|
|
|
|
watch(): vscode.Disposable {
|
|
return new vscode.Disposable(() => {});
|
|
}
|
|
private isDirectory(uri: vscode.Uri): boolean {
|
|
return (
|
|
uri.scheme === "runa" &&
|
|
(uri.path === "/" || uri.path === "/actors") &&
|
|
[...this.documents.values()].some((snapshot) => String(snapshot.session) === uri.authority)
|
|
);
|
|
}
|
|
|
|
readDirectory(uri: vscode.Uri): [string, vscode.FileType][] {
|
|
if (!this.isDirectory(uri)) throw vscode.FileSystemError.FileNotADirectory(uri);
|
|
if (uri.path === "/") return [["actors", vscode.FileType.Directory]];
|
|
return [...this.documents.values()]
|
|
.filter((snapshot) => String(snapshot.session) === uri.authority)
|
|
.map((snapshot) => [`${snapshot.actor.actor_ref}.json`, vscode.FileType.File]);
|
|
}
|
|
createDirectory(uri: vscode.Uri): void {
|
|
if (!this.isDirectory(uri))
|
|
throw vscode.FileSystemError.NoPermissions("Actor snapshot directories cannot be created");
|
|
}
|
|
delete(): never {
|
|
throw vscode.FileSystemError.NoPermissions("Actor snapshots cannot be deleted");
|
|
}
|
|
rename(): never {
|
|
throw vscode.FileSystemError.NoPermissions("Actor snapshots cannot be renamed");
|
|
}
|
|
dispose(): void {
|
|
this.changes.dispose();
|
|
this.documents.clear();
|
|
this.baselines.clear();
|
|
}
|
|
}
|