use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use color_eyre::Help;
use hex::FromHex;
use serde_yaml::Value;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Color, FontStyle, Style, StyleModifier, ThemeSet, ThemeSettings};
use syntect::parsing::{SyntaxDefinition, SyntaxSet};
use syntect::util::LinesWithEndings;
use crate::errors::LostTheWay;
use crate::utils;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct LanguageYML {
#[serde(default)]
extensions: Vec<String>,
#[serde(default)]
aliases: Vec<String>,
#[serde(default)]
color: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Language {
name: String,
extension: String,
pub(crate) color: Color,
}
impl Default for Language {
fn default() -> Self {
Self::new(String::from("text"), String::from(".txt"), None).unwrap()
}
}
impl Language {
fn new(name: String, extension: String, color: Option<String>) -> color_eyre::Result<Self> {
Ok(Self {
name,
extension,
color: Self::get_color(color)?,
})
}
fn get_color(color_string: Option<String>) -> color_eyre::Result<Color> {
let mut language_color = [0; 3];
if let Some(color) = color_string {
language_color = <[u8; 3]>::from_hex(&color.get(1..).unwrap_or("FFFFFF"))?;
}
Ok(Color {
r: language_color[0],
g: language_color[1],
b: language_color[2],
a: 0xFF,
})
}
pub(crate) fn get_extension(language_name: &str, languages: &HashMap<String, Self>) -> String {
let default = Self::default();
if let Some(l) = languages.get(language_name) {
l.extension.to_owned()
} else {
eprintln!(
"Couldn't find language {} in the list of extensions, defaulting to .txt",
language_name
);
default.extension
}
}
}
fn read_languages_from_yml(yml_string: &str) -> color_eyre::Result<HashMap<String, LanguageYML>> {
let language_strings: HashMap<String, Value> = serde_yaml::from_str(yml_string)?;
let mut languages = HashMap::with_capacity(language_strings.len());
for (key, value) in language_strings {
languages.insert(key, serde_yaml::from_value(value)?);
}
Ok(languages)
}
pub fn get_languages(yml_string: &str) -> color_eyre::Result<HashMap<String, Language>> {
let languages = read_languages_from_yml(yml_string)?;
let mut name_to_language = HashMap::new();
for (name, language_yml) in languages {
if let Some(extension) = language_yml.extensions.first() {
let mut language =
Language::new(name.to_owned(), extension.to_owned(), language_yml.color)?;
name_to_language.insert(name.to_ascii_lowercase(), language.clone());
name_to_language.insert(name, language.clone());
for alias in language_yml.aliases {
language.name = alias.clone();
name_to_language.insert(alias, language.clone());
}
}
}
Ok(name_to_language)
}
pub(crate) struct CodeHighlight {
syntax_set: SyntaxSet,
theme_set: ThemeSet,
theme_name: String,
syntect_dir: PathBuf,
pub(crate) main_style: Style,
pub(crate) accent_style: Style,
pub(crate) tag_style: Style,
pub(crate) selection_style: Style,
pub(crate) skim_theme: String,
}
fn syntect_theme_to_skim_theme(settings: &ThemeSettings) -> String {
let mut theme = String::new();
if let Some(c) = settings.foreground {
theme.push_str(&format!("fg:#{},", hex::encode(vec![c.r, c.g, c.b,])));
}
if let Some(c) = settings.selection {
theme.push_str(&format!(
"current_match_bg:#{},",
hex::encode(vec![c.r, c.g, c.b,])
));
}
if let Some(c) = settings.selection_foreground {
theme.push_str(&format!(
"current_match:#{},",
hex::encode(vec![c.r, c.g, c.b,])
));
}
if theme[theme.len() - 1..].starts_with(',') {
theme.pop();
}
theme
}
impl CodeHighlight {
pub(crate) fn new(theme: &str, syntect_dir: PathBuf) -> color_eyre::Result<Self> {
let mut theme_set = ThemeSet::load_defaults();
theme_set
.add_from_folder(&syntect_dir)
.map_err(|_| LostTheWay::ThemeError {
theme: String::from((&syntect_dir).to_str().unwrap()),
})
.suggestion(format!(
"Make sure {:#?} is a valid directory that has .tmTheme files",
&syntect_dir
))?;
let mut syntax_set = SyntaxSet::load_defaults_newlines().into_builder();
syntax_set
.add_from_folder(&syntect_dir, true)
.map_err(|_| LostTheWay::ThemeError {
theme: String::from((&syntect_dir).to_str().unwrap()),
})
.suggestion(format!(
"Make sure {:#?} is a valid directory that has .sublime-syntax files",
&syntect_dir
))?;
let syntax_set = syntax_set.build();
let mut highlighter = Self {
skim_theme: syntect_theme_to_skim_theme(&theme_set.themes[theme].settings),
syntax_set,
theme_name: theme.into(),
theme_set,
syntect_dir,
main_style: Style::default(),
accent_style: Style::default(),
tag_style: Style::default(),
selection_style: Style::default(),
};
highlighter.set_styles();
Ok(highlighter)
}
fn set_styles(&mut self) {
self.set_main_style();
self.set_accent_style();
self.set_tag_style();
self.set_selection_style();
}
fn set_main_style(&mut self) {
let main_color = self.theme_set.themes[&self.theme_name]
.settings
.foreground
.unwrap_or(Color::WHITE);
self.main_style = self.main_style.apply(StyleModifier {
foreground: Some(main_color),
background: None,
font_style: Some(FontStyle::BOLD),
});
}
fn set_tag_style(&mut self) {
let tag_color = self.theme_set.themes[&self.theme_name]
.settings
.tags_foreground
.unwrap_or_else(|| {
self.theme_set.themes[&self.theme_name]
.settings
.line_highlight
.unwrap_or(self.main_style.foreground)
});
self.tag_style = self.tag_style.apply(StyleModifier {
foreground: Some(tag_color),
background: self.theme_set.themes[&self.theme_name].settings.background,
font_style: Some(FontStyle::ITALIC),
});
}
fn set_accent_style(&mut self) {
let accent_color = self.theme_set.themes[&self.theme_name]
.settings
.caret
.unwrap_or(self.main_style.foreground);
self.accent_style = self.accent_style.apply(StyleModifier {
foreground: Some(accent_color),
background: None,
font_style: None,
});
}
fn set_selection_style(&mut self) {
self.selection_style = self.selection_style.apply(StyleModifier {
foreground: self.theme_set.themes[&self.theme_name]
.settings
.selection_foreground,
background: self.theme_set.themes[&self.theme_name].settings.selection,
font_style: None,
});
}
pub(crate) fn set_theme(&mut self, theme_name: String) -> color_eyre::Result<()> {
if self.theme_set.themes.contains_key(&theme_name) {
self.theme_name = theme_name;
self.set_styles();
Ok(())
} else {
let error: color_eyre::Result<()> =
Err(LostTheWay::ThemeError { theme: theme_name }.into());
error.suggestion(
"That theme doesn't exist. \
Use `the-way themes list` to see all theme possibilities \
or use `the-way themes add` to add a new one.",
)
}
}
pub(crate) fn get_themes(&self) -> Vec<String> {
self.theme_set.themes.keys().cloned().collect()
}
pub(crate) fn get_theme_name(&self) -> String {
self.theme_name.to_owned()
}
pub(crate) fn add_theme(&mut self, theme_file: &Path) -> color_eyre::Result<String> {
let theme = ThemeSet::get_theme(&theme_file)
.map_err(|_| LostTheWay::ThemeError {
theme: theme_file.to_str().unwrap().into(),
})
.suggestion(format!(
"Couldn't load a theme from {}, are you sure this is a valid .tmTheme file?",
theme_file.display()
))?;
let basename = theme_file
.file_stem()
.and_then(|x| x.to_str())
.ok_or(LostTheWay::ThemeError {
theme: theme_file.to_str().unwrap().into(),
})
.suggestion("Something's fishy with the filename, valid Unicode?")?;
let new_theme_file = self.syntect_dir.join(format!("{}.tmTheme", basename));
fs::copy(theme_file, new_theme_file)?;
self.theme_set.themes.insert(basename.to_owned(), theme);
Ok(basename.to_owned())
}
pub(crate) fn add_syntax(&mut self, syntax_file: &Path) -> color_eyre::Result<String> {
let syntax = SyntaxDefinition::load_from_str(
&fs::read_to_string(&syntax_file)?,
true,
None,
)
.map_err(|_| LostTheWay::SyntaxError {
syntax: syntax_file.to_str().unwrap().into(),
})
.suggestion(format!(
"Couldn't load a syntax from {}, are you sure this is a valid .sublime-syntax file with a \'name\' key?",
syntax_file.display()
))?;
let filename = syntax_file
.file_name()
.and_then(|x| x.to_str())
.ok_or(LostTheWay::SyntaxError {
syntax: syntax_file.to_str().unwrap().into(),
})
.suggestion("Something's fishy with the filename, valid Unicode?")?;
let new_syntax_file = self.syntect_dir.join(filename);
fs::copy(syntax_file, new_syntax_file)?;
Ok(syntax.name)
}
pub(crate) fn highlight_block(language_color: Color) -> (Style, String) {
(
Style::default().apply(StyleModifier {
foreground: Some(language_color),
background: None,
font_style: None,
}),
format!("{} ", utils::BOX),
)
}
pub(crate) fn highlight_code(&self, code: &str, extension: &str) -> Vec<(Style, String)> {
let mut colorized = Vec::new();
let extension = extension.split('.').nth(1).unwrap_or("txt");
let syntax = self.syntax_set.find_syntax_by_extension(extension);
let syntax = match syntax {
Some(syntax) => syntax,
None => self.syntax_set.find_syntax_by_extension("txt").unwrap(),
};
let mut h = HighlightLines::new(syntax, &self.theme_set.themes[&self.theme_name]);
for line in LinesWithEndings::from(code) {
colorized.extend(
h.highlight(line, &self.syntax_set)
.into_iter()
.map(|(style, s)| (style, s.to_string())),
);
}
colorized
}
}