gpui_kit_grid/actions.rs
1//! Commands of the grid, as GPUI Actions in the `live_grid` namespace, and
2//! its default key bindings.
3//!
4//! The grid handles them under the key context [`KEY_CONTEXT`], on the
5//! focused grid, together with the shared ones of `gpui_kit::base::actions`
6//! (`SelectUp`, `SelectPageDown`, `Confirm`, `Cancel` and the rest).
7//! [`init`] binds the default keymap of [`key_bindings`]: Excel's, with Cmd
8//! on macOS. An application rebinds a key in the same context after `init`
9//! (the later binding wins), unbinds one with `NoAction`, or binds a
10//! filtered [`key_bindings`] itself instead of calling `init`.
11//!
12//! Column commands act on [`LiveGridState::current_column`]: the header
13//! cell or band pressed last, or the focused cell's column. With no current
14//! column they do nothing. They have no default binding: Excel and Windows
15//! ListView bind none of them, and Ctrl+Plus is the application's zoom.
16//! Nor have the sort commands, `ClearAllSorting` included: the
17//! application binds them.
18//!
19//! Tab is not bound: the grid is one Tab stop and Tab leaves it, as in the
20//! WAI-ARIA grid pattern, so the grid never traps the keyboard focus of the
21//! window around it. In an open cell editor Tab commits and moves right,
22//! Shift+Tab left.
23//!
24//! F2 opens the focused cell's editor, Backspace and Delete open it with an
25//! empty draft; in the editor ([`EDITOR_KEY_CONTEXT`], inside
26//! [`KEY_CONTEXT`]) Enter commits and moves down, Shift+Enter and ↑ up, ↓
27//! down, Escape cancels, and every other key of the grid is `NoAction`, so
28//! the grid's navigation and commands stay out of the way while the field
29//! has the focus. Space on a focused cell of a `Control` column
30//! ([`CELL_CONTENT_KEY`] `== control`) presses its element; unhandled, it
31//! goes on to `ToggleSelection`.
32//!
33//! Group captions and tree rows expand with `+` (Shift+= on a US layout)
34//! and collapse with `-`; ← and → on a caption collapse, expand and move
35//! along the groups as in the WAI-ARIA treegrid pattern (they stay
36//! `SelectPrevColumn` and `SelectNextColumn`). Expand all, collapse all and
37//! captions only have no default binding.
38//!
39//! [`LiveGridState::current_column`]: crate::LiveGridState::current_column
40
41use gpui_kit::base::actions::{
42 Cancel, Confirm, SelectDown, SelectFirst, SelectLast, SelectNextColumn, SelectPageDown,
43 SelectPageUp, SelectPrevColumn, SelectUp,
44};
45use gpui_kit::{App, KeyBinding, NoAction};
46
47/// The key context of the grid root.
48pub const KEY_CONTEXT: &str = "LiveGrid";
49
50/// The key context of the group panel, inside [`KEY_CONTEXT`]: its keys act
51/// on the focused chip.
52pub const GROUP_PANEL_KEY_CONTEXT: &str = "LiveGridGroupPanel";
53
54/// The key context of an open cell editor's host, inside [`KEY_CONTEXT`]:
55/// its bindings are deeper than the grid's, and every key of the grid
56/// without one of its own is `NoAction` there.
57pub const EDITOR_KEY_CONTEXT: &str = "LiveGridEditor";
58
59/// The attribute of [`KEY_CONTEXT`] that names what the focused data cell's
60/// column paints: `text`, `element` or `control` (`CellContent`), or `link`
61/// for a cell whose value is a link; absent without a focused data cell.
62/// Space presses a control: `LiveGrid && cell == control`; Enter opens a
63/// link: `LiveGrid && cell == link`.
64pub const CELL_CONTENT_KEY: &str = "cell";
65
66/// The key context of the column chooser: a layer next to the grid root,
67/// outside [`KEY_CONTEXT`].
68pub const COLUMN_CHOOSER_KEY_CONTEXT: &str = "LiveGridColumnChooser";
69
70/// The predicate of a binding on a focused `Control` cell.
71const CONTROL_CELL_CONTEXT: &str = "LiveGrid && cell == control";
72
73/// The predicate of a binding on a focused link.
74const LINK_CELL_CONTEXT: &str = "LiveGrid && cell == link";
75
76gpui_kit::actions!(
77 live_grid,
78 [
79 // Column commands.
80 /// Fits every visible column to its header and painted rows.
81 AutofitColumns,
82 /// Fits the current column to its header and painted rows.
83 AutofitColumn,
84 /// Hides the current column (never the last visible one).
85 HideColumn,
86 /// Shows every hidden column at its place.
87 ShowAllColumns,
88 /// Freezes the current column at the left edge.
89 FreezeColumnLeft,
90 /// Freezes the current column at the right edge.
91 FreezeColumnRight,
92 /// Moves the current column back to the scrolling columns.
93 UnfreezeColumn,
94 /// Moves the current column one visible place left within its zone,
95 /// if the column move policy allows it.
96 MoveColumnLeft,
97 /// Moves the current column one visible place right within its
98 /// zone, if the column move policy allows it.
99 MoveColumnRight,
100 /// Restores the default layout of the schema
101 /// (`LiveGridState::reset_layout`, with the `User` origin).
102 ResetLayout,
103 // Selection and focus.
104 /// Focuses the first row, in the same column.
105 SelectFirstRow,
106 /// Focuses the last row, in the same column.
107 SelectLastRow,
108 /// Focuses the first row and the first visible column.
109 SelectFirstCell,
110 /// Focuses the last row and the last visible column.
111 SelectLastCell,
112 /// Extends the selection to the row above.
113 ExtendSelectionUp,
114 /// Extends the selection to the row below.
115 ExtendSelectionDown,
116 /// Extends the selection to the visible column on the left.
117 ExtendSelectionPrevColumn,
118 /// Extends the selection to the visible column on the right.
119 ExtendSelectionNextColumn,
120 /// Extends the selection a page up.
121 ExtendSelectionPageUp,
122 /// Extends the selection a page down.
123 ExtendSelectionPageDown,
124 /// Extends the selection to the first visible column.
125 ExtendSelectionToFirst,
126 /// Extends the selection to the last visible column.
127 ExtendSelectionToLast,
128 /// Extends the selection to the first row.
129 ExtendSelectionToFirstRow,
130 /// Extends the selection to the last row.
131 ExtendSelectionToLastRow,
132 /// Extends the selection to the first cell.
133 ExtendSelectionToFirstCell,
134 /// Extends the selection to the last cell.
135 ExtendSelectionToLastCell,
136 /// Selects every row on screen (in the cell modes, every visible
137 /// cell); nothing in the single modes.
138 SelectAll,
139 /// Toggles the focused row or cell; selects it in the single modes.
140 ToggleSelection,
141 // Copying, the context menu and sorting.
142 /// Copies the selected rows or cells as tab-separated text.
143 Copy,
144 /// Copies every shown row, with column headers, as tab-separated text.
145 CopyAll,
146 /// Opens the context menu at the focused cell
147 /// (`LiveGridState::show_context_menu`).
148 ShowContextMenu,
149 /// Sorts by the current column alone, ascending (`SortOrigin::User`),
150 /// if the column is sortable.
151 SortAscending,
152 /// Sorts by the current column alone, descending (`SortOrigin::User`),
153 /// if the column is sortable.
154 SortDescending,
155 /// Takes the current column out of the sort; the other sorted
156 /// columns stay.
157 ClearSort,
158 // The whole sort.
159 /// Clears the whole sort, every column (`SortOrigin::User`); does
160 /// nothing when nothing is sorted.
161 ClearAllSorting,
162 // Grouping, `GroupOrigin::User`.
163 /// Groups by the current column: a new last level, ascending. Does
164 /// nothing for a column already grouped by, one that is not
165 /// groupable, or in a tree view.
166 GroupByColumn,
167 /// Takes the current column's level out of the grouping; on a chip
168 /// of the group panel, the chip's level.
169 UngroupColumn,
170 /// Takes every level out of the grouping; does nothing when not
171 /// grouped.
172 ClearGrouping,
173 // Expanding and collapsing.
174 /// Expands the focused group caption, or the focused tree row with
175 /// children (`GroupOrigin::User`).
176 ExpandRow,
177 /// Collapses the focused group caption or tree row with children; on
178 /// a data row of a grouped view, its group, and the focus goes to
179 /// the group's caption.
180 CollapseRow,
181 /// Expands every group and tree row: the default depth `All`, every
182 /// toggle cleared.
183 ExpandAll,
184 /// Collapses every group and tree row: the default depth
185 /// `Levels(0)`, every toggle cleared.
186 CollapseAll,
187 /// Shows the captions of every group level but no data rows: the
188 /// default depth `Captions`, every toggle cleared.
189 ShowCaptionsOnly,
190 // Editing, and pressing a cell's control.
191 /// Opens the editor of the focused cell, starting from its value.
192 StartEdit,
193 /// Opens the editor of the focused cell with an empty draft.
194 StartEditEmpty,
195 /// Commits the open editor; the focus moves as the edit gesture says
196 /// for Enter (down by default).
197 CommitEdit,
198 /// Commits the open editor and moves up.
199 CommitEditUp,
200 /// Commits the open editor and moves down.
201 CommitEditDown,
202 /// Commits the open editor and moves right.
203 CommitEditNext,
204 /// Commits the open editor and moves left.
205 CommitEditPrev,
206 /// Closes the open editor without a commit.
207 CancelEdit,
208 /// Presses the element of the focused cell of a `Control` column,
209 /// through the handler it registered; without one, the key goes on.
210 ActivateCell,
211 /// Opens the link of the focused cell (`CellValue::Link`); on any
212 /// other cell, the key goes on.
213 OpenLink,
214 // Columns.
215 /// Opens the column chooser.
216 ShowColumnChooser,
217 ]
218);
219
220/// The default keymap in [`KEY_CONTEXT`]: Excel's, with Cmd on macOS.
221/// `secondary-` is Cmd on macOS and Ctrl elsewhere, so one entry serves
222/// every platform.
223///
224/// The context menu opens with Shift+F10 and the Menu key (`menu`; Mac
225/// keyboards have none). Not bound: `tab` (leaves the grid).
226///
227/// In [`GROUP_PANEL_KEY_CONTEXT`], on a chip, Delete and Backspace remove
228/// its level, and Enter and Space press the focused chip or button as in
229/// any toolbar: the grid's own Enter and Space are unbound there.
230///
231/// In [`EDITOR_KEY_CONTEXT`] the editor's keys commit and cancel; every
232/// other key of [`KEY_CONTEXT`] is `NoAction` there, built from the list
233/// itself, so a new key of the grid is silenced in the editor too. An
234/// application unsilences one with its own binding in that context after
235/// `init`.
236///
237/// In [`COLUMN_CHOOSER_KEY_CONTEXT`], Alt+↑ and Alt+↓ move the highlighted
238/// column up and down its zone, and Space shows or hides it.
239pub fn key_bindings() -> Vec<KeyBinding> {
240 let mut bindings = grid_key_bindings();
241 let editor = editor_key_bindings();
242 // Every key of the grid without a binding of its own in the editor.
243 let own: Vec<String> = editor.iter().filter_map(binding_key).collect();
244 let mut silenced: Vec<String> = Vec::new();
245 for key in bindings.iter().filter_map(binding_key) {
246 if !own.contains(&key) && !silenced.contains(&key) {
247 silenced.push(key);
248 }
249 }
250 let silenced = silenced
251 .iter()
252 .map(|key| KeyBinding::new(key, NoAction, Some(EDITOR_KEY_CONTEXT)));
253 bindings.extend(group_panel_key_bindings());
254 bindings.extend(editor);
255 bindings.extend(silenced);
256 bindings.extend(column_chooser_key_bindings());
257 bindings
258}
259
260/// The keystroke of a binding, as text that parses back to it.
261fn binding_key(binding: &KeyBinding) -> Option<String> {
262 match binding.keystrokes() {
263 [keystroke] => Some(keystroke.inner().unparse()),
264 _ => None,
265 }
266}
267
268/// The bindings of [`KEY_CONTEXT`], `ActivateCell` on a control included.
269fn grid_key_bindings() -> Vec<KeyBinding> {
270 let context = Some(KEY_CONTEXT);
271 vec![
272 KeyBinding::new("up", SelectUp, context),
273 KeyBinding::new("down", SelectDown, context),
274 KeyBinding::new("left", SelectPrevColumn, context),
275 KeyBinding::new("right", SelectNextColumn, context),
276 KeyBinding::new("pageup", SelectPageUp, context),
277 KeyBinding::new("pagedown", SelectPageDown, context),
278 KeyBinding::new("home", SelectFirst, context),
279 KeyBinding::new("end", SelectLast, context),
280 KeyBinding::new("secondary-left", SelectFirst, context),
281 KeyBinding::new("secondary-right", SelectLast, context),
282 KeyBinding::new("secondary-up", SelectFirstRow, context),
283 KeyBinding::new("secondary-down", SelectLastRow, context),
284 KeyBinding::new("secondary-home", SelectFirstCell, context),
285 KeyBinding::new("secondary-end", SelectLastCell, context),
286 KeyBinding::new("shift-up", ExtendSelectionUp, context),
287 KeyBinding::new("shift-down", ExtendSelectionDown, context),
288 KeyBinding::new("shift-left", ExtendSelectionPrevColumn, context),
289 KeyBinding::new("shift-right", ExtendSelectionNextColumn, context),
290 KeyBinding::new("shift-pageup", ExtendSelectionPageUp, context),
291 KeyBinding::new("shift-pagedown", ExtendSelectionPageDown, context),
292 KeyBinding::new("shift-home", ExtendSelectionToFirst, context),
293 KeyBinding::new("shift-end", ExtendSelectionToLast, context),
294 KeyBinding::new("shift-secondary-left", ExtendSelectionToFirst, context),
295 KeyBinding::new("shift-secondary-right", ExtendSelectionToLast, context),
296 KeyBinding::new("shift-secondary-up", ExtendSelectionToFirstRow, context),
297 KeyBinding::new("shift-secondary-down", ExtendSelectionToLastRow, context),
298 KeyBinding::new("shift-secondary-home", ExtendSelectionToFirstCell, context),
299 KeyBinding::new("shift-secondary-end", ExtendSelectionToLastCell, context),
300 KeyBinding::new("secondary-a", SelectAll, context),
301 KeyBinding::new("space", ToggleSelection, context),
302 KeyBinding::new("enter", Confirm { secondary: false }, context),
303 KeyBinding::new("secondary-enter", Confirm { secondary: true }, context),
304 KeyBinding::new("escape", Cancel, context),
305 // Copy and the context menu.
306 KeyBinding::new("secondary-c", Copy, context),
307 KeyBinding::new("shift-f10", ShowContextMenu, context),
308 KeyBinding::new("menu", ShowContextMenu, context),
309 // Expanding and collapsing. `+` is typed as Shift+= on a US layout,
310 // which matches both bindings.
311 KeyBinding::new("+", ExpandRow, context),
312 KeyBinding::new("shift-=", ExpandRow, context),
313 KeyBinding::new("-", CollapseRow, context),
314 // Editing.
315 KeyBinding::new("f2", StartEdit, context),
316 KeyBinding::new("backspace", StartEditEmpty, context),
317 KeyBinding::new("delete", StartEditEmpty, context),
318 // After `ToggleSelection`, so it comes first on a control (same depth,
319 // later binding); unhandled, Space goes on to `ToggleSelection`.
320 KeyBinding::new("space", ActivateCell, Some(CONTROL_CELL_CONTEXT)),
321 // Enter opens a focused link; unhandled, it goes on to `Confirm`.
322 KeyBinding::new("enter", OpenLink, Some(LINK_CELL_CONTEXT)),
323 ]
324}
325
326/// The bindings of [`GROUP_PANEL_KEY_CONTEXT`].
327fn group_panel_key_bindings() -> [KeyBinding; 4] {
328 let context = Some(GROUP_PANEL_KEY_CONTEXT);
329 [
330 KeyBinding::new("delete", UngroupColumn, context),
331 KeyBinding::new("backspace", UngroupColumn, context),
332 KeyBinding::new("enter", NoAction, context),
333 KeyBinding::new("space", NoAction, context),
334 ]
335}
336
337/// The editor's own bindings in [`EDITOR_KEY_CONTEXT`].
338fn editor_key_bindings() -> Vec<KeyBinding> {
339 let context = Some(EDITOR_KEY_CONTEXT);
340 vec![
341 KeyBinding::new("enter", CommitEdit, context),
342 KeyBinding::new("secondary-enter", CommitEdit, context),
343 KeyBinding::new("shift-enter", CommitEditUp, context),
344 KeyBinding::new("up", CommitEditUp, context),
345 KeyBinding::new("down", CommitEditDown, context),
346 KeyBinding::new("tab", CommitEditNext, context),
347 KeyBinding::new("shift-tab", CommitEditPrev, context),
348 KeyBinding::new("escape", CancelEdit, context),
349 ]
350}
351
352/// The bindings of [`COLUMN_CHOOSER_KEY_CONTEXT`]: move the highlighted
353/// column, show or hide it.
354fn column_chooser_key_bindings() -> [KeyBinding; 3] {
355 let context = Some(COLUMN_CHOOSER_KEY_CONTEXT);
356 [
357 KeyBinding::new("alt-up", MoveColumnLeft, context),
358 KeyBinding::new("alt-down", MoveColumnRight, context),
359 KeyBinding::new("space", Confirm { secondary: false }, context),
360 ]
361}
362
363/// Binds [`key_bindings`]. Call once, after `gpui_kit::init`. Without it the
364/// mouse works and the keys do not.
365pub fn init(cx: &mut App) {
366 cx.bind_keys(key_bindings());
367}
368
369#[cfg(test)]
370mod tests {
371 use std::collections::{HashMap, HashSet};
372
373 use gpui_kit::Keystroke;
374
375 use super::*;
376
377 /// A binding as "keystroke → action name", the keystroke in its parsed
378 /// form, so `secondary-a` and the platform's `cmd-a` agree.
379 fn binding(keystroke: &str, action: &str) -> (String, String) {
380 let keystroke = Keystroke::parse(keystroke).expect("valid keystroke");
381 (keystroke.unparse(), action.to_string())
382 }
383
384 #[test]
385 fn test_key_bindings() {
386 let bindings = key_bindings();
387 // Bindings by context, each key once per context.
388 let mut by_context: HashMap<String, HashSet<(String, String)>> = HashMap::new();
389 let mut keys_by_context: HashMap<String, HashSet<String>> = HashMap::new();
390 for binding in &bindings {
391 let [keystroke] = binding.keystrokes() else {
392 panic!("one keystroke per binding");
393 };
394 let key = keystroke.inner().unparse();
395 let context = binding
396 .predicate()
397 .expect("every binding has a context")
398 .to_string();
399 assert!(
400 keys_by_context
401 .entry(context.clone())
402 .or_default()
403 .insert(key.clone()),
404 "{key} is bound twice in {context}"
405 );
406 by_context
407 .entry(context)
408 .or_default()
409 .insert((key, binding.action().name().to_string()));
410 }
411 let set = |table: &[(&str, &str)]| -> HashSet<(String, String)> {
412 let set: HashSet<_> = table
413 .iter()
414 .map(|(keystroke, action)| binding(keystroke, action))
415 .collect();
416 assert_eq!(set.len(), table.len());
417 set
418 };
419 // Every default binding of the grid, once.
420 let grid = set(&[
421 ("up", "ui::SelectUp"),
422 ("down", "ui::SelectDown"),
423 ("left", "ui::SelectPrevColumn"),
424 ("right", "ui::SelectNextColumn"),
425 ("pageup", "ui::SelectPageUp"),
426 ("pagedown", "ui::SelectPageDown"),
427 ("home", "ui::SelectFirst"),
428 ("end", "ui::SelectLast"),
429 ("secondary-left", "ui::SelectFirst"),
430 ("secondary-right", "ui::SelectLast"),
431 ("secondary-up", "live_grid::SelectFirstRow"),
432 ("secondary-down", "live_grid::SelectLastRow"),
433 ("secondary-home", "live_grid::SelectFirstCell"),
434 ("secondary-end", "live_grid::SelectLastCell"),
435 ("shift-up", "live_grid::ExtendSelectionUp"),
436 ("shift-down", "live_grid::ExtendSelectionDown"),
437 ("shift-left", "live_grid::ExtendSelectionPrevColumn"),
438 ("shift-right", "live_grid::ExtendSelectionNextColumn"),
439 ("shift-pageup", "live_grid::ExtendSelectionPageUp"),
440 ("shift-pagedown", "live_grid::ExtendSelectionPageDown"),
441 ("shift-home", "live_grid::ExtendSelectionToFirst"),
442 ("shift-end", "live_grid::ExtendSelectionToLast"),
443 ("shift-secondary-left", "live_grid::ExtendSelectionToFirst"),
444 ("shift-secondary-right", "live_grid::ExtendSelectionToLast"),
445 ("shift-secondary-up", "live_grid::ExtendSelectionToFirstRow"),
446 (
447 "shift-secondary-down",
448 "live_grid::ExtendSelectionToLastRow",
449 ),
450 (
451 "shift-secondary-home",
452 "live_grid::ExtendSelectionToFirstCell",
453 ),
454 (
455 "shift-secondary-end",
456 "live_grid::ExtendSelectionToLastCell",
457 ),
458 ("secondary-a", "live_grid::SelectAll"),
459 ("space", "live_grid::ToggleSelection"),
460 ("enter", "ui::Confirm"),
461 ("secondary-enter", "ui::Confirm"),
462 ("escape", "ui::Cancel"),
463 // Copy and the context menu.
464 ("secondary-c", "live_grid::Copy"),
465 ("shift-f10", "live_grid::ShowContextMenu"),
466 ("menu", "live_grid::ShowContextMenu"),
467 // Expanding and collapsing.
468 ("+", "live_grid::ExpandRow"),
469 ("shift-=", "live_grid::ExpandRow"),
470 ("-", "live_grid::CollapseRow"),
471 // Editing.
472 ("f2", "live_grid::StartEdit"),
473 ("backspace", "live_grid::StartEditEmpty"),
474 ("delete", "live_grid::StartEditEmpty"),
475 ]);
476 // On a chip of the group panel.
477 let panel = set(&[
478 ("delete", "live_grid::UngroupColumn"),
479 ("backspace", "live_grid::UngroupColumn"),
480 ("enter", "zed::NoAction"),
481 ("space", "zed::NoAction"),
482 ]);
483 // Space on a focused control; the editor's own keys; the column
484 // chooser's.
485 let control = set(&[("space", "live_grid::ActivateCell")]);
486 let link = set(&[("enter", "live_grid::OpenLink")]);
487 let editor_own = set(&[
488 ("enter", "live_grid::CommitEdit"),
489 ("secondary-enter", "live_grid::CommitEdit"),
490 ("shift-enter", "live_grid::CommitEditUp"),
491 ("up", "live_grid::CommitEditUp"),
492 ("down", "live_grid::CommitEditDown"),
493 ("tab", "live_grid::CommitEditNext"),
494 ("shift-tab", "live_grid::CommitEditPrev"),
495 ("escape", "live_grid::CancelEdit"),
496 ]);
497 let chooser = set(&[
498 ("alt-up", "live_grid::MoveColumnLeft"),
499 ("alt-down", "live_grid::MoveColumnRight"),
500 ("space", "ui::Confirm"),
501 ]);
502 // In the editor, every key of the grid is either the editor's own or
503 // `NoAction`.
504 let own_keys: HashSet<String> = editor_own.iter().map(|(key, _)| key.clone()).collect();
505 let mut editor = editor_own.clone();
506 for (key, _) in grid.iter().chain(&control).chain(&link) {
507 if !own_keys.contains(key) {
508 editor.insert((key.clone(), "zed::NoAction".to_string()));
509 }
510 }
511 let expected = HashMap::from([
512 (KEY_CONTEXT.to_string(), grid),
513 (GROUP_PANEL_KEY_CONTEXT.to_string(), panel),
514 (CONTROL_CELL_CONTEXT.to_string(), control),
515 (LINK_CELL_CONTEXT.to_string(), link),
516 (EDITOR_KEY_CONTEXT.to_string(), editor),
517 (COLUMN_CHOOSER_KEY_CONTEXT.to_string(), chooser),
518 ]);
519 assert_eq!(by_context, expected);
520 assert!(
521 by_context[EDITOR_KEY_CONTEXT].contains(&binding("left", "zed::NoAction"))
522 && by_context[EDITOR_KEY_CONTEXT].contains(&binding("f2", "zed::NoAction"))
523 && by_context[EDITOR_KEY_CONTEXT].contains(&binding("space", "zed::NoAction")),
524 "the grid's navigation and commands are silenced in the editor"
525 );
526
527 // `ActivateCell` comes after `ToggleSelection`: at one depth the
528 // later binding wins.
529 let position = |action: &str| {
530 bindings
531 .iter()
532 .position(|binding| binding.action().name() == action)
533 .expect("bound")
534 };
535 assert!(position("live_grid::ActivateCell") > position("live_grid::ToggleSelection"));
536 // Enter and Cmd/Ctrl+Enter differ by `secondary`.
537 let confirms: Vec<_> = bindings
538 .iter()
539 .filter(|binding| {
540 binding
541 .predicate()
542 .is_some_and(|predicate| predicate.to_string() == KEY_CONTEXT)
543 })
544 .filter_map(|binding| binding.action().as_any().downcast_ref::<Confirm>())
545 .map(|confirm| confirm.secondary)
546 .collect();
547 assert_eq!(confirms, [false, true]);
548 // The sort commands are the application's to bind, as are expand
549 // all, collapse all, captions only, the grouping commands and the
550 // column chooser.
551 assert!(
552 bindings.iter().all(|binding| {
553 let action = binding.action().as_any();
554 !action.is::<ClearAllSorting>()
555 && !action.is::<ClearSort>()
556 && !action.is::<ExpandAll>()
557 && !action.is::<CollapseAll>()
558 && !action.is::<ShowCaptionsOnly>()
559 && !action.is::<GroupByColumn>()
560 && !action.is::<ClearGrouping>()
561 && !action.is::<ShowColumnChooser>()
562 }),
563 "no default key for the application's commands"
564 );
565 // Tab leaves the grid: free in the grid, bound in the editor.
566 for free in ["tab", "shift-tab"] {
567 let key = Keystroke::parse(free).expect("valid keystroke").unparse();
568 assert!(
569 !keys_by_context[KEY_CONTEXT].contains(&key),
570 "{free} is bound"
571 );
572 assert!(keys_by_context[EDITOR_KEY_CONTEXT].contains(&key), "{free}");
573 }
574 }
575}