lsp-cli 0.1.6

Command-line tool for talking to Language Server Protocol (LSP) servers from the terminal.
use super::test_support::{expect_id, expect_method, read_existing_message};
use super::{IncomingMessage, LspClient};
use crate::lsp::transport::write_message;
use crate::test_support::TestDir;
use serde_json::{Value, json};
use std::io::BufReader;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::thread::{self, JoinHandle};
use std::time::Duration;

struct SocketFixture {
    _dir: TestDir,
    socket_path: PathBuf,
    server: JoinHandle<()>,
}

impl SocketFixture {
    fn spawn(name: &str, run_server: impl FnOnce(Peer) + Send + 'static) -> Self {
        let dir = TestDir::new(name);
        let socket_path = dir.path().join("server.sock");
        let listener = UnixListener::bind(&socket_path).expect("socket should bind");
        let server = thread::spawn(move || {
            let (stream, _) = listener.accept().expect("client should connect");
            run_server(Peer::new(stream));
        });
        Self {
            _dir: dir,
            socket_path,
            server,
        }
    }

    fn connect(&self) -> LspClient {
        LspClient::connect_unix(&self.socket_path, false, Duration::from_secs(1))
            .expect("client should connect")
    }

    fn finish(self) {
        self.server.join().expect("server thread should finish");
    }
}

struct Peer {
    reader: BufReader<UnixStream>,
    writer: UnixStream,
}

impl Peer {
    fn new(stream: UnixStream) -> Self {
        let reader = BufReader::new(stream.try_clone().expect("stream should clone"));
        Self {
            reader,
            writer: stream,
        }
    }

    fn read(&mut self, context: &str) -> Value {
        read_existing_message(
            &mut self.reader,
            &format!("{context} should parse"),
            &format!("{context} should exist"),
        )
    }

    fn initialize(&mut self) -> Value {
        let request = self.read("initialize");
        expect_method(&request, "initialize");
        self.respond(&request, &json!({ "capabilities": {} }), "initialize");
        request
    }

    fn expect_initialized(&mut self) {
        expect_method(&self.read("initialized"), "initialized");
    }

    fn respond(&mut self, request: &Value, result: &Value, context: &str) {
        write_message(
            &mut self.writer,
            &json!({
                "jsonrpc": "2.0",
                "id": request.get("id").cloned().expect("request id should exist"),
                "result": result,
            }),
        )
        .unwrap_or_else(|error| panic!("{context} response should write: {error}"));
    }

    fn send(&mut self, message: &Value, context: &str) {
        write_message(&mut self.writer, message)
            .unwrap_or_else(|error| panic!("{context} should write: {error}"));
    }

    fn finish_shutdown(&mut self) {
        let shutdown = self.read("shutdown");
        expect_method(&shutdown, "shutdown");
        self.respond(&shutdown, &Value::Null, "shutdown");
        expect_method(&self.read("exit"), "exit");
    }
}

fn initialize_client(client: &mut LspClient) {
    client
        .initialize("file:///workspace", "workspace", false)
        .expect("initialize should succeed");
}

#[test]
fn queued_server_request_is_answered_before_next_client_request() {
    let fixture = SocketFixture::spawn("client-init-queue", |mut peer| {
        peer.initialize();
        peer.expect_initialized();

        let registration_response = peer.read("registration response");
        expect_id(&registration_response, "register-1");
        assert_eq!(registration_response.get("result"), Some(&Value::Null));

        let symbols = peer.read("workspace symbol request");
        expect_method(&symbols, "workspace/symbol");
        peer.respond(&symbols, &json!([]), "workspace symbol");
        peer.finish_shutdown();
    });
    let mut client = fixture.connect();
    initialize_client(&mut client);
    client
        .pending_messages
        .push_back(IncomingMessage::Message(json!({
            "jsonrpc": "2.0",
            "id": "register-1",
            "method": "client/registerCapability",
            "params": { "registrations": [] },
        })));

    assert_eq!(
        client
            .workspace_symbol("needle")
            .expect("workspace symbol should succeed"),
        json!([])
    );
    client.shutdown().expect("shutdown should succeed");
    fixture.finish();
}

#[test]
fn server_request_is_answered_while_client_request_is_outstanding() {
    let fixture = SocketFixture::spawn("client-init-in-flight", |mut peer| {
        peer.initialize();
        peer.expect_initialized();

        let symbols = peer.read("workspace symbol request");
        expect_method(&symbols, "workspace/symbol");
        peer.send(
            &json!({
                "jsonrpc": "2.0",
                "id": "configuration-1",
                "method": "workspace/configuration",
                "params": { "items": [] },
            }),
            "configuration request",
        );
        let configuration_response = peer.read("configuration response");
        expect_id(&configuration_response, "configuration-1");
        assert_eq!(configuration_response.get("result"), Some(&json!([])));
        peer.respond(&symbols, &json!([]), "workspace symbol");
        peer.finish_shutdown();
    });
    let mut client = fixture.connect();
    initialize_client(&mut client);

    assert_eq!(
        client
            .workspace_symbol("needle")
            .expect("workspace symbol should succeed"),
        json!([])
    );
    client.shutdown().expect("shutdown should succeed");
    fixture.finish();
}

#[test]
fn initialize_advertises_and_returns_workspace_folders() {
    let fixture = SocketFixture::spawn("client-init-workspace-folders", |mut peer| {
        let initialize = peer.initialize();
        let params = initialize
            .get("params")
            .expect("initialize params should exist");
        assert_eq!(
            params
                .pointer("/capabilities/workspace/workspaceFolders")
                .and_then(Value::as_bool),
            Some(true)
        );
        let workspace_folders = params
            .get("workspaceFolders")
            .cloned()
            .expect("workspaceFolders should exist");
        peer.expect_initialized();

        let folders_response = peer.read("workspace folders response");
        expect_id(&folders_response, "folders-1");
        assert_eq!(folders_response.get("result"), Some(&workspace_folders));
        peer.finish_shutdown();
    });
    let mut client = fixture.connect();
    initialize_client(&mut client);
    client
        .pending_messages
        .push_back(IncomingMessage::Message(json!({
            "jsonrpc": "2.0",
            "id": "folders-1",
            "method": "workspace/workspaceFolders",
        })));

    client.shutdown().expect("shutdown should succeed");
    fixture.finish();
}