use hx_remote::{read_lsp_message, write_lsp_message};
use serde_json::{Value, json};
use std::fs;
use std::io::{BufReader, Write};
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{self, Receiver};
use std::thread;
use std::time::{Duration, Instant};
use tempfile::tempdir;
use url::Url;
struct ChildGuard(Child);
impl Drop for ChildGuard {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
#[test]
fn opens_files_with_positions_and_piped_scratch_text() {
let directory = tempdir().unwrap();
let socket = directory.path().join("hxr.sock");
let file = directory.path().join("source file.rs");
fs::write(&file, "one\ntwo\nthree\n").unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_hxr"))
.args(["--socket", socket.to_str().unwrap(), "--lsp"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let mut lsp_input = child.stdin.take().unwrap();
let lsp_output = child.stdout.take().unwrap();
let mut child = ChildGuard(child);
let (message_tx, message_rx) = mpsc::channel();
thread::spawn(move || {
let mut reader = BufReader::new(lsp_output);
loop {
match read_lsp_message(&mut reader) {
Ok(Some(message)) => {
if message_tx.send(message).is_err() {
return;
}
}
_ => return,
}
}
});
send_lsp(
&mut lsp_input,
json!({
"jsonrpc": "2.0",
"id": 100,
"method": "initialize",
"params": {"capabilities": {"window": {"showDocument": {"support": true}}}}
}),
);
let initialize_response = receive(&message_rx);
assert_eq!(initialize_response["id"], 100);
send_lsp(
&mut lsp_input,
json!({"jsonrpc": "2.0", "method": "initialized", "params": {}}),
);
wait_for_path(&socket);
let positioned_path = format!("{}:3:2", file.display());
let output = Command::new(env!("CARGO_BIN_EXE_hxr"))
.args([
"--socket",
socket.to_str().unwrap(),
"--open",
&positioned_path,
])
.output()
.unwrap();
assert!(
output.status.success(),
"hxr failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let show_file = receive(&message_rx);
assert_eq!(show_file["method"], "window/showDocument");
assert_eq!(show_file["params"]["takeFocus"], true);
assert_eq!(show_file["params"]["selection"]["start"]["line"], 2);
assert_eq!(show_file["params"]["selection"]["start"]["character"], 1);
let opened_path = Url::parse(show_file["params"]["uri"].as_str().unwrap())
.unwrap()
.to_file_path()
.unwrap();
assert_eq!(opened_path, file);
reply_to_show_document(&mut lsp_input, &show_file);
let mut stdin_client = Command::new(env!("CARGO_BIN_EXE_hxr"))
.args([
"--socket",
socket.to_str().unwrap(),
"--stdin-name",
"changes.diff",
"--open",
"-",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
stdin_client
.stdin
.take()
.unwrap()
.write_all(b"-before\n+after\n")
.unwrap();
let stdin_output = stdin_client.wait_with_output().unwrap();
assert!(
stdin_output.status.success(),
"stdin hxr failed: {}",
String::from_utf8_lossy(&stdin_output.stderr)
);
let show_scratch = receive(&message_rx);
let scratch_path = Url::parse(show_scratch["params"]["uri"].as_str().unwrap())
.unwrap()
.to_file_path()
.unwrap();
assert!(
scratch_path
.file_name()
.unwrap()
.to_string_lossy()
.ends_with("changes.diff")
);
assert_eq!(
fs::read_to_string(&scratch_path).unwrap(),
"-before\n+after\n"
);
reply_to_show_document(&mut lsp_input, &show_scratch);
send_lsp(
&mut lsp_input,
json!({"jsonrpc": "2.0", "id": 101, "method": "shutdown", "params": null}),
);
assert_eq!(receive(&message_rx)["id"], 101);
send_lsp(
&mut lsp_input,
json!({"jsonrpc": "2.0", "method": "exit", "params": null}),
);
drop(lsp_input);
assert!(child.0.wait().unwrap().success());
}
fn send_lsp(input: &mut impl Write, message: Value) {
write_lsp_message(input, &message).unwrap();
}
fn receive(receiver: &Receiver<Value>) -> Value {
receiver
.recv_timeout(Duration::from_secs(5))
.expect("timed out waiting for an LSP message")
}
fn reply_to_show_document(input: &mut impl Write, request: &Value) {
send_lsp(
input,
json!({
"jsonrpc": "2.0",
"id": request["id"],
"result": {"success": true}
}),
);
}
fn wait_for_path(path: &Path) {
let deadline = Instant::now() + Duration::from_secs(5);
while !path.exists() {
assert!(Instant::now() < deadline, "socket was not created");
thread::sleep(Duration::from_millis(10));
}
}