rust-analyzer-cli 0.3.1

A library and CLI tool built on top of rust-analyzer for codebase navigation
Documentation
use reqwest::Client;
use rust_analyzer_cli::daemon::server::start_daemon_server;
use rust_analyzer_cli::lsp::types::*;
use std::net::TcpListener;
use std::path::PathBuf;
use tokio::time::{sleep, Duration};

fn get_free_port() -> u16 {
    let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind free port");
    let port = listener.local_addr().unwrap().port();
    drop(listener);
    port
}

#[tokio::test]
async fn test_daemon_full_workflow() {
    let port = get_free_port();
    let workspace = PathBuf::from(".");

    // Spawn daemon server in background
    let daemon_workspace = workspace.clone();
    tokio::spawn(async move {
        if let Err(e) = start_daemon_server(daemon_workspace, port).await {
            eprintln!("Daemon server error: {}", e);
        }
    });

    // Wait briefly for server to bind
    sleep(Duration::from_millis(500)).await;

    let client = Client::new();
    let base_url = format!("http://127.0.0.1:{}", port);

    // 1. Test GET /status
    let status_res = client
        .get(format!("{}/status", base_url))
        .send()
        .await
        .expect("Failed to send /status request");
    assert!(status_res.status().is_success());
    let status: DaemonStatusResponse = status_res.json().await.unwrap();
    let expected_root = workspace.canonicalize().unwrap().to_string_lossy().to_string();
    assert_eq!(
        status.workspace_root.trim_start_matches(r"\\?\"),
        expected_root.trim_start_matches(r"\\?\")
    );


    // 2. Test POST /api/symbol with retry until rust-analyzer indexing completes
    let mut symbol_found = false;
    for _ in 0..20 {
        let symbol_req = SymbolQueryRequest {
            name: "LspClient".to_string(),
            kind: "any".to_string(),
            exact: false,
        };
        let symbol_res = client
            .post(format!("{}/api/symbol", base_url))
            .json(&symbol_req)
            .send()
            .await
            .expect("Failed to send /api/symbol request");

        if symbol_res.status().is_success() {
            let symbols: Vec<SymbolItem> = symbol_res.json().await.unwrap();
            if symbols.iter().any(|s| s.name == "LspClient") {
                symbol_found = true;
                break;
            }
        }
        sleep(Duration::from_millis(500)).await;
    }
    assert!(symbol_found, "Expected at least one symbol for LspClient");


    // 3. Test POST /api/outline
    let outline_req = OutlineQueryRequest {
        file: PathBuf::from("src/main.rs"),
    };
    let outline_res = client
        .post(format!("{}/api/outline", base_url))
        .json(&outline_req)
        .send()
        .await
        .expect("Failed to send /api/outline request");
    assert!(outline_res.status().is_success());
    let outline: Vec<OutlineItem> = outline_res.json().await.unwrap();
    assert!(!outline.is_empty(), "Expected outline items for src/main.rs");
    assert!(outline.iter().any(|item| item.name == "main"));

    // 4. Test POST /api/definition
    let def_req = DefinitionQueryRequest {
        file: PathBuf::from("src/main.rs"),
        line: 13,
        col: 10,
    };
    let def_res = client
        .post(format!("{}/api/definition", base_url))
        .json(&def_req)
        .send()
        .await
        .expect("Failed to send /api/definition request");
    assert!(def_res.status().is_success());

    // 5. Test POST /api/check
    let check_req = CheckQueryRequest { target: None };
    let check_res = client
        .post(format!("{}/api/check", base_url))
        .json(&check_req)
        .send()
        .await
        .expect("Failed to send /api/check request");
    assert!(check_res.status().is_success());
    let check: CheckResponse = check_res.json().await.unwrap();
    assert!(check.success);
}