settings.rs

  1use super::assets::Assets;
  2use anyhow::{anyhow, Context, Result};
  3use gpui::{
  4    color::ColorU,
  5    font_cache::{FamilyId, FontCache},
  6    fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight},
  7};
  8use postage::watch;
  9use serde::Deserialize;
 10use std::{collections::HashMap, sync::Arc};
 11
 12const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX);
 13
 14#[derive(Clone)]
 15pub struct Settings {
 16    pub buffer_font_family: FamilyId,
 17    pub buffer_font_size: f32,
 18    pub tab_size: usize,
 19    pub ui_font_family: FamilyId,
 20    pub ui_font_size: f32,
 21    pub theme: Arc<Theme>,
 22}
 23
 24#[derive(Clone, Default)]
 25pub struct Theme {
 26    pub background_color: ColorU,
 27    pub line_number_color: ColorU,
 28    pub default_text_color: ColorU,
 29    syntax_styles: Vec<(String, ColorU, FontProperties)>,
 30}
 31
 32#[derive(Clone, Debug)]
 33pub struct ThemeMap(Arc<[StyleId]>);
 34
 35#[derive(Clone, Copy, Debug)]
 36pub struct StyleId(u32);
 37
 38impl Settings {
 39    pub fn new(font_cache: &FontCache) -> Result<Self> {
 40        Ok(Self {
 41            buffer_font_family: font_cache.load_family(&["Fira Code", "Monaco"])?,
 42            buffer_font_size: 14.0,
 43            tab_size: 4,
 44            ui_font_family: font_cache.load_family(&["SF Pro", "Helvetica"])?,
 45            ui_font_size: 12.0,
 46            theme: Arc::new(
 47                Theme::parse(Assets::get("themes/light.toml").unwrap())
 48                    .expect("Failed to parse built-in theme"),
 49            ),
 50        })
 51    }
 52}
 53
 54impl Theme {
 55    pub fn parse(source: impl AsRef<[u8]>) -> Result<Self> {
 56        #[derive(Deserialize)]
 57        struct ThemeToml {
 58            #[serde(default)]
 59            syntax: HashMap<String, StyleToml>,
 60            #[serde(default)]
 61            ui: HashMap<String, u32>,
 62        }
 63
 64        #[derive(Deserialize)]
 65        #[serde(untagged)]
 66        enum StyleToml {
 67            Color(u32),
 68            Full {
 69                color: Option<u32>,
 70                weight: Option<toml::Value>,
 71                #[serde(default)]
 72                italic: bool,
 73            },
 74        }
 75
 76        let theme_toml: ThemeToml =
 77            toml::from_slice(source.as_ref()).context("failed to parse theme TOML")?;
 78
 79        let mut syntax_styles = Vec::<(String, ColorU, FontProperties)>::new();
 80        for (key, style) in theme_toml.syntax {
 81            let (color, weight, italic) = match style {
 82                StyleToml::Color(color) => (color, None, false),
 83                StyleToml::Full {
 84                    color,
 85                    weight,
 86                    italic,
 87                } => (color.unwrap_or(0), weight, italic),
 88            };
 89            match syntax_styles.binary_search_by_key(&&key, |e| &e.0) {
 90                Ok(i) | Err(i) => {
 91                    let mut properties = FontProperties::new();
 92                    properties.weight = deserialize_weight(weight)?;
 93                    if italic {
 94                        properties.style = FontStyle::Italic;
 95                    }
 96                    syntax_styles.insert(i, (key, deserialize_color(color), properties));
 97                }
 98            }
 99        }
100
101        let background_color = theme_toml
102            .ui
103            .get("background")
104            .copied()
105            .map_or(ColorU::from_u32(0xffffffff), deserialize_color);
106        let line_number_color = theme_toml
107            .ui
108            .get("line_numbers")
109            .copied()
110            .map_or(ColorU::black(), deserialize_color);
111        let default_text_color = theme_toml
112            .ui
113            .get("text")
114            .copied()
115            .map_or(ColorU::black(), deserialize_color);
116
117        Ok(Theme {
118            background_color,
119            line_number_color,
120            default_text_color,
121            syntax_styles,
122        })
123    }
124
125    pub fn syntax_style(&self, id: StyleId) -> (ColorU, FontProperties) {
126        self.syntax_styles
127            .get(id.0 as usize)
128            .map_or((self.default_text_color, FontProperties::new()), |entry| {
129                (entry.1, entry.2)
130            })
131    }
132
133    #[cfg(test)]
134    pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> {
135        self.syntax_styles.get(id.0 as usize).map(|e| e.0.as_str())
136    }
137}
138
139impl ThemeMap {
140    pub fn new(capture_names: &[String], theme: &Theme) -> Self {
141        // For each capture name in the highlight query, find the longest
142        // key in the theme's syntax styles that matches all of the
143        // dot-separated components of the capture name.
144        ThemeMap(
145            capture_names
146                .iter()
147                .map(|capture_name| {
148                    theme
149                        .syntax_styles
150                        .iter()
151                        .enumerate()
152                        .filter_map(|(i, (key, _, _))| {
153                            let mut len = 0;
154                            let capture_parts = capture_name.split('.');
155                            for key_part in key.split('.') {
156                                if capture_parts.clone().any(|part| part == key_part) {
157                                    len += 1;
158                                } else {
159                                    return None;
160                                }
161                            }
162                            Some((i, len))
163                        })
164                        .max_by_key(|(_, len)| *len)
165                        .map_or(DEFAULT_STYLE_ID, |(i, _)| StyleId(i as u32))
166                })
167                .collect(),
168        )
169    }
170
171    pub fn get(&self, capture_id: u32) -> StyleId {
172        self.0
173            .get(capture_id as usize)
174            .copied()
175            .unwrap_or(DEFAULT_STYLE_ID)
176    }
177}
178
179impl Default for ThemeMap {
180    fn default() -> Self {
181        Self(Arc::new([]))
182    }
183}
184
185impl Default for StyleId {
186    fn default() -> Self {
187        DEFAULT_STYLE_ID
188    }
189}
190
191pub fn channel(
192    font_cache: &FontCache,
193) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
194    Ok(watch::channel_with(Settings::new(font_cache)?))
195}
196
197fn deserialize_color(color: u32) -> ColorU {
198    ColorU::from_u32((color << 8) + 0xFF)
199}
200
201fn deserialize_weight(weight: Option<toml::Value>) -> Result<FontWeight> {
202    match &weight {
203        None => return Ok(FontWeight::NORMAL),
204        Some(toml::Value::Integer(i)) => return Ok(FontWeight(*i as f32)),
205        Some(toml::Value::String(s)) => match s.as_str() {
206            "normal" => return Ok(FontWeight::NORMAL),
207            "bold" => return Ok(FontWeight::BOLD),
208            "light" => return Ok(FontWeight::LIGHT),
209            "semibold" => return Ok(FontWeight::SEMIBOLD),
210            _ => {}
211        },
212        _ => {}
213    }
214    Err(anyhow!("Invalid weight {}", weight.unwrap()))
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_parse_theme() {
223        let theme = Theme::parse(
224            r#"
225            [ui]
226            background = 0x00ed00
227            line_numbers = 0xdddddd
228
229            [syntax]
230            "beta.two" = 0xAABBCC
231            "alpha.one" = {color = 0x112233, weight = "bold"}
232            "gamma.three" = {weight = "light", italic = true}
233            "#,
234        )
235        .unwrap();
236
237        assert_eq!(theme.background_color, ColorU::from_u32(0x00ED00FF));
238        assert_eq!(theme.line_number_color, ColorU::from_u32(0xddddddff));
239        assert_eq!(
240            theme.syntax_styles,
241            &[
242                (
243                    "alpha.one".to_string(),
244                    ColorU::from_u32(0x112233FF),
245                    *FontProperties::new().weight(FontWeight::BOLD)
246                ),
247                (
248                    "beta.two".to_string(),
249                    ColorU::from_u32(0xAABBCCFF),
250                    *FontProperties::new().weight(FontWeight::NORMAL)
251                ),
252                (
253                    "gamma.three".to_string(),
254                    ColorU::from_u32(0x000000FF),
255                    *FontProperties::new()
256                        .weight(FontWeight::LIGHT)
257                        .style(FontStyle::Italic),
258                ),
259            ]
260        );
261    }
262
263    #[test]
264    fn test_parse_empty_theme() {
265        Theme::parse("").unwrap();
266    }
267
268    #[test]
269    fn test_theme_map() {
270        let theme = Theme {
271            default_text_color: Default::default(),
272            background_color: ColorU::default(),
273            line_number_color: ColorU::default(),
274            syntax_styles: [
275                ("function", ColorU::from_u32(0x100000ff)),
276                ("function.method", ColorU::from_u32(0x200000ff)),
277                ("function.async", ColorU::from_u32(0x300000ff)),
278                ("variable.builtin.self.rust", ColorU::from_u32(0x400000ff)),
279                ("variable.builtin", ColorU::from_u32(0x500000ff)),
280                ("variable", ColorU::from_u32(0x600000ff)),
281            ]
282            .iter()
283            .map(|e| (e.0.to_string(), e.1, FontProperties::new()))
284            .collect(),
285        };
286
287        let capture_names = &[
288            "function.special".to_string(),
289            "function.async.rust".to_string(),
290            "variable.builtin.self".to_string(),
291        ];
292
293        let map = ThemeMap::new(capture_names, &theme);
294        assert_eq!(theme.syntax_style_name(map.get(0)), Some("function"));
295        assert_eq!(theme.syntax_style_name(map.get(1)), Some("function.async"));
296        assert_eq!(
297            theme.syntax_style_name(map.get(2)),
298            Some("variable.builtin")
299        );
300    }
301}