fix(search): match complete paths in file and project search

Treat slash-separated fragments as searchable text and use the same full-path fuzzy matcher for daemon results and remembered project paths.
This commit is contained in:
Mohamed Boudra
2026-08-19 12:18:23 +02:00
parent 91024bc029
commit e55dd5a212
6 changed files with 245 additions and 45 deletions
@@ -5,6 +5,7 @@ import {
fuzzyPolicyForToken,
matchRanges,
scoreMatch,
scorePathMatch,
scoreTextFields,
} from "./text-match.js";
@@ -171,6 +172,28 @@ describe("scoreMatch typo tolerance", () => {
});
});
describe("scorePathMatch", () => {
it("matches a literal fragment anywhere in the complete path", () => {
expect(
scorePathMatch("skills/", "something/something-else/skills/paseo-advisor/SKILL.md"),
).not.toBeNull();
});
it("fuzzy-matches spaced text against a compact path", () => {
expect(scorePathMatch("blank page editor", "blankpage/editor")).toEqual({
tier: 0,
offset: 0,
});
});
it("keeps the original path offset for compact matches", () => {
expect(scorePathMatch("blank page editor", "projects/blankpage/editor")).toEqual({
tier: 4,
offset: 9,
});
});
});
describe("scoreTextFields", () => {
it("requires every token to match some field", () => {
expect(scoreTextFields("stripe billing", ["add stripe billing system"])).not.toBeNull();
@@ -201,6 +201,39 @@ export function scoreMatch(
return fuzzy ? scoreFuzzyMatch(q, t, fuzzy) : null;
}
interface CompactText {
value: string;
offsets: number[];
}
function compactText(value: string): CompactText {
let compact = "";
const offsets: number[] = [];
for (let index = 0; index < value.length; index += 1) {
if (!/[a-z0-9]/i.test(value[index])) continue;
compact += value[index].toLowerCase();
offsets.push(index);
}
return { value: compact, offsets };
}
/** Match a query against a complete displayed path, including its separators. */
export function scorePathMatch(query: string, path: string): MatchScore | null {
const direct = scoreMatch(query, path);
if (direct) return direct;
const compactQuery = compactText(query);
const compactPath = compactText(path);
if (!compactQuery.value || !compactPath.value) return null;
const compactScore = scoreMatch(compactQuery.value, compactPath.value);
if (!compactScore) return null;
return {
...compactScore,
offset: compactPath.offsets[compactScore.offset] ?? compactScore.offset,
};
}
export interface MatchRange {
start: number;
length: number;