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