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}