From 9f6de14934df03c2e3d521af78ce2e5d7362cc88 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 17:44:42 -0300 Subject: An Exit or Restore refused over a ctl write names every unsaved pane, not just the first Each pane got its own message, but a ctl write fails with its first error only, so a script was told about one pane, and the next Exit discarded both. The refusal now lists them all, as acme's rowclean does, and says the next one discards them all. Co-Authored-By: Claude Opus 5.5 --- src/builtins.zig | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'src/builtins.zig') diff --git a/src/builtins.zig b/src/builtins.zig index 97f7817f..ef425c91 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -240,6 +240,28 @@ test "Exit asks once about unsaved text, and quits when asked again" { try std.testing.expect(p.quit); } +test "Exit over a ctl write names every pane it asks about, and the next Exit discards them all" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const first = try p.setTestFile("saved\n"); + first.file.?.saved_revision = first.file.?.revision -% 1; + p.newScratchBelow(p.active); + const second = p.panes[p.active].?; + panes.File.setContent(p, &second.file.?, try std.testing.allocator.dupe(u8, "x" ** 120)); + try std.testing.expect(second.file.?.revision != second.file.?.saved_revision); + p.fs.no_prompt = true; + defer p.fs.no_prompt = false; + try std.testing.expect(p.executeBuiltinLine(p.active, "Exit")); + try std.testing.expect(!p.quit); + const said = p.fs.failure[0..p.fs.failure_len]; + try std.testing.expect(std.mem.indexOf(u8, said, first.file.?.path) != null); + try std.testing.expect(std.mem.indexOf(u8, said, second.file.?.path) != null); + try std.testing.expect(std.mem.endsWith(u8, said, ": Modified (Exit again to discard them all)")); + p.fs.failure_len = 0; + try std.testing.expect(p.executeBuiltinLine(p.active, "Exit")); + try std.testing.expect(p.quit); +} + /// A dump file that exists, for a Restore to get past reading it. fn testDump(buf: []u8) ![]const u8 { var tmp = std.testing.tmpDir(.{}); @@ -419,6 +441,10 @@ pub const Exec = struct { /// worth the question, as winclean does not ask for one. fn warnModified(c: Ctx, asking: Pane.Discarding) 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). + var all_buf: [256]u8 = undefined; + var all = std.Io.Writer.fixed(&all_buf); for (c.p.panes, 0..) |slot, id| { const pane = slot orelse continue; const f = pane.file orelse continue; @@ -428,8 +454,15 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool { pane.discard_warned = .{ .revision = f.revision, .by = asking }; var buf: [limits.host_path_cap + 48]u8 = undefined; c.p.reportFailure(id, std.fmt.bufPrint(&buf, "{s}: Modified ({s} again to discard)", .{ f.path, @tagName(asking) }) catch f.path); + all.print("{s}{s}", .{ if (modified) ", " else "", f.path }) catch {}; modified = true; } + if (modified and c.p.fs.no_prompt) { + all.print(": Modified ({s} again to discard them all)", .{@tagName(asking)}) catch {}; + const said = @import("Messages.zig").clip(all.buffered(), c.p.fs.failure.len); + @memcpy(c.p.fs.failure[0..said.len], said); + c.p.fs.failure_len = @intCast(said.len); + } return modified; } -- cgit v1.3