Skip to main content

fret_runtime/keymap/
ops.rs

1use crate::{CommandId, InputContext, KeyChord};
2use std::collections::HashSet;
3use std::sync::Arc;
4
5use super::{Binding, Keymap, KeymapContinuation, SequenceMatch};
6
7impl Keymap {
8    pub fn empty() -> Self {
9        Self::default()
10    }
11
12    pub fn push_binding(&mut self, binding: Binding) {
13        self.bindings.push(binding);
14    }
15
16    /// Last-wins resolution. If a later binding matches and its `command` is `None`, the key is
17    /// explicitly unbound and resolution stops.
18    pub fn resolve(&self, ctx: &InputContext, chord: KeyChord) -> Option<CommandId> {
19        self.resolve_with_key_contexts(ctx, &[], chord)
20    }
21
22    /// Like [`Self::resolve`], but evaluates `when` expressions with a key-context stack.
23    pub fn resolve_with_key_contexts(
24        &self,
25        ctx: &InputContext,
26        key_contexts: &[Arc<str>],
27        chord: KeyChord,
28    ) -> Option<CommandId> {
29        for b in self.bindings.iter().rev() {
30            if b.sequence.as_slice() != [chord] {
31                continue;
32            }
33            if !b.platform.matches(ctx.platform) {
34                continue;
35            }
36            if let Some(expr) = b.when.as_ref()
37                && !expr.eval_with_key_contexts(ctx, key_contexts)
38            {
39                continue;
40            }
41            return b.command.clone();
42        }
43        None
44    }
45
46    /// Sequence matching helper used by pending multi-stroke bindings (ADR 0043).
47    pub fn match_sequence(&self, ctx: &InputContext, sequence: &[KeyChord]) -> SequenceMatch {
48        self.match_sequence_with_key_contexts(ctx, &[], sequence)
49    }
50
51    /// Like [`Self::match_sequence`], but evaluates `when` expressions with a key-context stack.
52    pub fn match_sequence_with_key_contexts(
53        &self,
54        ctx: &InputContext,
55        key_contexts: &[Arc<str>],
56        sequence: &[KeyChord],
57    ) -> SequenceMatch {
58        let mut exact: Option<Option<CommandId>> = None;
59        let mut has_continuation = false;
60
61        // Track full sequences we've already evaluated to preserve last-wins semantics for
62        // continuations and exact matches under the current context.
63        let mut seen: HashSet<Vec<KeyChord>> = HashSet::new();
64
65        for b in self.bindings.iter().rev() {
66            if !b.platform.matches(ctx.platform) {
67                continue;
68            }
69            if let Some(expr) = b.when.as_ref()
70                && !expr.eval_with_key_contexts(ctx, key_contexts)
71            {
72                continue;
73            }
74
75            if b.sequence.len() < sequence.len() {
76                continue;
77            }
78            if b.sequence.get(0..sequence.len()) != Some(sequence) {
79                continue;
80            }
81
82            if !seen.insert(b.sequence.clone()) {
83                continue;
84            }
85
86            if b.sequence.len() == sequence.len() {
87                if exact.is_none() {
88                    exact = Some(b.command.clone());
89                }
90            } else if b.command.is_some() {
91                has_continuation = true;
92            }
93
94            if exact.is_some() && has_continuation {
95                break;
96            }
97        }
98
99        SequenceMatch {
100            exact,
101            has_continuation,
102        }
103    }
104
105    /// Lists the valid "next" keystrokes that can follow the provided prefix under the given
106    /// input context.
107    ///
108    /// This is intended for UI hint overlays (e.g. a leader-key popup): it enumerates candidate
109    /// next chords from the configured bindings, then uses `match_sequence` to filter down to
110    /// chords that either execute a command or have further continuations.
111    pub fn continuations(
112        &self,
113        ctx: &InputContext,
114        prefix: &[KeyChord],
115    ) -> Vec<KeymapContinuation> {
116        self.continuations_with_key_contexts(ctx, &[], prefix)
117    }
118
119    /// Like [`Self::continuations`], but evaluates `when` expressions with a key-context stack.
120    pub fn continuations_with_key_contexts(
121        &self,
122        ctx: &InputContext,
123        key_contexts: &[Arc<str>],
124        prefix: &[KeyChord],
125    ) -> Vec<KeymapContinuation> {
126        if prefix.is_empty() {
127            return Vec::new();
128        }
129
130        let mut candidates: Vec<KeyChord> = Vec::new();
131        let mut seen: HashSet<KeyChord> = HashSet::new();
132
133        for b in self.bindings.iter().rev() {
134            if !b.platform.matches(ctx.platform) {
135                continue;
136            }
137            if let Some(expr) = b.when.as_ref()
138                && !expr.eval_with_key_contexts(ctx, key_contexts)
139            {
140                continue;
141            }
142            if b.sequence.len() <= prefix.len() {
143                continue;
144            }
145            if b.sequence.get(0..prefix.len()) != Some(prefix) {
146                continue;
147            }
148
149            let next = b.sequence[prefix.len()];
150            if seen.insert(next) {
151                candidates.push(next);
152            }
153        }
154
155        let mut out: Vec<KeymapContinuation> = Vec::new();
156        for next in candidates {
157            let mut seq: Vec<KeyChord> = Vec::with_capacity(prefix.len() + 1);
158            seq.extend_from_slice(prefix);
159            seq.push(next);
160
161            let matched = self.match_sequence_with_key_contexts(ctx, key_contexts, &seq);
162            let exact_command = matched.exact.clone().flatten();
163            if exact_command.is_some() || matched.has_continuation {
164                out.push(KeymapContinuation { next, matched });
165            }
166        }
167
168        out
169    }
170
171    pub fn extend(&mut self, other: Keymap) {
172        self.bindings.extend(other.bindings);
173    }
174}