From 60367d8fe23f6af98ec28e3cf6c2094dfe332df0 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 6 Sep 2026 18:11:36 -0300 Subject: Refactor panes and filesystem; replace FUSE with 9P Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples. Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill. --- src/normal_input.zig | 659 --------------------------------------------------- 1 file changed, 659 deletions(-) delete mode 100644 src/normal_input.zig (limited to 'src/normal_input.zig') 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()); -} -- cgit v1.3