Add ~/.tree-sitter/config.json file, init-config command

Right now this is just used for two things:
* Specifying folders for locarting parsers to use with `tree-sitter 
parse` and `tree-sitter highlight`
* Specifying colors to use for `tree-sitter-highlight`
This commit is contained in:
Max Brunsfeld 2019-02-25 12:33:24 -08:00
parent 858b4ba8ac
commit 1bad6dc41e
5 changed files with 212 additions and 43 deletions

69
cli/src/config.rs Normal file
View file

@ -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<PathBuf>,
#[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
)
});
}
}

View file

@ -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<Self> {
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::<HashMap<Scope, Value>>(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<D>(deserializer: D) -> std::result::Result<Theme, D::Error>
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::<Scope, Value>::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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
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<Color> {
_ => 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),

View file

@ -1,3 +1,4 @@
pub mod config;
pub mod error;
pub mod generate;
pub mod highlight;

View file

@ -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 <maxbrunsfeld@gmail.com>")
.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!(

View file

@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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>(