From 602a4dff01cf505533f8de8307c3781ea947f2aa Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 21 Mar 2024 22:52:44 +0100 Subject: [PATCH] feat: WIP add project manager service The project manager service will provide fuzzy find, LRU, and similar background services for open projects. --- build.zig | 11 ++ src/project_manager.zig | 296 +++++++++++++++++++++++++++++++++++ src/service_template.zig | 78 +++++++++ src/tui/mainview.zig | 9 ++ src/tui/status/filestate.zig | 10 +- 5 files changed, 400 insertions(+), 4 deletions(-) create mode 100644 src/project_manager.zig create mode 100644 src/service_template.zig diff --git a/build.zig b/build.zig index 76311e8..161a2d2 100644 --- a/build.zig +++ b/build.zig @@ -137,6 +137,16 @@ pub fn build(b: *std.Build) void { }, }); + const project_manager_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/project_manager.zig" }, + .imports = &.{ + .{ .name = "log", .module = log_mod }, + .{ .name = "cbor", .module = cbor_mod }, + .{ .name = "thespian", .module = thespian_mod }, + .{ .name = "tracy", .module = tracy_mod }, + }, + }); + const diff_mod = b.createModule(.{ .root_source_file = .{ .path = "src/diff.zig" }, .imports = &.{ @@ -163,6 +173,7 @@ pub fn build(b: *std.Build) void { .{ .name = "config", .module = config_mod }, .{ .name = "log", .module = log_mod }, .{ .name = "location_history", .module = location_history_mod }, + .{ .name = "project_manager", .module = project_manager_mod }, .{ .name = "syntax", .module = syntax_dep.module("syntax") }, .{ .name = "text_manip", .module = text_manip_mod }, .{ .name = "Buffer", .module = Buffer_mod }, diff --git a/src/project_manager.zig b/src/project_manager.zig new file mode 100644 index 0000000..b1d0080 --- /dev/null +++ b/src/project_manager.zig @@ -0,0 +1,296 @@ +const std = @import("std"); +const tp = @import("thespian"); +const cbor = @import("cbor"); +const log = @import("log"); +const tracy = @import("tracy"); + +pid: ?tp.pid, + +const Self = @This(); +const module_name = @typeName(Self); +pub const Error = error{ OutOfMemory, Exit }; + +pub fn create(a: std.mem.Allocator) Error!Self { + return .{ .pid = try Process.create(a) }; +} + +pub fn from_pid(pid: tp.pid_ref) Error!Self { + return .{ .pid = pid.clone() }; +} + +pub fn deinit(self: *Self) void { + if (self.pid) |pid| { + self.pid = null; + pid.deinit(); + } +} + +pub fn shutdown(self: *Self) void { + if (self.pid) |pid| { + pid.send(.{"shutdown"}) catch {}; + self.deinit(); + } +} + +pub fn open(self: *const Self, project_directory: []const u8) tp.result { + const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Shutdown); + try pid.send(.{ "open", project_directory }); +} + +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); + + pub fn create(a: std.mem.Allocator) Error!tp.pid { + 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; + + if (try m.match(.{ "walk_tree_entry", tp.extract(&project_directory), tp.extract(&path) })) { + if (self.projects.get(project_directory)) |project| + project.add_file(path) 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; + self.logger.print("opened: {s} with {d} files in {d} ms", .{ + project_directory, + project.files.count(), + 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(.{"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); + } + } +}; + +const Project = struct { + a: std.mem.Allocator, + name: []const u8, + files: FilesMap, + open_time: i64, + + const FilesMap = std.StringHashMap(void); + + fn init(a: std.mem.Allocator, name: []const u8) error{OutOfMemory}!Project { + return .{ + .a = a, + .name = try a.dupe(u8, name), + .files = FilesMap.init(a), + .open_time = std.time.milliTimestamp(), + }; + } + + fn deinit(self: *Project) void { + var i = self.files.iterator(); + while (i.next()) |p| self.a.free(p.key_ptr.*); + self.files.deinit(); + self.a.free(self.name); + } + + fn add_file(self: *Project, path: []const u8) error{OutOfMemory}!void { + if (self.files.get(path) != null) return; + try self.files.put(try self.a.dupe(u8, 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| + try self.parent.send(.{ "walk_tree_entry", self.root_path, path }); + } + }.spawn_link(a_, root_path_); +} + +const filtered_dirs = [_][]const u8{ ".git", ".cache", "zig-out", "zig-cache" }; + +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 => unreachable, // 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, + }; +} diff --git a/src/service_template.zig b/src/service_template.zig new file mode 100644 index 0000000..8cdff0d --- /dev/null +++ b/src/service_template.zig @@ -0,0 +1,78 @@ +const std = @import("std"); +const tp = @import("thespian"); +const cbor = @import("cbor"); +const log = @import("log"); + +pid: ?tp.pid, + +const Self = @This(); +const module_name = @typeName(Self); +pub const Error = error{ OutOfMemory, Exit }; + +pub fn create(a: std.mem.Allocator) Error!Self { + return .{ .pid = try Process.create(a) }; +} + +pub fn from_pid(pid: tp.pid_ref) Error!Self { + return .{ .pid = pid.clone() }; +} + +pub fn deinit(self: *Self) void { + if (self.pid) |pid| { + self.pid = null; + pid.deinit(); + } +} + +pub fn shutdown(self: *Self) void { + if (self.pid) |pid| { + pid.send(.{"shutdown"}) catch {}; + self.deinit(); + } +} + +// pub fn send(self: *Self, m: tp.message) tp.result { +// const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Shutdown); +// try pid.send(m); +// } + +const Process = struct { + a: std.mem.Allocator, + parent: tp.pid, + logger: log.Logger, + receiver: Receiver, + + const Receiver = tp.Receiver(*Process); + + pub fn create(a: std.mem.Allocator) Error!tp.pid { + 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), + }; + return tp.spawn_link(self.a, self, Process.start) catch |e| tp.exit_error(e); + } + + fn deinit(self: *Process) void { + 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, _: tp.pid_ref, m: tp.message) tp.result { + errdefer self.deinit(); + + if (try m.match(.{"shutdown"})) { + return tp.exit_normal(); + } else { + self.logger.err("receive", tp.unexpected(m)); + return tp.unexpected(m); + } + } +}; diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 920f537..ab91f9c 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -4,6 +4,7 @@ const tp = @import("thespian"); const tracy = @import("tracy"); const root = @import("root"); const location_history = @import("location_history"); +const project_manager = @import("project_manager"); const tui = @import("tui.zig"); const command = @import("command.zig"); @@ -31,6 +32,7 @@ last_match_text: ?[]const u8 = null, logview_enabled: bool = false, location_history: location_history, +project_manager: project_manager, const NavState = struct { time: i64 = 0, @@ -42,6 +44,12 @@ const NavState = struct { }; pub fn create(a: std.mem.Allocator, n: nc.Plane) !Widget { + const project_manager_ = try project_manager.create(a); + tp.env.get().proc_set("project", project_manager_.pid.?.ref()); + var project_name_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const project_name = std.fs.cwd().realpath(".", &project_name_buf) catch "(none)"; + tp.env.get().str_set("project", project_name); + // try project_manager_.open(project_name); const self = try a.create(Self); self.* = .{ .a = a, @@ -51,6 +59,7 @@ pub fn create(a: std.mem.Allocator, n: nc.Plane) !Widget { .floating_views = WidgetStack.init(a), .statusbar = undefined, .location_history = try location_history.create(), + .project_manager = project_manager_, }; try self.commands.init(self); const w = Widget.to(self); diff --git a/src/tui/status/filestate.zig b/src/tui/status/filestate.zig index fbd6967..7c22f74 100644 --- a/src/tui/status/filestate.zig +++ b/src/tui/status/filestate.zig @@ -33,7 +33,7 @@ const Self = @This(); pub fn create(a: Allocator, parent: nc.Plane) !Widget { const self: *Self = try a.create(Self); self.* = try init(a, parent); - self.show_cwd(); + self.show_project(); return Widget.to(self); } @@ -171,7 +171,7 @@ pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { self.line = 0; self.column = 0; self.file_exists = true; - self.show_cwd(); + self.show_project(); } if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.any, tp.any, tp.any })) { self.detailed = !self.detailed; @@ -192,10 +192,12 @@ fn render_file_icon(self: *Self, _: *const Widget.Theme) void { self.plane.cursor_move_rel(0, 1) catch {}; } -fn show_cwd(self: *Self) void { +fn show_project(self: *Self) void { self.file_icon = ""; self.file_color = 0x000001; - self.name = std.fs.cwd().realpath(".", &self.name_buf) catch "(none)"; + const project_name = tp.env.get().str("project"); + @memcpy(self.name_buf[0..project_name.len], project_name); + self.name = self.name_buf[0..project_name.len]; self.abbrv_home(); }