Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions lci-core/src/lsp/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value> {
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),
Expand Down Expand Up @@ -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,
Expand Down
60 changes: 34 additions & 26 deletions lci-core/src/lsp/clangd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<dyn Fn(&Value) -> bool + Send + Sync> {
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
55 changes: 30 additions & 25 deletions lci-core/src/lsp/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<dyn Fn(&Value) -> bool + Send + Sync> {
Expand Down Expand Up @@ -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);
Expand Down
62 changes: 37 additions & 25 deletions lci-core/src/lsp/java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<dyn Fn(&Value) -> bool + Send + Sync> {
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading