Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/AUTOMATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,12 @@ While a demo plays:
same one a document's history transport hides into), filling with the demo's progress. The
pointer held on the dome (or a press on it) grows the bar back out; it goes back soon after
the pointer leaves it. Paused or ended it stays out, unless its chevron tab sent it in.
- **Nothing is persisted.** The stage sets the user's session aside when a demo loads (project
folder, open documents, layout as on disk, plugin settings) and gives it back when it is
stopped. A demo refuses to start over unsaved changes. Its files live in memory
- **Nothing is persisted, and nothing of the person's is touched.** A demo plays on a stage of
its own: when it loads, the person's panes are set aside whole and their documents stay open
behind them, hidden — never closed, saved, reloaded or put back by a snapshot, unsaved edits and
history and all — with their project folder, layout as on disk, settings and focus. The demo
opens its own documents in panes of its own, and when it is stopped the person gets back
exactly what they left. So a demo starts over unsaved work. Its files live in memory
(`demo://<name>`), so it never touches disk, and its folder never enters Recents.

## How it works
Expand Down
2 changes: 1 addition & 1 deletion docs/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1324,7 +1324,7 @@ The SDK ships definitions for the services in this ecosystem, in
| `"markdown"` | `markdown` | Render a markdown byte slice into the current dvui parent (native only — absent on web) |
| `"wikilink"` | any indexer (e.g. `brain`) | Resolve `[[Note]]` to a file, plus completion candidates and index state |
| `"automation"` | the application (fizzy: `app/automation/Service.zig`, over its live driver) | `play` a live tape — input on the app as it is, built with the `tape` module and handed over as ZON or binary bytes — and get a ticket to ask its `outcome` (finished, or interrupted / timed out / stopped before which op); `stop` your own tape; `settled` when nothing is in flight; `snapshot` / `snapshotText` for what is on screen as ZON, a line per widget a person acts on or a tape aims at (role, name, tag, rect, parent — `replay.Snapshot`). One tape at a time, refused while a demo plays; a person's input stops it. For plugin tests, macros, tutorials |
| `"demos"` | the application (fizzy: `src/editor/Demos.zig`, over its demo player) | `list` every demo the app can play — its own, and each one a plugin registered with `host.registerDemo` (a name, a title, the tape as ZON or binary bytes, copied by the host and dropped when the plugin goes) — with each one's length, chapters and where it came from; `play` one by name, from a chapter if asked; `current` names the one playing; `generation` moves when the list does. A demo opens on a keyframe and sets the person's session aside while it plays; refused over unsaved work or while a live tape drives. For a guides browser, a plugin's own demos, an agent |
| `"demos"` | the application (fizzy: `src/editor/Demos.zig`, over its demo player) | `list` every demo the app can play — its own, and each one a plugin registered with `host.registerDemo` (a name, a title, the tape as ZON or binary bytes, copied by the host and dropped when the plugin goes) — with each one's length, chapters and where it came from; `play` one by name, from a chapter if asked; `current` names the one playing; `generation` moves when the list does. A demo opens on a keyframe and sets the person's session aside untouched while it plays, unsaved work and all; refused only while a live tape drives. For a guides browser, a plugin's own demos, an agent |

Everything a plugin might reasonably ask *fizzy* for lives on `Host` instead — `openFile`,
`closeDocById`, `docFromPath`, `revealPosition`, `setProjectFolder`, `isPathIgnored`, … — where it
Expand Down
45 changes: 45 additions & 0 deletions plugins/workbench/src/Workbench.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub const Workbench = @This();
/// The split tree the panes live in. `core.widgets.DockLayout`, one panel per leaf.
pub const Panes = core.widgets.DockLayout;

/// A held pane's tabs (`held_tabs`): its grouping, and the surface ids it held, owned.
pub const HeldTabs = struct { grouping: u64, ids: [][]u8 };

allocator: std.mem.Allocator,
decorators: std.ArrayListUnmanaged(BranchDecorator) = .empty,

Expand All @@ -44,6 +47,11 @@ holding: bool = false,
/// not be made, or before there was one), and the pane they were in.
held_panes: ?Panes = null,
held_open: u64 = 0,
/// The person's panes themselves while a demo has the window, and what each held (its
/// assignment, copied): set aside whole, with their documents, which stay open and are never
/// seated in the demo's panes (`workbench_layout.holdPanes`).
held_workspaces: std.AutoArrayHashMapUnmanaged(u64, Workspace) = .empty,
held_tabs: std.ArrayListUnmanaged(HeldTabs) = .empty,
/// Where the panes are arranged when the app arranges them instead of `panes`: each pane a view
/// of the app's, in a place of the app's, beside another by a split of the app's (fizzy's
/// `Workspaces`, behind `FIZZY_WORKSPACES=1`). Null: the panes are this tree's leaves, as before.
Expand Down Expand Up @@ -293,6 +301,35 @@ pub fn documentClosed(self: *Workbench, doc: sdk.DocHandle) void {
if (active.id == doc.id) ws.active = null;
}
}
// One of the person's, closed while a demo has the window: gone from their held panes too.
self.forgetHeld(doc, id);
}

