Background
Noted in #362 as "worth handling in the same pass" and deliberately left out of #369 / #371 — it is a distinct defect with its own behaviour change, and folding it in would have hidden it inside a boundary-snapping diff.
Problem
generateHighlightedText skips a match region that covers a single character:
// src/runtime/utils/search.ts
indices.forEach((region) => {
// skip if region is a single character
if (region.length === 2 && region[0] === region[1]) {
return
}
region[0] === region[1] is true only when the region spans one UTF-16 code unit. An astral character occupies two, so its region is [i, i + 1] — the test is false and it is highlighted, while the equivalent BMP character is skipped.
'xZy', region [1, 1] → xZy — skipped, as intended
'x😀y', region [1, 2] → x<mark>😀</mark>y — highlighted, though it is also one character
Same inconsistency one level up: a single grapheme cluster — a flag, कि, an emoji with a skin-tone modifier — spans several code units and is likewise never skipped.
Why it matters
The rule exists to suppress noise: a one-character hit is rarely worth marking. Whether that noise appears currently depends on which plane the character is in, which is not a distinction anyone chose. It is most visible in ContentSearch over emoji-bearing content, where single-character matches are common.
Fix
Measure the region in the same unit the rest of the function now uses. createClusterSnapper (added in #371) already resolves the cluster straddling an offset; a region should be skipped when its snapped start and end bound exactly one cluster.
Note on scope
This is a behaviour change, not just a correctness fix — single astral characters that are highlighted today would stop being highlighted. That is the consistent behaviour, but it is worth stating in the changelog rather than shipping quietly, and it is why this is filed separately rather than carried in #371.
src/runtime/utils/search.ts is a ported file with three b24ui-only divergences already recorded in .sync/PORTING.md §2. A fix here adds a fourth; weigh that before starting, and read the invariant first.
Background
Noted in #362 as "worth handling in the same pass" and deliberately left out of #369 / #371 — it is a distinct defect with its own behaviour change, and folding it in would have hidden it inside a boundary-snapping diff.
Problem
generateHighlightedTextskips a match region that covers a single character:region[0] === region[1]is true only when the region spans one UTF-16 code unit. An astral character occupies two, so its region is[i, i + 1]— the test is false and it is highlighted, while the equivalent BMP character is skipped.Same inconsistency one level up: a single grapheme cluster — a flag,
कि, an emoji with a skin-tone modifier — spans several code units and is likewise never skipped.Why it matters
The rule exists to suppress noise: a one-character hit is rarely worth marking. Whether that noise appears currently depends on which plane the character is in, which is not a distinction anyone chose. It is most visible in
ContentSearchover emoji-bearing content, where single-character matches are common.Fix
Measure the region in the same unit the rest of the function now uses.
createClusterSnapper(added in #371) already resolves the cluster straddling an offset; a region should be skipped when its snappedstartandendbound exactly one cluster.Note on scope
This is a behaviour change, not just a correctness fix — single astral characters that are highlighted today would stop being highlighted. That is the consistent behaviour, but it is worth stating in the changelog rather than shipping quietly, and it is why this is filed separately rather than carried in #371.
src/runtime/utils/search.tsis a ported file with three b24ui-only divergences already recorded in.sync/PORTING.md§2. A fix here adds a fourth; weigh that before starting, and read the invariant first.