( s: SelectionState, dRow: number, minRow: number, maxRow: number, )
| 571 | * must follow it. Focus is left unchanged (it stays at the mouse position). |
| 572 | */ |
| 573 | export function shiftAnchor( |
| 574 | s: SelectionState, |
| 575 | dRow: number, |
| 576 | minRow: number, |
| 577 | maxRow: number, |
| 578 | ): void { |
| 579 | if (!s.anchor) return |
| 580 | // Same virtual-row tracking as shiftSelection/shiftSelectionForFollow: the |
| 581 | // drag→follow transition hands off to shiftSelectionForFollow, which reads |
| 582 | // (virtualAnchorRow ?? anchor.row). Without this, drag-phase clamping |
| 583 | // leaves virtual undefined → follow initializes from the already-clamped |
| 584 | // row, under-counting total drift → shiftSelection's invariant-restore |
| 585 | // prematurely clears valid drag-phase accumulator entries. |
| 586 | const raw = (s.virtualAnchorRow ?? s.anchor.row) + dRow |
| 587 | s.anchor = { col: s.anchor.col, row: clamp(raw, minRow, maxRow) } |
| 588 | s.virtualAnchorRow = raw < minRow || raw > maxRow ? raw : undefined |
| 589 | // anchorSpan not virtual-tracked (word/line extend, irrelevant to |
| 590 | // keyboard-scroll round-trip) — plain clamp from current row. |
| 591 | if (s.anchorSpan) { |
| 592 | const shift = (p: Point): Point => ({ |
| 593 | col: p.col, |
| 594 | row: clamp(p.row + dRow, minRow, maxRow), |
| 595 | }) |
| 596 | s.anchorSpan = { |
| 597 | lo: shift(s.anchorSpan.lo), |
| 598 | hi: shift(s.anchorSpan.hi), |
| 599 | kind: s.anchorSpan.kind, |
| 600 | } |
| 601 | } |
| 602 | } |
| 603 | |
| 604 | /** |
| 605 | * Shift the whole selection (anchor + focus + anchorSpan) by dRow, clamped |
no test coverage detected