Skip to content

Smarter emoji completion: recents, synonyms, fuzzy matching, popularity#496

Merged
FuJacob merged 2 commits into
mainfrom
feat/emoji-completion-ranking
Jun 1, 2026
Merged

Smarter emoji completion: recents, synonyms, fuzzy matching, popularity#496
FuJacob merged 2 commits into
mainfrom
feat/emoji-completion-ranking

Conversation

@FuJacob
Copy link
Copy Markdown
Owner

@FuJacob FuJacob commented Jun 1, 2026

Summary

Makes the inline :emoji: picker rank like a modern chat composer instead of a raw substring search, without changing the trigger/commit UX. Five additions, all behind the existing picker:

  • Recents + frequency (EmojiUsageStore, EmojiUsageSnapshot): per-user usage persisted to UserDefaults, keyed by primary alias so it is stable across skin-tone/gender variants. A bare : now shows your recents (padded with popular emoji) instead of an empty panel, and personal favorites float to the top within a relevance tier.
  • Curated synonym/intent overlay (EmojiSynonymCatalog): 164 hand-mapped words people actually type (lol→😂, ty→🙏, fire/lit→🔥, congrats→🎉, omg, smh, ...) boost the canonical alias they mean.
  • Fuzzy fallback (in EmojiMatcher): when lexical results are sparse, a subsequence + optimal-string-alignment edit-distance pass catches typos (hapy→😄, recieve→...). Gated to queries ≥3 chars so it never pollutes good results.
  • Popularity prior (EmojiPopularity): a curated, dataset-validated ordering used as a within-tier tiebreak and as the bare-: starter set for new users.
  • Clear Emoji History control in General settings.

Relevance order is unchanged (exact alias > prefix > keyword/name > substring > fuzzy); recents/favorites/popularity only break ties, so a more relevant match is never demoted. Ranking stays a pure function of (query, catalog, usage snapshot).

Validation

swiftlint lint --strict --quiet        # exit 0

xcodebuild test -project Cotabby.xcodeproj -scheme Cotabby \
  -destination 'platform=macOS' \
  -skip-testing:CotabbyTests/FoundationModelDriftEvalTests CODE_SIGNING_ALLOWED=NO
# ** TEST SUCCEEDED **  Executed 663 tests, 3 skipped, 0 failures

New unit tests cover every pure helper (EmojiPopularityTests, EmojiSynonymCatalogTests, EmojiRecentsTests, ranking/fuzzy additions in EmojiCatalogMatcherTests), the store (EmojiUsageStoreTests, in-memory backed), and commit-time usage recording (EmojiPickerControllerTests). Popularity and synonym tables are validated against the bundled emoji.json so no entry is dead. Local Team ID caveat: app-hosted suites run with CODE_SIGNING_ALLOWED=NO.

Linked issues

Refs #393 (emoji & gif auto-completion/search). This advances the emoji half; GIF search is out of scope.

Risk / rollout notes

  • Behavior change: suggestion ranking now factors personal recents/frequency, curated synonyms, fuzzy typos, and a popularity prior, and a bare : shows recents. Conservative by construction (tie-break-only personalization; fuzzy gated to ≥3 chars).
  • New persisted key: cotabbyEmojiUsage in UserDefaults (recents + frequency). User-clearable via Settings → General → Clear Emoji History.
  • macOS 14 crash dodge: EmojiUsageStore declares a nonisolated deinit to avoid the isolated-deinit back-deployment shim (swift_task_deinitOnExecutorMainActorBackDeploy) that over-releases and aborts the app-hosted test runner for a @MainActor class with non-trivial stored properties.
  • pbxproj: regenerated with xcodegen generate for the new source/test files; no project.yml edit.
  • No engine, model, or prompt changes.

🤖 Generated with Claude Code

Greptile Summary

Upgrades the inline :emoji: picker from a plain substring search to a multi-tier ranked search: exact alias → alias prefix / synonym exact → keyword/name prefix / synonym prefix → alias substring → keyword/name substring → fuzzy (OSA + subsequence, ≥3 chars), with personal recents/frequency and a curated popularity prior breaking ties within each tier. A bare : now seeds the panel with the user's recent emoji instead of showing nothing.

  • EmojiMatcher gains synonym boosting (via EmojiSynonymCatalog), a fuzzy fallback gated to sparse results and ≥3-char queries, and a four-key sort comparator (tier → favorite bucket → token length → popularity rank → catalog order); relevance is never sacrificed to personalization.
  • EmojiUsageStore persists per-user recents and frequency in a single JSON blob under cotabbyEmojiUsage in UserDefaults; the nonisolated deinit dodges a macOS 14 isolated-deinit back-deploy crash that was deterministically reproduced in the app-hosted test runner.
  • EmojiCatalog gains an aliasIndex dictionary for O(1) alias → entry resolution, used by EmojiRecents to fill the bare-: panel without iterating the full catalog.

Confidence Score: 5/5

Safe to merge — the ranking changes are additive and tie-break-only, so no previously-relevant match is demoted, and all new code is fully unit-tested.

