Skip to main content

lattice_host/
keymap_help.rs

1//! K.3.2 (2026-06-02): help-prefix (`<C-h>` map) bindings.
2//!
3//! Emacs-style discoverability for the §5.11 self-documenting
4//! help facility. From any Normal-mode buffer, `<C-h>` is a
5//! prefix that opens the help workflow:
6//!
7//! | Chord | Command | What it does |
8//! |---|---|---|
9//! | `<C-h> <C-h>` | `:help-for-help` | Open help index. |
10//! | `<C-h> ?` | `:help-for-help` | Alias — easier to type. |
11//! | `<C-h> k` | `:describe-key` | Prompt for chord, show binding. |
12//! | `<C-h> c` | `:describe-command` | Prompt for command name. |
13//! | `<C-h> o` | `:describe-option` | Prompt for option name. |
14//! | `<C-h> e` | `:describe-event` | Prompt for typed-event name. |
15//! | `<C-h> f` | `:describe-element` | Prompt for theme element / face name. |
16//! | `<C-h> m` | `:describe-active-modes` | Active major + minors on this buffer, with their chords. |
17//! | `<C-h> M` | `:describe-mode` | Prompt for a mode name; show that mode's metadata. |
18//! | `<C-h> b` | `:describe-buffer` | Buffer metadata (kind, flags, modes, …). |
19//! | `<C-h> a` | `:apropos` | Cross-cutting search. |
20//! | `<C-h> K` | `:describe-bindings` | Chords that can fire on this buffer. `:keymap` remains the full catalog. |
21//!
22//! ## DAM.4 correction (2026-08-04)
23//!
24//! K.3.2 bound `<C-h>m` to `:describe-mode`, whose `name` arg is
25//! `ArgDefault::Required` — so the no-arg invocation armed the
26//! interactive `mode:` prompt and asked which mode to describe. It
27//! never showed the active modes, though this table, the
28//! `HelpPrefixEntry` doc, and keymap-architecture §12.1 all said it
29//! did. `<C-h>m` now routes to `:describe-active-modes`; `<C-h>M`
30//! keeps the prompt-for-any-mode path that `<C-h>m` was accidentally
31//! providing, so nothing is lost.
32//!
33//! The `m` / `M` pair reads listing-then-specific while the older
34//! `k` / `K` pair reads specific-then-listing. Deliberate: lowercase
35//! is the common case in both, and `C-h m` = *active* modes is the
36//! emacs muscle memory worth preserving.
37//!
38//! ## Design choices (per K.3 slice plan)
39//!
40//! - **No bare `<C-h>` leaf.** K.3.0's trie audit found that
41//!   today's matcher returns `Bound` immediately when a node
42//!   carries a binding, even if it also has children — and the
43//!   dispatcher has no `timeoutlen` machinery to wait for a
44//!   follow-on chord. Vim's "leaf + prefix" ambiguity needs
45//!   timer-driven dispatch which isn't worth introducing for a
46//!   single help affordance. Instead, `<C-h>` is a pure prefix
47//!   node (returns `Partial`), and the slice plan's listed
48//!   alternative `<C-h><C-h>` (plus the easier-to-type
49//!   `<C-h>?`) is the explicit help-for-help entry. One
50//!   keystroke of extra friction; zero new infrastructure.
51//! - **Normal mode only.** Insert / Visual / OperatorPending /
52//!   Cmdline retain their existing `<C-h>` semantics (Insert
53//!   keeps backspace; cmdline keeps cmdline-backspace). The
54//!   K.1.c per-keystroke filter naturally enforces this because
55//!   bindings are registered with `BindingMode::Normal`; other
56//!   modes simply don't match.
57//! - **`KeymapLayer::Builtin`.** Universal availability across
58//!   every Normal-mode buffer — same shape as Normal-mode
59//!   motions, operators, and the rest of the vim default
60//!   catalog. The mode-architecture §13 convention ("feature-
61//!   gated bindings live at MinorMode") doesn't apply here
62//!   because the help prefix isn't feature-gated; it's a
63//!   universal discoverability affordance.
64
65use lattice_grammar::CommandRegistry;
66use lattice_grammar::command::CommandInvocation;
67use lattice_grammar::source::SourceLocation;
68
69use crate::chord::{KeyChord, KeyKind, KeyMods};
70use crate::keymap::BindingMode;
71use crate::keymap_registry::KeymapHandle;
72use crate::keymap_trie::{ChordPattern, KeymapLayer};
73
74/// One row of the K.3.2 help-prefix binding table.
75///
76/// `chord` is a two-element chord sequence (every help-prefix
77/// binding is `<C-h>` followed by exactly one key); `command`
78/// is the canonical name registered against the
79/// `CommandRegistry` (always an `ex:` form for this slice —
80/// every help-prefix chord routes to an ex-command, no App-side
81/// actions).
82struct HelpPrefixEntry {
83    chord: &'static [ChordPattern; 2],
84    command: &'static str,
85    doc: &'static str,
86}
87
88/// Register the `<C-h>` help-prefix bindings into the supplied
89/// handle's `Builtin` layer at `BindingMode::Normal`.
90///
91/// Looks up each row's canonical command name in the
92/// `CommandRegistry`. If a name doesn't resolve (catalog drift
93/// — every help-prefix command should be registered at boot
94/// before this pass runs), emits a `tracing::warn!` and skips
95/// the row rather than panicking; matches the K.2.4.A.0.3
96/// translation-pass convention.
97pub fn register_help_prefix_bindings(handle: &KeymapHandle, command_registry: &CommandRegistry) {
98    let layer = KeymapLayer::Builtin;
99    let mode = BindingMode::Normal;
100
101    for entry in help_prefix_table() {
102        let Some(cmd_id) = command_registry.id_by_name(entry.command) else {
103            tracing::warn!(
104                command = entry.command,
105                doc = entry.doc,
106                "keymap_help: command not registered in CommandRegistry; skipping <C-h>-prefix binding",
107            );
108            continue;
109        };
110        handle.bind(
111            layer,
112            mode,
113            entry.chord,
114            CommandInvocation::of(cmd_id),
115            SourceLocation::builtin_file(file!(), line!()),
116        );
117    }
118}
119
120/// Compile-time-static help-prefix binding table. Each row
121/// pairs a 2-chord sequence with the canonical ex-command name.
122fn help_prefix_table() -> &'static [HelpPrefixEntry] {
123    const C_H_C_H: &[ChordPattern; 2] = &[
124        ChordPattern::Literal(KeyChord {
125            key: KeyKind::Char('h'),
126            mods: KeyMods::CTRL,
127        }),
128        ChordPattern::Literal(KeyChord {
129            key: KeyKind::Char('h'),
130            mods: KeyMods::CTRL,
131        }),
132    ];
133    const C_H_QUESTION: &[ChordPattern; 2] = &[
134        ChordPattern::Literal(KeyChord {
135            key: KeyKind::Char('h'),
136            mods: KeyMods::CTRL,
137        }),
138        ChordPattern::Literal(KeyChord {
139            key: KeyKind::Char('?'),
140            mods: KeyMods::NONE,
141        }),
142    ];
143    const C_H_K: &[ChordPattern; 2] = &[
144        ChordPattern::Literal(KeyChord {
145            key: KeyKind::Char('h'),
146            mods: KeyMods::CTRL,
147        }),
148        ChordPattern::Literal(KeyChord {
149            key: KeyKind::Char('k'),
150            mods: KeyMods::NONE,
151        }),
152    ];
153    const C_H_C: &[ChordPattern; 2] = &[
154        ChordPattern::Literal(KeyChord {
155            key: KeyKind::Char('h'),
156            mods: KeyMods::CTRL,
157        }),
158        ChordPattern::Literal(KeyChord {
159            key: KeyKind::Char('c'),
160            mods: KeyMods::NONE,
161        }),
162    ];
163    const C_H_O: &[ChordPattern; 2] = &[
164        ChordPattern::Literal(KeyChord {
165            key: KeyKind::Char('h'),
166            mods: KeyMods::CTRL,
167        }),
168        ChordPattern::Literal(KeyChord {
169            key: KeyKind::Char('o'),
170            mods: KeyMods::NONE,
171        }),
172    ];
173    const C_H_E: &[ChordPattern; 2] = &[
174        ChordPattern::Literal(KeyChord {
175            key: KeyKind::Char('h'),
176            mods: KeyMods::CTRL,
177        }),
178        ChordPattern::Literal(KeyChord {
179            key: KeyKind::Char('e'),
180            mods: KeyMods::NONE,
181        }),
182    ];
183    const C_H_F: &[ChordPattern; 2] = &[
184        ChordPattern::Literal(KeyChord {
185            key: KeyKind::Char('h'),
186            mods: KeyMods::CTRL,
187        }),
188        ChordPattern::Literal(KeyChord {
189            key: KeyKind::Char('f'),
190            mods: KeyMods::NONE,
191        }),
192    ];
193    const C_H_M: &[ChordPattern; 2] = &[
194        ChordPattern::Literal(KeyChord {
195            key: KeyKind::Char('h'),
196            mods: KeyMods::CTRL,
197        }),
198        ChordPattern::Literal(KeyChord {
199            key: KeyKind::Char('m'),
200            mods: KeyMods::NONE,
201        }),
202    ];
203    const C_H_CAP_M: &[ChordPattern; 2] = &[
204        ChordPattern::Literal(KeyChord {
205            key: KeyKind::Char('h'),
206            mods: KeyMods::CTRL,
207        }),
208        ChordPattern::Literal(KeyChord {
209            key: KeyKind::Char('M'),
210            mods: KeyMods::NONE,
211        }),
212    ];
213    const C_H_B: &[ChordPattern; 2] = &[
214        ChordPattern::Literal(KeyChord {
215            key: KeyKind::Char('h'),
216            mods: KeyMods::CTRL,
217        }),
218        ChordPattern::Literal(KeyChord {
219            key: KeyKind::Char('b'),
220            mods: KeyMods::NONE,
221        }),
222    ];
223    const C_H_A: &[ChordPattern; 2] = &[
224        ChordPattern::Literal(KeyChord {
225            key: KeyKind::Char('h'),
226            mods: KeyMods::CTRL,
227        }),
228        ChordPattern::Literal(KeyChord {
229            key: KeyKind::Char('a'),
230            mods: KeyMods::NONE,
231        }),
232    ];
233    const C_H_CAP_K: &[ChordPattern; 2] = &[
234        ChordPattern::Literal(KeyChord {
235            key: KeyKind::Char('h'),
236            mods: KeyMods::CTRL,
237        }),
238        ChordPattern::Literal(KeyChord {
239            key: KeyKind::Char('K'),
240            mods: KeyMods::NONE,
241        }),
242    ];
243
244    &[
245        HelpPrefixEntry {
246            chord: C_H_C_H,
247            command: "ex:help",
248            doc: "Open the help-for-help index (alias of `<C-h>?`).",
249        },
250        HelpPrefixEntry {
251            chord: C_H_QUESTION,
252            command: "ex:help",
253            doc: "Open the help-for-help index.",
254        },
255        HelpPrefixEntry {
256            chord: C_H_K,
257            command: "ex:describe-key",
258            doc: "Prompt for a chord and show its resolved binding + provenance.",
259        },
260        HelpPrefixEntry {
261            chord: C_H_C,
262            command: "ex:describe-command",
263            doc: "Prompt for a command name and show its metadata.",
264        },
265        HelpPrefixEntry {
266            chord: C_H_O,
267            command: "ex:describe-option",
268            doc: "Prompt for an option name and show its metadata + current value.",
269        },
270        HelpPrefixEntry {
271            chord: C_H_E,
272            command: "ex:describe-event",
273            doc: "Prompt for a typed-event name and show its descriptor.",
274        },
275        HelpPrefixEntry {
276            chord: C_H_F,
277            command: "ex:describe-element",
278            doc: "Prompt for a theme element / face name and show its metadata + resolved style.",
279        },
280        HelpPrefixEntry {
281            chord: C_H_M,
282            command: "ex:describe-active-modes",
283            doc: "Show the active major + minor modes on the current buffer, with their chords.",
284        },
285        HelpPrefixEntry {
286            chord: C_H_CAP_M,
287            command: "ex:describe-mode",
288            doc: "Prompt for a mode name and show that mode's metadata.",
289        },
290        HelpPrefixEntry {
291            chord: C_H_B,
292            command: "ex:describe-buffer",
293            doc: "Show metadata for the current buffer (kind, flags, mode stack, …).",
294        },
295        HelpPrefixEntry {
296            chord: C_H_A,
297            command: "ex:apropos",
298            doc: "Cross-cutting search across commands / options / events.",
299        },
300        HelpPrefixEntry {
301            chord: C_H_CAP_K,
302            command: "ex:describe-bindings",
303            doc: "List the chords that can fire on the current buffer.",
304        },
305    ]
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    use lattice_grammar::CommandRegistry;
313
314    use crate::keymap_registry::KeymapHandle;
315    use crate::keymap_trie::LookupResult;
316
317    /// Build a CommandRegistry populated with the builtins +
318    /// ex-commands every help-prefix chord references.
319    fn registry_with_help_commands() -> CommandRegistry {
320        let mut r = CommandRegistry::new();
321        let _ = lattice_grammar::builtins::populate(&mut r);
322        let _ = lattice_grammar::ex_commands::populate(&mut r);
323        r
324    }
325
326    fn lookup_normal(handle: &KeymapHandle, chords: &[KeyChord]) -> LookupResult {
327        handle.lookup_with_context(BindingMode::Normal, chords, &[])
328    }
329
330    #[test]
331    fn help_prefix_chord_table_resolves_all_commands() {
332        // Every row's `command` name must resolve against the
333        // registry populated by `ex_commands::populate`. Catches
334        // catalog drift at test time — if a row points at an
335        // ex-command name that doesn't exist, this test fails
336        // loudly rather than the binding silently warn-and-skip
337        // at boot.
338        let registry = registry_with_help_commands();
339        for entry in help_prefix_table() {
340            assert!(
341                registry.id_by_name(entry.command).is_some(),
342                "help-prefix row `{}` ({}) references unregistered command",
343                entry.command,
344                entry.doc,
345            );
346        }
347    }
348
349    #[test]
350    fn register_help_prefix_binds_double_ctrl_h_to_ex_help() {
351        let handle = KeymapHandle::new();
352        let registry = registry_with_help_commands();
353        register_help_prefix_bindings(&handle, &registry);
354
355        let expected = registry
356            .id_by_name("ex:help")
357            .expect("ex:help registered by ex_commands::populate");
358        match lookup_normal(&handle, &[KeyChord::ctrl('h'), KeyChord::ctrl('h')]) {
359            LookupResult::Bound { command, .. } => {
360                assert_eq!(command.command.command, expected);
361                assert_eq!(command.layer, KeymapLayer::Builtin);
362            }
363            other => panic!("expected Bound on <C-h><C-h>, got {other:?}"),
364        }
365    }
366
367    #[test]
368    fn register_help_prefix_binds_describe_key_chord() {
369        let handle = KeymapHandle::new();
370        let registry = registry_with_help_commands();
371        register_help_prefix_bindings(&handle, &registry);
372
373        let expected = registry
374            .id_by_name("ex:describe-key")
375            .expect("ex:describe-key registered");
376        match lookup_normal(&handle, &[KeyChord::ctrl('h'), KeyChord::char('k')]) {
377            LookupResult::Bound { command, .. } => {
378                assert_eq!(command.command.command, expected);
379            }
380            other => panic!("expected Bound on <C-h>k, got {other:?}"),
381        }
382    }
383
384    #[test]
385    fn ctrl_h_m_shows_active_modes_without_prompting() {
386        // DAM.4, the whole point of the arc. `<C-h>m` must route to
387        // `:describe-active-modes` — a no-arg command — not to
388        // `:describe-mode`, whose required arg armed the `mode:`
389        // prompt and made the chord ask a question instead of
390        // answering one.
391        let handle = KeymapHandle::new();
392        let registry = registry_with_help_commands();
393        register_help_prefix_bindings(&handle, &registry);
394
395        let expected = registry
396            .id_by_name("ex:describe-active-modes")
397            .expect("ex:describe-active-modes registered");
398        match lookup_normal(&handle, &[KeyChord::ctrl('h'), KeyChord::char('m')]) {
399            LookupResult::Bound { command, .. } => {
400                assert_eq!(command.command.command, expected);
401                assert_eq!(command.layer, KeymapLayer::Builtin);
402            }
403            other => panic!("expected Bound on <C-h>m, got {other:?}"),
404        }
405
406        // And it takes no args, so the interactive prompt cannot arm.
407        let spec = registry
408            .lookup_by_name("ex:describe-active-modes")
409            .expect("registered");
410        assert!(
411            spec.args_schema.is_empty(),
412            "<C-h>m must not be able to prompt",
413        );
414    }
415
416    #[test]
417    fn ctrl_h_capital_m_keeps_the_prompt_for_any_mode_path() {
418        // The prompt `<C-h>m` was accidentally providing is preserved
419        // on `<C-h>M` rather than removed — `:describe-mode` still
420        // has its required arg + `gen:modes` completion.
421        let handle = KeymapHandle::new();
422        let registry = registry_with_help_commands();
423        register_help_prefix_bindings(&handle, &registry);
424
425        let expected = registry
426            .id_by_name("ex:describe-mode")
427            .expect("ex:describe-mode registered");
428        match lookup_normal(&handle, &[KeyChord::ctrl('h'), KeyChord::char('M')]) {
429            LookupResult::Bound { command, .. } => {
430                assert_eq!(command.command.command, expected);
431            }
432            other => panic!("expected Bound on <C-h>M, got {other:?}"),
433        }
434
435        let spec = registry
436            .lookup_by_name("ex:describe-mode")
437            .expect("registered");
438        assert_eq!(
439            spec.args_schema.len(),
440            1,
441            "<C-h>M must still prompt for a mode name",
442        );
443    }
444
445    #[test]
446    fn ctrl_h_m_and_capital_m_are_distinct_bindings() {
447        // Case matters in the trie: a regression that folded `M` into
448        // `m` would silently restore the old prompting behaviour on
449        // the chord users reach for most.
450        let handle = KeymapHandle::new();
451        let registry = registry_with_help_commands();
452        register_help_prefix_bindings(&handle, &registry);
453
454        let lower = match lookup_normal(&handle, &[KeyChord::ctrl('h'), KeyChord::char('m')]) {
455            LookupResult::Bound { command, .. } => command.command.command,
456            other => panic!("expected Bound on <C-h>m, got {other:?}"),
457        };
458        let upper = match lookup_normal(&handle, &[KeyChord::ctrl('h'), KeyChord::char('M')]) {
459            LookupResult::Bound { command, .. } => command.command.command,
460            other => panic!("expected Bound on <C-h>M, got {other:?}"),
461        };
462        assert_ne!(lower, upper, "<C-h>m and <C-h>M must not collapse");
463    }
464
465    #[test]
466    fn capital_m_help_prefix_does_not_fire_outside_normal() {
467        // Same mode-scope guarantee the rest of the map has — the new
468        // row must not leak into Insert/Visual/etc.
469        let handle = help_prefix_handle_for_mode_scope_tests();
470        for mode in [
471            BindingMode::Insert,
472            BindingMode::Visual,
473            BindingMode::OperatorPending,
474            BindingMode::Command,
475            BindingMode::Search,
476            BindingMode::Replace,
477        ] {
478            let result =
479                handle.lookup_with_context(mode, &[KeyChord::ctrl('h'), KeyChord::char('M')], &[]);
480            assert!(
481                matches!(result, LookupResult::Unbound),
482                "<C-h>M leaked into {mode:?}",
483            );
484        }
485    }
486
487    #[test]
488    fn ctrl_h_capital_k_scopes_bindings_to_the_buffer() {
489        // DAM.6: `<C-h>K` answers "what can I press here", so it
490        // routes to `:describe-bindings`, not `:keymap`. `:keymap`
491        // stays reachable as the exhaustive static catalog — it is
492        // just no longer what the chord opens.
493        let handle = KeymapHandle::new();
494        let registry = registry_with_help_commands();
495        register_help_prefix_bindings(&handle, &registry);
496
497        let expected = registry
498            .id_by_name("ex:describe-bindings")
499            .expect("ex:describe-bindings registered");
500        let keymap_id = registry
501            .id_by_name("ex:keymap")
502            .expect("ex:keymap still registered");
503        match lookup_normal(&handle, &[KeyChord::ctrl('h'), KeyChord::char('K')]) {
504            LookupResult::Bound { command, .. } => {
505                assert_eq!(command.command.command, expected);
506                assert_ne!(
507                    command.command.command, keymap_id,
508                    "<C-h>K must no longer open the full catalog",
509                );
510            }
511            other => panic!("expected Bound on <C-h>K, got {other:?}"),
512        }
513    }
514
515    #[test]
516    fn bare_ctrl_h_is_partial_not_bound() {
517        // Option 2 (K.3.0): no bare <C-h> leaf binding. The
518        // node is a pure prefix; lookup with the single chord
519        // returns `Partial` so the App's AbsorbPartialChord
520        // path takes over and waits for the next chord.
521        let handle = KeymapHandle::new();
522        let registry = registry_with_help_commands();
523        register_help_prefix_bindings(&handle, &registry);
524
525        match lookup_normal(&handle, &[KeyChord::ctrl('h')]) {
526            LookupResult::Partial => { /* expected */ }
527            other => panic!("expected Partial on bare <C-h>, got {other:?}"),
528        }
529    }
530
531    #[test]
532    fn help_prefix_does_not_fire_in_insert_mode() {
533        // K.3.3 mode-scope: the bindings register at
534        // BindingMode::Normal. Insert-mode lookup must miss —
535        // <C-h> in Insert retains its existing backspace
536        // semantics.
537        let handle = KeymapHandle::new();
538        let registry = registry_with_help_commands();
539        register_help_prefix_bindings(&handle, &registry);
540
541        let result = handle.lookup_with_context(
542            BindingMode::Insert,
543            &[KeyChord::ctrl('h'), KeyChord::char('k')],
544            &[],
545        );
546        assert!(matches!(result, LookupResult::Unbound));
547    }
548
549    #[test]
550    fn register_help_prefix_warns_and_skips_unresolvable_commands() {
551        // Defensive: an empty CommandRegistry — none of the
552        // `ex:*` commands resolve, every row should warn + skip.
553        // No bindings end up in the handle.
554        let handle = KeymapHandle::new();
555        let empty_registry = CommandRegistry::new();
556        register_help_prefix_bindings(&handle, &empty_registry);
557
558        match lookup_normal(&handle, &[KeyChord::ctrl('h'), KeyChord::ctrl('h')]) {
559            LookupResult::Unbound => { /* expected */ }
560            other => panic!("expected Unbound with empty registry, got {other:?}"),
561        }
562    }
563
564    // ---- K.3.3: mode-scope enforcement ----
565    //
566    // K.3.2 covers Normal (positive) and Insert (negative).
567    // K.3.3 fills out the remaining binding-modes the slice
568    // plan calls out: Visual, OperatorPending, and Cmdline
569    // (rich minibuffer's `Command` state). Each must NOT match
570    // the help-prefix bindings, so existing per-mode <C-h>
571    // semantics (cmdline backspace, etc.) keep working.
572
573    fn help_prefix_handle_for_mode_scope_tests() -> KeymapHandle {
574        let handle = KeymapHandle::new();
575        let registry = registry_with_help_commands();
576        register_help_prefix_bindings(&handle, &registry);
577        handle
578    }
579
580    #[test]
581    fn help_prefix_does_not_fire_in_visual_mode() {
582        let handle = help_prefix_handle_for_mode_scope_tests();
583        let result = handle.lookup_with_context(
584            BindingMode::Visual,
585            &[KeyChord::ctrl('h'), KeyChord::char('k')],
586            &[],
587        );
588        assert!(matches!(result, LookupResult::Unbound));
589    }
590
591    #[test]
592    fn help_prefix_does_not_fire_in_operator_pending_mode() {
593        // OperatorPending sees `<C-h>` mid-operator (after `d`,
594        // `y`, `c`, `>`, `<`). The help prefix must not absorb
595        // those keystrokes — letting `<C-h>k` resolve in
596        // OperatorPending would surprise users mid-operator.
597        let handle = help_prefix_handle_for_mode_scope_tests();
598        let result = handle.lookup_with_context(
599            BindingMode::OperatorPending,
600            &[KeyChord::ctrl('h'), KeyChord::char('k')],
601            &[],
602        );
603        assert!(matches!(result, LookupResult::Unbound));
604    }
605
606    #[test]
607    fn help_prefix_does_not_fire_in_cmdline_mode() {
608        // BindingMode::Command is the `:`-line / rich
609        // minibuffer state. <C-h> there has its own
610        // backspace-style semantics for editing the cmdline;
611        // the help-prefix bindings must not shadow them.
612        let handle = help_prefix_handle_for_mode_scope_tests();
613        let result = handle.lookup_with_context(
614            BindingMode::Command,
615            &[KeyChord::ctrl('h'), KeyChord::char('k')],
616            &[],
617        );
618        assert!(matches!(result, LookupResult::Unbound));
619    }
620
621    #[test]
622    fn help_prefix_does_not_fire_in_search_mode() {
623        // Search (`/` `?`) minibuffer — same shape as Command;
624        // <C-h> retains its existing search-line backspace.
625        let handle = help_prefix_handle_for_mode_scope_tests();
626        let result = handle.lookup_with_context(
627            BindingMode::Search,
628            &[KeyChord::ctrl('h'), KeyChord::char('k')],
629            &[],
630        );
631        assert!(matches!(result, LookupResult::Unbound));
632    }
633
634    #[test]
635    fn help_prefix_does_not_fire_in_replace_mode() {
636        // Replace mode is character-typing-with-overstrike;
637        // <C-h> there is restore-last-overwritten-byte. Help
638        // prefix must not interfere.
639        let handle = help_prefix_handle_for_mode_scope_tests();
640        let result = handle.lookup_with_context(
641            BindingMode::Replace,
642            &[KeyChord::ctrl('h'), KeyChord::char('k')],
643            &[],
644        );
645        assert!(matches!(result, LookupResult::Unbound));
646    }
647}