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 lsp.drain_events();
141
142 if let Err(e) = lsp.notify_file_changed(file_path, content) {
144 log::warn!("sync error for {}: {}", file_path.display(), e);
145 return Vec::new();
146 }
147
148 lsp.wait_for_diagnostics(file_path, timeout)
150 }
151
152 pub fn lsp_post_write(
159 &self,
160 file_path: &Path,
161 content: &str,
162 params: &serde_json::Value,
163 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
164 let wants_diagnostics = params
165 .get("diagnostics")
166 .and_then(|v| v.as_bool())
167 .unwrap_or(false);
168
169 if !wants_diagnostics {
170 self.lsp_notify_file_changed(file_path, content);
171 return Vec::new();
172 }
173
174 let wait_ms = params
175 .get("wait_ms")
176 .and_then(|v| v.as_u64())
177 .unwrap_or(1500)
178 .min(10_000); self.lsp_notify_and_collect_diagnostics(
181 file_path,
182 content,
183 std::time::Duration::from_millis(wait_ms),
184 )
185 }
186
187 pub fn validate_path(
196 &self,
197 req_id: &str,
198 path: &Path,
199 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
200 let config = self.config();
201 if !config.restrict_to_project_root {
203 return Ok(path.to_path_buf());
204 }
205 let root = match &config.project_root {
206 Some(r) => r.clone(),
207 None => return Ok(path.to_path_buf()), };
209 drop(config);
210
211 let resolved = std::fs::canonicalize(path).unwrap_or_else(|_| {
213 if let Some(parent) = path.parent() {
217 let resolved_parent =
218 std::fs::canonicalize(parent).unwrap_or_else(|_| normalize_path(parent));
219 if let Some(name) = path.file_name() {
220 return resolved_parent.join(name);
221 }
222 }
223 normalize_path(path)
224 });
225
226 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
227
228 if !resolved.starts_with(&resolved_root) {
229 return Err(crate::protocol::Response::error(
230 req_id,
231 "path_outside_root",
232 format!(
233 "path '{}' is outside the project root '{}'",
234 path.display(),
235 resolved_root.display()
236 ),
237 ));
238 }
239
240 Ok(resolved)
241 }
242}