sherlock-io 0.1.0

SherlockIO - Lightning-fast language detection CLI tool
Documentation
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use anyhow::{Context, Result};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanguageInfo {
    pub name: String,
    pub extensions: Vec<String>,
    pub patterns: Vec<String>,
    pub category: LanguageCategory,
    pub color: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LanguageCategory {
    Programming,
    Markup,
    Stylesheet,
    Data,
    Configuration,
    Documentation,
    Other,
}

#[derive(Debug, Deserialize)]
struct LanguageConfig {
    languages: HashMap<String, LanguageConfigEntry>,
    special_files: HashMap<String, String>,
}

#[derive(Debug, Deserialize)]
struct LanguageConfigEntry {
    extensions: Vec<String>,
    patterns: Vec<String>,
    category: LanguageCategory,
    color: String,
}

pub struct LanguageRegistry {
    languages: HashMap<String, LanguageInfo>,
    extension_map: HashMap<String, String>,
    special_files: HashMap<String, String>,
}

impl LanguageRegistry {
    pub fn new() -> Result<Self> {
        let mut registry = Self {
            languages: HashMap::new(),
            extension_map: HashMap::new(),
            special_files: HashMap::new(),
        };
        registry.load_languages_from_file()?;
        Ok(registry)
    }

    pub fn from_default() -> Self {
        let mut registry = Self {
            languages: HashMap::new(),
            extension_map: HashMap::new(),
            special_files: HashMap::new(),
        };
        registry.initialize_default_languages();
        registry
    }

    pub fn get_language_by_extension(&self, extension: &str) -> Option<&LanguageInfo> {
        self.extension_map
            .get(extension)
            .and_then(|lang_name| self.languages.get(lang_name))
    }

    pub fn get_language_by_name(&self, name: &str) -> Option<&LanguageInfo> {
        self.languages.get(name)
    }

    pub fn detect_by_content(&self, content: &str, filename: &str) -> Option<&LanguageInfo> {
        // Check special files first
        if let Some(language_name) = self.special_files.get(filename) {
            return self.get_language_by_name(language_name);
        }

        // Shebang detection
        if let Some(first_line) = content.lines().next() {
            if first_line.starts_with("#!") {
                if first_line.contains("python") {
                    return self.get_language_by_name("Python");
                }
                if first_line.contains("node") || first_line.contains("javascript") {
                    return self.get_language_by_name("JavaScript");
                }
                if first_line.contains("bash") || first_line.contains("sh") {
                    return self.get_language_by_name("Shell");
                }
            }
        }

        // Pattern-based detection
        for (_, lang) in &self.languages {
            for pattern in &lang.patterns {
                if content.contains(pattern) {
                    return Some(lang);
                }
            }
        }

        None
    }

    fn load_languages_from_file(&mut self) -> Result<()> {
        let yaml_content = fs::read_to_string("languages.yaml")
            .context("Failed to read languages.yaml file")?;
        
        let config: LanguageConfig = serde_yaml::from_str(&yaml_content)
            .context("Failed to parse languages.yaml")?;

        // Load special files mapping
        self.special_files = config.special_files;

        // Convert and load languages
        for (name, entry) in config.languages {
            let lang_info = LanguageInfo {
                name: name.clone(),
                extensions: entry.extensions.clone(),
                patterns: entry.patterns,
                category: entry.category,
                color: entry.color,
            };

            // Map extensions to language name
            for ext in &entry.extensions {
                self.extension_map.insert(ext.clone(), name.clone());
            }

            self.languages.insert(name, lang_info);
        }

        Ok(())
    }

    fn initialize_default_languages(&mut self) {
        let languages = vec![
            // Programming Languages
            ("Rust", vec!["rs"], vec!["fn main(", "use std::", "impl"], LanguageCategory::Programming, "#CE422B"),
            ("Python", vec!["py", "pyw"], vec!["def ", "import ", "from "], LanguageCategory::Programming, "#3776AB"),
            ("JavaScript", vec!["js", "mjs"], vec!["function", "const ", "let "], LanguageCategory::Programming, "#F7DF1E"),
            ("TypeScript", vec!["ts"], vec!["interface ", "type ", "export "], LanguageCategory::Programming, "#3178C6"),
            ("Go", vec!["go"], vec!["package ", "func ", "import "], LanguageCategory::Programming, "#00ADD8"),
            ("Java", vec!["java"], vec!["public class", "import java", "public static"], LanguageCategory::Programming, "#ED8B00"),
            ("C", vec!["c", "h"], vec!["#include", "int main", "printf"], LanguageCategory::Programming, "#A8B9CC"),
            ("C++", vec!["cpp", "cc", "cxx", "hpp"], vec!["#include", "std::", "namespace"], LanguageCategory::Programming, "#00599C"),
            ("C#", vec!["cs"], vec!["using System", "namespace", "public class"], LanguageCategory::Programming, "#239120"),
            ("PHP", vec!["php"], vec!["<?php", "function", "$"], LanguageCategory::Programming, "#777BB4"),
            ("Ruby", vec!["rb"], vec!["def ", "class ", "require"], LanguageCategory::Programming, "#CC342D"),
            ("Swift", vec!["swift"], vec!["import ", "func ", "var "], LanguageCategory::Programming, "#FA7343"),
            ("Kotlin", vec!["kt"], vec!["fun ", "class ", "import "], LanguageCategory::Programming, "#7F52FF"),
            ("Scala", vec!["scala"], vec!["object ", "class ", "def "], LanguageCategory::Programming, "#DC322F"),
            ("Shell", vec!["sh", "bash", "zsh"], vec!["#!/bin/", "echo ", "if ["], LanguageCategory::Programming, "#89E051"),
            
            // Web Technologies
            ("HTML", vec!["html", "htm"], vec!["<!DOCTYPE", "<html", "<body"], LanguageCategory::Markup, "#E34F26"),
            ("CSS", vec!["css"], vec!["{", "}", "color:"], LanguageCategory::Stylesheet, "#1572B6"),
            ("SCSS", vec!["scss"], vec!["$", "@import", "@mixin"], LanguageCategory::Stylesheet, "#CF649A"),
            ("Vue", vec!["vue"], vec!["<template>", "<script>", "export default"], LanguageCategory::Programming, "#4FC08D"),
            ("React JSX", vec!["jsx"], vec!["import React", "export default", "className="], LanguageCategory::Programming, "#61DAFB"),
            ("React TSX", vec!["tsx"], vec!["import React", "export default", "interface"], LanguageCategory::Programming, "#61DAFB"),
            
            // Data & Config
            ("JSON", vec!["json"], vec!["{", "}", "\""], LanguageCategory::Data, "#000000"),
            ("YAML", vec!["yml", "yaml"], vec!["---", ":", "- "], LanguageCategory::Data, "#CB171E"),
            ("TOML", vec!["toml"], vec!["[", "]", "="], LanguageCategory::Configuration, "#9C4221"),
            ("XML", vec!["xml"], vec!["<?xml", "<", "/>"], LanguageCategory::Data, "#0060AC"),
            ("SQL", vec!["sql"], vec!["SELECT", "FROM", "WHERE"], LanguageCategory::Data, "#4479A1"),
            
            // Documentation
            ("Markdown", vec!["md"], vec!["# ", "## ", "```"], LanguageCategory::Documentation, "#083FA1"),
            ("Text", vec!["txt"], vec![], LanguageCategory::Documentation, "#000000"),
            
            // Other
            ("Dockerfile", vec!["dockerfile"], vec!["FROM", "RUN", "COPY"], LanguageCategory::Configuration, "#384D54"),
            ("Makefile", vec!["makefile", "mk"], vec![":", "\t", ".PHONY"], LanguageCategory::Configuration, "#427819"),
        ];

        for (name, extensions, patterns, category, color) in languages {
            let lang_info = LanguageInfo {
                name: name.to_string(),
                extensions: extensions.iter().map(|s| s.to_string()).collect(),
                patterns: patterns.iter().map(|s| s.to_string()).collect(),
                category,
                color: color.to_string(),
            };

            // Map extensions to language name
            for ext in &lang_info.extensions {
                self.extension_map.insert(ext.clone(), name.to_string());
            }

            self.languages.insert(name.to_string(), lang_info);
        }
    }
}