/ ink / hooks / use-search-highlight.ts
use-search-highlight.ts
 1  import { useContext, useMemo } from 'react'
 2  import StdinContext from '../components/StdinContext.js'
 3  import type { DOMElement } from '../dom.js'
 4  import instances from '../instances.js'
 5  import type { MatchPosition } from '../render-to-screen.js'
 6  
 7  /**
 8   * Set the search highlight query on the Ink instance. Non-empty → all
 9   * visible occurrences are inverted on the next frame (SGR 7, screen-buffer
10   * overlay, same damage machinery as selection). Empty → clears.
11   *
12   * This is a screen-space highlight — it matches the RENDERED text, not the
13   * source message text. Works for anything visible (bash output, file paths,
14   * error messages) regardless of where it came from in the message tree. A
15   * query that matched in source but got truncated/ellipsized in rendering
16   * won't highlight; that's acceptable — we highlight what you see.
17   */
18  export function useSearchHighlight(): {
19    setQuery: (query: string) => void
20    /** Paint an existing DOM subtree (from the MAIN tree) to a fresh
21     *  Screen at its natural height, scan. Element-relative positions
22     *  (row 0 = element top). Zero context duplication — the element
23     *  IS the one built with all real providers. */
24    scanElement: (el: DOMElement) => MatchPosition[]
25    /** Position-based CURRENT highlight. Every frame writes yellow at
26     *  positions[currentIdx] + rowOffset. The scan-highlight (inverse on
27     *  all matches) still runs — this overlays on top. rowOffset tracks
28     *  scroll; positions stay stable (message-relative). null clears. */
29    setPositions: (
30      state: {
31        positions: MatchPosition[]
32        rowOffset: number
33        currentIdx: number
34      } | null,
35    ) => void
36  } {
37    useContext(StdinContext) // anchor to App subtree for hook rules
38    const ink = instances.get(process.stdout)
39    return useMemo(() => {
40      if (!ink) {
41        return {
42          setQuery: () => {},
43          scanElement: () => [],
44          setPositions: () => {},
45        }
46      }
47      return {
48        setQuery: (query: string) => ink.setSearchHighlight(query),
49        scanElement: (el: DOMElement) => ink.scanElementSubtree(el),
50        setPositions: state => ink.setSearchPositions(state),
51      }
52    }, [ink])
53  }