1use std::collections::HashMap;
2
3use once_cell::sync::Lazy;
4
5use crate::color::parse_rgb;
6
7use serde::Deserialize;
8use serde_json::{Map, Value};
9
10use super::{StatusLineStyle, Style, Theme, TokenStyle};
11
12static SYNTAX_HIGHLIGHTING_MAP: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
13 let mut m = HashMap::new();
14
15 m.insert("constant", "constant");
16 m.insert("entity.name.type", "type");
17 m.insert("support.type", "type");
18 m.insert("entity.name.function.constructor", "constructor");
19 m.insert("variable.other.enummember", "constructor");
20 m.insert("entity.name.function", "function");
21 m.insert("meta.function-call", "function");
22 m.insert("entity.name.function.member", "function.method");
23 m.insert("variable.function", "function.method");
24 m.insert("entity.name.function.macro", "function.macro");
25 m.insert("support.function.macro", "function.macro");
26 m.insert("variable.other.member", "property");
27 m.insert("variable.other.property", "property");
28 m.insert("variable.parameter", "variable.parameter");
29 m.insert("entity.name.label", "label");
30 m.insert("comment", "comment");
31 m.insert("punctuation.definition.comment", "comment");
32 m.insert("punctuation.section.block", "punctuation.bracket");
33 m.insert("punctuation.definition.brackets", "punctuation.bracket");
34 m.insert("punctuation.separator", "punctuation.delimiter");
35 m.insert("punctuation.accessor", "punctuation.delimiter");
36 m.insert("keyword", "keyword");
37 m.insert("keyword.control", "keyword");
38 m.insert("support.type.primitive", "type.builtin");
39 m.insert("keyword.type", "type.builtin");
40 m.insert("variable.language", "variable.builtin");
41 m.insert("support.variable", "variable.builtin");
42 m.insert("string.quoted.double", "string");
43 m.insert("string.quoted.single", "string");
44 m.insert("constant.language", "constant.builtin");
45 m.insert("constant.numeric", "constant.builtin");
46 m.insert("constant.character", "constant.builtin");
47 m.insert("constant.character.escape", "escape");
48 m.insert("keyword.operator", "operator");
49 m.insert("storage.modifier.attribute", "attribute");
50 m.insert("meta.attribute", "attribute");
51
52 m
53});
54
55fn translate_scope(theme_scope: String) -> String {
56 SYNTAX_HIGHLIGHTING_MAP
57 .get(&theme_scope.as_str())
58 .map(ToString::to_string)
59 .unwrap_or(theme_scope)
60}
61
62#[derive(Deserialize, Debug)]
63#[serde(untagged)]
64enum VSCodeScope {
65 Single(String),
66 Multiple(Vec<String>),
67}
68
69impl From<VSCodeScope> for Vec<String> {
70 fn from(scope: VSCodeScope) -> Self {
71 match scope {
72 VSCodeScope::Single(s) => vec![translate_scope(s)],
73 VSCodeScope::Multiple(v) => v.into_iter().map(translate_scope).collect(),
74 }
75 }
76}
77
78#[derive(Deserialize, Debug)]
79#[serde(rename_all = "camelCase")]
80struct VSCodeTokenColor {
81 name: Option<String>,
82 scope: Option<VSCodeScope>,
83 settings: Map<String, Value>,
84}
85
86impl TryFrom<VSCodeTokenColor> for TokenStyle {
87 type Error = anyhow::Error;
88
89 fn try_from(value: VSCodeTokenColor) -> Result<Self, Self::Error> {
90 let mut style = Style::default();
91
92 if let Some(foreground) = value.settings.get("foreground") {
93 style.foreground = Some(
94 parse_rgb(foreground.as_str().expect("foreground is string"))
95 .expect("converting string to rgb works"),
96 );
97 }
98
99 if let Some(background) = value.settings.get("background") {
100 style.background = Some(
101 parse_rgb(background.as_str().expect("soreground is string"))
102 .expect("converting string to rgb works"),
103 );
104 }
105
106 if let Some(font_style) = value.settings.get("fontStyle") {
107 style.bold = font_style
108 .as_str()
109 .expect("fontStyle is string")
110 .contains("bold");
111 style.italic = font_style
112 .as_str()
113 .expect("fontStyle is string")
114 .contains("italic");
115 }
116
117 let Some(scope) = value.scope else {
118 return Err(anyhow::anyhow!("TokenColor has no scope"));
119 };
120
121 Ok(Self {
122 name: value.name,
123 scope: scope.into(),
124 style,
125 })
126 }
127}
128
129#[derive(Deserialize, Debug)]
130#[serde(rename_all = "camelCase")]
131struct VSCodeTheme {
132 name: Option<String>,
133 #[serde(rename = "type")]
134 #[allow(dead_code)]
135 obj_type: Option<String>,
136 colors: Map<String, Value>,
137 token_colors: Vec<VSCodeTokenColor>,
138}
139
140#[allow(clippy::too_many_lines)]
141pub fn parse_vscode_theme(file: &str) -> anyhow::Result<Theme> {
157 let contents = std::fs::read_to_string(file)?;
158 let vscode_theme: VSCodeTheme = serde_json::from_str(&contents)?;
159
160 let (token_colors_with_scope, token_colors_without_scope): (
161 Vec<VSCodeTokenColor>,
162 Vec<VSCodeTokenColor>,
163 ) = vscode_theme
164 .token_colors
165 .into_iter()
166 .partition(|tc| tc.scope.is_some());
167
168 let token_styles = token_colors_with_scope
169 .into_iter()
170 .map(TryInto::try_into)
171 .collect::<Result<Vec<TokenStyle>, _>>()?;
172
173 let gutter_style = Style {
174 foreground: vscode_theme
175 .colors
176 .iter()
177 .find(|(c, _)| **c == "editorLineNumber.foreground")
178 .map(|(_, hex)| {
179 parse_rgb(hex.as_str().expect("colors are formatted as a hex string")).unwrap()
180 }),
181 background: vscode_theme
182 .colors
183 .iter()
184 .find(|(c, _)| **c == "editorLineNumber.background")
185 .map(|(_, hex)| {
186 parse_rgb(hex.as_str().expect("colors are formatted as a hex string")).unwrap()
187 }),
188 ..Default::default()
189 };
190
191 let status_line_style = StatusLineStyle {
192 outer_style: Style {
193 foreground: vscode_theme
194 .colors
195 .iter()
196 .find(|(c, _)| **c == "statusBar.background")
197 .map(|(_, hex)| {
198 parse_rgb(hex.as_str().expect("colors are formatted as a hex string")).unwrap()
199 }),
200 background: vscode_theme
201 .colors
202 .iter()
203 .find(|(c, _)| **c == "editorBracketHighlight.foreground4")
204 .map(|(_, hex)| {
205 parse_rgb(hex.as_str().expect("colors are formatted as a hex string")).unwrap()
206 }),
207 bold: true,
208 ..Default::default()
209 },
210 outer_chars: [' ', '', '', ' '],
211 inner_style: Style {
212 foreground: vscode_theme
213 .colors
214 .iter()
215 .find(|(c, _)| **c == "foreground")
216 .map(|(_, hex)| {
217 parse_rgb(hex.as_str().expect("colors are formatted as a hex string")).unwrap()
218 }),
219 background: vscode_theme
220 .colors
221 .iter()
222 .find(|(c, _)| **c == "editorSuggestWidget.border")
223 .map(|(_, hex)| {
224 parse_rgb(hex.as_str().expect("colors are formatted as a hex string")).unwrap()
225 }),
226 ..Default::default()
227 },
228 };
229
230 let selection_style = vscode_theme
231 .colors
232 .iter()
233 .find(|(c, _)| **c == "editor.selectionBackground")
234 .map(|(_, hex)| Style {
235 background: Some(
236 parse_rgb(hex.as_str().expect("colors are formatted as a hex string")).unwrap(),
237 ),
238 ..Default::default()
239 });
240
241 let foreground_token_color = token_colors_without_scope
242 .iter()
243 .find(|tc| tc.settings.contains_key("foreground"));
244 let background_token_color = token_colors_without_scope
245 .iter()
246 .find(|tc| tc.settings.contains_key("background"));
247
248 let foreground = match foreground_token_color {
249 Some(tc) => tc.settings.get("foreground"),
250 None => vscode_theme.colors.get("editor.foreground"),
251 };
252 let background = match background_token_color {
253 Some(tc) => tc.settings.get("background"),
254 None => vscode_theme.colors.get("editor.background"),
255 };
256
257 Ok(Theme {
258 name: vscode_theme.name.unwrap_or_default(),
259 style: Style {
260 foreground: Some(parse_rgb(
261 foreground
262 .expect("foreground color exists")
263 .as_str()
264 .expect("foreground color is string"),
265 )?),
266 background: Some(parse_rgb(
267 background
268 .expect("background color exists")
269 .as_str()
270 .expect("background color is string"),
271 )?),
272 bold: false,
273 italic: false,
274 },
275 token_styles,
276 gutter_style,
277 status_line_style,
278 selection_style,
279 })
280}
281
282#[cfg(test)]
283mod test {
284 use super::*;
285
286 #[test]
287 fn test_parse_vscode() {
288 let theme = parse_vscode_theme("./themes/frappe.json").unwrap();
289 println!("{:?}", theme);
290 }
291}