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}