-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inherit theme #3067
Inherit theme #3067
Changes from 7 commits
33daeaf
3370de8
cbbe8ae
07f6cf2
0ddd6d7
b5e267d
43e7cd4
0bc852b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,21 +3,24 @@ use std::{ | |
path::{Path, PathBuf}, | ||
}; | ||
|
||
use anyhow::Context; | ||
use anyhow::{anyhow, Context, Result}; | ||
use helix_core::hashmap; | ||
use helix_loader::merge_toml_values; | ||
use log::warn; | ||
use once_cell::sync::Lazy; | ||
use serde::{Deserialize, Deserializer}; | ||
use toml::Value; | ||
use toml::{map::Map, Value}; | ||
|
||
pub use crate::graphics::{Color, Modifier, Style}; | ||
|
||
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { | ||
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") | ||
let raw_theme: Value = toml::from_slice(include_bytes!("../../theme.toml")) | ||
.expect("Failed to parse default theme"); | ||
Theme::from(raw_theme) | ||
}); | ||
pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { | ||
toml::from_slice(include_bytes!("../../base16_theme.toml")) | ||
.expect("Failed to parse base 16 default theme") | ||
let raw_theme: Value = toml::from_slice(include_bytes!("../../base16_theme.toml")) | ||
.expect("Failed to parse base 16 default theme"); | ||
Theme::from(raw_theme) | ||
}); | ||
|
||
#[derive(Clone, Debug)] | ||
|
@@ -35,24 +38,51 @@ impl Loader { | |
} | ||
|
||
/// 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" { | ||
return Ok(self.default()); | ||
} | ||
if name == "base16_default" { | ||
return Ok(self.base16_default()); | ||
} | ||
let filename = format!("{}.toml", name); | ||
|
||
let user_path = self.user_dir.join(&filename); | ||
let path = if user_path.exists() { | ||
user_path | ||
self.load_theme(name, name, false).map(Theme::from) | ||
} | ||
|
||
// 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 { | ||
self.default_dir.join(filename) | ||
theme_toml | ||
}; | ||
|
||
let data = std::fs::read(&path)?; | ||
toml::from_slice(data.as_slice()).context("Failed to deserialize theme") | ||
Ok(theme_toml) | ||
} | ||
|
||
pub fn read_names(path: &Path) -> Vec<String> { | ||
|
@@ -70,6 +100,53 @@ impl Loader { | |
.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 | ||
pub fn names(&self) -> Vec<String> { | ||
let mut names = Self::read_names(&self.user_dir); | ||
|
@@ -105,16 +182,16 @@ pub struct Theme { | |
highlights: Vec<Style>, | ||
} | ||
|
||
impl<'de> Deserialize<'de> for Theme { | ||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||
where | ||
D: Deserializer<'de>, | ||
{ | ||
impl From<Value> for Theme { | ||
fn from(value: Value) -> Self { | ||
let mut styles = HashMap::new(); | ||
let mut scopes = Vec::new(); | ||
let mut highlights = Vec::new(); | ||
|
||
if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) { | ||
let theme_values: Result<HashMap<String, Value>> = | ||
toml::from_str(&value.to_string()).context("Failed to load theme"); | ||
|
||
if let Ok(mut colors) = theme_values { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Should still work since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does for a |
||
// TODO: alert user of parsing failures in editor | ||
let palette = colors | ||
.remove("palette") | ||
|
@@ -126,6 +203,9 @@ impl<'de> Deserialize<'de> for Theme { | |
}) | ||
.unwrap_or_default(); | ||
|
||
// remove inherits from value to prevent errors | ||
let _ = colors.remove("inherits"); | ||
|
||
styles.reserve(colors.len()); | ||
scopes.reserve(colors.len()); | ||
highlights.reserve(colors.len()); | ||
|
@@ -143,11 +223,11 @@ impl<'de> Deserialize<'de> for Theme { | |
} | ||
} | ||
|
||
Ok(Self { | ||
scopes, | ||
Self { | ||
styles, | ||
scopes, | ||
highlights, | ||
}) | ||
} | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the change? It now takes extra steps since you first call
from_slice
thenTheme::from
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I implemented the
From
trait forTheme
because I could not directly deserialize from a file. Since we would need to load the parent theme while deserializing. A pattern which I did not like.But we could implement both and share the code, I'll provide a solution.