lsp-cli 0.1.6

Command-line tool for talking to Language Server Protocol (LSP) servers from the terminal.
use super::{
    CallHierarchyDirection, CallHierarchyQueryContext, LocationQueryKind, NamedAnchorRequest,
    NamedLocationQueryContext, PreparedWorkspace, collect_call_hierarchy_matches,
    collect_named_location_matches, select_named_anchors,
};
use crate::config::ConfigStore;
use crate::config::{CliConfig, FiletypeConfig};
use crate::lsp::{SymbolMatch, path_to_file_uri};
use crate::test_support::{TestDir, detection_result, lsp_peer::LspPeer, suggested_language};
use lsp_types::SymbolKind;
use serde_json::{Value, json};
use std::collections::BTreeSet;
use std::num::NonZeroUsize;
use std::time::Duration;

struct Fixture {
    dir: TestDir,
    workspace: PreparedWorkspace,
    config: ConfigStore,
    initialize: crate::lsp::InitializeResponse,
}

impl Fixture {
    fn new(limit: usize) -> Self {
        let dir = TestDir::new("named-window");
        for file in ["a.lua", "b.lua", "c.lua"] {
            dir.write_file(file, "local target = 1\n");
        }
        let workspace = PreparedWorkspace {
            detection: detection_result(&["lua"], &[]),
            server: suggested_language("fake", "fake", "fake", "lua"),
            allowed_filetypes: BTreeSet::from(["lua".into()]),
            root_uri: path_to_file_uri(dir.path()).expect("uri"),
            workspace_name: "test".into(),
            daemon_socket_path: None,
            daemon_socket_error: None,
        };
        let config = ConfigStore {
            filetypes: vec![FiletypeConfig {
                id: "lua".into(),
                extensions: vec!["lua".into()],
                patterns: vec![],
            }],
            lsps: vec![],
            cli: CliConfig {
                max_requests_in_flight: NonZeroUsize::new(limit),
                ..Default::default()
            },
        };
        let initialize = crate::lsp::InitializeResponse::from_raw_value(json!({"capabilities": {
            "workspaceSymbolProvider":true, "documentSymbolProvider":true,
            "referencesProvider":true, "definitionProvider":true, "declarationProvider":true,
            "implementationProvider":true, "typeDefinitionProvider":true,
            "callHierarchyProvider":true
        }}))
        .expect("capabilities");
        Self {
            dir,
            workspace,
            config,
            initialize,
        }
    }

    fn request(&self, function_only: bool) -> NamedAnchorRequest<'_> {
        NamedAnchorRequest {
            directory: self.dir.path(),
            name: "target",
            function_only,
        }
    }
}

fn range() -> Value {
    json!({"start":{"line":0,"character":6},"end":{"line":0,"character":12}})
}

fn serve_symbols(peer: &mut LspPeer, limit: usize, matching: bool) {
    let mut remaining = 3;
    while remaining > 0 {
        let requests = (0..limit.min(remaining))
            .map(|_| peer.document_request())
            .collect::<Vec<_>>();
        for request in requests.iter().rev() {
            let variable = request["params"]["textDocument"]["uri"]
                .as_str()
                .expect("uri")
                .ends_with("c.lua");
            let symbol = json!({"name":if matching {"target"} else {"other"},
                "kind":if variable {13} else {12}, "range":range(), "selectionRange":range()});
            peer.reply(request, json!([symbol.clone(), symbol]));
        }
        remaining -= requests.len();
    }
}

