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.
This commit is contained in:
Jinjing
2026-08-06 17:52:21 -07:00
parent d0ba0bd829
commit 1258d508a4
4 changed files with 39 additions and 10 deletions
@@ -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')
})
})
@@ -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
}
@@ -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 <junk> 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()
@@ -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 <junk> 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 {