gpui_component/highlighter/
registry.rs

1use gpui::{App, FontWeight, HighlightStyle, Hsla, SharedString};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use serde_repr::{Deserialize_repr, Serialize_repr};
5use std::{
6    collections::HashMap,
7    ops::Deref,
8    sync::{Arc, LazyLock, Mutex},
9};
10
11use crate::{
12    highlighter::{languages, Language},
13    ActiveTheme, ThemeMode, DEFAULT_THEME_COLORS,
14};
15
16pub(super) const HIGHLIGHT_NAMES: [&str; 40] = [
17    "attribute",
18    "boolean",
19    "comment",
20    "comment.doc",
21    "constant",
22    "constructor",
23    "embedded",
24    "emphasis",
25    "emphasis.strong",
26    "enum",
27    "function",
28    "hint",
29    "keyword",
30    "label",
31    "link_text",
32    "link_uri",
33    "number",
34    "operator",
35    "predictive",
36    "preproc",
37    "primary",
38    "property",
39    "punctuation",
40    "punctuation.bracket",
41    "punctuation.delimiter",
42    "punctuation.list_marker",
43    "punctuation.special",
44    "string",
45    "string.escape",
46    "string.regex",
47    "string.special",
48    "string.special.symbol",
49    "tag",
50    "tag.doctype",
51    "text.literal",
52    "title",
53    "type",
54    "variable",
55    "variable.special",
56    "variant",
57];
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct LanguageConfig {
61    pub name: SharedString,
62    pub language: tree_sitter::Language,
63    pub injection_languages: Vec<SharedString>,
64    pub highlights: SharedString,
65    pub injections: SharedString,
66    pub locals: SharedString,
67}
68
69impl LanguageConfig {
70    pub fn new(
71        name: impl Into<SharedString>,
72        language: tree_sitter::Language,
73        injection_languages: Vec<SharedString>,
74        highlights: &str,
75        injections: &str,
76        locals: &str,
77    ) -> Self {
78        Self {
79            name: name.into(),
80            language,
81            injection_languages,
82            highlights: SharedString::from(highlights.to_string()),
83            injections: SharedString::from(injections.to_string()),
84            locals: SharedString::from(locals.to_string()),
85        }
86    }
87}
88
89/// Theme for Tree-sitter Highlight
90///
91/// https://docs.rs/tree-sitter-highlight/0.25.4/tree_sitter_highlight/
92#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
93pub struct SyntaxColors {
94    pub attribute: Option<ThemeStyle>,
95    pub boolean: Option<ThemeStyle>,
96    pub comment: Option<ThemeStyle>,
97    pub comment_doc: Option<ThemeStyle>,
98    pub constant: Option<ThemeStyle>,
99    pub constructor: Option<ThemeStyle>,
100    pub embedded: Option<ThemeStyle>,
101    pub emphasis: Option<ThemeStyle>,
102    #[serde(rename = "emphasis.strong")]
103    pub emphasis_strong: Option<ThemeStyle>,
104    #[serde(rename = "enum")]
105    pub enum_: Option<ThemeStyle>,
106    pub function: Option<ThemeStyle>,
107    pub hint: Option<ThemeStyle>,
108    pub keyword: Option<ThemeStyle>,
109    pub label: Option<ThemeStyle>,
110    #[serde(rename = "link_text")]
111    pub link_text: Option<ThemeStyle>,
112    #[serde(rename = "link_uri")]
113    pub link_uri: Option<ThemeStyle>,
114    pub number: Option<ThemeStyle>,
115    pub operator: Option<ThemeStyle>,
116    pub predictive: Option<ThemeStyle>,
117    pub preproc: Option<ThemeStyle>,
118    pub primary: Option<ThemeStyle>,
119    pub property: Option<ThemeStyle>,
120    pub punctuation: Option<ThemeStyle>,
121    #[serde(rename = "punctuation.bracket")]
122    pub punctuation_bracket: Option<ThemeStyle>,
123    #[serde(rename = "punctuation.delimiter")]
124    pub punctuation_delimiter: Option<ThemeStyle>,
125    #[serde(rename = "punctuation.list_marker")]
126    pub punctuation_list_marker: Option<ThemeStyle>,
127    #[serde(rename = "punctuation.special")]
128    pub punctuation_special: Option<ThemeStyle>,
129    pub string: Option<ThemeStyle>,
130    #[serde(rename = "string.escape")]
131    pub string_escape: Option<ThemeStyle>,
132    #[serde(rename = "string.regex")]
133    pub string_regex: Option<ThemeStyle>,
134    #[serde(rename = "string.special")]
135    pub string_special: Option<ThemeStyle>,
136    #[serde(rename = "string.special.symbol")]
137    pub string_special_symbol: Option<ThemeStyle>,
138    pub tag: Option<ThemeStyle>,
139    #[serde(rename = "tag.doctype")]
140    pub tag_doctype: Option<ThemeStyle>,
141    #[serde(rename = "text.literal")]
142    pub text_literal: Option<ThemeStyle>,
143    pub title: Option<ThemeStyle>,
144    #[serde(rename = "type")]
145    pub type_: Option<ThemeStyle>,
146    pub variable: Option<ThemeStyle>,
147    #[serde(rename = "variable.special")]
148    pub variable_special: Option<ThemeStyle>,
149    pub variant: Option<ThemeStyle>,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
153#[serde(rename_all = "lowercase")]
154pub enum FontStyle {
155    Normal,
156    Italic,
157    Underline,
158}
159
160impl From<FontStyle> for gpui::FontStyle {
161    fn from(style: FontStyle) -> Self {
162        match style {
163            FontStyle::Normal => gpui::FontStyle::Normal,
164            FontStyle::Italic => gpui::FontStyle::Italic,
165            FontStyle::Underline => gpui::FontStyle::Normal,
166        }
167    }
168}
169
170#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr, JsonSchema)]
171#[repr(u16)]
172pub enum FontWeightContent {
173    Thin = 100,
174    ExtraLight = 200,
175    Light = 300,
176    Normal = 400,
177    Medium = 500,
178    Semibold = 600,
179    Bold = 700,
180    ExtraBold = 800,
181    Black = 900,
182}
183
184impl From<FontWeightContent> for FontWeight {
185    fn from(value: FontWeightContent) -> Self {
186        match value {
187            FontWeightContent::Thin => FontWeight::THIN,
188            FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT,
189            FontWeightContent::Light => FontWeight::LIGHT,
190            FontWeightContent::Normal => FontWeight::NORMAL,
191            FontWeightContent::Medium => FontWeight::MEDIUM,
192            FontWeightContent::Semibold => FontWeight::SEMIBOLD,
193            FontWeightContent::Bold => FontWeight::BOLD,
194            FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD,
195            FontWeightContent::Black => FontWeight::BLACK,
196        }
197    }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
201pub struct ThemeStyle {
202    color: Option<Hsla>,
203    font_style: Option<FontStyle>,
204    font_weight: Option<FontWeightContent>,
205}
206
207impl From<ThemeStyle> for HighlightStyle {
208    fn from(style: ThemeStyle) -> Self {
209        HighlightStyle {
210            color: style.color,
211            font_weight: style.font_weight.map(Into::into),
212            font_style: style.font_style.map(Into::into),
213            ..Default::default()
214        }
215    }
216}
217
218impl SyntaxColors {
219    pub fn style(&self, name: &str) -> Option<HighlightStyle> {
220        if name.is_empty() {
221            return None;
222        }
223
224        let style = match name {
225            "attribute" => self.attribute,
226            "boolean" => self.boolean,
227            "comment" => self.comment,
228            "comment.doc" => self.comment_doc,
229            "constant" => self.constant,
230            "constructor" => self.constructor,
231            "embedded" => self.embedded,
232            "emphasis" => self.emphasis,
233            "emphasis.strong" => self.emphasis_strong,
234            "enum" => self.enum_,
235            "function" => self.function,
236            "hint" => self.hint,
237            "keyword" => self.keyword,
238            "label" => self.label,
239            "link_text" => self.link_text,
240            "link_uri" => self.link_uri,
241            "number" => self.number,
242            "operator" => self.operator,
243            "predictive" => self.predictive,
244            "preproc" => self.preproc,
245            "primary" => self.primary,
246            "property" => self.property,
247            "punctuation" => self.punctuation,
248            "punctuation.bracket" => self.punctuation_bracket,
249            "punctuation.delimiter" => self.punctuation_delimiter,
250            "punctuation.list_marker" => self.punctuation_list_marker,
251            "punctuation.special" => self.punctuation_special,
252            "string" => self.string,
253            "string.escape" => self.string_escape,
254            "string.regex" => self.string_regex,
255            "string.special" => self.string_special,
256            "string.special.symbol" => self.string_special_symbol,
257            "tag" => self.tag,
258            "tag.doctype" => self.tag_doctype,
259            "text.literal" => self.text_literal,
260            "title" => self.title,
261            "type" => self.type_,
262            "variable" => self.variable,
263            "variable.special" => self.variable_special,
264            "variant" => self.variant,
265            _ => None,
266        }
267        .map(|s| s.into());
268
269        if style.is_some() {
270            style
271        } else {
272            // Fallback `keyword.modifier` to `keyword`
273            if name.contains(".") {
274                if let Some(prefix) = name.split(".").next() {
275                    return self.style(prefix);
276                }
277
278                None
279            } else {
280                None
281            }
282        }
283    }
284
285    #[inline]
286    pub fn style_for_index(&self, index: usize) -> Option<HighlightStyle> {
287        HIGHLIGHT_NAMES.get(index).and_then(|name| self.style(name))
288    }
289}
290
291#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
292pub struct StatusColors {
293    #[serde(rename = "error")]
294    error: Option<Hsla>,
295    #[serde(rename = "error.background")]
296    error_background: Option<Hsla>,
297    #[serde(rename = "error.border")]
298    error_border: Option<Hsla>,
299    #[serde(rename = "warning")]
300    warning: Option<Hsla>,
301    #[serde(rename = "warning.background")]
302    warning_background: Option<Hsla>,
303    #[serde(rename = "warning.border")]
304    warning_border: Option<Hsla>,
305    #[serde(rename = "info")]
306    info: Option<Hsla>,
307    #[serde(rename = "info.background")]
308    info_background: Option<Hsla>,
309    #[serde(rename = "info.border")]
310    info_border: Option<Hsla>,
311    #[serde(rename = "success")]
312    success: Option<Hsla>,
313    #[serde(rename = "success.background")]
314    success_background: Option<Hsla>,
315    #[serde(rename = "success.border")]
316    success_border: Option<Hsla>,
317    #[serde(rename = "hint")]
318    hint: Option<Hsla>,
319    #[serde(rename = "hint.background")]
320    hint_background: Option<Hsla>,
321    #[serde(rename = "hint.border")]
322    hint_border: Option<Hsla>,
323}
324
325impl StatusColors {
326    #[inline]
327    pub fn error(&self, cx: &App) -> Hsla {
328        self.error.unwrap_or(cx.theme().red)
329    }
330
331    #[inline]
332    pub fn error_background(&self, cx: &App) -> Hsla {
333        let bg = cx.theme().background;
334        self.error_background
335            .unwrap_or(bg.blend(self.error(cx).alpha(0.2)))
336    }
337
338    #[inline]
339    pub fn error_border(&self, cx: &App) -> Hsla {
340        self.error_border.unwrap_or(self.error(cx))
341    }
342
343    #[inline]
344    pub fn warning(&self, cx: &App) -> Hsla {
345        self.warning.unwrap_or(cx.theme().yellow)
346    }
347
348    #[inline]
349    pub fn warning_background(&self, cx: &App) -> Hsla {
350        let bg = cx.theme().background;
351        self.warning_background
352            .unwrap_or(bg.blend(self.warning(cx).alpha(0.2)))
353    }
354
355    #[inline]
356    pub fn warning_border(&self, cx: &App) -> Hsla {
357        self.warning_border.unwrap_or(self.warning(cx))
358    }
359
360    #[inline]
361    pub fn info(&self, cx: &App) -> Hsla {
362        self.info.unwrap_or(cx.theme().blue)
363    }
364
365    #[inline]
366    pub fn info_background(&self, cx: &App) -> Hsla {
367        let bg = cx.theme().background;
368        self.info_background
369            .unwrap_or(bg.blend(self.info(cx).alpha(0.2)))
370    }
371
372    #[inline]
373    pub fn info_border(&self, cx: &App) -> Hsla {
374        self.info_border.unwrap_or(self.info(cx))
375    }
376
377    #[inline]
378    pub fn success(&self, cx: &App) -> Hsla {
379        self.success.unwrap_or(cx.theme().green)
380    }
381
382    #[inline]
383    pub fn success_background(&self, cx: &App) -> Hsla {
384        let bg = cx.theme().background;
385        self.success_background
386            .unwrap_or(bg.blend(self.success(cx).alpha(0.2)))
387    }
388
389    #[inline]
390    pub fn success_border(&self, cx: &App) -> Hsla {
391        self.success_border.unwrap_or(self.success(cx))
392    }
393
394    #[inline]
395    pub fn hint(&self, cx: &App) -> Hsla {
396        self.hint.unwrap_or(cx.theme().cyan)
397    }
398
399    #[inline]
400    pub fn hint_background(&self, cx: &App) -> Hsla {
401        let bg = cx.theme().background;
402        self.hint_background
403            .unwrap_or(bg.blend(self.hint(cx).alpha(0.2)))
404    }
405
406    #[inline]
407    pub fn hint_border(&self, cx: &App) -> Hsla {
408        self.hint_border.unwrap_or(self.hint(cx))
409    }
410}
411
412#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
413pub struct HighlightThemeStyle {
414    #[serde(rename = "editor.background")]
415    pub editor_background: Option<Hsla>,
416    #[serde(rename = "editor.foreground")]
417    pub editor_foreground: Option<Hsla>,
418    #[serde(rename = "editor.active_line.background")]
419    pub editor_active_line: Option<Hsla>,
420    #[serde(rename = "editor.line_number")]
421    pub editor_line_number: Option<Hsla>,
422    #[serde(rename = "editor.active_line_number")]
423    pub editor_active_line_number: Option<Hsla>,
424    #[serde(flatten)]
425    pub status: StatusColors,
426    #[serde(rename = "syntax")]
427    pub syntax: SyntaxColors,
428}
429
430/// Theme for Tree-sitter Highlight from JSON theme file.
431///
432/// This json is compatible with the Zed theme format.
433///
434/// https://zed.dev/docs/extensions/languages#syntax-highlighting
435#[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
436pub struct HighlightTheme {
437    pub name: String,
438    #[serde(default)]
439    pub appearance: ThemeMode,
440    pub style: HighlightThemeStyle,
441}
442
443impl Deref for HighlightTheme {
444    type Target = SyntaxColors;
445
446    fn deref(&self) -> &Self::Target {
447        &self.style.syntax
448    }
449}
450
451impl HighlightTheme {
452    pub fn default_dark() -> Arc<Self> {
453        DEFAULT_THEME_COLORS[&ThemeMode::Dark].1.clone()
454    }
455
456    pub fn default_light() -> Arc<Self> {
457        DEFAULT_THEME_COLORS[&ThemeMode::Light].1.clone()
458    }
459}
460
461/// Registry for code highlighter languages.
462pub struct LanguageRegistry {
463    languages: Mutex<HashMap<SharedString, LanguageConfig>>,
464}
465
466impl LanguageRegistry {
467    /// Returns the singleton instance of the `LanguageRegistry` with default languages and themes.
468    pub fn singleton() -> &'static LazyLock<LanguageRegistry> {
469        static INSTANCE: LazyLock<LanguageRegistry> = LazyLock::new(|| LanguageRegistry {
470            languages: Mutex::new(
471                languages::Language::all()
472                    .map(|language| (language.name().into(), language.config()))
473                    .collect(),
474            ),
475        });
476        &INSTANCE
477    }
478
479    /// Registers a new language configuration to the registry.
480    pub fn register(&self, lang: &str, config: &LanguageConfig) {
481        self.languages
482            .lock()
483            .unwrap()
484            .insert(lang.to_string().into(), config.clone());
485    }
486
487    /// Returns a list of all registered language names.
488    pub fn languages(&self) -> Vec<SharedString> {
489        self.languages.lock().unwrap().keys().cloned().collect()
490    }
491
492    /// Returns the language configuration for the given language name.
493    pub fn language(&self, name: &str) -> Option<LanguageConfig> {
494        // Try to get by name first, there may have a custom language registered
495        // Then try to get built-in language to support short language names, e.g. "js" for "javascript"
496        let languages = self.languages.lock().unwrap();
497        languages
498            .get(name)
499            .or_else(|| languages.get(Language::from_str(name).name()))
500            .cloned()
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use crate::highlighter::LanguageConfig;
507
508    #[test]
509    fn test_registry() {
510        use super::LanguageRegistry;
511        let registry = LanguageRegistry::singleton();
512
513        registry.register(
514            "foo",
515            &LanguageConfig::new("foo", tree_sitter_json::LANGUAGE.into(), vec![], "", "", ""),
516        );
517
518        assert!(registry.language("foo").is_some());
519        assert!(registry.language("rust").is_some());
520        assert!(registry.language("rs").is_some());
521        assert!(registry.language("javascript").is_some());
522        assert!(registry.language("js").is_some());
523    }
524}