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