use std::io::{BufRead, BufReader, Read, Write};
use std::process::{Child, Command, Stdio};
use serde_json::{json, Value};
const SOURCE: &str = "# a comment\ndef add(a, b)\n a + b\nend\n";
fn session(requests: &[Value]) -> Vec<Value> {
let mut child: Child = Command::new(env!("CARGO_BIN_EXE_blue"))
.arg("lsp")
.env_remove("BLUE_PATH")
.env_remove("BLUE_CONFIG")
.env_remove("BLUE_TIER")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn `blue lsp`");
{
let stdin = child.stdin.as_mut().expect("stdin");
for req in requests {
let body = serde_json::to_vec(req).expect("encode");
write!(stdin, "Content-Length: {}\r\n\r\n", body.len()).expect("write header");
stdin.write_all(&body).expect("write body");
}
stdin.flush().expect("flush");
}
drop(child.stdin.take());
let mut out = Vec::new();
child
.stdout
.as_mut()
.expect("stdout")
.read_to_end(&mut out)
.expect("read stdout");
let status = child.wait().expect("wait");
let mut err = String::new();
if let Some(mut e) = child.stderr.take() {
let _ = e.read_to_string(&mut err);
}
assert!(
status.success(),
"`blue lsp` exited {status:?}; stderr: {err}"
);
decode_all(&out)
}
fn decode_all(bytes: &[u8]) -> Vec<Value> {
let mut reader = BufReader::new(bytes);
let mut messages = Vec::new();
loop {
let mut length: Option<usize> = None;
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => return messages, Ok(_) => {}
Err(e) => panic!("read header: {e}"),
}
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
break;
}
if let Some(rest) = trimmed.strip_prefix("Content-Length:") {
length = rest.trim().parse().ok();
}
}
let Some(length) = length else {
return messages;
};
let mut buf = vec![0u8; length];
reader.read_exact(&mut buf).expect("read body");
messages.push(serde_json::from_slice(&buf).expect("decode body"));
}
}
fn req(id: i64, method: &str, params: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params })
}
fn notify(method: &str, params: Value) -> Value {
json!({ "jsonrpc": "2.0", "method": method, "params": params })
}
fn reply_to<'a>(messages: &'a [Value], id: i64) -> &'a Value {
messages
.iter()
.find(|m| m["id"] == json!(id))
.unwrap_or_else(|| panic!("no reply with id {id}; got {messages:#?}"))
}
#[test]
fn a_real_session_returns_semantic_tokens_for_an_open_buffer() {
let messages = session(&[
req(1, "initialize", json!({})),
notify("initialized", json!({})),
notify(
"textDocument/didOpen",
json!({ "textDocument": { "uri": "file:///s.b", "text": SOURCE } }),
),
req(
2,
"textDocument/semanticTokens/full",
json!({ "textDocument": { "uri": "file:///s.b" } }),
),
req(3, "shutdown", json!({})),
notify("exit", json!({})),
]);
let provider = &reply_to(&messages, 1)["result"]["capabilities"]["semanticTokensProvider"];
assert_eq!(provider["full"], json!(true));
let types = provider["legend"]["tokenTypes"]
.as_array()
.expect("a tokenTypes legend");
assert!(
types.contains(&json!("keyword")) && types.contains(&json!("function")),
"legend looks wrong: {types:?}",
);
let data: Vec<u64> = reply_to(&messages, 2)["result"]["data"]
.as_array()
.expect("a data array")
.iter()
.map(|v| v.as_u64().expect("a non-negative integer"))
.collect();
assert!(!data.is_empty(), "an editor would paint nothing");
assert_eq!(data.len() % 5, 0, "five integers per token");
let comment_index = types
.iter()
.position(|t| t == &json!("comment"))
.expect("a comment type") as u64;
assert_eq!(
&data[..5],
&[0, 0, 11, comment_index, 0],
"expected the leading comment",
);
let keyword_index = types
.iter()
.position(|t| t == &json!("keyword"))
.expect("a keyword type") as u64;
assert_eq!(&data[5..10], &[1, 0, 3, keyword_index, 0]);
let lines: Vec<&str> = SOURCE.lines().collect();
let (mut line, mut start) = (0u64, 0u64);
for tok in data.chunks(5) {
let (dl, ds, len) = (tok[0], tok[1], tok[2]);
line += dl;
start = if dl == 0 { start + ds } else { ds };
let width = lines[line as usize].chars().count() as u64;
assert!(
start + len <= width,
"token at {line}:{start} (len {len}) runs past a {width}-wide line",
);
}
}
#[test]
fn a_buffer_that_does_not_parse_still_answers_a_token_request() {
let messages = session(&[
req(1, "initialize", json!({})),
notify(
"textDocument/didOpen",
json!({ "textDocument": { "uri": "file:///bad.b", "text": "def (\n" } }),
),
req(
2,
"textDocument/semanticTokens/full",
json!({ "textDocument": { "uri": "file:///bad.b" } }),
),
req(3, "shutdown", json!({})),
notify("exit", json!({})),
]);
let result = &reply_to(&messages, 2)["result"];
assert!(
result.is_null() || result["data"].is_array(),
"expected null or a data array, got {result}",
);
assert_eq!(reply_to(&messages, 3)["result"], Value::Null);
}