From 1258d508a4a01a857e7e49d37088f7a9159d4e45 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:52:21 -0700 Subject: [PATCH] Fix cmd+j search to require query coverage and handle Unicode The search ranking now requires all query words to match candidate values before applying shortcut rules, preventing false positives where middle words could be ignored. Query normalization now iterates over Unicode characters instead of code units to properly lowercase supplementary-plane characters. --- .../cmd-j/palette-query-tokens.test.ts | 15 +++++++++++++++ .../components/cmd-j/palette-query-tokens.ts | 9 +++++---- .../components/cmd-j/palette-results.test.ts | 7 +++++++ .../src/components/cmd-j/palette-results.ts | 18 ++++++++++++------ 4 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 src/renderer/src/components/cmd-j/palette-query-tokens.test.ts 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 {