Compare commits

...

4 commits

Author SHA1 Message Date
c7b46856bb 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.
2025-10-10 09:35:44 +02:00
Igor Támara
a6f5ffcdc5 hx: add tests for some Helix mode movements 2025-10-10 09:35:44 +02:00
a5dc6d8a43 fix: build of helix_mode tests 2025-10-10 09:35:44 +02:00
Igor Támara
a64d7c3afa hx: attempt to add tests in separate file 2025-10-10 09:35:44 +02:00
6 changed files with 310 additions and 2 deletions

View file

@ -712,6 +712,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 +722,7 @@ pub fn build_exe(
}),
.use_llvm = use_llvm,
.use_lld = use_llvm,
.filters = test_filters,
});
tests.pie = pie;
@ -730,6 +732,8 @@ 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("tui", tui_mod);
tests.root_module.addImport("command", command_mod);
// b.installArtifact(tests);
const test_run_cmd = b.addRunArtifact(tests);

View file

@ -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';
}

View file

@ -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,98 @@ 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;
}
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;
}
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;
}
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;
};

View file

@ -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,

View file

@ -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());

158
test/tests_helix.zig Normal file
View file

@ -0,0 +1,158 @@
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 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.test_internal.move_cursor_long_word_right(root, cursor, the_metrics);
},
'B' => {
try helix.test_internal.move_cursor_long_word_left(root, cursor, the_metrics);
},
'E' => {
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.test_internal.move_cursor_word_left_helix(root, cursor, the_metrics);
},
'e' => {
try helix.test_internal.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,
.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,
};
}
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
var eol_mode: Buffer.EolMode = .lf;
var sanitized: bool = false;
var the_cursor = Cursor{ .row = 1, .col = 1, .target = 0 };
// To run a specific test
// zig build test -Dtest-filter=word_movement
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;
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 = 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);
}
}