on master help sign in
Refactor theme parsing (#570)
25a8a47Gokul Soumya
helix-view/src/graphics.rs+23−1
@@ -1,5 +1,8 @@
1use bitflags::bitflags;
1use bitflags::bitflags;
2+use std::{
2−use std::cmp::{max, min};
3+ cmp::{max, min},
4+ str::FromStr,
5+};
3
6
4#[derive(Debug, Clone, Copy, PartialEq)]
7#[derive(Debug, Clone, Copy, PartialEq)]
5/// UNSTABLE
8/// UNSTABLE
@@ -237,6 +240,25 @@ bitflags! {
237 }
240 }
238}
241}
239
242
243+impl FromStr for Modifier {
244+ type Err = &'static str;
245+
246+ fn from_str(modifier: &str) -> Result<Self, Self::Err> {
247+ match modifier {
248+ "bold" => Ok(Self::BOLD),
249+ "dim" => Ok(Self::DIM),
250+ "italic" => Ok(Self::ITALIC),
251+ "underlined" => Ok(Self::UNDERLINED),
252+ "slow_blink" => Ok(Self::SLOW_BLINK),
253+ "rapid_blink" => Ok(Self::RAPID_BLINK),
254+ "reversed" => Ok(Self::REVERSED),
255+ "hidden" => Ok(Self::HIDDEN),
256+ "crossed_out" => Ok(Self::CROSSED_OUT),
257+ _ => Err("Invalid modifier"),
258+ }
259+ }
260+}
261+
240/// Style let you control the main characteristics of the displayed elements.
262/// Style let you control the main characteristics of the displayed elements.
241///
263///
242/// ```rust
264/// ```rust
helix-view/src/theme.rs+121−126
@@ -1,5 +1,6 @@
1use std::{
1use std::{
2 collections::HashMap,
2 collections::HashMap,
3+ convert::TryFrom,
3 path::{Path, PathBuf},
4 path::{Path, PathBuf},
4};
5};
5
6
@@ -11,8 +12,6 @@ use toml::Value;
11
12
12pub use crate::graphics::{Color, Modifier, Style};
13pub use crate::graphics::{Color, Modifier, Style};
13
14
14−/// Color theme for syntax highlighting.
15−
16pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
15pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
17 toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
16 toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
18});
17});
@@ -54,22 +53,10 @@ impl Loader {
54 .map(|entries| {
53 .map(|entries| {
55 entries
54 entries
56 .filter_map(|entry| {
55 .filter_map(|entry| {
57− if let Ok(entry) = entry {
56+ let entry = entry.ok()?;
58− let path = entry.path();
57+ let path = entry.path();
59− if let Some(ext) = path.extension() {
58+ (path.extension()? == "toml")
60− if ext != "toml" {
61− return None;
62− }
63− return Some(
64− entry
65− .file_name()
66− .to_string_lossy()
59+ .then(|| path.file_stem().unwrap().to_string_lossy().into_owned())
67− .trim_end_matches(".toml")
68− .to_owned(),
69− );
70− }
71− }
72− None
73 })
60 })
74 .collect()
61 .collect()
75 })
62 })
@@ -103,13 +90,23 @@ impl<'de> Deserialize<'de> for Theme {
103 let mut styles = HashMap::new();
90 let mut styles = HashMap::new();
104
91
105 if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
92 if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
93+ // TODO: alert user of parsing failures in editor
106− let palette = parse_palette(colors.remove("palette"));
94+ let palette = colors
107− // scopes.reserve(colors.len());
95+ .remove("palette")
96+ .map(|value| {
97+ ThemePalette::try_from(value).unwrap_or_else(|err| {
98+ warn!("{}", err);
99+ ThemePalette::default()
100+ })
101+ })
102+ .unwrap_or_default();
103+
108 styles.reserve(colors.len());
104 styles.reserve(colors.len());
109 for (name, style_value) in colors {
105 for (name, style_value) in colors {
110 let mut style = Style::default();
106 let mut style = Style::default();
111− parse_style(&mut style, style_value, &palette);
107+ if let Err(err) = palette.parse_style(&mut style, style_value) {
112− // scopes.push(name);
108+ warn!("{}", err);
109+ }
113 styles.insert(name, style);
110 styles.insert(name, style);
114 }
111 }
115 }
112 }
@@ -119,121 +116,120 @@ impl<'de> Deserialize<'de> for Theme {
119 }
116 }
120}
117}
121
118
122−fn parse_palette(value: Option<Value>) -> HashMap<String, Color> {
119+impl Theme {
123− match value {
120+ pub fn get(&self, scope: &str) -> Style {
124− Some(Value::Table(entries)) => entries,
121+ self.try_get(scope)
125− _ => return HashMap::default(),
122+ .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
126 }
123 }
127− .into_iter()
128− .filter_map(|(name, value)| {
129− let color = parse_color(value, &HashMap::default())?;
130− Some((name, color))
131− })
132− .collect()
133−}
134
124
135−fn parse_style(style: &mut Style, value: Value, palette: &HashMap<String, Color>) {
125+ pub fn try_get(&self, scope: &str) -> Option<Style> {
136− //TODO: alert user of parsing failures
126+ self.styles.get(scope).copied()
137− if let Value::Table(entries) = value {
138− for (name, value) in entries {
139− match name.as_str() {
140− "fg" => {
141− if let Some(color) = parse_color(value, palette) {
142− *style = style.fg(color);
143− }
144− }
145− "bg" => {
146− if let Some(color) = parse_color(value, palette) {
147− *style = style.bg(color);
148− }
149− }
150− "modifiers" => {
151− if let Value::Array(arr) = value {
152− for modifier in arr.iter().filter_map(parse_modifier) {
153− *style = style.add_modifier(modifier);
154− }
155− }
156− }
157− _ => (),
158− }
159− }
160− } else if let Some(color) = parse_color(value, palette) {
161− *style = style.fg(color);
162 }
127 }
163−}
164
128
165−fn hex_string_to_rgb(s: &str) -> Option<(u8, u8, u8)> {
129+ #[inline]
166− if s.starts_with('#') && s.len() >= 7 {
130+ pub fn scopes(&self) -> &[String] {
167− if let (Ok(red), Ok(green), Ok(blue)) = (
131+ &self.scopes
168− u8::from_str_radix(&s[1..3], 16),
132+ }
169− u8::from_str_radix(&s[3..5], 16),
133+
170− u8::from_str_radix(&s[5..7], 16),
134+ pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
171− ) {
135+ self.scopes().iter().position(|s| s == scope)
172− Some((red, green, blue))
173− } else {
174− None
175− }
176− } else {
177− None
178 }
136 }
179}
137}
180
138
181−fn parse_color(value: Value, palette: &HashMap<String, Color>) -> Option<Color> {
139+struct ThemePalette {
182− if let Value::String(s) = value {
140+ palette: HashMap<String, Color>,
183− if let Some(color) = palette.get(&s) {
141+}
184− Some(*color)
142+
185− } else if let Some((red, green, blue)) = hex_string_to_rgb(&s) {
143+impl Default for ThemePalette {
186− Some(Color::Rgb(red, green, blue))
144+ fn default() -> Self {
187− } else {
145+ Self::new(HashMap::new())
188− warn!("malformed hexcode in theme: {}", s);
189− None
190− }
191− } else {
192− warn!("unrecognized value in theme: {}", value);
193− None
194 }
146 }
195}
147}
196
148
197−fn parse_modifier(value: &Value) -> Option<Modifier> {
149+impl ThemePalette {
198− if let Value::String(s) = value {
150+ pub fn new(palette: HashMap<String, Color>) -> Self {
199− match s.as_str() {
151+ Self { palette }
200− "bold" => Some(Modifier::BOLD),
152+ }
201− "dim" => Some(Modifier::DIM),
153+
202− "italic" => Some(Modifier::ITALIC),
154+ pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
203− "underlined" => Some(Modifier::UNDERLINED),
155+ if s.starts_with('#') && s.len() >= 7 {
204− "slow_blink" => Some(Modifier::SLOW_BLINK),
156+ if let (Ok(red), Ok(green), Ok(blue)) = (
205− "rapid_blink" => Some(Modifier::RAPID_BLINK),
157+ u8::from_str_radix(&s[1..3], 16),
206− "reversed" => Some(Modifier::REVERSED),
158+ u8::from_str_radix(&s[3..5], 16),
207− "hidden" => Some(Modifier::HIDDEN),
159+ u8::from_str_radix(&s[5..7], 16),
208− "crossed_out" => Some(Modifier::CROSSED_OUT),
160+ ) {
209− _ => {
161+ return Ok(Color::Rgb(red, green, blue));
210− warn!("unrecognized modifier in theme: {}", s);
211− None
212 }
162 }
213 }
163 }
214− } else {
164+
215− warn!("unrecognized modifier in theme: {}", value);
165+ Err(format!("Theme: malformed hexcode: {}", s))
216− None
217 }
166 }
218−}
219
167
220−impl Theme {
168+ fn parse_value_as_str(value: &Value) -> Result<&str, String> {
221− pub fn get(&self, scope: &str) -> Style {
169+ value
222− self.try_get(scope)
170+ .as_str()
223− .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
171+ .ok_or(format!("Theme: unrecognized value: {}", value))
224 }
172 }
225
173
226− pub fn try_get(&self, scope: &str) -> Option<Style> {
174+ pub fn parse_color(&self, value: Value) -> Result<Color, String> {
175+ let value = Self::parse_value_as_str(&value)?;
176+
177+ self.palette
178+ .get(value)
227− self.styles.get(scope).copied()
179+ .copied()
180+ .ok_or("")
181+ .or_else(|_| Self::hex_string_to_rgb(value))
228 }
182 }
229
183
230− #[inline]
184+ pub fn parse_modifier(value: &Value) -> Result<Modifier, String> {
231− pub fn scopes(&self) -> &[String] {
185+ value
232− &self.scopes
186+ .as_str()
187+ .and_then(|s| s.parse().ok())
188+ .ok_or(format!("Theme: invalid modifier: {}", value))
233 }
189 }
234
190
235− pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
191+ pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> {
236− self.scopes().iter().position(|s| s == scope)
192+ if let Value::Table(entries) = value {
193+ for (name, value) in entries {
194+ match name.as_str() {
195+ "fg" => *style = style.fg(self.parse_color(value)?),
196+ "bg" => *style = style.bg(self.parse_color(value)?),
197+ "modifiers" => {
198+ let modifiers = value
199+ .as_array()
200+ .ok_or("Theme: modifiers should be an array")?;
201+
202+ for modifier in modifiers {
203+ *style = style.add_modifier(Self::parse_modifier(modifier)?);
204+ }
205+ }
206+ _ => return Err(format!("Theme: invalid style attribute: {}", name)),
207+ }
208+ }
209+ } else {
210+ *style = style.fg(self.parse_color(value)?);
211+ }
212+ Ok(())
213+ }
214+}
215+
216+impl TryFrom<Value> for ThemePalette {
217+ type Error = String;
218+
219+ fn try_from(value: Value) -> Result<Self, Self::Error> {
220+ let map = match value {
221+ Value::Table(entries) => entries,
222+ _ => return Ok(Self::default()),
223+ };
224+
225+ let mut palette = HashMap::with_capacity(map.len());
226+ for (name, value) in map {
227+ let value = Self::parse_value_as_str(&value)?;
228+ let color = Self::hex_string_to_rgb(value)?;
229+ palette.insert(name, color);
230+ }
231+
232+ Ok(Self::new(palette))
237 }
233 }
238}
234}
239
235
@@ -242,23 +238,21 @@ fn test_parse_style_string() {
242 let fg = Value::String("#ffffff".to_string());
238 let fg = Value::String("#ffffff".to_string());
243
239
244 let mut style = Style::default();
240 let mut style = Style::default();
241+ let palette = ThemePalette::default();
245− parse_style(&mut style, fg, &HashMap::default());
242+ palette.parse_style(&mut style, fg).unwrap();
246
243
247 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
244 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
248}
245}
249
246
250#[test]
247#[test]
251fn test_palette() {
248fn test_palette() {
249+ use helix_core::hashmap;
252 let fg = Value::String("my_color".to_string());
250 let fg = Value::String("my_color".to_string());
253
251
254 let mut style = Style::default();
252 let mut style = Style::default();
255− parse_style(
253+ let palette =
256− &mut style,
257− fg,
258− &vec![("my_color".to_string(), Color::Rgb(255, 255, 255))]
254+ ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
259− .into_iter()
255+ palette.parse_style(&mut style, fg).unwrap();
260− .collect(),
261− );
262
256
263 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
257 assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
264}
258}
@@ -274,9 +268,10 @@ fn test_parse_style_table() {
274 };
268 };
275
269
276 let mut style = Style::default();
270 let mut style = Style::default();
271+ let palette = ThemePalette::default();
277 if let Value::Table(entries) = table {
272 if let Value::Table(entries) = table {
278 for (_name, value) in entries {
273 for (_name, value) in entries {
279− parse_style(&mut style, value, &HashMap::default());
274+ palette.parse_style(&mut style, value).unwrap();
280 }
275 }
281 }
276 }
282
277