1use std::cell::{Ref, RefCell, RefMut};
2use std::path::Path;
3use std::sync::mpsc;
4
5use notify::RecommendedWatcher;
6
7use crate::backup::BackupStore;
8use crate::callgraph::CallGraph;
9use crate::checkpoint::CheckpointStore;
10use crate::config::Config;
11use crate::language::LanguageProvider;
12use crate::lsp::manager::LspManager;
13
14pub struct AppContext {
24 provider: Box<dyn LanguageProvider>,
25 backup: RefCell<BackupStore>,
26 checkpoint: RefCell<CheckpointStore>,
27 config: RefCell<Config>,
28 callgraph: RefCell<Option<CallGraph>>,
29 watcher: RefCell<Option<RecommendedWatcher>>,
30 watcher_rx: RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>>,
31 lsp_manager: RefCell<LspManager>,
32}
33
34impl AppContext {
35 pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
36 AppContext {
37 provider,
38 backup: RefCell::new(BackupStore::new()),
39 checkpoint: RefCell::new(CheckpointStore::new()),
40 config: RefCell::new(config),
41 callgraph: RefCell::new(None),
42 watcher: RefCell::new(None),
43 watcher_rx: RefCell::new(None),
44 lsp_manager: RefCell::new(LspManager::new()),
45 }
46 }
47
48 pub fn provider(&self) -> &dyn LanguageProvider {
50 self.provider.as_ref()
51 }
52
53 pub fn backup(&self) -> &RefCell<BackupStore> {
55 &self.backup
56 }
57
58 pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
60 &self.checkpoint
61 }
62
63 pub fn config(&self) -> Ref<'_, Config> {
65 self.config.borrow()
66 }
67
68 pub fn config_mut(&self) -> RefMut<'_, Config> {
70 self.config.borrow_mut()
71 }
72
73 pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
75 &self.callgraph
76 }
77
78 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
80 &self.watcher
81 }
82
83 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
85 &self.watcher_rx
86 }
87
88 pub fn lsp(&self) -> RefMut<'_, LspManager> {
90 self.lsp_manager.borrow_mut()
91 }
92
93 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
96 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
97 if let Err(e) = lsp.notify_file_changed(file_path, content) {
98 log::warn!("sync error for {}: {}", file_path.display(), e);
99 }
100 }
101 }
102
103 pub fn lsp_notify_and_collect_diagnostics(
109 &self,
110 file_path: &Path,
111 content: &str,
112 timeout: std::time::Duration,
113 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
114 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
115 return Vec::new();
116 };
117
118 if let Err(e) = lsp.notify_file_changed(file_path, content) {
120 log::warn!("sync error for {}: {}", file_path.display(), e);
121 return Vec::new();
122 }
123
124 lsp.wait_for_diagnostics(file_path, timeout)
126 }
127
128 pub fn lsp_post_write(
135 &self,
136 file_path: &Path,
137 content: &str,
138 params: &serde_json::Value,
139 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
140 let wants_diagnostics = params
141 .get("diagnostics")
142 .and_then(|v| v.as_bool())
143 .unwrap_or(false);
144
145 self.lsp_notify_file_changed(file_path, content);
146
147 if !wants_diagnostics {
148 return Vec::new();
149 }
150
151 let mut lsp = self.lsp();
153 if !lsp.has_active_servers() {
154 return Vec::new();
155 }
156
157 let wait_ms = params
158 .get("wait_ms")
159 .and_then(|v| v.as_u64())
160 .unwrap_or(1500)
161 .min(10_000); std::thread::sleep(std::time::Duration::from_millis(wait_ms));
163
164 let canonical_path =
165 std::fs::canonicalize(file_path).unwrap_or_else(|_| file_path.to_path_buf());
166 lsp.drain_events();
167 lsp.get_diagnostics_for_file(&canonical_path)
168 .into_iter()
169 .cloned()
170 .collect()
171 }
172
173 pub fn validate_path(
182 &self,
183 req_id: &str,
184 path: &Path,
185 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
186 let config = self.config();
187 if !config.restrict_to_project_root {
189 return Ok(path.to_path_buf());
190 }
191 let root = match &config.project_root {
192 Some(r) => r.clone(),
193 None => return Ok(path.to_path_buf()), };
195 drop(config);
196
197 let resolved = std::fs::canonicalize(path).unwrap_or_else(|_| {
199 if let Some(parent) = path.parent() {
201 let resolved_parent =
202 std::fs::canonicalize(parent).unwrap_or_else(|_| parent.to_path_buf());
203 if let Some(name) = path.file_name() {
204 return resolved_parent.join(name);
205 }
206 }
207 path.to_path_buf()
208 });
209
210 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
211
212 if !resolved.starts_with(&resolved_root) {
213 return Err(crate::protocol::Response::error(
214 req_id,
215 "path_outside_root",
216 format!(
217 "path '{}' is outside the project root '{}'",
218 path.display(),
219 resolved_root.display()
220 ),
221 ));
222 }
223
224 Ok(resolved)
225 }
226}