searchFiles protobus migration (#3261)

This commit is contained in:
canvrno 2025-05-06 11:40:38 -07:00 committed by GitHub
parent 10f7b8ca9e
commit 06fc419a15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 438 additions and 46 deletions

View File

@ -0,0 +1,5 @@
---
"claude-dev": patch
---
searchFiles protobus migration

View File

@ -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
}

View File

@ -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)
}

View File

@ -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<FileSearchResults> => {
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,
})
}
}

View File

@ -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)

View File

@ -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,
}))
}

View File

@ -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<RelativePaths> = {
},
}
function createBaseFileSearchRequest(): FileSearchRequest {
return { metadata: undefined, query: "", mentionsRequestId: undefined, limit: undefined }
}
export const FileSearchRequest: MessageFns<FileSearchRequest> = {
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 extends Exact<DeepPartial<FileSearchRequest>, I>>(base?: I): FileSearchRequest {
return FileSearchRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<FileSearchRequest>, 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<FileSearchResults> = {
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 extends Exact<DeepPartial<FileSearchResults>, I>>(base?: I): FileSearchResults {
return FileSearchResults.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<FileSearchResults>, 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<FileInfo> = {
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 extends Exact<DeepPartial<FileInfo>, I>>(base?: I): FileInfo {
return FileInfo.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<FileInfo>, 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

View File

@ -686,11 +686,19 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// 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