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;
13use crate::search_index::SearchIndex;
14
15fn normalize_path(path: &Path) -> PathBuf {
19 let mut result = PathBuf::new();
20 for component in path.components() {
21 match component {
22 Component::ParentDir => {
23 if !result.pop() {
25 result.push(component);
26 }
27 }
28 Component::CurDir => {} _ => result.push(component),
30 }
31 }
32 result
33}
34
35fn resolve_with_existing_ancestors(path: &Path) -> PathBuf {
36 let mut existing = path.to_path_buf();
37 let mut tail_segments = Vec::new();
38
39 while !existing.exists() {
40 if let Some(name) = existing.file_name() {
41 tail_segments.push(name.to_owned());
42 } else {
43 break;
44 }
45
46 existing = match existing.parent() {
47 Some(parent) => parent.to_path_buf(),
48 None => break,
49 };
50 }
51
52 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
53 for segment in tail_segments.into_iter().rev() {
54 resolved.push(segment);
55 }
56
57 resolved
58}
59
60pub struct AppContext {
70 provider: Box<dyn LanguageProvider>,
71 backup: RefCell<BackupStore>,
72 checkpoint: RefCell<CheckpointStore>,
73 config: RefCell<Config>,
74 callgraph: RefCell<Option<CallGraph>>,
75 search_index: RefCell<Option<SearchIndex>>,
76 search_index_rx: RefCell<Option<crossbeam_channel::Receiver<SearchIndex>>>,
77 watcher: RefCell<Option<RecommendedWatcher>>,
78 watcher_rx: RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>>,
79 lsp_manager: RefCell<LspManager>,
80}
81
82impl AppContext {
83 pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
84 AppContext {
85 provider,
86 backup: RefCell::new(BackupStore::new()),
87 checkpoint: RefCell::new(CheckpointStore::new()),
88 config: RefCell::new(config),
89 callgraph: RefCell::new(None),
90 search_index: RefCell::new(None),
91 search_index_rx: RefCell::new(None),
92 watcher: RefCell::new(None),
93 watcher_rx: RefCell::new(None),
94 lsp_manager: RefCell::new(LspManager::new()),
95 }
96 }
97
98 pub fn provider(&self) -> &dyn LanguageProvider {
100 self.provider.as_ref()
101 }
102
103 pub fn backup(&self) -> &RefCell<BackupStore> {
105 &self.backup
106 }
107
108 pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
110 &self.checkpoint
111 }
112
113 pub fn config(&self) -> Ref<'_, Config> {
115 self.config.borrow()
116 }
117
118 pub fn config_mut(&self) -> RefMut<'_, Config> {
120 self.config.borrow_mut()
121 }
122
123 pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
125 &self.callgraph
126 }
127
128 pub fn search_index(&self) -> &RefCell<Option<SearchIndex>> {
130 &self.search_index
131 }
132
133 pub fn search_index_rx(&self) -> &RefCell<Option<crossbeam_channel::Receiver<SearchIndex>>> {
135 &self.search_index_rx
136 }
137
138 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
140 &self.watcher
141 }
142
143 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
145 &self.watcher_rx
146 }
147
148 pub fn lsp(&self) -> RefMut<'_, LspManager> {
150 self.lsp_manager.borrow_mut()
151 }
152
153 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
156 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
157 if let Err(e) = lsp.notify_file_changed(file_path, content) {
158 log::warn!("sync error for {}: {}", file_path.display(), e);
159 }
160 }
161 }
162
163 pub fn lsp_notify_and_collect_diagnostics(
169 &self,
170 file_path: &Path,
171 content: &str,
172 timeout: std::time::Duration,
173 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
174 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
175 return Vec::new();
176 };
177
178 lsp.drain_events();
181
182 if let Err(e) = lsp.notify_file_changed(file_path, content) {
184 log::warn!("sync error for {}: {}", file_path.display(), e);
185 return Vec::new();
186 }
187
188 lsp.wait_for_diagnostics(file_path, timeout)
190 }
191
192 pub fn lsp_post_write(
199 &self,
200 file_path: &Path,
201 content: &str,
202 params: &serde_json::Value,
203 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
204 let wants_diagnostics = params
205 .get("diagnostics")
206 .and_then(|v| v.as_bool())
207 .unwrap_or(false);
208
209 if !wants_diagnostics {
210 self.lsp_notify_file_changed(file_path, content);
211 return Vec::new();
212 }
213
214 let wait_ms = params
215 .get("wait_ms")
216 .and_then(|v| v.as_u64())
217 .unwrap_or(1500)
218 .min(10_000); self.lsp_notify_and_collect_diagnostics(
221 file_path,
222 content,
223 std::time::Duration::from_millis(wait_ms),
224 )
225 }
226
227 pub fn validate_path(
236 &self,
237 req_id: &str,
238 path: &Path,
239 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
240 let config = self.config();
241 if !config.restrict_to_project_root {
243 return Ok(path.to_path_buf());
244 }
245 let root = match &config.project_root {
246 Some(r) => r.clone(),
247 None => return Ok(path.to_path_buf()), };
249 drop(config);
250
251 let resolved = std::fs::canonicalize(path)
253 .unwrap_or_else(|_| resolve_with_existing_ancestors(&normalize_path(path)));
254
255 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
256
257 if !resolved.starts_with(&resolved_root) {
258 return Err(crate::protocol::Response::error(
259 req_id,
260 "path_outside_root",
261 format!(
262 "path '{}' is outside the project root '{}'",
263 path.display(),
264 resolved_root.display()
265 ),
266 ));
267 }
268
269 Ok(resolved)
270 }
271}