inkjet 0.11.1

A batteries-included syntax highlighting library for Rust, based on tree-sitter.
Documentation
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;

use anyhow::Result;
use fs_extra::dir::{self, CopyOptions};

use crate::{codegen, Config};
use crate::Language;

pub fn check(config: &Config) -> Result<()> {
    if std::env::var("INKJET_REDOWNLOAD_LANGS").is_ok() {
        download_langs(config)?;
    }

    if std::env::var("INKJET_REBUILD_LANGS_MODULE").is_ok() {
        generate_langs_module(&config.languages)?;
    }

    if std::env::var("INKJET_REBUILD_FEATURES").is_ok() {
        generate_features_list(&config.languages)?;
    }

    if std::env::var("INKJET_REBUILD_THEMES").is_ok() {
        generate_themes_module()?;
    }

    Ok(())
} 

pub fn download_langs(config: &Config) -> Result<()> {
    fs::remove_dir_all("languages")?;
    fs::create_dir_all("languages/temp/helix_queries")?;

    let languages = &config.languages;

    Command::new("git")
        .arg("clone")
        .arg("https://github.com/helix-editor/helix")
        .arg("languages/temp/helix_all")
        .spawn()?
        .wait()?;

    Command::new("git")
        .args(["reset", "--hard", &config.helix_sum])
        .current_dir(
            std::fs::canonicalize("./languages/temp/helix_all")?
        )
        .spawn()?
        .wait()?;

    dir::copy(
        "languages/temp/helix_all/runtime/queries/",
        "languages/temp/helix_queries",
        &CopyOptions::new().content_only(true)
    )?;

    let downloads: Vec<_> = languages
        .iter()
        .map(Language::download)
        .map(Result::unwrap)
        .zip(languages)
        .collect();

    for (mut child, lang) in downloads {
        child.wait()?;

        println!("Finished downloading {}.", lang.name);

        if let Some(hash) = &lang.hash {
            let repo_dir = format!("languages/temp/{}", lang.name);

            println!("Resetting {} onto {}...", lang.name, hash);

            Command::new("git")
                .current_dir(repo_dir)
                .args(["reset", "--hard", hash])
                .spawn()?
                .wait()?;
        }

        if let Some(command) = &lang.command {
            println!("Executing prep script for {}...", lang.name);

            Command::new("sh")
                .arg("-c")
                .arg(command)
                .spawn()?
                .wait()?;
        }

        fs::create_dir_all(format!("languages/{}/queries", lang.name))?;

        dir::copy(
            format!("languages/temp/{}/src", lang.name),
            format!("languages/{}", lang.name),
            &CopyOptions::new(),
        )?;

        let query_path = match &lang.helix_path {
            Some(path) => format!("languages/temp/helix_queries/{path}"),
            None => format!("languages/temp/helix_queries/{}", lang.name)
        };

        let query_path = match lang.helix_override {
            false => query_path,
            true => format!("languages/temp/{}/queries", lang.name)
        };

        dir::copy(
            query_path,
            format!("languages/{}/queries", lang.name),
            &CopyOptions::new().content_only(true),
        )?;

        // Remove unneeded queries and JSON data.
        // We discard the error because these files might not be there.
        let _ = fs::remove_file(format!("languages/{}/queries/textobjects.scm", lang.name));
        let _ = fs::remove_file(format!("languages/{}/queries/indents.scm", lang.name));
        let _ = fs::remove_file(format!("languages/{}/queries/folds.scm", lang.name));
        let _ = fs::remove_file(format!("languages/{}/src/grammar.json", lang.name));
        let _ = fs::remove_file(format!("languages/{}/src/node-types.json", lang.name));

        println!("Finished extracting {}.", lang.name);
    }

    fs::remove_dir_all("languages/temp")?;

    Ok(())
}

pub fn generate_langs_module(languages: &[Language]) -> Result<()> {
    let module_start = quote::quote! {
        //! This module is automatically generated by Inkjet.
        #![allow(dead_code)]
        #![allow(clippy::items_after_test_module)]

        use tree_sitter_highlight::HighlightConfiguration;
    };

    let modules = languages
        .iter()
        .map(codegen::language_module_def);

    let language_enum_def = codegen::languages_enum_def(languages);
    let language_impl_def = codegen::languages_impl_def(languages);

    let combined = quote::quote!{
        #module_start
        #(#modules)*
        #language_enum_def
        #language_impl_def
    };

    let combined = format!("{combined}");
    let combined = syn::parse_file(&combined).unwrap();
    let combined = prettyplease::unparse(&combined);

    let mut file = File::create("src/languages.rs")?;

    write!(&mut file, "{}", combined)?;

    Ok(())
}

pub fn generate_features_list(languages: &[Language]) -> Result<()> {
    let mut all_languages_buffer = String::new();
    let mut features_buffer = String::new();

    for lang in languages {
        all_languages_buffer += &format!("\"language-{}\",\n", lang.name.replace('_', "-"));
        features_buffer += &format!("language-{} = []\n", lang.name.replace('_', "-"));
    }

    let mut file = fs::File::create("features")?;

    write!(
        file,
        "
            all_languages = [
                {all_languages_buffer}
            ]

            {features_buffer}
        "
    )?;

    Ok(())
}

pub fn generate_themes_module() -> Result<()> {
    use std::ffi::OsStr;
    use std::path::Path;

    use proc_macro2::TokenStream;

    let mut themes = vec![];

    for entry in std::fs::read_dir("src/theme/vendored/data")? {
        let entry = entry?;
        let path  = entry.path();

        if path.extension() != Some( OsStr::new("toml") ) {
            continue;
        }

        let stem = path
            .file_stem()
            .expect("File should have a stem")
            .to_string_lossy()
            .to_string();

        themes.push(stem)
    }

    let mut file = File::create("src/theme/vendored/mod.rs")?;

    let include_path = |query| -> TokenStream {
        let path = format!("./data/{query}.toml");

        let query = format!("include_str!(\"{}\")", &path);

        query.parse().unwrap()
    };

    let module_start = quote::quote! {
        //! A collection of theme definitions vendored from the Helix editor project. View previews [here](https://github.com/helix-editor/helix/wiki/Themes).
        #![allow(dead_code)]
    };

    let consts = themes
        .iter()
        .map(|t| {
            let name = quote::format_ident!(
                "{}",
                t.replace('-', "_").to_uppercase()
            );

            let path = include_path(t);

            quote::quote! {
                pub const #name: &str = #path;
            }
        });

    let tests = themes
        .iter()
        .map(|t| {
            let name = quote::format_ident!(
                "{}",
                t.replace('-', "_").to_uppercase()
            );

            let path = include_path(t);

            quote::quote! {
                #[test]
                fn #name() {
                    let data = #path;

                    Theme::from_helix(data).unwrap();
                }
            }
        });

    let combined = quote::quote!{
        #module_start
        #(#consts)*

        #[cfg(test)]
        #[allow(non_snake_case)]
        mod tests {
            use crate::theme::*;

            #(#tests)*
        }
    };

    let combined = format!("{combined}");
    let combined = syn::parse_file(&combined).unwrap();
    let combined = prettyplease::unparse(&combined);

    write!(&mut file, "{}", combined)?;

    Ok(())
}