diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-06 18:11:36 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-07 13:59:12 -0300 |
| commit | 60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch) | |
| tree | 310fc734173cf771881f4691c71909135fadde97 /src/file_watch.zig | |
| parent | fa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff) | |
| download | pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip | |
Refactor panes and filesystem; replace FUSE with 9P
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples.
Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
Diffstat (limited to 'src/file_watch.zig')
| -rw-r--r-- | src/file_watch.zig | 167 |
1 files changed, 150 insertions, 17 deletions
diff --git a/src/file_watch.zig b/src/file_watch.zig index dfdc0556..b2b7e380 100644 --- a/src/file_watch.zig +++ b/src/file_watch.zig @@ -13,8 +13,8 @@ const builtin = @import("builtin"); const libc = std.c; const linux = std.os.linux; const pardes = @import("pardes.zig"); -const look = @import("look.zig"); -const message = @import("message.zig"); +const filesystem = @import("fs.zig"); +const message = pardes.Pardes.Message; pub const Identity = struct { inode: std.Io.File.INode, @@ -51,8 +51,8 @@ pub const Generation = union(enum) { /// rename-over gave the pathname a new inode, without every caller between it /// and a host having to carry the descriptor. /// -/// `generation` is the last snapshot the core accepted, not merely one a host -/// observed; serial prevents a reused pane slot from committing stale data. +/// `generation` is the reconciled disk snapshot; Restore establishes this +/// baseline without replacing its saved buffer. Serial rejects reused slots. pub const Watch = struct { wd: c_int, file_wd: c_int = -1, @@ -319,7 +319,7 @@ fn markFile(kq: c_int, path_z: [:0]const u8) c_int { fn remarkFile(live: *Watch, path: ?[]const u8) void { if (comptime builtin.os.tag != .macos) return; if (live.kq < 0) return; - const watched = path orelse return; + const watched = filesystem.localPath(path orelse return) orelse return; var path_buf: [4096:0]u8 = undefined; if (watched.len >= path_buf.len) return; @memcpy(path_buf[0..watched.len], watched); @@ -380,8 +380,10 @@ fn watchPath( watches[slot] = null; unmark(fd, old, !shared); } - const watched_path = path orelse return; - const dir = std.fs.path.dirname(watched_path) orelse "."; + const declared_path = path orelse return; + const watched_path = filesystem.localPath(declared_path) orelse return; + const stat = std.Io.Dir.cwd().statFile(std.Io.Threaded.global_single_threaded.io(), watched_path, .{}) catch null; + const dir = if (stat != null and stat.?.kind == .directory) watched_path else std.fs.path.dirname(watched_path) orelse "."; var dir_buf: [4096:0]u8 = undefined; if (dir.len >= dir_buf.len) return; @memcpy(dir_buf[0..dir.len], dir); @@ -391,7 +393,7 @@ fn watchPath( watches[slot] = .{ .wd = wd, .kq = fd, .serial = serial, .generation = generation }; // The file half, which only macos has and only for a path that exists yet. if (comptime builtin.os.tag == .macos) { - if (watches[slot]) |*live| remarkFile(live, watched_path); + if (watches[slot]) |*live| remarkFile(live, declared_path); } } @@ -416,7 +418,6 @@ pub fn watchPane( pub fn reloadPane( core: *pardes.Pardes, io: std.Io, - gpa: std.mem.Allocator, watches: *Table, id: usize, announce: bool, @@ -434,8 +435,8 @@ pub fn reloadPane( } if (pane.file) |file| { - const bytes = look.readFile(gpa, file.path) catch return false; - defer gpa.free(bytes); + const bytes = filesystem.read(core, file.path) catch return false; + defer core.gpa.free(bytes); const hash = std.hash.Wyhash.hash(0, bytes); switch (watched.generation) { .text => |accepted| if (accepted == hash) return false, @@ -485,11 +486,11 @@ pub fn reloadPane( pub fn applyEffect( core: *pardes.Pardes, io: std.Io, - gpa: std.mem.Allocator, fd: c_int, watches: *Table, id: u8, on: bool, + mode: pardes.WatchMode, ) bool { var path: ?[]const u8 = null; var serial: u32 = 0; @@ -505,7 +506,15 @@ pub fn applyEffect( } }; watchPane(fd, watches, id, path, serial, generation); - return on and watches[id] != null and reloadPane(core, io, gpa, watches, id, false); + if (on and mode == .baseline_disk) if (watches[id]) |*watched| { + const pane = core.panes[id] orelse return false; + const file = pane.file orelse return reloadPane(core, io, watches, id, false); + const bytes = filesystem.read(core, file.path) catch return false; + defer core.gpa.free(bytes); + watched.generation = .{ .text = std.hash.Wyhash.hash(0, bytes) }; + return false; + }; + return on and watches[id] != null and reloadPane(core, io, watches, id, false); } /// Reconcile every mark after a coalesced directory wake. @@ -517,7 +526,7 @@ pub fn reloadChanged( ) bool { var retry = false; for (watches[0..pardes.MAX_PANES], 0..) |slot, id| { - if (slot != null) retry = reloadPane(core, io, gpa, watches, id, true) or retry; + if (slot != null) retry = reloadPane(core, io, watches, id, true) or retry; } if (watches[theme_slot] != null) retry = reloadTheme(core, gpa, watches, true) or retry; @@ -539,7 +548,7 @@ pub fn applyThemeEffect( watchPath(fd, watches, theme_slot, null, 0, .{ .text = 0 }); if (!on) return false; const request = core.themeFileRequest(generation) orelse return false; - const bytes = look.readFile(gpa, request.path) catch |err| { + const bytes = filesystem.readFile(gpa, request.path) catch |err| { core.failThemeFile(generation, err); return false; }; @@ -567,7 +576,7 @@ pub fn reloadTheme( if (comptime builtin.os.tag == .macos) { if (watches[theme_slot]) |*entry| remarkFile(entry, request.path); } - const bytes = look.readFile(gpa, request.path) catch |err| { + const bytes = filesystem.readFile(gpa, request.path) catch |err| { core.failThemeFile(watched.serial, err); return false; }; @@ -589,7 +598,8 @@ pub fn reloadTheme( } pub fn identify(io: std.Io, path: []const u8) !Identity { - const stat = try std.Io.Dir.cwd().statFile(io, path, .{}); + const native = filesystem.localPath(path) orelse return error.NonLocalPath; + const stat = try std.Io.Dir.cwd().statFile(io, native, .{}); if (stat.kind != .file) return error.NotFile; return .{ .inode = stat.inode, @@ -599,6 +609,129 @@ pub fn identify(io: std.Io, path: []const u8) !Identity { }; } +test "restored file watches preserve snapshots and accept later disk changes" { + if (comptime !supported or !filesystem.platform_has_fs) return; + const io = std.testing.io; + const gpa = std.testing.allocator; + for ([_]struct { dirty: bool, exists: bool }{ + .{ .dirty = false, .exists = true }, + .{ .dirty = true, .exists = true }, + .{ .dirty = true, .exists = false }, + }) |case| { + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + if (case.exists) try tmp.dir.writeFile(io, .{ .sub_path = "watched", .data = "disk baseline\n" }); + var path_buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/watched", .{tmp.sub_path}); + const core = try pardes.Pardes.init(gpa, .{ .tty_only = true }); + defer core.deinit(); + while (core.nextEffect()) |_| {} + const id: u8 = @intCast(core.freeSlot().?); + const pane = try pardes.panes.File.restore(core, id, .{ + .kind = .file, + .tag = "", + .body = "", + .file = .{ .path = path, .content = "restored snapshot\n", .dirty = case.dirty }, + }); + const fd = init(true); + if (fd < 0) return error.NoWatcher; + defer _ = libc.close(fd); + var watches: Table = @splat(null); + defer watchPane(fd, &watches, id, null, pane.serial, .{ .text = 0 }); + var armed = false; + while (core.nextEffect()) |effect| switch (effect) { + .watch => |watch| if (watch.pane == id and watch.on) { + try std.testing.expectEqual(.baseline_disk, watch.mode); + try std.testing.expect(!applyEffect(core, io, fd, &watches, id, watch.on, watch.mode)); + armed = true; + }, + else => {}, + }; + try std.testing.expect(armed and watches[id] != null); + try std.testing.expectEqualStrings("restored snapshot\n", 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 tmp.dir.writeFile(io, .{ .sub_path = "watched", .data = "later disk save\n" }); + _ = reloadChanged(core, io, gpa, &watches); + 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); + _ = reloadChanged(core, io, gpa, &watches); + try std.testing.expectEqualStrings("restored snapshot\n", pane.file.?.content); + } +} + +test "ordinary file watches still reconcile changes between open and watch" { + if (comptime !supported or !filesystem.platform_has_fs) return; + const io = std.testing.io; + const gpa = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(io, .{ .sub_path = "watched", .data = "opened\n" }); + var path_buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/watched", .{tmp.sub_path}); + const core = try pardes.Pardes.init(gpa, .{ .tty_only = true }); + defer core.deinit(); + while (core.nextEffect()) |_| {} + const id: u8 = @intCast(core.freeSlot().?); + const pane = try pardes.panes.File.open(core, id, path, 0); + try tmp.dir.writeFile(io, .{ .sub_path = "watched", .data = "changed before watch\n" }); + const fd = init(true); + if (fd < 0) return error.NoWatcher; + defer _ = libc.close(fd); + var watches: Table = @splat(null); + defer watchPane(fd, &watches, id, null, pane.serial, .{ .text = 0 }); + var armed = false; + while (core.nextEffect()) |effect| switch (effect) { + .watch => |watch| if (watch.pane == id and watch.on) { + try std.testing.expectEqual(.reconcile, watch.mode); + try std.testing.expect(!applyEffect(core, io, fd, &watches, id, watch.on, watch.mode)); + armed = true; + }, + else => {}, + }; + try std.testing.expect(armed and watches[id] != null); + try std.testing.expectEqualStrings("changed before watch\n", pane.file.?.content); + pardes.panes.File.undo(core, pane); + try std.testing.expectEqualStrings("opened\n", pane.file.?.content); +} + +test "explicit OS directory watches refresh prefix-preserving listings" { + if (comptime !supported or !filesystem.platform_has_fs) return; + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .tty_only = true }); + defer core.deinit(); + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "first", .data = "first\n" }); + var directory_buf: [4096]u8 = undefined; + const directory = directory_buf[0..try tmp.dir.realPath(std.testing.io, &directory_buf)]; + var path_buf: [4102]u8 = undefined; + const path = try std.fmt.bufPrint(&path_buf, "/n/os{s}", .{directory}); + const pane_id = core.freeSlot().?; + const pane = try pardes.panes.File.open(core, pane_id, path, 0); + const fd = init(true); + if (fd < 0) return error.NoWatcher; + defer _ = libc.close(fd); + var watches: Table = @splat(null); + defer watchPane(fd, &watches, @intCast(pane_id), null, pane.serial, .{ .text = 0 }); + _ = applyEffect(core, std.testing.io, fd, &watches, @intCast(pane_id), true, .reconcile); + try std.testing.expect(watches[pane_id] != null); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "second", .data = "second\n" }); + _ = reloadChanged(core, std.testing.io, gpa, &watches); + try std.testing.expect(std.mem.indexOf(u8, pane.file.?.content, "/second\n") != null); + var rows = std.mem.tokenizeScalar(u8, pane.file.?.content, '\n'); + while (rows.next()) |row| { + try std.testing.expect(std.mem.startsWith(u8, row, path)); + const bytes = try filesystem.read(core, row); + defer gpa.free(bytes); + try std.testing.expect(std.mem.endsWith(u8, bytes, "\n")); + } +} + test "file identity changes for in-place and rename-over writes" { const io = std.testing.io; var tmp = std.testing.tmpDir(.{}); |
