use std::io::BufReader;
use std::path::Path;
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender};
use lsp_server::{Connection, Message, Notification, Request, RequestId, Response};
use lsp_types::notification::{Initialized, Notification as _};
use lsp_types::request::Initialize;
use lsp_types::{
GotoDefinitionResponse, InitializeParams, InitializedParams, Position, TextDocumentIdentifier,
TextDocumentPositionParams, Url, WorkspaceFolder,
};
use super::{LspError, REQUEST_TIMEOUT_MS};
pub(crate) struct Session {
pub(crate) child: Child,
pub(crate) connection: Connection,
pub(crate) _reader_handle: JoinHandle<()>,
pub(crate) _writer_handle: JoinHandle<()>,
pub(crate) next_request_id: i32,
}
pub(crate) fn spawn_transport(
stdin: ChildStdin,
stdout: ChildStdout,
) -> (Connection, JoinHandle<()>, JoinHandle<()>) {
let (writer_tx, writer_rx): (Sender<Message>, Receiver<Message>) = bounded(16);
let (reader_tx, reader_rx): (Sender<Message>, Receiver<Message>) = bounded(16);
let writer_handle = thread::Builder::new()
.name("codenexus-lsp-writer".to_owned())
.spawn(move || {
let mut stdin = stdin;
for msg in writer_rx.iter() {
if Message::write(&msg, &mut stdin).is_err() {
break;
}
}
})
.expect("spawn lsp writer thread");
let reader_handle = thread::Builder::new()
.name("codenexus-lsp-reader".to_owned())
.spawn(move || {
let mut reader = BufReader::new(stdout);
loop {
match Message::read(&mut reader) {
Ok(Some(msg)) => {
if reader_tx.send(msg).is_err() {
break;
}
}
Ok(None) => break,
Err(_) => break,
}
}
})
.expect("spawn lsp reader thread");
let connection = Connection {
sender: writer_tx,
receiver: reader_rx,
};
(connection, reader_handle, writer_handle)
}
pub(crate) fn spawn_server(
server_path: &Path,
workspace: &Path,
args: &[&str],
) -> Result<(Child, ChildStdin, ChildStdout), LspError> {
let mut child = Command::new(server_path)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.current_dir(workspace)
.spawn()
.map_err(|e| LspError::ServerStart(e.to_string()))?;
let stdin = child.stdin.take().expect("stdin was Stdio::piped");
let stdout = child.stdout.take().expect("stdout was Stdio::piped");
Ok((child, stdin, stdout))
}
pub(crate) fn initialize_session(session: &mut Session, workspace: &Path) -> Result<(), LspError> {
let root_uri = path_to_url(workspace)?;
let init_params = InitializeParams {
process_id: Some(std::process::id()),
workspace_folders: Some(vec![WorkspaceFolder {
uri: root_uri,
name: workspace
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "workspace".to_string()),
}]),
capabilities: lsp_types::ClientCapabilities::default(),
..Default::default()
};
let _init_result = send_request::<Initialize>(session, init_params)?;
send_notification(
&session.connection,
Initialized::METHOD,
&InitializedParams {},
)?;
Ok(())
}
pub(crate) fn send_request<R>(
session: &mut Session,
params: R::Params,
) -> Result<R::Result, LspError>
where
R: lsp_types::request::Request,
R::Params: serde::Serialize,
R::Result: serde::de::DeserializeOwned,
{
let id = session.next_request_id;
session.next_request_id += 1;
let request = Request::new(RequestId::from(id), R::METHOD.to_string(), params);
session
.connection
.sender
.send(Message::Request(request))
.map_err(|e| LspError::Communication(format!("send request: {e}")))?;
let deadline = Duration::from_millis(REQUEST_TIMEOUT_MS);
loop {
let msg = session
.connection
.receiver
.recv_timeout(deadline)
.map_err(|e| match e {
RecvTimeoutError::Timeout => LspError::Timeout(REQUEST_TIMEOUT_MS),
RecvTimeoutError::Disconnected => {
LspError::Communication("server connection closed".into())
}
})?;
match msg {
Message::Response(resp) => {
if resp.id != RequestId::from(id) {
continue;
}
return decode_response::<R>(resp);
}
Message::Notification(_) | Message::Request(_) => continue,
}
}
}
pub(crate) fn send_raw_request(session: &mut Session, method: &str, params: serde_json::Value) {
let id = session.next_request_id;
session.next_request_id += 1;
let request = Request {
id: RequestId::from(id),
method: method.to_string(),
params,
};
let _ = session.connection.sender.send(Message::Request(request));
}
pub(crate) fn send_notification<P: serde::Serialize>(
conn: &Connection,
method: &str,
params: &P,
) -> Result<(), LspError> {
let params_value =
serde_json::to_value(params).map_err(|e| LspError::Communication(e.to_string()))?;
let notif = Notification {
method: method.to_string(),
params: params_value,
};
conn.sender
.send(Message::Notification(notif))
.map_err(|e| LspError::Communication(format!("send notification: {e}")))
}
pub(crate) fn decode_response<R>(resp: Response) -> Result<R::Result, LspError>
where
R: lsp_types::request::Request,
R::Result: serde::de::DeserializeOwned,
{
if let Some(err) = resp.error {
return Err(LspError::Communication(format!(
"server error {}: {}",
err.code, err.message
)));
}
match resp.result {
Some(value) => serde_json::from_value::<R::Result>(value)
.map_err(|e| LspError::Communication(format!("decode response: {e}"))),
None => serde_json::from_value::<R::Result>(serde_json::Value::Null)
.map_err(|e| LspError::Communication(format!("decode null response: {e}"))),
}
}
pub(crate) fn extract_first_location(
resp: Option<GotoDefinitionResponse>,
) -> Option<lsp_types::Location> {
match resp? {
GotoDefinitionResponse::Scalar(loc) => Some(loc),
GotoDefinitionResponse::Array(locs) => locs.into_iter().next(),
GotoDefinitionResponse::Link(links) => {
links.into_iter().next().map(|link| lsp_types::Location {
uri: link.target_uri,
range: link.target_range,
})
}
}
}
pub(crate) fn make_position_params(
file: &Path,
line: u32,
col: u32,
) -> Result<TextDocumentPositionParams, LspError> {
let uri = path_to_url(file)?;
Ok(TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line,
character: col,
},
})
}
fn path_to_url(path: &Path) -> Result<Url, LspError> {
Url::from_file_path(path).map_err(|_| {
LspError::Communication(format!(
"path is not absolute or cannot be encoded as a file URL: {}",
path.display()
))
})
}
#[cfg(test)]
mod tests {
use super::*;
use lsp_types::request::Shutdown;
fn make_test_session() -> (Session, Sender<Message>, Receiver<Message>) {
let (writer_tx, writer_rx) = bounded::<Message>(16);
let (reader_tx, reader_rx) = bounded::<Message>(16);
let connection = Connection {
sender: writer_tx,
receiver: reader_rx,
};
let child = Command::new("true").spawn().expect("spawn");
let session = Session {
child,
connection,
_reader_handle: thread::spawn(|| {}),
_writer_handle: thread::spawn(|| {}),
next_request_id: 0,
};
(session, reader_tx, writer_rx)
}
#[test]
fn decode_response_returns_error_for_server_error() {
let resp = Response::new_err(RequestId::from(0), 1, "test error".to_string());
let result: Result<(), LspError> = decode_response::<Shutdown>(resp);
let err = result.expect_err("should return error");
match err {
LspError::Communication(msg) => {
assert!(msg.contains("server error 1"), "msg: {msg}");
assert!(msg.contains("test error"), "msg: {msg}");
}
other => panic!("expected Communication, got: {other:?}"),
}
}
#[test]
fn send_notification_succeeds() {
let (tx, rx) = bounded::<Message>(16);
let (_dummy_tx, dummy_rx) = bounded::<Message>(1);
let conn = Connection {
sender: tx,
receiver: dummy_rx,
};
send_notification(&conn, "test/method", &"params").expect("should succeed");
let msg = rx.recv().expect("should receive");
match msg {
Message::Notification(n) => assert_eq!(n.method, "test/method"),
other => panic!("expected Notification, got: {other:?}"),
}
}
#[test]
fn send_notification_returns_error_on_closed_channel() {
let (tx, rx) = bounded::<Message>(16);
drop(rx);
let (_dummy_tx, dummy_rx) = bounded::<Message>(1);
let conn = Connection {
sender: tx,
receiver: dummy_rx,
};
let err = send_notification(&conn, "test", &"x").expect_err("should fail");
assert!(matches!(err, LspError::Communication(_)));
}
#[test]
fn send_request_returns_response() {
let (mut session, reader_tx, writer_rx) = make_test_session();
thread::spawn(move || {
let req = writer_rx.recv().expect("recv");
if let Message::Request(request) = req {
let resp = Response::new_ok(request.id, serde_json::Value::Null);
reader_tx.send(Message::Response(resp)).expect("send");
}
});
let result = send_request::<Shutdown>(&mut session, ());
assert!(result.is_ok(), "should return Ok: {result:?}");
}
#[test]
fn send_request_skips_response_with_wrong_id() {
let (mut session, reader_tx, writer_rx) = make_test_session();
thread::spawn(move || {
let req = writer_rx.recv().expect("recv");
if let Message::Request(request) = req {
let wrong = Response::new_ok(RequestId::from(999), serde_json::Value::Null);
reader_tx.send(Message::Response(wrong)).expect("send");
let correct = Response::new_ok(request.id, serde_json::Value::Null);
reader_tx.send(Message::Response(correct)).expect("send");
}
});
let result = send_request::<Shutdown>(&mut session, ());
assert!(result.is_ok(), "should skip wrong id: {result:?}");
}
#[test]
fn send_request_skips_notifications() {
let (mut session, reader_tx, writer_rx) = make_test_session();
thread::spawn(move || {
let req = writer_rx.recv().expect("recv");
if let Message::Request(request) = req {
let notif = Notification::new("test".to_string(), serde_json::Value::Null);
reader_tx.send(Message::Notification(notif)).expect("send");
let resp = Response::new_ok(request.id, serde_json::Value::Null);
reader_tx.send(Message::Response(resp)).expect("send");
}
});
let result = send_request::<Shutdown>(&mut session, ());
assert!(result.is_ok(), "should skip notification: {result:?}");
}
#[test]
fn send_request_returns_error_on_disconnect() {
let (mut session, reader_tx, _writer_rx) = make_test_session();
drop(reader_tx);
let err = send_request::<Shutdown>(&mut session, ()).expect_err("should fail");
match err {
LspError::Communication(msg) => assert!(msg.contains("closed"), "msg: {msg}"),
other => panic!("expected Communication, got: {other:?}"),
}
}
#[test]
fn spawn_server_succeeds_with_cat() {
let workspace = std::env::temp_dir();
let result = spawn_server(Path::new("cat"), &workspace, &[]);
assert!(result.is_ok(), "spawn_server should succeed: {result:?}");
let (mut child, stdin, stdout) = result.unwrap();
drop(stdin);
drop(stdout);
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn spawn_server_fails_with_nonexistent_binary() {
let workspace = std::env::temp_dir();
let err = spawn_server(Path::new("/nonexistent/binary/path"), &workspace, &[])
.expect_err("should fail");
assert!(matches!(err, LspError::ServerStart(_)));
}
#[test]
fn spawn_transport_round_trips_message() {
let mut child = Command::new("cat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn cat");
let stdin = child.stdin.take().expect("stdin");
let stdout = child.stdout.take().expect("stdout");
let (conn, _reader_handle, _writer_handle) = spawn_transport(stdin, stdout);
let notif = Notification::new("test/method".to_string(), serde_json::Value::Null);
conn.sender
.send(Message::Notification(notif))
.expect("send");
let msg = conn
.receiver
.recv_timeout(Duration::from_secs(3))
.expect("should receive echo");
assert!(matches!(msg, Message::Notification(_)));
drop(conn);
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn initialize_session_completes_handshake() {
let (mut session, reader_tx, writer_rx) = make_test_session();
thread::spawn(move || {
if let Ok(Message::Request(request)) = writer_rx.recv_timeout(Duration::from_secs(2)) {
let init_result = serde_json::json!({ "capabilities": {} });
let resp = Response::new_ok(request.id, init_result);
reader_tx.send(Message::Response(resp)).expect("send");
}
thread::sleep(Duration::from_millis(500));
});
let workspace = std::env::current_dir().expect("cwd");
let result = initialize_session(&mut session, &workspace);
assert!(
result.is_ok(),
"initialize_session should succeed: {result:?}"
);
}
}