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)); +}