searchHighlight.ts
ink/searchHighlight.ts
94
Lines
3325
Bytes
1
Exports
1
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with ui-flow. It contains 94 lines, 1 detected imports, and 1 detected exports.
Important relationships
Detected exports
applySearchHighlight
Keywords
screencelltextcodeunittocellstylepoolquerywidenoselectcharcolof
Detected imports
./screen.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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
}