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 stdout_writer: SharedStdoutWriter,
241 progress_sender: SharedProgressSender,
242 bash_background: BgTaskRegistry,
243}
244
245impl AppContext {
246 pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
247 let progress_sender = Arc::new(Mutex::new(None));
248 let stdout_writer = Arc::new(Mutex::new(BufWriter::new(io::stdout())));
249 let symbol_cache = provider
250 .as_any()
251 .downcast_ref::<crate::parser::TreeSitterProvider>()
252 .map(|provider| provider.symbol_cache())
253 .unwrap_or_else(|| Arc::new(std::sync::RwLock::new(SymbolCache::new())));
254 AppContext {
255 provider,
256 backup: RefCell::new(BackupStore::new()),
257 checkpoint: RefCell::new(CheckpointStore::new()),
258 config: RefCell::new(config),
259 callgraph: RefCell::new(None),
260 search_index: RefCell::new(None),
261 search_index_rx: RefCell::new(None),
262 symbol_cache,
263 semantic_index: RefCell::new(None),
264 semantic_index_rx: RefCell::new(None),
265 semantic_index_status: RefCell::new(SemanticIndexStatus::Disabled),
266 semantic_embedding_model: RefCell::new(None),
267 watcher: RefCell::new(None),
268 watcher_rx: RefCell::new(None),
269 lsp_manager: RefCell::new(LspManager::new()),
270 stdout_writer,
271 progress_sender: Arc::clone(&progress_sender),
272 bash_background: BgTaskRegistry::new(progress_sender),
273 }
274 }
275
276 pub fn stdout_writer(&self) -> SharedStdoutWriter {
277 Arc::clone(&self.stdout_writer)
278 }
279
280 pub fn set_progress_sender(&self, sender: Option<ProgressSender>) {
281 if let Ok(mut progress_sender) = self.progress_sender.lock() {
282 *progress_sender = sender;
283 }
284 }
285
286 pub fn emit_progress(&self, frame: ProgressFrame) {
287 let Ok(progress_sender) = self.progress_sender.lock().map(|sender| sender.clone()) else {
288 return;
289 };
290 if let Some(sender) = progress_sender.as_ref() {
291 sender(PushFrame::Progress(frame));
292 }
293 }
294
295 pub fn progress_sender_handle(&self) -> Option<ProgressSender> {
303 self.progress_sender
304 .lock()
305 .ok()
306 .and_then(|sender| sender.clone())
307 }
308
309 pub fn bash_background(&self) -> &BgTaskRegistry {
310 &self.bash_background
311 }
312
313 pub fn drain_bg_completions(&self) -> Vec<BgCompletion> {
314 self.bash_background.drain_completions()
315 }
316
317 pub fn provider(&self) -> &dyn LanguageProvider {
319 self.provider.as_ref()
320 }
321
322 pub fn backup(&self) -> &RefCell<BackupStore> {
324 &self.backup
325 }
326
327 pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
329 &self.checkpoint
330 }
331
332 pub fn config(&self) -> Ref<'_, Config> {
334 self.config.borrow()
335 }
336
337 pub fn config_mut(&self) -> RefMut<'_, Config> {
339 self.config.borrow_mut()
340 }
341
342 pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
344 &self.callgraph
345 }
346
347 pub fn search_index(&self) -> &RefCell<Option<SearchIndex>> {
349 &self.search_index
350 }
351
352 pub fn search_index_rx(&self) -> &RefCell<Option<crossbeam_channel::Receiver<SearchIndex>>> {
354 &self.search_index_rx
355 }
356
357 pub fn symbol_cache(&self) -> SharedSymbolCache {
359 Arc::clone(&self.symbol_cache)
360 }
361
362 pub fn reset_symbol_cache(&self) -> u64 {
364 self.symbol_cache
365 .write()
366 .map(|mut cache| cache.reset())
367 .unwrap_or(0)
368 }
369
370 pub fn semantic_index(&self) -> &RefCell<Option<SemanticIndex>> {
372 &self.semantic_index
373 }
374
375 pub fn semantic_index_rx(
377 &self,
378 ) -> &RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>> {
379 &self.semantic_index_rx
380 }
381
382 pub fn semantic_index_status(&self) -> &RefCell<SemanticIndexStatus> {
383 &self.semantic_index_status
384 }
385
386 pub fn semantic_embedding_model(
388 &self,
389 ) -> &RefCell<Option<crate::semantic_index::EmbeddingModel>> {
390 &self.semantic_embedding_model
391 }
392
393 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
395 &self.watcher
396 }
397
398 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
400 &self.watcher_rx
401 }
402
403 pub fn lsp(&self) -> RefMut<'_, LspManager> {
405 self.lsp_manager.borrow_mut()
406 }
407
408 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
411 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
412 let config = self.config();
413 if let Err(e) = lsp.notify_file_changed(file_path, content, &config) {
414 log::warn!("sync error for {}: {}", file_path.display(), e);
415 }
416 }
417 }
418
419 pub fn lsp_notify_and_collect_diagnostics(
430 &self,
431 file_path: &Path,
432 content: &str,
433 timeout: std::time::Duration,
434 ) -> crate::lsp::manager::PostEditWaitOutcome {
435 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
436 return crate::lsp::manager::PostEditWaitOutcome::default();
437 };
438
439 lsp.drain_events();
442
443 let pre_snapshot = lsp.snapshot_diagnostic_epochs(file_path);
447
448 let config = self.config();
450 let expected_versions = match lsp.notify_file_changed_versioned(file_path, content, &config)
451 {
452 Ok(v) => v,
453 Err(e) => {
454 log::warn!("sync error for {}: {}", file_path.display(), e);
455 return crate::lsp::manager::PostEditWaitOutcome::default();
456 }
457 };
458
459 if expected_versions.is_empty() {
462 return crate::lsp::manager::PostEditWaitOutcome::default();
463 }
464
465 lsp.wait_for_post_edit_diagnostics(
466 file_path,
467 &config,
468 &expected_versions,
469 &pre_snapshot,
470 timeout,
471 )
472 }
473
474 fn custom_lsp_root_markers(&self) -> Vec<String> {
477 self.config()
478 .lsp_servers
479 .iter()
480 .flat_map(|s| s.root_markers.iter().cloned())
481 .collect()
482 }
483
484 fn notify_watched_config_files(&self, file_paths: &[PathBuf]) {
485 let custom_markers = self.custom_lsp_root_markers();
486 let config_paths: Vec<(PathBuf, FileChangeType)> = file_paths
487 .iter()
488 .filter(|path| is_config_file_path_with_custom(path, &custom_markers))
489 .cloned()
490 .map(|path| {
491 let change_type = if path.exists() {
492 FileChangeType::CHANGED
493 } else {
494 FileChangeType::DELETED
495 };
496 (path, change_type)
497 })
498 .collect();
499
500 self.notify_watched_config_events(&config_paths);
501 }
502
503 fn multi_file_write_paths(params: &serde_json::Value) -> Option<Vec<PathBuf>> {
504 let paths = params
505 .get("multi_file_write_paths")
506 .and_then(|value| value.as_array())?
507 .iter()
508 .filter_map(|value| value.as_str())
509 .map(PathBuf::from)
510 .collect::<Vec<_>>();
511
512 (!paths.is_empty()).then_some(paths)
513 }
514
515 fn watched_file_events_from_params(
527 params: &serde_json::Value,
528 extra_markers: &[String],
529 ) -> Option<Vec<(PathBuf, FileChangeType)>> {
530 let events = params
531 .get("multi_file_write_paths")
532 .and_then(|value| value.as_array())?
533 .iter()
534 .filter_map(|entry| {
535 let path = entry
537 .get("path")
538 .and_then(|value| value.as_str())
539 .map(PathBuf::from)?;
540
541 if !is_config_file_path_with_custom(&path, extra_markers) {
542 return None;
543 }
544
545 let change_type = entry
546 .get("type")
547 .and_then(|value| value.as_str())
548 .and_then(Self::parse_file_change_type)
549 .unwrap_or_else(|| Self::change_type_from_current_state(&path));
550
551 Some((path, change_type))
552 })
553 .collect::<Vec<_>>();
554
555 (!events.is_empty()).then_some(events)
556 }
557
558 fn parse_file_change_type(value: &str) -> Option<FileChangeType> {
559 match value {
560 "created" | "CREATED" | "Created" => Some(FileChangeType::CREATED),
561 "changed" | "CHANGED" | "Changed" => Some(FileChangeType::CHANGED),
562 "deleted" | "DELETED" | "Deleted" => Some(FileChangeType::DELETED),
563 _ => None,
564 }
565 }
566
567 fn change_type_from_current_state(path: &Path) -> FileChangeType {
568 if path.exists() {
569 FileChangeType::CHANGED
570 } else {
571 FileChangeType::DELETED
572 }
573 }
574
575 fn notify_watched_config_events(&self, config_paths: &[(PathBuf, FileChangeType)]) {
576 if config_paths.is_empty() {
577 return;
578 }
579
580 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
581 let config = self.config();
582 if let Err(e) = lsp.notify_files_watched_changed(config_paths, &config) {
583 log::warn!("watched-file sync error: {}", e);
584 }
585 }
586 }
587
588 pub fn lsp_notify_watched_config_file(&self, file_path: &Path, change_type: FileChangeType) {
589 let custom_markers = self.custom_lsp_root_markers();
590 if !is_config_file_path_with_custom(file_path, &custom_markers) {
591 return;
592 }
593
594 self.notify_watched_config_events(&[(file_path.to_path_buf(), change_type)]);
595 }
596
597 pub fn lsp_post_multi_file_write(
602 &self,
603 file_path: &Path,
604 content: &str,
605 file_paths: &[PathBuf],
606 params: &serde_json::Value,
607 ) -> Option<crate::lsp::manager::PostEditWaitOutcome> {
608 self.notify_watched_config_files(file_paths);
609
610 let wants_diagnostics = params
611 .get("diagnostics")
612 .and_then(|v| v.as_bool())
613 .unwrap_or(false);
614
615 if !wants_diagnostics {
616 self.lsp_notify_file_changed(file_path, content);
617 return None;
618 }
619
620 let wait_ms = params
621 .get("wait_ms")
622 .and_then(|v| v.as_u64())
623 .unwrap_or(3000)
624 .min(10_000);
625
626 Some(self.lsp_notify_and_collect_diagnostics(
627 file_path,
628 content,
629 std::time::Duration::from_millis(wait_ms),
630 ))
631 }
632
633 pub fn lsp_post_write(
650 &self,
651 file_path: &Path,
652 content: &str,
653 params: &serde_json::Value,
654 ) -> Option<crate::lsp::manager::PostEditWaitOutcome> {
655 let wants_diagnostics = params
656 .get("diagnostics")
657 .and_then(|v| v.as_bool())
658 .unwrap_or(false);
659
660 let custom_markers = self.custom_lsp_root_markers();
661
662 if !wants_diagnostics {
663 if let Some(file_paths) = Self::multi_file_write_paths(params) {
664 self.notify_watched_config_files(&file_paths);
665 } else if let Some(config_events) =
666 Self::watched_file_events_from_params(params, &custom_markers)
667 {
668 self.notify_watched_config_events(&config_events);
669 }
670 self.lsp_notify_file_changed(file_path, content);
671 return None;
672 }
673
674 let wait_ms = params
675 .get("wait_ms")
676 .and_then(|v| v.as_u64())
677 .unwrap_or(3000)
678 .min(10_000); if let Some(file_paths) = Self::multi_file_write_paths(params) {
681 return self.lsp_post_multi_file_write(file_path, content, &file_paths, params);
682 }
683
684 if let Some(config_events) = Self::watched_file_events_from_params(params, &custom_markers)
685 {
686 self.notify_watched_config_events(&config_events);
687 }
688
689 Some(self.lsp_notify_and_collect_diagnostics(
690 file_path,
691 content,
692 std::time::Duration::from_millis(wait_ms),
693 ))
694 }
695
696 pub fn validate_path(
705 &self,
706 req_id: &str,
707 path: &Path,
708 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
709 let config = self.config();
710 if !config.restrict_to_project_root {
712 return Ok(path.to_path_buf());
713 }
714 let root = match &config.project_root {
715 Some(r) => r.clone(),
716 None => return Ok(path.to_path_buf()), };
718 drop(config);
719
720 let raw_root = root.clone();
725 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
726
727 let resolved = match std::fs::canonicalize(path) {
732 Ok(resolved) => resolved,
733 Err(_) => {
734 let normalized = normalize_path(path);
735 reject_escaping_symlink(req_id, path, &normalized, &resolved_root, &raw_root)?;
736 resolve_with_existing_ancestors(&normalized)
737 }
738 };
739
740 if !resolved.starts_with(&resolved_root) {
741 return Err(path_error_response(req_id, path, &resolved_root));
742 }
743
744 Ok(resolved)
745 }
746
747 pub fn lsp_server_count(&self) -> usize {
749 self.lsp_manager
750 .try_borrow()
751 .map(|lsp| lsp.server_count())
752 .unwrap_or(0)
753 }
754
755 pub fn symbol_cache_stats(&self) -> serde_json::Value {
757 let entries = self
758 .symbol_cache
759 .read()
760 .map(|cache| cache.len())
761 .unwrap_or(0);
762 serde_json::json!({
763 "local_entries": entries,
764 "warm_entries": 0,
765 })
766 }
767}