From 8b2550c9cf8fac8c9759f48fa8eafbb376a4d79d Mon Sep 17 00:00:00 2001 From: Hector Villarreal Date: Tue, 3 Nov 2026 00:44:15 -0600 Subject: [PATCH] #1442: Ctrl+. moves to the next corner of the selection Excel's Next Corner of Selection (Ctrl+., Control+. on a Mac) moves the active cell clockwise to the next corner of the selected area (top-left, top-right, bottom-right, bottom-left) and keeps the area selected; the new active cell is also the anchor, so Shift+arrows then move the opposite corner. GridCraft had no such command. New command selection.nextCorner (Selection::next_corner) bound to Ctrl+. on every platform. A one-row or one-column area goes end to end; a single cell stays where it is. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/edit.rs | 11 +++++++- crates/engine/src/cmd/edit/selection.rs | 6 +++++ crates/engine/src/cmd/edit/tests.rs | 26 +++++++++++++++++++ crates/engine/src/selection.rs | 17 ++++++++++++ crates/ui-egui/src/keymap.rs | 2 ++ crates/ui-egui/tests/it/keyboard_shortcuts.rs | 5 ++++ docs/ui-parity.md | 4 ++- 7 files changed, 69 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 2aa9d8fe3..cf75a429d 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -36,7 +36,7 @@ use go_to::go_to_special; use go_to::{GO_TO_MAX_AREAS, GO_TO_RECENT, reference_parts}; use paste_special::paste_special; pub(crate) use paste_special::{rewrite_all_formulas, rewrite_cell_links, rewrite_python_refs}; -use selection::{select_all, select_col, select_region, select_row, selection_move, selection_next, selection_set}; +use selection::{select_all, select_col, select_region, select_row, selection_move, selection_next, selection_next_corner, selection_set}; pub fn specs() -> Vec { vec![ @@ -62,6 +62,15 @@ pub fn specs() -> Vec { cmd!(noundo "selection.set", "Select", [], None, "{range: \"A1:B2,D4\" | cell, active?: \"A1\", sheet?}", has_doc, selection_set), cmd!(noundo "selection.move", "Move Selection", [], None, "{dr, dc, extend?: bool, jump?: bool (Ctrl+arrow), page?: bool}", has_doc, selection_move), cmd!(noundo "selection.next", "Next Cell in Selection", [], None, "{forward?: true, byRow?: false} (Enter/Tab inside a selection)", has_doc, selection_next), + cmd!( + noundo "selection.nextCorner", + "Next Corner of Selection", + [], + Some("Ctrl+."), + "{} (the active cell moves clockwise to the next corner of the selected area, which stays selected)", + has_doc, + selection_next_corner + ), cmd!(noundo "edit.selectAll", "Select All", ["Edit"], Some("Cmd+A"), "{} (current region first, then the whole sheet)", has_doc, select_all), cmd!(noundo "selection.currentRegion", "Select Current Region", [], Some("Ctrl+Shift+8"), "{}", has_doc, select_region), cmd!(noundo "selection.row", "Select Entire Row", [], Some("Shift+Space"), "{}", has_doc, select_row), diff --git a/crates/engine/src/cmd/edit/selection.rs b/crates/engine/src/cmd/edit/selection.rs index ede66a34d..03e178156 100644 --- a/crates/engine/src/cmd/edit/selection.rs +++ b/crates/engine/src/cmd/edit/selection.rs @@ -137,6 +137,12 @@ pub(super) fn selection_next(s: &mut Session, p: &Json) -> Result { Ok(json!({"active": d.selection.active.a1()})) } +pub(super) fn selection_next_corner(s: &mut Session, _: &Json) -> Result { + let d = s.doc_mut()?; + d.selection.next_corner(); + Ok(json!({"active": d.selection.active.a1(), "selection": d.selection.a1()})) +} + pub(super) fn select_all(s: &mut Session, _: &Json) -> Result { let d = s.doc_mut()?; let Some(sheet) = d.wb.active() else { return Err(EngineError::NoDocument) }; diff --git a/crates/engine/src/cmd/edit/tests.rs b/crates/engine/src/cmd/edit/tests.rs index 3f3a2fb6d..ead610e50 100644 --- a/crates/engine/src/cmd/edit/tests.rs +++ b/crates/engine/src/cmd/edit/tests.rs @@ -371,3 +371,29 @@ fn replace_case_insensitive_keeps_text_when_folding_moves_boundaries() { let sh = s.doc().unwrap().wb.active().unwrap(); assert_eq!(sh.value(CellRef::new(0, 0)), Value::text(text)); } + +#[test] +fn next_corner_of_selection_goes_clockwise_and_anchors_there() { + // #1442, as Excel does it: B2 → D2 → D4 → B4 → B2, the area stays selected, and Shift+arrows + // then move the corner opposite the new active cell. + let mut s = Session::new(); + s.new_workbook(); + s.execute("selection.set", json!({"range": "B2:D4", "active": "B2"})).unwrap(); + let mut seen = Vec::new(); + for _ in 0..4 { + let r = s.execute("selection.nextCorner", json!({})).unwrap(); + assert_eq!(r["selection"], "B2:D4"); + seen.push(r["active"].as_str().unwrap().to_string()); + } + assert_eq!(seen, ["D2", "D4", "B4", "B2"]); + s.execute("selection.nextCorner", json!({})).unwrap(); + s.execute("selection.nextCorner", json!({})).unwrap(); + s.execute("selection.move", json!({"dr": 0, "dc": 1, "extend": true})).unwrap(); + assert_eq!(s.doc().unwrap().selection.current().a1(), "C2:D4"); + // One row goes end to end; a single cell stays. + s.execute("selection.set", json!({"range": "B2:D2", "active": "B2"})).unwrap(); + assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "D2"); + assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "B2"); + s.execute("selection.set", json!({"range": "C5"})).unwrap(); + assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "C5"); +} diff --git a/crates/engine/src/selection.rs b/crates/engine/src/selection.rs index 0e00e4869..a69870adb 100644 --- a/crates/engine/src/selection.rs +++ b/crates/engine/src/selection.rs @@ -128,6 +128,23 @@ impl Selection { } } +impl Selection { + /// Ctrl+. (Next Corner of Selection): the active cell moves clockwise to the next corner of + /// the current area (top-left, top-right, bottom-right, bottom-left) and becomes the anchor, + /// so Shift+arrows then move the opposite corner. The area stays selected. From a cell that + /// isn't a corner the next corner is the top-left one; a single cell stays where it is. + pub fn next_corner(&mut self) { + let r = self.current(); + let corners = [r.start, CellRef::new(r.start.row, r.end.col), r.end, CellRef::new(r.end.row, r.start.col)]; + let next = match corners.iter().position(|c| *c == self.active) { + Some(i) => (1..=4).filter_map(|k| corners.get((i + k) % 4)).copied().find(|c| *c != self.active).unwrap_or(self.active), + None => r.start, + }; + self.active = next; + self.anchor = next; + } +} + /// Ctrl+arrow: jump to the edge of the current data region (Excel's End mode). pub fn jump(sheet: &Sheet, from: CellRef, dr: i64, dc: i64) -> CellRef { if dr == 0 && dc == 0 { diff --git a/crates/ui-egui/src/keymap.rs b/crates/ui-egui/src/keymap.rs index 5217724c0..5acce9b4d 100644 --- a/crates/ui-egui/src/keymap.rs +++ b/crates/ui-egui/src/keymap.rs @@ -115,6 +115,8 @@ fn lookup_number_formats(ctx: LookupContext) -> Option { Num7 if ctrl && s => borders("outside"), Minus if ctrl && s => borders("none"), Num8 if ctrl && s => run("selection.currentRegion"), + // Ctrl+. (Control on a Mac too): Next Corner of Selection. + Period if ctrl && !s => run("selection.nextCorner"), Semicolon if ctrl && s => Shortcut::Time, Quote if ctrl && s => Shortcut::ValueAbove, Quote if ctrl => Shortcut::FormulaAbove, diff --git a/crates/ui-egui/tests/it/keyboard_shortcuts.rs b/crates/ui-egui/tests/it/keyboard_shortcuts.rs index 8d3de6fcb..f59504dee 100644 --- a/crates/ui-egui/tests/it/keyboard_shortcuts.rs +++ b/crates/ui-egui/tests/it/keyboard_shortcuts.rs @@ -100,6 +100,11 @@ fn windows_and_mac_variants() { assert_eq!(lookup(Key::ArrowLeft, Some(Key::Num4), alt_shift, false), run("data.ungroup")); assert_eq!(lookup(Key::PageDown, Some(Key::Num3), Modifiers::ALT, false), Some(Shortcut::ScreenRight)); assert_eq!(lookup(Key::PageDown, Some(Key::Num3), CTRL_SHIFT, false), None); + // Ctrl+. is Next Corner of Selection, with Control on a Mac too (#1442); Cmd+Shift+. stays + // Increase Font Size. + assert_eq!(lookup(Key::Period, Some(Key::Period), cmd, false), run("selection.nextCorner")); + assert_eq!(lookup(Key::Period, Some(Key::Period), ctrl, true), run("selection.nextCorner")); + assert_eq!(lookup(Key::Period, Some(Key::Period), Modifiers { shift: true, ..mac_cmd }, true), run("home.increaseFontSize")); // Plain keys stay with the grid (typing, navigation). assert_eq!(lookup(Key::A, Some(Key::A), Modifiers::NONE, false), None); assert_eq!(lookup(Key::F2, Some(Key::F2), Modifiers::NONE, false), None); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 125182d27..96fa3864d 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-11-02 · **Change:** minor (RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; 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 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (Ctrl+. Next Corner of Selection; earlier: RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; 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 @@ -125,6 +125,7 @@ otherwise. Number-format and border keys match the physical key (Ctrl+Shift+4, w | — | Cmd+Shift+H, Cmd+Shift+R, Cmd+Shift+K / J, Control+L, Control+U | replace, sort, group / ungroup, Define Name, edit cell | **added** | | Alt+Shift+Right / Left | Option+Right / Left is next/previous sheet | group / ungroup | **added** | | Ctrl+Shift+* (Ctrl+Shift+8), Ctrl+Shift+Space | Cmd+Shift+Space | current region, select all | **added** | +| Ctrl+. | Control+. | Next Corner of Selection: the active cell goes clockwise to the next corner and anchors there (`selection.nextCorner`, #1442) | **added** | | Ctrl+Shift+Home / End, Shift+Home | same | extend to A1 / last used cell / row start | **added** | | Ctrl+P | Cmd+P | print | **added** | | Ctrl+Shift+> / < | Cmd+Shift+> / < | font size | **added** | @@ -237,6 +238,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | Ctrl+. (Control+. on a Mac): Next Corner of Selection, `selection.nextCorner`: the active cell moves clockwise to the next corner of the selected area, which stays selected, and becomes the anchor, as in Excel (#1442) | | 2026-11-02 | minor | RefEdit reference boxes in dialogs: pick a range on the grid, collapse the dialog while selecting; Goal Seek, Solver (with the Model Range of Load/Save) and Data Analysis use them; while collapsed the arrow keys move the selection and Shift+arrow extends it, F4 cycles a reference's anchors, a click elsewhere keeps the box picking, a dialog opened over a picking one puts the selection back, OK stays on the sheet its command showed, and scripted cancel and confirm end a pick like the buttons; the dialog's sheet is followed through the recorded undo steps, so a sheet deleted and recreated under its name is not taken for it and an undo of the deletion brings it back; a box the dialog stops showing is no longer picked into | | 2026-11-02 | minor | Python mode edits a cell's code in the formula bar's code editor (Ctrl+Alt+Shift+P, `=PY` then Tab, editing a Python cell), with the PY badge beside fx; its keys apply in input order, also several in one frame, and Ctrl+Enter commits through `python.set` (empty code clears the cell), keeping the code and showing the command's message when the code is refused | | 2026-11-02 | minor | Insert › Symbols: Equation split button (access keys N E), a gallery of examples, and an equation editor with quick-insert buttons, the error underlined at its position and a live preview; double-clicking an equation edits it |