use crate::lsp::types::*;
use anyhow::{Context, Result, anyhow};
use lsp_types::*;
use serde_json::{Value, json};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::sync::{Mutex, mpsc, oneshot};
use url::Url;
pub struct LspClient {
tx_request: mpsc::Sender<Value>,
pending_requests: Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value>>>>>,
workspace_root: PathBuf,
pub process_id: u32,
}
impl LspClient {
pub async fn start(workspace_root: &Path) -> Result<Self> {
let canonical_root = workspace_root
.canonicalize()
.unwrap_or_else(|_| workspace_root.to_path_buf());
let mut child = tokio::process::Command::new("rust-analyzer")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.context("Failed to spawn rust-analyzer. Please make sure `rust-analyzer` is installed and in PATH.")?;
let pid = child.id().unwrap_or(0);
let stdin = child
.stdin
.take()
.ok_or_else(|| anyhow!("Failed to open rust-analyzer stdin"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow!("Failed to open rust-analyzer stdout"))?;
let (tx_request, mut rx_request) = mpsc::channel::<Value>(100);
let pending_requests: Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value>>>>> =
Arc::new(Mutex::new(HashMap::new()));
let mut writer_stdin = stdin;
tokio::spawn(async move {
while let Some(req) = rx_request.recv().await {
let body = serde_json::to_string(&req).unwrap_or_default();
let header = format!("Content-Length: {}\r\n\r\n", body.len());
let _ = writer_stdin.write_all(header.as_bytes()).await;
let _ = writer_stdin.write_all(body.as_bytes()).await;
let _ = writer_stdin.flush().await;
}
});
let pending_map = pending_requests.clone();
tokio::spawn(async move {
let mut reader = BufReader::new(stdout);
loop {
let mut line = String::new();
match reader.read_line(&mut line).await {
Ok(0) => break, Ok(_) => {
let line_trimmed = line.trim();
if line_trimmed.starts_with("Content-Length:") {
let len_str = line_trimmed.trim_start_matches("Content-Length:").trim();
if let Ok(len) = len_str.parse::<usize>() {
let mut blank = String::new();
let _ = reader.read_line(&mut blank).await;
let mut buf = vec![0u8; len];
if reader.read_exact(&mut buf).await.is_ok()
&& let Ok(v) = serde_json::from_slice::<Value>(&buf)
&& let Some(id) = v.get("id").and_then(|i| i.as_i64())
{
let mut map = pending_map.lock().await;
if let Some(sender) = map.remove(&id) {
if let Some(err) = v.get("error") {
let _ = sender.send(Err(anyhow!("LSP error: {}", err)));
} else {
let result =
v.get("result").cloned().unwrap_or(Value::Null);
let _ = sender.send(Ok(result));
}
}
}
}
}
}
Err(_) => break,
}
}
});
let client = Self {
tx_request,
pending_requests,
workspace_root: canonical_root,
process_id: pid,
};
client.initialize().await?;
Ok(client)
}
async fn send_request(&self, method: &str, params: Value) -> Result<Value> {
static REQ_ID: AtomicI64 = AtomicI64::new(1);
let id = REQ_ID.fetch_add(1, Ordering::SeqCst);
let req = json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params
});
let (tx, rx) = oneshot::channel();
{
let mut map = self.pending_requests.lock().await;
map.insert(id, tx);
}
self.tx_request
.send(req)
.await
.map_err(|_| anyhow!("LSP writer task channel closed"))?;
rx.await
.map_err(|_| anyhow!("LSP response channel dropped"))?
}
async fn send_notification(&self, method: &str, params: Value) -> Result<()> {
let req = json!({
"jsonrpc": "2.0",
"method": method,
"params": params
});
let _ = self.tx_request.send(req).await;
Ok(())
}
async fn initialize(&self) -> Result<()> {
let root_uri = Url::from_directory_path(&self.workspace_root)
.map_err(|_| anyhow!("Failed to convert workspace path to URI"))?;
let init_params = json!({
"processId": self.process_id,
"rootUri": root_uri.as_str(),
"capabilities": {
"textDocument": {
"documentSymbol": { "hierarchicalDocumentSymbolSupport": true },
"definition": { "dynamicRegistration": false },
"references": { "dynamicRegistration": false },
"callHierarchy": { "dynamicRegistration": false },
"typeHierarchy": { "dynamicRegistration": false }
},
"workspace": {
"symbol": { "dynamicRegistration": false }
}
}
});
let _res = self.send_request("initialize", init_params).await?;
self.send_notification("initialized", json!({})).await?;
Ok(())
}
pub async fn query_symbol(
&self,
name: &str,
kind_filter: &str,
exact: bool,
) -> Result<Vec<SymbolItem>> {
let params = json!({ "query": name });
let res = self.send_request("workspace/symbol", params).await?;
let symbols: Vec<SymbolInformation> =
match serde_json::from_value::<WorkspaceSymbolResponse>(res.clone()) {
Ok(WorkspaceSymbolResponse::Flat(syms)) => syms,
Ok(WorkspaceSymbolResponse::Nested(syms)) => syms
.into_iter()
.map(|s| SymbolInformation {
name: s.name,
kind: s.kind,
tags: s.tags,
#[allow(deprecated)]
deprecated: None,
location: match s.location {
lsp_types::OneOf::Left(loc) => loc,
lsp_types::OneOf::Right(w_loc) => Location {
uri: w_loc.uri,
range: Range::default(),
},
},
container_name: s.container_name,
})
.collect(),
Err(_) => serde_json::from_value(res).unwrap_or_default(),
};
let mut result = Vec::new();
for sym in symbols {
if exact && sym.name != name {
continue;
}
let kind_str = format!("{:?}", sym.kind).to_lowercase();
if kind_filter != "any" && !kind_str.contains(&kind_filter.to_lowercase()) {
continue;
}
let file_path = uri_to_file_path(&sym.location.uri);
let relative_file = file_path
.strip_prefix(&self.workspace_root)
.unwrap_or(&file_path)
.to_string_lossy()
.to_string();
result.push(SymbolItem {
name: sym.name,
kind: kind_str,
file: relative_file,
line: sym.location.range.start.line + 1,
col: sym.location.range.start.character + 1,
container_name: sym.container_name,
});
}
Ok(result)
}
async fn ensure_file_open(&self, canonical_file: &Path) -> Result<Url> {
let uri = Url::from_file_path(canonical_file)
.map_err(|_| anyhow!("Failed to convert file path to URI"))?;
if let Ok(text) = tokio::fs::read_to_string(canonical_file).await {
let _ = self
.send_notification(
"textDocument/didOpen",
json!({
"textDocument": {
"uri": uri.as_str(),
"languageId": "rust",
"version": 1,
"text": text
}
}),
)
.await;
}
Ok(uri)
}
pub async fn query_outline(&self, file: &Path) -> Result<Vec<OutlineItem>> {
let canonical_file = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
let uri = self.ensure_file_open(&canonical_file).await?;
let params = json!({
"textDocument": { "uri": uri.as_str() }
});
let res = self
.send_request("textDocument/documentSymbol", params)
.await?;
fn convert_symbols(symbols: Vec<DocumentSymbol>) -> Vec<OutlineItem> {
symbols
.into_iter()
.map(|s| OutlineItem {
name: s.name,
kind: format!("{:?}", s.kind).to_lowercase(),
detail: s.detail,
line: s.range.start.line + 1,
col: s.range.start.character + 1,
end_line: s.range.end.line + 1,
children: convert_symbols(s.children.unwrap_or_default()),
})
.collect()
}
let items = match serde_json::from_value::<DocumentSymbolResponse>(res.clone()) {
Ok(DocumentSymbolResponse::Nested(symbols)) => convert_symbols(symbols),
Ok(DocumentSymbolResponse::Flat(syms)) => syms
.into_iter()
.map(|s| OutlineItem {
name: s.name,
kind: format!("{:?}", s.kind).to_lowercase(),
detail: s.container_name,
line: s.location.range.start.line + 1,
col: s.location.range.start.character + 1,
end_line: s.location.range.end.line + 1,
children: vec![],
})
.collect(),
Err(_) => {
if let Ok(syms) = serde_json::from_value::<Vec<DocumentSymbol>>(res) {
convert_symbols(syms)
} else {
vec![]
}
}
};
Ok(items)
}
pub async fn query_definition(
&self,
file: &Path,
line: u32,
col: u32,
) -> Result<Vec<DefinitionItem>> {
let canonical_file = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
let uri = self.ensure_file_open(&canonical_file).await?;
let params = json!({
"textDocument": { "uri": uri.as_str() },
"position": { "line": line - 1, "character": col - 1 }
});
let res = self.send_request("textDocument/definition", params).await?;
let locations: Vec<Location> = match serde_json::from_value::<GotoDefinitionResponse>(res) {
Ok(GotoDefinitionResponse::Scalar(loc)) => vec![loc],
Ok(GotoDefinitionResponse::Array(locs)) => locs,
Ok(GotoDefinitionResponse::Link(links)) => links
.into_iter()
.map(|l| Location {
uri: l.target_uri,
range: l.target_selection_range,
})
.collect(),
Err(_) => vec![],
};
let mut items = Vec::new();
for loc in locations {
let loc_file = uri_to_file_path(&loc.uri);
let rel_file = loc_file
.strip_prefix(&self.workspace_root)
.unwrap_or(&loc_file)
.to_string_lossy()
.to_string();
items.push(DefinitionItem {
file: rel_file,
line: loc.range.start.line + 1,
col: loc.range.start.character + 1,
end_line: loc.range.end.line + 1,
end_col: loc.range.end.character + 1,
snippet: None,
});
}
Ok(items)
}
pub async fn query_cursor(
&self,
file: &Path,
line: u32,
col: u32,
mode: &str,
_depth: u32,
) -> Result<Vec<CursorItem>> {
let canonical_file = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
let uri = self.ensure_file_open(&canonical_file).await?;
if mode == "references" {
let params = json!({
"textDocument": { "uri": uri.as_str() },
"position": { "line": line - 1, "character": col - 1 },
"context": { "includeDeclaration": true }
});
let res = self.send_request("textDocument/references", params).await?;
let locs: Vec<Location> = serde_json::from_value(res).unwrap_or_default();
let mut items = Vec::new();
for loc in locs {
let loc_file = uri_to_file_path(&loc.uri);
let rel_file = loc_file
.strip_prefix(&self.workspace_root)
.unwrap_or(&loc_file)
.to_string_lossy()
.to_string();
items.push(CursorItem {
name: "reference".to_string(),
kind: "reference".to_string(),
file: rel_file,
line: loc.range.start.line + 1,
col: loc.range.start.character + 1,
caller_or_callee: None,
});
}
return Ok(items);
}
let prep_params = json!({
"textDocument": { "uri": uri.as_str() },
"position": { "line": line - 1, "character": col - 1 }
});
let prep_res = self
.send_request("textDocument/prepareCallHierarchy", prep_params)
.await?;
let items: Vec<CallHierarchyItem> = serde_json::from_value(prep_res).unwrap_or_default();
let mut results = Vec::new();
if let Some(item) = items.first() {
let item_json = serde_json::to_value(item)?;
if mode == "outgoing" {
let out_res = self
.send_request("callHierarchy/outgoingCalls", json!({ "item": item_json }))
.await?;
let out_calls: Vec<CallHierarchyOutgoingCall> =
serde_json::from_value(out_res).unwrap_or_default();
for call in out_calls {
let loc_file = uri_to_file_path(&call.to.uri);
let rel_file = loc_file
.strip_prefix(&self.workspace_root)
.unwrap_or(&loc_file)
.to_string_lossy()
.to_string();
results.push(CursorItem {
name: call.to.name,
kind: format!("{:?}", call.to.kind).to_lowercase(),
file: rel_file,
line: call.to.range.start.line + 1,
col: call.to.range.start.character + 1,
caller_or_callee: Some("callee".to_string()),
});
}
} else {
let in_res = self
.send_request("callHierarchy/incomingCalls", json!({ "item": item_json }))
.await?;
let in_calls: Vec<CallHierarchyIncomingCall> =
serde_json::from_value(in_res).unwrap_or_default();
for call in in_calls {
let loc_file = uri_to_file_path(&call.from.uri);
let rel_file = loc_file
.strip_prefix(&self.workspace_root)
.unwrap_or(&loc_file)
.to_string_lossy()
.to_string();
results.push(CursorItem {
name: call.from.name,
kind: format!("{:?}", call.from.kind).to_lowercase(),
file: rel_file,
line: call.from.range.start.line + 1,
col: call.from.range.start.character + 1,
caller_or_callee: Some("caller".to_string()),
});
}
}
}
Ok(results)
}
pub async fn query_type_hierarchy(
&self,
file: &Path,
line: u32,
col: u32,
mode: &str,
_depth: u32,
) -> Result<Vec<TypeHierarchyItemResult>> {
let canonical_file = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
let uri = self.ensure_file_open(&canonical_file).await?;
let prep_params = json!({
"textDocument": { "uri": uri.as_str() },
"position": { "line": line - 1, "character": col - 1 }
});
let prep_res = self
.send_request("textDocument/prepareTypeHierarchy", prep_params)
.await?;
let items: Vec<lsp_types::TypeHierarchyItem> =
serde_json::from_value(prep_res).unwrap_or_default();
let mut results = Vec::new();
if let Some(item) = items.first() {
let item_json = serde_json::to_value(item)?;
let endpoint = if mode == "subtypes" {
"typeHierarchy/subtypes"
} else {
"typeHierarchy/supertypes"
};
let type_res = self
.send_request(endpoint, json!({ "item": item_json }))
.await?;
let type_items: Vec<lsp_types::TypeHierarchyItem> =
serde_json::from_value(type_res).unwrap_or_default();
for ti in type_items {
let file_path = uri_to_file_path(&ti.uri);
let rel_file = file_path
.strip_prefix(&self.workspace_root)
.unwrap_or(&file_path)
.to_string_lossy()
.to_string();
results.push(TypeHierarchyItemResult {
name: ti.name,
kind: format!("{:?}", ti.kind).to_lowercase(),
file: rel_file,
line: ti.range.start.line + 1,
col: ti.range.start.character + 1,
detail: ti.detail,
});
}
}
Ok(results)
}
}
fn uri_to_file_path(uri: &lsp_types::Uri) -> PathBuf {
Url::parse(uri.as_str())
.ok()
.and_then(|u| u.to_file_path().ok())
.unwrap_or_default()
}