on master help sign in
Inherit theme (#3067)
2fac9e2Christoph Schmidler
Cargo.lock+1
@@ -523,6 +523,7 @@ dependencies = [
523 "futures-util",
523 "futures-util",
524 "helix-core",
524 "helix-core",
525 "helix-dap",
525 "helix-dap",
526+ "helix-loader",
526 "helix-lsp",
527 "helix-lsp",
527 "helix-tui",
528 "helix-tui",
528 "log",
529 "log",
helix-view/Cargo.toml+1
@@ -17,6 +17,7 @@ term = ["crossterm"]
17bitflags = "1.3"
17bitflags = "1.3"
18anyhow = "1"
18anyhow = "1"
19helix-core = { version = "0.6", path = "../helix-core" }
19helix-core = { version = "0.6", path = "../helix-core" }
20+helix-loader = { version = "0.6", path = "../helix-loader" }
20helix-lsp = { version = "0.6", path = "../helix-lsp" }
21helix-lsp = { version = "0.6", path = "../helix-lsp" }
21helix-dap = { version = "0.6", path = "../helix-dap" }
22helix-dap = { version = "0.6", path = "../helix-dap" }
22crossterm = { version = "0.25", optional = true }
23crossterm = { version = "0.25", optional = true }
helix-view/src/theme.rs+150−42
@@ -3,19 +3,28 @@ use std::{
3 path::{Path, PathBuf},
3 path::{Path, PathBuf},
4};
4};
5
5
6−use anyhow::Context;
6+use anyhow::{anyhow, Context, Result};
7use helix_core::hashmap;
7use helix_core::hashmap;
8+use helix_loader::merge_toml_values;
8use log::warn;
9use log::warn;
9use once_cell::sync::Lazy;
10use once_cell::sync::Lazy;
10use serde::{Deserialize, Deserializer};
11use serde::{Deserialize, Deserializer};
11−use toml::Value;
12+use toml::{map::Map, Value};
12
13
13pub use crate::graphics::{Color, Modifier, Style};
14pub use crate::graphics::{Color, Modifier, Style};
14
15
15pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
16pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
17+ // let raw_theme: Value = toml::from_slice(include_bytes!("../../theme.toml"))
18+ // .expect("Failed to parse default theme");
19+ // Theme::from(raw_theme)
20+
16 toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
21 toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
17});
22});
18pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
23pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
24+ // let raw_theme: Value = toml::from_slice(include_bytes!("../../base16_theme.toml"))
25+ // .expect("Failed to parse base 16 default theme");
26+ // Theme::from(raw_theme)
27+
19 toml::from_slice(include_bytes!("../../base16_theme.toml"))
28 toml::from_slice(include_bytes!("../../base16_theme.toml"))
20 .expect("Failed to parse base 16 default theme")
29 .expect("Failed to parse base 16 default theme")
21});
30});
@@ -35,24 +44,51 @@ impl Loader {
35 }
44 }
36
45
37 /// Loads a theme first looking in the `user_dir` then in `default_dir`
46 /// Loads a theme first looking in the `user_dir` then in `default_dir`
38− pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> {
47+ pub fn load(&self, name: &str) -> Result<Theme> {
39 if name == "default" {
48 if name == "default" {
40 return Ok(self.default());
49 return Ok(self.default());
41 }
50 }
42 if name == "base16_default" {
51 if name == "base16_default" {
43 return Ok(self.base16_default());
52 return Ok(self.base16_default());
44 }
53 }
45− let filename = format!("{}.toml", name);
46
54
47− let user_path = self.user_dir.join(&filename);
55+ self.load_theme(name, name, false).map(Theme::from)
48− let path = if user_path.exists() {
56+ }
49− user_path
57+
58+ // load the theme and its parent recursively and merge them
59+ // `base_theme_name` is the theme from the config.toml,
60+ // used to prevent some circular loading scenarios
61+ fn load_theme(
62+ &self,
63+ name: &str,
64+ base_them_name: &str,
65+ only_default_dir: bool,
66+ ) -> Result<Value> {
67+ let path = self.path(name, only_default_dir);
68+ let theme_toml = self.load_toml(path)?;
69+
70+ let inherits = theme_toml.get("inherits");
71+
72+ let theme_toml = if let Some(parent_theme_name) = inherits {
73+ let parent_theme_name = parent_theme_name.as_str().ok_or_else(|| {
74+ anyhow!(
75+ "Theme: expected 'inherits' to be a string: {}",
76+ parent_theme_name
77+ )
78+ })?;
79+
80+ let parent_theme_toml = self.load_theme(
81+ parent_theme_name,
82+ base_them_name,
83+ base_them_name == parent_theme_name,
84+ )?;
85+
86+ self.merge_themes(parent_theme_toml, theme_toml)
50 } else {
87 } else {
51− self.default_dir.join(filename)
88+ theme_toml
52 };
89 };
53
90
54− let data = std::fs::read(&path)?;
91+ Ok(theme_toml)
55− toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
56 }
92 }
57
93
58 pub fn read_names(path: &Path) -> Vec<String> {
94 pub fn read_names(path: &Path) -> Vec<String> {
@@ -70,6 +106,53 @@ impl Loader {
70 .unwrap_or_default()
106 .unwrap_or_default()
71 }
107 }
72
108
109+ // merge one theme into the parent theme
110+ fn merge_themes(&self, parent_theme_toml: Value, theme_toml: Value) -> Value {
111+ let parent_palette = parent_theme_toml.get("palette");
112+ let palette = theme_toml.get("palette");
113+
114+ // handle the table seperately since it needs a `merge_depth` of 2
115+ // this would conflict with the rest of the theme merge strategy
116+ let palette_values = match (parent_palette, palette) {
117+ (Some(parent_palette), Some(palette)) => {
118+ merge_toml_values(parent_palette.clone(), palette.clone(), 2)
119+ }
120+ (Some(parent_palette), None) => parent_palette.clone(),
121+ (None, Some(palette)) => palette.clone(),
122+ (None, None) => Map::new().into(),
123+ };
124+
125+ // add the palette correctly as nested table
126+ let mut palette = Map::new();
127+ palette.insert(String::from("palette"), palette_values);
128+
129+ // merge the theme into the parent theme
130+ let theme = merge_toml_values(parent_theme_toml, theme_toml, 1);
131+ // merge the before specially handled palette into the theme
132+ merge_toml_values(theme, palette.into(), 1)
133+ }
134+
135+ // Loads the theme data as `toml::Value` first from the user_dir then in default_dir
136+ fn load_toml(&self, path: PathBuf) -> Result<Value> {
137+ let data = std::fs::read(&path)?;
138+
139+ toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
140+ }
141+
142+ // Returns the path to the theme with the name
143+ // With `only_default_dir` as false the path will first search for the user path
144+ // disabled it ignores the user path and returns only the default path
145+ fn path(&self, name: &str, only_default_dir: bool) -> PathBuf {
146+ let filename = format!("{}.toml", name);
147+
148+ let user_path = self.user_dir.join(&filename);
149+ if !only_default_dir && user_path.exists() {
150+ user_path
151+ } else {
152+ self.default_dir.join(filename)
153+ }
154+ }
155+
73 /// Lists all theme names available in default and user directory
156 /// Lists all theme names available in default and user directory
74 pub fn names(&self) -> Vec<String> {
157 pub fn names(&self) -> Vec<String> {
75 let mut names = Self::read_names(&self.user_dir);
158 let mut names = Self::read_names(&self.user_dir);
@@ -105,52 +188,77 @@ pub struct Theme {
105 highlights: Vec<Style>,
188 highlights: Vec<Style>,
106}
189}
107
190
191+impl From<Value> for Theme {
192+ fn from(value: Value) -> Self {
193+ let values: Result<HashMap<String, Value>> =
194+ toml::from_str(&value.to_string()).context("Failed to load theme");
195+
196+ let (styles, scopes, highlights) = build_theme_values(values);
197+
198+ Self {
199+ styles,
200+ scopes,
201+ highlights,
202+ }
203+ }
204+}
205+
108impl<'de> Deserialize<'de> for Theme {
206impl<'de> Deserialize<'de> for Theme {
109 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
207 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110 where
208 where
111 D: Deserializer<'de>,
209 D: Deserializer<'de>,
112 {
210 {
113− let mut styles = HashMap::new();
114− let mut scopes = Vec::new();
115− let mut highlights = Vec::new();
116−
117− if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
211+ let values = HashMap::<String, Value>::deserialize(deserializer)?;
118− // TODO: alert user of parsing failures in editor
119− let palette = colors
120− .remove("palette")
121− .map(|value| {
122− ThemePalette::try_from(value).unwrap_or_else(|err| {
123− warn!("{}", err);
124− ThemePalette::default()
125− })
126− })
127− .unwrap_or_default();
128−
129− styles.reserve(colors.len());
130− scopes.reserve(colors.len());
131− highlights.reserve(colors.len());
132−
133− for (name, style_value) in colors {
134− let mut style = Style::default();
135− if let Err(err) = palette.parse_style(&mut style, style_value) {
136− warn!("{}", err);
137− }
138
212
139− // these are used both as UI and as highlights
213+ let (styles, scopes, highlights) = build_theme_values(Ok(values));
140− styles.insert(name.clone(), style);
141− scopes.push(name);
142− highlights.push(style);
143− }
144− }
145
214
146 Ok(Self {
215 Ok(Self {
147− scopes,
148 styles,
216 styles,
217+ scopes,
149 highlights,
218 highlights,
150 })
219 })
151 }
220 }
152}
221}
153
222
223+fn build_theme_values(
224+ values: Result<HashMap<String, Value>>,
225+) -> (HashMap<String, Style>, Vec<String>, Vec<Style>) {
226+ let mut styles = HashMap::new();
227+ let mut scopes = Vec::new();
228+ let mut highlights = Vec::new();
229+
230+ if let Ok(mut colors) = values {
231+ // TODO: alert user of parsing failures in editor
232+ let palette = colors
233+ .remove("palette")
234+ .map(|value| {
235+ ThemePalette::try_from(value).unwrap_or_else(|err| {
236+ warn!("{}", err);
237+ ThemePalette::default()
238+ })
239+ })
240+ .unwrap_or_default();
241+ // remove inherits from value to prevent errors
242+ let _ = colors.remove("inherits");
243+ styles.reserve(colors.len());
244+ scopes.reserve(colors.len());
245+ highlights.reserve(colors.len());
246+ for (name, style_value) in colors {
247+ let mut style = Style::default();
248+ if let Err(err) = palette.parse_style(&mut style, style_value) {
249+ warn!("{}", err);
250+ }
251+
252+ // these are used both as UI and as highlights
253+ styles.insert(name.clone(), style);
254+ scopes.push(name);
255+ highlights.push(style);
256+ }
257+ }
258+
259+ (styles, scopes, highlights)
260+}
261+
154impl Theme {
262impl Theme {
155 #[inline]
263 #[inline]
156 pub fn highlight(&self, index: usize) -> Style {
264 pub fn highlight(&self, index: usize) -> Style {