diff --git a/src/Project.zig b/src/Project.zig index b6f74d2..1a4d726 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -57,6 +57,7 @@ pub fn deinit(self: *Self) void { } pub fn write_state(self: *Self, writer: anytype) !void { + try cbor.writeValue(writer, self.name); for (self.files.items) |file| { if (!file.visited) continue; try cbor.writeArrayHeader(writer, 4); @@ -69,11 +70,13 @@ pub fn write_state(self: *Self, writer: anytype) !void { pub fn restore_state(self: *Self, data: []const u8) !void { defer self.sort_files_by_mtime(); + var name: []const u8 = undefined; var path: []const u8 = undefined; var mtime: i128 = undefined; var row: usize = undefined; var col: usize = undefined; var iter: []const u8 = data; + _ = cbor.matchValue(&iter, tp.extract(&name)) catch {}; while (cbor.matchValue(&iter, .{ tp.extract(&path), tp.extract(&mtime), @@ -107,7 +110,10 @@ fn get_lsp(self: *Self, language_server: []const u8) !LSP { fn get_file_lsp(self: *Self, file_path: []const u8) !LSP { const logger = log.logger("lsp"); - errdefer logger.print_err("get_file_lsp", "no LSP found for file: {s}", .{std.fmt.fmtSliceEscapeLower(file_path)}); + errdefer logger.print_err("get_file_lsp", "no LSP found for file: {s} ({s})", .{ + std.fmt.fmtSliceEscapeLower(file_path), + self.name, + }); const lsp = self.file_language_server.get(file_path) orelse return tp.exit("no language server"); if (lsp.pid.expired()) return tp.exit("no language server"); return lsp; diff --git a/src/project_manager.zig b/src/project_manager.zig index 2aa8c68..74f8f5f 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -12,6 +12,7 @@ pid: tp.pid_ref, const Self = @This(); const module_name = @typeName(Self); +const request_timeout = std.time.ns_per_s * 5; pub fn get() !Self { const pid = tp.env.get().proc(module_name); @@ -34,10 +35,6 @@ pub fn shutdown() void { pid.send(.{"shutdown"}) catch {}; } -pub fn open_cwd() !void { - return open("."); -} - pub fn open(rel_project_directory: []const u8) !void { var path_buf: [std.fs.max_path_bytes]u8 = undefined; const project_directory = std.fs.cwd().realpath(rel_project_directory, &path_buf) catch "(none)"; @@ -55,6 +52,11 @@ pub fn request_recent_files(max: usize) !void { return (try get()).pid.send(.{ "request_recent_files", project, max }); } +pub fn request_recent_projects(a: std.mem.Allocator) !tp.message { + const project = tp.env.get().str("project"); + return (try get()).pid.call(a, request_timeout, .{ "request_recent_projects", project }); +} + pub fn query_recent_files(max: usize, query: []const u8) !void { const project = tp.env.get().str("project"); if (project.len == 0) @@ -136,6 +138,10 @@ const Process = struct { const Receiver = tp.Receiver(*Process); const ProjectsMap = std.StringHashMap(*Project); + const RecentProject = struct { + name: []const u8, + last_used: i128, + }; fn create() !tp.pid { const a = std.heap.c_allocator; @@ -221,6 +227,8 @@ const Process = struct { self.open(project_directory) catch |e| return from.forward_error(e, @errorReturnTrace()); } else if (try m.match(.{ "request_recent_files", tp.extract(&project_directory), tp.extract(&max) })) { self.request_recent_files(from, project_directory, max) catch |e| return from.forward_error(e, @errorReturnTrace()); + } else if (try m.match(.{ "request_recent_projects", tp.extract(&project_directory) })) { + self.request_recent_projects(from, project_directory) catch |e| return from.forward_error(e, @errorReturnTrace()); } else if (try m.match(.{ "query_recent_files", tp.extract(&project_directory), tp.extract(&max), tp.extract(&query) })) { self.query_recent_files(from, project_directory, max, query) catch |e| return from.forward_error(e, @errorReturnTrace()); } else if (try m.match(.{ "did_open", tp.extract(&project_directory), tp.extract(&path), tp.extract(&file_type), tp.extract_cbor(&language_server), tp.extract(&version), tp.extract(&text_ptr), tp.extract(&text_len) })) { @@ -282,6 +290,19 @@ const Process = struct { return project.request_recent_files(from, max); } + fn request_recent_projects(self: *Process, from: tp.pid_ref, project_directory: []const u8) error{ OutOfMemory, Exit }!void { + var recent_projects = std.ArrayList(RecentProject).init(self.a); + defer recent_projects.deinit(); + self.load_recent_projects(&recent_projects, project_directory) catch {}; + self.sort_projects_by_last_used(&recent_projects); + var message = std.ArrayList(u8).init(self.a); + const writer = message.writer(); + try cbor.writeArrayHeader(writer, recent_projects.items.len); + for (recent_projects.items) |project| + try cbor.writeValue(writer, project.name); + try from.send_raw(.{ .buf = message.items }); + } + fn query_recent_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize, query: []const u8) error{ OutOfMemory, Exit }!void { const project = if (self.projects.get(project_directory)) |p| p else return tp.exit("No project"); const start_time = std.time.milliTimestamp(); @@ -427,6 +448,61 @@ const Process = struct { } return stream.toOwnedSlice(); } + + fn load_recent_projects(self: *Process, recent_projects: *std.ArrayList(RecentProject), project_directory: []const u8) !void { + var path = std.ArrayList(u8).init(self.a); + defer path.deinit(); + const writer = path.writer(); + _ = try writer.write(try root.get_state_dir()); + _ = try writer.writeByte(std.fs.path.sep); + _ = try writer.write("projects"); + + var dir = try std.fs.cwd().openDir(path.items, .{ .iterate = true }); + defer dir.close(); + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (entry.kind != .file) continue; + try self.read_project_name(path.items, entry.name, recent_projects, project_directory); + } + } + + fn read_project_name( + self: *Process, + state_dir: []const u8, + file_path: []const u8, + recent_projects: *std.ArrayList(RecentProject), + project_directory: []const u8, + ) !void { + var path = std.ArrayList(u8).init(self.a); + defer path.deinit(); + const writer = path.writer(); + _ = try writer.write(state_dir); + _ = try writer.writeByte(std.fs.path.sep); + _ = try writer.write(file_path); + + var file = try std.fs.openFileAbsolute(path.items, .{ .mode = .read_only }); + defer file.close(); + const stat = try file.stat(); + const buffer = try self.a.alloc(u8, @intCast(stat.size)); + defer self.a.free(buffer); + _ = try file.readAll(buffer); + + var iter: []const u8 = buffer; + var name: []const u8 = undefined; + if (cbor.matchValue(&iter, tp.extract(&name)) catch return) { + const last_used = if (std.mem.eql(u8, project_directory, name)) std.math.maxInt(@TypeOf(stat.mtime)) else stat.mtime; + (try recent_projects.addOne()).* = .{ .name = try self.a.dupe(u8, name), .last_used = last_used }; + } + } + + fn sort_projects_by_last_used(_: *Process, recent_projects: *std.ArrayList(RecentProject)) void { + const less_fn = struct { + fn less_fn(_: void, lhs: RecentProject, rhs: RecentProject) bool { + return lhs.last_used > rhs.last_used; + } + }.less_fn; + std.mem.sort(RecentProject, recent_projects.items, {}, less_fn); + } }; fn walk_tree_async(a_: std.mem.Allocator, root_path_: []const u8) !tp.pid { diff --git a/src/tui/home.zig b/src/tui/home.zig index 49dfce3..d0c1c91 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -39,7 +39,7 @@ pub fn create(a: std.mem.Allocator, parent: Widget) !Widget { try self.menu.add_item_with_handler("Help ······················· :h", menu_action_help); try self.menu.add_item_with_handler("Open file ·················· :o", menu_action_open_file); try self.menu.add_item_with_handler("Open recent file ··········· :e", menu_action_open_recent_file); - try self.menu.add_item_with_handler("Open recent project ·(wip)·· :r", menu_action_open_recent_project); + try self.menu.add_item_with_handler("Open recent project ········ :r", menu_action_open_recent_project); try self.menu.add_item_with_handler("Show/Run commands ·········· :p", menu_action_show_commands); try self.menu.add_item_with_handler("Open config file ··········· :c", menu_action_open_config); try self.menu.add_item_with_handler("Change theme ··············· :t", menu_action_change_theme); @@ -116,7 +116,7 @@ fn menu_action_open_recent_file(_: **Menu.State(*Self), _: *Button.State(*Menu.S } fn menu_action_open_recent_project(_: **Menu.State(*Self), _: *Button.State(*Menu.State(*Self))) void { - tp.self_pid().send(.{ "log", "home", "open recent project not implemented" }) catch {}; + command.executeName("open_recent_project", .{}) catch {}; } fn menu_action_show_commands(_: **Menu.State(*Self), _: *Button.State(*Menu.State(*Self))) void { diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index f5020d1..a5ac183 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -87,7 +87,7 @@ pub fn create(a: std.mem.Allocator) !Widget { pub fn deinit(self: *Self, a: std.mem.Allocator) void { self.close_all_panel_views(); - for (self.file_stack.items) |file_path| self.a.free(file_path); + self.clear_file_stack(); self.file_stack.deinit(); self.commands.deinit(); self.widgets.deinit(a); @@ -224,7 +224,7 @@ const cmds = struct { } pub fn open_project_cwd(self: *Self, _: Ctx) Result { - try project_manager.open_cwd(); + try project_manager.open("."); _ = try self.statusbar.msg(.{ "PRJ", "open" }); } @@ -236,6 +236,23 @@ const cmds = struct { _ = try self.statusbar.msg(.{ "PRJ", "open" }); } + pub fn change_project(self: *Self, ctx: Ctx) Result { + var project_dir: []const u8 = undefined; + if (!try ctx.args.match(.{tp.extract(&project_dir)})) + return; + if (self.editor) |editor| { + if (editor.is_dirty()) + return tp.exit("unsaved changes"); + self.clear_file_stack(); + try editor.close_file(.{}); + } else { + self.clear_file_stack(); + } + try project_manager.open(project_dir); + _ = try self.statusbar.msg(.{ "PRJ", "open" }); + log.logger("project").print("switched to project {s}", .{project_dir}); + } + pub fn navigate(self: *Self, ctx: Ctx) Result { tui.reset_drag_context(); const frame = tracy.initZone(@src(), .{ .name = "navigate" }); @@ -653,6 +670,11 @@ fn pop_file_stack(self: *Self, closed: ?[]const u8) ?[]const u8 { return self.file_stack.popOrNull(); } +fn clear_file_stack(self: *Self) void { + for (self.file_stack.items) |file_path| self.a.free(file_path); + self.file_stack.clearRetainingCapacity(); +} + fn add_find_in_files_result( self: *Self, file_list_type: FileListType, diff --git a/src/tui/mode/input/flow.zig b/src/tui/mode/input/flow.zig index 640cdef..19b8a14 100644 --- a/src/tui/mode/input/flow.zig +++ b/src/tui/mode/input/flow.zig @@ -81,6 +81,7 @@ fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) !void { return switch (modifiers) { mod.CTRL => switch (keynormal) { 'E' => self.cmd("open_recent", .{}), + 'R' => self.cmd("open_recent_project", .{}), 'J' => self.cmd("toggle_panel", .{}), 'Z' => self.cmd("undo", .{}), 'Y' => self.cmd("redo", .{}), @@ -369,6 +370,7 @@ const hints = tui.KeybindHints.initComptime(.{ .{ "move_word_right", "C-right, A-f" }, .{ "open_command_palette", "C-S-p, S-A-p" }, .{ "open_recent", "C-e" }, + .{ "open_recent_project", "C-r" }, .{ "paste", "A-v" }, .{ "pop_cursor", "C-u" }, .{ "pull_down", "A-down" }, diff --git a/src/tui/mode/input/home.zig b/src/tui/mode/input/home.zig index ac39839..c4ae459 100644 --- a/src/tui/mode/input/home.zig +++ b/src/tui/mode/input/home.zig @@ -63,6 +63,7 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { 'W' => self.cmd("quit", .{}), 'O' => self.cmd("open_file", .{}), 'E' => self.cmd("open_recent", .{}), + 'R' => self.cmd("open_recent_project", .{}), 'P' => self.cmd("open_command_palette", .{}), '/' => self.cmd("open_help", .{}), 'K' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers }, @@ -94,7 +95,7 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { 'h' => self.cmd("open_help", .{}), 'o' => self.cmd("open_file", .{}), 'e' => self.cmd("open_recent", .{}), - 'r' => self.msg("open recent project not implemented"), + 'r' => self.cmd("open_recent_project", .{}), 'p' => self.cmd("open_command_palette", .{}), 'c' => self.cmd("open_config", .{}), 't' => self.cmd("change_theme", .{}), @@ -157,6 +158,7 @@ const hints = tui.KeybindHints.initComptime(.{ .{ "find_in_files", "C-S-f" }, .{ "open_file", "o, C-o" }, .{ "open_recent", "e, C-e" }, + .{ "open_recent_project", "r, C-r" }, .{ "open_command_palette", "p, C-S-p, S-A-p" }, .{ "home_menu_activate", "enter" }, .{ "home_menu_down", "down" }, diff --git a/src/tui/mode/overlay/open_recent_project.zig b/src/tui/mode/overlay/open_recent_project.zig new file mode 100644 index 0000000..9546d4a --- /dev/null +++ b/src/tui/mode/overlay/open_recent_project.zig @@ -0,0 +1,55 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const tp = @import("thespian"); +const project_manager = @import("project_manager"); + +const Widget = @import("../../Widget.zig"); +const tui = @import("../../tui.zig"); + +pub const Type = @import("palette.zig").Create(@This()); + +pub const label = "Search projects"; + +pub const Entry = struct { + name: []const u8, +}; + +pub const Match = struct { + name: []const u8, + score: i32, + matches: []const usize, +}; + +pub fn load_entries(palette: *Type) !void { + const rsp = try project_manager.request_recent_projects(palette.a); + var iter: []const u8 = rsp.buf; + var len = try cbor.decodeArrayHeader(&iter); + while (len > 0) : (len -= 1) { + var name: []const u8 = undefined; + if (try cbor.matchValue(&iter, cbor.extract(&name))) { + (palette.entries.addOne() catch @panic("oom")).* = .{ + .name = name, + }; + } else return error.InvalidMessageField; + } +} + +pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { + var value = std.ArrayList(u8).init(palette.a); + defer value.deinit(); + const writer = value.writer(); + try cbor.writeValue(writer, entry.name); + try cbor.writeValue(writer, if (palette.hints) |hints| hints.get(entry.name) orelse "" else ""); + if (matches) |matches_| + try cbor.writeValue(writer, matches_); + try palette.menu.add_item_with_handler(value.items, select); + palette.items += 1; +} + +fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { + var name: []const u8 = undefined; + var iter = button.opts.label; + if (!(cbor.matchString(&iter, &name) catch false)) return; + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("open_recent_project", e); + tp.self_pid().send(.{ "cmd", "change_project", .{name} }) catch |e| menu.*.opts.ctx.logger.err("open_recent_project", e); +} diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 914a293..f9d2726 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -641,6 +641,10 @@ const cmds = struct { return self.enter_overlay_mode(@import("mode/overlay/open_recent.zig")); } + pub fn open_recent_project(self: *Self, _: Ctx) Result { + return self.enter_overlay_mode(@import("mode/overlay/open_recent_project.zig").Type); + } + pub fn change_theme(self: *Self, _: Ctx) Result { return self.enter_overlay_mode(@import("mode/overlay/theme_palette.zig").Type); }