diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 23:26:47 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | fd448801b83bdddf27a1d9af17bf8581356b2b35 (patch) | |
| tree | efb2ef616daf0e99ec6a278eabe3e9086ceba837 /src | |
| parent | 72129eba38522de8f3d0ec6d98b612676d226612 (diff) | |
| download | pardes-fd448801b83bdddf27a1d9af17bf8581356b2b35.tar.gz pardes-fd448801b83bdddf27a1d9af17bf8581356b2b35.zip | |
Del and Delcol refuse unsaved text once, as acme's do
acme's Del asks winclean first and Delcol colclean (exec.c del, delcol):
unsaved text is warned about, and the same word again closes. pardes closed
at once. Both now refuse once through the one-warning-per-word mechanism
Exit uses, from keys, tags, exec and ctl alike. A pane dirty only by a
rename, or an empty one that never held a file, has no text to lose and is
not asked about, and a scratch or a command's output under 100 bytes is not
either, as acme's winclean passes a small unnamed window.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/builtins.zig | 52 | ||||
| -rw-r--r-- | src/panes.zig | 2 |
2 files changed, 48 insertions, 6 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index 384e007f..5b1b4dcb 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -446,6 +446,14 @@ pub const Exec = struct { /// the person at the keyboard. A scratch of under 100 bytes is no loss /// worth the question, as winclean does not ask for one. fn warnModified(c: Ctx, asking: Pane.Discarding) bool { + return warnModifiedIn(c, asking, .all); +} + +/// Which panes a refusal asks about: every one (Exit, Restore), one (Del), +/// or a column's (Delcol). +const Asked = union(enum) { all, pane: usize, column: usize }; + +fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool { var modified = false; // Every pane asked about is named in the one refusal a ctl write gets, // as acme's rowclean names them all (wind.c:511). @@ -453,12 +461,23 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool { var all = std.Io.Writer.fixed(&all_buf); for (c.p.panes, 0..) |slot, id| { const pane = slot orelse continue; + switch (which) { + .all => {}, + .pane => |only| if (id != only) continue, + .column => |col| if ((layout.findPane(c.p, id) orelse continue).col != col) continue, + } const f = pane.file orelse continue; - // A +New scratch saves nowhere, but what is typed in it is as lost. + // A +New scratch saves nowhere, but what is typed in it is as lost; + // under 100 bytes it is not asked about, as acme's winclean does not + // whine about a small unnamed window, nor about a command's output. const scratch = if (f.output) |o| o.from == .cmd and o.from.cmd == .New else false; if (f.revision == f.saved_revision or !(panes.Output.fileTraits(f.output).saves or scratch)) continue; - if (f.output != null and !scratch and f.content.len < 100) continue; - if (scratch and f.content.len == 0) continue; + if (f.output != null and f.content.len < 100) continue; + // Dirty by a rename alone is no text to lose, as get says (ctl.zig). + // Nor is an empty buffer that never held a file. + if (f.saved_hash) |saved| { + if (std.hash.Wyhash.hash(0, f.content) == saved) continue; + } else if (f.content.len == 0) continue; if (pane.discard_warned) |w| if (w.revision == f.revision and w.by == asking) continue; pane.discard_warned = .{ .revision = f.revision, .by = asking }; var buf: [limits.host_path_cap + 48]u8 = undefined; @@ -847,6 +866,9 @@ pub const Del = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { const side = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); + // acme's Del asks winclean first (exec.c del): unsaved text is warned + // about once, and the same Del again, nothing edited since, closes. + if (warnModifiedIn(c, .Del, .{ .pane = c.id })) return; const at = layout.findPane(c.p, c.id); const above = if (at) |f| layout.expandedNeighbor(c.p, f.col, f.idx, .above) else null; const below = if (at) |f| layout.expandedNeighbor(c.p, f.col, f.idx, .below) else null; @@ -956,6 +978,9 @@ pub const Delcol = struct { // The column of the tag it came from, else the pane's: a pane ctl's // Delcol closes that pane's column wherever the keyboard is. const col = c.p.exec_column orelse (layout.findPane(c.p, c.id) orelse return).col; + // acme's Delcol refuses a column with unsaved text (exec.c delcol, + // cols.c colclean), once: the next Delcol closes it. + if (warnModifiedIn(c, .Delcol, .{ .column = col })) return; c.p.removeColumn(col) catch |err| c.p.reportError(c.id, "close column", err); } }; @@ -1744,8 +1769,8 @@ test "Exit and Restore name a modified +New scratch as well as files" { file.file.?.saved_revision = file.file.?.revision -% 1; // modified p.newScratchBelow(p.active); const scratch = &p.panes[p.active].?.file.?; - scratch.content = try p.gpa.realloc(scratch.content, 6); - @memcpy(scratch.content, "notes\n"); + scratch.content = try p.gpa.realloc(scratch.content, 120); + @memset(scratch.content, 'n'); // past acme's 100 bytes, where it whines scratch.revision +%= 1; // typed into, never saved p.fs.no_prompt = true; defer p.fs.no_prompt = false; @@ -1755,3 +1780,20 @@ test "Exit and Restore name a modified +New scratch as well as files" { try std.testing.expect(std.mem.indexOf(u8, said, "test.txt") != null); try std.testing.expect(std.mem.indexOf(u8, said, config.scratch_buffer) != null); } + +test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and close on the second" { + 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 + const serial = pane.serial; + try std.testing.expect(p.executeBuiltinLine(p.active, "Delcol")); + try std.testing.expect(p.paneBySerial(serial) != null); + try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Delcol again to discard)")); + // Each word is warned on its own: Del asks too, then closes. + try std.testing.expect(p.executeBuiltinLine(p.active, "Del")); + try std.testing.expect(p.paneBySerial(serial) != null); + try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Del again to discard)")); + try std.testing.expect(p.executeBuiltinLine(p.active, "Del")); + try std.testing.expect(p.paneBySerial(serial) == null); +} diff --git a/src/panes.zig b/src/panes.zig index 8ca2bb4d..c33fd577 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -25,7 +25,7 @@ pub const EditText = struct { text: []u8, row0: i32 }; pub const Pane = struct { /// The words that throw unsaved text away (builtins.zig warnModified). - pub const Discarding = enum { Exit, Restore, get }; + pub const Discarding = enum { Exit, Restore, get, Del, Delcol }; /// One mouse sweep, per button: the press (c0, r0) and where it is now /// (c1, r1), a stream between them as acme's (edit.sweepCols). c/r are |
