From 2c68c2a00cecb80a438d1cebed43a451c0fee75b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 22 Apr 2025 17:44:06 +0200 Subject: [PATCH 1/7] feat: add git.workspace_ignored_files function --- src/git.zig | 51 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/git.zig b/src/git.zig index fb02212..1e76a40 100644 --- a/src/git.zig +++ b/src/git.zig @@ -20,29 +20,56 @@ pub fn workspace_path(context_: usize) Error!void { pub fn current_branch(context_: usize) Error!void { const fn_name = @src().fn_name; + if (current_branch_cache) |p| { + tp.self_pid().send(.{ module_name, context_, fn_name, p.branch }) catch {}; + return; + } try git(context_, .{ "rev-parse", "--abbrev-ref", "HEAD" }, struct { fn result(context: usize, parent: tp.pid_ref, output: []const u8) void { var it = std.mem.splitScalar(u8, output, '\n'); - while (it.next()) |value| if (value.len > 0) + while (it.next()) |value| if (value.len > 0) { + blk: { + current_branch_cache = .{ .branch = allocator.dupeZ(u8, value) catch break :blk }; + } parent.send(.{ module_name, context, fn_name, value }) catch {}; + return; + }; } }.result, exit_null_on_error(fn_name)); } +var current_branch_cache: ?struct { + branch: ?[:0]const u8 = null, +} = null; -pub fn workspace_files(context_: usize) Error!void { - const fn_name = @src().fn_name; - try git_err(context_, .{ "ls-files", "--cached", "--others", "--exclude-standard" }, struct { +pub fn workspace_files(context: usize) Error!void { + return git_line_output( + context, + @src().fn_name, + .{ "ls-files", "--cached", "--others", "--exclude-standard" }, + ); +} + +pub fn workspace_ignored_files(context: usize) Error!void { + return git_line_output( + context, + @src().fn_name, + .{ "ls-files", "--cached", "--others", "--exclude-standard", "--ignored" }, + ); +} + +fn git_line_output(context_: usize, comptime tag: []const u8, cmd: anytype) Error!void { + try git_err(context_, cmd, struct { fn result(context: usize, parent: tp.pid_ref, output: []const u8) void { var it = std.mem.splitScalar(u8, output, '\n'); while (it.next()) |value| if (value.len > 0) - parent.send(.{ module_name, context, fn_name, value }) catch {}; + parent.send(.{ module_name, context, tag, value }) catch {}; } }.result, struct { fn result(_: usize, _: tp.pid_ref, output: []const u8) void { var it = std.mem.splitScalar(u8, output, '\n'); while (it.next()) |line| std.log.err("{s}: {s}", .{ module_name, line }); } - }.result, exit_null_on_error(fn_name)); + }.result, exit_null(tag)); } fn git( @@ -84,11 +111,19 @@ fn git_err( @compileError("git command should be a tuple: " ++ @typeName(@TypeOf(cmd))); } +fn exit_null(comptime tag: []const u8) shell.ExitHandler { + return struct { + fn exit(context: usize, parent: tp.pid_ref, _: []const u8, _: []const u8, _: i64) void { + parent.send(.{ module_name, context, tag, null }) catch {}; + } + }.exit; +} + fn exit_null_on_error(comptime tag: []const u8) shell.ExitHandler { return struct { - fn exit(_: usize, parent: tp.pid_ref, _: []const u8, _: []const u8, exit_code: i64) void { + fn exit(context: usize, parent: tp.pid_ref, _: []const u8, _: []const u8, exit_code: i64) void { if (exit_code > 0) - parent.send(.{ module_name, tag, null }) catch {}; + parent.send(.{ module_name, context, tag, null }) catch {}; } }.exit; } From 5ebd6353b37d5b67ea115251a1dc2d46901ebfde Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 22 Apr 2025 18:00:13 +0200 Subject: [PATCH 2/7] feat: make logview include precision for small tdiff values --- src/tui/logview.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/logview.zig b/src/tui/logview.zig index f5ed903..02776e0 100644 --- a/src/tui/logview.zig +++ b/src/tui/logview.zig @@ -74,7 +74,7 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { fn output_tdiff(self: *Self, tdiff: i64) !void { const msi = @divFloor(tdiff, time.us_per_ms); - if (msi == 0) { + if (msi < 10) { const d: f64 = @floatFromInt(tdiff); const ms = d / time.us_per_ms; _ = try self.plane.print("{d:6.2} ▏", .{ms}); From 1a0d4ca7b2c25012bbccbf94b73c2fcfd74e4c80 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 22 Apr 2025 20:44:14 +0200 Subject: [PATCH 3/7] refactor: move walk_tree to separate module --- src/project_manager.zig | 185 +-------------------------------------- src/walk_tree.zig | 189 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 184 deletions(-) create mode 100644 src/walk_tree.zig diff --git a/src/project_manager.zig b/src/project_manager.zig index d389a37..8558ef2 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -9,6 +9,7 @@ const Buffer = @import("Buffer"); const builtin = @import("builtin"); const Project = @import("Project.zig"); +const walk_tree = @import("walk_tree.zig"); pid: tp.pid_ref, @@ -815,190 +816,6 @@ fn request_path_files_async(a_: std.mem.Allocator, parent_: tp.pid_ref, project_ }.spawn_link(a_, parent_, project_, max_, path_); } -fn walk_tree_async(a_: std.mem.Allocator, root_path_: []const u8) (SpawnError || std.fs.Dir.OpenError)!tp.pid { - return struct { - allocator: std.mem.Allocator, - root_path: []const u8, - parent: tp.pid, - receiver: Receiver, - dir: std.fs.Dir, - walker: FilteredWalker, - - const tree_walker = @This(); - const Receiver = tp.Receiver(*tree_walker); - - fn spawn_link(allocator: std.mem.Allocator, root_path: []const u8) (SpawnError || std.fs.Dir.OpenError)!tp.pid { - const self = try allocator.create(tree_walker); - self.* = .{ - .allocator = allocator, - .root_path = try allocator.dupe(u8, root_path), - .parent = tp.self_pid().clone(), - .receiver = Receiver.init(tree_walker.receive, self), - .dir = try std.fs.cwd().openDir(self.root_path, .{ .iterate = true }), - .walker = try walk_filtered(self.dir, self.allocator), - }; - return tp.spawn_link(allocator, self, tree_walker.start, module_name ++ ".tree_walker"); - } - - fn start(self: *tree_walker) tp.result { - errdefer self.deinit(); - const frame = tracy.initZone(@src(), .{ .name = "project scan" }); - defer frame.deinit(); - tp.receive(&self.receiver); - self.next() catch |e| return tp.exit_error(e, @errorReturnTrace()); - } - - fn deinit(self: *tree_walker) void { - self.walker.deinit(); - self.dir.close(); - self.allocator.free(self.root_path); - self.parent.deinit(); - } - - fn receive(self: *tree_walker, _: tp.pid_ref, m: tp.message) tp.result { - errdefer self.deinit(); - const frame = tracy.initZone(@src(), .{ .name = "project scan" }); - defer frame.deinit(); - - if (try m.match(.{"next"})) { - self.next() catch |e| return tp.exit_error(e, @errorReturnTrace()); - } else if (try m.match(.{"stop"})) { - return tp.exit_normal(); - } else { - return tp.unexpected(m); - } - } - - fn next(self: *tree_walker) !void { - if (try self.walker.next()) |path| { - const stat = self.dir.statFile(path) catch return tp.self_pid().send(.{"next"}); - 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 }); - return tp.self_pid().send(.{"next"}); - } else { - self.parent.send(.{ "walk_tree_done", self.root_path }) catch {}; - return tp.exit_normal(); - } - } - }.spawn_link(a_, root_path_); -} - -const filtered_dirs = [_][]const u8{ - "build", - ".cache", - ".cargo", - ".git", - ".jj", - "node_modules", - ".npm", - ".rustup", - ".var", - ".zig-cache", - "zig-cache", - "zig-out", -}; - -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 { - allocator: std.mem.Allocator, - stack: std.ArrayListUnmanaged(StackItem), - name_buffer: std.ArrayListUnmanaged(u8), - - const Path = []const u8; - - const StackItem = struct { - iter: std.fs.Dir.Iterator, - dirname_len: usize, - }; - - pub fn next(self: *FilteredWalker) OutOfMemoryError!?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 (item_) |*item| - 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(self.allocator, std.fs.path.sep); - dirname_len += 1; - } - try self.name_buffer.appendSlice(self.allocator, 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(self.allocator, .{ - .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 (item_) |*item| - 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.allocator); - self.name_buffer.deinit(self.allocator); - } -}; - -fn walk_filtered(dir: std.fs.Dir, allocator: std.mem.Allocator) !FilteredWalker { - var stack: std.ArrayListUnmanaged(FilteredWalker.StackItem) = .{}; - errdefer stack.deinit(allocator); - - try stack.append(allocator, .{ - .iter = dir.iterate(), - .dirname_len = 0, - }); - - return .{ - .allocator = allocator, - .stack = stack, - .name_buffer = .{}, - }; -} - pub fn normalize_file_path(file_path: []const u8) []const u8 { const project = tp.env.get().str("project"); if (project.len == 0) return file_path; diff --git a/src/walk_tree.zig b/src/walk_tree.zig new file mode 100644 index 0000000..bae5a68 --- /dev/null +++ b/src/walk_tree.zig @@ -0,0 +1,189 @@ +const std = @import("std"); +const tp = @import("thespian"); +const tracy = @import("tracy"); + +const module_name = @typeName(@This()); + +const SpawnError = (OutOfMemoryError || error{ThespianSpawnFailed}); +const OutOfMemoryError = error{OutOfMemory}; + +pub fn start(a_: std.mem.Allocator, root_path_: []const u8) (SpawnError || std.fs.Dir.OpenError)!tp.pid { + return struct { + allocator: std.mem.Allocator, + root_path: []const u8, + parent: tp.pid, + receiver: Receiver, + dir: std.fs.Dir, + walker: FilteredWalker, + + const tree_walker = @This(); + const Receiver = tp.Receiver(*tree_walker); + + fn spawn_link(allocator: std.mem.Allocator, root_path: []const u8) (SpawnError || std.fs.Dir.OpenError)!tp.pid { + const self = try allocator.create(tree_walker); + self.* = .{ + .allocator = allocator, + .root_path = try allocator.dupe(u8, root_path), + .parent = tp.self_pid().clone(), + .receiver = .init(tree_walker.receive, self), + .dir = try std.fs.cwd().openDir(self.root_path, .{ .iterate = true }), + .walker = try .init(self.dir, self.allocator), + }; + return tp.spawn_link(allocator, self, tree_walker.start, module_name ++ ".tree_walker"); + } + + fn start(self: *tree_walker) tp.result { + errdefer self.deinit(); + const frame = tracy.initZone(@src(), .{ .name = "project scan" }); + defer frame.deinit(); + tp.receive(&self.receiver); + self.next() catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + + fn deinit(self: *tree_walker) void { + self.walker.deinit(); + self.dir.close(); + self.allocator.free(self.root_path); + self.parent.deinit(); + } + + fn receive(self: *tree_walker, _: tp.pid_ref, m: tp.message) tp.result { + errdefer self.deinit(); + const frame = tracy.initZone(@src(), .{ .name = "project scan" }); + defer frame.deinit(); + + if (try m.match(.{"next"})) { + self.next() catch |e| return tp.exit_error(e, @errorReturnTrace()); + } else if (try m.match(.{"stop"})) { + return tp.exit_normal(); + } else { + return tp.unexpected(m); + } + } + + fn next(self: *tree_walker) !void { + if (try self.walker.next()) |path| { + const stat = self.dir.statFile(path) catch return tp.self_pid().send(.{"next"}); + 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 }); + return tp.self_pid().send(.{"next"}); + } else { + self.parent.send(.{ "walk_tree_done", self.root_path }) catch {}; + return tp.exit_normal(); + } + } + }.spawn_link(a_, root_path_); +} + +const filtered_dirs = [_][]const u8{ + ".cache", + ".cargo", + ".git", + ".jj", + "node_modules", + ".npm", + ".rustup", + ".var", + ".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 { + allocator: std.mem.Allocator, + stack: std.ArrayListUnmanaged(StackItem), + name_buffer: std.ArrayListUnmanaged(u8), + + const Path = []const u8; + + const StackItem = struct { + iter: std.fs.Dir.Iterator, + dirname_len: usize, + }; + + pub fn init(dir: std.fs.Dir, allocator: std.mem.Allocator) !FilteredWalker { + var stack: std.ArrayListUnmanaged(FilteredWalker.StackItem) = .{}; + errdefer stack.deinit(allocator); + + try stack.append(allocator, .{ + .iter = dir.iterate(), + .dirname_len = 0, + }); + + return .{ + .allocator = allocator, + .stack = stack, + .name_buffer = .{}, + }; + } + + 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.allocator); + self.name_buffer.deinit(self.allocator); + } + + pub fn next(self: *FilteredWalker) OutOfMemoryError!?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 (item_) |*item| + 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(self.allocator, std.fs.path.sep); + dirname_len += 1; + } + try self.name_buffer.appendSlice(self.allocator, 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(self.allocator, .{ + .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 (item_) |*item| + if (self.stack.items.len != 0) { + item.iter.dir.close(); + }; + } + } + return null; + } +}; From ba5dc359139d312840551ebad6b97bf8d47a7564 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 22 Apr 2025 21:19:29 +0200 Subject: [PATCH 4/7] feat: load project file list from git if available If the loading project has a git repository we now load the file list from git instead of scanning the directory tree. This gives us automatic .gitignore support. --- src/Project.zig | 120 ++++++++++++++++++++++++++++++++-------- src/project_manager.zig | 35 +++--------- 2 files changed, 105 insertions(+), 50 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index 1b5d07c..2e1e314 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -11,23 +11,35 @@ const git = @import("git"); const builtin = @import("builtin"); const LSP = @import("LSP.zig"); +const walk_tree = @import("walk_tree.zig"); allocator: std.mem.Allocator, name: []const u8, -files: std.ArrayList(File), -pending: std.ArrayList(File), +files: std.ArrayListUnmanaged(File) = .empty, +pending: std.ArrayListUnmanaged(File) = .empty, longest_file_path: usize = 0, open_time: i64, language_servers: std.StringHashMap(LSP), file_language_server: std.StringHashMap(LSP), tasks: std.ArrayList(Task), persistent: bool = false, +logger: log.Logger, logger_lsp: log.Logger, logger_git: log.Logger, workspace: ?[]const u8 = null, branch: ?[]const u8 = null, +walker: ?tp.pid = null, + +// async task states +state: struct { + walk_tree: State = .none, + workspace_path: State = .none, + current_branch: State = .none, + workspace_files: State = .none, +} = .{}, + const Self = @This(); const OutOfMemoryError = error{OutOfMemory}; @@ -55,22 +67,24 @@ const Task = struct { mtime: i64, }; +const State = enum { none, running, done, failed }; + pub fn init(allocator: std.mem.Allocator, name: []const u8) OutOfMemoryError!Self { return .{ .allocator = allocator, .name = try allocator.dupe(u8, name), - .files = std.ArrayList(File).init(allocator), - .pending = std.ArrayList(File).init(allocator), .open_time = std.time.milliTimestamp(), .language_servers = std.StringHashMap(LSP).init(allocator), .file_language_server = std.StringHashMap(LSP).init(allocator), .tasks = std.ArrayList(Task).init(allocator), + .logger = log.logger("project"), .logger_lsp = log.logger("lsp"), .logger_git = log.logger("git"), }; } pub fn deinit(self: *Self) void { + if (self.walker) |pid| pid.send(.{"stop"}) catch {}; if (self.workspace) |p| self.allocator.free(p); if (self.branch) |p| self.allocator.free(p); var i_ = self.file_language_server.iterator(); @@ -83,11 +97,13 @@ pub fn deinit(self: *Self) void { p.value_ptr.*.term(); } for (self.files.items) |file| self.allocator.free(file.path); - self.files.deinit(); + self.files.deinit(self.allocator); + self.pending.deinit(self.allocator); for (self.tasks.items) |task| self.allocator.free(task.command); self.tasks.deinit(); self.logger_lsp.deinit(); self.logger_git.deinit(); + self.logger.deinit(); self.allocator.free(self.name); } @@ -294,11 +310,11 @@ fn make_URI(self: *Self, file_path: ?[]const u8) LspError![]const u8 { return buf.toOwnedSlice(); } -pub fn sort_files_by_mtime(self: *Self) void { +fn sort_files_by_mtime(self: *Self) void { sort_by_mtime(File, self.files.items); } -pub fn sort_tasks_by_mtime(self: *Self) void { +fn sort_tasks_by_mtime(self: *Self) void { sort_by_mtime(Task, self.tasks.items); } @@ -311,12 +327,14 @@ inline fn sort_by_mtime(T: type, items: []T) void { } pub fn request_n_most_recent_file(self: *Self, from: tp.pid_ref, n: usize) ClientError!void { + self.sort_files_by_mtime(); if (n >= self.files.items.len) return error.ClientFailed; const file_path = if (self.files.items.len > 0) self.files.items[n].path else null; from.send(.{file_path}) catch return error.ClientFailed; } pub fn request_recent_files(self: *Self, from: tp.pid_ref, max: usize) ClientError!void { + self.sort_files_by_mtime(); defer from.send(.{ "PRJ", "recent_done", self.longest_file_path, "" }) catch {}; for (self.files.items, 0..) |file, i| { from.send(.{ "PRJ", "recent", self.longest_file_path, file.path }) catch return error.ClientFailed; @@ -386,21 +404,47 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co return @min(max, matches.items.len); } -pub fn add_pending_file(self: *Self, file_path: []const u8, mtime: i128) OutOfMemoryError!void { +pub fn walk_tree_entry(self: *Self, file_path: []const u8, mtime: i128) OutOfMemoryError!void { self.longest_file_path = @max(self.longest_file_path, file_path.len); - (try self.pending.addOne()).* = .{ .path = try self.allocator.dupe(u8, file_path), .mtime = mtime }; + (try self.pending.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, file_path), .mtime = mtime }; } -pub fn merge_pending_files(self: *Self) OutOfMemoryError!void { +pub fn walk_tree_done(self: *Self) OutOfMemoryError!void { + self.state.walk_tree = .done; + if (self.walker) |pid| pid.deinit(); + self.walker = null; + return self.loaded(); +} + +fn merge_pending_files(self: *Self) OutOfMemoryError!void { defer self.sort_files_by_mtime(); - const existing = try self.files.toOwnedSlice(); + const existing = try self.files.toOwnedSlice(self.allocator); + defer self.allocator.free(existing); self.files = self.pending; - self.pending = std.ArrayList(File).init(self.allocator); + self.pending = .empty; + for (existing) |*file| { self.update_mru_internal(file.path, file.mtime, file.pos.row, file.pos.col) catch {}; self.allocator.free(file.path); } - self.allocator.free(existing); +} + +fn loaded(self: *Self) OutOfMemoryError!void { + inline for (@typeInfo(@TypeOf(self.state)).@"struct".fields) |f| + if (@field(self.state, f.name) == .running) return; + + self.logger.print("project files: {d} restored, {d} {s}", .{ + self.files.items.len, + self.pending.items.len, + if (self.state.workspace_files == .done) "tracked" else "walked", + }); + + try self.merge_pending_files(); + self.logger.print("opened: {s} with {d} files in {d} ms", .{ + self.name, + self.files.items.len, + std.time.milliTimestamp() - self.open_time, + }); } pub fn update_mru(self: *Self, file_path: []const u8, row: usize, col: usize) OutOfMemoryError!void { @@ -420,14 +464,14 @@ fn update_mru_internal(self: *Self, file_path: []const u8, mtime: i128, row: usi return; } if (row != 0) { - (try self.files.addOne()).* = .{ + (try self.files.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, file_path), .mtime = mtime, .pos = .{ .row = row, .col = col }, .visited = true, }; } else { - (try self.files.addOne()).* = .{ + (try self.files.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, file_path), .mtime = mtime, }; @@ -1858,23 +1902,55 @@ pub fn get_line(allocator: std.mem.Allocator, buf: []const u8) ![]const u8 { } pub fn query_git(self: *Self) void { - git.workspace_path(@intFromPtr(self)) catch {}; - git.current_branch(@intFromPtr(self)) catch {}; + self.state.workspace_path = .running; + git.workspace_path(@intFromPtr(self)) catch { + self.state.workspace_path = .failed; + self.start_walker(); + }; + self.state.current_branch = .running; + git.current_branch(@intFromPtr(self)) catch { + self.state.current_branch = .failed; + }; } -pub fn process_git(self: *Self, m: tp.message) !void { +fn start_walker(self: *Self) void { + self.state.walk_tree = .running; + self.walker = walk_tree.start(self.allocator, self.name) catch blk: { + self.state.walk_tree = .failed; + break :blk null; + }; +} + +pub fn process_git(self: *Self, m: tp.message) (OutOfMemoryError || error{Exit})!void { var value: []const u8 = undefined; + var path: []const u8 = undefined; if (try m.match(.{ tp.any, tp.any, "workspace_path", tp.null_ })) { - // no git workspace + self.state.workspace_path = .done; + self.start_walker(); + try self.loaded(); } else if (try m.match(.{ tp.any, tp.any, "workspace_path", tp.extract(&value) })) { if (self.workspace) |p| self.allocator.free(p); self.workspace = try self.allocator.dupe(u8, value); - git.workspace_files(@intFromPtr(self)) catch {}; + self.state.workspace_path = .done; + self.state.workspace_files = .running; + git.workspace_files(@intFromPtr(self)) catch { + self.state.workspace_files = .failed; + }; + } else if (try m.match(.{ tp.any, tp.any, "current_branch", tp.null_ })) { + self.state.current_branch = .done; + try self.loaded(); } else if (try m.match(.{ tp.any, tp.any, "current_branch", tp.extract(&value) })) { if (self.branch) |p| self.allocator.free(p); self.branch = try self.allocator.dupe(u8, value); - } else if (try m.match(.{ tp.any, tp.any, "workspace_files", tp.extract(&value) })) { - // TODO + self.state.current_branch = .done; + try self.loaded(); + } else if (try m.match(.{ tp.any, tp.any, "workspace_files", tp.extract(&path) })) { + self.longest_file_path = @max(self.longest_file_path, path.len); + const stat = std.fs.cwd().statFile(path) catch return; + (try self.pending.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, path), .mtime = stat.mtime }; + } else if (try m.match(.{ tp.any, tp.any, "workspace_files", tp.null_ })) { + self.state.workspace_files = .done; + try self.loaded(); } else { self.logger_git.err("git", tp.unexpected(m)); } diff --git a/src/project_manager.zig b/src/project_manager.zig index 8558ef2..972e110 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -9,7 +9,6 @@ const Buffer = @import("Buffer"); const builtin = @import("builtin"); const Project = @import("Project.zig"); -const walk_tree = @import("walk_tree.zig"); pid: tp.pid_ref, @@ -248,13 +247,12 @@ const Process = struct { logger: log.Logger, receiver: Receiver, projects: ProjectsMap, - walker: ?tp.pid = null, const InvalidArgumentError = error{InvalidArgument}; const UnsupportedError = error{Unsupported}; const Receiver = tp.Receiver(*Process); - const ProjectsMap = std.StringHashMap(*Project); + const ProjectsMap = std.StringHashMapUnmanaged(*Project); const RecentProject = struct { name: []const u8, last_used: i128, @@ -268,7 +266,7 @@ const Process = struct { .parent = tp.self_pid().clone(), .logger = log.logger(module_name), .receiver = Receiver.init(Process.receive, self), - .projects = ProjectsMap.init(allocator), + .projects = .empty, }; return tp.spawn_link(self.allocator, self, Process.start, module_name); } @@ -280,7 +278,7 @@ const Process = struct { p.value_ptr.*.deinit(); self.allocator.destroy(p.value_ptr.*); } - self.projects.deinit(); + self.projects.deinit(self.allocator); self.parent.deinit(); self.logger.deinit(); self.allocator.destroy(self); @@ -335,14 +333,10 @@ const Process = struct { if (try cbor.match(m.buf, .{ "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_pending_file( - path, - mtime, - ) catch |e| self.logger.err("walk_tree_entry", e); + project.walk_tree_entry(path, mtime) catch |e| self.logger.err("walk_tree_entry", e); } else if (try cbor.match(m.buf, .{ "walk_tree_done", tp.extract(&project_directory) })) { - if (self.walker) |pid| pid.deinit(); - self.walker = null; - self.loaded(project_directory) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed; + if (self.projects.get(project_directory)) |project| + project.walk_tree_done() catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed; } else if (try cbor.match(m.buf, .{ "git", tp.extract(&context), tp.more })) { const project: *Project = @ptrFromInt(context); project.process_git(m) catch {}; @@ -404,7 +398,6 @@ const Process = struct { } else if (try cbor.match(m.buf, .{ "get_mru_position", tp.extract(&project_directory), tp.extract(&path) })) { self.get_mru_position(from, project_directory, path) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed; } else if (try cbor.match(m.buf, .{"shutdown"})) { - if (self.walker) |pid| pid.send(.{"stop"}) catch {}; self.persist_projects(); from.send(.{ "project_manager", "shutdown" }) catch return error.ClientFailed; return error.ExitNormal; @@ -424,10 +417,8 @@ const Process = struct { self.logger.print("opening: {s}", .{project_directory}); const project = try self.allocator.create(Project); project.* = try Project.init(self.allocator, project_directory); - try self.projects.put(try self.allocator.dupe(u8, project_directory), project); - self.walker = try walk_tree_async(self.allocator, project_directory); + 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.sort_files_by_mtime(); project.query_git(); } else { self.logger.print("switched to: {s}", .{project_directory}); @@ -444,25 +435,13 @@ const Process = struct { } } - fn loaded(self: *Process, project_directory: []const u8) OutOfMemoryError!void { - const project = self.projects.get(project_directory) orelse return; - try project.merge_pending_files(); - self.logger.print("opened: {s} with {d} files in {d} ms", .{ - project_directory, - project.files.items.len, - std.time.milliTimestamp() - project.open_time, - }); - } - fn request_n_most_recent_file(self: *Process, from: tp.pid_ref, project_directory: []const u8, n: usize) (ProjectError || Project.ClientError)!void { const project = self.projects.get(project_directory) orelse return error.NoProject; - project.sort_files_by_mtime(); return project.request_n_most_recent_file(from, n); } fn request_recent_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize) (ProjectError || Project.ClientError)!void { const project = self.projects.get(project_directory) orelse return error.NoProject; - project.sort_files_by_mtime(); return project.request_recent_files(from, max); } From b82c582a832e0e7bdc4c165ee29b9d43a82aee33 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 22 Apr 2025 21:28:09 +0200 Subject: [PATCH 5/7] feat: add some whitespace to branch widget --- src/tui/status/branch.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tui/status/branch.zig b/src/tui/status/branch.zig index f4bd492..a505d9f 100644 --- a/src/tui/status/branch.zig +++ b/src/tui/status/branch.zig @@ -64,12 +64,14 @@ fn process_git( return true; } +const format = " {s} {s} "; + pub fn layout(self: *Self) Widget.Layout { const branch = self.branch orelse return .{ .static = 0 }; var buf: [256]u8 = undefined; var fbs = std.io.fixedBufferStream(&buf); const writer = fbs.writer(); - writer.print("{s} {s}", .{ branch_symbol, branch }) catch {}; + writer.print(format, .{ branch_symbol, branch }) catch {}; const len = self.plane.egc_chunk_width(fbs.getWritten(), 0, 1); return .{ .static = len }; } @@ -82,7 +84,7 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { self.plane.set_style(theme.statusbar); self.plane.fill(" "); self.plane.home(); - _ = self.plane.print("{s} {s}", .{ branch_symbol, branch }) catch {}; + _ = self.plane.print(format, .{ branch_symbol, branch }) catch {}; return false; } From 86b19501c4d2274eb7c9f30cf3c38b0a43fba5f2 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 22 Apr 2025 21:28:35 +0200 Subject: [PATCH 6/7] feat: add branch widget to default config --- src/config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.zig b/src/config.zig index 31f76fe..ec0e51d 100644 --- a/src/config.zig +++ b/src/config.zig @@ -23,7 +23,7 @@ indent_size: usize = 4, tab_width: usize = 8, top_bar: []const u8 = "tabs", -bottom_bar: []const u8 = "mode file log selection diagnostics keybind linenumber clock spacer", +bottom_bar: []const u8 = "mode file log selection diagnostics keybind branch linenumber clock spacer", show_scrollbars: bool = true, show_fileicons: bool = true, From 22b29b15b939c72a29f36e7617a1f560cd573954 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 22 Apr 2025 21:29:05 +0200 Subject: [PATCH 7/7] refactor: remove unneeded sort_files_by_mtime() calls in Project --- src/Project.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index 2e1e314..80a4fd6 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -327,14 +327,12 @@ inline fn sort_by_mtime(T: type, items: []T) void { } pub fn request_n_most_recent_file(self: *Self, from: tp.pid_ref, n: usize) ClientError!void { - self.sort_files_by_mtime(); if (n >= self.files.items.len) return error.ClientFailed; const file_path = if (self.files.items.len > 0) self.files.items[n].path else null; from.send(.{file_path}) catch return error.ClientFailed; } pub fn request_recent_files(self: *Self, from: tp.pid_ref, max: usize) ClientError!void { - self.sort_files_by_mtime(); defer from.send(.{ "PRJ", "recent_done", self.longest_file_path, "" }) catch {}; for (self.files.items, 0..) |file, i| { from.send(.{ "PRJ", "recent", self.longest_file_path, file.path }) catch return error.ClientFailed;