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