π File detail
ink/searchHighlight.ts
π§© .tsπ 94 linesπΎ 3,325 bytesπ text
β Back to All Filesπ― Use case
This file lives under βink/β, which covers Ink terminal UI (layouts, TTY IO, keyboard, renderer components). On the API surface it exposes applySearchHighlight β mainly functions, hooks, or classes. It composes internal code from screen (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { CellWidth, cellAtIndex, type Screen, type StylePool,
π€ Exports (heuristic)
applySearchHighlight
π₯οΈ Source preview
import {
CellWidth,
cellAtIndex,
type Screen,
type StylePool,
setCellStyleId,
} from './screen.js'
/**
* Highlight all visible occurrences of `query` in the screen buffer by
* inverting cell styles (SGR 7). Post-render, same damage-tracking machinery
* as applySelectionOverlay β the diff picks up highlighted cells as ordinary
* changes, LogUpdate stays a pure diff engine.
*
* Case-insensitive. Handles wide characters (CJK, emoji) by building a
* col-of-char map per row β the Nth character isn't at col N when wide chars
* are present (each occupies 2 cells: head + SpacerTail).
*
* This ONLY inverts β there is no "current match" logic here. The yellow
* current-match overlay is handled separately by applyPositionedHighlight
* (render-to-screen.ts), which writes on top using positions scanned from
* the target message's DOM subtree.
*
* Returns true if any match was highlighted (damage gate β caller forces
* full-frame damage when true).
*/
export function applySearchHighlight(
screen: Screen,
query: string,
stylePool: StylePool,
): boolean {
if (!query) return false
const lq = query.toLowerCase()
const qlen = lq.length
const w = screen.width
const noSelect = screen.noSelect
const height = screen.height
let applied = false
for (let row = 0; row < height; row++) {
const rowOff = row * w
// Build row text (already lowercased) + code-unitβcell-index map.
// Three skip conditions, all aligned with setCellStyleId /
// extractRowText (selection.ts):
// - SpacerTail: 2nd cell of a wide char, no char of its own
// - SpacerHead: end-of-line padding when a wide char wraps
// - noSelect: gutters (βΏ, line numbers) β same exclusion as
// applySelectionOverlay. "Highlight what you see" still holds for
// content; gutters aren't search targets.
// Lowercasing per-char (not on the joined string at the end) means
// codeUnitToCell maps positions in the LOWERCASED text β U+0130
// (Turkish Δ°) lowercases to 2 code units, so lowering the joined
// string would desync indexOf positions from the map.
let text = ''
const colOf: number[] = []
const codeUnitToCell: number[] = []
for (let col = 0; col < w; col++) {
const idx = rowOff + col
const cell = cellAtIndex(screen, idx)
if (
cell.width === CellWidth.SpacerTail ||
cell.width === CellWidth.SpacerHead ||
noSelect[idx] === 1
) {
continue
}
const lc = cell.char.toLowerCase()
const cellIdx = colOf.length
for (let i = 0; i < lc.length; i++) {
codeUnitToCell.push(cellIdx)
}
text += lc
colOf.push(col)
}
let pos = text.indexOf(lq)
while (pos >= 0) {
applied = true
const startCi = codeUnitToCell[pos]!
const endCi = codeUnitToCell[pos + qlen - 1]!
for (let ci = startCi; ci <= endCi; ci++) {
const col = colOf[ci]!
const cell = cellAtIndex(screen, rowOff + col)
setCellStyleId(screen, col, row, stylePool.withInverse(cell.styleId))
}
// Non-overlapping advance (less/vim/grep/Ctrl+F). pos+1 would find
// 'aa' at 0 AND 1 in 'aaa' β double-invert cell 1.
pos = text.indexOf(lq, pos + qlen)
}
}
return applied
}