summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/File.zig40
-rw-r--r--src/dump.zig8
-rw-r--r--src/file_watch.zig11
-rw-r--r--src/main.zig2
-rw-r--r--src/ninep/ctl.zig40
5 files changed, 92 insertions, 9 deletions
diff --git a/src/File.zig b/src/File.zig
index c738ee58..d41007c3 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -105,6 +105,7 @@ pub const State = struct {
pub const NodeStep = struct { ranges: []modal.Selection, primary: usize };
pub fn dumpPane(
+ p: *Pardes,
arena: std.mem.Allocator,
pane: *const Pane,
file: *const State,
@@ -114,6 +115,18 @@ pub fn dumpPane(
origin: []const u8,
origin_arg: []const u8,
) !dump.Pane {
+ const dirty = file.revision != file.saved_revision;
+ const on_disk = onDisk(file.path);
+ // A clean file's text is its file's: read from disk when restored, so
+ // a change made there since is what comes back. Any other is kept, once.
+ const from_disk = file.output == null and !dirty and on_disk;
+ // What an unsaved pane's text was read against: the disk as it is, or,
+ // when a change there was seen already, something it is not.
+ const disk_hash: ?u64 = if (file.output != null or !dirty or !on_disk) null else if (file.disk_newer) |newer| newer +% 1 else blk: {
+ const bytes = filesystem.read(p, file.path) catch break :blk null;
+ defer p.gpa.free(bytes);
+ break :blk std.hash.Wyhash.hash(0, bytes);
+ };
return .{
.kind = .file,
.tag = tag,
@@ -127,10 +140,10 @@ pub fn dumpPane(
.tree_context = file.tree_context,
.location_rows = try dumpLocationRows(arena, file.location_rows),
.path = file.path,
- .content = file.content,
- .content_b64 = try dump.encodeBytes(arena, file.content),
- .dirty = file.revision != file.saved_revision,
- .on_disk = onDisk(file.path),
+ .content_b64 = if (from_disk) "" else try dump.encodeBytes(arena, file.content),
+ .dirty = dirty,
+ .disk_hash = disk_hash,
+ .on_disk = on_disk,
.origin = origin,
.origin_arg = origin_arg,
.mini_source = if (file.mini) |mini| mini.source else "",
@@ -855,7 +868,16 @@ pub fn restore(p: *Pardes, id: usize, src: dump.Pane) !*Pane {
saved.content_b64.len > encoded_limit or saved.mini_colors_b64.len > encoded_limit)
return error.InvalidMini;
}
- const content: []u8 = if (saved.content_b64.len > 0)
+ // A clean file's text is read from its file: one changed on disk since
+ // the dump comes back changed, never as the old text marked clean (and
+ // a dump from before, which kept it, is read so too). Gone from disk, the
+ // dump's text if it kept one.
+ const from_disk: ?[]u8 = if (Output.fromWord(saved.origin) == null and !saved.dirty and saved.on_disk and
+ saved.mini_source.len == 0 and filesystem.localPath(saved.path) != null)
+ filesystem.read(p, saved.path) catch null
+ else
+ null;
+ const content: []u8 = if (from_disk) |bytes| bytes else if (saved.content_b64.len > 0)
try dump.decodeBytes(p.gpa, saved.content_b64)
else
try p.gpa.dupe(u8, saved.content);
@@ -923,6 +945,14 @@ pub fn restore(p: *Pardes, id: usize, src: dump.Pane) !*Pane {
// One never on disk (Config's new file, a name not yet saved) is
// restored as it was: clean, and nothing deleted.
if (output == null and saved.on_disk and !onDisk(path)) deleted(p, @intCast(id));
+ // Unsaved, and its file changed on disk since the text was read: said,
+ // and its first Save asks before overwriting the newer file.
+ if (saved.disk_hash) |read_against| if (output == null and onDisk(path)) {
+ if (filesystem.read(p, path)) |bytes| {
+ defer p.gpa.free(bytes);
+ if (std.hash.Wyhash.hash(0, bytes) != read_against) changed(p, @intCast(id), bytes);
+ } else |_| {}
+ };
return pane;
}
diff --git a/src/dump.zig b/src/dump.zig
index ca3751f2..1420ea8f 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -208,9 +208,16 @@ pub const LocationRow = struct {
pub const File = struct {
path: []const u8 = "",
+ /// The text, in older dumps plain and base64 both; now base64 alone,
+ /// and only for a pane whose text is not its file's on disk (unsaved,
+ /// an output, a file never written): a clean file's is read from disk.
content: []const u8 = "",
content_b64: []const u8 = "",
dirty: bool = false,
+ /// An unsaved pane's: the hash of the file on disk its text was read
+ /// against. A Restore that finds another there says it changed, and
+ /// Save asks before overwriting it.
+ disk_hash: ?u64 = null,
/// Its file was on disk when dumped: one gone by the Restore is said
/// deleted, one never written (Config's new file) is not. True in
/// older dumps.
@@ -728,6 +735,7 @@ pub fn dumpState(p: *Pardes) !void {
const body = try body_layer.bodyText(p, arena, pane);
const scroll: usize = @intCast(@max(0, pane.scroll()));
var dp: Pane = if (pane.file) |*f| try pardes.panes.File.dumpPane(
+ p,
arena,
pane,
f,
diff --git a/src/file_watch.zig b/src/file_watch.zig
index 6c746e74..46764a73 100644
--- a/src/file_watch.zig
+++ b/src/file_watch.zig
@@ -687,11 +687,14 @@ test "restored file watches preserve snapshots and accept later disk changes" {
else => {},
};
try std.testing.expect(armed and watches[id] != null);
- try std.testing.expectEqualStrings("restored snapshot\n", pane.file.?.content);
+ // A clean pane's text is its file's, read at the Restore: a dump
+ // that kept other text (an older dump) is not what comes back.
+ const restored: []const u8 = if (case.dirty) "restored snapshot\n" else "disk baseline\n";
+ try std.testing.expectEqualStrings(restored, pane.file.?.content);
try std.testing.expectEqual(@as(usize, 0), pane.file.?.history.undo_len);
try std.testing.expectEqual(case.dirty, pane.file.?.revision != pane.file.?.saved_revision);
_ = reloadChanged(core, io, gpa, &watches);
- try std.testing.expectEqualStrings("restored snapshot\n", pane.file.?.content);
+ try std.testing.expectEqualStrings(restored, pane.file.?.content);
try tmp.dir.writeFile(io, .{ .sub_path = "watched", .data = "later disk save\n" });
_ = reloadChanged(core, io, gpa, &watches);
@@ -704,9 +707,9 @@ test "restored file watches preserve snapshots and accept later disk changes" {
}
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);
+ try std.testing.expectEqualStrings(restored, pane.file.?.content);
_ = reloadChanged(core, io, gpa, &watches);
- try std.testing.expectEqualStrings("restored snapshot\n", pane.file.?.content);
+ try std.testing.expectEqualStrings(restored, pane.file.?.content);
}
}
diff --git a/src/main.zig b/src/main.zig
index 08ead756..5dd6a8e6 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -822,6 +822,8 @@ test {
_ = @import("host_io.zig");
_ = @import("host_io.zig").Shell;
_ = @import("host_io.zig").Lsp;
+ // Its tests too: imported by the hosts only, it was never collected.
+ _ = @import("file_watch.zig");
_ = @import("lsp/lsp_client.zig");
_ = @import("lsp/lsp.zig");
if (comptime pardes.platform == .tty) {
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index f4aff960..1a2dbdbc 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -4213,3 +4213,43 @@ test "Petscii, Palette and Ascii take the state their tag word names, set rather
_ = wr(p, ctl_node, "Petscii maybe\n");
try testing.expect(th.logHas(p, "Petscii takes on or off"));
}
+
+test "a Restore reads a clean file from disk; an unsaved one whose file changed since is said so, and its first Save asks" {
+ if (comptime !pardes.hosted) return;
+ const gpa = testing.allocator;
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "a.txt", .data = "one\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ var path_buf: [4200]u8 = undefined;
+ const path = try std.fmt.bufPrint(&path_buf, "{s}/a.txt", .{dir});
+ const p = try Pardes.init(gpa, .{ .file = path, .tty_only = true, .cols = 80, .rows = 24 });
+ defer p.deinit();
+ // Clean: the dump keeps no text, and the newer disk is what comes back.
+ try pardes.dump.dumpState(p);
+ try testing.expect(std.mem.indexOf(u8, p.dump_out.?, "b25lCg==") == null); // "one\n" in base64
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "a.txt", .data = "two\n" });
+ {
+ const restored = try pardes.dump.restore(p, p.dump_out.?, "/tmp/pardes.dump.zon");
+ defer restored.deinit();
+ const f = pane_files.fileOf(restored.panes[0].?).?;
+ try testing.expectEqualStrings("two\n", f.content);
+ try testing.expectEqual(f.saved_revision, f.revision);
+ }
+ // Unsaved: its text comes back, the disk changed under it is said, and
+ // Save asks once before overwriting it.
+ const f = pane_files.fileOf(p.panes[0].?).?;
+ const edited = try gpa.dupe(u8, "edited\n");
+ pardes.panes.File.setContent(p, f, edited);
+ try testing.expect(f.revision != f.saved_revision);
+ try pardes.dump.dumpState(p);
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "a.txt", .data = "three\n" });
+ const restored = try pardes.dump.restore(p, p.dump_out.?, "/tmp/pardes.dump.zon");
+ defer restored.deinit();
+ const rf = pane_files.fileOf(restored.panes[0].?).?;
+ try testing.expectEqualStrings("edited\n", rf.content);
+ try testing.expect(rf.disk_newer != null);
+ pardes.exec.saveFile(restored, 0);
+ try testing.expect(th.logHas(restored, "modified on disk since read (Save again to overwrite)"));
+}