From 10e93489018bfec04f26dd71d5bff9dc3fae4d0e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 21 Dec 2025 16:42:26 +0100 Subject: [PATCH 01/22] refactor: add insert trigger storage to editor widget --- src/tui/editor.zig | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 3188c1f..e037cfb 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -258,6 +258,21 @@ pub const CurSel = struct { } }; +pub const InsertTrigger = struct { + char: u8, + command: command.ID, + + pub fn cborEncode(self: @This(), writer: *std.Io.Writer) std.io.Writer.Error!void { + try cbor.writeArrayHeader(writer, 2); + try cbor.writeValue(writer, self.char); + try cbor.writeValue(writer, self.command); + } + + pub fn cborExtract(self: *@This(), iter: *[]const u8) cbor.Error!bool { + return try cbor.matchValue(iter, .{ cbor.extract(&self.char), cbor.extract(&self.command) }); + } +}; + pub const Diagnostic = struct { source: []const u8, code: []const u8, @@ -379,6 +394,8 @@ pub const Editor = struct { syntax_last_rendered_root: ?Buffer.Root = null, syntax_incremental_reparse: bool = false, + insert_triggers: std.ArrayList(InsertTrigger) = .empty, + style_cache: ?StyleCache = null, style_cache_theme: []const u8 = "", @@ -465,7 +482,7 @@ pub const Editor = struct { } pub fn write_state(self: *const Self, writer: *std.Io.Writer) !void { - try cbor.writeArrayHeader(writer, 10); + try cbor.writeArrayHeader(writer, 11); try cbor.writeValue(writer, self.file_path orelse ""); try cbor.writeValue(writer, self.last_find_query orelse ""); try cbor.writeValue(writer, self.enable_format_on_save); @@ -473,6 +490,7 @@ pub const Editor = struct { try cbor.writeValue(writer, self.tab_width); try cbor.writeValue(writer, self.indent_mode); try cbor.writeValue(writer, self.syntax_no_render); + try cbor.writeValue(writer, self.insert_triggers); if (self.find_history) |history| { try cbor.writeArrayHeader(writer, history.items.len); for (history.items) |item| @@ -503,6 +521,7 @@ pub const Editor = struct { tp.extract(&self.tab_width), tp.extract(&self.indent_mode), tp.extract(&self.syntax_no_render), + cbor.extractAlloc(&self.insert_triggers, self.allocator), tp.extract_cbor(&find_history), tp.extract_cbor(&view_cbor), tp.extract_cbor(&cursels_cbor), @@ -575,6 +594,7 @@ pub const Editor = struct { for (self.diagnostics.items) |*d| d.deinit(self.allocator); self.diagnostics.deinit(self.allocator); self.completions.deinit(self.allocator); + self.insert_triggers.deinit(self.allocator); if (self.syntax) |syn| syn.destroy(tui.query_cache()); self.cancel_all_tabstops(); self.cursels.deinit(self.allocator); @@ -6184,6 +6204,18 @@ pub const Editor = struct { self.need_render(); } + pub fn add_insert_trigger(self: *Self, char: u8, command_: command.ID) error{OutOfMemory}!void { + (try self.insert_triggers.addOne()).* = .{ char, command_ }; + } + + pub fn remove_insert_trigger(self: *Self, char: u8, command_: command.ID) bool { + for (self.insert_triggers.items, 0..) |item, i| if (item.char == char and item.command == command_) { + _ = self.insert_triggers.orderedRemove(i); + return true; + }; + return false; + } + pub fn add_completion(self: *Self, row: usize, col: usize, is_incomplete: bool, msg: tp.message) Result { if (!(row == self.completion_row and col == self.completion_col)) { self.completions.clearRetainingCapacity(); From d7112799388e0948d8acff682b612205520020ef Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 21 Dec 2025 17:03:22 +0100 Subject: [PATCH 02/22] refactor: call insert trigger commands when a single trigger char is inserted --- src/tui/editor.zig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index e037cfb..31635aa 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -2843,6 +2843,7 @@ pub const Editor = struct { cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, self.metrics); cursor.target = cursor.col; self.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); + if (s.len == 1) self.run_insert_triggers(s[0]); return root_; } @@ -6216,6 +6217,15 @@ pub const Editor = struct { return false; } + pub fn run_insert_triggers(self: *Self, char: u8) void { + switch (char) { + '\n', '\t', ' ' => return, + else => {}, + } + for (self.insert_triggers.items) |item| if (item.char == char) + tp.self_pid().send(.{ "cmd", item.command, .{[_]u8{char}} }) catch {}; + } + pub fn add_completion(self: *Self, row: usize, col: usize, is_incomplete: bool, msg: tp.message) Result { if (!(row == self.completion_row and col == self.completion_col)) { self.completions.clearRetainingCapacity(); From 24afc12f4c0048663c48f6552d099e2ff51767a5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 21 Dec 2025 19:37:34 +0100 Subject: [PATCH 03/22] refactor: add delete triggers --- src/tui/editor.zig | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 31635aa..a55ffe6 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -258,7 +258,12 @@ pub const CurSel = struct { } }; -pub const InsertTrigger = struct { +pub const TriggerEvent = enum { + insert, + delete, +}; + +pub const TriggerSymbol = struct { char: u8, command: command.ID, @@ -394,7 +399,8 @@ pub const Editor = struct { syntax_last_rendered_root: ?Buffer.Root = null, syntax_incremental_reparse: bool = false, - insert_triggers: std.ArrayList(InsertTrigger) = .empty, + insert_triggers: std.ArrayList(TriggerSymbol) = .empty, + delete_triggers: std.ArrayList(TriggerSymbol) = .empty, style_cache: ?StyleCache = null, style_cache_theme: []const u8 = "", @@ -482,7 +488,7 @@ pub const Editor = struct { } pub fn write_state(self: *const Self, writer: *std.Io.Writer) !void { - try cbor.writeArrayHeader(writer, 11); + try cbor.writeArrayHeader(writer, 12); try cbor.writeValue(writer, self.file_path orelse ""); try cbor.writeValue(writer, self.last_find_query orelse ""); try cbor.writeValue(writer, self.enable_format_on_save); @@ -491,6 +497,7 @@ pub const Editor = struct { try cbor.writeValue(writer, self.indent_mode); try cbor.writeValue(writer, self.syntax_no_render); try cbor.writeValue(writer, self.insert_triggers); + try cbor.writeValue(writer, self.delete_triggers); if (self.find_history) |history| { try cbor.writeArrayHeader(writer, history.items.len); for (history.items) |item| @@ -522,6 +529,7 @@ pub const Editor = struct { tp.extract(&self.indent_mode), tp.extract(&self.syntax_no_render), cbor.extractAlloc(&self.insert_triggers, self.allocator), + cbor.extractAlloc(&self.delete_triggers, self.allocator), tp.extract_cbor(&find_history), tp.extract_cbor(&view_cbor), tp.extract_cbor(&cursels_cbor), @@ -595,6 +603,7 @@ pub const Editor = struct { self.diagnostics.deinit(self.allocator); self.completions.deinit(self.allocator); self.insert_triggers.deinit(self.allocator); + self.delete_triggers.deinit(self.allocator); if (self.syntax) |syn| syn.destroy(tui.query_cache()); self.cancel_all_tabstops(); self.cursels.deinit(self.allocator); @@ -6205,13 +6214,21 @@ pub const Editor = struct { self.need_render(); } - pub fn add_insert_trigger(self: *Self, char: u8, command_: command.ID) error{OutOfMemory}!void { - (try self.insert_triggers.addOne()).* = .{ char, command_ }; + pub fn add_symbol_trigger(self: *Self, char: u8, command_: command.ID, event: TriggerEvent) error{OutOfMemory}!void { + const triggers = switch (event) { + .insert => &self.insert_triggers, + .delete => &self.delete_triggers, + }; + (try triggers.addOne()).* = .{ char, command_ }; } - pub fn remove_insert_trigger(self: *Self, char: u8, command_: command.ID) bool { - for (self.insert_triggers.items, 0..) |item, i| if (item.char == char and item.command == command_) { - _ = self.insert_triggers.orderedRemove(i); + pub fn remove_symbol_trigger(self: *Self, char: u8, command_: command.ID, event: TriggerEvent) bool { + const triggers = switch (event) { + .insert => &self.insert_triggers, + .delete => &self.delete_triggers, + }; + for (triggers.items, 0..) |item, i| if (item.char == char and item.command == command_) { + _ = triggers.orderedRemove(i); return true; }; return false; From 12159edfcfec9ad190f675c7aa422f9e3124e2be Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 21 Dec 2025 20:57:46 +0100 Subject: [PATCH 04/22] refactor: add Buffer.delete_range_char function --- src/buffer/Buffer.zig | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 006e7d8..4733e33 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -669,6 +669,22 @@ const Node = union(enum) { return self.delete_bytes(sel.begin.row, pos, size, allocator, metrics) catch return error.Stop; } + pub fn delete_range_char(self: *const Node, sel: Selection, allocator: Allocator, size_: ?*usize, metrics: Metrics) error{Stop}!struct { Root, ?u8 } { + var size: usize = 0; + defer if (size_) |p| { + p.* = size; + }; + _ = self.get_range(sel, null, &size, null, metrics) catch return error.Stop; + const char = if (size == 1) blk: { + var result_buf: [6]u8 = undefined; + const result = self.get_range(sel, &result_buf, null, null, metrics) catch break :blk null; + break :blk (result orelse break :blk null)[0]; + } else null; + const pos = try self.get_line_width_to_pos(sel.begin.row, sel.begin.col, metrics); + const root = self.delete_bytes(sel.begin.row, pos, size, allocator, metrics) catch return error.Stop; + return .{ root, char }; + } + pub fn delete_bytes(self: *const Node, line: usize, pos_: usize, bytes: usize, allocator: Allocator, metrics_: Metrics) !Root { const Ctx = struct { allocator: Allocator, From 1b9bb31a594a46bfba798f9a5f4c38bb6d1a92ed Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 21 Dec 2025 21:21:56 +0100 Subject: [PATCH 05/22] refactor: run triggers on delete events --- src/tui/editor.zig | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index a55ffe6..f2a0c70 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -2272,8 +2272,9 @@ pub const Editor = struct { cursel.cursor = sel.begin; cursel.disable_selection_normal(); var size: usize = 0; - const root_ = try root.delete_range(sel, allocator, &size, self.metrics); + const root_, const trigger_char = try root.delete_range_char(sel, allocator, &size, self.metrics); self.nudge_delete(sel, cursel, size); + if (trigger_char) |char| self.run_triggers(char, .delete); return root_; } @@ -2852,7 +2853,7 @@ pub const Editor = struct { cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, self.metrics); cursor.target = cursor.col; self.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); - if (s.len == 1) self.run_insert_triggers(s[0]); + if (s.len == 1) self.run_triggers(s[0], .insert); return root_; } @@ -6214,19 +6215,19 @@ pub const Editor = struct { self.need_render(); } - pub fn add_symbol_trigger(self: *Self, char: u8, command_: command.ID, event: TriggerEvent) error{OutOfMemory}!void { - const triggers = switch (event) { + fn get_event_triggers(self: *Self, event: TriggerEvent) *std.ArrayList(TriggerSymbol) { + return switch (event) { .insert => &self.insert_triggers, .delete => &self.delete_triggers, }; - (try triggers.addOne()).* = .{ char, command_ }; + } + + pub fn add_symbol_trigger(self: *Self, char: u8, command_: command.ID, event: TriggerEvent) error{OutOfMemory}!void { + (try self.get_event_triggers(event).addOne()).* = .{ char, command_ }; } pub fn remove_symbol_trigger(self: *Self, char: u8, command_: command.ID, event: TriggerEvent) bool { - const triggers = switch (event) { - .insert => &self.insert_triggers, - .delete => &self.delete_triggers, - }; + const triggers = self.get_event_triggers(event); for (triggers.items, 0..) |item, i| if (item.char == char and item.command == command_) { _ = triggers.orderedRemove(i); return true; @@ -6234,12 +6235,12 @@ pub const Editor = struct { return false; } - pub fn run_insert_triggers(self: *Self, char: u8) void { + pub fn run_triggers(self: *Self, char: u8, event: TriggerEvent) void { switch (char) { '\n', '\t', ' ' => return, else => {}, } - for (self.insert_triggers.items) |item| if (item.char == char) + for (self.get_event_triggers(event).items) |item| if (item.char == char) tp.self_pid().send(.{ "cmd", item.command, .{[_]u8{char}} }) catch {}; } From 6c60e5a0df43e2fcce4fd5a4cb94520828d915d7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 21 Dec 2025 23:03:43 +0100 Subject: [PATCH 06/22] refactor: add '.' as a default trigger symbol for completion --- src/tui/editor.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index f2a0c70..8e2d716 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -593,6 +593,7 @@ pub const Editor = struct { .enable_terminal_cursor = tui.config().enable_terminal_cursor, .render_whitespace = tui.config().whitespace_mode, }; + self.add_default_symbol_triggers(); } fn deinit(self: *Self) void { @@ -6222,8 +6223,13 @@ pub const Editor = struct { }; } + fn add_default_symbol_triggers(self: *Self) void { + const id = command.get_id("completion") orelse return; + self.add_symbol_trigger('.', id, .insert) catch {}; + } + pub fn add_symbol_trigger(self: *Self, char: u8, command_: command.ID, event: TriggerEvent) error{OutOfMemory}!void { - (try self.get_event_triggers(event).addOne()).* = .{ char, command_ }; + (try self.get_event_triggers(event).addOne(self.allocator)).* = .{ .char = char, .command = command_ }; } pub fn remove_symbol_trigger(self: *Self, char: u8, command_: command.ID, event: TriggerEvent) bool { From f75cc9b8453e8c67fc8e497cc1871479f14e8b4a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 13:45:13 +0100 Subject: [PATCH 07/22] refactor: use command.command_names index and improve logging of commands We now use the command name index more consistently to allow for pre-allocating command IDs and better logging when commands are not found. This is a major, but hopefully non-breaking, change to command execution. --- src/command.zig | 84 ++++++++++++++++++++++------------------- src/keybind/keybind.zig | 14 +++---- src/tui/editor.zig | 6 +-- src/tui/tui.zig | 6 +-- 4 files changed, 57 insertions(+), 53 deletions(-) diff --git a/src/command.zig b/src/command.zig index 82d097b..ae41689 100644 --- a/src/command.zig +++ b/src/command.zig @@ -3,6 +3,7 @@ const tp = @import("thespian"); const log = @import("log"); const cbor = @import("cbor"); +pub var log_execute: bool = false; pub var context_check: ?*const fn () void = null; pub const ID = usize; @@ -69,15 +70,10 @@ pub fn Closure(comptime T: type) type { } pub fn register(self: *Self) !void { - if (command_names.get(self.vtbl.name)) |id| { - self.vtbl.id = id; - reAddCommand(&self.vtbl) catch |e| return log.err("cmd", "reAddCommand", e); - // log.print("cmd", "reAddCommand({s}) => {d}", .{ self.vtbl.name, self.vtbl.id }); - } else { - self.vtbl.id = try addCommand(&self.vtbl); - command_names.put(self.vtbl.name, self.vtbl.id) catch |e| return log.err("cmd", "addCommand", e); - // log.print("cmd", "addCommand({s}) => {d}", .{ self.vtbl.name, self.vtbl.id }); - } + if (command_names.get(self.vtbl.name)) |id| + reAddCommand(id, &self.vtbl) catch |e| return log.err("cmd", "reAddCommand", e) + else + addCommand(&self.vtbl); } pub fn unregister(self: *Self) void { @@ -100,21 +96,31 @@ pub var commands: CommandTable = .empty; var command_names: std.StringHashMap(ID) = std.StringHashMap(ID).init(command_table_allocator); const command_table_allocator = std.heap.c_allocator; -fn addCommand(cmd: *Vtable) !ID { - try commands.append(command_table_allocator, cmd); - return commands.items.len - 1; +fn assignCommandId(name: []const u8) ID { + commands.append(command_table_allocator, null) catch |e| std.debug.panic("assignCommandId: {t}", .{e}); + const id = commands.items.len - 1; + command_names.put(name, id) catch |e| std.debug.panic("assignCommandId: {t}", .{e}); + return id; } -fn reAddCommand(cmd: *Vtable) !void { - if (commands.items[cmd.id] != null) return error.DuplicateCommand; - commands.items[cmd.id] = cmd; +fn addCommand(cmd: *Vtable) void { + commands.append(command_table_allocator, cmd) catch |e| std.debug.panic("addCommand: {t}", .{e}); + const id = commands.items.len - 1; + cmd.id = id; + command_names.put(cmd.name, id) catch |e| std.debug.panic("assignCommandId: {t}", .{e}); +} + +fn reAddCommand(id: ID, cmd: *Vtable) !void { + cmd.id = id; + if (commands.items[id] != null) return error.DuplicateCommand; + commands.items[id] = cmd; } pub fn removeCommand(id: ID) void { commands.items[id] = null; } -pub fn execute(id: ID, ctx: Context) tp.result { +pub fn execute(id: ID, name: []const u8, ctx: Context) tp.result { if (tp.env.get().enabled(tp.channel.debug)) trace: { var iter = ctx.args.buf; var len = cbor.decodeArrayHeader(&iter) catch break :trace; @@ -140,20 +146,26 @@ pub fn execute(id: ID, ctx: Context) tp.result { } if (context_check) |check| check(); if (id >= commands.items.len) - return tp.exit_fmt("CommandNotFound: {d}", .{id}); + return notFoundError(id, name); const cmd = commands.items[id]; if (cmd) |p| { - // var buf: [tp.max_message_size]u8 = undefined; - // log.print("cmd", "execute({s}) {s}", .{ p.name, ctx.args.to_json(&buf) catch "" }) catch |e| return tp.exit_error(e, @errorReturnTrace()); + if (log_execute) { + var buf: [tp.max_message_size]u8 = undefined; + log.print("cmd", "execute({d}) {s} {s}", .{ id, p.name, if (ctx.args.buf.len > 0) ctx.args.to_json(&buf) catch "(error)" else "" }); + } return p.run(p, ctx); } else { - return tp.exit_fmt("CommandNotAvailable: {d}", .{id}); + return notFoundError(id, name); } } pub fn get_id(name: []const u8) ?ID { - var id: ?ID = null; - return get_id_cache(name, &id); + const id = get_name_id(name); + return if (commands.items[id]) |_| id else null; +} + +pub fn get_name_id(name: []const u8) ID { + return command_names.get(name) orelse assignCommandId(name); } pub fn get_name(id: ID) ?[]const u8 { @@ -164,19 +176,17 @@ pub fn get_name(id: ID) ?[]const u8 { tp.trace(tp.channel.debug, .{ "command", "get_name", "null", id }); } if (id >= commands.items.len) return null; - return (commands.items[id] orelse return null).name; + if (commands.items[id]) |cmd| return cmd.name; + var iter = command_names.iterator(); + while (iter.next()) |kv| if (kv.value_ptr.* == id) + return kv.key_ptr.*; + return null; } -pub fn get_id_cache(name: []const u8, id: *?ID) ?ID { - for (commands.items) |cmd| { - if (cmd) |p| - if (std.mem.eql(u8, p.name, name)) { - id.* = p.id; - return p.id; - }; - } - tp.trace(tp.channel.debug, .{ "command", "get_id_cache", "failed", name }); - return null; +pub fn get_id_cache(name: []const u8, cached_id: *?ID) ID { + const id = get_name_id(name); + cached_id.* = id; + return id; } pub fn get_description(id: ID) ?[]const u8 { @@ -201,14 +211,12 @@ const suppressed_errors = std.StaticStringMap(void).initComptime(.{ }); pub fn executeName(name: []const u8, ctx: Context) tp.result { - const id = get_id(name); - if (id) |id_| return execute(id_, ctx); - return notFoundError(name); + return execute(get_name_id(name), name, ctx); } -pub fn notFoundError(name: []const u8) !void { +fn notFoundError(id: ID, name: []const u8) !void { if (!suppressed_errors.has(name)) - return tp.exit_fmt("CommandNotFound: {s}", .{name}); + return tp.exit_fmt("CommandNotFound: {s}({d})", .{ name, id }); } fn CmdDef(comptime T: type) type { diff --git a/src/keybind/keybind.zig b/src/keybind/keybind.zig index 6ab0e75..31d58d9 100644 --- a/src/keybind/keybind.zig +++ b/src/keybind/keybind.zig @@ -349,19 +349,17 @@ const Command = struct { fn execute(self: *@This()) !void { const id = self.command_id orelse - command.get_id_cache(self.command, &self.command_id) orelse { - return command.notFoundError(self.command); - }; + command.get_id_cache(self.command, &self.command_id); var buf: [2048]u8 = undefined; @memcpy(buf[0..self.args.len], self.args); if (integer_argument) |int_arg| { if (cbor.match(self.args, .{}) catch false and has_integer_argument(id)) { integer_argument = null; - try command.execute(id, command.fmt(.{int_arg})); + try command.execute(id, self.command, command.fmt(.{int_arg})); return; } } - try command.execute(id, .{ .args = .{ .buf = buf[0..self.args.len] } }); + try command.execute(id, self.command, .{ .args = .{ .buf = buf[0..self.args.len] } }); } fn execute_const(self: *const @This()) void { @@ -662,11 +660,9 @@ const BindingSet = struct { if (enable_insert_events) self.send_insert_event(globals.insert_command, globals.input_buffer.items); const id = globals.insert_command_id orelse - command.get_id_cache(globals.insert_command, &globals.insert_command_id) orelse { - return tp.exit_error(error.InputTargetNotFound, null); - }; + command.get_id_cache(globals.insert_command, &globals.insert_command_id); if (!builtin.is_test) { - try command.execute(id, command.fmt(.{globals.input_buffer.items})); + try command.execute(id, globals.insert_command, command.fmt(.{globals.input_buffer.items})); } } } diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 8e2d716..2e5bac1 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -5734,7 +5734,7 @@ pub const Editor = struct { pub fn goto_next_diagnostic(self: *Self, _: Context) Result { if (self.diagnostics.items.len == 0) { if (command.get_id("goto_next_file")) |id| - return command.execute(id, .{}); + return command.execute(id, "goto_next_file", .{}); return; } self.sort_diagnostics(); @@ -5750,7 +5750,7 @@ pub const Editor = struct { pub fn goto_prev_diagnostic(self: *Self, _: Context) Result { if (self.diagnostics.items.len == 0) { if (command.get_id("goto_prev_file")) |id| - return command.execute(id, .{}); + return command.execute(id, "goto_prev_file", .{}); return; } self.sort_diagnostics(); @@ -6224,7 +6224,7 @@ pub const Editor = struct { } fn add_default_symbol_triggers(self: *Self) void { - const id = command.get_id("completion") orelse return; + const id = command.get_name_id("completion"); self.add_symbol_trigger('.', id, .insert) catch {}; } diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 9d12feb..b498101 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -416,7 +416,7 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { if (try m.match(.{ "cmd", tp.extract(&cmd) })) return command.executeName(cmd, ctx) catch |e| self.logger.err(cmd, e); if (try m.match(.{ "cmd", tp.extract(&cmd_id) })) - return command.execute(cmd_id, ctx) catch |e| self.logger.err("command", e); + return command.execute(cmd_id, command.get_name(cmd_id) orelse "(unknown)", ctx) catch |e| self.logger.err("command", e); var arg: []const u8 = undefined; @@ -426,7 +426,7 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void { } if (try m.match(.{ "cmd", tp.extract(&cmd_id), tp.extract_cbor(&arg) })) { ctx.args = .{ .buf = arg }; - return command.execute(cmd_id, ctx) catch |e| self.logger.err("command", e); + return command.execute(cmd_id, command.get_name(cmd_id) orelse "(unknown)", ctx) catch |e| self.logger.err("command", e); } if (try m.match(.{"quit"})) { project_manager.shutdown(); @@ -736,7 +736,7 @@ fn dispatch_event(ctx: *anyopaque, cbor_msg: []const u8) void { fn handle_system_clipboard(self: *Self, text: []const u8) !void { if (command.get_id("mini_mode_paste")) |id| - return command.execute(id, command.fmt(.{text})); + return command.execute(id, "mini_mode_paste", command.fmt(.{text})); { const text_ = try clipboard_system_clipboard_text(self.allocator); From 0c92cd8b8ff0d148d3ec8883de3ee7009a5b7be5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 13:48:44 +0100 Subject: [PATCH 08/22] feat: add toggle_command_logging command --- src/tui/tui.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index b498101..92265f7 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1127,6 +1127,11 @@ const cmds = struct { } pub const toggle_keybind_hints_meta: Meta = .{ .description = "Toggle keybind hints" }; + pub fn toggle_command_logging(_: *Self, _: Ctx) Result { + command.log_execute = !command.log_execute; + } + pub const toggle_command_logging_meta: Meta = .{ .description = "Toggle logging of executed commands" }; + pub fn scroll_keybind_hints(_: *Self, _: Ctx) Result { @import("keyhints.zig").scroll(); } From 478f919051d3db8c3fdba85019d9001aae4242ad Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 13:48:59 +0100 Subject: [PATCH 09/22] refactor: also log triggers when command logging is enabled --- src/tui/editor.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 2e5bac1..27b70c6 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -6246,8 +6246,11 @@ pub const Editor = struct { '\n', '\t', ' ' => return, else => {}, } - for (self.get_event_triggers(event).items) |item| if (item.char == char) + for (self.get_event_triggers(event).items) |item| if (item.char == char) { + if (command.log_execute) + self.logger.print("trigger: {t} '{c}' {?s}({d})", .{ event, char, command.get_name(item.command), item.command }); tp.self_pid().send(.{ "cmd", item.command, .{[_]u8{char}} }) catch {}; + }; } pub fn add_completion(self: *Self, row: usize, col: usize, is_incomplete: bool, msg: tp.message) Result { From d59dc65e6b0a5bea362372a80d03de30f7163b8e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 15:01:50 +0100 Subject: [PATCH 10/22] refactor: run current command queue to end before running triggers --- src/tui/editor.zig | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 27b70c6..0a22e87 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -6249,10 +6249,19 @@ pub const Editor = struct { for (self.get_event_triggers(event).items) |item| if (item.char == char) { if (command.log_execute) self.logger.print("trigger: {t} '{c}' {?s}({d})", .{ event, char, command.get_name(item.command), item.command }); - tp.self_pid().send(.{ "cmd", item.command, .{[_]u8{char}} }) catch {}; + tp.self_pid().send(.{ "cmd", "run_trigger", .{ item.command, [_]u8{char} } }) catch {}; }; } + pub fn run_trigger(_: *Self, ctx: Context) Result { + var cmd: command.ID = undefined; + var trigger_char: []const u8 = undefined; + if (!try ctx.args.match(.{ tp.extract(&cmd), tp.extract(&trigger_char) })) + return error.InvalidRunTriggerArgument; + tp.self_pid().send(.{ "cmd", cmd, .{trigger_char} }) catch {}; + } + pub const run_trigger_meta: Meta = .{ .arguments = &.{ .integer, .string } }; + pub fn add_completion(self: *Self, row: usize, col: usize, is_incomplete: bool, msg: tp.message) Result { if (!(row == self.completion_row and col == self.completion_col)) { self.completions.clearRetainingCapacity(); From c4301c40d9b855318c8425e74cba91c1d9d87fa0 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 21:45:06 +0100 Subject: [PATCH 11/22] refactor: add dropdown_keybinds configuration option --- src/config.zig | 6 ++++++ src/keybind/builtin/flow.json | 15 ++++++++++++++- src/tui/mode/overlay/dropdown.zig | 5 ++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/config.zig b/src/config.zig index e12bab1..51cbe70 100644 --- a/src/config.zig +++ b/src/config.zig @@ -71,6 +71,7 @@ centered_view_min_screen_width: usize = 145, lsp_output: enum { quiet, verbose } = .quiet, keybind_mode: KeybindMode = .normal, +dropdown_keybinds: DropdownKeybindMode = .standard, include_files: []const u8 = "", @@ -158,6 +159,11 @@ pub const KeybindMode = enum { ignore_alt_text_modifiers, }; +pub const DropdownKeybindMode = enum { + standard, + noninvasive, +}; + pub const InitialFindQuery = enum { empty, selection, diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 9c613cc..081c8df 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -375,7 +375,7 @@ ["right_control", "palette_menu_activate_quick"] ] }, - "overlay/dropdown": { + "overlay/dropdown-noninvasive": { "inherit": "normal", "press": [ ["alt+f9", "dropdown_next_widget_style"], @@ -388,6 +388,19 @@ ["tab", "palette_menu_complete"] ] }, + "overlay/dropdown": { + "inherit": "normal", + "press": [ + ["alt+f9", "dropdown_next_widget_style"], + ["ctrl+p", "palette_menu_up"], + ["ctrl+n", "palette_menu_down"], + ["escape", "palette_menu_cancel"], + ["up", "palette_menu_up"], + ["down", "palette_menu_down"], + ["enter", "palette_menu_activate"], + ["tab", "palette_menu_complete"] + ] + }, "mini/numeric": { "press": [ ["ctrl+?", "toggle_keybind_hints"], diff --git a/src/tui/mode/overlay/dropdown.zig b/src/tui/mode/overlay/dropdown.zig index 4a480db..83c1cf4 100644 --- a/src/tui/mode/overlay/dropdown.zig +++ b/src/tui/mode/overlay/dropdown.zig @@ -93,7 +93,10 @@ pub fn Create(options: type) type { .query = .empty, .view_rows = get_view_rows(tui.screen()), .entries = .empty, - .mode = try keybind.mode("overlay/dropdown", allocator, .{ + .mode = try keybind.mode(switch (tui.config().dropdown_keybinds) { + .standard => "overlay/dropdown", + .noninvasive => "overlay/dropdown-noninvasive", + }, allocator, .{ .insert_command = "overlay_insert_bytes", }), .placement = if (@hasDecl(options, "placement")) options.placement else .top_center, From d55b5b695b07bb74ed35d4136dcd03d170854b74 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 21:45:38 +0100 Subject: [PATCH 12/22] refactor: avoid crash if dropdown has no space to be displayed --- src/tui/mode/overlay/dropdown.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/mode/overlay/dropdown.zig b/src/tui/mode/overlay/dropdown.zig index 83c1cf4..e89fc3b 100644 --- a/src/tui/mode/overlay/dropdown.zig +++ b/src/tui/mode/overlay/dropdown.zig @@ -428,7 +428,7 @@ pub fn Create(options: type) type { pub fn palette_menu_down(self: *Self, _: Ctx) Result { if (self.menu.selected) |selected| { - if (selected == self.view_rows - 1 and + if (selected == self.view_rows -| 1 and self.view_pos + self.view_rows < self.total_items) { self.view_pos += 1; From 35d823d0b8d21141d34d59e33f802418946d05d7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 21:46:15 +0100 Subject: [PATCH 13/22] refactor: also cancel completion dropdown if view changes --- src/tui/mode/overlay/completion_dropdown.zig | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index 1477348..9910fb9 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -30,6 +30,7 @@ pub const Entry = struct { pub const ValueType = struct { start: ed.CurSel = .{}, cursor: ed.Cursor = .{}, + view: ed.View = .{}, replace: ?Buffer.Selection = null, }; pub const defaultValue: ValueType = .{}; @@ -88,14 +89,16 @@ pub fn handle_event(self: *Type, _: tp.pid_ref, m: tp.message) tp.result { try m.match(.{ "E", "close" })) { const editor = tui.get_active_editor() orelse return; - if (!self.value.cursor.eql(editor.get_primary().cursor)) - tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| self.logger.err(module_name, e); + if (!self.value.cursor.eql(editor.get_primary().cursor) or !self.value.view.eql(editor.view)) { + tp.self_pid().send(.{ "cmd", "palette_menu_cancel" }) catch |e| self.logger.err(module_name, e); + } } } pub fn initial_query(self: *Type, allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 { const editor = tui.get_active_editor() orelse return allocator.dupe(u8, ""); self.value.cursor = editor.get_primary().cursor; + self.value.view = editor.view; return if (self.value.replace) |replace| blk: { const sel: Buffer.Selection = .{ .begin = replace.begin, .end = self.value.start.cursor }; break :blk editor.get_selection(sel, allocator) catch break :blk allocator.dupe(u8, ""); @@ -295,6 +298,8 @@ pub fn updated(self: *Type, button_: ?*Type.ButtonType) !void { } try mv.set_info_content(" ", .append); // blank line try mv.set_info_content(values.documentation, .append); + if (mv.get_active_editor()) |editor| + self.value.view = editor.view; } pub fn cancel(_: *Type) !void { From a5d7e76897e2c4d309e86e5069643fe665474596 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 22:03:56 +0100 Subject: [PATCH 14/22] refactor: remove modal background from dropdown --- src/tui/mode/overlay/dropdown.zig | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/tui/mode/overlay/dropdown.zig b/src/tui/mode/overlay/dropdown.zig index e89fc3b..a825e08 100644 --- a/src/tui/mode/overlay/dropdown.zig +++ b/src/tui/mode/overlay/dropdown.zig @@ -29,7 +29,6 @@ pub const Placement = enum { pub fn Create(options: type) type { return struct { allocator: std.mem.Allocator, - modal: *ModalBackground.State(*Self), menu: *Menu.State(*Self), mode: keybind.Mode, query: std.ArrayList(u8), @@ -68,17 +67,6 @@ pub fn Create(options: type) type { errdefer allocator.destroy(self); self.* = .{ .allocator = allocator, - .modal = try ModalBackground.create(*Self, allocator, tui.mainview_widget(), .{ - .ctx = self, - .on_click = mouse_palette_menu_cancel, - .on_render = if (@hasDecl(options, "modal_dim")) - if (options.modal_dim) - ModalBackground.Options(*Self).on_render_dim - else - ModalBackground.Options(*Self).on_render_default - else - ModalBackground.Options(*Self).on_render_dim, - }), .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, .style = widget_type, @@ -117,7 +105,6 @@ pub fn Create(options: type) type { try self.query.appendSlice(self.allocator, initial_query); } try self.start_query(0); - try mv.floating_views.add(self.modal.widget()); try mv.floating_views.add(self.menu.container_widget); if (@hasDecl(options, "handle_event")) blk: { @@ -136,10 +123,8 @@ pub fn Create(options: type) type { if (@hasDecl(options, "deinit")) options.deinit(self); self.entries.deinit(self.allocator); - if (tui.mainview()) |mv| { + if (tui.mainview()) |mv| mv.floating_views.remove(self.menu.container_widget); - mv.floating_views.remove(self.modal.widget()); - } self.logger.deinit(); self.allocator.destroy(self); } @@ -374,7 +359,7 @@ pub fn Create(options: type) type { try self.query.appendSlice(self.allocator, buf[0..bytes]); if (@hasDecl(options, "update_query")) options.update_query(self, self.query.items); - std.log.debug("insert_code_point: '{s}'", .{self.query.items}); + // std.log.debug("insert_code_point: '{s}'", .{self.query.items}); return self.start_query(0); } @@ -382,7 +367,7 @@ pub fn Create(options: type) type { try self.query.appendSlice(self.allocator, bytes); if (@hasDecl(options, "update_query")) options.update_query(self, self.query.items); - std.log.debug("insert_bytes: '{s}'", .{self.query.items}); + // std.log.debug("insert_bytes: '{s}'", .{self.query.items}); return self.start_query(0); } From 7d2e6a3b459f3cc2f26b901e01d8fc8a4c38ec37 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 22:07:40 +0100 Subject: [PATCH 15/22] refactor: don't indirectly call cancel on completion dropdown select --- src/tui/mode/overlay/completion_dropdown.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index 9910fb9..0884cfa 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -278,6 +278,8 @@ fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { 2 => editor.insert_snippet(text) catch |e| self.logger.err(module_name, e), else => editor.insert_cursels(text) catch |e| self.logger.err(module_name, e), } + self.value.cursor = editor.get_primary().cursor; + self.value.view = editor.view; const mv = tui.mainview() orelse return; mv.cancel_info_content() catch {}; tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| self.logger.err(module_name, e); From ed027b5f6d0052849192f94e5c38ddbcacc6cd29 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 22:36:16 +0100 Subject: [PATCH 16/22] refactor: store trigger commands as strings Because command IDs are not stable across restarts. --- src/tui/editor.zig | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 0a22e87..8a88886 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -270,11 +270,16 @@ pub const TriggerSymbol = struct { pub fn cborEncode(self: @This(), writer: *std.Io.Writer) std.io.Writer.Error!void { try cbor.writeArrayHeader(writer, 2); try cbor.writeValue(writer, self.char); - try cbor.writeValue(writer, self.command); + try cbor.writeValue(writer, command.get_name(self.command)); } pub fn cborExtract(self: *@This(), iter: *[]const u8) cbor.Error!bool { - return try cbor.matchValue(iter, .{ cbor.extract(&self.char), cbor.extract(&self.command) }); + var command_name: []const u8 = undefined; + if (try cbor.matchValue(iter, .{ cbor.extract(&self.char), cbor.extract(&command_name) })) { + self.command = command.get_id(command_name) orelse command.ID_unknown; + return true; + } + return false; } }; @@ -496,8 +501,8 @@ pub const Editor = struct { try cbor.writeValue(writer, self.tab_width); try cbor.writeValue(writer, self.indent_mode); try cbor.writeValue(writer, self.syntax_no_render); - try cbor.writeValue(writer, self.insert_triggers); - try cbor.writeValue(writer, self.delete_triggers); + try cbor.writeValue(writer, self.insert_triggers.items); + try cbor.writeValue(writer, self.delete_triggers.items); if (self.find_history) |history| { try cbor.writeArrayHeader(writer, history.items.len); for (history.items) |item| @@ -519,6 +524,8 @@ pub const Editor = struct { var view_cbor: []const u8 = undefined; var cursels_cbor: []const u8 = undefined; var last_find_query: []const u8 = undefined; + var insert_triggers: []TriggerSymbol = undefined; + var delete_triggers: []TriggerSymbol = undefined; var find_history: []const u8 = undefined; if (!try cbor.matchValue(iter, .{ tp.extract(&file_path), @@ -528,13 +535,17 @@ pub const Editor = struct { tp.extract(&self.tab_width), tp.extract(&self.indent_mode), tp.extract(&self.syntax_no_render), - cbor.extractAlloc(&self.insert_triggers, self.allocator), - cbor.extractAlloc(&self.delete_triggers, self.allocator), + cbor.extractAlloc(&insert_triggers, self.allocator), + cbor.extractAlloc(&delete_triggers, self.allocator), tp.extract_cbor(&find_history), tp.extract_cbor(&view_cbor), tp.extract_cbor(&cursels_cbor), })) return error.RestoreStateMatch; + self.insert_triggers.deinit(self.allocator); + self.insert_triggers = .fromOwnedSlice(insert_triggers); + self.delete_triggers.deinit(self.allocator); + self.delete_triggers = .fromOwnedSlice(delete_triggers); self.refresh_tab_width(); if (op == .open_file) try self.open(file_path); From 96676c738a41e7a96f04b42c185a9f2a10be74f6 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 22:36:56 +0100 Subject: [PATCH 17/22] refactor: prevent duplicate triggers --- src/tui/editor.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 8a88886..5cfefd8 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -6240,6 +6240,7 @@ pub const Editor = struct { } pub fn add_symbol_trigger(self: *Self, char: u8, command_: command.ID, event: TriggerEvent) error{OutOfMemory}!void { + for (self.get_event_triggers(event).items) |item| if (item.char == char and item.command == command_) return; (try self.get_event_triggers(event).addOne(self.allocator)).* = .{ .char = char, .command = command_ }; } From 6fce29f8761a2b4187a114285bee4fece895f7f9 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 22:37:08 +0100 Subject: [PATCH 18/22] refactor: ignore late palette_menu_cancel calls --- src/command.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/command.zig b/src/command.zig index ae41689..6991794 100644 --- a/src/command.zig +++ b/src/command.zig @@ -208,6 +208,7 @@ const suppressed_errors = std.StaticStringMap(void).initComptime(.{ .{ "enable_fast_scroll", void }, .{ "disable_fast_scroll", void }, .{ "clear_diagnostics", void }, + .{ "palette_menu_cancel", void }, }); pub fn executeName(name: []const u8, ctx: Context) tp.result { From b2996cddbd314288f13aba5873b6be61d14bcb44 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 22:53:12 +0100 Subject: [PATCH 19/22] refactor: also run triggers during completion --- src/tui/mode/overlay/completion_dropdown.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index 0884cfa..c09f92d 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -117,6 +117,10 @@ pub fn update_query(self: *Type, query: []const u8) void { editor.update_buf(root_) catch {}; editor.clamp(); editor.need_render(); + if (query.len > 0) { + const last_char = query[query.len - 1]; + editor.run_triggers(last_char, .insert); + } return; } From 911fc3160a8f35db405b155b8cf207c45338cd4d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 23 Dec 2025 23:53:14 +0100 Subject: [PATCH 20/22] refactor: add support for backspace during completion --- src/keybind/builtin/flow.json | 6 ++++-- src/tui/mode/overlay/completion_dropdown.zig | 5 ++++- src/tui/mode/overlay/dropdown.zig | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 081c8df..41af849 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -385,7 +385,8 @@ ["ctrl+up", "palette_menu_up"], ["ctrl+down", "palette_menu_down"], ["ctrl+enter", "palette_menu_activate"], - ["tab", "palette_menu_complete"] + ["tab", "palette_menu_complete"], + ["backspace", "overlay_delete_backwards"] ] }, "overlay/dropdown": { @@ -398,7 +399,8 @@ ["up", "palette_menu_up"], ["down", "palette_menu_down"], ["enter", "palette_menu_activate"], - ["tab", "palette_menu_complete"] + ["tab", "palette_menu_complete"], + ["backspace", "overlay_delete_backwards"] ] }, "mini/numeric": { diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index c09f92d..779559b 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -110,7 +110,10 @@ pub fn update_query(self: *Type, query: []const u8) void { const primary = editor.get_primary(); primary.selection = get_insert_selection(self, editor.get_primary().cursor); const b = editor.buf_for_update() catch return; - const root_ = editor.insert(b.root, primary, query, b.allocator) catch return; + const root_ = if (query.len > 0) + editor.insert(b.root, primary, query, b.allocator) catch return + else + editor.delete_selection(b.root, primary, b.allocator) catch return; self.value.cursor = editor.get_primary().cursor; if (self.value.replace) |*sel| sel.* = .{ .begin = sel.begin, .end = self.value.cursor }; primary.selection = null; diff --git a/src/tui/mode/overlay/dropdown.zig b/src/tui/mode/overlay/dropdown.zig index a825e08..a629c84 100644 --- a/src/tui/mode/overlay/dropdown.zig +++ b/src/tui/mode/overlay/dropdown.zig @@ -353,6 +353,15 @@ pub fn Create(options: type) type { return matches.items.len; } + fn delete_code_point(self: *Self) !void { + if (self.query.items.len > 0) { + self.query.shrinkRetainingCapacity(self.query.items.len - tui.egc_last(self.query.items).len); + if (@hasDecl(options, "update_query")) + options.update_query(self, self.query.items); + } + try self.start_query(0); + } + fn insert_code_point(self: *Self, c: u32) !void { var buf: [6]u8 = undefined; const bytes = try input.ucs32_to_utf8(&[_]u32{c}, &buf); @@ -523,6 +532,11 @@ pub fn Create(options: type) type { } pub const palette_menu_cancel_meta: Meta = .{}; + pub fn overlay_delete_backwards(self: *Self, _: Ctx) Result { + self.delete_code_point() catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + pub const overlay_delete_backwards_meta: Meta = .{ .description = "Delete backwards" }; + pub fn overlay_insert_code_point(self: *Self, ctx: Ctx) Result { var egc: u32 = 0; if (!try ctx.args.match(.{tp.extract(&egc)})) From 4f39bbbd41355b25bc8bfef36186c97d818f9ee4 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 24 Dec 2025 00:02:41 +0100 Subject: [PATCH 21/22] refactor: add support for delete word during completion --- src/keybind/builtin/flow.json | 2 ++ src/tui/mode/overlay/dropdown.zig | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 41af849..ed65972 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -386,6 +386,7 @@ ["ctrl+down", "palette_menu_down"], ["ctrl+enter", "palette_menu_activate"], ["tab", "palette_menu_complete"], + ["ctrl+backspace", "overlay_delete_word_left"], ["backspace", "overlay_delete_backwards"] ] }, @@ -400,6 +401,7 @@ ["down", "palette_menu_down"], ["enter", "palette_menu_activate"], ["tab", "palette_menu_complete"], + ["ctrl+backspace", "overlay_delete_word_left"], ["backspace", "overlay_delete_backwards"] ] }, diff --git a/src/tui/mode/overlay/dropdown.zig b/src/tui/mode/overlay/dropdown.zig index a629c84..1a54f85 100644 --- a/src/tui/mode/overlay/dropdown.zig +++ b/src/tui/mode/overlay/dropdown.zig @@ -353,6 +353,17 @@ pub fn Create(options: type) type { return matches.items.len; } + fn delete_word(self: *Self) !void { + if (std.mem.lastIndexOfAny(u8, self.query.items, "/\\. -_")) |pos| { + self.query.shrinkRetainingCapacity(pos); + } else { + self.query.shrinkRetainingCapacity(0); + } + if (@hasDecl(options, "update_query")) + options.update_query(self, self.query.items); + return self.start_query(0); + } + fn delete_code_point(self: *Self) !void { if (self.query.items.len > 0) { self.query.shrinkRetainingCapacity(self.query.items.len - tui.egc_last(self.query.items).len); @@ -532,6 +543,11 @@ pub fn Create(options: type) type { } pub const palette_menu_cancel_meta: Meta = .{}; + pub fn overlay_delete_word_left(self: *Self, _: Ctx) Result { + self.delete_word() catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + pub const overlay_delete_word_left_meta: Meta = .{ .description = "Delete word to the left" }; + pub fn overlay_delete_backwards(self: *Self, _: Ctx) Result { self.delete_code_point() catch |e| return tp.exit_error(e, @errorReturnTrace()); } From b5d137666fc791de946b4e3058316a678904c9b3 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 24 Dec 2025 00:10:56 +0100 Subject: [PATCH 22/22] refactor: run triggers only on primary cursel --- src/tui/editor.zig | 7 ++++--- src/tui/mode/overlay/completion_dropdown.zig | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 5cfefd8..945ec19 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -2286,7 +2286,7 @@ pub const Editor = struct { var size: usize = 0; const root_, const trigger_char = try root.delete_range_char(sel, allocator, &size, self.metrics); self.nudge_delete(sel, cursel, size); - if (trigger_char) |char| self.run_triggers(char, .delete); + if (trigger_char) |char| self.run_triggers(cursel, char, .delete); return root_; } @@ -2865,7 +2865,7 @@ pub const Editor = struct { cursor.row, cursor.col, root_ = try root_.insert_chars(cursor.row, cursor.col, s, allocator, self.metrics); cursor.target = cursor.col; self.nudge_insert(.{ .begin = begin, .end = cursor.* }, cursel, s.len); - if (s.len == 1) self.run_triggers(s[0], .insert); + if (s.len == 1) self.run_triggers(cursel, s[0], .insert); return root_; } @@ -6253,11 +6253,12 @@ pub const Editor = struct { return false; } - pub fn run_triggers(self: *Self, char: u8, event: TriggerEvent) void { + pub fn run_triggers(self: *Self, cursel: *const CurSel, char: u8, event: TriggerEvent) void { switch (char) { '\n', '\t', ' ' => return, else => {}, } + if (!cursel.cursor.eql(self.get_primary().cursor)) return; for (self.get_event_triggers(event).items) |item| if (item.char == char) { if (command.log_execute) self.logger.print("trigger: {t} '{c}' {?s}({d})", .{ event, char, command.get_name(item.command), item.command }); diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index 779559b..ab6e8c8 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -122,7 +122,7 @@ pub fn update_query(self: *Type, query: []const u8) void { editor.need_render(); if (query.len > 0) { const last_char = query[query.len - 1]; - editor.run_triggers(last_char, .insert); + editor.run_triggers(primary, last_char, .insert); } return; }