From a64d7c3afac6e6817f6c286024489b6cf160b580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Mon, 6 Oct 2025 07:46:54 -0500 Subject: [PATCH 1/4] hx: attempt to add tests in separate file --- build.zig | 64 +++++++++++++++++++++ src/tui/editor.zig | 4 +- src/tui/mode/helix.zig | 127 +++++++++++++++++++++++++++++++++++++++++ test/tests.zig | 1 + test/tests_helix.zig | 79 +++++++++++++++++++++++++ 5 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 test/tests_helix.zig diff --git a/build.zig b/build.zig index 833099c..107b584 100644 --- a/build.zig +++ b/build.zig @@ -621,6 +621,67 @@ pub fn build_exe( }, }); + const helix_mod = b.createModule(.{ + .root_source_file = b.path("src/tui/mode/helix.zig"), + }); + // const editor_mod = b.createModule(.{ + // .root_source_file = b.path("src/tui/editor.zig"), + // }); + // const Widget_mod = b.createModule(.{ + // .root_source_file = b.path("src/tui/Widget.zig"), + // }); + // const mainview_mod = b.createModule(.{ + // .root_source_file = b.path("src/tui/mainview.zig"), + // }); + // const WidgetList_mod = b.createModule(.{ + // .root_source_file = b.path("src/tui/WidgetList.zig"), + // }); + // const MessageFilter_mod = b.createModule(.{ + // .root_source_file = b.path("src/tui/MessageFilter.zig"), + // }); + // const editor_gutter_mod = b.createModule(.{ + // .root_source_file = b.path("src/tui/editor_gutter.zig"), + // }); + // const scrollbar_v_mod = b.createModule(.{ + // .root_source_file = b.path("src/tui//scrollbar_v.zig"), + // }); + // const palette_mod = b.createModule(.{ + // .root_source_file = b.path("src/tui/mode/overlay/palette.zig"), + // }); + // const open_recent_project_mod = b.createModule(.{ + // .root_source_file = b.path("src/tui/mode/overlay/open_recent_project.zig"), + // }); + + // const helix_test_run_cmd = blk: { + // const tests = b.addTest(.{ + // .root_module = b.createModule(.{ + // .root_source_file = b.path("src/tui/mode/helix.zig"), + // .target = target, + // .optimize = optimize, + // }), + // }); + // tests.root_module.addImport("cbor", cbor_mod); + // tests.root_module.addImport("command", command_mod); + // tests.root_module.addImport("EventHandler", EventHandler_mod); + // tests.root_module.addImport("input", input_mod); + // tests.root_module.addImport("thespian", thespian_mod); + // tests.root_module.addImport("log", log_mod); + // tests.root_module.addImport("tui", tui_mod); + // tests.root_module.addImport("helix", helix_mod); + // tests.root_module.addImport("editor", editor_mod); + // tests.root_module.addImport("Widget", Widget_mod); + // tests.root_module.addImport("WidgetList", WidgetList_mod); + // tests.root_module.addImport("mainview", mainview_mod); + // tests.root_module.addImport("Buffer", Buffer_mod); + // tests.root_module.addImport("MessageFilter", MessageFilter_mod); + // tests.root_module.addImport("editor_gutter", editor_gutter_mod); + // tests.root_module.addImport("scrollbar_v", scrollbar_v_mod); + // tests.root_module.addImport("palette", palette_mod); + // tests.root_module.addImport("open_recent_project", open_recent_project_mod); + // tests.root_module.addImport("renderer", renderer_mod); + // break :blk b.addRunArtifact(tests); + // }; + const exe_name = if (gui) "flow-gui" else "flow"; const exe = b.addExecutable(.{ @@ -712,6 +773,7 @@ pub fn build_exe( check_exe.root_module.addImport("version_info", b.createModule(.{ .root_source_file = version_info_file })); check_step.dependOn(&check_exe.step); + const test_filters = b.option([]const []const u8, "test-filter", "Skip tests that do not match any filter") orelse &[0][]const u8{}; const tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("test/tests.zig"), @@ -721,6 +783,7 @@ pub fn build_exe( }), .use_llvm = use_llvm, .use_lld = use_llvm, + .filters = test_filters, }); tests.pie = pie; @@ -730,6 +793,7 @@ pub fn build_exe( tests.root_module.addImport("log", log_mod); tests.root_module.addImport("Buffer", Buffer_mod); tests.root_module.addImport("color", color_mod); + tests.root_module.addImport("helix", helix_mod); // b.installArtifact(tests); const test_run_cmd = b.addRunArtifact(tests); diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 791b669..211e62a 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -2143,11 +2143,11 @@ pub const Editor = struct { return false; } - fn is_whitespace(c: []const u8) bool { + pub fn is_whitespace(c: []const u8) bool { return (c.len == 0) or (c[0] == ' ') or (c[0] == '\t'); } - fn is_whitespace_or_eol(c: []const u8) bool { + pub fn is_whitespace_or_eol(c: []const u8) bool { return is_whitespace(c) or c[0] == '\n'; } diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 9a0e589..c0f3a6b 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -232,6 +232,20 @@ const cmds_ = struct { } pub const move_next_word_start_meta: Meta = .{ .description = "Move next word start", .arguments = &.{.integer} }; + pub fn move_next_long_word_start(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = try ed.buf_root(); + + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + cursel.selection = null; + }; + + ed.with_selections_const_repeat(root, move_cursor_long_word_right, ctx) catch {}; + ed.clamp(); + } + pub const move_next_long_word_start_meta: Meta = .{ .description = "Move next long word start", .arguments = &.{.integer} }; + pub fn move_prev_word_start(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; @@ -246,6 +260,20 @@ const cmds_ = struct { } pub const move_prev_word_start_meta: Meta = .{ .description = "Move previous word start", .arguments = &.{.integer} }; + pub fn move_prev_long_word_start(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = try ed.buf_root(); + + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + cursel.selection = null; + }; + + ed.with_selections_const_repeat(root, move_cursor_long_word_left, ctx) catch {}; + ed.clamp(); + } + pub const move_prev_long_word_start_meta: Meta = .{ .description = "Move previous long word start", .arguments = &.{.integer} }; + pub fn move_next_word_end(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; @@ -260,6 +288,20 @@ const cmds_ = struct { } pub const move_next_word_end_meta: Meta = .{ .description = "Move next word end", .arguments = &.{.integer} }; + pub fn move_next_long_word_end(_: *void, ctx: Ctx) Result { + const mv = tui.mainview() orelse return; + const ed = mv.get_active_editor() orelse return; + const root = try ed.buf_root(); + + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + cursel.selection = null; + }; + + ed.with_selections_const_repeat(root, move_cursor_long_word_right_end, ctx) catch {}; + ed.clamp(); + } + pub const move_next_long_word_end_meta: Meta = .{ .description = "Move next long word end", .arguments = &.{.integer} }; + pub fn cut_forward_internal_inclusive(_: *void, _: Ctx) Result { const mv = tui.mainview() orelse return; const ed = mv.get_active_editor() orelse return; @@ -467,3 +509,88 @@ fn insert_line(ed: *Editor, root: Buffer.Root, cursel: *CurSel, s: []const u8, a cursel.selection = Selection{ .begin = begin, .end = cursor.* }; return root_; } + +fn is_not_whitespace_or_eol(c: []const u8) bool { + return !Editor.is_whitespace_or_eol(c); +} + +fn is_whitespace_or_eol_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + return cursor.test_at(root, Editor.is_whitespace_or_eol, metrics); +} + +fn is_non_whitespace_or_eol_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + return cursor.test_at(root, is_not_whitespace_or_eol, metrics); +} + +fn is_long_word_boundary_left(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + if (cursor.test_at(root, Editor.is_whitespace, metrics)) return false; + var next = cursor.*; + next.move_left(root, metrics) catch return true; + + const next_is_whitespace = Editor.is_whitespace_at_cursor(root, &next, metrics); + if (next_is_whitespace) return true; + + const curr_is_non_word = is_non_whitespace_or_eol_at_cursor(root, cursor, metrics); + const next_is_non_word = is_non_whitespace_or_eol_at_cursor(root, &next, metrics); + return curr_is_non_word != next_is_non_word; +} + +pub fn move_cursor_long_word_left(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { + try Editor.move_cursor_left(root, cursor, metrics); + + // Consume " " + while (Editor.is_whitespace_at_cursor(root, cursor, metrics)) { + try Editor.move_cursor_left(root, cursor, metrics); + } + + var next = cursor.*; + next.move_left(root, metrics) catch return; + var next_next = next; + next_next.move_left(root, metrics) catch return; + + const cur = next.test_at(root, is_not_whitespace_or_eol, metrics); + const nxt = next_next.test_at(root, is_not_whitespace_or_eol, metrics); + if (cur != nxt) { + try Editor.move_cursor_left(root, cursor, metrics); + return; + } else { + try move_cursor_long_word_left(root, cursor, metrics); + } +} + +fn is_word_boundary_right(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + if (Editor.is_whitespace_at_cursor(root, cursor, metrics)) return false; + var next = cursor.*; + next.move_right(root, metrics) catch return true; + + const next_is_whitespace = Editor.is_whitespace_at_cursor(root, &next, metrics); + if (next_is_whitespace) return true; + + const curr_is_non_word = is_non_whitespace_or_eol_at_cursor(root, cursor, metrics); + const next_is_non_word = is_non_whitespace_or_eol_at_cursor(root, &next, metrics); + return curr_is_non_word != next_is_non_word; +} + +pub fn move_cursor_long_word_right(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { + try cursor.move_right(root, metrics); + Editor.move_cursor_right_until(root, cursor, is_long_word_boundary_left, metrics); +} + +fn is_long_word_boundary_right(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + if (Editor.is_whitespace_at_cursor(root, cursor, metrics)) return false; + var next = cursor.*; + next.move_right(root, metrics) catch return true; + + const next_is_whitespace = Editor.is_whitespace_at_cursor(root, &next, metrics); + if (next_is_whitespace) return true; + + const curr_is_non_word = is_non_whitespace_or_eol_at_cursor(root, cursor, metrics); + const next_is_non_word = is_non_whitespace_or_eol_at_cursor(root, &next, metrics); + return curr_is_non_word != next_is_non_word; +} + +pub fn move_cursor_long_word_right_end(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, is_long_word_boundary_right, metrics); + try cursor.move_right(root, metrics); +} diff --git a/test/tests.zig b/test/tests.zig index 49c5e96..af75213 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -1,6 +1,7 @@ const std = @import("std"); pub const buffer = @import("tests_buffer.zig"); pub const color = @import("tests_color.zig"); +pub const helix = @import("tests_helix.zig"); test { std.testing.refAllDecls(@This()); diff --git a/test/tests_helix.zig b/test/tests_helix.zig new file mode 100644 index 0000000..69d5628 --- /dev/null +++ b/test/tests_helix.zig @@ -0,0 +1,79 @@ +const std = @import("std"); +const Buffer = @import("Buffer"); +const Cursor = @import("Cursor"); +const helix = @import("helix"); + +// error: import of file outside module path +// const helix = @import("../src/tui/mode/helix.zig"); + +const ArrayList = std.ArrayList; +const a = std.testing.allocator; + +fn metrics() Buffer.Metrics { + return .{ + .ctx = undefined, + .egc_length = struct { + fn f(_: Buffer.Metrics, _: []const u8, colcount: *c_int, _: usize) usize { + colcount.* = 1; + return 1; + } + }.f, + .egc_chunk_width = struct { + fn f(_: Buffer.Metrics, chunk_: []const u8, _: usize) usize { + return chunk_.len; + } + }.f, + .egc_last = struct { + fn f(_: Buffer.Metrics, _: []const u8) []const u8 { + @panic("not implemented"); + } + }.f, + .tab_width = 8, + }; +} + +fn the_pos(buffer: Buffer, pos: u8) Cursor { + return buffer.root.byte_offset_to_line_and_col(pos, metrics(), .lf); +} + +test "word_movement" { + const W = helix.move_cursor_long_word_right; + const B = helix.move_cursor_long_word_left; + const E = helix.move_cursor_long_word_right_end; + const doc: []const u8 = + \\a small $% Test.here, with.things()to demo + \\ with surrounding.space a bb AA a small and long + \\ + \\ + \\nospace. + \\ try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 0 }, buffer.root.byte_offset_to_line_and_col(0, test_metrics(), eol_mode)); + \\ + \\ + \\ $$%. []{{}. dart de + \\da + ; + + //44 55 0 8 0 + // TODO: test selections. Parity with Helix + + var eol_mode: Buffer.EolMode = .lf; + var sanitized: bool = false; + const buffer = try Buffer.create(a); + defer buffer.deinit(); + buffer.update(try buffer.load_from_string(doc, &eol_mode, &sanitized)); + const root: Buffer.Root = buffer.root; + var c = Cursor{ .row = 0, .col = 0, .target = 0 }; + const t = the_pos; + + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 0 }, buffer.root.byte_offset_to_line_and_col(0, metrics(), eol_mode)); + try std.testing.expectEqual(try buffer.root.line_width(0, metrics()), 44); + try std.testing.expectEqual(try buffer.root.line_width(1, metrics()), 55); + try E(root, &c, metrics()); + try std.testing.expectEqual(c, t(buffer.*, 1)); + try B(root, &c, metrics()); + try std.testing.expectEqual(c, t(buffer.*, 0)); + try W(root, &c, metrics()); + try std.testing.expectEqual(c, t(buffer.*, 2)); + try B(root, &c, metrics()); + try std.testing.expectEqual(c, t(buffer.*, 1)); +} From a5dc6d8a43ac260180fddc239cb645834b28e587 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 8 Oct 2025 14:16:09 +0200 Subject: [PATCH 2/4] fix: build of helix_mode tests --- build.zig | 63 +------------------------------------------- src/tui/tui.zig | 8 ++++++ test/tests_helix.zig | 6 ++--- 3 files changed, 11 insertions(+), 66 deletions(-) diff --git a/build.zig b/build.zig index 107b584..8415849 100644 --- a/build.zig +++ b/build.zig @@ -621,67 +621,6 @@ pub fn build_exe( }, }); - const helix_mod = b.createModule(.{ - .root_source_file = b.path("src/tui/mode/helix.zig"), - }); - // const editor_mod = b.createModule(.{ - // .root_source_file = b.path("src/tui/editor.zig"), - // }); - // const Widget_mod = b.createModule(.{ - // .root_source_file = b.path("src/tui/Widget.zig"), - // }); - // const mainview_mod = b.createModule(.{ - // .root_source_file = b.path("src/tui/mainview.zig"), - // }); - // const WidgetList_mod = b.createModule(.{ - // .root_source_file = b.path("src/tui/WidgetList.zig"), - // }); - // const MessageFilter_mod = b.createModule(.{ - // .root_source_file = b.path("src/tui/MessageFilter.zig"), - // }); - // const editor_gutter_mod = b.createModule(.{ - // .root_source_file = b.path("src/tui/editor_gutter.zig"), - // }); - // const scrollbar_v_mod = b.createModule(.{ - // .root_source_file = b.path("src/tui//scrollbar_v.zig"), - // }); - // const palette_mod = b.createModule(.{ - // .root_source_file = b.path("src/tui/mode/overlay/palette.zig"), - // }); - // const open_recent_project_mod = b.createModule(.{ - // .root_source_file = b.path("src/tui/mode/overlay/open_recent_project.zig"), - // }); - - // const helix_test_run_cmd = blk: { - // const tests = b.addTest(.{ - // .root_module = b.createModule(.{ - // .root_source_file = b.path("src/tui/mode/helix.zig"), - // .target = target, - // .optimize = optimize, - // }), - // }); - // tests.root_module.addImport("cbor", cbor_mod); - // tests.root_module.addImport("command", command_mod); - // tests.root_module.addImport("EventHandler", EventHandler_mod); - // tests.root_module.addImport("input", input_mod); - // tests.root_module.addImport("thespian", thespian_mod); - // tests.root_module.addImport("log", log_mod); - // tests.root_module.addImport("tui", tui_mod); - // tests.root_module.addImport("helix", helix_mod); - // tests.root_module.addImport("editor", editor_mod); - // tests.root_module.addImport("Widget", Widget_mod); - // tests.root_module.addImport("WidgetList", WidgetList_mod); - // tests.root_module.addImport("mainview", mainview_mod); - // tests.root_module.addImport("Buffer", Buffer_mod); - // tests.root_module.addImport("MessageFilter", MessageFilter_mod); - // tests.root_module.addImport("editor_gutter", editor_gutter_mod); - // tests.root_module.addImport("scrollbar_v", scrollbar_v_mod); - // tests.root_module.addImport("palette", palette_mod); - // tests.root_module.addImport("open_recent_project", open_recent_project_mod); - // tests.root_module.addImport("renderer", renderer_mod); - // break :blk b.addRunArtifact(tests); - // }; - const exe_name = if (gui) "flow-gui" else "flow"; const exe = b.addExecutable(.{ @@ -793,7 +732,7 @@ pub fn build_exe( tests.root_module.addImport("log", log_mod); tests.root_module.addImport("Buffer", Buffer_mod); tests.root_module.addImport("color", color_mod); - tests.root_module.addImport("helix", helix_mod); + tests.root_module.addImport("tui", tui_mod); // b.installArtifact(tests); const test_run_cmd = b.addRunArtifact(tests); diff --git a/src/tui/tui.zig b/src/tui/tui.zig index e8e5a8c..5ddf392 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -19,6 +19,14 @@ const Widget = @import("Widget.zig"); const MessageFilter = @import("MessageFilter.zig"); const MainView = @import("mainview.zig"); +// exports for unittesting +pub const exports = struct { + pub const mode = struct { + pub const helix = @import("mode/helix.zig"); + }; + pub const editor = @import("editor.zig"); +}; + const Allocator = std.mem.Allocator; allocator: Allocator, diff --git a/test/tests_helix.zig b/test/tests_helix.zig index 69d5628..aafa12e 100644 --- a/test/tests_helix.zig +++ b/test/tests_helix.zig @@ -1,10 +1,8 @@ const std = @import("std"); const Buffer = @import("Buffer"); -const Cursor = @import("Cursor"); -const helix = @import("helix"); +const Cursor = @import("Buffer").Cursor; -// error: import of file outside module path -// const helix = @import("../src/tui/mode/helix.zig"); +const helix = @import("tui").exports.mode.helix; const ArrayList = std.ArrayList; const a = std.testing.allocator; From a6f5ffcdc544763db00a0a1ac4a36aa4d9fdbf84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20T=C3=A1mara?= Date: Wed, 8 Oct 2025 23:49:59 -0500 Subject: [PATCH 3/4] hx: add tests for some Helix mode movements --- build.zig | 1 + src/tui/mode/helix.zig | 4 +- test/tests_helix.zig | 157 +++++++++++++++++++++++++++++++---------- 3 files changed, 122 insertions(+), 40 deletions(-) diff --git a/build.zig b/build.zig index 8415849..a93af85 100644 --- a/build.zig +++ b/build.zig @@ -733,6 +733,7 @@ pub fn build_exe( tests.root_module.addImport("Buffer", Buffer_mod); tests.root_module.addImport("color", color_mod); tests.root_module.addImport("tui", tui_mod); + tests.root_module.addImport("command", command_mod); // b.installArtifact(tests); const test_run_cmd = b.addRunArtifact(tests); diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index c0f3a6b..19f05a9 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -454,7 +454,7 @@ const cmds_ = struct { pub const paste_after_meta: Meta = .{ .description = "Paste from clipboard after selection" }; }; -fn move_cursor_word_left_helix(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { +pub fn move_cursor_word_left_helix(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { try Editor.move_cursor_left(root, cursor, metrics); // Consume " " @@ -479,7 +479,7 @@ fn move_cursor_word_left_helix(root: Buffer.Root, cursor: *Cursor, metrics: Buff 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 { +pub 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); diff --git a/test/tests_helix.zig b/test/tests_helix.zig index aafa12e..ca09a08 100644 --- a/test/tests_helix.zig +++ b/test/tests_helix.zig @@ -1,12 +1,47 @@ const std = @import("std"); const Buffer = @import("Buffer"); const Cursor = @import("Buffer").Cursor; +const Result = @import("command").Result; const helix = @import("tui").exports.mode.helix; +const Editor = @import("tui").exports.editor.Editor; -const ArrayList = std.ArrayList; const a = std.testing.allocator; +fn apply_movements(movements: []const u8, root: Buffer.Root, cursor: *Cursor, the_metrics: Buffer.Metrics, row: usize, col: usize) Result { + for (movements) |move| { + switch (move) { + 'W' => { + try helix.move_cursor_long_word_right(root, cursor, the_metrics); + }, + 'B' => { + try helix.move_cursor_long_word_left(root, cursor, the_metrics); + }, + 'E' => { + try helix.move_cursor_long_word_right_end(root, cursor, the_metrics); + }, + 'w' => { + try Editor.move_cursor_word_right_vim(root, cursor, the_metrics); + }, + 'b' => { + try helix.move_cursor_word_left_helix(root, cursor, the_metrics); + }, + 'e' => { + try helix.move_cursor_word_right_end_helix(root, cursor, the_metrics); + }, + else => {}, + } + } + try std.testing.expectEqual(col, cursor.col); + try std.testing.expectEqual(row, cursor.row); +} + +const MoveExpected = struct { + moves: []const u8, + row: usize, + col: usize, +}; + fn metrics() Buffer.Metrics { return .{ .ctx = undefined, @@ -29,49 +64,95 @@ fn metrics() Buffer.Metrics { .tab_width = 8, }; } +const doc: []const u8 = + \\gawk '{print length($0) }' testflowhelixwbe.txt | tr '\n' ' ' + \\a small $% Test.here, with.things()to demo + \\ with surrounding.space a bb AA a small and long + \\ + \\ + \\nospace. + \\ try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 0 }, buffer.root.byte_offset_to_line_and_col(0, test_metrics(), eol_mode)); + \\ + \\ + \\ $$%. []{{}. dart de + \\da +; +//60 44 54 0 2 8 138 0 0 22 2 0 -fn the_pos(buffer: Buffer, pos: u8) Cursor { - return buffer.root.byte_offset_to_line_and_col(pos, metrics(), .lf); -} +var eol_mode: Buffer.EolMode = .lf; +var sanitized: bool = false; +var the_cursor = Cursor{ .row = 1, .col = 1, .target = 0 }; -test "word_movement" { - const W = helix.move_cursor_long_word_right; - const B = helix.move_cursor_long_word_left; - const E = helix.move_cursor_long_word_right_end; - const doc: []const u8 = - \\a small $% Test.here, with.things()to demo - \\ with surrounding.space a bb AA a small and long - \\ - \\ - \\nospace. - \\ try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 0 }, buffer.root.byte_offset_to_line_and_col(0, test_metrics(), eol_mode)); - \\ - \\ - \\ $$%. []{{}. dart de - \\da - ; +// To run a specific test +// zig build test -Dtest-filter=word_movement - //44 55 0 8 0 - // TODO: test selections. Parity with Helix - - var eol_mode: Buffer.EolMode = .lf; - var sanitized: bool = false; +test "words_movement" { const buffer = try Buffer.create(a); defer buffer.deinit(); buffer.update(try buffer.load_from_string(doc, &eol_mode, &sanitized)); const root: Buffer.Root = buffer.root; - var c = Cursor{ .row = 0, .col = 0, .target = 0 }; - const t = the_pos; + the_cursor.col = 1; + the_cursor.row = 0; - try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 0 }, buffer.root.byte_offset_to_line_and_col(0, metrics(), eol_mode)); - try std.testing.expectEqual(try buffer.root.line_width(0, metrics()), 44); - try std.testing.expectEqual(try buffer.root.line_width(1, metrics()), 55); - try E(root, &c, metrics()); - try std.testing.expectEqual(c, t(buffer.*, 1)); - try B(root, &c, metrics()); - try std.testing.expectEqual(c, t(buffer.*, 0)); - try W(root, &c, metrics()); - try std.testing.expectEqual(c, t(buffer.*, 2)); - try B(root, &c, metrics()); - try std.testing.expectEqual(c, t(buffer.*, 1)); + const movements: [12]MoveExpected = .{ + .{ .moves = "b", .row = 0, .col = 0 }, + .{ .moves = "w", .row = 0, .col = 5 }, + .{ .moves = "b", .row = 0, .col = 1 }, + // TODO: Review the following line, an Stop is raising + // .{ .moves = "bb", .row = 0, .col = 0 }, + .{ .moves = "ww", .row = 0, .col = 7 }, + .{ .moves = "bb", .row = 0, .col = 1 }, + .{ .moves = "www", .row = 0, .col = 13 }, + .{ .moves = "bbb", .row = 0, .col = 1 }, + .{ .moves = "wwww", .row = 0, .col = 19 }, + .{ .moves = "bbbb", .row = 0, .col = 1 }, + .{ .moves = "wb", .row = 0, .col = 1 }, + .{ .moves = "e", .row = 0, .col = 4 }, + .{ .moves = "b", .row = 0, .col = 1 }, + // TODO: b has a bug when at the end of the view, it's + // not getting back. + // + // TODO: Another bug detected is when there are multiple + // lines, b is not able to get to the first non + // newline. + }; + + for (movements) |move| { + try apply_movements(move.moves, root, &the_cursor, metrics(), move.row, move.col); + } +} + +test "long_words_movement" { + const buffer = try Buffer.create(a); + defer buffer.deinit(); + buffer.update(try buffer.load_from_string(doc, &eol_mode, &sanitized)); + const root: Buffer.Root = buffer.root; + the_cursor.col = 1; + the_cursor.row = 0; + + const movements: [12]MoveExpected = .{ + .{ .moves = "B", .row = 0, .col = 0 }, + .{ .moves = "W", .row = 0, .col = 5 }, + .{ .moves = "B", .row = 0, .col = 1 }, + // TODO: Review the following line, an Stop is raising + // .{ .moves = "BB", .row = 0, .col = 0 }, + .{ .moves = "WW", .row = 0, .col = 13 }, + .{ .moves = "BB", .row = 0, .col = 1 }, + .{ .moves = "WWW", .row = 0, .col = 24 }, + .{ .moves = "BBB", .row = 0, .col = 1 }, + .{ .moves = "WWWW", .row = 0, .col = 27 }, + .{ .moves = "BBBB", .row = 0, .col = 1 }, + // TODO: + // WWWWW should report 48, is reporting 49, when changing modes + // the others report 48. This is an specific hx mode + // .{ .moves = "WWWWW", .row = 0, .col = 48 }, + // Same bugs detected in b are in B + .{ .moves = "WB", .row = 0, .col = 1 }, + .{ .moves = "E", .row = 0, .col = 4 }, + .{ .moves = "B", .row = 0, .col = 1 }, + }; + + for (movements) |move| { + try apply_movements(move.moves, root, &the_cursor, metrics(), move.row, move.col); + } } From c7b46856bb1f4fedff30484ce8e569071980ab8c Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Oct 2025 09:32:20 +0200 Subject: [PATCH 4/4] refactor: explicity publish internal helix functions for unittests only We don't want internal functions in the mode specific extention modules becoming shared code. To avoid this, mark the functions as private and publish only through a structure marked clearly as for testing only. If these functions are useful as shared code they can be moved to the editor module or else where. --- src/tui/mode/helix.zig | 20 +++++++++++++++----- test/tests_helix.zig | 10 +++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index 19f05a9..ad20003 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -454,7 +454,7 @@ const cmds_ = struct { pub const paste_after_meta: Meta = .{ .description = "Paste from clipboard after selection" }; }; -pub fn move_cursor_word_left_helix(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { +fn move_cursor_word_left_helix(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { try Editor.move_cursor_left(root, cursor, metrics); // Consume " " @@ -479,7 +479,7 @@ pub fn move_cursor_word_left_helix(root: Buffer.Root, cursor: *Cursor, metrics: fn move_noop(_: Buffer.Root, _: *Cursor, _: Buffer.Metrics) error{Stop}!void {} -pub fn move_cursor_word_right_end_helix(root: Buffer.Root, cursor: *Cursor, metrics: 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); @@ -535,7 +535,7 @@ fn is_long_word_boundary_left(root: Buffer.Root, cursor: *const Cursor, metrics: return curr_is_non_word != next_is_non_word; } -pub fn move_cursor_long_word_left(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { +fn move_cursor_long_word_left(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { try Editor.move_cursor_left(root, cursor, metrics); // Consume " " @@ -571,7 +571,7 @@ fn is_word_boundary_right(root: Buffer.Root, cursor: *const Cursor, metrics: Buf return curr_is_non_word != next_is_non_word; } -pub fn move_cursor_long_word_right(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { +fn move_cursor_long_word_right(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { try cursor.move_right(root, metrics); Editor.move_cursor_right_until(root, cursor, is_long_word_boundary_left, metrics); } @@ -589,8 +589,18 @@ fn is_long_word_boundary_right(root: Buffer.Root, cursor: *const Cursor, metrics return curr_is_non_word != next_is_non_word; } -pub fn move_cursor_long_word_right_end(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { +fn move_cursor_long_word_right_end(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, is_long_word_boundary_right, metrics); try cursor.move_right(root, metrics); } + +const private = @This(); +// exports for unittests +pub const test_internal = struct { + pub const move_cursor_long_word_right = private.move_cursor_long_word_right; + pub const move_cursor_long_word_left = private.move_cursor_long_word_left; + pub const move_cursor_long_word_right_end = private.move_cursor_long_word_right_end; + pub const move_cursor_word_left_helix = private.move_cursor_word_left_helix; + pub const move_cursor_word_right_end_helix = private.move_cursor_word_right_end_helix; +}; \ No newline at end of file diff --git a/test/tests_helix.zig b/test/tests_helix.zig index ca09a08..f94e63a 100644 --- a/test/tests_helix.zig +++ b/test/tests_helix.zig @@ -12,22 +12,22 @@ fn apply_movements(movements: []const u8, root: Buffer.Root, cursor: *Cursor, th for (movements) |move| { switch (move) { 'W' => { - try helix.move_cursor_long_word_right(root, cursor, the_metrics); + try helix.test_internal.move_cursor_long_word_right(root, cursor, the_metrics); }, 'B' => { - try helix.move_cursor_long_word_left(root, cursor, the_metrics); + try helix.test_internal.move_cursor_long_word_left(root, cursor, the_metrics); }, 'E' => { - try helix.move_cursor_long_word_right_end(root, cursor, the_metrics); + try helix.test_internal.move_cursor_long_word_right_end(root, cursor, the_metrics); }, 'w' => { try Editor.move_cursor_word_right_vim(root, cursor, the_metrics); }, 'b' => { - try helix.move_cursor_word_left_helix(root, cursor, the_metrics); + try helix.test_internal.move_cursor_word_left_helix(root, cursor, the_metrics); }, 'e' => { - try helix.move_cursor_word_right_end_helix(root, cursor, the_metrics); + try helix.test_internal.move_cursor_word_right_end_helix(root, cursor, the_metrics); }, else => {}, }