lazyspec 0.8.0

A little TUI & CLI for project documentation.
Documentation
use crate::engine::symbols::{RustSymbolExtractor, SymbolExtractor, TypeScriptSymbolExtractor};
use anyhow::Result;
use std::path::Path;
use std::process::Command;

use super::RefExpander;

impl RefExpander {
    pub(super) fn resolve_head_short_sha(&self) -> Option<String> {
        let output = Command::new("git")
            .args(["rev-parse", "--short", "HEAD"])
            .current_dir(&self.root)
            .output()
            .ok()?;
        if output.status.success() {
            Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        } else {
            None
        }
    }

    pub(super) fn resolve_ref(
        &self,
        path: &str,
        symbol: Option<&str>,
        sha: Option<&str>,
        head_sha: Option<&str>,
    ) -> Result<String> {
        let rev = sha.unwrap_or("HEAD");
        let output = Command::new("git")
            .args(["show", &format!("{}:{}", rev, path)])
            .current_dir(&self.root)
            .output()?;

        if !output.status.success() {
            let label = match symbol {
                Some(sym) => format!("{}#{}", path, sym),
                None => path.to_string(),
            };
            return Ok(format!("> [unresolved: {}]", label));
        }

        let file_content = String::from_utf8_lossy(&output.stdout);

        let content = if let Some(sym) = symbol {
            if sym.bytes().all(|b| b.is_ascii_digit()) {
                let line_num: usize = sym.parse().unwrap_or(0);
                let lines: Vec<&str> = file_content.lines().collect();
                if line_num == 0 || line_num > lines.len() {
                    return Ok(format!("> [unresolved: {}#{}]", path, sym));
                }
                lines[line_num - 1].to_string()
            } else {
                match self.extract_symbol(path, sym, &file_content) {
                    Some(content) => content,
                    None => return Ok(format!("> [unresolved: {}#{}]", path, sym)),
                }
            }
        } else {
            file_content.to_string()
        };

        let display_sha = sha.unwrap_or_else(|| head_sha.unwrap_or("HEAD"));

        let suffix = match symbol {
            Some(sym) if sym.bytes().all(|b| b.is_ascii_digit()) => format!(" (L{})", sym),
            Some(sym) => format!(" ({})", sym),
            None => String::new(),
        };

        let caption = format!("**{}** @ `{}`{}", path, display_sha, suffix);
        let lang = language_from_extension(path);

        let lines: Vec<&str> = content.lines().collect();
        let truncated = if lines.len() > self.max_lines {
            let remaining = lines.len() - self.max_lines;
            let comment = truncation_comment(lang, remaining);
            format!("{}\n{}", lines[..self.max_lines].join("\n"), comment)
        } else {
            content
        };

        Ok(format!("{}\n```{}\n{}\n```", caption, lang, truncated))
    }

    fn extract_symbol(&self, path: &str, symbol: &str, source: &str) -> Option<String> {
        let ext = Path::new(path).extension()?.to_str()?;
        match ext {
            "ts" | "tsx" => TypeScriptSymbolExtractor::new().extract(source, symbol),
            "rs" => RustSymbolExtractor::new().extract(source, symbol),
            _ => None,
        }
    }
}

fn truncation_comment(lang: &str, remaining: usize) -> String {
    match lang {
        "python" | "yaml" | "toml" => format!("# ... ({} more lines)", remaining),
        "markdown" => format!("<!-- ... ({} more lines) -->", remaining),
        _ => format!("// ... ({} more lines)", remaining),
    }
}

pub fn language_from_extension(path: &str) -> &'static str {
    let ext = Path::new(path)
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("");
    match ext {
        "ts" | "tsx" => "ts",
        "js" | "jsx" => "javascript",
        "rs" => "rust",
        "py" => "python",
        "go" => "go",
        "java" => "java",
        "c" | "h" => "c",
        "cpp" | "hpp" => "cpp",
        "md" => "markdown",
        "json" => "json",
        "yaml" | "yml" => "yaml",
        "toml" => "toml",
        _ => "",
    }
}