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:
77 RefCell<Option<crossbeam_channel::Receiver<(SearchIndex, crate::parser::SymbolCache)>>>,
78 watcher: RefCell<Option<RecommendedWatcher>>,
79 watcher_rx: RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>>,
80 lsp_manager: RefCell<LspManager>,
81}
82
83impl AppContext {
84 pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
85 AppContext {
86 provider,
87 backup: RefCell::new(BackupStore::new()),
88 checkpoint: RefCell::new(CheckpointStore::new()),
89 config: RefCell::new(config),
90 callgraph: RefCell::new(None),
91 search_index: RefCell::new(None),
92 search_index_rx: RefCell::new(None),
93 watcher: RefCell::new(None),
94 watcher_rx: RefCell::new(None),
95 lsp_manager: RefCell::new(LspManager::new()),
96 }
97 }
98
99 pub fn provider(&self) -> &dyn LanguageProvider {
101 self.provider.as_ref()
102 }
103
104 pub fn backup(&self) -> &RefCell<BackupStore> {
106 &self.backup
107 }
108
109 pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
111 &self.checkpoint
112 }
113
114 pub fn config(&self) -> Ref<'_, Config> {
116 self.config.borrow()
117 }
118
119 pub fn config_mut(&self) -> RefMut<'_, Config> {
121 self.config.borrow_mut()
122 }
123
124 pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
126 &self.callgraph
127 }
128
129 pub fn search_index(&self) -> &RefCell<Option<SearchIndex>> {
131 &self.search_index
132 }
133
134 pub fn search_index_rx(
136 &self,
137 ) -> &RefCell<Option<crossbeam_channel::Receiver<(SearchIndex, crate::parser::SymbolCache)>>>
138 {
139 &self.search_index_rx
140 }
141
142 pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
144 &self.watcher
145 }
146
147 pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
149 &self.watcher_rx
150 }
151
152 pub fn lsp(&self) -> RefMut<'_, LspManager> {
154 self.lsp_manager.borrow_mut()
155 }
156
157 pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
160 if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
161 if let Err(e) = lsp.notify_file_changed(file_path, content) {
162 log::warn!("sync error for {}: {}", file_path.display(), e);
163 }
164 }
165 }
166
167 pub fn lsp_notify_and_collect_diagnostics(
173 &self,
174 file_path: &Path,
175 content: &str,
176 timeout: std::time::Duration,
177 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
178 let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
179 return Vec::new();
180 };
181
182 lsp.drain_events();
185
186 if let Err(e) = lsp.notify_file_changed(file_path, content) {
188 log::warn!("sync error for {}: {}", file_path.display(), e);
189 return Vec::new();
190 }
191
192 lsp.wait_for_diagnostics(file_path, timeout)
194 }
195
196 pub fn lsp_post_write(
203 &self,
204 file_path: &Path,
205 content: &str,
206 params: &serde_json::Value,
207 ) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
208 let wants_diagnostics = params
209 .get("diagnostics")
210 .and_then(|v| v.as_bool())
211 .unwrap_or(false);
212
213 if !wants_diagnostics {
214 self.lsp_notify_file_changed(file_path, content);
215 return Vec::new();
216 }
217
218 let wait_ms = params
219 .get("wait_ms")
220 .and_then(|v| v.as_u64())
221 .unwrap_or(1500)
222 .min(10_000); self.lsp_notify_and_collect_diagnostics(
225 file_path,
226 content,
227 std::time::Duration::from_millis(wait_ms),
228 )
229 }
230
231 pub fn validate_path(
240 &self,
241 req_id: &str,
242 path: &Path,
243 ) -> Result<std::path::PathBuf, crate::protocol::Response> {
244 let config = self.config();
245 if !config.restrict_to_project_root {
247 return Ok(path.to_path_buf());
248 }
249 let root = match &config.project_root {
250 Some(r) => r.clone(),
251 None => return Ok(path.to_path_buf()), };
253 drop(config);
254
255 let resolved = std::fs::canonicalize(path)
257 .unwrap_or_else(|_| resolve_with_existing_ancestors(&normalize_path(path)));
258
259 let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
260
261 if !resolved.starts_with(&resolved_root) {
262 return Err(crate::protocol::Response::error(
263 req_id,
264 "path_outside_root",
265 format!(
266 "path '{}' is outside the project root '{}'",
267 path.display(),
268 resolved_root.display()
269 ),
270 ));
271 }
272
273 Ok(resolved)
274 }
275}