From 21d2240b9b86ae5ba0036988b0ed84fbcf18d3af Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 12:24:04 -0300 Subject: Restore asks about unsaved text first, as Exit does Restore replaces every pane with a dump's, so a pane edited since its last save lost that text without a word, where Exit asks. acme's Load only adds a dump's windows and so never asks; Restore now asks what Exit asks (acme's rowclean and winclean, wind.c:511-529): each modified pane says so once, and Restore again with nothing edited since goes ahead. Exit and Restore share the check and its warned-at revision, as acme's winclean clears one dirty flag for any asker. Co-Authored-By: Claude Opus 5.5 --- src/builtins.zig | 56 +++++++++++++++++++++++++++++++++++++++----------------- src/panes.zig | 5 +++-- 2 files changed, 42 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/builtins.zig b/src/builtins.zig index 93eae8ec..88c65704 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -226,6 +226,18 @@ test "Exit asks once about unsaved text, and quits when asked again" { try std.testing.expect(p.quit); } +test "Restore asks about unsaved text as Exit does, and restores when asked again" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const pane = try p.setTestFile("saved\n"); + pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified + try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /tmp/some.dump.zon")); + try std.testing.expect(p.restore_req == null); + try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified")); + try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /tmp/some.dump.zon")); + try std.testing.expectEqualStrings("/tmp/some.dump.zon", p.restore_req.?); +} + test "Kill stops the commands pardes typed, bare all and by name those that match, and never quits" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); defer p.deinit(); @@ -340,26 +352,33 @@ pub const Exec = struct { // ---- session ---- -/// acme's Exit (editors/acme/exec.c, `exit` and rowclean): quit, unless a -/// pane holds unsaved text nobody has been told about. Each such pane says -/// so, `: Modified`, and the Exit goes no further; an Exit after that, -/// with nothing edited since, quits. A scratch of under 100 bytes is no -/// loss worth the question, as acme's winclean does not ask for one. +/// acme's rowclean (editors/acme/wind.c:511-529, winclean), asked before +/// what would throw unsaved text away: each pane holding some nobody has +/// been told about says so, `: Modified`, and is then clean for the +/// asking, as winclean clears `dirty`; so the same word again, with +/// nothing edited since, goes ahead. A scratch of under 100 bytes is no +/// loss worth the question, as winclean does not ask for one. +fn warnModified(c: Ctx) bool { + var modified = false; + for (c.p.panes, 0..) |slot, id| { + const pane = slot orelse continue; + const f = pane.file orelse continue; + if (f.revision == f.saved_revision or !panes.Output.fileTraits(f.output).saves) continue; + if (f.output != null and f.content.len < 100) continue; + if (pane.discard_warned == f.revision) continue; + pane.discard_warned = f.revision; + c.p.reportError(id, f.path, error.Modified); + modified = true; + } + return modified; +} + +/// acme's Exit (editors/acme/exec.c:892): quit, once warnModified has +/// nothing to say. pub const Exit = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { - var modified = false; - for (c.p.panes, 0..) |slot, id| { - const pane = slot orelse continue; - const f = pane.file orelse continue; - if (f.revision == f.saved_revision or !panes.Output.fileTraits(f.output).saves) continue; - if (f.output != null and f.content.len < 100) continue; - if (pane.exit_warned == f.revision) continue; - pane.exit_warned = f.revision; - c.p.reportError(id, f.path, error.Modified); - modified = true; - } - if (modified) return; + if (warnModified(c)) return; c.p.quit = true; c.p.emit(.quit); } @@ -412,6 +431,9 @@ pub const Restore = struct { pub fn run(c: Ctx) void { const path = c.arg orelse (c.p.last_dump orelse return); if (path.len > c.p.restore_buf.len) return; + // acme's Load adds a dump's windows to the ones there; a Restore + // replaces them all, so it asks what Exit asks first. + if (warnModified(c)) return; @memcpy(c.p.restore_buf[0..path.len], path); c.p.restore_req = c.p.restore_buf[0..path.len]; } diff --git a/src/panes.zig b/src/panes.zig index 70b461eb..d4b336d7 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -99,8 +99,9 @@ pub const Pane = struct { /// middle click, a pty/run): its first word, which Kill matches, and /// the shell's command count its start (C mark) reaches. sent_command: ?struct { word: [64]u8 = undefined, len: u8 = 0, seq: u32 = 0 } = null, - /// The revision an Exit last warned this pane was modified at. - exit_warned: ?u32 = null, + /// The revision an Exit or a Restore last warned this pane was modified + /// at (builtins.zig warnModified). + discard_warned: ?u32 = null, /// Native host queues the Linux v9fs launcher in this pane's initial shell. v9fs_on_spawn: bool = false, file: ?File.State = null, -- cgit v1.3