Skip to main content

the_way/
language.rs

1//! Language specific code like highlighting and extensions
2use std::collections::HashMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use color_eyre::Help;
7use hex::FromHex;
8use serde_yaml::Value;
9use syntect::easy::HighlightLines;
10use syntect::highlighting::{Color, FontStyle, Style, StyleModifier, ThemeSet, ThemeSettings};
11use syntect::parsing::{SyntaxDefinition, SyntaxSet};
12use syntect::util::LinesWithEndings;
13
14use crate::errors::LostTheWay;
15use crate::utils;
16
17/// Relevant information from languages.yml file
18#[derive(Debug, PartialEq, Serialize, Deserialize)]
19struct LanguageYML {
20    #[serde(default)]
21    extensions: Vec<String>,
22    #[serde(default)]
23    aliases: Vec<String>,
24    #[serde(default)]
25    color: Option<String>,
26}
27
28#[derive(Debug, Clone)]
29pub struct Language {
30    name: String,
31    extension: String,
32    pub(crate) color: Color,
33}
34
35impl Default for Language {
36    fn default() -> Self {
37        Self::new(String::from("text"), String::from(".txt"), None).unwrap()
38    }
39}
40
41impl Language {
42    fn new(name: String, extension: String, color: Option<String>) -> color_eyre::Result<Self> {
43        Ok(Self {
44            name,
45            extension,
46            color: Self::get_color(color)?,
47        })
48    }
49
50    fn get_color(color_string: Option<String>) -> color_eyre::Result<Color> {
51        let mut language_color = [0; 3];
52        if let Some(color) = color_string {
53            language_color = <[u8; 3]>::from_hex(color.get(1..).unwrap_or("FFFFFF"))?;
54        }
55        Ok(Color {
56            r: language_color[0],
57            g: language_color[1],
58            b: language_color[2],
59            a: 0xFF,
60        })
61    }
62
63    /// Finds the appropriate file extension for a language
64    pub(crate) fn get_extension(language_name: &str, languages: &HashMap<String, Self>) -> String {
65        let default = Self::default();
66        if let Some(l) = languages.get(language_name) {
67            l.extension.clone()
68        } else {
69            eprintln!(
70                "Couldn't find language {} in the list of extensions, defaulting to .txt",
71                language_name
72            );
73            default.extension
74        }
75    }
76}
77
78/// Loads language information from GitHub's languages.yml file
79// TODO: find a way to keep this up to date without downloading it every time
80fn read_languages_from_yml(yml_string: &str) -> color_eyre::Result<HashMap<String, LanguageYML>> {
81    let language_strings: HashMap<String, Value> = serde_yaml::from_str(yml_string)?;
82    let mut languages = HashMap::with_capacity(language_strings.len());
83    for (key, value) in language_strings {
84        languages.insert(key, serde_yaml::from_value(value)?);
85    }
86    Ok(languages)
87}
88
89/// Loads language extension and color information for each language and its aliases
90pub fn get_languages(yml_string: &str) -> color_eyre::Result<HashMap<String, Language>> {
91    let languages = read_languages_from_yml(yml_string)?;
92    let mut name_to_language = HashMap::new();
93    for (name, language_yml) in languages {
94        if let Some(extension) = language_yml.extensions.first() {
95            let mut language = Language::new(name.clone(), extension.clone(), language_yml.color)?;
96            name_to_language.insert(name.to_ascii_lowercase(), language.clone());
97            name_to_language.insert(name, language.clone());
98            for alias in language_yml.aliases {
99                language.name = alias.clone();
100                name_to_language.insert(alias, language.clone());
101            }
102        }
103    }
104    Ok(name_to_language)
105}
106
107pub(crate) struct CodeHighlight {
108    syntax_set: SyntaxSet,
109    theme_set: ThemeSet,
110    theme_name: String,
111    syntect_dir: PathBuf,
112    /// Style used to print description
113    pub(crate) main_style: Style,
114    /// Style used to print language name
115    pub(crate) accent_style: Style,
116    /// Style used to print tags
117    pub(crate) tag_style: Style,
118    /// Style in `skim` when selecting during search
119    pub(crate) selection_style: Style,
120    /// Color settings for `skim`
121    pub(crate) skim_theme: String,
122}
123
124fn syntect_theme_to_skim_theme(settings: &ThemeSettings) -> String {
125    let mut theme = String::new();
126    if let Some(c) = settings.foreground {
127        theme.push_str(&format!("fg:#{},", hex::encode(vec![c.r, c.g, c.b,])));
128    }
129    if let Some(c) = settings.selection {
130        theme.push_str(&format!(
131            "current_match_bg:#{},",
132            hex::encode(vec![c.r, c.g, c.b,])
133        ));
134    }
135    if let Some(c) = settings.selection_foreground {
136        theme.push_str(&format!(
137            "current_match:#{},",
138            hex::encode(vec![c.r, c.g, c.b,])
139        ));
140    }
141    if theme[theme.len() - 1..].starts_with(',') {
142        theme.pop();
143    }
144    theme
145}
146
147impl CodeHighlight {
148    /// Loads themes from `theme_dir` and default syntax set.
149    /// Sets highlighting styles
150    pub(crate) fn new(theme: &str, syntect_dir: PathBuf) -> color_eyre::Result<Self> {
151        let mut theme_set = ThemeSet::load_defaults();
152        theme_set
153            .add_from_folder(&syntect_dir)
154            .map_err(|_e| LostTheWay::ThemeError {
155                theme: String::from((syntect_dir).to_str().unwrap()),
156            })
157            .suggestion(format!(
158                "Make sure {:#?} is a valid directory that has .tmTheme files",
159                &syntect_dir
160            ))?;
161        let mut syntax_set = SyntaxSet::load_defaults_newlines().into_builder();
162        syntax_set
163            .add_from_folder(&syntect_dir, true)
164            .map_err(|_e| LostTheWay::ThemeError {
165                theme: String::from((syntect_dir).to_str().unwrap()),
166            })
167            .suggestion(format!(
168                "Make sure {:#?} is a valid directory that has .sublime-syntax files",
169                &syntect_dir
170            ))?;
171        let syntax_set = syntax_set.build();
172        let mut highlighter = Self {
173            skim_theme: syntect_theme_to_skim_theme(&theme_set.themes[theme].settings),
174            syntax_set,
175            theme_name: theme.into(),
176            theme_set,
177            syntect_dir,
178            main_style: Style::default(),
179            accent_style: Style::default(),
180            tag_style: Style::default(),
181            selection_style: Style::default(),
182        };
183        highlighter.set_styles();
184        Ok(highlighter)
185    }
186
187    /// Sets styles according to current theme
188    fn set_styles(&mut self) {
189        self.set_main_style();
190        self.set_accent_style();
191        self.set_tag_style();
192        self.set_selection_style();
193    }
194
195    /// Style used to print description
196    fn set_main_style(&mut self) {
197        let main_color = self.theme_set.themes[&self.theme_name]
198            .settings
199            .foreground
200            .unwrap_or(Color::WHITE);
201        self.main_style = self.main_style.apply(StyleModifier {
202            foreground: Some(main_color),
203            background: None,
204            font_style: Some(FontStyle::BOLD),
205        });
206    }
207
208    /// Style used to print tags
209    fn set_tag_style(&mut self) {
210        let tag_color = self.theme_set.themes[&self.theme_name]
211            .settings
212            .tags_foreground
213            .unwrap_or_else(|| {
214                self.theme_set.themes[&self.theme_name]
215                    .settings
216                    .line_highlight
217                    .unwrap_or(self.main_style.foreground)
218            });
219        self.tag_style = self.tag_style.apply(StyleModifier {
220            foreground: Some(tag_color),
221            background: self.theme_set.themes[&self.theme_name].settings.background,
222            font_style: Some(FontStyle::ITALIC),
223        });
224    }
225
226    /// Style used to print language name
227    fn set_accent_style(&mut self) {
228        let accent_color = self.theme_set.themes[&self.theme_name]
229            .settings
230            .caret
231            .unwrap_or(self.main_style.foreground);
232        self.accent_style = self.accent_style.apply(StyleModifier {
233            foreground: Some(accent_color),
234            background: None,
235            font_style: None,
236        });
237    }
238
239    /// Style used to highlight matched text in search
240    fn set_selection_style(&mut self) {
241        self.selection_style = self.selection_style.apply(StyleModifier {
242            foreground: self.theme_set.themes[&self.theme_name]
243                .settings
244                .selection_foreground,
245            background: self.theme_set.themes[&self.theme_name].settings.selection,
246            font_style: None,
247        });
248    }
249
250    /// Sets the current theme
251    pub(crate) fn set_theme(&mut self, theme_name: String) -> color_eyre::Result<()> {
252        if self.theme_set.themes.contains_key(&theme_name) {
253            self.theme_name = theme_name;
254            self.set_styles();
255            Ok(())
256        } else {
257            let error: color_eyre::Result<()> =
258                Err(LostTheWay::ThemeError { theme: theme_name }.into());
259            error.suggestion(
260                "That theme doesn't exist. \
261            Use `the-way themes list` to see all theme possibilities \
262            or use `the-way themes add` to add a new one.",
263            )
264        }
265    }
266
267    /// Gets currently available theme names
268    pub(crate) fn get_themes(&self) -> Vec<String> {
269        self.theme_set.themes.keys().cloned().collect()
270    }
271
272    /// Gets current theme name
273    pub(crate) fn get_theme_name(&self) -> String {
274        self.theme_name.clone()
275    }
276
277    /// Adds a new theme from a .tmTheme file.
278    /// The file is copied to the themes folder
279    // TODO: should it automatically be set?
280    pub(crate) fn add_theme(&mut self, theme_file: &Path) -> color_eyre::Result<String> {
281        let theme = ThemeSet::get_theme(theme_file)
282            .map_err(|_e| LostTheWay::ThemeError {
283                theme: theme_file.to_str().unwrap().into(),
284            })
285            .suggestion(format!(
286                "Couldn't load a theme from {}, are you sure this is a valid .tmTheme file?",
287                theme_file.display()
288            ))?;
289        let basename = theme_file
290            .file_stem()
291            .and_then(std::ffi::OsStr::to_str)
292            .ok_or(LostTheWay::ThemeError {
293                theme: theme_file.to_str().unwrap().into(),
294            })
295            .suggestion("Something's fishy with the filename, valid Unicode?")?;
296        // Copy theme to theme file directory
297        let new_theme_file = self.syntect_dir.join(format!("{basename}.tmTheme"));
298        fs::copy(theme_file, new_theme_file)?;
299        self.theme_set.themes.insert(basename.to_owned(), theme);
300        Ok(basename.to_owned())
301    }
302
303    /// Adds a new language syntax from a .sublime-syntax file.
304    /// The file is copied to the themes folder
305    pub(crate) fn add_syntax(&mut self, syntax_file: &Path) -> color_eyre::Result<String> {
306        let syntax = SyntaxDefinition::load_from_str(
307            &fs::read_to_string(syntax_file)?,
308            true,
309            None,
310        )
311            .map_err(|e| LostTheWay::SyntaxError {
312                syntax_file: syntax_file.to_str().unwrap().into(),
313                message: e.to_string(),
314            })
315            .suggestion(format!(
316                "Couldn't load a syntax from {}, are you sure this is a valid .sublime-syntax file with a \'name\' key?",
317                syntax_file.display()
318            ))?;
319        let filename = syntax_file
320            .file_name()
321            .and_then(std::ffi::OsStr::to_str)
322            .ok_or(LostTheWay::SyntaxError {
323                syntax_file: syntax_file.to_str().unwrap().into(),
324                message: String::from("Filename is not valid Unicode"),
325            })
326            .suggestion("Something's fishy with the filename, valid Unicode?")?;
327        // Copy syntax file to syntect dir
328        let new_syntax_file = self.syntect_dir.join(filename);
329        fs::copy(syntax_file, new_syntax_file)?;
330        Ok(syntax.name)
331    }
332
333    /// Makes a box colored according to GitHub language colors
334    pub(crate) fn highlight_block(language_color: Color) -> (Style, String) {
335        (
336            Style::default().apply(StyleModifier {
337                foreground: Some(language_color),
338                background: None,
339                font_style: None,
340            }),
341            format!("{} ", utils::BOX),
342        )
343    }
344
345    /// Syntax highlight code block
346    pub(crate) fn highlight_code(
347        &self,
348        code: &str,
349        extension: &str,
350    ) -> color_eyre::Result<Vec<(Style, String)>> {
351        let mut colorized = Vec::new();
352        let extension = extension.split('.').nth(1).unwrap_or("txt");
353        let syntax = self.syntax_set.find_syntax_by_extension(extension);
354        let syntax = match syntax {
355            Some(syntax) => syntax,
356            None => self.syntax_set.find_syntax_by_extension("txt").unwrap(),
357        };
358        let mut h = HighlightLines::new(syntax, &self.theme_set.themes[&self.theme_name]);
359        for line in LinesWithEndings::from(code) {
360            colorized.extend(
361                h.highlight_line(line, &self.syntax_set)?
362                    .into_iter()
363                    .map(|(style, s)| (style, s.to_owned())),
364            );
365        }
366        Ok(colorized)
367    }
368}