All new logic (OSA distance, subsequence, synonym catalog, popularity prior, usage store) is covered by dedicated unit tests with in-memory backends. The behavior change (bare : shows recents; personalization and popularity only break ties within a tier) is conservative by construction. The macOS 14 nonisolated deinit workaround is well-documented and correctly targeted. No correctness bugs or data-safety issues were found in the changed code paths.

No files require special attention.

Important Files Changed

Filename Overview
Cotabby/Support/EmojiMatcher.swift Substantially expanded to support six relevance tiers, synonym boosting, fuzzy fallback (OSA + subsequence), and a multi-key sort comparator; the OSA implementation is correct and the fuzzy gate (≥3 chars, sparse lexical results) is conservative.
Cotabby/Models/EmojiUsageStore.swift New @mainactor persistence class wrapping a JSON blob in UserDefaults; the nonisolated deinit workaround for the macOS 14 back-deploy crash is correctly explained and the EmojiUsageDefaults protocol enables hermetic unit testing.
Cotabby/Support/EmojiSynonymCatalog.swift 164-entry hand-curated intent map; boostedAliases correctly separates exact vs. prefix boosts and excludes exact aliases from the prefix set. The ≥2-char prefix gate prevents single-letter spray.
Cotabby/Support/EmojiPopularity.swift Curated popularity prior used as a late tiebreak and bare-: seed; rankByAlias is built once as a lazy static dictionary, first-occurrence-wins rule handles duplicate entries gracefully.
Cotabby/Support/EmojiRecents.swift Pure recents builder; takes limit×3 popularity aliases for padding so the panel fills even when stored aliases go stale, and resolves each through the aliasIndex for O(1) lookup.
Cotabby/App/Coordinators/EmojiPickerController.swift Adds emojiUsage/recordEmojiUsage closure dependencies and wires bare-: recents; recordUsage correctly keys on entry.aliases.first (base primary alias) for variant-stable signal.
Cotabby/Support/EmojiCatalog.swift Adds aliasIndex dictionary and entry(forAlias:) lookup for O(1) resolution of stored aliases; first-occurrence-wins on duplicate aliases is the correct policy.
CotabbyTests/EmojiUsageStoreTests.swift Comprehensive store tests using in-memory defaults; covers record ordering, normalization, cap eviction, clear, cross-instance persistence, and the isFavorite boundary conditions.
CotabbyTests/EmojiCatalogMatcherTests.swift New ranking tests verify synonym intent surfacing, fuzzy typo/transposition matching, exact-beats-fuzzy precedence, favorite floating within tier, popularity tiebreak, and bare-: recents ordering.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User types in picker] --> B{query empty?}
    B -->|yes| C[EmojiRecents.suggestions\nrecents → popularity pad]
    B -->|no| D[EmojiSynonymCatalog\nboostedAliases for query]
    D --> E[Lexical pass over catalog\nalias / keyword / name hits]
    E --> F{scored.count < limit\nAND query.count ≥ 3?}
    F -->|no| G[Sort: tier → favoriteBucket\n→ tokenLength → popularityRank\n→ catalogIndex]
    F -->|yes| H[Fuzzy pass on unmatched entries\nOSA distance + subsequence]
    H --> G
    G --> I[prefix limit results]
    I --> J[EmojiVariantResolver.resolve\napply skin-tone / gender prefs]
    J --> K[Panel shown to user]
    K --> L{User commits emoji}
    L --> M[recordEmojiUsage alias\nEmojiUsageStore.record]
    M --> N[persist to UserDefaults\ncotabbyEmojiUsage JSON blob]
Loading

Reviews (2): Last reviewed commit: "Address Greptile review: dead-code retur..." | Re-trigger Greptile

Make the inline :emoji: picker rank like a chat app instead of a raw
substring search.

- EmojiUsageStore: persisted recents + frequency keyed by primary alias
  (variant-stable). A bare ":" now shows recents (popularity-padded) instead of
  an empty panel; personal favorites float up within a relevance tier.
- EmojiSynonymCatalog: curated intent/slang -> alias map (lol, ty, fire,
  congrats, ...) so intent ranks first.
- EmojiMatcher: synonym pre-tier + fuzzy fallback (subsequence + OSA edit
  distance for typos) + popularity tiebreak, preserving existing relevance order.
- EmojiPopularity: curated popularity prior, validated against the dataset.
- Clear Emoji History control in General settings.

EmojiUsageStore uses a nonisolated deinit to avoid the macOS 14 isolated-deinit
back-deploy crash that aborts the app-hosted tests. New pure helpers are unit
tested; regenerated the Xcode project for the new files.
Comment thread Cotabby/Support/EmojiMatcher.swift
Comment thread Cotabby/App/Coordinators/EmojiPickerController.swift
…ge closures

- EmojiMatcher.isSubsequence: the trailing `return matched == needle.count` was
  always false once the loop exits (the in-loop guard is the only true path);
  replace it with an explicit `return false`.
- EmojiPickerController: annotate the emojiUsage/recordEmojiUsage closures as
  @mainactor so the isolation contract is explicit, since they read and mutate
  main-actor EmojiUsageStore state.
@FuJacob FuJacob merged commit adf9621 into main Jun 1, 2026
4 checks passed
@FuJacob FuJacob deleted the feat/emoji-completion-ranking branch June 1, 2026 06:37
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.

1 participant