giallo-kak 0.2.2

Kakoune syntax highlighter using TextMate grammars
use std::io;
use std::path::Path;

use giallo::Registry;

use crate::config::expand_path;

#[derive(Deserialize)]
struct GrammarMeta {
    name: String,
    #[serde(default, rename = "fileTypes")]
    file_types: Vec<String>,
}

fn load_grammar_meta(path: &Path) -> Option<GrammarMeta> {
    if path
        .extension()
        .map(|ext| ext.to_string_lossy().to_lowercase())
        .map_or(true, |ext| ext != "json")
    {
        return None;
    }

    let contents = std::fs::read_to_string(path).ok()?;
    serde_json::from_str(&contents).ok()
}

fn file_stem_alias(path: &Path) -> Option<String> {
    let stem = path.file_stem()?.to_string_lossy();
    let alias = stem.split('.').next()?.trim();
    if alias.is_empty() {
        None
    } else {
        Some(alias.to_lowercase())
    }
}

fn add_grammar_aliases(registry: &mut Registry, meta: &GrammarMeta, path: &Path) {
    let grammar_name = meta.name.trim();
    if grammar_name.is_empty() {
        return;
    }

    for file_type in &meta.file_types {
        let alias = file_type.trim();
        if !alias.is_empty() {
            registry.add_alias(grammar_name, alias);
        }
    }

    if let Some(alias) = file_stem_alias(path) {
        registry.add_alias(grammar_name, &alias);
    }
}

fn is_grammar_file(path: &Path) -> bool {
    path.extension()
        .map(|ext| ext.to_string_lossy().to_lowercase())
        .map_or(false, |ext| {
            matches!(ext.as_str(), "json" | "plist" | "tmlanguage")
        })
}

fn load_custom_grammars_in_dir(
    registry: &mut Registry,
    dir: &Path,
    loaded_count: &mut usize,
) -> io::Result<()> {
    let entries = std::fs::read_dir(dir)?;

    for entry in entries {
        let entry = entry?;
        let path = entry.path();

        if path.is_dir() {
            load_custom_grammars_in_dir(registry, &path, loaded_count)?;
            continue;
        }

        if !is_grammar_file(&path) {
            continue;
        }

        log::debug!("loading grammar from: {}", path.display());
        match registry.add_grammar_from_path(&path) {
            Ok(_) => {
                log::info!("loaded grammar: {}", path.display());
                *loaded_count += 1;
                if let Some(meta) = load_grammar_meta(&path) {
                    add_grammar_aliases(registry, &meta, &path);
                }
            }
            Err(err) => {
                log::error!("failed to load grammar {}: {}", path.display(), err);
            }
        }
    }

    Ok(())
}

pub fn load_custom_grammars(registry: &mut Registry, grammars_path: &str) -> io::Result<()> {
    let path = expand_path(grammars_path);
    let path_str = path.display().to_string();
    if !path.exists() {
        log::debug!("grammars path does not exist: {}", path_str);
        return Ok(());
    }

    let mut loaded_count = 0;
    load_custom_grammars_in_dir(registry, &path, &mut loaded_count)?;

    log::info!(
        "loaded {} custom grammars from {}",
        loaded_count,
        grammars_path
    );
    Ok(())
}

pub fn load_custom_themes(registry: &mut Registry, themes_path: &str) -> io::Result<()> {
    let path = expand_path(themes_path);
    let path_str = path.display().to_string();
    if !path.exists() {
        log::debug!("themes path does not exist: {}", path_str);
        return Ok(());
    }

    let mut loaded_count = 0;

    for entry in std::fs::read_dir(&path)? {
        let entry = entry?;
        let path = entry.path();

        if path
            .file_name()
            .and_then(|n| n.to_str())
            .map(|n| n.starts_with('.'))
            .unwrap_or(true)
        {
            continue;
        }

        if !path.is_file() {
            continue;
        }

        if path.extension().and_then(|e| e.to_str()) != Some("json") {
            continue;
        }

        match registry.add_theme_from_path(&path) {
            Ok(_) => {
                loaded_count += 1;
                log::debug!("loaded custom theme from {:?}", path);
            }
            Err(err) => {
                log::warn!("failed to load theme from {:?}: {}", path, err);
            }
        }
    }

    log::info!("loaded {} custom themes from {}", loaded_count, themes_path);
    Ok(())
}

use serde::Deserialize;