Background
Follow-up from the review of #371. The fix there snaps highlight and truncation boundaries to grapheme clusters via Intl.Segmenter, guarded by a cheap screen so ordinary text never pays for segmentation.
The screen is a threshold:
// src/runtime/utils/search.ts
const CLUSTER_CONTINUATION_FLOOR = 0x0300
Nothing below U+0300 can continue a cluster, so a boundary between two such characters is a cluster boundary by construction and the segmenter is skipped.
Problem
Cyrillic is U+0430. So is every other non-Latin script — CJK, Devanagari, Hebrew, Greek. All of them sit above the floor, so the screen never fires and every boundary goes through Segments.containing().
For a product localised into Russian, the screen therefore covers markup and Latin identifiers, not the body text users actually search.
Measured at 979 characters, against the pre-fix cost (isolated processes, median of nine):
| content |
cost |
| ASCII |
+1% |
| Cyrillic |
+2.9 µs |
| CJK |
+2.5 µs |
Not a regression anyone will notice at resultLimit: 12 — roughly 0.1 ms per keystroke — but it is the one case the optimisation was written for and does not serve. test/bench/search.bench.ts has a cyrillic case pinning it.
Direction
Replace the threshold with a test for what a character can actually do: a boundary is safe unless the character at it can continue a cluster (\p{M}, ZWJ, variation selectors, emoji modifiers, regional indicators, a low surrogate) or the one before it is Prepend, ZWJ, or CR before LF.
A prototype of that screen measured 0.072 µs against 2.69 µs for the segmenter — a 37× saving on Cyrillic — and gave zero unsound answers over 81 boundaries across 18 fixtures (Cyrillic, Latin with diacritics, CJK, Hangul, Devanagari, flags, ZWJ families, skin-tone modifiers, keycaps, CRLF, Arabic prepend).
Why this is filed rather than done
81 boundaries is far too thin to justify a soundness claim. The screen must never answer "safe" where UAX #29 says there is no boundary; one false "safe" silently reintroduces #364, and the failure mode is a plausible-looking wrong character that nobody reports.
Anyone taking this on should first build an exhaustive differential harness against Intl.Segmenter — every code unit pair in the affected ranges, plus randomised multi-code-point sequences — and treat a single disagreement as disqualifying. The prototype above is a starting point, not a candidate.
Alternative worth considering first
Cache the segmenter view across the three highlight() calls a single item makes (label, suffix, description operate on different values, but a group's items are processed together). Cheaper to get right than a hand-rolled Unicode predicate, though it saves less.
Note
src/runtime/utils/search.ts is a ported file — see the .sync/PORTING.md §2 invariant before touching it.
Background
Follow-up from the review of #371. The fix there snaps highlight and truncation boundaries to grapheme clusters via
Intl.Segmenter, guarded by a cheap screen so ordinary text never pays for segmentation.The screen is a threshold:
Nothing below U+0300 can continue a cluster, so a boundary between two such characters is a cluster boundary by construction and the segmenter is skipped.
Problem
Cyrillic is U+0430. So is every other non-Latin script — CJK, Devanagari, Hebrew, Greek. All of them sit above the floor, so the screen never fires and every boundary goes through
Segments.containing().For a product localised into Russian, the screen therefore covers markup and Latin identifiers, not the body text users actually search.
Measured at 979 characters, against the pre-fix cost (isolated processes, median of nine):
Not a regression anyone will notice at
resultLimit: 12— roughly 0.1 ms per keystroke — but it is the one case the optimisation was written for and does not serve.test/bench/search.bench.tshas acyrilliccase pinning it.Direction
Replace the threshold with a test for what a character can actually do: a boundary is safe unless the character at it can continue a cluster (
\p{M}, ZWJ, variation selectors, emoji modifiers, regional indicators, a low surrogate) or the one before it is Prepend, ZWJ, or CR before LF.A prototype of that screen measured 0.072 µs against 2.69 µs for the segmenter — a 37× saving on Cyrillic — and gave zero unsound answers over 81 boundaries across 18 fixtures (Cyrillic, Latin with diacritics, CJK, Hangul, Devanagari, flags, ZWJ families, skin-tone modifiers, keycaps, CRLF, Arabic prepend).
Why this is filed rather than done
81 boundaries is far too thin to justify a soundness claim. The screen must never answer "safe" where UAX #29 says there is no boundary; one false "safe" silently reintroduces #364, and the failure mode is a plausible-looking wrong character that nobody reports.
Anyone taking this on should first build an exhaustive differential harness against
Intl.Segmenter— every code unit pair in the affected ranges, plus randomised multi-code-point sequences — and treat a single disagreement as disqualifying. The prototype above is a starting point, not a candidate.Alternative worth considering first
Cache the segmenter view across the three
highlight()calls a single item makes (label, suffix, description operate on different values, but a group's items are processed together). Cheaper to get right than a hand-rolled Unicode predicate, though it saves less.Note
src/runtime/utils/search.tsis a ported file — see the.sync/PORTING.md§2 invariant before touching it.