Skip to main content

fresh/services/
async_bridge.rs

1//! Async Bridge: Communication between async Tokio runtime and sync main loop
2//!
3//! This module implements the hybrid architecture described in TOKIO_ANALYSIS.md:
4//! - Tokio runtime handles I/O tasks (LSP, file watching, git, etc.)
5//! - Main UI loop stays synchronous (rendering, input, buffer manipulation)
6//! - std::sync::mpsc channels bridge the two worlds
7//!
8//! Philosophy:
9//! - I/O should be async (LSP, filesystem, network)
10//! - Computation should be sync (editing, rendering)
11//! - Main loop remains responsive and simple
12
13use crate::services::terminal::TerminalId;
14use crate::view::file_tree::{FileTreeView, NodeId};
15use lsp_types::{
16    CodeActionOrCommand, CompletionItem, Diagnostic, FoldingRange, InlayHint, Location,
17    SemanticTokensFullDeltaResult, SemanticTokensRangeResult, SemanticTokensResult, SignatureHelp,
18};
19use serde_json::Value;
20use std::sync::mpsc;
21
22/// Semantic token responses grouped by request type.
23#[derive(Debug)]
24pub enum LspSemanticTokensResponse {
25    Full(Result<Option<SemanticTokensResult>, String>),
26    FullDelta(Result<Option<SemanticTokensFullDeltaResult>, String>),
27    Range(Result<Option<SemanticTokensRangeResult>, String>),
28}
29
30/// Messages sent from async tasks to the synchronous main loop
31#[derive(Debug)]
32pub enum AsyncMessage {
33    /// LSP diagnostics received for a file
34    LspDiagnostics {
35        uri: String,
36        diagnostics: Vec<Diagnostic>,
37        /// Name of the server that sent these diagnostics (for per-server tracking)
38        server_name: String,
39    },
40
41    /// LSP server initialized successfully
42    LspInitialized {
43        language: String,
44        /// Name of the specific server (for per-server capability tracking)
45        server_name: String,
46        /// Capabilities reported by this server
47        capabilities: crate::services::lsp::manager::ServerCapabilitySummary,
48    },
49
50    /// LSP server crashed or failed
51    LspError {
52        language: String,
53        error: String,
54        /// Path to the stderr log file for this LSP session
55        stderr_log_path: Option<std::path::PathBuf>,
56    },
57
58    /// LSP completion response
59    LspCompletion {
60        request_id: u64,
61        items: Vec<CompletionItem>,
62    },
63
64    /// LSP go-to-definition response
65    LspGotoDefinition {
66        request_id: u64,
67        locations: Vec<Location>,
68    },
69
70    /// LSP rename response
71    LspRename {
72        request_id: u64,
73        result: Result<lsp_types::WorkspaceEdit, String>,
74    },
75
76    /// LSP hover response
77    LspHover {
78        request_id: u64,
79        /// Hover contents as a single string (joined if multiple parts)
80        contents: String,
81        /// Whether the content is markdown (true) or plaintext (false)
82        is_markdown: bool,
83        /// Optional range of the symbol that was hovered over (LSP line/character positions)
84        /// Used to highlight the hovered symbol
85        range: Option<((u32, u32), (u32, u32))>,
86    },
87
88    /// LSP find references response
89    LspReferences {
90        request_id: u64,
91        locations: Vec<Location>,
92    },
93
94    /// LSP signature help response
95    LspSignatureHelp {
96        request_id: u64,
97        signature_help: Option<SignatureHelp>,
98    },
99
100    /// LSP code actions response
101    LspCodeActions {
102        request_id: u64,
103        actions: Vec<CodeActionOrCommand>,
104    },
105
106    /// LSP completionItem/resolve response
107    LspCompletionResolved {
108        request_id: u64,
109        item: Result<lsp_types::CompletionItem, String>,
110    },
111
112    /// LSP textDocument/formatting response
113    LspFormatting {
114        request_id: u64,
115        uri: String,
116        edits: Vec<lsp_types::TextEdit>,
117    },
118
119    /// LSP textDocument/prepareRename response
120    LspPrepareRename {
121        request_id: u64,
122        result: Result<serde_json::Value, String>,
123    },
124
125    /// LSP pulled diagnostics response (textDocument/diagnostic)
126    LspPulledDiagnostics {
127        request_id: u64,
128        uri: String,
129        /// New result_id for incremental updates (None if server doesn't support)
130        result_id: Option<String>,
131        /// Diagnostics (empty if unchanged)
132        diagnostics: Vec<Diagnostic>,
133        /// True if diagnostics haven't changed since previous_result_id
134        unchanged: bool,
135    },
136
137    /// LSP inlay hints response (textDocument/inlayHint)
138    LspInlayHints {
139        request_id: u64,
140        uri: String,
141        /// Inlay hints for the requested range
142        hints: Vec<InlayHint>,
143    },
144
145    /// LSP folding ranges response (textDocument/foldingRange)
146    LspFoldingRanges {
147        request_id: u64,
148        uri: String,
149        ranges: Vec<FoldingRange>,
150    },
151
152    /// LSP semantic tokens response (full, full/delta, or range)
153    LspSemanticTokens {
154        request_id: u64,
155        uri: String,
156        response: LspSemanticTokensResponse,
157    },
158
159    /// LSP server status became quiescent (project fully loaded)
160    /// This is a rust-analyzer specific notification (experimental/serverStatus)
161    LspServerQuiescent { language: String },
162
163    /// LSP server requests diagnostic refresh (workspace/diagnostic/refresh)
164    /// Client should re-pull diagnostics for all open documents
165    LspDiagnosticRefresh { language: String },
166
167    /// File changed externally (future: file watching)
168    FileChanged { path: String },
169
170    /// Git status updated (future: git integration)
171    GitStatusChanged { status: String },
172
173    /// File explorer initialized with tree view
174    FileExplorerInitialized(FileTreeView),
175
176    /// File explorer node toggle completed
177    FileExplorerToggleNode(NodeId),
178
179    /// File explorer node refresh completed
180    FileExplorerRefreshNode(NodeId),
181
182    /// File explorer expand to path completed
183    /// Contains the updated FileTreeView with the path expanded and selected
184    FileExplorerExpandedToPath(FileTreeView),
185
186    /// Plugin-related async messages
187    Plugin(fresh_core::api::PluginAsyncMessage),
188
189    /// File open dialog: directory listing completed
190    FileOpenDirectoryLoaded(std::io::Result<Vec<crate::services::fs::DirEntry>>),
191
192    /// File open dialog: async shortcuts (Windows drive letters) loaded
193    FileOpenShortcutsLoaded(Vec<crate::app::file_open::NavigationShortcut>),
194
195    /// Terminal output received (triggers redraw)
196    TerminalOutput { terminal_id: TerminalId },
197
198    /// Terminal process exited
199    TerminalExited { terminal_id: TerminalId },
200
201    /// LSP progress notification ($/progress)
202    LspProgress {
203        language: String,
204        token: String,
205        value: LspProgressValue,
206    },
207
208    /// LSP window message (window/showMessage)
209    LspWindowMessage {
210        language: String,
211        message_type: LspMessageType,
212        message: String,
213    },
214
215    /// LSP log message (window/logMessage)
216    LspLogMessage {
217        language: String,
218        message_type: LspMessageType,
219        message: String,
220    },
221
222    /// LSP workspace/applyEdit (server -> client request)
223    /// Server asks client to apply a workspace edit (during executeCommand, etc.)
224    LspApplyEdit {
225        edit: lsp_types::WorkspaceEdit,
226        label: Option<String>,
227    },
228
229    /// LSP codeAction/resolve response
230    LspCodeActionResolved {
231        request_id: u64,
232        action: Result<lsp_types::CodeAction, String>,
233    },
234
235    /// LSP server request (server -> client)
236    /// Used for custom/extension methods that plugins can handle
237    LspServerRequest {
238        language: String,
239        server_command: String,
240        method: String,
241        params: Option<Value>,
242    },
243
244    /// Response for a plugin-initiated LSP request
245    PluginLspResponse {
246        language: String,
247        request_id: u64,
248        result: Result<Value, String>,
249    },
250
251    /// Plugin process completed with output
252    PluginProcessOutput {
253        /// Unique ID for this process (to match with callback)
254        process_id: u64,
255        /// Standard output
256        stdout: String,
257        /// Standard error
258        stderr: String,
259        /// Exit code
260        exit_code: i32,
261    },
262
263    /// LSP server status update (progress, messages, etc.)
264    LspStatusUpdate {
265        language: String,
266        /// Name of the specific server (for multi-server status tracking)
267        server_name: String,
268        status: LspServerStatus,
269        message: Option<String>,
270    },
271
272    /// Background grammar build completed — swap in the new registry.
273    /// `callback_ids` contains plugin callbacks to resolve (empty for the
274    /// initial startup build).
275    GrammarRegistryBuilt {
276        registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
277        callback_ids: Vec<fresh_core::api::JsCallbackId>,
278    },
279
280    /// Quick Open file list loaded by a background task.
281    /// `complete` is `true` when the scan is finished, `false` for incremental
282    /// partial updates sent while the walk is still in progress.
283    QuickOpenFilesLoaded {
284        files: std::sync::Arc<Vec<crate::input::quick_open::providers::FileEntry>>,
285        complete: bool,
286    },
287
288    /// Startup-async: a single plugin directory finished loading on the
289    /// plugin thread. Carries the same payload as the blocking
290    /// `load_plugins_from_dir_with_config` return value.
291    PluginsDirLoaded {
292        dir: std::path::PathBuf,
293        errors: Vec<String>,
294        discovered_plugins: std::collections::HashMap<String, fresh_core::config::PluginConfig>,
295    },
296
297    /// Startup-async: every directory in the startup batch has loaded and
298    /// the resulting `.d.ts` declarations have been collected from the
299    /// plugin runtime. Triggers `init_script::write_plugin_declarations`.
300    PluginDeclarationsReady { declarations: Vec<(String, String)> },
301
302    /// Startup-async: `init.ts` (auto-loaded source plugin) finished
303    /// running its top level and has either succeeded, failed, or was
304    /// skipped/fused. The handler logs and applies the corresponding
305    /// status message, and (on `Loaded`) clears the crash fuse.
306    PluginInitScriptLoaded(PluginInitScriptOutcome),
307}
308
309/// Async equivalent of `init_script::InitOutcome`. Wraps the same set
310/// of states but is plain data so it can travel across the bridge.
311#[derive(Debug, Clone)]
312pub enum PluginInitScriptOutcome {
313    NotFound,
314    Disabled,
315    CrashFused { failures: u32 },
316    Loaded,
317    Failed { message: String },
318}
319
320/// LSP progress value types
321#[derive(Debug, Clone)]
322pub enum LspProgressValue {
323    Begin {
324        title: String,
325        message: Option<String>,
326        percentage: Option<u32>,
327    },
328    Report {
329        message: Option<String>,
330        percentage: Option<u32>,
331    },
332    End {
333        message: Option<String>,
334    },
335}
336
337/// LSP message type (corresponds to MessageType in LSP spec)
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub enum LspMessageType {
340    Error = 1,
341    Warning = 2,
342    Info = 3,
343    Log = 4,
344}
345
346/// LSP server status
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum LspServerStatus {
349    Starting,
350    Initializing,
351    Running,
352    Error,
353    Shutdown,
354}
355
356/// Bridge between async Tokio runtime and sync main loop
357///
358/// Design:
359/// - Lightweight, cloneable sender that can be passed to async tasks
360/// - Non-blocking receiver checked each frame in main loop
361/// - No locks needed in main loop (channel handles synchronization)
362#[derive(Clone)]
363pub struct AsyncBridge {
364    sender: mpsc::Sender<AsyncMessage>,
365    // Receiver wrapped in Arc<Mutex<>> to allow cloning
366    receiver: std::sync::Arc<std::sync::Mutex<mpsc::Receiver<AsyncMessage>>>,
367}
368
369impl AsyncBridge {
370    /// Create a new async bridge with an unbounded channel
371    ///
372    /// Unbounded is appropriate here because:
373    /// 1. Main loop processes messages every 16ms (60fps)
374    /// 2. LSP messages are infrequent (< 100/sec typically)
375    /// 3. Memory usage is bounded by message rate × frame time
376    pub fn new() -> Self {
377        let (sender, receiver) = mpsc::channel();
378        Self {
379            sender,
380            receiver: std::sync::Arc::new(std::sync::Mutex::new(receiver)),
381        }
382    }
383
384    /// Get a cloneable sender for async tasks
385    ///
386    /// This sender can be:
387    /// - Cloned freely (cheap Arc internally)
388    /// - Sent to async tasks
389    /// - Stored in LspClient instances
390    pub fn sender(&self) -> mpsc::Sender<AsyncMessage> {
391        self.sender.clone()
392    }
393
394    /// Try to receive pending messages (non-blocking)
395    ///
396    /// Called each frame in the main loop to process async messages.
397    /// Returns all pending messages without blocking.
398    pub fn try_recv_all(&self) -> Vec<AsyncMessage> {
399        let mut messages = Vec::new();
400
401        // Lock the receiver and drain all pending messages
402        if let Ok(receiver) = self.receiver.lock() {
403            while let Ok(msg) = receiver.try_recv() {
404                messages.push(msg);
405            }
406        }
407
408        messages
409    }
410
411    /// Check if there are pending messages (non-blocking)
412    pub fn has_messages(&self) -> bool {
413        // Note: This is racy but safe - only used for optimization
414        if let Ok(receiver) = self.receiver.lock() {
415            receiver.try_recv().is_ok()
416        } else {
417            false
418        }
419    }
420}
421
422impl Default for AsyncBridge {
423    fn default() -> Self {
424        Self::new()
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn test_async_bridge_send_receive() {
434        let bridge = AsyncBridge::new();
435        let sender = bridge.sender();
436
437        // Send a message
438        sender
439            .send(AsyncMessage::LspInitialized {
440                language: "rust".to_string(),
441                server_name: "test".to_string(),
442                capabilities: Default::default(),
443            })
444            .unwrap();
445
446        // Receive it
447        let messages = bridge.try_recv_all();
448        assert_eq!(messages.len(), 1);
449
450        match &messages[0] {
451            AsyncMessage::LspInitialized {
452                language,
453                server_name,
454                ..
455            } => {
456                assert_eq!(language, "rust");
457                assert_eq!(server_name, "test");
458            }
459            _ => panic!("Wrong message type"),
460        }
461    }
462
463    #[test]
464    fn test_async_bridge_multiple_messages() {
465        let bridge = AsyncBridge::new();
466        let sender = bridge.sender();
467
468        // Send multiple messages
469        sender
470            .send(AsyncMessage::LspInitialized {
471                language: "rust".to_string(),
472                server_name: "test".to_string(),
473                capabilities: Default::default(),
474            })
475            .unwrap();
476        sender
477            .send(AsyncMessage::LspInitialized {
478                language: "typescript".to_string(),
479                server_name: "test".to_string(),
480                capabilities: Default::default(),
481            })
482            .unwrap();
483
484        // Receive all at once
485        let messages = bridge.try_recv_all();
486        assert_eq!(messages.len(), 2);
487    }
488
489    #[test]
490    fn test_async_bridge_no_messages() {
491        let bridge = AsyncBridge::new();
492
493        // Try to receive with no messages
494        let messages = bridge.try_recv_all();
495        assert_eq!(messages.len(), 0);
496    }
497
498    #[test]
499    fn test_async_bridge_clone_sender() {
500        let bridge = AsyncBridge::new();
501        let sender1 = bridge.sender();
502        let sender2 = sender1.clone();
503
504        // Both senders work
505        sender1
506            .send(AsyncMessage::LspInitialized {
507                language: "rust".to_string(),
508                server_name: "test".to_string(),
509                capabilities: Default::default(),
510            })
511            .unwrap();
512        sender2
513            .send(AsyncMessage::LspInitialized {
514                language: "typescript".to_string(),
515                server_name: "test".to_string(),
516                capabilities: Default::default(),
517            })
518            .unwrap();
519
520        let messages = bridge.try_recv_all();
521        assert_eq!(messages.len(), 2);
522    }
523
524    #[test]
525    fn test_async_bridge_diagnostics() {
526        let bridge = AsyncBridge::new();
527        let sender = bridge.sender();
528
529        // Send diagnostic message
530        let diagnostics = vec![lsp_types::Diagnostic {
531            range: lsp_types::Range {
532                start: lsp_types::Position {
533                    line: 0,
534                    character: 0,
535                },
536                end: lsp_types::Position {
537                    line: 0,
538                    character: 5,
539                },
540            },
541            severity: Some(lsp_types::DiagnosticSeverity::ERROR),
542            code: None,
543            code_description: None,
544            source: Some("rust-analyzer".to_string()),
545            message: "test error".to_string(),
546            related_information: None,
547            tags: None,
548            data: None,
549        }];
550
551        sender
552            .send(AsyncMessage::LspDiagnostics {
553                uri: "file:///test.rs".to_string(),
554                diagnostics: diagnostics.clone(),
555                server_name: "rust-analyzer".to_string(),
556            })
557            .unwrap();
558
559        let messages = bridge.try_recv_all();
560        assert_eq!(messages.len(), 1);
561
562        match &messages[0] {
563            AsyncMessage::LspDiagnostics {
564                uri,
565                diagnostics: diags,
566                server_name,
567            } => {
568                assert_eq!(uri, "file:///test.rs");
569                assert_eq!(diags.len(), 1);
570                assert_eq!(diags[0].message, "test error");
571                assert_eq!(server_name, "rust-analyzer");
572            }
573            _ => panic!("Expected LspDiagnostics message"),
574        }
575    }
576
577    #[test]
578    fn test_async_bridge_error_message() {
579        let bridge = AsyncBridge::new();
580        let sender = bridge.sender();
581
582        sender
583            .send(AsyncMessage::LspError {
584                language: "rust".to_string(),
585                error: "Failed to initialize".to_string(),
586                stderr_log_path: None,
587            })
588            .unwrap();
589
590        let messages = bridge.try_recv_all();
591        assert_eq!(messages.len(), 1);
592
593        match &messages[0] {
594            AsyncMessage::LspError {
595                language,
596                error,
597                stderr_log_path,
598            } => {
599                assert_eq!(language, "rust");
600                assert_eq!(error, "Failed to initialize");
601                assert!(stderr_log_path.is_none());
602            }
603            _ => panic!("Expected LspError message"),
604        }
605    }
606
607    #[test]
608    fn test_async_bridge_clone_bridge() {
609        let bridge = AsyncBridge::new();
610        let bridge_clone = bridge.clone();
611        let sender = bridge.sender();
612
613        // Send via original bridge's sender
614        sender
615            .send(AsyncMessage::LspInitialized {
616                language: "rust".to_string(),
617                server_name: "test".to_string(),
618                capabilities: Default::default(),
619            })
620            .unwrap();
621
622        // Receive via cloned bridge
623        let messages = bridge_clone.try_recv_all();
624        assert_eq!(messages.len(), 1);
625    }
626
627    #[test]
628    fn test_async_bridge_multiple_calls_to_try_recv_all() {
629        let bridge = AsyncBridge::new();
630        let sender = bridge.sender();
631
632        sender
633            .send(AsyncMessage::LspInitialized {
634                language: "rust".to_string(),
635                server_name: "test".to_string(),
636                capabilities: Default::default(),
637            })
638            .unwrap();
639
640        // First call gets the message
641        let messages1 = bridge.try_recv_all();
642        assert_eq!(messages1.len(), 1);
643
644        // Second call gets nothing
645        let messages2 = bridge.try_recv_all();
646        assert_eq!(messages2.len(), 0);
647    }
648
649    #[test]
650    fn test_async_bridge_ordering() {
651        let bridge = AsyncBridge::new();
652        let sender = bridge.sender();
653
654        // Send messages in order
655        sender
656            .send(AsyncMessage::LspInitialized {
657                language: "rust".to_string(),
658                server_name: "test".to_string(),
659                capabilities: Default::default(),
660            })
661            .unwrap();
662        sender
663            .send(AsyncMessage::LspInitialized {
664                language: "typescript".to_string(),
665                server_name: "test".to_string(),
666                capabilities: Default::default(),
667            })
668            .unwrap();
669        sender
670            .send(AsyncMessage::LspInitialized {
671                language: "python".to_string(),
672                server_name: "test".to_string(),
673                capabilities: Default::default(),
674            })
675            .unwrap();
676
677        // Messages should be received in same order
678        let messages = bridge.try_recv_all();
679        assert_eq!(messages.len(), 3);
680
681        match (&messages[0], &messages[1], &messages[2]) {
682            (
683                AsyncMessage::LspInitialized { language: l1, .. },
684                AsyncMessage::LspInitialized { language: l2, .. },
685                AsyncMessage::LspInitialized { language: l3, .. },
686            ) => {
687                assert_eq!(l1, "rust");
688                assert_eq!(l2, "typescript");
689                assert_eq!(l3, "python");
690            }
691            _ => panic!("Expected ordered LspInitialized messages"),
692        }
693    }
694}