sview 0.1.3

Agent-friendly structure views of source and document files
Documentation
use crate::model::{Language, Node, RenderOptions, StructureView};

pub fn render_text(view: &StructureView, options: &RenderOptions) -> String {
    let mut out = format!("{} ({})\n", view.path, language_label(view.language));
    let mut remaining = options.max_nodes;

    for (index, node) in view.nodes.iter().enumerate() {
        render_node(
            node,
            "",
            index + 1 == view.nodes.len(),
            0,
            options,
            &mut remaining,
            &mut out,
        );
        if remaining == 0 {
            out.push_str("...\n");
            break;
        }
    }

    out
}

fn language_label(language: Language) -> &'static str {
    match language {
        Language::C => "c",
        Language::Cpp => "cpp",
        Language::Java => "java",
        Language::JavaScript => "javascript",
        Language::Kotlin => "kotlin",
        Language::Markdown => "markdown",
        Language::ObjectiveC => "objective_c",
        Language::Rust => "rust",
        Language::Swift => "swift",
        Language::TypeScript | Language::Tsx => "typescript",
        Language::Unknown => "unknown",
    }
}

fn render_node(
    node: &Node,
    prefix: &str,
    is_last: bool,
    depth: usize,
    options: &RenderOptions,
    remaining: &mut usize,
    out: &mut String,
) {
    if *remaining == 0 || options.max_depth.is_some_and(|max_depth| depth > max_depth) {
        return;
    }

    *remaining -= 1;
    let name = node.name.as_deref().unwrap_or("<anonymous>");
    let connector = if is_last { "└─" } else { "├─" };
    out.push_str(&format!(
        "{prefix}{connector} {} {name} L{}-{}",
        node.kind, node.start_line, node.end_line
    ));
    if let Some(preview) = text_preview(node) {
        out.push_str(&format!("{preview}"));
    }
    out.push('\n');

    let child_prefix = format!("{prefix}{}", if is_last { "   " } else { "" });
    for (index, child) in node.children.iter().enumerate() {
        render_node(
            child,
            &child_prefix,
            index + 1 == node.children.len(),
            depth + 1,
            options,
            remaining,
            out,
        );
        if *remaining == 0 {
            break;
        }
    }
}

fn text_preview(node: &Node) -> Option<&str> {
    let preview = node.preview.as_deref()?.trim();
    let name = node.name.as_deref().unwrap_or_default();
    if preview.is_empty()
        || preview == name
        || preview
            .trim_start_matches('#')
            .trim()
            .trim_end_matches('#')
            .trim()
            == name
    {
        None
    } else {
        Some(preview)
    }
}