From 80fc3b7bc5cf3ee1c8f50676cc6d9060a373085b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 1 Oct 2025 20:20:08 +0200 Subject: [PATCH 1/8] refactor: explicitly pass theme to tui.set_terminal_style --- src/tui/tui.zig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index e0d934f..4336983 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -189,7 +189,7 @@ fn init(allocator: Allocator) InitError!*Self { } self.mainview_ = try MainView.create(allocator); resize(); - self.set_terminal_style(); + self.set_terminal_style(self.current_theme()); try save_config(); try self.init_input_namespace(); if (tp.env.get().is("restore-session")) { @@ -773,7 +773,7 @@ fn set_theme_by_name(self: *Self, name: []const u8, action: enum { none, store } self.light_parsed_theme = parsed_theme; }, } - self.set_terminal_style(); + self.set_terminal_style(&theme_); self.logger.print("theme: {s}", .{theme_.description}); switch (action) { .none => {}, @@ -1548,12 +1548,12 @@ pub const fallbacks: []const FallBack = &[_]FallBack{ .{ .ts = "text.title", .tm = "entity.name.section" }, }; -fn set_terminal_style(self: *Self) void { +fn set_terminal_style(self: *Self, theme_: *const Widget.Theme) void { if (build_options.gui or self.config_.enable_terminal_color_scheme) { - self.rdr_.set_terminal_style(self.current_theme().editor); - self.rdr_.set_terminal_cursor_color(self.current_theme().editor_cursor.bg.?); + self.rdr_.set_terminal_style(theme_.editor); + self.rdr_.set_terminal_cursor_color(theme_.editor_cursor.bg.?); if (self.rdr_.vx.caps.multi_cursor) - self.rdr_.set_terminal_secondary_cursor_color(self.current_theme().editor_cursor_secondary.bg orelse self.current_theme().editor_cursor.bg.?); + self.rdr_.set_terminal_secondary_cursor_color(theme_.editor_cursor_secondary.bg orelse theme_.editor_cursor.bg.?); } } From 2783120aefe2a6d30c3812ee1a125387a81b2e1e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 1 Oct 2025 20:20:34 +0200 Subject: [PATCH 2/8] fix: update terminal style when switching color scheme --- src/tui/tui.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 4336983..c584058 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -790,12 +790,14 @@ fn set_theme_by_name(self: *Self, name: []const u8, action: enum { none, store } fn force_color_scheme(self: *Self, color_scheme: @TypeOf(self.color_scheme)) void { self.color_scheme = color_scheme; self.color_scheme_locked = true; + self.set_terminal_style(self.current_theme()); self.logger.print("color scheme: {s} ({s})", .{ @tagName(self.color_scheme), self.current_theme().name }); } fn set_color_scheme(self: *Self, color_scheme: @TypeOf(self.color_scheme)) void { if (self.color_scheme_locked) return; self.color_scheme = color_scheme; + self.set_terminal_style(self.current_theme()); self.logger.print("color scheme: {s} ({s})", .{ @tagName(self.color_scheme), self.current_theme().name }); } From 3901d0cce5cc2316b99a0deea95704b56ba2b02e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 1 Oct 2025 20:50:32 +0200 Subject: [PATCH 3/8] feat: add support for state values in palettes --- src/tui/mode/overlay/palette.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index d6ad651..df64f59 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -40,8 +40,11 @@ pub fn Create(options: type) type { view_pos: usize = 0, total_items: usize = 0, + value: ValueType = if (@hasDecl(options, "defaultValue")) options.defaultValue else {}, + const Entry = options.Entry; const Self = @This(); + const ValueType = if (@hasDecl(options, "ValueType")) options.ValueType else void; pub const MenuState = Menu.State(*Self); pub const ButtonState = Button.State(*Menu.State(*Self)); From 03c82999b898012c12172cb0ffdd479bcce21b09 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 1 Oct 2025 21:01:26 +0200 Subject: [PATCH 4/8] feat: select replacement range during completion --- src/tui/mode/overlay/completion_palette.zig | 38 ++++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index ad22951..c335a69 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -3,6 +3,7 @@ const cbor = @import("cbor"); const tp = @import("thespian"); const root = @import("root"); const command = @import("command"); +const Buffer = @import("Buffer"); const tui = @import("../../tui.zig"); pub const Type = @import("palette.zig").Create(@This()); @@ -20,8 +21,14 @@ pub const Entry = struct { cbor: []const u8, }; +pub const ValueType = struct { + start: ?Buffer.Selection = null, +}; +pub const defaultValue: ValueType = .{}; + pub fn load_entries(palette: *Type) !usize { const editor = tui.get_active_editor() orelse return error.NotFound; + palette.value.start = editor.get_primary().selection; var iter: []const u8 = editor.completions.items; while (iter.len > 0) { var cbor_item: []const u8 = undefined; @@ -31,7 +38,7 @@ pub fn load_entries(palette: *Type) !usize { var max_label_len: usize = 0; for (palette.entries.items) |*item| { - const label_, const sort_text, _ = get_values(item.cbor); + const label_, const sort_text, _, _ = get_values(item.cbor); item.label = label_; item.sort_text = sort_text; max_label_len = @max(max_label_len, item.label.len); @@ -71,7 +78,7 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget. if (!(cbor.matchValue(&iter, cbor.extract_cbor(&item_cbor)) catch false)) return false; if (!(cbor.matchValue(&iter, cbor.extract_cbor(&matches_cbor)) catch false)) return false; - const label_, _, const kind = get_values(item_cbor); + const label_, _, const kind, _ = get_values(item_cbor); const icon_: []const u8 = kind_icon(@enumFromInt(kind)); const color: u24 = 0x0; const indicator: []const u8 = &.{}; @@ -79,10 +86,11 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget. return tui.render_file_item(&button.plane, label_, icon_, color, indicator, matches_cbor, button.active, selected, button.hover, theme); } -fn get_values(item_cbor: []const u8) struct { []const u8, []const u8, u8 } { +fn get_values(item_cbor: []const u8) struct { []const u8, []const u8, u8, Buffer.Selection } { var label_: []const u8 = ""; var sort_text: []const u8 = ""; var kind: u8 = 0; + var replace: Buffer.Selection = .{}; _ = cbor.match(item_cbor, .{ cbor.any, // file_path cbor.any, // row @@ -102,20 +110,32 @@ fn get_values(item_cbor: []const u8) struct { []const u8, []const u8, u8 } { cbor.any, // insert.begin.col cbor.any, // insert.end.row cbor.any, // insert.end.col - cbor.any, // replace.begin.row - cbor.any, // replace.begin.col - cbor.any, // replace.end.row - cbor.any, // replace.end.col + cbor.extract(&replace.begin.row), // replace.begin.row + cbor.extract(&replace.begin.col), // replace.begin.col + cbor.extract(&replace.end.row), // replace.end.row + cbor.extract(&replace.end.col), // replace.end.col }) catch false; - return .{ label_, sort_text, kind }; + return .{ label_, sort_text, kind, replace }; } fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { - const label_, _, _ = get_values(button.opts.label); + const label_, _, _, _ = get_values(button.opts.label); tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); tp.self_pid().send(.{ "cmd", "insert_chars", .{label_} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); } +pub fn updated(palette: *Type, button_: ?*Type.ButtonState) !void { + const button = button_ orelse return cancel(palette); + _, _, _, const replace = get_values(button.opts.label); + const editor = tui.get_active_editor() orelse return error.NotFound; + editor.get_primary().selection = replace; +} + +pub fn cancel(palette: *Type) !void { + const editor = tui.get_active_editor() orelse return; + editor.get_primary().selection = palette.value.start; +} + const CompletionItemKind = enum(u8) { None = 0, Text = 1, From 42b7ae46a0b620f4df95220522bee1854fa8de53 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 1 Oct 2025 21:29:05 +0200 Subject: [PATCH 5/8] fix: open competion palette after all completions are received --- src/Project.zig | 1 + src/tui/mainview.zig | 29 ++++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index 982cea6..2aaede9 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -1012,6 +1012,7 @@ fn send_completion_items(to: tp.pid_ref, file_path: []const u8, row: usize, col: if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&item)))) return error.InvalidMessageField; try send_completion_item(to, file_path, row, col, item, if (len > 1) true else is_incomplete); } + return to.send(.{ "cmd", "add_completion_done", .{ file_path, row, col } }) catch error.ClientFailed; } fn invalid_field(field: []const u8) error{InvalidMessage} { diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 0535ce4..6cce2b3 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -838,11 +838,8 @@ const cmds = struct { tp.more, })) return error.InvalidAddDiagnosticArgument; file_path = project_manager.normalize_file_path(file_path); - if (self.get_active_editor()) |editor| { - if (std.mem.eql(u8, file_path, editor.file_path orelse "")) - try editor.add_completion(row, col, is_incomplete, ctx.args); - try tui.open_overlay(@import("mode/overlay/completion_palette.zig").Type); - } + if (self.get_active_editor()) |editor| if (std.mem.eql(u8, file_path, editor.file_path orelse "")) + try editor.add_completion(row, col, is_incomplete, ctx.args); } pub const add_completion_meta: Meta = .{ .arguments = &.{ @@ -871,6 +868,28 @@ const cmds = struct { }, }; + pub fn add_completion_done(self: *Self, ctx: Ctx) Result { + var file_path: []const u8 = undefined; + var row: usize = undefined; + var col: usize = undefined; + + if (!try ctx.args.match(.{ + tp.extract(&file_path), + tp.extract(&row), + tp.extract(&col), + })) return error.InvalidAddDiagnosticArgument; + file_path = project_manager.normalize_file_path(file_path); + if (self.get_active_editor()) |editor| if (std.mem.eql(u8, file_path, editor.file_path orelse "")) + try tui.open_overlay(@import("mode/overlay/completion_palette.zig").Type); + } + pub const add_completion_done_meta: Meta = .{ + .arguments = &.{ + .string, // file_path + .integer, // row + .integer, // col + }, + }; + pub fn rename_symbol_item(self: *Self, ctx: Ctx) Result { const editor = self.get_active_editor() orelse return; // because the incoming message is an array of Renames, we manuallly From 74b011cf7e4b82ef388e189a3f69c4880be4a825 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 1 Oct 2025 21:54:46 +0200 Subject: [PATCH 6/8] feat: add support for setting the initial query value in palettes --- src/tui/mode/overlay/palette.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index df64f59..28b2b5b 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -91,6 +91,12 @@ pub fn Create(options: type) type { if (@hasDecl(options, "restore_state")) options.restore_state(self) catch {}; try self.commands.init(self); + if (@hasDecl(options, "initial_query")) blk: { + const initial_query = options.initial_query(self, self.allocator) catch break :blk; + defer self.allocator.free(initial_query); + try self.inputbox.text.appendSlice(self.allocator, initial_query); + self.inputbox.cursor = tui.egc_chunk_width(self.inputbox.text.items, 0, 8); + } try self.start_query(0); try mv.floating_views.add(self.modal.widget()); try mv.floating_views.add(self.menu.container_widget); From b02f096fef576f12c4be59d56a7fb7a0a7d88016 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 1 Oct 2025 21:55:12 +0200 Subject: [PATCH 7/8] feat: set the initial completion query based on the cursor position --- src/tui/mode/overlay/completion_palette.zig | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index c335a69..095b5be 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -7,6 +7,7 @@ const Buffer = @import("Buffer"); const tui = @import("../../tui.zig"); pub const Type = @import("palette.zig").Create(@This()); +const ed = @import("../../editor.zig"); const module_name = @typeName(@This()); const Widget = @import("../../Widget.zig"); @@ -22,13 +23,14 @@ pub const Entry = struct { }; pub const ValueType = struct { - start: ?Buffer.Selection = null, + start: ed.CurSel = .{}, + replace: ?Buffer.Selection = null, }; pub const defaultValue: ValueType = .{}; pub fn load_entries(palette: *Type) !usize { const editor = tui.get_active_editor() orelse return error.NotFound; - palette.value.start = editor.get_primary().selection; + palette.value.start = editor.get_primary().*; var iter: []const u8 = editor.completions.items; while (iter.len > 0) { var cbor_item: []const u8 = undefined; @@ -38,7 +40,9 @@ pub fn load_entries(palette: *Type) !usize { var max_label_len: usize = 0; for (palette.entries.items) |*item| { - const label_, const sort_text, _, _ = get_values(item.cbor); + const label_, const sort_text, _, const replace = get_values(item.cbor); + if (palette.value.replace == null) + palette.value.replace = replace; item.label = label_; item.sort_text = sort_text; max_label_len = @max(max_label_len, item.label.len); @@ -56,6 +60,14 @@ pub fn load_entries(palette: *Type) !usize { return if (max_label_len > label.len + 3) 0 else label.len + 3 - max_label_len; } +pub fn initial_query(palette: *Type, allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 { + return if (palette.value.replace) |replace| blk: { + const editor = tui.get_active_editor() orelse break :blk allocator.dupe(u8, ""); + const sel: Buffer.Selection = .{ .begin = replace.begin, .end = palette.value.start.cursor }; + break :blk editor.get_selection(sel, allocator) catch break :blk allocator.dupe(u8, ""); + } else allocator.dupe(u8, ""); +} + pub fn clear_entries(palette: *Type) void { palette.entries.clearRetainingCapacity(); } @@ -133,7 +145,7 @@ pub fn updated(palette: *Type, button_: ?*Type.ButtonState) !void { pub fn cancel(palette: *Type) !void { const editor = tui.get_active_editor() orelse return; - editor.get_primary().selection = palette.value.start; + editor.get_primary().selection = palette.value.start.selection; } const CompletionItemKind = enum(u8) { From b1e5b2f80ffe65ccadd68b7d8fd1bbc36624eb0c Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 1 Oct 2025 22:02:21 +0200 Subject: [PATCH 8/8] fix: never set completion selection to an empty range --- src/tui/mode/overlay/completion_palette.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index 095b5be..ce7afde 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -140,7 +140,7 @@ pub fn updated(palette: *Type, button_: ?*Type.ButtonState) !void { const button = button_ orelse return cancel(palette); _, _, _, const replace = get_values(button.opts.label); const editor = tui.get_active_editor() orelse return error.NotFound; - editor.get_primary().selection = replace; + editor.get_primary().selection = if (replace.empty()) null else replace; } pub fn cancel(palette: *Type) !void {