1use std::time::{Duration, Instant};
7
8pub const WHICH_KEY_DELAY_MS: u64 = 220;
10
11#[derive(Debug, Clone, Copy)]
13pub struct ChordHint {
14 pub key: &'static str,
15 pub desc: &'static str,
16}
17
18impl ChordHint {
19 pub const fn new(key: &'static str, desc: &'static str) -> Self {
20 Self { key, desc }
21 }
22}
23
24#[derive(Debug, Clone)]
26pub struct WhichKeyState {
27 pub leader: Option<String>,
30 pub title: String,
32 pub started: Option<Instant>,
34}
35
36impl Default for WhichKeyState {
37 fn default() -> Self {
38 Self {
39 leader: None,
40 title: String::new(),
41 started: None,
42 }
43 }
44}
45
46impl WhichKeyState {
47 pub fn clear(&mut self) {
48 self.leader = None;
49 self.title.clear();
50 self.started = None;
51 }
52
53 pub fn is_leader(&self) -> bool {
54 self.leader.is_some()
55 }
56
57 pub fn begin_prefix(&mut self, title: &str) {
59 self.leader = None;
60 self.title = title.to_string();
61 self.started = Some(Instant::now());
62 }
63
64 pub fn begin_leader(&mut self) {
66 self.leader = Some(String::new());
67 self.title = "SPC".into();
68 self.started = Some(Instant::now());
69 }
70
71 pub fn enter_leader_sub(&mut self, key: char, title_suffix: &str) {
73 self.leader = Some(key.to_string());
74 self.title = format!("SPC {title_suffix}");
75 self.started = Some(Instant::now());
76 }
77
78 pub fn ready(&self) -> bool {
80 match self.started {
81 Some(t) => t.elapsed() >= Duration::from_millis(WHICH_KEY_DELAY_MS),
82 None => false,
83 }
84 }
85
86 pub fn force_ready(&mut self) {
88 self.started = Some(Instant::now() - Duration::from_millis(WHICH_KEY_DELAY_MS + 1));
89 }
90}
91
92static MAP_G: &[ChordHint] = &[
95 ChordHint::new("g", "go to top"),
96 ChordHint::new("d", "go to definition"),
97 ChordHint::new("p", "peek definition"),
98 ChordHint::new("r", "references"),
99 ChordHint::new("C", "call hierarchy"),
100 ChordHint::new("I", "incoming calls"),
101 ChordHint::new("H", "outgoing calls"),
102 ChordHint::new("O", "document symbols"),
103 ChordHint::new("b", "git blame panel"),
104 ChordHint::new("t", "next tab"),
105 ChordHint::new("T", "prev tab"),
106];
107
108static MAP_Z: &[ChordHint] = &[
109 ChordHint::new("a", "toggle fold"),
110 ChordHint::new("c", "close fold"),
111 ChordHint::new("o", "open fold"),
112 ChordHint::new("M", "close all folds"),
113 ChordHint::new("R", "open all folds"),
114];
115
116static MAP_BRACKET_CLOSE: &[ChordHint] = &[
117 ChordHint::new("d", "next diagnostic"),
118 ChordHint::new("c", "next git change"),
119];
120
121static MAP_BRACKET_OPEN: &[ChordHint] = &[
122 ChordHint::new("d", "prev diagnostic"),
123 ChordHint::new("c", "prev git change"),
124];
125
126static MAP_CTRL_W: &[ChordHint] = &[
127 ChordHint::new("v", "vertical split"),
128 ChordHint::new("s", "horizontal split"),
129 ChordHint::new("w", "other pane"),
130 ChordHint::new("q", "close split"),
131 ChordHint::new("=", "equalize"),
132 ChordHint::new("h/l", "focus left/right"),
133 ChordHint::new("j/k", "focus down/up"),
134 ChordHint::new("</>", "resize"),
135];
136
137static MAP_REGISTER: &[ChordHint] = &[
138 ChordHint::new("a-z", "named register"),
139 ChordHint::new("A-Z", "append named"),
140 ChordHint::new("+/*", "system clipboard"),
141 ChordHint::new("\"", "unnamed"),
142];
143
144static MAP_MARK_SET: &[ChordHint] = &[ChordHint::new("a-z", "set mark")];
145static MAP_MARK_JUMP_LINE: &[ChordHint] = &[ChordHint::new("a-z", "jump to mark (line)")];
146static MAP_MARK_JUMP_EXACT: &[ChordHint] = &[ChordHint::new("a-z", "jump to mark (exact)")];
147static MAP_MACRO_RECORD: &[ChordHint] = &[ChordHint::new("a-z", "record macro")];
148static MAP_MACRO_PLAY: &[ChordHint] = &[
149 ChordHint::new("a-z", "play macro"),
150 ChordHint::new("@", "repeat last"),
151];
152
153static MAP_OP_DELETE: &[ChordHint] = &[
154 ChordHint::new("d", "delete line"),
155 ChordHint::new("w", "word"),
156 ChordHint::new("iw", "inner word"),
157 ChordHint::new("$", "to end of line"),
158 ChordHint::new("i\"", "in quotes"),
159 ChordHint::new("ib", "in parens"),
160 ChordHint::new("G", "to EOF"),
161 ChordHint::new("gg", "to BOF"),
162];
163
164static MAP_OP_CHANGE: &[ChordHint] = &[
165 ChordHint::new("c", "change line"),
166 ChordHint::new("w", "word"),
167 ChordHint::new("iw", "inner word"),
168 ChordHint::new("$", "to eol"),
169 ChordHint::new("i\"", "in quotes"),
170 ChordHint::new("ib", "in parens"),
171];
172
173static MAP_OP_YANK: &[ChordHint] = &[
174 ChordHint::new("y", "yank line"),
175 ChordHint::new("w", "word"),
176 ChordHint::new("iw", "inner word"),
177 ChordHint::new("$", "to eol"),
178 ChordHint::new("i\"", "in quotes"),
179];
180
181static MAP_TEXTOBJECT: &[ChordHint] = &[
182 ChordHint::new("w", "word"),
183 ChordHint::new("W", "WORD"),
184 ChordHint::new("\"/'", "quotes"),
185 ChordHint::new("b )", "parens"),
186 ChordHint::new("B }", "braces"),
187 ChordHint::new("[ ]", "brackets"),
188 ChordHint::new("t", "tag (html)"),
189];
190
191static MAP_SPACE_ROOT: &[ChordHint] = &[
192 ChordHint::new("f", "files…"),
193 ChordHint::new("b", "buffers…"),
194 ChordHint::new("g", "git…"),
195 ChordHint::new("l", "lsp…"),
196 ChordHint::new("d", "debug…"),
197 ChordHint::new("w", "window…"),
198 ChordHint::new("s", "search…"),
199 ChordHint::new("c", "code…"),
200 ChordHint::new("t", "toggle…"),
201 ChordHint::new("h", "help / settings"),
202 ChordHint::new("p", "command palette"),
203 ChordHint::new("/", "find in files"),
204 ChordHint::new(",", "settings"),
205 ChordHint::new("e", "file explorer"),
206 ChordHint::new(";", "XLC command"),
207];
208
209static MAP_SPACE_D: &[ChordHint] = &[
210 ChordHint::new("d", "debug panel focus"),
211 ChordHint::new("s", "start / continue (F5)"),
212 ChordHint::new("b", "toggle breakpoint (F9)"),
213 ChordHint::new("n", "step over (F10)"),
214 ChordHint::new("i", "step into (F11)"),
215 ChordHint::new("o", "step out (Shift+F11)"),
216 ChordHint::new("p", "pause (F6)"),
217 ChordHint::new("x", "stop (Shift+F5)"),
218 ChordHint::new("r", "restart"),
219 ChordHint::new("c", "launch.json configs"),
220 ChordHint::new("a", "attach help"),
221];
222
223static MAP_SPACE_F: &[ChordHint] = &[
224 ChordHint::new("f", "quick open file"),
225 ChordHint::new("e", "toggle explorer"),
226 ChordHint::new("s", "save"),
227 ChordHint::new("S", "save as (:w)"),
228 ChordHint::new("p", "pretty preview"),
229 ChordHint::new("r", "reload from disk"),
230];
231
232static MAP_SPACE_B: &[ChordHint] = &[
233 ChordHint::new("n", "next tab"),
234 ChordHint::new("p", "prev tab"),
235 ChordHint::new("d", "close buffer"),
236 ChordHint::new("b", "quick open"),
237 ChordHint::new("1-9", "goto tab (if open)"),
238];
239
240static MAP_SPACE_G: &[ChordHint] = &[
241 ChordHint::new("g", "git workbench"),
242 ChordHint::new("s", "source control"),
243 ChordHint::new("b", "blame panel"),
244 ChordHint::new("r", "interactive rebase"),
245 ChordHint::new("v", "PR review (selected)"),
246 ChordHint::new("f", "fetch"),
247 ChordHint::new("p", "pull"),
248 ChordHint::new("P", "push"),
249];
250
251static MAP_SPACE_L: &[ChordHint] = &[
252 ChordHint::new("d", "definition"),
253 ChordHint::new("r", "references"),
254 ChordHint::new("c", "call hierarchy"),
255 ChordHint::new("h", "hover (K)"),
256 ChordHint::new("a", "code actions"),
257 ChordHint::new("f", "format document"),
258 ChordHint::new("o", "outline / symbols"),
259 ChordHint::new("R", "rename"),
260 ChordHint::new("n", "next diagnostic"),
261 ChordHint::new("p", "prev diagnostic"),
262];
263
264static MAP_SPACE_W: &[ChordHint] = &[
265 ChordHint::new("v", "vertical split"),
266 ChordHint::new("s", "horizontal split"),
267 ChordHint::new("w", "other pane"),
268 ChordHint::new("q", "close split"),
269 ChordHint::new("=", "equalize"),
270 ChordHint::new("t", "terminal split"),
271];
272
273static MAP_SPACE_S: &[ChordHint] = &[
274 ChordHint::new("s", "search in buffer"),
275 ChordHint::new("S", "search backward"),
276 ChordHint::new("f", "find in files"),
277 ChordHint::new("o", "document symbols"),
278 ChordHint::new("w", "workspace symbols"),
279];
280
281static MAP_SPACE_C: &[ChordHint] = &[
282 ChordHint::new("a", "code actions"),
283 ChordHint::new("f", "format"),
284 ChordHint::new("r", "rename"),
285 ChordHint::new("d", "definition"),
286 ChordHint::new("R", "references"),
287];
288
289static MAP_SPACE_T: &[ChordHint] = &[
290 ChordHint::new("b", "blame panel"),
291 ChordHint::new("e", "explorer"),
292 ChordHint::new("t", "terminal side"),
293 ChordHint::new("T", "terminal full"),
294 ChordHint::new("i", "inlay hints"),
295 ChordHint::new("l", "code lens"),
296 ChordHint::new("r", "relative numbers"),
297 ChordHint::new("p", "pretty preview"),
298];
299
300static MAP_SPACE_H: &[ChordHint] = &[
301 ChordHint::new("h", "settings · help"),
302 ChordHint::new(",", "settings"),
303 ChordHint::new("k", "key hints on/off"),
304 ChordHint::new("s", "screensaver"),
305];
306
307pub fn map_g() -> &'static [ChordHint] {
308 MAP_G
309}
310pub fn map_z() -> &'static [ChordHint] {
311 MAP_Z
312}
313pub fn map_bracket_close() -> &'static [ChordHint] {
314 MAP_BRACKET_CLOSE
315}
316pub fn map_bracket_open() -> &'static [ChordHint] {
317 MAP_BRACKET_OPEN
318}
319pub fn map_ctrl_w() -> &'static [ChordHint] {
320 MAP_CTRL_W
321}
322pub fn map_register() -> &'static [ChordHint] {
323 MAP_REGISTER
324}
325pub fn map_mark_set() -> &'static [ChordHint] {
326 MAP_MARK_SET
327}
328pub fn map_mark_jump_line() -> &'static [ChordHint] {
329 MAP_MARK_JUMP_LINE
330}
331pub fn map_mark_jump_exact() -> &'static [ChordHint] {
332 MAP_MARK_JUMP_EXACT
333}
334pub fn map_macro_record() -> &'static [ChordHint] {
335 MAP_MACRO_RECORD
336}
337pub fn map_macro_play() -> &'static [ChordHint] {
338 MAP_MACRO_PLAY
339}
340pub fn map_operator_delete() -> &'static [ChordHint] {
341 MAP_OP_DELETE
342}
343pub fn map_operator_change() -> &'static [ChordHint] {
344 MAP_OP_CHANGE
345}
346pub fn map_operator_yank() -> &'static [ChordHint] {
347 MAP_OP_YANK
348}
349pub fn map_textobject() -> &'static [ChordHint] {
350 MAP_TEXTOBJECT
351}
352pub fn map_space_root() -> &'static [ChordHint] {
353 MAP_SPACE_ROOT
354}
355pub fn map_space_f() -> &'static [ChordHint] {
356 MAP_SPACE_F
357}
358pub fn map_space_b() -> &'static [ChordHint] {
359 MAP_SPACE_B
360}
361pub fn map_space_g() -> &'static [ChordHint] {
362 MAP_SPACE_G
363}
364pub fn map_space_l() -> &'static [ChordHint] {
365 MAP_SPACE_L
366}
367pub fn map_space_w() -> &'static [ChordHint] {
368 MAP_SPACE_W
369}
370pub fn map_space_s() -> &'static [ChordHint] {
371 MAP_SPACE_S
372}
373pub fn map_space_c() -> &'static [ChordHint] {
374 MAP_SPACE_C
375}
376pub fn map_space_d() -> &'static [ChordHint] {
377 MAP_SPACE_D
378}
379pub fn map_space_t() -> &'static [ChordHint] {
380 MAP_SPACE_T
381}
382pub fn map_space_h() -> &'static [ChordHint] {
383 MAP_SPACE_H
384}
385
386pub fn as_hints(map: &[ChordHint]) -> Vec<(&'static str, &'static str)> {
388 map.iter().map(|h| (h.key, h.desc)).collect()
389}
390
391pub fn leader_hints(path: &str) -> Vec<(&'static str, &'static str)> {
393 let map = match path {
394 "" => MAP_SPACE_ROOT,
395 "f" => MAP_SPACE_F,
396 "b" => MAP_SPACE_B,
397 "g" => MAP_SPACE_G,
398 "l" => MAP_SPACE_L,
399 "w" => MAP_SPACE_W,
400 "s" => MAP_SPACE_S,
401 "c" => MAP_SPACE_C,
402 "d" => MAP_SPACE_D,
403 "t" => MAP_SPACE_T,
404 "h" => MAP_SPACE_H,
405 _ => MAP_SPACE_ROOT,
406 };
407 as_hints(map)
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn delay_gate() {
416 let mut w = WhichKeyState::default();
417 w.begin_leader();
418 assert!(!w.ready());
419 w.force_ready();
420 assert!(w.ready());
421 }
422
423 #[test]
424 fn leader_maps_nonempty() {
425 assert!(!map_space_root().is_empty());
426 assert!(!leader_hints("f").is_empty());
427 assert!(!map_g().is_empty());
428 }
429}