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