minni 0.1.1

Local memory, task, and codebase indexing tool for AI agents
Documentation
use tree_sitter::Language;

/// Supported tree-sitter language.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SupportedLanguage {
    Rust,
    Python,
    JavaScript,
    TypeScript,
    Go,
    C,
    Cpp,
    Java,
    CSharp,
}

impl SupportedLanguage {
    pub fn name(&self) -> &'static str {
        match self {
            Self::Rust => "rust",
            Self::Python => "python",
            Self::JavaScript => "javascript",
            Self::TypeScript => "typescript",
            Self::Go => "go",
            Self::C => "c",
            Self::Cpp => "cpp",
            Self::Java => "java",
            Self::CSharp => "csharp",
        }
    }

    pub fn tree_sitter_language(&self) -> Language {
        match self {
            Self::Rust => tree_sitter_rust::language(),
            Self::Python => tree_sitter_python::language(),
            Self::JavaScript => tree_sitter_javascript::language(),
            Self::TypeScript => tree_sitter_typescript::language_typescript(),
            Self::Go => tree_sitter_go::language(),
            Self::C => tree_sitter_c::language(),
            Self::Cpp => tree_sitter_cpp::language(),
            Self::Java => tree_sitter_java::language(),
            Self::CSharp => tree_sitter_c_sharp::language(),
        }
    }

    pub fn function_node_types(&self) -> &'static [&'static str] {
        match self {
            Self::Rust => &["function_item"],
            Self::Python => &["function_definition"],
            Self::JavaScript => &[
                "function_declaration",
                "method_definition",
                "arrow_function",
            ],
            Self::TypeScript => &[
                "function_declaration",
                "method_definition",
                "arrow_function",
            ],
            Self::Go => &["function_declaration", "method_declaration"],
            Self::C => &["function_definition"],
            Self::Cpp => &["function_definition"],
            Self::Java => &["method_declaration"],
            Self::CSharp => &[
                "method_declaration",
                "constructor_declaration",
                "property_declaration",
            ],
        }
    }

    pub fn class_node_types(&self) -> &'static [&'static str] {
        match self {
            Self::Rust => &["struct_item", "enum_item", "trait_item", "impl_item"],
            Self::Python => &["class_definition"],
            Self::JavaScript | Self::TypeScript => &["class_declaration"],
            Self::Go => &["type_declaration"],
            Self::C => &["struct_specifier"],
            Self::Cpp => &["class_specifier", "struct_specifier"],
            Self::Java => &["class_declaration", "interface_declaration"],
            Self::CSharp => &[
                "class_declaration",
                "interface_declaration",
                "struct_declaration",
                "record_declaration",
            ],
        }
    }

    pub fn name_field(&self) -> &'static str {
        match self {
            Self::Rust => "name",
            Self::Python => "name",
            Self::JavaScript | Self::TypeScript => "name",
            Self::Go => "name",
            Self::C | Self::Cpp => "declarator",
            Self::Java => "name",
            Self::CSharp => "name",
        }
    }
}

pub fn get_language(extension: &str) -> Option<SupportedLanguage> {
    match extension.to_lowercase().as_str() {
        "rs" => Some(SupportedLanguage::Rust),
        "py" | "pyi" => Some(SupportedLanguage::Python),
        "js" | "jsx" | "mjs" | "cjs" => Some(SupportedLanguage::JavaScript),
        "ts" | "tsx" => Some(SupportedLanguage::TypeScript),
        "go" => Some(SupportedLanguage::Go),
        "c" | "h" => Some(SupportedLanguage::C),
        "cpp" | "cc" | "cxx" | "hpp" | "hxx" => Some(SupportedLanguage::Cpp),
        "java" => Some(SupportedLanguage::Java),
        "cs" => Some(SupportedLanguage::CSharp),
        _ => None,
    }
}

/// Return a plain-text label for non-code files.
pub fn get_non_code_language(filename: &str, extension: &str) -> Option<&'static str> {
    // Check exact filenames first (case-insensitive)
    match filename.to_lowercase().as_str() {
        "dockerfile" => return Some("dockerfile"),
        "package.json" | "tsconfig.json" | "appsettings.json" => return Some("json"),
        ".env.example" => return Some("env"),
        _ => {}
    }

    // Check by extension (case-insensitive)
    match extension.to_lowercase().as_str() {
        "md" => Some("markdown"),
        "yml" | "yaml" => Some("yaml"),
        "toml" => Some("toml"),
        "dockerfile" => Some("dockerfile"),
        "sql" => Some("sql"),
        "sh" | "bash" => Some("shell"),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_non_code_language_extensions() {
        assert_eq!(get_non_code_language("README.md", "md"), Some("markdown"));
        assert_eq!(get_non_code_language("ci.yml", "yml"), Some("yaml"));
        assert_eq!(get_non_code_language("ci.yaml", "yaml"), Some("yaml"));
        assert_eq!(get_non_code_language("Cargo.toml", "toml"), Some("toml"));
        assert_eq!(get_non_code_language("schema.sql", "sql"), Some("sql"));
        assert_eq!(get_non_code_language("build.sh", "sh"), Some("shell"));
        assert_eq!(get_non_code_language("install.bash", "bash"), Some("shell"));
    }

    #[test]
    fn test_get_non_code_language_exact_filenames() {
        assert_eq!(get_non_code_language("Dockerfile", ""), Some("dockerfile"));
        assert_eq!(get_non_code_language("dockerfile", ""), Some("dockerfile"));
        assert_eq!(get_non_code_language("package.json", "json"), Some("json"));
        assert_eq!(get_non_code_language("tsconfig.json", "json"), Some("json"));
        assert_eq!(
            get_non_code_language("appsettings.json", "json"),
            Some("json")
        );
        assert_eq!(
            get_non_code_language(".env.example", "example"),
            Some("env")
        );
    }

    #[test]
    fn test_get_non_code_language_env_never_indexed() {
        // .env must never be indexed — secrets protection
        assert_eq!(get_non_code_language(".env", ""), None);
        assert_eq!(get_non_code_language(".env.local", "local"), None);
        assert_eq!(get_non_code_language(".env.production", "production"), None);
    }

    #[test]
    fn test_get_non_code_language_arbitrary_json_not_indexed() {
        // Only package.json, tsconfig.json, and appsettings.json are whitelisted
        assert_eq!(get_non_code_language("config.json", "json"), None);
        assert_eq!(get_non_code_language("data.json", "json"), None);
        // Environment-specific variants are NOT indexed (exact filename match only)
        assert_eq!(
            get_non_code_language("appsettings.Development.json", "json"),
            None
        );
        assert_eq!(
            get_non_code_language("appsettings.Production.json", "json"),
            None
        );
    }

    #[test]
    fn test_get_non_code_language_unknown_returns_none() {
        assert_eq!(get_non_code_language("image.png", "png"), None);
        assert_eq!(get_non_code_language("binary.bin", "bin"), None);
        assert_eq!(get_non_code_language("notes.txt", "txt"), None);
    }
}