frigg 0.3.2

Local-first MCP server for code understanding.
Documentation
use std::path::Path;

use tree_sitter::Node;

use crate::indexer::SymbolKind;

use super::registry::{node_field_text, node_name_text};

pub(crate) fn is_roc_path(path: &Path) -> bool {
    path.extension()
        .and_then(|extension| extension.to_str())
        .is_some_and(|extension| extension.eq_ignore_ascii_case("roc"))
}

pub(super) fn symbol_from_node(source: &str, node: Node<'_>) -> Option<(SymbolKind, String)> {
    match node.kind() {
        "alias_type_def" | "nominal_type_def" | "opaque_type_def" => {
            roc_type_name(node, source).map(|name| (SymbolKind::TypeAlias, name))
        }
        "value_declaration" => {
            let name = value_declaration_name(node, source)?;
            let kind = roc_value_kind(node, source);
            Some((kind, name))
        }
        "var_declaration" => {
            node_field_text(node, source, "name").map(|name| (roc_value_kind(node, source), name))
        }
        _ => None,
    }
}

fn roc_type_name(node: Node<'_>, source: &str) -> Option<String> {
    let mut cursor = node.walk();
    node.children(&mut cursor)
        .filter(|child| child.is_named() && child.kind() == "apply_type")
        .find_map(|child| {
            let mut child_cursor = child.walk();
            child
                .children(&mut child_cursor)
                .filter(|grandchild| grandchild.is_named())
                .find(|grandchild| grandchild.kind() == "concrete_type")
                .and_then(|grandchild| grandchild.utf8_text(source.as_bytes()).ok())
                .map(str::trim)
                .filter(|text| !text.is_empty())
                .map(ToOwned::to_owned)
        })
}

fn roc_value_kind(node: Node<'_>, source: &str) -> SymbolKind {
    match node.child_by_field_name("body") {
        Some(body) => match body.utf8_text(source.as_bytes()).ok().map(str::trim_start) {
            Some(text) if text.starts_with('\\') || text.contains("->") => SymbolKind::Function,
            _ => SymbolKind::Const,
        },
        None => SymbolKind::Const,
    }
}

fn value_declaration_name(node: Node<'_>, source: &str) -> Option<String> {
    let mut cursor = node.walk();
    node.children(&mut cursor)
        .filter(|child| child.is_named())
        .find_map(|child| match child.kind() {
            "decl_left" => node_name_text(child, source).or_else(|| {
                let mut decl_cursor = child.walk();
                child
                    .children(&mut decl_cursor)
                    .filter(|grandchild| grandchild.is_named())
                    .find_map(|grandchild| {
                        node_name_text(grandchild, source).or_else(|| {
                            let mut pattern_cursor = grandchild.walk();
                            grandchild
                                .children(&mut pattern_cursor)
                                .filter(|pattern_child| pattern_child.is_named())
                                .find_map(|pattern_child| node_name_text(pattern_child, source))
                        })
                    })
            }),
            _ => None,
        })
}