summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 04:18:04 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit35e72930c11af5563da9d62df2b74a7439f36653 (patch)
treebe71dac5f6eea6629c0a40377a6b6fb2ad14c767 /src
parent4b64d4b237b2bc5f8197e2232c84c0d1ed1a8fdb (diff)
downloadpardes-35e72930c11af5563da9d62df2b74a7439f36653.tar.gz
pardes-35e72930c11af5563da9d62df2b74a7439f36653.zip
A file changed on disk never reloads over unsaved edits
A watched file rewritten on disk was loaded over a buffer's unsaved edits and its dirty flag cleared, so Exit and Del no longer asked about them. A clean buffer still reloads; one with edits keeps its text and stays dirty, says <file> changed on disk (get reloads it, Save overwrites it), logs changed <serial>, and its Save warns once before overwriting the newer file, as acme's Put refuses a file modified since read (exec.c:577); get asks as ever (winclean, exec.c:513). The restored-watch test's dirty case now keeps the snapshot, and the filewatch snapshot script (re-recorded by name) checks both halves: the edited pane keeps its edit and says so, the clean one reloads. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/File.zig28
-rw-r--r--src/exec.zig10
-rw-r--r--src/file_watch.zig7
-rw-r--r--src/ninep/ctl.zig27
4 files changed, 70 insertions, 2 deletions
diff --git a/src/File.zig b/src/File.zig
index 810bc766..4011301a 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -58,6 +58,12 @@ pub const State = struct {
/// as an undo to the save point is in acme.
saved_hash: ?u64 = null,
watch_after_save: bool = false,
+ /// The file changed on disk under unsaved edits, which were kept (see
+ /// `changed`): its hash, so the same change is said once, until a Save
+ /// or get settles it. A Save warns once before overwriting it, as acme's
+ /// Put does (exec.c:577, "modified since last read").
+ disk_newer: ?u64 = null,
+ disk_newer_warned: bool = false,
/// Seconds of the last content change, for the filesystem's stat; zero until edited.
mtime: u32 = 0,
/// Non-null for a generated output buffer rather than an on-disk file.
@@ -964,12 +970,30 @@ pub fn redo(p: *Pardes, pane: *Pane) void {
restoreSnap(pane, f, next);
}
-/// Commit an externally rewritten file onto the same undo history as typed
-/// edits. Unsaved work remains one `u` away; there is no third merge state.
+/// A file rewritten on disk. A clean buffer takes the new text, committed
+/// onto the same undo history as typed edits. A buffer with unsaved edits
+/// is never reloaded under them: it keeps its text and stays dirty, and
+/// says so once (`<file> changed on disk (get reloads it, Save overwrites
+/// it)`, logged `changed <serial>`); get then asks before throwing
+/// the edits away and Save before overwriting the newer file. acme reloads
+/// nothing by itself: its get asks (winclean) and its Put refuses a file
+/// modified since it was read (exec.c:577).
pub fn changed(p: *Pardes, id: u8, bytes: []const u8) void {
const pane = p.panes[id] orelse return;
const f = if (pane.file) |*file| file else return;
if (std.mem.eql(u8, f.content, bytes)) return;
+ if (f.saved_hash) |saved| if (f.revision != f.saved_revision and std.hash.Wyhash.hash(0, f.content) != saved) {
+ const disk = std.hash.Wyhash.hash(0, bytes);
+ if (f.disk_newer == disk) return;
+ f.disk_newer = disk;
+ f.disk_newer_warned = false;
+ var said: [limits.host_path_cap + 64]u8 = undefined;
+ p.setMessage(id, std.fmt.bufPrint(&said, "{s} changed on disk (get reloads it, Save overwrites it)", .{f.path}) catch "changed on disk (get reloads it, Save overwrites it)");
+ var rec: [16]u8 = undefined;
+ pardes.ctlfs.events.notePath(p, "changed", std.fmt.bufPrint(&rec, "{d}", .{pane.serial}) catch "");
+ return;
+ };
+ f.disk_newer = null;
const new = p.gpa.dupe(u8, bytes) catch return;
pushUndo(p, pane);
setContent(p, f, new);
diff --git a/src/exec.zig b/src/exec.zig
index b8312a21..953ae0f3 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -249,6 +249,16 @@ pub fn saveFile(p: *Pardes, id: usize) void {
const pane = p.panes[id] orelse return;
const f = if (pane.file) |*file| file else return;
if (f.output != null) return; // nothing behind it yet: saveTo, with a path
+ // Its file changed on disk under these edits: once, as acme's Put says
+ // "modified since last read" (exec.c:577), and the next Save writes.
+ if (f.disk_newer != null) {
+ if (!f.disk_newer_warned) {
+ f.disk_newer_warned = true;
+ var said: [Pardes.SavePath.cap + 64]u8 = undefined;
+ return p.reportFailure(id, std.fmt.bufPrint(&said, "{s} modified on disk since read (Save again to overwrite)", .{f.path}) catch "modified on disk since read (Save again to overwrite)");
+ }
+ f.disk_newer = null;
+ }
p.emit(.{ .save_file = .{ .pane = @intCast(id) } });
}
diff --git a/src/file_watch.zig b/src/file_watch.zig
index 5d998470..b262e8ce 100644
--- a/src/file_watch.zig
+++ b/src/file_watch.zig
@@ -665,6 +665,13 @@ test "restored file watches preserve snapshots and accept later disk changes" {
try tmp.dir.writeFile(io, .{ .sub_path = "watched", .data = "later disk save\n" });
_ = reloadChanged(core, io, gpa, &watches);
+ // A snapshot restored with unsaved text keeps it: a later disk
+ // change is said, never loaded over it (File.changed).
+ if (case.dirty) {
+ try std.testing.expectEqualStrings("restored snapshot\n", pane.file.?.content);
+ try std.testing.expect(pane.file.?.disk_newer != null);
+ continue;
+ }
try std.testing.expectEqualStrings("later disk save\n", pane.file.?.content);
pardes.panes.File.undo(core, pane);
try std.testing.expectEqualStrings("restored snapshot\n", pane.file.?.content);
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 19ca949e..1b1288a1 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -713,6 +713,7 @@ fn get(p: *Pardes, pane: *Pane) u16 {
panes.File.setContent(p, f, bytes);
f.saved_revision = f.revision;
f.saved_hash = std.hash.Wyhash.hash(0, f.content);
+ f.disk_newer = null;
return 0;
}
@@ -1558,3 +1559,29 @@ test "a 10k-line Edit text block is taken in linear time and memory" {
try testing.expect(p.scratch.queryCapacity() < 50 * 1024 * 1024);
try testing.expectEqual(@as(usize, 10_001), std.mem.count(u8, p.panes[0].?.file.?.content, "\n"));
}
+
+test "a file changed on disk under unsaved edits is not reloaded: it says so, get asks, Save warns once" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const f = &p.panes[0].?.file.?;
+ // Clean, it takes the new text.
+ pardes.panes.File.changed(p, 0, "two\n");
+ try testing.expectEqualStrings("two\n", f.content);
+ // Edited, it keeps its text and stays dirty.
+ _ = wr(p, Node.of(serial, .body), "mine\n");
+ pardes.panes.File.changed(p, 0, "three\n");
+ try testing.expectEqualStrings("two\nmine\n", f.content);
+ try testing.expect(f.revision != f.saved_revision);
+ try testing.expect(th.logHas(p, "changed on disk (get reloads it, Save overwrites it)"));
+ var want: [32]u8 = undefined;
+ try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "changed {d}\n", .{serial})));
+ // Exit and get still ask about the edits.
+ try testing.expectEqual(Status.err, wr(p, @intFromEnum(tree.TopFile.ctl), "Exit\n").reply.status);
+ try testing.expect(std.mem.indexOf(u8, wr(p, Node.of(serial, .ctl), "get\n").reply.ename, "Modified") != null);
+ // Save warns once before overwriting the newer file, then writes.
+ const first = wr(p, Node.of(serial, .ctl), "Save\n");
+ try testing.expectEqual(Status.err, first.reply.status);
+ try testing.expect(std.mem.indexOf(u8, first.reply.ename, "modified on disk since read") != null);
+ try testing.expect(wr(p, Node.of(serial, .ctl), "Save\n").saved);
+}