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
pull/4085/head
Christoph Schmidler 2 years ago committed by GitHub
parent 57dc5fbe3a
commit 2fac9e24e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
Cargo.lock generated

@ -523,6 +523,7 @@ dependencies = [
"futures-util", "futures-util",
"helix-core", "helix-core",
"helix-dap", "helix-dap",
"helix-loader",
"helix-lsp", "helix-lsp",
"helix-tui", "helix-tui",
"log", "log",

@ -17,6 +17,7 @@ term = ["crossterm"]
bitflags = "1.3" bitflags = "1.3"
anyhow = "1" anyhow = "1"
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }
helix-loader = { version = "0.6", path = "../helix-loader" }
helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" } helix-dap = { version = "0.6", path = "../helix-dap" }
crossterm = { version = "0.25", optional = true } crossterm = { version = "0.25", optional = true }

@ -3,19 +3,28 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use anyhow::Context; use anyhow::{anyhow, Context, Result};
use helix_core::hashmap; use helix_core::hashmap;
use helix_loader::merge_toml_values;
use log::warn; use log::warn;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use toml::Value; use toml::{map::Map, Value};
pub use crate::graphics::{Color, Modifier, Style}; pub use crate::graphics::{Color, Modifier, Style};
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
// let raw_theme: Value = toml::from_slice(include_bytes!("../../theme.toml"))
// .expect("Failed to parse default theme");
// Theme::from(raw_theme)
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
}); });
pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
// let raw_theme: Value = toml::from_slice(include_bytes!("../../base16_theme.toml"))
// .expect("Failed to parse base 16 default theme");
// Theme::from(raw_theme)
toml::from_slice(include_bytes!("../../base16_theme.toml")) toml::from_slice(include_bytes!("../../base16_theme.toml"))
.expect("Failed to parse base 16 default theme") .expect("Failed to parse base 16 default theme")
}); });
@ -35,24 +44,51 @@ impl Loader {
} }
/// Loads a theme first looking in the `user_dir` then in `default_dir` /// Loads a theme first looking in the `user_dir` then in `default_dir`
pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> { pub fn load(&self, name: &str) -> Result<Theme> {
if name == "default" { if name == "default" {
return Ok(self.default()); return Ok(self.default());
} }
if name == "base16_default" { if name == "base16_default" {
return Ok(self.base16_default()); return Ok(self.base16_default());
} }
let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename); self.load_theme(name, name, false).map(Theme::from)
let path = if user_path.exists() { }
user_path
// load the theme and its parent recursively and merge them
// `base_theme_name` is the theme from the config.toml,
// used to prevent some circular loading scenarios
fn load_theme(
&self,
name: &str,
base_them_name: &str,
only_default_dir: bool,
) -> Result<Value> {
let path = self.path(name, only_default_dir);
let theme_toml = self.load_toml(path)?;
let inherits = theme_toml.get("inherits");
let theme_toml = if let Some(parent_theme_name) = inherits {
let parent_theme_name = parent_theme_name.as_str().ok_or_else(|| {
anyhow!(
"Theme: expected 'inherits' to be a string: {}",
parent_theme_name
)
})?;
let parent_theme_toml = self.load_theme(
parent_theme_name,
base_them_name,
base_them_name == parent_theme_name,
)?;
self.merge_themes(parent_theme_toml, theme_toml)
} else { } else {
self.default_dir.join(filename) theme_toml
}; };
let data = std::fs::read(&path)?; Ok(theme_toml)
toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
} }
pub fn read_names(path: &Path) -> Vec<String> { pub fn read_names(path: &Path) -> Vec<String> {
@ -70,6 +106,53 @@ impl Loader {
.unwrap_or_default() .unwrap_or_default()
} }
// merge one theme into the parent theme
fn merge_themes(&self, parent_theme_toml: Value, theme_toml: Value) -> Value {
let parent_palette = parent_theme_toml.get("palette");
let palette = theme_toml.get("palette");
// handle the table seperately since it needs a `merge_depth` of 2
// this would conflict with the rest of the theme merge strategy
let palette_values = match (parent_palette, palette) {
(Some(parent_palette), Some(palette)) => {
merge_toml_values(parent_palette.clone(), palette.clone(), 2)
}
(Some(parent_palette), None) => parent_palette.clone(),
(None, Some(palette)) => palette.clone(),
(None, None) => Map::new().into(),
};
// add the palette correctly as nested table
let mut palette = Map::new();
palette.insert(String::from("palette"), palette_values);
// merge the theme into the parent theme
let theme = merge_toml_values(parent_theme_toml, theme_toml, 1);
// merge the before specially handled palette into the theme
merge_toml_values(theme, palette.into(), 1)
}
// Loads the theme data as `toml::Value` first from the user_dir then in default_dir
fn load_toml(&self, path: PathBuf) -> Result<Value> {
let data = std::fs::read(&path)?;
toml::from_slice(data.as_slice()).context("Failed to deserialize theme")
}
// Returns the path to the theme with the name
// With `only_default_dir` as false the path will first search for the user path
// disabled it ignores the user path and returns only the default path
fn path(&self, name: &str, only_default_dir: bool) -> PathBuf {
let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename);
if !only_default_dir && user_path.exists() {
user_path
} else {
self.default_dir.join(filename)
}
}
/// Lists all theme names available in default and user directory /// Lists all theme names available in default and user directory
pub fn names(&self) -> Vec<String> { pub fn names(&self) -> Vec<String> {
let mut names = Self::read_names(&self.user_dir); let mut names = Self::read_names(&self.user_dir);
@ -105,16 +188,46 @@ pub struct Theme {
highlights: Vec<Style>, highlights: Vec<Style>,
} }
impl From<Value> for Theme {
fn from(value: Value) -> Self {
let values: Result<HashMap<String, Value>> =
toml::from_str(&value.to_string()).context("Failed to load theme");
let (styles, scopes, highlights) = build_theme_values(values);
Self {
styles,
scopes,
highlights,
}
}
}
impl<'de> Deserialize<'de> for Theme { impl<'de> Deserialize<'de> for Theme {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let values = HashMap::<String, Value>::deserialize(deserializer)?;
let (styles, scopes, highlights) = build_theme_values(Ok(values));
Ok(Self {
styles,
scopes,
highlights,
})
}
}
fn build_theme_values(
values: Result<HashMap<String, Value>>,
) -> (HashMap<String, Style>, Vec<String>, Vec<Style>) {
let mut styles = HashMap::new(); let mut styles = HashMap::new();
let mut scopes = Vec::new(); let mut scopes = Vec::new();
let mut highlights = Vec::new(); let mut highlights = Vec::new();
if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) { if let Ok(mut colors) = values {
// TODO: alert user of parsing failures in editor // TODO: alert user of parsing failures in editor
let palette = colors let palette = colors
.remove("palette") .remove("palette")
@ -125,11 +238,11 @@ impl<'de> Deserialize<'de> for Theme {
}) })
}) })
.unwrap_or_default(); .unwrap_or_default();
// remove inherits from value to prevent errors
let _ = colors.remove("inherits");
styles.reserve(colors.len()); styles.reserve(colors.len());
scopes.reserve(colors.len()); scopes.reserve(colors.len());
highlights.reserve(colors.len()); highlights.reserve(colors.len());
for (name, style_value) in colors { for (name, style_value) in colors {
let mut style = Style::default(); let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) { if let Err(err) = palette.parse_style(&mut style, style_value) {
@ -143,12 +256,7 @@ impl<'de> Deserialize<'de> for Theme {
} }
} }
Ok(Self { (styles, scopes, highlights)
scopes,
styles,
highlights,
})
}
} }
impl Theme { impl Theme {

Loading…
Cancel
Save