(ord: number)
| 479 | // scrollTop fresh. If ord's position is off-viewport, scroll to bring |
| 480 | // it in, recompute rowOffset. setPositions triggers overlay write. |
| 481 | function highlight(ord: number): void { |
| 482 | const s = scrollRef.current; |
| 483 | const { |
| 484 | msgIdx, |
| 485 | positions |
| 486 | } = elementPositions.current; |
| 487 | if (!s || positions.length === 0 || msgIdx < 0) { |
| 488 | setPositions?.(null); |
| 489 | return; |
| 490 | } |
| 491 | const idx = Math.max(0, Math.min(ord, positions.length - 1)); |
| 492 | const p = positions[idx]!; |
| 493 | const top = jumpState.current.getItemTop(msgIdx); |
| 494 | // lo = item's position within scroll content (wrapper-relative). |
| 495 | // viewportTop = where the scroll content starts on SCREEN (after |
| 496 | // ScrollBox padding/border + any chrome above). Highlight writes to |
| 497 | // screen-absolute, so rowOffset = viewportTop + lo. Observed: off-by- |
| 498 | // 1+ without viewportTop (FullscreenLayout has paddingTop=1 on the |
| 499 | // ScrollBox, plus any header above). |
| 500 | const vpTop = s.getViewportTop(); |
| 501 | let lo = top - s.getScrollTop(); |
| 502 | const vp = s.getViewportHeight(); |
| 503 | let screenRow = vpTop + lo + p.row; |
| 504 | // Off viewport → scroll to bring it in (HEADROOM from top). |
| 505 | // scrollTo commits sync; read-back after gives fresh lo. |
| 506 | if (screenRow < vpTop || screenRow >= vpTop + vp) { |
| 507 | s.scrollTo(Math.max(0, top + p.row - HEADROOM)); |
| 508 | lo = top - s.getScrollTop(); |
| 509 | screenRow = vpTop + lo + p.row; |
| 510 | } |
| 511 | setPositions?.({ |
| 512 | positions, |
| 513 | rowOffset: vpTop + lo, |
| 514 | currentIdx: idx |
| 515 | }); |
| 516 | // Badge: global current = sum of occurrences before this msg + ord+1. |
| 517 | // prefixSum[ptr] is engine-counted (indexOf on extractSearchText); |
| 518 | // may drift from render-count for ghost messages but close enough — |
| 519 | // badge is a rough location hint, not a proof. |
| 520 | const st = searchState.current; |
| 521 | const total = st.prefixSum.at(-1) ?? 0; |
| 522 | const current = (st.prefixSum[st.ptr] ?? 0) + idx + 1; |
| 523 | onSearchMatchesChange?.(total, current); |
| 524 | logForDebugging(`highlight(i=${msgIdx}, ord=${idx}/${positions.length}): ` + `pos={row:${p.row},col:${p.col}} lo=${lo} screenRow=${screenRow} ` + `badge=${current}/${total}`); |
| 525 | } |
| 526 | highlightRef.current = highlight; |
| 527 | |
| 528 | // Seek effect. jump() sets scanRequestRef + scrollToIndex + bump. |
no test coverage detected