mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-12 21:32:40 +00:00
Move remaining theme-related code and tests from settings mod to theme mod
This commit is contained in:
parent
90b51c3356
commit
5761756fb4
2 changed files with 266 additions and 478 deletions
|
@ -1,20 +1,10 @@
|
||||||
use anyhow::{anyhow, Context, Result};
|
|
||||||
use gpui::{
|
|
||||||
color::Color,
|
|
||||||
font_cache::{FamilyId, FontCache},
|
|
||||||
fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight},
|
|
||||||
AssetSource,
|
|
||||||
};
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use postage::watch;
|
|
||||||
use serde::{de::value::MapDeserializer, Deserialize};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
pub use theme::Theme;
|
use anyhow::Result;
|
||||||
|
use gpui::font_cache::{FamilyId, FontCache};
|
||||||
|
use postage::watch;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX);
|
pub use theme::{StyleId, Theme, ThemeMap, ThemeRegistry};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
|
@ -26,32 +16,6 @@ pub struct Settings {
|
||||||
pub theme: Arc<Theme>,
|
pub theme: Arc<Theme>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ThemeRegistry {
|
|
||||||
assets: Box<dyn AssetSource>,
|
|
||||||
themes: Mutex<HashMap<String, Arc<Theme>>>,
|
|
||||||
theme_data: Mutex<HashMap<String, Arc<ThemeToml>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct ThemeToml {
|
|
||||||
#[serde(default)]
|
|
||||||
extends: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
variables: HashMap<String, Value>,
|
|
||||||
#[serde(default)]
|
|
||||||
ui: HashMap<String, Value>,
|
|
||||||
#[serde(default)]
|
|
||||||
editor: HashMap<String, Value>,
|
|
||||||
#[serde(default)]
|
|
||||||
syntax: HashMap<String, Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct ThemeMap(Arc<[StyleId]>);
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct StyleId(u32);
|
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
pub fn new(font_cache: &FontCache) -> Result<Self> {
|
pub fn new(font_cache: &FontCache) -> Result<Self> {
|
||||||
Self::new_with_theme(font_cache, Arc::new(Theme::default()))
|
Self::new_with_theme(font_cache, Arc::new(Theme::default()))
|
||||||
|
@ -74,182 +38,6 @@ impl Settings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeRegistry {
|
|
||||||
pub fn new(source: impl AssetSource) -> Arc<Self> {
|
|
||||||
Arc::new(Self {
|
|
||||||
assets: Box::new(source),
|
|
||||||
themes: Default::default(),
|
|
||||||
theme_data: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list(&self) -> impl Iterator<Item = String> {
|
|
||||||
self.assets.list("themes/").into_iter().filter_map(|path| {
|
|
||||||
let filename = path.strip_prefix("themes/")?;
|
|
||||||
let theme_name = filename.strip_suffix(".toml")?;
|
|
||||||
if theme_name.starts_with('_') {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(theme_name.to_string())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
|
|
||||||
if let Some(theme) = self.themes.lock().get(name) {
|
|
||||||
return Ok(theme.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let theme_toml = self.load(name)?;
|
|
||||||
let mut syntax = Vec::<(String, Color, FontProperties)>::new();
|
|
||||||
for (key, style) in theme_toml.syntax.iter() {
|
|
||||||
let mut color = Color::default();
|
|
||||||
let mut properties = FontProperties::new();
|
|
||||||
match style {
|
|
||||||
Value::Object(object) => {
|
|
||||||
if let Some(value) = object.get("color") {
|
|
||||||
color = serde_json::from_value(value.clone())?;
|
|
||||||
}
|
|
||||||
if let Some(Value::Bool(true)) = object.get("italic") {
|
|
||||||
properties.style = FontStyle::Italic;
|
|
||||||
}
|
|
||||||
properties.weight = deserialize_weight(object.get("weight"))?;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
color = serde_json::from_value(style.clone())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match syntax.binary_search_by_key(&key, |e| &e.0) {
|
|
||||||
Ok(i) | Err(i) => {
|
|
||||||
syntax.insert(i, (key.to_string(), color, properties));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let theme = Arc::new(Theme {
|
|
||||||
ui: theme::Ui::deserialize(MapDeserializer::new(theme_toml.ui.clone().into_iter()))?,
|
|
||||||
editor: theme::Editor::deserialize(MapDeserializer::new(
|
|
||||||
theme_toml.editor.clone().into_iter(),
|
|
||||||
))?,
|
|
||||||
syntax,
|
|
||||||
});
|
|
||||||
|
|
||||||
self.themes.lock().insert(name.to_string(), theme.clone());
|
|
||||||
Ok(theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load(&self, name: &str) -> Result<Arc<ThemeToml>> {
|
|
||||||
if let Some(data) = self.theme_data.lock().get(name) {
|
|
||||||
return Ok(data.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let asset_path = format!("themes/{}.toml", name);
|
|
||||||
let source_code = self
|
|
||||||
.assets
|
|
||||||
.load(&asset_path)
|
|
||||||
.with_context(|| format!("failed to load theme file {}", asset_path))?;
|
|
||||||
|
|
||||||
let mut theme_toml: ThemeToml = toml::from_slice(source_code.as_ref())
|
|
||||||
.with_context(|| format!("failed to parse {}.toml", name))?;
|
|
||||||
|
|
||||||
// If this theme extends another base theme, merge in the raw data from the base theme.
|
|
||||||
if let Some(base_name) = theme_toml.extends.as_ref() {
|
|
||||||
let base_theme_toml = self
|
|
||||||
.load(base_name)
|
|
||||||
.with_context(|| format!("failed to load base theme {}", base_name))?;
|
|
||||||
merge_map(&mut theme_toml.ui, &base_theme_toml.ui);
|
|
||||||
merge_map(&mut theme_toml.editor, &base_theme_toml.editor);
|
|
||||||
merge_map(&mut theme_toml.syntax, &base_theme_toml.syntax);
|
|
||||||
merge_map(&mut theme_toml.variables, &base_theme_toml.variables);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Substitute any variable references for their definitions.
|
|
||||||
let values = theme_toml
|
|
||||||
.ui
|
|
||||||
.values_mut()
|
|
||||||
.chain(theme_toml.editor.values_mut())
|
|
||||||
.chain(theme_toml.syntax.values_mut());
|
|
||||||
let mut name_stack = Vec::new();
|
|
||||||
for value in values {
|
|
||||||
name_stack.clear();
|
|
||||||
evaluate_variables(value, &theme_toml.variables, &mut name_stack)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = Arc::new(theme_toml);
|
|
||||||
self.theme_data
|
|
||||||
.lock()
|
|
||||||
.insert(name.to_string(), result.clone());
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Theme {
|
|
||||||
pub fn syntax_style(&self, id: StyleId) -> (Color, FontProperties) {
|
|
||||||
self.syntax
|
|
||||||
.get(id.0 as usize)
|
|
||||||
.map_or((self.editor.text, FontProperties::new()), |entry| {
|
|
||||||
(entry.1, entry.2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> {
|
|
||||||
self.syntax.get(id.0 as usize).map(|e| e.0.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ThemeMap {
|
|
||||||
pub fn new(capture_names: &[String], theme: &Theme) -> Self {
|
|
||||||
// For each capture name in the highlight query, find the longest
|
|
||||||
// key in the theme's syntax styles that matches all of the
|
|
||||||
// dot-separated components of the capture name.
|
|
||||||
ThemeMap(
|
|
||||||
capture_names
|
|
||||||
.iter()
|
|
||||||
.map(|capture_name| {
|
|
||||||
theme
|
|
||||||
.syntax
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(i, (key, _, _))| {
|
|
||||||
let mut len = 0;
|
|
||||||
let capture_parts = capture_name.split('.');
|
|
||||||
for key_part in key.split('.') {
|
|
||||||
if capture_parts.clone().any(|part| part == key_part) {
|
|
||||||
len += 1;
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some((i, len))
|
|
||||||
})
|
|
||||||
.max_by_key(|(_, len)| *len)
|
|
||||||
.map_or(DEFAULT_STYLE_ID, |(i, _)| StyleId(i as u32))
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, capture_id: u32) -> StyleId {
|
|
||||||
self.0
|
|
||||||
.get(capture_id as usize)
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(DEFAULT_STYLE_ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ThemeMap {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(Arc::new([]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for StyleId {
|
|
||||||
fn default() -> Self {
|
|
||||||
DEFAULT_STYLE_ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn channel(
|
pub fn channel(
|
||||||
font_cache: &FontCache,
|
font_cache: &FontCache,
|
||||||
) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
|
) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
|
||||||
|
@ -265,264 +53,3 @@ pub fn channel_with_themes(
|
||||||
themes.get("dark").expect("failed to load default theme"),
|
themes.get("dark").expect("failed to load default theme"),
|
||||||
)?))
|
)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_weight(weight: Option<&Value>) -> Result<FontWeight> {
|
|
||||||
match weight {
|
|
||||||
None => return Ok(FontWeight::NORMAL),
|
|
||||||
Some(Value::Number(number)) => {
|
|
||||||
if let Some(weight) = number.as_f64() {
|
|
||||||
return Ok(FontWeight(weight as f32));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(Value::String(s)) => match s.as_str() {
|
|
||||||
"normal" => return Ok(FontWeight::NORMAL),
|
|
||||||
"bold" => return Ok(FontWeight::BOLD),
|
|
||||||
"light" => return Ok(FontWeight::LIGHT),
|
|
||||||
"semibold" => return Ok(FontWeight::SEMIBOLD),
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Err(anyhow!("Invalid weight {}", weight.unwrap()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn evaluate_variables(
|
|
||||||
expr: &mut Value,
|
|
||||||
variables: &HashMap<String, Value>,
|
|
||||||
stack: &mut Vec<String>,
|
|
||||||
) -> Result<()> {
|
|
||||||
match expr {
|
|
||||||
Value::String(s) => {
|
|
||||||
if let Some(name) = s.strip_prefix("$") {
|
|
||||||
if stack.iter().any(|e| e == name) {
|
|
||||||
Err(anyhow!("variable {} is defined recursively", name))?;
|
|
||||||
}
|
|
||||||
if validate_variable_name(name) {
|
|
||||||
stack.push(name.to_string());
|
|
||||||
if let Some(definition) = variables.get(name).cloned() {
|
|
||||||
*expr = definition;
|
|
||||||
evaluate_variables(expr, variables, stack)?;
|
|
||||||
}
|
|
||||||
stack.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Value::Array(a) => {
|
|
||||||
for value in a.iter_mut() {
|
|
||||||
evaluate_variables(value, variables, stack)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Value::Object(object) => {
|
|
||||||
for value in object.values_mut() {
|
|
||||||
evaluate_variables(value, variables, stack)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_variable_name(name: &str) -> bool {
|
|
||||||
let mut chars = name.chars();
|
|
||||||
if let Some(first) = chars.next() {
|
|
||||||
if first.is_alphabetic() || first == '_' {
|
|
||||||
if chars.all(|c| c.is_alphanumeric() || c == '_') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_map(left: &mut HashMap<String, Value>, right: &HashMap<String, Value>) {
|
|
||||||
for (name, value) in right {
|
|
||||||
if !left.contains_key(name) {
|
|
||||||
left.insert(name.clone(), value.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_simple_theme() {
|
|
||||||
let assets = TestAssets(&[(
|
|
||||||
"themes/my-theme.toml",
|
|
||||||
r#"
|
|
||||||
[ui.tab.active]
|
|
||||||
background = 0x100000
|
|
||||||
|
|
||||||
[editor]
|
|
||||||
background = 0x00ed00
|
|
||||||
line_number = 0xdddddd
|
|
||||||
|
|
||||||
[syntax]
|
|
||||||
"beta.two" = 0xAABBCC
|
|
||||||
"alpha.one" = {color = 0x112233, weight = "bold"}
|
|
||||||
"gamma.three" = {weight = "light", italic = true}
|
|
||||||
"#,
|
|
||||||
)]);
|
|
||||||
|
|
||||||
let registry = ThemeRegistry::new(assets);
|
|
||||||
let theme = registry.get("my-theme").unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
theme.ui.active_tab.container.background_color,
|
|
||||||
Some(Color::from_u32(0x100000ff))
|
|
||||||
);
|
|
||||||
assert_eq!(theme.editor.background, Color::from_u32(0x00ed00ff));
|
|
||||||
assert_eq!(theme.editor.line_number, Color::from_u32(0xddddddff));
|
|
||||||
assert_eq!(
|
|
||||||
theme.syntax,
|
|
||||||
&[
|
|
||||||
(
|
|
||||||
"alpha.one".to_string(),
|
|
||||||
Color::from_u32(0x112233ff),
|
|
||||||
*FontProperties::new().weight(FontWeight::BOLD)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"beta.two".to_string(),
|
|
||||||
Color::from_u32(0xaabbccff),
|
|
||||||
*FontProperties::new().weight(FontWeight::NORMAL)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"gamma.three".to_string(),
|
|
||||||
Color::from_u32(0x00000000),
|
|
||||||
*FontProperties::new()
|
|
||||||
.weight(FontWeight::LIGHT)
|
|
||||||
.style(FontStyle::Italic),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_extended_theme() {
|
|
||||||
let assets = TestAssets(&[
|
|
||||||
(
|
|
||||||
"themes/_base.toml",
|
|
||||||
r#"
|
|
||||||
abstract = true
|
|
||||||
|
|
||||||
[ui.tab]
|
|
||||||
background = 0x111111
|
|
||||||
text = "$variable_1"
|
|
||||||
|
|
||||||
[editor]
|
|
||||||
background = 0x222222
|
|
||||||
default_text = "$variable_2"
|
|
||||||
"#,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"themes/light.toml",
|
|
||||||
r#"
|
|
||||||
extends = "_base"
|
|
||||||
|
|
||||||
[variables]
|
|
||||||
variable_1 = 0x333333
|
|
||||||
variable_2 = 0x444444
|
|
||||||
|
|
||||||
[ui.tab]
|
|
||||||
background = 0x555555
|
|
||||||
|
|
||||||
[editor]
|
|
||||||
background = 0x666666
|
|
||||||
"#,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"themes/dark.toml",
|
|
||||||
r#"
|
|
||||||
extends = "_base"
|
|
||||||
|
|
||||||
[variables]
|
|
||||||
variable_1 = 0x555555
|
|
||||||
variable_2 = 0x666666
|
|
||||||
"#,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let registry = ThemeRegistry::new(assets);
|
|
||||||
let theme = registry.get("light").unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
theme.ui.tab.container.background_color,
|
|
||||||
Some(Color::from_u32(0x555555ff))
|
|
||||||
);
|
|
||||||
assert_eq!(theme.ui.tab.label.color, Color::from_u32(0x333333ff));
|
|
||||||
assert_eq!(theme.editor.background, Color::from_u32(0x666666ff));
|
|
||||||
assert_eq!(theme.editor.text, Color::from_u32(0x444444ff));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
registry.list().collect::<Vec<_>>(),
|
|
||||||
&["light".to_string(), "dark".to_string()]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_empty_theme() {
|
|
||||||
let assets = TestAssets(&[("themes/my-theme.toml", "")]);
|
|
||||||
let registry = ThemeRegistry::new(assets);
|
|
||||||
registry.get("my-theme").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_theme_map() {
|
|
||||||
let theme = Theme {
|
|
||||||
ui: Default::default(),
|
|
||||||
editor: Default::default(),
|
|
||||||
syntax: [
|
|
||||||
("function", Color::from_u32(0x100000ff)),
|
|
||||||
("function.method", Color::from_u32(0x200000ff)),
|
|
||||||
("function.async", Color::from_u32(0x300000ff)),
|
|
||||||
("variable.builtin.self.rust", Color::from_u32(0x400000ff)),
|
|
||||||
("variable.builtin", Color::from_u32(0x500000ff)),
|
|
||||||
("variable", Color::from_u32(0x600000ff)),
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.map(|e| (e.0.to_string(), e.1, FontProperties::new()))
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let capture_names = &[
|
|
||||||
"function.special".to_string(),
|
|
||||||
"function.async.rust".to_string(),
|
|
||||||
"variable.builtin.self".to_string(),
|
|
||||||
];
|
|
||||||
|
|
||||||
let map = ThemeMap::new(capture_names, &theme);
|
|
||||||
assert_eq!(theme.syntax_style_name(map.get(0)), Some("function"));
|
|
||||||
assert_eq!(theme.syntax_style_name(map.get(1)), Some("function.async"));
|
|
||||||
assert_eq!(
|
|
||||||
theme.syntax_style_name(map.get(2)),
|
|
||||||
Some("variable.builtin")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestAssets(&'static [(&'static str, &'static str)]);
|
|
||||||
|
|
||||||
impl AssetSource for TestAssets {
|
|
||||||
fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
|
|
||||||
if let Some(row) = self.0.iter().find(|e| e.0 == path) {
|
|
||||||
Ok(row.1.as_bytes().into())
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("no such path {}", path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
|
|
||||||
self.0
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter_map(|(path, _)| {
|
|
||||||
if path.starts_with(prefix) {
|
|
||||||
Some(path.into())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
261
zed/src/theme.rs
261
zed/src/theme.rs
|
@ -11,12 +11,20 @@ use serde::{de, Deserialize, Deserializer};
|
||||||
use serde_json as json;
|
use serde_json as json;
|
||||||
use std::{cmp::Ordering, collections::HashMap, sync::Arc};
|
use std::{cmp::Ordering, collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX);
|
||||||
|
|
||||||
pub struct ThemeRegistry {
|
pub struct ThemeRegistry {
|
||||||
assets: Box<dyn AssetSource>,
|
assets: Box<dyn AssetSource>,
|
||||||
themes: Mutex<HashMap<String, Arc<Theme>>>,
|
themes: Mutex<HashMap<String, Arc<Theme>>>,
|
||||||
theme_data: Mutex<HashMap<String, Arc<Value>>>,
|
theme_data: Mutex<HashMap<String, Arc<Value>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ThemeMap(Arc<[StyleId]>);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct StyleId(u32);
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub ui: Ui,
|
pub ui: Ui,
|
||||||
|
@ -204,6 +212,73 @@ impl ThemeRegistry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
pub fn syntax_style(&self, id: StyleId) -> (Color, FontProperties) {
|
||||||
|
self.syntax
|
||||||
|
.get(id.0 as usize)
|
||||||
|
.map_or((self.editor.text, FontProperties::new()), |entry| {
|
||||||
|
(entry.1, entry.2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> {
|
||||||
|
self.syntax.get(id.0 as usize).map(|e| e.0.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeMap {
|
||||||
|
pub fn new(capture_names: &[String], theme: &Theme) -> Self {
|
||||||
|
// For each capture name in the highlight query, find the longest
|
||||||
|
// key in the theme's syntax styles that matches all of the
|
||||||
|
// dot-separated components of the capture name.
|
||||||
|
ThemeMap(
|
||||||
|
capture_names
|
||||||
|
.iter()
|
||||||
|
.map(|capture_name| {
|
||||||
|
theme
|
||||||
|
.syntax
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, (key, _, _))| {
|
||||||
|
let mut len = 0;
|
||||||
|
let capture_parts = capture_name.split('.');
|
||||||
|
for key_part in key.split('.') {
|
||||||
|
if capture_parts.clone().any(|part| part == key_part) {
|
||||||
|
len += 1;
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((i, len))
|
||||||
|
})
|
||||||
|
.max_by_key(|(_, len)| *len)
|
||||||
|
.map_or(DEFAULT_STYLE_ID, |(i, _)| StyleId(i as u32))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, capture_id: u32) -> StyleId {
|
||||||
|
self.0
|
||||||
|
.get(capture_id as usize)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(DEFAULT_STYLE_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ThemeMap {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Arc::new([]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StyleId {
|
||||||
|
fn default() -> Self {
|
||||||
|
DEFAULT_STYLE_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>) {
|
fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>) {
|
||||||
for (key, extension_value) in extension {
|
for (key, extension_value) in extension {
|
||||||
if let Value::Object(extension_object) = extension_value {
|
if let Value::Object(extension_object) = extension_value {
|
||||||
|
@ -384,3 +459,189 @@ where
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use gpui::fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_simple_theme() {
|
||||||
|
let assets = TestAssets(&[(
|
||||||
|
"themes/my-theme.toml",
|
||||||
|
r#"
|
||||||
|
[ui.tab.active]
|
||||||
|
background = 0x100000
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
background = 0x00ed00
|
||||||
|
line_number = 0xdddddd
|
||||||
|
|
||||||
|
[syntax]
|
||||||
|
"beta.two" = 0xAABBCC
|
||||||
|
"alpha.one" = {color = 0x112233, weight = "bold"}
|
||||||
|
"gamma.three" = {weight = "light", italic = true}
|
||||||
|
"#,
|
||||||
|
)]);
|
||||||
|
|
||||||
|
let registry = ThemeRegistry::new(assets);
|
||||||
|
let theme = registry.get("my-theme").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
theme.ui.active_tab.container.background_color,
|
||||||
|
Some(Color::from_u32(0x100000ff))
|
||||||
|
);
|
||||||
|
assert_eq!(theme.editor.background, Color::from_u32(0x00ed00ff));
|
||||||
|
assert_eq!(theme.editor.line_number, Color::from_u32(0xddddddff));
|
||||||
|
assert_eq!(
|
||||||
|
theme.syntax,
|
||||||
|
&[
|
||||||
|
(
|
||||||
|
"alpha.one".to_string(),
|
||||||
|
Color::from_u32(0x112233ff),
|
||||||
|
*FontProperties::new().weight(FontWeight::BOLD)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"beta.two".to_string(),
|
||||||
|
Color::from_u32(0xaabbccff),
|
||||||
|
*FontProperties::new().weight(FontWeight::NORMAL)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gamma.three".to_string(),
|
||||||
|
Color::from_u32(0x00000000),
|
||||||
|
*FontProperties::new()
|
||||||
|
.weight(FontWeight::LIGHT)
|
||||||
|
.style(FontStyle::Italic),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_extended_theme() {
|
||||||
|
let assets = TestAssets(&[
|
||||||
|
(
|
||||||
|
"themes/_base.toml",
|
||||||
|
r#"
|
||||||
|
abstract = true
|
||||||
|
|
||||||
|
[ui.tab]
|
||||||
|
background = 0x111111
|
||||||
|
text = "$variable_1"
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
background = 0x222222
|
||||||
|
default_text = "$variable_2"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"themes/light.toml",
|
||||||
|
r#"
|
||||||
|
extends = "_base"
|
||||||
|
|
||||||
|
[variables]
|
||||||
|
variable_1 = 0x333333
|
||||||
|
variable_2 = 0x444444
|
||||||
|
|
||||||
|
[ui.tab]
|
||||||
|
background = 0x555555
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
background = 0x666666
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"themes/dark.toml",
|
||||||
|
r#"
|
||||||
|
extends = "_base"
|
||||||
|
|
||||||
|
[variables]
|
||||||
|
variable_1 = 0x555555
|
||||||
|
variable_2 = 0x666666
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let registry = ThemeRegistry::new(assets);
|
||||||
|
let theme = registry.get("light").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
theme.ui.tab.container.background_color,
|
||||||
|
Some(Color::from_u32(0x555555ff))
|
||||||
|
);
|
||||||
|
assert_eq!(theme.ui.tab.label.color, Color::from_u32(0x333333ff));
|
||||||
|
assert_eq!(theme.editor.background, Color::from_u32(0x666666ff));
|
||||||
|
assert_eq!(theme.editor.text, Color::from_u32(0x444444ff));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
registry.list().collect::<Vec<_>>(),
|
||||||
|
&["light".to_string(), "dark".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_empty_theme() {
|
||||||
|
let assets = TestAssets(&[("themes/my-theme.toml", "")]);
|
||||||
|
let registry = ThemeRegistry::new(assets);
|
||||||
|
registry.get("my-theme").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_theme_map() {
|
||||||
|
let theme = Theme {
|
||||||
|
ui: Default::default(),
|
||||||
|
editor: Default::default(),
|
||||||
|
syntax: [
|
||||||
|
("function", Color::from_u32(0x100000ff)),
|
||||||
|
("function.method", Color::from_u32(0x200000ff)),
|
||||||
|
("function.async", Color::from_u32(0x300000ff)),
|
||||||
|
("variable.builtin.self.rust", Color::from_u32(0x400000ff)),
|
||||||
|
("variable.builtin", Color::from_u32(0x500000ff)),
|
||||||
|
("variable", Color::from_u32(0x600000ff)),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|e| (e.0.to_string(), e.1, FontProperties::new()))
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let capture_names = &[
|
||||||
|
"function.special".to_string(),
|
||||||
|
"function.async.rust".to_string(),
|
||||||
|
"variable.builtin.self".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let map = ThemeMap::new(capture_names, &theme);
|
||||||
|
assert_eq!(theme.syntax_style_name(map.get(0)), Some("function"));
|
||||||
|
assert_eq!(theme.syntax_style_name(map.get(1)), Some("function.async"));
|
||||||
|
assert_eq!(
|
||||||
|
theme.syntax_style_name(map.get(2)),
|
||||||
|
Some("variable.builtin")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestAssets(&'static [(&'static str, &'static str)]);
|
||||||
|
|
||||||
|
impl AssetSource for TestAssets {
|
||||||
|
fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
|
||||||
|
if let Some(row) = self.0.iter().find(|e| e.0 == path) {
|
||||||
|
Ok(row.1.as_bytes().into())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("no such path {}", path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter_map(|(path, _)| {
|
||||||
|
if path.starts_with(prefix) {
|
||||||
|
Some(path.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue