1use std::sync::LazyLock;
5
6use super::{Binding, BINDINGS, SECTIONS};
7
8pub fn expand(keys: &str) -> Vec<Vec<&str>> {
10 let toks: Vec<&str> = keys.split(' ').filter(|t| !t.is_empty()).collect();
11 let mut seqs: Vec<Vec<&str>> = Vec::new();
12 let mut i = 0;
13 while i < toks.len() {
14 match toks[i] {
15 "/" if seqs.is_empty() => seqs.push(vec!["/"]),
16 "/" => {
17 let base = match seqs.last() {
18 Some(s) => s[..s.len() - 1].to_vec(),
19 None => Vec::new(),
20 };
21 for alt in &toks[i + 1..] {
22 if *alt != "/" {
23 let mut seq = base.clone();
24 seq.push(alt);
25 seqs.push(seq);
26 }
27 }
28 break;
29 }
30 "space" => {
31 let end = match (i + 1..toks.len()).find(|&j| toks[j] == "/" && j + 1 < toks.len())
32 {
33 Some(end) => end,
34 None => toks.len(),
35 };
36 seqs.push(toks[i..end].to_vec());
37 i = end;
38 continue;
39 }
40 "ctrl-w" => {
41 if let Some(k) = toks.get(i + 1) {
42 seqs.push(vec!["ctrl-w", k]);
43 i += 2;
44 } else {
45 seqs.push(vec!["ctrl-w"]);
46 i += 1;
47 }
48 continue;
49 }
50 t => seqs.push(vec![t]),
51 }
52 i += 1;
53 }
54 seqs
55}
56
57fn per_key(seq: &[&str]) -> Vec<String> {
58 let mut out = Vec::new();
59 for &t in seq {
60 if t == "<space>" {
61 out.push("space".to_string());
64 continue;
65 }
66 if t.len() > 1 && !t.starts_with('<') && !t.starts_with(':') && !NAMED.contains(&t) {
67 if let Some(i) = t.find('<') {
68 out.extend(t[..i].chars().map(|c| c.to_string()));
69 out.push(if t[i..] == *"<space>" {
72 "space".to_string()
73 } else {
74 t[i..].to_string()
75 });
76 } else {
77 out.extend(t.chars().map(|c| c.to_string()));
78 }
79 } else {
80 out.push(t.to_string());
81 }
82 }
83 out
84}
85
86pub(crate) const NAMED: &[&str] = &[
87 "space",
88 "ctrl-w",
89 "ctrl-o",
90 "ctrl-i",
91 "up",
92 "down",
93 "left",
94 "right",
95 "tab",
96 "s-tab",
97 "esc",
98 "enter",
99 "backspace",
100 "ctrl-r",
101 "ctrl-x",
102 "ctrl-d",
103 "ctrl-u",
104 "ctrl-f",
105 "ctrl-b",
106 "ctrl-^",
107 "ctrl-v",
108 "ctrl-l",
109 "ctrl-space",
110];
111
112fn is_placeholder(k: &str) -> bool {
114 k.len() > 1 && k.starts_with('<')
115}
116
117#[derive(Clone, Copy)]
118struct NodeId(usize);
119
120const ROOT: NodeId = NodeId(0);
121
122#[derive(Default)]
123struct Node {
124 literals: Vec<(String, NodeId)>,
125 wildcard: Option<NodeId>,
126 row: Option<usize>,
127 live_child: bool,
129}
130
131struct HintSequence {
132 row: usize,
133 tokens: Vec<&'static str>,
134 flat: String,
135 bounds: Vec<usize>,
136 char_len: usize,
137 weight: usize,
138}
139
140impl HintSequence {
141 fn new(row: usize, tokens: Vec<&'static str>) -> Self {
142 let mut flat = String::new();
143 let mut bounds = Vec::new();
144 let mut weight = 0;
145 for &t in &tokens {
146 bounds.push(flat.len());
147 let text = if t == "space" { " " } else { t };
148 flat.push_str(text);
149 weight += text.len();
150 }
151 let char_len = flat.chars().count();
152 Self {
153 row,
154 tokens,
155 flat,
156 bounds,
157 char_len,
158 weight,
159 }
160 }
161
162 fn child_key(&self, prefix: &str, plen: usize) -> Option<String> {
165 if plen == 0 || !self.flat.starts_with(prefix) || self.char_len <= plen {
166 return None;
167 }
168 match self.bounds.iter().position(|&b| b == plen) {
169 Some(i) => Some(self.tokens[i].to_string()),
170 None => {
171 let i = self.bounds.iter().rposition(|&b| b < plen)?;
172 Some(self.tokens[i].chars().skip(plen - self.bounds[i]).collect())
173 }
174 }
175 }
176}
177
178struct Index {
179 nodes: Vec<Node>,
180 hints: Vec<HintSequence>,
181}
182
183static INDEX: LazyLock<Index> = LazyLock::new(Index::compile);
184
185impl Index {
186 fn compile() -> Self {
187 let mut index = Self {
188 nodes: vec![Node::default()],
189 hints: Vec::new(),
190 };
191 for (row, binding) in BINDINGS.iter().enumerate() {
192 for tokens in expand(binding.keys) {
193 if binding.live && !matches!(binding.handler, super::Handler::Contextual) {
194 index.insert(row, per_key(&tokens));
195 }
196 index.hints.push(HintSequence::new(row, tokens));
197 }
198 }
199 index.hints.sort_by_key(|seq| seq.weight);
201 index
202 }
203
204 fn insert(&mut self, row: usize, keys: Vec<String>) {
205 let mut at = ROOT;
206 for key in keys {
207 self.nodes[at.0].live_child = true;
208 let wildcard = is_placeholder(&key);
209 let existing = if wildcard {
210 self.nodes[at.0].wildcard
211 } else {
212 self.nodes[at.0]
213 .literals
214 .iter()
215 .find(|(literal, _)| literal == &key)
216 .map(|(_, id)| *id)
217 };
218 at = match existing {
219 Some(id) => id,
220 None => {
221 let id = NodeId(self.nodes.len());
222 self.nodes.push(Node::default());
223 if wildcard {
224 self.nodes[at.0].wildcard = Some(id);
225 } else {
226 self.nodes[at.0].literals.push((key, id));
227 }
228 id
229 }
230 };
231 }
232 if self.nodes[at.0].row.is_none() {
234 self.nodes[at.0].row = Some(row);
235 }
236 }
237
238 fn literal_child(&self, at: NodeId, key: &str) -> Option<NodeId> {
239 self.nodes[at.0]
240 .literals
241 .iter()
242 .find(|(literal, _)| literal == key)
243 .map(|(_, id)| *id)
244 }
245
246 fn find(&self, at: NodeId, path: &[String]) -> Option<usize> {
247 let Some((key, rest)) = path.split_first() else {
248 return self.nodes[at.0].row;
249 };
250 let literal = self
251 .literal_child(at, key)
252 .and_then(|id| self.find(id, rest));
253 let wildcard = self.nodes[at.0].wildcard.and_then(|id| self.find(id, rest));
254 match (literal, wildcard) {
256 (Some(a), Some(b)) => Some(a.min(b)),
257 (Some(a), None) => Some(a),
258 (None, b) => b,
259 }
260 }
261
262 fn has_child(&self, at: NodeId, path: &[String]) -> bool {
263 let Some((key, rest)) = path.split_first() else {
264 return self.nodes[at.0].live_child;
265 };
266 self.literal_child(at, key)
267 .is_some_and(|id| self.has_child(id, rest))
268 || self.nodes[at.0]
269 .wildcard
270 .is_some_and(|id| self.has_child(id, rest))
271 }
272}
273
274pub fn find_row(path: &[String]) -> Option<&'static Binding> {
276 INDEX.find(ROOT, path).map(|row| &BINDINGS[row])
277}
278
279pub fn any_child(path: &[String]) -> bool {
281 INDEX.has_child(ROOT, path)
282}
283
284pub struct Hint {
286 pub key: String,
287 pub desc: &'static str,
288 pub live: bool,
289}
290
291pub fn children_of(prefix: &str, mode: crate::editor::Mode) -> Vec<Hint> {
294 use crate::editor::Mode;
295 let sections: &[&str] = match mode {
296 Mode::Normal => &["normal", "leader", "git", "ex+panes"],
297 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => &["visual"],
298 Mode::Insert => &[],
299 };
300 let mut out: Vec<Hint> = Vec::new();
301 let plen = prefix.chars().count();
302 for seq in &INDEX.hints {
303 let b = &BINDINGS[seq.row];
304 if !b.sections.iter().any(|s| sections.contains(s)) {
305 continue;
306 }
307 if let Some(key) = seq.child_key(prefix, plen) {
308 if !out.iter().any(|hint| hint.key == key) {
309 out.push(Hint {
310 key,
311 desc: b.desc,
312 live: b.live,
313 });
314 }
315 }
316 }
317 out
318}
319
320pub fn compat_report() -> String {
322 let mut out = String::from(
323 "# Vim compatibility\n\nGenerated from the command table (`cargo test` pins freshness; \
324 STROP_REGEN=1 rewrites).\n`✓` ships exactly; `(soon)` is a planned slot.\n",
325 );
326 for section in SECTIONS {
327 out.push_str(&format!("\n## {section}\n\n"));
328 for b in BINDINGS.iter().filter(|b| b.sections.contains(section)) {
329 let mark = if b.live { "✓" } else { "·" };
330 let soon = if b.live { "" } else { " (soon)" };
331 out.push_str(&format!("- `{mark} {}` — {}{}\n", b.keys, b.desc, soon));
332 }
333 }
334 out
335}
336
337#[cfg(test)]
338mod index_tests {
339 use super::*;
340
341 #[test]
342 fn table_precedence_wins_over_literal_specificity() {
343 let mut index = Index {
344 nodes: vec![Node::default()],
345 hints: Vec::new(),
346 };
347 index.insert(1, vec!["g".into(), "<c>".into()]);
348 index.insert(3, vec!["g".into(), "x".into()]);
349 assert_eq!(index.find(ROOT, &["g".into(), "x".into()]), Some(1));
350 assert_eq!(index.find(ROOT, &["g".into(), "z".into()]), Some(1));
351 assert!(index.has_child(ROOT, &["g".into()]));
352 assert!(!index.has_child(ROOT, &["g".into(), "x".into()]));
353 }
354
355 #[test]
356 fn literals_and_longer_paths_keep_independent_terminals() {
357 let mut index = Index {
358 nodes: vec![Node::default()],
359 hints: Vec::new(),
360 };
361 index.insert(0, vec!["g".into(), "x".into()]);
362 index.insert(2, vec!["g".into(), "<c>".into()]);
363 index.insert(4, vec!["g".into(), "x".into(), "y".into()]);
364 assert_eq!(index.find(ROOT, &["g".into(), "x".into()]), Some(0));
365 assert_eq!(index.find(ROOT, &["g".into(), "z".into()]), Some(2));
366 assert!(index.has_child(ROOT, &["g".into(), "x".into()]));
367 assert_eq!(
368 index.find(ROOT, &["g".into(), "x".into(), "y".into()]),
369 Some(4)
370 );
371 }
372}