feat(workspaces): derive readable emoji identifiers
This commit is contained in:
parent
84b335f80c
commit
038fd7a50c
|
|
@ -195,6 +195,7 @@
|
|||
"electron-builder-squirrel-windows": "^26.15.3",
|
||||
"electron-vite": "^5.0.0",
|
||||
"emoji-picker-react": "^4.19.1",
|
||||
"emojibase-data": "17.0.0",
|
||||
"happy-dom": "^20.9.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"husky": "^9.1.7",
|
||||
|
|
|
|||
|
|
@ -252,6 +252,9 @@ importers:
|
|||
emoji-picker-react:
|
||||
specifier: ^4.19.1
|
||||
version: 4.19.1(react@19.2.7)
|
||||
emojibase-data:
|
||||
specifier: 17.0.0
|
||||
version: 17.0.0(emojibase@17.0.0)
|
||||
happy-dom:
|
||||
specifier: ^20.9.0
|
||||
version: 20.9.0
|
||||
|
|
@ -3986,6 +3989,15 @@ packages:
|
|||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
emojibase-data@17.0.0:
|
||||
resolution: {integrity: sha512-Yvgb5AWoHViHV/gq1qr5ZAarcBip+B27/ZLRsUJkbgAEaLlZ/fof9g882LTpmEpyhBNEC0m2SEmItljHsTygjA==}
|
||||
peerDependencies:
|
||||
emojibase: '*'
|
||||
|
||||
emojibase@17.0.0:
|
||||
resolution: {integrity: sha512-bXdpf4HPY3p41zK5swVKZdC/VynsMZ4LoLxdYDE+GucqkFwzcM1GVc4ODfYAlwoKaf2U2oNNUoOO78N96ovpBA==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
encodeurl@2.0.0:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -10031,6 +10043,12 @@ snapshots:
|
|||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emojibase-data@17.0.0(emojibase@17.0.0):
|
||||
dependencies:
|
||||
emojibase: 17.0.0
|
||||
|
||||
emojibase@17.0.0: {}
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
|
|
|
|||
|
|
@ -74,15 +74,19 @@ describe('sanitizeWorktreeName', () => {
|
|||
expect(sanitizeWorktreeName('feat: 中文 (v2)')).toBe('feat-中文-v2')
|
||||
})
|
||||
|
||||
it('uses a git-safe fallback when a name contains only emoji', () => {
|
||||
expect(sanitizeWorktreeName('🚀')).toBe('workspace')
|
||||
expect(sanitizeWorktreeName('👩💻✨')).toBe('workspace')
|
||||
expect(sanitizeWorktreeName('🇯🇵')).toBe('workspace')
|
||||
expect(sanitizeWorktreeName('1️⃣')).toBe('1')
|
||||
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('1️⃣')).toBe('one')
|
||||
})
|
||||
|
||||
it('keeps readable text while removing emoji from branch and path names', () => {
|
||||
expect(sanitizeWorktreeName('Ship it 🚀')).toBe('Ship-it')
|
||||
it('keeps readable text and emoji shortcodes in branch and path names', () => {
|
||||
expect(sanitizeWorktreeName('Ship it 🚀')).toBe('Ship-it-rocket')
|
||||
})
|
||||
|
||||
it('uses a git-safe fallback for emoji newer than the shortcode catalog', () => {
|
||||
expect(sanitizeWorktreeName('\u{1fae9}')).toBe('workspace')
|
||||
})
|
||||
|
||||
it('does not treat arbitrary punctuation as a workspace name', () => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { GlobalSettings, OrcaWorkspaceLayout, Repo } from '../../shared/typ
|
|||
import { isWindowsAbsolutePathLike, resolveRuntimePath } from '../../shared/cross-platform-path'
|
||||
import { isWslUncPath } from '../../shared/wsl-paths'
|
||||
import { splitWorktreeId } from '../../shared/worktree-id'
|
||||
import { replaceKnownEmojiWithShortcodes } from '../../shared/emoji-shortcode-catalog'
|
||||
import { getWslHome, parseWslPath } from '../wsl'
|
||||
|
||||
type WorktreePathSettings = Pick<GlobalSettings, 'nestWorkspaces' | 'workspaceDir'>
|
||||
|
|
@ -25,7 +26,7 @@ export function sanitizeWorktreeName(input: string): string {
|
|||
// name workspaces in their own language. Git ref-format permits non-ASCII
|
||||
// bytes, and modern filesystems handle UTF-8 paths. Only strip characters
|
||||
// git or the filesystem actually rejects.
|
||||
const sanitized = input
|
||||
const sanitized = replaceKnownEmojiWithShortcodes(input)
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}._-]+/gu, '-')
|
||||
.replace(/-+/g, '-')
|
||||
|
|
|
|||
|
|
@ -899,9 +899,9 @@ describe('registerWorktreeHandlers', () => {
|
|||
it('keeps an emoji-only display name while using safe branch and path names', async () => {
|
||||
listWorktreesMock.mockResolvedValue([
|
||||
{
|
||||
path: '/workspace/workspace',
|
||||
path: '/workspace/rocket',
|
||||
head: 'abc123',
|
||||
branch: 'workspace',
|
||||
branch: 'rocket',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
|
|
@ -914,13 +914,13 @@ describe('registerWorktreeHandlers', () => {
|
|||
|
||||
expect(addWorktreeMock).toHaveBeenCalledWith(
|
||||
'/workspace/repo',
|
||||
'/workspace/workspace',
|
||||
'workspace',
|
||||
'/workspace/rocket',
|
||||
'rocket',
|
||||
'origin/main',
|
||||
false
|
||||
)
|
||||
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
|
||||
'repo-1::/workspace/workspace',
|
||||
'repo-1::/workspace/rocket',
|
||||
expect.objectContaining({ displayName: '🚀' })
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -86,6 +86,23 @@ describe('worktree-palette-search', () => {
|
|||
])
|
||||
})
|
||||
|
||||
it('finds an emoji-named workspace by its readable branch shortcode', () => {
|
||||
const results = searchWorktrees(
|
||||
[makeWorktree({ displayName: '🚀', branch: 'refs/heads/rocket' })],
|
||||
'rocket',
|
||||
repoMap,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0]).toMatchObject({
|
||||
worktreeId: 'wt-1',
|
||||
matchedField: 'branch',
|
||||
branchRange: { start: 0, end: 6 }
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects oversized pasted queries before reading worktree metadata', () => {
|
||||
const oversizedQuery = 'secret-worktree-palette-search'.repeat(WORKTREE_PALETTE_QUERY_MAX_BYTES)
|
||||
const worktree = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import emojiShortcodes from 'emojibase-data/en/shortcodes/github.json'
|
||||
|
||||
export type StandardEmojiShortcodeEntry = {
|
||||
emoji: string
|
||||
shortcode: string
|
||||
}
|
||||
|
||||
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 }))
|
||||
})
|
||||
|
||||
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]
|
||||
})
|
||||
)
|
||||
|
||||
const EMOJI_SEGMENTER = new Intl.Segmenter('en', { granularity: 'grapheme' })
|
||||
|
||||
export function replaceKnownEmojiWithShortcodes(input: string): string {
|
||||
return Array.from(EMOJI_SEGMENTER.segment(input), ({ segment }) => {
|
||||
const shortcode = PRIMARY_SHORTCODE_BY_EMOJI.get(normalizeEmojiLookup(segment))
|
||||
return shortcode ? ` ${shortcode.replaceAll('_', '-')} ` : segment
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function normalizeEmojiLookup(emoji: string): string {
|
||||
return Array.from(emoji)
|
||||
.filter((character) => {
|
||||
const codepoint = character.codePointAt(0)
|
||||
return (
|
||||
character !== '\ufe0f' &&
|
||||
(codepoint === undefined || codepoint < 0x1f3fb || codepoint > 0x1f3ff)
|
||||
)
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
|
||||
function hexcodeToEmoji(hexcode: string): string {
|
||||
return hexcode
|
||||
.split('-')
|
||||
.map((codepoint) => String.fromCodePoint(Number.parseInt(codepoint, 16)))
|
||||
.join('')
|
||||
}
|
||||
|
|
@ -178,8 +178,7 @@ test.describe('Create Workspace', () => {
|
|||
const worktrees = Object.values(window.__store!.getState().worktreesByRepo).flat()
|
||||
return worktrees.find((worktree) => worktree.displayName === displayName)?.branch ?? null
|
||||
}, workspaceName)
|
||||
expect(branch).not.toBeNull()
|
||||
expect(branch).not.toMatch(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/u)
|
||||
expect(branch).toBe('refs/heads/rocket-test-tube-sparkles')
|
||||
} finally {
|
||||
await orcaPage
|
||||
.evaluate(() => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue