diff options
| -rw-r--r-- | src/File.zig | 40 | ||||
| -rw-r--r-- | src/dump.zig | 8 | ||||
| -rw-r--r-- | src/file_watch.zig | 11 | ||||
| -rw-r--r-- | src/main.zig | 2 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 40 | ||||
| -rw-r--r-- | test/fs_namespace.zig | 3 |
6 files changed, 94 insertions, 10 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)")); +} diff --git a/test/fs_namespace.zig b/test/fs_namespace.zig index bd620c2d..5bce9eda 100644 --- a/test/fs_namespace.zig +++ b/test/fs_namespace.zig @@ -206,7 +206,8 @@ test "explicit OS file opens and restores keep their names and request watches" .kind = .file, .tag = path, .body = "", - .file = .{ .path = path, .content = "const unsaved = 2;\n" }, + // Unsaved text: kept (a clean pane's is read from disk). + .file = .{ .path = path, .content = "const unsaved = 2;\n", .dirty = true }, }); try std.testing.expectEqualStrings(path, copy.file.?.path); try std.testing.expectEqualStrings("const unsaved = 2;\n", copy.file.?.content); |
