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}
248
249impl AppContext {
250 pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
251 let progress_sender = Arc::new(Mutex::new(None));
252 let stdout_writer = Arc::new(Mutex::new(BufWriter::new(io::stdout())));
253 let symbol_cache = provider
254 .as_any()
255 .downcast_ref::<crate::parser::TreeSitterProvider>()
256 .map(|provider| provider.symbol_cache())
257 .unwrap_or_else(|| Arc::new(std::sync::RwLock::new(SymbolCache::new())));
258 let lsp_child_registry = crate::lsp::child_registry::LspChildRegistry::new();
259 let mut lsp_manager = LspManager::new();
260 lsp_manager.set_child_registry(lsp_child_registry.clone());
261 AppContext {
262 provider,
263 backup: RefCell::new(BackupStore::new()),
264 checkpoint: RefCell::new(CheckpointStore::new()),
265 config: RefCell::new(config),
266 callgraph: RefCell::new(None),
267 search_index: RefCell::new(None),
268 search_index_rx: RefCell::new(None),
269 symbol_cache,
270 semantic_index: RefCell::new(None),
271 semantic_index_rx: RefCell::new(None),
272 semantic_index_status: RefCell::new(SemanticIndexStatus::Disabled),
273 semantic_embedding_model: RefCell::new(None),
274 watcher: RefCell::new(None),
275 watcher_rx: RefCell::new(None),
276 lsp_manager: RefCell::new(lsp_manager),
277 lsp_child_registry,
278 stdout_writer,
279 progress_sender: Arc::clone(&progress_sender),
280 bash_background: BgTaskRegistry::new(progress_sender),
281 }
282 }
283
284 pub fn lsp_child_registry(&self) -> crate::lsp::child_registry::LspChildRegistry {
287 self.lsp_child_registry.clone()
288 }
289
290 pub fn stdout_writer(&self) -> SharedStdoutWriter {
291 Arc::clone(&self.stdout_writer)
292 }
293
294 pub fn set_progress_sender(&self, sender: Option<ProgressSender>) {
295 if let Ok(mut progress_sender) = self.progress_sender.lock() {
296 *progress_sender = sender;
297 }
298 }
299
300 pub fn emit_progress(&self, frame: ProgressFrame) {
301 let Ok(progress_sender) = self.progress_sender.lock().map(|sender| sender.clone()) else {
302 return;
303 };
304 if let Some(sender) = progress_sender.as_ref() {
305 sender(PushFrame::Progress(frame));
306 }
307 }
308
309 pub fn progress_sender_handle(&self) -> Option<ProgressSender> {
317 self.progress_sender
318 .lock()
319 .ok()
320 .and_then(|sender| sender.clone())
321 }
322
323 pub fn bash_background(&self) -> &BgTaskRegistry {
324 &self.bash_background
325 }
326
327 pub fn drain_bg_completions(&self) -> Vec<BgCompletion> {
328 self.bash_background.drain_completions()
329 }
330
331 pub fn provider(&self) -> &dyn LanguageProvider {
333 self.provider.as_ref()
334 }
335
336 pub fn backup(&self) -> &RefCell<BackupStore> {
338 &self.backup
339 }
340
341 pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
343 &self.checkpoint
344 }
345
346 pub fn config(&self) -> Ref<'_, Config> {
348 self.config.borrow()
349 }
350
351 pub fn config_mut(&self) -> RefMut<'_, Config> {
353 self.config.borrow_mut()
354 }
355
356 pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
358 &self.callgraph
359 }
360
361 pub fn search_index(&self) -> &RefCell<Option<SearchIndex>> {
363 &self.search_index
364 }
365
366 pub fn search_index_rx(&self) -> &RefCell<Option<crossbeam_channel::Receiver<SearchIndex>>> {
368 &self.search_index_rx
369 }
370
371 pub fn symbol_cache(&self) -> SharedSymbolCache {
373 Arc::clone(&self.symbol_cache)
374 }
375
376 pub fn reset_symbol_cache(&self) -> u64 {
378 self.symbol_cache
379 .write()
380 .map(|mut cache| cache.reset())
381 .unwrap_or(0)
382 }
383
384 pub fn semantic_index(&self) -> &RefCell<Option<SemanticIndex>> {
386 &self.semantic_index
387 }
388
389 pub fn semantic_index_rx(
391 &self,
392 ) -> &RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>> {
393 &self.semantic_index_rx
394 }
395
396 pub fn semantic_index_status(&self) -> &RefCell<SemanticIndexStatus> {
397 &self.semantic_index_status
398 }
399
400 pub fn semantic_embedding_model(
402 &self,
403 ) -> &RefCell<Option<crate::semantic_index::EmbeddingModel>> {
404 &self.semantic_embedding_model
405 }
406
407 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
409 &self.watcher
410 }
411
412 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
414 &self.watcher_rx
415 }
416
417 pub fn lsp(&self) -> RefMut<'_, LspManager> {
419 self.lsp_manager.borrow_mut()
420 }
421
422 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
425 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
426 let config = self.config();
427 if let Err(e) = lsp.notify_file_changed(file_path, content, &config) {
428 log::warn!("sync error for {}: {}", file_path.display(), e);
429 }
430 }
431 }
432
433 pub fn lsp_notify_and_collect_diagnostics(
444 &self,
445 file_path: &Path,
446 content: &str,
447 timeout: std::time::Duration,
448 ) -> crate::lsp::manager::PostEditWaitOutcome {
449 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
450 return crate::lsp::manager::PostEditWaitOutcome::default();
451 };
452
453 lsp.drain_events();
456
457 let pre_snapshot = lsp.snapshot_diagnostic_epochs(file_path);
461
462 let config = self.config();
464 let expected_versions = match lsp.notify_file_changed_versioned(file_path, content, &config)
465 {
466 Ok(v) => v,
467 Err(e) => {
468 log::warn!("sync error for {}: {}", file_path.display(), e);
469 return crate::lsp::manager::PostEditWaitOutcome::default();
470 }
471 };
472
473 if expected_versions.is_empty() {
476 return crate::lsp::manager::PostEditWaitOutcome::default();
477 }
478
479 lsp.wait_for_post_edit_diagnostics(
480 file_path,
481 &config,
482 &expected_versions,
483 &pre_snapshot,
484 timeout,
485 )
486 }
487
488 fn custom_lsp_root_markers(&self) -> Vec<String> {
491 self.config()
492 .lsp_servers
493 .iter()
494 .flat_map(|s| s.root_markers.iter().cloned())
495 .collect()
496 }
497
498 fn notify_watched_config_files(&self, file_paths: &[PathBuf]) {
499 let custom_markers = self.custom_lsp_root_markers();
500 let config_paths: Vec<(PathBuf, FileChangeType)> = file_paths
501 .iter()
502 .filter(|path| is_config_file_path_with_custom(path, &custom_markers))
503 .cloned()
504 .map(|path| {
505 let change_type = if path.exists() {
506 FileChangeType::CHANGED
507 } else {
508 FileChangeType::DELETED
509 };
510 (path, change_type)
511 })
512 .collect();
513
514 self.notify_watched_config_events(&config_paths);
515 }
516
517 fn multi_file_write_paths(params: &serde_json::Value) -> Option<Vec<PathBuf>> {
518 let paths = params
519 .get("multi_file_write_paths")
520 .and_then(|value| value.as_array())?
521 .iter()
522 .filter_map(|value| value.as_str())
523 .map(PathBuf::from)
524 .collect::<Vec<_>>();
525
526 (!paths.is_empty()).then_some(paths)
527 }
528
529 fn watched_file_events_from_params(
541 params: &serde_json::Value,
542 extra_markers: &[String],
543 ) -> Option<Vec<(PathBuf, FileChangeType)>> {
544 let events = params
545 .get("multi_file_write_paths")
546 .and_then(|value| value.as_array())?
547 .iter()
548 .filter_map(|entry| {
549 let path = entry
551 .get("path")
552 .and_then(|value| value.as_str())
553 .map(PathBuf::from)?;
554
555 if !is_config_file_path_with_custom(&path, extra_markers) {
556 return None;
557 }
558
559 let change_type = entry
560 .get("type")
561 .and_then(|value| value.as_str())
562 .and_then(Self::parse_file_change_type)
563 .unwrap_or_else(|| Self::change_type_from_current_state(&path));
564
565 Some((path, change_type))
566 })
567 .collect::<Vec<_>>();
568
569 (!events.is_empty()).then_some(events)
570 }
571
572 fn parse_file_change_type(value: &str) -> Option<FileChangeType> {
573 match value {
574 "created" | "CREATED" | "Created" => Some(FileChangeType::CREATED),
575 "changed" | "CHANGED" | "Changed" => Some(FileChangeType::CHANGED),
576 "deleted" | "DELETED" | "Deleted" => Some(FileChangeType::DELETED),
577 _ => None,
578 }
579 }
580
581 fn change_type_from_current_state(path: &Path) -> FileChangeType {
582 if path.exists() {
583 FileChangeType::CHANGED
584 } else {
585 FileChangeType::DELETED
586 }
587 }
588
589 fn notify_watched_config_events(&self, config_paths: &[(PathBuf, FileChangeType)]) {
590 if config_paths.is_empty() {
591 return;
592 }
593
594 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
595 let config = self.config();
596 if let Err(e) = lsp.notify_files_watched_changed(config_paths, &config) {
597 log::warn!("watched-file sync error: {}", e);
598 }
599 }
600 }
601
602 pub fn lsp_notify_watched_config_file(&self, file_path: &Path, change_type: FileChangeType) {
603 let custom_markers = self.custom_lsp_root_markers();
604 if !is_config_file_path_with_custom(file_path, &custom_markers) {
605 return;
606 }
607
608 self.notify_watched_config_events(&[(file_path.to_path_buf(), change_type)]);
609 }
610
611 pub fn lsp_post_multi_file_write(
616 &self,
617 file_path: &Path,
618 content: &str,
619 file_paths: &[PathBuf],
620 params: &serde_json::Value,
621 ) -> Option<crate::lsp::manager::PostEditWaitOutcome> {
622 self.notify_watched_config_files(file_paths);
623
624 let wants_diagnostics = params
625 .get("diagnostics")
626 .and_then(|v| v.as_bool())
627 .unwrap_or(false);
628
629 if !wants_diagnostics {
630 self.lsp_notify_file_changed(file_path, content);
631 return None;
632 }
633
634 let wait_ms = params
635 .get("wait_ms")
636 .and_then(|v| v.as_u64())
637 .unwrap_or(3000)
638 .min(10_000);
639
640 Some(self.lsp_notify_and_collect_diagnostics(
641 file_path,
642 content,
643 std::time::Duration::from_millis(wait_ms),
644 ))
645 }
646
647 pub fn lsp_post_write(
664 &self,
665 file_path: &Path,
666 content: &str,
667 params: &serde_json::Value,
668 ) -> Option<crate::lsp::manager::PostEditWaitOutcome> {
669 let wants_diagnostics = params
670 .get("diagnostics")
671 .and_then(|v| v.as_bool())
672 .unwrap_or(false);
673
674 let custom_markers = self.custom_lsp_root_markers();
675
676 if !wants_diagnostics {
677 if let Some(file_paths) = Self::multi_file_write_paths(params) {
678 self.notify_watched_config_files(&file_paths);
679 } else if let Some(config_events) =
680 Self::watched_file_events_from_params(params, &custom_markers)
681 {
682 self.notify_watched_config_events(&config_events);
683 }
684 self.lsp_notify_file_changed(file_path, content);
685 return None;
686 }
687
688 let wait_ms = params
689 .get("wait_ms")
690 .and_then(|v| v.as_u64())
691 .unwrap_or(3000)
692 .min(10_000); if let Some(file_paths) = Self::multi_file_write_paths(params) {
695 return self.lsp_post_multi_file_write(file_path, content, &file_paths, params);
696 }
697
698 if let Some(config_events) = Self::watched_file_events_from_params(params, &custom_markers)
699 {
700 self.notify_watched_config_events(&config_events);
701 }
702
703 Some(self.lsp_notify_and_collect_diagnostics(
704 file_path,
705 content,
706 std::time::Duration::from_millis(wait_ms),
707 ))
708 }
709
710 pub fn validate_path(
719 &self,
720 req_id: &str,
721 path: &Path,
722 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
723 let config = self.config();
724 if !config.restrict_to_project_root {
726 return Ok(path.to_path_buf());
727 }
728 let root = match &config.project_root {
729 Some(r) => r.clone(),
730 None => return Ok(path.to_path_buf()), };
732 drop(config);
733
734 let raw_root = root.clone();
739 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
740
741 let resolved = match std::fs::canonicalize(path) {
746 Ok(resolved) => resolved,
747 Err(_) => {
748 let normalized = normalize_path(path);
749 reject_escaping_symlink(req_id, path, &normalized, &resolved_root, &raw_root)?;
750 resolve_with_existing_ancestors(&normalized)
751 }
752 };
753
754 if !resolved.starts_with(&resolved_root) {
755 return Err(path_error_response(req_id, path, &resolved_root));
756 }
757
758 Ok(resolved)
759 }
760
761 pub fn lsp_server_count(&self) -> usize {
763 self.lsp_manager
764 .try_borrow()
765 .map(|lsp| lsp.server_count())
766 .unwrap_or(0)
767 }
768
769 pub fn symbol_cache_stats(&self) -> serde_json::Value {
771 let entries = self
772 .symbol_cache
773 .read()
774 .map(|cache| cache.len())
775 .unwrap_or(0);
776 serde_json::json!({
777 "local_entries": entries,
778 "warm_entries": 0,
779 })
780 }
781}