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> {
if let Some(language_name) = self.special_files.get(filename) {
return self.get_language_by_name(language_name);
}
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");
}
}
}
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")?;
self.special_files = config.special_files;
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,
};
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![
("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"),
("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"),
("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"),
("Markdown", vec!["md"], vec!["# ", "## ", "```"], LanguageCategory::Documentation, "#083FA1"),
("Text", vec!["txt"], vec![], LanguageCategory::Documentation, "#000000"),
("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(),
};
for ext in &lang_info.extensions {
self.extension_map.insert(ext.clone(), name.to_string());
}
self.languages.insert(name.to_string(), lang_info);
}
}
}