(delta: 1 | -1)
| 648 | // jump) triggers auto-advance from scan-effect. Wraparound guard stops |
| 649 | // if every message is a phantom. |
| 650 | function step(delta: 1 | -1): void { |
| 651 | const st = searchState.current; |
| 652 | const { |
| 653 | matches, |
| 654 | prefixSum |
| 655 | } = st; |
| 656 | const total = prefixSum.at(-1) ?? 0; |
| 657 | if (matches.length === 0) return; |
| 658 | |
| 659 | // Seek in-flight — queue this press (one-deep, latest overwrites). |
| 660 | // The seek effect fires it after highlight. |
| 661 | if (scanRequestRef.current) { |
| 662 | pendingStepRef.current = delta; |
| 663 | return; |
| 664 | } |
| 665 | if (startPtrRef.current < 0) startPtrRef.current = st.ptr; |
| 666 | const { |
| 667 | positions |
| 668 | } = elementPositions.current; |
| 669 | const newOrd = st.screenOrd + delta; |
| 670 | if (newOrd >= 0 && newOrd < positions.length) { |
| 671 | st.screenOrd = newOrd; |
| 672 | highlight(newOrd); // updates badge internally |
| 673 | startPtrRef.current = -1; |
| 674 | return; |
| 675 | } |
| 676 | |
| 677 | // Exhausted visible. Advance ptr → jump → re-scan. |
| 678 | const ptr = (st.ptr + delta + matches.length) % matches.length; |
| 679 | if (ptr === startPtrRef.current) { |
| 680 | setPositions?.(null); |
| 681 | startPtrRef.current = -1; |
| 682 | logForDebugging(`step: wraparound at ptr=${ptr}, all ${matches.length} msgs phantoms`); |
| 683 | return; |
| 684 | } |
| 685 | st.ptr = ptr; |
| 686 | st.screenOrd = 0; // resolved after scan (wantLast → length-1) |
| 687 | jump(matches[ptr]!, delta < 0); |
| 688 | // screenOrd will resolve after scan. Best-effort: prefixSum[ptr] + 0 |
| 689 | // for n (first pos), prefixSum[ptr+1] for N (last pos = count-1). |
| 690 | // The scan-effect's highlight will be the real value; this is a |
| 691 | // pre-scan placeholder so the badge updates immediately. |
| 692 | const placeholder = delta < 0 ? prefixSum[ptr + 1] ?? total : prefixSum[ptr]! + 1; |
| 693 | onSearchMatchesChange?.(total, placeholder); |
| 694 | } |
| 695 | stepRef.current = step; |
| 696 | useImperativeHandle(jumpRef, () => ({ |
| 697 | // Non-search jump (sticky header click, etc). No scan, no positions. |
no test coverage detected