Fix inline code file matching

This commit is contained in:
Mohamed Boudra
2026-05-17 23:40:31 +07:00
parent b55aa042d4
commit 721f1ee8d3
11 changed files with 301 additions and 38 deletions
@@ -1497,6 +1497,7 @@ test("requests directory suggestions via RPC", async () => {
cwd: "/tmp/project",
includeFiles: true,
includeDirectories: true,
matchMode: "suffix",
},
"req-directories",
);
@@ -1508,6 +1509,7 @@ test("requests directory suggestions via RPC", async () => {
expect(request.cwd).toBe("/tmp/project");
expect(request.includeFiles).toBe(true);
expect(request.includeDirectories).toBe(true);
expect(request.matchMode).toBe("suffix");
expect(request.limit).toBe(10);
expect(request.requestId).toBe("req-directories");
@@ -3090,6 +3090,7 @@ export class DaemonClient {
cwd?: string;
includeFiles?: boolean;
includeDirectories?: boolean;
matchMode?: "fuzzy" | "suffix";
},
requestId?: string,
): Promise<DirectorySuggestionsPayload> {
@@ -3101,6 +3102,7 @@ export class DaemonClient {
cwd: options.cwd,
includeFiles: options.includeFiles,
includeDirectories: options.includeDirectories,
matchMode: options.matchMode,
limit: options.limit,
},
responseType: "directory_suggestions_response",
+2 -1
View File
@@ -4705,7 +4705,7 @@ export class Session {
}
private async handleDirectorySuggestionsRequest(msg: DirectorySuggestionsRequest): Promise<void> {
const { query, limit, requestId, cwd, includeFiles, includeDirectories } = msg;
const { query, limit, requestId, cwd, includeFiles, includeDirectories, matchMode } = msg;
try {
const workspaceCwd = cwd?.trim();
@@ -4716,6 +4716,7 @@ export class Session {
limit,
includeFiles,
includeDirectories,
matchMode,
})
: (
await searchHomeDirectories({
@@ -330,6 +330,7 @@ describe("shared messages stream parsing", () => {
cwd: "/tmp/project",
includeFiles: true,
includeDirectories: true,
matchMode: "suffix",
limit: 20,
requestId: "req-dir-1",
});
+1
View File
@@ -1441,6 +1441,7 @@ export const DirectorySuggestionsRequestSchema = z.object({
cwd: z.string().optional(),
includeFiles: z.boolean().optional(),
includeDirectories: z.boolean().optional(),
matchMode: z.enum(["fuzzy", "suffix"]).optional(),
limit: z.number().int().min(1).max(100).optional(),
requestId: z.string(),
});
@@ -268,6 +268,39 @@ describe("searchWorkspaceEntries", () => {
]);
});
it("suffix mode matches whole path segment suffixes without fuzzy matches", async () => {
mkdirSync(path.join(workspaceDir, "packages", "app", "src"), { recursive: true });
writeFileSync(path.join(workspaceDir, "src", "file.ts"), "");
writeFileSync(path.join(workspaceDir, "packages", "app", "src", "file.ts"), "");
writeFileSync(path.join(workspaceDir, "src", "paseo-config-file.ts"), "");
const basenameResults = await searchWorkspaceEntries({
cwd: workspaceDir,
query: "file.ts",
limit: 20,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
});
const suffixResults = await searchWorkspaceEntries({
cwd: workspaceDir,
query: "src/file.ts",
limit: 20,
includeFiles: true,
includeDirectories: false,
matchMode: "suffix",
});
expect(basenameResults).toEqual([
{ path: "src/file.ts", kind: "file" },
{ path: "packages/app/src/file.ts", kind: "file" },
]);
expect(suffixResults).toEqual([
{ path: "src/file.ts", kind: "file" },
{ path: "packages/app/src/file.ts", kind: "file" },
]);
});
// POSIX-only: creates and follows a symlink escape fixture.
it.skipIf(isWindows)(
"supports path-style queries and does not escape cwd through symlinks",
@@ -23,10 +23,13 @@ export interface SearchWorkspaceEntriesOptions {
limit?: number;
includeFiles?: boolean;
includeDirectories?: boolean;
matchMode?: WorkspaceMatchMode;
maxDepth?: number;
maxEntriesScanned?: number;
}
export type WorkspaceMatchMode = "fuzzy" | "suffix";
const DEFAULT_LIMIT = 30;
const MAX_LIMIT = 100;
const DEFAULT_MAX_DEPTH = 6;
@@ -153,7 +156,8 @@ export async function searchWorkspaceEntries(
return [];
}
if (queryParts.isPathQuery) {
const matchMode = options.matchMode ?? "fuzzy";
if (queryParts.isPathQuery && matchMode !== "suffix") {
return searchWorkspaceWithinParentDirectory({
workspaceRoot,
parentPart: queryParts.parentPart,
@@ -164,12 +168,17 @@ export async function searchWorkspaceEntries(
});
}
const searchTerm =
matchMode === "suffix"
? [queryParts.parentPart, queryParts.searchTerm].filter(Boolean).join("/")
: queryParts.searchTerm;
return searchWorkspaceAcrossTree({
workspaceRoot,
searchTerm: queryParts.searchTerm,
searchTerm,
limit,
includeDirectories,
includeFiles,
matchMode,
maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
maxEntriesScanned: options.maxEntriesScanned ?? DEFAULT_MAX_DIRECTORIES_SCANNED,
});
@@ -328,6 +337,7 @@ async function searchWorkspaceAcrossTree(input: {
limit: number;
includeDirectories: boolean;
includeFiles: boolean;
matchMode: WorkspaceMatchMode;
maxDepth: number;
maxEntriesScanned: number;
}): Promise<WorkspaceSuggestionEntry[]> {
@@ -375,6 +385,16 @@ async function searchWorkspaceAcrossTree(input: {
if (entry.kind === "file" && !input.includeFiles) {
continue;
}
if (
input.matchMode === "suffix" &&
!workspaceEntryMatchesSuffixQuery({
absolutePath: entry.absolutePath,
workspaceRoot: input.workspaceRoot,
query: input.searchTerm,
})
) {
continue;
}
const rankedEntry = rankWorkspaceEntry({
absolutePath: entry.absolutePath,
@@ -382,7 +402,11 @@ async function searchWorkspaceAcrossTree(input: {
workspaceRoot: input.workspaceRoot,
searchLower,
});
if (searchLower && rankedEntry.matchTier === NO_WORKSPACE_MATCH_TIER) {
if (
input.matchMode !== "suffix" &&
searchLower &&
rankedEntry.matchTier === NO_WORKSPACE_MATCH_TIER
) {
continue;
}
@@ -393,6 +417,34 @@ async function searchWorkspaceAcrossTree(input: {
return dedupeAndSortWorkspaceEntries(ranked).slice(0, input.limit);
}
function workspaceEntryMatchesSuffixQuery(input: {
absolutePath: string;
workspaceRoot: string;
query: string;
}): boolean {
const querySegments = input.query
.trim()
.replace(/\\/g, "/")
.replace(/^\.\/+/, "")
.split("/")
.filter(Boolean)
.map((segment) => segment.toLowerCase());
if (querySegments.length === 0) {
return false;
}
const pathSegments = normalizeRelativePath(input.workspaceRoot, input.absolutePath)
.split("/")
.filter(Boolean)
.map((segment) => segment.toLowerCase());
if (querySegments.length > pathSegments.length) {
return false;
}
const offset = pathSegments.length - querySegments.length;
return querySegments.every((segment, index) => pathSegments[offset + index] === segment);
}
function dedupeAndSortWorkspaceEntries(
rankedEntries: RankedWorkspaceEntry[],
): WorkspaceSuggestionEntry[] {