Skip to main content

aft/
context.rs

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
14/// Shared application context threaded through all command handlers.
15///
16/// Holds the language provider, backup/checkpoint stores, configuration,
17/// and call graph engine. Constructed once at startup and passed by
18/// reference to `dispatch`.
19///
20/// Stores use `RefCell` for interior mutability — the binary is single-threaded
21/// (one request at a time on the stdin read loop) so runtime borrow checking
22/// is safe and never contended.
23pub 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    /// Access the language provider.
49    pub fn provider(&self) -> &dyn LanguageProvider {
50        self.provider.as_ref()
51    }
52
53    /// Access the backup store.
54    pub fn backup(&self) -> &RefCell<BackupStore> {
55        &self.backup
56    }
57
58    /// Access the checkpoint store.
59    pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
60        &self.checkpoint
61    }
62
63    /// Access the configuration (shared borrow).
64    pub fn config(&self) -> Ref<'_, Config> {
65        self.config.borrow()
66    }
67
68    /// Access the configuration (mutable borrow).
69    pub fn config_mut(&self) -> RefMut<'_, Config> {
70        self.config.borrow_mut()
71    }
72
73    /// Access the call graph engine.
74    pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
75        &self.callgraph
76    }
77
78    /// Access the file watcher handle (kept alive to continue watching).
79    pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
80        &self.watcher
81    }
82
83    /// Access the watcher event receiver.
84    pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
85        &self.watcher_rx
86    }
87
88    /// Access the LSP manager.
89    pub fn lsp(&self) -> RefMut<'_, LspManager> {
90        self.lsp_manager.borrow_mut()
91    }
92
93    /// Notify LSP servers that a file was written.
94    /// Call this after write_format_validate in command handlers.
95    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    /// Notify LSP and optionally wait for diagnostics.
104    ///
105    /// Call this after `write_format_validate` when the request has `"diagnostics": true`.
106    /// Sends didChange to the server, waits briefly for publishDiagnostics, and returns
107    /// any diagnostics for the file. If no server is running, returns empty immediately.
108    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        // Send didChange/didOpen
119        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        // Wait for diagnostics to arrive
125        lsp.wait_for_diagnostics(file_path, timeout)
126    }
127
128    /// Post-write LSP hook: notify server and optionally collect diagnostics.
129    ///
130    /// This is the single call site for all command handlers after `write_format_validate`.
131    /// When `diagnostics` is true, it notifies the server, waits briefly, drains
132    /// queued LSP notifications, and returns diagnostics for the file.
133    /// When false, it just notifies (fire-and-forget).
134    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        // Only wait if an LSP server is actually running
152        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); // Cap at 10 seconds to prevent hangs from adversarial input
162        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    /// Validate that a file path falls within the configured project root.
174    ///
175    /// When `project_root` is configured (normal plugin usage), this resolves the
176    /// path and checks it starts with the root. Returns the canonicalized path on
177    /// success, or an error response on violation.
178    ///
179    /// When no `project_root` is configured (direct CLI usage), all paths pass
180    /// through unrestricted for backward compatibility.
181    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        // When restrict_to_project_root is false (default), allow all paths
188        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()), // No root configured, allow all
194        };
195        drop(config);
196
197        // Resolve the path (follow symlinks, normalize ..)
198        let resolved = std::fs::canonicalize(path).unwrap_or_else(|_| {
199            // For new files that don't exist yet, resolve parent + filename
200            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}