mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Extract syntax_theme crate (#52798)
Extract `SyntaxTheme` into its own lightweight crate so that downstream consumers can use syntax highlighting colors without pulling in the full `theme` crate and its transitive dependencies. ## Changes **Commit 1 — Extract SyntaxTheme into its own crate** Move `SyntaxTheme`, `SyntaxThemeSettings`, `HighlightStyle`, and supporting types from `theme/src/styles/syntax.rs` into a new `syntax_theme` crate that depends only on `gpui`. The `theme` crate re-exports everything for backward compatibility — no call-site changes needed. **Commit 2 — Add `bundled-themes` feature with One Dark** Add an optional `bundled-themes` feature that bundles `one_dark()`, a `SyntaxTheme` loaded from the existing One Dark JSON theme file. This lets consumers get a usable syntax theme without depending on the full theme machinery. Release Notes: - N/A
This commit is contained in:
parent
8ef64d99d8
commit
121e2bbe84
8 changed files with 379 additions and 242 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -17110,6 +17110,15 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syntax_theme"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"gpui",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sys-locale"
|
name = "sys-locale"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
|
@ -17569,6 +17578,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_json_lenient",
|
"serde_json_lenient",
|
||||||
"strum 0.27.2",
|
"strum 0.27.2",
|
||||||
|
"syntax_theme",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,7 @@ members = [
|
||||||
"crates/sum_tree",
|
"crates/sum_tree",
|
||||||
"crates/svg_preview",
|
"crates/svg_preview",
|
||||||
"crates/system_specs",
|
"crates/system_specs",
|
||||||
|
"crates/syntax_theme",
|
||||||
"crates/tab_switcher",
|
"crates/tab_switcher",
|
||||||
"crates/task",
|
"crates/task",
|
||||||
"crates/tasks_ui",
|
"crates/tasks_ui",
|
||||||
|
|
@ -435,6 +436,7 @@ streaming_diff = { path = "crates/streaming_diff" }
|
||||||
sum_tree = { path = "crates/sum_tree" }
|
sum_tree = { path = "crates/sum_tree" }
|
||||||
codestral = { path = "crates/codestral" }
|
codestral = { path = "crates/codestral" }
|
||||||
system_specs = { path = "crates/system_specs" }
|
system_specs = { path = "crates/system_specs" }
|
||||||
|
syntax_theme = { path = "crates/syntax_theme" }
|
||||||
tab_switcher = { path = "crates/tab_switcher" }
|
tab_switcher = { path = "crates/tab_switcher" }
|
||||||
task = { path = "crates/task" }
|
task = { path = "crates/task" }
|
||||||
tasks_ui = { path = "crates/tasks_ui" }
|
tasks_ui = { path = "crates/tasks_ui" }
|
||||||
|
|
|
||||||
26
crates/syntax_theme/Cargo.toml
Normal file
26
crates/syntax_theme/Cargo.toml
Normal file
|
|
@ -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"] }
|
||||||
1
crates/syntax_theme/LICENSE-APACHE
Symbolic link
1
crates/syntax_theme/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-APACHE
|
||||||
1
crates/syntax_theme/LICENSE-GPL
Symbolic link
1
crates/syntax_theme/LICENSE-GPL
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-GPL
|
||||||
336
crates/syntax_theme/src/syntax_theme.rs
Normal file
336
crates/syntax_theme/src/syntax_theme.rs
Normal file
|
|
@ -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]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
test-support = ["gpui/test-support"]
|
test-support = ["gpui/test-support", "syntax_theme/test-support"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/theme.rs"
|
path = "src/theme.rs"
|
||||||
|
|
@ -21,6 +21,7 @@ anyhow.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
syntax_theme.workspace = true
|
||||||
palette = { workspace = true, default-features = false, features = ["std"] }
|
palette = { workspace = true, default-features = false, features = ["std"] }
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
refineable.workspace = true
|
refineable.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,241 +1 @@
|
||||||
#![allow(missing_docs)]
|
pub use syntax_theme::SyntaxTheme;
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue