Fix inline code file matching
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user