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::view::file_tree::{FileTreeView, NodeId};
14use lsp_types::{
15    CodeActionOrCommand, CompletionItem, Diagnostic, FoldingRange, InlayHint, Location,
16    SemanticTokensFullDeltaResult, SemanticTokensRangeResult, SemanticTokensResult, SignatureHelp,
17};
18use serde_json::Value;
19use std::sync::mpsc;
20
21/// Semantic token responses grouped by request type.
22#[derive(Debug)]
23pub enum LspSemanticTokensResponse {
24    Full(Result<Option<SemanticTokensResult>, String>),
25    FullDelta(Result<Option<SemanticTokensFullDeltaResult>, String>),
26    Range(Result<Option<SemanticTokensRangeResult>, String>),
27}
28
29/// How a completed remote attach is installed.
30pub enum RemoteAttachMode {
31    /// Global: replace the editor's single authority and restart the whole
32    /// editor around the remote backend (the original `setAuthority`-style
33    /// destructive transition). Every window becomes remote.
34    Restart,
35    /// Born-attached: spawn a *new window* whose authority is the remote
36    /// backend, leaving existing (local / other-remote) windows untouched.
37    /// The session coexists warm beside them; switching windows retargets the
38    /// active authority (see `set_active_window` / Gap A). `command` is the
39    /// optional agent argv for the window's seed terminal.
40    Window {
41        label: String,
42        command: Option<Vec<String>>,
43    },
44}
45
46/// A completed remote-agent attach: the assembled authority plus the
47/// keepalive that must outlive it. Carried back from the async connect
48/// task to the main loop, which installs it per `mode`. Manual `Debug`
49/// because the fields are not `Debug`.
50pub struct RemoteAttachReady {
51    pub authority: crate::services::authority::Authority,
52    pub keepalive: Box<dyn std::any::Any + Send>,
53    /// Pod-side root to re-open the editor at (the remote workspace, e.g.
54    /// `/workspace`). Without this the editor keeps the *local* working
55    /// directory after attach, so the explorer / quick-open / open-file all
56    /// look at a host path that doesn't exist in the pod. `None` falls back to
57    /// the remote home directory.
58    pub working_dir: Option<std::path::PathBuf>,
59    /// Restart (global) vs. born-attached new window.
60    pub mode: RemoteAttachMode,
61    /// JS callback id of the `attachRemoteAgent` promise to settle once the
62    /// session (authority + window) is fully constructed. The main loop
63    /// resolves it on success and rejects it if window creation fails, so the
64    /// plugin's dialog only closes when there is a real session to show.
65    pub request_id: u64,
66}
67
68impl std::fmt::Debug for RemoteAttachReady {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.debug_struct("RemoteAttachReady")
71            .field("label", &self.authority.display_label)
72            .finish_non_exhaustive()
73    }
74}
75
76/// Messages sent from async tasks to the synchronous main loop
77#[derive(Debug)]
78pub enum AsyncMessage {
79    /// An async `attachRemoteAgent` connect succeeded — install the
80    /// authority + keepalive and restart.
81    RemoteAttachReady(RemoteAttachReady),
82
83    /// An async `attachRemoteAgent` connect failed — reject the plugin's
84    /// promise with `error` (the plugin shows it and creates no window); the
85    /// editor stays on its current authority.
86    RemoteAttachFailed { error: String, request_id: u64 },
87
88    /// LSP diagnostics received for a file
89    LspDiagnostics {
90        uri: String,
91        diagnostics: Vec<Diagnostic>,
92        /// Name of the server that sent these diagnostics (for per-server tracking)
93        server_name: String,
94    },
95
96    /// LSP server initialized successfully
97    LspInitialized {
98        language: String,
99        /// Name of the specific server (for per-server capability tracking)
100        server_name: String,
101        /// Capabilities reported by this server
102        capabilities: crate::services::lsp::manager::ServerCapabilitySummary,
103    },
104
105    /// LSP server crashed or failed
106    LspError {
107        language: String,
108        error: String,
109        /// Path to the stderr log file for this LSP session
110        stderr_log_path: Option<std::path::PathBuf>,
111    },
112
113    /// LSP completion response
114    LspCompletion {
115        request_id: u64,
116        items: Vec<CompletionItem>,
117    },
118
119    /// LSP go-to-definition response
120    LspGotoDefinition {
121        request_id: u64,
122        locations: Vec<Location>,
123    },
124
125    /// LSP rename response
126    LspRename {
127        request_id: u64,
128        result: Result<lsp_types::WorkspaceEdit, String>,
129    },
130
131    /// LSP hover response
132    LspHover {
133        request_id: u64,
134        /// Hover contents as a single string (joined if multiple parts)
135        contents: String,
136        /// Whether the content is markdown (true) or plaintext (false)
137        is_markdown: bool,
138        /// Optional range of the symbol that was hovered over (LSP line/character positions)
139        /// Used to highlight the hovered symbol
140        range: Option<((u32, u32), (u32, u32))>,
141    },
142
143    /// LSP find references response
144    LspReferences {
145        request_id: u64,
146        locations: Vec<Location>,
147    },
148
149    /// LSP signature help response
150    LspSignatureHelp {
151        request_id: u64,
152        signature_help: Option<SignatureHelp>,
153    },
154
155    /// LSP code actions response
156    LspCodeActions {
157        request_id: u64,
158        actions: Vec<CodeActionOrCommand>,
159    },
160
161    /// LSP completionItem/resolve response
162    LspCompletionResolved {
163        request_id: u64,
164        item: Result<lsp_types::CompletionItem, String>,
165    },
166
167    /// LSP textDocument/formatting response
168    LspFormatting {
169        request_id: u64,
170        uri: String,
171        edits: Vec<lsp_types::TextEdit>,
172    },
173
174    /// LSP textDocument/prepareRename response
175    LspPrepareRename {
176        request_id: u64,
177        result: Result<serde_json::Value, String>,
178    },
179
180    /// LSP pulled diagnostics response (textDocument/diagnostic)
181    LspPulledDiagnostics {
182        request_id: u64,
183        uri: String,
184        /// New result_id for incremental updates (None if server doesn't support)
185        result_id: Option<String>,
186        /// Diagnostics (empty if unchanged)
187        diagnostics: Vec<Diagnostic>,
188        /// True if diagnostics haven't changed since previous_result_id
189        unchanged: bool,
190    },
191
192    /// LSP inlay hints response (textDocument/inlayHint)
193    LspInlayHints {
194        request_id: u64,
195        uri: String,
196        /// Inlay hints for the requested range
197        hints: Vec<InlayHint>,
198    },
199
200    /// LSP folding ranges response (textDocument/foldingRange)
201    LspFoldingRanges {
202        request_id: u64,
203        uri: String,
204        ranges: Vec<FoldingRange>,
205    },
206
207    /// LSP semantic tokens response (full, full/delta, or range)
208    LspSemanticTokens {
209        request_id: u64,
210        uri: String,
211        response: LspSemanticTokensResponse,
212    },
213
214    /// LSP server status became quiescent (project fully loaded)
215    /// This is a rust-analyzer specific notification (experimental/serverStatus)
216    LspServerQuiescent { language: String },
217
218    /// LSP server requests diagnostic refresh (workspace/diagnostic/refresh)
219    /// Client should re-pull diagnostics for all open documents
220    LspDiagnosticRefresh { language: String },
221
222    /// LSP server requests an inlay-hint refresh (workspace/inlayHint/refresh).
223    /// Client should re-pull inlay hints for all open documents — used when the
224    /// server learns more later (e.g. a change in file A alters inferred types
225    /// in file B, which the user never edited so was never otherwise re-pulled).
226    LspInlayHintRefresh { language: String },
227
228    /// LSP server requests a semantic-tokens refresh
229    /// (workspace/semanticTokens/refresh). Client should re-pull semantic
230    /// tokens for all open documents.
231    LspSemanticTokensRefresh { language: String },
232
233    /// LSP server registered (`client/registerCapability`) or unregistered
234    /// (`client/unregisterCapability`) one or more capabilities dynamically.
235    /// Many servers advertise little or nothing statically in their
236    /// `initialize` result and instead register providers afterwards, so these
237    /// must update the stored `ServerCapabilities` or the features stay gated
238    /// off for the whole session. `register == false` means unregister.
239    /// Each entry is `(method, register_options)`.
240    LspDynamicCapabilities {
241        language: String,
242        server_name: String,
243        register: bool,
244        registrations: Vec<(String, Option<Value>)>,
245    },
246
247    /// File changed externally (future: file watching)
248    FileChanged { path: String },
249
250    /// Git status updated (future: git integration)
251    GitStatusChanged { status: String },
252
253    /// File explorer initialized with tree view. Carries the id of the window
254    /// that requested it: a background preview/materialize can init a
255    /// *non-active* window's explorer, so the view must land on that window —
256    /// applying it to whatever is active would clobber an unrelated explorer.
257    FileExplorerInitialized {
258        window: fresh_core::WindowId,
259        view: FileTreeView,
260    },
261
262    /// File explorer node toggle completed
263    FileExplorerToggleNode(NodeId),
264
265    /// File explorer node refresh completed
266    FileExplorerRefreshNode(NodeId),
267
268    /// File explorer expand to path completed. Carries the requesting window id
269    /// (see `FileExplorerInitialized`) so the expanded view returns to its own
270    /// window rather than the active one.
271    FileExplorerExpandedToPath {
272        window: fresh_core::WindowId,
273        view: FileTreeView,
274    },
275
276    /// Plugin-related async messages
277    Plugin(fresh_core::api::PluginAsyncMessage),
278
279    /// File open dialog: directory listing completed
280    FileOpenDirectoryLoaded(std::io::Result<Vec<crate::services::fs::DirEntry>>),
281
282    /// File open dialog: async shortcuts (Windows drive letters) loaded
283    FileOpenShortcutsLoaded(Vec<crate::app::file_open::NavigationShortcut>),
284
285    /// Terminal output received (triggers redraw). Tagged with the
286    /// owning window: terminal ids are only unique within a window, so a
287    /// bare id can't be attributed to a session without guessing.
288    TerminalOutput {
289        terminal: fresh_core::WindowTerminalId,
290    },
291
292    /// Result of an asynchronous system-clipboard read. The main loop
293    /// blocks input dispatch while a paste is in flight; the matching
294    /// `request_id` ensures a late result that arrived after the
295    /// timeout fallback fired is discarded as stale. `text` is `None`
296    /// when the read errored, returned empty, or was cancelled by the
297    /// deadline.
298    ClipboardPasteResult {
299        request_id: u64,
300        text: Option<String>,
301    },
302
303    /// File watcher delivered an event for a path under a
304    /// `WatchPath`-registered watcher. Routed to the
305    /// `path_changed` plugin hook by the main loop.
306    PathChanged {
307        /// Watch handle the event came from (matches the value
308        /// returned by `WatchPath`).
309        handle: u64,
310        path: std::path::PathBuf,
311        /// Conservative bucketing of `notify::EventKind`.
312        kind: PathChangeKind,
313    },
314
315    /// Terminal process exited.
316    ///
317    /// `exit_code` is `None` when the editor cannot determine a status
318    /// (the wait happens in a separate thread, signal exits, kill
319    /// before wait, etc.). Populated end-to-end is a follow-up; the
320    /// initial wiring sends `None` so plugin handlers see the variant
321    /// shape that matches `HookArgs::TerminalExited`.
322    TerminalExited {
323        terminal: fresh_core::WindowTerminalId,
324        exit_code: Option<i32>,
325    },
326
327    /// LSP progress notification ($/progress)
328    LspProgress {
329        language: String,
330        token: String,
331        value: LspProgressValue,
332    },
333
334    /// LSP window message (window/showMessage)
335    LspWindowMessage {
336        language: String,
337        message_type: LspMessageType,
338        message: String,
339    },
340
341    /// LSP log message (window/logMessage)
342    LspLogMessage {
343        language: String,
344        message_type: LspMessageType,
345        message: String,
346    },
347
348    /// LSP workspace/applyEdit (server -> client request)
349    /// Server asks client to apply a workspace edit (during executeCommand, etc.)
350    LspApplyEdit {
351        edit: lsp_types::WorkspaceEdit,
352        label: Option<String>,
353    },
354
355    /// LSP codeAction/resolve response
356    LspCodeActionResolved {
357        request_id: u64,
358        action: Result<lsp_types::CodeAction, String>,
359    },
360
361    /// LSP server request (server -> client)
362    /// Used for custom/extension methods that plugins can handle
363    LspServerRequest {
364        language: String,
365        server_command: String,
366        method: String,
367        params: Option<Value>,
368    },
369
370    /// Response for a plugin-initiated LSP request
371    PluginLspResponse {
372        language: String,
373        request_id: u64,
374        result: Result<Value, String>,
375    },
376
377    /// Plugin process completed with output
378    PluginProcessOutput {
379        /// Unique ID for this process (to match with callback)
380        process_id: u64,
381        /// Standard output
382        stdout: String,
383        /// Standard error
384        stderr: String,
385        /// Exit code
386        exit_code: i32,
387    },
388
389    /// LSP server status update (progress, messages, etc.)
390    LspStatusUpdate {
391        language: String,
392        /// Name of the specific server (for multi-server status tracking)
393        server_name: String,
394        status: LspServerStatus,
395        message: Option<String>,
396    },
397
398    /// Background grammar build completed — swap in the new registry.
399    /// `callback_ids` contains plugin callbacks to resolve (empty for the
400    /// initial startup build).
401    GrammarRegistryBuilt {
402        registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
403        callback_ids: Vec<fresh_core::api::JsCallbackId>,
404    },
405
406    /// Quick Open file list loaded by a background task.
407    /// `complete` is `true` when the scan is finished, `false` for incremental
408    /// partial updates sent while the walk is still in progress.
409    QuickOpenFilesLoaded {
410        /// The working directory the files were enumerated under. Lets
411        /// the editor drop results that arrive after the user has
412        /// switched windows/projects (the cache is keyed by cwd).
413        cwd: String,
414        files: std::sync::Arc<Vec<crate::input::quick_open::providers::FileEntry>>,
415        complete: bool,
416    },
417
418    /// Startup-async: a single plugin directory finished loading on the
419    /// plugin thread. Carries the same payload as the blocking
420    /// `load_plugins_from_dir_with_config` return value.
421    PluginsDirLoaded {
422        dir: std::path::PathBuf,
423        errors: Vec<String>,
424        discovered_plugins: std::collections::HashMap<String, fresh_core::config::PluginConfig>,
425    },
426
427    /// Startup-async: every directory in the startup batch has loaded and
428    /// the resulting `.d.ts` declarations have been collected from the
429    /// plugin runtime. Triggers `init_script::write_plugin_declarations`.
430    PluginDeclarationsReady { declarations: Vec<(String, String)> },
431
432    /// Startup-async: `init.ts` (auto-loaded source plugin) finished
433    /// running its top level and has either succeeded, failed, or was
434    /// skipped/fused. The handler logs and applies the corresponding
435    /// status message, and (on `Loaded`) clears the crash fuse.
436    PluginInitScriptLoaded(PluginInitScriptOutcome),
437}
438
439/// Async equivalent of `init_script::InitOutcome`. Wraps the same set
440/// of states but is plain data so it can travel across the bridge.
441#[derive(Debug, Clone)]
442pub enum PluginInitScriptOutcome {
443    NotFound,
444    Disabled,
445    CrashFused { failures: u32 },
446    Loaded,
447    Failed { message: String },
448}
449
450/// Conservative bucketing of `notify::EventKind`. We don't expose
451/// the full notify enum to plugins because the kind set varies by
452/// platform and changes between notify releases. Plugins switch on
453/// these strings; refining requires a new variant + a new string
454/// (additive, no breakage).
455#[derive(Debug, Clone, Copy)]
456pub enum PathChangeKind {
457    Modify,
458    Create,
459    Delete,
460    Rename,
461    Other,
462}
463
464impl PathChangeKind {
465    pub fn as_str(&self) -> &'static str {
466        match self {
467            PathChangeKind::Modify => "modify",
468            PathChangeKind::Create => "create",
469            PathChangeKind::Delete => "delete",
470            PathChangeKind::Rename => "rename",
471            PathChangeKind::Other => "other",
472        }
473    }
474}
475
476/// LSP progress value types
477#[derive(Debug, Clone)]
478pub enum LspProgressValue {
479    Begin {
480        title: String,
481        message: Option<String>,
482        percentage: Option<u32>,
483    },
484    Report {
485        message: Option<String>,
486        percentage: Option<u32>,
487    },
488    End {
489        message: Option<String>,
490    },
491}
492
493/// LSP message type (corresponds to MessageType in LSP spec)
494#[derive(Debug, Clone, Copy, PartialEq, Eq)]
495pub enum LspMessageType {
496    Error = 1,
497    Warning = 2,
498    Info = 3,
499    Log = 4,
500}
501
502/// LSP server status
503#[derive(Debug, Clone, Copy, PartialEq, Eq)]
504pub enum LspServerStatus {
505    Starting,
506    Initializing,
507    Running,
508    Error,
509    Shutdown,
510}
511
512/// Bridge between async Tokio runtime and sync main loop
513///
514/// Design:
515/// - Lightweight, cloneable sender that can be passed to async tasks
516/// - Non-blocking receiver checked each frame in main loop
517/// - No locks needed in main loop (channel handles synchronization)
518#[derive(Clone)]
519pub struct AsyncBridge {
520    sender: mpsc::Sender<AsyncMessage>,
521    // Receiver wrapped in Arc<Mutex<>> to allow cloning
522    receiver: std::sync::Arc<std::sync::Mutex<mpsc::Receiver<AsyncMessage>>>,
523}
524
525impl AsyncBridge {
526    /// Create a new async bridge with an unbounded channel
527    ///
528    /// Unbounded is appropriate here because:
529    /// 1. Main loop processes messages every 16ms (60fps)
530    /// 2. LSP messages are infrequent (< 100/sec typically)
531    /// 3. Memory usage is bounded by message rate × frame time
532    pub fn new() -> Self {
533        let (sender, receiver) = mpsc::channel();
534        Self {
535            sender,
536            receiver: std::sync::Arc::new(std::sync::Mutex::new(receiver)),
537        }
538    }
539
540    /// Get a cloneable sender for async tasks
541    ///
542    /// This sender can be:
543    /// - Cloned freely (cheap Arc internally)
544    /// - Sent to async tasks
545    /// - Stored in LspClient instances
546    pub fn sender(&self) -> mpsc::Sender<AsyncMessage> {
547        self.sender.clone()
548    }
549
550    /// Try to receive pending messages (non-blocking)
551    ///
552    /// Called each frame in the main loop to process async messages.
553    /// Returns all pending messages without blocking.
554    pub fn try_recv_all(&self) -> Vec<AsyncMessage> {
555        let mut messages = Vec::new();
556
557        // Lock the receiver and drain all pending messages
558        if let Ok(receiver) = self.receiver.lock() {
559            while let Ok(msg) = receiver.try_recv() {
560                messages.push(msg);
561            }
562        }
563
564        messages
565    }
566
567    /// Check if there are pending messages (non-blocking)
568    pub fn has_messages(&self) -> bool {
569        // Note: This is racy but safe - only used for optimization
570        if let Ok(receiver) = self.receiver.lock() {
571            receiver.try_recv().is_ok()
572        } else {
573            false
574        }
575    }
576}
577
578impl Default for AsyncBridge {
579    fn default() -> Self {
580        Self::new()
581    }
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587
588    #[test]
589    fn test_async_bridge_send_receive() {
590        let bridge = AsyncBridge::new();
591        let sender = bridge.sender();
592
593        // Send a message
594        sender
595            .send(AsyncMessage::LspInitialized {
596                language: "rust".to_string(),
597                server_name: "test".to_string(),
598                capabilities: Default::default(),
599            })
600            .unwrap();
601
602        // Receive it
603        let messages = bridge.try_recv_all();
604        assert_eq!(messages.len(), 1);
605
606        match &messages[0] {
607            AsyncMessage::LspInitialized {
608                language,
609                server_name,
610                ..
611            } => {
612                assert_eq!(language, "rust");
613                assert_eq!(server_name, "test");
614            }
615            _ => panic!("Wrong message type"),
616        }
617    }
618
619    #[test]
620    fn test_async_bridge_multiple_messages() {
621        let bridge = AsyncBridge::new();
622        let sender = bridge.sender();
623
624        // Send multiple messages
625        sender
626            .send(AsyncMessage::LspInitialized {
627                language: "rust".to_string(),
628                server_name: "test".to_string(),
629                capabilities: Default::default(),
630            })
631            .unwrap();
632        sender
633            .send(AsyncMessage::LspInitialized {
634                language: "typescript".to_string(),
635                server_name: "test".to_string(),
636                capabilities: Default::default(),
637            })
638            .unwrap();
639
640        // Receive all at once
641        let messages = bridge.try_recv_all();
642        assert_eq!(messages.len(), 2);
643    }
644
645    #[test]
646    fn test_async_bridge_no_messages() {
647        let bridge = AsyncBridge::new();
648
649        // Try to receive with no messages
650        let messages = bridge.try_recv_all();
651        assert_eq!(messages.len(), 0);
652    }
653
654    #[test]
655    fn test_async_bridge_clone_sender() {
656        let bridge = AsyncBridge::new();
657        let sender1 = bridge.sender();
658        let sender2 = sender1.clone();
659
660        // Both senders work
661        sender1
662            .send(AsyncMessage::LspInitialized {
663                language: "rust".to_string(),
664                server_name: "test".to_string(),
665                capabilities: Default::default(),
666            })
667            .unwrap();
668        sender2
669            .send(AsyncMessage::LspInitialized {
670                language: "typescript".to_string(),
671                server_name: "test".to_string(),
672                capabilities: Default::default(),
673            })
674            .unwrap();
675
676        let messages = bridge.try_recv_all();
677        assert_eq!(messages.len(), 2);
678    }
679
680    #[test]
681    fn test_async_bridge_diagnostics() {
682        let bridge = AsyncBridge::new();
683        let sender = bridge.sender();
684
685        // Send diagnostic message
686        let diagnostics = vec![lsp_types::Diagnostic {
687            range: lsp_types::Range {
688                start: lsp_types::Position {
689                    line: 0,
690                    character: 0,
691                },
692                end: lsp_types::Position {
693                    line: 0,
694                    character: 5,
695                },
696            },
697            severity: Some(lsp_types::DiagnosticSeverity::ERROR),
698            code: None,
699            code_description: None,
700            source: Some("rust-analyzer".to_string()),
701            message: "test error".to_string(),
702            related_information: None,
703            tags: None,
704            data: None,
705        }];
706
707        sender
708            .send(AsyncMessage::LspDiagnostics {
709                uri: "file:///test.rs".to_string(),
710                diagnostics: diagnostics.clone(),
711                server_name: "rust-analyzer".to_string(),
712            })
713            .unwrap();
714
715        let messages = bridge.try_recv_all();
716        assert_eq!(messages.len(), 1);
717
718        match &messages[0] {
719            AsyncMessage::LspDiagnostics {
720                uri,
721                diagnostics: diags,
722                server_name,
723            } => {
724                assert_eq!(uri, "file:///test.rs");
725                assert_eq!(diags.len(), 1);
726                assert_eq!(diags[0].message, "test error");
727                assert_eq!(server_name, "rust-analyzer");
728            }
729            _ => panic!("Expected LspDiagnostics message"),
730        }
731    }
732
733    #[test]
734    fn test_async_bridge_error_message() {
735        let bridge = AsyncBridge::new();
736        let sender = bridge.sender();
737
738        sender
739            .send(AsyncMessage::LspError {
740                language: "rust".to_string(),
741                error: "Failed to initialize".to_string(),
742                stderr_log_path: None,
743            })
744            .unwrap();
745
746        let messages = bridge.try_recv_all();
747        assert_eq!(messages.len(), 1);
748
749        match &messages[0] {
750            AsyncMessage::LspError {
751                language,
752                error,
753                stderr_log_path,
754            } => {
755                assert_eq!(language, "rust");
756                assert_eq!(error, "Failed to initialize");
757                assert!(stderr_log_path.is_none());
758            }
759            _ => panic!("Expected LspError message"),
760        }
761    }
762
763    #[test]
764    fn test_async_bridge_clone_bridge() {
765        let bridge = AsyncBridge::new();
766        let bridge_clone = bridge.clone();
767        let sender = bridge.sender();
768
769        // Send via original bridge's sender
770        sender
771            .send(AsyncMessage::LspInitialized {
772                language: "rust".to_string(),
773                server_name: "test".to_string(),
774                capabilities: Default::default(),
775            })
776            .unwrap();
777
778        // Receive via cloned bridge
779        let messages = bridge_clone.try_recv_all();
780        assert_eq!(messages.len(), 1);
781    }
782
783    #[test]
784    fn test_async_bridge_multiple_calls_to_try_recv_all() {
785        let bridge = AsyncBridge::new();
786        let sender = bridge.sender();
787
788        sender
789            .send(AsyncMessage::LspInitialized {
790                language: "rust".to_string(),
791                server_name: "test".to_string(),
792                capabilities: Default::default(),
793            })
794            .unwrap();
795
796        // First call gets the message
797        let messages1 = bridge.try_recv_all();
798        assert_eq!(messages1.len(), 1);
799
800        // Second call gets nothing
801        let messages2 = bridge.try_recv_all();
802        assert_eq!(messages2.len(), 0);
803    }
804
805    #[test]
806    fn test_async_bridge_ordering() {
807        let bridge = AsyncBridge::new();
808        let sender = bridge.sender();
809
810        // Send messages in order
811        sender
812            .send(AsyncMessage::LspInitialized {
813                language: "rust".to_string(),
814                server_name: "test".to_string(),
815                capabilities: Default::default(),
816            })
817            .unwrap();
818        sender
819            .send(AsyncMessage::LspInitialized {
820                language: "typescript".to_string(),
821                server_name: "test".to_string(),
822                capabilities: Default::default(),
823            })
824            .unwrap();
825        sender
826            .send(AsyncMessage::LspInitialized {
827                language: "python".to_string(),
828                server_name: "test".to_string(),
829                capabilities: Default::default(),
830            })
831            .unwrap();
832
833        // Messages should be received in same order
834        let messages = bridge.try_recv_all();
835        assert_eq!(messages.len(), 3);
836
837        match (&messages[0], &messages[1], &messages[2]) {
838            (
839                AsyncMessage::LspInitialized { language: l1, .. },
840                AsyncMessage::LspInitialized { language: l2, .. },
841                AsyncMessage::LspInitialized { language: l3, .. },
842            ) => {
843                assert_eq!(l1, "rust");
844                assert_eq!(l2, "typescript");
845                assert_eq!(l3, "python");
846            }
847            _ => panic!("Expected ordered LspInitialized messages"),
848        }
849    }
850}