summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 17:44:42 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit9f6de14934df03c2e3d521af78ce2e5d7362cc88 (patch)
tree6c05f115b5e6a21c1ad81d9d0236c5b969332ae8 /src
parent5d29c3151e40c05e764a9112d407082df89a19e1 (diff)
downloadpardes-9f6de14934df03c2e3d521af78ce2e5d7362cc88.tar.gz
pardes-9f6de14934df03c2e3d521af78ce2e5d7362cc88.zip
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 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig33
1 files changed, 33 insertions, 0 deletions
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;
}