MCPcopy Create free account
hub / github.com/codeaashu/claude-code / useDragToScroll

Function useDragToScroll

src/components/ScrollKeybindingHandler.tsx:637–792  ·  view source on GitHub ↗

* Auto-scroll the ScrollBox when the user drags a selection past its top or * bottom edge. The anchor is shifted in the opposite direction so it stays * on the same content (content that was at viewport row N is now at row N±d * after scrolling by d). Focus stays at the mouse position (edge row).

(scrollRef: RefObject<ScrollBoxHandle | null>, selection: ReturnType<typeof useSelection>, isActive: boolean, onScroll: Props['onScroll'])

Source from the content-addressed store, hash-verified

635 * getSelectedText.
636 */
637function useDragToScroll(scrollRef: RefObject<ScrollBoxHandle | null>, selection: ReturnType<typeof useSelection>, isActive: boolean, onScroll: Props['onScroll']): void {
638 const timerRef = useRef<NodeJS.Timeout | null>(null);
639 const dirRef = useRef<-1 | 0 | 1>(0); // -1 scrolling up, +1 down, 0 idle
640 // Survives stop() — reset only on drag-finish. See check() for semantics.
641 const lastScrolledDirRef = useRef<-1 | 0 | 1>(0);
642 const ticksRef = useRef(0);
643 // onScroll may change identity every render (if not memoized by caller).
644 // Read through a ref so the effect doesn't re-subscribe and kill the timer
645 // on each scroll-induced re-render.
646 const onScrollRef = useRef(onScroll);
647 onScrollRef.current = onScroll;
648 useEffect(() => {
649 if (!isActive) return;
650 function stop(): void {
651 dirRef.current = 0;
652 if (timerRef.current) {
653 clearInterval(timerRef.current);
654 timerRef.current = null;
655 }
656 }
657 function tick(): void {
658 const sel = selection.getState();
659 const s = scrollRef.current;
660 const dir = dirRef.current;
661 // dir === 0 defends against a stale interval (start() may have set one
662 // after the immediate tick already called stop() at a scroll boundary).
663 // ticks cap defends against a lost release event (mouse released
664 // outside terminal window) leaving isDragging stuck true.
665 if (!sel?.isDragging || !sel.focus || !s || dir === 0 || ++ticksRef.current > AUTOSCROLL_MAX_TICKS) {
666 stop();
667 return;
668 }
669 // scrollBy accumulates into pendingScrollDelta; the screen buffer
670 // doesn't update until the next render drains it. If a previous
671 // tick's scroll hasn't drained yet, captureScrolledRows would read
672 // stale content (same rows as last tick → duplicated in the
673 // accumulator AND missing the rows that actually scrolled out).
674 // Skip this tick; the 50ms interval will retry after Ink's 16ms
675 // render catches up. Also prevents shiftAnchor from desyncing.
676 if (s.getPendingDelta() !== 0) return;
677 const top = s.getViewportTop();
678 const bottom = top + s.getViewportHeight() - 1;
679 // Clamp anchor within [top, bottom]. Not [0, bottom]: the ScrollBox
680 // padding row at 0 would produce a blank line between scrolledOffAbove
681 // and the on-screen content in getSelectedText. The padding-row
682 // highlight was a minor visual nicety; text correctness wins.
683 if (dir < 0) {
684 if (s.getScrollTop() <= 0) {
685 stop();
686 return;
687 }
688 // Scrolling up: content moves down in viewport, so anchor row +N.
689 // Clamp to actual scroll distance so anchor stays in sync when near
690 // the top boundary (renderer clamps scrollTop to 0 on drain).
691 const actual = Math.min(AUTOSCROLL_LINES, s.getScrollTop());
692 // Capture rows about to scroll out the BOTTOM before scrollBy
693 // overwrites them. Only rows inside the selection are captured
694 // (captureScrolledRows intersects with selection bounds).

Callers 1

ScrollKeybindingHandlerFunction · 0.85

Calls 1

stopFunction · 0.70

Tested by

no test coverage detected