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