#1442: Ctrl+. skips corners the sheet can't select and stops once on a merged corner

Addresses CodeRabbit review on #3226:
- "Check the next corner against protected-sheet selection rules": on a
  sheet protected without Select Locked Cells, Ctrl+. could make a locked
  corner active. It now goes on to the next corner it may select.
- "Keep the active cell at a merged cell's anchor": a corner inside a
  merged cell made that cell active instead of the merge's top-left
  cell. The corner now lands on the top-left cell, and corners in the
  same merged cell are one stop.

Selection::next_corner_by takes where each corner leads (or None to skip
it); next_corner keeps its plain behaviour.

Tests: next_corner_skips_corners_the_sheet_protection_does_not_let_you_select,
next_corner_stops_once_on_a_merged_corner_at_its_top_left_cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hector VillarrealandClaude Opus 5.5 committed 2026-11-03 01:08:01 -06:00
1 parent 8b2550c9cf
commit 5acb4af789
3 files changed
+48 -6

No files matched your search

+7 -1
View File
@@ -139,7 +139,13 @@ pub(super) fn selection_next(s: &mut Session, p: &Json) -> Result<Json> {
pub(super) fn selection_next_corner(s: &mut Session, _: &Json) -> Result<Json> {
let d = s.doc_mut()?;
d.selection.next_corner();
let Some(sheet) = d.wb.active() else { return Err(EngineError::NoDocument) };
// A corner in a merged cell lands on its top-left cell; one the sheet's protection doesn't
// let you select is skipped.
let stop = |c: CellRef| Some(sheet.merge_at(c).map_or(c, |m| m.start)).filter(|c| cell_selectable(&d.wb, sheet, *c));
let mut next = d.selection.clone();
next.next_corner_by(stop);
d.selection = next;
Ok(json!({"active": d.selection.active.a1(), "selection": d.selection.a1()}))
}
+27
View File
@@ -397,3 +397,30 @@ fn next_corner_of_selection_goes_clockwise_and_anchors_there() {
s.execute("selection.set", json!({"range": "C5"})).unwrap();
assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "C5");
}
#[test]
fn next_corner_skips_corners_the_sheet_protection_does_not_let_you_select() {
// With locked cells unselectable, Ctrl+. goes on to the next corner it may select.
let mut s = Session::new();
s.new_workbook();
s.execute("home.lockCell", json!({"range": "B2:C3", "on": false})).unwrap();
s.execute("selection.set", json!({"range": "B2:D3", "active": "B2"})).unwrap();
s.execute("review.protectSheet", json!({"selectLockedCells": false})).unwrap();
let r = s.execute("selection.nextCorner", json!({})).unwrap();
assert_eq!((r["active"].as_str(), r["selection"].as_str()), (Some("B3"), Some("B2:D3")), "D2 and D3 are locked");
assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "B2");
}
#[test]
fn next_corner_stops_once_on_a_merged_corner_at_its_top_left_cell() {
// A corner inside a merged cell is that merged cell, whose active cell is its top-left one.
let mut s = Session::new();
s.new_workbook();
s.execute("home.mergeCells", json!({"range": "C2:D2"})).unwrap();
s.execute("selection.set", json!({"range": "B2:D3", "active": "B2"})).unwrap();
let seen: Vec<String> = (0..4).map(|_| s.execute("selection.nextCorner", json!({})).unwrap()["active"].as_str().unwrap().to_string()).collect();
assert_eq!(seen, ["C2", "D3", "B3", "B2"]);
// A selection that is one merged cell has no other corner.
s.execute("selection.set", json!({"range": "C2:D2", "active": "C2"})).unwrap();
assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "C2");
}
+14 -5
View File
@@ -134,14 +134,23 @@ impl Selection {
/// 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) {
self.next_corner_by(Some);
}
/// [`Selection::next_corner`] where `stop` says where each corner takes the active cell: the
/// top-left cell of a merged cell the corner is in, or `None` for a corner that can't be
/// selected (it is skipped). Corners that end up on the active cell are one stop with it.
pub fn next_corner_by(&mut self, stop: impl Fn(CellRef) -> Option<CellRef>) {
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,
let next = match corners.iter().rposition(|c| stop(*c) == Some(self.active)) {
Some(i) => (1..=4).filter_map(|k| corners.get((i + k) % 4)).filter_map(|c| stop(*c)).find(|c| *c != self.active),
None => corners.iter().find_map(|c| stop(*c)),
};
self.active = next;
self.anchor = next;
if let Some(next) = next {
self.active = next;
self.anchor = next;
}
}
}