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