(screen: Screen, query: string)
| 147 | * height, not viewport-clipped). Positions are stable — to highlight |
| 148 | * on the real screen, add the message's screen offset (lo). */ |
| 149 | export function scanPositions(screen: Screen, query: string): MatchPosition[] { |
| 150 | const lq = query.toLowerCase() |
| 151 | if (!lq) return [] |
| 152 | const qlen = lq.length |
| 153 | const w = screen.width |
| 154 | const h = screen.height |
| 155 | const noSelect = screen.noSelect |
| 156 | const positions: MatchPosition[] = [] |
| 157 | |
| 158 | const t0 = performance.now() |
| 159 | for (let row = 0; row < h; row++) { |
| 160 | const rowOff = row * w |
| 161 | // Same text-build as applySearchHighlight. Keep in sync — or extract |
| 162 | // to a shared helper (TODO once both are stable). codeUnitToCell |
| 163 | // maps indexOf positions (code units in the LOWERCASED text) to cell |
| 164 | // indices in colOf — surrogate pairs (emoji) and multi-unit lowercase |
| 165 | // (Turkish İ → i + U+0307) make text.length > colOf.length. |
| 166 | let text = '' |
| 167 | const colOf: number[] = [] |
| 168 | const codeUnitToCell: number[] = [] |
| 169 | for (let col = 0; col < w; col++) { |
| 170 | const idx = rowOff + col |
| 171 | const cell = cellAtIndex(screen, idx) |
| 172 | if ( |
| 173 | cell.width === CellWidth.SpacerTail || |
| 174 | cell.width === CellWidth.SpacerHead || |
| 175 | noSelect[idx] === 1 |
| 176 | ) { |
| 177 | continue |
| 178 | } |
| 179 | const lc = cell.char.toLowerCase() |
| 180 | const cellIdx = colOf.length |
| 181 | for (let i = 0; i < lc.length; i++) { |
| 182 | codeUnitToCell.push(cellIdx) |
| 183 | } |
| 184 | text += lc |
| 185 | colOf.push(col) |
| 186 | } |
| 187 | // Non-overlapping — same advance as applySearchHighlight. |
| 188 | let pos = text.indexOf(lq) |
| 189 | while (pos >= 0) { |
| 190 | const startCi = codeUnitToCell[pos]! |
| 191 | const endCi = codeUnitToCell[pos + qlen - 1]! |
| 192 | const col = colOf[startCi]! |
| 193 | const endCol = colOf[endCi]! + 1 |
| 194 | positions.push({ row, col, len: endCol - col }) |
| 195 | pos = text.indexOf(lq, pos + qlen) |
| 196 | } |
| 197 | } |
| 198 | timing.scan += performance.now() - t0 |
| 199 | |
| 200 | return positions |
| 201 | } |
| 202 | |
| 203 | /** Write CURRENT (yellow+bold+underline) at positions[currentIdx] + |
| 204 | * rowOffset. OTHER positions are NOT styled here — the scan-highlight |
no test coverage detected