Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/theme.rs')
-rw-r--r--helix-view/src/theme.rs339
1 files changed, 62 insertions, 277 deletions
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 173a40f3..4acc5664 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -5,7 +5,7 @@ use std::{
};
use anyhow::{anyhow, Result};
-use helix_core::{hashmap, syntax::Highlight};
+use helix_core::hashmap;
use helix_loader::merge_toml_values;
use log::warn;
use once_cell::sync::Lazy;
@@ -35,75 +35,6 @@ pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| Theme {
..Theme::from(BASE16_DEFAULT_THEME_DATA.clone())
});
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum Mode {
- Dark,
- Light,
-}
-
-#[cfg(feature = "term")]
-impl From<termina::escape::csi::ThemeMode> for Mode {
- fn from(mode: termina::escape::csi::ThemeMode) -> Self {
- match mode {
- termina::escape::csi::ThemeMode::Dark => Self::Dark,
- termina::escape::csi::ThemeMode::Light => Self::Light,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct Config {
- light: String,
- dark: String,
- /// A theme to choose when the terminal did not declare either light or dark mode.
- /// When not specified the dark theme is preferred.
- fallback: Option<String>,
-}
-
-impl Config {
- pub fn choose(&self, preference: Option<Mode>) -> &str {
- match preference {
- Some(Mode::Light) => &self.light,
- Some(Mode::Dark) => &self.dark,
- None => self.fallback.as_ref().unwrap_or(&self.dark),
- }
- }
-}
-
-impl<'de> Deserialize<'de> for Config {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: serde::Deserializer<'de>,
- {
- #[derive(Deserialize)]
- #[serde(untagged, deny_unknown_fields, rename_all = "kebab-case")]
- enum InnerConfig {
- Constant(String),
- Adaptive {
- dark: String,
- light: String,
- fallback: Option<String>,
- },
- }
-
- let inner = InnerConfig::deserialize(deserializer)?;
-
- let (light, dark, fallback) = match inner {
- InnerConfig::Constant(theme) => (theme.clone(), theme.clone(), None),
- InnerConfig::Adaptive {
- light,
- dark,
- fallback,
- } => (light, dark, fallback),
- };
-
- Ok(Self {
- light,
- dark,
- fallback,
- })
- }
-}
#[derive(Clone, Debug)]
pub struct Loader {
/// Theme directories to search from highest to lowest priority
@@ -122,34 +53,20 @@ impl Loader {
/// Loads a theme searching directories in priority order.
pub fn load(&self, name: &str) -> Result<Theme> {
- let (theme, warnings) = self.load_with_warnings(name)?;
-
- for warning in warnings {
- warn!("Theme '{}': {}", name, warning);
- }
-
- Ok(theme)
- }
-
- /// Loads a theme searching directories in priority order, returning any warnings
- pub fn load_with_warnings(&self, name: &str) -> Result<(Theme, Vec<String>)> {
if name == "default" {
- return Ok((self.default(), Vec::new()));
+ return Ok(self.default());
}
if name == "base16_default" {
- return Ok((self.base16_default(), Vec::new()));
+ return Ok(self.base16_default());
}
let mut visited_paths = HashSet::new();
- let (theme, warnings) = self
- .load_theme(name, &mut visited_paths)
- .map(Theme::from_toml)?;
+ let theme = self.load_theme(name, &mut visited_paths).map(Theme::from)?;
- let theme = Theme {
+ Ok(Theme {
name: name.into(),
..theme
- };
- Ok((theme, warnings))
+ })
}
/// Recursively load a theme, merging with any inherited parent themes.
@@ -170,7 +87,10 @@ impl Loader {
let theme_toml = if let Some(parent_theme_name) = inherits {
let parent_theme_name = parent_theme_name.as_str().ok_or_else(|| {
- anyhow!("Expected 'inherits' to be a string: {}", parent_theme_name)
+ anyhow!(
+ "Theme: expected 'inherits' to be a string: {}",
+ parent_theme_name
+ )
})?;
let parent_theme_toml = match parent_theme_name {
@@ -261,9 +181,9 @@ impl Loader {
})
.ok_or_else(|| {
if cycle_found {
- anyhow!("Cycle found in inheriting: {}", name)
+ anyhow!("Theme: cycle found in inheriting: {}", name)
} else {
- anyhow!("File not found for: {}", name)
+ anyhow!("Theme: file not found for: {}", name)
}
})
}
@@ -296,16 +216,23 @@ pub struct Theme {
// tree-sitter highlight styles are stored in a Vec to optimize lookups
scopes: Vec<String>,
highlights: Vec<Style>,
- rainbow_length: usize,
}
impl From<Value> for Theme {
fn from(value: Value) -> Self {
- let (theme, warnings) = Theme::from_toml(value);
- for warning in warnings {
- warn!("{}", warning);
+ if let Value::Table(table) = value {
+ let (styles, scopes, highlights) = build_theme_values(table);
+
+ Self {
+ styles,
+ scopes,
+ highlights,
+ ..Default::default()
+ }
+ } else {
+ warn!("Expected theme TOML value to be a table, found {:?}", value);
+ Default::default()
}
- theme
}
}
@@ -315,37 +242,31 @@ impl<'de> Deserialize<'de> for Theme {
D: Deserializer<'de>,
{
let values = Map::<String, Value>::deserialize(deserializer)?;
- let (theme, warnings) = Theme::from_keys(values);
- for warning in warnings {
- warn!("{}", warning);
- }
- Ok(theme)
+
+ let (styles, scopes, highlights) = build_theme_values(values);
+
+ Ok(Self {
+ styles,
+ scopes,
+ highlights,
+ ..Default::default()
+ })
}
}
-#[allow(clippy::type_complexity)]
fn build_theme_values(
mut values: Map<String, Value>,
-) -> (
- HashMap<String, Style>,
- Vec<String>,
- Vec<Style>,
- usize,
- Vec<String>,
-) {
+) -> (HashMap<String, Style>, Vec<String>, Vec<Style>) {
let mut styles = HashMap::new();
let mut scopes = Vec::new();
let mut highlights = Vec::new();
- let mut rainbow_length = 0;
-
- let mut warnings = Vec::new();
// TODO: alert user of parsing failures in editor
let palette = values
.remove("palette")
.map(|value| {
ThemePalette::try_from(value).unwrap_or_else(|err| {
- warnings.push(err);
+ warn!("{}", err);
ThemePalette::default()
})
})
@@ -355,31 +276,10 @@ fn build_theme_values(
styles.reserve(values.len());
scopes.reserve(values.len());
highlights.reserve(values.len());
-
- for (i, style) in values
- .remove("rainbow")
- .and_then(|value| match palette.parse_style_array(value) {
- Ok(styles) => Some(styles),
- Err(err) => {
- warnings.push(err);
- None
- }
- })
- .unwrap_or_else(default_rainbow)
- .into_iter()
- .enumerate()
- {
- let name = format!("rainbow.{i}");
- styles.insert(name.clone(), style);
- scopes.push(name);
- highlights.push(style);
- rainbow_length += 1;
- }
-
for (name, style_value) in values {
let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) {
- warnings.push(format!("Failed to parse style for key {name:?}. {err}"));
+ warn!("{}", err);
}
// these are used both as UI and as highlights
@@ -388,51 +288,18 @@ fn build_theme_values(
highlights.push(style);
}
- (styles, scopes, highlights, rainbow_length, warnings)
+ (styles, scopes, highlights)
}
-fn default_rainbow() -> Vec<Style> {
- vec![
- Style::default().fg(Color::Red),
- Style::default().fg(Color::Yellow),
- Style::default().fg(Color::Green),
- Style::default().fg(Color::Blue),
- Style::default().fg(Color::Cyan),
- Style::default().fg(Color::Magenta),
- ]
-}
impl Theme {
- /// To allow `Highlight` to represent arbitrary RGB colors without turning it into an enum,
- /// we interpret the last 256^3 numbers as RGB.
- const RGB_START: u32 = (u32::MAX << (8 + 8 + 8)) - 1 - (u32::MAX - Highlight::MAX);
-
- /// Interpret a Highlight with the RGB foreground
- fn decode_rgb_highlight(highlight: Highlight) -> Option<(u8, u8, u8)> {
- (highlight.get() > Self::RGB_START).then(|| {
- let [b, g, r, ..] = (highlight.get() + 1).to_le_bytes();
- (r, g, b)
- })
- }
-
- /// Create a Highlight that represents an RGB color
- pub fn rgb_highlight(r: u8, g: u8, b: u8) -> Highlight {
- // -1 because highlight is "non-max": u32::MAX is reserved for the null pointer
- // optimization.
- Highlight::new(u32::from_le_bytes([b, g, r, u8::MAX]) - 1)
- }
-
#[inline]
- pub fn highlight(&self, highlight: Highlight) -> Style {
- if let Some((red, green, blue)) = Self::decode_rgb_highlight(highlight) {
- Style::new().fg(Color::Rgb(red, green, blue))
- } else {
- self.highlights[highlight.idx()]
- }
+ pub fn highlight(&self, index: usize) -> Style {
+ self.highlights[index]
}
#[inline]
- pub fn scope(&self, highlight: Highlight) -> &str {
- &self.scopes[highlight.idx()]
+ pub fn scope(&self, index: usize) -> &str {
+ &self.scopes[index]
}
pub fn name(&self) -> &str {
@@ -463,16 +330,13 @@ impl Theme {
&self.scopes
}
- pub fn find_highlight_exact(&self, scope: &str) -> Option<Highlight> {
- self.scopes()
- .iter()
- .position(|s| s == scope)
- .map(|idx| Highlight::new(idx as u32))
+ pub fn find_scope_index_exact(&self, scope: &str) -> Option<usize> {
+ self.scopes().iter().position(|s| s == scope)
}
- pub fn find_highlight(&self, mut scope: &str) -> Option<Highlight> {
+ pub fn find_scope_index(&self, mut scope: &str) -> Option<usize> {
loop {
- if let Some(highlight) = self.find_highlight_exact(scope) {
+ if let Some(highlight) = self.find_scope_index_exact(scope) {
return Some(highlight);
}
if let Some(new_end) = scope.rfind('.') {
@@ -490,33 +354,6 @@ impl Theme {
.all(|color| !matches!(color, Some(Color::Rgb(..))))
})
}
-
- pub fn rainbow_length(&self) -> usize {
- self.rainbow_length
- }
-
- fn from_toml(value: Value) -> (Self, Vec<String>) {
- if let Value::Table(table) = value {
- Theme::from_keys(table)
- } else {
- warn!("Expected theme TOML value to be a table, found {:?}", value);
- Default::default()
- }
- }
-
- fn from_keys(toml_keys: Map<String, Value>) -> (Self, Vec<String>) {
- let (styles, scopes, highlights, rainbow_length, load_errors) =
- build_theme_values(toml_keys);
-
- let theme = Self {
- styles,
- scopes,
- highlights,
- rainbow_length,
- ..Default::default()
- };
- (theme, load_errors)
- }
}
struct ThemePalette {
@@ -571,7 +408,7 @@ impl ThemePalette {
if let Ok(index) = s.parse::<u8>() {
return Ok(Color::Indexed(index));
}
- Err(format!("Malformed ANSI: {}", s))
+ Err(format!("Theme: malformed ANSI: {}", s))
}
fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
@@ -585,13 +422,13 @@ impl ThemePalette {
}
}
- Err(format!("Malformed hexcode: {}", s))
+ Err(format!("Theme: malformed hexcode: {}", s))
}
fn parse_value_as_str(value: &Value) -> Result<&str, String> {
value
.as_str()
- .ok_or(format!("Unrecognized value: {}", value))
+ .ok_or(format!("Theme: unrecognized value: {}", value))
}
pub fn parse_color(&self, value: Value) -> Result<Color, String> {
@@ -608,14 +445,14 @@ impl ThemePalette {
value
.as_str()
.and_then(|s| s.parse().ok())
- .ok_or(format!("Invalid modifier: {}", value))
+ .ok_or(format!("Theme: invalid modifier: {}", value))
}
pub fn parse_underline_style(value: &Value) -> Result<UnderlineStyle, String> {
value
.as_str()
.and_then(|s| s.parse().ok())
- .ok_or(format!("Invalid underline style: {}", value))
+ .ok_or(format!("Theme: invalid underline style: {}", value))
}
pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> {
@@ -625,7 +462,9 @@ impl ThemePalette {
"fg" => *style = style.fg(self.parse_color(value)?),
"bg" => *style = style.bg(self.parse_color(value)?),
"underline" => {
- let table = value.as_table_mut().ok_or("Underline must be table")?;
+ let table = value
+ .as_table_mut()
+ .ok_or("Theme: underline must be table")?;
if let Some(value) = table.remove("color") {
*style = style.underline_color(self.parse_color(value)?);
}
@@ -634,21 +473,26 @@ impl ThemePalette {
}
if let Some(attr) = table.keys().next() {
- return Err(format!("Invalid underline attribute: {attr}"));
+ return Err(format!("Theme: invalid underline attribute: {attr}"));
}
}
"modifiers" => {
- let modifiers = value.as_array().ok_or("Modifiers should be an array")?;
+ let modifiers = value
+ .as_array()
+ .ok_or("Theme: modifiers should be an array")?;
for modifier in modifiers {
- if modifier.as_str() == Some("underlined") {
+ if modifier
+ .as_str()
+ .map_or(false, |modifier| modifier == "underlined")
+ {
*style = style.underline_style(UnderlineStyle::Line);
} else {
*style = style.add_modifier(Self::parse_modifier(modifier)?);
}
}
}
- _ => return Err(format!("Invalid style attribute: {}", name)),
+ _ => return Err(format!("Theme: invalid style attribute: {}", name)),
}
}
} else {
@@ -656,21 +500,6 @@ impl ThemePalette {
}
Ok(())
}
-
- fn parse_style_array(&self, value: Value) -> Result<Vec<Style>, String> {
- let mut styles = Vec::new();
-
- for v in value
- .as_array()
- .ok_or_else(|| format!("Could not parse value as an array: '{value}'"))?
- {
- let mut style = Style::default();
- self.parse_style(&mut style, v.clone())?;
- styles.push(style);
- }
-
- Ok(styles)
- }
}
impl TryFrom<Value> for ThemePalette {
@@ -745,48 +574,4 @@ mod tests {
.add_modifier(Modifier::BOLD)
);
}
-
- // tests for parsing an RGB `Highlight`
-
- #[test]
- fn convert_to_and_from() {
- let (r, g, b) = (0xFF, 0xFE, 0xFA);
- let highlight = Theme::rgb_highlight(r, g, b);
- assert_eq!(Theme::decode_rgb_highlight(highlight), Some((r, g, b)));
- }
-
- /// make sure we can store all the colors at the end
- #[test]
- fn full_numeric_range() {
- assert_eq!(Highlight::MAX - Theme::RGB_START, 256_u32.pow(3));
- }
-
- #[test]
- fn retrieve_color() {
- // color in the middle
- let (r, g, b) = (0x14, 0xAA, 0xF7);
- assert_eq!(
- Theme::default().highlight(Theme::rgb_highlight(r, g, b)),
- Style::new().fg(Color::Rgb(r, g, b))
- );
- // pure black
- let (r, g, b) = (0x00, 0x00, 0x00);
- assert_eq!(
- Theme::default().highlight(Theme::rgb_highlight(r, g, b)),
- Style::new().fg(Color::Rgb(r, g, b))
- );
- // pure white
- let (r, g, b) = (0xff, 0xff, 0xff);
- assert_eq!(
- Theme::default().highlight(Theme::rgb_highlight(r, g, b)),
- Style::new().fg(Color::Rgb(r, g, b))
- );
- }
-
- #[test]
- #[should_panic(expected = "index out of bounds: the len is 0 but the index is 4278190078")]
- fn out_of_bounds() {
- let highlight = Highlight::new(Theme::rgb_highlight(0, 0, 0).get() - 1);
- Theme::default().highlight(highlight);
- }
}