From 340faaa839921c85848a2dc1f27240ebbf8dc826 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:40:47 -0700 Subject: [PATCH] fix(workspaces): use the emojibase shortcode preset for emoji suggestions (#11888) Swap the worktree-name emoji picker from emojibase-data's `github` shortcode preset to `emojibase`, which carries both `flag_kr` and `south_korea` style flag names, and drop the hand-maintained `kr` entry that patched around the gap. Filter skin-tone aliases so they neither crowd the suggestion list nor clobber base-emoji branch names. Search now matches anywhere in the shortcode, ranked exact > prefix > word-start > substring, so `:korea` surfaces both Koreas. Emoji-derived branch names now prefer spelled-out aliases: flags use country names (japan, germany, south-korea) and cryptic stubs are skipped (thumbsdown over no, victory over v). --- src/main/ipc/worktree-logic.test.ts | 6 ++- .../lib/workspace-emoji-shortcodes.test.ts | 31 ++++++++++++-- .../src/lib/workspace-emoji-shortcodes.ts | 34 ++++++++------- src/shared/emoji-shortcode-catalog.ts | 42 ++++++++++++++----- tests/e2e/worktree.spec.ts | 8 ++-- 5 files changed, 86 insertions(+), 35 deletions(-) diff --git a/src/main/ipc/worktree-logic.test.ts b/src/main/ipc/worktree-logic.test.ts index 5a29189c9..ae76b28da 100644 --- a/src/main/ipc/worktree-logic.test.ts +++ b/src/main/ipc/worktree-logic.test.ts @@ -77,7 +77,8 @@ describe('sanitizeWorktreeName', () => { it('uses readable git-safe shortcodes for known emoji', () => { expect(sanitizeWorktreeName('🚀')).toBe('rocket') expect(sanitizeWorktreeName('👩‍💻✨')).toBe('woman-technologist-sparkles') - expect(sanitizeWorktreeName('🇯🇵')).toBe('jp') + expect(sanitizeWorktreeName('🇯🇵')).toBe('japan') + expect(sanitizeWorktreeName('👎')).toBe('thumbsdown') expect(sanitizeWorktreeName('1️⃣')).toBe('one') }) @@ -86,7 +87,8 @@ describe('sanitizeWorktreeName', () => { }) it('uses a git-safe fallback for emoji newer than the shortcode catalog', () => { - expect(sanitizeWorktreeName('\u{1fae9}')).toBe('workspace') + // Unassigned in Unicode 17, so no emojibase shortcode can cover it yet. + expect(sanitizeWorktreeName('\u{1faeb}')).toBe('workspace') }) it('does not treat arbitrary punctuation as a workspace name', () => { diff --git a/src/renderer/src/lib/workspace-emoji-shortcodes.test.ts b/src/renderer/src/lib/workspace-emoji-shortcodes.test.ts index 836637943..0f67df139 100644 --- a/src/renderer/src/lib/workspace-emoji-shortcodes.test.ts +++ b/src/renderer/src/lib/workspace-emoji-shortcodes.test.ts @@ -11,8 +11,14 @@ describe('workspace emoji shortcodes', () => { expect(searchWorkspaceEmojiShortcodes('wink', 1)).toEqual([{ emoji: '😉', shortcode: 'wink' }]) }) - it('finds the Korean flag by kr shortcode', () => { - expect(searchWorkspaceEmojiShortcodes('kr', 1)).toEqual([{ emoji: '🇰🇷', shortcode: 'kr' }]) + it('finds flags by country name and by ISO code fragment', () => { + expect(searchWorkspaceEmojiShortcodes('south_korea', 1)).toEqual([ + { emoji: '🇰🇷', shortcode: 'south_korea' } + ]) + expect(searchWorkspaceEmojiShortcodes('kr', 1)).toEqual([{ emoji: '🇰🇷', shortcode: 'flag_kr' }]) + expect(searchWorkspaceEmojiShortcodes('germany', 1)).toEqual([ + { emoji: '🇩🇪', shortcode: 'germany' } + ]) }) it('ranks an exact shortcode before longer aliases', () => { @@ -21,6 +27,23 @@ describe('workspace emoji shortcodes', () => { }) }) + it('matches inside a shortcode, ranking word starts above incidental substrings', () => { + expect(searchWorkspaceEmojiShortcodes('korea', 2)).toEqual([ + { emoji: '🇰🇵', shortcode: 'north_korea' }, + { emoji: '🇰🇷', shortcode: 'south_korea' } + ]) + expect( + searchWorkspaceEmojiShortcodes('kr', 3).map((suggestion) => suggestion.shortcode) + ).toEqual(['flag_kr', 'ukraine', 'cockroach']) + }) + + it('omits skin-tone aliases from suggestions', () => { + expect(searchWorkspaceEmojiShortcodes('wave')).toEqual([ + { emoji: '👋', shortcode: 'wave' }, + { emoji: '🌊', shortcode: 'water_wave' } + ]) + }) + it('deduplicates aliases that resolve to the same emoji', () => { const suggestions = searchWorkspaceEmojiShortcodes('wink') expect(new Set(suggestions.map((suggestion) => suggestion.emoji)).size).toBe(suggestions.length) @@ -43,8 +66,8 @@ describe('workspace emoji shortcodes', () => { }) }) - it('replaces the completed kr shortcode', () => { - expect(replaceCompletedWorkspaceEmojiShortcode(':kr:', 4)).toEqual({ + it('replaces a completed flag shortcode', () => { + expect(replaceCompletedWorkspaceEmojiShortcode(':flag_kr:', 9)).toEqual({ value: '🇰🇷', cursor: 4 }) diff --git a/src/renderer/src/lib/workspace-emoji-shortcodes.ts b/src/renderer/src/lib/workspace-emoji-shortcodes.ts index a75da33d2..3a5efc7e8 100644 --- a/src/renderer/src/lib/workspace-emoji-shortcodes.ts +++ b/src/renderer/src/lib/workspace-emoji-shortcodes.ts @@ -16,19 +16,24 @@ export type WorkspaceEmojiReplacement = { value: string } -const WORKSPACE_EMOJI_SHORTCODE_ADDITIONS: readonly WorkspaceEmojiSuggestion[] = [ - { emoji: '🇰🇷', shortcode: 'kr' } -] - -const SHORTCODE_ENTRIES = [ - ...STANDARD_EMOJI_SHORTCODE_ENTRIES, - ...WORKSPACE_EMOJI_SHORTCODE_ADDITIONS -] - const EXACT_SHORTCODE = new Map( - SHORTCODE_ENTRIES.map(({ emoji, shortcode }) => [shortcode, { emoji, shortcode }]) + STANDARD_EMOJI_SHORTCODE_ENTRIES.map(({ emoji, shortcode }) => [shortcode, { emoji, shortcode }]) ) +// Lower tiers rank first, so `korea` surfaces `south_korea` above `dishwasher`-style incidental hits. +const MATCH_TIER = { exact: 0, prefix: 1, wordStart: 2, substring: 3 } as const + +function matchTier(shortcode: string, query: string): number | null { + const index = shortcode.indexOf(query) + if (index < 0) { + return null + } + if (index === 0) { + return shortcode.length === query.length ? MATCH_TIER.exact : MATCH_TIER.prefix + } + return /[_-]/.test(shortcode[index - 1]) ? MATCH_TIER.wordStart : MATCH_TIER.substring +} + export function searchWorkspaceEmojiShortcodes( query: string, limit = 8 @@ -38,11 +43,12 @@ export function searchWorkspaceEmojiShortcodes( return [] } - const matches = SHORTCODE_ENTRIES.filter(({ shortcode }) => - shortcode.startsWith(normalizedQuery) - ).sort( + const matches = STANDARD_EMOJI_SHORTCODE_ENTRIES.flatMap((entry) => { + const tier = matchTier(entry.shortcode, normalizedQuery) + return tier === null ? [] : [{ ...entry, tier }] + }).sort( (left, right) => - Number(right.shortcode === normalizedQuery) - Number(left.shortcode === normalizedQuery) || + left.tier - right.tier || left.shortcode.length - right.shortcode.length || left.shortcode.localeCompare(right.shortcode) ) diff --git a/src/shared/emoji-shortcode-catalog.ts b/src/shared/emoji-shortcode-catalog.ts index fba621b7a..63477e206 100644 --- a/src/shared/emoji-shortcode-catalog.ts +++ b/src/shared/emoji-shortcode-catalog.ts @@ -1,25 +1,45 @@ -import emojiShortcodes from 'emojibase-data/en/shortcodes/github.json' +import emojiShortcodes from 'emojibase-data/en/shortcodes/emojibase.json' export type StandardEmojiShortcodeEntry = { emoji: string shortcode: string } +// Skin-tone aliases (`wave_tone3`) are ~40% of the dataset and would drown the suggestion list. +const SKIN_TONE_SHORTCODE = /_tone\d(?:-\d)?$/ + +const CATALOG = Object.entries(emojiShortcodes).flatMap(([hexcode, value]) => { + const shortcodes = (typeof value === 'string' ? [value] : value).filter( + (shortcode) => !SKIN_TONE_SHORTCODE.test(shortcode) + ) + return shortcodes.length > 0 ? [{ emoji: hexcodeToEmoji(hexcode), shortcodes }] : [] +}) + export const STANDARD_EMOJI_SHORTCODE_ENTRIES: readonly StandardEmojiShortcodeEntry[] = - Object.entries(emojiShortcodes).flatMap(([hexcode, value]) => { - const shortcodes = typeof value === 'string' ? [value] : value - const emoji = hexcodeToEmoji(hexcode) - return shortcodes.map((shortcode) => ({ emoji, shortcode })) - }) + CATALOG.flatMap(({ emoji, shortcodes }) => shortcodes.map((shortcode) => ({ emoji, shortcode }))) const PRIMARY_SHORTCODE_BY_EMOJI = new Map( - Object.entries(emojiShortcodes).map(([hexcode, value]) => { - const shortcodes = typeof value === 'string' ? [value] : value - const shortcode = shortcodes.find((candidate) => /^[a-z]/i.test(candidate)) ?? shortcodes[0] - return [normalizeEmojiLookup(hexcodeToEmoji(hexcode)), shortcode] - }) + CATALOG.map(({ emoji, shortcodes }) => [ + normalizeEmojiLookup(emoji), + primaryShortcode(shortcodes) + ]) ) +/** + * Pick the alias that reads best as a branch or directory name: skip `+1`/`-1` so the name + * starts with a letter, then cryptic stubs (👎 `no`, ✌ `v`) and the `flag_xx` namespacing + * prefix, both of which have a spelled-out alias (`thumbsdown`, `victory`, `germany`). + */ +function primaryShortcode(shortcodes: readonly string[]): string { + const named = shortcodes.filter((candidate) => /^[a-z]/i.test(candidate)) + return ( + named.find((candidate) => candidate.length >= 3 && !candidate.startsWith('flag_')) ?? + named.find((candidate) => candidate.length >= 3) ?? + named[0] ?? + shortcodes[0] + ) +} + const EMOJI_SEGMENTER = new Intl.Segmenter('en', { granularity: 'grapheme' }) export function replaceKnownEmojiWithShortcodes(input: string): string { diff --git a/tests/e2e/worktree.spec.ts b/tests/e2e/worktree.spec.ts index 6c2969f53..37c9cc9b1 100644 --- a/tests/e2e/worktree.spec.ts +++ b/tests/e2e/worktree.spec.ts @@ -190,7 +190,7 @@ test.describe('Create Workspace', () => { } }) - test('enters the Korean flag with the kr shortcode suggestion', async ({ orcaPage }) => { + test('enters the Korean flag with the flag_kr shortcode suggestion', async ({ orcaPage }) => { try { await orcaPage.getByRole('button', { name: 'New workspace', exact: true }).click() @@ -198,11 +198,11 @@ test.describe('Create Workspace', () => { const nameInput = dialog.getByPlaceholder(/Type a name/i) await expect(nameInput).toBeVisible() - await nameInput.pressSequentially('Launch :kr', { delay: 100 }) + await nameInput.pressSequentially('Launch :flag_kr', { delay: 100 }) const emojiSuggestions = orcaPage.locator('[data-workspace-emoji-suggestions="true"]') const sourceSuggestions = orcaPage.locator('[data-workspace-source-suggestions="true"]') await expect(emojiSuggestions).toBeVisible() - await expect(emojiSuggestions.getByRole('option', { name: ':kr:' })).toBeVisible() + await expect(emojiSuggestions.getByRole('option', { name: ':flag_kr:' })).toBeVisible() await expect(emojiSuggestions).toHaveAttribute('data-side', 'top') await expect(sourceSuggestions).toBeVisible() await expect(sourceSuggestions).toHaveAttribute('data-side', 'bottom') @@ -211,7 +211,7 @@ test.describe('Create Workspace', () => { await nameInput.pressSequentially(':') await expect(nameInput).toHaveValue('Launch 🇰🇷') - await expect(orcaPage.getByRole('option', { name: /:kr:/i })).toHaveCount(0) + await expect(orcaPage.getByRole('option', { name: /:flag_kr:/i })).toHaveCount(0) await nameInput.pressSequentially(' experiment') await expect(nameInput).toHaveValue('Launch 🇰🇷 experiment') // Keep the asserted result visible in retained proof recordings.