Compare commits

...
Sign in to create a new pull request.

12 commits

7 changed files with 367 additions and 11 deletions

View file

@ -349,6 +349,12 @@ pub fn build_exe(
});
const syntax_mod = syntax_dep.module("syntax");
const nightwatch_dep = b.dependency("nightwatch", .{
.target = target,
.optimize = optimize,
});
const nightwatch_mod = nightwatch_dep.module("nightwatch");
const help_mod = b.createModule(.{
.root_source_file = b.path("help.md"),
});
@ -604,6 +610,15 @@ pub fn build_exe(
},
});
const file_watcher_mod = b.createModule(.{
.root_source_file = b.path("src/file_watcher.zig"),
.imports = &.{
.{ .name = "nightwatch", .module = nightwatch_mod },
.{ .name = "cbor", .module = cbor_mod },
.{ .name = "thespian", .module = thespian_mod },
},
});
const project_manager_mod = b.createModule(.{
.root_source_file = b.path("src/project_manager.zig"),
.imports = &.{
@ -619,6 +634,7 @@ pub fn build_exe(
.{ .name = "fuzzig", .module = fuzzig_dep.module("fuzzig") },
.{ .name = "git", .module = git_mod },
.{ .name = "VcsStatus", .module = VcsStatus_mod },
.{ .name = "file_watcher", .module = file_watcher_mod },
},
});
@ -723,6 +739,13 @@ pub fn build_exe(
exe.root_module.addImport("version", b.createModule(.{ .root_source_file = version_file }));
exe.root_module.addImport("version_info", b.createModule(.{ .root_source_file = version_info_file }));
if (target.result.os.tag == .macos) {
exe.addFrameworkPath(b.dependency("xcode-frameworks", .{}).path("Frameworks"));
exe.addLibraryPath(b.dependency("xcode-frameworks", .{}).path("lib"));
exe.linkFramework("CoreServices");
exe.linkFramework("CoreFoundation");
}
if (target.result.os.tag == .windows) {
exe.addWin32ResourceFile(.{
.file = b.path("src/win32/flow.rc"),

View file

@ -46,6 +46,14 @@
.url = "git+https://github.com/ziglibs/diffz.git#fbdf690b87db6b1142bbce6d4906f90b09ce60bb",
.hash = "diffz-0.0.1-G2tlIezMAQBwGNGDs7Hn_N25dWSjEzgR_FAx9GFAvCuZ",
},
.@"xcode-frameworks" = .{
.url = "git+https://github.com/hexops/xcode-frameworks?ref=main#8a1cfb373587ea4c9bb1468b7c986462d8d4e10e",
.hash = "N-V-__8AALShqgXkvqYU6f__FrA22SMWmi2TXCJjNTO1m8XJ",
},
.nightwatch = .{
.url = "git+https://git.flow-control.dev/neurocyte/nightwatch?ref=master#a1e5e3e9a5126d1ff7ce4959a823ea12f20ef0ae",
.hash = "nightwatch-0.1.0-uXzeH8WuAAC95apH6JQZQDzCGrXK2PRPq0rDPPxwUI3Z",
},
},
.paths = .{
"include",

View file

@ -587,6 +587,10 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query_: []c
return @min(max, matches.items.len);
}
fn walk_tree_dir_callback(parent: tp.pid_ref, root_path: []const u8, dir_path: []const u8) error{Exit}!void {
try parent.send(.{ "walk_tree_dir", root_path, dir_path });
}
fn walk_tree_entry_callback(parent: tp.pid_ref, root_path: []const u8, file_path: []const u8, mtime_high: i64, mtime_low: i64) error{Exit}!void {
const file_type: []const u8, const file_icon: []const u8, const file_color: u24 = guess_file_type(file_path);
try parent.send(.{ "walk_tree_entry", root_path, file_path, mtime_high, mtime_low, file_type, file_icon, file_color });
@ -777,6 +781,61 @@ fn loaded(self: *Self, parent: tp.pid_ref) OutOfMemoryError!void {
parent.send(.{ "PRJ", "open_done", self.name, self.longest_file_path, self.files.items.len }) catch {};
}
pub fn file_added(self: *Self, file_path: []const u8) OutOfMemoryError!void {
for (self.files.items) |file|
if (std.mem.eql(u8, file.path, file_path)) return;
for (self.pending.items) |file|
if (std.mem.eql(u8, file.path, file_path)) return;
const file_type, const file_icon, const file_color = guess_file_type(file_path);
(try self.files.addOne(self.allocator)).* = .{
.path = try self.allocator.dupe(u8, file_path),
.type = file_type,
.icon = file_icon,
.color = file_color,
.mtime = std.time.nanoTimestamp(),
};
self.longest_file_path = @max(self.longest_file_path, file_path.len);
self.sort_files_by_mtime();
}
pub fn file_modified(self: *Self, file_path: []const u8) void {
for (self.files.items) |*file| {
if (!std.mem.eql(u8, file.path, file_path)) continue;
file.mtime = std.time.nanoTimestamp();
self.sort_files_by_mtime();
return;
}
}
pub fn file_renamed(self: *Self, from_path: []const u8, to_path: []const u8) OutOfMemoryError!void {
for (self.files.items) |*file| {
if (!std.mem.eql(u8, file.path, to_path)) continue;
self.file_deleted(from_path);
return;
}
for (self.files.items) |*file| {
if (!std.mem.eql(u8, file.path, from_path)) continue;
const new_path = try self.allocator.dupe(u8, to_path);
self.allocator.free(file.path);
file.path = new_path;
file.mtime = std.time.nanoTimestamp();
self.longest_file_path = @max(self.longest_file_path, to_path.len);
self.sort_files_by_mtime();
return;
}
return self.file_added(to_path);
}
pub fn file_deleted(self: *Self, file_path: []const u8) void {
for (self.files.items, 0..) |file, i| {
if (!std.mem.eql(u8, file.path, file_path)) continue;
self.allocator.free(file.path);
_ = self.files.swapRemove(i);
self.sort_files_by_mtime();
return;
}
}
pub fn update_mru(self: *Self, file_path: []const u8, row: usize, col: usize) OutOfMemoryError!void {
defer self.sort_files_by_mtime();
try self.update_mru_internal(file_path, std.time.nanoTimestamp(), row, col);
@ -2796,6 +2855,7 @@ fn start_walker(self: *Self) void {
.follow_directory_symlinks = tp.env.get().is("follow_directory_symlinks"),
.maximum_symlink_depth = @intCast(tp.env.get().num("maximum_symlink_depth")),
.log_ignored_links = tp.env.get().is("log_ignored_links"),
.dir_callback = walk_tree_dir_callback,
}) catch blk: {
self.state.walk_tree = .failed;
break :blk null;

171
src/file_watcher.zig Normal file
View file

@ -0,0 +1,171 @@
const std = @import("std");
const tp = @import("thespian");
const cbor = @import("cbor");
const nightwatch = @import("nightwatch");
const builtin = @import("builtin");
pid: tp.pid_ref,
const Self = @This();
const module_name = @typeName(Self);
pub const EventType = nightwatch.EventType;
pub const Error = error{
FileWatcherSendFailed,
ThespianSpawnFailed,
OutOfMemory,
};
const SpawnError = error{ OutOfMemory, ThespianSpawnFailed };
pub fn watch(path: []const u8) Error!void {
return send(.{ "watch", path });
}
pub fn unwatch(path: []const u8) Error!void {
return send(.{ "unwatch", path });
}
pub fn start() SpawnError!void {
_ = try get();
}
pub fn shutdown() void {
const pid = tp.env.get().proc(module_name);
if (pid.expired()) return;
pid.send(.{"shutdown"}) catch {};
}
fn get() SpawnError!Self {
const pid = tp.env.get().proc(module_name);
return if (pid.expired()) create() else .{ .pid = pid };
}
fn send(message: anytype) Error!void {
return (try get()).pid.send(message) catch error.FileWatcherSendFailed;
}
fn create() SpawnError!Self {
const pid = try Process.create();
defer pid.deinit();
tp.env.get().proc_set(module_name, pid.ref());
return .{ .pid = tp.env.get().proc(module_name) };
}
const Process = struct {
allocator: std.mem.Allocator,
parent: tp.pid,
receiver: Receiver,
nw: nightwatch,
fd_watcher: if (builtin.os.tag == .linux) tp.file_descriptor else void,
handler: nightwatch.Handler,
const Receiver = tp.Receiver(*@This());
fn create() SpawnError!tp.pid {
const allocator = std.heap.c_allocator;
const self = try allocator.create(@This());
errdefer allocator.destroy(self);
self.* = .{
.allocator = allocator,
.parent = tp.self_pid().clone(),
.receiver = Receiver.init(@This().receive, self),
.nw = undefined,
.fd_watcher = if (builtin.os.tag == .linux) undefined else {},
.handler = init_handler(),
};
return tp.spawn_link(self.allocator, self, @This().start, module_name);
}
fn deinit(self: *@This()) void {
if (builtin.os.tag == .linux) self.fd_watcher.deinit();
self.nw.deinit();
self.parent.deinit();
self.allocator.destroy(self);
}
pub fn init_handler() nightwatch.Handler {
return .{
.vtable = &.{
.change = handle_change,
.rename = handle_rename,
.wait_readable = if (builtin.os.tag == .linux) wait_readable else {},
},
};
}
fn start(self: *@This()) tp.result {
errdefer self.deinit();
_ = tp.set_trap(true);
self.nw = nightwatch.init(self.allocator, &self.handler) catch |e| return tp.exit_error(e, @errorReturnTrace());
if (builtin.os.tag == .linux)
self.fd_watcher = tp.file_descriptor.init(module_name, self.nw.backend.inotify_fd) catch |e| {
std.log.err("file_watcher.start: {}", .{e});
return tp.exit_error(e, @errorReturnTrace());
};
tp.receive(&self.receiver);
}
fn receive(self: *@This(), from: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
return self.receive_safe(from, m) catch |e| switch (e) {
error.ExitNormal => tp.exit_normal(),
else => {
const err = tp.exit_error(e, @errorReturnTrace());
std.log.err("file_watcher.receive: {}", .{err});
return err;
},
};
}
fn receive_safe(self: *@This(), _: tp.pid_ref, m: tp.message) (error{ExitNormal} || cbor.Error)!void {
var path: []const u8 = undefined;
var tag: []const u8 = undefined;
var err_code: i64 = 0;
var err_msg: []const u8 = undefined;
if (try cbor.match(m.buf, .{ "fd", tp.extract(&tag), "read_ready" })) {
// re-arm the file_discriptor
if (builtin.os.tag == .linux) {
self.fd_watcher.wait_read() catch |e| std.log.err("file_watcher wait_read: {}", .{e});
self.nw.handle_read_ready() catch |e| std.log.err("file_watcher handle_read_ready: {}", .{e});
}
} else if (try cbor.match(m.buf, .{ "fd", tp.extract(&tag), "read_error", tp.extract(&err_code), tp.extract(&err_msg) })) {
std.log.err("fd read error on {s}: ({d}) {s}", .{ tag, err_code, err_msg });
} else if (try cbor.match(m.buf, .{ "watch", tp.extract(&path) })) {
self.nw.watch(path) catch |e| std.log.err("file_watcher watch: {s} -> {}", .{ path, e });
} else if (try cbor.match(m.buf, .{ "unwatch", tp.extract(&path) })) {
self.nw.unwatch(path) catch |e| std.log.err("file_watcher unwatch: {s} -> {}", .{ path, e });
} else if (try cbor.match(m.buf, .{"shutdown"})) {
return error.ExitNormal;
} else if (try cbor.match(m.buf, .{ "exit", tp.more })) {
return error.ExitNormal;
} else {
std.log.err("file_watcher.receive: {}", .{tp.unexpected(m)});
}
}
fn handle_change(handler: *nightwatch.Handler, path: []const u8, event_type: EventType) error{HandlerFailed}!void {
const self: *@This() = @alignCast(@fieldParentPtr("handler", handler));
_ = self;
_ = path;
_ = event_type;
}
fn handle_rename(handler: *nightwatch.Handler, src_path: []const u8, dst_path: []const u8) error{HandlerFailed}!void {
const self: *@This() = @alignCast(@fieldParentPtr("handler", handler));
_ = self;
_ = src_path;
_ = dst_path;
}
fn wait_readable(handler: *nightwatch.Handler) error{HandlerFailed}!nightwatch.ReadableStatus {
const self: *@This() = @alignCast(@fieldParentPtr("handler", handler));
if (builtin.os.tag == .linux)
self.fd_watcher.wait_read() catch |e| {
std.log.err("file_watcher.wait_readable: {}", .{e});
return error.HandlerFailed;
};
return .will_notify;
}
};

View file

@ -3,6 +3,7 @@ const tp = @import("thespian");
const cbor = @import("cbor");
const log = @import("log");
const tracy = @import("tracy");
const file_watcher = @import("file_watcher");
const file_type_config = @import("file_type_config");
const lsp_config = @import("lsp_config");
const root = @import("soft_root").root;
@ -420,8 +421,18 @@ const Process = struct {
var vcs_id: []const u8 = undefined;
var eol_mode: Buffer.EolModeTag = @intFromEnum(Buffer.EolMode.lf);
var event_type: file_watcher.EventType = undefined;
var from_path: []const u8 = undefined;
if (try cbor.match(m.buf, .{ "walk_tree_entry", tp.extract(&project_directory), tp.more })) {
if (try cbor.match(m.buf, .{ "FW", "rename", tp.extract(&from_path), tp.extract(&path) })) {
self.handle_file_watch_rename(from_path, path);
} else if (try cbor.match(m.buf, .{ "FW", "change", tp.extract(&path), tp.extract(&event_type) })) {
self.handle_file_watch_event(path, event_type);
} else if (try cbor.match(m.buf, .{ "walk_tree_dir", tp.extract(&project_directory), tp.extract(&path) })) {
var abs_buf: [std.fs.max_path_bytes]u8 = undefined;
const abs_path = std.fmt.bufPrint(&abs_buf, "{s}{c}{s}", .{ project_directory, std.fs.path.sep, path }) catch project_directory;
file_watcher.watch(abs_path) catch |e| self.logger.err("file_watcher.watch_dir", e);
} else if (try cbor.match(m.buf, .{ "walk_tree_entry", tp.extract(&project_directory), tp.more })) {
if (self.projects.get(project_directory)) |project|
project.walk_tree_entry(m) catch |e| self.logger.err("walk_tree_entry", e);
} else if (try cbor.match(m.buf, .{ "walk_tree_done", tp.extract(&project_directory) })) {
@ -436,6 +447,15 @@ const Process = struct {
} else if (try cbor.match(m.buf, .{ "git", tp.extract(&context), "blame", tp.more })) {
const request: *Project.GitBlameRequest = @ptrFromInt(context);
request.project.process_git_response(self.parent.ref(), m) catch |e| self.logger.err("git-blame", e);
} else if (try cbor.match(m.buf, .{ "git", tp.extract(&context), "workspace_files", tp.extract(&path) })) {
const project: *Project = @ptrFromInt(context);
const dir_path = std.fs.path.dirname(path) orelse "";
if (dir_path.len > 0) blk: {
var abs_buf: [std.fs.max_path_bytes]u8 = undefined;
const abs_path = std.fmt.bufPrint(&abs_buf, "{s}{c}{s}", .{ project.name, std.fs.path.sep, dir_path }) catch break :blk;
file_watcher.watch(abs_path) catch |e| self.logger.err("file_watcher.watch_dir", e);
}
project.process_git(self.parent.ref(), m) catch {};
} else if (try cbor.match(m.buf, .{ "git", tp.extract(&context), tp.more })) {
const project: *Project = @ptrFromInt(context);
project.process_git(self.parent.ref(), m) catch {};
@ -520,6 +540,7 @@ const Process = struct {
self.logger.print("{s} error: {s}", .{ tag, message });
} else if (try cbor.match(m.buf, .{"shutdown"})) {
self.persist_projects();
file_watcher.shutdown();
from.send(.{ "project_manager", "shutdown" }) catch return error.ClientFailed;
return error.ExitNormal;
} else if (try cbor.match(m.buf, .{ "exit", "normal" })) {
@ -537,6 +558,59 @@ const Process = struct {
}
}
fn project_for_path(self: *Process, abs_path: []const u8) ?struct { project: *Project, rel_path: []const u8 } {
var it = self.projects.iterator();
while (it.next()) |entry| {
const dir = entry.key_ptr.*;
if (!std.mem.startsWith(u8, abs_path, dir)) continue;
if (abs_path.len <= dir.len or abs_path[dir.len] != std.fs.path.sep) continue;
return .{ .project = entry.value_ptr.*, .rel_path = abs_path[dir.len + 1 ..] };
}
return null;
}
fn handle_file_watch_rename(self: *Process, abs_from: []const u8, abs_to: []const u8) void {
std.log.debug("file_watch_event: rename {s} -> {s}", .{ abs_from, abs_to });
const src = self.project_for_path(abs_from);
const dst = self.project_for_path(abs_to);
if (src) |s| {
if (dst) |d| {
if (s.project == d.project) {
s.project.file_renamed(s.rel_path, d.rel_path) catch |e| self.logger.err("file_watcher.file_renamed", e);
} else {
s.project.file_deleted(s.rel_path);
d.project.file_added(d.rel_path) catch |e| self.logger.err("file_watcher.file_added", e);
}
} else {
s.project.file_deleted(s.rel_path);
}
} else if (dst) |d| {
d.project.file_added(d.rel_path) catch |e| self.logger.err("file_watcher.file_added", e);
} else {
self.parent.send(.{ "FW", "rename", abs_from, abs_to }) catch {};
}
}
fn handle_file_watch_event(self: *Process, abs_path: []const u8, event_type: file_watcher.EventType) void {
std.log.debug("file_watch_event: {s} {s}", .{ @tagName(event_type), abs_path });
if (event_type == .dir_created) {
file_watcher.watch(abs_path) catch |e| self.logger.err("file_watcher.watch(dir_created)", e);
return;
}
if (self.project_for_path(abs_path)) |match| {
switch (event_type) {
.created => match.project.file_added(match.rel_path) catch |e| self.logger.err("file_watcher.file_added", e),
.modified => match.project.file_modified(match.rel_path),
.deleted => match.project.file_deleted(match.rel_path),
.renamed => match.project.file_deleted(match.rel_path),
.dir_created => unreachable,
}
} else {
self.parent.send(.{ "FW", "change", abs_path, event_type }) catch {};
}
}
fn open(self: *Process, project_directory: []const u8) (SpawnError || std.fs.Dir.OpenError)!void {
if (self.projects.get(project_directory)) |project| {
project.last_used = std.time.nanoTimestamp();
@ -548,6 +622,7 @@ const Process = struct {
try self.projects.put(self.allocator, try self.allocator.dupe(u8, project_directory), project);
self.restore_project(project) catch |e| self.logger.err("restore_project", e);
project.query_git();
file_watcher.watch(project_directory) catch |e| self.logger.err("file_watcher.watch", e);
}
}
@ -558,6 +633,7 @@ const Process = struct {
kv.value.deinit();
self.allocator.destroy(kv.value);
self.logger.print("closed: {s}", .{project_directory});
file_watcher.unwatch(project_directory) catch |e| self.logger.err("file_watcher.unwatch", e);
}
}

View file

@ -622,6 +622,12 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void {
if (try m.match(.{ "line_number_mode", tp.more })) // drop broadcast messages
return;
if (try m.match(.{ "FW", "change", tp.more })) // file watcher events
return;
if (try m.match(.{ "FW", "rename", tp.more })) // file watcher rename events
return;
return tp.unexpected(m);
}

View file

@ -9,11 +9,13 @@ const OutOfMemoryError = error{OutOfMemory};
pub const EntryCallBack = *const fn (parent: tp.pid_ref, root_path: []const u8, path: []const u8, mtime_high: i64, mtime_low: i64) error{Exit}!void;
pub const DoneCallBack = *const fn (parent: tp.pid_ref, root_path: []const u8) error{Exit}!void;
pub const DirCallBack = *const fn (parent: tp.pid_ref, root_path: []const u8, path: []const u8) error{Exit}!void;
pub const Options = struct {
follow_directory_symlinks: bool = false,
maximum_symlink_depth: usize = 1,
log_ignored_links: bool = false,
dir_callback: ?DirCallBack = null,
};
pub fn start(a_: std.mem.Allocator, root_path_: []const u8, entry_handler: EntryCallBack, done_handler: DoneCallBack, options: Options) (SpawnError || std.fs.Dir.OpenError)!tp.pid {
@ -78,7 +80,13 @@ pub fn start(a_: std.mem.Allocator, root_path_: []const u8, entry_handler: Entry
}
fn next(self: *tree_walker) !void {
if (try self.walker.next()) |path| {
if (try self.walker.next()) |entry| {
if (entry.kind == .dir) {
if (self.options.dir_callback) |cb|
cb(self.parent.ref(), self.root_path, entry.path) catch {};
return tp.self_pid().send(.{"next"});
}
const path = entry.path;
const stat = self.dir.statFile(path) catch {
try self.entry_handler(self.parent.ref(), self.root_path, path, 0, 0);
return tp.self_pid().send(.{"next"});
@ -123,6 +131,8 @@ const FilteredWalker = struct {
name_buffer: std.ArrayListUnmanaged(u8),
options: Options,
const Kind = enum { file, dir };
const Entry = struct { path: []const u8, kind: Kind };
const Path = []const u8;
const StackItem = struct {
@ -160,7 +170,7 @@ const FilteredWalker = struct {
self.name_buffer.deinit(self.allocator);
}
fn next(self: *FilteredWalker) OutOfMemoryError!?Path {
fn next(self: *FilteredWalker) OutOfMemoryError!?Entry {
while (self.stack.items.len != 0) {
var top = &self.stack.items[self.stack.items.len - 1];
var containing = top;
@ -182,17 +192,17 @@ const FilteredWalker = struct {
switch (base.kind) {
.directory => {
_ = try self.next_directory(&base, &top, &containing, top.symlink_depth);
continue;
return .{ .path = self.name_buffer.items, .kind = .dir };
},
.file => return self.name_buffer.items,
.file => return .{ .path = self.name_buffer.items, .kind = .file },
.sym_link => {
if (top.symlink_depth == 0) {
if (self.options.log_ignored_links)
std.log.warn("TOO MANY LINKS! ignoring symlink: {s}", .{base.name});
continue;
}
if (try self.next_sym_link(&base, &top, &containing, top.symlink_depth -| 1)) |file|
return file
if (try self.next_sym_link(&base, &top, &containing, top.symlink_depth -| 1)) |entry|
return entry
else
continue;
},
@ -229,15 +239,17 @@ const FilteredWalker = struct {
return;
}
fn next_sym_link(self: *FilteredWalker, base: *const std.fs.Dir.Entry, top: **StackItem, containing: **StackItem, symlink_depth: usize) !?[]const u8 {
fn next_sym_link(self: *FilteredWalker, base: *const std.fs.Dir.Entry, top: **StackItem, containing: **StackItem, symlink_depth: usize) !?Entry {
const st = top.*.iter.dir.statFile(base.name) catch return null;
switch (st.kind) {
.directory => {
if (self.options.follow_directory_symlinks)
_ = try self.next_directory(base, top, containing, symlink_depth);
if (self.options.follow_directory_symlinks) {
try self.next_directory(base, top, containing, symlink_depth);
return .{ .path = self.name_buffer.items, .kind = .dir };
}
return null;
},
.file => return self.name_buffer.items,
.file => return .{ .path = self.name_buffer.items, .kind = .file },
.sym_link => {
if (symlink_depth == 0) {
if (self.options.log_ignored_links)