summaryrefslogtreecommitdiff
path: root/src/modal.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/modal.zig')
-rw-r--r--src/modal.zig176
1 files changed, 167 insertions, 9 deletions
diff --git a/src/modal.zig b/src/modal.zig
index 3827d11f..cc7f1ebd 100644
--- a/src/modal.zig
+++ b/src/modal.zig
@@ -15,6 +15,8 @@ pub const Normal = struct {
prefix_replace,
prefix_next,
prefix_prev,
+ prefix_register,
+ prefix_view_sticky,
goto_file_start,
goto_last_line,
@@ -27,6 +29,8 @@ pub const Normal = struct {
goto_view_top,
goto_view_center,
goto_view_bottom,
+ goto_last_modification,
+ goto_word,
goto_definition,
goto_declaration,
goto_type_definition,
@@ -36,6 +40,7 @@ pub const Normal = struct {
view_top,
view_center,
view_bottom,
+ view_middle,
view_scroll_down,
view_scroll_up,
@@ -49,6 +54,13 @@ pub const Normal = struct {
add_newline,
goto_diagnostic,
goto_diagnostic_end,
+ goto_function,
+ goto_class,
+ goto_parameter,
+ goto_comment,
+ goto_test,
+ goto_entry,
+ goto_xml_element,
move_left,
move_right,
@@ -96,6 +108,17 @@ pub const Normal = struct {
trim_sels,
select_regex,
split_regex,
+ keep_regex,
+ remove_regex,
+ ensure_forward,
+ expand_node,
+ shrink_node,
+ next_sibling,
+ prev_sibling,
+ all_siblings,
+ all_children,
+ parent_node_end,
+ parent_node_start,
delete,
delete_noyank,
@@ -108,6 +131,10 @@ pub const Normal = struct {
to_lowercase,
to_uppercase,
join_lines,
+ join_select_space,
+ rotate_contents_fwd,
+ rotate_contents_back,
+ align_selections,
indent,
unindent,
format,
@@ -123,6 +150,10 @@ pub const Normal = struct {
pipe_selection_to,
insert_output,
append_output,
+ keep_pipe,
+ macro_record,
+ macro_replay,
+ repeat_insert,
search,
search_next,
search_prev,
@@ -156,6 +187,7 @@ pub const Normal = struct {
replace,
next,
prev,
+ register,
};
pub const MatchSub = enum(u8) {
@@ -172,9 +204,14 @@ pub const Normal = struct {
prefix: Prefix = .none,
match_sub: MatchSub = .none,
held_char: u21 = 0,
+ /// `"<reg>`: the register the next command names, 0 for none
+ register: u21 = 0,
+ /// `Z`: the view keys stay armed until Esc (helix's sticky view)
+ sticky: bool = false,
+ /// Drop what is pending; sticky view mode stays armed through it.
pub fn clear(state: *State) void {
- state.* = .{};
+ state.* = if (state.sticky) .{ .sticky = true, .prefix = .view } else .{};
}
};
@@ -188,13 +225,22 @@ pub const Normal = struct {
insert,
/// `A-!` — no stdin, and the output is appended AFTER each selection.
append,
+ /// `$` — stdin is the selection; the output is discarded, and only the
+ /// selections the command exits 0 on are kept.
+ keep,
/// Do the selections become stdin? helix's `pipe` flag.
pub fn pipes(b: PipeBehavior) bool {
- return b == .replace or b == .ignore;
+ return b == .replace or b == .ignore or b == .keep;
}
};
+ /// helix's tree-sitter selection keys: where a range goes in the tree
+ /// helix's tree-sitter textobjects, by what their queries call them
+ pub const Object = enum { function, class, parameter, comment, @"test", entry, xml_element };
+ pub const NodeWalk = enum { expand, shrink, next_sibling, prev_sibling, all_siblings, all_children, parent_end, parent_start };
+ /// what the s/S/K/Alt-K prompt does with its pattern's matches
+ pub const RegexMode = enum { select, split, keep, remove };
pub const Scope = enum { once, per_selection };
pub const Direction = enum { backward, forward };
pub const Motion = enum {
@@ -221,8 +267,9 @@ pub const Normal = struct {
view_top,
view_center,
view_bottom,
+ last_modification,
};
- pub const View = enum { top, center, bottom, scroll_down, scroll_up };
+ pub const View = enum { top, center, bottom, middle, scroll_down, scroll_up };
pub const Find = enum { forward, backward, till_forward, till_backward };
pub const Line = enum { start, end, first_nonws };
pub const Page = enum { half_down, half_up, down, up };
@@ -247,6 +294,7 @@ pub const Normal = struct {
merge,
merge_consecutive,
trim,
+ ensure_forward,
};
pub const Edit = enum {
delete,
@@ -260,6 +308,10 @@ pub const Normal = struct {
lowercase,
uppercase,
join_lines,
+ join_select_space,
+ rotate_contents_forward,
+ rotate_contents_backward,
+ align_selections,
indent,
unindent,
comment_toggle,
@@ -295,7 +347,7 @@ pub const Normal = struct {
insert: struct { kind: Insert, count: u32 },
select: struct { kind: Select, count: u32 },
multi: struct { kind: Multi, count: u32 },
- select_regex: bool, // false = select, true = split
+ select_regex: RegexMode,
edit: struct { kind: Edit, count: u32 },
lsp: Lsp,
adjust_number: i64,
@@ -304,6 +356,12 @@ pub const Normal = struct {
pipe_selection: PipeBehavior,
search,
search_step: Direction,
+ goto_word,
+ macro_record,
+ macro_replay: u32,
+ node: NodeWalk,
+ object_jump: struct { object: Object, direction: Direction, count: u32 },
+ repeat_insert: u32,
pub fn scope(value: Action) Scope {
return switch (value) {
@@ -315,9 +373,22 @@ pub const Normal = struct {
.pipe_selection,
.search,
.search_step,
+ .macro_record,
+ .macro_replay,
+ .node,
+ .goto_word,
+ .repeat_insert,
=> .once,
.edit => |edit| switch (edit.kind) {
- .comment_toggle, .undo, .redo => .once,
+ .join_lines,
+ .join_select_space,
+ .rotate_contents_forward,
+ .rotate_contents_backward,
+ .align_selections,
+ .comment_toggle,
+ .undo,
+ .redo,
+ => .once,
else => .per_selection,
},
else => .per_selection,
@@ -354,7 +425,10 @@ pub const Normal = struct {
pub fn parse(state: *State, key: Input) Result {
if (key.has(.escape)) {
- state.clear();
+ // Esc out of sticky view mode leaves that mode and nothing else
+ const sticky = state.sticky;
+ state.* = .{};
+ if (sticky) return .ignored;
return resultAction(.escape);
}
@@ -386,6 +460,8 @@ pub const Normal = struct {
if (key.has(.goto_view_top)) return resultAction(.{ .goto = .{ .target = .view_top, .count = count, .explicit_count = counted.explicit } });
if (key.has(.goto_view_center)) return resultAction(.{ .goto = .{ .target = .view_center, .count = count, .explicit_count = counted.explicit } });
if (key.has(.goto_view_bottom)) return resultAction(.{ .goto = .{ .target = .view_bottom, .count = count, .explicit_count = counted.explicit } });
+ if (key.has(.goto_word)) return resultAction(.goto_word);
+ if (key.has(.goto_last_modification)) return resultAction(.{ .goto = .{ .target = .last_modification, .count = count, .explicit_count = counted.explicit } });
if (key.has(.goto_definition)) return resultAction(.{ .lsp = .definition });
if (key.has(.goto_declaration)) return resultAction(.{ .lsp = .declaration });
if (key.has(.goto_type_definition)) return resultAction(.{ .lsp = .type_definition });
@@ -394,8 +470,9 @@ pub const Normal = struct {
return .ignored;
},
.view => {
- state.prefix = .none;
+ if (!state.sticky) state.prefix = .none;
if (key.has(.view_top)) return resultAction(.{ .view = .top });
+ if (key.has(.view_middle)) return resultAction(.{ .view = .middle });
if (key.has(.view_center)) return resultAction(.{ .view = .center });
if (key.has(.view_bottom)) return resultAction(.{ .view = .bottom });
if (key.has(.view_scroll_down)) return resultAction(.{ .view = .scroll_down });
@@ -473,14 +550,28 @@ pub const Normal = struct {
if (key.has(.add_newline)) return resultAction(.{ .add_newline = .{ .direction = direction, .count = count } });
if (key.has(.goto_diagnostic)) return resultAction(.{ .diagnostic = .{ .direction = direction, .endpoint = false } });
if (key.has(.goto_diagnostic_end)) return resultAction(.{ .diagnostic = .{ .direction = direction, .endpoint = true } });
+ inline for (.{ .{ .goto_function, .function }, .{ .goto_class, .class }, .{ .goto_parameter, .parameter }, .{ .goto_comment, .comment }, .{ .goto_test, .@"test" }, .{ .goto_entry, .entry }, .{ .goto_xml_element, .xml_element } }) |pair|
+ if (key.has(pair[0])) return resultAction(.{ .object_jump = .{ .object = pair[1], .direction = direction, .count = count } });
return .ignored;
},
+ .register => {
+ state.prefix = .none;
+ const char = key.literal() orelse return .ignored;
+ state.register = char;
+ // the count typed before `"` is still the command's
+ state.count = if (counted.explicit) count else 0;
+ return .pending;
+ },
.none => {},
}
// Prefix setters retain the count for their continuation.
if (key.has(.prefix_goto)) return armPrefix(state, .goto, if (counted.explicit) count else 0);
if (key.has(.prefix_view)) return armPrefix(state, .view, if (counted.explicit) count else 0);
+ if (key.has(.prefix_view_sticky)) {
+ state.sticky = true;
+ return armPrefix(state, .view, 0);
+ }
if (key.has(.prefix_find_fwd)) return armPrefix(state, .find_fwd, if (counted.explicit) count else 0);
if (key.has(.prefix_find_back)) return armPrefix(state, .find_back, if (counted.explicit) count else 0);
if (key.has(.prefix_till_fwd)) return armPrefix(state, .till_fwd, if (counted.explicit) count else 0);
@@ -489,6 +580,7 @@ pub const Normal = struct {
if (key.has(.prefix_next)) return armPrefix(state, .next, if (counted.explicit) count else 0);
if (key.has(.prefix_prev)) return armPrefix(state, .prev, if (counted.explicit) count else 0);
if (key.has(.prefix_match)) return armPrefix(state, .match, 0);
+ if (key.has(.prefix_register)) return armPrefix(state, .register, if (counted.explicit) count else 0);
if (key.has(.move_left)) return resultAction(.{ .move = .{ .motion = .left, .count = count } });
if (key.has(.move_right)) return resultAction(.{ .move = .{ .motion = .right, .count = count } });
@@ -535,8 +627,19 @@ pub const Normal = struct {
if (key.has(.merge_sels)) return resultAction(.{ .multi = .{ .kind = .merge, .count = count } });
if (key.has(.merge_consecutive_sels)) return resultAction(.{ .multi = .{ .kind = .merge_consecutive, .count = count } });
if (key.has(.trim_sels)) return resultAction(.{ .multi = .{ .kind = .trim, .count = count } });
- if (key.has(.select_regex)) return resultAction(.{ .select_regex = false });
- if (key.has(.split_regex)) return resultAction(.{ .select_regex = true });
+ if (key.has(.select_regex)) return resultAction(.{ .select_regex = .select });
+ if (key.has(.split_regex)) return resultAction(.{ .select_regex = .split });
+ if (key.has(.keep_regex)) return resultAction(.{ .select_regex = .keep });
+ if (key.has(.remove_regex)) return resultAction(.{ .select_regex = .remove });
+ if (key.has(.ensure_forward)) return resultAction(.{ .multi = .{ .kind = .ensure_forward, .count = count } });
+ if (key.has(.expand_node)) return resultAction(.{ .node = .expand });
+ if (key.has(.shrink_node)) return resultAction(.{ .node = .shrink });
+ if (key.has(.next_sibling)) return resultAction(.{ .node = .next_sibling });
+ if (key.has(.prev_sibling)) return resultAction(.{ .node = .prev_sibling });
+ if (key.has(.all_siblings)) return resultAction(.{ .node = .all_siblings });
+ if (key.has(.all_children)) return resultAction(.{ .node = .all_children });
+ if (key.has(.parent_node_end)) return resultAction(.{ .node = .parent_end });
+ if (key.has(.parent_node_start)) return resultAction(.{ .node = .parent_start });
if (key.has(.delete)) return resultAction(.{ .edit = .{ .kind = .delete, .count = count } });
if (key.has(.delete_noyank)) return resultAction(.{ .edit = .{ .kind = .delete_noyank, .count = count } });
@@ -549,6 +652,10 @@ pub const Normal = struct {
if (key.has(.to_lowercase)) return resultAction(.{ .edit = .{ .kind = .lowercase, .count = count } });
if (key.has(.to_uppercase)) return resultAction(.{ .edit = .{ .kind = .uppercase, .count = count } });
if (key.has(.join_lines)) return resultAction(.{ .edit = .{ .kind = .join_lines, .count = count } });
+ if (key.has(.join_select_space)) return resultAction(.{ .edit = .{ .kind = .join_select_space, .count = count } });
+ if (key.has(.rotate_contents_fwd)) return resultAction(.{ .edit = .{ .kind = .rotate_contents_forward, .count = count } });
+ if (key.has(.rotate_contents_back)) return resultAction(.{ .edit = .{ .kind = .rotate_contents_backward, .count = count } });
+ if (key.has(.align_selections)) return resultAction(.{ .edit = .{ .kind = .align_selections, .count = count } });
if (key.has(.indent)) return resultAction(.{ .edit = .{ .kind = .indent, .count = count } });
if (key.has(.unindent)) return resultAction(.{ .edit = .{ .kind = .unindent, .count = count } });
if (key.has(.format)) return resultAction(.{ .lsp = .format });
@@ -564,6 +671,10 @@ pub const Normal = struct {
if (key.has(.pipe_selection_to)) return resultAction(.{ .pipe_selection = .ignore });
if (key.has(.insert_output)) return resultAction(.{ .pipe_selection = .insert });
if (key.has(.append_output)) return resultAction(.{ .pipe_selection = .append });
+ if (key.has(.keep_pipe)) return resultAction(.{ .pipe_selection = .keep });
+ if (key.has(.macro_record)) return resultAction(.macro_record);
+ if (key.has(.macro_replay)) return resultAction(.{ .macro_replay = count });
+ if (key.has(.repeat_insert)) return resultAction(.{ .repeat_insert = count });
if (key.has(.search)) return resultAction(.search);
if (key.has(.search_next)) return resultAction(.{ .search_step = .forward });
if (key.has(.search_prev)) return resultAction(.{ .search_step = .backward });
@@ -1287,6 +1398,53 @@ fn wmIsPrev(t: WordTarget) bool {
}
/// w/b/e/W/B/E: helix word_move — each step selects the traversed span.
+/// helix jump_to_word's candidates: the words of two or more word
+/// characters between `start` and `end`, nearest the cursor first, taking one
+/// forward and one back in turn, each over its word pointing the way it was
+/// found, as many as `out` holds.
+pub fn jumpWords(text: []const u8, cursor: usize, start: usize, end: usize, out: []Selection) usize {
+ var fwd: Selection = .{ .anchor = cursor, .head = cursor };
+ var rev = fwd;
+ if (cursor < text.len and !isWordWhitespace(wordClassAt(text, cursor))) {
+ const word_end = moveWord(text, fwd, 1, .next_word_end);
+ if (word_end.anchor == cursor) fwd = word_end; // a word of one grapheme
+ const word_start = moveWord(text, rev, 1, .prev_word_start);
+ if (word_start.anchor == nextGrapheme(text, cursor)) rev = word_start;
+ }
+ var n: usize = 0;
+ while (n < out.len) {
+ var changed = false;
+ while (fwd.head < end) {
+ fwd = moveWord(text, fwd, 1, .next_word_end);
+ // the two graphemes before the head are word characters
+ const one = prevGrapheme(text, fwd.head);
+ if (fwd.head == 0 or one == 0 or wordClassAt(text, one) != .word or wordClassAt(text, prevGrapheme(text, one)) != .word) continue;
+ changed = true;
+ var from = fwd.anchor;
+ while (from < fwd.head and wordClassAt(text, from) != .word) from = nextGrapheme(text, from);
+ fwd.anchor = from;
+ out[n] = fwd;
+ n += 1;
+ break;
+ }
+ if (n == out.len) break;
+ while (rev.head > start) {
+ rev = moveWord(text, rev, 1, .prev_word_start);
+ // the two graphemes from the head are word characters
+ if (wordClassAt(text, rev.head) != .word or wordClassAt(text, nextGrapheme(text, rev.head)) != .word) continue;
+ changed = true;
+ var to = rev.anchor;
+ while (to > rev.head and wordClassAt(text, prevGrapheme(text, to)) != .word) to = prevGrapheme(text, to);
+ rev.anchor = to;
+ out[n] = rev;
+ n += 1;
+ break;
+ }
+ if (!changed) break;
+ }
+ return n;
+}
+
pub fn moveWord(text: []const u8, r0: Selection, count: usize, target: WordTarget) Selection {
const is_prev = wmIsPrev(target);
if ((is_prev and r0.head == 0) or (!is_prev and r0.head == text.len)) return r0;