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::protocol::{ProgressFrame, PushFrame};
18
19pub type ProgressSender = Arc<Box<dyn Fn(PushFrame) + Send + Sync>>;
20pub type SharedProgressSender = Arc<Mutex<Option<ProgressSender>>>;
21pub type SharedStdoutWriter = Arc<Mutex<BufWriter<io::Stdout>>>;
22use crate::search_index::SearchIndex;
23use crate::semantic_index::SemanticIndex;
24
25#[derive(Debug, Clone)]
26pub enum SemanticIndexStatus {
27 Disabled,
28 Building {
29 stage: String,
30 files: Option<usize>,
31 entries_done: Option<usize>,
32 entries_total: Option<usize>,
33 },
34 Ready,
35 Failed(String),
36}
37
38pub enum SemanticIndexEvent {
39 Progress {
40 stage: String,
41 files: Option<usize>,
42 entries_done: Option<usize>,
43 entries_total: Option<usize>,
44 },
45 Ready(SemanticIndex),
46 Failed(String),
47}
48
49fn normalize_path(path: &Path) -> PathBuf {
53 let mut result = PathBuf::new();
54 for component in path.components() {
55 match component {
56 Component::ParentDir => {
57 if !result.pop() {
59 result.push(component);
60 }
61 }
62 Component::CurDir => {} _ => result.push(component),
64 }
65 }
66 result
67}
68
69fn resolve_with_existing_ancestors(path: &Path) -> PathBuf {
70 let mut existing = path.to_path_buf();
71 let mut tail_segments = Vec::new();
72
73 while !existing.exists() {
74 if let Some(name) = existing.file_name() {
75 tail_segments.push(name.to_owned());
76 } else {
77 break;
78 }
79
80 existing = match existing.parent() {
81 Some(parent) => parent.to_path_buf(),
82 None => break,
83 };
84 }
85
86 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
87 for segment in tail_segments.into_iter().rev() {
88 resolved.push(segment);
89 }
90
91 resolved
92}
93
94fn path_error_response(
95 req_id: &str,
96 path: &Path,
97 resolved_root: &Path,
98) -> crate::protocol::Response {
99 crate::protocol::Response::error(
100 req_id,
101 "path_outside_root",
102 format!(
103 "path '{}' is outside the project root '{}'",
104 path.display(),
105 resolved_root.display()
106 ),
107 )
108}
109
110fn reject_escaping_symlink(
120 req_id: &str,
121 original_path: &Path,
122 candidate: &Path,
123 resolved_root: &Path,
124 raw_root: &Path,
125) -> Result<(), crate::protocol::Response> {
126 let mut current = PathBuf::new();
127
128 for component in candidate.components() {
129 current.push(component);
130
131 let Ok(metadata) = std::fs::symlink_metadata(¤t) else {
132 continue;
133 };
134
135 if !metadata.file_type().is_symlink() {
136 continue;
137 }
138
139 let inside_root = current.starts_with(resolved_root) || current.starts_with(raw_root);
148 if !inside_root {
149 continue;
150 }
151
152 iterative_follow_chain(req_id, original_path, ¤t, resolved_root)?;
153 }
154
155 Ok(())
156}
157
158fn iterative_follow_chain(
161 req_id: &str,
162 original_path: &Path,
163 start: &Path,
164 resolved_root: &Path,
165) -> Result<(), crate::protocol::Response> {
166 let mut link = start.to_path_buf();
167 let mut depth = 0usize;
168
169 loop {
170 if depth > 40 {
171 return Err(path_error_response(req_id, original_path, resolved_root));
172 }
173
174 let target = match std::fs::read_link(&link) {
175 Ok(t) => t,
176 Err(_) => {
177 return Err(path_error_response(req_id, original_path, resolved_root));
179 }
180 };
181
182 let resolved_target = if target.is_absolute() {
183 normalize_path(&target)
184 } else {
185 let parent = link.parent().unwrap_or_else(|| Path::new(""));
186 normalize_path(&parent.join(&target))
187 };
188
189 let canonical_target =
193 std::fs::canonicalize(&resolved_target).unwrap_or_else(|_| resolved_target.clone());
194
195 if !canonical_target.starts_with(resolved_root)
196 && !resolved_target.starts_with(resolved_root)
197 {
198 return Err(path_error_response(req_id, original_path, resolved_root));
199 }
200
201 match std::fs::symlink_metadata(&resolved_target) {
203 Ok(meta) if meta.file_type().is_symlink() => {
204 link = resolved_target;
205 depth += 1;
206 }
207 _ => break, }
209 }
210
211 Ok(())
212}
213
214pub struct AppContext {
224 provider: Box<dyn LanguageProvider>,
225 backup: RefCell<BackupStore>,
226 checkpoint: RefCell<CheckpointStore>,
227 config: RefCell<Config>,
228 callgraph: RefCell<Option<CallGraph>>,
229 search_index: RefCell<Option<SearchIndex>>,
230 search_index_rx:
231 RefCell<Option<crossbeam_channel::Receiver<(SearchIndex, crate::parser::SymbolCache)>>>,
232 semantic_index: RefCell<Option<SemanticIndex>>,
233 semantic_index_rx: RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>>,
234 semantic_index_status: RefCell<SemanticIndexStatus>,
235 semantic_embedding_model: RefCell<Option<crate::semantic_index::EmbeddingModel>>,
236 watcher: RefCell<Option<RecommendedWatcher>>,
237 watcher_rx: RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>>,
238 lsp_manager: RefCell<LspManager>,
239 stdout_writer: SharedStdoutWriter,
240 progress_sender: SharedProgressSender,
241 bash_background: BgTaskRegistry,
242}
243
244impl AppContext {
245 pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
246 let progress_sender = Arc::new(Mutex::new(None));
247 let stdout_writer = Arc::new(Mutex::new(BufWriter::new(io::stdout())));
248 AppContext {
249 provider,
250 backup: RefCell::new(BackupStore::new()),
251 checkpoint: RefCell::new(CheckpointStore::new()),
252 config: RefCell::new(config),
253 callgraph: RefCell::new(None),
254 search_index: RefCell::new(None),
255 search_index_rx: RefCell::new(None),
256 semantic_index: RefCell::new(None),
257 semantic_index_rx: RefCell::new(None),
258 semantic_index_status: RefCell::new(SemanticIndexStatus::Disabled),
259 semantic_embedding_model: RefCell::new(None),
260 watcher: RefCell::new(None),
261 watcher_rx: RefCell::new(None),
262 lsp_manager: RefCell::new(LspManager::new()),
263 stdout_writer,
264 progress_sender: Arc::clone(&progress_sender),
265 bash_background: BgTaskRegistry::new(progress_sender),
266 }
267 }
268
269 pub fn stdout_writer(&self) -> SharedStdoutWriter {
270 Arc::clone(&self.stdout_writer)
271 }
272
273 pub fn set_progress_sender(&self, sender: Option<ProgressSender>) {
274 if let Ok(mut progress_sender) = self.progress_sender.lock() {
275 *progress_sender = sender;
276 }
277 }
278
279 pub fn emit_progress(&self, frame: ProgressFrame) {
280 let Ok(progress_sender) = self.progress_sender.lock().map(|sender| sender.clone()) else {
281 return;
282 };
283 if let Some(sender) = progress_sender.as_ref() {
284 sender(PushFrame::Progress(frame));
285 }
286 }
287
288 pub fn bash_background(&self) -> &BgTaskRegistry {
289 &self.bash_background
290 }
291
292 pub fn drain_bg_completions(&self) -> Vec<BgCompletion> {
293 self.bash_background.drain_completions()
294 }
295
296 pub fn provider(&self) -> &dyn LanguageProvider {
298 self.provider.as_ref()
299 }
300
301 pub fn backup(&self) -> &RefCell<BackupStore> {
303 &self.backup
304 }
305
306 pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
308 &self.checkpoint
309 }
310
311 pub fn config(&self) -> Ref<'_, Config> {
313 self.config.borrow()
314 }
315
316 pub fn config_mut(&self) -> RefMut<'_, Config> {
318 self.config.borrow_mut()
319 }
320
321 pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
323 &self.callgraph
324 }
325
326 pub fn search_index(&self) -> &RefCell<Option<SearchIndex>> {
328 &self.search_index
329 }
330
331 pub fn search_index_rx(
333 &self,
334 ) -> &RefCell<Option<crossbeam_channel::Receiver<(SearchIndex, crate::parser::SymbolCache)>>>
335 {
336 &self.search_index_rx
337 }
338
339 pub fn semantic_index(&self) -> &RefCell<Option<SemanticIndex>> {
341 &self.semantic_index
342 }
343
344 pub fn semantic_index_rx(
346 &self,
347 ) -> &RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>> {
348 &self.semantic_index_rx
349 }
350
351 pub fn semantic_index_status(&self) -> &RefCell<SemanticIndexStatus> {
352 &self.semantic_index_status
353 }
354
355 pub fn semantic_embedding_model(
357 &self,
358 ) -> &RefCell<Option<crate::semantic_index::EmbeddingModel>> {
359 &self.semantic_embedding_model
360 }
361
362 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
364 &self.watcher
365 }
366
367 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
369 &self.watcher_rx
370 }
371
372 pub fn lsp(&self) -> RefMut<'_, LspManager> {
374 self.lsp_manager.borrow_mut()
375 }
376
377 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
380 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
381 let config = self.config();
382 if let Err(e) = lsp.notify_file_changed(file_path, content, &config) {
383 log::warn!("sync error for {}: {}", file_path.display(), e);
384 }
385 }
386 }
387
388 pub fn lsp_notify_and_collect_diagnostics(
399 &self,
400 file_path: &Path,
401 content: &str,
402 timeout: std::time::Duration,
403 ) -> crate::lsp::manager::PostEditWaitOutcome {
404 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
405 return crate::lsp::manager::PostEditWaitOutcome::default();
406 };
407
408 lsp.drain_events();
411
412 let pre_snapshot = lsp.snapshot_diagnostic_epochs(file_path);
416
417 let config = self.config();
419 let expected_versions = match lsp.notify_file_changed_versioned(file_path, content, &config)
420 {
421 Ok(v) => v,
422 Err(e) => {
423 log::warn!("sync error for {}: {}", file_path.display(), e);
424 return crate::lsp::manager::PostEditWaitOutcome::default();
425 }
426 };
427
428 if expected_versions.is_empty() {
431 return crate::lsp::manager::PostEditWaitOutcome::default();
432 }
433
434 lsp.wait_for_post_edit_diagnostics(
435 file_path,
436 &config,
437 &expected_versions,
438 &pre_snapshot,
439 timeout,
440 )
441 }
442
443 fn custom_lsp_root_markers(&self) -> Vec<String> {
446 self.config()
447 .lsp_servers
448 .iter()
449 .flat_map(|s| s.root_markers.iter().cloned())
450 .collect()
451 }
452
453 fn notify_watched_config_files(&self, file_paths: &[PathBuf]) {
454 let custom_markers = self.custom_lsp_root_markers();
455 let config_paths: Vec<(PathBuf, FileChangeType)> = file_paths
456 .iter()
457 .filter(|path| is_config_file_path_with_custom(path, &custom_markers))
458 .cloned()
459 .map(|path| {
460 let change_type = if path.exists() {
461 FileChangeType::CHANGED
462 } else {
463 FileChangeType::DELETED
464 };
465 (path, change_type)
466 })
467 .collect();
468
469 self.notify_watched_config_events(&config_paths);
470 }
471
472 fn multi_file_write_paths(params: &serde_json::Value) -> Option<Vec<PathBuf>> {
473 let paths = params
474 .get("multi_file_write_paths")
475 .and_then(|value| value.as_array())?
476 .iter()
477 .filter_map(|value| value.as_str())
478 .map(PathBuf::from)
479 .collect::<Vec<_>>();
480
481 (!paths.is_empty()).then_some(paths)
482 }
483
484 fn watched_file_events_from_params(
496 params: &serde_json::Value,
497 extra_markers: &[String],
498 ) -> Option<Vec<(PathBuf, FileChangeType)>> {
499 let events = params
500 .get("multi_file_write_paths")
501 .and_then(|value| value.as_array())?
502 .iter()
503 .filter_map(|entry| {
504 let path = entry
506 .get("path")
507 .and_then(|value| value.as_str())
508 .map(PathBuf::from)?;
509
510 if !is_config_file_path_with_custom(&path, extra_markers) {
511 return None;
512 }
513
514 let change_type = entry
515 .get("type")
516 .and_then(|value| value.as_str())
517 .and_then(Self::parse_file_change_type)
518 .unwrap_or_else(|| Self::change_type_from_current_state(&path));
519
520 Some((path, change_type))
521 })
522 .collect::<Vec<_>>();
523
524 (!events.is_empty()).then_some(events)
525 }
526
527 fn parse_file_change_type(value: &str) -> Option<FileChangeType> {
528 match value {
529 "created" | "CREATED" | "Created" => Some(FileChangeType::CREATED),
530 "changed" | "CHANGED" | "Changed" => Some(FileChangeType::CHANGED),
531 "deleted" | "DELETED" | "Deleted" => Some(FileChangeType::DELETED),
532 _ => None,
533 }
534 }
535
536 fn change_type_from_current_state(path: &Path) -> FileChangeType {
537 if path.exists() {
538 FileChangeType::CHANGED
539 } else {
540 FileChangeType::DELETED
541 }
542 }
543
544 fn notify_watched_config_events(&self, config_paths: &[(PathBuf, FileChangeType)]) {
545 if config_paths.is_empty() {
546 return;
547 }
548
549 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
550 let config = self.config();
551 if let Err(e) = lsp.notify_files_watched_changed(config_paths, &config) {
552 log::warn!("watched-file sync error: {}", e);
553 }
554 }
555 }
556
557 pub fn lsp_notify_watched_config_file(&self, file_path: &Path, change_type: FileChangeType) {
558 let custom_markers = self.custom_lsp_root_markers();
559 if !is_config_file_path_with_custom(file_path, &custom_markers) {
560 return;
561 }
562
563 self.notify_watched_config_events(&[(file_path.to_path_buf(), change_type)]);
564 }
565
566 pub fn lsp_post_multi_file_write(
571 &self,
572 file_path: &Path,
573 content: &str,
574 file_paths: &[PathBuf],
575 params: &serde_json::Value,
576 ) -> Option<crate::lsp::manager::PostEditWaitOutcome> {
577 self.notify_watched_config_files(file_paths);
578
579 let wants_diagnostics = params
580 .get("diagnostics")
581 .and_then(|v| v.as_bool())
582 .unwrap_or(false);
583
584 if !wants_diagnostics {
585 self.lsp_notify_file_changed(file_path, content);
586 return None;
587 }
588
589 let wait_ms = params
590 .get("wait_ms")
591 .and_then(|v| v.as_u64())
592 .unwrap_or(3000)
593 .min(10_000);
594
595 Some(self.lsp_notify_and_collect_diagnostics(
596 file_path,
597 content,
598 std::time::Duration::from_millis(wait_ms),
599 ))
600 }
601
602 pub fn lsp_post_write(
619 &self,
620 file_path: &Path,
621 content: &str,
622 params: &serde_json::Value,
623 ) -> Option<crate::lsp::manager::PostEditWaitOutcome> {
624 let wants_diagnostics = params
625 .get("diagnostics")
626 .and_then(|v| v.as_bool())
627 .unwrap_or(false);
628
629 let custom_markers = self.custom_lsp_root_markers();
630
631 if !wants_diagnostics {
632 if let Some(file_paths) = Self::multi_file_write_paths(params) {
633 self.notify_watched_config_files(&file_paths);
634 } else if let Some(config_events) =
635 Self::watched_file_events_from_params(params, &custom_markers)
636 {
637 self.notify_watched_config_events(&config_events);
638 }
639 self.lsp_notify_file_changed(file_path, content);
640 return None;
641 }
642
643 let wait_ms = params
644 .get("wait_ms")
645 .and_then(|v| v.as_u64())
646 .unwrap_or(3000)
647 .min(10_000); if let Some(file_paths) = Self::multi_file_write_paths(params) {
650 return self.lsp_post_multi_file_write(file_path, content, &file_paths, params);
651 }
652
653 if let Some(config_events) = Self::watched_file_events_from_params(params, &custom_markers)
654 {
655 self.notify_watched_config_events(&config_events);
656 }
657
658 Some(self.lsp_notify_and_collect_diagnostics(
659 file_path,
660 content,
661 std::time::Duration::from_millis(wait_ms),
662 ))
663 }
664
665 pub fn validate_path(
674 &self,
675 req_id: &str,
676 path: &Path,
677 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
678 let config = self.config();
679 if !config.restrict_to_project_root {
681 return Ok(path.to_path_buf());
682 }
683 let root = match &config.project_root {
684 Some(r) => r.clone(),
685 None => return Ok(path.to_path_buf()), };
687 drop(config);
688
689 let raw_root = root.clone();
694 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
695
696 let resolved = match std::fs::canonicalize(path) {
701 Ok(resolved) => resolved,
702 Err(_) => {
703 let normalized = normalize_path(path);
704 reject_escaping_symlink(req_id, path, &normalized, &resolved_root, &raw_root)?;
705 resolve_with_existing_ancestors(&normalized)
706 }
707 };
708
709 if !resolved.starts_with(&resolved_root) {
710 return Err(path_error_response(req_id, path, &resolved_root));
711 }
712
713 Ok(resolved)
714 }
715
716 pub fn lsp_server_count(&self) -> usize {
718 self.lsp_manager
719 .try_borrow()
720 .map(|lsp| lsp.server_count())
721 .unwrap_or(0)
722 }
723
724 pub fn symbol_cache_stats(&self) -> serde_json::Value {
726 if let Some(tsp) = self
727 .provider
728 .as_any()
729 .downcast_ref::<crate::parser::TreeSitterProvider>()
730 {
731 let (local, warm) = tsp.symbol_cache_stats();
732 serde_json::json!({
733 "local_entries": local,
734 "warm_entries": warm,
735 })
736 } else {
737 serde_json::json!({
738 "local_entries": 0,
739 "warm_entries": 0,
740 })
741 }
742 }
743}