Fix assistant file link resolution (#1025)

This commit is contained in:
Mohamed Boudra
2026-05-14 20:55:41 +08:00
committed by GitHub
parent a19b780eca
commit 7e255a5249
9 changed files with 1421 additions and 59 deletions
@@ -227,6 +227,47 @@ describe("searchWorkspaceEntries", () => {
expect(filesOnly).toEqual([{ path: "README.md", kind: "file" }]);
});
it("supports fuzzy basename queries for nested workspace files", async () => {
writeFileSync(path.join(workspaceDir, "src", "components", "message-renderer.tsx"), "");
const results = await searchWorkspaceEntries({
cwd: workspaceDir,
query: "msgrndr",
limit: 20,
includeFiles: true,
includeDirectories: false,
});
expect(results).toEqual([
{
path: "src/components/message-renderer.tsx",
kind: "file",
},
]);
});
it("ranks fuzzy basename matches after exact, prefix, and substring matches", async () => {
writeFileSync(path.join(workspaceDir, "src", "components", "msgrndr"), "");
writeFileSync(path.join(workspaceDir, "src", "components", "msgrndr-panel.tsx"), "");
writeFileSync(path.join(workspaceDir, "src", "components", "use-msgrndr.ts"), "");
writeFileSync(path.join(workspaceDir, "src", "components", "message-renderer.tsx"), "");
const results = await searchWorkspaceEntries({
cwd: workspaceDir,
query: "msgrndr",
limit: 20,
includeFiles: true,
includeDirectories: false,
});
expect(results.map((entry) => entry.path)).toEqual([
"src/components/msgrndr",
"src/components/msgrndr-panel.tsx",
"src/components/use-msgrndr.ts",
"src/components/message-renderer.tsx",
]);
});
// POSIX-only: creates and follows a symlink escape fixture.
it.skipIf(isWindows)(
"supports path-style queries and does not escape cwd through symlinks",
@@ -73,6 +73,8 @@ const directoryListCache = new Map<string, DirectoryListCacheEntry>();
const workspaceEntryListCache = new Map<string, WorkspaceEntryListCacheEntry>();
const NO_SEGMENT_INDEX = Number.MAX_SAFE_INTEGER;
const NO_MATCH_OFFSET = Number.MAX_SAFE_INTEGER;
const NO_FUZZY_SCORE = Number.MAX_SAFE_INTEGER;
const NO_WORKSPACE_MATCH_TIER = 5;
const WORKSPACE_IGNORED_DIRECTORY_NAMES = new Set([
"node_modules",
"dist",
@@ -127,6 +129,7 @@ interface RankedWorkspaceEntry {
matchTier: number;
segmentIndex: number;
matchOffset: number;
fuzzyScore: number;
depth: number;
}
@@ -303,18 +306,17 @@ async function searchWorkspaceWithinParentDirectory(input: {
if (entry.kind === "file" && !input.includeFiles) {
continue;
}
if (searchLower && !entry.name.toLowerCase().includes(searchLower)) {
const rankedEntry = rankWorkspaceEntry({
absolutePath: entry.absolutePath,
kind: entry.kind,
workspaceRoot: input.workspaceRoot,
searchLower,
});
if (searchLower && rankedEntry.matchTier === NO_WORKSPACE_MATCH_TIER) {
continue;
}
ranked.push(
rankWorkspaceEntry({
absolutePath: entry.absolutePath,
kind: entry.kind,
workspaceRoot: input.workspaceRoot,
searchLower,
}),
);
ranked.push(rankedEntry);
}
return dedupeAndSortWorkspaceEntries(ranked).slice(0, input.limit);
@@ -374,23 +376,17 @@ async function searchWorkspaceAcrossTree(input: {
continue;
}
const relativePath = normalizeRelativePath(input.workspaceRoot, entry.absolutePath);
if (
searchLower &&
!relativePath.toLowerCase().includes(searchLower) &&
!entry.name.toLowerCase().includes(searchLower)
) {
const rankedEntry = rankWorkspaceEntry({
absolutePath: entry.absolutePath,
kind: entry.kind,
workspaceRoot: input.workspaceRoot,
searchLower,
});
if (searchLower && rankedEntry.matchTier === NO_WORKSPACE_MATCH_TIER) {
continue;
}
ranked.push(
rankWorkspaceEntry({
absolutePath: entry.absolutePath,
kind: entry.kind,
workspaceRoot: input.workspaceRoot,
searchLower,
}),
);
ranked.push(rankedEntry);
}
}
@@ -430,6 +426,9 @@ function compareRankedWorkspaceEntries(
if (left.matchOffset !== right.matchOffset) {
return left.matchOffset - right.matchOffset;
}
if (left.fuzzyScore !== right.fuzzyScore) {
return left.fuzzyScore - right.fuzzyScore;
}
if (left.depth !== right.depth) {
return left.depth - right.depth;
}
@@ -538,6 +537,7 @@ function rankWorkspaceEntry(input: {
matchTier: 3,
segmentIndex: NO_SEGMENT_INDEX,
matchOffset: 0,
fuzzyScore: NO_FUZZY_SCORE,
depth,
};
}
@@ -551,7 +551,9 @@ function rankWorkspaceEntry(input: {
segment.includes(searchLower),
);
const matchOffset = relativeLower.indexOf(searchLower);
let matchTier = 4;
const basename = segments.at(-1) ?? "";
const fuzzyScore = scoreFuzzySubsequence(searchLower, basename);
let matchTier = NO_WORKSPACE_MATCH_TIER;
let segmentIndex = NO_SEGMENT_INDEX;
if (exactSegmentIndex >= 0) {
@@ -565,6 +567,8 @@ function rankWorkspaceEntry(input: {
segmentIndex = partialSegmentIndex;
} else if (relativeLower.startsWith(searchLower)) {
matchTier = 3;
} else if (fuzzyScore !== null) {
matchTier = 4;
}
return {
@@ -573,10 +577,47 @@ function rankWorkspaceEntry(input: {
matchTier,
segmentIndex,
matchOffset: matchOffset >= 0 ? matchOffset : NO_MATCH_OFFSET,
fuzzyScore: fuzzyScore ?? NO_FUZZY_SCORE,
depth,
};
}
function scoreFuzzySubsequence(query: string, candidate: string): number | null {
if (!query) {
return 0;
}
let queryIndex = 0;
let firstMatchIndex = -1;
let previousMatchIndex = -1;
let gapScore = 0;
for (
let candidateIndex = 0;
candidateIndex < candidate.length && queryIndex < query.length;
candidateIndex += 1
) {
if (candidate[candidateIndex] !== query[queryIndex]) {
continue;
}
if (firstMatchIndex === -1) {
firstMatchIndex = candidateIndex;
}
if (previousMatchIndex >= 0) {
gapScore += candidateIndex - previousMatchIndex - 1;
}
previousMatchIndex = candidateIndex;
queryIndex += 1;
}
if (queryIndex !== query.length || firstMatchIndex === -1) {
return null;
}
return firstMatchIndex + gapScore;
}
function findSegmentMatchIndex(
segments: string[],
predicate: (segment: string) => boolean,