summaryrefslogtreecommitdiff
path: root/src/file_watch.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/file_watch.zig')
-rw-r--r--src/file_watch.zig167
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(.{});