runa/vscode/src/snapshotProvider.ts

161 lines
5.5 KiB
TypeScript
Raw Normal View History

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;
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,
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 snapshot of this.documents.values()) {
const actor = byRef.get(snapshot.actor.actor_ref);
if (snapshot.session === session && actor) this.set(session, endpoint, actor);
}
}
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,
);
}
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 && 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 || !canEditActor(snapshot.actor))
throw vscode.FileSystemError.NoPermissions(
"Actor must be paused, stopped, or failed with no active invocation",
);
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);
}
async didSave(document: vscode.TextDocument): Promise<void> {
if (document.uri.scheme === "runa" && !document.isDirty)
await vscode.commands.executeCommand("workbench.action.files.revertResource", 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();
}
}