xi_core_lib/
styles.rs

1// Copyright 2017 The xi-editor Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Management of styles.
16
17use std::collections::{BTreeMap, HashMap, HashSet};
18use std::ffi::OsStr;
19use std::fmt;
20use std::fs;
21use std::iter::FromIterator;
22use std::path::{Path, PathBuf};
23
24use serde_json::{self, Value};
25use syntect::dumps::{dump_to_file, from_dump_file};
26use syntect::highlighting::StyleModifier as SynStyleModifier;
27use syntect::highlighting::{Color, Highlighter, Theme, ThemeSet};
28use syntect::LoadingError;
29
30pub use syntect::highlighting::ThemeSettings;
31
32pub const N_RESERVED_STYLES: usize = 8;
33const SYNTAX_PRIORITY_DEFAULT: u16 = 200;
34const SYNTAX_PRIORITY_LOWEST: u16 = 0;
35pub const DEFAULT_THEME: &str = "InspiredGitHub";
36
37#[derive(Clone, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
38/// A mergeable style. All values except priority are optional.
39///
40/// Note: A `None` value represents the absense of preference; in the case of
41/// boolean options, `Some(false)` means that this style will override a lower
42/// priority value in the same field.
43pub struct Style {
44    /// The priority of this style, in the range (0, 1000). Used to resolve
45    /// conflicting fields when merging styles. The higher priority wins.
46    #[serde(skip_serializing)]
47    pub priority: u16,
48    /// The foreground text color, in ARGB.
49    pub fg_color: Option<u32>,
50    /// The background text color, in ARGB.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub bg_color: Option<u32>,
53    /// The font-weight, in the range 100-900, interpreted like the CSS
54    /// font-weight property.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub weight: Option<u16>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub underline: Option<bool>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub italic: Option<bool>,
61}
62
63impl Style {
64    /// Creates a new `Style` by converting from a `Syntect::StyleModifier`.
65    pub fn from_syntect_style_mod(style: &SynStyleModifier) -> Self {
66        let font_style = style.font_style.map(|s| s.bits()).unwrap_or_default();
67        let weight = if (font_style & 1) != 0 { Some(700) } else { None };
68        let underline = if (font_style & 2) != 0 { Some(true) } else { None };
69        let italic = if (font_style & 4) != 0 { Some(true) } else { None };
70
71        Self::new(
72            SYNTAX_PRIORITY_DEFAULT,
73            style.foreground.map(Self::rgba_from_syntect_color),
74            style.background.map(Self::rgba_from_syntect_color),
75            weight,
76            underline,
77            italic,
78        )
79    }
80
81    pub fn new<O32, O16, OB>(
82        priority: u16,
83        fg_color: O32,
84        bg_color: O32,
85        weight: O16,
86        underline: OB,
87        italic: OB,
88    ) -> Self
89    where
90        O32: Into<Option<u32>>,
91        O16: Into<Option<u16>>,
92        OB: Into<Option<bool>>,
93    {
94        assert!(priority <= 1000);
95        Style {
96            priority,
97            fg_color: fg_color.into(),
98            bg_color: bg_color.into(),
99            weight: weight.into(),
100            underline: underline.into(),
101            italic: italic.into(),
102        }
103    }
104
105    /// Returns the default style for the given `Theme`.
106    pub fn default_for_theme(theme: &Theme) -> Self {
107        let fg = theme.settings.foreground.unwrap_or(Color::BLACK);
108        Style::new(
109            SYNTAX_PRIORITY_LOWEST,
110            Some(Self::rgba_from_syntect_color(fg)),
111            None,
112            None,
113            None,
114            None,
115        )
116    }
117
118    /// Creates a new style by combining attributes of `self` and `other`.
119    /// If both styles define an attribute, the highest priority wins; `other`
120    /// wins in the case of a tie.
121    ///
122    /// Note: when merging multiple styles, apply them in increasing priority.
123    pub fn merge(&self, other: &Style) -> Style {
124        let (p1, p2) = if self.priority > other.priority { (self, other) } else { (other, self) };
125
126        Style::new(
127            p1.priority,
128            p1.fg_color.or(p2.fg_color),
129            p1.bg_color.or(p2.bg_color),
130            p1.weight.or(p2.weight),
131            p1.underline.or(p2.underline),
132            p1.italic.or(p2.italic),
133        )
134    }
135
136    /// Encode this `Style`, setting the `id` property.
137    ///
138    /// Note: this should only be used when sending the `def_style` RPC.
139    pub fn to_json(&self, id: usize) -> Value {
140        let mut as_val = serde_json::to_value(self).expect("failed to encode style");
141        as_val["id"] = id.into();
142        as_val
143    }
144
145    fn rgba_from_syntect_color(color: Color) -> u32 {
146        let Color { r, g, b, a } = color;
147        ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
148    }
149}
150
151/// A map from styles to client identifiers for a given `Theme`.
152pub struct ThemeStyleMap {
153    themes: ThemeSet,
154    theme_name: String,
155    theme: Theme,
156
157    // Keeps a list of default themes.
158    default_themes: Vec<String>,
159    default_style: Style,
160    map: HashMap<Style, usize>,
161
162    // Maintains the map of found themes and their paths.
163    path_map: BTreeMap<String, PathBuf>,
164
165    // It's not obvious we actually have to store the style, we seem to only need it
166    // as the key in the map.
167    styles: Vec<Style>,
168    themes_dir: Option<PathBuf>,
169    cache_dir: Option<PathBuf>,
170    caching_enabled: bool,
171}
172
173impl ThemeStyleMap {
174    pub fn new(themes_dir: Option<PathBuf>) -> ThemeStyleMap {
175        let themes = ThemeSet::load_defaults();
176        let theme_name = DEFAULT_THEME.to_owned();
177        let theme = themes.themes.get(&theme_name).expect("missing theme").to_owned();
178        let default_themes = themes.themes.keys().cloned().collect();
179        let default_style = Style::default_for_theme(&theme);
180        let cache_dir = None;
181        let caching_enabled = true;
182
183        ThemeStyleMap {
184            themes,
185            theme_name,
186            theme,
187            default_themes,
188            default_style,
189            map: HashMap::new(),
190            path_map: BTreeMap::new(),
191            styles: Vec::new(),
192            themes_dir,
193            cache_dir,
194            caching_enabled,
195        }
196    }
197
198    pub fn get_default_style(&self) -> &Style {
199        &self.default_style
200    }
201
202    pub fn get_highlighter(&self) -> Highlighter {
203        Highlighter::new(&self.theme)
204    }
205
206    pub fn get_theme_name(&self) -> &str {
207        &self.theme_name
208    }
209
210    pub fn get_theme_settings(&self) -> &ThemeSettings {
211        &self.theme.settings
212    }
213
214    pub fn get_theme_names(&self) -> Vec<String> {
215        self.path_map.keys().chain(self.default_themes.iter()).cloned().collect()
216    }
217
218    pub fn contains_theme(&self, k: &str) -> bool {
219        self.themes.themes.contains_key(k)
220    }
221
222    pub fn set_theme(&mut self, theme_name: &str) -> Result<(), &'static str> {
223        match self.load_theme(theme_name) {
224            Ok(()) => {
225                if let Some(new_theme) = self.themes.themes.get(theme_name) {
226                    self.theme = new_theme.to_owned();
227                    self.theme_name = theme_name.to_owned();
228                    self.default_style = Style::default_for_theme(&self.theme);
229                    self.map = HashMap::new();
230                    self.styles = Vec::new();
231                    Ok(())
232                } else {
233                    Err("unknown theme")
234                }
235            }
236            Err(e) => {
237                error!("Encountered error {:?} while trying to load {:?}", e, theme_name);
238                Err("could not load theme")
239            }
240        }
241    }
242
243    pub fn merge_with_default(&self, style: &Style) -> Style {
244        self.default_style.merge(style)
245    }
246
247    pub fn lookup(&self, style: &Style) -> Option<usize> {
248        self.map.get(style).cloned()
249    }
250
251    pub fn add(&mut self, style: &Style) -> usize {
252        let result = self.styles.len() + N_RESERVED_STYLES;
253        self.map.insert(style.clone(), result);
254        self.styles.push(style.clone());
255        result
256    }
257
258    /// Delete key and the corresponding dump file from the themes map.
259    pub(crate) fn remove_theme(&mut self, path: &Path) -> Option<String> {
260        validate_theme_file(path).ok()?;
261
262        let theme_name = path.file_stem().and_then(OsStr::to_str)?;
263        self.themes.themes.remove(theme_name);
264        self.path_map.remove(theme_name);
265
266        let dump_p = self.get_dump_path(theme_name)?;
267        if dump_p.exists() {
268            let _ = fs::remove_file(dump_p);
269        }
270
271        Some(theme_name.to_string())
272    }
273
274    /// Cache all themes names and their paths inside the given directory.
275    pub(crate) fn load_theme_dir(&mut self) {
276        if let Some(themes_dir) = self.themes_dir.clone() {
277            match ThemeSet::discover_theme_paths(themes_dir) {
278                Ok(themes) => {
279                    self.caching_enabled = self.caching_enabled && self.init_cache_dir();
280                    // We look through the theme folder here and cache their names/paths to a
281                    // path hashmap.
282                    for theme_p in &themes {
283                        match self.load_theme_info_from_path(theme_p) {
284                            Ok(_) => (),
285                            Err(e) => {
286                                error!("Encountered error {:?} loading theme at {:?}", e, theme_p)
287                            }
288                        }
289                    }
290                }
291                Err(e) => error!("Error loading themes dir: {:?}", e),
292            }
293        }
294    }
295
296    /// A wrapper around `from_dump_file`
297    /// to validate the state of dump file.
298    /// Invalidates if mod time of dump is less
299    /// than the original one.
300    fn try_load_from_dump(&self, theme_p: &Path) -> Option<(String, Theme)> {
301        if !self.caching_enabled {
302            return None;
303        }
304
305        let theme_name = theme_p.file_stem().and_then(OsStr::to_str)?;
306
307        let dump_p = self.get_dump_path(theme_name)?;
308
309        if !&dump_p.exists() {
310            return None;
311        }
312
313        //NOTE: `try_load_from_dump` will return `None` if the file at
314        //`dump_p` or `theme_p` is deleted before the execution of this fn.
315        let mod_t = fs::metadata(&dump_p).and_then(|md| md.modified()).ok()?;
316        let mod_t_orig = fs::metadata(theme_p).and_then(|md| md.modified()).ok()?;
317
318        if mod_t >= mod_t_orig {
319            from_dump_file(&dump_p).ok().map(|t| (theme_name.to_owned(), t))
320        } else {
321            // Delete dump file
322            let _ = fs::remove_file(&dump_p);
323            None
324        }
325    }
326
327    /// Loads a theme's name and its respective path into the theme path map.
328    pub(crate) fn load_theme_info_from_path(
329        &mut self,
330        theme_p: &Path,
331    ) -> Result<String, LoadingError> {
332        validate_theme_file(theme_p)?;
333        let theme_name =
334            theme_p.file_stem().and_then(OsStr::to_str).ok_or(LoadingError::BadPath)?;
335
336        self.path_map.insert(theme_name.to_string(), theme_p.to_path_buf());
337
338        Ok(theme_name.to_owned())
339    }
340
341    /// Loads theme using syntect's `get_theme` fn to our `theme` path map.
342    /// Stores binary dump in a file with `tmdump` extension, only if
343    /// caching is enabled.
344    fn load_theme(&mut self, theme_name: &str) -> Result<(), LoadingError> {
345        // If it is the current theme (the user has edited it), we load it again.
346        // Otherwise, if it's a default theme or the theme has already been loaded, we can move on.
347        if self.contains_theme(theme_name) && self.get_theme_name() != theme_name {
348            return Ok(());
349        }
350        // If we haven't loaded the theme before, we try to load it from the dump if a dump
351        // exists or load it from the theme file itself.
352        let theme_p = &self.path_map.get(theme_name).cloned();
353        if let Some(theme_p) = theme_p {
354            match self.try_load_from_dump(theme_p) {
355                Some((dump_theme_name, dump_theme_data)) => {
356                    self.insert_to_map(dump_theme_name, dump_theme_data);
357                }
358                None => {
359                    let theme = ThemeSet::get_theme(theme_p)?;
360                    if self.caching_enabled {
361                        if let Some(dump_p) = self.get_dump_path(theme_name) {
362                            let _ = dump_to_file(&theme, dump_p);
363                        }
364                    }
365                    self.insert_to_map(theme_name.to_owned(), theme);
366                }
367            }
368            Ok(())
369        } else {
370            Err(LoadingError::BadPath)
371        }
372    }
373
374    fn insert_to_map(&mut self, k: String, v: Theme) {
375        self.themes.themes.insert(k, v);
376    }
377
378    /// Returns dump's path corresponding to the given theme name.
379    fn get_dump_path(&self, theme_name: &str) -> Option<PathBuf> {
380        self.cache_dir.as_ref().map(|p| p.join(theme_name).with_extension("tmdump"))
381    }
382
383    /// Compare the stored file paths in `self.state`
384    /// to the present ones.
385    pub(crate) fn sync_dir(&mut self, dir: Option<&Path>) {
386        if let Some(themes_dir) = dir {
387            if let Ok(paths) = ThemeSet::discover_theme_paths(themes_dir) {
388                let current_state: HashSet<PathBuf> = HashSet::from_iter(paths.into_iter());
389                let maintained_state: HashSet<PathBuf> =
390                    HashSet::from_iter(self.path_map.values().cloned());
391
392                let to_insert = current_state.difference(&maintained_state);
393                for path in to_insert {
394                    let _ = self.load_theme_info_from_path(path);
395                }
396                let to_remove = maintained_state.difference(&current_state);
397                for path in to_remove {
398                    self.remove_theme(path);
399                }
400            }
401        }
402    }
403    /// Creates the cache dir returns true
404    /// if it is successfully initialized or
405    /// already exists.
406    fn init_cache_dir(&mut self) -> bool {
407        self.cache_dir = self.themes_dir.clone().map(|p| p.join("cache"));
408
409        if let Some(ref p) = self.cache_dir {
410            if p.exists() {
411                return true;
412            }
413            fs::DirBuilder::new().create(&p).is_ok()
414        } else {
415            false
416        }
417    }
418}
419
420/// Used to remove files with extension other than `tmTheme`.
421fn validate_theme_file(path: &Path) -> Result<bool, LoadingError> {
422    path.extension().map(|e| e != "tmTheme").ok_or(LoadingError::BadPath)
423}
424
425impl fmt::Debug for Style {
426    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
427        fn fmt_color(f: &mut fmt::Formatter, c: Option<u32>) -> fmt::Result {
428            if let Some(c) = c {
429                write!(f, "#{:X}", c)
430            } else {
431                write!(f, "None")
432            }
433        }
434
435        write!(f, "Style( P{}, fg: ", self.priority)?;
436        fmt_color(f, self.fg_color)?;
437        write!(f, " bg: ")?;
438        fmt_color(f, self.bg_color)?;
439
440        if let Some(w) = self.weight {
441            write!(f, " weight {}", w)?;
442        }
443        if let Some(i) = self.italic {
444            write!(f, " ital: {}", i)?;
445        }
446        if let Some(u) = self.underline {
447            write!(f, " uline: {}", u)?;
448        }
449        write!(f, " )")
450    }
451}