From 631e8fca410599f029c05d02695455a7c045dcd5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 27 Nov 2025 16:45:42 +0100 Subject: [PATCH] refactor: reduce duplication of context getting code in helix mode --- src/tui/mode/helix.zig | 227 +++++++++++++++++++---------------------- src/tui/tui.zig | 2 +- 2 files changed, 108 insertions(+), 121 deletions(-) diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index ee70840..65a0db4 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -7,6 +7,7 @@ const command = @import("command"); const cmd = command.executeName; const tui = @import("../tui.zig"); +const MainView = tui.MainView; const Editor = @import("../editor.zig").Editor; const CurSel = @import("../editor.zig").CurSel; const Buffer = @import("Buffer"); @@ -29,11 +30,12 @@ pub fn deinit() void { } const Commands = command.Collection(cmds_); +const Ctx = command.Context; +const Meta = command.Metadata; +const Result = command.Result; + const cmds_ = struct { pub const Target = void; - const Ctx = command.Context; - const Meta = command.Metadata; - const Result = command.Result; pub fn w(_: *void, _: Ctx) Result { try cmd("save_file", .{}); @@ -201,9 +203,7 @@ const cmds_ = struct { pub const save_selection_meta: Meta = .{ .description = "Save current selection to location history" }; pub fn split_selection_on_newline(_: *void, _: Ctx) Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = try ed.buf_root(); + const ed, const root = get_buf() orelse return; const cursels = try ed.cursels.toOwnedSlice(ed.allocator); defer ed.allocator.free(cursels); for (cursels) |*cursel_| if (cursel_.*) |*cursel| { @@ -214,19 +214,14 @@ const cmds_ = struct { pub const split_selection_on_newline_meta: Meta = .{ .description = "Add cursor to each line in selection helix" }; pub fn match_brackets(_: *void, ctx: Ctx) Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = ed.buf_root() catch return; + const ed, const root = get_buf() orelse return; try ed.with_cursels_const_once_arg(root, &match_bracket, ctx); ed.clamp(); } pub const match_brackets_meta: Meta = .{ .description = "Goto matching bracket" }; pub fn extend_line_below(_: *void, ctx: Ctx) Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = try ed.buf_root(); - + const ed, const root = get_buf() orelse return; var repeat: usize = 1; _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; while (repeat > 0) : (repeat -= 1) { @@ -265,73 +260,6 @@ 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_cursels_const_repeat(move_cursor_prev_word_start, ctx); } @@ -374,9 +302,7 @@ const cmds_ = struct { pub const extend_next_long_word_end_meta: Meta = .{ .description = "Extend 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; - const b = try ed.buf_for_update(); + const ed, const b = get_buf_for_update() orelse return; tui.clipboard_start_group(); const root = try ed.cut_to(move_noop, b.root); try ed.update_buf(root); @@ -385,10 +311,7 @@ const cmds_ = struct { pub const cut_forward_internal_inclusive_meta: Meta = .{ .description = "Cut next character to internal clipboard (inclusive)" }; pub fn select_right_helix(_: *void, ctx: Ctx) Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = try ed.buf_root(); - + const ed, const root = get_buf() orelse return; var repeat: usize = 1; _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; while (repeat > 0) : (repeat -= 1) { @@ -413,10 +336,7 @@ const cmds_ = struct { pub const select_right_helix_meta: Meta = .{ .description = "Select right", .arguments = &.{.integer} }; pub fn select_left_helix(_: *void, ctx: Ctx) Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = try ed.buf_root(); - + const ed, const root = get_buf() orelse return; var repeat: usize = 1; _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; while (repeat > 0) : (repeat -= 1) { @@ -484,12 +404,10 @@ const cmds_ = struct { pub const extend_to_char_right_helix_meta: Meta = .{ .description = "Extend Selection to char right" }; pub fn select_textobject_inner(_: *void, ctx: Ctx) Result { - var action: []const u8 = ""; + const ed, const root = get_buf() orelse return error.Stop; + var action: []const u8 = ""; if (!try ctx.args.match(.{tp.extract(&action)})) return error.Stop; - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = ed.buf_root() catch return; if (std.mem.eql(u8, action, "w")) { try ed.with_cursels_const(root, select_inner_word, ed.metrics); @@ -503,12 +421,10 @@ const cmds_ = struct { pub const select_textobject_inner_meta: Meta = .{ .description = "select inside object helix" }; pub fn select_textobject_around(_: *void, ctx: Ctx) Result { - var action: []const u8 = ""; + const ed, const root = get_buf() orelse return; + var action: []const u8 = ""; if (!try ctx.args.match(.{tp.extract(&action)})) return error.Stop; - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = ed.buf_root() catch return; if (std.mem.eql(u8, action, "w")) { try ed.with_cursels_const(root, select_around_word, ed.metrics); @@ -522,10 +438,7 @@ const cmds_ = struct { pub const select_textobject_around_meta: Meta = .{ .description = "select around object helix" }; pub fn copy_helix(_: *void, _: Ctx) Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = ed.buf_root() catch return; - + const ed, const root = get_buf() orelse return; tui.clipboard_start_group(); for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| if (cursel.selection) |sel| @@ -551,9 +464,8 @@ const cmds_ = struct { pub const paste_clipboard_before_meta: Meta = .{ .description = "Paste from clipboard before selection" }; pub fn replace_with_character_helix(_: *void, ctx: Ctx) Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - var root = ed.buf_root() catch return; + const ed, const b = get_buf_for_update() orelse return; + var root = b.root; root = try ed.with_cursels_mut_once_arg(root, replace_cursel_with_character, ed.allocator, ctx); try ed.update_buf(root); ed.clamp(); @@ -562,6 +474,87 @@ const cmds_ = struct { pub const replace_with_character_helix_meta: Meta = .{ .description = "Replace with character" }; }; +fn get_context() ?struct { *MainView, *Editor } { + const mv = tui.mainview() orelse return null; + const ed = mv.get_active_editor() orelse return null; + return .{ mv, ed }; +} + +fn get_buf() ?struct { *Editor, Buffer.Root } { + _, const ed = get_context() orelse return null; + return .{ ed, ed.buf_root() catch return null }; +} + +fn get_buf_for_update() ?struct { *Editor, *const Buffer } { + _, const ed = get_context() orelse return null; + return .{ ed, ed.buf_for_update() catch return null }; +} + +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 ed, const root = get_buf() orelse return; + try ed.with_cursels_const_repeat(root, move, ctx); + ed.clamp(); +} + fn match_bracket(root: Buffer.Root, cursel: *CurSel, ctx: command.Context, metrics: Buffer.Metrics) error{Stop}!void { var symbol: []const u8 = undefined; const mode: enum { helix_sel_mode, helix_nor_mode } = if ((ctx.args.match(.{tp.extract(&symbol)}) catch false) and @@ -599,9 +592,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 { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = try ed.buf_root(); + const ed, const root = get_buf() orelse return; // NOR mode moves n words selecting the last one var repeat: usize = 0; @@ -624,6 +615,8 @@ fn move_to_word(ctx: command.Context, move: Editor.cursor_operator_const, direct sel.begin = sel.end; } cursel.cursor = sel.end; + if (direction == .forwards) + sel.end.move_right(root, ed.metrics) catch {}; cursel.selection = sel; }; ed.with_selections_const_repeat(root, move, command.fmt(.{1})) catch {}; @@ -631,9 +624,7 @@ fn move_to_word(ctx: command.Context, move: Editor.cursor_operator_const, direct } fn extend_to_word(ctx: command.Context, move: Editor.cursor_operator_const, _: Direction) command.Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = try ed.buf_root(); + const ed, const root = get_buf() orelse return; var repeat: usize = 1; _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; @@ -652,9 +643,7 @@ fn extend_to_word(ctx: command.Context, move: Editor.cursor_operator_const, _: D } fn to_char_helix(ctx: command.Context, move: Editor.cursel_operator_mut_once_arg) command.Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; - const root = ed.buf_root() catch return; + const ed, const root = get_buf() orelse return; try ed.with_cursels_const_once_arg(root, move, ctx); ed.clamp(); } @@ -1229,8 +1218,9 @@ const pasting_function = @TypeOf(insert_before); const find_char_function = @TypeOf(move_cursor_to_char_left_beyond_eol); fn paste_helix(ctx: command.Context, do_paste: pasting_function) command.Result { - const mv = tui.mainview() orelse return; - const ed = mv.get_active_editor() orelse return; + const ed, const b = get_buf_for_update() orelse return; + var root = b.root; + var text_: []const u8 = undefined; const clipboard: []const tui.ClipboardEntry = if (ctx.args.buf.len > 0 and try ctx.args.match(.{tp.extract(&text_)})) @@ -1243,9 +1233,6 @@ fn paste_helix(ctx: command.Context, do_paste: pasting_function) command.Result return; } - const b = try ed.buf_for_update(); - var root = b.root; - // Chunks from clipboard are paired to selections // If more selections than chunks in the clipboard, the exceding selections // use the last chunk in the clipboard diff --git a/src/tui/tui.zig b/src/tui/tui.zig index a61f193..5621184 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -18,7 +18,7 @@ const syntax = @import("syntax"); const Widget = @import("Widget.zig"); const MessageFilter = @import("MessageFilter.zig"); -const MainView = @import("mainview.zig"); +pub const MainView = @import("mainview.zig"); // exports for unittesting pub const exports = struct {