use crate::services::terminal::TerminalId;
use crate::view::file_tree::{FileTreeView, NodeId};
use lsp_types::{
CodeActionOrCommand, CompletionItem, Diagnostic, InlayHint, Location, SignatureHelp,
};
use serde_json::Value;
use std::sync::mpsc;
#[derive(Debug)]
pub enum AsyncMessage {
LspDiagnostics {
uri: String,
diagnostics: Vec<Diagnostic>,
},
LspInitialized { language: String },
LspError {
language: String,
error: String,
stderr_log_path: Option<std::path::PathBuf>,
},
LspCompletion {
request_id: u64,
items: Vec<CompletionItem>,
},
LspGotoDefinition {
request_id: u64,
locations: Vec<Location>,
},
LspRename {
request_id: u64,
result: Result<lsp_types::WorkspaceEdit, String>,
},
LspHover {
request_id: u64,
contents: String,
is_markdown: bool,
range: Option<((u32, u32), (u32, u32))>,
},
LspReferences {
request_id: u64,
locations: Vec<Location>,
},
LspSignatureHelp {
request_id: u64,
signature_help: Option<SignatureHelp>,
},
LspCodeActions {
request_id: u64,
actions: Vec<CodeActionOrCommand>,
},
LspPulledDiagnostics {
request_id: u64,
uri: String,
result_id: Option<String>,
diagnostics: Vec<Diagnostic>,
unchanged: bool,
},
LspInlayHints {
request_id: u64,
uri: String,
hints: Vec<InlayHint>,
},
LspServerQuiescent { language: String },
FileChanged { path: String },
GitStatusChanged { status: String },
FileExplorerInitialized(FileTreeView),
FileExplorerToggleNode(NodeId),
FileExplorerRefreshNode(NodeId),
FileExplorerExpandedToPath(FileTreeView),
PluginProcessOutput {
process_id: u64,
stdout: String,
stderr: String,
exit_code: i32,
},
LspProgress {
language: String,
token: String,
value: LspProgressValue,
},
LspWindowMessage {
language: String,
message_type: LspMessageType,
message: String,
},
LspLogMessage {
language: String,
message_type: LspMessageType,
message: String,
},
LspStatusUpdate {
language: String,
status: LspServerStatus,
},
CustomNotification {
language: String,
method: String,
params: Option<Value>,
},
LspServerRequest {
language: String,
server_command: String,
method: String,
params: Option<Value>,
},
PluginLspResponse {
language: String,
request_id: u64,
result: Result<Value, String>,
},
FileOpenDirectoryLoaded(std::io::Result<Vec<crate::services::fs::FsEntry>>),
TerminalOutput { terminal_id: TerminalId },
TerminalExited { terminal_id: TerminalId },
}
#[derive(Debug, Clone)]
pub enum LspProgressValue {
Begin {
title: String,
message: Option<String>,
percentage: Option<u32>,
},
Report {
message: Option<String>,
percentage: Option<u32>,
},
End {
message: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LspMessageType {
Error = 1,
Warning = 2,
Info = 3,
Log = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LspServerStatus {
Starting,
Initializing,
Running,
Error,
Shutdown,
}
#[derive(Clone)]
pub struct AsyncBridge {
sender: mpsc::Sender<AsyncMessage>,
receiver: std::sync::Arc<std::sync::Mutex<mpsc::Receiver<AsyncMessage>>>,
}
impl AsyncBridge {
pub fn new() -> Self {
let (sender, receiver) = mpsc::channel();
Self {
sender,
receiver: std::sync::Arc::new(std::sync::Mutex::new(receiver)),
}
}
pub fn sender(&self) -> mpsc::Sender<AsyncMessage> {
self.sender.clone()
}
pub fn try_recv_all(&self) -> Vec<AsyncMessage> {
let mut messages = Vec::new();
if let Ok(receiver) = self.receiver.lock() {
while let Ok(msg) = receiver.try_recv() {
messages.push(msg);
}
}
messages
}
pub fn has_messages(&self) -> bool {
if let Ok(receiver) = self.receiver.lock() {
receiver.try_recv().is_ok()
} else {
false
}
}
}
impl Default for AsyncBridge {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_async_bridge_send_receive() {
let bridge = AsyncBridge::new();
let sender = bridge.sender();
sender
.send(AsyncMessage::LspInitialized {
language: "rust".to_string(),
})
.unwrap();
let messages = bridge.try_recv_all();
assert_eq!(messages.len(), 1);
match &messages[0] {
AsyncMessage::LspInitialized { language } => {
assert_eq!(language, "rust");
}
_ => panic!("Wrong message type"),
}
}
#[test]
fn test_async_bridge_multiple_messages() {
let bridge = AsyncBridge::new();
let sender = bridge.sender();
sender
.send(AsyncMessage::LspInitialized {
language: "rust".to_string(),
})
.unwrap();
sender
.send(AsyncMessage::LspInitialized {
language: "typescript".to_string(),
})
.unwrap();
let messages = bridge.try_recv_all();
assert_eq!(messages.len(), 2);
}
#[test]
fn test_async_bridge_no_messages() {
let bridge = AsyncBridge::new();
let messages = bridge.try_recv_all();
assert_eq!(messages.len(), 0);
}
#[test]
fn test_async_bridge_clone_sender() {
let bridge = AsyncBridge::new();
let sender1 = bridge.sender();
let sender2 = sender1.clone();
sender1
.send(AsyncMessage::LspInitialized {
language: "rust".to_string(),
})
.unwrap();
sender2
.send(AsyncMessage::LspInitialized {
language: "typescript".to_string(),
})
.unwrap();
let messages = bridge.try_recv_all();
assert_eq!(messages.len(), 2);
}
#[test]
fn test_async_bridge_diagnostics() {
let bridge = AsyncBridge::new();
let sender = bridge.sender();
let diagnostics = vec![lsp_types::Diagnostic {
range: lsp_types::Range {
start: lsp_types::Position {
line: 0,
character: 0,
},
end: lsp_types::Position {
line: 0,
character: 5,
},
},
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: Some("rust-analyzer".to_string()),
message: "test error".to_string(),
related_information: None,
tags: None,
data: None,
}];
sender
.send(AsyncMessage::LspDiagnostics {
uri: "file:///test.rs".to_string(),
diagnostics: diagnostics.clone(),
})
.unwrap();
let messages = bridge.try_recv_all();
assert_eq!(messages.len(), 1);
match &messages[0] {
AsyncMessage::LspDiagnostics {
uri,
diagnostics: diags,
} => {
assert_eq!(uri, "file:///test.rs");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "test error");
}
_ => panic!("Expected LspDiagnostics message"),
}
}
#[test]
fn test_async_bridge_error_message() {
let bridge = AsyncBridge::new();
let sender = bridge.sender();
sender
.send(AsyncMessage::LspError {
language: "rust".to_string(),
error: "Failed to initialize".to_string(),
stderr_log_path: None,
})
.unwrap();
let messages = bridge.try_recv_all();
assert_eq!(messages.len(), 1);
match &messages[0] {
AsyncMessage::LspError {
language,
error,
stderr_log_path,
} => {
assert_eq!(language, "rust");
assert_eq!(error, "Failed to initialize");
assert!(stderr_log_path.is_none());
}
_ => panic!("Expected LspError message"),
}
}
#[test]
fn test_async_bridge_clone_bridge() {
let bridge = AsyncBridge::new();
let bridge_clone = bridge.clone();
let sender = bridge.sender();
sender
.send(AsyncMessage::LspInitialized {
language: "rust".to_string(),
})
.unwrap();
let messages = bridge_clone.try_recv_all();
assert_eq!(messages.len(), 1);
}
#[test]
fn test_async_bridge_multiple_calls_to_try_recv_all() {
let bridge = AsyncBridge::new();
let sender = bridge.sender();
sender
.send(AsyncMessage::LspInitialized {
language: "rust".to_string(),
})
.unwrap();
let messages1 = bridge.try_recv_all();
assert_eq!(messages1.len(), 1);
let messages2 = bridge.try_recv_all();
assert_eq!(messages2.len(), 0);
}
#[test]
fn test_async_bridge_ordering() {
let bridge = AsyncBridge::new();
let sender = bridge.sender();
sender
.send(AsyncMessage::LspInitialized {
language: "rust".to_string(),
})
.unwrap();
sender
.send(AsyncMessage::LspInitialized {
language: "typescript".to_string(),
})
.unwrap();
sender
.send(AsyncMessage::LspInitialized {
language: "python".to_string(),
})
.unwrap();
let messages = bridge.try_recv_all();
assert_eq!(messages.len(), 3);
match (&messages[0], &messages[1], &messages[2]) {
(
AsyncMessage::LspInitialized { language: l1 },
AsyncMessage::LspInitialized { language: l2 },
AsyncMessage::LspInitialized { language: l3 },
) => {
assert_eq!(l1, "rust");
assert_eq!(l2, "typescript");
assert_eq!(l3, "python");
}
_ => panic!("Expected ordered LspInitialized messages"),
}
}
}