1use std::cell::{Ref, RefCell, RefMut};
2use std::path::{Component, Path, PathBuf};
3use std::sync::mpsc;
4
5use notify::RecommendedWatcher;
6
7use crate::backup::BackupStore;
8use crate::callgraph::CallGraph;
9use crate::checkpoint::CheckpointStore;
10use crate::config::Config;
11use crate::language::LanguageProvider;
12use crate::lsp::manager::LspManager;
13use crate::search_index::SearchIndex;
14use crate::semantic_index::SemanticIndex;
15
16#[derive(Debug, Clone)]
17pub enum SemanticIndexStatus {
18 Disabled,
19 Building {
20 stage: String,
21 files: Option<usize>,
22 entries_done: Option<usize>,
23 entries_total: Option<usize>,
24 },
25 Ready,
26 Failed(String),
27}
28
29pub enum SemanticIndexEvent {
30 Progress {
31 stage: String,
32 files: Option<usize>,
33 entries_done: Option<usize>,
34 entries_total: Option<usize>,
35 },
36 Ready(SemanticIndex),
37 Failed(String),
38}
39
40fn normalize_path(path: &Path) -> PathBuf {
44 let mut result = PathBuf::new();
45 for component in path.components() {
46 match component {
47 Component::ParentDir => {
48 if !result.pop() {
50 result.push(component);
51 }
52 }
53 Component::CurDir => {} _ => result.push(component),
55 }
56 }
57 result
58}
59
60fn resolve_with_existing_ancestors(path: &Path) -> PathBuf {
61 let mut existing = path.to_path_buf();
62 let mut tail_segments = Vec::new();
63
64 while !existing.exists() {
65 if let Some(name) = existing.file_name() {
66 tail_segments.push(name.to_owned());
67 } else {
68 break;
69 }
70
71 existing = match existing.parent() {
72 Some(parent) => parent.to_path_buf(),
73 None => break,
74 };
75 }
76
77 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
78 for segment in tail_segments.into_iter().rev() {
79 resolved.push(segment);
80 }
81
82 resolved
83}
84
85pub struct AppContext {
95 provider: Box<dyn LanguageProvider>,
96 backup: RefCell<BackupStore>,
97 checkpoint: RefCell<CheckpointStore>,
98 config: RefCell<Config>,
99 callgraph: RefCell<Option<CallGraph>>,
100 search_index: RefCell<Option<SearchIndex>>,
101 search_index_rx:
102 RefCell<Option<crossbeam_channel::Receiver<(SearchIndex, crate::parser::SymbolCache)>>>,
103 semantic_index: RefCell<Option<SemanticIndex>>,
104 semantic_index_rx: RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>>,
105 semantic_index_status: RefCell<SemanticIndexStatus>,
106 semantic_embedding_model: RefCell<Option<crate::semantic_index::EmbeddingModel>>,
107 watcher: RefCell<Option<RecommendedWatcher>>,
108 watcher_rx: RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>>,
109 lsp_manager: RefCell<LspManager>,
110}
111
112impl AppContext {
113 pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
114 AppContext {
115 provider,
116 backup: RefCell::new(BackupStore::new()),
117 checkpoint: RefCell::new(CheckpointStore::new()),
118 config: RefCell::new(config),
119 callgraph: RefCell::new(None),
120 search_index: RefCell::new(None),
121 search_index_rx: RefCell::new(None),
122 semantic_index: RefCell::new(None),
123 semantic_index_rx: RefCell::new(None),
124 semantic_index_status: RefCell::new(SemanticIndexStatus::Disabled),
125 semantic_embedding_model: RefCell::new(None),
126 watcher: RefCell::new(None),
127 watcher_rx: RefCell::new(None),
128 lsp_manager: RefCell::new(LspManager::new()),
129 }
130 }
131
132 pub fn provider(&self) -> &dyn LanguageProvider {
134 self.provider.as_ref()
135 }
136
137 pub fn backup(&self) -> &RefCell<BackupStore> {
139 &self.backup
140 }
141
142 pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
144 &self.checkpoint
145 }
146
147 pub fn config(&self) -> Ref<'_, Config> {
149 self.config.borrow()
150 }
151
152 pub fn config_mut(&self) -> RefMut<'_, Config> {
154 self.config.borrow_mut()
155 }
156
157 pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
159 &self.callgraph
160 }
161
162 pub fn search_index(&self) -> &RefCell<Option<SearchIndex>> {
164 &self.search_index
165 }
166
167 pub fn search_index_rx(
169 &self,
170 ) -> &RefCell<Option<crossbeam_channel::Receiver<(SearchIndex, crate::parser::SymbolCache)>>>
171 {
172 &self.search_index_rx
173 }
174
175 pub fn semantic_index(&self) -> &RefCell<Option<SemanticIndex>> {
177 &self.semantic_index
178 }
179
180 pub fn semantic_index_rx(
182 &self,
183 ) -> &RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>> {
184 &self.semantic_index_rx
185 }
186
187 pub fn semantic_index_status(&self) -> &RefCell<SemanticIndexStatus> {
188 &self.semantic_index_status
189 }
190
191 pub fn semantic_embedding_model(
193 &self,
194 ) -> &RefCell<Option<crate::semantic_index::EmbeddingModel>> {
195 &self.semantic_embedding_model
196 }
197
198 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
200 &self.watcher
201 }
202
203 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
205 &self.watcher_rx
206 }
207
208 pub fn lsp(&self) -> RefMut<'_, LspManager> {
210 self.lsp_manager.borrow_mut()
211 }
212
213 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
216 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
217 let config = self.config();
218 if let Err(e) = lsp.notify_file_changed(file_path, content, &config) {
219 log::warn!("sync error for {}: {}", file_path.display(), e);
220 }
221 }
222 }
223
224 pub fn lsp_notify_and_collect_diagnostics(
230 &self,
231 file_path: &Path,
232 content: &str,
233 timeout: std::time::Duration,
234 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
235 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
236 return Vec::new();
237 };
238
239 lsp.drain_events();
242
243 let config = self.config();
245 if let Err(e) = lsp.notify_file_changed(file_path, content, &config) {
246 log::warn!("sync error for {}: {}", file_path.display(), e);
247 return Vec::new();
248 }
249
250 lsp.wait_for_diagnostics(file_path, &config, timeout)
252 }
253
254 pub fn lsp_post_write(
261 &self,
262 file_path: &Path,
263 content: &str,
264 params: &serde_json::Value,
265 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
266 let wants_diagnostics = params
267 .get("diagnostics")
268 .and_then(|v| v.as_bool())
269 .unwrap_or(false);
270
271 if !wants_diagnostics {
272 self.lsp_notify_file_changed(file_path, content);
273 return Vec::new();
274 }
275
276 let wait_ms = params
277 .get("wait_ms")
278 .and_then(|v| v.as_u64())
279 .unwrap_or(1500)
280 .min(10_000); self.lsp_notify_and_collect_diagnostics(
283 file_path,
284 content,
285 std::time::Duration::from_millis(wait_ms),
286 )
287 }
288
289 pub fn validate_path(
298 &self,
299 req_id: &str,
300 path: &Path,
301 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
302 let config = self.config();
303 if !config.restrict_to_project_root {
305 return Ok(path.to_path_buf());
306 }
307 let root = match &config.project_root {
308 Some(r) => r.clone(),
309 None => return Ok(path.to_path_buf()), };
311 drop(config);
312
313 let resolved = std::fs::canonicalize(path)
315 .unwrap_or_else(|_| resolve_with_existing_ancestors(&normalize_path(path)));
316
317 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
318
319 if !resolved.starts_with(&resolved_root) {
320 return Err(crate::protocol::Response::error(
321 req_id,
322 "path_outside_root",
323 format!(
324 "path '{}' is outside the project root '{}'",
325 path.display(),
326 resolved_root.display()
327 ),
328 ));
329 }
330
331 Ok(resolved)
332 }
333
334 pub fn lsp_server_count(&self) -> usize {
336 self.lsp_manager
337 .try_borrow()
338 .map(|lsp| lsp.server_count())
339 .unwrap_or(0)
340 }
341
342 pub fn symbol_cache_stats(&self) -> serde_json::Value {
344 if let Some(tsp) = self
345 .provider
346 .as_any()
347 .downcast_ref::<crate::parser::TreeSitterProvider>()
348 {
349 let (local, warm) = tsp.symbol_cache_stats();
350 serde_json::json!({
351 "local_entries": local,
352 "warm_entries": warm,
353 })
354 } else {
355 serde_json::json!({
356 "local_entries": 0,
357 "warm_entries": 0,
358 })
359 }
360 }
361}