Skip to main content

strop_engine/keymap/
mod.rs

1//! The keymap table (0008 §3 — keymaps as data). v1: this table drives
2//! the `Space ?` keybinds popup and the which-key prefix cards; the trie
3//! dispatch cutover (0008 §5) makes it the dispatch source later.
4//!
5//! Coverage contract (0003 §5.7): every dispatchable binding renders in
6//! the popup; the tests below pin both directions. `live: false` rows
7//! are planned slots — reserved keys with no dispatch yet (0003 §6) —
8//! and render muted with a "(soon)" suffix, never as live bindings.
9//!
10//! ## Notation
11//!
12//! `keys` is space-separated tokens, one key per token (`esc`, `:w`,
13//! `"+y`; `<a>`/`<c>` parameterize a char):
14//!
15//! - `space` is the leader: its sequence runs to the row's end.
16//! - `ctrl-w` takes exactly one following key.
17//! - ` / ` separates alternatives that replace the last key of the
18//!   sequence before them (`space d / k` → `space d`, `space k`);
19//!   alternatives run to the row's end.
20//! - Every other token is one single-key sequence; a row may list
21//!   several (`h j k l`), and a leading bare `/` is the search-forward
22//!   key (`/ ?`).
23
24/// How a sequence dispatches (0008 stage 2). The walker consults these;
25/// `?`/which-key read the same row — the table is the single source.
26#[derive(Clone, Copy)]
27pub enum Handler {
28    /// A direct command.
29    /// The key that completed the sequence rides along (J vs ., p vs P).
30    Leaf(fn(&mut crate::editor::Editor, char)),
31    /// Alias: expands to these keys through the walker (D → d$).
32    Alias(&'static str),
33    /// A grammar motion: the key completes a motion (or the pending
34    /// operator's target) via strop_grammar.
35    Motion,
36    /// A grammar operator (d/y/c/>/<): the walker's typed op-pending.
37    Operator,
38    /// An object prefix (i/a) after an operator or in visual mode —
39    /// bare i/a in normal mode is the insert entry (the walker knows
40    /// from ParserState).
41    ObjectPrefix,
42    /// A prefix: children follow; which-key renders them.
43    Prefix,
44    /// Free-text line (`:` `/` `?` `|`…): a modal text field (0003 §1),
45    /// NOT a key sequence — the text layer owns it.
46    TextLine,
47    /// One-char absorbers: r<c> replace, m<a> mark, '/` jump, f<c> find.
48    AbsorbChar(AbsorbKind),
49    /// `"x` — register selection.
50    AbsorbRegister,
51    /// Planned slot: no dispatch yet (renders muted "(soon)").
52    Soon,
53}
54
55/// The one-char absorber flavors.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum AbsorbKind {
58    Replace,
59    MarkSet,
60    MarkJump,
61    Find,
62    /// q<a> — toggle macro recording into register a.
63    MacroRecord,
64    /// @<a> — replay register a's macro.
65    MacroPlay,
66}
67
68/// One binding as it appears in `Space ?` / which-key AND in dispatch
69/// (0008 stage 2: one table). `id` is the stable command identity —
70/// macros, dot-repeat, a future palette, and config rebinds ride it.
71pub struct Binding {
72    pub keys: &'static str,
73    pub desc: &'static str,
74    /// Sidebar section: normal · visual · insert · leader · git · ex+panes
75    pub section: &'static str,
76    /// false = planned slot: no dispatch yet, renders muted "(soon)".
77    pub live: bool,
78    /// Stable identity per row; per-sequence ids derive as `{id}:{key}`.
79    pub id: &'static str,
80    pub handler: Handler,
81}
82
83pub const SECTIONS: &[&str] = &["normal", "visual", "insert", "leader", "git", "ex+panes"];
84
85pub const BINDINGS: &[Binding] = &[
86    // normal: motions
87    Binding {
88        keys: "h j k l",
89        desc: "move (never off the line)",
90        section: "normal",
91        live: true,
92        id: "move",
93        handler: Handler::Motion,
94    },
95    Binding {
96        keys: "w b e W B E",
97        desc: "word / WORD motions",
98        section: "normal",
99        live: true,
100        id: "word-motions",
101        handler: Handler::Motion,
102    },
103    Binding {
104        keys: "zz zt zb",
105        desc: "center/top/bottom the cursor line",
106        section: "normal",
107        live: true,
108        id: "view-place",
109        handler: Handler::Leaf(|e, k| e.view_place(k)),
110    },
111    Binding {
112        keys: "H M L",
113        desc: "top/middle/bottom visible line",
114        section: "normal",
115        live: true,
116        id: "visible-jumps",
117        handler: Handler::Leaf(|e, k| e.jump_visible(k, 1)),
118    },
119    Binding {
120        keys: "ZZ",
121        desc: "write + close window",
122        section: "normal",
123        live: true,
124        id: "write-quit",
125        handler: Handler::Leaf(|e, _| e.write_quit()),
126    },
127    Binding {
128        keys: "gv",
129        desc: "reselect last visual",
130        section: "normal",
131        live: true,
132        id: "reselect-visual",
133        handler: Handler::Leaf(|e, _| e.reselect_visual()),
134    },
135    Binding {
136        keys: "gi",
137        desc: "insert at last insert",
138        section: "normal",
139        live: true,
140        id: "insert-at-last",
141        handler: Handler::Leaf(|e, _| e.insert_at_last()),
142    },
143    Binding {
144        keys: "g;",
145        desc: "older change (changelist)",
146        section: "normal",
147        live: true,
148        id: "change-back",
149        handler: Handler::Leaf(|e, _| e.change_jump(true)),
150    },
151    Binding {
152        keys: "g,",
153        desc: "newer change (changelist)",
154        section: "normal",
155        live: true,
156        id: "change-forward",
157        handler: Handler::Leaf(|e, _| e.change_jump(false)),
158    },
159    Binding {
160        keys: "ge gE { }",
161        desc: "word-end back / paragraph motions",
162        section: "normal",
163        live: true,
164        id: "paragraph-motions",
165        handler: Handler::Motion,
166    },
167    Binding {
168        keys: "0 $ G %",
169        desc: "line/file/pair jumps",
170        section: "normal",
171        live: true,
172        id: "line-jumps",
173        handler: Handler::Motion,
174    },
175    Binding {
176        keys: "gg",
177        desc: "top of file",
178        section: "normal",
179        live: true,
180        id: "top",
181        handler: Handler::Motion,
182    },
183    Binding {
184        keys: "enter",
185        desc: "line down, first non-blank (blame gutter: dive)",
186        section: "normal",
187        live: true,
188        id: "enter",
189        handler: Handler::Leaf(|e, _| e.enter_pub()),
190    },
191    Binding {
192        keys: "tab",
193        desc: "jump list forward (ctrl-i)",
194        section: "normal",
195        live: true,
196        id: "jump-forward",
197        handler: Handler::Leaf(|e, _| e.jump_forward()),
198    },
199    Binding {
200        keys: "ctrl-r",
201        desc: "redo",
202        section: "normal",
203        live: true,
204        id: "redo",
205        handler: Handler::Leaf(|e, _| e.redo()),
206    },
207    Binding {
208        keys: "ctrl-d ctrl-u ctrl-f ctrl-b",
209        desc: "half/full page scroll (count = lines)",
210        section: "normal",
211        live: true,
212        id: "scroll-pages",
213        handler: Handler::Leaf(|_, _| {}), // count-aware: dispatch_row
214    },
215    Binding {
216        keys: "ctrl-^",
217        desc: "alternate buffer",
218        section: "normal",
219        live: true,
220        id: "alternate-buffer",
221        handler: Handler::Leaf(|e, _| e.alternate_buffer()),
222    },
223    Binding {
224        keys: "q<a>",
225        desc: "record macro into register",
226        section: "normal",
227        live: true,
228        id: "macro-record",
229        handler: Handler::AbsorbChar(AbsorbKind::MacroRecord),
230    },
231    Binding {
232        keys: "@<a>",
233        desc: "play macro (count repeats)",
234        section: "normal",
235        live: true,
236        id: "macro-play",
237        handler: Handler::AbsorbChar(AbsorbKind::MacroPlay),
238    },
239    Binding {
240        keys: "gr",
241        desc: "references (LSP)",
242        section: "normal",
243        live: true,
244        id: "references",
245        handler: Handler::Leaf(|e, _| e.lsp_locations_pub(strop_lsp::LocKind::References)),
246    },
247    Binding {
248        keys: "gI",
249        desc: "implementation (LSP)",
250        section: "normal",
251        live: true,
252        id: "implementation",
253        handler: Handler::Leaf(|e, _| e.lsp_locations_pub(strop_lsp::LocKind::Implementation)),
254    },
255    Binding {
256        keys: "gy",
257        desc: "type definition (LSP)",
258        section: "normal",
259        live: true,
260        id: "type-definition",
261        handler: Handler::Leaf(|e, _| e.lsp_locations_pub(strop_lsp::LocKind::TypeDefinition)),
262    },
263    Binding {
264        keys: "gD",
265        desc: "declaration (LSP)",
266        section: "normal",
267        live: true,
268        id: "declaration",
269        handler: Handler::Leaf(|e, _| e.lsp_locations_pub(strop_lsp::LocKind::Declaration)),
270    },
271    Binding {
272        keys: "]d [d",
273        desc: "next/prev diagnostic",
274        section: "normal",
275        live: true,
276        id: "diagnostic-jumps",
277        handler: Handler::Leaf(|e, k| e.jump_diagnostic_pub(k != '[')),
278    },
279    Binding {
280        keys: "gd",
281        desc: "goto definition (LSP)",
282        section: "normal",
283        live: true,
284        id: "goto-definition",
285        handler: Handler::Leaf(|e, _| crate::editor::Editor::lsp_goto_definition_pub(e)),
286    },
287    Binding {
288        keys: "gs",
289        desc: "switch source/header (clangd)",
290        section: "normal",
291        live: true,
292        id: "switch-source-header",
293        handler: Handler::Leaf(|e, _| crate::editor::Editor::lsp_switch_source_header_pub(e)),
294    },
295    Binding {
296        keys: "f<c> F<c> t<c> T<c>",
297        desc: "find/till char (candidates light up)",
298        section: "normal",
299        live: true,
300        id: "find-char",
301        handler: Handler::AbsorbChar(AbsorbKind::Find),
302    },
303    Binding {
304        keys: ":",
305        desc: "ex command line",
306        section: "normal",
307        live: true,
308        id: "ex-line",
309        handler: Handler::TextLine,
310    },
311    Binding {
312        keys: "/ ?",
313        desc: "literal search forward / backward (live as you type or delete)",
314        section: "normal",
315        live: true,
316        id: "search",
317        handler: Handler::TextLine,
318    },
319    Binding {
320        keys: "n",
321        desc: "next match",
322        section: "normal",
323        live: true,
324        id: "search-next",
325        handler: Handler::Leaf(|e, _| e.repeat_search_pub(false)),
326    },
327    Binding {
328        keys: "N",
329        desc: "previous match",
330        section: "normal",
331        live: true,
332        id: "search-prev",
333        handler: Handler::Leaf(|e, _| e.repeat_search_pub(true)),
334    },
335    Binding {
336        keys: "]c [c",
337        desc: "next / prev git hunk",
338        section: "normal",
339        live: true,
340        id: "hunk-nav",
341        handler: Handler::Leaf(|e, k| e.jump_hunk_pub(k != '[')),
342    },
343    Binding {
344        keys: "m<a>",
345        desc: "set mark at cursor",
346        section: "normal",
347        live: true,
348        id: "mark-set",
349        handler: Handler::AbsorbChar(AbsorbKind::MarkSet),
350    },
351    Binding {
352        keys: "'<a> `<a>",
353        desc: "jump to mark",
354        section: "normal",
355        live: true,
356        id: "mark-jump",
357        handler: Handler::AbsorbChar(AbsorbKind::MarkJump),
358    },
359    Binding {
360        keys: "* #",
361        desc: "word under cursor, forward / backward",
362        section: "normal",
363        live: true,
364        id: "word-search",
365        handler: Handler::Leaf(|e, k| e.search_word_under_cursor_pub(k == '#')),
366    },
367    Binding {
368        keys: "; ,",
369        desc: "repeat find, same / reversed",
370        section: "normal",
371        live: true,
372        id: "find-repeat",
373        handler: Handler::Leaf(|e, k| e.repeat_find_pub(k == ',')),
374    },
375    Binding {
376        keys: "|",
377        desc: "column motion (vim); pipe moved to space |",
378        section: "normal",
379        live: true,
380        id: "column-motion",
381        handler: Handler::Motion,
382    },
383    Binding {
384        keys: "space |",
385        desc: "pipe line/selection through shell (:! runs)",
386        section: "leader",
387        live: true,
388        id: "pipe-shell",
389        handler: Handler::TextLine,
390    },
391    Binding {
392        keys: "Q",
393        desc: "toggle cursor at point (multicursor)",
394        section: "normal",
395        live: true,
396        id: "cursor-toggle",
397        handler: Handler::Leaf(|e, _| crate::editor::Editor::toggle_cursor(e)),
398    },
399    // normal: operators
400    Binding {
401        keys: "d y c > <",
402        desc: "operators + motion/object (live preview)",
403        section: "normal",
404        live: true,
405        id: "operators",
406        handler: Handler::Operator,
407    },
408    Binding {
409        keys: "dd yy cc",
410        desc: "line delete/yank/change",
411        section: "normal",
412        live: true,
413        id: "line-ops",
414        handler: Handler::Operator,
415    },
416    Binding {
417        keys: "D",
418        desc: "delete to line end",
419        section: "normal",
420        live: true,
421        id: "op-alias-d",
422        handler: Handler::Alias("d$"),
423    },
424    Binding {
425        keys: "C",
426        desc: "change to line end",
427        section: "normal",
428        live: true,
429        id: "op-alias-c",
430        handler: Handler::Alias("c$"),
431    },
432    Binding {
433        keys: "Y",
434        desc: "yank line",
435        section: "normal",
436        live: true,
437        id: "op-alias-y",
438        handler: Handler::Alias("yy"),
439    },
440    Binding {
441        keys: "s",
442        desc: "substitute char",
443        section: "normal",
444        live: true,
445        id: "op-alias-s",
446        handler: Handler::Alias("cl"),
447    },
448    Binding {
449        keys: "x X",
450        desc: "delete char / char back",
451        section: "normal",
452        live: true,
453        id: "char-delete",
454        handler: Handler::Leaf(|e, _| crate::editor::Editor::delete_char(e)),
455    },
456    Binding {
457        keys: "iw i\" i' i( i[ i{",
458        desc: "inner objects (quotes scan the line)",
459        section: "normal",
460        live: true,
461        id: "objects",
462        handler: Handler::ObjectPrefix,
463    },
464    Binding {
465        keys: "ds\" cs\"' ysiw\"",
466        desc: "surround: delete / change / add",
467        section: "normal",
468        live: true,
469        id: "surround",
470        handler: Handler::ObjectPrefix,
471    },
472    Binding {
473        keys: "i a A o O I",
474        desc: "insert (auto-indent)",
475        section: "normal",
476        live: true,
477        id: "insert-entries",
478        handler: Handler::Leaf(crate::editor::Editor::insert_entry_pub),
479    },
480    Binding {
481        keys: "p P",
482        desc: "paste after / before",
483        section: "normal",
484        live: true,
485        id: "paste",
486        handler: Handler::Leaf(|e, k| e.paste_named_pub(None, 1, k == 'P')),
487    },
488    Binding {
489        keys: "r<c>",
490        desc: "replace char",
491        section: "normal",
492        live: true,
493        id: "replace-char",
494        handler: Handler::AbsorbChar(AbsorbKind::Replace),
495    },
496    Binding {
497        keys: "J .",
498        desc: "join lines · repeat change",
499        section: "normal",
500        live: true,
501        id: "join-repeat",
502        handler: Handler::Leaf(crate::editor::Editor::join_or_repeat),
503    },
504    Binding {
505        keys: "^",
506        desc: "first non-blank",
507        section: "normal",
508        live: true,
509        id: "first-non-blank",
510        handler: Handler::Motion,
511    },
512    Binding {
513        keys: "~",
514        desc: "toggle case",
515        section: "normal",
516        live: true,
517        id: "toggle-case",
518        handler: Handler::Leaf(|e, _| crate::editor::Editor::toggle_case_pub(e)),
519    },
520    Binding {
521        keys: "S",
522        desc: "change line",
523        section: "normal",
524        live: true,
525        id: "subst-line",
526        handler: Handler::Alias("cc"),
527    },
528    Binding {
529        keys: "u ctrl-r",
530        desc: "undo / redo (one unit per command)",
531        section: "normal",
532        live: true,
533        id: "undo-redo",
534        handler: Handler::Leaf(|e, _| crate::editor::Editor::undo(e)),
535    },
536    Binding {
537        keys: "\"+y \"+p \"+P",
538        desc: "system clipboard: yank / paste after / before",
539        section: "normal",
540        live: true,
541        id: "reg-clipboard",
542        handler: Handler::AbsorbRegister,
543    },
544    Binding {
545        keys: "\"xy \"xp",
546        desc: "named register: yank / paste",
547        section: "normal",
548        live: true,
549        id: "reg-named",
550        handler: Handler::AbsorbRegister,
551    },
552    Binding {
553        keys: "ctrl-v",
554        desc: "visual block",
555        section: "normal",
556        live: true,
557        id: "visual-block",
558        handler: Handler::Leaf(|e, _| e.enter_block_pub()),
559    },
560    Binding {
561        keys: "v V",
562        desc: "visual / visual-line",
563        section: "normal",
564        live: true,
565        id: "visual-enter",
566        handler: Handler::Leaf(|e, k| e.enter_visual_pub(k)),
567    },
568    // visual
569    Binding {
570        keys: "d y c x > <",
571        desc: "operate on selection",
572        section: "visual",
573        live: true,
574        id: "visual-ops",
575        handler: Handler::Operator,
576    },
577    Binding {
578        keys: "S<c>",
579        desc: "wrap selection in pair",
580        section: "visual",
581        live: true,
582        id: "visual-surround",
583        handler: Handler::AbsorbChar(AbsorbKind::Replace),
584    },
585    Binding {
586        keys: "i<a> a<a>",
587        desc: "objects select (vi[ works)",
588        section: "visual",
589        live: true,
590        id: "visual-objects",
591        handler: Handler::ObjectPrefix,
592    },
593    Binding {
594        keys: "space y",
595        desc: "yank selection → clipboard",
596        section: "visual",
597        live: true,
598        id: "clip-yank",
599        handler: Handler::Leaf(|e, _| e.clipboard_yank_pub()),
600    },
601    // insert
602    Binding {
603        keys: "esc",
604        desc: "normal mode (session = one undo unit)",
605        section: "insert",
606        live: true,
607        id: "insert-esc",
608        handler: Handler::Prefix,
609    },
610    Binding {
611        keys: "backspace",
612        desc: "delete back",
613        section: "insert",
614        live: true,
615        id: "insert-bs",
616        handler: Handler::Prefix,
617    },
618    Binding {
619        keys: "enter",
620        desc: "new line (auto-indent)",
621        section: "insert",
622        live: true,
623        id: "insert-enter",
624        handler: Handler::Prefix,
625    },
626    Binding {
627        keys: "} ] )",
628        desc: "closer on indent-only line dedents",
629        section: "insert",
630        live: true,
631        id: "insert-closers",
632        handler: Handler::Prefix,
633    },
634    // leader
635    Binding {
636        keys: "space f",
637        desc: "file finder",
638        section: "leader",
639        live: true,
640        id: "files",
641        handler: Handler::Leaf(|e, _| e.open_picker(strop_picker::Kind::Files)),
642    },
643    Binding {
644        keys: "space o",
645        desc: "open remote destination",
646        section: "leader",
647        live: true,
648        id: "remote-open",
649        handler: Handler::Leaf(|e, _| e.open_remote_picker()),
650    },
651    Binding {
652        keys: "space b",
653        desc: "buffers (MRU)",
654        section: "leader",
655        live: true,
656        id: "buffers",
657        handler: Handler::Leaf(|e, _| e.open_picker(strop_picker::Kind::Buffers)),
658    },
659    Binding {
660        keys: "space /",
661        desc: "live grep",
662        section: "leader",
663        live: true,
664        id: "grep",
665        handler: Handler::Leaf(|e, _| e.open_picker(strop_picker::Kind::Grep)),
666    },
667    Binding {
668        keys: "space R",
669        desc: "global search & replace",
670        section: "leader",
671        live: true,
672        id: "replace-global",
673        handler: Handler::Leaf(|e, _| e.open_picker(strop_picker::Kind::Replace)),
674    },
675    Binding {
676        keys: "space ?",
677        desc: "this popup",
678        section: "leader",
679        live: true,
680        id: "help",
681        handler: Handler::Leaf(|e, _| crate::editor::Editor::open_help(e)),
682    },
683    Binding {
684        keys: "space y",
685        desc: "yank motion → system clipboard",
686        section: "leader",
687        live: true,
688        id: "clip-yank",
689        handler: Handler::Leaf(|e, _| e.clipboard_yank_pub()),
690    },
691    Binding {
692        keys: "space p",
693        desc: "paste clipboard after",
694        section: "leader",
695        live: true,
696        id: "clip-paste",
697        handler: Handler::Leaf(|e, k| e.clipboard_paste_pub(k == 'P')),
698    },
699    Binding {
700        keys: "space P",
701        desc: "paste clipboard before",
702        section: "leader",
703        live: true,
704        id: "clip-paste-before",
705        handler: Handler::Leaf(|e, k| e.clipboard_paste_pub(k == 'P')),
706    },
707    Binding {
708        keys: "space d",
709        desc: "diagnostics picker",
710        section: "leader",
711        live: true,
712        id: "diagnostics",
713        handler: Handler::Leaf(|e, _| e.open_diagnostics_picker()),
714    },
715    Binding {
716        keys: "space k",
717        desc: "hover docs",
718        section: "leader",
719        live: true,
720        id: "hover",
721        handler: Handler::Leaf(|e, _| e.lsp_hover_pub()),
722    },
723    Binding {
724        keys: "space a",
725        desc: "code actions",
726        section: "leader",
727        live: true,
728        id: "code-actions",
729        handler: Handler::Leaf(|e, _| e.lsp_code_actions_pub()),
730    },
731    Binding {
732        keys: "space s",
733        desc: "document symbols picker",
734        section: "leader",
735        live: true,
736        id: "document-symbols",
737        handler: Handler::Leaf(|e, _| e.lsp_document_symbols_pub()),
738    },
739    Binding {
740        keys: "space S",
741        desc: "workspace symbols picker",
742        section: "leader",
743        live: false,
744        id: "workspace-symbols",
745        handler: Handler::Soon,
746    },
747    Binding {
748        keys: "space j",
749        desc: "jumplist picker",
750        section: "leader",
751        live: true,
752        id: "jumplist-picker",
753        handler: Handler::Leaf(|e, _| e.open_picker(strop_picker::Kind::Jumps)),
754    },
755    Binding {
756        keys: "space u",
757        desc: "undo-tree browser",
758        section: "leader",
759        live: true,
760        id: "undo-tree",
761        handler: Handler::Leaf(|e, _| crate::editor::Editor::open_undo_tree(e)),
762    },
763    Binding {
764        keys: "space c",
765        desc: "cursor on next line too (multicursor)",
766        section: "leader",
767        live: true,
768        id: "cursor-stack",
769        handler: Handler::Leaf(|e, _| crate::editor::Editor::add_cursor_next_line(e)),
770    },
771    // git
772    Binding {
773        keys: "space g",
774        desc: "git…",
775        section: "git",
776        live: true,
777        id: "git-prefix",
778        handler: Handler::Prefix,
779    },
780    Binding {
781        keys: "space g l",
782        desc: "commit browser",
783        section: "git",
784        live: true,
785        id: "git-log",
786        handler: Handler::Leaf(|e, _| e.open_log_pub(false)),
787    },
788    Binding {
789        keys: "space g h",
790        desc: "file history (visual: selected lines)",
791        section: "git",
792        live: true,
793        id: "git-file-history",
794        handler: Handler::Leaf(|e, _| e.open_log_pub(true)),
795    },
796    Binding {
797        keys: "space g b",
798        desc: "toggle blame gutter / card",
799        section: "git",
800        live: true,
801        id: "git-blame",
802        handler: Handler::Leaf(|e, _| e.toggle_blame_gutter()),
803    },
804    Binding {
805        keys: "space g y",
806        desc: "permalink: copy",
807        section: "git",
808        live: true,
809        id: "git-permalink-yank",
810        handler: Handler::Leaf(|e, _| e.yank_permalink()),
811    },
812    Binding {
813        keys: "space g o",
814        desc: "permalink: open",
815        section: "git",
816        live: true,
817        id: "git-permalink-open",
818        handler: Handler::Leaf(|e, _| e.open_permalink()),
819    },
820    Binding {
821        keys: "space g u",
822        desc: "hunk: undo unstaged (restore from index)",
823        section: "git",
824        live: true,
825        id: "git-hunk-undo",
826        handler: Handler::Leaf(|e, _| e.undo_hunk()),
827    },
828    Binding {
829        keys: "space g s",
830        desc: "hunk: stage (live→index)",
831        section: "git",
832        live: true,
833        id: "git-hunk-stage",
834        handler: Handler::Leaf(|e, _| e.stage_hunk()),
835    },
836    Binding {
837        keys: "space g S",
838        desc: "hunk: unstage (index→HEAD)",
839        section: "git",
840        live: true,
841        id: "git-hunk-unstage",
842        handler: Handler::Leaf(|e, _| e.unstage_hunk()),
843    },
844    Binding {
845        keys: "space g p",
846        desc: "hunk: preview",
847        section: "git",
848        live: true,
849        id: "git-hunk-preview",
850        handler: Handler::Leaf(|e, _| e.preview_hunk()),
851    },
852    Binding {
853        keys: "]f [f",
854        desc: "next / prev file in commit diff",
855        section: "git",
856        live: true,
857        id: "commit-file-nav",
858        handler: Handler::Soon,
859    },
860    Binding {
861        keys: "enter",
862        desc: "dive into the line's commit (blame gutter)",
863        section: "git",
864        live: true,
865        id: "surface-dive",
866        handler: Handler::Prefix,
867    },
868    Binding {
869        keys: "q",
870        desc: "close surface (readonly buffers)",
871        section: "git",
872        live: true,
873        id: "surface-close",
874        handler: Handler::Prefix,
875    },
876    // ex + panes
877    Binding {
878        keys: ":w :q :q! :wq :w {file} :trust",
879        desc: "write / quit (force) / write-quit / write-as",
880        section: "ex+panes",
881        live: true,
882        id: "ex-write-quit",
883        handler: Handler::TextLine,
884    },
885    Binding {
886        keys: ":[range]s/a/b/[g] :N :% :N,Md :N,My",
887        desc: "substitute (literal) / goto line / ranged delete+yank",
888        section: "ex+panes",
889        live: true,
890        id: "ex-ranges",
891        handler: Handler::TextLine,
892    },
893    Binding {
894        keys: "ctrl-d ctrl-u ctrl-f ctrl-b",
895        desc: "half/full page scroll (count = lines)",
896        section: "ex+panes",
897        live: true,
898        id: "scroll-pages",
899        handler: Handler::TextLine,
900    },
901    Binding {
902        keys: ":e",
903        desc: "edit file",
904        section: "ex+panes",
905        live: true,
906        id: "ex-edit",
907        handler: Handler::TextLine,
908    },
909    Binding {
910        keys: ":help",
911        desc: "help buffer (this text — / searches it)",
912        section: "ex+panes",
913        live: true,
914        id: "ex-help",
915        handler: Handler::TextLine,
916    },
917    Binding {
918        keys: ":vs :sp",
919        desc: "split vertical / horizontal",
920        section: "ex+panes",
921        live: true,
922        id: "ex-split",
923        handler: Handler::TextLine,
924    },
925    Binding {
926        keys: "ctrl-w h / l / j / k / w",
927        desc: "pane move / cycle",
928        section: "ex+panes",
929        live: true,
930        id: "pane-nav",
931        handler: Handler::Leaf(crate::editor::Editor::pane_move_pub),
932    },
933    Binding {
934        keys: "ctrl-o / ctrl-i (tab)",
935        desc: "jump back / forward (jumplist)",
936        section: "ex+panes",
937        live: true,
938        id: "jumplist",
939        handler: Handler::Leaf(|e, _| crate::editor::Editor::jump_back(e)),
940    },
941    Binding {
942        keys: "ctrl-w v / s",
943        desc: "pane split (vs / sp)",
944        section: "ex+panes",
945        live: true,
946        id: "pane-split",
947        handler: Handler::Leaf(crate::editor::Editor::split_pub),
948    },
949    Binding {
950        keys: ":view :set",
951        desc: "readonly browsing (:set ro/noro; CLI: -R)",
952        section: "ex+panes",
953        live: true,
954        id: "readonly",
955        handler: Handler::TextLine,
956    },
957    Binding {
958        keys: "ctrl-w q",
959        desc: "close pane (last → buffer)",
960        section: "ex+panes",
961        live: true,
962        id: "pane-close",
963        handler: Handler::Leaf(|e, _| crate::editor::Editor::pane_close_pub(e)),
964    },
965    Binding {
966        keys: "up down left right tab s-tab",
967        desc: "picker navigation / arrows = hjkl everywhere",
968        section: "ex+panes",
969        live: true,
970        id: "picker-nav",
971        handler: Handler::Prefix,
972    },
973    Binding {
974        keys: "ctrl-x",
975        desc: "replace picker: exclude/include match",
976        section: "ex+panes",
977        live: true,
978        id: "replace-exclude",
979        handler: Handler::Prefix,
980    },
981    Binding {
982        keys: "ctrl-l",
983        desc: "redraw: full repaint when the terminal desyncs",
984        section: "ex+panes",
985        live: true,
986        id: "redraw",
987        handler: Handler::Leaf(|e, _| e.needs_repaint = true),
988    },
989];
990
991pub(crate) mod lookup;
992
993pub use lookup::{any_child, children_of, compat_report, find_row, Hint};
994
995#[cfg(test)]
996mod tests;