diff --git a/lci-core/src/lsp/adapter.rs b/lci-core/src/lsp/adapter.rs index 7807643a..3894d066 100644 --- a/lci-core/src/lsp/adapter.rs +++ b/lci-core/src/lsp/adapter.rs @@ -191,6 +191,51 @@ pub trait LspAdapter: Send + Sync { } } +/// How the `initialize` payload of a plain stdio adapter differs from the +/// base one (definition, references, synchronization, workspace symbols and +/// folders, `workspace/configuration`, empty `initializationOptions`). +#[derive(Debug, Clone, Copy)] +pub(crate) enum InitializeVariant { + /// The base payload: the server may ask for `workspace/configuration`. + Base, + /// The base payload without `workspace.configuration`, plus a + /// hierarchical `documentSymbol` (marksman's heading outline). + DocumentSymbol, + /// The base payload without `workspace.configuration`, plus + /// `publishDiagnostics`: typescript-language-server publishes diagnostics + /// only to a client that declares it takes them, and the open hook waits + /// for the publication of a file it opened (measured against a real + /// server: none without this, one with it). + PublishDiagnostics, +} + +/// The `initialize` params of an adapter with no server-specific options: the +/// process id, the root as `rootUri` and the single workspace folder, and the +/// capabilities the variant names. `None` when `root` has no `file:` URI. +pub(crate) fn initialize_params_for(root: &Path, variant: InitializeVariant) -> Option { + let root_uri = reqwest::Url::from_directory_path(root).ok()?.to_string(); + let name = root + .file_name() + .and_then(|v| v.to_str()) + .unwrap_or("workspace"); + let mut text_document = json!({ "definition": {}, "references": {}, "synchronization": {} }); + let mut workspace = json!({ "symbol": {}, "workspaceFolders": true }); + match variant { + InitializeVariant::Base => workspace["configuration"] = json!(true), + InitializeVariant::DocumentSymbol => { + text_document["documentSymbol"] = json!({ "hierarchicalDocumentSymbolSupport": true }); + } + InitializeVariant::PublishDiagnostics => text_document["publishDiagnostics"] = json!({}), + } + Some(json!({ + "processId": std::process::id(), + "rootUri": root_uri, + "capabilities": { "textDocument": text_document, "workspace": workspace }, + "workspaceFolders": [{ "uri": root_uri, "name": name }], + "initializationOptions": {} + })) +} + /// The largest file a `before_position_request` hook reads and sends whole in /// `textDocument/didOpen`. While it is sent the text is held about three /// times over (the read, the JSON value the transport clones, and the frame), @@ -1229,6 +1274,16 @@ pub(crate) mod contract { ); } + /// A fixed absolute root for pinning `initialize` payloads, with the + /// `file:` URI it maps to (its last component, `proj`, is the folder name). + pub(crate) fn pin_root() -> (&'static Path, &'static str) { + if cfg!(windows) { + (Path::new(r"C:\lci-pin\proj"), "file:///C:/lci-pin/proj/") + } else { + (Path::new("/lci-pin/proj"), "file:///lci-pin/proj/") + } + } + /// The initialize payload can be built without a process. pub(crate) fn assert_initialize_params_name_this_process( adapter: &impl LspAdapter, diff --git a/lci-core/src/lsp/clangd.rs b/lci-core/src/lsp/clangd.rs index 3bd0ebc6..6dcb56ca 100644 --- a/lci-core/src/lsp/clangd.rs +++ b/lci-core/src/lsp/clangd.rs @@ -17,12 +17,15 @@ //! (`--compile-commands-dir=...`); this adapter does not generate one. use crate::config::ClangdLspConfig; -use crate::lsp::adapter::{LspAdapter, MAX_DIAGNOSTICS_WAIT, SentTexts, open_if_changed}; +use crate::lsp::adapter::{ + InitializeVariant, LspAdapter, MAX_DIAGNOSTICS_WAIT, SentTexts, initialize_params_for, + open_if_changed, +}; use crate::lsp::transport::{JsonRpcClient, TransportConfig}; use crate::path::has_extension_ignore_case; use anyhow::Result; use async_trait::async_trait; -use serde_json::{Value, json}; +use serde_json::Value; use std::{path::Path, time::Duration}; /// Every Tree-sitter language identifier one clangd instance navigates. @@ -98,30 +101,7 @@ impl LspAdapter for ClangdServer { if !self.enabled() { return None; } - let root_uri = reqwest::Url::from_directory_path(root).ok()?.to_string(); - let name = root - .file_name() - .and_then(|v| v.to_str()) - .unwrap_or("workspace") - .to_string(); - Some(json!({ - "processId": std::process::id(), - "rootUri": root_uri, - "capabilities": { - "textDocument": { - "definition": {}, - "references": {}, - "synchronization": {} - }, - "workspace": { - "symbol": {}, - "workspaceFolders": true, - "configuration": true - } - }, - "workspaceFolders": [{ "uri": root_uri, "name": name }], - "initializationOptions": {} - })) + initialize_params_for(root, InitializeVariant::Base) } fn ready_predicate(&self) -> Box bool + Send + Sync> { @@ -193,6 +173,7 @@ fn language_id_for(relative: &str) -> &'static str { mod tests { use super::*; use crate::lsp::adapter::contract; + use serde_json::json; fn server(path: Option<&str>, args: &[&str], disabled: bool) -> ClangdServer { let config = ClangdLspConfig { @@ -203,6 +184,33 @@ mod tests { ClangdServer::new(&config, Duration::from_secs(60)) } + #[test] + fn the_clangd_initialize_params_are_pinned() { + let (root, uri) = contract::pin_root(); + let params = server(Some("clangd"), &[], false).initialize_params(root); + assert_eq!( + params, + Some(json!({ + "processId": std::process::id(), + "rootUri": uri, + "capabilities": { + "textDocument": { + "definition": {}, + "references": {}, + "synchronization": {} + }, + "workspace": { + "symbol": {}, + "workspaceFolders": true, + "configuration": true + } + }, + "workspaceFolders": [{ "uri": uri, "name": "proj" }], + "initializationOptions": {} + })) + ); + } + #[test] fn the_clangd_adapter_follows_the_adapter_contract() { let usable = server(Some("clangd"), &[], false); diff --git a/lci-core/src/lsp/go.rs b/lci-core/src/lsp/go.rs index 35742861..bbfa8b33 100644 --- a/lci-core/src/lsp/go.rs +++ b/lci-core/src/lsp/go.rs @@ -15,7 +15,8 @@ use crate::config::GoLspConfig; use crate::lsp::adapter::{ - LspAdapter, MAX_DIAGNOSTICS_WAIT, ProbeExit, SentTexts, open_if_changed, probe_subprocess, + InitializeVariant, LspAdapter, MAX_DIAGNOSTICS_WAIT, ProbeExit, SentTexts, + initialize_params_for, open_if_changed, probe_subprocess, }; use crate::lsp::transport::{JsonRpcClient, TransportConfig}; use anyhow::Result; @@ -99,30 +100,7 @@ impl LspAdapter for GoServer { if !self.enabled() { return None; } - let root_uri = reqwest::Url::from_directory_path(root).ok()?.to_string(); - let name = root - .file_name() - .and_then(|v| v.to_str()) - .unwrap_or("workspace") - .to_string(); - Some(json!({ - "processId": std::process::id(), - "rootUri": root_uri, - "capabilities": { - "textDocument": { - "definition": {}, - "references": {}, - "synchronization": {} - }, - "workspace": { - "symbol": {}, - "workspaceFolders": true, - "configuration": true - } - }, - "workspaceFolders": [{ "uri": root_uri, "name": name }], - "initializationOptions": {} - })) + initialize_params_for(root, InitializeVariant::Base) } fn ready_predicate(&self) -> Box bool + Send + Sync> { @@ -212,6 +190,33 @@ mod tests { .unwrap_err(); } + #[test] + fn the_go_initialize_params_are_pinned() { + let (root, uri) = contract::pin_root(); + let params = server(Some("gopls"), &[], false).initialize_params(root); + assert_eq!( + params, + Some(json!({ + "processId": std::process::id(), + "rootUri": uri, + "capabilities": { + "textDocument": { + "definition": {}, + "references": {}, + "synchronization": {} + }, + "workspace": { + "symbol": {}, + "workspaceFolders": true, + "configuration": true + } + }, + "workspaceFolders": [{ "uri": uri, "name": "proj" }], + "initializationOptions": {} + })) + ); + } + #[test] fn the_go_adapter_follows_the_adapter_contract() { let usable = server(Some("gopls"), &[], false); diff --git a/lci-core/src/lsp/java.rs b/lci-core/src/lsp/java.rs index e60eef3c..9a240051 100644 --- a/lci-core/src/lsp/java.rs +++ b/lci-core/src/lsp/java.rs @@ -21,7 +21,9 @@ //! processes. use crate::config::JavaLspConfig; -use crate::lsp::adapter::{LspAdapter, SentTexts, open_if_changed}; +use crate::lsp::adapter::{ + InitializeVariant, LspAdapter, SentTexts, initialize_params_for, open_if_changed, +}; use crate::lsp::transport::{JsonRpcClient, TransportConfig}; use crate::workspace; use anyhow::{Context, Result}; @@ -326,30 +328,7 @@ impl LspAdapter for JavaServer { if !self.enabled() { return None; } - let root_uri = reqwest::Url::from_directory_path(root).ok()?.to_string(); - let name = root - .file_name() - .and_then(|v| v.to_str()) - .unwrap_or("workspace") - .to_string(); - Some(json!({ - "processId": std::process::id(), - "rootUri": root_uri, - "capabilities": { - "textDocument": { - "definition": {}, - "references": {}, - "synchronization": {} - }, - "workspace": { - "symbol": {}, - "workspaceFolders": true, - "configuration": true - } - }, - "workspaceFolders": [{ "uri": root_uri, "name": name }], - "initializationOptions": {} - })) + initialize_params_for(root, InitializeVariant::Base) } fn ready_predicate(&self) -> Box bool + Send + Sync> { @@ -478,6 +457,39 @@ mod tests { } } + #[test] + fn the_java_initialize_params_are_pinned() { + let (root, uri) = crate::lsp::adapter::contract::pin_root(); + let jdtls = fake_jdtls_install(); + let config = config_with_java(JavaLspConfig { + path: Some(jdtls.path().to_string_lossy().into_owned()), + args: vec![], + disabled: false, + }); + let params = JavaServer::new(&config, Duration::from_secs(30)).initialize_params(root); + assert_eq!( + params, + Some(json!({ + "processId": std::process::id(), + "rootUri": uri, + "capabilities": { + "textDocument": { + "definition": {}, + "references": {}, + "synchronization": {} + }, + "workspace": { + "symbol": {}, + "workspaceFolders": true, + "configuration": true + } + }, + "workspaceFolders": [{ "uri": uri, "name": "proj" }], + "initializationOptions": {} + })) + ); + } + #[test] fn the_jdtls_transport() { // a missing launcher jar yields no transport diff --git a/lci-core/src/lsp/markdown.rs b/lci-core/src/lsp/markdown.rs index 21630772..b43d194a 100644 --- a/lci-core/src/lsp/markdown.rs +++ b/lci-core/src/lsp/markdown.rs @@ -29,11 +29,13 @@ //! [`joins_search_channel`]: LspAdapter::joins_search_channel use crate::config::MarkdownLspConfig; -use crate::lsp::adapter::{LspAdapter, SentTexts, open_if_changed}; +use crate::lsp::adapter::{ + InitializeVariant, LspAdapter, SentTexts, initialize_params_for, open_if_changed, +}; use crate::lsp::transport::{JsonRpcClient, TransportConfig}; use anyhow::Result; use async_trait::async_trait; -use serde_json::{Value, json}; +use serde_json::Value; use std::{path::Path, time::Duration}; pub const LANGUAGE_IDENTIFIER: &str = "markdown"; @@ -102,30 +104,7 @@ impl LspAdapter for MarkdownServer { if !self.enabled() { return None; } - let root_uri = reqwest::Url::from_directory_path(root).ok()?.to_string(); - let name = root - .file_name() - .and_then(|v| v.to_str()) - .unwrap_or("workspace") - .to_string(); - Some(json!({ - "processId": std::process::id(), - "rootUri": root_uri, - "capabilities": { - "textDocument": { - "definition": {}, - "references": {}, - "documentSymbol": { "hierarchicalDocumentSymbolSupport": true }, - "synchronization": {} - }, - "workspace": { - "symbol": {}, - "workspaceFolders": true - } - }, - "workspaceFolders": [{ "uri": root_uri, "name": name }], - "initializationOptions": {} - })) + initialize_params_for(root, InitializeVariant::DocumentSymbol) } fn ready_predicate(&self) -> Box bool + Send + Sync> { @@ -164,6 +143,7 @@ impl LspAdapter for MarkdownServer { mod tests { use super::*; use crate::lsp::adapter::contract; + use serde_json::json; fn server(path: Option<&str>, args: &[&str], disabled: bool) -> MarkdownServer { let config = MarkdownLspConfig { @@ -174,6 +154,33 @@ mod tests { MarkdownServer::new(&config, Duration::from_secs(60)) } + #[test] + fn the_markdown_initialize_params_are_pinned() { + let (root, uri) = contract::pin_root(); + let params = server(Some("marksman"), &[], false).initialize_params(root); + assert_eq!( + params, + Some(json!({ + "processId": std::process::id(), + "rootUri": uri, + "capabilities": { + "textDocument": { + "definition": {}, + "references": {}, + "documentSymbol": { "hierarchicalDocumentSymbolSupport": true }, + "synchronization": {} + }, + "workspace": { + "symbol": {}, + "workspaceFolders": true + } + }, + "workspaceFolders": [{ "uri": uri, "name": "proj" }], + "initializationOptions": {} + })) + ); + } + #[test] fn the_markdown_adapter_follows_the_adapter_contract() { let usable = server(Some("marksman"), &[], false); diff --git a/lci-core/src/lsp/python.rs b/lci-core/src/lsp/python.rs index 2df76452..608b4c41 100644 --- a/lci-core/src/lsp/python.rs +++ b/lci-core/src/lsp/python.rs @@ -14,7 +14,8 @@ use crate::config::PythonLspConfig; use crate::lsp::adapter::{ - LspAdapter, MAX_DIAGNOSTICS_WAIT, ProbeExit, SentTexts, open_if_changed, probe_subprocess, + InitializeVariant, LspAdapter, MAX_DIAGNOSTICS_WAIT, ProbeExit, SentTexts, + initialize_params_for, open_if_changed, probe_subprocess, }; use crate::lsp::transport::{JsonRpcClient, TransportConfig}; use anyhow::Result; @@ -106,30 +107,7 @@ impl LspAdapter for PythonServer { if !self.enabled() { return None; } - let root_uri = reqwest::Url::from_directory_path(root).ok()?.to_string(); - let name = root - .file_name() - .and_then(|v| v.to_str()) - .unwrap_or("workspace") - .to_string(); - Some(json!({ - "processId": std::process::id(), - "rootUri": root_uri, - "capabilities": { - "textDocument": { - "definition": {}, - "references": {}, - "synchronization": {} - }, - "workspace": { - "symbol": {}, - "workspaceFolders": true, - "configuration": true - } - }, - "workspaceFolders": [{ "uri": root_uri, "name": name }], - "initializationOptions": {} - })) + initialize_params_for(root, InitializeVariant::Base) } fn ready_predicate(&self) -> Box bool + Send + Sync> { @@ -216,6 +194,33 @@ mod tests { assert_eq!(failure, Some(ProbeFailure::Missing), "{error:#}"); } + #[test] + fn the_python_initialize_params_are_pinned() { + let (root, uri) = contract::pin_root(); + let params = server(Some("pyright-langserver"), &[], false).initialize_params(root); + assert_eq!( + params, + Some(json!({ + "processId": std::process::id(), + "rootUri": uri, + "capabilities": { + "textDocument": { + "definition": {}, + "references": {}, + "synchronization": {} + }, + "workspace": { + "symbol": {}, + "workspaceFolders": true, + "configuration": true + } + }, + "workspaceFolders": [{ "uri": uri, "name": "proj" }], + "initializationOptions": {} + })) + ); + } + #[test] fn the_python_adapter_follows_the_adapter_contract() { let usable = server(Some("pyright-langserver"), &[], false); diff --git a/lci-core/src/lsp/typescript.rs b/lci-core/src/lsp/typescript.rs index 9d00dcd3..c02bb53e 100644 --- a/lci-core/src/lsp/typescript.rs +++ b/lci-core/src/lsp/typescript.rs @@ -14,12 +14,15 @@ //! being present. use crate::config::TypeScriptLspConfig; -use crate::lsp::adapter::{LspAdapter, MAX_DIAGNOSTICS_WAIT, SentTexts, open_if_changed}; +use crate::lsp::adapter::{ + InitializeVariant, LspAdapter, MAX_DIAGNOSTICS_WAIT, SentTexts, initialize_params_for, + open_if_changed, +}; use crate::lsp::transport::{JsonRpcClient, TransportConfig}; use crate::path::has_extension_ignore_case; use anyhow::Result; use async_trait::async_trait; -use serde_json::{Value, json}; +use serde_json::Value; use std::{path::Path, time::Duration}; /// Every Tree-sitter language identifier one typescript-language-server @@ -102,31 +105,7 @@ impl LspAdapter for TypeScriptServer { if !self.enabled() { return None; } - let root_uri = reqwest::Url::from_directory_path(root).ok()?.to_string(); - let name = root - .file_name() - .and_then(|v| v.to_str()) - .unwrap_or("workspace") - .to_string(); - Some(json!({ - "processId": std::process::id(), - "rootUri": root_uri, - "capabilities": { - "textDocument": { - "definition": {}, - "references": {}, - "synchronization": {}, - // typescript-language-server publishes diagnostics only to - // a client that declares it takes them; the hook waits for - // the publication of a file it opened (measured against a - // real server: none without this, one with it). - "publishDiagnostics": {} - }, - "workspace": { "symbol": {}, "workspaceFolders": true } - }, - "workspaceFolders": [{ "uri": root_uri, "name": name }], - "initializationOptions": {} - })) + initialize_params_for(root, InitializeVariant::PublishDiagnostics) } fn ready_predicate(&self) -> Box bool + Send + Sync> { @@ -198,6 +177,7 @@ fn language_id_for(relative: &str) -> &'static str { mod tests { use super::*; use crate::lsp::adapter::contract; + use serde_json::json; fn server(path: Option<&str>, args: &[&str], disabled: bool) -> TypeScriptServer { let config = TypeScriptLspConfig { @@ -208,6 +188,30 @@ mod tests { TypeScriptServer::new(&config, Duration::from_secs(60)) } + #[test] + fn the_typescript_initialize_params_are_pinned() { + let (root, uri) = contract::pin_root(); + let params = server(Some("typescript-language-server"), &[], false).initialize_params(root); + assert_eq!( + params, + Some(json!({ + "processId": std::process::id(), + "rootUri": uri, + "capabilities": { + "textDocument": { + "definition": {}, + "references": {}, + "synchronization": {}, + "publishDiagnostics": {} + }, + "workspace": { "symbol": {}, "workspaceFolders": true } + }, + "workspaceFolders": [{ "uri": uri, "name": "proj" }], + "initializationOptions": {} + })) + ); + } + #[test] fn the_typescript_adapter_follows_the_adapter_contract() { let usable = server(Some("typescript-language-server"), &[], false); diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 9304a7c4..5c7887c5 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -558,7 +558,6 @@ fn with_fake_documents_server_as( /// `rust-analyzer-fake.args` beside it (see `fake_lsp.rs`); the copy and its /// sidecar are the test's own, so no two tests share either. Keep `config` from /// [`fixture`]. -#[allow(dead_code)] fn with_fake_rust_analyzer( mut config: Config, temp: &Path, @@ -631,7 +630,6 @@ fn strip_lci_env_named( /// `lsp: restarted: ...`, N104, N109): which of two calls starts /// the server decides which of them carries such a note, so tests that compare /// two calls' notes compare them without these. -#[allow(dead_code)] fn without_barrier_notes(notes: &[String]) -> Vec { notes .iter()