Skip to content

Commit 2fac9e2

Browse files
authored
Inherit theme (#3067)
* Add RawTheme to handle inheritance with theme palette * Add a intermediate step in theme loading it uses RawTheme struct to load the original ThemePalette, so we can merge it with the inherited one. * Load default themes via RawThemes, remove Theme deserialization * Allow naming custom theme same as inherited one * Remove RawTheme and use toml::Value directly * Resolve all review changes resulting in a cleaner code * Simplify return for Loader::load * Add implementation to avoid extra step for loading of base themes
1 parent 57dc5fb commit 2fac9e2

File tree

3 files changed

+152
-42
lines changed

3 files changed

+152
-42
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

helix-view/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ term = ["crossterm"]
1717
bitflags = "1.3"
1818
anyhow = "1"
1919
helix-core = { version = "0.6", path = "../helix-core" }
20+
helix-loader = { version = "0.6", path = "../helix-loader" }
2021
helix-lsp = { version = "0.6", path = "../helix-lsp" }
2122
helix-dap = { version = "0.6", path = "../helix-dap" }
2223
crossterm = { version = "0.25", optional = true }

helix-view/src/theme.rs

Lines changed: 150 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,28 @@ use std::{
33
path::{Path, PathBuf},
44
};
55

6-
use anyhow::Context;
6+
use anyhow::{anyhow, Context, Result};
77
use helix_core::hashmap;
8+
use helix_loader::merge_toml_values;
89
use log::warn;
910
use once_cell::sync::Lazy;
1011
use serde::{Deserialize, Deserializer};
11-
use toml::Value;
12+
use toml::{map::Map, Value};
1213

1314
pub use crate::graphics::{Color, Modifier, Style};
1415

1516
pub 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+
1621
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
1722
});
1823
pub 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+
1928
toml::from_slice(include_bytes!("../../base16_theme.toml"))
2029
.expect("Failed to parse base 16 default theme")
2130
});
@@ -35,24 +44,51 @@ impl Loader {
3544
}
3645

3746
/// 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> {
3948
if name == "default" {
4049
return Ok(self.default());
4150
}
4251
if name == "base16_default" {
4352
return Ok(self.base16_default());
4453
}
45-
let filename = format!("{}.toml", name);
4654

47-
let user_path = self.user_dir.join(&filename);
48-
let path = if user_path.exists() {
49-
user_path
55+
self.load_theme(name, name, false).map(Theme::from)
56+
}
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)
5087
} else {
51-
self.default_dir.join(filename)
88+
theme_toml
5289
};
5390

54-
let data = std::fs::read(&path)?;
55-
toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
91+
Ok(theme_toml)
5692
}
5793

5894
pub fn read_names(path: &Path) -> Vec<String> {
@@ -70,6 +106,53 @@ impl Loader {
70106
.unwrap_or_default()
71107
}
72108

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+
73156
/// Lists all theme names available in default and user directory
74157
pub fn names(&self) -> Vec<String> {
75158
let mut names = Self::read_names(&self.user_dir);
@@ -105,52 +188,77 @@ pub struct Theme {
105188
highlights: Vec<Style>,
106189
}
107190

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+
108206
impl<'de> Deserialize<'de> for Theme {
109207
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110208
where
111209
D: Deserializer<'de>,
112210
{
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) {
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-
}
211+
let values = HashMap::<String, Value>::deserialize(deserializer)?;
138212

139-
// these are used both as UI and as highlights
140-
styles.insert(name.clone(), style);
141-
scopes.push(name);
142-
highlights.push(style);
143-
}
144-
}
213+
let (styles, scopes, highlights) = build_theme_values(Ok(values));
145214

146215
Ok(Self {
147-
scopes,
148216
styles,
217+
scopes,
149218
highlights,
150219
})
151220
}
152221
}
153222

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+
154262
impl Theme {
155263
#[inline]
156264
pub fn highlight(&self, index: usize) -> Style {

0 commit comments

Comments
 (0)