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