use std::path::Path;
use lsp_types::{
ClientCapabilities, DiagnosticClientCapabilities, DidChangeWatchedFilesClientCapabilities,
GeneralClientCapabilities, InitializeParams, InitializeResult,
PublishDiagnosticsClientCapabilities, TextDocumentClientCapabilities,
TextDocumentSyncClientCapabilities, WindowClientCapabilities, WorkspaceClientCapabilities,
WorkspaceFolder,
};
use crate::lsp::rpc::{RpcClient, RpcError};
use crate::lsp::uri::path_to_file_uri;
pub async fn initialize(
client: &RpcClient,
root: &Path,
process_id: Option<u32>,
initialization_options: serde_json::Value,
) -> Result<InitializeResult, RpcError> {
let root_uri = path_to_file_uri(root).map_err(RpcError::Io)?;
let params = InitializeParams {
process_id,
workspace_folders: Some(vec![WorkspaceFolder {
name: "workspace".to_string(),
uri: root_uri.clone(),
}]),
#[allow(deprecated)]
root_uri: Some(root_uri),
initialization_options: if initialization_options.is_null() {
None
} else {
Some(initialization_options)
},
capabilities: client_capabilities(),
..Default::default()
};
let init_timeout = crate::timeout::Timeouts::get().lsp_initialize;
let result: InitializeResult = client.request("initialize", params, init_timeout).await?;
client.notify("initialized", serde_json::json!({})).await?;
Ok(result)
}
fn client_capabilities() -> ClientCapabilities {
ClientCapabilities {
workspace: Some(WorkspaceClientCapabilities {
configuration: Some(true),
did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
dynamic_registration: Some(true),
relative_pattern_support: Some(false),
}),
workspace_folders: Some(true),
..Default::default()
}),
text_document: Some(TextDocumentClientCapabilities {
synchronization: Some(TextDocumentSyncClientCapabilities {
did_save: Some(false),
dynamic_registration: Some(false),
will_save: Some(false),
will_save_wait_until: Some(false),
}),
publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
related_information: Some(true),
version_support: Some(false),
..Default::default()
}),
diagnostic: Some(DiagnosticClientCapabilities {
dynamic_registration: Some(true),
related_document_support: Some(true),
}),
..Default::default()
}),
window: Some(WindowClientCapabilities {
work_done_progress: Some(true),
..Default::default()
}),
general: Some(GeneralClientCapabilities {
..Default::default()
}),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jsonrpc_framing::{decode_frame, encode_frame};
use serde_json::{Value, json};
use tokio::io::BufReader;
async fn with_fake_server<F>(respond: F) -> (RpcClient, tokio::task::JoinHandle<()>)
where
F: Fn(Value) -> Value + Send + 'static,
{
let (client_in, server_out) = tokio::io::duplex(4096);
let (server_in, client_out) = tokio::io::duplex(4096);
let (client_reader, _) = tokio::io::split(client_in);
let (_, client_writer) = tokio::io::split(client_out);
let (server_reader_half, _) = tokio::io::split(server_in);
let (_, mut server_writer) = tokio::io::split(server_out);
let (client, _task) = RpcClient::new(BufReader::new(client_reader), client_writer);
let server = tokio::spawn(async move {
let mut reader = BufReader::new(server_reader_half);
loop {
let frame = match decode_frame(&mut reader).await {
Ok(b) => b,
Err(_) => break,
};
let req: Value = serde_json::from_slice(&frame).unwrap();
let reply = respond(req);
if reply.is_null() {
continue; }
let bytes = serde_json::to_vec(&reply).unwrap();
if encode_frame(&mut server_writer, &bytes).await.is_err() {
break;
}
}
});
(client, server)
}
#[tokio::test]
async fn initialize_round_trips_params_and_returns_capabilities() {
let (client, _server) = with_fake_server(|req: Value| {
if req["method"] == "initialize" {
let id = req["id"].clone();
json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"capabilities": {
"textDocumentSync": 2,
"diagnosticProvider": {
"interFileDependencies": true,
"workspaceDiagnostics": false
}
}
}
})
} else {
Value::Null }
})
.await;
let root = std::env::temp_dir();
let result = initialize(&client, &root, Some(12345), Value::Null)
.await
.unwrap();
assert!(result.capabilities.diagnostic_provider.is_some());
assert!(result.capabilities.text_document_sync.is_some());
}
#[tokio::test]
async fn regression_initialize_request_carries_root_uri() {
let (client_in, server_out) = tokio::io::duplex(4096);
let (server_in, client_out) = tokio::io::duplex(4096);
let (client_reader, _) = tokio::io::split(client_in);
let (_, client_writer) = tokio::io::split(client_out);
let (server_reader, _) = tokio::io::split(server_in);
let (_, mut server_writer) = tokio::io::split(server_out);
let (client, _task) = RpcClient::new(BufReader::new(client_reader), client_writer);
let root = std::env::temp_dir();
let root_str = root.to_string_lossy().to_string();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
tokio::spawn(async move {
let mut reader = BufReader::new(server_reader);
let frame = decode_frame(&mut reader).await.unwrap();
let req: Value = serde_json::from_slice(&frame).unwrap();
let id = req["id"].clone();
let _ = tx.send(req);
let resp = json!({
"jsonrpc": "2.0",
"id": id,
"result": {"capabilities": {}}
});
encode_frame(&mut server_writer, &serde_json::to_vec(&resp).unwrap())
.await
.unwrap();
let _ = decode_frame(&mut reader).await;
});
let _ = initialize(&client, &root, Some(1), Value::Null)
.await
.unwrap();
let req = rx.recv().await.unwrap();
let uri = req["params"]["rootUri"].as_str().unwrap();
assert!(uri.starts_with("file://"), "got: {uri}");
assert!(
uri.contains(&*root_str),
"expected {root_str} in uri: {uri}"
);
let folders = req["params"]["workspaceFolders"].as_array().unwrap();
assert_eq!(folders.len(), 1);
assert_eq!(folders[0]["name"], "workspace");
}
#[tokio::test]
async fn regression_initialization_options_propagate_when_provided() {
let (client_in, server_out) = tokio::io::duplex(4096);
let (server_in, client_out) = tokio::io::duplex(4096);
let (client_reader, _) = tokio::io::split(client_in);
let (_, client_writer) = tokio::io::split(client_out);
let (server_reader, _) = tokio::io::split(server_in);
let (_, mut server_writer) = tokio::io::split(server_out);
let (client, _task) = RpcClient::new(BufReader::new(client_reader), client_writer);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
tokio::spawn(async move {
let mut reader = BufReader::new(server_reader);
let frame = decode_frame(&mut reader).await.unwrap();
let req: Value = serde_json::from_slice(&frame).unwrap();
let id = req["id"].clone();
let _ = tx.send(req);
let resp = json!({"jsonrpc":"2.0","id":id,"result":{"capabilities":{}}});
encode_frame(&mut server_writer, &serde_json::to_vec(&resp).unwrap())
.await
.unwrap();
let _ = decode_frame(&mut reader).await;
});
let opts = json!({"tsserver": {"path": "/path/to/tsserver"}});
let _ = initialize(&client, &std::env::temp_dir(), None, opts)
.await
.unwrap();
let req = rx.recv().await.unwrap();
assert_eq!(
req["params"]["initializationOptions"]["tsserver"]["path"],
"/path/to/tsserver"
);
}
#[tokio::test]
async fn null_initialization_options_omits_field() {
let (client_in, server_out) = tokio::io::duplex(4096);
let (server_in, client_out) = tokio::io::duplex(4096);
let (client_reader, _) = tokio::io::split(client_in);
let (_, client_writer) = tokio::io::split(client_out);
let (server_reader, _) = tokio::io::split(server_in);
let (_, mut server_writer) = tokio::io::split(server_out);
let (client, _task) = RpcClient::new(BufReader::new(client_reader), client_writer);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
tokio::spawn(async move {
let mut reader = BufReader::new(server_reader);
let frame = decode_frame(&mut reader).await.unwrap();
let req: Value = serde_json::from_slice(&frame).unwrap();
let id = req["id"].clone();
let _ = tx.send(req);
let resp = json!({"jsonrpc":"2.0","id":id,"result":{"capabilities":{}}});
encode_frame(&mut server_writer, &serde_json::to_vec(&resp).unwrap())
.await
.unwrap();
let _ = decode_frame(&mut reader).await;
});
let _ = initialize(&client, &std::env::temp_dir(), None, Value::Null)
.await
.unwrap();
let req = rx.recv().await.unwrap();
let opts = req["params"].get("initializationOptions");
assert!(
opts.is_none() || opts.unwrap().is_null(),
"expected omitted or explicit null; got: {opts:?}"
);
}
#[tokio::test]
async fn initialized_notification_is_sent_after_response() {
let (client_in, server_out) = tokio::io::duplex(4096);
let (server_in, client_out) = tokio::io::duplex(4096);
let (client_reader, _) = tokio::io::split(client_in);
let (_, client_writer) = tokio::io::split(client_out);
let (server_reader, _) = tokio::io::split(server_in);
let (_, mut server_writer) = tokio::io::split(server_out);
let (client, _task) = RpcClient::new(BufReader::new(client_reader), client_writer);
let observed = tokio::spawn(async move {
let mut reader = BufReader::new(server_reader);
let first = decode_frame(&mut reader).await.unwrap();
let req: Value = serde_json::from_slice(&first).unwrap();
assert_eq!(req["method"], "initialize");
let id = req["id"].clone();
let resp = json!({"jsonrpc":"2.0","id":id,"result":{"capabilities":{}}});
encode_frame(&mut server_writer, &serde_json::to_vec(&resp).unwrap())
.await
.unwrap();
let second = decode_frame(&mut reader).await.unwrap();
let notif: Value = serde_json::from_slice(&second).unwrap();
assert_eq!(notif["method"], "initialized");
assert!(
notif.get("id").is_none(),
"initialized must be a notification"
);
});
let _ = initialize(&client, &std::env::temp_dir(), None, Value::Null)
.await
.unwrap();
observed.await.unwrap();
}
}