Detailed changes
@@ -17110,6 +17110,15 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "syntax_theme"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "sys-locale"
version = "0.3.2"
@@ -17569,6 +17578,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"strum 0.27.2",
+ "syntax_theme",
"thiserror 2.0.17",
"uuid",
]
@@ -187,6 +187,7 @@ members = [
"crates/sum_tree",
"crates/svg_preview",
"crates/system_specs",
+ "crates/syntax_theme",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
@@ -435,6 +436,7 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
codestral = { path = "crates/codestral" }
system_specs = { path = "crates/system_specs" }
+syntax_theme = { path = "crates/syntax_theme" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
@@ -0,0 +1,26 @@
+[package]
+name = "syntax_theme"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[features]
+default = []
+test-support = ["gpui/test-support"]
+bundled-themes = ["dep:serde", "dep:serde_json"]
+
+[lib]
+path = "src/syntax_theme.rs"
+doctest = false
+
+[dependencies]
+gpui.workspace = true
+serde = { workspace = true, optional = true }
+serde_json = { workspace = true, optional = true }
+
+[dev-dependencies]
+gpui = { workspace = true, features = ["test-support"] }
@@ -0,0 +1 @@
+../../LICENSE-APACHE
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,336 @@
+#![allow(missing_docs)]
+
+use std::{
+ collections::{BTreeMap, btree_map::Entry},
+ sync::Arc,
+};
+
+use gpui::HighlightStyle;
+#[cfg(any(test, feature = "test-support"))]
+use gpui::Hsla;
+
+#[derive(Debug, PartialEq, Eq, Clone, Default)]
+pub struct SyntaxTheme {
+ highlights: Vec<HighlightStyle>,
+ capture_name_map: BTreeMap<String, usize>,
+}
+
+impl SyntaxTheme {
+ pub fn new(highlights: impl IntoIterator<Item = (String, HighlightStyle)>) -> Self {
+ let (capture_names, highlights) = highlights.into_iter().unzip();
+
+ Self {
+ capture_name_map: Self::create_capture_name_map(capture_names),
+ highlights,
+ }
+ }
+
+ fn create_capture_name_map(highlights: Vec<String>) -> BTreeMap<String, usize> {
+ highlights
+ .into_iter()
+ .enumerate()
+ .map(|(i, key)| (key, i))
+ .collect()
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn new_test(colors: impl IntoIterator<Item = (&'static str, Hsla)>) -> Self {
+ Self::new_test_styles(colors.into_iter().map(|(key, color)| {
+ (
+ key,
+ HighlightStyle {
+ color: Some(color),
+ ..Default::default()
+ },
+ )
+ }))
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn new_test_styles(
+ colors: impl IntoIterator<Item = (&'static str, HighlightStyle)>,
+ ) -> Self {
+ Self::new(
+ colors
+ .into_iter()
+ .map(|(key, style)| (key.to_owned(), style)),
+ )
+ }
+
+ pub fn get(&self, highlight_index: impl Into<usize>) -> Option<&HighlightStyle> {
+ self.highlights.get(highlight_index.into())
+ }
+
+ pub fn style_for_name(&self, name: &str) -> Option<HighlightStyle> {
+ self.capture_name_map
+ .get(name)
+ .map(|highlight_idx| self.highlights[*highlight_idx])
+ }
+
+ pub fn get_capture_name(&self, idx: impl Into<usize>) -> Option<&str> {
+ let idx = idx.into();
+ self.capture_name_map
+ .iter()
+ .find(|(_, value)| **value == idx)
+ .map(|(key, _)| key.as_ref())
+ }
+
+ pub fn highlight_id(&self, capture_name: &str) -> Option<u32> {
+ self.capture_name_map
+ .range::<str, _>((
+ capture_name.split(".").next().map_or(
+ std::ops::Bound::Included(capture_name),
+ std::ops::Bound::Included,
+ ),
+ std::ops::Bound::Included(capture_name),
+ ))
+ .rfind(|(prefix, _)| {
+ capture_name
+ .strip_prefix(*prefix)
+ .is_some_and(|remainder| remainder.is_empty() || remainder.starts_with('.'))
+ })
+ .map(|(_, index)| *index as u32)
+ }
+
+ /// Returns a new [`Arc<SyntaxTheme>`] with the given syntax styles merged in.
+ pub fn merge(base: Arc<Self>, user_syntax_styles: Vec<(String, HighlightStyle)>) -> Arc<Self> {
+ if user_syntax_styles.is_empty() {
+ return base;
+ }
+
+ let mut base = Arc::try_unwrap(base).unwrap_or_else(|base| (*base).clone());
+
+ for (name, highlight) in user_syntax_styles {
+ match base.capture_name_map.entry(name) {
+ Entry::Occupied(entry) => {
+ if let Some(existing_highlight) = base.highlights.get_mut(*entry.get()) {
+ existing_highlight.color = highlight.color.or(existing_highlight.color);
+ existing_highlight.font_weight =
+ highlight.font_weight.or(existing_highlight.font_weight);
+ existing_highlight.font_style =
+ highlight.font_style.or(existing_highlight.font_style);
+ existing_highlight.background_color = highlight
+ .background_color
+ .or(existing_highlight.background_color);
+ existing_highlight.underline =
+ highlight.underline.or(existing_highlight.underline);
+ existing_highlight.strikethrough =
+ highlight.strikethrough.or(existing_highlight.strikethrough);
+ existing_highlight.fade_out =
+ highlight.fade_out.or(existing_highlight.fade_out);
+ }
+ }
+ Entry::Vacant(vacant) => {
+ vacant.insert(base.highlights.len());
+ base.highlights.push(highlight);
+ }
+ }
+ }
+
+ Arc::new(base)
+ }
+}
+
+#[cfg(feature = "bundled-themes")]
+mod bundled_themes {
+ use std::collections::BTreeMap;
+ use std::sync::Arc;
+
+ use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, Rgba, rgb};
+ use serde::Deserialize;
+
+ use super::SyntaxTheme;
+
+ #[derive(Deserialize)]
+ struct ThemeFile {
+ themes: Vec<ThemeEntry>,
+ }
+
+ #[derive(Deserialize)]
+ struct ThemeEntry {
+ name: String,
+ style: ThemeStyle,
+ }
+
+ #[derive(Deserialize)]
+ struct ThemeStyle {
+ syntax: BTreeMap<String, SyntaxStyleEntry>,
+ }
+
+ #[derive(Deserialize)]
+ struct SyntaxStyleEntry {
+ color: Option<String>,
+ font_weight: Option<f32>,
+ font_style: Option<String>,
+ }
+
+ impl SyntaxStyleEntry {
+ fn to_highlight_style(&self) -> HighlightStyle {
+ HighlightStyle {
+ color: self.color.as_deref().map(hex_to_hsla),
+ font_weight: self.font_weight.map(FontWeight),
+ font_style: self.font_style.as_deref().and_then(|s| match s {
+ "italic" => Some(FontStyle::Italic),
+ "normal" => Some(FontStyle::Normal),
+ "oblique" => Some(FontStyle::Oblique),
+ _ => None,
+ }),
+ ..Default::default()
+ }
+ }
+ }
+
+ fn hex_to_hsla(hex: &str) -> Hsla {
+ let hex = hex.trim_start_matches('#');
+ let rgba: Rgba = match hex.len() {
+ 6 => rgb(u32::from_str_radix(hex, 16).unwrap_or(0)),
+ 8 => {
+ let value = u32::from_str_radix(hex, 16).unwrap_or(0);
+ Rgba {
+ r: ((value >> 24) & 0xff) as f32 / 255.0,
+ g: ((value >> 16) & 0xff) as f32 / 255.0,
+ b: ((value >> 8) & 0xff) as f32 / 255.0,
+ a: (value & 0xff) as f32 / 255.0,
+ }
+ }
+ _ => rgb(0),
+ };
+ rgba.into()
+ }
+
+ fn load_theme(json: &str, theme_name: &str) -> Arc<SyntaxTheme> {
+ let theme_file: ThemeFile = serde_json::from_str(json).expect("failed to parse theme JSON");
+ let theme_entry = theme_file
+ .themes
+ .iter()
+ .find(|entry| entry.name == theme_name)
+ .unwrap_or_else(|| panic!("theme {theme_name:?} not found in theme JSON"));
+
+ let highlights = theme_entry
+ .style
+ .syntax
+ .iter()
+ .map(|(name, entry)| (name.clone(), entry.to_highlight_style()));
+
+ Arc::new(SyntaxTheme::new(highlights))
+ }
+
+ impl SyntaxTheme {
+ /// Load the "One Dark" syntax theme from the bundled theme JSON.
+ pub fn one_dark() -> Arc<Self> {
+ load_theme(
+ include_str!("../../../assets/themes/one/one.json"),
+ "One Dark",
+ )
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use gpui::FontStyle;
+
+ use super::*;
+
+ #[test]
+ fn test_syntax_theme_merge() {
+ // Merging into an empty `SyntaxTheme` keeps all the user-defined styles.
+ let syntax_theme = SyntaxTheme::merge(
+ Arc::new(SyntaxTheme::new_test([])),
+ vec![
+ (
+ "foo".to_string(),
+ HighlightStyle {
+ color: Some(gpui::red()),
+ ..Default::default()
+ },
+ ),
+ (
+ "foo.bar".to_string(),
+ HighlightStyle {
+ color: Some(gpui::green()),
+ ..Default::default()
+ },
+ ),
+ ],
+ );
+ assert_eq!(
+ syntax_theme,
+ Arc::new(SyntaxTheme::new_test([
+ ("foo", gpui::red()),
+ ("foo.bar", gpui::green())
+ ]))
+ );
+
+ // Merging empty user-defined styles keeps all the base styles.
+ let syntax_theme = SyntaxTheme::merge(
+ Arc::new(SyntaxTheme::new_test([
+ ("foo", gpui::blue()),
+ ("foo.bar", gpui::red()),
+ ])),
+ Vec::new(),
+ );
+ assert_eq!(
+ syntax_theme,
+ Arc::new(SyntaxTheme::new_test([
+ ("foo", gpui::blue()),
+ ("foo.bar", gpui::red())
+ ]))
+ );
+
+ let syntax_theme = SyntaxTheme::merge(
+ Arc::new(SyntaxTheme::new_test([
+ ("foo", gpui::red()),
+ ("foo.bar", gpui::green()),
+ ])),
+ vec![(
+ "foo.bar".to_string(),
+ HighlightStyle {
+ color: Some(gpui::yellow()),
+ ..Default::default()
+ },
+ )],
+ );
+ assert_eq!(
+ syntax_theme,
+ Arc::new(SyntaxTheme::new_test([
+ ("foo", gpui::red()),
+ ("foo.bar", gpui::yellow())
+ ]))
+ );
+
+ let syntax_theme = SyntaxTheme::merge(
+ Arc::new(SyntaxTheme::new_test([
+ ("foo", gpui::red()),
+ ("foo.bar", gpui::green()),
+ ])),
+ vec![(
+ "foo.bar".to_string(),
+ HighlightStyle {
+ font_style: Some(FontStyle::Italic),
+ ..Default::default()
+ },
+ )],
+ );
+ assert_eq!(
+ syntax_theme,
+ Arc::new(SyntaxTheme::new_test_styles([
+ (
+ "foo",
+ HighlightStyle {
+ color: Some(gpui::red()),
+ ..Default::default()
+ }
+ ),
+ (
+ "foo.bar",
+ HighlightStyle {
+ color: Some(gpui::green()),
+ font_style: Some(FontStyle::Italic),
+ ..Default::default()
+ }
+ )
+ ]))
+ );
+ }
+}
@@ -10,7 +10,7 @@ workspace = true
[features]
default = []
-test-support = ["gpui/test-support"]
+test-support = ["gpui/test-support", "syntax_theme/test-support"]
[lib]
path = "src/theme.rs"
@@ -21,6 +21,7 @@ anyhow.workspace = true
collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
+syntax_theme.workspace = true
palette = { workspace = true, default-features = false, features = ["std"] }
parking_lot.workspace = true
refineable.workspace = true
@@ -1,241 +1 @@
-#![allow(missing_docs)]
-
-use std::{
- collections::{BTreeMap, btree_map::Entry},
- sync::Arc,
-};
-
-use gpui::HighlightStyle;
-#[cfg(any(test, feature = "test-support"))]
-use gpui::Hsla;
-
-#[derive(Debug, PartialEq, Eq, Clone, Default)]
-pub struct SyntaxTheme {
- pub(self) highlights: Vec<HighlightStyle>,
- pub(self) capture_name_map: BTreeMap<String, usize>,
-}
-
-impl SyntaxTheme {
- pub fn new(highlights: impl IntoIterator<Item = (String, HighlightStyle)>) -> Self {
- let (capture_names, highlights) = highlights.into_iter().unzip();
-
- Self {
- capture_name_map: Self::create_capture_name_map(capture_names),
- highlights,
- }
- }
-
- fn create_capture_name_map(highlights: Vec<String>) -> BTreeMap<String, usize> {
- highlights
- .into_iter()
- .enumerate()
- .map(|(i, key)| (key, i))
- .collect()
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn new_test(colors: impl IntoIterator<Item = (&'static str, Hsla)>) -> Self {
- Self::new_test_styles(colors.into_iter().map(|(key, color)| {
- (
- key,
- HighlightStyle {
- color: Some(color),
- ..Default::default()
- },
- )
- }))
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn new_test_styles(
- colors: impl IntoIterator<Item = (&'static str, HighlightStyle)>,
- ) -> Self {
- Self::new(
- colors
- .into_iter()
- .map(|(key, style)| (key.to_owned(), style)),
- )
- }
-
- pub fn get(&self, highlight_index: impl Into<usize>) -> Option<&HighlightStyle> {
- self.highlights.get(highlight_index.into())
- }
-
- pub fn style_for_name(&self, name: &str) -> Option<HighlightStyle> {
- self.capture_name_map
- .get(name)
- .map(|highlight_idx| self.highlights[*highlight_idx])
- }
-
- pub fn get_capture_name(&self, idx: impl Into<usize>) -> Option<&str> {
- let idx = idx.into();
- self.capture_name_map
- .iter()
- .find(|(_, value)| **value == idx)
- .map(|(key, _)| key.as_ref())
- }
-
- pub fn highlight_id(&self, capture_name: &str) -> Option<u32> {
- self.capture_name_map
- .range::<str, _>((
- capture_name.split(".").next().map_or(
- std::ops::Bound::Included(capture_name),
- std::ops::Bound::Included,
- ),
- std::ops::Bound::Included(capture_name),
- ))
- .rfind(|(prefix, _)| {
- capture_name
- .strip_prefix(*prefix)
- .is_some_and(|remainder| remainder.is_empty() || remainder.starts_with('.'))
- })
- .map(|(_, index)| *index as u32)
- }
-
- /// Returns a new [`Arc<SyntaxTheme>`] with the given syntax styles merged in.
- pub fn merge(base: Arc<Self>, user_syntax_styles: Vec<(String, HighlightStyle)>) -> Arc<Self> {
- if user_syntax_styles.is_empty() {
- return base;
- }
-
- let mut base = Arc::try_unwrap(base).unwrap_or_else(|base| (*base).clone());
-
- for (name, highlight) in user_syntax_styles {
- match base.capture_name_map.entry(name) {
- Entry::Occupied(entry) => {
- if let Some(existing_highlight) = base.highlights.get_mut(*entry.get()) {
- existing_highlight.color = highlight.color.or(existing_highlight.color);
- existing_highlight.font_weight =
- highlight.font_weight.or(existing_highlight.font_weight);
- existing_highlight.font_style =
- highlight.font_style.or(existing_highlight.font_style);
- existing_highlight.background_color = highlight
- .background_color
- .or(existing_highlight.background_color);
- existing_highlight.underline =
- highlight.underline.or(existing_highlight.underline);
- existing_highlight.strikethrough =
- highlight.strikethrough.or(existing_highlight.strikethrough);
- existing_highlight.fade_out =
- highlight.fade_out.or(existing_highlight.fade_out);
- }
- }
- Entry::Vacant(vacant) => {
- vacant.insert(base.highlights.len());
- base.highlights.push(highlight);
- }
- }
- }
-
- Arc::new(base)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use gpui::FontStyle;
-
- use super::*;
-
- #[test]
- fn test_syntax_theme_merge() {
- // Merging into an empty `SyntaxTheme` keeps all the user-defined styles.
- let syntax_theme = SyntaxTheme::merge(
- Arc::new(SyntaxTheme::new_test([])),
- vec![
- (
- "foo".to_string(),
- HighlightStyle {
- color: Some(gpui::red()),
- ..Default::default()
- },
- ),
- (
- "foo.bar".to_string(),
- HighlightStyle {
- color: Some(gpui::green()),
- ..Default::default()
- },
- ),
- ],
- );
- assert_eq!(
- syntax_theme,
- Arc::new(SyntaxTheme::new_test([
- ("foo", gpui::red()),
- ("foo.bar", gpui::green())
- ]))
- );
-
- // Merging empty user-defined styles keeps all the base styles.
- let syntax_theme = SyntaxTheme::merge(
- Arc::new(SyntaxTheme::new_test([
- ("foo", gpui::blue()),
- ("foo.bar", gpui::red()),
- ])),
- Vec::new(),
- );
- assert_eq!(
- syntax_theme,
- Arc::new(SyntaxTheme::new_test([
- ("foo", gpui::blue()),
- ("foo.bar", gpui::red())
- ]))
- );
-
- let syntax_theme = SyntaxTheme::merge(
- Arc::new(SyntaxTheme::new_test([
- ("foo", gpui::red()),
- ("foo.bar", gpui::green()),
- ])),
- vec![(
- "foo.bar".to_string(),
- HighlightStyle {
- color: Some(gpui::yellow()),
- ..Default::default()
- },
- )],
- );
- assert_eq!(
- syntax_theme,
- Arc::new(SyntaxTheme::new_test([
- ("foo", gpui::red()),
- ("foo.bar", gpui::yellow())
- ]))
- );
-
- let syntax_theme = SyntaxTheme::merge(
- Arc::new(SyntaxTheme::new_test([
- ("foo", gpui::red()),
- ("foo.bar", gpui::green()),
- ])),
- vec![(
- "foo.bar".to_string(),
- HighlightStyle {
- font_style: Some(FontStyle::Italic),
- ..Default::default()
- },
- )],
- );
- assert_eq!(
- syntax_theme,
- Arc::new(SyntaxTheme::new_test_styles([
- (
- "foo",
- HighlightStyle {
- color: Some(gpui::red()),
- ..Default::default()
- }
- ),
- (
- "foo.bar",
- HighlightStyle {
- color: Some(gpui::green()),
- font_style: Some(FontStyle::Italic),
- ..Default::default()
- }
- )
- ]))
- );
- }
-}
+pub use syntax_theme::SyntaxTheme;