use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use anyhow::{Context, bail};
use crossbeam_channel::Sender;
use serde_json::{Value, json};
use tokio::io::{AsyncRead, AsyncWrite, BufReader};
use tokio::process::Child;
use tokio::sync::mpsc;
use crate::codec;
use crate::config::ServerConfig;
use crate::event::{LspEvent, RpcError, ServerKey};
type PendingMap = Arc<Mutex<HashMap<i64, i64>>>;
pub struct Server {
pub key: ServerKey,
pub capabilities: Value,
stdin_tx: mpsc::UnboundedSender<Vec<u8>>,
next_request_id: i64,
pending: PendingMap,
}
impl Server {
pub async fn spawn(
key: ServerKey,
cmd: &ServerConfig,
evt_tx: Sender<LspEvent>,
) -> anyhow::Result<Self> {
let mut child = tokio::process::Command::new(&cmd.command)
.args(&cmd.args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.with_context(|| format!("failed to spawn LSP server {:?}", cmd.command))?;
let stdin = child.stdin.take().context("no stdin")?;
let stdout = child.stdout.take().context("no stdout")?;
let stderr = child.stderr.take().context("no stderr")?;
let (stdin_tx, stdin_rx) = mpsc::unbounded_channel::<Vec<u8>>();
tokio::spawn(stdin_task(stdin_rx, stdin));
tokio::spawn(stderr_task(stderr, key.language.clone()));
let pending: PendingMap = Arc::new(Mutex::new(HashMap::new()));
let capabilities =
initialize_handshake(&key, &stdin_tx, stdout, evt_tx.clone(), pending.clone()).await?;
spawn_wait_task(child, key.clone(), evt_tx);
Ok(Self {
key,
capabilities,
stdin_tx,
next_request_id: 1,
pending,
})
}
pub fn send_notification(&mut self, method: &str, params: Value) {
let msg = json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
});
self.enqueue(msg);
}
pub fn send_request(&mut self, app_id: i64, method: &str, params: Value) {
let id = self.next_request_id;
self.next_request_id += 1;
if let Ok(mut map) = self.pending.lock() {
map.insert(id, app_id);
}
let msg = json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
});
self.enqueue(msg);
}
pub async fn shutdown(mut self) {
self.send_request(-1, "shutdown", Value::Null);
tracing::debug!(key = ?self.key, "sent shutdown request");
self.send_notification("exit", Value::Null);
drop(self.stdin_tx);
}
fn enqueue(&self, msg: Value) {
match serde_json::to_vec(&msg) {
Ok(bytes) => {
let _ = self.stdin_tx.send(bytes);
}
Err(e) => {
tracing::warn!("failed to serialize JSON-RPC message: {e}");
}
}
}
}
async fn initialize_handshake(
key: &ServerKey,
stdin_tx: &mpsc::UnboundedSender<Vec<u8>>,
stdout: impl AsyncRead + Unpin + Send + 'static,
evt_tx: Sender<LspEvent>,
pending: PendingMap,
) -> anyhow::Result<Value> {
let root_uri = crate::uri::from_path(&key.root).map_err(|_| {
anyhow::anyhow!(
"cannot convert workspace root {:?} to file:// URI",
key.root
)
})?;
let init_msg = json!({
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"processId": std::process::id(),
"clientInfo": { "name": "hjkl", "version": env!("CARGO_PKG_VERSION") },
"rootUri": root_uri.as_str(),
"capabilities": {
"textDocument": {
"synchronization": {
"dynamicRegistration": false,
"willSave": false,
"willSaveWaitUntil": false,
"didSave": false,
}
},
"workspace": {}
},
},
});
let bytes = serde_json::to_vec(&init_msg)?;
stdin_tx.send(bytes).ok();
let mut reader = BufReader::with_capacity(256 * 1024, stdout);
let capabilities = loop {
let raw = codec::read_message(&mut reader)
.await?
.ok_or_else(|| anyhow::anyhow!("server closed stdout before initialize response"))?;
let val: Value = serde_json::from_slice(&raw)?;
if val.get("id").and_then(Value::as_i64) == Some(0) {
if let Some(err) = val.get("error") {
bail!("initialize error: {err}");
}
let caps = val
.get("result")
.and_then(|r| r.get("capabilities"))
.cloned()
.unwrap_or(Value::Null);
break caps;
}
tracing::debug!(
key = ?key,
"received server message before initialize response; ignoring"
);
};
let init_notif = json!({
"jsonrpc": "2.0",
"method": "initialized",
"params": {},
});
let bytes = serde_json::to_vec(&init_notif)?;
stdin_tx.send(bytes).ok();
tracing::info!(key = ?key, "LSP server initialized");
let _ = evt_tx.send(LspEvent::ServerInitialized {
key: key.clone(),
capabilities: capabilities.clone(),
});
let key_clone = key.clone();
tokio::spawn(stdout_task(reader, key_clone, evt_tx, pending));
Ok(capabilities)
}
pub async fn spawn_from_io<R, W>(
key: ServerKey,
stdin_writer: W,
stdout_reader: R,
evt_tx: Sender<LspEvent>,
) -> anyhow::Result<Server>
where
R: AsyncRead + Unpin + Send + 'static,
W: AsyncWrite + Unpin + Send + 'static,
{
let (stdin_tx, stdin_rx) = mpsc::unbounded_channel::<Vec<u8>>();
tokio::spawn(stdin_task(stdin_rx, stdin_writer));
let pending: PendingMap = Arc::new(Mutex::new(HashMap::new()));
let capabilities =
initialize_handshake(&key, &stdin_tx, stdout_reader, evt_tx, pending.clone()).await?;
Ok(Server {
key,
capabilities,
stdin_tx,
next_request_id: 1,
pending,
})
}
async fn stdin_task<W: AsyncWrite + Unpin>(mut rx: mpsc::UnboundedReceiver<Vec<u8>>, mut w: W) {
while let Some(bytes) = rx.recv().await {
if let Err(e) = codec::write_message(&mut w, &bytes).await {
tracing::debug!("LSP stdin write error: {e}");
break;
}
}
}
async fn stdout_task<R: AsyncRead + Unpin>(
mut reader: BufReader<R>,
key: ServerKey,
evt_tx: Sender<LspEvent>,
pending: PendingMap,
) {
loop {
let raw = match codec::read_message(&mut reader).await {
Ok(Some(r)) => r,
Ok(None) => {
tracing::debug!(key = ?key, "LSP stdout closed (clean EOF)");
break;
}
Err(e) => {
tracing::warn!(key = ?key, "LSP stdout read error: {e}");
break;
}
};
let val: Value = match serde_json::from_slice(&raw) {
Ok(v) => v,
Err(e) => {
tracing::warn!(key = ?key, "LSP: failed to parse JSON frame: {e}");
continue;
}
};
dispatch_message(&key, val, &evt_tx, &pending);
}
}
fn dispatch_message(key: &ServerKey, val: Value, evt_tx: &Sender<LspEvent>, pending: &PendingMap) {
let has_id = val.get("id").is_some();
let has_method = val.get("method").is_some();
if has_id && !has_method {
let jsonrpc_id = match val.get("id").and_then(Value::as_i64) {
Some(i) => i,
None => {
tracing::warn!(key = ?key, "LSP response with non-integer id; ignoring");
return;
}
};
let app_id = match pending.lock().ok().and_then(|mut m| m.remove(&jsonrpc_id)) {
Some(id) => id,
None => {
tracing::debug!(key = ?key, jsonrpc_id, "LSP response for unknown id; ignoring");
return;
}
};
let result = if let Some(err) = val.get("error") {
let code = err.get("code").and_then(Value::as_i64).unwrap_or(-1);
let message = err
.get("message")
.and_then(Value::as_str)
.unwrap_or("unknown error")
.to_string();
Err(RpcError { code, message })
} else {
Ok(val.get("result").cloned().unwrap_or(Value::Null))
};
let _ = evt_tx.send(LspEvent::Response {
request_id: app_id,
result,
});
} else if has_method {
if has_id {
let method = val
.get("method")
.and_then(Value::as_str)
.unwrap_or("<unknown>");
tracing::debug!(key = ?key, method, "LSP server-initiated request; ignoring in Phase 1");
} else {
let method = val
.get("method")
.and_then(Value::as_str)
.unwrap_or("<unknown>")
.to_string();
let params = val.get("params").cloned().unwrap_or(Value::Null);
tracing::debug!(key = ?key, method, "LSP notification received");
let _ = evt_tx.send(LspEvent::Notification {
key: key.clone(),
method,
params,
});
}
} else {
tracing::warn!(key = ?key, "LSP: unrecognized message shape; ignoring");
}
}
async fn stderr_task<R: tokio::io::AsyncRead + Unpin>(stderr: R, lang: String) {
use tokio::io::{AsyncBufReadExt, BufReader};
let mut reader = BufReader::new(stderr);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => break,
Ok(_) => {
let trimmed = line.trim_end();
if !trimmed.is_empty() {
tracing::warn!(lang, "LSP stderr: {trimmed}");
}
}
Err(e) => {
tracing::debug!(lang, "LSP stderr read error: {e}");
break;
}
}
}
}
pub fn spawn_wait_task(mut child: Child, key: ServerKey, evt_tx: Sender<LspEvent>) {
tokio::spawn(async move {
match child.wait().await {
Ok(status) => {
tracing::info!(key = ?key, ?status, "LSP server exited");
let _ = evt_tx.send(LspEvent::ServerExited { key, status });
}
Err(e) => {
tracing::warn!(key = ?key, "error waiting for LSP server: {e}");
}
}
});
}