From 57e40d2d14ea1a4d3928cc1404db1a893efae932 Mon Sep 17 00:00:00 2001 From: Kameron Date: Thu, 29 Oct 2026 15:11:55 -0700 Subject: [PATCH] Formula pointing: Ctrl+arrow and Ctrl+Shift+arrow jump to the edge of the data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While a formula reference is being pointed from the keyboard, Ctrl+arrow (⌘arrow on a Mac) moved the pointed cell by one, and Ctrl+Shift+arrow extended the pointed range by one cell, so `=COUNTIF(`, a click on the top of a column and ⌘⇧↓ pointed at two cells instead of the column of data. Both now go to the edge of the data region, as the same keys do on the selection outside an edit: `point_move` takes `jump` and uses the engine's `selection::jump`, which Ctrl+arrow on the grid already uses, so the two agree (last filled cell of a run, else the next filled cell, else the sheet edge). A pointed range that reaches the sheet edge is written as whole columns or rows (`A:B`), as Excel writes it. The editor used to read Shift from `i.modifiers`, the modifier state when the frame's input was collected; an arrow pressed and released within one frame (a control client's `ui.key`, an automation tool's keystroke) had already lost it. It now reads the modifiers carried by the arrow's own key event, as the grid's keyboard handler does. Test: ctrl_arrows_in_point_mode_jump_to_the_edge_of_the_data (down to the last filled cell, right across the data, down to the sheet edge, Ctrl+Up without Shift to the next filled cell, then a plain arrow). Co-Authored-By: Claude Fable 5.1 --- crates/ui-egui/src/formula_bar.rs | 29 +++++++++++++++++++++----- crates/ui-egui/src/grid/interact.rs | 8 ++++--- crates/ui-egui/tests/it/interaction.rs | 27 ++++++++++++++++++++++++ docs/ui-parity.md | 3 ++- 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/formula_bar.rs b/crates/ui-egui/src/formula_bar.rs index 574f766de..3db0c3da4 100644 --- a/crates/ui-egui/src/formula_bar.rs +++ b/crates/ui-egui/src/formula_bar.rs @@ -518,9 +518,23 @@ fn editor_arrow_keys( ed: &mut crate::editor::EditState, mut action: Option, ) -> (Option, bool) { - let (up, down, left, right, shift) = ui.input(|i| { - (i.key_pressed(Key::ArrowUp), i.key_pressed(Key::ArrowDown), i.key_pressed(Key::ArrowLeft), i.key_pressed(Key::ArrowRight), i.modifiers.shift) + // The modifiers come with the key event: an arrow pressed and released within one frame (a + // control client's or an automation tool's keystroke) has already lost them from `i.modifiers`. + let arrow = ui.input(|i| { + i.events.iter().find_map(|e| match e { + egui::Event::Key { key: key @ (Key::ArrowUp | Key::ArrowDown | Key::ArrowLeft | Key::ArrowRight), pressed: true, modifiers, .. } => { + Some((*key, *modifiers)) + } + _ => None, + }) }); + let (up, down, left, right) = ( + arrow.is_some_and(|(k, _)| k == Key::ArrowUp), + arrow.is_some_and(|(k, _)| k == Key::ArrowDown), + arrow.is_some_and(|(k, _)| k == Key::ArrowLeft), + arrow.is_some_and(|(k, _)| k == Key::ArrowRight), + ); + let (shift, jump) = arrow.map_or((false, false), |(_, m)| (m.shift, m.command)); if (up || down) && !ed.autocomplete.is_empty() { ui.input_mut(|i| { i.consume_key(egui::Modifiers::NONE, Key::ArrowUp); @@ -545,13 +559,18 @@ fn editor_arrow_keys( let rtl = app.session.active().and_then(|d| d.wb.active()).is_some_and(|s| s.right_to_left); let dc = if rtl { -dc } else { dc }; if ed.can_point() { + // Ctrl+arrow (⌘arrow) jumps the pointed cell to the edge of its data, Ctrl+Shift+arrow + // extends the pointed range there, as the same keys do on the selection. ui.input_mut(|i| { for key in [Key::ArrowUp, Key::ArrowDown, Key::ArrowLeft, Key::ArrowRight] { - i.consume_key(egui::Modifiers::NONE, key); - i.consume_key(egui::Modifiers::SHIFT, key); + for modifiers in + [egui::Modifiers::NONE, egui::Modifiers::SHIFT, egui::Modifiers::COMMAND, egui::Modifiers::COMMAND | egui::Modifiers::SHIFT] + { + i.consume_key(modifiers, key); + } } }); - crate::grid::point_move(app, ed, dr, dc, shift); + crate::grid::point_move(app, ed, dr, dc, shift, jump); (action, false) } else { action = Some((dr, dc, false, false)); diff --git a/crates/ui-egui/src/grid/interact.rs b/crates/ui-egui/src/grid/interact.rs index fdbf632a5..4ffeaeb83 100644 --- a/crates/ui-egui/src/grid/interact.rs +++ b/crates/ui-egui/src/grid/interact.rs @@ -1014,11 +1014,13 @@ pub fn follow_link(app: &mut SheetApp, target: &str) { } } -/// Used by `EditState` users to keep the active cell visible while pointing. -pub fn point_move(app: &mut SheetApp, ed: &mut EditState, dr: i64, dc: i64, extend: bool) { +/// Moves the pointed cell one step, or with `jump` (Ctrl+arrow) to the edge of its data region; +/// `extend` (Shift) moves the far end of the pointed range instead. Keeps the pointed cell visible. +pub fn point_move(app: &mut SheetApp, ed: &mut EditState, dr: i64, dc: i64, extend: bool, jump: bool) { let Some(d) = app.session.active() else { return }; + let Some(sheet) = d.wb.active() else { return }; let from = ed.point_cell.unwrap_or(ed.cell); - let to = from.offset_clamped(dr, dc); + let to = if jump { gridcraft_engine::selection::jump(sheet, from, dr.signum(), dc.signum()) } else { from.offset_clamped(dr, dc) }; let anchor = if extend { // The anchor is the corner opposite the moving end: the written range is normalized, so // after extending up or left its first cell is the moving end, not the anchor. diff --git a/crates/ui-egui/tests/it/interaction.rs b/crates/ui-egui/tests/it/interaction.rs index e48a5cd32..39754591e 100644 --- a/crates/ui-egui/tests/it/interaction.rs +++ b/crates/ui-egui/tests/it/interaction.rs @@ -331,6 +331,33 @@ fn shift_arrows_in_point_mode_extend_from_the_pointed_cell() { assert_eq!(edited(&h).as_deref(), Some("=SUM(A8:A10"), "Shift+Down shrinks back towards the anchor"); } +#[test] +fn ctrl_arrows_in_point_mode_jump_to_the_edge_of_the_data() { + // Typing `=COUNTIF(`, clicking the top of a column and pressing Ctrl+Shift+Down (⌘⇧↓) points + // at the whole column of data, as the same keys select it outside an edit. + let mut s = blank(); + s.execute("range.setValues", json!({"range": "A1:B5", "values": [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]})).unwrap(); + s.execute("selection.set", json!({"cell": "D1"})).unwrap(); + let mut h = harness(s); + text(&mut h, "=COUNTIF("); + click_cell(&mut h, "A1"); + let edited = |h: &egui_kittest::Harness<'static, SheetApp>| h.state().editor.as_ref().map(|e| e.text.clone()); + assert_eq!(edited(&h).as_deref(), Some("=COUNTIF(A1")); + h.input_mut().events.push(Event::ModifiersChanged(Modifiers::COMMAND | Modifiers::SHIFT)); + key(&mut h, Key::ArrowDown, Modifiers::COMMAND | Modifiers::SHIFT); + assert_eq!(edited(&h).as_deref(), Some("=COUNTIF(A1:A5"), "Ctrl+Shift+Down extends to the last filled cell"); + key(&mut h, Key::ArrowRight, Modifiers::COMMAND | Modifiers::SHIFT); + assert_eq!(edited(&h).as_deref(), Some("=COUNTIF(A1:B5"), "Ctrl+Shift+Right extends the moving end across the data"); + key(&mut h, Key::ArrowDown, Modifiers::COMMAND | Modifiers::SHIFT); + assert_eq!(edited(&h).as_deref(), Some("=COUNTIF(A:B"), "past the data, Ctrl+Shift+Down reaches the sheet edge (written as whole columns)"); + h.input_mut().events.push(Event::ModifiersChanged(Modifiers::COMMAND)); + key(&mut h, Key::ArrowUp, Modifiers::COMMAND); + assert_eq!(edited(&h).as_deref(), Some("=COUNTIF(B5"), "Ctrl+Up without Shift points at the next filled cell"); + h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE)); + key(&mut h, Key::ArrowUp, Modifiers::NONE); + assert_eq!(edited(&h).as_deref(), Some("=COUNTIF(B4"), "a plain arrow still moves one cell"); +} + #[test] fn shortcuts_format_and_extend() { let mut s = blank(); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 2329a2720..c99fda5ad 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (AutoSum over a selected row puts its total to the right; earlier: macro interface on the Automate tab; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; earlier: AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md). **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured @@ -195,6 +195,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | 2026-10-24 | minor | Macro step editor: step list with breakpoints, drag to reorder and a command picker, an inspector with reference boxes, a debug bar and grid overlays | | 2026-10-24 | minor | Automate › Macros group (Record Macro, Use Relative References, Run Macro), recording status in the status bar, Macros dialog with macros beside scripts, Record Macro dialog, Ctrl+Shift+letter macro shortcuts; View › Macros keytips open the dialogs | | 2026-10-24 | minor | Ribbon catalog count 275 / 291 → 276 / 292 (View › Macros › Record Macro) | +| 2026-10-29 | minor | Formula pointing from the keyboard: Ctrl+arrow (⌘arrow) moves the pointed cell to the edge of its data and Ctrl+Shift+arrow (⌘⇧arrow) extends the pointed range there, as the same keys move and extend the selection, so typing `=COUNTIF(`, clicking the top of a column and pressing ⌘⇧↓ points at the whole column of data; the keys used to move or extend by one cell. A range reaching the sheet edge is written as whole columns or rows (`A:B`). The editor reads the modifiers of the arrow's own key event, so a keystroke pressed and released within one frame (a control client's `ui.key`, an automation tool) keeps its Shift and Ctrl | | 2026-10-24 | minor | Go To lists the named ranges on every sheet, as Excel's does ("The Go to popup window shows named ranges on every worksheet in your workbook"): a name scoped to another sheet appears as `Sheet2!Name`, and hidden names, constants and formulas are left out. It keeps the references entered (newest first, ten at most), and a double-click on an entry goes there. The Reference box and `edit.goTo` take several names or references separated by commas (`Price, Type`, `B14:C22,F19:G30,H21:H29`) and select them all; `edit.goTo {list: true}` returns the list | | 2026-10-24 | minor | Right-clicking the status bar opens a menu of its statistics, as in Excel ("If you want to customize the status bar, right-click it, and then click the options that you want"): Average, Count and Sum are on by default, and Numerical Count, Minimum and Maximum are new and off by default. The choice is a preference (`view.statusBar`), kept in `prefs.json` | | 2026-10-24 | minor | Ctrl+Shift+U (Control+Shift+U on a Mac) expands or collapses the formula bar, as in Excel ("Expand or collapse the formula bar. Ctrl+Shift+U"), on the grid and in the cell editor or formula bar while editing. On Windows and Linux it used to underline the selection, as Ctrl+U does. The command is `view.expandFormulaBar` (`{on?}`, toggles without it) |