use crate::cargo::run_cargo_check;
use crate::daemon::state::AppState;
use crate::lsp::types::*;
use axum::{
Json, Router,
extract::State,
http::StatusCode,
routing::{get, post},
};
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::info;
pub async fn start_daemon_server(workspace_root: PathBuf, port: u16) -> anyhow::Result<()> {
start_daemon_server_with_output(workspace_root, port, false).await
}
pub async fn start_daemon_server_with_output(
workspace_root: PathBuf,
port: u16,
json_output: bool,
) -> anyhow::Result<()> {
let state = Arc::new(AppState::new(workspace_root));
let app = Router::new()
.route("/status", get(handle_status))
.route("/refresh", post(handle_refresh))
.route("/api/symbol", post(handle_symbol))
.route("/api/outline", post(handle_outline))
.route("/api/definition", post(handle_definition))
.route("/api/body", post(handle_body))
.route("/api/hover", post(handle_hover))
.route("/api/cursor", post(handle_cursor))
.route("/api/type-hierarchy", post(handle_type_hierarchy))
.route("/api/check", post(handle_check))
.with_state(state.clone());
let addr = SocketAddr::from(([127, 0, 0, 1], port));
let listener = tokio::net::TcpListener::bind(addr).await?;
let actual_addr = listener.local_addr()?;
info!(
"rust-analyzer-cli daemon listening on http://{}",
actual_addr
);
if json_output {
println!(
"{}",
daemon_started_payload(actual_addr, &state.workspace_root)
);
} else {
println!(
"rust-analyzer-cli daemon started on http://{}\nWorkspace: {}\nKeep this process running while issuing query commands.",
actual_addr,
state.workspace_root.display()
);
}
axum::serve(listener, app).await?;
Ok(())
}
fn daemon_started_payload(address: SocketAddr, workspace_root: &Path) -> serde_json::Value {
serde_json::json!({
"success": true,
"message": "rust-analyzer-cli daemon started",
"address": format!("http://{}", address),
"workspace": workspace_root.to_string_lossy(),
})
}
async fn handle_status(
State(state): State<Arc<AppState>>,
) -> Result<Json<DaemonStatusResponse>, (StatusCode, String)> {
let client_opt = state.lsp_client.read().await;
let (ready, pid) = match client_opt.as_ref() {
Some(c) => (true, c.process_id),
None => (false, 0),
};
Ok(Json(DaemonStatusResponse {
ready,
indexed_at: Some(chrono_now_str()),
workspace_root: state.workspace_root.to_string_lossy().to_string(),
process_id: pid,
}))
}
async fn handle_refresh(
State(state): State<Arc<AppState>>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
state
.refresh_lsp()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(
serde_json::json!({ "success": true, "message": "LSP session refreshed" }),
))
}
async fn handle_symbol(
State(state): State<Arc<AppState>>,
Json(req): Json<SymbolQueryRequest>,
) -> Result<Json<Vec<SymbolItem>>, (StatusCode, String)> {
let client = state
.get_or_start_lsp()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let items = client
.query_symbol(&req.name, &req.kind, req.exact, false, 100)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(items))
}
async fn handle_outline(
State(state): State<Arc<AppState>>,
Json(req): Json<OutlineQueryRequest>,
) -> Result<Json<Vec<OutlineItem>>, (StatusCode, String)> {
let client = state
.get_or_start_lsp()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let items = client
.query_outline(&req.file, false, 100)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(items))
}
async fn handle_definition(
State(state): State<Arc<AppState>>,
Json(req): Json<DefinitionQueryRequest>,
) -> Result<Json<Vec<DefinitionItem>>, (StatusCode, String)> {
let client = state
.get_or_start_lsp()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let items = client
.query_definition(&req.file, req.line, req.col, false, 100)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(items))
}
async fn handle_body(
State(state): State<Arc<AppState>>,
Json(req): Json<BodyQueryRequest>,
) -> Result<Json<BodyItem>, (StatusCode, String)> {
let client = state
.get_or_start_lsp()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let item = client
.query_body(&req.file, req.line, req.col, req.max_lines)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(item))
}
async fn handle_hover(
State(state): State<Arc<AppState>>,
Json(req): Json<HoverQueryRequest>,
) -> Result<Json<Option<HoverItem>>, (StatusCode, String)> {
let client = state
.get_or_start_lsp()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let item = client
.query_hover(&req.file, req.line, req.col)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(item))
}
async fn handle_cursor(
State(state): State<Arc<AppState>>,
Json(req): Json<CursorQueryRequest>,
) -> Result<Json<Vec<CursorItem>>, (StatusCode, String)> {
let client = state
.get_or_start_lsp()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let items = client
.query_cursor(&req.file, req.line, req.col, &req.mode, req.depth)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(items))
}
async fn handle_type_hierarchy(
State(state): State<Arc<AppState>>,
Json(req): Json<TypeHierarchyQueryRequest>,
) -> Result<Json<Vec<TypeHierarchyItemResult>>, (StatusCode, String)> {
let client = state
.get_or_start_lsp()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let items = client
.query_type_hierarchy(&req.file, req.line, req.col, &req.mode, req.depth)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(items))
}
async fn handle_check(
State(state): State<Arc<AppState>>,
Json(req): Json<CheckQueryRequest>,
) -> Result<Json<CheckResponse>, (StatusCode, String)> {
let res = run_cargo_check(&state.workspace_root, req.target.as_deref())
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(res))
}
fn chrono_now_str() -> String {
let now = std::time::SystemTime::now();
format!("{:?}", now)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_daemon_started_payload_is_machine_readable() {
let payload = daemon_started_payload(
SocketAddr::from(([127, 0, 0, 1], 60094)),
Path::new("workspace"),
);
let encoded = serde_json::to_string(&payload).unwrap();
let decoded: serde_json::Value = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded["success"], true);
assert_eq!(decoded["address"], "http://127.0.0.1:60094");
}
}