diff --git a/cli/src/config.rs b/cli/src/config.rs new file mode 100644 index 00000000..1c9cc8f6 --- /dev/null +++ b/cli/src/config.rs @@ -0,0 +1,69 @@ +use super::highlight::Theme; +use serde_derive::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::{env, fs, io}; + +#[derive(Default, Deserialize, Serialize)] +pub struct Config { + #[serde(skip)] + pub binary_directory: PathBuf, + + #[serde(default)] + #[serde(rename = "parser-directories")] + pub parser_directories: Vec, + + #[serde(default)] + pub theme: Theme, +} + +impl Config { + pub fn get_path(home_dir: &Path) -> PathBuf { + env::var("TREE_SITTER_DIR") + .map(|p| p.into()) + .unwrap_or_else(|_| home_dir.join(".tree-sitter")) + } + + pub fn load(home_dir: &Path) -> Self { + let tree_sitter_dir = Self::get_path(home_dir); + let config_path = tree_sitter_dir.join("config.json"); + let mut result = fs::read_to_string(&config_path) + .map_err(drop) + .and_then(|json| serde_json::from_str(&json).map_err(drop)) + .unwrap_or_else(|_| Self::default()); + result.init(home_dir, &tree_sitter_dir); + result + } + + pub fn save(&self, home_dir: &Path) -> io::Result<()> { + let tree_sitter_dir = Self::get_path(home_dir); + let config_path = tree_sitter_dir.join("config.json"); + let json = serde_json::to_string_pretty(self).expect("Failed to serialize config"); + fs::write(config_path, json) + } + + pub fn new(home_dir: &Path) -> Self { + let tree_sitter_dir = Self::get_path(home_dir); + let mut result = Self::default(); + result.init(home_dir, &tree_sitter_dir); + result + } + + fn init(&mut self, home_dir: &Path, tree_sitter_dir: &Path) { + if self.parser_directories.is_empty() { + self.parser_directories = vec![ + home_dir.join("github"), + home_dir.join("src"), + home_dir.join("source"), + ] + } + + let binary_path = tree_sitter_dir.join("bin"); + self.binary_directory = binary_path; + fs::create_dir_all(&self.binary_directory).unwrap_or_else(|error| { + panic!( + "Could not find or create parser binary directory {:?}. Error: {}", + self.binary_directory, error + ) + }); + } +} diff --git a/cli/src/highlight.rs b/cli/src/highlight.rs index 55ef4bc2..703c4053 100644 --- a/cli/src/highlight.rs +++ b/cli/src/highlight.rs @@ -2,7 +2,9 @@ use crate::error::Result; use crate::loader::Loader; use ansi_term::{Color, Style}; use lazy_static::lazy_static; -use serde_json::Value; +use serde::ser::SerializeMap; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::{json, Value}; use std::collections::HashMap; use std::{fmt, fs, io, path}; use tree_sitter::{Language, PropertySheet}; @@ -21,24 +23,7 @@ pub struct Theme { impl Theme { pub fn load(path: &path::Path) -> io::Result { let json = fs::read_to_string(path)?; - Ok(Self::new(&json)) - } - - pub fn new(json: &str) -> Self { - let mut ansi_styles = vec![None; 30]; - let mut css_styles = vec![None; 30]; - if let Ok(colors) = serde_json::from_str::>(json) { - for (scope, style_value) in colors { - let mut style = Style::default(); - parse_style(&mut style, style_value); - ansi_styles[scope as usize] = Some(style); - css_styles[scope as usize] = Some(style_to_css(style)); - } - } - Self { - ansi_styles, - css_styles, - } + Ok(serde_json::from_str(&json).unwrap_or_default()) } fn ansi_style(&self, scope: Scope) -> Option<&Style> { @@ -50,9 +35,85 @@ impl Theme { } } +impl<'de> Deserialize<'de> for Theme { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let scope_count = Scope::Unknown as usize + 1; + let mut ansi_styles = vec![None; scope_count]; + let mut css_styles = vec![None; scope_count]; + if let Ok(colors) = HashMap::::deserialize(deserializer) { + for (scope, style_value) in colors { + let mut style = Style::default(); + parse_style(&mut style, style_value); + ansi_styles[scope as usize] = Some(style); + css_styles[scope as usize] = Some(style_to_css(style)); + } + } + Ok(Self { + ansi_styles, + css_styles, + }) + } +} + +impl Serialize for Theme { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + let entry_count = self.ansi_styles.iter().filter(|i| i.is_some()).count(); + let mut map = serializer.serialize_map(Some(entry_count))?; + for (i, style) in self.ansi_styles.iter().enumerate() { + let scope = Scope::from_usize(i).unwrap(); + if scope == Scope::Unknown { + break; + } + if let Some(style) = style { + let color = style.foreground.map(|color| match color { + Color::Black => json!("black"), + Color::Blue => json!("blue"), + Color::Cyan => json!("cyan"), + Color::Green => json!("green"), + Color::Purple => json!("purple"), + Color::Red => json!("red"), + Color::White => json!("white"), + Color::Yellow => json!("yellow"), + Color::RGB(r, g, b) => json!(format!("#{:x?}{:x?}{:x?}", r, g, b)), + Color::Fixed(n) => json!(n), + }); + if style.is_bold || style.is_italic || style.is_underline { + let mut entry = HashMap::new(); + if let Some(color) = color { + entry.insert("color", color); + } + if style.is_bold { + entry.insert("bold", Value::Bool(true)); + } + if style.is_italic { + entry.insert("italic", Value::Bool(true)); + } + if style.is_underline { + entry.insert("underline", Value::Bool(true)); + } + map.serialize_entry(&scope, &entry)?; + } else if let Some(color) = color { + map.serialize_entry(&scope, &color)?; + } else { + map.serialize_entry(&scope, &Value::Null)?; + } + } else { + map.serialize_entry(&scope, &Value::Null)?; + } + } + map.end() + } +} + impl Default for Theme { fn default() -> Self { - Theme::new( + serde_json::from_str( r#" { "attribute": {"color": 124, "italic": true}, @@ -71,11 +132,14 @@ impl Default for Theme { "punctuation.delimiter": 239, "string.special": 30, "string": 28, - "tag": {"color": 18}, + "tag": 18, + "type": 23, + "type.builtin": {"color": 23, "bold": true}, "variable.builtin": {"bold": true} } "#, ) + .unwrap() } } @@ -102,9 +166,8 @@ fn parse_style(style: &mut Style, json: Value) { if let Value::Object(entries) = json { for (property_name, value) in entries { match property_name.as_str() { - "italic" => *style = style.italic(), "bold" => *style = style.bold(), - "dimmed" => *style = style.dimmed(), + "italic" => *style = style.italic(), "underline" => *style = style.underline(), "color" => { if let Some(color) = parse_color(value) { @@ -126,6 +189,7 @@ fn parse_color(json: Value) -> Option { _ => None, }, Value::String(s) => match s.to_lowercase().as_str() { + "black" => Some(Color::Black), "blue" => Some(Color::Blue), "cyan" => Some(Color::Cyan), "green" => Some(Color::Green), diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 0ece9cac..19b82194 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod error; pub mod generate; pub mod highlight; diff --git a/cli/src/main.rs b/cli/src/main.rs index 255f680b..147f7fad 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -4,7 +4,9 @@ use std::fs; use std::path::Path; use std::process::exit; use std::usize; -use tree_sitter_cli::{error, generate, highlight, loader, logger, parse, properties, test}; +use tree_sitter_cli::{ + config, error, generate, highlight, loader, logger, parse, properties, test, +}; fn main() { if let Err(e) = run() { @@ -24,6 +26,7 @@ fn run() -> error::Result<()> { .setting(AppSettings::SubcommandRequiredElseHelp) .author("Max Brunsfeld ") .about("Generates and tests parsers") + .subcommand(SubCommand::with_name("init-config").about("Generate a default config file")) .subcommand( SubCommand::with_name("generate") .about("Generate a parser") @@ -77,19 +80,15 @@ fn run() -> error::Result<()> { ) .get_matches(); - let home_dir = dirs::home_dir().unwrap(); + let home_dir = dirs::home_dir().expect("Failed to read home directory"); let current_dir = env::current_dir().unwrap(); - let config_dir = home_dir.join(".tree-sitter"); - let theme_path = config_dir.join("theme.json"); - let parsers_dir = config_dir.join("parsers"); + let config = config::Config::load(&home_dir); + let mut loader = loader::Loader::new(config.binary_directory.clone()); - // TODO - make configurable - let parser_repo_paths = vec![home_dir.join("github")]; - - fs::create_dir_all(&parsers_dir).unwrap(); - let mut loader = loader::Loader::new(config_dir); - - if let Some(matches) = matches.subcommand_matches("generate") { + if matches.subcommand_matches("init-config").is_some() { + let config = config::Config::new(&home_dir); + config.save(&home_dir)?; + } else if let Some(matches) = matches.subcommand_matches("generate") { if matches.is_present("log") { logger::init(); } @@ -127,7 +126,7 @@ fn run() -> error::Result<()> { let debug_graph = matches.is_present("debug-graph"); let quiet = matches.is_present("quiet"); let time = matches.is_present("time"); - loader.find_all_languages(&parser_repo_paths)?; + loader.find_all_languages(&config.parser_directories)?; let paths = matches .values_of("path") .unwrap() @@ -161,10 +160,9 @@ fn run() -> error::Result<()> { return Err(error::Error(String::new())); } } else if let Some(matches) = matches.subcommand_matches("highlight") { - loader.find_all_languages(&parser_repo_paths)?; - let theme = highlight::Theme::load(&theme_path).unwrap_or_default(); let paths = matches.values_of("path").unwrap().into_iter(); let html_mode = matches.is_present("html"); + loader.find_all_languages(&config.parser_directories)?; if html_mode { println!("{}", highlight::HTML_HEADER); @@ -182,7 +180,7 @@ fn run() -> error::Result<()> { for path in paths { let path = Path::new(path); - let (language, config) = match language_config { + let (language, language_config) = match language_config { Some(v) => v, None => match loader.language_configuration_for_file_name(path)? { Some(v) => v, @@ -193,12 +191,12 @@ fn run() -> error::Result<()> { }, }; - if let Some(sheet) = config.highlight_property_sheet(language)? { + if let Some(sheet) = language_config.highlight_property_sheet(language)? { let source = fs::read(path)?; if html_mode { - highlight::html(&loader, &theme, &source, language, sheet)?; + highlight::html(&loader, &config.theme, &source, language, sheet)?; } else { - highlight::ansi(&loader, &theme, &source, language, sheet)?; + highlight::ansi(&loader, &config.theme, &source, language, sheet)?; } } else { return Err(error::Error(format!( diff --git a/highlight/src/lib.rs b/highlight/src/lib.rs index 647064bb..e5499fbc 100644 --- a/highlight/src/lib.rs +++ b/highlight/src/lib.rs @@ -1,6 +1,6 @@ mod escape; -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_derive::*; use std::cmp; use std::fmt::Write; @@ -742,6 +742,43 @@ impl<'de> Deserialize<'de> for Scope { } } +impl Serialize for Scope { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Scope::Attribute => serializer.serialize_str("attribute"), + Scope::Comment => serializer.serialize_str("comment"), + Scope::Constant => serializer.serialize_str("constant"), + Scope::ConstantBuiltin => serializer.serialize_str("constant.builtin"), + Scope::Constructor => serializer.serialize_str("constructor"), + Scope::ConstructorBuiltin => serializer.serialize_str("constructor.builtin"), + Scope::Embedded => serializer.serialize_str("embedded"), + Scope::Escape => serializer.serialize_str("escape"), + Scope::Function => serializer.serialize_str("function"), + Scope::FunctionBuiltin => serializer.serialize_str("function.builtin"), + Scope::Keyword => serializer.serialize_str("keyword"), + Scope::Number => serializer.serialize_str("number"), + Scope::Operator => serializer.serialize_str("operator"), + Scope::Property => serializer.serialize_str("property"), + Scope::PropertyBuiltin => serializer.serialize_str("property.builtin"), + Scope::Punctuation => serializer.serialize_str("punctuation"), + Scope::PunctuationBracket => serializer.serialize_str("punctuation.bracket"), + Scope::PunctuationDelimiter => serializer.serialize_str("punctuation.delimiter"), + Scope::PunctuationSpecial => serializer.serialize_str("punctuation.special"), + Scope::String => serializer.serialize_str("string"), + Scope::StringSpecial => serializer.serialize_str("string.special"), + Scope::Type => serializer.serialize_str("type"), + Scope::TypeBuiltin => serializer.serialize_str("type.builtin"), + Scope::Variable => serializer.serialize_str("variable"), + Scope::VariableBuiltin => serializer.serialize_str("variable.builtin"), + Scope::Tag => serializer.serialize_str("tag"), + Scope::Unknown => serializer.serialize_str(""), + } + } +} + pub trait HTMLAttributeCallback<'a>: Fn(Scope) -> &'a str {} pub fn highlight<'a, F>(