source dump of claude code
at main 93 lines 3.3 kB view raw
1import { 2 CellWidth, 3 cellAtIndex, 4 type Screen, 5 type StylePool, 6 setCellStyleId, 7} from './screen.js' 8 9/** 10 * Highlight all visible occurrences of `query` in the screen buffer by 11 * inverting cell styles (SGR 7). Post-render, same damage-tracking machinery 12 * as applySelectionOverlay — the diff picks up highlighted cells as ordinary 13 * changes, LogUpdate stays a pure diff engine. 14 * 15 * Case-insensitive. Handles wide characters (CJK, emoji) by building a 16 * col-of-char map per row — the Nth character isn't at col N when wide chars 17 * are present (each occupies 2 cells: head + SpacerTail). 18 * 19 * This ONLY inverts — there is no "current match" logic here. The yellow 20 * current-match overlay is handled separately by applyPositionedHighlight 21 * (render-to-screen.ts), which writes on top using positions scanned from 22 * the target message's DOM subtree. 23 * 24 * Returns true if any match was highlighted (damage gate — caller forces 25 * full-frame damage when true). 26 */ 27export function applySearchHighlight( 28 screen: Screen, 29 query: string, 30 stylePool: StylePool, 31): boolean { 32 if (!query) return false 33 const lq = query.toLowerCase() 34 const qlen = lq.length 35 const w = screen.width 36 const noSelect = screen.noSelect 37 const height = screen.height 38 39 let applied = false 40 for (let row = 0; row < height; row++) { 41 const rowOff = row * w 42 // Build row text (already lowercased) + code-unit→cell-index map. 43 // Three skip conditions, all aligned with setCellStyleId / 44 // extractRowText (selection.ts): 45 // - SpacerTail: 2nd cell of a wide char, no char of its own 46 // - SpacerHead: end-of-line padding when a wide char wraps 47 // - noSelect: gutters (⎿, line numbers) — same exclusion as 48 // applySelectionOverlay. "Highlight what you see" still holds for 49 // content; gutters aren't search targets. 50 // Lowercasing per-char (not on the joined string at the end) means 51 // codeUnitToCell maps positions in the LOWERCASED text — U+0130 52 // (Turkish İ) lowercases to 2 code units, so lowering the joined 53 // string would desync indexOf positions from the map. 54 let text = '' 55 const colOf: number[] = [] 56 const codeUnitToCell: number[] = [] 57 for (let col = 0; col < w; col++) { 58 const idx = rowOff + col 59 const cell = cellAtIndex(screen, idx) 60 if ( 61 cell.width === CellWidth.SpacerTail || 62 cell.width === CellWidth.SpacerHead || 63 noSelect[idx] === 1 64 ) { 65 continue 66 } 67 const lc = cell.char.toLowerCase() 68 const cellIdx = colOf.length 69 for (let i = 0; i < lc.length; i++) { 70 codeUnitToCell.push(cellIdx) 71 } 72 text += lc 73 colOf.push(col) 74 } 75 76 let pos = text.indexOf(lq) 77 while (pos >= 0) { 78 applied = true 79 const startCi = codeUnitToCell[pos]! 80 const endCi = codeUnitToCell[pos + qlen - 1]! 81 for (let ci = startCi; ci <= endCi; ci++) { 82 const col = colOf[ci]! 83 const cell = cellAtIndex(screen, rowOff + col) 84 setCellStyleId(screen, col, row, stylePool.withInverse(cell.styleId)) 85 } 86 // Non-overlapping advance (less/vim/grep/Ctrl+F). pos+1 would find 87 // 'aa' at 0 AND 1 in 'aaa' → double-invert cell 1. 88 pos = text.indexOf(lq, pos + qlen) 89 } 90 } 91 92 return applied 93}