* Find the bounds of the same-class character run at (col, row). Returns * null if the click is out of bounds or lands on a noSelect cell. Used by * selectWordAt (initial double-click) and extendWordSelection (drag).
( screen: Screen, col: number, row: number, )
| 160 | * selectWordAt (initial double-click) and extendWordSelection (drag). |
| 161 | */ |
| 162 | function wordBoundsAt( |
| 163 | screen: Screen, |
| 164 | col: number, |
| 165 | row: number, |
| 166 | ): { lo: number; hi: number } | null { |
| 167 | if (row < 0 || row >= screen.height) return null |
| 168 | const width = screen.width |
| 169 | const noSelect = screen.noSelect |
| 170 | const rowOff = row * width |
| 171 | |
| 172 | // If the click landed on the spacer tail of a wide char, step back to |
| 173 | // the head so the class check sees the actual grapheme. |
| 174 | let c = col |
| 175 | if (c > 0) { |
| 176 | const cell = cellAt(screen, c, row) |
| 177 | if (cell && cell.width === CellWidth.SpacerTail) c -= 1 |
| 178 | } |
| 179 | if (c < 0 || c >= width || noSelect[rowOff + c] === 1) return null |
| 180 | |
| 181 | const startCell = cellAt(screen, c, row) |
| 182 | if (!startCell) return null |
| 183 | const cls = charClass(startCell.char) |
| 184 | |
| 185 | // Expand left: include cells of the same class, stop at noSelect or |
| 186 | // class change. SpacerTail cells are stepped over (the wide-char head |
| 187 | // at the preceding column determines the class). |
| 188 | let lo = c |
| 189 | while (lo > 0) { |
| 190 | const prev = lo - 1 |
| 191 | if (noSelect[rowOff + prev] === 1) break |
| 192 | const pc = cellAt(screen, prev, row) |
| 193 | if (!pc) break |
| 194 | if (pc.width === CellWidth.SpacerTail) { |
| 195 | // Step over the spacer to the wide-char head |
| 196 | if (prev === 0 || noSelect[rowOff + prev - 1] === 1) break |
| 197 | const head = cellAt(screen, prev - 1, row) |
| 198 | if (!head || charClass(head.char) !== cls) break |
| 199 | lo = prev - 1 |
| 200 | continue |
| 201 | } |
| 202 | if (charClass(pc.char) !== cls) break |
| 203 | lo = prev |
| 204 | } |
| 205 | |
| 206 | // Expand right: same logic, skipping spacer tails. |
| 207 | let hi = c |
| 208 | while (hi < width - 1) { |
| 209 | const next = hi + 1 |
| 210 | if (noSelect[rowOff + next] === 1) break |
| 211 | const nc = cellAt(screen, next, row) |
| 212 | if (!nc) break |
| 213 | if (nc.width === CellWidth.SpacerTail) { |
| 214 | // Include the spacer tail in the selection range (it belongs to |
| 215 | // the wide char at hi) and continue past it. |
| 216 | hi = next |
| 217 | continue |
| 218 | } |
| 219 | if (charClass(nc.char) !== cls) break |
no test coverage detected