94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
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
|
|
}
|