( s: SelectionState, dRow: number, minRow: number, maxRow: number, )
| 623 | * notifySelectionChange (recursion), must fire listeners directly. |
| 624 | */ |
| 625 | export function shiftSelectionForFollow( |
| 626 | s: SelectionState, |
| 627 | dRow: number, |
| 628 | minRow: number, |
| 629 | maxRow: number, |
| 630 | ): boolean { |
| 631 | if (!s.anchor) return false |
| 632 | // Mirror shiftSelection: compute raw (unclamped) positions from virtual |
| 633 | // if set, else current. This handles BOTH the update path (virtual already |
| 634 | // set from a prior keyboard scroll) AND the initialize path (first clamp |
| 635 | // happens HERE via follow-scroll, no prior keyboard scroll). Without the |
| 636 | // initialize path, follow-scroll-first leaves virtual undefined even |
| 637 | // though the clamp below occurred → a later PgUp computes debt from the |
| 638 | // clamped row instead of the true pre-clamp row and never pops the |
| 639 | // accumulator — getSelectedText double-counts the off-screen rows. |
| 640 | const rawAnchor = (s.virtualAnchorRow ?? s.anchor.row) + dRow |
| 641 | const rawFocus = s.focus |
| 642 | ? (s.virtualFocusRow ?? s.focus.row) + dRow |
| 643 | : undefined |
| 644 | if (rawAnchor < minRow && rawFocus !== undefined && rawFocus < minRow) { |
| 645 | clearSelection(s) |
| 646 | return true |
| 647 | } |
| 648 | // Clamp from raw, not p.row+dRow — so a virtual position coming back |
| 649 | // in-bounds lands at the TRUE position, not the stale clamped one. |
| 650 | s.anchor = { col: s.anchor.col, row: clamp(rawAnchor, minRow, maxRow) } |
| 651 | if (s.focus && rawFocus !== undefined) { |
| 652 | s.focus = { col: s.focus.col, row: clamp(rawFocus, minRow, maxRow) } |
| 653 | } |
| 654 | s.virtualAnchorRow = |
| 655 | rawAnchor < minRow || rawAnchor > maxRow ? rawAnchor : undefined |
| 656 | s.virtualFocusRow = |
| 657 | rawFocus !== undefined && (rawFocus < minRow || rawFocus > maxRow) |
| 658 | ? rawFocus |
| 659 | : undefined |
| 660 | // anchorSpan not virtual-tracked (word/line extend, irrelevant to |
| 661 | // keyboard-scroll round-trip) — plain clamp from current row. |
| 662 | if (s.anchorSpan) { |
| 663 | const shift = (p: Point): Point => ({ |
| 664 | col: p.col, |
| 665 | row: clamp(p.row + dRow, minRow, maxRow), |
| 666 | }) |
| 667 | s.anchorSpan = { |
| 668 | lo: shift(s.anchorSpan.lo), |
| 669 | hi: shift(s.anchorSpan.hi), |
| 670 | kind: s.anchorSpan.kind, |
| 671 | } |
| 672 | } |
| 673 | return false |
| 674 | } |
| 675 | |
| 676 | export function hasSelection(s: SelectionState): boolean { |
| 677 | return s.anchor !== null && s.focus !== null |
no test coverage detected