#[test]
fn named_location_queries_find_local_and_duplicate_names_in_file_order() {
    for kind in [
        LocationQueryKind::References,
        LocationQueryKind::Definition,
        LocationQueryKind::Declaration,
        LocationQueryKind::Implementation,
        LocationQueryKind::TypeDefinition,
    ] {
        for limit in [1, 20] {
            let fixture = Fixture::new(limit);
            let (mut client, server) =
                LspPeer::spawn(&fixture.dir, Duration::from_secs(3), move |peer| {
                    serve_symbols(peer, limit, true);
                    for file in ["a.lua", "b.lua", "c.lua"] {
                        let request = peer.read();
                        assert_eq!(request["method"], format!("textDocument/{}", kind.label()));
                        let uri = &request["params"]["textDocument"]["uri"];
                        assert!(uri.as_str().expect("uri").ends_with(file));
                        assert_eq!(
                            request["params"]["position"],
                            json!({"line":0,"character":6})
                        );
                        peer.reply(&request, json!([{"uri":uri, "range":range()}]));
                    }
                    peer.finish();
                });
            let matches = collect_named_location_matches(
                &fixture.workspace,
                &fixture.initialize,
                &mut client,
                NamedLocationQueryContext {
                    config: &fixture.config,
                    directory: fixture.dir.path(),
                    name: "target",
                    kind,
                    include_full_content: false,
                },
            )
            .expect("named query");
            assert_eq!(
                matches
                    .iter()
                    .map(|m| m.path.file_name().expect("file").to_str().expect("utf8"))
                    .collect::<Vec<_>>(),
                ["a.lua", "b.lua", "c.lua"]
            );
            assert!(
                matches
                    .iter()
                    .all(|m| m.name == "target" && m.line == 1 && m.col == 7)
            );
            client.shutdown().expect("shutdown");
            server.join().expect("server finishes");
        }
    }
}

#[test]
fn call_hierarchy_queries_filter_non_functions() {
    for direction in [
        CallHierarchyDirection::Incoming,
        CallHierarchyDirection::Outgoing,
    ] {
        let fixture = Fixture::new(20);
        let (mut client, server) = LspPeer::spawn(
            &fixture.dir,
            Duration::from_secs(3),
            move |peer| {
                serve_symbols(peer, 20, true);
                for file in ["a.lua", "b.lua"] {
                    let prepare = peer.read();
                    assert_eq!(prepare["method"], "textDocument/prepareCallHierarchy");
                    let uri = &prepare["params"]["textDocument"]["uri"];
                    assert!(uri.as_str().expect("uri").ends_with(file));
                    let item = json!({"name":"target", "kind":12, "uri":uri, "range":range(), "selectionRange":range()});
                    peer.reply(&prepare, json!([item]));
                    let calls = peer.read();
                    let method = match direction {
                        CallHierarchyDirection::Incoming => "callHierarchy/incomingCalls",
                        CallHierarchyDirection::Outgoing => "callHierarchy/outgoingCalls",
                    };
                    assert_eq!(calls["method"], method);
                    peer.reply(&calls, json!([]));
                }
                peer.finish();
            },
        );
        let matches = collect_call_hierarchy_matches(
            &fixture.workspace,
            &fixture.initialize,
            &mut client,
            CallHierarchyQueryContext {
                config: &fixture.config,
                directory: fixture.dir.path(),
                name: "target",
                direction,
            },
        )
        .expect("call hierarchy");
        assert!(matches.is_empty());
        client.shutdown().expect("shutdown");
        server.join().expect("server finishes");
    }
}

#[test]
fn falls_back_to_workspace_symbols_when_no_document_name_matches() {
    let fixture = Fixture::new(20);
    let anchor = SymbolMatch {
        name: "target".into(),
        kind: SymbolKind::FUNCTION,
        path: fixture.dir.path().join("a.lua"),
        line: 1,
        col: 7,
        line_content: "local target = 1".into(),
        full_content: None,
    };
    let anchor_uri = path_to_file_uri(&fixture.dir.path().join("a.lua")).expect("uri");
    let (mut client, server) = LspPeer::spawn(&fixture.dir, Duration::from_secs(3), move |peer| {
        serve_symbols(peer, 20, false);
        let request = peer.read();
        assert_eq!(request["method"], "workspace/symbol");
        let symbol = json!({
            "name": "target",
            "kind": 12,
            "location": {"uri": anchor_uri, "range": range()},
        });
        peer.reply(&request, json!([symbol]));
        peer.finish();
    });
    let matches = select_named_anchors(
        &fixture.workspace,
        &fixture.initialize,
        &mut client,
        &fixture.config,
        fixture.request(false),
    )
    .expect("fallback");
    assert_eq!(matches, vec![anchor]);
    client.shutdown().expect("shutdown");
    server.join().expect("server finishes");
}