From 74d033cb72074e567be04c9e08971a98127b0f34 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 3 Feb 2026 11:32:34 +0100 Subject: [PATCH 1/8] refactor: remove some spammy debug logs in Buffer cache --- src/buffer/Buffer.zig | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 86624e0..0f9a4fd 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -1508,9 +1508,6 @@ pub fn refresh_from_file(self: *Self) LoadFromFileError!void { } pub fn store_to_string_cached(self: *Self, root: *const Node, eol_mode: EolMode) [:0]const u8 { - std.log.debug("BEGIN store_to_string_cached 0x{x}", .{root.to_ref()}); - defer std.log.debug("END store_to_string_cached 0x{x}", .{root.to_ref()}); - if (get_cached_text(self.cache, root.to_ref(), eol_mode)) |text| return text; var s: std.Io.Writer.Allocating = std.Io.Writer.Allocating.initCapacity(self.external_allocator, root.weights_sum().len) catch @panic("OOM store_to_string_cached"); root.store(&s.writer, eol_mode) catch @panic("store_to_string_cached"); @@ -1519,9 +1516,6 @@ pub fn store_to_string_cached(self: *Self, root: *const Node, eol_mode: EolMode) pub fn store_last_save_to_string_cached(self: *Self, eol_mode: EolMode) ?[]const u8 { const root = self.last_save orelse return null; - std.log.debug("BEGIN store_last_save_to_string_cached 0x{x}", .{root.to_ref()}); - defer std.log.debug("END store_last_save_to_string_cached 0x{x}", .{root.to_ref()}); - if (get_cached_text(self.last_save_cache, root.to_ref(), eol_mode)) |text| return text; var s: std.Io.Writer.Allocating = std.Io.Writer.Allocating.initCapacity(self.external_allocator, root.weights_sum().len) catch @panic("OOM store_last_save_to_string_cached"); root.store(&s.writer, eol_mode) catch @panic("store_last_save_to_string_cached"); @@ -1530,20 +1524,12 @@ pub fn store_last_save_to_string_cached(self: *Self, eol_mode: EolMode) ?[]const fn get_cached_text(cache_: ?StringCache, ref: Node.Ref, eol_mode: EolMode) ?[:0]const u8 { const cache = cache_ orelse return null; - return if (cache.ref == ref and cache.eol_mode == eol_mode) blk: { - std.log.debug("fetched string cache 0x{x}", .{cache.ref}); - break :blk cache.text; - } else blk: { - std.log.debug("cache miss for 0x{x} (was 0x{x})", .{ ref, cache.ref }); - break :blk null; - }; + return if (cache.ref == ref and cache.eol_mode == eol_mode) cache.text else null; } fn store_cached_text(self: *Self, cache: *?StringCache, ref: Node.Ref, eol_mode: EolMode, text: [:0]const u8) [:0]const u8 { - if (cache.*) |*c| { - std.log.debug("0x{x} updated string cache 0x{x} -> 0x{x}", .{ @intFromPtr(self), c.ref, ref }); + if (cache.*) |*c| c.deinit(self.external_allocator); - } else std.log.debug("0x{x} stored string cache 0x{x}", .{ @intFromPtr(self), ref }); cache.* = .{ .ref = ref, .eol_mode = eol_mode, @@ -1559,7 +1545,6 @@ const StringCache = struct { code_folded: ?[:0]const u8 = null, fn deinit(self: *@This(), allocator: std.mem.Allocator) void { - std.log.debug("cleared string cache 0x{x}", .{self.ref}); allocator.free(self.text); if (self.code_folded) |text| allocator.free(text); } From b14ff8ac52356368b87f720a5f5030ff4f6de0c7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 3 Feb 2026 11:37:06 +0100 Subject: [PATCH 2/8] refactor: only count scored dropdown entries as matches --- src/tui/mode/overlay/dropdown.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tui/mode/overlay/dropdown.zig b/src/tui/mode/overlay/dropdown.zig index cd619b4..5dd88af 100644 --- a/src/tui/mode/overlay/dropdown.zig +++ b/src/tui/mode/overlay/dropdown.zig @@ -328,16 +328,18 @@ pub fn Create(options: type) type { }; var matches: std.ArrayList(Match) = .empty; + var match_count: usize = 0; for (self.entries.items) |*entry| { const match = searcher.scoreMatches(entry.label, query); + if (match.score) |_| match_count += 1; (try matches.addOne(self.allocator)).* = .{ .entry = entry, .score = match.score orelse 0, .matches = try self.allocator.dupe(usize, match.matches), }; } - if (matches.items.len == 0) return 0; + if (matches.items.len == 0) return match_count; const less_fn = struct { fn less_fn(_: void, lhs: Match, rhs: Match) bool { @@ -358,7 +360,7 @@ pub fn Create(options: type) type { if (self.items < self.view_rows) try options.add_menu_entry(self, match.entry, match.matches); } - return matches.items.len; + return match_count; } fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result { From f590d7cec8ae6466b6e2e6297f6ba9da801c6967 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 3 Feb 2026 14:41:28 +0100 Subject: [PATCH 3/8] fix: project manager should not drop empty completion responses --- src/Project.zig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Project.zig b/src/Project.zig index 36142e2..830cc9f 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -1250,7 +1250,7 @@ pub fn completion(self: *Self, from: tp.pid_ref, file_path: []const u8, row: usi pub fn receive(self_: @This(), response: tp.message) (CompletionError || cbor.Error)!void { var result: []const u8 = undefined; if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.null_ })) { - try send_content_msg_empty(self_.from.ref(), "hover", self_.file_path, self_.row, self_.col); + send_completion_done(self_.from.ref(), self_.file_path, self_.row, self_.col); } else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.array })) { if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, tp.extract_cbor(&result) })) try send_completion_items(self_.from.ref(), self_.file_path, self_.row, self_.col, result, false); @@ -1343,9 +1343,7 @@ fn send_completion_list(to: tp.pid_ref, file_path: []const u8, row: usize, col: return if (items.len > 0) send_completion_items(to, file_path, row, col, items, is_incomplete) else - to.send(.{ "cmd", "add_completion_done", .{ file_path, row, col } }) catch |e| { - std.log.err("send add_completion_done failed: {t}", .{e}); - }; + send_completion_done(to, file_path, row, col); } pub const CompletionItemError = error{ @@ -1361,6 +1359,10 @@ 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.InvalidCompletionItem; try send_completion_item(to, file_path, row, col, item, if (len > 1) true else is_incomplete); } + send_completion_done(to, file_path, row, col); +} + +fn send_completion_done(to: tp.pid_ref, file_path: []const u8, row: usize, col: usize) void { return to.send(.{ "cmd", "add_completion_done", .{ file_path, row, col } }) catch |e| { std.log.err("send add_completion_done failed: {t}", .{e}); }; From 8767dc9dc130c4a59fe7627ad29158e387e363ea Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 3 Feb 2026 14:46:12 +0100 Subject: [PATCH 4/8] fix: do not set completion refresh pending for duplicate requests --- src/tui/editor.zig | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 5447f32..bde6c58 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -440,9 +440,15 @@ pub const Editor = struct { is_complete: bool = true, const empty: @This() = .{}; - const pending: @This() = .empty; const done: ?@This() = null; + fn pending(row: usize, col: usize) @This() { + return .{ + .row = row, + .col = col, + }; + } + fn deinit(self: *@This(), allocator: std.mem.Allocator) void { self.data.deinit(allocator); self.* = .empty; @@ -6285,10 +6291,14 @@ pub const Editor = struct { pub fn completion(self: *Self, _: Context) Result { const mv = tui.mainview() orelse return; - if (self.completions_request) |_| - self.completions_refresh_pending = true - else - self.completions_request = .pending; + const cursor = self.get_primary().cursor; + if (self.completions_request) |request| { + if (request.row != cursor.row or request.col != cursor.col) + self.completions_refresh_pending = true; + return; + } else { + self.completions_request = .pending(cursor.row, cursor.col); + } if (!mv.is_any_panel_view_showing()) self.clamp_offset(mv.get_panel_height()); return self.pm_with_primary_cursor_pos(project_manager.completion); From 2296930f4d2315b0672a79ea975c9bb408bbfb01 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 3 Feb 2026 15:15:58 +0100 Subject: [PATCH 5/8] fix: re-trigger completion if we run out of suggestion matches --- src/tui/mode/overlay/completion_dropdown.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index 7c9592a..7415c9d 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -151,6 +151,9 @@ fn maybe_update_query(self: *Type, cursor: Buffer.Cursor) error{OutOfMemory}!voi if (!std.mem.eql(u8, query, last)) try update_query_text(self, cursor); } else try update_query_text(self, cursor); + + if (self.match_count == 0) + tp.self_pid().send(.{ "cmd", "completion" }) catch |e| self.logger.err(module_name, e); } fn update_query_text(self: *Type, cursor: ed.Cursor) error{OutOfMemory}!void { From d298e1ed4c9125031b26b113d0e20bca430162cd Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 3 Feb 2026 15:17:24 +0100 Subject: [PATCH 6/8] fix: don't cancel completions in update_completion This is not needed anymore. --- src/tui/mode/overlay/completion_dropdown.zig | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index 7415c9d..8d6ca28 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -375,11 +375,6 @@ const cmds = struct { const Result = command.Result; pub fn update_completion(self: *Type, _: Ctx) Result { - if (self.value.editor.completions.data.items.len == 0) { - tp.self_pid().send(.{ "cmd", "palette_menu_cancel" }) catch |e| self.logger.err(module_name, e); - return; - } - clear_entries(self); self.longest_hint = try load_entries(self); try update_query_text(self, self.value.editor.get_primary().cursor); From 5cf52171f28447c0b23763ed4121696929382d0c Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 3 Feb 2026 16:15:42 +0100 Subject: [PATCH 7/8] fix: use word-at-cursor for inserting completion if LSP does not provide a range This is not perfect in every situation, but seems to be enough to use basic completion with LSPs that do not send full insert/replace range information. closes #475, #484 --- src/tui/editor.zig | 2 +- src/tui/mode/overlay/completion_dropdown.zig | 31 +++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index bde6c58..4ebad96 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -3565,7 +3565,7 @@ pub const Editor = struct { try move_cursor_right(root, cursor, metrics); } - fn move_cursor_word_left(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { + pub fn move_cursor_word_left(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { try move_cursor_left(root, cursor, metrics); move_cursor_left_until(root, cursor, is_word_boundary_left, metrics); } diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index 8d6ca28..0a87a19 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -301,15 +301,30 @@ const Range = struct { start: Position, end: Position }; const Position = struct { line: usize, character: usize }; pub fn get_query_selection(editor: *ed.Editor, values: Values) ?Buffer.Selection { - return get_replacement_selection(editor, values.insert, values.replace); + return get_replacement_selection(editor, values.insert, values.replace, null); } -fn get_replacement_selection(editor: *ed.Editor, insert_: ?Buffer.Selection, replace_: ?Buffer.Selection) Buffer.Selection { +fn get_replacement_selection(editor: *ed.Editor, insert_: ?Buffer.Selection, replace_: ?Buffer.Selection, query: ?Buffer.Selection) Buffer.Selection { const pos = switch (tui.config().completion_insert_mode) { - .replace => replace_ orelse insert_ orelse return ed.Selection.from_cursor(&editor.get_primary().cursor), - .insert => insert_ orelse replace_ orelse return ed.Selection.from_cursor(&editor.get_primary().cursor), + .replace => replace_ orelse insert_, + .insert => insert_ orelse replace_, + }; + + var sel = if (pos) |p| + p.from_pos(editor.buf_root() catch return ed.Selection.from_cursor(&editor.get_primary().cursor), editor.metrics) + else blk: { + if (query) |sel| break :blk sel; + var cursel = editor.get_primary().*; + var sel = ed.Selection.from_cursor(&cursel.cursor); + if (cursel.cursor.col == 0) break :blk sel; + const root_ = editor.buf_root() catch break :blk sel; + ed.Editor.move_cursor_word_left(root_, &sel.begin, editor.metrics) catch break :blk sel; + if (tui.config().completion_insert_mode == .replace) { + sel.end = sel.begin; + ed.Editor.move_cursor_word_right(root_, &sel.end, editor.metrics) catch break :blk sel; + } + break :blk sel; }; - var sel = pos.from_pos(editor.buf_root() catch return ed.Selection.from_cursor(&editor.get_primary().cursor), editor.metrics); sel.normalize(); const cursor = editor.get_primary().cursor; return switch (tui.config().completion_insert_mode) { @@ -318,8 +333,8 @@ fn get_replacement_selection(editor: *ed.Editor, insert_: ?Buffer.Selection, rep }; } -fn get_insert_selection(editor: *ed.Editor, values: Values) Buffer.Selection { - return get_replacement_selection(editor, values.insert, values.replace); +fn get_insert_selection(editor: *ed.Editor, values: Values, query: ?Buffer.Selection) Buffer.Selection { + return get_replacement_selection(editor, values.insert, values.replace, query); } pub fn complete(self: *Type, _: ?*Type.ButtonType) !void { @@ -329,7 +344,7 @@ pub fn complete(self: *Type, _: ?*Type.ButtonType) !void { fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { const self = menu.*.opts.ctx; const values = get_values(button.opts.label); - const sel = get_insert_selection(self.value.editor, values); + const sel = get_insert_selection(self.value.editor, values, self.value.query); const text = if (values.insertText.len > 0) values.insertText else if (values.textEdit_newText.len > 0) From 56238c776d85027a5526f47c517f98fb01a3ed61 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 3 Feb 2026 16:20:47 +0100 Subject: [PATCH 8/8] fix: clamp cursor to buffer in indent command --- src/tui/editor.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 4ebad96..3869249 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -4208,8 +4208,13 @@ pub const Editor = struct { root = try self.indent_cursor(root, sel.end, true, allocator); if (sel_from_start) sel_.begin.col = 0; + cursel.cursor.clamp_to_buffer(root, self.metrics); return root; - } else return try self.indent_cursor(root_, cursel.cursor, self.cursels.items.len > 1, allocator); + } else { + const root = try self.indent_cursor(root_, cursel.cursor, self.cursels.items.len > 1, allocator); + cursel.cursor.clamp_to_buffer(root, self.metrics); + return root; + } } pub fn indent(self: *Self, ctx: Context) Result {