From 939537ed84e2b6cff8cd200ed2891e8880586a93 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 27 Jan 2025 18:59:13 +0100 Subject: [PATCH] feat(buffers): add support for ephemeral buffers Ephemeral buffers are not hidden and kept when closed. Ephemeral buffers can be turned into regular buffers by saving them with save_as. --- src/buffer/Buffer.zig | 14 ++++++++++++++ src/buffer/Manager.zig | 22 +++++++++++++++++++--- src/project_manager.zig | 6 ++++-- src/tui/Button.zig | 5 +++++ src/tui/editor.zig | 34 ++++++++++++++++++++++++---------- src/tui/mainview.zig | 25 +++++++++++++++++++++++-- src/tui/status/tabs.zig | 4 +++- 7 files changed, 92 insertions(+), 18 deletions(-) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 7761ee9..c2a4b16 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -40,6 +40,7 @@ file_eol_mode: EolMode = .lf, last_save_eol_mode: EolMode = .lf, file_utf8_sanitized: bool = false, hidden: bool = false, +ephemeral: bool = false, undo_history: ?*UndoNode = null, redo_history: ?*UndoNode = null, @@ -1279,6 +1280,7 @@ pub const StoreToFileError = error{ NotDir, NotOpenForWriting, OperationAborted, + OutOfMemory, PathAlreadyExists, PipeBusy, ProcessFdQuotaExceeded, @@ -1315,12 +1317,24 @@ pub fn store_to_file_and_clean(self: *Self, file_path: []const u8) StoreToFileEr self.last_save_eol_mode = self.file_eol_mode; self.file_exists = true; self.file_utf8_sanitized = false; + if (self.ephemeral) { + self.ephemeral = false; + self.file_path = try self.allocator.dupe(u8, file_path); + } } pub fn mark_clean(self: *Self) void { self.last_save = self.root; } +pub fn is_hidden(self: *const Self) bool { + return self.hidden; +} + +pub fn is_ephemeral(self: *const Self) bool { + return self.ephemeral; +} + pub fn is_dirty(self: *const Self) bool { return if (!self.file_exists) self.root.length() > 0 diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig index a0c23fa..2bcfa8e 100644 --- a/src/buffer/Manager.zig +++ b/src/buffer/Manager.zig @@ -47,6 +47,7 @@ pub fn open_scratch(self: *Self, file_path: []const u8, content: []const u8) Buf }; buffer.update_last_used_time(); buffer.hidden = false; + buffer.ephemeral = true; return buffer; } @@ -56,11 +57,23 @@ pub fn get_buffer_for_file(self: *Self, file_path: []const u8) ?*Buffer { pub fn delete_buffer(self: *Self, file_path: []const u8) bool { const buffer = self.buffers.get(file_path) orelse return false; + const did_remove = self.buffers.remove(file_path); buffer.deinit(); - return self.buffers.remove(file_path); + return did_remove; } -pub fn retire(_: *Self, _: *Buffer) void {} +pub fn retire(_: *Self, buffer: *Buffer) void { + tp.trace(tp.channel.debug, .{ "buffer", "retire", buffer.file_path, "hidden", buffer.hidden, "ephemeral", buffer.ephemeral }); +} + +pub fn close_buffer(self: *Self, buffer: *Buffer) void { + buffer.hidden = true; + tp.trace(tp.channel.debug, .{ "buffer", "close", buffer.file_path, "hidden", buffer.hidden, "ephemeral", buffer.ephemeral }); + if (buffer.is_ephemeral()) { + _ = self.buffers.remove(buffer.file_path); + buffer.deinit(); + } +} pub fn list_most_recently_used(self: *Self, allocator: std.mem.Allocator) error{OutOfMemory}![]*Buffer { const result = try self.list_unordered(allocator); @@ -98,7 +111,10 @@ pub fn save_all(self: *const Self) Buffer.StoreToFileError!void { var i = self.buffers.iterator(); while (i.next()) |kv| { const buffer = kv.value_ptr.*; - try buffer.store_to_file_and_clean(buffer.file_path); + if (buffer.is_ephemeral()) + buffer.mark_clean() + else + try buffer.store_to_file_and_clean(buffer.file_path); } } diff --git a/src/project_manager.zig b/src/project_manager.zig index 3a4b228..2712399 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -125,7 +125,8 @@ pub fn delete_task(task: []const u8) (ProjectManagerError || ProjectError)!void return send(.{ "delete_task", project, task }); } -pub fn did_open(file_path: []const u8, file_type: *const FileType, version: usize, text: []const u8) (ProjectManagerError || ProjectError)!void { +pub fn did_open(file_path: []const u8, file_type: *const FileType, version: usize, text: []const u8, ephemeral: bool) (ProjectManagerError || ProjectError)!void { + if (ephemeral) return; const project = tp.env.get().str("project"); if (project.len == 0) return error.NoProject; @@ -211,7 +212,8 @@ pub fn hover(file_path: []const u8, row: usize, col: usize) (ProjectManagerError return send(.{ "hover", project, file_path, row, col }); } -pub fn update_mru(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void { +pub fn update_mru(file_path: []const u8, row: usize, col: usize, ephemeral: bool) (ProjectManagerError || ProjectError)!void { + if (ephemeral) return; const project = tp.env.get().str("project"); if (project.len == 0) return error.NoProject; diff --git a/src/tui/Button.zig b/src/tui/Button.zig index d356ebe..ac9fa5b 100644 --- a/src/tui/Button.zig +++ b/src/tui/Button.zig @@ -82,6 +82,11 @@ pub fn State(ctx_type: type) type { allocator.destroy(self); } + pub fn update_label(self: *Self, label: []const u8) error{OutOfMemory}!void { + self.allocator.free(self.opts.label); + self.opts.label = try self.allocator.dupe(u8, label); + } + pub fn layout(self: *Self) Widget.Layout { return self.opts.on_layout(&self.opts.ctx, self); } diff --git a/src/tui/editor.zig b/src/tui/editor.zig index f12c282..4c71ba4 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -537,7 +537,13 @@ pub const Editor = struct { else syntax.create_guess_file_type(self.allocator, content.items, self.file_path) catch null; if (syn) |syn_| - project_manager.did_open(file_path, syn_.file_type, self.lsp_version, try content.toOwnedSlice()) catch |e| + project_manager.did_open( + file_path, + syn_.file_type, + self.lsp_version, + try content.toOwnedSlice(), + new_buf.is_ephemeral(), + ) catch |e| self.logger.print("project_manager.did_open failed: {any}", .{e}); break :syntax syn; }; @@ -563,6 +569,7 @@ pub const Editor = struct { fn save(self: *Self) !void { const b = self.buffer orelse return error.Stop; + if (b.is_ephemeral()) return self.logger.print_err("save", "ephemeral buffer, use save as", .{}); if (!b.is_dirty()) return self.logger.print("no changes to save", .{}); if (self.file_path) |file_path| { if (self.buffer) |b_mut| try b_mut.store_to_file_and_clean(file_path); @@ -3785,23 +3792,24 @@ pub const Editor = struct { pub const save_file_as_meta = .{ .arguments = &.{.string} }; pub fn close_file(self: *Self, _: Context) Result { + const buffer_ = self.buffer; + if (buffer_) |buffer| if (buffer.is_dirty()) + return tp.exit("unsaved changes"); self.cancel_all_selections(); - if (self.buffer) |buffer| { - if (buffer.is_dirty()) - return tp.exit("unsaved changes"); - buffer.hidden = true; - } try self.close(); + if (buffer_) |buffer| + self.buffer_manager.close_buffer(buffer); } pub const close_file_meta = .{ .description = "Close file" }; pub fn close_file_without_saving(self: *Self, _: Context) Result { self.cancel_all_selections(); - if (self.buffer) |buffer| { + const buffer_ = self.buffer; + if (buffer_) |buffer| buffer.reset_to_last_saved(); - buffer.hidden = true; - } try self.close(); + if (buffer_) |buffer| + self.buffer_manager.close_buffer(buffer); } pub const close_file_without_saving_meta = .{ .description = "Close file without saving" }; @@ -4781,7 +4789,13 @@ pub const Editor = struct { try root.store(content.writer(), try self.buf_eol_mode()); const syn = syntax.create_file_type(self.allocator, file_type) catch null; if (syn) |syn_| if (self.file_path) |file_path| - project_manager.did_open(file_path, syn_.file_type, self.lsp_version, try content.toOwnedSlice()) catch |e| + project_manager.did_open( + file_path, + syn_.file_type, + self.lsp_version, + try content.toOwnedSlice(), + if (self.buffer) |p| p.is_ephemeral() else true, + ) catch |e| self.logger.print("project_manager.did_open failed: {any}", .{e}); break :syntax syn; }; diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 933852d..cf47dfe 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -412,6 +412,22 @@ const cmds = struct { } pub const delete_buffer_meta = .{ .arguments = &.{.string} }; + pub fn close_buffer(self: *Self, ctx: Ctx) Result { + var file_path: []const u8 = undefined; + if (!(ctx.args.match(.{tp.extract(&file_path)}) catch false)) + return error.InvalidDeleteBufferArgument; + const buffer = self.buffer_manager.get_buffer_for_file(file_path) orelse return; + if (buffer.is_dirty()) + return tp.exit("unsaved changes"); + if (self.get_active_editor()) |editor| if (editor.buffer == buffer) { + editor.close_file(.{}) catch |e| return e; + return; + }; + _ = self.buffer_manager.close_buffer(buffer); + tui.need_render(); + } + pub const close_buffer_meta = .{ .arguments = &.{.string} }; + pub fn restore_session(self: *Self, _: Ctx) Result { if (tp.env.get().str("project").len == 0) { try open_project_cwd(self, .{}); @@ -859,16 +875,17 @@ pub fn location_update(self: *Self, m: tp.message) tp.result { var row: usize = 0; var col: usize = 0; const file_path = self.get_active_file_path() orelse return; + const ephemeral = if (self.get_active_buffer()) |buffer| buffer.is_ephemeral() else false; if (try m.match(.{ tp.any, tp.any, tp.any, tp.extract(&row), tp.extract(&col) })) { if (row == 0 and col == 0) return; - project_manager.update_mru(file_path, row, col) catch {}; + project_manager.update_mru(file_path, row, col, ephemeral) catch {}; return self.location_history.update(file_path, .{ .row = row + 1, .col = col + 1 }, null); } var sel: location_history.Selection = .{}; if (try m.match(.{ tp.any, tp.any, tp.any, tp.extract(&row), tp.extract(&col), tp.extract(&sel.begin.row), tp.extract(&sel.begin.col), tp.extract(&sel.end.row), tp.extract(&sel.end.col) })) { - project_manager.update_mru(file_path, row, col) catch {}; + project_manager.update_mru(file_path, row, col, ephemeral) catch {}; return self.location_history.update(file_path, .{ .row = row + 1, .col = col + 1 }, sel); } } @@ -911,6 +928,10 @@ pub fn get_active_file_path(self: *Self) ?[]const u8 { return if (self.get_active_editor()) |editor| editor.file_path orelse null else null; } +pub fn get_active_buffer(self: *Self) ?*Buffer { + return if (self.get_active_editor()) |editor| editor.buffer orelse null else null; +} + pub fn walk(self: *Self, ctx: *anyopaque, f: Widget.WalkFn, w: *Widget) bool { return self.floating_views.walk(ctx, f) or self.widgets.walk(ctx, f, &self.widgets_widget) or f(ctx, w); } diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index eed46a2..dc5c29c 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -117,6 +117,8 @@ const TabBar = struct { try self.widget_list.add(try self.make_spacer()); } try self.widget_list.add(tab.widget); + if (tab.widget.dynamic_cast(Button.State(Tab))) |btn| + try btn.update_label(Tab.name_from_buffer(tab.buffer)); } } @@ -212,7 +214,7 @@ const Tab = struct { } fn on_click2(self: *@This(), _: *Button.State(@This())) void { - tp.self_pid().send(.{ "cmd", "delete_buffer", .{self.buffer.file_path} }) catch {}; + tp.self_pid().send(.{ "cmd", "close_buffer", .{self.buffer.file_path} }) catch {}; } fn render(self: *@This(), btn: *Button.State(@This()), theme: *const Widget.Theme) bool {