diff --git a/packages/shared/src/searchRanking.test.ts b/packages/shared/src/searchRanking.test.ts index 49e4da100b70e..62c9ca9cb4df2 100644 --- a/packages/shared/src/searchRanking.test.ts +++ b/packages/shared/src/searchRanking.test.ts @@ -33,6 +33,19 @@ describe("scoreQueryMatch", () => { }); }); +describe("scoreQueryMatch typo tolerance", () => { + const match = (value: string, query: string) => + scoreQueryMatch({ value, query, exactBase: 0, prefixBase: 10, fuzzyBase: 50 }); + + it("recovers a single adjacent transposition without accepting arbitrary edits", () => { + expect(match("search", "serach")).toBe(51); + expect(match("search", "search")).toBe(0); + expect(match("search", "searxh")).toBeNull(); + expect(match("cat", "cta")).toBeNull(); + expect(match("search", "serahc")).toBeNull(); + }); +}); + describe("scoreSubsequenceMatch", () => { it("scores tighter subsequences ahead of looser ones", () => { const compact = scoreSubsequenceMatch("ghfixci", "gfc"); diff --git a/packages/shared/src/searchRanking.ts b/packages/shared/src/searchRanking.ts index 1b8d710fef734..79481331e4041 100644 --- a/packages/shared/src/searchRanking.ts +++ b/packages/shared/src/searchRanking.ts @@ -58,6 +58,16 @@ export function scoreSubsequenceMatch( return null; } +/** A single adjacent-key ordering mistake, without broad edit-distance matches. */ +function isAdjacentTransposition(value: string, query: string): boolean { + if (query.length < 4 || value.length !== query.length) return false; + let first = 0; + while (first < query.length && value[first] === query[first]) first += 1; + if (first >= query.length - 1) return false; + if (value[first] !== query[first + 1] || value[first + 1] !== query[first]) return false; + return value.slice(first + 2) === query.slice(first + 2); +} + function lengthPenalty(value: string, query: string): number { return Math.min(64, Math.max(0, value.length - query.length)); } @@ -135,6 +145,9 @@ export function scoreQueryMatch(input: { if (fuzzyScore !== null) { return input.fuzzyBase + fuzzyScore; } + if (isAdjacentTransposition(value, query)) { + return input.fuzzyBase + 1; + } } return null;