use ratatui::style::{Modifier, Style as RStyle};
use crate::theme::Theme;
enum Role {
Keyword,
String,
Comment,
Function,
Type,
Constant,
Tag,
Namespace,
Invalid,
Number,
Attribute,
Operator,
Punctuation,
Variable,
Property,
Heading,
Link,
Code,
MarkupBold,
MarkupItalic,
MarkupStrike,
}
const CAPTURES: &[(&str, Role)] = &[
("keyword", Role::Keyword),
("conditional", Role::Keyword), ("repeat", Role::Keyword), ("include", Role::Keyword), ("exception", Role::Keyword), ("label", Role::Keyword), ("variable.builtin", Role::Keyword), ("string", Role::String),
("string.escape", Role::String),
("string.regexp", Role::String),
("string.special", Role::String),
("comment", Role::Comment),
("constant", Role::Constant), ("number", Role::Number),
("constant.numeric", Role::Number),
("boolean", Role::Constant),
("constructor", Role::Function), ("function", Role::Function),
("attribute", Role::Attribute),
("type", Role::Type), ("tag", Role::Tag),
("module", Role::Namespace),
("namespace", Role::Namespace), ("error", Role::Invalid), ("invalid", Role::Invalid), ("operator", Role::Operator),
("punctuation", Role::Punctuation),
("variable", Role::Variable),
("property", Role::Property),
("variable.member", Role::Property),
("text.title", Role::Heading),
("text.literal", Role::Code),
("text.emphasis", Role::MarkupItalic),
("text.strong", Role::MarkupBold),
("text.uri", Role::Link),
("punctuation.special", Role::Keyword), ("markup.heading", Role::Heading),
("markup.raw", Role::Code),
("markup.bold", Role::MarkupBold),
("markup.italic", Role::MarkupItalic),
("markup.strikethrough", Role::MarkupStrike),
("markup.list", Role::Keyword),
("markup.quote", Role::Comment),
("markup.link.url", Role::Link),
];
pub(super) fn capture_names() -> Vec<&'static str> {
CAPTURES.iter().map(|(name, _)| *name).collect()
}
pub(super) struct StyleTable {
styles: Vec<RStyle>,
default: RStyle,
}
impl StyleTable {
pub(super) fn new(theme: &Theme) -> Self {
let syntax = &theme.syntax;
let fg = |color| RStyle::new().fg(color);
let plain = fg(theme.semantic.text);
StyleTable {
default: plain,
styles: CAPTURES
.iter()
.map(|(_, role)| match role {
Role::Keyword => fg(syntax.keyword),
Role::String => fg(syntax.string),
Role::Comment => fg(syntax.comment).add_modifier(Modifier::ITALIC),
Role::Function => fg(syntax.function),
Role::Type => fg(syntax.type_),
Role::Constant => fg(syntax.constant),
Role::Tag => fg(syntax.tag),
Role::Namespace => fg(syntax.namespace),
Role::Invalid => fg(syntax.invalid),
Role::Number => fg(syntax.number.unwrap_or(syntax.constant)),
Role::Attribute => fg(syntax.attribute.unwrap_or(syntax.function)),
Role::Operator => fg(syntax.operator.unwrap_or(theme.semantic.text)),
Role::Punctuation => fg(syntax.punctuation.unwrap_or(theme.semantic.text)),
Role::Variable => fg(syntax.variable.unwrap_or(theme.semantic.text)),
Role::Property => fg(syntax.property.unwrap_or(theme.semantic.text)),
Role::Heading => fg(syntax.heading.unwrap_or(syntax.tag)),
Role::Link => {
fg(syntax.link.unwrap_or(syntax.string)).add_modifier(Modifier::UNDERLINED)
}
Role::Code => fg(syntax.code.unwrap_or(syntax.string)),
Role::MarkupBold => fg(syntax.constant).add_modifier(Modifier::BOLD),
Role::MarkupItalic => fg(syntax.keyword).add_modifier(Modifier::ITALIC),
Role::MarkupStrike => plain.add_modifier(Modifier::CROSSED_OUT),
})
.collect(),
}
}
pub(super) fn style(&self, slot: Option<usize>) -> RStyle {
match slot {
Some(i) => self.styles.get(i).copied().unwrap_or(self.default),
None => self.default,
}
}
pub(super) fn default_style(&self) -> RStyle {
self.default
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::style::Color;
fn capture(theme: &Theme, name: &str) -> RStyle {
let index = CAPTURES.iter().position(|(n, _)| *n == name).unwrap();
StyleTable::new(theme).style(Some(index))
}
#[test]
fn hone_distinguishes_numbers_constants_and_punctuation() {
let theme = Theme::hone();
assert_eq!(
capture(&theme, "number").fg,
Some(Color::from_u32(0xe39a7b))
);
assert_eq!(
capture(&theme, "constant").fg,
Some(Color::from_u32(0xe8a848))
);
assert_eq!(
capture(&theme, "punctuation").fg,
Some(Color::from_u32(0x9da3af))
);
assert!(
capture(&theme, "comment")
.add_modifier
.contains(Modifier::ITALIC)
);
assert!(
capture(&theme, "text.uri")
.add_modifier
.contains(Modifier::UNDERLINED)
);
}
#[test]
fn legacy_roles_and_overrides_remain_the_fallback() {
let mut theme = Theme::catppuccin_mocha();
theme.syntax.constant = Color::Red;
theme.semantic.text = Color::Blue;
assert_eq!(capture(&theme, "number").fg, Some(Color::Red));
assert_eq!(capture(&theme, "punctuation").fg, Some(Color::Blue));
theme.syntax.number = Some(Color::Green);
assert_eq!(capture(&theme, "number").fg, Some(Color::Green));
assert_eq!(capture(&theme, "constant").fg, Some(Color::Red));
}
}