Fix assistant file link resolution (#1025)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user