- Added a lot of default fallbacks for theme colors based on `base` colors. - Added `hue`, `saturation`, `lightness` method to change Hsla color. - Update stories to use theme colors, not a direct color. - Improve `Badge`, `Avatar` to use theme colors. - Added a new theme: `Fahrenheit`.
530 lines
16 KiB
Rust
530 lines
16 KiB
Rust
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<HighlightTheme> = LazyLock::new(|| {
|
|
let json = include_str!("./themes/dark.json");
|
|
serde_json::from_str(json).unwrap()
|
|
});
|
|
const DEFAULT_LIGHT: LazyLock<HighlightTheme> = 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<ThemeStyle>,
|
|
pub boolean: Option<ThemeStyle>,
|
|
pub comment: Option<ThemeStyle>,
|
|
pub comment_doc: Option<ThemeStyle>,
|
|
pub constant: Option<ThemeStyle>,
|
|
pub constructor: Option<ThemeStyle>,
|
|
pub embedded: Option<ThemeStyle>,
|
|
pub emphasis: Option<ThemeStyle>,
|
|
#[serde(rename = "emphasis.strong")]
|
|
pub emphasis_strong: Option<ThemeStyle>,
|
|
#[serde(rename = "enum")]
|
|
pub enum_: Option<ThemeStyle>,
|
|
pub function: Option<ThemeStyle>,
|
|
pub hint: Option<ThemeStyle>,
|
|
pub keyword: Option<ThemeStyle>,
|
|
pub label: Option<ThemeStyle>,
|
|
#[serde(rename = "link_text")]
|
|
pub link_text: Option<ThemeStyle>,
|
|
#[serde(rename = "link_uri")]
|
|
pub link_uri: Option<ThemeStyle>,
|
|
pub number: Option<ThemeStyle>,
|
|
pub operator: Option<ThemeStyle>,
|
|
pub predictive: Option<ThemeStyle>,
|
|
pub preproc: Option<ThemeStyle>,
|
|
pub primary: Option<ThemeStyle>,
|
|
pub property: Option<ThemeStyle>,
|
|
pub punctuation: Option<ThemeStyle>,
|
|
#[serde(rename = "punctuation.bracket")]
|
|
pub punctuation_bracket: Option<ThemeStyle>,
|
|
#[serde(rename = "punctuation.delimiter")]
|
|
pub punctuation_delimiter: Option<ThemeStyle>,
|
|
#[serde(rename = "punctuation.list_marker")]
|
|
pub punctuation_list_marker: Option<ThemeStyle>,
|
|
#[serde(rename = "punctuation.special")]
|
|
pub punctuation_special: Option<ThemeStyle>,
|
|
pub string: Option<ThemeStyle>,
|
|
#[serde(rename = "string.escape")]
|
|
pub string_escape: Option<ThemeStyle>,
|
|
#[serde(rename = "string.regex")]
|
|
pub string_regex: Option<ThemeStyle>,
|
|
#[serde(rename = "string.special")]
|
|
pub string_special: Option<ThemeStyle>,
|
|
#[serde(rename = "string.special.symbol")]
|
|
pub string_special_symbol: Option<ThemeStyle>,
|
|
pub tag: Option<ThemeStyle>,
|
|
#[serde(rename = "tag.doctype")]
|
|
pub tag_doctype: Option<ThemeStyle>,
|
|
#[serde(rename = "text.literal")]
|
|
pub text_literal: Option<ThemeStyle>,
|
|
pub title: Option<ThemeStyle>,
|
|
#[serde(rename = "type")]
|
|
pub type_: Option<ThemeStyle>,
|
|
pub variable: Option<ThemeStyle>,
|
|
#[serde(rename = "variable.special")]
|
|
pub variable_special: Option<ThemeStyle>,
|
|
pub variant: Option<ThemeStyle>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum FontStyle {
|
|
Normal,
|
|
Italic,
|
|
Underline,
|
|
}
|
|
|
|
impl From<FontStyle> 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<FontWeightContent> 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<Hsla>,
|
|
font_style: Option<FontStyle>,
|
|
font_weight: Option<FontWeightContent>,
|
|
}
|
|
|
|
impl From<ThemeStyle> 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<HighlightStyle> {
|
|
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<HighlightStyle> {
|
|
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<Hsla>,
|
|
#[serde(rename = "error.background")]
|
|
error_background: Option<Hsla>,
|
|
#[serde(rename = "error.border")]
|
|
error_border: Option<Hsla>,
|
|
#[serde(rename = "warning")]
|
|
warning: Option<Hsla>,
|
|
#[serde(rename = "warning.background")]
|
|
warning_background: Option<Hsla>,
|
|
#[serde(rename = "warning.border")]
|
|
warning_border: Option<Hsla>,
|
|
#[serde(rename = "info")]
|
|
info: Option<Hsla>,
|
|
#[serde(rename = "info.background")]
|
|
info_background: Option<Hsla>,
|
|
#[serde(rename = "info.border")]
|
|
info_border: Option<Hsla>,
|
|
#[serde(rename = "success")]
|
|
success: Option<Hsla>,
|
|
#[serde(rename = "success.background")]
|
|
success_background: Option<Hsla>,
|
|
#[serde(rename = "success.border")]
|
|
success_border: Option<Hsla>,
|
|
#[serde(rename = "hint")]
|
|
hint: Option<Hsla>,
|
|
#[serde(rename = "hint.background")]
|
|
hint_background: Option<Hsla>,
|
|
#[serde(rename = "hint.border")]
|
|
hint_border: Option<Hsla>,
|
|
}
|
|
|
|
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<Hsla>,
|
|
#[serde(rename = "editor.foreground")]
|
|
pub foreground: Option<Hsla>,
|
|
#[serde(rename = "editor.active_line.background")]
|
|
pub active_line: Option<Hsla>,
|
|
#[serde(rename = "editor.line_number")]
|
|
pub line_number: Option<Hsla>,
|
|
#[serde(rename = "editor.active_line_number")]
|
|
pub active_line_number: Option<Hsla>,
|
|
#[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<String, LanguageConfig>,
|
|
}
|
|
|
|
impl gpui::Global for LanguageRegistry {}
|
|
|
|
impl LanguageRegistry {
|
|
pub fn global(cx: &App) -> &LanguageRegistry {
|
|
cx.global::<LanguageRegistry>()
|
|
}
|
|
|
|
pub fn global_mut(cx: &mut App) -> &mut LanguageRegistry {
|
|
cx.global_mut::<LanguageRegistry>()
|
|
}
|
|
|
|
/// 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<String, LanguageConfig> {
|
|
&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());
|
|
}
|
|
}
|