mermaid_cli/models/
lazy_context.rs1use anyhow::Result;
2use rustc_hash::FxHashMap;
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::{Arc, Mutex};
6use tokio::sync::RwLock;
7
8#[derive(Debug, Clone)]
10pub struct LazyProjectContext {
11 pub root_path: String,
13 pub project_type: String,
15 pub file_paths: Arc<Vec<PathBuf>>,
17 pub files: Arc<RwLock<FxHashMap<String, String>>>,
19 pub token_count: Arc<AtomicUsize>,
21 pub loading_queue: Arc<Mutex<Vec<PathBuf>>>,
23 pub cache: Option<Arc<crate::cache::CacheManager>>,
25}
26
27impl LazyProjectContext {
28 pub fn new(root_path: String, file_paths: Vec<PathBuf>) -> Self {
30 let cache = crate::cache::CacheManager::new().ok().map(Arc::new);
31
32 Self {
33 root_path: root_path.clone(),
34 project_type: detect_project_type(&root_path),
35 file_paths: Arc::new(file_paths),
36 files: Arc::new(RwLock::new(FxHashMap::default())),
37 token_count: Arc::new(AtomicUsize::new(0)),
38 loading_queue: Arc::new(Mutex::new(Vec::new())),
39 cache,
40 }
41 }
42
43 pub async fn get_file(&self, path: &str) -> Result<Option<String>> {
45 {
47 let files = self.files.read().await;
48 if let Some(content) = files.get(path) {
49 return Ok(Some(content.clone()));
50 }
51 }
52
53 let full_path = if path.starts_with(&self.root_path) {
55 PathBuf::from(path)
56 } else {
57 PathBuf::from(&self.root_path).join(path)
58 };
59
60 if full_path.exists() {
62 let content = tokio::fs::read_to_string(&full_path).await?;
63
64 if let Some(ref cache) = self.cache {
66 if let Ok(tokens) = cache.get_or_compute_tokens(&full_path, &content, "cl100k_base")
67 {
68 self.token_count.fetch_add(tokens, Ordering::Relaxed);
69 }
70 }
71
72 let mut files = self.files.write().await;
74 files.insert(path.to_string(), content.clone());
75
76 Ok(Some(content))
77 } else {
78 Ok(None)
79 }
80 }
81
82 pub async fn load_files_batch(&self, paths: Vec<String>) -> Result<()> {
84 use futures::future::join_all;
85
86 let futures = paths.into_iter().map(|path| {
87 let self_clone = self.clone();
88 async move {
89 let _ = self_clone.get_file(&path).await;
90 }
91 });
92
93 join_all(futures).await;
94 Ok(())
95 }
96
97 pub fn get_file_list(&self) -> Vec<String> {
99 self.file_paths
100 .iter()
101 .filter_map(|p| {
102 p.strip_prefix(&self.root_path)
103 .ok()
104 .and_then(|p| p.to_str())
105 .map(|s| s.to_string())
106 })
107 .collect()
108 }
109
110 pub async fn loaded_file_count(&self) -> usize {
112 self.files.read().await.len()
113 }
114
115 pub fn total_file_count(&self) -> usize {
117 self.file_paths.len()
118 }
119
120 pub async fn is_fully_loaded(&self) -> bool {
122 self.loaded_file_count().await >= self.total_file_count()
123 }
124
125 pub async fn to_project_context(&self) -> crate::models::ProjectContext {
127 let files = self.files.read().await;
128 let mut context = crate::models::ProjectContext::new(self.root_path.clone());
129 context.project_type = Some(self.project_type.clone());
130 context.token_count = self.token_count.load(Ordering::Relaxed);
131
132 for (path, content) in files.iter() {
133 context.add_file(path.clone(), content.clone());
134 }
135
136 context
137 }
138}
139
140fn detect_project_type(root_path: &str) -> String {
142 let path = Path::new(root_path);
143
144 if path.join("Cargo.toml").exists() {
146 "Rust".to_string()
147 } else if path.join("package.json").exists() {
148 "JavaScript/TypeScript".to_string()
149 } else if path.join("requirements.txt").exists() || path.join("setup.py").exists() {
150 "Python".to_string()
151 } else if path.join("go.mod").exists() {
152 "Go".to_string()
153 } else if path.join("pom.xml").exists() || path.join("build.gradle").exists() {
154 "Java".to_string()
155 } else if path.join("*.csproj").exists() || path.join("*.sln").exists() {
156 "C#/.NET".to_string()
157 } else if path.join("Gemfile").exists() {
158 "Ruby".to_string()
159 } else if path.join("composer.json").exists() {
160 "PHP".to_string()
161 } else {
162 "Unknown".to_string()
163 }
164}
165
166pub fn get_priority_files(root_path: &str) -> Vec<String> {
168 vec![
169 "README.md",
170 "readme.md",
171 "README.rst",
172 "README.txt",
173 "CLAUDE.md", "Cargo.toml",
175 "package.json",
176 "pyproject.toml",
177 "go.mod",
178 ".gitignore",
179 "LICENSE",
180 ]
181 .into_iter()
182 .filter_map(|f| {
183 let path = Path::new(root_path).join(f);
184 if path.exists() {
185 Some(f.to_string())
186 } else {
187 None
188 }
189 })
190 .collect()
191}