#![cfg(feature = "lsp")]
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::timeout;
use tower_lsp::{LspService, Server};
use splitrs::lsp::Backend;
fn lsp_frame(msg: &serde_json::Value) -> Vec<u8> {
let body = serde_json::to_vec(msg).expect("json serialization should not fail");
let header = format!("Content-Length: {}\r\n\r\n", body.len());
let mut out = header.into_bytes();
out.extend_from_slice(&body);
out
}
async fn read_lsp_frame(reader: &mut (impl AsyncReadExt + Unpin)) -> serde_json::Value {
let mut header_buf: Vec<u8> = Vec::new();
loop {
let b = reader.read_u8().await.expect("read_u8 should succeed");
header_buf.push(b);
if header_buf.ends_with(b"\r\n\r\n") {
break;
}
}
let header_str = String::from_utf8_lossy(&header_buf);
let content_length: usize = header_str
.lines()
.find(|l| l.starts_with("Content-Length:"))
.and_then(|l| l.split(':').nth(1))
.and_then(|v| v.trim().parse().ok())
.expect("Content-Length header should be present and numeric");
let mut body = vec![0u8; content_length];
reader
.read_exact(&mut body)
.await
.expect("read_exact body should succeed");
serde_json::from_slice(&body).expect("body should be valid JSON")
}
async fn read_until<T, F>(
reader: &mut (impl AsyncReadExt + Unpin),
deadline: Duration,
mut predicate: F,
) -> Option<T>
where
F: FnMut(&serde_json::Value) -> Option<T>,
{
let start = tokio::time::Instant::now();
loop {
let remaining = deadline.saturating_sub(start.elapsed());
if remaining.is_zero() {
return None;
}
let frame = match timeout(remaining, read_lsp_frame(reader)).await {
Ok(v) => v,
Err(_) => return None,
};
if let Some(result) = predicate(&frame) {
return Some(result);
}
}
}
fn start_server() -> (
impl AsyncReadExt + Unpin + Send + 'static,
impl AsyncWriteExt + Unpin + Send + 'static,
tokio::task::JoinHandle<()>,
) {
let (client_stream, server_stream) = tokio::io::duplex(4 * 1024 * 1024);
let (client_read, client_write) = tokio::io::split(client_stream);
let (service, socket) = LspService::build(Backend::new).finish();
let handle = tokio::spawn(async move {
let (server_read, server_write) = tokio::io::split(server_stream);
Server::new(server_read, server_write, socket)
.serve(service)
.await;
});
(client_read, client_write, handle)
}
async fn do_handshake(
reader: &mut (impl AsyncReadExt + Unpin),
writer: &mut (impl AsyncWriteExt + Unpin),
) -> serde_json::Value {
let init_req = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"processId": null,
"capabilities": {},
"rootUri": null
}
});
writer
.write_all(&lsp_frame(&init_req))
.await
.expect("write initialize request");
let caps = read_until(reader, Duration::from_secs(10), |frame| {
if frame.get("id") == Some(&serde_json::json!(1)) {
Some(frame["result"]["capabilities"].clone())
} else {
None
}
})
.await
.expect("initialize response should arrive within 10 s");
let init_notif = serde_json::json!({
"jsonrpc": "2.0",
"method": "initialized",
"params": {}
});
writer
.write_all(&lsp_frame(&init_notif))
.await
.expect("write initialized notification");
caps
}
#[tokio::test]
async fn test_initialize_handshake() {
let (mut client_read, mut client_write, server_handle) = start_server();
let caps = do_handshake(&mut client_read, &mut client_write).await;
let hover = &caps["hoverProvider"];
assert!(
hover.as_bool() == Some(true) || hover.is_object(),
"Expected hoverProvider capability, got: {hover}"
);
assert!(
!caps["codeActionProvider"].is_null(),
"Expected codeActionProvider capability"
);
server_handle.abort();
}
#[tokio::test]
async fn test_diagnostic_on_open_oversize_file() {
let (mut client_read, mut client_write, server_handle) = start_server();
do_handshake(&mut client_read, &mut client_write).await;
let mut src = String::from("fn placeholder() {}\n");
for i in 0..1499usize {
src.push_str(&format!("// generated line {i}\n"));
}
let open_notif = serde_json::json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///tmp/oversize_test.rs",
"languageId": "rust",
"version": 1,
"text": src
}
}
});
client_write
.write_all(&lsp_frame(&open_notif))
.await
.expect("write didOpen");
let diags = read_until(&mut client_read, Duration::from_secs(10), |frame| {
if frame.get("method")
== Some(&serde_json::Value::String(
"textDocument/publishDiagnostics".into(),
))
{
Some(frame["params"]["diagnostics"].clone())
} else {
None
}
})
.await
.expect("publishDiagnostics notification should arrive within 10 s");
assert!(
diags.as_array().is_some_and(|a| !a.is_empty()),
"Expected at least one diagnostic for an oversize file, got: {diags}"
);
if let Some(arr) = diags.as_array() {
for d in arr {
assert_eq!(
d["source"].as_str(),
Some("splitrs"),
"Diagnostic source should be 'splitrs'"
);
}
}
server_handle.abort();
}
#[tokio::test]
async fn test_hover_at_line_zero() {
let (mut client_read, mut client_write, server_handle) = start_server();
do_handshake(&mut client_read, &mut client_write).await;
let src = "pub struct Foo {\n pub value: i32,\n}\n\nimpl Foo {\n pub fn get(&self) -> i32 { self.value }\n}\n";
let open_notif = serde_json::json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///tmp/hover_test.rs",
"languageId": "rust",
"version": 1,
"text": src
}
}
});
client_write
.write_all(&lsp_frame(&open_notif))
.await
.expect("write didOpen for hover test");
let _ = read_until::<(), _>(&mut client_read, Duration::from_secs(3), |frame| {
if frame.get("method")
== Some(&serde_json::Value::String(
"textDocument/publishDiagnostics".into(),
))
{
Some(())
} else {
None
}
})
.await;
let hover_req = serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": "file:///tmp/hover_test.rs" },
"position": { "line": 0, "character": 0 }
}
});
client_write
.write_all(&lsp_frame(&hover_req))
.await
.expect("write hover request");
let hover_result = read_until(&mut client_read, Duration::from_secs(10), |frame| {
if frame.get("id") == Some(&serde_json::json!(2)) {
Some(frame["result"].clone())
} else {
None
}
})
.await
.expect("hover response should arrive within 10 s");
let content_value = &hover_result["contents"]["value"];
let markdown = content_value
.as_str()
.expect("hover contents value should be a string");
assert!(
markdown.contains("splitrs"),
"Hover should mention 'splitrs'. Got: {markdown}"
);
assert!(
markdown.contains("Lines") || markdown.contains("lines"),
"Hover should mention line count. Got: {markdown}"
);
server_handle.abort();
}
#[tokio::test]
async fn test_no_diagnostic_for_small_file() {
let (mut client_read, mut client_write, server_handle) = start_server();
do_handshake(&mut client_read, &mut client_write).await;
let src = "fn main() {\n println!(\"hello\");\n}\n";
let open_notif = serde_json::json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///tmp/small_test.rs",
"languageId": "rust",
"version": 1,
"text": src
}
}
});
client_write
.write_all(&lsp_frame(&open_notif))
.await
.expect("write didOpen for small file");
let diags = read_until(&mut client_read, Duration::from_secs(10), |frame| {
if frame.get("method")
== Some(&serde_json::Value::String(
"textDocument/publishDiagnostics".into(),
))
{
Some(frame["params"]["diagnostics"].clone())
} else {
None
}
})
.await
.expect("publishDiagnostics should arrive");
assert!(
diags.as_array().is_none_or(|a| a.is_empty()),
"Expected empty diagnostics for a small file, got: {diags}"
);
server_handle.abort();
}