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                eprintln!("[aft-lsp] 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            eprintln!("[aft-lsp] 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        std::thread::sleep(std::time::Duration::from_millis(1500));
152        let canonical_path =
153            std::fs::canonicalize(file_path).unwrap_or_else(|_| file_path.to_path_buf());
154
155        let mut lsp = self.lsp();
156        lsp.drain_events();
157        lsp.get_diagnostics_for_file(&canonical_path)
158            .into_iter()
159            .cloned()
160            .collect()
161    }
162}