Skip to main content

tsu_editor/theme/
vscode.rs

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)]
141/// Attempts to parse a theme file using visual studio code theme schema
142///
143/// # Arguments
144/// - `file`: Path to the theme file to be parsed
145///
146/// # Returns
147/// - A `Theme` if the parse is successful
148///
149/// # Errors
150/// Can fail if the provided file cannot be read from or if `serde_json` is not able to deserialize
151/// the contents of the file
152///
153/// # Panics
154/// This function might panic if a hex string within the file is not able to be parsed into a
155/// `Color::Rgb`
156pub 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}