use anyhow::Result;
use log::{debug, error, info, warn};
use serde_json::{json, Value};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use tokio::{
io::{
AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter,
},
sync::{oneshot, watch, Mutex},
task::JoinHandle,
};
use crate::{protocol::lsp::LSPResponse, uri};
pub type Diagnostics = Arc<Mutex<HashMap<String, Vec<Value>>>>;
pub type Answer = std::result::Result<Value, String>;
pub type Pending = Arc<Mutex<HashMap<u64, oneshot::Sender<Answer>>>>;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Flycheck {
running: HashSet<String>,
started: u64,
}
impl Flycheck {
pub fn begin(&mut self, token: &str) {
self.running.insert(token.to_string());
self.started += 1;
}
pub fn end(&mut self, token: &str) {
self.running.remove(token);
}
pub fn caught_up_with(&self, earlier: &Self) -> bool {
self.started > earlier.started && self.running.is_empty()
}
pub fn started_since(&self, earlier: &Self) -> bool {
self.started > earlier.started
}
pub fn never_ran_one(&self) -> bool {
self.started == 0
}
}
const FLYCHECK_TOKEN: &str = "rust-analyzer/flycheck/";
pub type Outgoing<W> = Arc<Mutex<BufWriter<W>>>;
pub async fn send_message<W: AsyncWrite + Unpin>(
outgoing: &Outgoing<W>,
message: &Value,
) -> Result<()> {
let content = serde_json::to_string(message)?;
let framed = format!("Content-Length: {}\r\n\r\n{}", content.len(), content);
let mut writer = outgoing.lock().await;
writer.write_all(framed.as_bytes()).await?;
writer.flush().await?;
Ok(())
}
pub struct Connection<W> {
pub pending_requests: Pending,
pub diagnostics: Diagnostics,
pub quiescent: watch::Sender<bool>,
pub flycheck: watch::Sender<Flycheck>,
pub outgoing: Outgoing<W>,
pub settings: Value,
}
pub fn start_handlers<W: AsyncWrite + Unpin + Send + 'static>(
stdout: impl AsyncRead + Unpin + Send + 'static,
stderr: impl AsyncRead + Unpin + Send + 'static,
connection: Connection<W>,
) -> JoinHandle<()> {
tokio::spawn(handle_stderr(stderr));
tokio::spawn(handle_stdout(stdout, connection))
}
async fn handle_stderr(stderr: impl AsyncRead + Unpin + Send + 'static) {
let mut reader = BufReader::new(stderr);
let mut buffer = String::new();
loop {
buffer.clear();
let bytes_read = match reader.read_line(&mut buffer).await {
Ok(n) => n,
Err(e) => {
error!("Error reading rust-analyzer stderr: {}", e);
break;
}
};
if bytes_read == 0 {
break; }
let trimmed = buffer.trim();
if !trimmed.is_empty() {
info!("rust-analyzer stderr: {}", trimmed);
}
}
}
async fn handle_stdout<W: AsyncWrite + Unpin>(
stdout: impl AsyncRead + Unpin + Send + 'static,
connection: Connection<W>,
) {
let mut reader = BufReader::new(stdout);
let mut buffer = String::new();
loop {
buffer.clear();
let Ok(bytes_read) = reader.read_line(&mut buffer).await else {
error!("Error reading from rust-analyzer stdout");
break;
};
if bytes_read == 0 {
break; }
if buffer.trim().is_empty() {
continue;
}
if !buffer.starts_with("Content-Length: ") {
continue;
}
let Some(length) = parse_content_length(&buffer) else {
continue;
};
buffer.clear();
let _ = reader.read_line(&mut buffer).await;
let mut json_buffer = vec![0u8; length];
let Ok(_) = reader.read_exact(&mut json_buffer).await else {
continue;
};
let response_str = String::from_utf8_lossy(&json_buffer);
debug!("Received LSP message: {}", response_str);
handle_lsp_message(&json_buffer, &connection).await;
}
connection.pending_requests.lock().await.clear();
}
fn parse_content_length(header: &str) -> Option<usize> {
header
.strip_prefix("Content-Length: ")
.and_then(|s| s.trim().parse().ok())
}
async fn handle_lsp_message<W: AsyncWrite + Unpin>(json_buffer: &[u8], connection: &Connection<W>) {
let Ok(json_value) = serde_json::from_slice::<Value>(json_buffer) else {
error!(
"Failed to parse LSP message: {}",
String::from_utf8_lossy(json_buffer)
);
return;
};
match (json_value.get("method"), json_value.get("id")) {
(Some(method), Some(id)) => {
let method = method.as_str().unwrap_or_default().to_string();
answer_request(&method, id.clone(), &json_value, connection).await;
}
(Some(_), None) => handle_notification(json_value, connection).await,
(None, Some(_)) => handle_response(json_value, &connection.pending_requests).await,
(None, None) => debug!("Ignoring LSP message that is neither request nor response"),
}
}
async fn handle_response(json_value: Value, pending: &Pending) {
let Ok(response) = serde_json::from_value::<LSPResponse>(json_value) else {
return;
};
let Some(id) = response.id else {
return;
};
let mut pending_lock = pending.lock().await;
let Some(sender) = pending_lock.remove(&id) else {
return;
};
if let Some(error) = response.error {
error!("LSP error for request {}: {}", id, error);
let _ = sender.send(Err(message_of(&error)));
} else {
let result = response.result.unwrap_or(json!(null));
info!("Sending result for request {}: {:?}", id, result);
let _ = sender.send(Ok(result));
}
}
fn message_of(error: &Value) -> String {
error
.get("message")
.and_then(|message| message.as_str())
.map_or_else(|| error.to_string(), str::to_string)
}
async fn answer_request<W: AsyncWrite + Unpin>(
method: &str,
id: Value,
request: &Value,
connection: &Connection<W>,
) {
debug!("Received request from rust-analyzer: {}", method);
let response = match method {
"workspace/configuration" => {
let sections = request
.pointer("/params/items")
.and_then(Value::as_array)
.map_or(1, Vec::len);
json!({
"jsonrpc": "2.0",
"id": id,
"result": vec![connection.settings.clone(); sections],
})
}
"window/workDoneProgress/create" => json!({
"jsonrpc": "2.0",
"id": id,
"result": null,
}),
_ => {
info!(
"Declining unsupported request from rust-analyzer: {}",
method
);
json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32601,
"message": format!("Method not found: {method}"),
},
})
}
};
if let Err(e) = send_message(&connection.outgoing, &response).await {
error!("Failed to answer rust-analyzer's {} request: {}", method, e);
}
}
async fn handle_notification<W: AsyncWrite + Unpin>(json_value: Value, connection: &Connection<W>) {
let Some(method) = json_value.get("method").and_then(|m| m.as_str()) else {
return;
};
debug!("Received notification: {}", method);
let Some(params) = json_value.get("params") else {
return;
};
match method {
"textDocument/publishDiagnostics" => {
let Some(uri) = params.get("uri").and_then(|u| u.as_str()) else {
return;
};
let Some(diags) = params.get("diagnostics").and_then(|d| d.as_array()) else {
return;
};
let mut diag_lock = connection.diagnostics.lock().await;
diag_lock.insert(uri::normalize(uri), diags.clone());
info!("Stored {} diagnostics for {}", diags.len(), uri);
}
"experimental/serverStatus" => {
let Some(is_quiescent) = params.get("quiescent").and_then(|q| q.as_bool()) else {
return;
};
info!("rust-analyzer reports quiescent: {}", is_quiescent);
if let Some(message) = params.get("message").and_then(|m| m.as_str()) {
warn!("rust-analyzer status: {}", message);
}
connection.quiescent.send_replace(is_quiescent);
}
"$/progress" => {
let Some(token) = params.get("token").and_then(|t| t.as_str()) else {
return;
};
if !token.starts_with(FLYCHECK_TOKEN) {
return;
}
match params.pointer("/value/kind").and_then(|k| k.as_str()) {
Some("begin") => {
info!("cargo check started: {}", token);
connection
.flycheck
.send_modify(|flycheck| flycheck.begin(token));
}
Some("end") => {
info!("cargo check finished: {}", token);
connection
.flycheck
.send_modify(|flycheck| flycheck.end(token));
}
_ => {}
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tokio::io::DuplexStream;
#[tokio::test]
async fn server_status_tracks_quiescence() {
let (connection, _rust_analyzer) = connection();
let status = connection.quiescent.subscribe();
notify(
"experimental/serverStatus",
json!({ "health": "ok", "quiescent": true }),
&connection,
)
.await;
assert!(*status.borrow());
notify(
"experimental/serverStatus",
json!({ "health": "warning", "quiescent": false, "message": "Loading" }),
&connection,
)
.await;
assert!(!*status.borrow());
}
#[tokio::test]
async fn server_status_without_quiescent_flag_is_ignored() {
let (connection, _rust_analyzer) = connection();
connection.quiescent.send_replace(true);
let status = connection.quiescent.subscribe();
notify(
"experimental/serverStatus",
json!({ "health": "ok" }),
&connection,
)
.await;
assert!(*status.borrow());
}
#[tokio::test]
async fn publish_diagnostics_are_stored() {
let (connection, _rust_analyzer) = connection();
notify(
"textDocument/publishDiagnostics",
json!({ "uri": "file:///a.rs", "diagnostics": [{ "message": "boom" }] }),
&connection,
)
.await;
assert_eq!(connection.diagnostics.lock().await["file:///a.rs"].len(), 1);
}
#[tokio::test]
async fn closed_stdout_fails_pending_requests() {
let (connection, _rust_analyzer) = connection();
let (sender, response) = oneshot::channel();
let pending = Arc::clone(&connection.pending_requests);
pending.lock().await.insert(1, sender);
handle_stdout(tokio::io::empty(), connection).await;
assert!(response.await.is_err());
assert!(pending.lock().await.is_empty());
}
#[tokio::test]
async fn cargo_checks_are_followed_from_start_to_finish() {
let (connection, _rust_analyzer) = connection();
let idle = connection.flycheck.borrow().clone();
progress("rust-analyzer/flycheck/0", "begin", &connection).await;
assert!(connection.flycheck.borrow().started_since(&idle));
assert!(!connection.flycheck.borrow().caught_up_with(&idle));
progress("rust-analyzer/flycheck/1", "begin", &connection).await;
progress("rust-analyzer/flycheck/0", "end", &connection).await;
assert!(!connection.flycheck.borrow().caught_up_with(&idle));
progress("rust-analyzer/flycheck/1", "end", &connection).await;
assert!(connection.flycheck.borrow().caught_up_with(&idle));
}
#[tokio::test]
async fn a_check_cancelled_for_a_restart_is_not_a_check_that_ran() {
let (connection, _rust_analyzer) = connection();
progress("rust-analyzer/flycheck/0", "begin", &connection).await;
let when_we_asked = connection.flycheck.borrow().clone();
progress("rust-analyzer/flycheck/0", "end", &connection).await;
assert!(!connection.flycheck.borrow().caught_up_with(&when_we_asked));
progress("rust-analyzer/flycheck/0", "begin", &connection).await;
assert!(!connection.flycheck.borrow().caught_up_with(&when_we_asked));
progress("rust-analyzer/flycheck/0", "end", &connection).await;
assert!(connection.flycheck.borrow().caught_up_with(&when_we_asked));
}
#[tokio::test]
async fn progress_on_anything_else_is_not_a_cargo_check() {
let (connection, _rust_analyzer) = connection();
for token in [
"rustAnalyzer/cachePriming",
"rustAnalyzer/Fetching",
"rustAnalyzer/Indexing",
] {
progress(token, "begin", &connection).await;
progress(token, "end", &connection).await;
}
assert_eq!(*connection.flycheck.borrow(), Flycheck::default());
}
#[tokio::test]
async fn a_request_from_rust_analyzer_is_not_taken_for_an_answer() {
let (connection, mut rust_analyzer) = connection();
let (sender, mut response) = oneshot::channel();
connection.pending_requests.lock().await.insert(3, sender);
deliver(
json!({
"jsonrpc": "2.0",
"id": 3,
"method": "window/workDoneProgress/create",
"params": { "token": "rust-analyzer/flycheck/0" }
}),
&connection,
)
.await;
assert!(connection.pending_requests.lock().await.contains_key(&3));
assert!(response.try_recv().is_err(), "nothing may have been sent");
let answer = framed(&mut rust_analyzer).await;
assert_eq!(answer["id"], 3);
assert_eq!(answer["result"], Value::Null);
}
#[tokio::test]
async fn configuration_requests_are_answered_with_the_settings_we_asked_for() {
let (mut connection, mut rust_analyzer) = connection();
let settings = json!({ "checkOnSave": { "enable": true } });
connection.settings = settings.clone();
deliver(
json!({
"jsonrpc": "2.0",
"id": 0,
"method": "workspace/configuration",
"params": { "items": [
{ "section": "rust-analyzer" },
{ "section": "rust-analyzer", "scopeUri": "file:///ws" }
] }
}),
&connection,
)
.await;
let answer = framed(&mut rust_analyzer).await;
assert_eq!(answer["id"], 0);
assert_eq!(answer["result"], json!([settings, settings]));
}
#[tokio::test]
async fn requests_we_cannot_serve_are_declined_rather_than_dropped() {
let (connection, mut rust_analyzer) = connection();
deliver(
json!({ "jsonrpc": "2.0", "id": 7, "method": "client/registerCapability" }),
&connection,
)
.await;
let answer = framed(&mut rust_analyzer).await;
assert_eq!(answer["id"], 7);
assert_eq!(answer["error"]["code"], -32601);
}
#[tokio::test]
async fn what_rust_analyzer_refused_to_do_reaches_whoever_asked() {
let (connection, _rust_analyzer) = connection();
let (sender, response) = oneshot::channel();
connection.pending_requests.lock().await.insert(1, sender);
deliver(
json!({
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32602, "message": "Invalid name `1`: not an identifier" }
}),
&connection,
)
.await;
assert_eq!(
response.await.unwrap(),
Err("Invalid name `1`: not an identifier".to_string())
);
}
#[tokio::test]
async fn answers_still_reach_whoever_is_waiting() {
let (connection, _rust_analyzer) = connection();
let (sender, response) = oneshot::channel();
connection.pending_requests.lock().await.insert(1, sender);
deliver(
json!({ "jsonrpc": "2.0", "id": 1, "result": { "contents": "docs" } }),
&connection,
)
.await;
assert_eq!(
response.await.unwrap().unwrap(),
json!({ "contents": "docs" })
);
assert!(connection.pending_requests.lock().await.is_empty());
}
fn connection() -> (Connection<DuplexStream>, DuplexStream) {
let (ours, theirs) = tokio::io::duplex(4096);
let connection = Connection {
pending_requests: Arc::new(Mutex::new(HashMap::new())),
diagnostics: Arc::new(Mutex::new(HashMap::new())),
quiescent: watch::channel(false).0,
flycheck: watch::channel(Flycheck::default()).0,
outgoing: Arc::new(Mutex::new(BufWriter::new(ours))),
settings: json!({}),
};
(connection, theirs)
}
async fn deliver(message: Value, connection: &Connection<DuplexStream>) {
handle_lsp_message(message.to_string().as_bytes(), connection).await;
}
async fn notify(method: &str, params: Value, connection: &Connection<DuplexStream>) {
deliver(
json!({ "jsonrpc": "2.0", "method": method, "params": params }),
connection,
)
.await;
}
async fn progress(token: &str, kind: &str, connection: &Connection<DuplexStream>) {
notify(
"$/progress",
json!({ "token": token, "value": { "kind": kind } }),
connection,
)
.await;
}
async fn framed(rust_analyzer: &mut DuplexStream) -> Value {
let mut buffer = vec![0u8; 4096];
let read = tokio::time::timeout(
std::time::Duration::from_secs(5),
rust_analyzer.read(&mut buffer),
)
.await
.expect("a message must be written")
.unwrap();
let message = String::from_utf8_lossy(&buffer[..read]).to_string();
let (header, body) = message.split_once("\r\n\r\n").expect("{message}");
assert!(header.starts_with("Content-Length: "), "{header}");
serde_json::from_str(body).unwrap()
}
}