use gpui::{App, FontWeight, HighlightStyle, Hsla}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{collections::HashMap, ops::Deref, sync::LazyLock}; use super::LanguageConfig; use crate::{ highlighter::{languages, Language}, ActiveTheme, Colorize, ThemeMode, }; pub(super) fn init(cx: &mut App) { let register = LanguageRegistry::new(); cx.set_global(register); } pub(super) const HIGHLIGHT_NAMES: [&str; 40] = [ "attribute", "boolean", "comment", "comment.doc", "constant", "constructor", "embedded", "emphasis", "emphasis.strong", "enum", "function", "hint", "keyword", "label", "link_text", "link_uri", "number", "operator", "predictive", "preproc", "primary", "property", "punctuation", "punctuation.bracket", "punctuation.delimiter", "punctuation.list_marker", "punctuation.special", "string", "string.escape", "string.regex", "string.special", "string.special.symbol", "tag", "tag.doctype", "text.literal", "title", "type", "variable", "variable.special", "variant", ]; const DEFAULT_DARK: LazyLock = LazyLock::new(|| { let json = include_str!("./themes/dark.json"); serde_json::from_str(json).unwrap() }); const DEFAULT_LIGHT: LazyLock = LazyLock::new(|| { let json = include_str!("./themes/light.json"); serde_json::from_str(json).unwrap() }); /// Theme for Tree-sitter Highlight /// /// https://docs.rs/tree-sitter-highlight/0.25.4/tree_sitter_highlight/ #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)] pub struct SyntaxColors { pub attribute: Option, pub boolean: Option, pub comment: Option, pub comment_doc: Option, pub constant: Option, pub constructor: Option, pub embedded: Option, pub emphasis: Option, #[serde(rename = "emphasis.strong")] pub emphasis_strong: Option, #[serde(rename = "enum")] pub enum_: Option, pub function: Option, pub hint: Option, pub keyword: Option, pub label: Option, #[serde(rename = "link_text")] pub link_text: Option, #[serde(rename = "link_uri")] pub link_uri: Option, pub number: Option, pub operator: Option, pub predictive: Option, pub preproc: Option, pub primary: Option, pub property: Option, pub punctuation: Option, #[serde(rename = "punctuation.bracket")] pub punctuation_bracket: Option, #[serde(rename = "punctuation.delimiter")] pub punctuation_delimiter: Option, #[serde(rename = "punctuation.list_marker")] pub punctuation_list_marker: Option, #[serde(rename = "punctuation.special")] pub punctuation_special: Option, pub string: Option, #[serde(rename = "string.escape")] pub string_escape: Option, #[serde(rename = "string.regex")] pub string_regex: Option, #[serde(rename = "string.special")] pub string_special: Option, #[serde(rename = "string.special.symbol")] pub string_special_symbol: Option, pub tag: Option, #[serde(rename = "tag.doctype")] pub tag_doctype: Option, #[serde(rename = "text.literal")] pub text_literal: Option, pub title: Option, #[serde(rename = "type")] pub type_: Option, pub variable: Option, #[serde(rename = "variable.special")] pub variable_special: Option, pub variant: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum FontStyle { Normal, Italic, Underline, } impl From for gpui::FontStyle { fn from(style: FontStyle) -> Self { match style { FontStyle::Normal => gpui::FontStyle::Normal, FontStyle::Italic => gpui::FontStyle::Italic, FontStyle::Underline => gpui::FontStyle::Normal, } } } #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr, JsonSchema)] #[repr(u16)] pub enum FontWeightContent { Thin = 100, ExtraLight = 200, Light = 300, Normal = 400, Medium = 500, Semibold = 600, Bold = 700, ExtraBold = 800, Black = 900, } impl From for FontWeight { fn from(value: FontWeightContent) -> Self { match value { FontWeightContent::Thin => FontWeight::THIN, FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT, FontWeightContent::Light => FontWeight::LIGHT, FontWeightContent::Normal => FontWeight::NORMAL, FontWeightContent::Medium => FontWeight::MEDIUM, FontWeightContent::Semibold => FontWeight::SEMIBOLD, FontWeightContent::Bold => FontWeight::BOLD, FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD, FontWeightContent::Black => FontWeight::BLACK, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)] pub struct ThemeStyle { color: Option, font_style: Option, font_weight: Option, } impl From for HighlightStyle { fn from(style: ThemeStyle) -> Self { HighlightStyle { color: style.color, font_weight: style.font_weight.map(Into::into), font_style: style.font_style.map(Into::into), ..Default::default() } } } impl SyntaxColors { pub fn style(&self, name: &str) -> Option { if name.is_empty() { return None; } let style = match name { "attribute" => self.attribute, "boolean" => self.boolean, "comment" => self.comment, "comment.doc" => self.comment_doc, "constant" => self.constant, "constructor" => self.constructor, "embedded" => self.embedded, "emphasis" => self.emphasis, "emphasis.strong" => self.emphasis_strong, "enum" => self.enum_, "function" => self.function, "hint" => self.hint, "keyword" => self.keyword, "label" => self.label, "link_text" => self.link_text, "link_uri" => self.link_uri, "number" => self.number, "operator" => self.operator, "predictive" => self.predictive, "preproc" => self.preproc, "primary" => self.primary, "property" => self.property, "punctuation" => self.punctuation, "punctuation.bracket" => self.punctuation_bracket, "punctuation.delimiter" => self.punctuation_delimiter, "punctuation.list_marker" => self.punctuation_list_marker, "punctuation.special" => self.punctuation_special, "string" => self.string, "string.escape" => self.string_escape, "string.regex" => self.string_regex, "string.special" => self.string_special, "string.special.symbol" => self.string_special_symbol, "tag" => self.tag, "tag.doctype" => self.tag_doctype, "text.literal" => self.text_literal, "title" => self.title, "type" => self.type_, "variable" => self.variable, "variable.special" => self.variable_special, "variant" => self.variant, _ => None, } .map(|s| s.into()); if style.is_some() { style } else { // Fallback `keyword.modifier` to `keyword` if name.contains(".") { if let Some(prefix) = name.split(".").next() { return self.style(prefix); } None } else { None } } } #[inline] pub fn style_for_index(&self, index: usize) -> Option { HIGHLIGHT_NAMES.get(index).and_then(|name| self.style(name)) } } #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)] pub struct StatusColors { #[serde(rename = "error")] error: Option, #[serde(rename = "error.background")] error_background: Option, #[serde(rename = "error.border")] error_border: Option, #[serde(rename = "warning")] warning: Option, #[serde(rename = "warning.background")] warning_background: Option, #[serde(rename = "warning.border")] warning_border: Option, #[serde(rename = "info")] info: Option, #[serde(rename = "info.background")] info_background: Option, #[serde(rename = "info.border")] info_border: Option, #[serde(rename = "success")] success: Option, #[serde(rename = "success.background")] success_background: Option, #[serde(rename = "success.border")] success_border: Option, #[serde(rename = "hint")] hint: Option, #[serde(rename = "hint.background")] hint_background: Option, #[serde(rename = "hint.border")] hint_border: Option, } impl StatusColors { #[inline] pub fn error(&self, cx: &App) -> Hsla { self.error.unwrap_or(cx.theme().red) } #[inline] pub fn error_background(&self, cx: &App) -> Hsla { let bg = cx.theme().background; self.error_background .unwrap_or(self.error(cx).lightness(bg.l).saturation(bg.s)) } #[inline] pub fn error_border(&self, cx: &App) -> Hsla { self.error_border.unwrap_or(self.error(cx)) } #[inline] pub fn warning(&self, cx: &App) -> Hsla { self.warning.unwrap_or(cx.theme().yellow) } #[inline] pub fn warning_background(&self, cx: &App) -> Hsla { let bg = cx.theme().background; self.warning_background .unwrap_or(self.warning(cx).lightness(bg.l).saturation(bg.s)) } #[inline] pub fn warning_border(&self, cx: &App) -> Hsla { self.warning_border.unwrap_or(self.warning(cx)) } #[inline] pub fn info(&self, cx: &App) -> Hsla { self.info.unwrap_or(cx.theme().blue) } #[inline] pub fn info_background(&self, cx: &App) -> Hsla { let bg = cx.theme().background; self.info_background .unwrap_or(self.info(cx).lightness(bg.l).saturation(bg.s)) } #[inline] pub fn info_border(&self, cx: &App) -> Hsla { self.info_border.unwrap_or(self.info(cx)) } #[inline] pub fn success(&self, cx: &App) -> Hsla { self.success.unwrap_or(cx.theme().green) } #[inline] pub fn success_background(&self, cx: &App) -> Hsla { let bg = cx.theme().background; self.success_background .unwrap_or(self.success(cx).lightness(bg.l).saturation(bg.s)) } #[inline] pub fn success_border(&self, cx: &App) -> Hsla { self.success_border.unwrap_or(self.success(cx)) } #[inline] pub fn hint(&self, cx: &App) -> Hsla { self.hint.unwrap_or(cx.theme().cyan) } #[inline] pub fn hint_background(&self, cx: &App) -> Hsla { let bg = cx.theme().background; self.hint_background .unwrap_or(self.hint(cx).lightness(bg.l).saturation(bg.s)) } #[inline] pub fn hint_border(&self, cx: &App) -> Hsla { self.hint_border.unwrap_or(self.hint(cx)) } } #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)] pub struct HighlightThemeStyle { #[serde(rename = "editor.background")] pub background: Option, #[serde(rename = "editor.foreground")] pub foreground: Option, #[serde(rename = "editor.active_line.background")] pub active_line: Option, #[serde(rename = "editor.line_number")] pub line_number: Option, #[serde(rename = "editor.active_line_number")] pub active_line_number: Option, #[serde(flatten)] pub status: StatusColors, #[serde(rename = "syntax")] pub syntax: SyntaxColors, } /// Theme for Tree-sitter Highlight from JSON theme file. /// /// This json is compatible with the Zed theme format. /// /// https://zed.dev/docs/extensions/languages#syntax-highlighting #[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)] pub struct HighlightTheme { pub name: String, #[serde(default)] pub appearance: ThemeMode, pub style: HighlightThemeStyle, } impl Deref for HighlightTheme { type Target = SyntaxColors; fn deref(&self) -> &Self::Target { &self.style.syntax } } impl HighlightTheme { pub fn default_dark() -> Self { DEFAULT_DARK.clone() } pub fn default_light() -> Self { DEFAULT_LIGHT.clone() } } /// Registry for code highlighter languages. #[derive(Clone)] pub struct LanguageRegistry { languages: HashMap, } impl gpui::Global for LanguageRegistry {} impl LanguageRegistry { pub fn global(cx: &App) -> &LanguageRegistry { cx.global::() } pub fn global_mut(cx: &mut App) -> &mut LanguageRegistry { cx.global_mut::() } /// Create a new language registry with default languages and themes. pub fn new() -> Self { let mut registry = Self { languages: HashMap::new(), }; for language in languages::Language::all() { registry.register(language.name(), &language.config()); } registry } pub fn register(&mut self, lang: &str, config: &LanguageConfig) { self.languages.insert(lang.to_string(), config.clone()); } /// Returns a reference to the map of registered languages. pub fn languages(&self) -> &HashMap { &self.languages } /// Returns the language configuration for the given language name. pub fn language(&self, name: &str) -> Option<&LanguageConfig> { // Try to get by name first, there may have a custom language registered if let Some(language) = self.languages.get(name) { return Some(language); } // Then try to get built-in language to support short language names, e.g. "js" for "javascript" let language = Language::from_str(name); self.languages.get(language.name()) } } #[cfg(test)] mod tests { use gpui::rgb; use crate::highlighter::LanguageConfig; #[test] fn test_syntax_colors() { use super::{HighlightTheme, SyntaxColors}; let theme: HighlightTheme = serde_json::from_str(include_str!("./themes/light.json")).unwrap(); let syntax: &SyntaxColors = &theme.style.syntax; assert_eq!(syntax.style("keyword"), Some(rgb(0x0433ff).into())); assert_eq!(syntax.style("keyword.repeat"), Some(rgb(0x0433ff).into())); assert_eq!(syntax.style("foo"), None); } #[test] fn test_registry() { use super::LanguageRegistry; let mut registry = LanguageRegistry::new(); registry.register( "foo", &LanguageConfig::new("foo", tree_sitter_bash::LANGUAGE.into(), vec![], "", "", ""), ); assert!(registry.language("foo").is_some()); assert!(registry.language("rust").is_some()); assert!(registry.language("rs").is_some()); assert!(registry.language("javascript").is_some()); assert!(registry.language("js").is_some()); } }