/// `doc`, whose tab is `id`, from the person's held panes (`holdPanes`): its tab, where `id` is
/// given, and any pane's cached handle of it.
fn forgetHeld(self: *Workbench, doc: sdk.DocHandle, id: ?[]const u8) void {
for (self.held_workspaces.values()) |*ws| {
if (ws.active) |active| {
if (active.id == doc.id) ws.active = null;
}
}
const gone = id orelse return;
for (self.held_tabs.items) |*t| {
const k = for (t.ids, 0..) |held, k| {
if (std.mem.eql(u8, held, gone)) break k;
} else continue;
self.allocator.free(t.ids[k]);
std.mem.copyForwards([]u8, t.ids[k..], t.ids[k + 1 ..]);
t.ids = self.allocator.realloc(t.ids, t.ids.len - 1) catch t.ids[0 .. t.ids.len - 1];
}
}

/// Whether `doc` is one of the person's, set aside with their panes while a demo has the window
/// (`holdPanes`).
pub fn holdsDocument(self: *const Workbench, doc: sdk.DocHandle) bool {
if (!self.holding) return false;
const id = sdk.document.surfaceId(runtime.host().arena(), doc.owner.id, doc.owner.documentPath(doc)) catch return false;
return workbench_layout.heldDocument(self, doc, id);
}

/// The tab naming surface `id` goes from whichever pane holds it: a document that will not come
Expand All @@ -309,6 +346,7 @@ pub fn documentDetached(self: *Workbench, doc: sdk.DocHandle) void {
if (active.id == doc.id) ws.active = null;
}
}
self.forgetHeld(doc, null);
}

/// A document's path changed under it (an explorer rename, a Save As): its tab is keyed by
Expand All @@ -318,6 +356,13 @@ pub fn documentDetached(self: *Workbench, doc: sdk.DocHandle) void {
/// starts from `activeDoc()` — Save first among them — quietly does nothing.
pub fn documentRenamed(self: *Workbench, doc: sdk.DocHandle, old_id: []const u8) void {
const new_id = sdk.document.surfaceId(runtime.host().arena(), doc.owner.id, doc.owner.documentPath(doc)) catch return;
// One of the person's, renamed while a demo has the window: its held tab follows.
for (self.held_tabs.items) |t| for (t.ids, 0..) |held, k| {
if (!std.mem.eql(u8, held, old_id)) continue;
const copy = self.allocator.dupe(u8, new_id) catch break;
self.allocator.free(held);
t.ids[k] = copy;
};
const ws = self.swapTabId(old_id, new_id) orelse return;
// Selection is by id too. Only the pane showing this document re-selects, and only
// within itself — this is not a focus change, so `open_workspace_grouping` stays.
Expand Down
108 changes: 87 additions & 21 deletions plugins/workbench/src/workbench_layout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ pub fn rebuildWorkspaces(wb: *Workbench) !void {
}
}
if (held) continue;
// One of the person's, set aside with its pane while a demo has the window: it waits
// there for them, never seated in the demo's panes.
if (wb.holding and heldDocument(wb, doc, id)) continue;
// The same file under another owner's id (a plugin installed since last session) would
// otherwise sit beside it as a tab that never opens.
const path = doc.owner.documentPath(doc);
Expand Down Expand Up @@ -239,29 +242,74 @@ fn adoptPanes(wb: *Workbench, loaded: Panes) void {
}
}

