summaryrefslogtreecommitdiff
path: root/src/normal_input.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/normal_input.zig')
-rw-r--r--src/normal_input.zig659
1 files changed, 0 insertions, 659 deletions
diff --git a/src/normal_input.zig b/src/normal_input.zig
deleted file mode 100644
index 5fa29bea..00000000
--- a/src/normal_input.zig
+++ /dev/null
@@ -1,659 +0,0 @@
-//! Pure BODY-NORMAL input recognition.
-//!
-//! The platform/core boundary first normalizes a physical key into every
-//! configured `Role` it matches. This module then owns the state machine:
-//! counts, prefixes and literal character arguments. It deliberately knows
-//! nothing about panes or text, so the text and PDF adapters consume exactly
-//! the same semantic `Action` values.
-const std = @import("std");
-
-pub const Role = enum {
- escape,
-
- prefix_goto,
- prefix_view,
- prefix_match,
- prefix_find_fwd,
- prefix_find_back,
- prefix_till_fwd,
- prefix_till_back,
- prefix_replace,
- prefix_next,
- prefix_prev,
-
- goto_file_start,
- goto_last_line,
- goto_line_start,
- goto_line_end,
- goto_first_nonws,
- goto_line_down,
- goto_line_up,
- goto_column,
- goto_view_top,
- goto_view_center,
- goto_view_bottom,
- goto_definition,
- goto_declaration,
- goto_type_definition,
- goto_implementation,
- goto_references,
-
- view_top,
- view_center,
- view_bottom,
- view_scroll_down,
- view_scroll_up,
-
- match_inside,
- match_around,
- surround_add,
- surround_replace,
- surround_delete,
-
- goto_paragraph,
- add_newline,
- goto_diagnostic,
- goto_diagnostic_end,
-
- move_left,
- move_right,
- move_down,
- move_up,
- next_word_start,
- prev_word_start,
- next_word_end,
- next_long_word_start,
- prev_long_word_start,
- next_long_word_end,
- repeat_find,
- line_start,
- line_end,
- line_first_nonws,
- goto_line,
- half_page_down,
- half_page_up,
- page_down,
- page_up,
-
- insert,
- append,
- insert_line_start,
- insert_line_end,
- open_below,
- open_above,
-
- select_mode,
- select_line,
- select_line_bounds,
- shrink_to_line_bounds,
- collapse_selection,
- flip_selection,
- select_all,
- copy_sel_below,
- copy_sel_above,
- keep_primary_sel,
- remove_primary_sel,
- rotate_sel_fwd,
- rotate_sel_back,
- split_sel_newline,
- merge_sels,
- merge_consecutive_sels,
- trim_sels,
- select_regex,
- split_regex,
-
- delete,
- delete_noyank,
- change,
- yank,
- replace_with_yank,
- paste_after,
- paste_before,
- switch_case,
- to_lowercase,
- to_uppercase,
- join_lines,
- indent,
- unindent,
- format,
- increment,
- decrement,
- comment_toggle,
- undo,
- redo,
-
- leader,
- command_line,
- pipe_selection,
- pipe_selection_to,
- insert_output,
- append_output,
- search,
- search_next,
- search_prev,
-};
-
-pub const Input = struct {
- roles: std.EnumSet(Role) = .initEmpty(),
- cp: u21,
- ctrl: bool = false,
- alt: bool = false,
-
- pub fn has(value: Input, role: Role) bool {
- return value.roles.contains(role);
- }
-
- fn literal(value: Input) ?u21 {
- if (value.ctrl or value.alt or value.cp >= 0xF0000) return null;
- return value.cp;
- }
-};
-
-pub const Prefix = enum(u8) {
- none,
- goto,
- view,
- match,
- find_fwd,
- find_back,
- till_fwd,
- till_back,
- replace,
- next,
- prev,
-};
-
-pub const MatchSub = enum(u8) {
- none,
- inside,
- around,
- surround_add,
- surround_replace,
- surround_delete,
-};
-
-pub const State = struct {
- count: u32 = 0,
- prefix: Prefix = .none,
- match_sub: MatchSub = .none,
- held_char: u21 = 0,
-
- pub fn clear(state: *State) void {
- state.* = .{};
- }
-};
-
-/// WHERE a filter's output goes, and whether the selection is its stdin.
-/// helix's `|`, `A-|`, `!` and `A-!` in one word each (commands.rs
-/// `ShellBehavior`); its `$` (keep selections by exit status) is not here yet
-/// because it needs a per-selection verdict rather than one atomic answer.
-pub const PipeBehavior = enum {
- /// `|` — stdin is the selection, and the output REPLACES it.
- replace,
- /// `A-|` — stdin is the selection, and the output is discarded. The text
- /// is not touched at all; the point is the command's side effect.
- ignore,
- /// `!` — no stdin, and the output is inserted BEFORE each selection.
- insert,
- /// `A-!` — no stdin, and the output is appended AFTER each selection.
- append,
-
- /// Do the selections become stdin? helix's `pipe` flag.
- pub fn pipes(b: PipeBehavior) bool {
- return b == .replace or b == .ignore;
- }
-};
-
-pub const Scope = enum { once, per_selection };
-pub const Direction = enum { backward, forward };
-pub const Motion = enum {
- left,
- right,
- down,
- up,
- next_word_start,
- prev_word_start,
- next_word_end,
- next_long_word_start,
- prev_long_word_start,
- next_long_word_end,
-};
-pub const Goto = enum {
- file_start,
- last_line,
- line_start,
- line_end,
- first_nonws,
- line_down,
- line_up,
- column,
- view_top,
- view_center,
- view_bottom,
-};
-pub const View = enum { top, center, bottom, 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 };
-pub const Insert = enum { at, append, line_start, line_end, open_below, open_above };
-pub const Select = enum {
- mode,
- line,
- line_bounds,
- shrink_to_line_bounds,
- collapse,
- flip,
- all,
-};
-pub const Multi = enum {
- copy_below,
- copy_above,
- keep_primary,
- remove_primary,
- rotate_forward,
- rotate_backward,
- split_newline,
- merge,
- merge_consecutive,
- trim,
-};
-pub const Edit = enum {
- delete,
- delete_noyank,
- change,
- yank,
- replace_with_yank,
- paste_after,
- paste_before,
- switch_case,
- lowercase,
- uppercase,
- join_lines,
- indent,
- unindent,
- comment_toggle,
- undo,
- redo,
-};
-pub const Lsp = enum { definition, declaration, type_definition, implementation, references, format };
-
-pub const Counted = struct {
- count: u32,
- explicit: bool,
-};
-
-pub const Action = union(enum) {
- escape,
- goto: struct { target: Goto, count: u32, explicit_count: bool },
- view: View,
- find: struct { kind: Find, char: u21, count: u32 },
- replace_char: u21,
- match_bracket,
- textobject: struct { char: u21, around: bool },
- surround_add: u21,
- surround_delete: u21,
- surround_replace: struct { from: u21, to: u21 },
- paragraph: struct { direction: Direction, count: u32 },
- add_newline: struct { direction: Direction, count: u32 },
- diagnostic: struct { direction: Direction, endpoint: bool },
- move: struct { motion: Motion, count: u32 },
- repeat_find: u32,
- line: Line,
- goto_line: Counted,
- page: struct { kind: Page, count: u32 },
- 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
- edit: struct { kind: Edit, count: u32 },
- lsp: Lsp,
- adjust_number: i64,
- leader,
- command_line,
- /// helix's five shell commands are one action with a behaviour, because
- /// they differ only in where the output lands and whether the selection is
- /// stdin. See `PipeBehavior`.
- pipe_selection: PipeBehavior,
- search,
- search_step: Direction,
-
- pub fn scope(value: Action) Scope {
- return switch (value) {
- .escape,
- .multi,
- .select_regex,
- .leader,
- .command_line,
- .pipe_selection,
- .search,
- .search_step,
- => .once,
- .edit => |edit| switch (edit.kind) {
- .comment_toggle, .undo, .redo => .once,
- else => .per_selection,
- },
- else => .per_selection,
- };
- }
-};
-
-pub const Result = union(enum) {
- pending,
- ignored,
- unbound,
- action: Action,
-};
-
-fn resultAction(value: Action) Result {
- return .{ .action = value };
-}
-
-fn consumeCount(state: *State) Counted {
- const count = state.count;
- state.count = 0;
- return .{ .count = @max(1, count), .explicit = count != 0 };
-}
-
-fn armPrefix(state: *State, prefix: Prefix, saved_count: u32) Result {
- state.prefix = prefix;
- state.count = saved_count;
- if (prefix == .match) {
- state.match_sub = .none;
- state.held_char = 0;
- }
- return .pending;
-}
-
-/// Consume one normalized physical key. Invalid continuations are
-/// distinguished from genuinely unbound top-level keys, and always clear the
-/// prefix that owned them.
-pub fn parse(state: *State, key: Input) Result {
- if (key.has(.escape)) {
- state.clear();
- return resultAction(.escape);
- }
-
- // A digit is a count only before a command/prefix. A leading zero keeps
- // its configured line-start role; after another digit it extends count.
- if (state.prefix == .none and !key.ctrl and !key.alt and
- key.cp >= '0' and key.cp <= '9' and
- !(key.cp == '0' and state.count == 0))
- {
- if (state.count < 0xffff)
- state.count = state.count * 10 + key.cp - '0';
- return .pending;
- }
-
- const counted = consumeCount(state);
- const count = counted.count;
-
- switch (state.prefix) {
- .goto => {
- state.prefix = .none;
- if (key.has(.goto_file_start)) return resultAction(.{ .goto = .{ .target = .file_start, .count = count, .explicit_count = counted.explicit } });
- if (key.has(.goto_last_line)) return resultAction(.{ .goto = .{ .target = .last_line, .count = count, .explicit_count = counted.explicit } });
- if (key.has(.goto_line_start)) return resultAction(.{ .goto = .{ .target = .line_start, .count = count, .explicit_count = counted.explicit } });
- if (key.has(.goto_line_end)) return resultAction(.{ .goto = .{ .target = .line_end, .count = count, .explicit_count = counted.explicit } });
- if (key.has(.goto_first_nonws)) return resultAction(.{ .goto = .{ .target = .first_nonws, .count = count, .explicit_count = counted.explicit } });
- if (key.has(.goto_line_down)) return resultAction(.{ .goto = .{ .target = .line_down, .count = count, .explicit_count = counted.explicit } });
- if (key.has(.goto_line_up)) return resultAction(.{ .goto = .{ .target = .line_up, .count = count, .explicit_count = counted.explicit } });
- if (key.has(.goto_column)) return resultAction(.{ .goto = .{ .target = .column, .count = count, .explicit_count = counted.explicit } });
- 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_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 });
- if (key.has(.goto_implementation)) return resultAction(.{ .lsp = .implementation });
- if (key.has(.goto_references)) return resultAction(.{ .lsp = .references });
- return .ignored;
- },
- .view => {
- state.prefix = .none;
- if (key.has(.view_top)) return resultAction(.{ .view = .top });
- 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 });
- if (key.has(.view_scroll_up)) return resultAction(.{ .view = .scroll_up });
- if (key.has(.half_page_down)) return resultAction(.{ .page = .{ .kind = .half_down, .count = count } });
- if (key.has(.half_page_up)) return resultAction(.{ .page = .{ .kind = .half_up, .count = count } });
- if (key.has(.page_down)) return resultAction(.{ .page = .{ .kind = .down, .count = count } });
- if (key.has(.page_up)) return resultAction(.{ .page = .{ .kind = .up, .count = count } });
- return .ignored;
- },
- .find_fwd, .find_back, .till_fwd, .till_back => |prefix| {
- state.prefix = .none;
- const char = key.literal() orelse return .ignored;
- const kind: Find = switch (prefix) {
- .find_fwd => .forward,
- .find_back => .backward,
- .till_fwd => .till_forward,
- .till_back => .till_backward,
- else => unreachable,
- };
- return resultAction(.{ .find = .{ .kind = kind, .char = char, .count = count } });
- },
- .replace => {
- state.prefix = .none;
- const char = key.literal() orelse return .ignored;
- return resultAction(.{ .replace_char = char });
- },
- .match => {
- if (state.match_sub == .none) {
- if (key.has(.prefix_match)) {
- state.prefix = .none;
- return resultAction(.match_bracket);
- }
- const sub: MatchSub = if (key.has(.match_inside))
- .inside
- else if (key.has(.match_around))
- .around
- else if (key.has(.surround_add))
- .surround_add
- else if (key.has(.surround_replace))
- .surround_replace
- else if (key.has(.surround_delete))
- .surround_delete
- else {
- state.prefix = .none;
- return .ignored;
- };
- state.match_sub = sub;
- return .pending;
- }
- const char = key.literal() orelse {
- state.clear();
- return .ignored;
- };
- if (state.match_sub == .surround_replace and state.held_char == 0) {
- state.held_char = char;
- return .pending;
- }
- const sub = state.match_sub;
- const from = state.held_char;
- state.clear();
- return switch (sub) {
- .inside => resultAction(.{ .textobject = .{ .char = char, .around = false } }),
- .around => resultAction(.{ .textobject = .{ .char = char, .around = true } }),
- .surround_add => resultAction(.{ .surround_add = char }),
- .surround_delete => resultAction(.{ .surround_delete = char }),
- .surround_replace => resultAction(.{ .surround_replace = .{ .from = from, .to = char } }),
- .none => unreachable,
- };
- },
- .next, .prev => |prefix| {
- state.prefix = .none;
- const direction: Direction = if (prefix == .next) .forward else .backward;
- if (key.has(.goto_paragraph)) return resultAction(.{ .paragraph = .{ .direction = direction, .count = count } });
- 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 } });
- return .ignored;
- },
- .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_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);
- if (key.has(.prefix_till_back)) return armPrefix(state, .till_back, if (counted.explicit) count else 0);
- if (key.has(.prefix_replace)) return armPrefix(state, .replace, if (counted.explicit) count else 0);
- 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(.move_left)) return resultAction(.{ .move = .{ .motion = .left, .count = count } });
- if (key.has(.move_right)) return resultAction(.{ .move = .{ .motion = .right, .count = count } });
- if (key.has(.move_down)) return resultAction(.{ .move = .{ .motion = .down, .count = count } });
- if (key.has(.move_up)) return resultAction(.{ .move = .{ .motion = .up, .count = count } });
- if (key.has(.next_word_start)) return resultAction(.{ .move = .{ .motion = .next_word_start, .count = count } });
- if (key.has(.prev_word_start)) return resultAction(.{ .move = .{ .motion = .prev_word_start, .count = count } });
- if (key.has(.next_word_end)) return resultAction(.{ .move = .{ .motion = .next_word_end, .count = count } });
- if (key.has(.next_long_word_start)) return resultAction(.{ .move = .{ .motion = .next_long_word_start, .count = count } });
- if (key.has(.prev_long_word_start)) return resultAction(.{ .move = .{ .motion = .prev_long_word_start, .count = count } });
- if (key.has(.next_long_word_end)) return resultAction(.{ .move = .{ .motion = .next_long_word_end, .count = count } });
- if (key.has(.repeat_find)) return resultAction(.{ .repeat_find = count });
- if (key.has(.line_start)) return resultAction(.{ .line = .start });
- if (key.has(.line_end)) return resultAction(.{ .line = .end });
- if (key.has(.line_first_nonws)) return resultAction(.{ .line = .first_nonws });
- if (key.has(.goto_line)) return resultAction(.{ .goto_line = counted });
- if (key.has(.half_page_down)) return resultAction(.{ .page = .{ .kind = .half_down, .count = count } });
- if (key.has(.half_page_up)) return resultAction(.{ .page = .{ .kind = .half_up, .count = count } });
- if (key.has(.page_down)) return resultAction(.{ .page = .{ .kind = .down, .count = count } });
- if (key.has(.page_up)) return resultAction(.{ .page = .{ .kind = .up, .count = count } });
-
- if (key.has(.insert)) return resultAction(.{ .insert = .{ .kind = .at, .count = count } });
- if (key.has(.append)) return resultAction(.{ .insert = .{ .kind = .append, .count = count } });
- if (key.has(.insert_line_start)) return resultAction(.{ .insert = .{ .kind = .line_start, .count = count } });
- if (key.has(.insert_line_end)) return resultAction(.{ .insert = .{ .kind = .line_end, .count = count } });
- if (key.has(.open_below)) return resultAction(.{ .insert = .{ .kind = .open_below, .count = count } });
- if (key.has(.open_above)) return resultAction(.{ .insert = .{ .kind = .open_above, .count = count } });
-
- if (key.has(.select_mode)) return resultAction(.{ .select = .{ .kind = .mode, .count = count } });
- if (key.has(.select_line)) return resultAction(.{ .select = .{ .kind = .line, .count = count } });
- if (key.has(.select_line_bounds)) return resultAction(.{ .select = .{ .kind = .line_bounds, .count = count } });
- if (key.has(.shrink_to_line_bounds)) return resultAction(.{ .select = .{ .kind = .shrink_to_line_bounds, .count = count } });
- if (key.has(.collapse_selection)) return resultAction(.{ .select = .{ .kind = .collapse, .count = count } });
- if (key.has(.flip_selection)) return resultAction(.{ .select = .{ .kind = .flip, .count = count } });
- if (key.has(.select_all)) return resultAction(.{ .select = .{ .kind = .all, .count = count } });
-
- if (key.has(.copy_sel_below)) return resultAction(.{ .multi = .{ .kind = .copy_below, .count = count } });
- if (key.has(.copy_sel_above)) return resultAction(.{ .multi = .{ .kind = .copy_above, .count = count } });
- if (key.has(.keep_primary_sel)) return resultAction(.{ .multi = .{ .kind = .keep_primary, .count = count } });
- if (key.has(.remove_primary_sel)) return resultAction(.{ .multi = .{ .kind = .remove_primary, .count = count } });
- if (key.has(.rotate_sel_fwd)) return resultAction(.{ .multi = .{ .kind = .rotate_forward, .count = count } });
- if (key.has(.rotate_sel_back)) return resultAction(.{ .multi = .{ .kind = .rotate_backward, .count = count } });
- if (key.has(.split_sel_newline)) return resultAction(.{ .multi = .{ .kind = .split_newline, .count = count } });
- 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(.delete)) return resultAction(.{ .edit = .{ .kind = .delete, .count = count } });
- if (key.has(.delete_noyank)) return resultAction(.{ .edit = .{ .kind = .delete_noyank, .count = count } });
- if (key.has(.change)) return resultAction(.{ .edit = .{ .kind = .change, .count = count } });
- if (key.has(.yank)) return resultAction(.{ .edit = .{ .kind = .yank, .count = count } });
- if (key.has(.replace_with_yank)) return resultAction(.{ .edit = .{ .kind = .replace_with_yank, .count = count } });
- if (key.has(.paste_after)) return resultAction(.{ .edit = .{ .kind = .paste_after, .count = count } });
- if (key.has(.paste_before)) return resultAction(.{ .edit = .{ .kind = .paste_before, .count = count } });
- if (key.has(.switch_case)) return resultAction(.{ .edit = .{ .kind = .switch_case, .count = count } });
- 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(.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 });
- if (key.has(.increment)) return resultAction(.{ .adjust_number = @intCast(count) });
- if (key.has(.decrement)) return resultAction(.{ .adjust_number = -@as(i64, @intCast(count)) });
- if (key.has(.comment_toggle)) return resultAction(.{ .edit = .{ .kind = .comment_toggle, .count = count } });
- if (key.has(.undo)) return resultAction(.{ .edit = .{ .kind = .undo, .count = count } });
- if (key.has(.redo)) return resultAction(.{ .edit = .{ .kind = .redo, .count = count } });
-
- if (key.has(.leader)) return resultAction(.leader);
- if (key.has(.command_line)) return resultAction(.command_line);
- if (key.has(.pipe_selection)) return resultAction(.{ .pipe_selection = .replace });
- 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(.search)) return resultAction(.search);
- if (key.has(.search_next)) return resultAction(.{ .search_step = .forward });
- if (key.has(.search_prev)) return resultAction(.{ .search_step = .backward });
- return .unbound;
-}
-
-fn input(cp: u21, roles: []const Role) Input {
- return .{ .cp = cp, .roles = .initMany(roles) };
-}
-
-test "counts survive prefixes and identical parser actions can feed both adapters" {
- var text: State = .{};
- var pdf: State = .{};
- const sequence = [_]Input{
- input('1', &.{}),
- input('2', &.{}),
- input('g', &.{ .prefix_goto, .goto_file_start }),
- input('j', &.{ .move_down, .goto_line_down, .view_scroll_down }),
- };
- for (sequence[0 .. sequence.len - 1]) |key| {
- try std.testing.expectEqualDeep(parse(&text, key), parse(&pdf, key));
- }
- const ta = parse(&text, sequence[sequence.len - 1]);
- const pa = parse(&pdf, sequence[sequence.len - 1]);
- try std.testing.expectEqualDeep(ta, pa);
- try std.testing.expectEqualDeep(Result{ .action = .{ .goto = .{
- .target = .line_down,
- .count = 12,
- .explicit_count = true,
- } } }, ta);
- try std.testing.expectEqual(State{}, text);
- try std.testing.expectEqual(State{}, pdf);
-}
-
-test "invalid continuations are ignored and clear prefix plus count" {
- var state: State = .{};
- try std.testing.expectEqual(Result.pending, parse(&state, input('4', &.{})));
- try std.testing.expectEqual(Result.pending, parse(&state, input('g', &.{.prefix_goto})));
- try std.testing.expectEqual(Result.ignored, parse(&state, input('?', &.{})));
- try std.testing.expectEqual(State{}, state);
- try std.testing.expectEqualDeep(Result{ .action = .{ .move = .{ .motion = .down, .count = 1 } } }, parse(&state, input('j', &.{.move_down})));
-}
-
-test "literal arguments retain conflicting command characters" {
- var state: State = .{};
- try std.testing.expectEqual(Result.pending, parse(&state, input('f', &.{.prefix_find_fwd})));
- try std.testing.expectEqualDeep(Result{ .action = .{ .find = .{ .kind = .forward, .char = 'p', .count = 1 } } }, parse(&state, input('p', &.{.paste_after})));
-
- try std.testing.expectEqual(Result.pending, parse(&state, input('m', &.{.prefix_match})));
- try std.testing.expectEqual(Result.pending, parse(&state, input('r', &.{.surround_replace})));
- try std.testing.expectEqual(Result.pending, parse(&state, input('[', &.{.prefix_prev})));
- try std.testing.expectEqualDeep(Result{ .action = .{ .surround_replace = .{ .from = '[', .to = ']' } } }, parse(&state, input(']', &.{.prefix_next})));
- try std.testing.expectEqual(State{}, state);
-}
-
-test "modified and special keys cannot satisfy literal continuations" {
- var state: State = .{};
- _ = parse(&state, input('r', &.{.prefix_replace}));
- try std.testing.expectEqual(Result.ignored, parse(&state, .{ .cp = 'x', .ctrl = true }));
- try std.testing.expectEqual(State{}, state);
- _ = parse(&state, input('f', &.{.prefix_find_fwd}));
- try std.testing.expectEqual(Result.ignored, parse(&state, .{ .cp = 0xF0001 }));
- try std.testing.expectEqual(State{}, state);
-}
-
-test "replace accepts a Unicode literal" {
- var state: State = .{};
- try std.testing.expectEqual(Result.pending, parse(&state, input('r', &.{.prefix_replace})));
- try std.testing.expectEqualDeep(Result{ .action = .{ .replace_char = '界' } }, parse(&state, input('界', &.{})));
- try std.testing.expectEqual(State{}, state);
-}
-
-test "once versus per-selection is semantic action metadata" {
- try std.testing.expectEqual(Scope.once, (@as(Action, .search)).scope());
- try std.testing.expectEqual(Scope.once, (Action{ .edit = .{ .kind = .undo, .count = 1 } }).scope());
- try std.testing.expectEqual(Scope.per_selection, (Action{ .edit = .{ .kind = .delete, .count = 1 } }).scope());
- try std.testing.expectEqual(Scope.per_selection, (Action{ .move = .{ .motion = .down, .count = 3 } }).scope());
-}