1use std::cell::{Ref, RefCell, RefMut};
2use std::path::{Component, Path, PathBuf};
3use std::sync::{mpsc, Arc, Mutex};
4
5use lsp_types::FileChangeType;
6use notify::RecommendedWatcher;
7
8use crate::backup::BackupStore;
9use crate::bash_background::{BgCompletion, BgTaskRegistry};
10use crate::callgraph::CallGraph;
11use crate::checkpoint::CheckpointStore;
12use crate::config::Config;
13use crate::language::LanguageProvider;
14use crate::lsp::manager::LspManager;
15use crate::lsp::registry::is_config_file_path_with_custom;
16use crate::protocol::{ProgressFrame, PushFrame};
17
18pub type ProgressSender = Arc<Box<dyn Fn(PushFrame) + Send + Sync>>;
19pub type SharedProgressSender = Arc<Mutex<Option<ProgressSender>>>;
20use crate::search_index::SearchIndex;
21use crate::semantic_index::SemanticIndex;
22
23#[derive(Debug, Clone)]
24pub enum SemanticIndexStatus {
25 Disabled,
26 Building {
27 stage: String,
28 files: Option<usize>,
29 entries_done: Option<usize>,
30 entries_total: Option<usize>,
31 },
32 Ready,
33 Failed(String),
34}
35
36pub enum SemanticIndexEvent {
37 Progress {
38 stage: String,
39 files: Option<usize>,
40 entries_done: Option<usize>,
41 entries_total: Option<usize>,
42 },
43 Ready(SemanticIndex),
44 Failed(String),
45}
46
47fn normalize_path(path: &Path) -> PathBuf {
51 let mut result = PathBuf::new();
52 for component in path.components() {
53 match component {
54 Component::ParentDir => {
55 if !result.pop() {
57 result.push(component);
58 }
59 }
60 Component::CurDir => {} _ => result.push(component),
62 }
63 }
64 result
65}
66
67fn resolve_with_existing_ancestors(path: &Path) -> PathBuf {
68 let mut existing = path.to_path_buf();
69 let mut tail_segments = Vec::new();
70
71 while !existing.exists() {
72 if let Some(name) = existing.file_name() {
73 tail_segments.push(name.to_owned());
74 } else {
75 break;
76 }
77
78 existing = match existing.parent() {
79 Some(parent) => parent.to_path_buf(),
80 None => break,
81 };
82 }
83
84 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
85 for segment in tail_segments.into_iter().rev() {
86 resolved.push(segment);
87 }
88
89 resolved
90}
91
92fn path_error_response(
93 req_id: &str,
94 path: &Path,
95 resolved_root: &Path,
96) -> crate::protocol::Response {
97 crate::protocol::Response::error(
98 req_id,
99 "path_outside_root",
100 format!(
101 "path '{}' is outside the project root '{}'",
102 path.display(),
103 resolved_root.display()
104 ),
105 )
106}
107
108fn reject_escaping_symlink(
118 req_id: &str,
119 original_path: &Path,
120 candidate: &Path,
121 resolved_root: &Path,
122 raw_root: &Path,
123) -> Result<(), crate::protocol::Response> {
124 let mut current = PathBuf::new();
125
126 for component in candidate.components() {
127 current.push(component);
128
129 let Ok(metadata) = std::fs::symlink_metadata(¤t) else {
130 continue;
131 };
132
133 if !metadata.file_type().is_symlink() {
134 continue;
135 }
136
137 let inside_root = current.starts_with(resolved_root) || current.starts_with(raw_root);
146 if !inside_root {
147 continue;
148 }
149
150 iterative_follow_chain(req_id, original_path, ¤t, resolved_root)?;
151 }
152
153 Ok(())
154}
155
156fn iterative_follow_chain(
159 req_id: &str,
160 original_path: &Path,
161 start: &Path,
162 resolved_root: &Path,
163) -> Result<(), crate::protocol::Response> {
164 let mut link = start.to_path_buf();
165 let mut depth = 0usize;
166
167 loop {
168 if depth > 40 {
169 return Err(path_error_response(req_id, original_path, resolved_root));
170 }
171
172 let target = match std::fs::read_link(&link) {
173 Ok(t) => t,
174 Err(_) => {
175 return Err(path_error_response(req_id, original_path, resolved_root));
177 }
178 };
179
180 let resolved_target = if target.is_absolute() {
181 normalize_path(&target)
182 } else {
183 let parent = link.parent().unwrap_or_else(|| Path::new(""));
184 normalize_path(&parent.join(&target))
185 };
186
187 let canonical_target =
191 std::fs::canonicalize(&resolved_target).unwrap_or_else(|_| resolved_target.clone());
192
193 if !canonical_target.starts_with(resolved_root)
194 && !resolved_target.starts_with(resolved_root)
195 {
196 return Err(path_error_response(req_id, original_path, resolved_root));
197 }
198
199 match std::fs::symlink_metadata(&resolved_target) {
201 Ok(meta) if meta.file_type().is_symlink() => {
202 link = resolved_target;
203 depth += 1;
204 }
205 _ => break, }
207 }
208
209 Ok(())
210}
211
212pub struct AppContext {
222 provider: Box<dyn LanguageProvider>,
223 backup: RefCell<BackupStore>,
224 checkpoint: RefCell<CheckpointStore>,
225 config: RefCell<Config>,
226 callgraph: RefCell<Option<CallGraph>>,
227 search_index: RefCell<Option<SearchIndex>>,
228 search_index_rx:
229 RefCell<Option<crossbeam_channel::Receiver<(SearchIndex, crate::parser::SymbolCache)>>>,
230 semantic_index: RefCell<Option<SemanticIndex>>,
231 semantic_index_rx: RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>>,
232 semantic_index_status: RefCell<SemanticIndexStatus>,
233 semantic_embedding_model: RefCell<Option<crate::semantic_index::EmbeddingModel>>,
234 watcher: RefCell<Option<RecommendedWatcher>>,
235 watcher_rx: RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>>,
236 lsp_manager: RefCell<LspManager>,
237 progress_sender: SharedProgressSender,
238 bash_background: BgTaskRegistry,
239}
240
241impl AppContext {
242 pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
243 let progress_sender = Arc::new(Mutex::new(None));
244 AppContext {
245 provider,
246 backup: RefCell::new(BackupStore::new()),
247 checkpoint: RefCell::new(CheckpointStore::new()),
248 config: RefCell::new(config),
249 callgraph: RefCell::new(None),
250 search_index: RefCell::new(None),
251 search_index_rx: RefCell::new(None),
252 semantic_index: RefCell::new(None),
253 semantic_index_rx: RefCell::new(None),
254 semantic_index_status: RefCell::new(SemanticIndexStatus::Disabled),
255 semantic_embedding_model: RefCell::new(None),
256 watcher: RefCell::new(None),
257 watcher_rx: RefCell::new(None),
258 lsp_manager: RefCell::new(LspManager::new()),
259 progress_sender: Arc::clone(&progress_sender),
260 bash_background: BgTaskRegistry::new(progress_sender),
261 }
262 }
263
264 pub fn set_progress_sender(&self, sender: Option<ProgressSender>) {
265 if let Ok(mut progress_sender) = self.progress_sender.lock() {
266 *progress_sender = sender;
267 }
268 }
269
270 pub fn emit_progress(&self, frame: ProgressFrame) {
271 let Ok(progress_sender) = self.progress_sender.lock().map(|sender| sender.clone()) else {
272 return;
273 };
274 if let Some(sender) = progress_sender.as_ref() {
275 sender(PushFrame::Progress(frame));
276 }
277 }
278
279 pub fn bash_background(&self) -> &BgTaskRegistry {
280 &self.bash_background
281 }
282
283 pub fn drain_bg_completions(&self) -> Vec<BgCompletion> {
284 self.bash_background.drain_completions()
285 }
286
287 pub fn provider(&self) -> &dyn LanguageProvider {
289 self.provider.as_ref()
290 }
291
292 pub fn backup(&self) -> &RefCell<BackupStore> {
294 &self.backup
295 }
296
297 pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
299 &self.checkpoint
300 }
301
302 pub fn config(&self) -> Ref<'_, Config> {
304 self.config.borrow()
305 }
306
307 pub fn config_mut(&self) -> RefMut<'_, Config> {
309 self.config.borrow_mut()
310 }
311
312 pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
314 &self.callgraph
315 }
316
317 pub fn search_index(&self) -> &RefCell<Option<SearchIndex>> {
319 &self.search_index
320 }
321
322 pub fn search_index_rx(
324 &self,
325 ) -> &RefCell<Option<crossbeam_channel::Receiver<(SearchIndex, crate::parser::SymbolCache)>>>
326 {
327 &self.search_index_rx
328 }
329
330 pub fn semantic_index(&self) -> &RefCell<Option<SemanticIndex>> {
332 &self.semantic_index
333 }
334
335 pub fn semantic_index_rx(
337 &self,
338 ) -> &RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>> {
339 &self.semantic_index_rx
340 }
341
342 pub fn semantic_index_status(&self) -> &RefCell<SemanticIndexStatus> {
343 &self.semantic_index_status
344 }
345
346 pub fn semantic_embedding_model(
348 &self,
349 ) -> &RefCell<Option<crate::semantic_index::EmbeddingModel>> {
350 &self.semantic_embedding_model
351 }
352
353 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
355 &self.watcher
356 }
357
358 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
360 &self.watcher_rx
361 }
362
363 pub fn lsp(&self) -> RefMut<'_, LspManager> {
365 self.lsp_manager.borrow_mut()
366 }
367
368 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
371 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
372 let config = self.config();
373 if let Err(e) = lsp.notify_file_changed(file_path, content, &config) {
374 log::warn!("sync error for {}: {}", file_path.display(), e);
375 }
376 }
377 }
378
379 pub fn lsp_notify_and_collect_diagnostics(
390 &self,
391 file_path: &Path,
392 content: &str,
393 timeout: std::time::Duration,
394 ) -> crate::lsp::manager::PostEditWaitOutcome {
395 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
396 return crate::lsp::manager::PostEditWaitOutcome::default();
397 };
398
399 lsp.drain_events();
402
403 let pre_snapshot = lsp.snapshot_diagnostic_epochs(file_path);
407
408 let config = self.config();
410 let expected_versions = match lsp.notify_file_changed_versioned(file_path, content, &config)
411 {
412 Ok(v) => v,
413 Err(e) => {
414 log::warn!("sync error for {}: {}", file_path.display(), e);
415 return crate::lsp::manager::PostEditWaitOutcome::default();
416 }
417 };
418
419 if expected_versions.is_empty() {
422 return crate::lsp::manager::PostEditWaitOutcome::default();
423 }
424
425 lsp.wait_for_post_edit_diagnostics(
426 file_path,
427 &config,
428 &expected_versions,
429 &pre_snapshot,
430 timeout,
431 )
432 }
433
434 fn custom_lsp_root_markers(&self) -> Vec<String> {
437 self.config()
438 .lsp_servers
439 .iter()
440 .flat_map(|s| s.root_markers.iter().cloned())
441 .collect()
442 }
443
444 fn notify_watched_config_files(&self, file_paths: &[PathBuf]) {
445 let custom_markers = self.custom_lsp_root_markers();
446 let config_paths: Vec<(PathBuf, FileChangeType)> = file_paths
447 .iter()
448 .filter(|path| is_config_file_path_with_custom(path, &custom_markers))
449 .cloned()
450 .map(|path| {
451 let change_type = if path.exists() {
452 FileChangeType::CHANGED
453 } else {
454 FileChangeType::DELETED
455 };
456 (path, change_type)
457 })
458 .collect();
459
460 self.notify_watched_config_events(&config_paths);
461 }
462
463 fn multi_file_write_paths(params: &serde_json::Value) -> Option<Vec<PathBuf>> {
464 let paths = params
465 .get("multi_file_write_paths")
466 .and_then(|value| value.as_array())?
467 .iter()
468 .filter_map(|value| value.as_str())
469 .map(PathBuf::from)
470 .collect::<Vec<_>>();
471
472 (!paths.is_empty()).then_some(paths)
473 }
474
475 fn watched_file_events_from_params(
487 params: &serde_json::Value,
488 extra_markers: &[String],
489 ) -> Option<Vec<(PathBuf, FileChangeType)>> {
490 let events = params
491 .get("multi_file_write_paths")
492 .and_then(|value| value.as_array())?
493 .iter()
494 .filter_map(|entry| {
495 let path = entry
497 .get("path")
498 .and_then(|value| value.as_str())
499 .map(PathBuf::from)?;
500
501 if !is_config_file_path_with_custom(&path, extra_markers) {
502 return None;
503 }
504
505 let change_type = entry
506 .get("type")
507 .and_then(|value| value.as_str())
508 .and_then(Self::parse_file_change_type)
509 .unwrap_or_else(|| Self::change_type_from_current_state(&path));
510
511 Some((path, change_type))
512 })
513 .collect::<Vec<_>>();
514
515 (!events.is_empty()).then_some(events)
516 }
517
518 fn parse_file_change_type(value: &str) -> Option<FileChangeType> {
519 match value {
520 "created" | "CREATED" | "Created" => Some(FileChangeType::CREATED),
521 "changed" | "CHANGED" | "Changed" => Some(FileChangeType::CHANGED),
522 "deleted" | "DELETED" | "Deleted" => Some(FileChangeType::DELETED),
523 _ => None,
524 }
525 }
526
527 fn change_type_from_current_state(path: &Path) -> FileChangeType {
528 if path.exists() {
529 FileChangeType::CHANGED
530 } else {
531 FileChangeType::DELETED
532 }
533 }
534
535 fn notify_watched_config_events(&self, config_paths: &[(PathBuf, FileChangeType)]) {
536 if config_paths.is_empty() {
537 return;
538 }
539
540 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
541 let config = self.config();
542 if let Err(e) = lsp.notify_files_watched_changed(config_paths, &config) {
543 log::warn!("watched-file sync error: {}", e);
544 }
545 }
546 }
547
548 pub fn lsp_notify_watched_config_file(&self, file_path: &Path, change_type: FileChangeType) {
549 let custom_markers = self.custom_lsp_root_markers();
550 if !is_config_file_path_with_custom(file_path, &custom_markers) {
551 return;
552 }
553
554 self.notify_watched_config_events(&[(file_path.to_path_buf(), change_type)]);
555 }
556
557 pub fn lsp_post_multi_file_write(
562 &self,
563 file_path: &Path,
564 content: &str,
565 file_paths: &[PathBuf],
566 params: &serde_json::Value,
567 ) -> Option<crate::lsp::manager::PostEditWaitOutcome> {
568 self.notify_watched_config_files(file_paths);
569
570 let wants_diagnostics = params
571 .get("diagnostics")
572 .and_then(|v| v.as_bool())
573 .unwrap_or(false);
574
575 if !wants_diagnostics {
576 self.lsp_notify_file_changed(file_path, content);
577 return None;
578 }
579
580 let wait_ms = params
581 .get("wait_ms")
582 .and_then(|v| v.as_u64())
583 .unwrap_or(3000)
584 .min(10_000);
585
586 Some(self.lsp_notify_and_collect_diagnostics(
587 file_path,
588 content,
589 std::time::Duration::from_millis(wait_ms),
590 ))
591 }
592
593 pub fn lsp_post_write(
610 &self,
611 file_path: &Path,
612 content: &str,
613 params: &serde_json::Value,
614 ) -> Option<crate::lsp::manager::PostEditWaitOutcome> {
615 let wants_diagnostics = params
616 .get("diagnostics")
617 .and_then(|v| v.as_bool())
618 .unwrap_or(false);
619
620 let custom_markers = self.custom_lsp_root_markers();
621
622 if !wants_diagnostics {
623 if let Some(file_paths) = Self::multi_file_write_paths(params) {
624 self.notify_watched_config_files(&file_paths);
625 } else if let Some(config_events) =
626 Self::watched_file_events_from_params(params, &custom_markers)
627 {
628 self.notify_watched_config_events(&config_events);
629 }
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); if let Some(file_paths) = Self::multi_file_write_paths(params) {
641 return self.lsp_post_multi_file_write(file_path, content, &file_paths, params);
642 }
643
644 if let Some(config_events) = Self::watched_file_events_from_params(params, &custom_markers)
645 {
646 self.notify_watched_config_events(&config_events);
647 }
648
649 Some(self.lsp_notify_and_collect_diagnostics(
650 file_path,
651 content,
652 std::time::Duration::from_millis(wait_ms),
653 ))
654 }
655
656 pub fn validate_path(
665 &self,
666 req_id: &str,
667 path: &Path,
668 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
669 let config = self.config();
670 if !config.restrict_to_project_root {
672 return Ok(path.to_path_buf());
673 }
674 let root = match &config.project_root {
675 Some(r) => r.clone(),
676 None => return Ok(path.to_path_buf()), };
678 drop(config);
679
680 let raw_root = root.clone();
685 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
686
687 let resolved = match std::fs::canonicalize(path) {
692 Ok(resolved) => resolved,
693 Err(_) => {
694 let normalized = normalize_path(path);
695 reject_escaping_symlink(req_id, path, &normalized, &resolved_root, &raw_root)?;
696 resolve_with_existing_ancestors(&normalized)
697 }
698 };
699
700 if !resolved.starts_with(&resolved_root) {
701 return Err(path_error_response(req_id, path, &resolved_root));
702 }
703
704 Ok(resolved)
705 }
706
707 pub fn lsp_server_count(&self) -> usize {
709 self.lsp_manager
710 .try_borrow()
711 .map(|lsp| lsp.server_count())
712 .unwrap_or(0)
713 }
714
715 pub fn symbol_cache_stats(&self) -> serde_json::Value {
717 if let Some(tsp) = self
718 .provider
719 .as_any()
720 .downcast_ref::<crate::parser::TreeSitterProvider>()
721 {
722 let (local, warm) = tsp.symbol_cache_stats();
723 serde_json::json!({
724 "local_entries": local,
725 "warm_entries": warm,
726 })
727 } else {
728 serde_json::json!({
729 "local_entries": 0,
730 "warm_entries": 0,
731 })
732 }
733 }
734}