code_analyze_core/
completion.rs1use crate::cache::AnalysisCache;
9use ignore::WalkBuilder;
10use std::path::Path;
11use tracing::instrument;
12
13#[instrument(skip_all, fields(prefix = %prefix))]
17pub fn path_completions(root: &Path, prefix: &str) -> Vec<String> {
18 if prefix.is_empty() {
19 return Vec::new();
20 }
21
22 let (search_dir, name_prefix) = if let Some(last_slash) = prefix.rfind('/') {
24 let dir_part = &prefix[..=last_slash];
25 let name_part = &prefix[last_slash + 1..];
26 let full_path = root.join(dir_part);
27 (full_path, name_part.to_string())
28 } else {
29 (root.to_path_buf(), prefix.to_string())
30 };
31
32 if !search_dir.exists() {
34 return Vec::new();
35 }
36
37 let mut results = Vec::new();
38
39 let mut builder = WalkBuilder::new(&search_dir);
41 builder
42 .hidden(true)
43 .standard_filters(true)
44 .max_depth(Some(1));
45
46 for result in builder.build() {
47 if results.len() >= 100 {
48 break;
49 }
50
51 let Ok(entry) = result else { continue };
52 let path = entry.path();
53 if path == search_dir {
55 continue;
56 }
57 if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
59 && file_name.starts_with(&name_prefix)
60 {
61 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
69 results
70}
71
72#[instrument(skip(cache), fields(path = %path.display(), prefix = %prefix))]
76pub fn symbol_completions(cache: &AnalysisCache, path: &Path, prefix: &str) -> Vec<String> {
77 if prefix.is_empty() {
78 return Vec::new();
79 }
80
81 let cache_key = match std::fs::metadata(path) {
83 Ok(meta) => match meta.modified() {
84 Ok(mtime) => crate::cache::CacheKey {
85 path: path.to_path_buf(),
86 modified: mtime,
87 mode: crate::types::AnalysisMode::FileDetails,
88 },
89 Err(_) => return Vec::new(),
90 },
91 Err(_) => return Vec::new(),
92 };
93
94 let Some(cached) = cache.get(&cache_key) else {
96 return Vec::new();
97 };
98
99 let mut results = Vec::new();
100
101 for func in &cached.semantic.functions {
103 if results.len() >= 100 {
104 break;
105 }
106 if func.name.starts_with(prefix) {
107 results.push(func.name.clone());
108 }
109 }
110
111 for class in &cached.semantic.classes {
113 if results.len() >= 100 {
114 break;
115 }
116 if class.name.starts_with(prefix) {
117 results.push(class.name.clone());
118 }
119 }
120
121 results
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use std::fs;
128 use tempfile::TempDir;
129
130 #[test]
131 fn test_path_completions_slash_prefix() {
132 let temp = TempDir::new().unwrap();
134 let root = temp.path();
135 fs::create_dir(root.join("src")).unwrap();
136 fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
137 let results = path_completions(root, "src/ma");
139 assert!(
141 results.iter().any(|r| r.contains("main.rs")),
142 "expected 'main.rs' in completions, got {:?}",
143 results
144 );
145 }
146
147 #[test]
148 fn test_path_completions_empty_prefix() {
151 let temp = TempDir::new().unwrap();
153 let results = path_completions(temp.path(), "");
154 assert!(
155 results.is_empty(),
156 "expected empty results for empty prefix"
157 );
158 }
159}