From 7bf532bdfd692a6697031e22c96ffba021baebf5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 27 Nov 2025 16:44:16 +0100 Subject: [PATCH] fix: make helix move_prev_word_start an exact match to real helix --- src/tui/editor.zig | 2 +- src/tui/mode/helix.zig | 72 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index cd094ff..ea2a059 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -2196,7 +2196,7 @@ pub const Editor = struct { const cursor_operator_const_arg = *const fn (root: Buffer.Root, cursor: *Cursor, ctx: Context, metrics: Buffer.Metrics) error{Stop}!void; pub const cursel_operator_mut_once_arg = *const fn (root: Buffer.Root, cursel: *CurSel, ctx: Context, metrics: Buffer.Metrics) error{Stop}!void; const cursor_view_operator_const = *const fn (root: Buffer.Root, cursor: *Cursor, view: *const View, metrics: Buffer.Metrics) error{Stop}!void; - const cursel_operator_const = *const fn (root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) error{Stop}!void; + pub const cursel_operator_const = *const fn (root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) error{Stop}!void; const cursor_operator = *const fn (root: Buffer.Root, cursor: *Cursor, allocator: Allocator) error{Stop}!Buffer.Root; const cursel_operator = *const fn (root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root; const cursel_operator_mut = *const fn (self: *Self, root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root; diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index cceab4e..ee70840 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -13,6 +13,8 @@ const Buffer = @import("Buffer"); const Cursor = Buffer.Cursor; const Selection = Buffer.Selection; +const char_class = Editor.char_class; + const Direction = enum { backwards, forwards }; var commands: Commands = undefined; @@ -263,12 +265,80 @@ const cmds_ = struct { } pub const extend_next_long_word_start_meta: Meta = .{ .description = "Extend next long word start", .arguments = &.{.integer} }; + fn is_eol(c: []const u8) bool { + return char_class(c) == .eol; + } + + fn is_whitespace(c: []const u8) bool { + return char_class(c) == .whitespace; + } + + fn is_word_boundary(root: Buffer.Root, cursor: Cursor, metrics: Buffer.Metrics, comptime direction: enum { left, right }) bool { + const nxt = char_class(switch (direction) { + .left => cursor.char_left(root, metrics), + .right => cursor.char_right(root, metrics), + }); + const cur = char_class(cursor.char_at(root, metrics)); + if (cur == .eol) return false; + return switch (nxt) { + .end, .eol => true, + .whitespace => cur != .whitespace, + else => nxt != cur, + }; + } + + fn move_cursor_prev_word_start(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) error{Stop}!void { + var cursor = cursel.cursor; + if (is_word_boundary(root, cursor, metrics, .left)) + cursor.move_left(root, metrics) catch {}; + + var sel = Selection.from_cursor_inclusive(&cursor, root, metrics); + defer { + sel.begin = cursor; + cursel.cursor = cursor; + cursel.selection = sel; + } + + // Consume whitespace + while (cursor.test_at(root, is_whitespace, metrics)) { + cursor.move_left(root, metrics) catch return; + // Stop at beginning of line + if (cursor.test_left(root, is_eol, metrics)) return; + } + + // Consume word/non-word chars + while (!is_word_boundary(root, cursor, metrics, .left)) { + cursor.move_left(root, metrics) catch return; + // Stop at beginning of line + if (cursor.test_left(root, is_eol, metrics)) return; + } + } + + fn move_cursor_prev_word_start_extend(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) error{Stop}!void { + var selection = cursel.selection; + // check if we already had a selection and extend it + defer if (selection) |*pre_sel| { + pre_sel.normalize(); + if (cursel.selection) |*sel| sel.end = pre_sel.end; + }; + try move_cursor_prev_word_start(root, cursel, metrics); + } + + fn move_cursels_const_repeat(move: Editor.cursel_operator_const, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = try ed.buf_root(); + try ed.with_cursels_const_repeat(root, move, ctx); + ed.clamp(); + } + pub fn move_prev_word_start(_: *void, ctx: Ctx) Result { - try move_to_word(ctx, move_cursor_word_left_helix, .backwards); + try move_cursels_const_repeat(move_cursor_prev_word_start, ctx); } 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 move_cursels_const_repeat(move_cursor_prev_word_start_extend, ctx); try extend_to_word(ctx, move_cursor_word_left_helix, .backwards); } pub const extend_prev_word_start_meta: Meta = .{ .description = "Extend previous word start", .arguments = &.{.integer} };