1use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use serde_json::{Value, json};
9use std::path::PathBuf;
10
11use super::file_search_bridge::{self, FileMatchType, FileSearchConfig};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SearchFilesRequest {
16 pub pattern: String,
18 pub workspace_root: PathBuf,
20 pub max_results: usize,
22 pub exclude_patterns: Vec<String>,
24 pub respect_gitignore: bool,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ListFilesRequest {
31 pub workspace_root: PathBuf,
33 pub exclude_patterns: Vec<String>,
35 pub respect_gitignore: bool,
37 pub max_results: usize,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct FileMatchRpc {
44 pub path: String,
46 pub match_type: FileMatchType,
48 pub score: u32,
50 pub indices: Option<Vec<u32>>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct SearchFilesResponse {
57 pub matches: Vec<FileMatchRpc>,
59 pub total_match_count: usize,
61 pub truncated: bool,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ListFilesResponse {
68 pub files: Vec<String>,
70 pub total: usize,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct RpcError {
77 pub code: i32,
79 pub message: String,
81 pub data: Option<Value>,
83}
84
85impl RpcError {
86 pub fn new(code: i32, message: impl Into<String>) -> Self {
88 Self { code, message: message.into(), data: None }
89 }
90
91 pub fn invalid_request(message: impl Into<String>) -> Self {
93 Self::new(-32600, message)
94 }
95
96 pub fn method_not_found() -> Self {
98 Self::new(-32601, "Method not found")
99 }
100
101 pub fn invalid_params(message: impl Into<String>) -> Self {
103 Self::new(-32602, message)
104 }
105
106 pub fn internal_error(message: impl Into<String>) -> Self {
108 Self::new(-32603, message)
109 }
110
111 pub fn custom(code: i32, message: impl Into<String>) -> Self {
113 Self::new(code, message)
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct RpcRequest {
120 pub jsonrpc: String,
122 pub method: String,
124 pub params: Value,
126 pub id: Option<Value>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct RpcResponse {
133 pub jsonrpc: String,
135 pub id: Option<Value>,
137 pub result: Option<Value>,
139 pub error: Option<RpcError>,
141}
142
143impl RpcResponse {
144 pub fn success(id: Option<Value>, result: Value) -> Self {
146 Self {
147 jsonrpc: "2.0".to_string(),
148 id,
149 result: Some(result),
150 error: None,
151 }
152 }
153
154 pub fn error(id: Option<Value>, error: RpcError) -> Self {
156 Self {
157 jsonrpc: "2.0".to_string(),
158 id,
159 result: None,
160 error: Some(error),
161 }
162 }
163}
164
165pub struct FileSearchRpcHandler;
167
168impl FileSearchRpcHandler {
169 pub async fn handle_request(request: RpcRequest) -> RpcResponse {
179 let id = request.id.clone();
180
181 if request.jsonrpc != "2.0" {
183 return RpcResponse::error(id, RpcError::invalid_request("Invalid JSON-RPC version"));
184 }
185
186 let result = match request.method.as_str() {
188 "search_files" => Self::handle_search_files(&request.params, id.clone()).await,
189 "list_files" => Self::handle_list_files(&request.params).await,
190 "find_references" => Self::handle_find_references(&request.params).await,
191 _ => return RpcResponse::error(id, RpcError::method_not_found()),
192 };
193
194 match result {
195 Ok(response) => RpcResponse::success(id, response),
196 Err(error) => RpcResponse::error(id, RpcError::internal_error(error.to_string())),
197 }
198 }
199
200 async fn handle_search_files(params: &Value, _id: Option<Value>) -> Result<Value> {
204 let request: SearchFilesRequest =
205 serde_json::from_value(params.clone()).context("Failed to parse search_files parameters")?;
206
207 if !tokio::fs::try_exists(&request.workspace_root).await.unwrap_or(false) {
209 return Err(anyhow::anyhow!("Workspace root does not exist: {}", request.workspace_root.display()));
210 }
211
212 let max_results = file_search_bridge::effective_max_results(request.max_results);
214 let config = FileSearchConfig::new(request.pattern, request.workspace_root)
215 .with_limit(max_results)
216 .respect_gitignore(request.respect_gitignore);
217
218 let results = file_search_bridge::search_files_async(config, None).await?;
220
221 let matches: Vec<FileMatchRpc> = results
223 .matches
224 .into_iter()
225 .map(|m| FileMatchRpc {
226 path: m.path,
227 match_type: m.match_type,
228 score: m.score,
229 indices: m.indices,
230 })
231 .collect();
232
233 Ok(json!({
234 "matches": matches,
235 "total_match_count": results.total_match_count,
236 "truncated": matches.len() >= max_results,
237 }))
238 }
239
240 async fn handle_list_files(params: &Value) -> Result<Value> {
244 let request: ListFilesRequest =
245 serde_json::from_value(params.clone()).context("Failed to parse list_files parameters")?;
246
247 if !tokio::fs::try_exists(&request.workspace_root).await.unwrap_or(false) {
249 return Err(anyhow::anyhow!("Workspace root does not exist: {}", request.workspace_root.display()));
250 }
251
252 let max_results = file_search_bridge::effective_max_results(request.max_results);
254 let mut config = FileSearchConfig::new(String::new(), request.workspace_root)
255 .with_limit(max_results)
256 .respect_gitignore(request.respect_gitignore);
257
258 for pattern in request.exclude_patterns {
259 config = config.exclude(pattern);
260 }
261
262 let results = file_search_bridge::search_files_async(config, None).await?;
264
265 let files: Vec<String> = file_search_bridge::file_matches_only(results.matches)
267 .into_iter()
268 .map(|m| m.path)
269 .collect();
270 let total = files.len();
271
272 Ok(json!({
273 "files": files,
274 "total": total,
275 }))
276 }
277
278 async fn handle_find_references(params: &Value) -> Result<Value> {
282 let _symbol: String =
285 serde_json::from_value(params.clone()).context("Failed to parse find_references parameters")?;
286
287 Ok(json!({
288 "matches": [],
289 "message": "find_references not yet implemented",
290 }))
291 }
292}
293
294pub fn parse_rpc_request(json_string: &str) -> Result<RpcRequest, Box<RpcResponse>> {
304 match serde_json::from_str::<RpcRequest>(json_string) {
305 Ok(request) => Ok(request),
306 Err(err) => {
307 let error_response = RpcResponse::error(None, RpcError::invalid_request(err.to_string()));
308 Err(Box::new(error_response))
309 }
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_rpc_error_codes() {
319 assert_eq!(RpcError::invalid_request("test").code, -32600);
320 assert_eq!(RpcError::method_not_found().code, -32601);
321 assert_eq!(RpcError::invalid_params("test").code, -32602);
322 assert_eq!(RpcError::internal_error("test").code, -32603);
323 }
324
325 #[test]
326 fn test_rpc_response_success() {
327 let response = RpcResponse::success(Some(json!(1)), json!({"ok": true}));
328 assert_eq!(response.jsonrpc, "2.0");
329 assert_eq!(response.id, Some(json!(1)));
330 assert!(response.result.is_some());
331 assert!(response.error.is_none());
332 }
333
334 #[test]
335 fn test_rpc_response_error() {
336 let error = RpcError::internal_error("test error");
337 let response = RpcResponse::error(Some(json!(1)), error);
338 assert_eq!(response.jsonrpc, "2.0");
339 assert_eq!(response.id, Some(json!(1)));
340 assert!(response.result.is_none());
341 assert!(response.error.is_some());
342 }
343
344 #[test]
345 fn test_search_files_request_parsing() {
346 let json = r#"{
347 "pattern": "main",
348 "workspace_root": "/workspace",
349 "max_results": 100,
350 "exclude_patterns": [],
351 "respect_gitignore": true
352 }"#;
353
354 let value: Value = serde_json::from_str(json).unwrap();
355 let request: SearchFilesRequest = serde_json::from_value(value).unwrap();
356
357 assert_eq!(request.pattern, "main");
358 assert_eq!(request.max_results, 100);
359 assert!(request.respect_gitignore);
360 }
361
362 #[test]
363 fn test_list_files_request_parsing() {
364 let json = r#"{
365 "workspace_root": "/workspace",
366 "exclude_patterns": ["**/node_modules/**"],
367 "respect_gitignore": true,
368 "max_results": 1000
369 }"#;
370
371 let value: Value = serde_json::from_str(json).unwrap();
372 let request: ListFilesRequest = serde_json::from_value(value).unwrap();
373
374 assert_eq!(request.exclude_patterns.len(), 1);
375 assert_eq!(request.max_results, 1000);
376 }
377
378 #[test]
379 fn test_parse_invalid_rpc_request() {
380 let invalid_json = "not valid json";
381 let result = parse_rpc_request(invalid_json);
382 result.unwrap_err();
383 }
384
385 #[test]
386 fn test_file_match_rpc_serialization() {
387 let file_match = FileMatchRpc {
388 path: "src/main.rs".to_string(),
389 match_type: FileMatchType::File,
390 score: 100,
391 indices: Some(vec![4, 5]),
392 };
393
394 let json = serde_json::to_string(&file_match).unwrap();
395 let deserialized: FileMatchRpc = serde_json::from_str(&json).unwrap();
396
397 assert_eq!(deserialized.path, "src/main.rs");
398 assert_eq!(deserialized.score, 100u32);
399 assert_eq!(deserialized.indices, Some(vec![4u32, 5u32]));
400 }
401}