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

Add: validation of bundled themes in build workflow #11627

Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v4

- name: Install stable toolchain
uses: dtolnay/[email protected]

Expand Down Expand Up @@ -107,6 +108,9 @@ jobs:
- name: Validate queries
run: cargo xtask query-check

- name: Validate themes
run: cargo xtask theme-check

- name: Generate docs
run: cargo xtask docgen

Expand Down
116 changes: 67 additions & 49 deletions helix-view/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,34 @@ 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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging theme name along with 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());
return Ok((self.default(), Vec::new()));
}
if name == "base16_default" {
return Ok(self.base16_default());
return Ok((self.base16_default(), Vec::new()));
}

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

Ok(Theme {
let theme = Theme {
name: name.into(),
..theme
})
};
Ok((theme, warnings))
}

/// Recursively load a theme, merging with any inherited parent themes.
Expand All @@ -87,10 +101,7 @@ 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!(
"Theme: expected 'inherits' to be a string: {}",
parent_theme_name
)
anyhow!("Expected 'inherits' to be a string: {}", parent_theme_name)
})?;

let parent_theme_toml = match parent_theme_name {
Expand Down Expand Up @@ -181,9 +192,9 @@ impl Loader {
})
.ok_or_else(|| {
if cycle_found {
anyhow!("Theme: cycle found in inheriting: {}", name)
anyhow!("Cycle found in inheriting: {}", name)
} else {
anyhow!("Theme: file not found for: {}", name)
anyhow!("File not found for: {}", name)
}
})
}
Expand Down Expand Up @@ -220,19 +231,11 @@ pub struct Theme {

impl From<Value> for Theme {
fn from(value: Value) -> Self {
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()
let (theme, warnings) = Theme::from_toml(value);
for warning in warnings {
warn!("{}", warning);
}
theme
}
}

Expand All @@ -242,31 +245,29 @@ impl<'de> Deserialize<'de> for Theme {
D: Deserializer<'de>,
{
let values = Map::<String, Value>::deserialize(deserializer)?;

let (styles, scopes, highlights) = build_theme_values(values);

Ok(Self {
styles,
scopes,
highlights,
..Default::default()
})
let (theme, warnings) = Theme::from_keys(values);
for warning in warnings {
warn!("{}", warning);
}
Ok(theme)
}
}

fn build_theme_values(
mut values: Map<String, Value>,
) -> (HashMap<String, Style>, Vec<String>, Vec<Style>) {
) -> (HashMap<String, Style>, Vec<String>, Vec<Style>, Vec<String>) {
let mut styles = HashMap::new();
let mut scopes = Vec::new();
let mut highlights = Vec::new();

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| {
warn!("{}", err);
warnings.push(err);
ThemePalette::default()
})
})
Expand All @@ -279,7 +280,7 @@ fn build_theme_values(
for (name, style_value) in values {
let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) {
warn!("{}", err);
warnings.push(err);
}

// these are used both as UI and as highlights
Expand All @@ -288,7 +289,7 @@ fn build_theme_values(
highlights.push(style);
}

(styles, scopes, highlights)
(styles, scopes, highlights, warnings)
}

impl Theme {
Expand Down Expand Up @@ -354,6 +355,27 @@ impl Theme {
.all(|color| !matches!(color, Some(Color::Rgb(..))))
})
}

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, load_errors) = build_theme_values(toml_keys);

let theme = Self {
styles,
scopes,
highlights,
..Default::default()
};
(theme, load_errors)
}
}

struct ThemePalette {
Expand Down Expand Up @@ -408,7 +430,7 @@ impl ThemePalette {
if let Ok(index) = s.parse::<u8>() {
return Ok(Color::Indexed(index));
}
Err(format!("Theme: malformed ANSI: {}", s))
Err(format!("Malformed ANSI: {}", s))
}

fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
Expand All @@ -422,13 +444,13 @@ impl ThemePalette {
}
}

Err(format!("Theme: malformed hexcode: {}", s))
Err(format!("Malformed hexcode: {}", s))
}

fn parse_value_as_str(value: &Value) -> Result<&str, String> {
value
.as_str()
.ok_or(format!("Theme: unrecognized value: {}", value))
.ok_or(format!("Unrecognized value: {}", value))
}

