From 1babf86ce4b56acf9199aa0a3a07531d88d8eb1a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 6 Aug 2025 19:45:28 +0200 Subject: [PATCH 01/11] fix: crash in Buffer.set_file_path --- src/buffer/Buffer.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index c094845..7f265f0 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -1100,6 +1100,7 @@ pub fn get_meta(self: *Self) ?[]const u8 { } pub fn set_file_path(self: *Self, file_path: []const u8) void { + if (file_path.ptr == self.file_path_buf.items.ptr) return; self.file_path_buf.clearRetainingCapacity(); self.file_path_buf.appendSlice(self.external_allocator, file_path) catch |e| switch (e) { error.OutOfMemory => @panic("OOM in Buffer.set_file_path"), From 991c47f3b3f72ef276864c237a2669e27b8bc180 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 5 Aug 2025 20:08:29 +0200 Subject: [PATCH 02/11] feat: restore buffer manager state on restart --- build.zig | 1 + src/buffer/Buffer.zig | 57 ++++++++++++++++++++++++++++++++++++++++++ src/buffer/Manager.zig | 31 ++++++++++++++++++++++- src/main.zig | 2 +- src/tui/editor.zig | 23 +++++++++++------ src/tui/mainview.zig | 21 +++++++++++++--- 6 files changed, 121 insertions(+), 14 deletions(-) diff --git a/build.zig b/build.zig index 313e84b..d698061 100644 --- a/build.zig +++ b/build.zig @@ -361,6 +361,7 @@ pub fn build_exe( .{ .name = "cbor", .module = cbor_mod }, .{ .name = "thespian", .module = thespian_mod }, .{ .name = "LetterCasing", .module = zg_dep.module("LetterCasing") }, + .{ .name = "file_type_config", .module = file_type_config_mod }, }, }); diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 7f265f0..6d5a1d2 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -1,5 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const cbor = @import("cbor"); +const file_type_config = @import("file_type_config"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const cwd = std.fs.cwd; @@ -1487,3 +1489,58 @@ pub fn redo(self: *Self) error{Stop}![]const u8 { self.mtime = std.time.milliTimestamp(); return h.meta; } + +pub fn write_state(self: *const Self, writer: MetaWriter) error{ Stop, OutOfMemory }!void { + var content = std.ArrayListUnmanaged(u8).empty; + defer content.deinit(self.external_allocator); + try self.root.store(content.writer(self.external_allocator), self.file_eol_mode); + + try cbor.writeArrayHeader(writer, 8); + try cbor.writeValue(writer, self.get_file_path()); + try cbor.writeValue(writer, self.file_exists); + try cbor.writeValue(writer, self.file_eol_mode); + try cbor.writeValue(writer, self.hidden); + try cbor.writeValue(writer, self.ephemeral); + try cbor.writeValue(writer, self.meta orelse &.{}); + try cbor.writeValue(writer, self.file_type_name); + try cbor.writeValue(writer, content.items); +} + +pub const ExtractStateOperation = enum { none, open_file }; + +pub fn extract_state(self: *Self, iter: *[]const u8) !void { + var file_path: []const u8 = undefined; + var file_type_name: []const u8 = undefined; + var meta: []const u8 = &.{}; + var content: []const u8 = undefined; + + if (!try cbor.matchValue(iter, .{ + cbor.extract(&file_path), + cbor.extract(&self.file_exists), + cbor.extract(&self.file_eol_mode), + cbor.extract(&self.hidden), + cbor.extract(&self.ephemeral), + cbor.extract(&meta), + cbor.extract(&file_type_name), + cbor.extract(&content), + })) + return error.Stop; + + self.set_file_path(file_path); + + if (try file_type_config.get(try self.allocator.dupe(u8, file_type_name))) |config| { + self.file_type_name = config.name; + self.file_type_icon = config.icon; + self.file_type_color = config.color; + } else { + self.file_type_name = file_type_config.default.name; + self.file_type_icon = file_type_config.default.icon; + self.file_type_color = file_type_config.default.color; + } + + if (meta.len > 0) { + if (self.meta) |buf| self.external_allocator.free(buf); + self.meta = if (self.meta) |buf| try self.external_allocator.dupe(u8, buf) else null; + } + try self.reset_from_string_and_update(content); +} diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig index d2484ee..9097d58 100644 --- a/src/buffer/Manager.zig +++ b/src/buffer/Manager.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const cbor = @import("cbor"); const tp = @import("thespian"); const Buffer = @import("Buffer.zig"); @@ -51,6 +52,34 @@ pub fn open_scratch(self: *Self, file_path: []const u8, content: []const u8) Buf return buffer; } +pub fn write_state(self: *const Self, writer: Buffer.MetaWriter) error{ Stop, OutOfMemory }!void { + const buffers = self.list_unordered(self.allocator) catch return; + defer self.allocator.free(buffers); + try cbor.writeArrayHeader(writer, buffers.len); + for (buffers) |buffer| { + tp.trace(tp.channel.debug, .{ @typeName(Self), "write_state", buffer.get_file_path(), buffer.file_type_name }); + buffer.write_state(writer) catch |e| { + tp.trace(tp.channel.debug, .{ @typeName(Self), "write_state", "failed", e }); + return; + }; + } +} + +pub fn extract_state(self: *Self, iter: *[]const u8) !void { + var len = try cbor.decodeArrayHeader(iter); + tp.trace(tp.channel.debug, .{ @typeName(Self), "extract_state", len }); + while (len > 0) : (len -= 1) { + var buffer = try Buffer.create(self.allocator); + errdefer |e| { + tp.trace(tp.channel.debug, .{ "buffer", "extract", "failed", buffer.get_file_path(), e }); + buffer.deinit(); + } + try buffer.extract_state(iter); + try self.buffers.put(self.allocator, try self.allocator.dupe(u8, buffer.get_file_path()), buffer); + tp.trace(tp.channel.debug, .{ "buffer", "extract", buffer.get_file_path(), buffer.file_type_name }); + } +} + pub fn get_buffer_for_file(self: *Self, file_path: []const u8) ?*Buffer { return self.buffers.get(file_path); } @@ -89,7 +118,7 @@ pub fn list_most_recently_used(self: *Self, allocator: std.mem.Allocator) error{ return result; } -pub fn list_unordered(self: *Self, allocator: std.mem.Allocator) error{OutOfMemory}![]*Buffer { +pub fn list_unordered(self: *const Self, allocator: std.mem.Allocator) error{OutOfMemory}![]*Buffer { var buffers = try std.ArrayListUnmanaged(*Buffer).initCapacity(allocator, self.buffers.size); var i = self.buffers.iterator(); while (i.next()) |kv| diff --git a/src/main.zig b/src/main.zig index 07e1cf5..f337409 100644 --- a/src/main.zig +++ b/src/main.zig @@ -942,7 +942,7 @@ pub fn get_restore_file_name() ![]const u8 { const restore_file = if (local.restore_file) |file| file else - try std.fmt.bufPrint(&local.restore_file_buffer, "{s}/{s}", .{ try get_app_cache_dir(application_name), restore_file_name }); + try std.fmt.bufPrint(&local.restore_file_buffer, "{s}/{s}", .{ try get_app_state_dir(application_name), restore_file_name }); local.restore_file = restore_file; return restore_file; } diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 09e232c..c73168a 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -371,6 +371,14 @@ pub const Editor = struct { const Meta = command.Metadata; const Result = command.Result; + pub fn update_meta(self: *const Self) void { + var meta = std.ArrayListUnmanaged(u8).empty; + defer meta.deinit(self.allocator); + if (self.buffer) |_| self.write_state(meta.writer(self.allocator)) catch {}; + if (self.buffer) |_| self.write_state(meta.writer(self.allocator)) catch {}; + if (self.buffer) |p| p.set_meta(meta.items) catch {}; + } + pub fn write_state(self: *const Self, writer: Buffer.MetaWriter) !void { try cbor.writeArrayHeader(writer, 12); try cbor.writeValue(writer, self.file_path orelse ""); @@ -401,9 +409,7 @@ pub const Editor = struct { }; } - pub fn extract_state(self: *Self, buf: []const u8, comptime op: enum { none, open_file }) !void { - tp.trace(tp.channel.debug, .{ "extract_state", self.file_path }); - tp.trace(tp.channel.debug, tp.message{ .buf = buf }); + pub fn extract_state(self: *Self, iter: *[]const u8, comptime op: Buffer.ExtractStateOperation) !void { self.restored_state = true; var file_path: []const u8 = undefined; var view_cbor: []const u8 = undefined; @@ -411,7 +417,7 @@ pub const Editor = struct { var clipboard: []const u8 = undefined; var last_find_query: []const u8 = undefined; var find_history: []const u8 = undefined; - if (!try cbor.match(buf, .{ + if (!try cbor.matchValue(iter, .{ tp.extract(&file_path), tp.extract(&clipboard), tp.extract(&last_find_query), @@ -440,11 +446,11 @@ pub const Editor = struct { if (cursels_cbor.len > 0) self.clear_all_cursors(); - var iter = cursels_cbor; - var len = cbor.decodeArrayHeader(&iter) catch return error.RestoreCurSels; + var cursels_iter = cursels_cbor; + var len = cbor.decodeArrayHeader(&cursels_iter) catch return error.RestoreCurSels; while (len > 0) : (len -= 1) { var cursel: CurSel = .{}; - if (!(cursel.extract(&iter) catch false)) break; + if (!(cursel.extract(&cursels_iter) catch false)) break; (try self.cursels.addOne(self.allocator)).* = cursel; } @@ -659,7 +665,8 @@ pub const Editor = struct { if (buffer_meta) |meta| { const frame_ = tracy.initZone(@src(), .{ .name = "extract_state" }); defer frame_.deinit(); - try self.extract_state(meta, .none); + var iter = meta; + try self.extract_state(&iter, .none); } try self.send_editor_open(file_path, new_buf.file_exists, ftn, fti, ftc); } diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index c210882..e9e5a4f 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1288,11 +1288,18 @@ fn create_home_split(self: *Self) !void { } pub fn write_restore_info(self: *Self) void { - const editor = self.get_active_editor() orelse return; var sfa = std.heap.stackFallback(512, self.allocator); const a = sfa.get(); var meta = std.ArrayListUnmanaged(u8).empty; - editor.write_state(meta.writer(a)) catch return; + const writer = meta.writer(a); + + const editor = self.get_active_editor() orelse return; + cbor.writeValue(writer, editor.file_path) catch return; + editor.update_meta(); + + const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); + buffer_manager.write_state(writer) catch return; + const file_name = root.get_restore_file_name() catch return; var file = std.fs.createFileAbsolute(file_name, .{ .truncate = true }) catch return; defer file.close(); @@ -1300,7 +1307,6 @@ pub fn write_restore_info(self: *Self) void { } fn read_restore_info(self: *Self) !void { - const editor = self.get_active_editor() orelse return; const file_name = try root.get_restore_file_name(); const file = try std.fs.cwd().openFile(file_name, .{ .mode = .read_only }); defer file.close(); @@ -1308,7 +1314,14 @@ fn read_restore_info(self: *Self) !void { var buf = try self.allocator.alloc(u8, @intCast(stat.size)); defer self.allocator.free(buf); const size = try file.readAll(buf); - try editor.extract_state(buf[0..size], .open_file); + var iter: []const u8 = buf[0..size]; + + tp.trace(tp.channel.debug, .{ "mainview", "extract" }); + var editor_file_path: []const u8 = undefined; + if (!try cbor.matchValue(&iter, cbor.extract(&editor_file_path))) return error.Stop; + try self.buffer_manager.extract_state(&iter); + + try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = editor_file_path } }); } fn get_next_mru_buffer(self: *Self) ?[]const u8 { From e7e3aaecfd392fce4f26c618b40169f686af894c Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 8 Aug 2025 12:52:06 +0200 Subject: [PATCH 03/11] feat: update thespian for cbor null optionals support --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 0a34e23..57fc456 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -15,8 +15,8 @@ .hash = "dizzy-1.0.0-AAAAAM1wAAAiDbx_6RwcVEOBk8p2XOu8t9WPNc3K7kBK", }, .thespian = .{ - .url = "git+https://github.com/neurocyte/thespian#f2980d3a747abdf0d18a01596dd8b953dd3e6243", - .hash = "thespian-0.0.1-owFOjk0aBgC8w9ibeiVdhftyEIaVIHCnubsJWfkktE8v", + .url = "git+https://github.com/neurocyte/thespian?ref=master#0a386496cda74ef827d5770f6f071a7d2d54b91a", + .hash = "thespian-0.0.1-owFOjlgaBgCAOkRjH9_mDN7dnL8n8K3XA2hqqchjXZIk", }, .themes = .{ .url = "https://github.com/neurocyte/flow-themes/releases/download/master-952f9f630ea9544088fd30293666ee0650b7a690/flow-themes.tar.gz", From 68afe421618080bb5d853d41c736acdc70746ab4 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 8 Aug 2025 13:07:34 +0200 Subject: [PATCH 04/11] feat: add command to add new task to task list --- src/tui/mode/overlay/task_palette.zig | 76 ++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/src/tui/mode/overlay/task_palette.zig b/src/tui/mode/overlay/task_palette.zig index 0bf9d0c..0baae7b 100644 --- a/src/tui/mode/overlay/task_palette.zig +++ b/src/tui/mode/overlay/task_palette.zig @@ -5,6 +5,7 @@ const command = @import("command"); const project_manager = @import("project_manager"); const tui = @import("../../tui.zig"); +const Widget = @import("../../Widget.zig"); pub const Type = @import("palette.zig").Create(@This()); const module_name = @typeName(@This()); @@ -14,7 +15,7 @@ pub const description = "task"; pub const Entry = struct { label: []const u8, - hint: []const u8, + command: ?[]const u8 = null, }; pub fn deinit(palette: *Type) void { @@ -29,9 +30,13 @@ pub fn load_entries(palette: *Type) !usize { while (len > 0) : (len -= 1) { var task: []const u8 = undefined; if (try cbor.matchValue(&iter, cbor.extract(&task))) { - (try palette.entries.addOne()).* = .{ .label = try palette.allocator.dupe(u8, task), .hint = "" }; + (try palette.entries.addOne()).* = .{ .label = try palette.allocator.dupe(u8, task) }; } else return error.InvalidTaskMessageField; } + (try palette.entries.addOne()).* = .{ + .label = try palette.allocator.dupe(u8, " Add new task"), + .command = "add_task", + }; return if (palette.entries.items.len == 0) label.len else blk: { var longest: usize = 0; for (palette.entries.items) |item| longest = @max(longest, item.label.len); @@ -49,30 +54,73 @@ pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !v var value = std.ArrayList(u8).init(palette.allocator); defer value.deinit(); const writer = value.writer(); - try cbor.writeValue(writer, entry.label); - try cbor.writeValue(writer, entry.hint); + try cbor.writeValue(writer, entry); try cbor.writeValue(writer, matches orelse &[_]usize{}); try palette.menu.add_item_with_handler(value.items, select); palette.items += 1; } +pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget.Theme, selected: bool) bool { + var entry: Entry = undefined; + var iter = button.opts.label; // label contains cbor entry object and matches + if (!(cbor.matchValue(&iter, cbor.extract(&entry)) catch false)) + entry.label = "#ERROR#"; + + const style_base = theme.editor_widget; + const style_label = + if (button.active) + theme.editor_cursor + else if (button.hover or selected) + theme.editor_selection + else if (entry.command) |_| + theme.input_placeholder + else + theme.editor_widget; + + const style_hint = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_label; + button.plane.set_base_style(style_base); + button.plane.erase(); + button.plane.home(); + button.plane.set_style(style_label); + button.plane.fill(" "); + button.plane.home(); + button.plane.set_style(style_hint); + const pointer = if (selected) "⏵" else " "; + _ = button.plane.print("{s}", .{pointer}) catch {}; + button.plane.set_style(style_label); + _ = button.plane.print("{s} ", .{entry.label}) catch {}; + var index: usize = 0; + var len = cbor.decodeArrayHeader(&iter) catch return false; + while (len > 0) : (len -= 1) { + if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { + tui.render_match_cell(&button.plane, 0, index + 1, theme) catch break; + } else break; + } + return false; +} + fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { - var task: []const u8 = undefined; + var entry: Entry = undefined; var iter = button.opts.label; - if (!(cbor.matchString(&iter, &task) catch false)) return; + if (!(cbor.matchValue(&iter, cbor.extract(&entry)) catch false)) return; var buffer_name = std.ArrayList(u8).init(menu.*.opts.ctx.allocator); defer buffer_name.deinit(); - buffer_name.writer().print("*{s}*", .{task}) catch {}; - project_manager.add_task(task) catch {}; - tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - tp.self_pid().send(.{ "cmd", "create_scratch_buffer", .{ buffer_name.items, "", "conf" } }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - tp.self_pid().send(.{ "cmd", "shell_execute_stream", .{task} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + buffer_name.writer().print("*{s}*", .{entry.label}) catch {}; + if (entry.command) |cmd| { + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + tp.self_pid().send(.{ "cmd", cmd, .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + } else { + project_manager.add_task(entry.label) catch {}; + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + tp.self_pid().send(.{ "cmd", "create_scratch_buffer", .{ buffer_name.items, "", "conf" } }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + tp.self_pid().send(.{ "cmd", "shell_execute_stream", .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + } } pub fn delete_item(menu: *Type.MenuState, button: *Type.ButtonState) bool { - var task: []const u8 = undefined; + var entry: Entry = undefined; var iter = button.opts.label; - if (!(cbor.matchString(&iter, &task) catch false)) return false; - command.executeName("delete_task", command.fmt(.{task})) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + if (!(cbor.matchValue(&iter, cbor.extract(&entry)) catch false)) return false; + command.executeName("delete_task", command.fmt(.{entry.label})) catch |e| menu.*.opts.ctx.logger.err(module_name, e); return true; //refresh list } From f2a3075d587e9198290f458c6bdf6aef2f813b5d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 8 Aug 2025 13:11:36 +0200 Subject: [PATCH 05/11] fix: correct restore of buffer meta data --- src/buffer/Buffer.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 6d5a1d2..304a52e 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -1501,7 +1501,7 @@ pub fn write_state(self: *const Self, writer: MetaWriter) error{ Stop, OutOfMemo try cbor.writeValue(writer, self.file_eol_mode); try cbor.writeValue(writer, self.hidden); try cbor.writeValue(writer, self.ephemeral); - try cbor.writeValue(writer, self.meta orelse &.{}); + try cbor.writeValue(writer, self.meta); try cbor.writeValue(writer, self.file_type_name); try cbor.writeValue(writer, content.items); } @@ -1511,7 +1511,7 @@ pub const ExtractStateOperation = enum { none, open_file }; pub fn extract_state(self: *Self, iter: *[]const u8) !void { var file_path: []const u8 = undefined; var file_type_name: []const u8 = undefined; - var meta: []const u8 = &.{}; + var meta: ?[]const u8 = null; var content: []const u8 = undefined; if (!try cbor.matchValue(iter, .{ @@ -1538,9 +1538,9 @@ pub fn extract_state(self: *Self, iter: *[]const u8) !void { self.file_type_color = file_type_config.default.color; } - if (meta.len > 0) { - if (self.meta) |buf| self.external_allocator.free(buf); - self.meta = if (self.meta) |buf| try self.external_allocator.dupe(u8, buf) else null; + if (meta) |buf| { + if (self.meta) |old_buf| self.external_allocator.free(old_buf); + self.meta = try self.external_allocator.dupe(u8, buf); } try self.reset_from_string_and_update(content); } From dd2601b62795960d4f8b85a14c41db5bb50b137d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 8 Aug 2025 13:16:17 +0200 Subject: [PATCH 06/11] feat: store/restore buffer dirty state --- src/buffer/Buffer.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 304a52e..e3d5c47 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -1494,13 +1494,15 @@ pub fn write_state(self: *const Self, writer: MetaWriter) error{ Stop, OutOfMemo var content = std.ArrayListUnmanaged(u8).empty; defer content.deinit(self.external_allocator); try self.root.store(content.writer(self.external_allocator), self.file_eol_mode); + const dirty = self.is_dirty(); - try cbor.writeArrayHeader(writer, 8); + try cbor.writeArrayHeader(writer, 9); try cbor.writeValue(writer, self.get_file_path()); try cbor.writeValue(writer, self.file_exists); try cbor.writeValue(writer, self.file_eol_mode); try cbor.writeValue(writer, self.hidden); try cbor.writeValue(writer, self.ephemeral); + try cbor.writeValue(writer, dirty); try cbor.writeValue(writer, self.meta); try cbor.writeValue(writer, self.file_type_name); try cbor.writeValue(writer, content.items); @@ -1511,6 +1513,7 @@ pub const ExtractStateOperation = enum { none, open_file }; pub fn extract_state(self: *Self, iter: *[]const u8) !void { var file_path: []const u8 = undefined; var file_type_name: []const u8 = undefined; + var dirty: bool = undefined; var meta: ?[]const u8 = null; var content: []const u8 = undefined; @@ -1520,6 +1523,7 @@ pub fn extract_state(self: *Self, iter: *[]const u8) !void { cbor.extract(&self.file_eol_mode), cbor.extract(&self.hidden), cbor.extract(&self.ephemeral), + cbor.extract(&dirty), cbor.extract(&meta), cbor.extract(&file_type_name), cbor.extract(&content), @@ -1543,4 +1547,5 @@ pub fn extract_state(self: *Self, iter: *[]const u8) !void { self.meta = try self.external_allocator.dupe(u8, buf); } try self.reset_from_string_and_update(content); + if (dirty) self.mark_dirty(); } From a98d4e02a7cc580a6a70bf79ade01d481525e0d2 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 8 Aug 2025 13:32:57 +0200 Subject: [PATCH 07/11] fix: restart with no open file should return to home screen --- src/tui/mainview.zig | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index e9e5a4f..a4aa7b0 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1293,9 +1293,12 @@ pub fn write_restore_info(self: *Self) void { var meta = std.ArrayListUnmanaged(u8).empty; const writer = meta.writer(a); - const editor = self.get_active_editor() orelse return; - cbor.writeValue(writer, editor.file_path) catch return; - editor.update_meta(); + if (self.get_active_editor()) |editor| { + cbor.writeValue(writer, editor.file_path) catch return; + editor.update_meta(); + } else { + cbor.writeValue(writer, null) catch return; + } const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); buffer_manager.write_state(writer) catch return; @@ -1317,11 +1320,15 @@ fn read_restore_info(self: *Self) !void { var iter: []const u8 = buf[0..size]; tp.trace(tp.channel.debug, .{ "mainview", "extract" }); - var editor_file_path: []const u8 = undefined; + var editor_file_path: ?[]const u8 = undefined; if (!try cbor.matchValue(&iter, cbor.extract(&editor_file_path))) return error.Stop; try self.buffer_manager.extract_state(&iter); - try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = editor_file_path } }); + if (editor_file_path) |file_path| { + try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }); + } else { + try tp.self_pid().send(.{ "cmd", "close_file" }); + } } fn get_next_mru_buffer(self: *Self) ?[]const u8 { From 1ebdae310ea13dd1f1f958d69a28d82d70c3e214 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 8 Aug 2025 20:40:00 +0200 Subject: [PATCH 08/11] feat: save and restore tabbar state --- src/tui/mainview.zig | 12 +++++++++ src/tui/status/tabs.zig | 58 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index a4aa7b0..02ae484 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1303,6 +1303,10 @@ pub fn write_restore_info(self: *Self) void { const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); buffer_manager.write_state(writer) catch return; + if (self.widgets.get("tabs")) |tabs_widget| + if (tabs_widget.dynamic_cast(@import("status/tabs.zig").TabBar)) |tabs| + tabs.write_state(writer) catch return; + const file_name = root.get_restore_file_name() catch return; var file = std.fs.createFileAbsolute(file_name, .{ .truncate = true }) catch return; defer file.close(); @@ -1324,6 +1328,14 @@ fn read_restore_info(self: *Self) !void { if (!try cbor.matchValue(&iter, cbor.extract(&editor_file_path))) return error.Stop; try self.buffer_manager.extract_state(&iter); + if (self.widgets.get("tabs")) |tabs_widget| + if (tabs_widget.dynamic_cast(@import("status/tabs.zig").TabBar)) |tabs| + tabs.extract_state(&iter) catch |e| { + const logger = log.logger("mainview"); + defer logger.deinit(); + logger.print_err("mainview", "failed to restore tabs: {}", .{e}); + }; + if (editor_file_path) |file_path| { try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }); } else { diff --git a/src/tui/status/tabs.zig b/src/tui/status/tabs.zig index c2de5b2..97ffbf6 100644 --- a/src/tui/status/tabs.zig +++ b/src/tui/status/tabs.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const cbor = @import("cbor"); const tp = @import("thespian"); const root = @import("root"); @@ -64,7 +65,7 @@ pub fn create(allocator: std.mem.Allocator, parent: Plane, event_handler: ?Event return Widget.to(self); } -const TabBar = struct { +pub const TabBar = struct { allocator: std.mem.Allocator, plane: Plane, widget_list: *WidgetList, @@ -267,6 +268,45 @@ const TabBar = struct { if (buffer_manager.buffer_from_ref(tab.buffer_ref)) |buffer| tp.self_pid().send(.{ "cmd", "navigate", .{ .file = buffer.get_file_path() } }) catch {}; } + + pub fn write_state(self: *const Self, writer: Buffer.MetaWriter) error{OutOfMemory}!void { + try cbor.writeArrayHeader(writer, self.tabs.len); + for (self.tabs) |tab| try cbor.writeValue(writer, ref_to_name(tab.buffer_ref)); + } + + fn ref_to_name(buffer_ref: usize) ?[]const u8 { + const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); + return if (buffer_manager.buffer_from_ref(buffer_ref)) |buffer| buffer.get_file_path() else null; + } + + pub fn extract_state(self: *Self, iter: *[]const u8) !void { + var iter2 = iter.*; + self.allocator.free(self.tabs); + self.tabs = &.{}; + + var result: std.ArrayListUnmanaged(TabBarTab) = .{}; + errdefer result.deinit(self.allocator); + + var count = cbor.decodeArrayHeader(&iter2) catch return error.MatchTabArrayFailed; + while (count > 0) : (count -= 1) { + var buffer_name: ?[]const u8 = undefined; + if (!(cbor.matchValue(&iter2, cbor.extract(&buffer_name)) catch false)) return error.MatchTabBufferNameFailed; + if (buffer_name) |name| if (name_to_ref(name)) |buffer_ref| { + (try result.addOne(self.allocator)).* = .{ + .buffer_ref = buffer_ref, + .widget = try Tab.create(self, buffer_ref, &self.tab_style, self.event_handler), + }; + }; + } + + self.tabs = try result.toOwnedSlice(self.allocator); + iter.* = iter2; + } + + fn name_to_ref(buffer_name: []const u8) ?usize { + const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); + return if (buffer_manager.get_buffer_for_file(buffer_name)) |buffer| buffer_manager.buffer_to_ref(buffer) else null; + } }; const Tab = struct { @@ -457,6 +497,22 @@ const Tab = struct { const basename = if (basename_begin) |begin| file_path[begin + 1 ..] else file_path; return basename; } + + fn write_state(self: *const @This(), writer: Buffer.MetaWriter) error{OutOfMemory}!void { + try cbor.writeArrayHeader(writer, 9); + try cbor.writeValue(writer, self.get_file_path()); + try cbor.writeValue(writer, self.file_exists); + try cbor.writeValue(writer, self.file_eol_mode); + try cbor.writeValue(writer, self.hidden); + try cbor.writeValue(writer, self.ephemeral); + try cbor.writeValue(writer, self.meta); + try cbor.writeValue(writer, self.file_type_name); + } + + fn extract_state(self: *@This(), iter: *[]const u8) !void { + _ = self; + _ = iter; + } }; const spacer = struct { From 68a5de5aa7ac59da9b009434b4d26894ba793d03 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 8 Aug 2025 21:02:16 +0200 Subject: [PATCH 09/11] refactor: move lsp_version field to Buffer --- src/buffer/Buffer.zig | 1 + src/tui/editor.zig | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index e3d5c47..ddcf889 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -44,6 +44,7 @@ file_utf8_sanitized: bool = false, hidden: bool = false, ephemeral: bool = false, meta: ?[]const u8 = null, +lsp_version: usize = 1, undo_history: ?*UndoNode = null, redo_history: ?*UndoNode = null, diff --git a/src/tui/editor.zig b/src/tui/editor.zig index c73168a..6205e22 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -271,7 +271,6 @@ pub const Editor = struct { file_path: ?[]const u8, buffer: ?*Buffer, buffer_manager: *Buffer.Manager, - lsp_version: usize = 1, pause_undo: bool = false, pause_undo_root: ?Buffer.Root = null, @@ -642,7 +641,7 @@ pub const Editor = struct { project_manager.did_open( file_path, ft, - self.lsp_version, + new_buf.lsp_version, try content.toOwnedSlice(std.heap.c_allocator), new_buf.is_ephemeral(), ) catch |e| @@ -1644,7 +1643,8 @@ pub const Editor = struct { if (token_from(self.last.root) != token_from(root)) { try self.send_editor_update(self.last.root, root, eol_mode); - self.lsp_version += 1; + if (self.buffer) |buf| + buf.lsp_version += 1; } if (self.last.eol_mode != eol_mode or self.last.utf8_sanitized != utf8_sanitized) @@ -1769,8 +1769,8 @@ pub const Editor = struct { fn send_editor_update(self: *const Self, old_root: ?Buffer.Root, new_root: ?Buffer.Root, eol_mode: Buffer.EolMode) !void { _ = try self.handlers.msg(.{ "E", "update", token_from(new_root), token_from(old_root), @intFromEnum(eol_mode) }); - if (self.syntax) |_| if (self.file_path) |file_path| if (old_root != null and new_root != null) - project_manager.did_change(file_path, self.lsp_version, try text_from_root(new_root, eol_mode), try text_from_root(old_root, eol_mode), eol_mode) catch {}; + if (self.buffer) |buffer| if (self.syntax) |_| if (self.file_path) |file_path| if (old_root != null and new_root != null) + project_manager.did_change(file_path, buffer.lsp_version, try text_from_root(new_root, eol_mode), try text_from_root(old_root, eol_mode), eol_mode) catch {}; if (self.enable_auto_save) tp.self_pid().send(.{ "cmd", "save_file", .{} }) catch {}; } @@ -5986,11 +5986,11 @@ pub const Editor = struct { const root = try self.buf_root(); try root.store(content.writer(std.heap.c_allocator), try self.buf_eol_mode()); - if (self.file_path) |file_path| + if (self.buffer) |buffer| if (self.file_path) |file_path| project_manager.did_open( file_path, ft, - self.lsp_version, + buffer.lsp_version, try content.toOwnedSlice(std.heap.c_allocator), if (self.buffer) |p| p.is_ephemeral() else true, ) catch |e| From 009972309cc93f63b6f13e4460874525cccde860 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 8 Aug 2025 21:02:51 +0200 Subject: [PATCH 10/11] feat: send LSP did_open messages when restoring workspace state --- src/tui/mainview.zig | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 02ae484..ce8c911 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1336,6 +1336,11 @@ fn read_restore_info(self: *Self) !void { logger.print_err("mainview", "failed to restore tabs: {}", .{e}); }; + const buffers = try self.buffer_manager.list_unordered(self.allocator); + defer self.allocator.free(buffers); + for (buffers) |buffer| if (!buffer.is_ephemeral()) + send_buffer_did_open(self.allocator, buffer) catch {}; + if (editor_file_path) |file_path| { try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }); } else { @@ -1343,6 +1348,21 @@ fn read_restore_info(self: *Self) !void { } } +fn send_buffer_did_open(allocator: std.mem.Allocator, buffer: *Buffer) !void { + const ft = try file_type_config.get(buffer.file_type_name orelse return) orelse return; + var content = std.ArrayListUnmanaged(u8).empty; + defer content.deinit(allocator); + try buffer.root.store(content.writer(allocator), buffer.file_eol_mode); + + try project_manager.did_open( + buffer.get_file_path(), + ft, + buffer.lsp_version, + try content.toOwnedSlice(allocator), + buffer.is_ephemeral(), + ); +} + fn get_next_mru_buffer(self: *Self) ?[]const u8 { const buffers = self.buffer_manager.list_most_recently_used(self.allocator) catch return null; defer self.allocator.free(buffers); From aee7c30c655a7f2dfef86f3c60c1d453910d7070 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 8 Aug 2025 21:30:42 +0200 Subject: [PATCH 11/11] feat: display indent mode in status bar --- src/tui/editor.zig | 9 +++++---- src/tui/status/filestate.zig | 17 +++++++++++------ src/tui/status/linenumstate.zig | 12 ++++++++---- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 6205e22..f9fa3de 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -330,6 +330,7 @@ pub const Editor = struct { dirty: bool = false, eol_mode: Buffer.EolMode = .lf, utf8_sanitized: bool = false, + indent_mode: IndentMode = .spaces, } = .{}, file_type: ?file_type_config = null, @@ -1647,8 +1648,8 @@ pub const Editor = struct { buf.lsp_version += 1; } - if (self.last.eol_mode != eol_mode or self.last.utf8_sanitized != utf8_sanitized) - try self.send_editor_eol_mode(eol_mode, utf8_sanitized); + if (self.last.eol_mode != eol_mode or self.last.utf8_sanitized != utf8_sanitized or self.last.indent_mode != self.indent_mode) + try self.send_editor_eol_mode(eol_mode, utf8_sanitized, self.indent_mode); if (self.last.dirty != dirty) try self.send_editor_dirty(dirty); @@ -1775,8 +1776,8 @@ pub const Editor = struct { tp.self_pid().send(.{ "cmd", "save_file", .{} }) catch {}; } - fn send_editor_eol_mode(self: *const Self, eol_mode: Buffer.EolMode, utf8_sanitized: bool) !void { - _ = try self.handlers.msg(.{ "E", "eol_mode", @intFromEnum(eol_mode), utf8_sanitized }); + fn send_editor_eol_mode(self: *const Self, eol_mode: Buffer.EolMode, utf8_sanitized: bool, indent_mode: IndentMode) !void { + _ = try self.handlers.msg(.{ "E", "eol_mode", eol_mode, utf8_sanitized, indent_mode }); } fn clamp_abs(self: *Self, abs: bool) void { diff --git a/src/tui/status/filestate.zig b/src/tui/status/filestate.zig index 9493d07..861b2b1 100644 --- a/src/tui/status/filestate.zig +++ b/src/tui/status/filestate.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const tp = @import("thespian"); const tracy = @import("tracy"); +const config = @import("config"); const Buffer = @import("Buffer"); const root = @import("root"); const project_manager = @import("project_manager"); @@ -35,6 +36,7 @@ detailed: bool = false, file: bool = false, eol_mode: Buffer.EolMode = .lf, utf8_sanitized: bool = false, +indent_mode: config.IndentMode = .spaces, const project_icon = ""; const Self = @This(); @@ -155,15 +157,19 @@ fn render_detailed(self: *Self, plane: *Plane, theme: *const Widget.Theme) void _ = plane.print("{s} ({s})", .{ self.name, project_name }) catch {}; } else { const eol_mode = switch (self.eol_mode) { - .lf => " [↩ = ␊]", - .crlf => " [↩ = ␍␊]", + .lf => "[↩ = ␊]", + .crlf => "[↩ = ␍␊]", + }; + const indent_mode = switch (self.indent_mode) { + .spaces, .auto => "[⭾ = ␠]", + .tabs => "[⭾ = ␉]", }; _ = plane.putstr(if (!self.file_exists) "󰽂" else if (self.file_dirty) "󰆓" else "󱣪") catch {}; _ = plane.print(" {s}:{d}:{d}", .{ self.name, self.line + 1, self.column + 1 }) catch {}; _ = plane.print(" of {d} lines", .{self.lines}) catch {}; if (self.file_type.len > 0) - _ = plane.print(" ({s}){s}", .{ self.file_type, eol_mode }) catch {}; + _ = plane.print(" ({s}) {s}{s}", .{ self.file_type, eol_mode, indent_mode }) catch {}; if (self.utf8_sanitized) { plane.set_style(.{ .fg = theme.editor_error.fg.? }); @@ -214,13 +220,12 @@ fn process_event(self: *Self, m: tp.message) error{Exit}!bool { var file_type: []const u8 = undefined; var file_icon: []const u8 = undefined; var file_dirty: bool = undefined; - var eol_mode: Buffer.EolModeTag = @intFromEnum(Buffer.EolMode.lf); if (try m.match(.{ tp.any, "pos", tp.extract(&self.lines), tp.extract(&self.line), tp.extract(&self.column) })) return false; if (try m.match(.{ tp.any, "dirty", tp.extract(&file_dirty) })) { self.file_dirty = file_dirty; - } else if (try m.match(.{ tp.any, "eol_mode", tp.extract(&eol_mode), tp.extract(&self.utf8_sanitized) })) { - self.eol_mode = @enumFromInt(eol_mode); + } else if (try m.match(.{ tp.any, "eol_mode", tp.extract(&self.eol_mode), tp.extract(&self.utf8_sanitized), tp.extract(&self.indent_mode) })) { + // } else if (try m.match(.{ tp.any, "save", tp.extract(&file_path) })) { @memcpy(self.name_buf[0..file_path.len], file_path); self.name = self.name_buf[0..file_path.len]; diff --git a/src/tui/status/linenumstate.zig b/src/tui/status/linenumstate.zig index b545e8e..3c29e48 100644 --- a/src/tui/status/linenumstate.zig +++ b/src/tui/status/linenumstate.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const tp = @import("thespian"); const Buffer = @import("Buffer"); +const config = @import("config"); const Plane = @import("renderer").Plane; const command = @import("command"); @@ -22,6 +23,7 @@ buf: [256]u8 = undefined, rendered: [:0]const u8 = "", eol_mode: Buffer.EolMode = .lf, utf8_sanitized: bool = false, +indent_mode: config.IndentMode = .spaces, padding: ?usize, leader: ?Leader, style: ?DigitStyle, @@ -90,7 +92,11 @@ fn format(self: *Self) void { .lf => "", .crlf => " [␍␊]", }; - std.fmt.format(writer, "{s} Ln ", .{eol_mode}) catch {}; + const indent_mode = switch (self.indent_mode) { + .spaces, .auto => "", + .tabs => " [⭾]", + }; + std.fmt.format(writer, "{s}{s} Ln ", .{ eol_mode, indent_mode }) catch {}; self.format_count(writer, self.line + 1, self.padding orelse 0) catch {}; std.fmt.format(writer, ", Col ", .{}) catch {}; self.format_count(writer, self.column + 1, self.padding orelse 0) catch {}; @@ -115,11 +121,9 @@ fn format_count(self: *Self, writer: anytype, value: usize, width: usize) !void } pub fn receive(self: *Self, _: *Button.State(Self), _: tp.pid_ref, m: tp.message) error{Exit}!bool { - var eol_mode: Buffer.EolModeTag = @intFromEnum(Buffer.EolMode.lf); if (try m.match(.{ "E", "pos", tp.extract(&self.lines), tp.extract(&self.line), tp.extract(&self.column) })) { self.format(); - } else if (try m.match(.{ "E", "eol_mode", tp.extract(&eol_mode), tp.extract(&self.utf8_sanitized) })) { - self.eol_mode = @enumFromInt(eol_mode); + } else if (try m.match(.{ "E", "eol_mode", tp.extract(&self.eol_mode), tp.extract(&self.utf8_sanitized), tp.extract(&self.indent_mode) })) { self.format(); } else if (try m.match(.{ "E", "open", tp.more })) { self.eol_mode = .lf;