Skip to content

Add Ranges type for multi-range selection support#48

Merged
hackerwins merged 7 commits into
mainfrom
feat/ranges-type
Mar 18, 2026
Merged

Add Ranges type for multi-range selection support#48
hackerwins merged 7 commits into
mainfrom
feat/ranges-type

Conversation

@hackerwins

@hackerwins hackerwins commented Mar 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add Ranges type (Range[]) and 8 utility functions (toRanges, inRanges, isIntersectRanges, toRefsFromRanges, toSrngFromRanges, parseRanges, mergeOverlapping, removeRange)
  • Replace single-range selection (range?: Range) with Ranges in Sheet model, with backward-compatible getRange() returning the last range
  • Add addSelection() / addSelectionEnd() for Ctrl/Cmd+click multi-selection
  • Render multiple selection highlights in Canvas overlay (both simple and frozen paths)
  • Formula bar shows only the range containing the active cell
  • Tab/Enter navigate across all selected ranges, wrapping from one to the next
  • Style operations (setRangeStyle, resetRangeStyleToDefault) apply to all ranges

Test plan

  • 35 unit tests for Ranges utility functions (coordinates.test.ts)
  • 12 unit tests for multi-selection behavior (sheet.test.ts)
  • pnpm verify:fast passes (1079 sheet + 104 backend + 60 cli + 59 frontend tests)
  • Manual: Ctrl+click multiple cells/ranges, verify overlay highlights all
  • Manual: Tab/Shift+Tab cycles through all selected ranges
  • Manual: Apply bold/color with multi-selection, verify all ranges styled
  • Manual: Formula bar shows only the active cell's range label

🤖 Generated with Claude Code

Introduce `Ranges = Range[]` type and 8 utility functions in
coordinates.ts to represent multiple disjoint rectangular regions.
This is phase 1 of multi-range support — type and utilities only,
with Sheet selection model and UI integration to follow.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a new exported Ranges type (alias for Range[]) and a suite of eight range utilities, plus docs and tests. Core model, worksheet, view (overlay, formulabar, spreadsheet) and public API were updated to support multi-range selection and operations.

Changes

Cohort / File(s) Summary
Documentation
docs/tasks/active/20260319-ranges-type-todo.md, docs/tasks/active/20260319-ranges-type-lessons.md
New design and lessons docs describing the Ranges type, phased rollout, requirements, and integration notes.
Type Definitions
packages/sheet/src/model/core/types.ts
Added exported Ranges type: export type Ranges = Range[];.
Ranges Utilities Implementation
packages/sheet/src/model/core/coordinates.ts
Implemented utilities: toRanges, inRanges, isIntersectRanges, toRefsFromRanges, toSrngFromRanges, parseRanges, mergeOverlapping, removeRange plus private isAdjacent. Parsing, serialization, merging, containment, and iteration handled.
Unit Tests
packages/sheet/src/model/core/coordinates.test.ts
Comprehensive tests for normalization, containment, intersection, serialization/parse roundtrips, merging, and removal behaviors, including edge cases.
Public API Export
packages/sheet/src/index.ts
Exported the Ranges type and re-exported the new range utilities from the coordinates module.
Worksheet Model
packages/sheet/src/model/worksheet/sheet.ts
Replaced single-range state with ranges: Ranges; added getRanges, addSelection, addSelectionEnd; adapted selection, movement, wrapping, autofill, and style logic to operate on multiple ranges.
View — Overlay
packages/sheet/src/view/overlay.ts
Render pipeline updated to accept/iterate Ranges; changed signatures (render, renderSelectionsSimple, renderSelectionFrozen) and rendering logic to draw multiple selection ranges.
View — FormulaBar
packages/sheet/src/view/formulabar.ts
Now selects an active range from getRanges() (prefers range containing active cell or last range) and serializes with toSrng/toSref accordingly.
View — Worksheet & Spreadsheet
packages/sheet/src/view/worksheet.ts, packages/sheet/src/view/spreadsheet.ts
Mouse/drag flows updated to support Ctrl/Cmd multi-select (use addSelection/addSelectionEnd); public Spreadsheet methods added: getSelectionRanges, addSelection, addSelectionEnd.
Tests — Sheet behavior
packages/sheet/test/sheet/sheet.test.ts
Added multi-selection tests validating creation, active-cell logic, navigation across ranges, clearing, and style application across ranges.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,200,255,0.5)
    participant User
    end
    rect rgba(200,255,200,0.5)
    participant Worksheet
    participant Overlay
    participant FormulaBar
    end
    rect rgba(255,200,200,0.5)
    participant SheetModel
    participant Spreadsheet
    end

    User->>Worksheet: mouseDown (with/without Ctrl)
    Worksheet->>SheetModel: addSelection / selectStart
    User->>Worksheet: mouseMove / mouseUp
    Worksheet->>SheetModel: addSelectionEnd / selectEnd
    SheetModel->>Spreadsheet: notifySelectionChange
    Spreadsheet->>Overlay: render(getRanges())
    Spreadsheet->>FormulaBar: updateDisplay(getRanges(), activeRef)
    Overlay->>User: draw selections (iterate ranges)
    FormulaBar->>User: display active-range string
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibbled cells and hopped in rows,
Now many ranges make me pose.
Merge and parse, serialize with glee,
Multiple selections — oh what a spree! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title accurately and concisely summarizes the main objective of the PR: introducing a Ranges type for multi-range selection support, which is the primary deliverable.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ranges-type
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 99.6s

