Files
zesdex/apps/infrastructure/src/lsp/client.rs
T
asepharyana da2ed6da25 feat(tui): add usage overlay and sidebar for displaying usage statistics and tasks
feat(tui): implement status bar with connection and turn state indicators
feat(tui): create workflow panel for agent status and progress visualization
feat(web): introduce web frontend interface with static file serving
feat(ws): add WebSocket interface for real-time communication and session management
2026-07-20 09:04:57 +07:00

113 lines
3.4 KiB
Rust

//! LSP client — sends JSON-RPC requests to language servers.
use anyhow::Result;
use serde_json::Value;
use std::io::{BufRead, BufReader, Read, Write};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::Mutex;
use tracing::{debug, info};
/// Mutable inner state of an LSP client, protected by a mutex so that
/// `send_request` and `shutdown` can be called via `&self` (required by
/// [`LspManager`](super::manager::LspManager)).
struct LspClientInner {
process: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
request_id: u64,
}
/// A minimal but functional LSP client.
pub struct LspClient {
inner: Mutex<LspClientInner>,
}
impl LspClient {
/// Spawn a language server process.
pub fn start(command: &str, args: &[String]) -> Result<Self> {
let mut child = Command::new(command)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdin = child.stdin.take().unwrap();
let stdout = BufReader::new(child.stdout.take().unwrap());
info!("LSP client spawned: {command}");
Ok(LspClient {
inner: Mutex::new(LspClientInner {
process: child,
stdin,
stdout,
request_id: 0,
}),
})
}
/// Send a JSON-RPC request and read the response.
pub fn send_request(&self, method: &str, params: &Value) -> Result<Value> {
let mut inner = self.inner.lock().unwrap();
inner.request_id += 1;
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": inner.request_id,
"method": method,
"params": params.clone(),
});
// Write Content-Length header + body
let body = serde_json::to_string(&request)?;
let header = format!("Content-Length: {}\r\n\r\n", body.len());
inner.stdin.write_all(header.as_bytes())?;
inner.stdin.write_all(body.as_bytes())?;
inner.stdin.flush()?;
debug!("LSP request: {method} (id={})", inner.request_id);
// Read Content-Length header
let mut content_length = 0usize;
loop {
let mut line = String::new();
inner.stdout.read_line(&mut line)?;
let trimmed = line.trim();
if trimmed.is_empty() {
break; // end of headers
}
if let Some(len_str) = trimmed.strip_prefix("Content-Length: ") {
content_length = len_str.parse::<usize>()?;
}
}
// Read the JSON body
let mut buf = vec![0u8; content_length];
inner.stdout.read_exact(&mut buf)?;
let response: Value = serde_json::from_slice(&buf)?;
debug!("LSP response for {method}: response received");
Ok(response)
}
/// Gracefully shut down the server.
pub fn shutdown(&self) -> Result<()> {
let null = Value::Null;
let _ = self.send_request("shutdown", &null);
let _ = self.send_request("exit", &null);
if let Ok(mut inner) = self.inner.lock() {
let _ = inner.process.wait();
}
info!("LSP client shut down");
Ok(())
}
}
impl Drop for LspClient {
fn drop(&mut self) {
if let Ok(mut inner) = self.inner.lock() {
let _ = inner.process.kill();
let _ = inner.process.wait();
}
}
}