call-graph-cli 0.3.0

Interactive call and type hierarchy TUI
Documentation
use std::{env, path::PathBuf, time::Duration};

use anyhow::{Context, Result, anyhow, bail};
use cgraph::{
    fetch::{
        HierarchyQuery,
        lsp::{LspConfig, LspProvider},
    },
    state::{HierarchyDirection, HierarchyKind, SourceLocation, SymbolIdentity},
};
use tower_lsp::lsp_types::Url;

#[tokio::main]
async fn main() -> Result<()> {
    let mut args = env::args_os().skip(1);
    let program = args.next().context(
        "usage: lsp_hierarchy <SERVER> <call|type> <incoming|outgoing> <SYMBOL> [WORKSPACE]",
    )?;
    let kind = match utf8_arg(args.next(), "hierarchy kind")?.as_str() {
        "call" => HierarchyKind::Call,
        "type" => HierarchyKind::Type,
        value => bail!("unknown hierarchy kind {value:?}; expected call or type"),
    };
    let direction = match utf8_arg(args.next(), "direction")?.as_str() {
        "incoming" => HierarchyDirection::Incoming,
        "outgoing" => HierarchyDirection::Outgoing,
        value => bail!("unknown direction {value:?}; expected incoming or outgoing"),
    };
    let symbol = utf8_arg(args.next(), "symbol")?;
    let workspace_root = match args.next() {
        Some(path) => PathBuf::from(path),
        None => env::current_dir().context("failed to determine current directory")?,
    };
    let location = match args.next() {
        Some(file) => {
            let file = PathBuf::from(file)
                .canonicalize()
                .context("failed to resolve source file")?;
            let line = utf8_arg(args.next(), "one-based line")?
                .parse::<u32>()
                .context("line must be a positive integer")?;
            let character = utf8_arg(args.next(), "one-based character")?
                .parse::<u32>()
                .context("character must be a positive integer")?;
            if line == 0 || character == 0 {
                bail!("line and character are one-based and must be positive");
            }
            let uri = Url::from_file_path(&file)
                .map_err(|()| anyhow!("source file cannot be represented as a file URI"))?;
            Some(SourceLocation {
                uri: uri.to_string(),
                line: Some(line - 1),
                character: Some(character - 1),
            })
        }
        None => None,
    };

    let lsp = LspProvider::start(LspConfig::for_server(program, workspace_root)).await?;
    tokio::time::sleep(Duration::from_secs(12)).await;
    let response = lsp
        .hierarchy_client()
        .query(HierarchyQuery {
            symbol: SymbolIdentity {
                symbol,
                kind,
                location,
            },
            direction,
        })
        .await;

    let shutdown_result = lsp.shutdown().await;
    let response = response?;
    for child in response.children {
        let location = child.location.map_or_else(
            || "unknown location".to_owned(),
            |location| {
                format!(
                    "{}:{}:{}",
                    location.uri,
                    location.line.unwrap_or(0) + 1,
                    location.character.unwrap_or(0) + 1
                )
            },
        );
        println!("{}\t{location}", child.symbol);
    }
    shutdown_result
}

fn utf8_arg(value: Option<std::ffi::OsString>, name: &str) -> Result<String> {
    value
        .with_context(|| format!("missing {name}"))?
        .into_string()
        .map_err(|_| anyhow!("{name} must be valid UTF-8"))
}