Convert query capture indices to style ids
* Introduce a Theme struct as a new part of the app's settings * Store on each Language a ThemeMap, which converts the capture ids from that language's highlight query into StyleIds, which identify styles in the current Theme. * Update `highlighted_chunks` methods to provide StyleIds instead of capture ids.
This commit is contained in:
+261
-2
@@ -1,6 +1,15 @@
|
||||
use anyhow::Result;
|
||||
use gpui::font_cache::{FamilyId, FontCache};
|
||||
use super::assets::Assets;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use gpui::{
|
||||
color::ColorU,
|
||||
font_cache::{FamilyId, FontCache},
|
||||
fonts::Weight,
|
||||
};
|
||||
use postage::watch;
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Settings {
|
||||
@@ -9,8 +18,23 @@ pub struct Settings {
|
||||
pub tab_size: usize,
|
||||
pub ui_font_family: FamilyId,
|
||||
pub ui_font_size: f32,
|
||||
pub theme: Arc<Theme>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Theme {
|
||||
pub background_color: ColorU,
|
||||
pub line_number_color: ColorU,
|
||||
pub default_text_color: ColorU,
|
||||
syntax_styles: Vec<(String, ColorU, Weight)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ThemeMap(Arc<[StyleId]>);
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct StyleId(u32);
|
||||
|
||||
impl Settings {
|
||||
pub fn new(font_cache: &FontCache) -> Result<Self> {
|
||||
Ok(Self {
|
||||
@@ -19,12 +43,247 @@ impl Settings {
|
||||
tab_size: 4,
|
||||
ui_font_family: font_cache.load_family(&["SF Pro", "Helvetica"])?,
|
||||
ui_font_size: 12.0,
|
||||
theme: Arc::new(
|
||||
Theme::parse(Assets::get("themes/light.toml").unwrap())
|
||||
.expect("Failed to parse built-in theme"),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn parse(source: impl AsRef<[u8]>) -> Result<Self> {
|
||||
#[derive(Deserialize)]
|
||||
struct ThemeToml {
|
||||
#[serde(default)]
|
||||
syntax: HashMap<String, StyleToml>,
|
||||
#[serde(default)]
|
||||
ui: HashMap<String, u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum StyleToml {
|
||||
Color(u32),
|
||||
Full {
|
||||
color: Option<u32>,
|
||||
weight: Option<toml::Value>,
|
||||
},
|
||||
}
|
||||
|
||||
let theme_toml: ThemeToml =
|
||||
toml::from_slice(source.as_ref()).context("failed to parse theme TOML")?;
|
||||
|
||||
let mut syntax_styles = Vec::<(String, ColorU, Weight)>::new();
|
||||
for (key, style) in theme_toml.syntax {
|
||||
let (color, weight) = match style {
|
||||
StyleToml::Color(color) => (color, None),
|
||||
StyleToml::Full { color, weight } => (color.unwrap_or(0), weight),
|
||||
};
|
||||
match syntax_styles.binary_search_by_key(&&key, |e| &e.0) {
|
||||
Ok(i) | Err(i) => syntax_styles.insert(
|
||||
i,
|
||||
(key, deserialize_color(color), deserialize_weight(weight)?),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let background_color = theme_toml
|
||||
.ui
|
||||
.get("background")
|
||||
.copied()
|
||||
.map_or(ColorU::from_u32(0xffffffff), deserialize_color);
|
||||
let line_number_color = theme_toml
|
||||
.ui
|
||||
.get("line_numbers")
|
||||
.copied()
|
||||
.map_or(ColorU::black(), deserialize_color);
|
||||
let default_text_color = theme_toml
|
||||
.ui
|
||||
.get("text")
|
||||
.copied()
|
||||
.map_or(ColorU::black(), deserialize_color);
|
||||
|
||||
Ok(Theme {
|
||||
background_color,
|
||||
line_number_color,
|
||||
default_text_color,
|
||||
syntax_styles,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn syntax_style(&self, id: StyleId) -> (ColorU, Weight) {
|
||||
self.syntax_styles
|
||||
.get(id.0 as usize)
|
||||
.map_or((self.default_text_color, Weight::NORMAL), |entry| {
|
||||
(entry.1, entry.2)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> {
|
||||
self.syntax_styles.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_styles
|
||||
.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(
|
||||
font_cache: &FontCache,
|
||||
) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
|
||||
Ok(watch::channel_with(Settings::new(font_cache)?))
|
||||
}
|
||||
|
||||
fn deserialize_color(color: u32) -> ColorU {
|
||||
ColorU::from_u32((color << 8) + 0xFF)
|
||||
}
|
||||
|
||||
fn deserialize_weight(weight: Option<toml::Value>) -> Result<Weight> {
|
||||
match &weight {
|
||||
None => return Ok(Weight::NORMAL),
|
||||
Some(toml::Value::Integer(i)) => return Ok(Weight(*i as f32)),
|
||||
Some(toml::Value::String(s)) => match s.as_str() {
|
||||
"normal" => return Ok(Weight::NORMAL),
|
||||
"bold" => return Ok(Weight::BOLD),
|
||||
"light" => return Ok(Weight::LIGHT),
|
||||
"semibold" => return Ok(Weight::SEMIBOLD),
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Err(anyhow!("Invalid weight {}", weight.unwrap()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_theme() {
|
||||
let theme = Theme::parse(
|
||||
r#"
|
||||
[ui]
|
||||
background = 0x00ed00
|
||||
line_numbers = 0xdddddd
|
||||
|
||||
[syntax]
|
||||
"beta.two" = 0xAABBCC
|
||||
"alpha.one" = {color = 0x112233, weight = "bold"}
|
||||
"gamma.three" = {weight = "light"}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(theme.background_color, ColorU::from_u32(0x00ED00FF));
|
||||
assert_eq!(theme.line_number_color, ColorU::from_u32(0xddddddff));
|
||||
assert_eq!(
|
||||
theme.syntax_styles,
|
||||
&[
|
||||
(
|
||||
"alpha.one".to_string(),
|
||||
ColorU::from_u32(0x112233FF),
|
||||
Weight::BOLD
|
||||
),
|
||||
(
|
||||
"beta.two".to_string(),
|
||||
ColorU::from_u32(0xAABBCCFF),
|
||||
Weight::NORMAL
|
||||
),
|
||||
(
|
||||
"gamma.three".to_string(),
|
||||
ColorU::from_u32(0x000000FF),
|
||||
Weight::LIGHT,
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty_theme() {
|
||||
Theme::parse("").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_map() {
|
||||
let theme = Theme {
|
||||
default_text_color: Default::default(),
|
||||
background_color: ColorU::default(),
|
||||
line_number_color: ColorU::default(),
|
||||
syntax_styles: [
|
||||
("function", ColorU::from_u32(0x100000ff)),
|
||||
("function.method", ColorU::from_u32(0x200000ff)),
|
||||
("function.async", ColorU::from_u32(0x300000ff)),
|
||||
("variable.builtin.self.rust", ColorU::from_u32(0x400000ff)),
|
||||
("variable.builtin", ColorU::from_u32(0x500000ff)),
|
||||
("variable", ColorU::from_u32(0x600000ff)),
|
||||
]
|
||||
.iter()
|
||||
.map(|e| (e.0.to_string(), e.1, Weight::NORMAL))
|
||||
.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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user