Skip to main content

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}