From 875816fdfcd3c176f8cd5976bcd7fbeecc332d88 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 8 Apr 2025 17:59:40 +0200 Subject: [PATCH 1/9] fix: don't match against legacy unshifted keys when we have extended input text closes #205 --- src/renderer/vaxis/input.zig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/renderer/vaxis/input.zig b/src/renderer/vaxis/input.zig index 15aaed9..3d4740f 100644 --- a/src/renderer/vaxis/input.zig +++ b/src/renderer/vaxis/input.zig @@ -2,6 +2,8 @@ const vaxis = @import("vaxis"); const meta = @import("std").meta; const utf8Encode = @import("std").unicode.utf8Encode; +const utf8Decode = @import("std").unicode.utf8Decode; +const utf8ValidateSlice = @import("std").unicode.utf8ValidateSlice; const FormatOptions = @import("std").fmt.FormatOptions; pub const key = vaxis.Key; @@ -74,6 +76,7 @@ pub const KeyEvent = struct { } pub fn eql_unshifted(self: @This(), other: @This()) bool { + if (self.text.len > 0 or other.text.len > 0) return false; const self_mods = self.mods_no_caps(); const other_mods = other.mods_no_caps(); return self.key_unshifted == other.key_unshifted and self_mods == other_mods; @@ -117,7 +120,7 @@ pub const KeyEvent = struct { pub fn from_message( event_: Event, keypress_: Key, - keypress_shifted: Key, + keypress_shifted_: Key, text: []const u8, modifiers: Mods, ) @This() { @@ -128,6 +131,11 @@ pub const KeyEvent = struct { key.left_alt, key.right_alt => modifiers & ~mod.alt, else => modifiers, }; + + var keypress_shifted: Key = keypress_shifted_; + if (text.len > 0 and text.len < 5 and utf8ValidateSlice(text)) blk: { + keypress_shifted = utf8Decode(text) catch break :blk; + } const keypress, const mods = if (keypress_shifted == keypress_) map_key_to_unshifed_legacy(keypress_shifted, mods_) else From 5f05fd97e619f7b868a926b2c2bf8d15ac2ed089 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 8 Apr 2025 18:00:49 +0200 Subject: [PATCH 2/9] refactor: remove duplicate implementation of command.get_id --- src/command.zig | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/command.zig b/src/command.zig index 26a4dcc..e2b5ef4 100644 --- a/src/command.zig +++ b/src/command.zig @@ -150,13 +150,8 @@ pub fn execute(id: ID, ctx: Context) tp.result { } pub fn get_id(name: []const u8) ?ID { - for (commands.items) |cmd| { - if (cmd) |p| - if (std.mem.eql(u8, p.name, name)) - return p.id; - } - tp.trace(tp.channel.debug, .{ "command", "get_id", "failed", name }); - return null; + var id: ?ID = null; + return get_id_cache(name, &id); } pub fn get_name(id: ID) ?[]const u8 { @@ -178,6 +173,7 @@ pub fn get_id_cache(name: []const u8, id: *?ID) ?ID { return p.id; }; } + tp.trace(tp.channel.debug, .{ "command", "get_id_cache", "failed", name }); return null; } From 6946cb4010c47ed1c88eeb868fc26a1e1e9c7e21 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 8 Apr 2025 18:02:39 +0200 Subject: [PATCH 3/9] feat: re-add support for integer command arguments closes #182 --- src/keybind/keybind.zig | 67 ++++++++++++++++++++++++++++++++++++----- src/tui/tui.zig | 1 + 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/keybind/keybind.zig b/src/keybind/keybind.zig index 5394792..0a69f6c 100644 --- a/src/keybind/keybind.zig +++ b/src/keybind/keybind.zig @@ -25,6 +25,31 @@ const builtin_keybinds = std.static_string_map.StaticStringMap([]const u8).initC .{ "emacs", @embedFile("builtin/emacs.json") }, }); +var integer_argument: ?usize = null; + +var commands: Commands = undefined; +const Commands = command.Collection(struct { + pub const Target = void; + const Ctx = command.Context; + const Meta = command.Metadata; + const Result = command.Result; + + pub fn add_integer_argument_digit(_: *void, ctx: Ctx) Result { + var digit: usize = undefined; + if (!try ctx.args.match(.{tp.extract(&digit)})) + return error.InvalidIntegerParameterArgument; + if (digit > 9) + return error.InvalidIntegerParameterDigit; + integer_argument = if (integer_argument) |x| x * 10 + digit else digit; + } + pub const add_integer_argument_digit_meta: Meta = .{ .arguments = &.{.integer} }; +}); + +pub fn init() !void { + var v: void = {}; + try commands.init(&v); +} + pub fn mode(mode_name: []const u8, allocator: std.mem.Allocator, opts: anytype) !Mode { return Handler.create(mode_name, allocator, opts) catch |e| switch (e) { error.NotFound => return error.Stop, @@ -86,10 +111,18 @@ pub const Mode = struct { selection_style: SelectionStyle, init_command: ?Command = null, deinit_command: ?Command = null, + initialized: bool = false, + + pub fn run_init(self: *Mode) void { + if (self.initialized) return; + self.initialized = true; + clear_integer_argument(); + if (self.init_command) |init_command| init_command.execute_const(); + } pub fn deinit(self: *Mode) void { - if (self.deinit_command) |deinit_| - deinit_.execute_const(); + if (self.deinit_command) |deinit_command| + deinit_command.execute_const(); self.allocator.free(self.mode); self.input_handler.deinit(); if (self.event_handler) |eh| eh.deinit(); @@ -144,11 +177,11 @@ fn get_or_load_namespace(namespace_name: []const u8) LoadError!*const Namespace pub fn set_namespace(namespace_name: []const u8) LoadError!void { const new_namespace = try get_or_load_namespace(namespace_name); if (globals.current_namespace) |old_namespace| - if (old_namespace.deinit_command) |deinit| - deinit.execute_const(); + if (old_namespace.deinit_command) |deinit_command| + deinit_command.execute_const(); globals.current_namespace = new_namespace; - if (new_namespace.init_command) |init| - init.execute_const(); + if (new_namespace.init_command) |init_command| + init_command.execute_const(); } fn get_mode_binding_set(mode_name: []const u8, insert_command: []const u8) LoadError!*const BindingSet { @@ -262,10 +295,17 @@ const Command = struct { }; var buf: [2048]u8 = undefined; @memcpy(buf[0..self.args.len], self.args); + if (integer_argument) |int_arg| { + if (cbor.match(self.args, .{}) catch false and has_integer_argument(id)) { + integer_argument = null; + try command.execute(id, command.fmt(.{int_arg})); + return; + } + } try command.execute(id, .{ .args = .{ .buf = buf[0..self.args.len] } }); } - pub fn execute_const(self: *const @This()) void { + fn execute_const(self: *const @This()) void { var buf: [2048]u8 = undefined; @memcpy(buf[0..self.args.len], self.args); command.executeName(self.command, .{ .args = .{ .buf = buf[0..self.args.len] } }) catch |e| { @@ -275,6 +315,11 @@ const Command = struct { }; } + fn has_integer_argument(id: command.ID) bool { + const args = command.get_arguments(id) orelse return false; + return args.len == 1 and args[0] == .integer; + } + fn load(allocator: std.mem.Allocator, tokens: []const std.json.Value) (parse_flow.ParseError || parse_vim.ParseError)!Command { if (tokens.len == 0) return error.InvalidFormat; var state: enum { command, args } = .command; @@ -724,6 +769,14 @@ pub fn current_key_event_sequence_fmt() KeyEventSequenceFmt { return .{ .key_events = globals.current_sequence.items }; } +pub fn current_integer_argument() ?usize { + return integer_argument; +} + +pub fn clear_integer_argument() void { + integer_argument = null; +} + const expectEqual = std.testing.expectEqual; const parse_test_cases = .{ diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 206b970..d8a3144 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -154,6 +154,7 @@ fn init(allocator: Allocator) InitError!*Self { try frame_clock.start(); try self.commands.init(self); + try keybind.init(); errdefer self.deinit(); switch (builtin.os.tag) { .windows => { From d594e42f1abe8cd796628db1cd2b846d81828827 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 8 Apr 2025 18:04:02 +0200 Subject: [PATCH 4/9] feat: display pending integer argument in keybind widget --- src/tui/status/keybindstate.zig | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/tui/status/keybindstate.zig b/src/tui/status/keybindstate.zig index 1c2feee..3253c19 100644 --- a/src/tui/status/keybindstate.zig +++ b/src/tui/status/keybindstate.zig @@ -29,9 +29,12 @@ pub fn layout(_: *Self) Widget.Layout { var buf: [256]u8 = undefined; var fbs = std.io.fixedBufferStream(&buf); const writer = fbs.writer(); - writer.print("{}", .{keybind.current_key_event_sequence_fmt()}) catch {}; + writer.print(" ", .{}) catch {}; + if (keybind.current_integer_argument()) |integer_argument| + writer.print("{}", .{integer_argument}) catch {}; + writer.print("{} ", .{keybind.current_key_event_sequence_fmt()}) catch {}; const len = fbs.getWritten().len; - return .{ .static = if (len > 0) len + 2 else 0 }; + return .{ .static = if (len > 0) len else 0 }; } pub fn render(self: *Self, theme: *const Widget.Theme) bool { @@ -41,6 +44,9 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { self.plane.set_style(theme.statusbar); self.plane.fill(" "); self.plane.home(); - _ = self.plane.print(" {} ", .{keybind.current_key_event_sequence_fmt()}) catch {}; + _ = self.plane.print(" ", .{}) catch {}; + if (keybind.current_integer_argument()) |integer_argument| + _ = self.plane.print("{}", .{integer_argument}) catch {}; + _ = self.plane.print("{} ", .{keybind.current_key_event_sequence_fmt()}) catch {}; return false; } From 545b470e0314c2e07ff1df60767164ce1cdee0b2 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 8 Apr 2025 18:04:31 +0200 Subject: [PATCH 5/9] fix: call configured init_commands for all mode types --- src/tui/tui.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index d8a3144..8bd8cbd 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -659,6 +659,7 @@ fn enter_overlay_mode(self: *Self, mode: type) command.Result { if (self.input_mode_outer_) |_| try cmds.exit_overlay_mode(self, .{}); self.input_mode_outer_ = self.input_mode_; self.input_mode_ = try mode.create(self.allocator); + if (self.input_mode_) |*m| m.run_init(); refresh_hover(); } @@ -674,8 +675,7 @@ fn enter_input_mode(self: *Self, new_mode: Mode) command.Result { self.input_mode_ = null; } self.input_mode_ = new_mode; - if (new_mode.init_command) |cmd| - cmd.execute_const(); + if (self.input_mode_) |*m| m.run_init(); } fn refresh_input_mode(self: *Self) command.Result { @@ -690,6 +690,7 @@ fn refresh_input_mode(self: *Self) command.Result { self.input_mode_ = null; } self.input_mode_ = new_mode; + if (self.input_mode_) |*m| m.run_init(); } fn set_theme_by_name(self: *Self, name: []const u8) !void { @@ -951,6 +952,7 @@ const cmds = struct { self.input_mode_outer_ = self.input_mode_; self.input_mode_ = input_mode_; self.mini_mode_ = mini_mode_; + if (self.input_mode_) |*m| m.run_init(); } pub fn exit_mini_mode(self: *Self, _: Ctx) Result { From 94b222fb1b47395279f44b648abda9ac77d77ffe Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 8 Apr 2025 18:06:12 +0200 Subject: [PATCH 6/9] feat: add flow mode keybindings for setting integer argument --- src/keybind/builtin/flow.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index c6e4b21..2eaa4bf 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -190,6 +190,17 @@ ["«", "smart_insert_pair", "«", "»"], ["»", "smart_insert_pair_close", "«", "»"], + ["alt+0", "add_integer_argument_digit", 0], + ["alt+1", "add_integer_argument_digit", 1], + ["alt+2", "add_integer_argument_digit", 2], + ["alt+3", "add_integer_argument_digit", 3], + ["alt+4", "add_integer_argument_digit", 4], + ["alt+5", "add_integer_argument_digit", 5], + ["alt+6", "add_integer_argument_digit", 6], + ["alt+7", "add_integer_argument_digit", 7], + ["alt+8", "add_integer_argument_digit", 8], + ["alt+9", "add_integer_argument_digit", 9], + ["left_control", "enable_jump_mode"], ["right_control", "enable_jump_mode"], ["left_alt", "enable_fast_scroll"], From 6bed62e51da05e0aab64ef845bd0b34caa469955 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 8 Apr 2025 18:07:21 +0200 Subject: [PATCH 7/9] feat: add support for repeat integer arguments to many commands --- src/tui/editor.zig | 409 +++++++++++++++++++++++------------------ src/tui/mode/helix.zig | 85 +++++---- 2 files changed, 272 insertions(+), 222 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 359f749..5790736 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -1743,7 +1743,7 @@ pub const Editor = struct { try move(root, &cursel.cursor, metrics); } - fn with_cursors_const(self: *Self, root: Buffer.Root, move: cursor_operator_const) error{Stop}!void { + fn with_cursors_const_once(self: *Self, root: Buffer.Root, move: cursor_operator_const) error{Stop}!void { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { cursel.disable_selection(root, self.metrics); try with_cursor_const(root, move, cursel, self.metrics); @@ -1751,6 +1751,18 @@ pub const Editor = struct { self.collapse_cursors(); } + fn with_cursors_const_repeat(self: *Self, root: Buffer.Root, move: cursor_operator_const, ctx: Context) error{Stop}!void { + var repeat: usize = 1; + _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; + while (repeat > 0) : (repeat -= 1) { + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + cursel.disable_selection(root, self.metrics); + try with_cursor_const(root, move, cursel, self.metrics); + }; + self.collapse_cursors(); + } + } + fn with_cursor_const_arg(root: Buffer.Root, move: cursor_operator_const_arg, cursel: *CurSel, ctx: Context, metrics: Buffer.Metrics) error{Stop}!void { try move(root, &cursel.cursor, ctx, metrics); } @@ -1788,7 +1800,7 @@ pub const Editor = struct { cursel.check_selection(root, metrics); } - pub fn with_selections_const(self: *Self, root: Buffer.Root, move: cursor_operator_const) error{Stop}!void { + pub fn with_selections_const_once(self: *Self, root: Buffer.Root, move: cursor_operator_const) error{Stop}!void { var someone_stopped = false; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| with_selection_const(root, move, cursel, self.metrics) catch { @@ -1798,6 +1810,21 @@ pub const Editor = struct { return if (someone_stopped) error.Stop else {}; } + pub fn with_selections_const_repeat(self: *Self, root: Buffer.Root, move: cursor_operator_const, ctx: Context) error{Stop}!void { + var someone_stopped = false; + var repeat: usize = 1; + _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; + while (repeat > 0) : (repeat -= 1) { + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| + with_selection_const(root, move, cursel, self.metrics) catch { + someone_stopped = true; + }; + self.collapse_cursors(); + if (someone_stopped) break; + } + return if (someone_stopped) error.Stop else {}; + } + fn with_selection_const_arg(root: Buffer.Root, move: cursor_operator_const_arg, cursel: *CurSel, ctx: Context, metrics: Buffer.Metrics) error{Stop}!void { const sel = try cursel.enable_selection(root, metrics); try move(root, &sel.end, ctx, metrics); @@ -1831,28 +1858,11 @@ pub const Editor = struct { return if (someone_stopped) error.Stop else {}; } - fn with_cursel(root: Buffer.Root, op: cursel_operator, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { - return op(root, cursel, allocator); - } - - fn with_cursels(self: *Self, root_: Buffer.Root, move: cursel_operator, allocator: Allocator) error{Stop}!Buffer.Root { - var root = root_; - var someone_stopped = false; - for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - root = with_cursel(root, move, cursel, allocator) catch ret: { - someone_stopped = true; - break :ret root; - }; - }; - self.collapse_cursors(); - return if (someone_stopped) error.Stop else root; - } - fn with_cursel_mut(self: *Self, root: Buffer.Root, op: cursel_operator_mut, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { return op(self, root, cursel, allocator); } - fn with_cursels_mut(self: *Self, root_: Buffer.Root, move: cursel_operator_mut, allocator: Allocator) error{Stop}!Buffer.Root { + fn with_cursels_mut_once(self: *Self, root_: Buffer.Root, move: cursel_operator_mut, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; var someone_stopped = false; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { @@ -1865,6 +1875,24 @@ pub const Editor = struct { return if (someone_stopped) error.Stop else root; } + fn with_cursels_mut_repeat(self: *Self, root_: Buffer.Root, move: cursel_operator_mut, allocator: Allocator, ctx: Context) error{Stop}!Buffer.Root { + var root = root_; + var someone_stopped = false; + var repeat: usize = 1; + _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; + while (repeat > 0) : (repeat -= 1) { + for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + root = self.with_cursel_mut(root, move, cursel, allocator) catch ret: { + someone_stopped = true; + break :ret root; + }; + }; + self.collapse_cursors(); + if (someone_stopped) break; + } + return if (someone_stopped) error.Stop else root; + } + fn with_cursel_const(root: Buffer.Root, op: cursel_operator_const, cursel: *CurSel) error{Stop}!void { return op(root, cursel); } @@ -2889,9 +2917,9 @@ pub const Editor = struct { } pub const cut_to_end_vim_meta: Meta = .{ .description = "Cut to end of line (vim)" }; - pub fn join_next_line(self: *Self, _: Context) Result { + pub fn join_next_line(self: *Self, ctx: Context) Result { const b = try self.buf_for_update(); - try self.with_cursors_const(b.root, move_cursor_end); + try self.with_cursors_const_repeat(b.root, move_cursor_end, ctx); var root = try self.delete_to(move_cursor_right_until_non_whitespace, b.root, b.allocator); for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { root = try self.insert(root, cursel, " ", b.allocator); @@ -2899,21 +2927,21 @@ pub const Editor = struct { try self.update_buf(root); self.clamp(); } - pub const join_next_line_meta: Meta = .{ .description = "Join next line" }; + pub const join_next_line_meta: Meta = .{ .description = "Join next line", .arguments = &.{.integer} }; - pub fn move_left(self: *Self, _: Context) Result { + pub fn move_left(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_left) catch {}; + self.with_cursors_const_repeat(root, move_cursor_left, ctx) catch {}; self.clamp(); } - pub const move_left_meta: Meta = .{ .description = "Move cursor left" }; + pub const move_left_meta: Meta = .{ .description = "Move cursor left", .arguments = &.{.integer} }; - pub fn move_right(self: *Self, _: Context) Result { + pub fn move_right(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_right) catch {}; + self.with_cursors_const_repeat(root, move_cursor_right, ctx) catch {}; self.clamp(); } - pub const move_right_meta: Meta = .{ .description = "Move cursor right" }; + pub const move_right_meta: Meta = .{ .description = "Move cursor right", .arguments = &.{.integer} }; fn move_cursor_left_vim(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { move_cursor_left_unless(root, cursor, is_eol_left, metrics); @@ -2923,19 +2951,19 @@ pub const Editor = struct { move_cursor_right_unless(root, cursor, is_eol_right_vim, metrics); } - pub fn move_left_vim(self: *Self, _: Context) Result { + pub fn move_left_vim(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_left_vim) catch {}; + self.with_cursors_const_repeat(root, move_cursor_left_vim, ctx) catch {}; self.clamp(); } - pub const move_left_vim_meta: Meta = .{ .description = "Move cursor left (vim)" }; + pub const move_left_vim_meta: Meta = .{ .description = "Move cursor left (vim)", .arguments = &.{.integer} }; - pub fn move_right_vim(self: *Self, _: Context) Result { + pub fn move_right_vim(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_right_vim) catch {}; + self.with_cursors_const_repeat(root, move_cursor_right_vim, ctx) catch {}; self.clamp(); } - pub const move_right_vim_meta: Meta = .{ .description = "Move cursor right (vim)" }; + pub const move_right_vim_meta: Meta = .{ .description = "Move cursor right (vim)", .arguments = &.{.integer} }; fn move_cursor_word_begin(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void { if (is_non_word_char_at_cursor(root, cursor, metrics)) { @@ -3010,40 +3038,40 @@ pub const Editor = struct { move_cursor_right_until(root, cursor, is_non_whitespace_at_cursor, metrics); } - pub fn move_word_left(self: *Self, _: Context) Result { + pub fn move_word_left(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_word_left) catch {}; + self.with_cursors_const_repeat(root, move_cursor_word_left, ctx) catch {}; self.clamp(); } - pub const move_word_left_meta: Meta = .{ .description = "Move cursor left by word" }; + pub const move_word_left_meta: Meta = .{ .description = "Move cursor left by word", .arguments = &.{.integer} }; - pub fn move_word_left_vim(self: *Self, _: Context) Result { + pub fn move_word_left_vim(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_word_left_vim) catch {}; + self.with_cursors_const_repeat(root, move_cursor_word_left_vim, ctx) catch {}; self.clamp(); } - pub const move_word_left_vim_meta: Meta = .{ .description = "Move cursor left by word (vim)" }; + pub const move_word_left_vim_meta: Meta = .{ .description = "Move cursor left by word (vim)", .arguments = &.{.integer} }; - pub fn move_word_right(self: *Self, _: Context) Result { + pub fn move_word_right(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_word_right) catch {}; + self.with_cursors_const_repeat(root, move_cursor_word_right, ctx) catch {}; self.clamp(); } - pub const move_word_right_meta: Meta = .{ .description = "Move cursor right by word" }; + pub const move_word_right_meta: Meta = .{ .description = "Move cursor right by word", .arguments = &.{.integer} }; - pub fn move_word_right_vim(self: *Self, _: Context) Result { + pub fn move_word_right_vim(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_word_right_vim) catch {}; + self.with_cursors_const_repeat(root, move_cursor_word_right_vim, ctx) catch {}; self.clamp(); } - pub const move_word_right_vim_meta: Meta = .{ .description = "Move cursor right by word (vim)" }; + pub const move_word_right_vim_meta: Meta = .{ .description = "Move cursor right by word (vim)", .arguments = &.{.integer} }; - pub fn move_word_right_end_vim(self: *Self, _: Context) Result { + pub fn move_word_right_end_vim(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_word_right_end_vim) catch {}; + self.with_cursors_const_repeat(root, move_cursor_word_right_end_vim, ctx) catch {}; self.clamp(); } - pub const move_word_right_end_vim_meta: Meta = .{ .description = "Move cursor right by end of word (vim)" }; + pub const move_word_right_end_vim_meta: Meta = .{ .description = "Move cursor right by end of word (vim)", .arguments = &.{.integer} }; fn move_cursor_to_char_left(root: Buffer.Root, cursor: *Cursor, ctx: Context, metrics: Buffer.Metrics) error{Stop}!void { var egc: []const u8 = undefined; @@ -3101,70 +3129,82 @@ pub const Editor = struct { } pub const move_or_select_to_char_right_meta: Meta = .{ .arguments = &.{.integer} }; - pub fn move_up(self: *Self, _: Context) Result { + pub fn move_up(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_up) catch {}; + self.with_cursors_const_repeat(root, move_cursor_up, ctx) catch {}; self.clamp(); } - pub const move_up_meta: Meta = .{ .description = "Move cursor up" }; + pub const move_up_meta: Meta = .{ .description = "Move cursor up", .arguments = &.{.integer} }; - pub fn move_up_vim(self: *Self, _: Context) Result { + pub fn move_up_vim(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_up_vim) catch {}; + self.with_cursors_const_repeat(root, move_cursor_up_vim, ctx) catch {}; self.clamp(); } - pub const move_up_vim_meta: Meta = .{ .description = "Move cursor up (vim)" }; + pub const move_up_vim_meta: Meta = .{ .description = "Move cursor up (vim)", .arguments = &.{.integer} }; - pub fn add_cursor_up(self: *Self, _: Context) Result { - try self.push_cursor(); - const primary = self.get_primary(); + pub fn add_cursor_up(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - move_cursor_up(root, &primary.cursor, self.metrics) catch {}; - self.clamp(); - } - pub const add_cursor_up_meta: Meta = .{ .description = "Add cursor up" }; - - pub fn move_down(self: *Self, _: Context) Result { - const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_down) catch {}; - self.clamp(); - } - pub const move_down_meta: Meta = .{ .description = "Move cursor down" }; - - pub fn move_down_vim(self: *Self, _: Context) Result { - const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_down_vim) catch {}; - self.clamp(); - } - pub const move_down_vim_meta: Meta = .{ .description = "Move cursor down (vim)" }; - - pub fn add_cursor_down(self: *Self, _: Context) Result { - try self.push_cursor(); - const primary = self.get_primary(); - const root = try self.buf_root(); - move_cursor_down(root, &primary.cursor, self.metrics) catch {}; - self.clamp(); - } - pub const add_cursor_down_meta: Meta = .{ .description = "Add cursor down" }; - - pub fn add_cursor_next_match(self: *Self, _: Context) Result { - try self.send_editor_jump_source(); - if (self.matches.items.len == 0) { - const root = self.buf_root() catch return; - self.with_cursors_const(root, move_cursor_word_begin) catch {}; - try self.with_selections_const(root, move_cursor_word_end); - } else if (self.get_next_match(self.get_primary().cursor)) |match| { + var repeat: usize = 1; + _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; + while (repeat > 0) : (repeat -= 1) { try self.push_cursor(); const primary = self.get_primary(); - const root = self.buf_root() catch return; - primary.selection = match.to_selection(); - match.has_selection = true; - primary.cursor.move_to(root, match.end.row, match.end.col, self.metrics) catch return; + move_cursor_up(root, &primary.cursor, self.metrics) catch {}; + } + self.clamp(); + } + pub const add_cursor_up_meta: Meta = .{ .description = "Add cursor up", .arguments = &.{.integer} }; + + pub fn move_down(self: *Self, ctx: Context) Result { + const root = try self.buf_root(); + self.with_cursors_const_repeat(root, move_cursor_down, ctx) catch {}; + self.clamp(); + } + pub const move_down_meta: Meta = .{ .description = "Move cursor down", .arguments = &.{.integer} }; + + pub fn move_down_vim(self: *Self, ctx: Context) Result { + const root = try self.buf_root(); + self.with_cursors_const_repeat(root, move_cursor_down_vim, ctx) catch {}; + self.clamp(); + } + pub const move_down_vim_meta: Meta = .{ .description = "Move cursor down (vim)", .arguments = &.{.integer} }; + + pub fn add_cursor_down(self: *Self, ctx: Context) Result { + var repeat: usize = 1; + _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; + while (repeat > 0) : (repeat -= 1) { + try self.push_cursor(); + const primary = self.get_primary(); + const root = try self.buf_root(); + move_cursor_down(root, &primary.cursor, self.metrics) catch {}; + } + self.clamp(); + } + pub const add_cursor_down_meta: Meta = .{ .description = "Add cursor down", .arguments = &.{.integer} }; + + pub fn add_cursor_next_match(self: *Self, ctx: Context) Result { + try self.send_editor_jump_source(); + var repeat: usize = 1; + _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; + while (repeat > 0) : (repeat -= 1) { + if (self.matches.items.len == 0) { + const root = self.buf_root() catch return; + self.with_cursors_const_once(root, move_cursor_word_begin) catch {}; + try self.with_selections_const_once(root, move_cursor_word_end); + } else if (self.get_next_match(self.get_primary().cursor)) |match| { + try self.push_cursor(); + const primary = self.get_primary(); + const root = self.buf_root() catch return; + primary.selection = match.to_selection(); + match.has_selection = true; + primary.cursor.move_to(root, match.end.row, match.end.col, self.metrics) catch return; + } } self.clamp(); try self.send_editor_jump_destination(); } - pub const add_cursor_next_match_meta: Meta = .{ .description = "Add cursor at next highlighted match" }; + pub const add_cursor_next_match_meta: Meta = .{ .description = "Add cursor at next highlighted match", .arguments = &.{.integer} }; pub fn add_cursor_all_matches(self: *Self, _: Context) Result { if (self.matches.items.len == 0) return; @@ -3229,13 +3269,13 @@ pub const Editor = struct { return root; } - pub fn pull_up(self: *Self, _: Context) Result { + pub fn pull_up(self: *Self, ctx: Context) Result { const b = try self.buf_for_update(); - const root = try self.with_cursels_mut(b.root, pull_cursel_up, b.allocator); + const root = try self.with_cursels_mut_repeat(b.root, pull_cursel_up, b.allocator, ctx); try self.update_buf(root); self.clamp(); } - pub const pull_up_meta: Meta = .{ .description = "Pull line up" }; + pub const pull_up_meta: Meta = .{ .description = "Pull line up", .arguments = &.{.integer} }; fn pull_cursel_down(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; @@ -3256,13 +3296,13 @@ pub const Editor = struct { return root; } - pub fn pull_down(self: *Self, _: Context) Result { + pub fn pull_down(self: *Self, ctx: Context) Result { const b = try self.buf_for_update(); - const root = try self.with_cursels_mut(b.root, pull_cursel_down, b.allocator); + const root = try self.with_cursels_mut_repeat(b.root, pull_cursel_down, b.allocator, ctx); try self.update_buf(root); self.clamp(); } - pub const pull_down_meta: Meta = .{ .description = "Pull line down" }; + pub const pull_down_meta: Meta = .{ .description = "Pull line down", .arguments = &.{.integer} }; fn dupe_cursel_up(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; @@ -3278,13 +3318,13 @@ pub const Editor = struct { return root; } - pub fn dupe_up(self: *Self, _: Context) Result { + pub fn dupe_up(self: *Self, ctx: Context) Result { const b = try self.buf_for_update(); - const root = try self.with_cursels_mut(b.root, dupe_cursel_up, b.allocator); + const root = try self.with_cursels_mut_repeat(b.root, dupe_cursel_up, b.allocator, ctx); try self.update_buf(root); self.clamp(); } - pub const dupe_up_meta: Meta = .{ .description = "Duplicate line or selection up/backwards" }; + pub const dupe_up_meta: Meta = .{ .description = "Duplicate line or selection up/backwards", .arguments = &.{.integer} }; fn dupe_cursel_down(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; @@ -3299,13 +3339,13 @@ pub const Editor = struct { return root; } - pub fn dupe_down(self: *Self, _: Context) Result { + pub fn dupe_down(self: *Self, ctx: Context) Result { const b = try self.buf_for_update(); - const root = try self.with_cursels_mut(b.root, dupe_cursel_down, b.allocator); + const root = try self.with_cursels_mut_repeat(b.root, dupe_cursel_down, b.allocator, ctx); try self.update_buf(root); self.clamp(); } - pub const dupe_down_meta: Meta = .{ .description = "Duplicate line or selection down/forwards" }; + pub const dupe_down_meta: Meta = .{ .description = "Duplicate line or selection down/forwards", .arguments = &.{.integer} }; fn toggle_cursel_prefix(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { var root = root_; @@ -3330,7 +3370,7 @@ pub const Editor = struct { @memcpy(self.prefix_buf[0..prefix.len], prefix); self.prefix = self.prefix_buf[0..prefix.len]; const b = try self.buf_for_update(); - const root = try self.with_cursels_mut(b.root, toggle_cursel_prefix, b.allocator); + const root = try self.with_cursels_mut_once(b.root, toggle_cursel_prefix, b.allocator); try self.update_buf(root); } pub const toggle_prefix_meta: Meta = .{ .arguments = &.{.string} }; @@ -3366,12 +3406,12 @@ pub const Editor = struct { } else return try self.indent_cursor(root_, cursel.cursor, allocator); } - pub fn indent(self: *Self, _: Context) Result { + pub fn indent(self: *Self, ctx: Context) Result { const b = try self.buf_for_update(); - const root = try self.with_cursels_mut(b.root, indent_cursel, b.allocator); + const root = try self.with_cursels_mut_repeat(b.root, indent_cursel, b.allocator, ctx); try self.update_buf(root); } - pub const indent_meta: Meta = .{ .description = "Indent current line" }; + pub const indent_meta: Meta = .{ .description = "Indent current line", .arguments = &.{.integer} }; fn unindent_cursor(self: *Self, root: Buffer.Root, cursor: *Cursor, cursor_protect: ?*Cursor, allocator: Allocator) error{Stop}!Buffer.Root { var newroot = root; @@ -3415,32 +3455,32 @@ pub const Editor = struct { self.cursels = self.cursels_saved.clone() catch return; } - pub fn unindent(self: *Self, _: Context) Result { + pub fn unindent(self: *Self, ctx: Context) Result { const b = try self.buf_for_update(); errdefer self.restore_cursels(); const previous_len = self.cursels.items.len; - const root = try self.with_cursels_mut(b.root, unindent_cursel, b.allocator); + const root = try self.with_cursels_mut_repeat(b.root, unindent_cursel, b.allocator, ctx); if (self.cursels.items.len != previous_len) self.restore_cursels(); try self.update_buf(root); } - pub const unindent_meta: Meta = .{ .description = "Unindent current line" }; + pub const unindent_meta: Meta = .{ .description = "Unindent current line", .arguments = &.{.integer} }; - pub fn move_scroll_up(self: *Self, _: Context) Result { + pub fn move_scroll_up(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_up) catch {}; + self.with_cursors_const_repeat(root, move_cursor_up, ctx) catch {}; self.view.move_up() catch {}; self.clamp(); } - pub const move_scroll_up_meta: Meta = .{ .description = "Move and scroll up" }; + pub const move_scroll_up_meta: Meta = .{ .description = "Move and scroll up", .arguments = &.{.integer} }; - pub fn move_scroll_down(self: *Self, _: Context) Result { + pub fn move_scroll_down(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - self.with_cursors_const(root, move_cursor_down) catch {}; + self.with_cursors_const_repeat(root, move_cursor_down, ctx) catch {}; self.view.move_down(root) catch {}; self.clamp(); } - pub const move_scroll_down_meta: Meta = .{ .description = "Move and scroll down" }; + pub const move_scroll_down_meta: Meta = .{ .description = "Move and scroll down", .arguments = &.{.integer} }; pub fn move_scroll_left(self: *Self, _: Context) Result { self.view.move_left() catch {}; @@ -3526,21 +3566,21 @@ pub const Editor = struct { pub fn smart_move_begin(self: *Self, _: Context) Result { const root = try self.buf_root(); - try self.with_cursors_const(root, smart_move_cursor_begin); + try self.with_cursors_const_once(root, smart_move_cursor_begin); self.clamp(); } pub const smart_move_begin_meta: Meta = .{ .description = "Move cursor to beginning of line (smart)" }; pub fn move_begin(self: *Self, _: Context) Result { const root = try self.buf_root(); - try self.with_cursors_const(root, move_cursor_begin); + try self.with_cursors_const_once(root, move_cursor_begin); self.clamp(); } pub const move_begin_meta: Meta = .{ .description = "Move cursor to beginning of line" }; pub fn move_end(self: *Self, _: Context) Result { const root = try self.buf_root(); - try self.with_cursors_const(root, move_cursor_end); + try self.with_cursors_const_once(root, move_cursor_end); self.clamp(); } pub const move_end_meta: Meta = .{ .description = "Move cursor to end of line" }; @@ -3601,6 +3641,7 @@ pub const Editor = struct { pub fn cancel(self: *Self, _: Context) Result { self.cancel_all_selections(); self.cancel_all_matches(); + @import("keybind").clear_integer_argument(); } pub const cancel_meta: Meta = .{ .description = "Cancel current action" }; @@ -3620,98 +3661,98 @@ pub const Editor = struct { } pub const select_line_vim_meta: Meta = .{ .description = "Select the line around the cursor (vim)" }; - pub fn select_up(self: *Self, _: Context) Result { + pub fn select_up(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_up); + try self.with_selections_const_repeat(root, move_cursor_up, ctx); self.clamp(); } - pub const select_up_meta: Meta = .{ .description = "Select up" }; + pub const select_up_meta: Meta = .{ .description = "Select up", .arguments = &.{.integer} }; - pub fn select_down(self: *Self, _: Context) Result { + pub fn select_down(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_down); + try self.with_selections_const_repeat(root, move_cursor_down, ctx); self.clamp(); } - pub const select_down_meta: Meta = .{ .description = "Select down" }; + pub const select_down_meta: Meta = .{ .description = "Select down", .arguments = &.{.integer} }; - pub fn select_scroll_up(self: *Self, _: Context) Result { + pub fn select_scroll_up(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_up); + try self.with_selections_const_repeat(root, move_cursor_up, ctx); self.view.move_up() catch {}; self.clamp(); } - pub const select_scroll_up_meta: Meta = .{ .description = "Select and scroll up" }; + pub const select_scroll_up_meta: Meta = .{ .description = "Select and scroll up", .arguments = &.{.integer} }; - pub fn select_scroll_down(self: *Self, _: Context) Result { + pub fn select_scroll_down(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_down); + try self.with_selections_const_repeat(root, move_cursor_down, ctx); self.view.move_down(root) catch {}; self.clamp(); } - pub const select_scroll_down_meta: Meta = .{ .description = "Select and scroll down" }; + pub const select_scroll_down_meta: Meta = .{ .description = "Select and scroll down", .arguments = &.{.integer} }; - pub fn select_left(self: *Self, _: Context) Result { + pub fn select_left(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_left); + try self.with_selections_const_repeat(root, move_cursor_left, ctx); self.clamp(); } - pub const select_left_meta: Meta = .{ .description = "Select left" }; + pub const select_left_meta: Meta = .{ .description = "Select left", .arguments = &.{.integer} }; - pub fn select_right(self: *Self, _: Context) Result { + pub fn select_right(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_right); + try self.with_selections_const_repeat(root, move_cursor_right, ctx); self.clamp(); } - pub const select_right_meta: Meta = .{ .description = "Select right" }; + pub const select_right_meta: Meta = .{ .description = "Select right", .arguments = &.{.integer} }; - pub fn select_word_left(self: *Self, _: Context) Result { + pub fn select_word_left(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_word_left); + try self.with_selections_const_repeat(root, move_cursor_word_left, ctx); self.clamp(); } - pub const select_word_left_meta: Meta = .{ .description = "Select left by word" }; + pub const select_word_left_meta: Meta = .{ .description = "Select left by word", .arguments = &.{.integer} }; - pub fn select_word_left_vim(self: *Self, _: Context) Result { + pub fn select_word_left_vim(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_word_left_vim); + try self.with_selections_const_repeat(root, move_cursor_word_left_vim, ctx); self.clamp(); } - pub const select_word_left_vim_meta: Meta = .{ .description = "Select left by word (vim)" }; + pub const select_word_left_vim_meta: Meta = .{ .description = "Select left by word (vim)", .arguments = &.{.integer} }; - pub fn select_word_right(self: *Self, _: Context) Result { + pub fn select_word_right(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_word_right); + try self.with_selections_const_repeat(root, move_cursor_word_right, ctx); self.clamp(); } - pub const select_word_right_meta: Meta = .{ .description = "Select right by word" }; + pub const select_word_right_meta: Meta = .{ .description = "Select right by word", .arguments = &.{.integer} }; - pub fn select_word_right_vim(self: *Self, _: Context) Result { + pub fn select_word_right_vim(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_word_right_vim); + try self.with_selections_const_repeat(root, move_cursor_word_right_vim, ctx); self.clamp(); } - pub const select_word_right_vim_meta: Meta = .{ .description = "Select right by word (vim)" }; + pub const select_word_right_vim_meta: Meta = .{ .description = "Select right by word (vim)", .arguments = &.{.integer} }; - pub fn select_word_right_end_vim(self: *Self, _: Context) Result { + pub fn select_word_right_end_vim(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_word_right_end_vim); + try self.with_selections_const_repeat(root, move_cursor_word_right_end_vim, ctx); self.clamp(); } - pub const select_word_right_end_vim_meta: Meta = .{ .description = "Select right by end of word (vim)" }; + pub const select_word_right_end_vim_meta: Meta = .{ .description = "Select right by end of word (vim)", .arguments = &.{.integer} }; - pub fn select_word_begin(self: *Self, _: Context) Result { + pub fn select_word_begin(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_word_begin); + try self.with_selections_const_repeat(root, move_cursor_word_begin, ctx); self.clamp(); } - pub const select_word_begin_meta: Meta = .{ .description = "Select to beginning of word" }; + pub const select_word_begin_meta: Meta = .{ .description = "Select to beginning of word", .arguments = &.{.integer} }; - pub fn select_word_end(self: *Self, _: Context) Result { + pub fn select_word_end(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_word_end); + try self.with_selections_const_repeat(root, move_cursor_word_end, ctx); self.clamp(); } - pub const select_word_end_meta: Meta = .{ .description = "Select to end of word" }; + pub const select_word_end_meta: Meta = .{ .description = "Select to end of word", .arguments = &.{.integer} }; pub fn select_to_char_left(self: *Self, ctx: Context) Result { const root = try self.buf_root(); @@ -3737,44 +3778,44 @@ pub const Editor = struct { } pub const select_to_char_right_meta: Meta = .{ .arguments = &.{.integer} }; - pub fn select_begin(self: *Self, _: Context) Result { + pub fn select_begin(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_begin); + try self.with_selections_const_repeat(root, move_cursor_begin, ctx); self.clamp(); } - pub const select_begin_meta: Meta = .{ .description = "Select to beginning of line" }; + pub const select_begin_meta: Meta = .{ .description = "Select to beginning of line", .arguments = &.{.integer} }; - pub fn smart_select_begin(self: *Self, _: Context) Result { + pub fn smart_select_begin(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, smart_move_cursor_begin); + try self.with_selections_const_repeat(root, smart_move_cursor_begin, ctx); self.clamp(); } - pub const smart_select_begin_meta: Meta = .{ .description = "Select to beginning of line (smart)" }; + pub const smart_select_begin_meta: Meta = .{ .description = "Select to beginning of line (smart)", .arguments = &.{.integer} }; - pub fn select_end(self: *Self, _: Context) Result { + pub fn select_end(self: *Self, ctx: Context) Result { const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_end); + try self.with_selections_const_repeat(root, move_cursor_end, ctx); self.clamp(); } - pub const select_end_meta: Meta = .{ .description = "Select to end of line" }; + pub const select_end_meta: Meta = .{ .description = "Select to end of line", .arguments = &.{.integer} }; - pub fn select_buffer_begin(self: *Self, _: Context) Result { + pub fn select_buffer_begin(self: *Self, ctx: Context) Result { try self.send_editor_jump_source(); const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_buffer_begin); + try self.with_selections_const_repeat(root, move_cursor_buffer_begin, ctx); self.clamp(); try self.send_editor_jump_destination(); } - pub const select_buffer_begin_meta: Meta = .{ .description = "Select to start of file" }; + pub const select_buffer_begin_meta: Meta = .{ .description = "Select to start of file", .arguments = &.{.integer} }; - pub fn select_buffer_end(self: *Self, _: Context) Result { + pub fn select_buffer_end(self: *Self, ctx: Context) Result { try self.send_editor_jump_source(); const root = try self.buf_root(); - try self.with_selections_const(root, move_cursor_buffer_end); + try self.with_selections_const_repeat(root, move_cursor_buffer_end, ctx); self.clamp(); try self.send_editor_jump_destination(); } - pub const select_buffer_end_meta: Meta = .{ .description = "Select to end of file" }; + pub const select_buffer_end_meta: Meta = .{ .description = "Select to end of file", .arguments = &.{.integer} }; pub fn select_page_up(self: *Self, _: Context) Result { try self.send_editor_jump_source(); @@ -5369,7 +5410,7 @@ pub const Editor = struct { pub fn to_upper(self: *Self, _: Context) Result { const b = try self.buf_for_update(); - const root = try self.with_cursels_mut(b.root, to_upper_cursel, b.allocator); + const root = try self.with_cursels_mut_once(b.root, to_upper_cursel, b.allocator); try self.update_buf(root); self.clamp(); } @@ -5397,7 +5438,7 @@ pub const Editor = struct { pub fn to_lower(self: *Self, _: Context) Result { const b = try self.buf_for_update(); - const root = try self.with_cursels_mut(b.root, to_lower_cursel, b.allocator); + const root = try self.with_cursels_mut_once(b.root, to_lower_cursel, b.allocator); try self.update_buf(root); self.clamp(); } @@ -5444,7 +5485,7 @@ pub const Editor = struct { pub fn switch_case(self: *Self, _: Context) Result { const b = try self.buf_for_update(); - const root = try self.with_cursels_mut(b.root, switch_case_cursel, b.allocator); + const root = try self.with_cursels_mut_once(b.root, switch_case_cursel, b.allocator); try self.update_buf(root); self.clamp(); } diff --git a/src/tui/mode/helix.zig b/src/tui/mode/helix.zig index bb7d250..e0d0e88 100644 --- a/src/tui/mode/helix.zig +++ b/src/tui/mode/helix.zig @@ -1,5 +1,6 @@ const std = @import("std"); const log = @import("log"); +const tp = @import("thespian"); const location_history = @import("location_history"); const command = @import("command"); const cmd = command.executeName; @@ -97,7 +98,7 @@ const cmds_ = struct { } pub const extend_line_below_meta: Meta = .{ .description = "Select current line, if already selected, extend to next line" }; - pub fn move_next_word_start(_: *void, _: Ctx) Result { + pub fn move_next_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(); @@ -106,13 +107,13 @@ const cmds_ = struct { cursel.selection = null; }; - ed.with_selections_const(root, Editor.move_cursor_word_right_vim) catch {}; + ed.with_selections_const_repeat(root, Editor.move_cursor_word_right_vim, ctx) catch {}; ed.clamp(); } - pub const move_next_word_start_meta: Meta = .{ .description = "Move next word start" }; + pub const move_next_word_start_meta: Meta = .{ .description = "Move next word start", .arguments = &.{.integer} }; - pub fn move_prev_word_start(_: *void, _: Ctx) Result { + pub fn move_prev_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(); @@ -121,10 +122,10 @@ const cmds_ = struct { cursel.selection = null; }; - ed.with_selections_const(root, move_cursor_word_left_helix) catch {}; + ed.with_selections_const_repeat(root, move_cursor_word_left_helix, ctx) catch {}; ed.clamp(); } - pub const move_prev_word_start_meta: Meta = .{ .description = "Move previous word start" }; + pub const move_prev_word_start_meta: Meta = .{ .description = "Move previous word start", .arguments = &.{.integer} }; pub fn cut_forward_internal_inclusive(_: *void, _: Ctx) Result { const mv = tui.mainview() orelse return; @@ -137,56 +138,64 @@ 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) Result { + 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(); - for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - const sel = try cursel.enable_selection(root, ed.metrics); + var repeat: usize = 1; + _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; + while (repeat > 0) : (repeat -= 1) { + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + const sel = try cursel.enable_selection(root, ed.metrics); + + // handling left to right transition + const sel_begin: i32 = @intCast(sel.begin.col); + const sel_end: i32 = @intCast(sel.end.col); + if ((sel_begin - sel_end) == 1 and sel.begin.row == sel.end.row) { + try Editor.move_cursor_right(root, &sel.end, ed.metrics); + sel.begin.col -= 1; + } - // handling left to right transition - const sel_begin: i32 = @intCast(sel.begin.col); - const sel_end: i32 = @intCast(sel.end.col); - if ((sel_begin - sel_end) == 1 and sel.begin.row == sel.end.row) { try Editor.move_cursor_right(root, &sel.end, ed.metrics); - sel.begin.col -= 1; - } - - try Editor.move_cursor_right(root, &sel.end, ed.metrics); - cursel.cursor = sel.end; - cursel.check_selection(root, ed.metrics); - }; + cursel.cursor = sel.end; + cursel.check_selection(root, ed.metrics); + }; + } ed.clamp(); } - pub const select_right_helix_meta: Meta = .{ .description = "Select right" }; + pub const select_right_helix_meta: Meta = .{ .description = "Select right", .arguments = &.{.integer} }; - pub fn select_left_helix(_: *void, _: Ctx) Result { + 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(); - for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - if (cursel.selection == null) { - cursel.selection = Selection.from_cursor(&cursel.cursor); - try cursel.selection.?.begin.move_right(root, ed.metrics); - } - if (cursel.selection) |*sel| { - try Editor.move_cursor_left(root, &sel.end, ed.metrics); - cursel.cursor = sel.end; - - if (sel.begin.col == sel.end.col and sel.begin.row == sel.end.row) { - try sel.begin.move_right(root, ed.metrics); + var repeat: usize = 1; + _ = ctx.args.match(.{tp.extract(&repeat)}) catch false; + while (repeat > 0) : (repeat -= 1) { + for (ed.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { + if (cursel.selection == null) { + cursel.selection = Selection.from_cursor(&cursel.cursor); + try cursel.selection.?.begin.move_right(root, ed.metrics); + } + if (cursel.selection) |*sel| { try Editor.move_cursor_left(root, &sel.end, ed.metrics); cursel.cursor = sel.end; - } - } - cursel.check_selection(root, ed.metrics); - }; + if (sel.begin.col == sel.end.col and sel.begin.row == sel.end.row) { + try sel.begin.move_right(root, ed.metrics); + try Editor.move_cursor_left(root, &sel.end, ed.metrics); + cursel.cursor = sel.end; + } + } + + cursel.check_selection(root, ed.metrics); + }; + } ed.clamp(); } - pub const select_left_helix_meta: Meta = .{ .description = "Select left" }; + pub const select_left_helix_meta: Meta = .{ .description = "Select left", .arguments = &.{.integer} }; pub fn select_to_char_right_helix(_: *void, ctx: Ctx) Result { const mv = tui.mainview() orelse return; From ec483d34d54e7479488fb15f531896cc1d7889db Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 8 Apr 2025 18:08:52 +0200 Subject: [PATCH 8/9] feat: add emacs mode keybindings for setting integer argument --- src/keybind/builtin/emacs.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/keybind/builtin/emacs.json b/src/keybind/builtin/emacs.json index b7349ec..b6f14bd 100644 --- a/src/keybind/builtin/emacs.json +++ b/src/keybind/builtin/emacs.json @@ -47,6 +47,17 @@ ["ctrl+x ctrl+r", "open_recent"], ["ctrl+space", "enter_mode", "select"], + ["alt+0", "add_integer_argument_digit", 0], + ["alt+1", "add_integer_argument_digit", 1], + ["alt+2", "add_integer_argument_digit", 2], + ["alt+3", "add_integer_argument_digit", 3], + ["alt+4", "add_integer_argument_digit", 4], + ["alt+5", "add_integer_argument_digit", 5], + ["alt+6", "add_integer_argument_digit", 6], + ["alt+7", "add_integer_argument_digit", 7], + ["alt+8", "add_integer_argument_digit", 8], + ["alt+9", "add_integer_argument_digit", 9], + ["ctrl+c l = =", "format"], ["ctrl+c l = r", "format"], ["ctrl+c l g g", "goto_definition"], From ae815043d9f915a887a9b4c873aad44b3c943ee8 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 8 Apr 2025 18:23:40 +0200 Subject: [PATCH 9/9] feat: add vim mode keybindings for setting integer argument --- src/keybind/builtin/vim.json | 14 ++++++++++++-- src/tui/mode/vim.zig | 8 ++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index 020f386..9aa765b 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -41,7 +41,6 @@ ["n", "goto_next_match"], ["N", "goto_prev_match"], - ["0", "move_begin"], ["^", "smart_move_begin"], ["$", "move_end"], [":", "open_command_palette"], @@ -90,7 +89,18 @@ ["f", "move_to_char", "move_to_char_right"], ["", ["move_down"], ["move_begin"]], - ["", ["move_down"], ["move_begin"]] + ["", ["move_down"], ["move_begin"]], + + ["0", "move_begin_or_add_integer_argument_zero"], + ["1", "add_integer_argument_digit", 1], + ["2", "add_integer_argument_digit", 2], + ["3", "add_integer_argument_digit", 3], + ["4", "add_integer_argument_digit", 4], + ["5", "add_integer_argument_digit", 5], + ["6", "add_integer_argument_digit", 6], + ["7", "add_integer_argument_digit", 7], + ["8", "add_integer_argument_digit", 8], + ["9", "add_integer_argument_digit", 9] ] }, "visual": { diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index 26fc44e..f427e80 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -46,6 +46,14 @@ const cmds_ = struct { } pub const @"wq!_meta": Meta = .{ .description = "wq! (write file and quit without saving)" }; + pub fn move_begin_or_add_integer_argument_zero(_: *void, _: Ctx) Result { + return if (@import("keybind").current_integer_argument()) |_| + command.executeName("add_integer_argument_digit", command.fmt(.{0})) + else + command.executeName("move_begin", .{}); + } + pub const move_begin_or_add_integer_argument_zero_meta: Meta = .{ .description = "Move cursor to beginning of line (vim)" }; + pub fn enter_mode_at_next_char(self: *void, ctx: Ctx) Result { _ = self; // autofix _ = ctx; // autofix