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(&self) -> &RefCell<Option<crate::semantic_index::EmbeddingModel>> {
193 &self.semantic_embedding_model
194 }
195
196 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
198 &self.watcher
199 }
200
201 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
203 &self.watcher_rx
204 }
205
206 pub fn lsp(&self) -> RefMut<'_, LspManager> {
208 self.lsp_manager.borrow_mut()
209 }
210
211 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
214 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
215 if let Err(e) = lsp.notify_file_changed(file_path, content) {
216 log::warn!("sync error for {}: {}", file_path.display(), e);
217 }
218 }
219 }
220
221 pub fn lsp_notify_and_collect_diagnostics(
227 &self,
228 file_path: &Path,
229 content: &str,
230 timeout: std::time::Duration,
231 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
232 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
233 return Vec::new();
234 };
235
236 lsp.drain_events();
239
240 if let Err(e) = lsp.notify_file_changed(file_path, content) {
242 log::warn!("sync error for {}: {}", file_path.display(), e);
243 return Vec::new();
244 }
245
246 lsp.wait_for_diagnostics(file_path, timeout)
248 }
249
250 pub fn lsp_post_write(
257 &self,
258 file_path: &Path,
259 content: &str,
260 params: &serde_json::Value,
261 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
262 let wants_diagnostics = params
263 .get("diagnostics")
264 .and_then(|v| v.as_bool())
265 .unwrap_or(false);
266
267 if !wants_diagnostics {
268 self.lsp_notify_file_changed(file_path, content);
269 return Vec::new();
270 }
271
272 let wait_ms = params
273 .get("wait_ms")
274 .and_then(|v| v.as_u64())
275 .unwrap_or(1500)
276 .min(10_000); self.lsp_notify_and_collect_diagnostics(
279 file_path,
280 content,
281 std::time::Duration::from_millis(wait_ms),
282 )
283 }
284
285 pub fn validate_path(
294 &self,
295 req_id: &str,
296 path: &Path,
297 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
298 let config = self.config();
299 if !config.restrict_to_project_root {
301 return Ok(path.to_path_buf());
302 }
303 let root = match &config.project_root {
304 Some(r) => r.clone(),
305 None => return Ok(path.to_path_buf()), };
307 drop(config);
308
309 let resolved = std::fs::canonicalize(path)
311 .unwrap_or_else(|_| resolve_with_existing_ancestors(&normalize_path(path)));
312
313 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
314
315 if !resolved.starts_with(&resolved_root) {
316 return Err(crate::protocol::Response::error(
317 req_id,
318 "path_outside_root",
319 format!(
320 "path '{}' is outside the project root '{}'",
321 path.display(),
322 resolved_root.display()
323 ),
324 ));
325 }
326
327 Ok(resolved)
328 }
329
330 pub fn lsp_server_count(&self) -> usize {
332 self.lsp_manager
333 .try_borrow()
334 .map(|lsp| lsp.server_count())
335 .unwrap_or(0)
336 }
337
338 pub fn symbol_cache_stats(&self) -> serde_json::Value {
340 if let Some(tsp) = self
341 .provider
342 .as_any()
343 .downcast_ref::<crate::parser::TreeSitterProvider>()
344 {
345 let (local, warm) = tsp.symbol_cache_stats();
346 serde_json::json!({
347 "local_entries": local,
348 "warm_entries": warm,
349 })
350 } else {
351 serde_json::json!({
352 "local_entries": 0,
353 "warm_entries": 0,
354 })
355 }
356 }
357}