use std::collections::HashSet;
use std::io::{BufRead, BufReader, Read, Write};
use std::path::Path;
use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Stdio};
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use anyhow::{Context, Result, anyhow, bail};
use serde_json::{Value, json};
use super::backend::Launcher;
use super::map::path_to_uri;
const STDERR_TAIL_LINES: usize = 40;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Readiness {
Ready,
TimedOut,
}
pub struct LspClient {
child: Child,
stdin: ChildStdin,
inbox: Receiver<Value>,
stderr_tail: Arc<Mutex<Vec<String>>>,
next_id: i64,
outstanding_progress: HashSet<String>,
quiescent: bool,
name: String,
root: String,
}
impl LspClient {
pub fn start(launcher: &Launcher, root: &Path) -> Result<Self> {
let mut command = launcher.command();
command
.current_dir(root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn().with_context(|| {
format!(
"starting language server `{}`.\n \
It was found at {}, but could not be executed. \
Check that it runs from a terminal and that any runtime it needs \
(Node.js for pyright, a JDK for jdtls/kotlin-language-server) is installed.",
launcher.name,
launcher.program.display()
)
})?;
let stdin = child
.stdin
.take()
.ok_or_else(|| anyhow!("language server `{}` gave no stdin", launcher.name))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow!("language server `{}` gave no stdout", launcher.name))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| anyhow!("language server `{}` gave no stderr", launcher.name))?;
let (tx, inbox) = channel();
thread::spawn(move || read_messages(stdout, tx));
let stderr_tail = Arc::new(Mutex::new(Vec::new()));
thread::spawn({
let tail = Arc::clone(&stderr_tail);
move || drain_stderr(stderr, tail)
});
Ok(Self {
child,
stdin,
inbox,
stderr_tail,
next_id: 1,
outstanding_progress: HashSet::new(),
quiescent: false,
name: launcher.name.clone(),
root: path_to_uri(root),
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn stderr_tail(&self) -> String {
self.stderr_tail
.lock()
.map(|t| t.join("\n"))
.unwrap_or_default()
}
pub fn initialize(&mut self, root: &Path, timeout: Duration) -> Result<()> {
let params = json!({
"processId": std::process::id(),
"clientInfo": { "name": "code-rcl", "version": env!("CARGO_PKG_VERSION") },
"rootUri": self.root,
"rootPath": root.to_string_lossy(),
"workspaceFolders": [{
"uri": self.root,
"name": root.file_name().map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "workspace".to_string()),
}],
"capabilities": {
"workspace": {
"workspaceFolders": true,
"configuration": true,
"didChangeConfiguration": { "dynamicRegistration": false },
},
"textDocument": {
"synchronization": { "dynamicRegistration": false, "didSave": false },
"definition": { "dynamicRegistration": false, "linkSupport": true },
},
"window": { "workDoneProgress": true },
"general": { "positionEncodings": ["utf-16"] },
"experimental": { "serverStatusNotification": true },
},
});
self.request("initialize", params, timeout)
.with_context(|| format!("handshaking with language server `{}`", self.name))?;
self.notify("initialized", json!({}))?;
Ok(())
}
pub fn wait_until_ready(&mut self, max: Duration) -> Readiness {
let start = Instant::now();
let grace = Duration::from_secs(3);
let settle = Duration::from_millis(400);
let mut saw_progress = false;
let mut idle_since: Option<Instant> = None;
while start.elapsed() < max {
if self.quiescent {
return Readiness::Ready;
}
if !self.outstanding_progress.is_empty() {
saw_progress = true;
idle_since = None;
} else if saw_progress {
match idle_since {
Some(t) if t.elapsed() >= settle => return Readiness::Ready,
Some(_) => {}
None => idle_since = Some(Instant::now()),
}
} else if start.elapsed() >= grace {
return Readiness::Ready;
}
let _ = self.pump(None, Instant::now() + Duration::from_millis(200));
}
Readiness::TimedOut
}
pub fn did_open(&mut self, uri: &str, language_id: &str, text: &str) -> Result<()> {
self.notify(
"textDocument/didOpen",
json!({
"textDocument": {
"uri": uri,
"languageId": language_id,
"version": 1,
"text": text,
}
}),
)
}
pub fn did_close(&mut self, uri: &str) -> Result<()> {
self.notify(
"textDocument/didClose",
json!({ "textDocument": { "uri": uri } }),
)
}
pub fn definition(
&mut self,
uri: &str,
line: u32,
character: u32,
timeout: Duration,
) -> Result<Value> {
self.request(
"textDocument/definition",
json!({
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
}),
timeout,
)
}
pub fn shutdown(mut self) {
let _ = self.request("shutdown", Value::Null, Duration::from_secs(5));
let _ = self.notify("exit", Value::Null);
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
match self.child.try_wait() {
Ok(Some(_)) => return,
Ok(None) => thread::sleep(Duration::from_millis(50)),
Err(_) => break,
}
}
let _ = self.child.kill();
let _ = self.child.wait();
}
fn request(&mut self, method: &str, params: Value, timeout: Duration) -> Result<Value> {
let id = self.next_id;
self.next_id += 1;
self.send(json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
}))?;
let deadline = Instant::now() + timeout;
match self.pump(Some(id), deadline)? {
Some(result) => Ok(result),
None => Err(anyhow!(
"language server `{}` did not answer `{method}` within {}s.\n \
The project may be larger than the timeout allows — raise it with \
`--precise-timeout <seconds>`.{}",
self.name,
timeout.as_secs(),
self.stderr_hint(),
)),
}
}
fn notify(&mut self, method: &str, params: Value) -> Result<()> {
self.send(json!({ "jsonrpc": "2.0", "method": method, "params": params }))
}
fn send(&mut self, message: Value) -> Result<()> {
let body = serde_json::to_vec(&message)?;
self.stdin
.write_all(format!("Content-Length: {}\r\n\r\n", body.len()).as_bytes())
.and_then(|_| self.stdin.write_all(&body))
.and_then(|_| self.stdin.flush())
.map_err(|e| {
anyhow!(
"lost the connection to language server `{}` while sending a request ({e}).\n \
It most likely exited early.{}",
self.name,
self.stderr_hint(),
)
})
}
fn pump(&mut self, want: Option<i64>, deadline: Instant) -> Result<Option<Value>> {
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Ok(None);
}
let message = match self.inbox.recv_timeout(remaining) {
Ok(m) => m,
Err(RecvTimeoutError::Timeout) => return Ok(None),
Err(RecvTimeoutError::Disconnected) => {
bail!(
"language server `{}` exited while code-rcl was talking to it.{}",
self.name,
self.stderr_hint(),
);
}
};
let method = message.get("method").and_then(Value::as_str);
let id = message.get("id");
match (method, id) {
(Some(method), Some(id)) => {
let reply = self.server_request_result(method, &message);
self.send(json!({ "jsonrpc": "2.0", "id": id, "result": reply }))?;
}
(Some(method), None) => {
self.handle_notification(method, message.get("params"));
}
(None, Some(id)) => {
if id.as_i64() != want {
continue; }
if let Some(error) = message.get("error") {
let text = error
.get("message")
.and_then(Value::as_str)
.unwrap_or("unknown error");
bail!("language server `{}` returned an error: {text}", self.name);
}
return Ok(Some(message.get("result").cloned().unwrap_or(Value::Null)));
}
(None, None) => {}
}
}
}
fn server_request_result(&self, method: &str, message: &Value) -> Value {
match method {
"workspace/configuration" => {
let n = message
.get("params")
.and_then(|p| p.get("items"))
.and_then(Value::as_array)
.map_or(0, Vec::len);
Value::Array(vec![json!({}); n])
}
"workspace/workspaceFolders" => json!([{ "uri": self.root, "name": "workspace" }]),
_ => Value::Null,
}
}
fn handle_notification(&mut self, method: &str, params: Option<&Value>) {
match method {
"$/progress" => {
let Some(params) = params else { return };
let Some(token) = params.get("token") else {
return;
};
let token = token.to_string();
match params
.get("value")
.and_then(|v| v.get("kind"))
.and_then(Value::as_str)
{
Some("begin") => {
self.outstanding_progress.insert(token);
}
Some("end") => {
self.outstanding_progress.remove(&token);
}
_ => {}
}
}
"experimental/serverStatus" => {
if let Some(true) = params
.and_then(|p| p.get("quiescent"))
.and_then(Value::as_bool)
{
self.quiescent = true;
}
}
_ => {}
}
}
fn stderr_hint(&self) -> String {
let tail = self.stderr_tail();
if tail.trim().is_empty() {
String::new()
} else {
format!("\n Last output from the server:\n{}", indent(&tail))
}
}
}
fn indent(text: &str) -> String {
text.lines()
.map(|l| format!(" {l}"))
.collect::<Vec<_>>()
.join("\n")
}
fn read_messages(stdout: ChildStdout, tx: Sender<Value>) {
let mut reader = BufReader::new(stdout);
loop {
let mut length: Option<usize> = None;
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => return, Ok(_) => {}
}
let line = line.trim_end();
if line.is_empty() {
break; }
if let Some(value) = line.strip_prefix("Content-Length:") {
length = value.trim().parse().ok();
}
}
let Some(length) = length else { return };
let mut body = vec![0u8; length];
if reader.read_exact(&mut body).is_err() {
return;
}
match serde_json::from_slice(&body) {
Err(_) => continue,
Ok(value) => {
if tx.send(value).is_err() {
return; }
}
}
}
}
fn drain_stderr(stderr: ChildStderr, tail: Arc<Mutex<Vec<String>>>) {
let reader = BufReader::new(stderr);
for line in reader.lines().map_while(std::result::Result::ok) {
if let Ok(mut tail) = tail.lock() {
if tail.len() == STDERR_TAIL_LINES {
tail.remove(0);
}
tail.push(line);
}
}
}