1use crate::BindingMode;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct KeymapEntry {
45 pub chord: &'static str,
50 pub modes: &'static [BindingMode],
57 pub doc: &'static str,
60 pub command: Option<&'static str>,
64 pub fall_through: bool,
73 source: lattice_grammar::SourceLocation,
79}
80
81impl KeymapEntry {
82 pub fn source(&self) -> &lattice_grammar::SourceLocation {
85 &self.source
86 }
87
88 #[doc(hidden)]
99 pub fn __new(
100 chord: &'static str,
101 modes: &'static [BindingMode],
102 doc: &'static str,
103 command: Option<&'static str>,
104 fall_through: bool,
105 source: lattice_grammar::SourceLocation,
106 ) -> Self {
107 debug_assert!(
108 !modes.is_empty(),
109 "keymap_entry! requires at least one mode"
110 );
111 Self {
112 chord,
113 modes,
114 doc,
115 command,
116 fall_through,
117 source,
118 }
119 }
120
121 pub fn modes_label(&self) -> String {
124 self.modes
125 .iter()
126 .map(|m| m.label())
127 .collect::<Vec<_>>()
128 .join(", ")
129 }
130}
131
132#[doc(hidden)]
137pub fn __builtin_source(file: &'static str, line: u32) -> lattice_grammar::SourceLocation {
138 lattice_grammar::SourceLocation::builtin_file(file, line)
139}
140
141impl lattice_grammar::Introspectable for KeymapEntry {
142 fn kind_label(&self) -> &'static str {
143 "key"
144 }
145
146 fn identifier(&self) -> String {
147 format!("{} ({})", self.chord, self.modes_label())
148 }
149
150 fn doc(&self) -> &str {
151 self.doc
152 }
153
154 fn sources(&self) -> Vec<lattice_grammar::SourceEntry<'_>> {
155 vec![lattice_grammar::SourceEntry {
156 label: lattice_grammar::SourceLabel::BoundAt,
157 source: &self.source,
158 }]
159 }
160
161 fn extra_sections(&self) -> Vec<lattice_grammar::HelpSection> {
162 let mut lines = Vec::new();
163 if let Some(name) = self.command {
164 lines.push(format!("Invokes: [{name}](command:{name})"));
167 }
168 if lines.is_empty() {
169 return Vec::new();
170 }
171 vec![lattice_grammar::HelpSection {
172 heading: "Action:".to_string(),
173 lines,
174 anchor: Some("action".to_string()),
175 }]
176 }
177}
178
179#[macro_export]
224macro_rules! keymap_entry {
225 { mode: $m:ident, $($rest:tt)* } => {
235 $crate::keymap_entry!(@build (&[$crate::BindingMode::$m]) $($rest)*)
236 };
237 { mode: [ $($m:ident),+ $(,)? ], $($rest:tt)* } => {
238 $crate::keymap_entry!(@build (&[$($crate::BindingMode::$m),+]) $($rest)*)
239 };
240
241 (@build $modes:tt chord: $chord:expr, doc: $doc:expr $(,)?) => {
247 $crate::keymap_entry!(@build $modes chord: $chord, doc: $doc, cmd: None)
248 };
249 (@build $modes:tt chord: $chord:expr, doc: $doc:expr, cmd: $cmd:literal, fall_through: $ft:expr $(,)?) => {
251 $crate::keymap_entry!(@build $modes chord: $chord, doc: $doc, cmd: Some($cmd), fall_through: $ft)
252 };
253 (@build $modes:tt chord: $chord:expr, doc: $doc:expr, cmd: $cmd:literal $(,)?) => {
255 $crate::keymap_entry!(@build $modes chord: $chord, doc: $doc, cmd: Some($cmd))
256 };
257 (@build $modes:tt chord: $chord:expr, doc: $doc:expr, cmd: $cmd:expr, fall_through: $ft:expr $(,)?) => {
259 $crate::keymap_entry::KeymapEntry::__new(
260 $chord,
261 $modes,
262 $doc,
263 $cmd,
264 $ft,
265 $crate::keymap_entry::__builtin_source(file!(), line!()),
266 )
267 };
268 (@build $modes:tt chord: $chord:expr, doc: $doc:expr, cmd: $cmd:expr $(,)?) => {
270 $crate::keymap_entry::KeymapEntry::__new(
271 $chord,
272 $modes,
273 $doc,
274 $cmd,
275 false,
276 $crate::keymap_entry::__builtin_source(file!(), line!()),
277 )
278 };
279}
280
281pub fn default_keymap() -> &'static [KeymapEntry] {
290 static CACHE: std::sync::OnceLock<Vec<KeymapEntry>> = std::sync::OnceLock::new();
291 CACHE.get_or_init(build_default_keymap)
292}
293
294fn build_default_keymap() -> Vec<KeymapEntry> {
295 vec![
296 keymap_entry! { mode: Normal, chord: "h", doc: "Move cursor left", cmd: "motion:char-left" },
298 keymap_entry! { mode: Normal, chord: "<Left>", doc: "Move cursor left", cmd: "motion:char-left" },
299 keymap_entry! { mode: Normal, chord: "j", doc: "Move cursor down", cmd: "motion:line-down" },
300 keymap_entry! { mode: Normal, chord: "<Down>", doc: "Move cursor down", cmd: "motion:line-down" },
301 keymap_entry! { mode: Normal, chord: "k", doc: "Move cursor up", cmd: "motion:line-up" },
302 keymap_entry! { mode: Normal, chord: "<Up>", doc: "Move cursor up", cmd: "motion:line-up" },
303 keymap_entry! { mode: Normal, chord: "l", doc: "Move cursor right", cmd: "motion:char-right" },
304 keymap_entry! { mode: Normal, chord: "<Right>", doc: "Move cursor right", cmd: "motion:char-right" },
305 keymap_entry! { mode: Normal, chord: "0", doc: "Jump to start of line (column 0)", cmd: "motion:line-start" },
306 keymap_entry! { mode: Normal, chord: "<Home>", doc: "Jump to start of line", cmd: "motion:line-start" },
307 keymap_entry! { mode: Normal, chord: "$", doc: "Jump to end of line", cmd: "motion:line-end" },
308 keymap_entry! { mode: Normal, chord: "<End>", doc: "Jump to end of line", cmd: "motion:line-end" },
309 keymap_entry! { mode: Normal, chord: "^", doc: "Jump to first non-blank on line", cmd: "motion:first-non-blank" },
310 keymap_entry! { mode: Normal, chord: "w", doc: "Word forward (start of next word)", cmd: "motion:word-forward" },
311 keymap_entry! { mode: Normal, chord: "b", doc: "Word backward (start of previous word)", cmd: "motion:word-backward" },
312 keymap_entry! { mode: Normal, chord: "e", doc: "Word end (last byte of current/next word)", cmd: "motion:word-end" },
313 keymap_entry! { mode: Normal, chord: "W", doc: "WORD forward (whitespace-delimited)", cmd: "motion:big-word-forward" },
314 keymap_entry! { mode: Normal, chord: "B", doc: "WORD backward", cmd: "motion:big-word-backward" },
315 keymap_entry! { mode: Normal, chord: "E", doc: "WORD end", cmd: "motion:big-word-end" },
316 keymap_entry! { mode: Normal, chord: "}", doc: "Next paragraph boundary", cmd: "motion:paragraph-forward" },
317 keymap_entry! { mode: Normal, chord: "{", doc: "Previous paragraph boundary", cmd: "motion:paragraph-backward" },
318 keymap_entry! { mode: Normal, chord: ")", doc: "Next sentence", cmd: "motion:sentence-forward" },
319 keymap_entry! { mode: Normal, chord: "(", doc: "Previous sentence", cmd: "motion:sentence-backward" },
320 keymap_entry! { mode: Normal, chord: "G", doc: "Jump to last line", cmd: "motion:goto-last-line" },
321 keymap_entry! { mode: Normal, chord: "gg", doc: "Jump to first line", cmd: "motion:goto-first-line" },
322 keymap_entry! { mode: Normal, chord: "H", doc: "Cursor to top of viewport" },
324 keymap_entry! { mode: Normal, chord: "M", doc: "Cursor to middle of viewport" },
325 keymap_entry! { mode: Normal, chord: "L", doc: "Cursor to bottom of viewport" },
326 keymap_entry! { mode: Normal, chord: "<C-d>", doc: "Scroll half-page down (10 lines)", cmd: "motion:line-down" },
328 keymap_entry! { mode: Normal, chord: "<C-u>", doc: "Scroll half-page up (10 lines)", cmd: "motion:line-up" },
329 keymap_entry! { mode: Normal, chord: "<C-f>", doc: "Page down" },
330 keymap_entry! { mode: Normal, chord: "<C-b>", doc: "Page up" },
331 keymap_entry! { mode: Normal, chord: "<C-e>", doc: "Scroll viewport down one line" },
332 keymap_entry! { mode: Normal, chord: "<C-y>", doc: "Scroll viewport up one line" },
333 keymap_entry! { mode: Normal, chord: "<PageDown>", doc: "Page down (10 lines)", cmd: "motion:line-down" },
334 keymap_entry! { mode: Normal, chord: "<PageUp>", doc: "Page up (10 lines)", cmd: "motion:line-up" },
335 keymap_entry! { mode: Normal, chord: "u", doc: "Undo last change" },
337 keymap_entry! { mode: Normal, chord: "<C-r>", doc: "Redo (reverse undo)" },
338 keymap_entry! { mode: Normal, chord: ".", doc: "Repeat last change (dot-repeat)" },
339 keymap_entry! { mode: Normal, chord: "<C-o>", doc: "Jump-list back (previous AutoJump position)" },
340 keymap_entry! { mode: Normal, chord: "<C-i>", doc: "Jump-list forward" },
341 keymap_entry! { mode: Normal, chord: "<C-6>", doc: "Back through this pane's buffer history (restores the cursor you left)", cmd: "action:pane-history-back" },
342 keymap_entry! { mode: Normal, chord: "<C-7>", doc: "Forward through this pane's buffer history", cmd: "action:pane-history-forward" },
343 keymap_entry! { mode: Normal, chord: "<Tab>", doc: "Jump-list forward (terminal alias for Ctrl-I)" },
344 keymap_entry! { mode: Normal, chord: "<C-l>", doc: "Force redraw (clear terminal, reparse syntax, reset highlight cache)" },
345 keymap_entry! { mode: Normal, chord: "g", doc: "Pending: second key resolves to gg/gU/gu/g~/gv/gJ/g;/g," },
347 keymap_entry! { mode: Normal, chord: "z", doc: "Pending: scroll/fold sub-commands" },
348 keymap_entry! { mode: Normal, chord: "d", doc: "Delete operator -- use with motion/text-object; doubled (`dd`) deletes current line", cmd: "operator:delete" },
349 keymap_entry! { mode: Normal, chord: "c", doc: "Change operator -- delete then enter Insert", cmd: "operator:change" },
350 keymap_entry! { mode: Normal, chord: "y", doc: "Yank operator -- copy without modifying", cmd: "operator:yank" },
351 keymap_entry! { mode: Normal, chord: ">", doc: "Indent-right operator", cmd: "operator:indent-right" },
352 keymap_entry! { mode: Normal, chord: "<", doc: "Indent-left operator", cmd: "operator:indent-left" },
353 keymap_entry! { mode: Normal, chord: "x", doc: "Delete one char to the right", cmd: "operator:delete" },
355 keymap_entry! { mode: Normal, chord: "r", doc: "Replace [count] char(s) under the cursor with the next typed char (waits for it); stays in Normal", cmd: "operator:replace-char" },
356 keymap_entry! { mode: Normal, chord: "D", doc: "Delete to end of line (== d$)", cmd: "operator:delete" },
357 keymap_entry! { mode: Normal, chord: "C", doc: "Change to end of line (== c$)", cmd: "operator:change" },
358 keymap_entry! { mode: Normal, chord: "S", doc: "Substitute current line (== cc)", cmd: "operator:change" },
359 keymap_entry! { mode: Normal, chord: "Y", doc: "Yank current line (== yy)", cmd: "operator:yank" },
360 keymap_entry! { mode: Normal, chord: "J", doc: "Join current line with next (insert space at boundary)" },
361 keymap_entry! { mode: Normal, chord: "p", doc: "Paste after cursor / below current line" },
363 keymap_entry! { mode: Normal, chord: "P", doc: "Paste before cursor / above current line" },
364 keymap_entry! { mode: Normal, chord: "~", doc: "Toggle case at cursor and advance", cmd: "operator:toggle-case" },
366 keymap_entry! { mode: Normal, chord: "i", doc: "Enter Insert mode at cursor" },
368 keymap_entry! { mode: Normal, chord: "a", doc: "Enter Insert mode after cursor" },
369 keymap_entry! { mode: Normal, chord: "o", doc: "Open new line below + Insert" },
370 keymap_entry! { mode: Normal, chord: "O", doc: "Open new line above + Insert" },
371 keymap_entry! { mode: Normal, chord: "v", doc: "Enter Visual (charwise)" },
372 keymap_entry! { mode: Normal, chord: "V", doc: "Enter Visual (linewise)" },
373 keymap_entry! { mode: Normal, chord: "<C-v>", doc: "Enter Visual (blockwise)" },
374 keymap_entry! { mode: Normal, chord: "<C-q>", doc: "Enter Visual (blockwise) -- alternate when terminal hijacks Ctrl-V" },
375 keymap_entry! { mode: Normal, chord: "R", doc: "Enter Replace mode" },
376 keymap_entry! { mode: Normal, chord: ":", doc: "Enter command-line" },
377 keymap_entry! { mode: Normal, chord: "/", doc: "Forward search" },
379 keymap_entry! { mode: Normal, chord: "?", doc: "Backward search" },
380 keymap_entry! { mode: Normal, chord: "n", doc: "Next search match (same direction)" },
381 keymap_entry! { mode: Normal, chord: "N", doc: "Previous search match (reverse direction)" },
382 keymap_entry! { mode: Normal, chord: "*", doc: "Search word under cursor forward" },
383 keymap_entry! { mode: Normal, chord: "#", doc: "Search word under cursor backward" },
384 keymap_entry! { mode: Normal, chord: "%", doc: "Jump to matching bracket" },
385 keymap_entry! { mode: Normal, chord: "f", doc: "Find char forward (waits for target char)", cmd: "motion:find-char-forward" },
387 keymap_entry! { mode: Normal, chord: "F", doc: "Find char backward", cmd: "motion:find-char-backward" },
388 keymap_entry! { mode: Normal, chord: "t", doc: "Till char forward (one before)", cmd: "motion:till-char-forward" },
389 keymap_entry! { mode: Normal, chord: "T", doc: "Till char backward (one after)", cmd: "motion:till-char-backward" },
390 keymap_entry! { mode: Normal, chord: ";", doc: "Repeat last find/till in same direction" },
391 keymap_entry! { mode: Normal, chord: ",", doc: "Repeat last find/till in reverse direction" },
392 keymap_entry! { mode: Normal, chord: "m", doc: "Set named mark (next key is mark name)" },
394 keymap_entry! { mode: Normal, chord: "'", doc: "Jump to mark line (next key is mark name)" },
395 keymap_entry! { mode: Normal, chord: "`", doc: "Jump to mark exact position (next key is mark name)" },
396 keymap_entry! { mode: Normal, chord: "\"", doc: "Select register for next operator/paste (next key is register name)" },
398 keymap_entry! { mode: Normal, chord: "q", doc: "Start macro recording (next key is register; press q again to stop)" },
399 keymap_entry! { mode: Normal, chord: "@", doc: "Play macro from register (next key is register, or @ for last)" },
400 keymap_entry! { mode: Normal, chord: "<C-w>", doc: "Pending: window-management chord (split / close / navigate)" },
402 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>s", doc: "Split active pane horizontally (new pane below)" },
404 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>v", doc: "Split active pane vertically (new pane right)" },
405 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>c", doc: "Close active pane" },
406 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>q", doc: "Close active pane (alias of <C-w>c)" },
407 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>h", doc: "Navigate to pane on the left" },
408 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>j", doc: "Navigate to pane below" },
409 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>k", doc: "Navigate to pane above" },
410 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>l", doc: "Navigate to pane on the right" },
411 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>w", doc: "Cycle to next pane" },
412 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>W", doc: "Cycle to previous pane" },
413 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>z", doc: "Toggle zoom on the active pane (layout preserved; tmux `prefix z`)" },
416 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>=", doc: "Equalize all split ratios (reset to 50/50)" },
418 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>+", doc: "Grow active pane vertically" },
419 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>-", doc: "Shrink active pane vertically" },
420 keymap_entry! { mode: AfterCtrlW, chord: "<C-w>>", doc: "Grow active pane horizontally" },
421 keymap_entry! { mode: AfterCtrlW, chord: "<C-w><", doc: "Shrink active pane horizontally" },
422 keymap_entry! { mode: AfterG, chord: "gU", doc: "Uppercase operator -- prefix to motion/text-object; doubled = current line", cmd: "operator:upper" },
424 keymap_entry! { mode: AfterG, chord: "gu", doc: "Lowercase operator", cmd: "operator:lower" },
425 keymap_entry! { mode: AfterG, chord: "g~", doc: "Toggle-case operator", cmd: "operator:toggle-case" },
426 keymap_entry! { mode: AfterG, chord: "gv", doc: "Re-enter Visual with last selection" },
427 keymap_entry! { mode: AfterG, chord: "gt", doc: "Switch to the next tab (or {N}gt for absolute tab N)" },
429 keymap_entry! { mode: AfterG, chord: "gT", doc: "Switch to the previous tab" },
430 keymap_entry! { mode: Normal, chord: "<C-PageDown>", doc: "Switch to the next tab (alias of gt)" },
432 keymap_entry! { mode: Normal, chord: "<C-PageUp>", doc: "Switch to the previous tab (alias of gT)" },
433 keymap_entry! { mode: AfterG, chord: "gJ", doc: "Join lines without inserting a space" },
434 keymap_entry! { mode: AfterG, chord: "g;", doc: "Walk named-mark history backward" },
435 keymap_entry! { mode: AfterG, chord: "g,", doc: "Walk named-mark history forward" },
436 keymap_entry! { mode: Normal, chord: "<C-t>", doc: "Tag stack: pop -- walk back through the LIFO chain of LSP go-to-definition drill-downs (independent of <C-o> jump list)" },
438 keymap_entry! { mode: AfterZ, chord: "zz", doc: "Center cursor in viewport" },
440 keymap_entry! { mode: AfterZ, chord: "z.", doc: "Center cursor in viewport (alias of zz)" },
441 keymap_entry! { mode: AfterZ, chord: "zt", doc: "Cursor to top of viewport" },
442 keymap_entry! { mode: AfterZ, chord: "z<CR>", doc: "Cursor to top of viewport (alias of zt)" },
443 keymap_entry! { mode: AfterZ, chord: "zb", doc: "Cursor to bottom of viewport" },
444 keymap_entry! { mode: AfterZ, chord: "z-", doc: "Cursor to bottom of viewport (alias of zb)" },
445 keymap_entry! { mode: AfterZ, chord: "zf", doc: "Create fold from current Visual selection" },
446 keymap_entry! { mode: AfterZ, chord: "zo", doc: "Open fold at cursor" },
447 keymap_entry! { mode: AfterZ, chord: "zc", doc: "Close fold at cursor" },
448 keymap_entry! { mode: AfterZ, chord: "za", doc: "Toggle fold at cursor" },
449 keymap_entry! { mode: AfterZ, chord: "zR", doc: "Open all folds" },
450 keymap_entry! { mode: AfterZ, chord: "zM", doc: "Close all folds" },
451 keymap_entry! { mode: AfterZ, chord: "zd", doc: "Delete fold at cursor" },
452 keymap_entry! { mode: AfterZ, chord: "zj", doc: "Jump to next fold" },
453 keymap_entry! { mode: AfterZ, chord: "zk", doc: "Jump to previous fold" },
454 keymap_entry! { mode: AfterZ, chord: "zi", doc: "Toggle foldenable" },
455 keymap_entry! { mode: AfterZ, chord: "zl", doc: "Scroll view right [count] columns" },
456 keymap_entry! { mode: AfterZ, chord: "zh", doc: "Scroll view left [count] columns" },
457 keymap_entry! { mode: AfterZ, chord: "zL", doc: "Scroll view right half a screen" },
458 keymap_entry! { mode: AfterZ, chord: "zH", doc: "Scroll view left half a screen" },
459 keymap_entry! { mode: AfterZ, chord: "zs", doc: "Scroll cursor column to left edge" },
460 keymap_entry! { mode: AfterZ, chord: "ze", doc: "Scroll cursor column to right edge" },
461 keymap_entry! { mode: Visual, chord: "<Esc>", doc: "Exit to Normal" },
463 keymap_entry! { mode: Visual, chord: "v", doc: "Toggle: exit Visual" },
464 keymap_entry! { mode: Visual, chord: "V", doc: "Toggle: exit Visual" },
465 keymap_entry! { mode: Visual, chord: "h", doc: "Extend selection left", cmd: "motion:char-left" },
466 keymap_entry! { mode: Visual, chord: "j", doc: "Extend selection down", cmd: "motion:line-down" },
467 keymap_entry! { mode: Visual, chord: "k", doc: "Extend selection up", cmd: "motion:line-up" },
468 keymap_entry! { mode: Visual, chord: "l", doc: "Extend selection right", cmd: "motion:char-right" },
469 keymap_entry! { mode: Visual, chord: "0", doc: "Extend to start of line", cmd: "motion:line-start" },
470 keymap_entry! { mode: Visual, chord: "$", doc: "Extend to end of line", cmd: "motion:line-end" },
471 keymap_entry! { mode: Visual, chord: "^", doc: "Extend to first non-blank", cmd: "motion:first-non-blank" },
472 keymap_entry! { mode: Visual, chord: "w", doc: "Extend by word forward", cmd: "motion:word-forward" },
473 keymap_entry! { mode: Visual, chord: "b", doc: "Extend by word backward", cmd: "motion:word-backward" },
474 keymap_entry! { mode: Visual, chord: "e", doc: "Extend to word end", cmd: "motion:word-end" },
475 keymap_entry! { mode: Visual, chord: "G", doc: "Extend to last line", cmd: "motion:goto-last-line" },
476 keymap_entry! { mode: Visual, chord: "d", doc: "Delete selection", cmd: "operator:delete" },
477 keymap_entry! { mode: Visual, chord: "x", doc: "Delete selection (alias of d)", cmd: "operator:delete" },
478 keymap_entry! { mode: Visual, chord: "c", doc: "Change selection (delete + Insert)", cmd: "operator:change" },
479 keymap_entry! { mode: Visual, chord: "s", doc: "Change selection (alias of c)", cmd: "operator:change" },
480 keymap_entry! { mode: Visual, chord: "y", doc: "Yank selection", cmd: "operator:yank" },
481 keymap_entry! { mode: Visual, chord: "r", doc: "Replace every selected char with the next typed char (waits for it); returns to Normal", cmd: "operator:replace-char" },
482 keymap_entry! { mode: Insert, chord: "<Esc>", doc: "Exit to Normal" },
484 keymap_entry! { mode: Insert, chord: "<BS>", doc: "Delete char to the left" },
485 keymap_entry! { mode: Insert, chord: "<CR>", doc: "Insert newline" },
486 keymap_entry! { mode: Insert, chord: "<Tab>", doc: "Insert tab character" },
487 keymap_entry! { mode: Insert, chord: "<C-Space>", doc: "Manual completion trigger -- opens the popup with sources matching the prefix at the cursor" },
493 keymap_entry! { mode: Insert, chord: "<C-a>", doc: "Move the caret to the start of the line (readline)" },
495 keymap_entry! { mode: Insert, chord: "<C-e>", doc: "Move the caret to the end of the line (readline)" },
496 keymap_entry! { mode: Insert, chord: "<C-b>", doc: "Move the caret one character left (readline)" },
497 keymap_entry! { mode: Insert, chord: "<C-f>", doc: "Move the caret one character right (readline)" },
498 keymap_entry! { mode: Insert, chord: "<C-w>", doc: "Delete the word before the caret (readline/vim)" },
499 keymap_entry! { mode: Insert, chord: "<C-u>", doc: "Delete from the line start to the caret (readline/vim)" },
500 keymap_entry! { mode: Insert, chord: "<C-k>", doc: "Delete from the caret to the line end (readline)" },
501 keymap_entry! { mode: Insert, chord: "<C-t>", doc: "Indent the current line by one shiftwidth (vim)" },
502 keymap_entry! { mode: Insert, chord: "<C-d>", doc: "Dedent the current line by one shiftwidth (vim)" },
503 keymap_entry! { mode: Insert, chord: "<C-x>", doc: "Pending: vim's expansion-prefix family. `<C-x><C-s>` (snippet-expand-at-cursor) is the only live chord today, contributed by `snippet-mode` (SN.3c.1), not Builtin." },
504 keymap_entry! { mode: CompletionPopup, chord: "<C-n>", doc: "Completion popup: select next candidate" },
510 keymap_entry! { mode: CompletionPopup, chord: "<C-p>", doc: "Completion popup: select previous candidate" },
511 keymap_entry! { mode: CompletionPopup, chord: "<C-y>", doc: "Completion popup: accept selected candidate (vim convention)" },
512 keymap_entry! { mode: CompletionPopup, chord: "<Tab>", doc: "Completion popup: accept selected candidate" },
513 keymap_entry! { mode: CompletionPopup, chord: "<CR>", doc: "Completion popup: accept selected candidate" },
514 keymap_entry! { mode: CompletionPopup, chord: "<C-e>", doc: "Completion popup: cancel popup, stay in Insert (vim convention)" },
515 keymap_entry! { mode: CompletionPopup, chord: "<Esc>", doc: "Completion popup: cancel popup AND exit Insert (vim convention)" },
516 keymap_entry! { mode: CompletionPopup, chord: "<C-Space>", doc: "Completion popup: re-trigger / refresh (LSP isIncomplete path)" },
517 keymap_entry! { mode: CompletionPopup, chord: "<C-d>", doc: "Completion popup: toggle side documentation popup for the focused candidate" },
518 keymap_entry! { mode: CompletionPopup, chord: "<C-f>", doc: "Completion popup: page docs popup forward" },
519 keymap_entry! { mode: CompletionPopup, chord: "<C-b>", doc: "Completion popup: page docs popup backward" },
520 keymap_entry! { mode: AfterCtrlX, chord: "<C-x><C-s>", doc: "Direct snippet expansion: look up the word at the cursor in the snippet registry; expand without surfacing the popup" },
522 keymap_entry! { mode: Snippet, chord: "<Tab>", doc: "Snippet: jump to next placeholder (or exit on $0)" },
527 keymap_entry! { mode: Snippet, chord: "<S-Tab>", doc: "Snippet: jump to previous placeholder" },
528 keymap_entry! { mode: Snippet, chord: "<Esc>", doc: "Snippet: exit the snippet (placeholders become plain text) and return to Normal" },
529 keymap_entry! { mode: Replace, chord: "<Esc>", doc: "Exit to Normal" },
531 keymap_entry! { mode: Replace, chord: "<BS>", doc: "Restore last overwritten byte" },
532 keymap_entry! { mode: Replace, chord: "<CR>", doc: "Insert newline" },
533 keymap_entry! { mode: Command, chord: "<Esc>", doc: "Cancel command line (or dismiss completion popup if open)" },
535 keymap_entry! { mode: Command, chord: "<CR>", doc: "Submit command line (or accept completion if popup is open)" },
536 keymap_entry! { mode: Command, chord: "<BS>", doc: "Delete previous char" },
537 keymap_entry! { mode: Command, chord: "<Up>", doc: "Walk command-history backward" },
538 keymap_entry! { mode: Command, chord: "<Down>", doc: "Walk command-history forward" },
539 keymap_entry! { mode: Command, chord: "<Tab>", doc: "Trigger completion / advance to next candidate" },
540 keymap_entry! { mode: Command, chord: "<C-h>", doc: "Describe command word / arg under cursor", cmd: "ex:describe-command" },
541 keymap_entry! { mode: Command, chord: "<C-u>", doc: "Clear the command line" },
542 keymap_entry! { mode: Command, chord: "<C-w>", doc: "Delete the trailing word" },
543 keymap_entry! { mode: Search, chord: "<Esc>", doc: "Cancel search" },
545 keymap_entry! { mode: Search, chord: "<CR>", doc: "Submit search" },
546 keymap_entry! { mode: Search, chord: "<BS>", doc: "Delete previous char" },
547 keymap_entry! { mode: Help, chord: "q", doc: "Dismiss help" },
562 keymap_entry! { mode: Help, chord: "<CR>", doc: "Follow link under cursor" },
563 ]
564}
565
566pub fn lookup(chord: &str) -> Vec<&'static KeymapEntry> {
587 default_keymap()
588 .iter()
589 .filter(|e| e.chord == chord)
590 .collect()
591}
592
593pub fn entries() -> &'static [KeymapEntry] {
596 default_keymap()
597}
598
599#[cfg(test)]
600mod tests {
601 #![allow(clippy::unwrap_used, clippy::panic)]
602 use super::*;
603 use std::collections::HashSet;
604
605 #[test]
606 fn lookup_returns_every_mode_a_chord_appears_in() {
607 let hits = lookup("j");
611 assert_eq!(hits.len(), 2);
612 let modes: HashSet<_> = hits.iter().flat_map(|e| e.modes.iter().copied()).collect();
613 assert!(modes.contains(&BindingMode::Normal));
614 assert!(modes.contains(&BindingMode::Visual));
615 }
616
617 #[test]
618 fn lookup_unknown_chord_is_empty() {
619 assert!(lookup("nope-not-a-chord").is_empty());
620 }
621
622 #[test]
623 fn macro_single_mode_sugars_to_one_element_slice() {
624 let e = keymap_entry! { mode: Normal, chord: "zz", doc: "center", cmd: None };
627 assert_eq!(e.modes, [BindingMode::Normal].as_slice());
628 assert_eq!(e.modes_label(), "Normal");
629 }
630
631 #[test]
632 fn macro_multi_mode_builds_a_slice_in_order() {
633 let e = keymap_entry! {
636 mode: [Normal, Visual],
637 chord: "zn",
638 doc: "narrow",
639 cmd: "operator:narrow"
640 };
641 assert_eq!(
642 e.modes,
643 [BindingMode::Normal, BindingMode::Visual].as_slice()
644 );
645 assert_eq!(e.command, Some("operator:narrow"));
646 assert_eq!(e.modes_label(), "Normal, Visual");
647 }
648
649 #[test]
650 fn macro_multi_mode_carries_fall_through() {
651 let e = keymap_entry! {
654 mode: [Insert, Select],
655 chord: "<Esc>",
656 doc: "leave",
657 cmd: "action:snippet-leave",
658 fall_through: true
659 };
660 assert_eq!(
661 e.modes,
662 [BindingMode::Insert, BindingMode::Select].as_slice()
663 );
664 assert!(e.fall_through);
665 }
666
667 #[test]
668 fn motion_chords_link_to_registered_command_names() {
669 let mut registry = lattice_grammar::CommandRegistry::new();
687 let _ = lattice_grammar::builtins::populate(&mut registry);
688 let _ = lattice_grammar::ex_commands::populate(&mut registry);
689 for entry in default_keymap() {
690 let Some(name) = entry.command else { continue };
691 if name.starts_with("action:") {
692 continue;
693 }
694 assert!(
695 registry.id_by_name(name).is_some(),
696 "binding `{}` ({}) claims `{}` but registry has no such command",
697 entry.chord,
698 entry.modes_label(),
699 name
700 );
701 }
702 }
703
704 #[test]
705 fn no_duplicate_chord_mode_pairs() {
706 let mut seen: HashSet<(&str, BindingMode)> = HashSet::new();
709 for entry in default_keymap() {
710 for mode in entry.modes {
711 assert!(
712 seen.insert((entry.chord, *mode)),
713 "duplicate keymap entry: chord={} mode={:?}",
714 entry.chord,
715 mode
716 );
717 }
718 }
719 }
720
721 #[test]
722 fn every_entry_has_a_builtin_source_with_a_line() {
723 for entry in default_keymap() {
728 assert_eq!(
729 entry.source().layer,
730 lattice_grammar::SourceLayer::Builtin,
731 "entry `{}` ({}) source layer should be Builtin",
732 entry.chord,
733 entry.modes_label()
734 );
735 match &entry.source().kind {
736 lattice_grammar::SourceKind::File { path, line } => {
737 assert!(
738 path.to_string_lossy().contains("keymap_entry.rs"),
739 "expected source path to contain `keymap_entry.rs`, got `{}` (entry `{}` {})",
740 path.display(),
741 entry.chord,
742 entry.modes_label(),
743 );
744 assert!(
745 line.is_some(),
746 "entry `{}` ({}) has no captured line",
747 entry.chord,
748 entry.modes_label()
749 );
750 }
751 other => panic!("expected File source kind, got {other:?}"),
752 }
753 }
754 }
755
756 #[test]
757 fn adjacent_entries_capture_distinct_lines() {
758 let entries = default_keymap();
762 for window in entries.windows(2) {
763 let (a, b) = (&window[0], &window[1]);
764 let line_a = match &a.source.kind {
765 lattice_grammar::SourceKind::File { line, .. } => *line,
766 _ => None,
767 };
768 let line_b = match &b.source.kind {
769 lattice_grammar::SourceKind::File { line, .. } => *line,
770 _ => None,
771 };
772 if let (Some(a_line), Some(b_line)) = (line_a, line_b) {
776 assert!(
777 a_line != b_line,
778 "adjacent entries `{}` ({}) and `{}` ({}) both captured line {a_line}",
779 a.chord,
780 a.modes_label(),
781 b.chord,
782 b.modes_label(),
783 );
784 }
785 }
786 }
787
788 #[test]
789 fn introspectable_impl_emits_bound_at_source() {
790 use lattice_grammar::{Introspectable, SourceLabel};
791 let entries = default_keymap();
792 let entry = entries.first().expect("at least one entry");
793 let sources = entry.sources();
794 assert_eq!(sources.len(), 1);
795 assert_eq!(sources[0].label, SourceLabel::BoundAt);
796 }
797
798 #[test]
799 fn introspectable_render_includes_source_link_and_invokes_section() {
800 let entries = default_keymap();
805 let entry = entries
806 .iter()
807 .find(|e| e.command.is_some())
808 .expect("at least one entry has a command");
809 let lines = lattice_grammar::render_introspection_lines(entry);
810 let body = lines.join("\n");
811 assert!(
812 body.contains("Bound at:"),
813 "body missing source label: {body}"
814 );
815 assert!(
816 body.contains("(file:") && body.contains("keymap_entry.rs"),
817 "body missing source link: {body}"
818 );
819 assert!(
820 body.contains("(command:"),
821 "body missing command cross-reference: {body}"
822 );
823 assert!(
824 body.contains("(built-in)"),
825 "body missing source layer label: {body}"
826 );
827 }
828
829 #[test]
830 fn binding_mode_label_is_non_empty() {
831 for mode in [
832 BindingMode::Normal,
833 BindingMode::Insert,
834 BindingMode::Visual,
835 BindingMode::Replace,
836 BindingMode::Command,
837 BindingMode::Search,
838 BindingMode::OperatorPending,
839 BindingMode::AfterG,
840 BindingMode::AfterZ,
841 BindingMode::AfterMark,
842 BindingMode::AfterJumpMarkLine,
843 BindingMode::AfterJumpMarkExact,
844 BindingMode::AfterRegister,
845 BindingMode::AfterMacroStart,
846 BindingMode::AfterMacroPlay,
847 BindingMode::AfterFindChar,
848 BindingMode::AfterTextObject,
849 BindingMode::Help,
850 ] {
851 assert!(!mode.label().is_empty());
852 }
853 }
854}