sview 0.1.3

Agent-friendly structure views of source and document files
Documentation
use crate::{
    model::Node,
    tree::{ast_preview, build_tree, collect_items, parse, FlatItem},
};
use tree_sitter::Node as AstNode;

pub(crate) fn analyze_swift(source: &str, preview_len: usize) -> Vec<Node> {
    let Some(tree) = parse(tree_sitter_swift::LANGUAGE.into(), source) else {
        return Vec::new();
    };

    let mut items = Vec::<FlatItem>::new();
    collect_items(tree.root_node(), None, &mut items, &|ast_node| {
        swift_node_from_ast(ast_node, source, preview_len)
    });
    build_tree(&items, None)
}

fn swift_node_from_ast(ast_node: AstNode, source: &str, preview_len: usize) -> Option<Node> {
    let (kind, name) = match ast_node.kind() {
        "import_declaration" => ("import", swift_import_name(ast_node, source)?),
        "class_declaration" => (
            swift_declaration_kind(ast_node, source),
            swift_ast_name(ast_node, source)?,
        ),
        "protocol_declaration" => ("protocol", swift_ast_name(ast_node, source)?),
        "function_declaration" | "protocol_function_declaration" => {
            ("function", swift_ast_name(ast_node, source)?)
        }
        "init_declaration" => ("initializer", "init".to_string()),
        "property_declaration" | "protocol_property_declaration" => {
            ("property", swift_property_name(ast_node, source)?)
        }
        _ => return None,
    };

    let (start_line, end_line) = line_range(ast_node, source);

    Some(Node {
        kind: kind.to_string(),
        level: None,
        name: Some(name),
        start_line,
        end_line,
        preview: ast_preview(ast_node, source, preview_len),
        children: Vec::new(),
    })
}

fn line_range(ast_node: AstNode, source: &str) -> (usize, usize) {
    let start = ast_node.start_position();
    let end = ast_node.end_position();
    let mut end_line = if end.column == 0 && end.row > start.row {
        end.row
    } else {
        end.row + 1
    };
    let start_line = start.row + 1;
    let lines = source.lines().collect::<Vec<_>>();
    while end_line > start_line
        && lines
            .get(end_line.saturating_sub(1))
            .is_some_and(|line| line.trim().is_empty())
    {
        end_line -= 1;
    }
    (start_line, end_line)
}

fn swift_declaration_kind(ast_node: AstNode, source: &str) -> &'static str {
    let preview = ast_preview(ast_node, source, 32).unwrap_or_default();
    match preview.split_whitespace().next() {
        Some("struct") => "struct",
        Some("enum") => "enum",
        Some("extension") => "extension",
        _ => "class",
    }
}

fn swift_ast_name(ast_node: AstNode, source: &str) -> Option<String> {
    ast_node
        .child_by_field_name("name")
        .and_then(|node| node.utf8_text(source.as_bytes()).ok())
        .map(ToString::to_string)
}

fn swift_import_name(ast_node: AstNode, source: &str) -> Option<String> {
    let mut cursor = ast_node.walk();
    let name = ast_node
        .named_children(&mut cursor)
        .find(|child| matches!(child.kind(), "identifier" | "simple_identifier"))
        .and_then(|node| node.utf8_text(source.as_bytes()).ok())
        .map(ToString::to_string);
    name
}

fn swift_property_name(ast_node: AstNode, source: &str) -> Option<String> {
    ast_node
        .child_by_field_name("name")
        .and_then(|node| swift_bound_identifier(node, source))
        .or_else(|| swift_bound_identifier(ast_node, source))
}

fn swift_bound_identifier(ast_node: AstNode, source: &str) -> Option<String> {
    if ast_node.kind() == "simple_identifier" {
        return ast_node
            .utf8_text(source.as_bytes())
            .ok()
            .map(ToString::to_string);
    }

    let mut cursor = ast_node.walk();
    let name = ast_node
        .named_children(&mut cursor)
        .find_map(|child| swift_bound_identifier(child, source));
    name
}