Skip to content

bug(search): the single-character skip in highlight() counts code units, so an emoji is treated as two #375

Description

@IgorShevchik

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions