context_creator/mcp_server/
handlers.rs

1//! RPC method handlers for the MCP server
2
3use super::{
4    CodebaseRpcServer, HealthResponse, HealthRpcServer, ProcessLocalRequest, ProcessLocalResponse,
5    ProcessRemoteRequest, ProcessRemoteResponse,
6};
7use anyhow::Result;
8use jsonrpsee::core::RpcResult;
9use std::path::Path;
10use std::time::{Instant, SystemTime};
11
12/// Implementation of health check RPC methods
13pub struct HealthRpcImpl;
14
15#[jsonrpsee::core::async_trait]
16impl HealthRpcServer for HealthRpcImpl {
17    async fn health_check(&self) -> RpcResult<HealthResponse> {
18        let timestamp = SystemTime::now()
19            .duration_since(SystemTime::UNIX_EPOCH)
20            .map_err(|e| {
21                jsonrpsee::types::ErrorObject::owned(-32000, "Timestamp error", Some(e.to_string()))
22            })?
23            .as_secs();
24
25        Ok(HealthResponse {
26            status: "healthy".to_string(),
27            timestamp,
28            version: env!("CARGO_PKG_VERSION").to_string(),
29        })
30    }
31}
32
33/// Implementation of codebase processing RPC handlers
34pub struct CodebaseRpcImpl {
35    cache: std::sync::Arc<crate::mcp_server::cache::McpCache>,
36}
37
38impl CodebaseRpcImpl {
39    pub fn new(cache: std::sync::Arc<crate::mcp_server::cache::McpCache>) -> Self {
40        Self { cache }
41    }
42}
43
44#[jsonrpsee::core::async_trait]
45impl CodebaseRpcServer for CodebaseRpcImpl {
46    async fn process_local_codebase(
47        &self,
48        request: ProcessLocalRequest,
49    ) -> RpcResult<ProcessLocalResponse> {
50        let start = Instant::now();
51
52        // Validate path security
53        validate_path(&request.path)?;
54
55        // Check cache first
56        let cache_key = crate::mcp_server::cache::ProcessLocalCacheKey::from_request(&request);
57        if let Some(cached) = self.cache.get_process_local(&cache_key).await {
58            let processing_time_ms = start.elapsed().as_millis() as u64;
59            return Ok(ProcessLocalResponse {
60                answer: cached.answer,
61                context: if request.include_context.unwrap_or(false) {
62                    Some(cached.markdown)
63                } else {
64                    None
65                },
66                file_count: cached.file_count,
67                token_count: cached.token_count,
68                processing_time_ms,
69                llm_tool: cached.llm_tool,
70            });
71        }
72
73        // Use blocking task for file I/O
74        let cache = self.cache.clone();
75        let response = tokio::task::spawn_blocking(move || process_codebase_sync(request, start))
76            .await
77            .map_err(|e| {
78                jsonrpsee::types::ErrorObject::owned(-32603, "Internal error", Some(e.to_string()))
79            })?
80            .map_err(|e| {
81                jsonrpsee::types::ErrorObject::owned(
82                    -32603,
83                    "Processing error",
84                    Some(e.to_string()),
85                )
86            })?;
87
88        // Cache the response
89        let cache_value = crate::mcp_server::cache::ProcessLocalCacheValue {
90            answer: response.answer.clone(),
91            markdown: response.context.clone().unwrap_or_default(),
92            file_count: response.file_count,
93            token_count: response.token_count,
94            llm_tool: response.llm_tool.clone(),
95        };
96        cache.set_process_local(cache_key, cache_value).await;
97
98        Ok(response)
99    }
100
101    async fn process_remote_repo(
102        &self,
103        request: ProcessRemoteRequest,
104    ) -> RpcResult<ProcessRemoteResponse> {
105        let start = Instant::now();
106
107        // Validate URL
108        validate_url(&request.repo_url)?;
109
110        // Clone the repository and process it
111        let response = tokio::task::spawn_blocking(move || process_remote_sync(request, start))
112            .await
113            .map_err(|e| {
114                jsonrpsee::types::ErrorObject::owned(-32603, "Internal error", Some(e.to_string()))
115            })?
116            .map_err(|e| {
117                jsonrpsee::types::ErrorObject::owned(
118                    -32603,
119                    "Processing error",
120                    Some(e.to_string()),
121                )
122            })?;
123
124        Ok(response)
125    }
126
127    async fn get_file_metadata(
128        &self,
129        request: super::GetFileMetadataRequest,
130    ) -> RpcResult<super::GetFileMetadataResponse> {
131        // Validate path security
132        validate_path(&request.file_path)?;
133
134        // Use blocking task for file I/O
135        let response = tokio::task::spawn_blocking(move || get_file_metadata_sync(request))
136            .await
137            .map_err(|e| {
138                jsonrpsee::types::ErrorObject::owned(-32603, "Internal error", Some(e.to_string()))
139            })?
140            .map_err(|e| {
141                jsonrpsee::types::ErrorObject::owned(
142                    -32603,
143                    "Processing error",
144                    Some(e.to_string()),
145                )
146            })?;
147
148        Ok(response)
149    }
150
151    async fn search_codebase(
152        &self,
153        request: super::SearchCodebaseRequest,
154    ) -> RpcResult<super::SearchCodebaseResponse> {
155        use std::time::Instant;
156        let start = Instant::now();
157
158        // Validate path security
159        validate_path(&request.path)?;
160
161        // Use blocking task for file I/O
162        let response = tokio::task::spawn_blocking(move || search_codebase_sync(request, start))
163            .await
164            .map_err(|e| {
165                jsonrpsee::types::ErrorObject::owned(-32603, "Internal error", Some(e.to_string()))
166            })?
167            .map_err(|e| {
168                jsonrpsee::types::ErrorObject::owned(-32603, "Search error", Some(e.to_string()))
169            })?;
170
171        Ok(response)
172    }
173
174    async fn diff_files(
175        &self,
176        request: super::DiffFilesRequest,
177    ) -> RpcResult<super::DiffFilesResponse> {
178        // Validate paths security
179        validate_path(&request.file1_path)?;
180        validate_path(&request.file2_path)?;
181
182        // Use blocking task for file I/O
183        let response = tokio::task::spawn_blocking(move || diff_files_sync(request))
184            .await
185            .map_err(|e| {
186                jsonrpsee::types::ErrorObject::owned(-32603, "Internal error", Some(e.to_string()))
187            })?
188            .map_err(|e| {
189                jsonrpsee::types::ErrorObject::owned(-32603, "Diff error", Some(e.to_string()))
190            })?;
191
192        Ok(response)
193    }
194
195    async fn semantic_search(
196        &self,
197        request: super::SemanticSearchRequest,
198    ) -> RpcResult<super::SemanticSearchResponse> {
199        use std::time::Instant;
200        let start = Instant::now();
201
202        // Validate path security
203        validate_path(&request.path)?;
204
205        // Use blocking task for file I/O and AST parsing
206        let response = tokio::task::spawn_blocking(move || semantic_search_sync(request, start))
207            .await
208            .map_err(|e| {
209                jsonrpsee::types::ErrorObject::owned(-32603, "Internal error", Some(e.to_string()))
210            })?
211            .map_err(|e| {
212                jsonrpsee::types::ErrorObject::owned(
213                    -32603,
214                    "Semantic search error",
215                    Some(e.to_string()),
216                )
217            })?;
218
219        Ok(response)
220    }
221}
222
223/// Validate path for security issues
224pub(super) fn validate_path(path: &Path) -> RpcResult<()> {
225    // Check for path traversal attempts
226    let path_str = path.to_string_lossy();
227    if path_str.contains("..") || path_str.contains('~') {
228        return Err(jsonrpsee::types::ErrorObject::owned(
229            -32602,
230            "Invalid path: potential security risk",
231            None::<String>,
232        ));
233    }
234
235    // Ensure path is absolute or relative to current directory
236    if !path.is_absolute() && !path.starts_with(".") && !path.exists() {
237        return Err(jsonrpsee::types::ErrorObject::owned(
238            -32602,
239            "Invalid path: must be absolute or relative to current directory",
240            None::<String>,
241        ));
242    }
243
244    Ok(())
245}
246
247/// Validate URL for remote repositories
248fn validate_url(url: &str) -> RpcResult<()> {
249    // Basic URL validation
250    if !url.starts_with("https://") && !url.starts_with("http://") {
251        return Err(jsonrpsee::types::ErrorObject::owned(
252            -32602,
253            "Invalid URL: must start with http:// or https://",
254            None::<String>,
255        ));
256    }
257
258    // GitHub URL validation
259    if url.contains("github.com") && !url.contains("/") {
260        return Err(jsonrpsee::types::ErrorObject::owned(
261            -32602,
262            "Invalid GitHub URL format",
263            None::<String>,
264        ));
265    }
266
267    Ok(())
268}
269
270/// Synchronous implementation of codebase processing
271pub(super) fn process_codebase_sync(
272    request: ProcessLocalRequest,
273    start: Instant,
274) -> Result<ProcessLocalResponse> {
275    use crate::cli::{Config, LlmTool};
276    use crate::core::cache::FileCache;
277    use crate::core::context_builder::{generate_markdown, ContextOptions};
278    use crate::core::prioritizer::prioritize_files;
279    use crate::core::token::TokenCounter;
280    use crate::core::walker::{walk_directory, WalkOptions};
281    use std::sync::Arc;
282
283    // Determine LLM tool
284    let llm_tool = if let Some(tool_str) = &request.llm_tool {
285        match tool_str.as_str() {
286            "gemini" => LlmTool::Gemini,
287            "codex" => LlmTool::Codex,
288            _ => LlmTool::Gemini,
289        }
290    } else {
291        LlmTool::Gemini
292    };
293
294    // Calculate effective token limit considering prompt
295    let effective_max_tokens = if let Some(max_tokens) = request.max_tokens {
296        max_tokens as usize
297    } else {
298        // Use LLM default if not specified
299        llm_tool.default_max_tokens()
300    };
301
302    // Reserve tokens for prompt and response
303    let prompt_tokens = if let Ok(counter) = TokenCounter::new() {
304        counter
305            .count_tokens(&request.prompt)
306            .unwrap_or(request.prompt.len() / 4)
307    } else {
308        request.prompt.len() / 4 // Rough estimate
309    };
310
311    let safety_buffer = 1000; // For LLM response
312    let context_tokens = effective_max_tokens.saturating_sub(prompt_tokens + safety_buffer);
313
314    // Create a Config from the request
315    let config = Config {
316        paths: Some(vec![request.path.clone()]),
317        include: if request.include_patterns.is_empty() {
318            None
319        } else {
320            Some(request.include_patterns.clone())
321        },
322        ignore: if request.ignore_patterns.is_empty() {
323            None
324        } else {
325            Some(request.ignore_patterns.clone())
326        },
327        trace_imports: request.include_imports,
328        max_tokens: Some(context_tokens),
329        llm_tool,
330        // Enable prompt for proper context calculation
331        prompt: Some(request.prompt.clone()),
332        // Disable other options
333        output_file: None,
334        copy: false,
335        verbose: 0,
336        quiet: true,
337        ..Default::default()
338    };
339
340    // Create walker options
341    let walk_options = WalkOptions::from_config(&config)?;
342
343    // Create context options
344    let context_options = ContextOptions::from_config(&config)?;
345
346    // Create file cache
347    let cache = Arc::new(FileCache::new());
348
349    // Walk the directory
350    let files = walk_directory(&request.path, walk_options)?;
351
352    // Prioritize files if max tokens is set
353    let prioritized_files = if context_options.max_tokens.is_some() {
354        prioritize_files(files, &context_options, cache.clone())?
355    } else {
356        files
357    };
358
359    // Generate markdown
360    let output = generate_markdown(prioritized_files, context_options, cache)?;
361
362    // Count tokens
363    let token_counter = crate::core::token::TokenCounter::new()?;
364    let token_count = token_counter.count_tokens(&output)?;
365
366    // Count files (count markdown headers starting with ##)
367    let file_count = output
368        .lines()
369        .filter(|line| {
370            line.starts_with("## ")
371                && !line.starts_with("## Table of")
372                && !line.starts_with("## Statistics")
373                && !line.starts_with("## File Structure")
374        })
375        .count();
376
377    // Execute LLM with prompt and context
378    let answer = execute_llm_sync(&request.prompt, &output, request.llm_tool.as_deref())?;
379
380    let processing_time_ms = start.elapsed().as_millis() as u64;
381
382    Ok(ProcessLocalResponse {
383        answer,
384        context: if request.include_context.unwrap_or(false) {
385            Some(output)
386        } else {
387            None
388        },
389        file_count,
390        token_count,
391        processing_time_ms,
392        llm_tool: request.llm_tool.unwrap_or_else(|| "gemini".to_string()),
393    })
394}
395
396/// Synchronous implementation of remote repository processing
397pub(super) fn process_remote_sync(
398    request: ProcessRemoteRequest,
399    start: Instant,
400) -> Result<ProcessRemoteResponse> {
401    use crate::cli::Config;
402    use crate::core::cache::FileCache;
403    use crate::core::context_builder::{generate_markdown, ContextOptions};
404    use crate::core::prioritizer::prioritize_files;
405    use crate::core::walker::{walk_directory, WalkOptions};
406    use crate::remote;
407    use std::sync::Arc;
408
409    // Clone the repository
410    let temp_dir = remote::fetch_repository(&request.repo_url, false)?;
411    let repo_path = remote::get_repo_path(&temp_dir, &request.repo_url)?;
412
413    // Extract repo name from URL
414    let repo_name = request
415        .repo_url
416        .split('/')
417        .next_back()
418        .unwrap_or("unknown")
419        .trim_end_matches(".git")
420        .to_string();
421
422    // Determine LLM tool
423    let llm_tool = if let Some(tool_str) = &request.llm_tool {
424        match tool_str.as_str() {
425            "gemini" => crate::cli::LlmTool::Gemini,
426            "codex" => crate::cli::LlmTool::Codex,
427            _ => crate::cli::LlmTool::Gemini,
428        }
429    } else {
430        crate::cli::LlmTool::Gemini
431    };
432
433    // Calculate effective token limit considering prompt
434    let effective_max_tokens = if let Some(max_tokens) = request.max_tokens {
435        max_tokens as usize
436    } else {
437        llm_tool.default_max_tokens()
438    };
439
440    // Reserve tokens for prompt and response
441    let prompt_tokens = if let Ok(counter) = crate::core::token::TokenCounter::new() {
442        counter
443            .count_tokens(&request.prompt)
444            .unwrap_or(request.prompt.len() / 4)
445    } else {
446        request.prompt.len() / 4
447    };
448
449    let safety_buffer = 1000;
450    let context_tokens = effective_max_tokens.saturating_sub(prompt_tokens + safety_buffer);
451
452    // Create a Config from the request
453    let config = Config {
454        paths: Some(vec![repo_path.clone()]),
455        include: if request.include_patterns.is_empty() {
456            None
457        } else {
458            Some(request.include_patterns.clone())
459        },
460        ignore: if request.ignore_patterns.is_empty() {
461            None
462        } else {
463            Some(request.ignore_patterns.clone())
464        },
465        trace_imports: request.include_imports,
466        max_tokens: Some(context_tokens),
467        llm_tool,
468        prompt: Some(request.prompt.clone()),
469        // Disable other options
470        output_file: None,
471        copy: false,
472        verbose: 0,
473        quiet: true,
474        ..Default::default()
475    };
476
477    // Create walker options
478    let walk_options = WalkOptions::from_config(&config)?;
479
480    // Create context options
481    let context_options = ContextOptions::from_config(&config)?;
482
483    // Create file cache
484    let cache = Arc::new(FileCache::new());
485
486    // Walk the directory
487    let files = walk_directory(&repo_path, walk_options)?;
488
489    // Prioritize files if max tokens is set
490    let prioritized_files = if context_options.max_tokens.is_some() {
491        prioritize_files(files, &context_options, cache.clone())?
492    } else {
493        files
494    };
495
496    // Generate markdown
497    let output = generate_markdown(prioritized_files, context_options, cache)?;
498
499    // Count tokens
500    let token_counter = crate::core::token::TokenCounter::new()?;
501    let token_count = token_counter.count_tokens(&output)?;
502
503    // Count files (count markdown headers starting with ##)
504    let file_count = output
505        .lines()
506        .filter(|line| {
507            line.starts_with("## ")
508                && !line.starts_with("## Table of")
509                && !line.starts_with("## Statistics")
510                && !line.starts_with("## File Structure")
511        })
512        .count();
513
514    // Execute LLM with prompt and context
515    let answer = execute_llm_sync(&request.prompt, &output, request.llm_tool.as_deref())?;
516
517    let processing_time_ms = start.elapsed().as_millis() as u64;
518
519    Ok(ProcessRemoteResponse {
520        answer,
521        context: if request.include_context.unwrap_or(false) {
522            Some(output)
523        } else {
524            None
525        },
526        file_count,
527        token_count,
528        processing_time_ms,
529        repo_name,
530        llm_tool: request.llm_tool.unwrap_or_else(|| "gemini".to_string()),
531    })
532}
533
534/// Synchronous implementation of file metadata retrieval
535pub(super) fn get_file_metadata_sync(
536    request: super::GetFileMetadataRequest,
537) -> Result<super::GetFileMetadataResponse> {
538    use std::fs;
539    use std::time::SystemTime;
540
541    // Check if file exists
542    if !request.file_path.exists() {
543        return Err(anyhow::anyhow!(
544            "File not found: {}",
545            request.file_path.display()
546        ));
547    }
548
549    // Get file metadata
550    let metadata = fs::metadata(&request.file_path)?;
551
552    // Get modification time as Unix timestamp
553    let modified = metadata
554        .modified()?
555        .duration_since(SystemTime::UNIX_EPOCH)?
556        .as_secs();
557
558    // Check if it's a symlink
559    let symlink_metadata = fs::symlink_metadata(&request.file_path)?;
560    let is_symlink = symlink_metadata.is_symlink();
561
562    // Determine language from file extension
563    let language = request
564        .file_path
565        .extension()
566        .and_then(|ext| ext.to_str())
567        .and_then(|ext| match ext {
568            "rs" => Some("rust"),
569            "py" => Some("python"),
570            "js" | "jsx" => Some("javascript"),
571            "ts" | "tsx" => Some("typescript"),
572            "go" => Some("go"),
573            "java" => Some("java"),
574            "c" => Some("c"),
575            "cpp" | "cc" | "cxx" => Some("cpp"),
576            "h" | "hpp" => Some("cpp"),
577            "cs" => Some("csharp"),
578            "rb" => Some("ruby"),
579            "php" => Some("php"),
580            "swift" => Some("swift"),
581            "kt" | "kts" => Some("kotlin"),
582            "scala" => Some("scala"),
583            "r" => Some("r"),
584            "lua" => Some("lua"),
585            "dart" => Some("dart"),
586            "jl" => Some("julia"),
587            "hs" => Some("haskell"),
588            "elm" => Some("elm"),
589            "clj" | "cljs" => Some("clojure"),
590            "ex" | "exs" => Some("elixir"),
591            "ml" | "mli" => Some("ocaml"),
592            "nim" => Some("nim"),
593            "zig" => Some("zig"),
594            _ => None,
595        })
596        .map(String::from);
597
598    Ok(super::GetFileMetadataResponse {
599        path: request.file_path,
600        size: metadata.len(),
601        modified,
602        is_symlink,
603        language,
604    })
605}
606
607/// Synchronous implementation of codebase search
608pub(super) fn search_codebase_sync(
609    request: super::SearchCodebaseRequest,
610    start: std::time::Instant,
611) -> Result<super::SearchCodebaseResponse> {
612    use std::fs;
613    use std::io::{BufRead, BufReader};
614    use walkdir::WalkDir;
615
616    let mut results = Vec::new();
617    let mut total_matches = 0;
618    let mut files_searched = 0;
619
620    // Create walker with optional file pattern
621    let walker = WalkDir::new(&request.path)
622        .into_iter()
623        .filter_map(|e| e.ok())
624        .filter(|e| e.file_type().is_file());
625
626    for entry in walker {
627        let path = entry.path();
628
629        // Apply file pattern filter if specified
630        if let Some(ref pattern) = request.file_pattern {
631            let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
632
633            // Simple glob pattern matching
634            if pattern.starts_with("*.") {
635                let extension = pattern.trim_start_matches("*.");
636                if !file_name.ends_with(extension) {
637                    continue;
638                }
639            } else if !file_name.contains(pattern) {
640                continue;
641            }
642        }
643
644        // Skip binary files
645        if is_binary_file(path) {
646            continue;
647        }
648
649        files_searched += 1;
650
651        // Search file content
652        if let Ok(file) = fs::File::open(path) {
653            let reader = BufReader::new(file);
654
655            for (line_number, line_result) in reader.lines().enumerate() {
656                if let Ok(line) = line_result {
657                    if line.to_lowercase().contains(&request.query.to_lowercase()) {
658                        total_matches += 1;
659
660                        // Create search result
661                        let result = super::SearchResult {
662                            file_path: path.to_path_buf(),
663                            line_number: line_number + 1, // 1-based line numbers
664                            line_content: line.clone(),
665                            match_context: get_match_context(&line, &request.query),
666                        };
667
668                        results.push(result);
669
670                        // Check max results limit
671                        if let Some(max) = request.max_results {
672                            if results.len() >= max as usize {
673                                break;
674                            }
675                        }
676                    }
677                }
678            }
679
680            // Stop searching if we've hit the max results
681            if let Some(max) = request.max_results {
682                if results.len() >= max as usize {
683                    break;
684                }
685            }
686        }
687    }
688
689    let search_time_ms = start.elapsed().as_millis() as u64;
690
691    Ok(super::SearchCodebaseResponse {
692        results,
693        total_matches,
694        files_searched,
695        search_time_ms,
696    })
697}
698
699/// Check if a file is likely binary
700fn is_binary_file(path: &Path) -> bool {
701    // Check common binary extensions
702    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
703        if matches!(
704            ext,
705            "exe"
706                | "dll"
707                | "so"
708                | "dylib"
709                | "bin"
710                | "obj"
711                | "o"
712                | "jpg"
713                | "jpeg"
714                | "png"
715                | "gif"
716                | "bmp"
717                | "ico"
718                | "webp"
719                | "mp3"
720                | "mp4"
721                | "avi"
722                | "mov"
723                | "wav"
724                | "flac"
725                | "zip"
726                | "tar"
727                | "gz"
728                | "bz2"
729                | "7z"
730                | "rar"
731                | "pdf"
732                | "doc"
733                | "docx"
734                | "xls"
735                | "xlsx"
736                | "ppt"
737                | "pptx"
738        ) {
739            return true;
740        }
741    }
742
743    // Check by reading first few bytes
744    if let Ok(mut file) = std::fs::File::open(path) {
745        use std::io::Read;
746        let mut buffer = [0; 512];
747        if let Ok(n) = file.read(&mut buffer) {
748            // Check for null bytes (common in binary files)
749            return buffer[..n].contains(&0);
750        }
751    }
752
753    false
754}
755
756/// Get context around the match
757fn get_match_context(line: &str, query: &str) -> String {
758    if let Some(pos) = line.to_lowercase().find(&query.to_lowercase()) {
759        let start = pos.saturating_sub(20);
760        let end = (pos + query.len() + 20).min(line.len());
761
762        let mut context = String::new();
763        if start > 0 {
764            context.push_str("...");
765        }
766        context.push_str(&line[start..end]);
767        if end < line.len() {
768            context.push_str("...");
769        }
770        context
771    } else {
772        line.to_string()
773    }
774}
775
776/// Synchronous implementation of file diff
777pub(super) fn diff_files_sync(
778    request: super::DiffFilesRequest,
779) -> Result<super::DiffFilesResponse> {
780    use std::fs;
781
782    // Check if files exist
783    if !request.file1_path.exists() {
784        return Err(anyhow::anyhow!(
785            "File not found: {}",
786            request.file1_path.display()
787        ));
788    }
789    if !request.file2_path.exists() {
790        return Err(anyhow::anyhow!(
791            "File not found: {}",
792            request.file2_path.display()
793        ));
794    }
795
796    // Read files
797    let content1 = fs::read(&request.file1_path)?;
798    let content2 = fs::read(&request.file2_path)?;
799
800    // Check if either file is binary
801    let is_binary = content1.contains(&0) || content2.contains(&0);
802
803    if is_binary {
804        return Ok(super::DiffFilesResponse {
805            file1_path: request.file1_path,
806            file2_path: request.file2_path,
807            hunks: vec![],
808            added_lines: 0,
809            removed_lines: 0,
810            is_binary: true,
811        });
812    }
813
814    // Convert to strings for text diff
815    let text1 = String::from_utf8_lossy(&content1);
816    let text2 = String::from_utf8_lossy(&content2);
817
818    // Split into lines
819    let lines1: Vec<&str> = text1.lines().collect();
820    let lines2: Vec<&str> = text2.lines().collect();
821
822    // Perform simple line-by-line diff
823    let hunks = compute_diff(&lines1, &lines2, request.context_lines.unwrap_or(3));
824
825    // Count added and removed lines
826    let mut added_lines = 0;
827    let mut removed_lines = 0;
828    for hunk in &hunks {
829        for line in hunk.content.lines() {
830            if line.starts_with('+') && !line.starts_with("+++") {
831                added_lines += 1;
832            } else if line.starts_with('-') && !line.starts_with("---") {
833                removed_lines += 1;
834            }
835        }
836    }
837
838    Ok(super::DiffFilesResponse {
839        file1_path: request.file1_path,
840        file2_path: request.file2_path,
841        hunks,
842        added_lines,
843        removed_lines,
844        is_binary: false,
845    })
846}
847
848/// Compute diff between two sets of lines
849fn compute_diff(lines1: &[&str], lines2: &[&str], context_lines: u32) -> Vec<super::DiffHunk> {
850    use std::cmp::min;
851
852    let mut hunks = Vec::new();
853    let mut i = 0;
854    let mut j = 0;
855
856    while i < lines1.len() || j < lines2.len() {
857        // Find next difference
858        while i < lines1.len() && j < lines2.len() && lines1[i] == lines2[j] {
859            i += 1;
860            j += 1;
861        }
862
863        if i >= lines1.len() && j >= lines2.len() {
864            break;
865        }
866
867        // Start of a hunk
868        let hunk_start_i = i.saturating_sub(context_lines as usize);
869        let hunk_start_j = j.saturating_sub(context_lines as usize);
870        let mut hunk_content = String::new();
871
872        // Add context before
873        for k in hunk_start_i..i {
874            if k < lines1.len() {
875                hunk_content.push_str(&format!(" {}\n", lines1[k]));
876            }
877        }
878
879        // Find end of differences
880        let mut end_i = i;
881        let mut end_j = j;
882
883        // Simple algorithm: advance until we find matching lines again
884        while end_i < lines1.len() || end_j < lines2.len() {
885            let mut found_match = false;
886
887            // Check if we can find a match by advancing either side
888            if end_i < lines1.len() && end_j < lines2.len() && lines1[end_i] == lines2[end_j] {
889                found_match = true;
890            }
891
892            if found_match {
893                // Check if we have enough context lines matching
894                let mut k = 0;
895                while k < context_lines as usize
896                    && end_i + k < lines1.len()
897                    && end_j + k < lines2.len()
898                    && lines1[end_i + k] == lines2[end_j + k]
899                {
900                    k += 1;
901                }
902
903                if k >= context_lines as usize {
904                    break;
905                }
906            }
907
908            // Add removed lines
909            if end_i < lines1.len()
910                && (end_j >= lines2.len()
911                    || (end_i < lines1.len()
912                        && end_j < lines2.len()
913                        && lines1[end_i] != lines2[end_j]))
914            {
915                hunk_content.push_str(&format!("-{}\n", lines1[end_i]));
916                end_i += 1;
917            }
918
919            // Add added lines
920            if end_j < lines2.len()
921                && (end_i >= lines1.len()
922                    || (end_i < lines1.len()
923                        && end_j < lines2.len()
924                        && lines1[end_i - 1] != lines2[end_j]))
925            {
926                hunk_content.push_str(&format!("+{}\n", lines2[end_j]));
927                end_j += 1;
928            }
929        }
930
931        // Add context after
932        let context_end_i = min(end_i + context_lines as usize, lines1.len());
933        let context_end_j = min(end_j + context_lines as usize, lines2.len());
934
935        for k in end_i..context_end_i {
936            if k < lines1.len() && k - end_i < context_end_j - end_j {
937                hunk_content.push_str(&format!(" {}\n", lines1[k]));
938            }
939        }
940
941        // Create hunk
942        let hunk = super::DiffHunk {
943            old_start: hunk_start_i + 1, // 1-based
944            old_lines: end_i - hunk_start_i,
945            new_start: hunk_start_j + 1, // 1-based
946            new_lines: end_j - hunk_start_j,
947            content: hunk_content,
948        };
949
950        hunks.push(hunk);
951
952        // Move to next position
953        i = end_i;
954        j = end_j;
955    }
956
957    hunks
958}
959
960/// Synchronous implementation of semantic search
961pub(super) fn semantic_search_sync(
962    request: super::SemanticSearchRequest,
963    start: std::time::Instant,
964) -> Result<super::SemanticSearchResponse> {
965    use crate::core::semantic::{get_analyzer_for_file, SemanticContext};
966    use std::fs;
967    use walkdir::WalkDir;
968
969    let mut results = Vec::new();
970    let mut total_matches = 0;
971    let mut files_analyzed = 0;
972
973    // Walk through files
974    let walker = WalkDir::new(&request.path)
975        .into_iter()
976        .filter_map(|e| e.ok())
977        .filter(|e| e.file_type().is_file());
978
979    for entry in walker {
980        let path = entry.path();
981
982        // Skip non-source files
983        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
984        if !is_supported_language_file(ext) {
985            continue;
986        }
987
988        // Get analyzer for the file
989        let analyzer = match get_analyzer_for_file(path)? {
990            Some(analyzer) => analyzer,
991            None => continue, // No analyzer for this file type
992        };
993
994        // Read file content
995        let content = match fs::read_to_string(path) {
996            Ok(content) => content,
997            Err(_) => continue,
998        };
999
1000        files_analyzed += 1;
1001
1002        // Create semantic context
1003        let context = SemanticContext::new(
1004            path.to_path_buf(),
1005            request.path.clone(),
1006            3, // max depth
1007        );
1008
1009        // Analyze file
1010        if let Ok(analysis) = analyzer.analyze_file(path, &content, &context) {
1011            match request.search_type {
1012                super::SemanticSearchType::Functions => {
1013                    // Search for function definitions
1014                    for func in &analysis.exported_functions {
1015                        if func
1016                            .name
1017                            .to_lowercase()
1018                            .contains(&request.query.to_lowercase())
1019                        {
1020                            total_matches += 1;
1021
1022                            let result = super::SemanticSearchResult {
1023                                file_path: path.to_path_buf(),
1024                                symbol_name: func.name.clone(),
1025                                symbol_type: "function".to_string(),
1026                                line_number: func.line + 1, // Convert to 1-based
1027                                context: format!(
1028                                    "{}function {}",
1029                                    if func.is_exported { "pub " } else { "" },
1030                                    func.name
1031                                ),
1032                            };
1033
1034                            results.push(result);
1035
1036                            if let Some(max) = request.max_results {
1037                                if results.len() >= max as usize {
1038                                    break;
1039                                }
1040                            }
1041                        }
1042                    }
1043                }
1044                super::SemanticSearchType::Types => {
1045                    // Search for type references
1046                    // Note: This searches for type usage, not definitions
1047                    // Full type definition search would require AST parsing
1048                    for typ in &analysis.type_references {
1049                        if typ
1050                            .name
1051                            .to_lowercase()
1052                            .contains(&request.query.to_lowercase())
1053                        {
1054                            total_matches += 1;
1055
1056                            let result = super::SemanticSearchResult {
1057                                file_path: path.to_path_buf(),
1058                                symbol_name: typ.name.clone(),
1059                                symbol_type: "type".to_string(),
1060                                line_number: typ.line + 1,
1061                                context: format!("Type reference: {}", typ.name),
1062                            };
1063
1064                            results.push(result);
1065
1066                            if let Some(max) = request.max_results {
1067                                if results.len() >= max as usize {
1068                                    break;
1069                                }
1070                            }
1071                        }
1072                    }
1073                }
1074                super::SemanticSearchType::Imports => {
1075                    // Search for imports
1076                    for import in &analysis.imports {
1077                        if import
1078                            .module
1079                            .to_lowercase()
1080                            .contains(&request.query.to_lowercase())
1081                            || import.items.iter().any(|item| {
1082                                item.to_lowercase().contains(&request.query.to_lowercase())
1083                            })
1084                        {
1085                            total_matches += 1;
1086
1087                            let context = if import.items.is_empty() {
1088                                format!("import {}", import.module)
1089                            } else {
1090                                format!(
1091                                    "import {{ {} }} from {}",
1092                                    import.items.join(", "),
1093                                    import.module
1094                                )
1095                            };
1096
1097                            let result = super::SemanticSearchResult {
1098                                file_path: path.to_path_buf(),
1099                                symbol_name: import.module.clone(),
1100                                symbol_type: "import".to_string(),
1101                                line_number: import.line + 1,
1102                                context,
1103                            };
1104
1105                            results.push(result);
1106
1107                            if let Some(max) = request.max_results {
1108                                if results.len() >= max as usize {
1109                                    break;
1110                                }
1111                            }
1112                        }
1113                    }
1114                }
1115                super::SemanticSearchType::References => {
1116                    // Search for function calls that match the query
1117                    for call in &analysis.function_calls {
1118                        if call
1119                            .name
1120                            .to_lowercase()
1121                            .contains(&request.query.to_lowercase())
1122                        {
1123                            total_matches += 1;
1124
1125                            let result = super::SemanticSearchResult {
1126                                file_path: path.to_path_buf(),
1127                                symbol_name: call.name.clone(),
1128                                symbol_type: "reference".to_string(),
1129                                line_number: call.line + 1,
1130                                context: format!("Function call: {}", call.name),
1131                            };
1132
1133                            results.push(result);
1134
1135                            if let Some(max) = request.max_results {
1136                                if results.len() >= max as usize {
1137                                    break;
1138                                }
1139                            }
1140                        }
1141                    }
1142
1143                    // Also search type references
1144                    for typ in &analysis.type_references {
1145                        if typ
1146                            .name
1147                            .to_lowercase()
1148                            .contains(&request.query.to_lowercase())
1149                        {
1150                            total_matches += 1;
1151
1152                            let result = super::SemanticSearchResult {
1153                                file_path: path.to_path_buf(),
1154                                symbol_name: typ.name.clone(),
1155                                symbol_type: "reference".to_string(),
1156                                line_number: typ.line + 1,
1157                                context: format!("Type usage: {}", typ.name),
1158                            };
1159
1160                            results.push(result);
1161
1162                            if let Some(max) = request.max_results {
1163                                if results.len() >= max as usize {
1164                                    break;
1165                                }
1166                            }
1167                        }
1168                    }
1169                }
1170            }
1171        }
1172
1173        // Stop if we've hit the max results
1174        if let Some(max) = request.max_results {
1175            if results.len() >= max as usize {
1176                break;
1177            }
1178        }
1179    }
1180
1181    let search_time_ms = start.elapsed().as_millis() as u64;
1182
1183    Ok(super::SemanticSearchResponse {
1184        results,
1185        total_matches,
1186        files_analyzed,
1187        search_time_ms,
1188    })
1189}
1190
1191/// Check if file extension is a supported language
1192fn is_supported_language_file(ext: &str) -> bool {
1193    matches!(
1194        ext,
1195        "rs" | "py"
1196            | "js"
1197            | "jsx"
1198            | "ts"
1199            | "tsx"
1200            | "go"
1201            | "java"
1202            | "c"
1203            | "cpp"
1204            | "cc"
1205            | "cxx"
1206            | "h"
1207            | "hpp"
1208            | "cs"
1209            | "rb"
1210            | "php"
1211            | "swift"
1212            | "kt"
1213            | "kts"
1214            | "scala"
1215            | "r"
1216            | "lua"
1217            | "dart"
1218            | "jl"
1219            | "hs"
1220            | "elm"
1221            | "clj"
1222            | "cljs"
1223            | "ex"
1224            | "exs"
1225            | "ml"
1226            | "mli"
1227            | "nim"
1228            | "zig"
1229    )
1230}
1231
1232/// Execute LLM with prompt and context
1233fn execute_llm_sync(prompt: &str, context: &str, llm_tool: Option<&str>) -> Result<String> {
1234    use crate::cli::LlmTool;
1235    use crate::utils::error::ContextCreatorError;
1236    use std::io::Write;
1237    use std::process::{Command, Stdio};
1238
1239    // Determine which LLM tool to use
1240    let tool = if let Some(tool_str) = llm_tool {
1241        match tool_str {
1242            "gemini" => LlmTool::Gemini,
1243            "codex" => LlmTool::Codex,
1244            _ => LlmTool::Gemini, // Default to gemini for unknown tools
1245        }
1246    } else {
1247        LlmTool::Gemini // Default
1248    };
1249
1250    let full_input = format!("{prompt}\n\n{context}");
1251    let tool_command = tool.command();
1252
1253    let mut child = Command::new(tool_command)
1254        .stdin(Stdio::piped())
1255        .stdout(Stdio::piped())
1256        .stderr(Stdio::piped())
1257        .spawn()
1258        .map_err(|e| {
1259            if e.kind() == std::io::ErrorKind::NotFound {
1260                ContextCreatorError::LlmToolNotFound {
1261                    tool: tool_command.to_string(),
1262                    install_instructions: tool.install_instructions().to_string(),
1263                }
1264            } else {
1265                ContextCreatorError::SubprocessError(e.to_string())
1266            }
1267        })?;
1268
1269    // Write input to stdin
1270    if let Some(mut stdin) = child.stdin.take() {
1271        stdin.write_all(full_input.as_bytes())?;
1272        stdin.flush()?;
1273    }
1274
1275    // Capture output
1276    let output = child.wait_with_output()?;
1277
1278    if !output.status.success() {
1279        let stderr = String::from_utf8_lossy(&output.stderr);
1280        return Err(ContextCreatorError::SubprocessError(format!(
1281            "{tool_command} failed: {stderr}"
1282        ))
1283        .into());
1284    }
1285
1286    Ok(String::from_utf8_lossy(&output.stdout).into_owned())
1287}