Skip to content

feat(desktop): fuzzy emoji shortcode autocomplete#1945

Open
torstenrudolf wants to merge 1 commit into
block:mainfrom
torstenrudolf:trudolf/emoji-fuzzy-search
Open

feat(desktop): fuzzy emoji shortcode autocomplete#1945
torstenrudolf wants to merge 1 commit into
block:mainfrom
torstenrudolf:trudolf/emoji-fuzzy-search

Conversation

@torstenrudolf

@torstenrudolf torstenrudolf commented Jul 16, 2026

Copy link
Copy Markdown

What

Make the inline :shortcode emoji autocomplete match fuzzily, so a query that omits separators (or characters) still finds the emoji. pointup now finds :point_up:; pntup finds it too.

Why

Two limitations stacked up:

  • Custom emoji matched via shortcode.includes(query) — a literal substring, so the _ separator blocked pointuppoint_up.
  • Standard emoji go through emoji-mart's SearchIndex.search, which is token-prefix based: it splits point_up into point/up and matches a query as a token prefix, so the single token pointup matched neither.

How

New shared matcher desktop/src/shared/lib/emojiSearch.ts with tiered, separator-insensitive ranking:

  1. exact → 2. prefix → 3. substring → 4. subsequence (chars in order with gaps)

Both sides are normalized by stripping :, _, -, and whitespace. Tiers 1–3 keep normal queries ordered exactly as before; the subsequence tier only adds looser matches and always ranks last, so recall improves without noisy reordering.

Wiring in useEmojiAutocomplete.ts:

  • Custom emoji now rank through the shared matcher.
  • Standard results keep emoji-mart's search as primary, then top up only the leftover slots (up to the existing cap of 8) with fuzzy matches emoji-mart missed, deduped against what's already shown.

The full emoji set (~1.8k) is scanned per keystroke, behind the existing 120 ms debounce — sub-millisecond, and the flat index is built once at module level.

Scope: composer autocomplete only. The emoji picker popover routes through emoji-mart's own search and is unchanged (could follow up via a patch since both surfaces share SearchIndex).

Complexity

Per candidate shortcode, scoreShortcodeMatch tries the four tiers in order and short-circuits on the first hit. Let q = normalized query length, L = candidate shortcode length, N = number of candidates (~1.8k standard + workspace custom).

Tier Method Time (per candidate)
exact q === target O(min(q, L))
prefix target.startsWith(q) O(q)
substring target.indexOf(q) O(q·L) worst, ~O(L) typical
subsequence single linear scan (subsequenceSpan) O(L)

Per keystroke the matcher scans all N candidates, so worst case is O(N · q · L), dominated by the substring tier. Since q and L are tiny bounded values (shortcodes are short), this is effectively a linear O(N) scan — ~1.8k short-string comparisons, sub-millisecond behind the 120 ms debounce. Ranking the k matches adds an O(k log k) sort. Space is O(k) for the scored buffer plus the one-time O(N) flat index built at module load.

Testing

  • New unit tests (emojiSearch.test.mjs, 12 cases): tier classification, pointup/pntup/ointppoint_up, ranking order, limit handling, and fuzzyStandardEmoji against the real dataset.
  • pnpm typecheck and biome check pass on the changed files.

The inline :shortcode autocomplete matched only strict substrings, and
emoji-mart's own search is token-prefix based, so it can't cross the
`_` separator — `pointup` never found `point_up`.

Add a shared separator-insensitive matcher (emojiSearch.ts) with tiered
ranking: exact > prefix > substring > subsequence. Custom emoji now rank
through it, and standard-emoji results are topped up with fuzzy matches
emoji-mart missed, filling only leftover slots so normal queries are
unchanged. Subsequence hits rank last to keep recall gains low-noise.

Scoped to the composer autocomplete; the picker popover (which routes
through emoji-mart's own search) is unchanged.

Signed-off-by: Torsten Rudolf <trudolf@squareup.com>
@torstenrudolf
torstenrudolf requested a review from a team as a code owner July 16, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants