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
113 lines
3.4 KiB
Rust
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();
|
|
}
|
|
}
|
|
}
|