vscode_theme_syntect/
lib.rs1use log::debug;
21use serde::Deserialize;
22use std::{collections::HashMap, path::Path, str::FromStr};
23use syntect::highlighting::{
24 Color, FontStyle, ScopeSelectors, StyleModifier, Theme, ThemeItem, ThemeSettings,
25};
26
27pub mod error;
28mod named_color;
29
30use crate::error::ParseError;
31
32#[derive(Debug, Deserialize)]
34pub struct TokenColor {
35 pub scope: Option<Scope>,
36 pub settings: TokenSettings,
37}
38
39#[derive(Debug, Deserialize)]
41#[serde(untagged)]
42pub enum Scope {
43 Single(String),
44 Multiple(Vec<String>),
45}
46
47#[derive(Debug, Deserialize)]
49pub struct TokenSettings {
50 pub foreground: Option<String>,
51 pub background: Option<String>,
52 pub font_style: Option<String>,
53}
54
55#[derive(Debug, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct VscodeTheme {
59 pub name: Option<String>,
60 pub author: Option<String>,
61 pub maintainers: Option<Vec<String>>,
62 pub type_: Option<String>,
63 pub colors: HashMap<String, Option<String>>,
64 pub token_colors: Vec<TokenColor>,
65}
66
67impl FromStr for VscodeTheme {
68 type Err = ParseError;
69 fn from_str(s: &str) -> Result<Self, Self::Err> {
70 let value: serde_json::Value = jsonc_parser::parse_to_ast(
73 s,
74 &jsonc_parser::CollectOptions {
75 comments: jsonc_parser::CommentCollectionStrategy::Off,
76 tokens: false,
77 },
78 &jsonc_parser::ParseOptions::default(),
79 )?
80 .value
81 .into();
82
83 serde_json::from_value(value).map_err(ParseError::Json)
84 }
85}
86
87fn get_color(s: &str) -> Result<Color, ParseError> {
88 debug!("get_color: {}", s);
89 if let Some(color) = named_color::from_name(s) {
90 Ok(color)
91 } else {
92 Ok(Color::from_str(s)?)
93 }
94}
95
96impl TryFrom<VscodeTheme> for Theme {
97 type Error = ParseError;
98 fn try_from(value: VscodeTheme) -> Result<Self, Self::Error> {
99 let mut settings = ThemeSettings::default();
100
101 for (key, value) in &value.colors {
102 if value.is_none() {
103 continue;
104 }
105
106 let value = value.as_ref().unwrap();
107 match &key[..] {
108 "editor.background" => settings.background = get_color(value).ok(),
109 "editor.foreground" => {
110 settings.foreground = get_color(value).ok();
111 settings.caret = settings.foreground;
112 }
113 "foreground" => settings.foreground = get_color(value).ok(),
114 "editorCursor.background" => settings.caret = get_color(value).ok(),
115 "editor.lineHighlightBackground" => settings.line_highlight = get_color(value).ok(),
116 "editorEditor.foreground" => settings.misspelling = get_color(value).ok(),
117 "list.highlightForeground" => {
118 settings.find_highlight_foreground = get_color(value).ok();
119 settings.accent = get_color(value).ok()
120 }
121 "editorGutter.background" => settings.gutter = get_color(value).ok(),
122 "editorLineNumber.foreground" => settings.gutter_foreground = get_color(value).ok(),
123 "editor.selectionBackground" => settings.selection = get_color(value).ok(),
124 "list.inactiveSelectionBackground" => {
125 settings.inactive_selection = get_color(value).ok()
126 }
127 "list.inactiveSelectionForeground" => {
128 settings.inactive_selection_foreground = get_color(value).ok()
129 }
130 "editor.findMatchBackground" | "peekViewEditor.matchHighlightBorder" => {
131 settings.highlight = get_color(value).ok();
132 settings.find_highlight = get_color(value).ok();
133 }
134 "editorIndentGuide.background" => settings.guide = get_color(value).ok(),
135 "breadcrumb.activeSelectionForeground" => {
136 settings.active_guide = get_color(value).ok()
137 }
138 "breadcrumb.foreground" => settings.stack_guide = get_color(value).ok(),
139 "selection.background" => {
140 settings.tags_foreground = get_color(value).ok();
141 settings.brackets_foreground = get_color(value).ok();
142 }
143 "widget.shadow" | "scrollbar.shadow" => settings.shadow = get_color(value).ok(),
144 _ => (),
145 }
146 }
147
148 Ok(Self {
149 name: value.name,
150 author: value.author,
151 scopes: value
152 .token_colors
153 .iter()
154 .map(|color| {
155 Ok(ThemeItem {
156 scope: if let Some(scope) = &color.scope {
157 match scope {
158 Scope::Single(s) => ScopeSelectors::from_str(s)?,
159 Scope::Multiple(s) => ScopeSelectors::from_str(&s.join(","))?,
160 }
161 } else {
162 ScopeSelectors::from_str("*")?
163 },
164 style: StyleModifier {
165 foreground: color
166 .settings
167 .foreground
168 .clone()
169 .and_then(|s| get_color(&s).ok()),
170 background: color
171 .settings
172 .background
173 .clone()
174 .and_then(|s| get_color(&s).ok()),
175 font_style: color
176 .settings
177 .font_style
178 .clone()
179 .map(|s| FontStyle::from_str(&s))
180 .transpose()?,
181 },
182 })
183 })
184 .collect::<Result<Vec<_>, ParseError>>()?,
185 settings,
186 })
187 }
188}
189
190pub fn parse_vscode_theme(scheme: &str) -> Result<VscodeTheme, ParseError> {
205 VscodeTheme::from_str(scheme)
206}
207
208pub fn parse_vscode_theme_file(path: &Path) -> Result<VscodeTheme, ParseError> {
225 let scheme = std::fs::read_to_string(path)?;
226 parse_vscode_theme(&scheme)
227}
228
229#[cfg(test)]
230mod tests {
231 use log::debug;
232
233 use super::*;
234
235 fn start_log() {
236 let _ = env_logger::builder().is_test(true).try_init();
237 }
238
239 #[test]
240 fn convert_theme() {
241 start_log();
242 let schemes = vec![
243 (
244 "Synthwave",
245 include_str!("../assets/synthwave-color-theme.json"),
246 ),
247 (
248 "Tokyo Night",
249 include_str!("../assets/tokyo-night-color-theme.json"),
250 ),
251 ("Pale Night", include_str!("../assets/palenight.json")),
252 ("One Dark", include_str!("../assets/OneDark.json")),
253 ];
254
255 for (name, scheme) in schemes {
256 let now = std::time::Instant::now();
257 let scheme = VscodeTheme::from_str(scheme).expect("Failed to parse theme");
258
259 debug!("Parsed {} in {} ms", name, now.elapsed().as_millis());
260 Theme::try_from(scheme).expect("Failed to convert to theme");
261
262 debug!(
263 "Converted {} to SytectTheme in {} ms",
264 name,
265 now.elapsed().as_millis()
266 );
267 }
268 }
269}