summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 01:21:21 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit8b04e53c194f85256ba6ea4f3d32a06367ee56db (patch)
tree7bdead176c8b0e51d4eceb99459cc1ee43c055e4
parent8743acbf8edd5e08e639f68022d135a244052658 (diff)
downloadpardes-8b04e53c194f85256ba6ea4f3d32a06367ee56db.tar.gz
pardes-8b04e53c194f85256ba6ea4f3d32a06367ee56db.zip
A pane renamed but never edited closes on the first Del
The GUI golden's wide-glyph pane, renamed and never edited, was refused by Del as modified, and every frame after it kept the pane. A rename marks a pane dirty, but its text is what it loaded: the check now asks whether the text was ever edited (its first edit keeps a hash of what it had), not only whether it is dirty, as get's refusal does. Tests that faked an edit by the dirty mark alone now fake the hash too. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/builtins.zig27
1 files changed, 23 insertions, 4 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index 32e3cd84..51771c8f 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -234,6 +234,7 @@ test "Exit asks once about unsaved text, and quits when asked again" {
defer p.deinit();
const pane = try p.setTestFile("saved\n");
pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ pane.file.?.saved_hash = 0; // edited: what it had is not what it has
try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
try std.testing.expect(!p.quit);
try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Exit again to discard)"));
@@ -247,6 +248,7 @@ test "Exit over a ctl write names every pane it asks about, and the next Exit di
defer p.deinit();
const first = try p.setTestFile("saved\n");
first.file.?.saved_revision = first.file.?.revision -% 1;
+ first.file.?.saved_hash = 0; // edited: what it had is not what it has
p.newScratchBelow(p.active);
const second = p.panes[p.active].?;
panes.File.setContent(p, &second.file.?, try std.testing.allocator.dupe(u8, "x" ** 120));
@@ -282,6 +284,7 @@ test "Restore asks about unsaved text as Exit does, and restores when asked agai
defer p.deinit();
const pane = try p.setTestFile("saved\n");
pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ pane.file.?.saved_hash = 0; // edited: what it had is not what it has
var line_buf: [4200]u8 = undefined;
const line = try testDump(&line_buf);
try std.testing.expect(p.executeBuiltinLine(p.active, line));
@@ -296,6 +299,7 @@ test "a Restore of a dump that cannot be read says so, before the unsaved-text w
defer p.deinit();
const pane = try p.setTestFile("saved\n");
pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ pane.file.?.saved_hash = 0; // edited: what it had is not what it has
p.fs.no_prompt = true;
defer p.fs.no_prompt = false;
try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /nope/missing.zon"));
@@ -310,6 +314,7 @@ test "a Restore's warning is not an Exit's: each word is warned on its own" {
defer p.deinit();
const pane = try p.setTestFile("saved\n");
pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ pane.file.?.saved_hash = 0; // edited: what it had is not what it has
var line_buf: [4200]u8 = undefined;
try std.testing.expect(p.executeBuiltinLine(p.active, try testDump(&line_buf)));
try std.testing.expect(p.restore_req == null);
@@ -474,10 +479,10 @@ fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) bool {
if (f.revision == f.saved_revision or !(panes.Output.fileTraits(f.output).saves or scratch)) 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;
+ // Nor is a buffer never edited at all: its first edit is what keeps
+ // the text it had (File.zig), so with none there is nothing to lose.
+ const saved = f.saved_hash orelse continue;
+ if (std.hash.Wyhash.hash(0, f.content) == saved) 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;
@@ -1767,11 +1772,13 @@ test "Exit and Restore name a modified +New scratch as well as files" {
defer p.deinit();
const file = try p.setTestFile("saved\n");
file.file.?.saved_revision = file.file.?.revision -% 1; // modified
+ file.file.?.saved_hash = 0; // edited: what it had is not what it has
p.newScratchBelow(p.active);
const scratch = &p.panes[p.active].?.file.?;
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
+ scratch.saved_hash = 0; // edited: what it had is not what it has
p.fs.no_prompt = true;
defer p.fs.no_prompt = false;
try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
@@ -1786,6 +1793,7 @@ test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and clo
defer p.deinit();
const pane = try p.setTestFile("saved\n");
pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ pane.file.?.saved_hash = 0; // edited: what it had is not what it has
const serial = pane.serial;
try std.testing.expect(p.executeBuiltinLine(p.active, "Delcol"));
try std.testing.expect(p.paneBySerial(serial) != null);
@@ -1797,3 +1805,14 @@ test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and clo
try std.testing.expect(p.executeBuiltinLine(p.active, "Del"));
try std.testing.expect(p.paneBySerial(serial) == null);
}
+
+test "a pane renamed but never edited has no text to lose: Del closes it at once" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ p.newScratchBelow(p.active);
+ const pane = try p.setTestFile("loaded\n");
+ pane.file.?.saved_revision = pane.file.?.revision -% 1; // dirty by the rename alone
+ const serial = pane.serial;
+ try std.testing.expect(p.executeBuiltinLine(0, "Del"));
+ try std.testing.expect(p.paneBySerial(serial) == null);
+}