diff --git a/src/renderer/src/components/cmd-j/palette-query-tokens.test.ts b/src/renderer/src/components/cmd-j/palette-query-tokens.test.ts new file mode 100644 index 00000000000..3f829ace14a --- /dev/null +++ b/src/renderer/src/components/cmd-j/palette-query-tokens.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest' +import { normalizeCmdJPaletteQuery, tokenizeCmdJPaletteQuery } from './palette-query-tokens' + +describe('Cmd+J palette query normalization', () => { + it('lowercases supplementary-plane characters as whole code points', () => { + // Why: code-unit iteration split '\u{10400}' into surrogate halves, which lowercase to + // themselves, so the query never matched its lowercase counterpart. + expect(normalizeCmdJPaletteQuery('\u{10400} HELLO')).toBe('\u{10428} hello') + expect(tokenizeCmdJPaletteQuery('\u{10400} HELLO')).toEqual(['\u{10428}', 'hello']) + }) + + it('collapses surrounding and repeated whitespace', () => { + expect(normalizeCmdJPaletteQuery(' New\n\tTerminal ')).toBe('new terminal') + }) +}) diff --git a/src/renderer/src/components/cmd-j/palette-query-tokens.ts b/src/renderer/src/components/cmd-j/palette-query-tokens.ts index 5fe2862b3ba..3ba9068b0c1 100644 --- a/src/renderer/src/components/cmd-j/palette-query-tokens.ts +++ b/src/renderer/src/components/cmd-j/palette-query-tokens.ts @@ -17,12 +17,13 @@ function isCmdJPaletteWhitespace(code: number): boolean { ) } +// Why: iterating code units would lowercase surrogate halves separately, leaving +// supplementary-plane characters uncased and unmatchable. export function normalizeCmdJPaletteQuery(value: string): string { let normalized = '' let pendingWhitespace = false - for (let index = 0; index < value.length; index += 1) { - const code = value.charCodeAt(index) - if (isCmdJPaletteWhitespace(code)) { + for (const character of value) { + if (isCmdJPaletteWhitespace(character.codePointAt(0) ?? 0)) { pendingWhitespace = normalized.length > 0 continue } @@ -30,7 +31,7 @@ export function normalizeCmdJPaletteQuery(value: string): string { normalized += ' ' pendingWhitespace = false } - normalized += value.charAt(index).toLowerCase() + normalized += character.toLowerCase() } return normalized } diff --git a/src/renderer/src/components/cmd-j/palette-results.test.ts b/src/renderer/src/components/cmd-j/palette-results.test.ts index e8ad556d43a..bf875ebe9a2 100644 --- a/src/renderer/src/components/cmd-j/palette-results.test.ts +++ b/src/renderer/src/components/cmd-j/palette-results.test.ts @@ -242,6 +242,13 @@ describe('Cmd+J palette middle-band ranking', () => { expect(rank('linear integrations triage')).toEqual(['settings:integrations']) }) + it('drops verb-prefixed settings queries whose middle words match nothing', () => { + // Why: the verb-plus-settings-keyword rule reads only the head and tail of the query, so + // "new terminal browser settings" used to win on those two ends alone. + expect(top('new terminal sparkles glitter browser settings')).toBeUndefined() + expect(top('new terminal settings')).toBe('settings:terminal') + }) + it('keeps out-of-order partial-word matches when every query word lands somewhere', () => { expect(top('font term')).toBe('settings:terminal') expect(top('font sparkles')).toBeUndefined() diff --git a/src/renderer/src/components/cmd-j/palette-results.ts b/src/renderer/src/components/cmd-j/palette-results.ts index 274d0336cc5..3b643c1fe8e 100644 --- a/src/renderer/src/components/cmd-j/palette-results.ts +++ b/src/renderer/src/components/cmd-j/palette-results.ts @@ -131,6 +131,17 @@ function rankingForCandidate( return null } + // Why: the shortcut rules below only inspect the head and tail of the query, so coverage has + // to gate all of them — otherwise "new terminal browser settings" still wins on rule 3. + const values = + candidate.kind === 'settings' + ? [candidate.title, ...candidate.configKeywords] + : [candidate.title, ...candidate.verbKeywords] + const score = cmdJPaletteTokenScore(query, values) + if (score === 0) { + return null + } + if (candidate.kind === 'action' && candidate.verbKeywords.some((keyword) => query === keyword)) { return { result: candidate, rule: 1, score: 0 } } @@ -165,12 +176,7 @@ function rankingForCandidate( return { result: candidate, rule: 5, score: 0 } } - const values = - candidate.kind === 'settings' - ? [candidate.title, ...candidate.configKeywords] - : [candidate.title, ...candidate.verbKeywords] - const score = cmdJPaletteTokenScore(query, values) - return score > 0 ? { result: candidate, rule: 6, score } : null + return { result: candidate, rule: 6, score } } function compareRanked(a: RankedResult, b: RankedResult): number {