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 {
89 code,
90 message: message.into(),
91 data: None,
92 }
93 }
94
95 pub fn invalid_request(message: impl Into<String>) -> Self {
97 Self::new(-32600, message)
98 }
99
100 pub fn method_not_found() -> Self {
102 Self::new(-32601, "Method not found")
103 }
104
105 pub fn invalid_params(message: impl Into<String>) -> Self {
107 Self::new(-32602, message)
108 }
109
110 pub fn internal_error(message: impl Into<String>) -> Self {
112 Self::new(-32603, message)
113 }
114
115 pub fn custom(code: i32, message: impl Into<String>) -> Self {
117 Self::new(code, message)
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct RpcRequest {
124 pub jsonrpc: String,
126 pub method: String,
128 pub params: Value,
130 pub id: Option<Value>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct RpcResponse {
137 pub jsonrpc: String,
139 pub id: Option<Value>,
141 pub result: Option<Value>,
143 pub error: Option<RpcError>,
145}
146
147impl RpcResponse {
148 pub fn success(id: Option<Value>, result: Value) -> Self {
150 Self {
151 jsonrpc: "2.0".to_string(),
152 id,
153 result: Some(result),
154 error: None,
155 }
156 }
157
158 pub fn error(id: Option<Value>, error: RpcError) -> Self {
160 Self {
161 jsonrpc: "2.0".to_string(),
162 id,
163 result: None,
164 error: Some(error),
165 }
166 }
167}
168
169pub struct FileSearchRpcHandler;
171
172impl FileSearchRpcHandler {
173 pub async fn handle_request(request: RpcRequest) -> RpcResponse {
183 let id = request.id.clone();
184
185 if request.jsonrpc != "2.0" {
187 return RpcResponse::error(id, RpcError::invalid_request("Invalid JSON-RPC version"));
188 }
189
190 let result = match request.method.as_str() {
192 "search_files" => Self::handle_search_files(&request.params, id.clone()).await,
193 "list_files" => Self::handle_list_files(&request.params).await,
194 "find_references" => Self::handle_find_references(&request.params).await,
195 _ => return RpcResponse::error(id, RpcError::method_not_found()),
196 };
197
198 match result {
199 Ok(response) => RpcResponse::success(id, response),
200 Err(error) => RpcResponse::error(id, RpcError::internal_error(error.to_string())),
201 }
202 }
203
204 async fn handle_search_files(params: &Value, _id: Option<Value>) -> Result<Value> {
208 let request: SearchFilesRequest = serde_json::from_value(params.clone())
209 .context("Failed to parse search_files parameters")?;
210
211 if !request.workspace_root.exists() {
213 return Err(anyhow::anyhow!(
214 "Workspace root does not exist: {}",
215 request.workspace_root.display()
216 ));
217 }
218
219 let config = FileSearchConfig::new(request.pattern, request.workspace_root)
221 .with_limit(request.max_results)
222 .respect_gitignore(request.respect_gitignore);
223
224 let results = file_search_bridge::search_files(config, None)?;
226
227 let matches: Vec<FileMatchRpc> = results
229 .matches
230 .into_iter()
231 .map(|m| FileMatchRpc {
232 path: m.path,
233 match_type: m.match_type,
234 score: m.score,
235 indices: m.indices,
236 })
237 .collect();
238
239 Ok(json!({
240 "matches": matches,
241 "total_match_count": results.total_match_count,
242 "truncated": matches.len() >= request.max_results,
243 }))
244 }
245
246 async fn handle_list_files(params: &Value) -> Result<Value> {
250 let request: ListFilesRequest = serde_json::from_value(params.clone())
251 .context("Failed to parse list_files parameters")?;
252
253 if !request.workspace_root.exists() {
255 return Err(anyhow::anyhow!(
256 "Workspace root does not exist: {}",
257 request.workspace_root.display()
258 ));
259 }
260
261 let mut config = FileSearchConfig::new(String::new(), request.workspace_root)
263 .with_limit(request.max_results)
264 .respect_gitignore(request.respect_gitignore);
265
266 for pattern in request.exclude_patterns {
267 config = config.exclude(pattern);
268 }
269
270 let results = file_search_bridge::search_files(config, None)?;
272
273 let files: Vec<String> = file_search_bridge::file_matches_only(results.matches)
275 .into_iter()
276 .map(|m| m.path)
277 .collect();
278 let total = files.len();
279
280 Ok(json!({
281 "files": files,
282 "total": total,
283 }))
284 }
285
286 async fn handle_find_references(params: &Value) -> Result<Value> {
290 let _symbol: String = serde_json::from_value(params.clone())
293 .context("Failed to parse find_references parameters")?;
294
295 Ok(json!({
296 "matches": [],
297 "message": "find_references not yet implemented",
298 }))
299 }
300}
301
302pub fn parse_rpc_request(json_string: &str) -> Result<RpcRequest, Box<RpcResponse>> {
312 match serde_json::from_str::<RpcRequest>(json_string) {
313 Ok(request) => Ok(request),
314 Err(err) => {
315 let error_response =
316 RpcResponse::error(None, RpcError::invalid_request(err.to_string()));
317 Err(Box::new(error_response))
318 }
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_rpc_error_codes() {
328 assert_eq!(RpcError::invalid_request("test").code, -32600);
329 assert_eq!(RpcError::method_not_found().code, -32601);
330 assert_eq!(RpcError::invalid_params("test").code, -32602);
331 assert_eq!(RpcError::internal_error("test").code, -32603);
332 }
333
334 #[test]
335 fn test_rpc_response_success() {
336 let response = RpcResponse::success(Some(json!(1)), json!({"ok": true}));
337 assert_eq!(response.jsonrpc, "2.0");
338 assert_eq!(response.id, Some(json!(1)));
339 assert!(response.result.is_some());
340 assert!(response.error.is_none());
341 }
342
343 #[test]
344 fn test_rpc_response_error() {
345 let error = RpcError::internal_error("test error");
346 let response = RpcResponse::error(Some(json!(1)), error);
347 assert_eq!(response.jsonrpc, "2.0");
348 assert_eq!(response.id, Some(json!(1)));
349 assert!(response.result.is_none());
350 assert!(response.error.is_some());
351 }
352
353 #[test]
354 fn test_search_files_request_parsing() {
355 let json = r#"{
356 "pattern": "main",
357 "workspace_root": "/workspace",
358 "max_results": 100,
359 "exclude_patterns": [],
360 "respect_gitignore": true
361 }"#;
362
363 let value: Value = serde_json::from_str(json).unwrap();
364 let request: SearchFilesRequest = serde_json::from_value(value).unwrap();
365
366 assert_eq!(request.pattern, "main");
367 assert_eq!(request.max_results, 100);
368 assert!(request.respect_gitignore);
369 }
370
371 #[test]
372 fn test_list_files_request_parsing() {
373 let json = r#"{
374 "workspace_root": "/workspace",
375 "exclude_patterns": ["**/node_modules/**"],
376 "respect_gitignore": true,
377 "max_results": 1000
378 }"#;
379
380 let value: Value = serde_json::from_str(json).unwrap();
381 let request: ListFilesRequest = serde_json::from_value(value).unwrap();
382
383 assert_eq!(request.exclude_patterns.len(), 1);
384 assert_eq!(request.max_results, 1000);
385 }
386
387 #[test]
388 fn test_parse_invalid_rpc_request() {
389 let invalid_json = "not valid json";
390 let result = parse_rpc_request(invalid_json);
391 result.unwrap_err();
392 }
393
394 #[test]
395 fn test_file_match_rpc_serialization() {
396 let file_match = FileMatchRpc {
397 path: "src/main.rs".to_string(),
398 match_type: FileMatchType::File,
399 score: 100,
400 indices: Some(vec![4, 5]),
401 };
402
403 let json = serde_json::to_string(&file_match).unwrap();
404 let deserialized: FileMatchRpc = serde_json::from_str(&json).unwrap();
405
406 assert_eq!(deserialized.path, "src/main.rs");
407 assert_eq!(deserialized.score, 100u32);
408 assert_eq!(deserialized.indices, Some(vec![4u32, 5u32]));
409 }
410}