Lane Status Duration
sheet:build ✅ pass 13.9s
verify:fast ✅ pass 48.9s
frontend:build ✅ pass 14.3s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.4s
cli:build ✅ pass 1.8s
verify:entropy ✅ pass 15.9s

Verification: verify:integration

Result: ✅ PASS

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/sheet/src/model/core/coordinates.ts (1)

442-443: Make single-item return path clone-consistent in mergeOverlapping.

Line 442 returns a shallow array copy for length === 1, while later paths clone ranges. Returning [cloneRange(ranges[0])] keeps mutability behavior consistent.

♻️ Proposed refactor
 export function mergeOverlapping(ranges: Ranges): Ranges {
-  if (ranges.length <= 1) return [...ranges];
+  if (ranges.length === 0) return [];
+  if (ranges.length === 1) return [cloneRange(ranges[0])];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheet/src/model/core/coordinates.ts` around lines 442 - 443, The
single-item early return in mergeOverlapping currently returns a shallow
reference [...ranges] which is inconsistent with later paths that return cloned
ranges; change the early-return to return a cloned single item by using
cloneRange(ranges[0]) so callers always receive independent Range objects;
locate mergeOverlapping and replace the return [...ranges] for the length === 1
case with an array containing cloneRange(ranges[0]) to ensure consistent
immutability behavior.
packages/sheet/src/model/core/coordinates.test.ts (1)

120-152: Add parseRanges edge-case coverage for empty/invalid segments.

Given this suite already defines serialization behavior for empty ranges (Line 110-112), please also assert parse behavior for '' and malformed comma segments (e.g., 'A1,,B2') to prevent regressions.

🧪 Suggested tests
   describe('parseRanges', () => {
+    it('should return empty array for empty string', () => {
+      expect(parseRanges('')).toEqual([]);
+    });
+
+    it('should throw for empty range segments', () => {
+      expect(() => parseRanges('A1,,B2')).toThrow();
+    });
+
     it('should parse comma-separated ranges', () => {
       const result = parseRanges('A1:B2,C3:D4');
       expect(result).toEqual([r(1, 1, 2, 2), r(3, 3, 4, 4)]);
     });

As per coding guidelines, "Write tests for critical business logic and edge cases."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheet/src/model/core/coordinates.test.ts` around lines 120 - 152,
Add tests covering parseRanges edge cases: assert parseRanges('') returns []
(empty array) and parseRanges('A1,,B2') treats empty/malformed segments
gracefully (e.g., ignores empty segment and returns ranges for A1 and B2) and
that round-tripping via toSrngFromRanges on the parsed result yields the
expected normalized string ('' -> '' and 'A1,,B2' -> 'A1,B2'). Update the
existing describe blocks that exercise parseRanges and toSrngFromRanges to
include these assertions so regressions for empty/invalid segments are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/tasks/active/20260319-ranges-type-todo.md`:
- Around line 1-72: Create a paired lessons file named
20260319-ranges-type-lessons.md alongside the existing
20260319-ranges-type-todo.md and populate it with concise entries for each phase
(Phase 1, Phase 2, Phase 3) capturing key learnings, blockers encountered (e.g.,
changes to Range -> Ranges, serialization/parsing pitfalls), decisions made
(e.g., keeping backward-compatible single-range support), test outcomes
(reference the 33 unit tests and pnpm verify:fast), and next actions; ensure you
reference the task ID/date, mention the types and utilities added (Ranges,
toRanges, inRanges, isIntersectRanges, toRefsFromRanges, toSrngFromRanges,
parseRanges, mergeOverlapping, removeRange) and include short timestamps/status
notes so future reviewers can see progress and outcomes.

In `@packages/sheet/src/model/core/coordinates.ts`:
- Around line 427-435: parseRanges currently throws on empty input which breaks
the roundtrip with toSrngFromRanges (which returns '' for an empty set); update
parseRanges to handle empty or whitespace-only strings by returning an empty
Ranges array immediately (before splitting/processing). Locate the parseRanges
function and add a guard like checking s.trim() === '' and returning [] so
callers that expect the serialized empty selection (produced by
toSrngFromRanges) will parse back to an empty Ranges without invoking
parseRange/parseRef.

---

Nitpick comments:
In `@packages/sheet/src/model/core/coordinates.test.ts`:
- Around line 120-152: Add tests covering parseRanges edge cases: assert
parseRanges('') returns [] (empty array) and parseRanges('A1,,B2') treats
empty/malformed segments gracefully (e.g., ignores empty segment and returns
ranges for A1 and B2) and that round-tripping via toSrngFromRanges on the parsed
result yields the expected normalized string ('' -> '' and 'A1,,B2' -> 'A1,B2').
Update the existing describe blocks that exercise parseRanges and
toSrngFromRanges to include these assertions so regressions for empty/invalid
segments are caught.

In `@packages/sheet/src/model/core/coordinates.ts`:
- Around line 442-443: The single-item early return in mergeOverlapping
currently returns a shallow reference [...ranges] which is inconsistent with
later paths that return cloned ranges; change the early-return to return a
cloned single item by using cloneRange(ranges[0]) so callers always receive
independent Range objects; locate mergeOverlapping and replace the return
[...ranges] for the length === 1 case with an array containing
cloneRange(ranges[0]) to ensure consistent immutability behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 07c02be5-4173-4a6f-807d-63b2d73b5065

📥 Commits

Reviewing files that changed from the base of the PR and between 3e251a3 and 8b261eb.

📒 Files selected for processing (5)
  • docs/tasks/active/20260319-ranges-type-todo.md
  • packages/sheet/src/index.ts
  • packages/sheet/src/model/core/coordinates.test.ts
  • packages/sheet/src/model/core/coordinates.ts
  • packages/sheet/src/model/core/types.ts

Comment on lines +1 to +72
# Add `Ranges` Type — Multi-Range Data Type

**Goal:** Introduce a `Ranges` type and utility functions to represent multiple disjoint rectangular regions, then incrementally integrate into the selection model and UI.

**Status:** Phase 1 DONE

**Context:** The current `Range = [Ref, Ref]` can only represent a single rectangle. Multi-range support is needed for Ctrl+click multi-selection, formula engine, conditional formatting, and chart data ranges. This task is broken into three phases to allow incremental delivery.

## Design

### Type Definition (`types.ts`)

```typescript
type Ranges = Range[];
```

- Empty array `[]` means "no selection"
- A single Range is representable as `[range]` (backward-compatible)

### Utility Functions (`coordinates.ts`)

| Function | Signature | Description |
|----------|-----------|-------------|
| `toRanges` | `(...ranges: Range[]) => Ranges` | Creation helper; normalizes each Range |
| `inRanges` | `(ref: Ref, ranges: Ranges) => boolean` | Whether ref falls inside any Range |
| `isIntersectRanges` | `(a: Ranges, b: Ranges) => boolean` | Whether two Ranges overlap |
| `toRefsFromRanges` | `(ranges: Ranges) => Generator<Ref>` | Iterate all cells across all ranges |
| `toSrngFromRanges` | `(ranges: Ranges) => string` | Serialize to `"A1:A2,B1,B2:B3"` |
| `parseRanges` | `(s: string) => Ranges` | Parse comma-separated string |
| `mergeOverlapping` | `(ranges: Ranges) => Ranges` | Merge overlapping ranges |
| `removeRange` | `(ranges: Ranges, target: Range) => Ranges` | Remove a specific range |

### Design Principles

- Reuse existing `Range` utilities (`inRange`, `isIntersect`, `toRefs`, etc.) internally
- Follow the existing pattern of adding functions to `coordinates.ts`
- Unit tests for every function

## Phase 1: Type and Utility Functions (current scope)

- [x] Task 1: Add `Ranges` type definition and export in `types.ts`
- [x] Task 2: Add `toRanges`, `inRanges` to `coordinates.ts`
- [x] Task 3: Add `isIntersectRanges`, `toRefsFromRanges` to `coordinates.ts`
- [x] Task 4: Add `toSrngFromRanges`, `parseRanges` to `coordinates.ts`
- [x] Task 5: Add `mergeOverlapping`, `removeRange` to `coordinates.ts`
- [x] Task 6: Write unit tests for all functions (33 tests)
- [x] Task 7: Confirm `pnpm verify:fast` passes

## Phase 2: Sheet Selection Model

Replace the single-range selection in Sheet with `Ranges` support.

- [ ] Replace `range?: Range` field with `ranges: Ranges` in `Sheet`
- [ ] Add `addSelection(ref: Ref)` method for Ctrl+click (appends a new range)
- [ ] Active cell follows the start of the last added range (Google Sheets behavior)
- [ ] Update `getRange()` / `getRangeOrActiveCell()` for backward compatibility
- [ ] Update selection-related methods (`selectRow`, `selectColumn`, etc.)
- [ ] Unit tests for multi-selection behavior

## Phase 3: Frontend UI Integration

Wire multi-range selection into the frontend rendering and interaction layer.

- [ ] Handle Ctrl+click / Cmd+click events in `sheet-view.tsx` to call `addSelection()`
- [ ] Render multiple selection highlights on the Canvas
- [ ] Update selection-dependent UI (formula bar range display, context menu)
- [ ] Update copy/paste to operate on multiple ranges
- [ ] Interaction tests for multi-selection

## Future Phases (out of scope)

- **Phase 4:** Data model expansion — formula engine multi-range arguments, conditional formatting with `Ranges`, chart data ranges

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify paired task docs exist for this slug.
fd -a '20260319-ranges-type-(todo|lessons)\.md' docs/tasks/active

Repository: wafflebase/wafflebase

Length of output: 130


Add the paired lessons file for this task.

This is a non-trivial task (3 phases with 18+ subtasks), and the coding guidelines require maintaining paired task files: 20260319-ranges-type-todo.md and 20260319-ranges-type-lessons.md. Only the -todo file is present. Create docs/tasks/active/20260319-ranges-type-lessons.md to document learnings, blockers, and outcomes as this task progresses through its phases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/tasks/active/20260319-ranges-type-todo.md` around lines 1 - 72, Create a
paired lessons file named 20260319-ranges-type-lessons.md alongside the existing
20260319-ranges-type-todo.md and populate it with concise entries for each phase
(Phase 1, Phase 2, Phase 3) capturing key learnings, blockers encountered (e.g.,
changes to Range -> Ranges, serialization/parsing pitfalls), decisions made
(e.g., keeping backward-compatible single-range support), test outcomes
(reference the 33 unit tests and pnpm verify:fast), and next actions; ensure you
reference the task ID/date, mention the types and utilities added (Ranges,
toRanges, inRanges, isIntersectRanges, toRefsFromRanges, toSrngFromRanges,
parseRanges, mergeOverlapping, removeRange) and include short timestamps/status
notes so future reviewers can see progress and outcomes.

Comment thread packages/sheet/src/model/core/coordinates.ts
hackerwins and others added 6 commits March 19, 2026 06:10
Migrate Sheet.range to Sheet.ranges (Ranges = Range[]) for
multi-range selection support. Add addSelection() and
addSelectionEnd() methods for Ctrl+click multi-selection.
Backward-compatible: getRange() returns the last range,
getRangeOrActiveCell() works with the last range.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
parseRanges('') now returns [] instead of throwing, fixing the
roundtrip with toSrngFromRanges([]). Add paired lessons file
per project conventions.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Update overlay to iterate all ranges in Ranges array for both
simple (no-freeze) and frozen rendering paths. The formula bar
now displays comma-separated range labels (e.g. "A1:B2,C3")
when multiple ranges are selected. Ctrl/Cmd+click in worksheet
creates additive selections with proper drag support.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
1. moveInRange now navigates across all ranges: when reaching
   the end of one range, it wraps to the next/previous range.
2. Formula bar shows only the range containing the active cell
   instead of all ranges (which made the label too wide).
3. setRangeStyle and resetRangeStyleToDefault now apply to all
   ranges in a multi-selection, not just the last one.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Cover 3+ range cycling, fallback when active cell is outside
all ranges, and verify no crashes in all movement directions.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sheet/src/view/worksheet.ts (1)

2892-2922: ⚠️ Potential issue | 🟠 Major

Preserve context-menu clicks across all selected ranges.

With multi-range enabled, this still only preserves the active/last range, and e.ctrlKey || e.metaKey also treats macOS Ctrl+click as additive selection. A context-menu click on an already selected secondary block can therefore rewrite the selection before the menu action runs.

🔧 Suggested fix
-    if (e.button === 2) {
-      const clickedRef = this.toRefFromMouse(e.offsetX, e.offsetY);
-      const currentRange = this.sheet!.getRangeOrActiveCell();
-      if (inRange(clickedRef, currentRange)) {
+    const clickedRef = this.toRefFromMouse(e.offsetX, e.offsetY);
+    const isMacCtrlClick =
+      navigator.platform.includes('Mac') &&
+      e.button === 0 &&
+      e.ctrlKey &&
+      !e.metaKey;
+    const currentRanges = this.sheet!.getRanges();
+    const inExistingSelection =
+      currentRanges.length > 0
+        ? currentRanges.some((range) => inRange(clickedRef, range))
+        : inRange(clickedRef, this.sheet!.getRangeOrActiveCell());
+    if (e.button === 2 || isMacCtrlClick) {
+      if (inExistingSelection) {
         return;
       }
     }
@@
-    const isMultiSelect = e.ctrlKey || e.metaKey;
+    const isMultiSelect =
+      e.metaKey || (e.ctrlKey && !navigator.platform.includes('Mac'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheet/src/view/worksheet.ts` around lines 2892 - 2922,
Right-click/context menu logic only checks the active range
(this.sheet!.getRangeOrActiveCell()) and treats Ctrl+click the same as Cmd on
macOS, so right-clicking an already-selected secondary range can replace
selection; change the early-return to test whether clickedRef (from
toRefFromMouse) lies inside any existing selection range (iterate the sheet's
selection collection, e.g. this.sheet!.getSelections() / selection ranges) and
return if so, and adjust the multi-select detection so macOS uses only metaKey
for additive clicks (use platform check to make isMultiSelect true only for
e.metaKey on macOS, otherwise e.ctrlKey || e.metaKey). Ensure the rest of the
flow (addSelection/selectStart and addSelectionEnd/selectEnd) remains unchanged.
🧹 Nitpick comments (2)
packages/sheet/src/model/core/coordinates.ts (1)

444-462: Consider edge case: non-contiguous ranges with same coordinates but different order may not merge correctly.

The mergeOverlapping function sorts ranges by top-left corner (a[0].r, then a[0].c) and performs a single-pass merge. However, for rectangles that overlap but don't share the same starting row, a single pass may miss merges. Consider whether multi-pass or union-find would be more robust for complex overlapping scenarios.

For example, three ranges where A overlaps B, and B overlaps C, but after sorting A comes before C — if A and B merge first, the result may not properly merge with C if C was processed before the merged result.

That said, for typical multi-selection use cases (disjoint Ctrl+click regions), this implementation should work correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheet/src/model/core/coordinates.ts` around lines 444 - 462,
mergeOverlapping can miss transitive overlaps because sorting by top-left and
one-pass merging (using isIntersect/isAdjacent + mergeRanges) doesn't guarantee
that after merging two ranges the result will be compared with earlier/skipped
ranges; change mergeOverlapping to compute transitive unions: either (A) run the
merge pass repeatedly until no changes (keep using sorted, cloneRange,
isIntersect/isAdjacent, mergeRanges) or (B) build connectivity by checking all
pairs and unioning overlapping/adjacent ranges (union-find), then for each
connected component merge its member ranges with mergeRanges; use the existing
helpers (isIntersect, isAdjacent, mergeRanges, cloneRange) to form group merges
so all transitive merges are captured.
packages/sheet/src/view/overlay.ts (1)

454-504: Extract the per-range painter before these paths drift.

The row/column/cell drawing branches are now duplicated in both the simple and frozen renderers. A small helper that paints one range for a given scroll context would keep future selection-style changes from diverging between the two paths.

As per coding guidelines, "Keep functions small and focused on a single responsibility".

Also applies to: 644-707

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheet/src/view/overlay.ts` around lines 454 - 504, The selection
rendering logic inside renderSelectionsSimple duplicates per-range painting for
'row', 'column', and cell cases and should be extracted into a small helper
(e.g., paintRangeForScroll or paintSelectionRange) that takes (ctx, range,
scroll, selectionType, rowDim, colDim, port) and uses the existing helpers
toBoundingRect and expandBoundingRect and theme colors from getThemeColor; move
the fillStyle/strokeStyle/lineWidth + fillRect/strokeRect calls for the three
branches into that helper and replace each per-range block in
renderSelectionsSimple (and the similar code in the frozen renderer around lines
644-707) with a single call to the new helper to avoid duplication and keep
functions focused.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/sheet/src/view/worksheet.ts`:
- Around line 2892-2922: Right-click/context menu logic only checks the active
range (this.sheet!.getRangeOrActiveCell()) and treats Ctrl+click the same as Cmd
on macOS, so right-clicking an already-selected secondary range can replace
selection; change the early-return to test whether clickedRef (from
toRefFromMouse) lies inside any existing selection range (iterate the sheet's
selection collection, e.g. this.sheet!.getSelections() / selection ranges) and
return if so, and adjust the multi-select detection so macOS uses only metaKey
for additive clicks (use platform check to make isMultiSelect true only for
e.metaKey on macOS, otherwise e.ctrlKey || e.metaKey). Ensure the rest of the
flow (addSelection/selectStart and addSelectionEnd/selectEnd) remains unchanged.

---

Nitpick comments:
In `@packages/sheet/src/model/core/coordinates.ts`:
- Around line 444-462: mergeOverlapping can miss transitive overlaps because
sorting by top-left and one-pass merging (using isIntersect/isAdjacent +
mergeRanges) doesn't guarantee that after merging two ranges the result will be
compared with earlier/skipped ranges; change mergeOverlapping to compute
transitive unions: either (A) run the merge pass repeatedly until no changes
(keep using sorted, cloneRange, isIntersect/isAdjacent, mergeRanges) or (B)
build connectivity by checking all pairs and unioning overlapping/adjacent
ranges (union-find), then for each connected component merge its member ranges
with mergeRanges; use the existing helpers (isIntersect, isAdjacent,
mergeRanges, cloneRange) to form group merges so all transitive merges are
captured.

In `@packages/sheet/src/view/overlay.ts`:
- Around line 454-504: The selection rendering logic inside
renderSelectionsSimple duplicates per-range painting for 'row', 'column', and
cell cases and should be extracted into a small helper (e.g.,
paintRangeForScroll or paintSelectionRange) that takes (ctx, range, scroll,
selectionType, rowDim, colDim, port) and uses the existing helpers
toBoundingRect and expandBoundingRect and theme colors from getThemeColor; move
the fillStyle/strokeStyle/lineWidth + fillRect/strokeRect calls for the three
branches into that helper and replace each per-range block in
renderSelectionsSimple (and the similar code in the frozen renderer around lines
644-707) with a single call to the new helper to avoid duplication and keep
functions focused.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a54f1d73-fddf-4807-b948-e1abb943ce6f

📥 Commits

Reviewing files that changed from the base of the PR and between 8b261eb and c226ce7.

📒 Files selected for processing (10)
  • docs/tasks/active/20260319-ranges-type-lessons.md
  • docs/tasks/active/20260319-ranges-type-todo.md
  • packages/sheet/src/model/core/coordinates.test.ts
  • packages/sheet/src/model/core/coordinates.ts
  • packages/sheet/src/model/worksheet/sheet.ts
  • packages/sheet/src/view/formulabar.ts
  • packages/sheet/src/view/overlay.ts
  • packages/sheet/src/view/spreadsheet.ts
  • packages/sheet/src/view/worksheet.ts
  • packages/sheet/test/sheet/sheet.test.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/tasks/active/20260319-ranges-type-lessons.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/tasks/active/20260319-ranges-type-todo.md
  • packages/sheet/src/model/core/coordinates.test.ts

@hackerwins hackerwins changed the title Add Ranges type for multi-range support Add Ranges type for multi-range selection support Mar 18, 2026
@hackerwins
hackerwins merged commit 8164f23 into main Mar 18, 2026
3 checks passed
@hackerwins
hackerwins deleted the feat/ranges-type branch March 18, 2026 23:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant