const std = @import("std"); const tp = @import("thespian"); const cbor = @import("cbor"); const log = @import("log"); const tracy = @import("tracy"); pid: tp.pid_ref, const Self = @This(); const module_name = @typeName(Self); pub fn get() error{Exit}!Self { const pid = tp.env.get().proc(module_name); return if (pid.expired()) create() else .{ .pid = pid }; } fn create() error{Exit}!Self { const pid = Process.create() catch |e| return tp.exit_error(e); defer pid.deinit(); tp.env.get().proc_set(module_name, pid.ref()); return .{ .pid = tp.env.get().proc(module_name) }; } pub fn shutdown() void { const pid = tp.env.get().proc(module_name); if (pid.expired()) return; pid.send(.{"shutdown"}) catch {}; } pub fn open_cwd() tp.result { var cwd_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; const cwd = std.fs.cwd().realpath(".", &cwd_buf) catch "(none)"; return open(cwd); } pub fn open(project_directory: []const u8) tp.result { tp.env.get().str_set("project", project_directory); return (try get()).pid.send(.{ "open", project_directory }); } pub fn request_recent_files() tp.result { const project = tp.env.get().str("project"); if (project.len == 0) return tp.exit("No project"); return (try get()).pid.send(.{ "request_recent_files", project }); } const Process = struct { a: std.mem.Allocator, parent: tp.pid, logger: log.Logger, receiver: Receiver, projects: ProjectsMap, const Receiver = tp.Receiver(*Process); const ProjectsMap = std.StringHashMap(*Project); fn create() !tp.pid { const a = std.heap.c_allocator; const self = try a.create(Process); self.* = .{ .a = a, .parent = tp.self_pid().clone(), .logger = log.logger(module_name), .receiver = Receiver.init(Process.receive, self), .projects = ProjectsMap.init(a), }; return tp.spawn_link(self.a, self, Process.start, module_name) catch |e| tp.exit_error(e); } fn deinit(self: *Process) void { var i = self.projects.iterator(); while (i.next()) |p| { self.a.free(p.key_ptr.*); p.value_ptr.*.deinit(); self.a.destroy(p.value_ptr.*); } self.projects.deinit(); self.parent.deinit(); self.a.destroy(self); } fn start(self: *Process) tp.result { _ = tp.set_trap(true); tp.receive(&self.receiver); } fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result { errdefer self.deinit(); var project_directory: []const u8 = undefined; var path: []const u8 = undefined; var high: i64 = 0; var low: i64 = 0; if (try m.match(.{ "walk_tree_entry", tp.extract(&project_directory), tp.extract(&path), tp.extract(&high), tp.extract(&low) })) { const mtime = (@as(i128, @intCast(high)) << 64) | @as(i128, @intCast(low)); if (self.projects.get(project_directory)) |project| project.add_file(path, mtime) catch |e| self.logger.err("walk_tree_entry", e); // self.logger.print("file: {s}", .{path}); } else if (try m.match(.{ "walk_tree_done", tp.extract(&project_directory) })) { const project = self.projects.get(project_directory) orelse return; project.sort_files_by_mtime(); self.logger.print("opened: {s} with {d} files in {d} ms", .{ project_directory, project.files.items.len, std.time.milliTimestamp() - project.open_time, }); } else if (try m.match(.{ "open", tp.extract(&project_directory) })) { self.open(project_directory) catch |e| return from.send_raw(tp.exit_message(e)); } else if (try m.match(.{ "request_recent_files", tp.extract(&project_directory) })) { self.request_recent_files(from, project_directory) catch |e| return from.send_raw(tp.exit_message(e)); } else if (try m.match(.{"shutdown"})) { return tp.exit_normal(); } else if (try m.match(.{ "exit", "normal" })) { return; } else { self.logger.err("receive", tp.unexpected(m)); } } fn open(self: *Process, project_directory: []const u8) error{ OutOfMemory, Exit }!void { self.logger.print("opening: {s}", .{project_directory}); if (self.projects.get(project_directory) == null) { const project = try self.a.create(Project); project.* = try Project.init(self.a, project_directory); try self.projects.put(try self.a.dupe(u8, project_directory), project); try walk_tree_async(self.a, project_directory); } } fn request_recent_files(self: *Process, from: tp.pid_ref, project_directory: []const u8) error{ OutOfMemory, Exit }!void { const project = if (self.projects.get(project_directory)) |p| p else return tp.exit("No project"); return project.request_recent_files(from); } }; const Project = struct { a: std.mem.Allocator, name: []const u8, files: std.ArrayList(File), open_time: i64, const File = struct { path: []const u8, mtime: i128, }; fn init(a: std.mem.Allocator, name: []const u8) error{OutOfMemory}!Project { return .{ .a = a, .name = try a.dupe(u8, name), .files = std.ArrayList(File).init(a), .open_time = std.time.milliTimestamp(), }; } fn deinit(self: *Project) void { for (self.files.items) |file| self.a.free(file.path); self.files.deinit(); self.a.free(self.name); } fn add_file(self: *Project, path: []const u8, mtime: i128) error{OutOfMemory}!void { (try self.files.addOne()).* = .{ .path = try self.a.dupe(u8, path), .mtime = mtime }; } fn sort_files_by_mtime(self: *Project) void { const less_fn = struct { fn less_fn(_: void, lhs: File, rhs: File) bool { return lhs.mtime > rhs.mtime; } }.less_fn; std.mem.sort(File, self.files.items, {}, less_fn); } fn request_recent_files(self: *Project, from: tp.pid_ref) error{ OutOfMemory, Exit }!void { for (self.files.items) |file| try from.send(.{ "PRJ", "recent", file.path }); } }; fn walk_tree_async(a_: std.mem.Allocator, root_path_: []const u8) tp.result { return struct { a: std.mem.Allocator, root_path: []const u8, parent: tp.pid, const tree_walker = @This(); fn spawn_link(a: std.mem.Allocator, root_path: []const u8) tp.result { const self = a.create(tree_walker) catch |e| return tp.exit_error(e); self.* = tree_walker.init(a, root_path) catch |e| return tp.exit_error(e); const pid = tp.spawn_link(a, self, tree_walker.start, module_name ++ ".tree_walker") catch |e| return tp.exit_error(e); pid.deinit(); } fn init(a: std.mem.Allocator, root_path: []const u8) error{OutOfMemory}!tree_walker { return .{ .a = a, .root_path = try a.dupe(u8, root_path), .parent = tp.self_pid().clone(), }; } fn start(self: *tree_walker) tp.result { self.walk() catch |e| return tp.exit_error(e); return tp.exit_normal(); } fn deinit(self: *tree_walker) void { self.a.free(self.root_path); self.parent.deinit(); } fn walk(self: *tree_walker) !void { const frame = tracy.initZone(@src(), .{ .name = "project scan" }); defer frame.deinit(); defer { self.parent.send(.{ "walk_tree_done", self.root_path }) catch {}; self.deinit(); } var dir = try std.fs.cwd().openDir(self.root_path, .{ .iterate = true }); defer dir.close(); var walker = try walk_filtered(dir, self.a); defer walker.deinit(); while (try walker.next()) |path| { const stat = dir.statFile(path) catch continue; const mtime = stat.mtime; const high: i64 = @intCast(mtime >> 64); const low: i64 = @truncate(mtime); std.debug.assert(mtime == (@as(i128, @intCast(high)) << 64) | @as(i128, @intCast(low))); try self.parent.send(.{ "walk_tree_entry", self.root_path, path, high, low }); } } }.spawn_link(a_, root_path_); } const filtered_dirs = [_][]const u8{ ".git", ".cache", ".var", "zig-out", "zig-cache", ".rustup", ".npm", ".cargo", "node_modules", }; fn is_filtered_dir(dirname: []const u8) bool { for (filtered_dirs) |filter| if (std.mem.eql(u8, filter, dirname)) return true; return false; } const FilteredWalker = struct { stack: std.ArrayList(StackItem), name_buffer: std.ArrayList(u8), const Path = []const u8; const StackItem = struct { iter: std.fs.Dir.Iterator, dirname_len: usize, }; pub fn next(self: *FilteredWalker) error{OutOfMemory}!?Path { while (self.stack.items.len != 0) { var top = &self.stack.items[self.stack.items.len - 1]; var containing = top; var dirname_len = top.dirname_len; if (top.iter.next() catch { var item = self.stack.pop(); if (self.stack.items.len != 0) { item.iter.dir.close(); } continue; }) |base| { self.name_buffer.shrinkRetainingCapacity(dirname_len); if (self.name_buffer.items.len != 0) { try self.name_buffer.append(std.fs.path.sep); dirname_len += 1; } try self.name_buffer.appendSlice(base.name); switch (base.kind) { .directory => { if (is_filtered_dir(base.name)) continue; var new_dir = top.iter.dir.openDir(base.name, .{ .iterate = true }) catch |err| switch (err) { error.NameTooLong => @panic("unexpected error.NameTooLong"), // no path sep in base.name else => continue, }; { errdefer new_dir.close(); try self.stack.append(StackItem{ .iter = new_dir.iterateAssumeFirstIteration(), .dirname_len = self.name_buffer.items.len, }); top = &self.stack.items[self.stack.items.len - 1]; containing = &self.stack.items[self.stack.items.len - 2]; } }, .file => return self.name_buffer.items, else => continue, } } else { var item = self.stack.pop(); if (self.stack.items.len != 0) { item.iter.dir.close(); } } } return null; } pub fn deinit(self: *FilteredWalker) void { // Close any remaining directories except the initial one (which is always at index 0) if (self.stack.items.len > 1) { for (self.stack.items[1..]) |*item| { item.iter.dir.close(); } } self.stack.deinit(); self.name_buffer.deinit(); } }; fn walk_filtered(dir: std.fs.Dir, allocator: std.mem.Allocator) !FilteredWalker { var name_buffer = std.ArrayList(u8).init(allocator); errdefer name_buffer.deinit(); var stack = std.ArrayList(FilteredWalker.StackItem).init(allocator); errdefer stack.deinit(); try stack.append(FilteredWalker.StackItem{ .iter = dir.iterate(), .dirname_len = 0, }); return FilteredWalker{ .stack = stack, .name_buffer = name_buffer, }; }