Skip to main content

lattice_host/
decline.rs

1//! AP.0.2 — the layer peel behind `Effect::Declined`, in one place.
2//!
3//! A mode can bind a chord it only sometimes wants. auto-pair binds `(` and
4//! `<BS>`; table-mode binds `<Tab>`. When the situation is not theirs, the
5//! action returns [`Effect::Declined`](lattice_grammar::Effect::Declined) —
6//! "I did nothing" — and the chord must be re-resolved **as if that mode's
7//! layer were not there**, so it reaches whatever is underneath. A declined
8//! `(` types a bracket; a declined `<Tab>` folds.
9//!
10//! ## Why this is a type and not a loop at each call site
11//!
12//! It was a loop at each call site, twice, and the third caller — the GPUI
13//! peer — had neither. Every key auto-pair binds (`( [ { ) ] } " ' \``, and
14//! backspace) was therefore dead in that renderer, and `<Tab>` never fell
15//! through to org's fold cycle. The TUI's copy carried the comment "mirrors
16//! the host peel exactly (GPUI rides that path)", which was not true and
17//! could not be checked.
18//!
19//! What varies between callers is real: each builds its translate context
20//! from a different place and applies the resulting action through its own
21//! path (the peers have renderer-coupled intercepts the host does not). What
22//! does NOT vary is the walk — which layer to drop, what the prefix is, and
23//! when to stop — and that is what lives here. Two bugs came out of getting
24//! exactly those wrong:
25//!
26//! - **Dropping every layer at once.** `active_minor_modes: &[]` means a
27//!   second declining layer never sees the chord, so table-mode's `<Tab>`
28//!   declining outside a table skipped org-mode's fold cycle entirely and
29//!   landed on the builtin jump-list. Layers compose; a chain of length two
30//!   is the first case anyone writes.
31//! - **Losing the prefix.** Re-translating the final chord alone turned a
32//!   declined `<leader>oJ` into vim's `J`, which joined two lines. The peel
33//!   re-resolves the SEQUENCE.
34
35use lattice_grammar::ModalState;
36use lattice_mode::ModeId;
37
38use crate::chord::KeyChord;
39use crate::keymap::BindingMode;
40use crate::keymap_registry::KeymapHandle;
41
42/// The [`BindingMode`] a modal state resolves chords in.
43///
44/// One definition: the host's dispatcher, the TUI's runtime and this walk all
45/// used to carry their own copy of this match, and a mode added to one of
46/// them would silently resolve against the wrong keymap in the others.
47pub fn binding_mode_for(modal: ModalState) -> BindingMode {
48    match modal {
49        ModalState::Insert => BindingMode::Insert,
50        ModalState::Visual(_) | ModalState::Select(_) => BindingMode::Visual,
51        ModalState::OperatorPending => BindingMode::OperatorPending,
52        ModalState::Replace => BindingMode::Replace,
53        _ => BindingMode::Normal,
54    }
55}
56
57/// The layer-peeling walk behind a declined chord.
58///
59/// Hold one across the re-resolution loop: each [`peel`](Self::peel) removes
60/// the single layer that produced the declining binding and hands back the
61/// reduced set to re-translate against. The caller translates and applies;
62/// this decides what to translate against and when to stop.
63#[derive(Debug, Clone)]
64pub struct DeclinePeel {
65    binding_mode: BindingMode,
66    prefix: Vec<KeyChord>,
67    chord: KeyChord,
68    layers: Vec<ModeId>,
69}
70
71impl DeclinePeel {
72    /// `prefix` is the partial chord as it stood **before** the declined
73    /// dispatch — the sequence this chord completes, not what the dispatch
74    /// left behind.
75    pub fn new(
76        modal: ModalState,
77        prefix: Vec<KeyChord>,
78        chord: KeyChord,
79        layers: Vec<ModeId>,
80    ) -> Self {
81        Self {
82            binding_mode: binding_mode_for(modal),
83            prefix,
84            chord,
85            layers,
86        }
87    }
88
89    /// Drop the layer that produced the declining binding, and yield the
90    /// reduced layer set to re-translate against.
91    ///
92    /// `None` means there is nothing left to peel: the winning binding came
93    /// from the always-on Builtin / User layers, which cannot decline. The
94    /// walk is bounded — every pass removes exactly one layer.
95    pub fn peel(&mut self, keymap: &KeymapHandle) -> Option<&[ModeId]> {
96        let declining = crate::keymap_normal::binding_layer_mode(
97            keymap,
98            self.binding_mode,
99            &self.prefix,
100            &self.chord,
101            &self.layers,
102        )?;
103        self.layers.retain(|m| *m != declining);
104        Some(&self.layers)
105    }
106
107    /// The sequence this chord completes. Re-translation uses it, so a
108    /// declined multi-key chord re-resolves as the sequence it was.
109    pub fn prefix(&self) -> &[KeyChord] {
110        &self.prefix
111    }
112
113    pub fn chord(&self) -> KeyChord {
114        self.chord
115    }
116
117    pub fn layers(&self) -> &[ModeId] {
118        &self.layers
119    }
120
121    pub fn binding_mode(&self) -> BindingMode {
122        self.binding_mode
123    }
124
125    /// True when no mode layers remain, so a further decline has nowhere to
126    /// go and the caller should stop.
127    pub fn exhausted(&self) -> bool {
128        self.layers.is_empty()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn every_modal_state_maps_to_the_keymap_it_resolves_in() {
138        assert_eq!(binding_mode_for(ModalState::Insert), BindingMode::Insert);
139        assert_eq!(binding_mode_for(ModalState::Replace), BindingMode::Replace);
140        assert_eq!(
141            binding_mode_for(ModalState::OperatorPending),
142            BindingMode::OperatorPending
143        );
144        assert_eq!(binding_mode_for(ModalState::Normal), BindingMode::Normal);
145    }
146
147    /// The prefix is what the chord COMPLETES, and it survives every pass —
148    /// re-resolving the trailing key alone is what turned a declined
149    /// `<leader>oJ` into vim's `J`.
150    #[test]
151    fn the_prefix_and_chord_are_held_across_the_walk() {
152        let prefix = vec![KeyChord::char('g')];
153        let peel = DeclinePeel::new(
154            ModalState::Normal,
155            prefix.clone(),
156            KeyChord::char('J'),
157            vec![ModeId::new("a-mode")],
158        );
159        assert_eq!(peel.prefix(), prefix.as_slice());
160        assert_eq!(peel.chord(), KeyChord::char('J'));
161        assert_eq!(peel.binding_mode(), BindingMode::Normal);
162    }
163
164    /// A walk with no layers is already finished — the caller must not spin.
165    #[test]
166    fn a_walk_with_no_layers_is_exhausted() {
167        let peel = DeclinePeel::new(
168            ModalState::Insert,
169            Vec::new(),
170            KeyChord::char('('),
171            Vec::new(),
172        );
173        assert!(peel.exhausted());
174        assert!(peel.layers().is_empty());
175    }
176}