#![allow(clippy::wildcard_imports, clippy::default_trait_access)]
use lsp_types::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::mpsc::{self, Receiver};
use std::time::{Duration, Instant};
use super::config::LspServerConfig;
const INIT_TIMEOUT_SECS: u64 = 60;
const REQUEST_TIMEOUT_SECS: u64 = 30;
const SHUTDOWN_TIMEOUT_SECS: u64 = 5;
pub fn file_path_to_uri(path: &str) -> Result<Uri, String> {
let abs = if path.starts_with('/') || (path.len() >= 2 && path.as_bytes()[1] == b':') {
path.to_string()
} else {
std::fs::canonicalize(path)
.map(|p| p.to_string_lossy().to_string())
.map_err(|e| format!("Cannot resolve path '{path}': {e}"))?
};
let normalized = abs.replace('\\', "/");
let uri_str = if normalized.starts_with('/') {
format!("file://{normalized}")
} else {
format!("file:///{normalized}")
};
uri_str
.parse::<Uri>()
.map_err(|e| format!("Invalid URI: {e}"))
}
pub fn uri_to_file_path(uri: &Uri) -> Option<String> {
let s = uri.as_str();
s.strip_prefix("file://")
.map(|p| urlencoding::decode(p).map_or_else(|_| p.to_string(), |d| d.to_string()))
}
pub struct LspClient {
child: Child,
stdin: ChildStdin,
response_rx: Receiver<Result<Value, String>>,
next_id: AtomicI64,
initialized: bool,
}
#[derive(Serialize)]
struct JsonRpcRequest {
jsonrpc: &'static str,
id: i64,
method: String,
params: Value,
}
#[derive(Deserialize)]
struct JsonRpcResponse {
#[allow(dead_code)]
id: Option<i64>,
result: Option<Value>,
error: Option<JsonRpcError>,
}
#[derive(Deserialize)]
struct JsonRpcError {
#[allow(dead_code)]
code: i64,
message: String,
}
fn read_one_message(reader: &mut BufReader<ChildStdout>) -> Result<Value, String> {
let mut content_length = 0usize;
loop {
let mut line = String::new();
let bytes_read = reader
.read_line(&mut line)
.map_err(|e| format!("Read header: {e}"))?;
if bytes_read == 0 {
return Err("LSP server closed connection (EOF)".into());
}
let trimmed = line.trim();
if trimmed.is_empty() {
break;
}
if let Some(val) = trimmed.strip_prefix("Content-Length: ") {
content_length = val.parse().map_err(|e| format!("Parse length: {e}"))?;
}
}
if content_length == 0 {
return Err("Zero content length from LSP server".into());
}
let mut body = vec![0u8; content_length];
std::io::Read::read_exact(reader, &mut body).map_err(|e| format!("Read body: {e}"))?;
let text = String::from_utf8_lossy(&body);
serde_json::from_str(&text).map_err(|e| format!("Parse response: {e}"))
}
fn spawn_reader(stdout: ChildStdout) -> Receiver<Result<Value, String>> {
let (tx, rx) = mpsc::channel();
std::thread::Builder::new()
.name("lsp-reader".into())
.spawn(move || {
let mut reader = BufReader::new(stdout);
loop {
match read_one_message(&mut reader) {
Ok(msg) => {
if tx.send(Ok(msg)).is_err() {
break;
}
}
Err(e) => {
let _ = tx.send(Err(e));
break;
}
}
}
})
.ok();
rx
}
impl LspClient {
pub fn start(config: &LspServerConfig, root_uri: &Uri) -> Result<Self, String> {
let mut child = Command::new(&config.command)
.args(&config.args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start LSP server '{}': {e}", config.command))?;
let stdin = child.stdin.take().ok_or("No stdin")?;
let stdout = child.stdout.take().ok_or("No stdout")?;
let response_rx = spawn_reader(stdout);
let mut client = Self {
child,
stdin,
response_rx,
next_id: AtomicI64::new(1),
initialized: false,
};
client.initialize(root_uri)?;
Ok(client)
}
fn check_alive(&mut self) -> Result<(), String> {
match self.child.try_wait() {
Ok(Some(status)) => Err(format!("LSP server exited: {status}")),
Ok(None) => Ok(()),
Err(e) => Err(format!("Cannot check LSP server status: {e}")),
}
}
#[allow(deprecated)]
fn initialize(&mut self, root_uri: &Uri) -> Result<(), String> {
let params = InitializeParams {
root_uri: Some(root_uri.clone()),
capabilities: ClientCapabilities {
text_document: Some(TextDocumentClientCapabilities {
rename: Some(RenameClientCapabilities {
dynamic_registration: Some(false),
prepare_support: Some(true),
..Default::default()
}),
references: Some(DynamicRegistrationClientCapabilities {
dynamic_registration: Some(false),
}),
definition: Some(GotoCapability {
dynamic_registration: Some(false),
link_support: Some(false),
}),
implementation: Some(GotoCapability {
dynamic_registration: Some(false),
link_support: Some(false),
}),
..Default::default()
}),
..Default::default()
},
..Default::default()
};
let _result = self.request_with_timeout::<request::Initialize>(
params,
Duration::from_secs(INIT_TIMEOUT_SECS),
)?;
self.send_notification::<notification::Initialized>(InitializedParams {})?;
self.initialized = true;
Ok(())
}
pub fn did_open(&mut self, uri: &Uri, language_id: &str, text: &str) -> Result<(), String> {
self.check_alive()?;
self.send_notification::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: language_id.to_string(),
version: 1,
text: text.to_string(),
},
})
}
pub fn references(&mut self, uri: &Uri, position: Position) -> Result<Vec<Location>, String> {
self.check_alive()?;
let params = ReferenceParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position,
},
context: ReferenceContext {
include_declaration: true,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = self.request_with_timeout::<request::References>(
params,
Duration::from_secs(REQUEST_TIMEOUT_SECS),
)?;
Ok(result.unwrap_or_default())
}
pub fn definition(
&mut self,
uri: &Uri,
position: Position,
) -> Result<GotoDefinitionResponse, String> {
self.check_alive()?;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = self.request_with_timeout::<request::GotoDefinition>(
params,
Duration::from_secs(REQUEST_TIMEOUT_SECS),
)?;
Ok(result.unwrap_or(GotoDefinitionResponse::Array(vec![])))
}
pub fn rename(
&mut self,
uri: &Uri,
position: Position,
new_name: &str,
) -> Result<Option<WorkspaceEdit>, String> {
self.check_alive()?;
let params = RenameParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position,
},
new_name: new_name.to_string(),
work_done_progress_params: Default::default(),
};
self.request_with_timeout::<request::Rename>(
params,
Duration::from_secs(REQUEST_TIMEOUT_SECS),
)
}
pub fn implementations(
&mut self,
uri: &Uri,
position: Position,
) -> Result<Vec<Location>, String> {
self.check_alive()?;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let value = self.request_raw_with_timeout(
"textDocument/implementation",
serde_json::to_value(params).unwrap_or_default(),
Duration::from_secs(REQUEST_TIMEOUT_SECS),
)?;
match value {
Some(v) => {
let locations: Vec<Location> = serde_json::from_value(v).unwrap_or_default();
Ok(locations)
}
None => Ok(vec![]),
}
}
fn request_with_timeout<R: request::Request>(
&mut self,
params: R::Params,
timeout: Duration,
) -> Result<R::Result, String>
where
R::Params: Serialize,
R::Result: for<'de> Deserialize<'de>,
{
let value = self.request_raw_with_timeout(
R::METHOD,
serde_json::to_value(params).map_err(|e| e.to_string())?,
timeout,
)?;
match value {
Some(v) => serde_json::from_value(v).map_err(|e| format!("Deserialize error: {e}")),
None => serde_json::from_value(Value::Null).map_err(|e| format!("Null result: {e}")),
}
}
fn request_raw_with_timeout(
&mut self,
method: &str,
params: Value,
timeout: Duration,
) -> Result<Option<Value>, String> {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let req = JsonRpcRequest {
jsonrpc: "2.0",
id,
method: method.to_string(),
params,
};
self.send_message(&serde_json::to_value(req).unwrap())?;
self.read_response(id, timeout)
}
fn send_notification<N: notification::Notification>(
&mut self,
params: N::Params,
) -> Result<(), String>
where
N::Params: Serialize,
{
let msg = serde_json::json!({
"jsonrpc": "2.0",
"method": N::METHOD,
"params": serde_json::to_value(params).map_err(|e| e.to_string())?
});
self.send_message(&msg)
}
fn send_message(&mut self, msg: &Value) -> Result<(), String> {
let body = serde_json::to_string(msg).map_err(|e| e.to_string())?;
let header = format!("Content-Length: {}\r\n\r\n", body.len());
self.stdin
.write_all(header.as_bytes())
.map_err(|e| format!("Write to LSP server: {e}"))?;
self.stdin
.write_all(body.as_bytes())
.map_err(|e| format!("Write to LSP server: {e}"))?;
self.stdin
.flush()
.map_err(|e| format!("Flush LSP server: {e}"))?;
Ok(())
}
fn read_response(&self, expected_id: i64, timeout: Duration) -> Result<Option<Value>, String> {
let deadline = Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(format!(
"LSP response timeout ({}s) for request id={expected_id}",
timeout.as_secs()
));
}
match self.response_rx.recv_timeout(remaining) {
Ok(Ok(msg)) => {
if msg.get("id").and_then(Value::as_i64) == Some(expected_id) {
let resp: JsonRpcResponse =
serde_json::from_value(msg).map_err(|e| e.to_string())?;
if let Some(err) = resp.error {
return Err(format!("LSP error: {}", err.message));
}
return Ok(resp.result);
}
}
Ok(Err(e)) => return Err(format!("LSP reader error: {e}")),
Err(mpsc::RecvTimeoutError::Timeout) => {
return Err(format!("LSP response timeout ({}s)", timeout.as_secs()));
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
return Err("LSP server connection lost".into());
}
}
}
}
pub fn shutdown(&mut self) {
let _ = self.request_raw_with_timeout(
"shutdown",
Value::Null,
Duration::from_secs(SHUTDOWN_TIMEOUT_SECS),
);
let _ = self.send_notification::<notification::Exit>(());
let _ = self.child.wait();
}
}
impl Drop for LspClient {
fn drop(&mut self) {
if self.initialized {
self.shutdown();
}
}
}