pub fn parse_color(&self, value: Value) -> Result<Color, String> {
Expand All @@ -445,14 +467,14 @@ impl ThemePalette {
value
.as_str()
.and_then(|s| s.parse().ok())
.ok_or(format!("Theme: invalid modifier: {}", value))
.ok_or(format!("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!("Theme: invalid underline style: {}", value))
.ok_or(format!("Invalid underline style: {}", value))
}

pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> {
Expand All @@ -462,9 +484,7 @@ 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("Theme: underline must be table")?;
let table = value.as_table_mut().ok_or("Underline must be table")?;
if let Some(value) = table.remove("color") {
*style = style.underline_color(self.parse_color(value)?);
}
Expand All @@ -473,13 +493,11 @@ impl ThemePalette {
}

if let Some(attr) = table.keys().next() {
return Err(format!("Theme: invalid underline attribute: {attr}"));
return Err(format!("Invalid underline attribute: {attr}"));
}
}
"modifiers" => {
let modifiers = value
.as_array()
.ok_or("Theme: modifiers should be an array")?;
let modifiers = value.as_array().ok_or("Modifiers should be an array")?;

for modifier in modifiers {
if modifier
Expand All @@ -492,7 +510,7 @@ impl ThemePalette {
}
}
}
_ => return Err(format!("Theme: invalid style attribute: {}", name)),
_ => return Err(format!("Invalid style attribute: {}", name)),
}
}
} else {
Expand Down
6 changes: 4 additions & 2 deletions runtime/themes/autumn.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@
"ui.statusline.select" = { fg = "my_gray7", bg = "my_black", modifiers = ["bold"] }
"ui.text.focus" = "my_white1"
"ui.text" = "my_white1"
"ui.virtual.inlay-hint" = { fg = "my_gray4", bg="my_black", modifiers = ["normal"] }
"ui.virtual.inlay-hint.parameter" = { fg = "my_gray4", modifiers = ["normal"] }
# Invalid modifier: "normal". See 'https://github.com/helix-editor/helix/issues/5709'
"ui.virtual.inlay-hint" = { fg = "my_gray4", bg="my_black" } #, modifiers = ["normal"] }
# "ui.virtual.inlay-hint.parameter" = { fg = "my_gray4", modifiers = ["normal"] }
"ui.virtual.inlay-hint.parameter" = "my_gray4"
"ui.virtual.inlay-hint.type" = { fg = "my_gray4", modifiers = ["italic"] }
"ui.virtual.jump-label" = { fg = "my_yellow2", modifiers = ["bold"] }
"ui.virtual.ruler" = { bg = "my_gray1" }
Expand Down
3 changes: 2 additions & 1 deletion runtime/themes/emacs.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
"ui.menu.selected" = { fg = "dark_red", bg = "light_blue" }
"ui.selection" = { bg = "lightgoldenrod1" }
"ui.selection.primary" = { bg = "lightgoldenrod2" }
"ui.virtual.whitespace" = "highlight"
# Malformed ANSI: highlight. See 'https://github.com/helix-editor/helix/issues/5709'
# "ui.virtual.whitespace" = "highlight"
"ui.virtual.ruler" = { bg = "gray95" }
"ui.virtual.inlay-hint" = { fg = "gray75" }
"ui.cursorline.primary" = { bg = "darkseagreen2" }
Expand Down
7 changes: 5 additions & 2 deletions runtime/themes/flatwhite.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@
"ui.virtual" = { fg = "base5", bg = "base6" }
"ui.virtual.whitespace" = { fg = "base5" }
"ui.virtual.ruler" = { bg = "base6" }
"ui.virtual.inlay-hint" = { fg = "base4", modifiers = ["normal"] }
"ui.virtual.inlay-hint.parameter" = { fg = "base3", modifiers = ["normal"] }
# Invalid modifier: "normal". See 'https://github.com/helix-editor/helix/issues/5709'
# "ui.virtual.inlay-hint" = { fg = "base4", modifiers = ["normal"] }
# "ui.virtual.inlay-hint.parameter" = { fg = "base3", modifiers = ["normal"] }
"ui.virtual.inlay-hint" = "base4"
"ui.virtual.inlay-hint.parameter" = "base3"
"ui.virtual.inlay-hint.type" = { fg = "base3", modifiers = ["italic"] }

"ui.linenr" = { bg = "base6" }
Expand Down
3 changes: 2 additions & 1 deletion runtime/themes/kanagawa.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"ui.statusline.normal" = { fg = "sumiInk0", bg = "crystalBlue", modifiers = ["bold"] }
"ui.statusline.insert" = { fg = "sumiInk0", bg = "autumnGreen", modifiers = ["bold"] }
"ui.statusline.select" = { fg = "sumiInk0", bg = "oniViolet", modifiers = ["bold"] }
"ui.statusline.separator" = { fg = "", bg = "" }
# Malformed ANSI: "". See 'https://github.com/helix-editor/helix/issues/5709'
# "ui.statusline.separator" = { fg = "", bg = "" }

"ui.bufferline" = { fg = "fujiGray", bg = "sumiInk0" }
"ui.bufferline.active" = { fg = "oldWhite", bg = "sumiInk0" }
Expand Down
3 changes: 2 additions & 1 deletion runtime/themes/monokai.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"ui.statusline" = { fg = "active_text", bg = "#414339" }
"ui.statusline.inactive" = { fg = "active_text", bg = "#75715e" }

"ui.bufferline" = { fg = "grey2", bg = "bg3" }
# Malformed ANSI: grey2, bg3. See 'https://github.com/helix-editor/helix/issues/5709'
# "ui.bufferline" = { fg = "grey2", bg = "bg3" }
"ui.bufferline.active" = { fg = "active_text", bg = "selection", modifiers = [
"bold",
] }
Expand Down
9 changes: 6 additions & 3 deletions runtime/themes/monokai_aqua.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ inherits = "monokai"

"type" = { fg = "type", modifiers = ["bold"] }

"ui.statusline.normal" = { fg = "light-black", bg = "cyan" }
"ui.statusline.insert" = { fg = "light-black", bg = "green" }
"ui.statusline.select" = { fg = "light-black", bg = "purple" }
# Malformed ANSI: light-black, purple. See 'https://github.com/helix-editor/helix/issues/5709'
# "ui.statusline.normal" = { fg = "light-black", bg = "cyan" }
"ui.statusline.normal" = { bg = "cyan" }
# "ui.statusline.insert" = { fg = "light-black", bg = "green" }
"ui.statusline.insert" = { bg = "green" }
# "ui.statusline.select" = { fg = "light-black", bg = "purple" }

"ui.virtual.jump-label" = { fg = "cyan", modifiers = ["bold"] }

Expand Down
3 changes: 2 additions & 1 deletion runtime/themes/zed_onedark.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
"ui.cursor" = { fg = "white", modifiers = ["reversed"] }
"ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] }
"ui.cursor.match" = { fg = "blue", modifiers = ["underlined"] }
"ui.cursor.insert" = { fg = "dark-blue" }
# Malformed ANSI: dark-blue. See 'https://github.com/helix-editor/helix/issues/5709'
# "ui.cursor.insert" = { fg = "dark-blue" }

"ui.selection" = { bg = "faint-gray" }
"ui.selection.primary" = { bg = "#293b5bff" }
Expand Down
7 changes: 7 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod docgen;
mod helpers;
mod path;
mod querycheck;
mod theme_check;

use std::{env, error::Error};

Expand All @@ -11,6 +12,7 @@ pub mod tasks {
use crate::docgen::{lang_features, typable_commands, write};
use crate::docgen::{LANG_SUPPORT_MD_OUTPUT, TYPABLE_COMMANDS_MD_OUTPUT};
use crate::querycheck::query_check;
use crate::theme_check::theme_check;
use crate::DynError;

pub fn docgen() -> Result<(), DynError> {
Expand All @@ -23,6 +25,10 @@ pub mod tasks {
query_check()
}

pub fn themecheck() -> Result<(), DynError> {
theme_check()
}

pub fn print_help() {
println!(
"
Expand All @@ -43,6 +49,7 @@ fn main() -> Result<(), DynError> {
Some(t) => match t.as_str() {
"docgen" => tasks::docgen()?,
"query-check" => tasks::querycheck()?,
"theme-check" => tasks::themecheck()?,
invalid => return Err(format!("Invalid task name: {}", invalid).into()),
},
};
Expand Down
Loading