From 06fc419a1531f511c6f40d08fe731191554c9993 Mon Sep 17 00:00:00 2001 From: canvrno <46584286+canvrno@users.noreply.github.com> Date: Tue, 6 May 2025 11:40:38 -0700 Subject: [PATCH] searchFiles protobus migration (#3261) --- .changeset/red-carrots-shout.md | 5 + proto/file.proto | 25 +- src/core/controller/file/methods.ts | 2 + src/core/controller/file/searchFiles.ts | 55 +++ src/core/controller/index.ts | 43 --- .../file/search-result-conversion.ts | 27 ++ src/shared/proto/file.ts | 315 ++++++++++++++++++ .../src/components/chat/ChatTextArea.tsx | 12 +- 8 files changed, 438 insertions(+), 46 deletions(-) create mode 100644 .changeset/red-carrots-shout.md create mode 100644 src/core/controller/file/searchFiles.ts create mode 100644 src/shared/proto-conversions/file/search-result-conversion.ts diff --git a/.changeset/red-carrots-shout.md b/.changeset/red-carrots-shout.md new file mode 100644 index 000000000..c47cc9411 --- /dev/null +++ b/.changeset/red-carrots-shout.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +searchFiles protobus migration diff --git a/proto/file.proto b/proto/file.proto index b03b0cc17..f3621e347 100644 --- a/proto/file.proto +++ b/proto/file.proto @@ -25,6 +25,9 @@ service FileService { // Convert URIs to workspace-relative paths rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths); + + // Search for files in the workspace with fuzzy matching + rpc searchFiles(FileSearchRequest) returns (FileSearchResults); } // Request to convert a list of URIs to relative paths @@ -38,6 +41,27 @@ message RelativePaths { repeated string paths = 1; } +// Request for file search operations +message FileSearchRequest { + Metadata metadata = 1; + string query = 2; // Search query string + optional string mentions_request_id = 3; // Optional request ID for tracking requests + optional int32 limit = 4; // Optional limit for results (default: 20) +} + +// Result for file search operations +message FileSearchResults { + repeated FileInfo results = 1; // Array of file/folder results + optional string mentions_request_id = 2; // Echo of the request ID for tracking +} + +// File information structure for search results +message FileInfo { + string path = 1; // Relative path from workspace root + string type = 2; // "file" or "folder" + optional string label = 3; // Display name (usually basename) +} + // Response for searchCommits message GitCommits { repeated GitCommit commits = 1; @@ -66,4 +90,3 @@ message RuleFile { string display_name = 2; // Filename for display purposes bool already_exists = 3; // For createRuleFile, indicates if file already existed } - diff --git a/src/core/controller/file/methods.ts b/src/core/controller/file/methods.ts index 528251e51..c18e9c2a0 100644 --- a/src/core/controller/file/methods.ts +++ b/src/core/controller/file/methods.ts @@ -9,6 +9,7 @@ import { getRelativePaths } from "./getRelativePaths" import { openFile } from "./openFile" import { openImage } from "./openImage" import { searchCommits } from "./searchCommits" +import { searchFiles } from "./searchFiles" // Register all file service methods export function registerAllMethods(): void { @@ -19,4 +20,5 @@ export function registerAllMethods(): void { registerMethod("openFile", openFile) registerMethod("openImage", openImage) registerMethod("searchCommits", searchCommits) + registerMethod("searchFiles", searchFiles) } diff --git a/src/core/controller/file/searchFiles.ts b/src/core/controller/file/searchFiles.ts new file mode 100644 index 000000000..f68fb254c --- /dev/null +++ b/src/core/controller/file/searchFiles.ts @@ -0,0 +1,55 @@ +import { Controller } from ".." +import { FileSearchRequest, FileSearchResults } from "@shared/proto/file" +import { searchWorkspaceFiles } from "@services/search/file-search" +import { getWorkspacePath } from "@utils/path" +import { FileMethodHandler } from "./index" +import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion" + +/** + * Searches for files in the workspace with fuzzy matching + * @param controller The controller instance + * @param request The request containing search query and optionally a mentionsRequestId + * @returns Results containing matching files/folders + */ +export const searchFiles: FileMethodHandler = async ( + controller: Controller, + request: FileSearchRequest, +): Promise => { + const workspacePath = getWorkspacePath() + + if (!workspacePath) { + // Handle case where workspace path is not available + console.error("Error in searchFiles: No workspace path available") + return FileSearchResults.create({ + results: [], + mentionsRequestId: request.mentionsRequestId, + }) + } + + try { + // Call file search service with query from request + const searchResults = await searchWorkspaceFiles( + request.query || "", + workspacePath, + request.limit || 20, // Use default limit of 20 if not specified + ) + + // Convert search results to proto FileInfo objects using the conversion function + const protoResults = convertSearchResultsToProtoFileInfos(searchResults) + + // Return successful results + return FileSearchResults.create({ + results: protoResults, + mentionsRequestId: request.mentionsRequestId, + }) + } catch (error) { + // Log the error but don't include it in the response, following the pattern in searchCommits + console.error("Error in searchFiles:", error instanceof Error ? error.message : String(error)) + + // Return empty results without error message + return FileSearchResults.create({ + results: [], + mentionsRequestId: request.mentionsRequestId, + }) + } +} diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts index 8309b6229..917c3a61d 100644 --- a/src/core/controller/index.ts +++ b/src/core/controller/index.ts @@ -626,49 +626,6 @@ export class Controller { this.postMessageToWebview({ type: "relinquishControl" }) break } - case "searchFiles": { - const workspacePath = getWorkspacePath() - - if (!workspacePath) { - // Handle case where workspace path is not available - await this.postMessageToWebview({ - type: "fileSearchResults", - results: [], - mentionsRequestId: message.mentionsRequestId, - error: "No workspace path available", - }) - break - } - try { - // Call file search service with query from message - const results = await searchWorkspaceFiles( - message.query || "", - workspacePath, - 20, // Use default limit, as filtering is now done in the backend - ) - - // debug logging to be removed - //console.log(`controller/index.ts: Search results: ${results.length}`) - - // Send results back to webview - await this.postMessageToWebview({ - type: "fileSearchResults", - results, - mentionsRequestId: message.mentionsRequestId, - }) - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - - // Send error response to webview - await this.postMessageToWebview({ - type: "fileSearchResults", - results: [], - error: errorMessage, - mentionsRequestId: message.mentionsRequestId, - }) - } - break - } case "toggleFavoriteModel": { if (message.modelId) { const { apiConfiguration } = await getAllExtensionState(this.context) diff --git a/src/shared/proto-conversions/file/search-result-conversion.ts b/src/shared/proto-conversions/file/search-result-conversion.ts new file mode 100644 index 000000000..0da440cf6 --- /dev/null +++ b/src/shared/proto-conversions/file/search-result-conversion.ts @@ -0,0 +1,27 @@ +import { FileInfo } from "@shared/proto/file" + +/** + * Converts domain search result objects to proto FileInfo objects + */ +export function convertSearchResultsToProtoFileInfos( + results: { path: string; type: "file" | "folder"; label?: string }[], +): FileInfo[] { + return results.map((result) => ({ + path: result.path, + type: result.type, + label: result.label, + })) +} + +/** + * Converts proto FileInfo objects to domain search result objects + */ +export function convertProtoFileInfosToSearchResults( + protoResults: FileInfo[], +): { path: string; type: "file" | "folder"; label?: string }[] { + return protoResults.map((protoResult) => ({ + path: protoResult.path, + type: protoResult.type as "file" | "folder", + label: protoResult.label, + })) +} diff --git a/src/shared/proto/file.ts b/src/shared/proto/file.ts index f6ea208ce..50819d810 100644 --- a/src/shared/proto/file.ts +++ b/src/shared/proto/file.ts @@ -21,6 +21,35 @@ export interface RelativePaths { paths: string[] } +/** Request for file search operations */ +export interface FileSearchRequest { + metadata?: Metadata | undefined + /** Search query string */ + query: string + /** Optional request ID for tracking requests */ + mentionsRequestId?: string | undefined + /** Optional limit for results (default: 20) */ + limit?: number | undefined +} + +/** Result for file search operations */ +export interface FileSearchResults { + /** Array of file/folder results */ + results: FileInfo[] + /** Echo of the request ID for tracking */ + mentionsRequestId?: string | undefined +} + +/** File information structure for search results */ +export interface FileInfo { + /** Relative path from workspace root */ + path: string + /** "file" or "folder" */ + type: string + /** Display name (usually basename) */ + label?: string | undefined +} + /** Response for searchCommits */ export interface GitCommits { commits: GitCommit[] @@ -191,6 +220,283 @@ export const RelativePaths: MessageFns = { }, } +function createBaseFileSearchRequest(): FileSearchRequest { + return { metadata: undefined, query: "", mentionsRequestId: undefined, limit: undefined } +} + +export const FileSearchRequest: MessageFns = { + encode(message: FileSearchRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.metadata !== undefined) { + Metadata.encode(message.metadata, writer.uint32(10).fork()).join() + } + if (message.query !== "") { + writer.uint32(18).string(message.query) + } + if (message.mentionsRequestId !== undefined) { + writer.uint32(26).string(message.mentionsRequestId) + } + if (message.limit !== undefined) { + writer.uint32(32).int32(message.limit) + } + return writer + }, + + decode(input: BinaryReader | Uint8Array, length?: number): FileSearchRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input) + let end = length === undefined ? reader.len : reader.pos + length + const message = createBaseFileSearchRequest() + while (reader.pos < end) { + const tag = reader.uint32() + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break + } + + message.metadata = Metadata.decode(reader, reader.uint32()) + continue + } + case 2: { + if (tag !== 18) { + break + } + + message.query = reader.string() + continue + } + case 3: { + if (tag !== 26) { + break + } + + message.mentionsRequestId = reader.string() + continue + } + case 4: { + if (tag !== 32) { + break + } + + message.limit = reader.int32() + continue + } + } + if ((tag & 7) === 4 || tag === 0) { + break + } + reader.skip(tag & 7) + } + return message + }, + + fromJSON(object: any): FileSearchRequest { + return { + metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined, + query: isSet(object.query) ? globalThis.String(object.query) : "", + mentionsRequestId: isSet(object.mentionsRequestId) ? globalThis.String(object.mentionsRequestId) : undefined, + limit: isSet(object.limit) ? globalThis.Number(object.limit) : undefined, + } + }, + + toJSON(message: FileSearchRequest): unknown { + const obj: any = {} + if (message.metadata !== undefined) { + obj.metadata = Metadata.toJSON(message.metadata) + } + if (message.query !== "") { + obj.query = message.query + } + if (message.mentionsRequestId !== undefined) { + obj.mentionsRequestId = message.mentionsRequestId + } + if (message.limit !== undefined) { + obj.limit = Math.round(message.limit) + } + return obj + }, + + create, I>>(base?: I): FileSearchRequest { + return FileSearchRequest.fromPartial(base ?? ({} as any)) + }, + fromPartial, I>>(object: I): FileSearchRequest { + const message = createBaseFileSearchRequest() + message.metadata = + object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined + message.query = object.query ?? "" + message.mentionsRequestId = object.mentionsRequestId ?? undefined + message.limit = object.limit ?? undefined + return message + }, +} + +function createBaseFileSearchResults(): FileSearchResults { + return { results: [], mentionsRequestId: undefined } +} + +export const FileSearchResults: MessageFns = { + encode(message: FileSearchResults, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.results) { + FileInfo.encode(v!, writer.uint32(10).fork()).join() + } + if (message.mentionsRequestId !== undefined) { + writer.uint32(18).string(message.mentionsRequestId) + } + return writer + }, + + decode(input: BinaryReader | Uint8Array, length?: number): FileSearchResults { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input) + let end = length === undefined ? reader.len : reader.pos + length + const message = createBaseFileSearchResults() + while (reader.pos < end) { + const tag = reader.uint32() + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break + } + + message.results.push(FileInfo.decode(reader, reader.uint32())) + continue + } + case 2: { + if (tag !== 18) { + break + } + + message.mentionsRequestId = reader.string() + continue + } + } + if ((tag & 7) === 4 || tag === 0) { + break + } + reader.skip(tag & 7) + } + return message + }, + + fromJSON(object: any): FileSearchResults { + return { + results: globalThis.Array.isArray(object?.results) ? object.results.map((e: any) => FileInfo.fromJSON(e)) : [], + mentionsRequestId: isSet(object.mentionsRequestId) ? globalThis.String(object.mentionsRequestId) : undefined, + } + }, + + toJSON(message: FileSearchResults): unknown { + const obj: any = {} + if (message.results?.length) { + obj.results = message.results.map((e) => FileInfo.toJSON(e)) + } + if (message.mentionsRequestId !== undefined) { + obj.mentionsRequestId = message.mentionsRequestId + } + return obj + }, + + create, I>>(base?: I): FileSearchResults { + return FileSearchResults.fromPartial(base ?? ({} as any)) + }, + fromPartial, I>>(object: I): FileSearchResults { + const message = createBaseFileSearchResults() + message.results = object.results?.map((e) => FileInfo.fromPartial(e)) || [] + message.mentionsRequestId = object.mentionsRequestId ?? undefined + return message + }, +} + +function createBaseFileInfo(): FileInfo { + return { path: "", type: "", label: undefined } +} + +export const FileInfo: MessageFns = { + encode(message: FileInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.path !== "") { + writer.uint32(10).string(message.path) + } + if (message.type !== "") { + writer.uint32(18).string(message.type) + } + if (message.label !== undefined) { + writer.uint32(26).string(message.label) + } + return writer + }, + + decode(input: BinaryReader | Uint8Array, length?: number): FileInfo { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input) + let end = length === undefined ? reader.len : reader.pos + length + const message = createBaseFileInfo() + while (reader.pos < end) { + const tag = reader.uint32() + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break + } + + message.path = reader.string() + continue + } + case 2: { + if (tag !== 18) { + break + } + + message.type = reader.string() + continue + } + case 3: { + if (tag !== 26) { + break + } + + message.label = reader.string() + continue + } + } + if ((tag & 7) === 4 || tag === 0) { + break + } + reader.skip(tag & 7) + } + return message + }, + + fromJSON(object: any): FileInfo { + return { + path: isSet(object.path) ? globalThis.String(object.path) : "", + type: isSet(object.type) ? globalThis.String(object.type) : "", + label: isSet(object.label) ? globalThis.String(object.label) : undefined, + } + }, + + toJSON(message: FileInfo): unknown { + const obj: any = {} + if (message.path !== "") { + obj.path = message.path + } + if (message.type !== "") { + obj.type = message.type + } + if (message.label !== undefined) { + obj.label = message.label + } + return obj + }, + + create, I>>(base?: I): FileInfo { + return FileInfo.fromPartial(base ?? ({} as any)) + }, + fromPartial, I>>(object: I): FileInfo { + const message = createBaseFileInfo() + message.path = object.path ?? "" + message.type = object.type ?? "" + message.label = object.label ?? undefined + return message + }, +} + function createBaseGitCommits(): GitCommits { return { commits: [] } } @@ -636,6 +942,15 @@ export const FileServiceDefinition = { responseStream: false, options: {}, }, + /** Search for files in the workspace with fuzzy matching */ + searchFiles: { + name: "searchFiles", + requestType: FileSearchRequest, + requestStream: false, + responseType: FileSearchResults, + responseStream: false, + options: {}, + }, }, } as const diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 9def1e9a4..84a4301c6 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -686,11 +686,19 @@ const ChatTextArea = forwardRef( // Set a timeout to debounce the search requests searchTimeoutRef.current = setTimeout(() => { - vscode.postMessage({ - type: "searchFiles", + FileServiceClient.searchFiles({ query: query, mentionsRequestId: query, }) + .then((results) => { + setFileSearchResults(results.results || []) + setSearchLoading(false) + }) + .catch((error) => { + console.error("Error searching files:", error) + setFileSearchResults([]) + setSearchLoading(false) + }) }, 200) // 200ms debounce } else { setSelectedMenuIndex(3) // Set to "File" option by default