1use std::cell::{Ref, RefCell, RefMut};
2use std::io::{self, BufWriter};
3use std::path::{Component, Path, PathBuf};
4use std::sync::{mpsc, Arc, Mutex};
5
6use lsp_types::FileChangeType;
7use notify::RecommendedWatcher;
8
9use crate::backup::BackupStore;
10use crate::bash_background::{BgCompletion, BgTaskRegistry};
11use crate::callgraph::CallGraph;
12use crate::checkpoint::CheckpointStore;
13use crate::config::Config;
14use crate::language::LanguageProvider;
15use crate::lsp::manager::LspManager;
16use crate::lsp::registry::is_config_file_path_with_custom;
17use crate::parser::{SharedSymbolCache, SymbolCache};
18use crate::protocol::{ProgressFrame, PushFrame};
19
20pub type ProgressSender = Arc<Box<dyn Fn(PushFrame) + Send + Sync>>;
21pub type SharedProgressSender = Arc<Mutex<Option<ProgressSender>>>;
22pub type SharedStdoutWriter = Arc<Mutex<BufWriter<io::Stdout>>>;
23use crate::search_index::SearchIndex;
24use crate::semantic_index::SemanticIndex;
25
26#[derive(Debug, Clone)]
27pub enum SemanticIndexStatus {
28 Disabled,
29 Building {
30 stage: String,
31 files: Option<usize>,
32 entries_done: Option<usize>,
33 entries_total: Option<usize>,
34 },
35 Ready,
36 Failed(String),
37}
38
39pub enum SemanticIndexEvent {
40 Progress {
41 stage: String,
42 files: Option<usize>,
43 entries_done: Option<usize>,
44 entries_total: Option<usize>,
45 },
46 Ready(SemanticIndex),
47 Failed(String),
48}
49
50fn normalize_path(path: &Path) -> PathBuf {
54 let mut result = PathBuf::new();
55 for component in path.components() {
56 match component {
57 Component::ParentDir => {
58 if !result.pop() {
60 result.push(component);
61 }
62 }
63 Component::CurDir => {} _ => result.push(component),
65 }
66 }
67 result
68}
69
70fn resolve_with_existing_ancestors(path: &Path) -> PathBuf {
71 let mut existing = path.to_path_buf();
72 let mut tail_segments = Vec::new();
73
74 while !existing.exists() {
75 if let Some(name) = existing.file_name() {
76 tail_segments.push(name.to_owned());
77 } else {
78 break;
79 }
80
81 existing = match existing.parent() {
82 Some(parent) => parent.to_path_buf(),
83 None => break,
84 };
85 }
86
87 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
88 for segment in tail_segments.into_iter().rev() {
89 resolved.push(segment);
90 }
91
92 resolved
93}
94
95fn path_error_response(
96 req_id: &str,
97 path: &Path,
98 resolved_root: &Path,
99) -> crate::protocol::Response {
100 crate::protocol::Response::error(
101 req_id,
102 "path_outside_root",
103 format!(
104 "path '{}' is outside the project root '{}'",
105 path.display(),
106 resolved_root.display()
107 ),
108 )
109}
110
111fn reject_escaping_symlink(
121 req_id: &str,
122 original_path: &Path,
123 candidate: &Path,
124 resolved_root: &Path,
125 raw_root: &Path,
126) -> Result<(), crate::protocol::Response> {
127 let mut current = PathBuf::new();
128
129 for component in candidate.components() {
130 current.push(component);
131
132 let Ok(metadata) = std::fs::symlink_metadata(¤t) else {
133 continue;
134 };
135
136 if !metadata.file_type().is_symlink() {
137 continue;
138 }
139
140 let inside_root = current.starts_with(resolved_root) || current.starts_with(raw_root);
149 if !inside_root {
150 continue;
151 }
152
153 iterative_follow_chain(req_id, original_path, ¤t, resolved_root)?;
154 }
155
156 Ok(())
157}
158
159fn iterative_follow_chain(
162 req_id: &str,
163 original_path: &Path,
164 start: &Path,
165 resolved_root: &Path,
166) -> Result<(), crate::protocol::Response> {
167 let mut link = start.to_path_buf();
168 let mut depth = 0usize;
169
170 loop {
171 if depth > 40 {
172 return Err(path_error_response(req_id, original_path, resolved_root));
173 }
174
175 let target = match std::fs::read_link(&link) {
176 Ok(t) => t,
177 Err(_) => {
178 return Err(path_error_response(req_id, original_path, resolved_root));
180 }
181 };
182
183 let resolved_target = if target.is_absolute() {
184 normalize_path(&target)
185 } else {
186 let parent = link.parent().unwrap_or_else(|| Path::new(""));
187 normalize_path(&parent.join(&target))
188 };
189
190 let canonical_target =
194 std::fs::canonicalize(&resolved_target).unwrap_or_else(|_| resolved_target.clone());
195
196 if !canonical_target.starts_with(resolved_root)
197 && !resolved_target.starts_with(resolved_root)
198 {
199 return Err(path_error_response(req_id, original_path, resolved_root));
200 }
201
202 match std::fs::symlink_metadata(&resolved_target) {
204 Ok(meta) if meta.file_type().is_symlink() => {
205 link = resolved_target;
206 depth += 1;
207 }
208 _ => break, }
210 }
211
212 Ok(())
213}
214
215pub struct AppContext {
225 provider: Box<dyn LanguageProvider>,
226 backup: RefCell<BackupStore>,
227 checkpoint: RefCell<CheckpointStore>,
228 config: RefCell<Config>,
229 callgraph: RefCell<Option<CallGraph>>,
230 search_index: RefCell<Option<SearchIndex>>,
231 search_index_rx: RefCell<Option<crossbeam_channel::Receiver<SearchIndex>>>,
232 symbol_cache: SharedSymbolCache,
233 semantic_index: RefCell<Option<SemanticIndex>>,
234 semantic_index_rx: RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>>,
235 semantic_index_status: RefCell<SemanticIndexStatus>,
236 semantic_embedding_model: RefCell<Option<crate::semantic_index::EmbeddingModel>>,
237 watcher: RefCell<Option<RecommendedWatcher>>,
238 watcher_rx: RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>>,
239 lsp_manager: RefCell<LspManager>,
240 lsp_child_registry: crate::lsp::child_registry::LspChildRegistry,
244 stdout_writer: SharedStdoutWriter,
245 progress_sender: SharedProgressSender,
246 bash_background: BgTaskRegistry,
247 filter_registry: crate::compress::SharedFilterRegistry,
254 filter_registry_loaded: std::sync::atomic::AtomicBool,
257 bash_compress_flag: Arc<std::sync::atomic::AtomicBool>,
262}
263
264impl AppContext {
265 pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
266 let bash_compress_enabled = config.experimental_bash_compress;
267 let progress_sender = Arc::new(Mutex::new(None));
268 let stdout_writer = Arc::new(Mutex::new(BufWriter::new(io::stdout())));
269 let symbol_cache = provider
270 .as_any()
271 .downcast_ref::<crate::parser::TreeSitterProvider>()
272 .map(|provider| provider.symbol_cache())
273 .unwrap_or_else(|| Arc::new(std::sync::RwLock::new(SymbolCache::new())));
274 let lsp_child_registry = crate::lsp::child_registry::LspChildRegistry::new();
275 let mut lsp_manager = LspManager::new();
276 lsp_manager.set_child_registry(lsp_child_registry.clone());
277 AppContext {
278 provider,
279 backup: RefCell::new(BackupStore::new()),
280 checkpoint: RefCell::new(CheckpointStore::new()),
281 config: RefCell::new(config),
282 callgraph: RefCell::new(None),
283 search_index: RefCell::new(None),
284 search_index_rx: RefCell::new(None),
285 symbol_cache,
286 semantic_index: RefCell::new(None),
287 semantic_index_rx: RefCell::new(None),
288 semantic_index_status: RefCell::new(SemanticIndexStatus::Disabled),
289 semantic_embedding_model: RefCell::new(None),
290 watcher: RefCell::new(None),
291 watcher_rx: RefCell::new(None),
292 lsp_manager: RefCell::new(lsp_manager),
293 lsp_child_registry,
294 stdout_writer,
295 progress_sender: Arc::clone(&progress_sender),
296 bash_background: BgTaskRegistry::new(progress_sender),
297 filter_registry: Arc::new(std::sync::RwLock::new(
298 crate::compress::toml_filter::FilterRegistry::default(),
299 )),
300 filter_registry_loaded: std::sync::atomic::AtomicBool::new(false),
301 bash_compress_flag: Arc::new(std::sync::atomic::AtomicBool::new(bash_compress_enabled)),
302 }
303 }
304
305 pub fn bash_compress_flag(&self) -> Arc<std::sync::atomic::AtomicBool> {
308 Arc::clone(&self.bash_compress_flag)
309 }
310
311 pub fn sync_bash_compress_flag(&self) {
315 let value = self.config().experimental_bash_compress;
316 self.bash_compress_flag
317 .store(value, std::sync::atomic::Ordering::Relaxed);
318 }
319
320 pub fn set_bash_compress_enabled(&self, enabled: bool) {
321 self.config_mut().experimental_bash_compress = enabled;
322 self.bash_compress_flag
323 .store(enabled, std::sync::atomic::Ordering::Relaxed);
324 }
325
326 pub fn filter_registry(
330 &self,
331 ) -> std::sync::RwLockReadGuard<'_, crate::compress::toml_filter::FilterRegistry> {
332 self.ensure_filter_registry_loaded();
333 match self.filter_registry.read() {
334 Ok(g) => g,
335 Err(poisoned) => poisoned.into_inner(),
336 }
337 }
338
339 pub fn shared_filter_registry(&self) -> crate::compress::SharedFilterRegistry {
343 self.ensure_filter_registry_loaded();
344 Arc::clone(&self.filter_registry)
345 }
346
347 pub fn reset_filter_registry(&self) {
351 let new_registry = crate::compress::build_registry_for_context(self);
352 match self.filter_registry.write() {
353 Ok(mut slot) => *slot = new_registry,
354 Err(poisoned) => *poisoned.into_inner() = new_registry,
355 }
356 self.filter_registry_loaded
357 .store(true, std::sync::atomic::Ordering::Release);
358 }
359
360 fn ensure_filter_registry_loaded(&self) {
361 use std::sync::atomic::Ordering;
362 if self.filter_registry_loaded.load(Ordering::Acquire) {
363 return;
364 }
365 let new_registry = crate::compress::build_registry_for_context(self);
368 if let Ok(mut slot) = self.filter_registry.write() {
369 *slot = new_registry;
370 self.filter_registry_loaded.store(true, Ordering::Release);
371 }
372 }
373
374 pub fn lsp_child_registry(&self) -> crate::lsp::child_registry::LspChildRegistry {
377 self.lsp_child_registry.clone()
378 }
379
380 pub fn stdout_writer(&self) -> SharedStdoutWriter {
381 Arc::clone(&self.stdout_writer)
382 }
383
384 pub fn set_progress_sender(&self, sender: Option<ProgressSender>) {
385 if let Ok(mut progress_sender) = self.progress_sender.lock() {
386 *progress_sender = sender;
387 }
388 }
389
390 pub fn emit_progress(&self, frame: ProgressFrame) {
391 let Ok(progress_sender) = self.progress_sender.lock().map(|sender| sender.clone()) else {
392 return;
393 };
394 if let Some(sender) = progress_sender.as_ref() {
395 sender(PushFrame::Progress(frame));
396 }
397 }
398
399 pub fn progress_sender_handle(&self) -> Option<ProgressSender> {
407 self.progress_sender
408 .lock()
409 .ok()
410 .and_then(|sender| sender.clone())
411 }
412
413 pub fn bash_background(&self) -> &BgTaskRegistry {
414 &self.bash_background
415 }
416
417 pub fn drain_bg_completions(&self) -> Vec<BgCompletion> {
418 self.bash_background.drain_completions()
419 }
420
421 pub fn provider(&self) -> &dyn LanguageProvider {
423 self.provider.as_ref()
424 }
425
426 pub fn backup(&self) -> &RefCell<BackupStore> {
428 &self.backup
429 }
430
431 pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
433 &self.checkpoint
434 }
435
436 pub fn config(&self) -> Ref<'_, Config> {
438 self.config.borrow()
439 }
440
441 pub fn config_mut(&self) -> RefMut<'_, Config> {
443 self.config.borrow_mut()
444 }
445
446 pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
448 &self.callgraph
449 }
450
451 pub fn search_index(&self) -> &RefCell<Option<SearchIndex>> {
453 &self.search_index
454 }
455
456 pub fn search_index_rx(&self) -> &RefCell<Option<crossbeam_channel::Receiver<SearchIndex>>> {
458 &self.search_index_rx
459 }
460
461 pub fn symbol_cache(&self) -> SharedSymbolCache {
463 Arc::clone(&self.symbol_cache)
464 }
465
466 pub fn reset_symbol_cache(&self) -> u64 {
468 self.symbol_cache
469 .write()
470 .map(|mut cache| cache.reset())
471 .unwrap_or(0)
472 }
473
474 pub fn semantic_index(&self) -> &RefCell<Option<SemanticIndex>> {
476 &self.semantic_index
477 }
478
479 pub fn semantic_index_rx(
481 &self,
482 ) -> &RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>> {
483 &self.semantic_index_rx
484 }
485
486 pub fn semantic_index_status(&self) -> &RefCell<SemanticIndexStatus> {
487 &self.semantic_index_status
488 }
489
490 pub fn semantic_embedding_model(
492 &self,
493 ) -> &RefCell<Option<crate::semantic_index::EmbeddingModel>> {
494 &self.semantic_embedding_model
495 }
496
497 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
499 &self.watcher
500 }
501
502 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
504 &self.watcher_rx
505 }
506
507 pub fn lsp(&self) -> RefMut<'_, LspManager> {
509 self.lsp_manager.borrow_mut()
510 }
511
512 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
515 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
516 let config = self.config();
517 if let Err(e) = lsp.notify_file_changed(file_path, content, &config) {
518 log::warn!("sync error for {}: {}", file_path.display(), e);
519 }
520 }
521 }
522
523 pub fn lsp_notify_and_collect_diagnostics(
534 &self,
535 file_path: &Path,
536 content: &str,
537 timeout: std::time::Duration,
538 ) -> crate::lsp::manager::PostEditWaitOutcome {
539 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
540 return crate::lsp::manager::PostEditWaitOutcome::default();
541 };
542
543 lsp.drain_events();
546
547 let pre_snapshot = lsp.snapshot_diagnostic_epochs(file_path);
551
552 let config = self.config();
554 let expected_versions = match lsp.notify_file_changed_versioned(file_path, content, &config)
555 {
556 Ok(v) => v,
557 Err(e) => {
558 log::warn!("sync error for {}: {}", file_path.display(), e);
559 return crate::lsp::manager::PostEditWaitOutcome::default();
560 }
561 };
562
563 if expected_versions.is_empty() {
566 return crate::lsp::manager::PostEditWaitOutcome::default();
567 }
568
569 lsp.wait_for_post_edit_diagnostics(
570 file_path,
571 &config,
572 &expected_versions,
573 &pre_snapshot,
574 timeout,
575 )
576 }
577
578 fn custom_lsp_root_markers(&self) -> Vec<String> {
581 self.config()
582 .lsp_servers
583 .iter()
584 .flat_map(|s| s.root_markers.iter().cloned())
585 .collect()
586 }
587
588 fn notify_watched_config_files(&self, file_paths: &[PathBuf]) {
589 let custom_markers = self.custom_lsp_root_markers();
590 let config_paths: Vec<(PathBuf, FileChangeType)> = file_paths
591 .iter()
592 .filter(|path| is_config_file_path_with_custom(path, &custom_markers))
593 .cloned()
594 .map(|path| {
595 let change_type = if path.exists() {
596 FileChangeType::CHANGED
597 } else {
598 FileChangeType::DELETED
599 };
600 (path, change_type)
601 })
602 .collect();
603
604 self.notify_watched_config_events(&config_paths);
605 }
606
607 fn multi_file_write_paths(params: &serde_json::Value) -> Option<Vec<PathBuf>> {
608 let paths = params
609 .get("multi_file_write_paths")
610 .and_then(|value| value.as_array())?
611 .iter()
612 .filter_map(|value| value.as_str())
613 .map(PathBuf::from)
614 .collect::<Vec<_>>();
615
616 (!paths.is_empty()).then_some(paths)
617 }
618
619 fn watched_file_events_from_params(
631 params: &serde_json::Value,
632 extra_markers: &[String],
633 ) -> Option<Vec<(PathBuf, FileChangeType)>> {
634 let events = params
635 .get("multi_file_write_paths")
636 .and_then(|value| value.as_array())?
637 .iter()
638 .filter_map(|entry| {
639 let path = entry
641 .get("path")
642 .and_then(|value| value.as_str())
643 .map(PathBuf::from)?;
644
645 if !is_config_file_path_with_custom(&path, extra_markers) {
646 return None;
647 }
648
649 let change_type = entry
650 .get("type")
651 .and_then(|value| value.as_str())
652 .and_then(Self::parse_file_change_type)
653 .unwrap_or_else(|| Self::change_type_from_current_state(&path));
654
655 Some((path, change_type))
656 })
657 .collect::<Vec<_>>();
658
659 (!events.is_empty()).then_some(events)
660 }
661
662 fn parse_file_change_type(value: &str) -> Option<FileChangeType> {
663 match value {
664 "created" | "CREATED" | "Created" => Some(FileChangeType::CREATED),
665 "changed" | "CHANGED" | "Changed" => Some(FileChangeType::CHANGED),
666 "deleted" | "DELETED" | "Deleted" => Some(FileChangeType::DELETED),
667 _ => None,
668 }
669 }
670
671 fn change_type_from_current_state(path: &Path) -> FileChangeType {
672 if path.exists() {
673 FileChangeType::CHANGED
674 } else {
675 FileChangeType::DELETED
676 }
677 }
678
679 fn notify_watched_config_events(&self, config_paths: &[(PathBuf, FileChangeType)]) {
680 if config_paths.is_empty() {
681 return;
682 }
683
684 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
685 let config = self.config();
686 if let Err(e) = lsp.notify_files_watched_changed(config_paths, &config) {
687 log::warn!("watched-file sync error: {}", e);
688 }
689 }
690 }
691
692 pub fn lsp_notify_watched_config_file(&self, file_path: &Path, change_type: FileChangeType) {
693 let custom_markers = self.custom_lsp_root_markers();
694 if !is_config_file_path_with_custom(file_path, &custom_markers) {
695 return;
696 }
697
698 self.notify_watched_config_events(&[(file_path.to_path_buf(), change_type)]);
699 }
700
701 pub fn lsp_post_multi_file_write(
706 &self,
707 file_path: &Path,
708 content: &str,
709 file_paths: &[PathBuf],
710 params: &serde_json::Value,
711 ) -> Option<crate::lsp::manager::PostEditWaitOutcome> {
712 self.notify_watched_config_files(file_paths);
713
714 let wants_diagnostics = params
715 .get("diagnostics")
716 .and_then(|v| v.as_bool())
717 .unwrap_or(false);
718
719 if !wants_diagnostics {
720 self.lsp_notify_file_changed(file_path, content);
721 return None;
722 }
723
724 let wait_ms = params
725 .get("wait_ms")
726 .and_then(|v| v.as_u64())
727 .unwrap_or(3000)
728 .min(10_000);
729
730 Some(self.lsp_notify_and_collect_diagnostics(
731 file_path,
732 content,
733 std::time::Duration::from_millis(wait_ms),
734 ))
735 }
736
737 pub fn lsp_post_write(
754 &self,
755 file_path: &Path,
756 content: &str,
757 params: &serde_json::Value,
758 ) -> Option<crate::lsp::manager::PostEditWaitOutcome> {
759 let wants_diagnostics = params
760 .get("diagnostics")
761 .and_then(|v| v.as_bool())
762 .unwrap_or(false);
763
764 let custom_markers = self.custom_lsp_root_markers();
765
766 if !wants_diagnostics {
767 if let Some(file_paths) = Self::multi_file_write_paths(params) {
768 self.notify_watched_config_files(&file_paths);
769 } else if let Some(config_events) =
770 Self::watched_file_events_from_params(params, &custom_markers)
771 {
772 self.notify_watched_config_events(&config_events);
773 }
774 self.lsp_notify_file_changed(file_path, content);
775 return None;
776 }
777
778 let wait_ms = params
779 .get("wait_ms")
780 .and_then(|v| v.as_u64())
781 .unwrap_or(3000)
782 .min(10_000); if let Some(file_paths) = Self::multi_file_write_paths(params) {
785 return self.lsp_post_multi_file_write(file_path, content, &file_paths, params);
786 }
787
788 if let Some(config_events) = Self::watched_file_events_from_params(params, &custom_markers)
789 {
790 self.notify_watched_config_events(&config_events);
791 }
792
793 Some(self.lsp_notify_and_collect_diagnostics(
794 file_path,
795 content,
796 std::time::Duration::from_millis(wait_ms),
797 ))
798 }
799
800 pub fn validate_path(
809 &self,
810 req_id: &str,
811 path: &Path,
812 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
813 let config = self.config();
814 if !config.restrict_to_project_root {
816 return Ok(path.to_path_buf());
817 }
818 let root = match &config.project_root {
819 Some(r) => r.clone(),
820 None => return Ok(path.to_path_buf()), };
822 drop(config);
823
824 let raw_root = root.clone();
829 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
830
831 let resolved = match std::fs::canonicalize(path) {
836 Ok(resolved) => resolved,
837 Err(_) => {
838 let normalized = normalize_path(path);
839 reject_escaping_symlink(req_id, path, &normalized, &resolved_root, &raw_root)?;
840 resolve_with_existing_ancestors(&normalized)
841 }
842 };
843
844 if !resolved.starts_with(&resolved_root) {
845 return Err(path_error_response(req_id, path, &resolved_root));
846 }
847
848 Ok(resolved)
849 }
850
851 pub fn lsp_server_count(&self) -> usize {
853 self.lsp_manager
854 .try_borrow()
855 .map(|lsp| lsp.server_count())
856 .unwrap_or(0)
857 }
858
859 pub fn symbol_cache_stats(&self) -> serde_json::Value {
861 let entries = self
862 .symbol_cache
863 .read()
864 .map(|cache| cache.len())
865 .unwrap_or(0);
866 serde_json::json!({
867 "local_entries": entries,
868 "warm_entries": 0,
869 })
870 }
871}