lsp-cli 0.1.0

Command-line tool for talking to Language Server Protocol (LSP) servers from the terminal.
use std::io::{BufRead, BufReader, Read, Write};

use serde_json::Value;

pub(crate) fn read_message<R>(reader: &mut BufReader<R>) -> Result<Option<Value>, String>
where
    R: Read,
{
    let mut content_length = None;
    let mut line = String::new();

    loop {
        line.clear();
        let bytes = reader
            .read_line(&mut line)
            .map_err(|error| format!("failed to read LSP header: {error}"))?;

        if bytes == 0 {
            return if content_length.is_none() {
                Ok(None)
            } else {
                Err("unexpected EOF while reading LSP headers".to_string())
            };
        }

        let trimmed = line.trim_end_matches(['\r', '\n']);
        if trimmed.is_empty() {
            break;
        }

        let Some((name, value)) = trimmed.split_once(':') else {
            return Err(format!("invalid LSP header: {trimmed}"));
        };

        if name.eq_ignore_ascii_case("Content-Length") {
            content_length = Some(
                value
                    .trim()
                    .parse::<usize>()
                    .map_err(|error| format!("invalid Content-Length {value:?}: {error}"))?,
            );
        }
    }

    let Some(content_length) = content_length else {
        return Err("missing Content-Length header".to_string());
    };

    let mut body = vec![0; content_length];
    reader
        .read_exact(&mut body)
        .map_err(|error| format!("failed to read LSP body: {error}"))?;
    serde_json::from_slice(&body).map_err(|error| format!("invalid JSON-RPC payload: {error}"))
}

pub(crate) fn write_message<W>(writer: &mut W, message: &Value) -> Result<(), String>
where
    W: Write,
{
    let body = serde_json::to_vec(message)
        .map_err(|error| format!("failed to serialize JSON-RPC message: {error}"))?;
    writer
        .write_all(format!("Content-Length: {}\r\n\r\n", body.len()).as_bytes())
        .and_then(|()| writer.write_all(&body))
        .and_then(|()| writer.flush())
        .map_err(|error| format!("failed to write JSON-RPC message: {error}"))
}

pub(crate) fn serialize_debug_message(message: &Value) -> String {
    serde_json::to_string_pretty(message)
        .unwrap_or_else(|_| "<failed to serialize debug message>".to_string())
}

pub(crate) fn log_debug_message(debug: bool, prefix: &str, message: &Value) {
    if debug {
        eprintln!("{prefix}{}", serialize_debug_message(message));
    }
}

#[cfg(test)]
mod tests {
    use super::{read_message, serialize_debug_message, write_message};
    use serde_json::json;
    use std::io::BufReader;

    #[test]
    fn writes_and_reads_lsp_message() {
        let mut buffer = Vec::new();
        let message = json!({"jsonrpc": "2.0", "id": 1, "result": null});
        write_message(&mut buffer, &message).expect("message should be written");

        let mut reader = BufReader::new(buffer.as_slice());
        assert_eq!(
            read_message(&mut reader).expect("message should read"),
            Some(message)
        );
    }

    #[test]
    fn serializes_debug_messages_as_json() {
        assert_eq!(
            serialize_debug_message(&json!({"jsonrpc": "2.0", "id": 1})),
            "{\n  \"id\": 1,\n  \"jsonrpc\": \"2.0\"\n}"
        );
    }
}