rust-analyzer-cli 0.7.0

A library and CLI tool built on top of rust-analyzer for codebase navigation
Documentation
use crate::daemon::state::AppState;
use crate::lsp::types::*;
use axum::{
    Json, Router,
    extract::{State, rejection::JsonRejection},
    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/references", post(handle_references))
        .route("/api/calls", post(handle_calls))
        .route("/api/relations", post(handle_relations))
        .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()
        );
    }

    tokio::select! {
        result = axum::serve(listener, app) => {
            result?;
        }
        _ = tokio::signal::ctrl_c() => {
            if let Some(client) = state.lsp_client.read().await.clone() {
                client.shutdown().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 error = state.last_error.read().await.clone();
    let (state_value, pid) = match client_opt.as_ref() {
        Some(c) => (DaemonState::Ready, Some(c.process_id)),
        None if error.is_some() => (DaemonState::Failed, None),
        None => (DaemonState::Starting, None),
    };

    Ok(Json(DaemonStatusResponse {
        state: state_value,
        workspace_root: state.workspace_root.to_string_lossy().to_string(),
        process_id: pid,
        error,
    }))
}

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,
            req.include_body,
            req.max_lines,
        )
        .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, req.include_body, req.max_lines)
        .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,
            req.include_body,
            req.max_lines,
        )
        .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_references(
    State(state): State<Arc<AppState>>,
    request: Result<Json<ReferenceQueryRequest>, JsonRejection>,
) -> Result<Json<Vec<ReferenceItem>>, (StatusCode, String)> {
    let Json(req) = request.map_err(|error| invalid_json_request("references", error))?;
    let client = state
        .get_or_start_lsp()
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    let items = client
        .query_references(&req.file, req.line, req.col)
        .await
        .map_err(query_error)?;

    Ok(Json(items))
}

async fn handle_calls(
    State(state): State<Arc<AppState>>,
    request: Result<Json<CallQueryRequest>, JsonRejection>,
) -> Result<Json<Vec<CallItem>>, (StatusCode, String)> {
    let Json(req) = request.map_err(|error| invalid_json_request("calls", error))?;
    let client = state
        .get_or_start_lsp()
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    let items = client
        .query_calls(&req.file, req.line, req.col, req.direction, req.depth)
        .await
        .map_err(query_error)?;

    Ok(Json(items))
}

async fn handle_relations(
    State(state): State<Arc<AppState>>,
    request: Result<Json<RelationQueryRequest>, JsonRejection>,
) -> Result<Json<Vec<RelationItem>>, (StatusCode, String)> {
    let Json(req) = request.map_err(|error| invalid_json_request("relations", error))?;
    let client = state
        .get_or_start_lsp()
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    let items = client
        .query_relations(&req.file, req.line, req.col, req.mode)
        .await
        .map_err(query_error)?;

    Ok(Json(items))
}

fn invalid_json_request(operation: &str, error: JsonRejection) -> (StatusCode, String) {
    (
        StatusCode::BAD_REQUEST,
        format!("Invalid {operation} request: {error}"),
    )
}

fn query_error(error: anyhow::Error) -> (StatusCode, String) {
    let message = error.to_string();
    if message.contains("-32601") {
        return (
            StatusCode::NOT_IMPLEMENTED,
            format!("rust-analyzer does not support this navigation capability: {message}"),
        );
    }
    if message.starts_with("Invalid ")
        || message.contains("must be at least")
        || message.contains("outside workspace")
        || message.contains("Failed to resolve Rust source file")
    {
        return (StatusCode::BAD_REQUEST, message);
    }
    (StatusCode::INTERNAL_SERVER_ERROR, message)
}

#[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");
    }
}