Skip to content
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

Merged
merged 8 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions helix-view/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ term = ["crossterm"]
bitflags = "1.3"
anyhow = "1"
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-dap = { version = "0.6", path = "../helix-dap" }
crossterm = { version = "0.24", optional = true }
Expand Down
192 changes: 150 additions & 42 deletions helix-view/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,28 @@ 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(|| {
// 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")
});
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"))
.expect("Failed to parse base 16 default theme")
});
Expand All @@ -35,24 +44,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> {
Expand All @@ -70,6 +106,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);
Expand Down Expand Up @@ -105,52 +188,77 @@ pub struct Theme {
highlights: Vec<Style>,
}

impl From<Value> for Theme {
fn from(value: Value) -> Self {
Comment on lines +191 to +192
Copy link
Member

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 then Theme::from

Copy link
Contributor Author

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 for Theme 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.

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 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
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) {
// TODO: alert user of parsing failures in editor
let palette = colors
.remove("palette")
.map(|value| {
ThemePalette::try_from(value).unwrap_or_else(|err| {
warn!("{}", err);
ThemePalette::default()
})
})
.unwrap_or_default();

styles.reserve(colors.len());
scopes.reserve(colors.len());
highlights.reserve(colors.len());

for (name, style_value) in colors {
let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) {
warn!("{}", err);
}
let values = HashMap::<String, Value>::deserialize(deserializer)?;

// these are used both as UI and as highlights
styles.insert(name.clone(), style);
scopes.push(name);
highlights.push(style);
}
}
let (styles, scopes, highlights) = build_theme_values(Ok(values));

Ok(Self {
scopes,
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 scopes = Vec::new();
let mut highlights = Vec::new();

if let Ok(mut colors) = values {
// TODO: alert user of parsing failures in editor
let palette = colors
.remove("palette")
.map(|value| {
ThemePalette::try_from(value).unwrap_or_else(|err| {
warn!("{}", err);
ThemePalette::default()
})
})
.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());
for (name, style_value) in colors {
let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) {
warn!("{}", err);
}

// these are used both as UI and as highlights
styles.insert(name.clone(), style);
scopes.push(name);
highlights.push(style);
}
}

(styles, scopes, highlights)
}

impl Theme {
#[inline]
pub fn highlight(&self, index: usize) -> Style {
Expand Down