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

The ability to define global theme overrides #5839

Closed
wants to merge 9 commits into from
Closed
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
5 changes: 3 additions & 2 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,15 @@ impl Application {
let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs));

let true_color = config.editor.true_color || crate::true_color();

let theme = config
.theme
.as_ref()
.and_then(|theme| {
theme_loader
.load(theme)
.map_err(|e| {
log::warn!("failed to load theme `{}` - {}", theme, e);
log::warn!("failed to load theme `{}` - {}", theme.name, e);
e
})
.ok()
Expand Down Expand Up @@ -405,7 +406,7 @@ impl Application {
self.theme_loader
.load(theme)
.map_err(|e| {
log::warn!("failed to load theme `{}` - {}", theme, e);
log::warn!("failed to load theme `{}` - {}", theme.name, e);
e
})
.ok()
Expand Down
12 changes: 10 additions & 2 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,12 @@ fn theme(
event: PromptEvent,
) -> anyhow::Result<()> {
let true_color = cx.editor.config.load().true_color || crate::true_color();

// Take the new theme name and return a theme config with currently applied overrides
use helix_view::theme::ThemeConfig;
let inherit_overrides =
|name: String| ThemeConfig::new(name, cx.editor.theme.overrides.clone());

match event {
PromptEvent::Abort => {
cx.editor.unset_theme_preview();
Expand All @@ -853,7 +859,8 @@ fn theme(
// Ensures that a preview theme gets cleaned up if the user backspaces until the prompt is empty.
cx.editor.unset_theme_preview();
} else if let Some(theme_name) = args.first() {
if let Ok(theme) = cx.editor.theme_loader.load(theme_name) {
let theme_config = inherit_overrides(theme_name.to_string());
if let Ok(theme) = cx.editor.theme_loader.load(&theme_config) {
if !(true_color || theme.is_16_color()) {
bail!("Unsupported theme: theme requires true color support");
}
Expand All @@ -863,10 +870,11 @@ fn theme(
}
PromptEvent::Validate => {
if let Some(theme_name) = args.first() {
let theme_config = inherit_overrides(theme_name.to_string());
let theme = cx
.editor
.theme_loader
.load(theme_name)
.load(&theme_config)
.map_err(|err| anyhow::anyhow!("Could not load theme: {}", err))?;
if !(true_color || theme.is_16_color()) {
bail!("Unsupported theme: theme requires true color support");
Expand Down
6 changes: 4 additions & 2 deletions helix-term/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::keymap;
use crate::keymap::{merge_keys, Keymap};
use helix_loader::merge_toml_values;
use helix_view::document::Mode;
use helix_view::theme::{opt_string_or_struct, ThemeConfig};
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt::Display;
Expand All @@ -11,15 +12,16 @@ use toml::de::Error as TomlError;

#[derive(Debug, Clone, PartialEq)]
pub struct Config {
pub theme: Option<String>,
pub theme: Option<ThemeConfig>,
pub keys: HashMap<Mode, Keymap>,
pub editor: helix_view::editor::Config,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfigRaw {
pub theme: Option<String>,
#[serde(default, deserialize_with = "opt_string_or_struct")]
pub theme: Option<ThemeConfig>,
pub keys: Option<HashMap<Mode, Keymap>>,
pub editor: Option<toml::Value>,
}
Expand Down
152 changes: 143 additions & 9 deletions helix-view/src/theme.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
use std::{
collections::{HashMap, HashSet},
fmt,
marker::PhantomData,
path::{Path, PathBuf},
str,
str::{self, FromStr},
};

use anyhow::{anyhow, Result};
use helix_core::hashmap;
use helix_loader::merge_toml_values;
use log::warn;
use once_cell::sync::Lazy;
use serde::{Deserialize, Deserializer};
use serde::{
de::{self, MapAccess, Visitor},
Deserialize, Deserializer,
};
use toml::{map::Map, Value};

use crate::graphics::UnderlineStyle;
Expand Down Expand Up @@ -52,16 +57,37 @@ impl Loader {
}

/// Loads a theme searching directories in priority order.
pub fn load(&self, name: &str) -> Result<Theme> {
pub fn load(&self, config: &ThemeConfig) -> Result<Theme> {
// Persist the overrides in the Theme to be able to access them
// while using ":theme"
//
// The default themes also need to be overriden. Otherwise, while
// browsing through the options, `Theme.overrides` is set to `None`
// causing us to lose them until the session is restarted
let persist_overrides = |theme: Theme| -> Theme {
Theme {
overrides: config.overrides.clone(),
..theme
}
};

let name = &config.name;
if name == "default" {
return Ok(self.default());
let default = self.default();
let default = persist_overrides(default);
return Ok(default);
}
if name == "base16_default" {
return Ok(self.base16_default());
let base16_default = self.base16_default();
let base16_default = persist_overrides(base16_default);
return Ok(base16_default);
}

let mut visited_paths = HashSet::new();
let theme = self.load_theme(name, &mut visited_paths).map(Theme::from)?;
let theme = self
.load_theme(config, &mut visited_paths)
.map(Theme::from)?;
let theme = persist_overrides(theme);

Ok(Theme {
name: name.into(),
Expand All @@ -78,9 +104,13 @@ impl Loader {
/// so long as the second file is in a themes directory with lower priority.
/// However, it is not recommended that users do this as it will make tracing
/// errors more difficult.
fn load_theme(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<Value> {
fn load_theme(
&self,
config: &ThemeConfig,
visited_paths: &mut HashSet<PathBuf>,
) -> Result<Value> {
let name = &config.name;
let path = self.path(name, visited_paths)?;

let theme_toml = self.load_toml(path)?;

let inherits = theme_toml.get("inherits");
Expand All @@ -97,14 +127,26 @@ impl Loader {
// load default themes's toml from const.
"default" => DEFAULT_THEME_DATA.clone(),
"base16_default" => BASE16_DEFAULT_THEME_DATA.clone(),
_ => self.load_theme(parent_theme_name, visited_paths)?,
_ => {
let parent_theme_config = ThemeConfig {
name: parent_theme_name.to_string(),
overrides: None,
};
self.load_theme(&parent_theme_config, visited_paths)?
}
};

self.merge_themes(parent_theme_toml, theme_toml)
} else {
theme_toml
};

let theme_toml = if let Some(overrides) = config.overrides.clone() {
self.merge_themes(theme_toml, overrides)
} else {
theme_toml
};

Ok(theme_toml)
}

Expand Down Expand Up @@ -216,6 +258,7 @@ pub struct Theme {
// tree-sitter highlight styles are stored in a Vec to optimize lookups
scopes: Vec<String>,
highlights: Vec<Style>,
pub overrides: Option<Value>,
}

impl From<Value> for Theme {
Expand Down Expand Up @@ -501,6 +544,97 @@ impl TryFrom<Value> for ThemePalette {
}
}

#[derive(Clone, Default, PartialEq, Debug, Deserialize)]
pub struct ThemeConfig {
pub name: String,
pub overrides: Option<Value>,
}

impl ThemeConfig {
pub fn new(name: String, overrides: Option<Value>) -> Self {
Self { name, overrides }
}
}

impl FromStr for ThemeConfig {
type Err = anyhow::Error;
fn from_str(name: &str) -> Result<Self, Self::Err> {
Ok(Self {
name: name.to_string(),
overrides: None,
})
}
}

fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Deserialize<'de> + FromStr<Err = anyhow::Error>,
D: Deserializer<'de>,
{
struct StringOrStruct<T>(PhantomData<fn() -> T>);

impl<'de, T> Visitor<'de> for StringOrStruct<T>
where
T: Deserialize<'de> + FromStr<Err = anyhow::Error>,
{
type Value = T;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Either a TOML table for theme or 'theme=<value>'")
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(FromStr::from_str(value).unwrap())
}

fn visit_map<M>(self, visitor: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
Deserialize::deserialize(de::value::MapAccessDeserializer::new(visitor))
}
}

deserializer.deserialize_any(StringOrStruct(PhantomData))
}

pub fn opt_string_or_struct<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: Deserialize<'de> + FromStr<Err = anyhow::Error>,
D: Deserializer<'de>,
{
struct OptStringOrStruct<T>(PhantomData<T>);

impl<'de, T> Visitor<'de> for OptStringOrStruct<T>
where
T: Deserialize<'de> + FromStr<Err = anyhow::Error>,
{
type Value = Option<T>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Either a TOML table for theme or 'theme=<value>'. Or None")
}

fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}

fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
string_or_struct(deserializer).map(Some)
}
}

deserializer.deserialize_option(OptStringOrStruct(PhantomData))
}
#[cfg(test)]
mod tests {
use super::*;
Expand Down