Skip to main content

code_analyze_mcp/
completion.rs

1//! Path completion support for file and directory paths.
2//!
3//! Provides completion suggestions for partial paths within a directory tree,
4//! respecting .gitignore and .ignore files.
5
6use crate::cache::AnalysisCache;
7use ignore::WalkBuilder;
8use std::path::Path;
9use tracing::instrument;
10
11/// Get path completions for a given prefix within a root directory.
12/// Uses ignore crate with standard filters to respect .gitignore.
13/// Returns matching file and directory paths up to 100 results.
14#[instrument(skip_all, fields(prefix = %prefix))]
15pub fn path_completions(root: &Path, prefix: &str) -> Vec<String> {
16    if prefix.is_empty() {
17        return Vec::new();
18    }
19
20    // Determine the search directory and filename prefix
21    let (search_dir, name_prefix) = if let Some(last_slash) = prefix.rfind('/') {
22        let dir_part = &prefix[..=last_slash];
23        let name_part = &prefix[last_slash + 1..];
24        let full_path = root.join(dir_part);
25        (full_path, name_part.to_string())
26    } else {
27        (root.to_path_buf(), prefix.to_string())
28    };
29
30    // If search directory doesn't exist, return empty
31    if !search_dir.exists() {
32        return Vec::new();
33    }
34
35    let mut results = Vec::new();
36
37    // Walk with depth 1 to get immediate children
38    let mut builder = WalkBuilder::new(&search_dir);
39    builder
40        .hidden(true)
41        .standard_filters(true)
42        .max_depth(Some(1));
43
44    for result in builder.build() {
45        if results.len() >= 100 {
46            break;
47        }
48
49        match result {
50            Ok(entry) => {
51                let path = entry.path();
52                // Skip the root directory itself
53                if path == search_dir {
54                    continue;
55                }
56
57                // Get the filename
58                if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
59                    && file_name.starts_with(&name_prefix)
60                {
61                    // Construct relative path from root
62                    if let Ok(rel_path) = path.strip_prefix(root) {
63                        let rel_str = rel_path.to_string_lossy().to_string();
64                        results.push(rel_str);
65                    }
66                }
67            }
68            Err(_) => {
69                // Skip unreadable entries
70                continue;
71            }
72        }
73    }
74
75    results
76}
77
78/// Get symbol completions (function and class names) for a given file path.
79/// Looks up cached FileAnalysisOutput and extracts matching symbols.
80/// Returns matching function and class names up to 100 results.
81#[instrument(skip(cache), fields(path = %path.display(), prefix = %prefix))]
82pub fn symbol_completions(cache: &AnalysisCache, path: &Path, prefix: &str) -> Vec<String> {
83    if prefix.is_empty() {
84        return Vec::new();
85    }
86
87    // Get file metadata for cache key
88    let cache_key = match std::fs::metadata(path) {
89        Ok(meta) => match meta.modified() {
90            Ok(mtime) => crate::cache::CacheKey {
91                path: path.to_path_buf(),
92                modified: mtime,
93                mode: crate::types::AnalysisMode::FileDetails,
94            },
95            Err(_) => return Vec::new(),
96        },
97        Err(_) => return Vec::new(),
98    };
99
100    // Look up in cache
101    let cached = match cache.get(&cache_key) {
102        Some(output) => output,
103        None => return Vec::new(),
104    };
105
106    let mut results = Vec::new();
107
108    // Extract function names matching prefix
109    for func in &cached.semantic.functions {
110        if results.len() >= 100 {
111            break;
112        }
113        if func.name.starts_with(prefix) {
114            results.push(func.name.clone());
115        }
116    }
117
118    // Extract class names matching prefix
119    for class in &cached.semantic.classes {
120        if results.len() >= 100 {
121            break;
122        }
123        if class.name.starts_with(prefix) {
124            results.push(class.name.clone());
125        }
126    }
127
128    results
129}