1use std::cell::{Ref, RefCell, RefMut};
2use std::path::{Component, Path, PathBuf};
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
14fn normalize_path(path: &Path) -> PathBuf {
18 let mut result = PathBuf::new();
19 for component in path.components() {
20 match component {
21 Component::ParentDir => {
22 if !result.pop() {
24 result.push(component);
25 }
26 }
27 Component::CurDir => {} _ => result.push(component),
29 }
30 }
31 result
32}
33
34pub struct AppContext {
44 provider: Box<dyn LanguageProvider>,
45 backup: RefCell<BackupStore>,
46 checkpoint: RefCell<CheckpointStore>,
47 config: RefCell<Config>,
48 callgraph: RefCell<Option<CallGraph>>,
49 watcher: RefCell<Option<RecommendedWatcher>>,
50 watcher_rx: RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>>,
51 lsp_manager: RefCell<LspManager>,
52}
53
54impl AppContext {
55 pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
56 AppContext {
57 provider,
58 backup: RefCell::new(BackupStore::new()),
59 checkpoint: RefCell::new(CheckpointStore::new()),
60 config: RefCell::new(config),
61 callgraph: RefCell::new(None),
62 watcher: RefCell::new(None),
63 watcher_rx: RefCell::new(None),
64 lsp_manager: RefCell::new(LspManager::new()),
65 }
66 }
67
68 pub fn provider(&self) -> &dyn LanguageProvider {
70 self.provider.as_ref()
71 }
72
73 pub fn backup(&self) -> &RefCell<BackupStore> {
75 &self.backup
76 }
77
78 pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
80 &self.checkpoint
81 }
82
83 pub fn config(&self) -> Ref<'_, Config> {
85 self.config.borrow()
86 }
87
88 pub fn config_mut(&self) -> RefMut<'_, Config> {
90 self.config.borrow_mut()
91 }
92
93 pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
95 &self.callgraph
96 }
97
98 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
100 &self.watcher
101 }
102
103 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
105 &self.watcher_rx
106 }
107
108 pub fn lsp(&self) -> RefMut<'_, LspManager> {
110 self.lsp_manager.borrow_mut()
111 }
112
113 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
116 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
117 if let Err(e) = lsp.notify_file_changed(file_path, content) {
118 log::warn!("sync error for {}: {}", file_path.display(), e);
119 }
120 }
121 }
122
123 pub fn lsp_notify_and_collect_diagnostics(
129 &self,
130 file_path: &Path,
131 content: &str,
132 timeout: std::time::Duration,
133 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
134 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
135 return Vec::new();
136 };
137
138 if let Err(e) = lsp.notify_file_changed(file_path, content) {
140 log::warn!("sync error for {}: {}", file_path.display(), e);
141 return Vec::new();
142 }
143
144 lsp.wait_for_diagnostics(file_path, timeout)
146 }
147
148 pub fn lsp_post_write(
155 &self,
156 file_path: &Path,
157 content: &str,
158 params: &serde_json::Value,
159 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
160 let wants_diagnostics = params
161 .get("diagnostics")
162 .and_then(|v| v.as_bool())
163 .unwrap_or(false);
164
165 self.lsp_notify_file_changed(file_path, content);
166
167 if !wants_diagnostics {
168 return Vec::new();
169 }
170
171 let mut lsp = self.lsp();
173 if !lsp.has_active_servers() {
174 return Vec::new();
175 }
176
177 let wait_ms = params
178 .get("wait_ms")
179 .and_then(|v| v.as_u64())
180 .unwrap_or(1500)
181 .min(10_000); std::thread::sleep(std::time::Duration::from_millis(wait_ms));
183
184 let canonical_path =
185 std::fs::canonicalize(file_path).unwrap_or_else(|_| file_path.to_path_buf());
186 lsp.drain_events();
187 lsp.get_diagnostics_for_file(&canonical_path)
188 .into_iter()
189 .cloned()
190 .collect()
191 }
192
193 pub fn validate_path(
202 &self,
203 req_id: &str,
204 path: &Path,
205 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
206 let config = self.config();
207 if !config.restrict_to_project_root {
209 return Ok(path.to_path_buf());
210 }
211 let root = match &config.project_root {
212 Some(r) => r.clone(),
213 None => return Ok(path.to_path_buf()), };
215 drop(config);
216
217 let resolved = std::fs::canonicalize(path).unwrap_or_else(|_| {
219 if let Some(parent) = path.parent() {
223 let resolved_parent =
224 std::fs::canonicalize(parent).unwrap_or_else(|_| normalize_path(parent));
225 if let Some(name) = path.file_name() {
226 return resolved_parent.join(name);
227 }
228 }
229 normalize_path(path)
230 });
231
232 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
233
234 if !resolved.starts_with(&resolved_root) {
235 return Err(crate::protocol::Response::error(
236 req_id,
237 "path_outside_root",
238 format!(
239 "path '{}' is outside the project root '{}'",
240 path.display(),
241 resolved_root.display()
242 ),
243 ));
244 }
245
246 Ok(resolved)
247 }
248}