/// Keep the person's arrangement while a demo has the window, and save nothing until it is put
/// back (`restorePanes`): the demo's splits are not theirs. A second hold keeps the first.
/// Set the person's panes aside while a demo has the window, and save nothing until they are put
/// back (`restorePanes`): the demo plays in a pane of its own, and its splits are not theirs. A
/// second hold keeps the first.
///
/// Nothing is saved while holding, whatever else happens, so the arrangement on disk stays the
/// person's: a copy that could not be made is read back from there instead. Before the first
/// rebuild has restored last session's panes there is nothing of theirs here to copy; that
/// restore waits for the demo to end.
/// Their panes go aside whole — the workspaces, what each holds, and the tree they sit in — and
/// their documents stay open behind them, untouched: a document of a held pane is never seated in
/// the demo's (`rebuildWorkspaces`). Nothing is saved while holding, so the arrangement on disk
/// stays theirs: a tree that could not be copied is read back from there. Before the first rebuild
/// has restored last session's panes there is no tree of theirs to copy; that restore waits for
/// the demo to end.
pub fn holdPanes(wb: *Workbench) void {
if (wb.holding) return;
wb.holding = true;
wb.held_open = wb.open_workspace_grouping;
if (!wb.restored) return;
// A copy through a snapshot: a snapshot borrows the pane names from the live tree, which the
// demo's first cut replaces (`onePane`).
const snap = wb.panes.snapshot(wb.allocator) catch |err| {
dvui.log.err("workbench: could not set the panes aside ({t}); they come back from disk", .{err});
return;
};
defer snap.deinit(wb.allocator);
wb.held_panes = Panes.fromSnapshot(wb.allocator, snap) catch |err| blk: {
dvui.log.err("workbench: could not set the panes aside ({t}); they come back from disk", .{err});
break :blk null;
};
if (wb.restored) {
// A copy through a snapshot: a snapshot borrows the pane names from the live tree, which
// the demo's own pane replaces below.
if (wb.panes.snapshot(wb.allocator)) |snap| {
defer snap.deinit(wb.allocator);
wb.held_panes = Panes.fromSnapshot(wb.allocator, snap) catch |err| blk: {
dvui.log.err("workbench: could not set the panes aside ({t}); they come back from disk", .{err});
break :blk null;
};
} else |err| dvui.log.err("workbench: could not set the panes aside ({t}); they come back from disk", .{err});
}
const host = runtime.host();
for (wb.workspaces.values()) |*ws| {
var buf: [32]u8 = undefined;
const ids = host.assignedSurfaces(Workspace.name(&buf, ws.grouping)) orelse continue;
const copy = dupeIds(wb.allocator, ids) catch |err| {
dvui.log.err("workbench: could not set pane {d}'s tabs aside ({t}); they come back from disk", .{ ws.grouping, err });
continue;
};
wb.held_tabs.append(wb.allocator, .{ .grouping = ws.grouping, .ids = copy }) catch |err| {
dvui.log.err("workbench: could not set pane {d}'s tabs aside ({t}); they come back from disk", .{ ws.grouping, err });
freeIds(wb.allocator, copy);
};
}
wb.held_workspaces = wb.workspaces;
wb.workspaces = .empty;
// The demo's pane: a grouping no pane of theirs has.
const stage = wb.newGroupingID();
wb.workspaces.put(wb.allocator, stage, Workspace.init(stage)) catch {};
wb.open_workspace_grouping = stage;
onePane(wb);
}

/// Whether `doc` (its tab `id`) is one of the person's, set aside with their panes.
pub fn heldDocument(wb: *const Workbench, doc: sdk.DocHandle, id: []const u8) bool {
if (wb.held_workspaces.contains(doc.owner.documentGrouping(doc))) return true;
for (wb.held_tabs.items) |t| for (t.ids) |held| if (std.mem.eql(u8, held, id)) return true;
return false;
}

fn dupeIds(gpa: std.mem.Allocator, ids: []const []const u8) ![][]u8 {
const out = try gpa.alloc([]u8, ids.len);
var n: usize = 0;
errdefer freeIds(gpa, out[0..n]);
for (ids, out) |id, *o| {
o.* = try gpa.dupe(u8, id);
n += 1;
}
return out;
}

fn freeIds(gpa: std.mem.Allocator, ids: [][]u8) void {
for (ids) |id| gpa.free(id);
gpa.free(ids);
}

/// One pane, as a demo's keyframe declares the window: the open one, the others closing with
Expand All @@ -279,12 +327,30 @@ pub fn onePane(wb: *Workbench) void {
adoptPanes(wb, one);
}

/// The person's arrangement again (`holdPanes`), and saving with it: the copy, or what is on disk
/// where there is none. Before last session's panes were ever restored, one pane, for the next
/// rebuild to restore them into.
/// The person's panes again (`holdPanes`), and saving with them: the demo's go, theirs come back
/// holding what they held, in their tree — the copy, or what is on disk where there is none.
/// Before last session's panes were ever restored, one pane, for the next rebuild to restore
/// them into.
pub fn restorePanes(wb: *Workbench) void {
if (!wb.holding) return;
wb.holding = false;
const host = runtime.host();
for (wb.workspaces.values()) |*ws| {
var buf: [32]u8 = undefined;
host.assignSurfaces(Workspace.name(&buf, ws.grouping), null) catch {};
ws.deinit();
}
wb.workspaces.deinit(wb.allocator);
wb.workspaces = wb.held_workspaces;
wb.held_workspaces = .empty;
for (wb.held_tabs.items) |t| {
var buf: [32]u8 = undefined;
const ids: []const []const u8 = t.ids;
host.assignSurfaces(Workspace.name(&buf, t.grouping), ids) catch |err|
dvui.log.err("workbench: pane {d}'s tabs did not come back ({t})", .{ t.grouping, err });
freeIds(wb.allocator, t.ids);
}
wb.held_tabs.clearAndFree(wb.allocator);
if (wb.held_panes) |held| {
wb.held_panes = null;
adoptPanes(wb, held);
Expand Down
4 changes: 3 additions & 1 deletion sdk/src/services/demos.zig
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ pub const Api = struct {
unknown,
/// Something else is driving the app (a live tape): try again once it is done.
busy,
/// The person has unsaved work open, which a demo would set aside: they are told so.
/// Never answered: a demo plays on a stage of its own, the person's work, saved or not,
/// set aside untouched. Kept so a plugin switching on it still builds; it goes at the
/// next SDK release that moves the boundary for another reason.
unsaved,
};

Expand Down
Loading
Loading