@@ -3,19 +3,28 @@ use std::{
3
3
path:: { Path , PathBuf } ,
4
4
} ;
5
5
6
- use anyhow:: Context ;
6
+ use anyhow:: { anyhow , Context , Result } ;
7
7
use helix_core:: hashmap;
8
+ use helix_loader:: merge_toml_values;
8
9
use log:: warn;
9
10
use once_cell:: sync:: Lazy ;
10
11
use serde:: { Deserialize , Deserializer } ;
11
- use toml:: Value ;
12
+ use toml:: { map :: Map , Value } ;
12
13
13
14
pub use crate :: graphics:: { Color , Modifier , Style } ;
14
15
15
16
pub static DEFAULT_THEME : Lazy < Theme > = Lazy :: new ( || {
17
+ // let raw_theme: Value = toml::from_slice(include_bytes!("../../theme.toml"))
18
+ // .expect("Failed to parse default theme");
19
+ // Theme::from(raw_theme)
20
+
16
21
toml:: from_slice ( include_bytes ! ( "../../theme.toml" ) ) . expect ( "Failed to parse default theme" )
17
22
} ) ;
18
23
pub static BASE16_DEFAULT_THEME : Lazy < Theme > = Lazy :: new ( || {
24
+ // let raw_theme: Value = toml::from_slice(include_bytes!("../../base16_theme.toml"))
25
+ // .expect("Failed to parse base 16 default theme");
26
+ // Theme::from(raw_theme)
27
+
19
28
toml:: from_slice ( include_bytes ! ( "../../base16_theme.toml" ) )
20
29
. expect ( "Failed to parse base 16 default theme" )
21
30
} ) ;
@@ -35,24 +44,51 @@ impl Loader {
35
44
}
36
45
37
46
/// Loads a theme first looking in the `user_dir` then in `default_dir`
38
- pub fn load ( & self , name : & str ) -> Result < Theme , anyhow :: Error > {
47
+ pub fn load ( & self , name : & str ) -> Result < Theme > {
39
48
if name == "default" {
40
49
return Ok ( self . default ( ) ) ;
41
50
}
42
51
if name == "base16_default" {
43
52
return Ok ( self . base16_default ( ) ) ;
44
53
}
45
- let filename = format ! ( "{}.toml" , name) ;
46
54
47
- let user_path = self . user_dir . join ( & filename) ;
48
- let path = if user_path. exists ( ) {
49
- user_path
55
+ self . load_theme ( name, name, false ) . map ( Theme :: from)
56
+ }
57
+
58
+ // load the theme and its parent recursively and merge them
59
+ // `base_theme_name` is the theme from the config.toml,
60
+ // used to prevent some circular loading scenarios
61
+ fn load_theme (
62
+ & self ,
63
+ name : & str ,
64
+ base_them_name : & str ,
65
+ only_default_dir : bool ,
66
+ ) -> Result < Value > {
67
+ let path = self . path ( name, only_default_dir) ;
68
+ let theme_toml = self . load_toml ( path) ?;
69
+
70
+ let inherits = theme_toml. get ( "inherits" ) ;
71
+
72
+ let theme_toml = if let Some ( parent_theme_name) = inherits {
73
+ let parent_theme_name = parent_theme_name. as_str ( ) . ok_or_else ( || {
74
+ anyhow ! (
75
+ "Theme: expected 'inherits' to be a string: {}" ,
76
+ parent_theme_name
77
+ )
78
+ } ) ?;
79
+
80
+ let parent_theme_toml = self . load_theme (
81
+ parent_theme_name,
82
+ base_them_name,
83
+ base_them_name == parent_theme_name,
84
+ ) ?;
85
+
86
+ self . merge_themes ( parent_theme_toml, theme_toml)
50
87
} else {
51
- self . default_dir . join ( filename )
88
+ theme_toml
52
89
} ;
53
90
54
- let data = std:: fs:: read ( & path) ?;
55
- toml:: from_slice ( data. as_slice ( ) ) . context ( "Failed to deserialize theme" )
91
+ Ok ( theme_toml)
56
92
}
57
93
58
94
pub fn read_names ( path : & Path ) -> Vec < String > {
@@ -70,6 +106,53 @@ impl Loader {
70
106
. unwrap_or_default ( )
71
107
}
72
108
109
+ // merge one theme into the parent theme
110
+ fn merge_themes ( & self , parent_theme_toml : Value , theme_toml : Value ) -> Value {
111
+ let parent_palette = parent_theme_toml. get ( "palette" ) ;
112
+ let palette = theme_toml. get ( "palette" ) ;
113
+
114
+ // handle the table seperately since it needs a `merge_depth` of 2
115
+ // this would conflict with the rest of the theme merge strategy
116
+ let palette_values = match ( parent_palette, palette) {
117
+ ( Some ( parent_palette) , Some ( palette) ) => {
118
+ merge_toml_values ( parent_palette. clone ( ) , palette. clone ( ) , 2 )
119
+ }
120
+ ( Some ( parent_palette) , None ) => parent_palette. clone ( ) ,
121
+ ( None , Some ( palette) ) => palette. clone ( ) ,
122
+ ( None , None ) => Map :: new ( ) . into ( ) ,
123
+ } ;
124
+
125
+ // add the palette correctly as nested table
126
+ let mut palette = Map :: new ( ) ;
127
+ palette. insert ( String :: from ( "palette" ) , palette_values) ;
128
+
129
+ // merge the theme into the parent theme
130
+ let theme = merge_toml_values ( parent_theme_toml, theme_toml, 1 ) ;
131
+ // merge the before specially handled palette into the theme
132
+ merge_toml_values ( theme, palette. into ( ) , 1 )
133
+ }
134
+
135
+ // Loads the theme data as `toml::Value` first from the user_dir then in default_dir
136
+ fn load_toml ( & self , path : PathBuf ) -> Result < Value > {
137
+ let data = std:: fs:: read ( & path) ?;
138
+
139
+ toml:: from_slice ( data. as_slice ( ) ) . context ( "Failed to deserialize theme" )
140
+ }
141
+
142
+ // Returns the path to the theme with the name
143
+ // With `only_default_dir` as false the path will first search for the user path
144
+ // disabled it ignores the user path and returns only the default path
145
+ fn path ( & self , name : & str , only_default_dir : bool ) -> PathBuf {
146
+ let filename = format ! ( "{}.toml" , name) ;
147
+
148
+ let user_path = self . user_dir . join ( & filename) ;
149
+ if !only_default_dir && user_path. exists ( ) {
150
+ user_path
151
+ } else {
152
+ self . default_dir . join ( filename)
153
+ }
154
+ }
155
+
73
156
/// Lists all theme names available in default and user directory
74
157
pub fn names ( & self ) -> Vec < String > {
75
158
let mut names = Self :: read_names ( & self . user_dir ) ;
@@ -105,52 +188,77 @@ pub struct Theme {
105
188
highlights : Vec < Style > ,
106
189
}
107
190
191
+ impl From < Value > for Theme {
192
+ fn from ( value : Value ) -> Self {
193
+ let values: Result < HashMap < String , Value > > =
194
+ toml:: from_str ( & value. to_string ( ) ) . context ( "Failed to load theme" ) ;
195
+
196
+ let ( styles, scopes, highlights) = build_theme_values ( values) ;
197
+
198
+ Self {
199
+ styles,
200
+ scopes,
201
+ highlights,
202
+ }
203
+ }
204
+ }
205
+
108
206
impl < ' de > Deserialize < ' de > for Theme {
109
207
fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
110
208
where
111
209
D : Deserializer < ' de > ,
112
210
{
113
- let mut styles = HashMap :: new ( ) ;
114
- let mut scopes = Vec :: new ( ) ;
115
- let mut highlights = Vec :: new ( ) ;
116
-
117
- if let Ok ( mut colors) = HashMap :: < String , Value > :: deserialize ( deserializer) {
118
- // TODO: alert user of parsing failures in editor
119
- let palette = colors
120
- . remove ( "palette" )
121
- . map ( |value| {
122
- ThemePalette :: try_from ( value) . unwrap_or_else ( |err| {
123
- warn ! ( "{}" , err) ;
124
- ThemePalette :: default ( )
125
- } )
126
- } )
127
- . unwrap_or_default ( ) ;
128
-
129
- styles. reserve ( colors. len ( ) ) ;
130
- scopes. reserve ( colors. len ( ) ) ;
131
- highlights. reserve ( colors. len ( ) ) ;
132
-
133
- for ( name, style_value) in colors {
134
- let mut style = Style :: default ( ) ;
135
- if let Err ( err) = palette. parse_style ( & mut style, style_value) {
136
- warn ! ( "{}" , err) ;
137
- }
211
+ let values = HashMap :: < String , Value > :: deserialize ( deserializer) ?;
138
212
139
- // these are used both as UI and as highlights
140
- styles. insert ( name. clone ( ) , style) ;
141
- scopes. push ( name) ;
142
- highlights. push ( style) ;
143
- }
144
- }
213
+ let ( styles, scopes, highlights) = build_theme_values ( Ok ( values) ) ;
145
214
146
215
Ok ( Self {
147
- scopes,
148
216
styles,
217
+ scopes,
149
218
highlights,
150
219
} )
151
220
}
152
221
}
153
222
223
+ fn build_theme_values (
224
+ values : Result < HashMap < String , Value > > ,
225
+ ) -> ( HashMap < String , Style > , Vec < String > , Vec < Style > ) {
226
+ let mut styles = HashMap :: new ( ) ;
227
+ let mut scopes = Vec :: new ( ) ;
228
+ let mut highlights = Vec :: new ( ) ;
229
+
230
+ if let Ok ( mut colors) = values {
231
+ // TODO: alert user of parsing failures in editor
232
+ let palette = colors
233
+ . remove ( "palette" )
234
+ . map ( |value| {
235
+ ThemePalette :: try_from ( value) . unwrap_or_else ( |err| {
236
+ warn ! ( "{}" , err) ;
237
+ ThemePalette :: default ( )
238
+ } )
239
+ } )
240
+ . unwrap_or_default ( ) ;
241
+ // remove inherits from value to prevent errors
242
+ let _ = colors. remove ( "inherits" ) ;
243
+ styles. reserve ( colors. len ( ) ) ;
244
+ scopes. reserve ( colors. len ( ) ) ;
245
+ highlights. reserve ( colors. len ( ) ) ;
246
+ for ( name, style_value) in colors {
247
+ let mut style = Style :: default ( ) ;
248
+ if let Err ( err) = palette. parse_style ( & mut style, style_value) {
249
+ warn ! ( "{}" , err) ;
250
+ }
251
+
252
+ // these are used both as UI and as highlights
253
+ styles. insert ( name. clone ( ) , style) ;
254
+ scopes. push ( name) ;
255
+ highlights. push ( style) ;
256
+ }
257
+ }
258
+
259
+ ( styles, scopes, highlights)
260
+ }
261
+
154
262
impl Theme {
155
263
#[ inline]
156
264
pub fn highlight ( & self , index : usize ) -> Style {
0 commit comments