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).
This commit is contained in:
parent
96c954f3be
commit
340faaa839
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue