From 9dd12ad7dcb75e9e719d3fcea03cffa5f5c67959 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 12:40:08 +0100 Subject: [PATCH 01/15] fix: broken tests --- src/keybind/keybind.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/keybind/keybind.zig b/src/keybind/keybind.zig index 79e5094..ea7b648 100644 --- a/src/keybind/keybind.zig +++ b/src/keybind/keybind.zig @@ -985,7 +985,7 @@ test "match" { } test "json" { - var bindings: BindingSet = .{ .name = "test", .selection_style = .normal }; + var bindings: BindingSet = .{ .name = "test", .config_section = "test_section", .selection_style = .normal }; _ = try bindings.process_key_event(input.KeyEvent.from_key('j')); _ = try bindings.process_key_event(input.KeyEvent.from_key('k')); _ = try bindings.process_key_event(input.KeyEvent.from_key('g')); From 43949405940a6a8436c5b5411433f1286c931fd2 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 12:52:23 +0100 Subject: [PATCH 02/15] refactor: migrate keybind module to std.log --- build.zig | 1 - src/keybind/keybind.zig | 47 +++++++++++------------------------------ 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/build.zig b/build.zig index 37af278..60af927 100644 --- a/build.zig +++ b/build.zig @@ -502,7 +502,6 @@ pub fn build_exe( .{ .name = "EventHandler", .module = EventHandler_mod }, .{ .name = "input", .module = input_mod }, .{ .name = "thespian", .module = thespian_mod }, - .{ .name = "log", .module = log_mod }, .{ .name = "Buffer", .module = Buffer_mod }, .{ .name = "config", .module = config_mod }, }, diff --git a/src/keybind/keybind.zig b/src/keybind/keybind.zig index ea7b648..c6e4e75 100644 --- a/src/keybind/keybind.zig +++ b/src/keybind/keybind.zig @@ -6,7 +6,6 @@ const std = @import("std"); const tp = @import("thespian"); const cbor = @import("cbor"); const builtin = @import("builtin"); -const log = @import("log"); const root = @import("soft_root").root; const input = @import("input"); @@ -245,11 +244,9 @@ pub fn set_namespace(namespace_name: []const u8) LoadError!void { fn get_mode_binding_set(mode_name: []const u8, insert_command: []const u8) LoadError!*const BindingSet { const namespace = current_namespace(); var binding_set = namespace.get_mode(mode_name) orelse { - const logger = log.logger("keybind"); - logger.print_err("get_namespace_mode", "ERROR: mode not found: {s}", .{mode_name}); + std.log.err("ERROR: mode not found: {s}", .{mode_name}); var iter = namespace.modes.iterator(); - while (iter.next()) |entry| logger.print("available modes: {s}", .{entry.key_ptr.*}); - logger.deinit(); + while (iter.next()) |entry| std.log.info("available modes: {s}", .{entry.key_ptr.*}); return error.NotFound; }; binding_set.set_insert_command(insert_command); @@ -367,11 +364,8 @@ const Command = struct { fn execute_const(self: *const @This()) void { var buf: [2048]u8 = undefined; @memcpy(buf[0..self.args.len], self.args); - command.executeName(self.command, .{ .args = .{ .buf = buf[0..self.args.len] } }) catch |e| { - const logger = log.logger("keybind"); - logger.print_err("init/deinit_command", "ERROR: {s} {s}", .{ self.command, @errorName(e) }); - logger.deinit(); - }; + command.executeName(self.command, .{ .args = .{ .buf = buf[0..self.args.len] } }) catch |e| + std.log.err("ERROR: {s} {s}", .{ self.command, @errorName(e) }); } fn has_integer_argument(id: command.ID) bool { @@ -390,9 +384,7 @@ const Command = struct { switch (state) { .command => { if (token != .string) { - const logger = log.logger("keybind"); - logger.print_err("keybind.load", "ERROR: invalid command token {any}", .{token}); - logger.deinit(); + std.log.err("ERROR: invalid command token {any}", .{token}); return error.InvalidFormat; } command_ = try allocator.dupe(u8, token.string); @@ -404,9 +396,7 @@ const Command = struct { else => { const json = try std.json.Stringify.valueAlloc(allocator, token, .{}); defer allocator.free(json); - const logger = log.logger("keybind"); - logger.print_err("keybind.load", "ERROR: invalid command argument '{s}'", .{json}); - logger.deinit(); + std.log.err("ERROR: invalid command argument '{s}'", .{json}); return error.InvalidFormat; }, } @@ -538,30 +528,22 @@ const BindingSet = struct { _ = event; bindings: for (bindings) |entry| { if (entry.len < 2) { - const logger = log.logger("keybind"); - logger.print_err("keybind.load", "ERROR: invalid binding definition {any}", .{entry}); - logger.deinit(); + std.log.err("ERROR: invalid binding definition {any}", .{entry}); continue :bindings; } const keys = entry[0]; if (keys != .string) { - const logger = log.logger("keybind"); - logger.print_err("keybind.load", "ERROR: invalid binding key definition {any}", .{keys}); - logger.deinit(); + std.log.err("ERROR: invalid binding key definition {any}", .{keys}); continue :bindings; } const key_events = switch (self.syntax) { .flow => parse_flow.parse_key_events(allocator, keys.string) catch |e| { - const logger = log.logger("keybind"); - logger.print_err("keybind.load", "ERROR: {s} {s}", .{ @errorName(e), parse_flow.parse_error_message }); - logger.deinit(); + std.log.err("ERROR: {s} {s}", .{ @errorName(e), parse_flow.parse_error_message }); break; }, .vim => parse_vim.parse_key_events(allocator, keys.string) catch |e| { - const logger = log.logger("keybind"); - logger.print_err("keybind.load.vim", "ERROR: {s} {s}", .{ @errorName(e), parse_vim.parse_error_message }); - logger.deinit(); + std.log.err("ERROR: {s} {s}", .{ @errorName(e), parse_vim.parse_error_message }); break; }, }; @@ -577,9 +559,7 @@ const BindingSet = struct { if (cmd_entry != .array) { const json = try std.json.Stringify.valueAlloc(allocator, cmd_entry, .{}); defer allocator.free(json); - const logger = log.logger("keybind"); - logger.print_err("keybind.load", "ERROR: invalid command definition {s}", .{json}); - logger.deinit(); + std.log.err("ERROR: invalid command definition {s}", .{json}); continue :bindings; } try cmds.append(allocator, try Command.load(allocator, cmd_entry.array.items)); @@ -793,10 +773,7 @@ const BindingSet = struct { input.key.left_shift, input.key.right_shift => return, else => {}, }; - - const logger = log.logger("keybind"); - defer logger.deinit(); - logger.print("C-? for key hints", .{}); + std.log.info("C-? for key hints", .{}); } /// Retrieve bindings that will match a key event sequence From fb5c67280fdcdad1563ba977df2099d0f1d1f7a6 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 13:24:26 +0100 Subject: [PATCH 03/15] refactor: drop level prefix from std.log error and info messages Errors are already logged as errors and info messages don't need any extra context. --- src/log.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/log.zig b/src/log.zig index 7561a73..0be31e3 100644 --- a/src/log.zig +++ b/src/log.zig @@ -260,7 +260,11 @@ pub fn std_log_function( const log_pid = std_log_pid orelse return; const prefix = "[" ++ comptime level.asText() ++ "] "; var buf: [max_log_message]u8 = undefined; - const output = std.fmt.bufPrint(&buf, prefix ++ format, args) catch "MESSAGE TOO LARGE"; + const fmt = switch (level) { + .warn, .debug => prefix ++ format, + .err, .info => format, + }; + const output = std.fmt.bufPrint(&buf, fmt, args) catch "MESSAGE TOO LARGE"; if (level == .err) { log_pid.send(.{ "log", "error", @tagName(scope), "std.log", "->", output }) catch {}; } else { From 67c6965eaa886f688b73543cfc416c566a6a5f80 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 13:25:33 +0100 Subject: [PATCH 04/15] refactor: use scoped log in keybind module --- src/keybind/keybind.zig | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/keybind/keybind.zig b/src/keybind/keybind.zig index c6e4e75..ed7d1a7 100644 --- a/src/keybind/keybind.zig +++ b/src/keybind/keybind.zig @@ -15,6 +15,8 @@ const KeyEvent = input.KeyEvent; const SelectionStyle = @import("Buffer").Selection.Style; pub const CursorShape = @import("config").CursorShape; +const log = std.log.scoped(.keybind); + const parse_flow = @import("parse_flow.zig"); const parse_vim = @import("parse_vim.zig"); @@ -244,9 +246,9 @@ pub fn set_namespace(namespace_name: []const u8) LoadError!void { fn get_mode_binding_set(mode_name: []const u8, insert_command: []const u8) LoadError!*const BindingSet { const namespace = current_namespace(); var binding_set = namespace.get_mode(mode_name) orelse { - std.log.err("ERROR: mode not found: {s}", .{mode_name}); + log.err("ERROR: mode not found: {s}", .{mode_name}); var iter = namespace.modes.iterator(); - while (iter.next()) |entry| std.log.info("available modes: {s}", .{entry.key_ptr.*}); + while (iter.next()) |entry| log.info("available modes: {s}", .{entry.key_ptr.*}); return error.NotFound; }; binding_set.set_insert_command(insert_command); @@ -365,7 +367,7 @@ const Command = struct { var buf: [2048]u8 = undefined; @memcpy(buf[0..self.args.len], self.args); command.executeName(self.command, .{ .args = .{ .buf = buf[0..self.args.len] } }) catch |e| - std.log.err("ERROR: {s} {s}", .{ self.command, @errorName(e) }); + log.err("ERROR: {s} {s}", .{ self.command, @errorName(e) }); } fn has_integer_argument(id: command.ID) bool { @@ -384,7 +386,7 @@ const Command = struct { switch (state) { .command => { if (token != .string) { - std.log.err("ERROR: invalid command token {any}", .{token}); + log.err("ERROR: invalid command token {any}", .{token}); return error.InvalidFormat; } command_ = try allocator.dupe(u8, token.string); @@ -396,7 +398,7 @@ const Command = struct { else => { const json = try std.json.Stringify.valueAlloc(allocator, token, .{}); defer allocator.free(json); - std.log.err("ERROR: invalid command argument '{s}'", .{json}); + log.err("ERROR: invalid command argument '{s}'", .{json}); return error.InvalidFormat; }, } @@ -528,22 +530,22 @@ const BindingSet = struct { _ = event; bindings: for (bindings) |entry| { if (entry.len < 2) { - std.log.err("ERROR: invalid binding definition {any}", .{entry}); + log.err("ERROR: invalid binding definition {any}", .{entry}); continue :bindings; } const keys = entry[0]; if (keys != .string) { - std.log.err("ERROR: invalid binding key definition {any}", .{keys}); + log.err("ERROR: invalid binding key definition {any}", .{keys}); continue :bindings; } const key_events = switch (self.syntax) { .flow => parse_flow.parse_key_events(allocator, keys.string) catch |e| { - std.log.err("ERROR: {s} {s}", .{ @errorName(e), parse_flow.parse_error_message }); + log.err("ERROR: {s} {s}", .{ @errorName(e), parse_flow.parse_error_message }); break; }, .vim => parse_vim.parse_key_events(allocator, keys.string) catch |e| { - std.log.err("ERROR: {s} {s}", .{ @errorName(e), parse_vim.parse_error_message }); + log.err("ERROR: {s} {s}", .{ @errorName(e), parse_vim.parse_error_message }); break; }, }; @@ -559,7 +561,7 @@ const BindingSet = struct { if (cmd_entry != .array) { const json = try std.json.Stringify.valueAlloc(allocator, cmd_entry, .{}); defer allocator.free(json); - std.log.err("ERROR: invalid command definition {s}", .{json}); + log.err("ERROR: invalid command definition {s}", .{json}); continue :bindings; } try cmds.append(allocator, try Command.load(allocator, cmd_entry.array.items)); From 1803584940cca3766910adb9ff85735a5a963220 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 13:25:53 +0100 Subject: [PATCH 05/15] refactor: include unbound keypress in key hints message --- src/keybind/keybind.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/keybind/keybind.zig b/src/keybind/keybind.zig index ed7d1a7..a0cc948 100644 --- a/src/keybind/keybind.zig +++ b/src/keybind/keybind.zig @@ -775,7 +775,7 @@ const BindingSet = struct { input.key.left_shift, input.key.right_shift => return, else => {}, }; - std.log.info("C-? for key hints", .{}); + log.info("{f} is unbound, press C-? for key hints", .{current_key_event_sequence_fmt()}); } /// Retrieve bindings that will match a key event sequence From fd9fa4ee8f9a6a9aff4577dae39ba05461537df0 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 13:29:12 +0100 Subject: [PATCH 06/15] Revert "fix: build fix after rebase/merge" This reverts commit 423b8c1613e429f6b8258ac00d7de268bcc16bac. --- src/tui/mode/helix.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index cceab4e..4a42991 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -568,7 +568,7 @@ fn extend_to_word(ctx: command.Context, move: Editor.cursor_operator_const, _: D var repeat: usize = 1; _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - const sel = cursel.enable_selection(root, ed.metrics); + const sel = try cursel.enable_selection(root, ed.metrics); const pivot: usize = if (sel.is_reversed()) sel.begin.col -| 1 else sel.begin.col; var i: usize = repeat; while (i > 0) : (i -= 1) { From 989557fb6d6a7dc1a474dd40429d353ab053dc6e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 13:29:25 +0100 Subject: [PATCH 07/15] Revert "Fixed selection extensions with new helper functions" This reverts commit 1bae8640228ffdfade68582995453e889b9625f4. --- src/tui/mode/helix.zig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 4a42991..21b3258 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -560,7 +560,7 @@ fn move_to_word(ctx: command.Context, move: Editor.cursor_operator_const, direct ed.clamp(); } -fn extend_to_word(ctx: command.Context, move: Editor.cursor_operator_const, _: Direction) command.Result { +fn extend_to_word(ctx: command.Context, move: Editor.cursor_operator_const, direction: Direction) command.Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; const root = try ed.buf_root(); @@ -568,16 +568,16 @@ fn extend_to_word(ctx: command.Context, move: Editor.cursor_operator_const, _: D var repeat: usize = 1; _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - const sel = try cursel.enable_selection(root, ed.metrics); - const pivot: usize = if (sel.is_reversed()) sel.begin.col -| 1 else sel.begin.col; - var i: usize = repeat; - while (i > 0) : (i -= 1) { - try move(root, &sel.end, ed.metrics); + if (cursel.selection == null) { + cursel.selection = Selection.from_cursor(cursel.cursor); } - sel.begin.col = if (sel.is_reversed()) pivot +| 1 else pivot; - cursel.cursor = sel.end; + const sel = &cursel.selection.?; + const pivot = if (sel.is_reversed()) cursel.begin - 1 else cursel.begin; + var i: usize = repeat; + while (i > 0) : (i -= 1) {} }; + ed.with_selections_const_repeat(root, move, ctx) catch {}; ed.clamp(); } From 6f1806cd9597950f69c40742a8baafa72c3e89b4 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 13:29:31 +0100 Subject: [PATCH 08/15] Revert "Initial attempt to fix prev and next word movement" This reverts commit 608df1518a612826f39ed6a839dec56218285f44. --- src/tui/mode/helix.zig | 60 +++++++++++------------------------------- 1 file changed, 16 insertions(+), 44 deletions(-) diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 21b3258..4ecaca1 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -13,8 +13,6 @@ const Buffer = @import("Buffer"); const Cursor = Buffer.Cursor; const Selection = Buffer.Selection; -const Direction = enum { backwards, forwards }; - var commands: Commands = undefined; pub fn init() !void { @@ -244,62 +242,62 @@ const cmds_ = struct { pub const extend_line_below_meta: Meta = .{ .arguments = &.{.integer}, .description = "Select current line, if already selected, extend to next line" }; pub fn move_next_word_start(_: *void, ctx: Ctx) Result { - try move_to_word(ctx, Editor.move_cursor_word_right_vim, .forwards); + try move_to_word(ctx, Editor.move_cursor_word_right_vim); } pub const move_next_word_start_meta: Meta = .{ .description = "Move next word start", .arguments = &.{.integer} }; pub fn extend_next_word_start(_: *void, ctx: Ctx) Result { - try extend_to_word(ctx, Editor.move_cursor_word_right_vim, .forwards); + try extend_to_word(ctx, Editor.move_cursor_word_right_vim); } pub const extend_next_word_start_meta: Meta = .{ .description = "Extend next word start", .arguments = &.{.integer} }; pub fn move_next_long_word_start(_: *void, ctx: Ctx) Result { - try move_to_word(ctx, move_cursor_long_word_right, .forwards); + try move_to_word(ctx, move_cursor_long_word_right); } pub const move_next_long_word_start_meta: Meta = .{ .description = "Move next long word start", .arguments = &.{.integer} }; pub fn extend_next_long_word_start(_: *void, ctx: Ctx) Result { - try extend_to_word(ctx, move_cursor_long_word_right, .forwards); + try extend_to_word(ctx, move_cursor_long_word_right); } pub const extend_next_long_word_start_meta: Meta = .{ .description = "Extend next long word start", .arguments = &.{.integer} }; pub fn move_prev_word_start(_: *void, ctx: Ctx) Result { - try move_to_word(ctx, move_cursor_word_left_helix, .backwards); + try move_to_word(ctx, move_cursor_word_left_helix); } pub const move_prev_word_start_meta: Meta = .{ .description = "Move previous word start", .arguments = &.{.integer} }; pub fn extend_prev_word_start(_: *void, ctx: Ctx) Result { - try extend_to_word(ctx, move_cursor_word_left_helix, .backwards); + try extend_to_word(ctx, move_cursor_word_left_helix); } pub const extend_prev_word_start_meta: Meta = .{ .description = "Extend previous word start", .arguments = &.{.integer} }; pub fn move_prev_long_word_start(_: *void, ctx: Ctx) Result { - try move_to_word(ctx, move_cursor_long_word_left, .backwards); + try move_to_word(ctx, move_cursor_long_word_left); } pub const move_prev_long_word_start_meta: Meta = .{ .description = "Move previous long word start", .arguments = &.{.integer} }; pub fn extend_prev_long_word_start(_: *void, ctx: Ctx) Result { - try extend_to_word(ctx, move_cursor_long_word_left, .backwards); + try extend_to_word(ctx, move_cursor_long_word_left); } pub const extend_prev_long_word_start_meta: Meta = .{ .description = "Extend previous word start", .arguments = &.{.integer} }; pub fn move_next_word_end(_: *void, ctx: Ctx) Result { - try move_to_word(ctx, move_cursor_word_right_end_helix, .forwards); + try move_to_word(ctx, move_cursor_word_right_end_helix); } pub const move_next_word_end_meta: Meta = .{ .description = "Move next word end", .arguments = &.{.integer} }; pub fn extend_next_word_end(_: *void, ctx: Ctx) Result { - try extend_to_word(ctx, move_cursor_word_right_end_helix, .forwards); + try extend_to_word(ctx, move_cursor_word_right_end_helix); } pub const extend_next_word_end_meta: Meta = .{ .description = "Extend next word end", .arguments = &.{.integer} }; pub fn move_next_long_word_end(_: *void, ctx: Ctx) Result { - try move_to_word(ctx, move_cursor_long_word_right_end, .forwards); + try move_to_word(ctx, move_cursor_long_word_right_end); } pub const move_next_long_word_end_meta: Meta = .{ .description = "Move next long word end", .arguments = &.{.integer} }; pub fn extend_next_long_word_end(_: *void, ctx: Ctx) Result { - try extend_to_word(ctx, move_cursor_long_word_right_end, .forwards); + try extend_to_word(ctx, move_cursor_long_word_right_end); } pub const extend_next_long_word_end_meta: Meta = .{ .description = "Extend next long word end", .arguments = &.{.integer} }; @@ -528,7 +526,7 @@ fn match_bracket(root: Buffer.Root, cursel: *CurSel, ctx: command.Context, metri } } -fn move_to_word(ctx: command.Context, move: Editor.cursor_operator_const, direction: Direction) command.Result { +fn move_to_word(ctx: command.Context, move: Editor.cursor_operator_const) command.Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; const root = try ed.buf_root(); @@ -539,44 +537,17 @@ fn move_to_word(ctx: command.Context, move: Editor.cursor_operator_const, direct if (repeat > 1) ed.with_cursors_const_repeat(root, move, command.fmt(.{repeat - 1})) catch {}; for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - var sel = Selection.from_cursor(&cursel.cursor); - const cur = sel.begin.test_at(root, is_not_whitespace_or_eol, ed.metrics); - if (direction == .backwards) { - sel.begin.move_left(root, ed.metrics) catch continue; - const prev = sel.begin.test_at(root, Editor.is_not_word_char, ed.metrics); - sel.begin = sel.end; - if (!cur or cur != prev) - sel.begin.move_right(root, ed.metrics) catch continue; - } else { - sel.end.move_right(root, ed.metrics) catch continue; - const next = sel.end.test_at(root, Editor.is_not_word_char, ed.metrics); - if (!cur and cur != next) - sel.begin = sel.end; - } - cursel.cursor = sel.end; - cursel.selection = sel; + cursel.selection = null; }; ed.with_selections_const_repeat(root, move, command.fmt(.{1})) catch {}; ed.clamp(); } -fn extend_to_word(ctx: command.Context, move: Editor.cursor_operator_const, direction: Direction) command.Result { +fn extend_to_word(ctx: command.Context, move: Editor.cursor_operator_const) command.Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; const root = try ed.buf_root(); - var repeat: usize = 1; - _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; - for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - if (cursel.selection == null) { - cursel.selection = Selection.from_cursor(cursel.cursor); - } - const sel = &cursel.selection.?; - const pivot = if (sel.is_reversed()) cursel.begin - 1 else cursel.begin; - var i: usize = repeat; - while (i > 0) : (i -= 1) {} - }; - ed.with_selections_const_repeat(root, move, ctx) catch {}; ed.clamp(); } @@ -859,6 +830,7 @@ fn move_noop(_: Buffer.Root, _: *Cursor, _: Buffer.Metrics) error{Stop}!void {} fn move_cursor_word_right_end_helix(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { try Editor.move_cursor_right(root, cursor, metrics); Editor.move_cursor_right_until(root, cursor, Editor.is_word_boundary_right_vim, metrics); + try cursor.move_right(root, metrics); } fn move_cursor_to_char_left_beyond_eol(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics, ctx: command.Context) error{Stop}!void { From b3425d5c59ba0b354b589fcd907201f2a5fa438e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 13:42:31 +0100 Subject: [PATCH 09/15] refactor: remove duplicate "shell: " in shell execute messages --- src/shell.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell.zig b/src/shell.zig index b349395..d82ecd4 100644 --- a/src/shell.zig +++ b/src/shell.zig @@ -206,7 +206,7 @@ const Process = struct { var buf: [1024]u8 = undefined; const json = self.argv.to_json(&buf) catch |e| return tp.exit_error(e, @errorReturnTrace()); if (self.handlers.log_execute) - self.logger.print("shell: execute {s}", .{json}); + self.logger.print("execute {s}", .{json}); self.sp = tp.subprocess.init(self.allocator, self.argv, module_name, self.stdin_behavior) catch |e| return tp.exit_error(e, @errorReturnTrace()); tp.receive(&self.receiver); } From 4d81123c763781483ebf211ef16e6f351acbeffd Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 14:17:40 +0100 Subject: [PATCH 10/15] fix: don't add an extra space at the beginning of formatted keybinds --- src/keybind/keybind.zig | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/keybind/keybind.zig b/src/keybind/keybind.zig index a0cc948..e20a1a0 100644 --- a/src/keybind/keybind.zig +++ b/src/keybind/keybind.zig @@ -879,8 +879,15 @@ const KeyEventSequenceFmt = struct { key_events: []const KeyEvent, pub fn format(self: @This(), writer: anytype) !void { - for (self.key_events) |key_event| - try writer.print(" {f}", .{input.key_event_short_fmt(key_event)}); + var first = true; + for (self.key_events) |key_event| { + if (first) { + first = false; + } else { + try writer.print(" ", .{}); + } + try writer.print("{f}", .{input.key_event_short_fmt(key_event)}); + } } }; From 83bbbfebe3c30aaf24499976d9c10d06703bc5cb Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 14:24:11 +0100 Subject: [PATCH 11/15] fix: set default log level to .info in release builds --- src/main.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.zig b/src/main.zig index e33201c..5997395 100644 --- a/src/main.zig +++ b/src/main.zig @@ -30,8 +30,7 @@ pub const application_subtext = "a programmer's text editor"; pub const application_description = application_title ++ ": " ++ application_subtext; pub const std_options: std.Options = .{ - // .log_level = if (builtin.mode == .Debug) .debug else .warn, - .log_level = if (builtin.mode == .Debug) .info else .warn, + .log_level = if (builtin.mode == .Debug) .debug else .info, .logFn = log.std_log_function, }; From 4638c38032b58238e45f30d82ba07bdda809943d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 14:24:46 +0100 Subject: [PATCH 12/15] fix: compensate in keyhint formatting for cleaned-up keybind format --- src/tui/keyhints.zig | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/tui/keyhints.zig b/src/tui/keyhints.zig index 577831f..9559e65 100644 --- a/src/tui/keyhints.zig +++ b/src/tui/keyhints.zig @@ -133,14 +133,17 @@ fn render(mode: *keybind.Mode, bindings: []const keybind.Binding, theme: *const break :blk writer.buffered(); }; plane.cursor_move_yx(@intCast(y), 0) catch break; - _ = plane.print("{s}", .{keybind_txt[key_events.len..]}) catch {}; + switch (render_mode) { + .no_key_event_prefix => _ = plane.print("{s}", .{keybind_txt[key_events.len..]}) catch {}, + .full => _ = plane.print(" {s}", .{keybind_txt}) catch {}, + } } plane.set_style(style_label); for (bindings[top..], 0..) |binding, y| { if (y >= max_items) break; - const padding = max_prefix_len + 2; + const padding = max_prefix_len + 3; const description = blk: { const id = binding.commands[0].command_id orelse From d069250d1725dc64cb7a872a1f222fcc7ae8c43a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 14:25:54 +0100 Subject: [PATCH 13/15] fix: formatting of keybind prefix in explicitly enabled keyhint mode --- src/tui/keyhints.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/tui/keyhints.zig b/src/tui/keyhints.zig index 9559e65..ede636a 100644 --- a/src/tui/keyhints.zig +++ b/src/tui/keyhints.zig @@ -12,12 +12,13 @@ var show_page: usize = 0; pub fn render_current_input_mode(allocator: std.mem.Allocator, select_mode: keybind.SelectMode, theme: *const Widget.Theme) void { const mode = tui.input_mode() orelse return; - const bindings = blk: { - const b = mode.current_key_event_sequence_bindings(allocator, select_mode) catch return; - break :blk if (b.len > 0) b else mode.current_bindings(allocator, select_mode) catch return; - }; + const key_events = mode.current_key_event_sequence_bindings(allocator, select_mode) catch return; + const bindings = if (key_events.len > 0) + key_events + else + mode.current_bindings(allocator, select_mode) catch return; defer allocator.free(bindings); - return render(mode, bindings, theme, .full); + return render(mode, bindings, theme, if (key_events.len > 0) .no_key_event_prefix else .full); } pub fn render_current_key_event_sequence(allocator: std.mem.Allocator, select_mode: keybind.SelectMode, theme: *const Widget.Theme) void { From 4e4ec855ed1f0d6c73a522bfd5de02fe45f2c617 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 14:38:52 +0100 Subject: [PATCH 14/15] refactor: tweak max hints window size --- src/tui/keyhints.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/keyhints.zig b/src/tui/keyhints.zig index ede636a..c9ffc6c 100644 --- a/src/tui/keyhints.zig +++ b/src/tui/keyhints.zig @@ -54,7 +54,7 @@ fn render(mode: *keybind.Mode, bindings: []const keybind.Binding, theme: *const const max_len = max_prefix_len + max_description_len + 2 + 2; const widget_style = tui.get_widget_style(widget_type); const scr = tui.screen(); - const max_screen_height = scr.h -| widget_style.padding.top -| widget_style.padding.bottom -| 1; + const max_screen_height = scr.h -| widget_style.padding.top -| widget_style.padding.bottom -| 3; const max_items = @min(bindings.len, max_screen_height); const page_size = max_screen_height; var top = show_page * page_size; From 3dfb93fbd2311e2df91d83f88a130ece97c1963e Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 2 Dec 2025 14:43:18 +0100 Subject: [PATCH 15/15] refactor: add key event prefix to hints window title --- src/tui/keyhints.zig | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/tui/keyhints.zig b/src/tui/keyhints.zig index c9ffc6c..4c17a5a 100644 --- a/src/tui/keyhints.zig +++ b/src/tui/keyhints.zig @@ -91,12 +91,22 @@ fn render(mode: *keybind.Mode, bindings: []const keybind.Binding, theme: *const } if (widget_style.padding.top > 0) { top_layer_.cursor_move_yx(@intCast(0), @intCast(3)) catch return; - _ = top_layer_.print("{s} {s}/{s} {s}", .{ - widget_style.border.nib, - keybind.get_namespace(), - mode.bindings.config_section, - widget_style.border.nie, - }) catch {}; + if (key_events.len > 0) { + _ = top_layer_.print("{s} {s}/{s} prefix: {s} {s}", .{ + widget_style.border.nib, + keybind.get_namespace(), + mode.bindings.config_section, + key_events, + widget_style.border.nie, + }) catch {}; + } else { + _ = top_layer_.print("{s} {s}/{s} {s}", .{ + widget_style.border.nib, + keybind.get_namespace(), + mode.bindings.config_section, + widget_style.border.nie, + }) catch {}; + } } // workaround vaxis.Layer issue