diff --git a/Cargo.lock b/Cargo.lock index 695d4e9b4fb4c92c7af52465f7586524cd6138d4..a24b7b81edf21c93cbd8cd71fa934dc8daf79f50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2172,7 +2172,6 @@ dependencies = [ "png 0.16.8", "postage", "rand 0.8.3", - "replace_with", "resvg", "seahash", "serde 1.0.125", @@ -3927,12 +3926,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "replace_with" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690" - [[package]] name = "resvg" version = "0.14.0" diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index d6e8640327d328421615cad7426fe7f11f365d7e..535fe16155ba9f2a1b16397cf2dcf27f050578cd 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -19,7 +19,6 @@ pathfinder_color = "0.5" pathfinder_geometry = "0.5" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" -replace_with = "0.1.7" resvg = "0.14" seahash = "4.1" serde = { version = "1.0.125", features = ["derive"] } diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index 11c327e2bb75488e1a9c33d8065af4aa920ffb37..45aa5931129ed43f9e6bf76a1063392827063ed1 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -1,5 +1,5 @@ use gpui::{ - color::ColorU, + color::Color, fonts::{Properties, Weight}, DebugContext, Element as _, Quad, }; @@ -28,7 +28,7 @@ impl gpui::View for TextView { "View" } - fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox { + fn render(&self, _: &gpui::RenderContext) -> gpui::ElementBox { TextElement.boxed() } } @@ -82,17 +82,17 @@ impl gpui::Element for TextElement { text, font_size, &[ - (1, normal, ColorU::default()), - (1, bold, ColorU::default()), - (1, normal, ColorU::default()), - (1, bold, ColorU::default()), - (text.len() - 4, normal, ColorU::default()), + (1, normal, Color::default()), + (1, bold, Color::default()), + (1, normal, Color::default()), + (1, bold, Color::default()), + (text.len() - 4, normal, Color::default()), ], ); cx.scene.push_quad(Quad { bounds: bounds, - background: Some(ColorU::white()), + background: Some(Color::white()), ..Default::default() }); line.paint(bounds.origin(), bounds, cx); diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 05e5ee15c47fa1757f720d7d15f12e713697b343..579bf5c4682dba2fcfbe15d48166396f3a9ea81d 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -36,9 +36,9 @@ pub trait Entity: 'static + Send + Sync { fn release(&mut self, _: &mut MutableAppContext) {} } -pub trait View: Entity { +pub trait View: Entity + Sized { fn ui_name() -> &'static str; - fn render<'a>(&self, cx: &AppContext) -> ElementBox; + fn render(&self, cx: &RenderContext<'_, Self>) -> ElementBox; fn on_focus(&mut self, _: &mut ViewContext) {} fn on_blur(&mut self, _: &mut ViewContext) {} fn keymap_context(&self, _: &AppContext) -> keymap::Context { @@ -813,6 +813,16 @@ impl MutableAppContext { .push_back(Effect::ViewNotification { window_id, view_id }); } + pub(crate) fn notify_all_views(&mut self) { + let notifications = self + .views + .keys() + .copied() + .map(|(window_id, view_id)| Effect::ViewNotification { window_id, view_id }) + .collect::>(); + self.pending_effects.extend(notifications); + } + pub fn dispatch_action( &mut self, window_id: usize, @@ -1503,7 +1513,7 @@ impl AppContext { pub fn render_view(&self, window_id: usize, view_id: usize) -> Result { self.views .get(&(window_id, view_id)) - .map(|v| v.render(self)) + .map(|v| v.render(window_id, view_id, self)) .ok_or(anyhow!("view not found")) } @@ -1512,7 +1522,7 @@ impl AppContext { .iter() .filter_map(|((win_id, view_id), view)| { if *win_id == window_id { - Some((*view_id, view.render(self))) + Some((*view_id, view.render(*win_id, *view_id, self))) } else { None } @@ -1650,7 +1660,7 @@ pub trait AnyView: Send + Sync { fn as_any_mut(&mut self) -> &mut dyn Any; fn release(&mut self, cx: &mut MutableAppContext); fn ui_name(&self) -> &'static str; - fn render<'a>(&self, cx: &AppContext) -> ElementBox; + fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox; fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn keymap_context(&self, cx: &AppContext) -> keymap::Context; @@ -1676,8 +1686,16 @@ where T::ui_name() } - fn render<'a>(&self, cx: &AppContext) -> ElementBox { - View::render(self, cx) + fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox { + View::render( + self, + &RenderContext { + window_id, + view_id, + app: cx, + view_type: PhantomData::, + }, + ) } fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize) { @@ -2079,6 +2097,10 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.notify_view(self.window_id, self.view_id); } + pub fn notify_all(&mut self) { + self.app.notify_all_views(); + } + pub fn propagate_action(&mut self) { self.halt_action_dispatch = false; } @@ -2094,12 +2116,33 @@ impl<'a, T: View> ViewContext<'a, T> { } } +pub struct RenderContext<'a, T: View> { + pub app: &'a AppContext, + window_id: usize, + view_id: usize, + view_type: PhantomData, +} + +impl<'a, T: View> RenderContext<'a, T> { + pub fn handle(&self) -> WeakViewHandle { + WeakViewHandle::new(self.window_id, self.view_id) + } +} + impl AsRef for &AppContext { fn as_ref(&self) -> &AppContext { self } } +impl Deref for RenderContext<'_, V> { + type Target = AppContext; + + fn deref(&self) -> &Self::Target { + &self.app + } +} + impl AsRef for ViewContext<'_, M> { fn as_ref(&self) -> &AppContext { &self.app.cx @@ -3004,7 +3047,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3067,7 +3110,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { let mouse_down_count = self.mouse_down_count.clone(); EventHandler::new(Empty::new().boxed()) .on_mouse_down(move |_| { @@ -3129,7 +3172,7 @@ mod tests { "View" } - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3169,7 +3212,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3222,7 +3265,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3272,7 +3315,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3315,7 +3358,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3362,7 +3405,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3420,7 +3463,7 @@ mod tests { } impl View for ViewA { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3438,7 +3481,7 @@ mod tests { } impl View for ViewB { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3541,7 +3584,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3674,7 +3717,7 @@ mod tests { "test view" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3719,7 +3762,7 @@ mod tests { "test view" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3742,7 +3785,7 @@ mod tests { "test view" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } diff --git a/gpui/src/assets.rs b/gpui/src/assets.rs index 63c6c07570a35de73415afe97c61e6229e2c6514..ac0d72dee97a8d5553963c4677bc3f6051fbaf34 100644 --- a/gpui/src/assets.rs +++ b/gpui/src/assets.rs @@ -1,8 +1,9 @@ use anyhow::{anyhow, Result}; use std::{borrow::Cow, cell::RefCell, collections::HashMap}; -pub trait AssetSource: 'static { +pub trait AssetSource: 'static + Send + Sync { fn load(&self, path: &str) -> Result>; + fn list(&self, path: &str) -> Vec>; } impl AssetSource for () { @@ -12,6 +13,10 @@ impl AssetSource for () { path )) } + + fn list(&self, _: &str) -> Vec> { + vec![] + } } pub struct AssetCache { diff --git a/gpui/src/color.rs b/gpui/src/color.rs index 95b966493e08c0005132d8942436f45ffd6dbe73..9c6de6247a62c5fb96b793c1f6332e495eb442a2 100644 --- a/gpui/src/color.rs +++ b/gpui/src/color.rs @@ -1,9 +1,89 @@ +use std::{ + borrow::Cow, + fmt, + ops::{Deref, DerefMut}, +}; + use crate::json::ToJson; -pub use pathfinder_color::*; +use pathfinder_color::ColorU; +use serde::{ + de::{self, Unexpected}, + Deserialize, Deserializer, +}; use serde_json::json; -impl ToJson for ColorU { +#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Color(ColorU); + +impl Color { + pub fn transparent_black() -> Self { + Self(ColorU::transparent_black()) + } + + pub fn black() -> Self { + Self(ColorU::black()) + } + + pub fn white() -> Self { + Self(ColorU::white()) + } + + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self(ColorU::new(r, g, b, a)) + } + + pub fn from_u32(rgba: u32) -> Self { + Self(ColorU::from_u32(rgba)) + } +} + +impl<'de> Deserialize<'de> for Color { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let literal: Cow = Deserialize::deserialize(deserializer)?; + if let Some(digits) = literal.strip_prefix('#') { + if let Ok(value) = u32::from_str_radix(digits, 16) { + if digits.len() == 6 { + return Ok(Color::from_u32((value << 8) | 0xFF)); + } else if digits.len() == 8 { + return Ok(Color::from_u32(value)); + } + } + } + Err(de::Error::invalid_value( + Unexpected::Str(literal.as_ref()), + &"#RRGGBB[AA]", + )) + } +} + +impl ToJson for Color { fn to_json(&self) -> serde_json::Value { - json!(format!("0x{:x}{:x}{:x}", self.r, self.g, self.b)) + json!(format!( + "0x{:x}{:x}{:x}{:x}", + self.0.r, self.0.g, self.0.b, self.0.a + )) + } +} + +impl Deref for Color { + type Target = ColorU; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Color { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl fmt::Debug for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) } } diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 869a26b542d99d551537b579e5bad6222045fa6d..3d0357409484aafb8916bcc70b3c3bf561f2fa17 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -34,8 +34,7 @@ use crate::{ }; use core::panic; use json::ToJson; -use replace_with::replace_with_or_abort; -use std::{any::Any, borrow::Cow}; +use std::{any::Any, borrow::Cow, mem}; trait AnyElement { fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F; @@ -115,6 +114,7 @@ pub trait Element { } pub enum Lifecycle { + Empty, Init { element: T, }, @@ -139,8 +139,9 @@ pub struct ElementBox { impl AnyElement for Lifecycle { fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F { - let mut result = None; - replace_with_or_abort(self, |me| match me { + let result; + *self = match mem::take(self) { + Lifecycle::Empty => unreachable!(), Lifecycle::Init { mut element } | Lifecycle::PostLayout { mut element, .. } | Lifecycle::PostPaint { mut element, .. } => { @@ -148,7 +149,7 @@ impl AnyElement for Lifecycle { debug_assert!(size.x().is_finite()); debug_assert!(size.y().is_finite()); - result = Some(size); + result = size; Lifecycle::PostLayout { element, constraint, @@ -156,8 +157,8 @@ impl AnyElement for Lifecycle { layout, } } - }); - result.unwrap() + }; + result } fn after_layout(&mut self, cx: &mut AfterLayoutContext) { @@ -175,27 +176,25 @@ impl AnyElement for Lifecycle { } fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) { - replace_with_or_abort(self, |me| { - if let Lifecycle::PostLayout { - mut element, + *self = if let Lifecycle::PostLayout { + mut element, + constraint, + size, + mut layout, + } = mem::take(self) + { + let bounds = RectF::new(origin, size); + let paint = element.paint(bounds, &mut layout, cx); + Lifecycle::PostPaint { + element, constraint, - size, - mut layout, - } = me - { - let bounds = RectF::new(origin, size); - let paint = element.paint(bounds, &mut layout, cx); - Lifecycle::PostPaint { - element, - constraint, - bounds, - layout, - paint, - } - } else { - panic!("invalid element lifecycle state"); + bounds, + layout, + paint, } - }); + } else { + panic!("invalid element lifecycle state"); + }; } fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool { @@ -215,7 +214,7 @@ impl AnyElement for Lifecycle { fn size(&self) -> Vector2F { match self { - Lifecycle::Init { .. } => panic!("invalid element lifecycle state"), + Lifecycle::Empty | Lifecycle::Init { .. } => panic!("invalid element lifecycle state"), Lifecycle::PostLayout { size, .. } => *size, Lifecycle::PostPaint { bounds, .. } => bounds.size(), } @@ -223,6 +222,7 @@ impl AnyElement for Lifecycle { fn metadata(&self) -> Option<&dyn Any> { match self { + Lifecycle::Empty => unreachable!(), Lifecycle::Init { element } | Lifecycle::PostLayout { element, .. } | Lifecycle::PostPaint { element, .. } => element.metadata(), @@ -257,6 +257,12 @@ impl AnyElement for Lifecycle { } } +impl Default for Lifecycle { + fn default() -> Self { + Self::Empty + } +} + impl ElementBox { pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F { self.element.layout(constraint, cx) diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index c0b829fbe6f881b0cf24e8c80d2bf92a16cabcb2..ae13b5d82116539ac1cefc2ef5c8b66bbf1ff644 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -1,62 +1,77 @@ use pathfinder_geometry::rect::RectF; +use serde::Deserialize; use serde_json::json; use crate::{ - color::ColorU, - geometry::vector::{vec2f, Vector2F}, + color::Color, + geometry::{ + deserialize_vec2f, + vector::{vec2f, Vector2F}, + }, json::ToJson, scene::{self, Border, Quad}, AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; +#[derive(Clone, Debug, Default, Deserialize)] +pub struct ContainerStyle { + #[serde(default)] + pub margin: Margin, + #[serde(default)] + pub padding: Padding, + #[serde(rename = "background")] + pub background_color: Option, + #[serde(default)] + pub border: Border, + #[serde(default)] + pub corner_radius: f32, + #[serde(default)] + pub shadow: Option, +} + pub struct Container { - margin: Margin, - padding: Padding, - background_color: Option, - border: Border, - corner_radius: f32, - shadow: Option, child: ElementBox, + style: ContainerStyle, } impl Container { pub fn new(child: ElementBox) -> Self { Self { - margin: Margin::default(), - padding: Padding::default(), - background_color: None, - border: Border::default(), - corner_radius: 0.0, - shadow: None, child, + style: Default::default(), } } + pub fn with_style(mut self, style: &ContainerStyle) -> Self { + self.style = style.clone(); + self + } + pub fn with_margin_top(mut self, margin: f32) -> Self { - self.margin.top = margin; + self.style.margin.top = margin; self } pub fn with_margin_left(mut self, margin: f32) -> Self { - self.margin.left = margin; + self.style.margin.left = margin; self } pub fn with_horizontal_padding(mut self, padding: f32) -> Self { - self.padding.left = padding; - self.padding.right = padding; + self.style.padding.left = padding; + self.style.padding.right = padding; self } pub fn with_vertical_padding(mut self, padding: f32) -> Self { - self.padding.top = padding; - self.padding.bottom = padding; + self.style.padding.top = padding; + self.style.padding.bottom = padding; self } pub fn with_uniform_padding(mut self, padding: f32) -> Self { - self.padding = Padding { + self.style.padding = Padding { top: padding, left: padding, bottom: padding, @@ -66,68 +81,68 @@ impl Container { } pub fn with_padding_right(mut self, padding: f32) -> Self { - self.padding.right = padding; + self.style.padding.right = padding; self } pub fn with_padding_bottom(mut self, padding: f32) -> Self { - self.padding.bottom = padding; + self.style.padding.bottom = padding; self } - pub fn with_background_color(mut self, color: impl Into) -> Self { - self.background_color = Some(color.into()); + pub fn with_background_color(mut self, color: Color) -> Self { + self.style.background_color = Some(color); self } pub fn with_border(mut self, border: Border) -> Self { - self.border = border; + self.style.border = border; self } pub fn with_corner_radius(mut self, radius: f32) -> Self { - self.corner_radius = radius; + self.style.corner_radius = radius; self } - pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: impl Into) -> Self { - self.shadow = Some(Shadow { + pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: Color) -> Self { + self.style.shadow = Some(Shadow { offset, blur, - color: color.into(), + color, }); self } fn margin_size(&self) -> Vector2F { vec2f( - self.margin.left + self.margin.right, - self.margin.top + self.margin.bottom, + self.style.margin.left + self.style.margin.right, + self.style.margin.top + self.style.margin.bottom, ) } fn padding_size(&self) -> Vector2F { vec2f( - self.padding.left + self.padding.right, - self.padding.top + self.padding.bottom, + self.style.padding.left + self.style.padding.right, + self.style.padding.top + self.style.padding.bottom, ) } fn border_size(&self) -> Vector2F { let mut x = 0.0; - if self.border.left { - x += self.border.width; + if self.style.border.left { + x += self.style.border.width; } - if self.border.right { - x += self.border.width; + if self.style.border.right { + x += self.style.border.width; } let mut y = 0.0; - if self.border.top { - y += self.border.width; + if self.style.border.top { + y += self.style.border.width; } - if self.border.bottom { - y += self.border.width; + if self.style.border.bottom { + y += self.style.border.width; } vec2f(x, y) @@ -168,28 +183,31 @@ impl Element for Container { cx: &mut PaintContext, ) -> Self::PaintState { let quad_bounds = RectF::from_points( - bounds.origin() + vec2f(self.margin.left, self.margin.top), - bounds.lower_right() - vec2f(self.margin.right, self.margin.bottom), + bounds.origin() + vec2f(self.style.margin.left, self.style.margin.top), + bounds.lower_right() - vec2f(self.style.margin.right, self.style.margin.bottom), ); - if let Some(shadow) = self.shadow.as_ref() { + if let Some(shadow) = self.style.shadow.as_ref() { cx.scene.push_shadow(scene::Shadow { bounds: quad_bounds + shadow.offset, - corner_radius: self.corner_radius, + corner_radius: self.style.corner_radius, sigma: shadow.blur, color: shadow.color, }); } cx.scene.push_quad(Quad { bounds: quad_bounds, - background: self.background_color, - border: self.border, - corner_radius: self.corner_radius, + background: self.style.background_color, + border: self.style.border, + corner_radius: self.style.corner_radius, }); let child_origin = quad_bounds.origin() - + vec2f(self.padding.left, self.padding.top) - + vec2f(self.border.left_width(), self.border.top_width()); + + vec2f(self.style.padding.left, self.style.padding.top) + + vec2f( + self.style.border.left_width(), + self.style.border.top_width(), + ); self.child.paint(child_origin, cx); } @@ -214,24 +232,34 @@ impl Element for Container { json!({ "type": "Container", "bounds": bounds.to_json(), - "details": { - "margin": self.margin.to_json(), - "padding": self.padding.to_json(), - "background_color": self.background_color.to_json(), - "border": self.border.to_json(), - "corner_radius": self.corner_radius, - "shadow": self.shadow.to_json(), - }, + "details": self.style.to_json(), "child": self.child.debug(cx), }) } } -#[derive(Default)] +impl ToJson for ContainerStyle { + fn to_json(&self) -> serde_json::Value { + json!({ + "margin": self.margin.to_json(), + "padding": self.padding.to_json(), + "background_color": self.background_color.to_json(), + "border": self.border.to_json(), + "corner_radius": self.corner_radius, + "shadow": self.shadow.to_json(), + }) + } +} + +#[derive(Clone, Debug, Default, Deserialize)] pub struct Margin { + #[serde(default)] top: f32, + #[serde(default)] left: f32, + #[serde(default)] bottom: f32, + #[serde(default)] right: f32, } @@ -254,11 +282,15 @@ impl ToJson for Margin { } } -#[derive(Default)] +#[derive(Clone, Debug, Default, Deserialize)] pub struct Padding { + #[serde(default)] top: f32, + #[serde(default)] left: f32, + #[serde(default)] bottom: f32, + #[serde(default)] right: f32, } @@ -281,11 +313,14 @@ impl ToJson for Padding { } } -#[derive(Default)] +#[derive(Clone, Debug, Default, Deserialize)] pub struct Shadow { + #[serde(default, deserialize_with = "deserialize_vec2f")] offset: Vector2F, + #[serde(default)] blur: f32, - color: ColorU, + #[serde(default)] + color: Color, } impl ToJson for Shadow { diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index 6b6c3358f2742bd818841e34a81fd73b2eda6521..72f755905cad91b179b9b9421c5feeec0a3b7c5f 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -1,10 +1,7 @@ -use serde_json::json; -use smallvec::{smallvec, SmallVec}; - use crate::{ - color::ColorU, + color::Color, font_cache::FamilyId, - fonts::{FontId, Properties}, + fonts::{FontId, TextStyle}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -14,20 +11,22 @@ use crate::{ AfterLayoutContext, DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext, SizeConstraint, }; +use serde::Deserialize; +use serde_json::json; +use smallvec::{smallvec, SmallVec}; pub struct Label { text: String, family_id: FamilyId, - font_properties: Properties, font_size: f32, - default_color: ColorU, - highlights: Option, + style: LabelStyle, + highlight_indices: Vec, } -pub struct Highlights { - color: ColorU, - indices: Vec, - font_properties: Properties, +#[derive(Clone, Debug, Default, Deserialize)] +pub struct LabelStyle { + pub text: TextStyle, + pub highlight_text: Option, } impl Label { @@ -35,29 +34,24 @@ impl Label { Self { text, family_id, - font_properties: Properties::new(), font_size, - default_color: ColorU::black(), - highlights: None, + highlight_indices: Default::default(), + style: Default::default(), } } - pub fn with_default_color(mut self, color: ColorU) -> Self { - self.default_color = color; + pub fn with_style(mut self, style: &LabelStyle) -> Self { + self.style = style.clone(); self } - pub fn with_highlights( - mut self, - color: ColorU, - font_properties: Properties, - indices: Vec, - ) -> Self { - self.highlights = Some(Highlights { - color, - font_properties, - indices, - }); + pub fn with_default_color(mut self, color: Color) -> Self { + self.style.text.color = color; + self + } + + pub fn with_highlights(mut self, indices: Vec) -> Self { + self.highlight_indices = indices; self } @@ -65,47 +59,58 @@ impl Label { &self, font_cache: &FontCache, font_id: FontId, - ) -> SmallVec<[(usize, FontId, ColorU); 8]> { - if let Some(highlights) = self.highlights.as_ref() { - let highlight_font_id = font_cache - .select_font(self.family_id, &highlights.font_properties) - .unwrap_or(font_id); - - let mut highlight_indices = highlights.indices.iter().copied().peekable(); - let mut runs = SmallVec::new(); - - for (char_ix, c) in self.text.char_indices() { - let mut font_id = font_id; - let mut color = self.default_color; - if let Some(highlight_ix) = highlight_indices.peek() { - if char_ix == *highlight_ix { - font_id = highlight_font_id; - color = highlights.color; - highlight_indices.next(); - } - } + ) -> SmallVec<[(usize, FontId, Color); 8]> { + if self.highlight_indices.is_empty() { + return smallvec![(self.text.len(), font_id, self.style.text.color)]; + } - let push_new_run = - if let Some((last_len, last_font_id, last_color)) = runs.last_mut() { - if font_id == *last_font_id && color == *last_color { - *last_len += c.len_utf8(); - false - } else { - true - } - } else { - true - }; - - if push_new_run { - runs.push((c.len_utf8(), font_id, color)); + let highlight_font_id = self + .style + .highlight_text + .as_ref() + .and_then(|style| { + font_cache + .select_font(self.family_id, &style.font_properties) + .ok() + }) + .unwrap_or(font_id); + + let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); + let mut runs = SmallVec::new(); + + for (char_ix, c) in self.text.char_indices() { + let mut font_id = font_id; + let mut color = self.style.text.color; + if let Some(highlight_ix) = highlight_indices.peek() { + if char_ix == *highlight_ix { + font_id = highlight_font_id; + color = self + .style + .highlight_text + .as_ref() + .unwrap_or(&self.style.text) + .color; + highlight_indices.next(); } } - runs - } else { - smallvec![(self.text.len(), font_id, self.default_color)] + let push_new_run = if let Some((last_len, last_font_id, last_color)) = runs.last_mut() { + if font_id == *last_font_id && color == *last_color { + *last_len += c.len_utf8(); + false + } else { + true + } + } else { + true + }; + + if push_new_run { + runs.push((c.len_utf8(), font_id, color)); + } } + + runs } } @@ -120,7 +125,7 @@ impl Element for Label { ) -> (Vector2F, Self::LayoutState) { let font_id = cx .font_cache - .select_font(self.family_id, &self.font_properties) + .select_font(self.family_id, &self.style.text.font_properties) .unwrap(); let runs = self.compute_runs(&cx.font_cache, font_id); let line = @@ -172,56 +177,63 @@ impl Element for Label { json!({ "type": "Label", "bounds": bounds.to_json(), + "text": &self.text, + "highlight_indices": self.highlight_indices, "font_family": cx.font_cache.family_name(self.family_id).unwrap(), "font_size": self.font_size, - "font_properties": self.font_properties.to_json(), - "text": &self.text, - "highlights": self.highlights.to_json(), + "style": self.style.to_json(), }) } } -impl ToJson for Highlights { +impl ToJson for LabelStyle { fn to_json(&self) -> Value { json!({ - "color": self.color.to_json(), - "indices": self.indices, - "font_properties": self.font_properties.to_json(), + "text": self.text.to_json(), + "highlight_text": self.highlight_text + .as_ref() + .map_or(serde_json::Value::Null, |style| style.to_json()) }) } } #[cfg(test)] mod tests { - use font_kit::properties::Weight; - use super::*; + use crate::fonts::{Properties as FontProperties, Weight}; #[crate::test(self)] fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) { let menlo = cx.font_cache().load_family(&["Menlo"]).unwrap(); let menlo_regular = cx .font_cache() - .select_font(menlo, &Properties::new()) + .select_font(menlo, &FontProperties::new()) .unwrap(); let menlo_bold = cx .font_cache() - .select_font(menlo, Properties::new().weight(Weight::BOLD)) + .select_font(menlo, FontProperties::new().weight(Weight::BOLD)) .unwrap(); - let black = ColorU::black(); - let red = ColorU::new(255, 0, 0, 255); - - let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0).with_highlights( - red, - *Properties::new().weight(Weight::BOLD), - vec![ + let black = Color::black(); + let red = Color::new(255, 0, 0, 255); + + let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0) + .with_style(&LabelStyle { + text: TextStyle { + color: black, + font_properties: Default::default(), + }, + highlight_text: Some(TextStyle { + color: red, + font_properties: *FontProperties::new().weight(Weight::BOLD), + }), + }) + .with_highlights(vec![ ".α".len(), ".αβ".len(), ".αβγδ".len(), ".αβγδε.ⓐ".len(), ".αβγδε.ⓐⓑ".len(), - ], - ); + ]); let runs = label.compute_runs(cx.font_cache().as_ref(), menlo_regular); assert_eq!( diff --git a/gpui/src/elements/svg.rs b/gpui/src/elements/svg.rs index 855d30b1a3b619f902b06a354100dcebaac5276e..93d26f9656a45ba2328972c99226cc6a3c58588b 100644 --- a/gpui/src/elements/svg.rs +++ b/gpui/src/elements/svg.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use serde_json::json; use crate::{ - color::ColorU, + color::Color, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -14,18 +14,18 @@ use crate::{ pub struct Svg { path: Cow<'static, str>, - color: ColorU, + color: Color, } impl Svg { pub fn new(path: impl Into>) -> Self { Self { path: path.into(), - color: ColorU::black(), + color: Color::black(), } } - pub fn with_color(mut self, color: ColorU) -> Self { + pub fn with_color(mut self, color: Color) -> Self { self.color = color; self } diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index b414a20430159ff373422240add657792687f2e9..74ebccdf379080beb603f1464d19a08e132154a0 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -13,17 +13,10 @@ use json::ToJson; use parking_lot::Mutex; use std::{cmp, ops::Range, sync::Arc}; -#[derive(Clone)] +#[derive(Clone, Default)] pub struct UniformListState(Arc>); impl UniformListState { - pub fn new() -> Self { - Self(Arc::new(Mutex::new(StateInner { - scroll_top: 0.0, - scroll_to: None, - }))) - } - pub fn scroll_to(&self, item_ix: usize) { self.0.lock().scroll_to = Some(item_ix); } @@ -33,6 +26,7 @@ impl UniformListState { } } +#[derive(Default)] struct StateInner { scroll_top: f32, scroll_to: Option, @@ -57,11 +51,11 @@ impl UniformList where F: Fn(Range, &mut Vec, &AppContext), { - pub fn new(state: UniformListState, item_count: usize, build_items: F) -> Self { + pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self { Self { state, item_count, - append_items: build_items, + append_items, } } @@ -79,7 +73,7 @@ where let mut state = self.state.0.lock(); state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max); - cx.dispatch_action("uniform_list:scroll", state.scroll_top); + cx.notify(); true } diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index e7e6b5dedb308de91635846a44d5fce340236235..e9f84676e791f19fdd7badf3a7e52de277f22a99 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -1,14 +1,109 @@ -use crate::json::json; -pub use font_kit::metrics::Metrics; -pub use font_kit::properties::{Properties, Stretch, Style, Weight}; - -use crate::json::ToJson; +use crate::{ + color::Color, + json::{json, ToJson}, +}; +pub use font_kit::{ + metrics::Metrics, + properties::{Properties, Stretch, Style, Weight}, +}; +use serde::{de, Deserialize}; +use serde_json::Value; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct FontId(pub usize); pub type GlyphId = u32; +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TextStyle { + pub color: Color, + pub font_properties: Properties, +} + +#[allow(non_camel_case_types)] +#[derive(Deserialize)] +enum WeightJson { + thin, + extra_light, + light, + normal, + medium, + semibold, + bold, + extra_bold, + black, +} + +#[derive(Deserialize)] +struct TextStyleJson { + color: Color, + weight: Option, + #[serde(default)] + italic: bool, +} + +impl<'de> Deserialize<'de> for TextStyle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let json = Value::deserialize(deserializer)?; + if json.is_object() { + let style_json: TextStyleJson = + serde_json::from_value(json).map_err(de::Error::custom)?; + Ok(style_json.into()) + } else { + Ok(Self { + color: serde_json::from_value(json).map_err(de::Error::custom)?, + font_properties: Properties::new(), + }) + } + } +} + +impl From for TextStyle { + fn from(color: Color) -> Self { + Self { + color, + font_properties: Default::default(), + } + } +} + +impl ToJson for TextStyle { + fn to_json(&self) -> Value { + json!({ + "color": self.color.to_json(), + "font_properties": self.font_properties.to_json(), + }) + } +} + +impl Into for TextStyleJson { + fn into(self) -> TextStyle { + let weight = match self.weight.unwrap_or(WeightJson::normal) { + WeightJson::thin => Weight::THIN, + WeightJson::extra_light => Weight::EXTRA_LIGHT, + WeightJson::light => Weight::LIGHT, + WeightJson::normal => Weight::NORMAL, + WeightJson::medium => Weight::MEDIUM, + WeightJson::semibold => Weight::SEMIBOLD, + WeightJson::bold => Weight::BOLD, + WeightJson::extra_bold => Weight::EXTRA_BOLD, + WeightJson::black => Weight::BLACK, + }; + let style = if self.italic { + Style::Italic + } else { + Style::Normal + }; + TextStyle { + color: self.color, + font_properties: *Properties::new().weight(weight).style(style), + } + } +} + impl ToJson for Properties { fn to_json(&self) -> crate::json::Value { json!({ diff --git a/gpui/src/geometry.rs b/gpui/src/geometry.rs index cf8ec9cb40e681cadcfd38783f25742f0eccc401..705184fa6bae2f2b1ffd5f9ede4e713cbb9a828e 100644 --- a/gpui/src/geometry.rs +++ b/gpui/src/geometry.rs @@ -1,7 +1,8 @@ use super::scene::{Path, PathVertex}; -use crate::{color::ColorU, json::ToJson}; +use crate::{color::Color, json::ToJson}; pub use pathfinder_geometry::*; use rect::RectF; +use serde::{Deserialize, Deserializer}; use serde_json::json; use vector::{vec2f, Vector2F}; @@ -55,7 +56,7 @@ impl PathBuilder { self.current = point; } - pub fn build(mut self, color: ColorU, clip_bounds: Option) -> Path { + pub fn build(mut self, color: Color, clip_bounds: Option) -> Path { if let Some(clip_bounds) = clip_bounds { self.bounds = self .bounds @@ -108,6 +109,14 @@ impl PathBuilder { } } +pub fn deserialize_vec2f<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let [x, y]: [f32; 2] = Deserialize::deserialize(deserializer)?; + Ok(vec2f(x, y)) +} + impl ToJson for Vector2F { fn to_json(&self) -> serde_json::Value { json!([self.x(), self.y()]) diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 7159e0c14969f72cf58f8c6877a7c348158aeccd..7107d7763dd373383d874cb90421c4eedd2d0ca2 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -8,7 +8,7 @@ pub mod current { } use crate::{ - color::ColorU, + color::Color, executor, fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties}, geometry::{ @@ -134,7 +134,7 @@ pub trait FontSystem: Send + Sync { &self, text: &str, font_size: f32, - runs: &[(usize, FontId, ColorU)], + runs: &[(usize, FontId, Color)], ) -> LineLayout; fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec; } diff --git a/gpui/src/platform/mac/fonts.rs b/gpui/src/platform/mac/fonts.rs index 80e962f9b260bc9bede6f07349e0a1652d55331a..ba9e3ae3cf764d15fcb2bb8178ccd256819335ce 100644 --- a/gpui/src/platform/mac/fonts.rs +++ b/gpui/src/platform/mac/fonts.rs @@ -1,5 +1,5 @@ use crate::{ - color::ColorU, + color::Color, fonts::{FontId, GlyphId, Metrics, Properties}, geometry::{ rect::{RectF, RectI}, @@ -82,7 +82,7 @@ impl platform::FontSystem for FontSystem { &self, text: &str, font_size: f32, - runs: &[(usize, FontId, ColorU)], + runs: &[(usize, FontId, Color)], ) -> LineLayout { self.0.read().layout_line(text, font_size, runs) } @@ -191,7 +191,7 @@ impl FontSystemState { &self, text: &str, font_size: f32, - runs: &[(usize, FontId, ColorU)], + runs: &[(usize, FontId, Color)], ) -> LineLayout { let font_id_attr_name = CFString::from_static_string("zed_font_id"); @@ -445,9 +445,9 @@ mod tests { text, 16.0, &[ - (9, zapfino_regular, ColorU::default()), - (13, menlo_regular, ColorU::default()), - (text.len() - 22, zapfino_regular, ColorU::default()), + (9, zapfino_regular, Color::default()), + (13, menlo_regular, Color::default()), + (text.len() - 22, zapfino_regular, Color::default()), ], ); assert_eq!( diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index 82521cdf86464ddb548c0da1a9a64f611d6801f9..82e80790cb83d0dfe542cb2aaf4a50ef4d41aff6 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -1,6 +1,6 @@ use super::{atlas::AtlasAllocator, sprite_cache::SpriteCache}; use crate::{ - color::ColorU, + color::Color, geometry::{ rect::RectF, vector::{vec2f, vec2i, Vector2F}, @@ -11,7 +11,7 @@ use crate::{ }; use cocoa::foundation::NSUInteger; use metal::{MTLPixelFormat, MTLResourceOptions, NSRange}; -use shaders::{ToFloat2 as _, ToUchar4 as _}; +use shaders::ToFloat2 as _; use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec}; const SHADERS_METALLIB: &'static [u8] = @@ -438,17 +438,13 @@ impl Renderer { size: bounds.size().round().to_float2(), background_color: quad .background - .unwrap_or(ColorU::transparent_black()) + .unwrap_or(Color::transparent_black()) .to_uchar4(), border_top: border_width * (quad.border.top as usize as f32), border_right: border_width * (quad.border.right as usize as f32), border_bottom: border_width * (quad.border.bottom as usize as f32), border_left: border_width * (quad.border.left as usize as f32), - border_color: quad - .border - .color - .unwrap_or(ColorU::transparent_black()) - .to_uchar4(), + border_color: quad.border.color.to_uchar4(), corner_radius: quad.corner_radius * scene.scale_factor(), }; unsafe { @@ -782,7 +778,7 @@ mod shaders { use pathfinder_geometry::vector::Vector2I; - use crate::{color::ColorU, geometry::vector::Vector2F}; + use crate::{color::Color, geometry::vector::Vector2F}; use std::mem; include!(concat!(env!("OUT_DIR"), "/shaders.rs")); @@ -791,10 +787,6 @@ mod shaders { fn to_float2(&self) -> vector_float2; } - pub trait ToUchar4 { - fn to_uchar4(&self) -> vector_uchar4; - } - impl ToFloat2 for (f32, f32) { fn to_float2(&self) -> vector_float2 { unsafe { @@ -823,8 +815,8 @@ mod shaders { } } - impl ToUchar4 for ColorU { - fn to_uchar4(&self) -> vector_uchar4 { + impl Color { + pub fn to_uchar4(&self) -> vector_uchar4 { let mut vec = self.a as vector_uchar4; vec <<= 8; vec |= self.b as vector_uchar4; diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 10460ed2e0a526af5c4de1a7b6edac3bf823531f..3818a0870120ba7a0183db69ad3d9dfc1ecb8b60 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -1,9 +1,9 @@ -use std::borrow::Cow; - +use serde::Deserialize; use serde_json::json; +use std::borrow::Cow; use crate::{ - color::ColorU, + color::Color, fonts::{FontId, GlyphId}, geometry::{rect::RectF, vector::Vector2F}, json::ToJson, @@ -28,7 +28,7 @@ pub struct Layer { #[derive(Default, Debug)] pub struct Quad { pub bounds: RectF, - pub background: Option, + pub background: Option, pub border: Border, pub corner_radius: f32, } @@ -38,7 +38,7 @@ pub struct Shadow { pub bounds: RectF, pub corner_radius: f32, pub sigma: f32, - pub color: ColorU, + pub color: Color, } #[derive(Debug)] @@ -47,30 +47,68 @@ pub struct Glyph { pub font_size: f32, pub id: GlyphId, pub origin: Vector2F, - pub color: ColorU, + pub color: Color, } pub struct Icon { pub bounds: RectF, pub svg: usvg::Tree, pub path: Cow<'static, str>, - pub color: ColorU, + pub color: Color, } #[derive(Clone, Copy, Default, Debug)] pub struct Border { pub width: f32, - pub color: Option, + pub color: Color, pub top: bool, pub right: bool, pub bottom: bool, pub left: bool, } +impl<'de> Deserialize<'de> for Border { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct BorderData { + pub width: f32, + pub color: Color, + #[serde(default)] + pub top: bool, + #[serde(default)] + pub right: bool, + #[serde(default)] + pub bottom: bool, + #[serde(default)] + pub left: bool, + } + + let data = BorderData::deserialize(deserializer)?; + let mut border = Border { + width: data.width, + color: data.color, + top: data.top, + bottom: data.bottom, + left: data.left, + right: data.right, + }; + if !border.top && !border.bottom && !border.left && !border.right { + border.top = true; + border.bottom = true; + border.left = true; + border.right = true; + } + Ok(border) + } +} + #[derive(Debug)] pub struct Path { pub bounds: RectF, - pub color: ColorU, + pub color: Color, pub vertices: Vec, } @@ -193,10 +231,10 @@ impl Layer { } impl Border { - pub fn new(width: f32, color: impl Into) -> Self { + pub fn new(width: f32, color: Color) -> Self { Self { width, - color: Some(color.into()), + color, top: false, left: false, bottom: false, @@ -204,10 +242,10 @@ impl Border { } } - pub fn all(width: f32, color: impl Into) -> Self { + pub fn all(width: f32, color: Color) -> Self { Self { width, - color: Some(color.into()), + color, top: true, left: true, bottom: true, @@ -215,25 +253,25 @@ impl Border { } } - pub fn top(width: f32, color: impl Into) -> Self { + pub fn top(width: f32, color: Color) -> Self { let mut border = Self::new(width, color); border.top = true; border } - pub fn left(width: f32, color: impl Into) -> Self { + pub fn left(width: f32, color: Color) -> Self { let mut border = Self::new(width, color); border.left = true; border } - pub fn bottom(width: f32, color: impl Into) -> Self { + pub fn bottom(width: f32, color: Color) -> Self { let mut border = Self::new(width, color); border.bottom = true; border } - pub fn right(width: f32, color: impl Into) -> Self { + pub fn right(width: f32, color: Color) -> Self { let mut border = Self::new(width, color); border.right = true; border diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index ee4401788fcc8691f0ba7d416e4cb24c6a8d6f6b..57556d61b6b25b25ee28bbda20893dd2c0c196b0 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -1,5 +1,5 @@ use crate::{ - color::ColorU, + color::Color, fonts::{FontId, GlyphId}, geometry::{ rect::RectF, @@ -43,7 +43,7 @@ impl TextLayoutCache { &'a self, text: &'a str, font_size: f32, - runs: &'a [(usize, FontId, ColorU)], + runs: &'a [(usize, FontId, Color)], ) -> Line { let key = &CacheKeyRef { text, @@ -94,7 +94,7 @@ impl<'a> Hash for (dyn CacheKey + 'a) { struct CacheKeyValue { text: String, font_size: OrderedFloat, - runs: SmallVec<[(usize, FontId, ColorU); 1]>, + runs: SmallVec<[(usize, FontId, Color); 1]>, } impl CacheKey for CacheKeyValue { @@ -123,7 +123,7 @@ impl<'a> Borrow for CacheKeyValue { struct CacheKeyRef<'a> { text: &'a str, font_size: OrderedFloat, - runs: &'a [(usize, FontId, ColorU)], + runs: &'a [(usize, FontId, Color)], } impl<'a> CacheKey for CacheKeyRef<'a> { @@ -135,7 +135,7 @@ impl<'a> CacheKey for CacheKeyRef<'a> { #[derive(Default, Debug)] pub struct Line { layout: Arc, - color_runs: SmallVec<[(u32, ColorU); 32]>, + color_runs: SmallVec<[(u32, Color); 32]>, } #[derive(Default, Debug)] @@ -162,7 +162,7 @@ pub struct Glyph { } impl Line { - fn new(layout: Arc, runs: &[(usize, FontId, ColorU)]) -> Self { + fn new(layout: Arc, runs: &[(usize, FontId, Color)]) -> Self { let mut color_runs = SmallVec::new(); for (len, _, color) in runs { color_runs.push((*len as u32, *color)); @@ -206,7 +206,7 @@ impl Line { let mut color_runs = self.color_runs.iter(); let mut color_end = 0; - let mut color = ColorU::black(); + let mut color = Color::black(); for run in &self.layout.runs { let max_glyph_width = cx @@ -230,7 +230,7 @@ impl Line { color = next_run.1; } else { color_end = self.layout.len; - color = ColorU::black(); + color = Color::black(); } } diff --git a/server/src/tests.rs b/server/src/tests.rs index 8767155ca0b220f9139c41896fd036f525c26ae9..66d904746772c5c4d0813ac85ab33b108c074207 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -607,7 +607,7 @@ impl gpui::View for EmptyView { "empty view" } - fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox { + fn render<'a>(&self, _: &gpui::RenderContext) -> gpui::ElementBox { gpui::Element::boxed(gpui::elements::Empty) } } diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 7e4d4949d6732de853f942fae8b6e234dc9bc677..25aafaac5f9f71adecd8b4f99bc1425194cbc755 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -14,7 +14,7 @@ name = "Zed" path = "src/main.rs" [features] -test-support = ["tempdir", "serde_json", "zrpc/test-support"] +test-support = ["tempdir", "zrpc/test-support"] [dependencies] anyhow = "1.0.38" @@ -41,9 +41,7 @@ rsa = "0.4" rust-embed = "5.9.0" seahash = "4.1" serde = { version = "1", features = ["derive"] } -serde_json = { version = "1.0.64", features = [ - "preserve_order", -], optional = true } +serde_json = { version = "1.0.64", features = ["preserve_order"] } similar = "1.3" simplelog = "0.9" smallvec = { version = "1.6", features = ["union"] } diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml new file mode 100644 index 0000000000000000000000000000000000000000..20cc34ee0e8d418b84caedebb1c29df26ba04b83 --- /dev/null +++ b/zed/assets/themes/_base.toml @@ -0,0 +1,47 @@ +[ui] +background = "$elevation_1" + +[ui.tab] +background = "$elevation_2" +text = "$text_dull" +border = { color = "#000000", width = 1.0 } +padding = { left = 10, right = 10 } +icon_close = "#383839" +icon_dirty = "#556de8" +icon_conflict = "#e45349" + +[ui.active_tab] +extends = "ui.tab" +background = "$elevation_3" +text = "$text_bright" + +[ui.selector] +background = "$elevation_4" +text = "$text_bright" +padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } +margin.top = 12.0 +corner_radius = 6.0 +shadow = { offset = [0.0, 0.0], blur = 12.0, color = "#00000088" } + +[ui.selector.item] +background = "#424344" +text = "#cccccc" +highlight_text = { color = "#18a3ff", weight = "bold" } +border = { color = "#000000", width = 1.0 } +padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } + +[ui.selector.active_item] +extends = "ui.selector.item" +background = "#094771" + +[editor] +background = "$elevation_3" +gutter_background = "$elevation_3" +active_line_background = "$elevation_4" +line_number = "$text_dull" +line_number_active = "$text_bright" +text = "$text_normal" +replicas = [ + { selection = "#264f78", cursor = "$text_bright" }, + { selection = "#504f31", cursor = "#fcf154" }, +] diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index e4b064ca97a74f91ed0cca7ab2d1460dd7a93cd7..dae0bdf1eceefea9c2d5994ca0932271238e52cd 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -1,38 +1,21 @@ -[ui] -tab_background = 0x131415 -tab_background_active = 0x1c1d1e -tab_text = 0x5a5a5b -tab_text_active = 0xffffff -tab_border = 0x000000 -tab_icon_close = 0x383839 -tab_icon_dirty = 0x556de8 -tab_icon_conflict = 0xe45349 -modal_background = 0x3a3b3c -modal_match_background = 0x424344 -modal_match_background_active = 0x094771 -modal_match_border = 0x000000 -modal_match_text = 0xcccccc -modal_match_text_highlight = 0x18a3ff +extends = "_base" -[editor] -background = 0x131415 -gutter_background = 0x131415 -active_line_background = 0x1c1d1e -line_number = 0x5a5a5b -line_number_active = 0xffffff -default_text = 0xd4d4d4 -replicas = [ - { selection = 0x264f78, cursor = 0xffffff }, - { selection = 0x504f31, cursor = 0xfcf154 }, -] +[variables] +elevation_1 = "#050101" +elevation_2 = "#131415" +elevation_3 = "#1c1d1e" +elevation_4 = "#3a3b3c" +text_dull = "#5a5a5b" +text_bright = "#ffffff" +text_normal = "#d4d4d4" [syntax] -keyword = 0xc586c0 -function = 0xdcdcaa -string = 0xcb8f77 -type = 0x4ec9b0 -number = 0xb5cea8 -comment = 0x6a9955 -property = 0x4e94ce -variant = 0x4fc1ff -constant = 0x9cdcfe +keyword = { color = "#0086c0", weight = "bold" } +function = "#dcdcaa" +string = "#cb8f77" +type = "#4ec9b0" +number = "#b5cea8" +comment = "#6a9955" +property = "#4e94ce" +variant = "#4fc1ff" +constant = "#9cdcfe" diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml new file mode 100644 index 0000000000000000000000000000000000000000..ba7ec9915a6fbf054939dc7431292acd13d8d408 --- /dev/null +++ b/zed/assets/themes/light.toml @@ -0,0 +1,21 @@ +extends = "_base" + +[variables] +elevation_1 = "#ffffff" +elevation_2 = "#f3f3f3" +elevation_3 = "#ececec" +elevation_4 = "#3a3b3c" +text_dull = "#acacac" +text_bright = "#111111" +text_normal = "#333333" + +[syntax] +keyword = "#0000fa" +function = "#795e26" +string = "#a82121" +type = "#267f29" +number = "#b5cea8" +comment = "#6a9955" +property = "#4e94ce" +variant = "#4fc1ff" +constant = "#9cdcfe" diff --git a/zed/src/assets.rs b/zed/src/assets.rs index 072a1a049620ab42152fd4b1b413a9d761f65815..e7c0103421620b6888ee2dd28b723ed22d4daa11 100644 --- a/zed/src/assets.rs +++ b/zed/src/assets.rs @@ -10,4 +10,8 @@ impl AssetSource for Assets { fn load(&self, path: &str) -> Result> { Self::get(path).ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) } + + fn list(&self, path: &str) -> Vec> { + Self::iter().filter(|p| p.starts_with(path)).collect() + } } diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 3911970249c95bf3afaa646399b242aec9a4801b..d5507f38c9a17b2be23a38e24c0c3182dd3809f1 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -4,7 +4,7 @@ mod element; pub mod movement; use crate::{ - settings::{Settings, StyleId, Theme}, + settings::{HighlightId, Settings, Theme}, time::ReplicaId, util::{post_inc, Bias}, workspace, @@ -16,10 +16,10 @@ pub use display_map::DisplayPoint; use display_map::*; pub use element::*; use gpui::{ - color::ColorU, font_cache::FamilyId, fonts::Properties as FontProperties, + color::Color, font_cache::FamilyId, fonts::Properties as FontProperties, geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, - ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, Task, TextLayoutCache, View, - ViewContext, WeakViewHandle, + ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, RenderContext, Task, + TextLayoutCache, View, ViewContext, WeakViewHandle, }; use postage::watch; use serde::{Deserialize, Serialize}; @@ -2349,7 +2349,7 @@ impl Snapshot { .layout_str( "1".repeat(digit_count).as_str(), font_size, - &[(digit_count, font_id, ColorU::black())], + &[(digit_count, font_id, Color::black())], ) .width()) } @@ -2374,9 +2374,9 @@ impl Snapshot { { let display_row = rows.start + ix as u32; let color = if active_rows.contains_key(&display_row) { - theme.editor.line_number_active.0 + theme.editor.line_number_active } else { - theme.editor.line_number.0 + theme.editor.line_number }; if soft_wrapped { layouts.push(None); @@ -2419,7 +2419,7 @@ impl Snapshot { .display_snapshot .highlighted_chunks_for_rows(rows.clone()); - 'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", StyleId::default()))) { + 'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) { for (ix, mut line_chunk) in chunk.split('\n').enumerate() { if ix > 0 { layouts.push(layout_cache.layout_str(&line, self.font_size, &styles)); @@ -2433,12 +2433,12 @@ impl Snapshot { } if !line_chunk.is_empty() && !line_exceeded_max_len { - let (color, font_properties) = self.theme.syntax_style(style_ix); + let style = self.theme.highlight_style(style_ix); // Avoid a lookup if the font properties match the previous ones. - let font_id = if font_properties == prev_font_properties { + let font_id = if style.font_properties == prev_font_properties { prev_font_id } else { - font_cache.select_font(self.font_family, &font_properties)? + font_cache.select_font(self.font_family, &style.font_properties)? }; if line.len() + line_chunk.len() > MAX_LINE_LEN { @@ -2451,9 +2451,9 @@ impl Snapshot { } line.push_str(line_chunk); - styles.push((line_chunk.len(), font_id, color)); + styles.push((line_chunk.len(), font_id, style.color)); prev_font_id = font_id; - prev_font_properties = font_properties; + prev_font_properties = style.font_properties; } } } @@ -2485,7 +2485,7 @@ impl Snapshot { &[( self.display_snapshot.line_len(row) as usize, font_id, - ColorU::black(), + Color::black(), )], )) } @@ -2533,7 +2533,7 @@ impl Entity for Editor { } impl View for Editor { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { EditorElement::new(self.handle.clone()).boxed() } diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 3a3876d808696f94a62d5ba7eb19e5d6f3dc3fc0..9b1f169e48c71a10e37cb60ae12ef7e6a0e2d49b 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -16,7 +16,7 @@ use zrpc::proto; use crate::{ language::{Language, Tree}, operation_queue::{self, OperationQueue}, - settings::{StyleId, ThemeMap}, + settings::{HighlightId, HighlightMap}, sum_tree::{self, FilterCursor, SumTree}, time::{self, ReplicaId}, util::Bias, @@ -1985,7 +1985,7 @@ impl Snapshot { captures, next_capture: None, stack: Default::default(), - theme_mapping: language.theme_mapping(), + highlight_map: language.highlight_map(), }), } } else { @@ -2316,8 +2316,8 @@ impl<'a> tree_sitter::TextProvider<'a> for TextProvider<'a> { struct Highlights<'a> { captures: tree_sitter::QueryCaptures<'a, 'a, TextProvider<'a>>, next_capture: Option<(tree_sitter::QueryMatch<'a, 'a>, usize)>, - stack: Vec<(usize, StyleId)>, - theme_mapping: ThemeMap, + stack: Vec<(usize, HighlightId)>, + highlight_map: HighlightMap, } pub struct HighlightedChunks<'a> { @@ -2341,7 +2341,7 @@ impl<'a> HighlightedChunks<'a> { if offset < next_capture_end { highlights.stack.push(( next_capture_end, - highlights.theme_mapping.get(capture.index), + highlights.highlight_map.get(capture.index), )); } highlights.next_capture.take(); @@ -2357,7 +2357,7 @@ impl<'a> HighlightedChunks<'a> { } impl<'a> Iterator for HighlightedChunks<'a> { - type Item = (&'a str, StyleId); + type Item = (&'a str, HighlightId); fn next(&mut self) -> Option { let mut next_capture_start = usize::MAX; @@ -2381,7 +2381,7 @@ impl<'a> Iterator for HighlightedChunks<'a> { next_capture_start = capture.node.start_byte(); break; } else { - let style_id = highlights.theme_mapping.get(capture.index); + let style_id = highlights.highlight_map.get(capture.index); highlights.stack.push((capture.node.end_byte(), style_id)); highlights.next_capture = highlights.captures.next(); } @@ -2391,7 +2391,7 @@ impl<'a> Iterator for HighlightedChunks<'a> { if let Some(chunk) = self.chunks.peek() { let chunk_start = self.range.start; let mut chunk_end = (self.chunks.offset() + chunk.len()).min(next_capture_start); - let mut style_id = StyleId::default(); + let mut style_id = HighlightId::default(); if let Some((parent_capture_end, parent_style_id)) = self.highlights.as_ref().and_then(|h| h.stack.last()) { diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 8efb42f71eef45021f39535436fed60809bd4153..ab97179a6b2ffaf28143a83d1b20cbe2ccf6a796 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -340,7 +340,7 @@ mod tests { util::RandomCharIter, }; use buffer::{History, SelectionGoal}; - use gpui::MutableAppContext; + use gpui::{color::Color, MutableAppContext}; use rand::{prelude::StdRng, Rng}; use std::{env, sync::Arc}; use Bias::*; @@ -652,13 +652,13 @@ mod tests { (function_item name: (identifier) @fn.name)"#, ) .unwrap(); - let theme = Theme::parse( - r#" - [syntax] - "mod.body" = 0xff0000 - "fn.name" = 0x00ff00"#, - ) - .unwrap(); + let theme = Theme { + syntax: vec![ + ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), + ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), + ], + ..Default::default() + }; let lang = Arc::new(Language { config: LanguageConfig { name: "Test".to_string(), @@ -668,7 +668,7 @@ mod tests { grammar: grammar.clone(), highlight_query, brackets_query: tree_sitter::Query::new(grammar, "").unwrap(), - theme_mapping: Default::default(), + highlight_map: Default::default(), }); lang.set_theme(&theme); @@ -742,13 +742,13 @@ mod tests { (function_item name: (identifier) @fn.name)"#, ) .unwrap(); - let theme = Theme::parse( - r#" - [syntax] - "mod.body" = 0xff0000 - "fn.name" = 0x00ff00"#, - ) - .unwrap(); + let theme = Theme { + syntax: vec![ + ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), + ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), + ], + ..Default::default() + }; let lang = Arc::new(Language { config: LanguageConfig { name: "Test".to_string(), @@ -758,7 +758,7 @@ mod tests { grammar: grammar.clone(), highlight_query, brackets_query: tree_sitter::Query::new(grammar, "").unwrap(), - theme_mapping: Default::default(), + highlight_map: Default::default(), }); lang.set_theme(&theme); @@ -937,7 +937,7 @@ mod tests { let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx)); let mut chunks: Vec<(String, Option<&str>)> = Vec::new(); for (chunk, style_id) in snapshot.highlighted_chunks_for_rows(rows) { - let style_name = theme.syntax_style_name(style_id); + let style_name = theme.highlight_name(style_id); if let Some((last_chunk, last_style_name)) = chunks.last_mut() { if style_name == *last_style_name { last_chunk.push_str(chunk); diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index b8ab39cce169e86c5ea4e83588d42aceaf005b8f..3bfa1f3d240429e74f7ed9532dca9bf8eda04be2 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/zed/src/editor/display_map/fold_map.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::{ editor::buffer, - settings::StyleId, + settings::HighlightId, sum_tree::{self, Cursor, FilterCursor, SumTree}, time, util::Bias, @@ -1004,12 +1004,12 @@ impl<'a> Iterator for Chunks<'a> { pub struct HighlightedChunks<'a> { transform_cursor: Cursor<'a, Transform, FoldOffset, usize>, buffer_chunks: buffer::HighlightedChunks<'a>, - buffer_chunk: Option<(usize, &'a str, StyleId)>, + buffer_chunk: Option<(usize, &'a str, HighlightId)>, buffer_offset: usize, } impl<'a> Iterator for HighlightedChunks<'a> { - type Item = (&'a str, StyleId); + type Item = (&'a str, HighlightId); fn next(&mut self) -> Option { let transform = if let Some(item) = self.transform_cursor.item() { @@ -1031,7 +1031,7 @@ impl<'a> Iterator for HighlightedChunks<'a> { self.transform_cursor.next(&()); } - return Some((output_text, StyleId::default())); + return Some((output_text, HighlightId::default())); } // Retrieve a chunk from the current location in the buffer. diff --git a/zed/src/editor/display_map/tab_map.rs b/zed/src/editor/display_map/tab_map.rs index 99257c847fa686e3e3e47f5ae5d754e04a815bc7..299320ca321adb9e197ddfc5f4295b69aaacdd4a 100644 --- a/zed/src/editor/display_map/tab_map.rs +++ b/zed/src/editor/display_map/tab_map.rs @@ -1,7 +1,7 @@ use parking_lot::Mutex; use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot}; -use crate::{editor::rope, settings::StyleId, util::Bias}; +use crate::{editor::rope, settings::HighlightId, util::Bias}; use std::{mem, ops::Range}; pub struct TabMap(Mutex); @@ -416,14 +416,14 @@ impl<'a> Iterator for Chunks<'a> { pub struct HighlightedChunks<'a> { fold_chunks: fold_map::HighlightedChunks<'a>, chunk: &'a str, - style_id: StyleId, + style_id: HighlightId, column: usize, tab_size: usize, skip_leading_tab: bool, } impl<'a> Iterator for HighlightedChunks<'a> { - type Item = (&'a str, StyleId); + type Item = (&'a str, HighlightId); fn next(&mut self) -> Option { if self.chunk.is_empty() { diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index 18d6dbba6afe2cecdd0eb021015947e02324b424..358730748b89173c361decf4bbe9db23864da7d3 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/zed/src/editor/display_map/wrap_map.rs @@ -5,7 +5,7 @@ use super::{ }; use crate::{ editor::Point, - settings::StyleId, + settings::HighlightId, sum_tree::{self, Cursor, SumTree}, util::Bias, Settings, @@ -59,7 +59,7 @@ pub struct Chunks<'a> { pub struct HighlightedChunks<'a> { input_chunks: tab_map::HighlightedChunks<'a>, input_chunk: &'a str, - style_id: StyleId, + style_id: HighlightId, output_position: WrapPoint, max_output_row: u32, transforms: Cursor<'a, Transform, WrapPoint, TabPoint>, @@ -487,7 +487,7 @@ impl Snapshot { HighlightedChunks { input_chunks: self.tab_snapshot.highlighted_chunks(input_start..input_end), input_chunk: "", - style_id: StyleId::default(), + style_id: HighlightId::default(), output_position: output_start, max_output_row: rows.end, transforms, @@ -670,7 +670,7 @@ impl<'a> Iterator for Chunks<'a> { } impl<'a> Iterator for HighlightedChunks<'a> { - type Item = (&'a str, StyleId); + type Item = (&'a str, HighlightId); fn next(&mut self) -> Option { if self.output_position.row() >= self.max_output_row { diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index c2a486afdd884cc73019f5b40bb02f33dc9b8c30..44b283dee1d7bf7108be123c535578b33a402560 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -1,7 +1,7 @@ use super::{DisplayPoint, Editor, SelectAction, Snapshot}; use crate::time::ReplicaId; use gpui::{ - color::ColorU, + color::Color, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -196,14 +196,14 @@ impl EditorElement { let theme = &settings.theme; cx.scene.push_quad(Quad { bounds: gutter_bounds, - background: Some(theme.editor.gutter_background.0), - border: Border::new(0., ColorU::transparent_black()), + background: Some(theme.editor.gutter_background), + border: Border::new(0., Color::transparent_black()), corner_radius: 0., }); cx.scene.push_quad(Quad { bounds: text_bounds, - background: Some(theme.editor.background.0), - border: Border::new(0., ColorU::transparent_black()), + background: Some(theme.editor.background), + border: Border::new(0., Color::transparent_black()), corner_radius: 0., }); @@ -229,7 +229,7 @@ impl EditorElement { ); cx.scene.push_quad(Quad { bounds: RectF::new(origin, size), - background: Some(theme.editor.active_line_background.0), + background: Some(theme.editor.active_line_background), border: Border::default(), corner_radius: 0., }); @@ -290,7 +290,7 @@ impl EditorElement { }; let selection = Selection { - color: replica_theme.selection.0, + color: replica_theme.selection, line_height: layout.line_height, start_y: content_origin.y() + row_range.start as f32 * layout.line_height - scroll_top, @@ -333,7 +333,7 @@ impl EditorElement { - scroll_left; let y = selection.end.row() as f32 * layout.line_height - scroll_top; cursors.push(Cursor { - color: replica_theme.cursor.0, + color: replica_theme.cursor, origin: content_origin + vec2f(x, y), line_height: layout.line_height, }); @@ -707,7 +707,7 @@ impl PaintState { struct Cursor { origin: Vector2F, line_height: f32, - color: ColorU, + color: Color, } impl Cursor { @@ -715,7 +715,7 @@ impl Cursor { cx.scene.push_quad(Quad { bounds: RectF::new(self.origin, vec2f(2.0, self.line_height)), background: Some(self.color), - border: Border::new(0., ColorU::black()), + border: Border::new(0., Color::black()), corner_radius: 0., }); } @@ -726,7 +726,7 @@ struct Selection { start_y: f32, line_height: f32, lines: Vec, - color: ColorU, + color: Color, } #[derive(Debug)] diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 26820b3f593ef8ca1ad8212565449f110891cfcc..7c9900c3f4133b84b9b1063f5d6efabf6b96f6d2 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -6,13 +6,10 @@ use crate::{ worktree::{match_paths, PathMatch}, }; use gpui::{ - color::ColorF, elements::*, - fonts::{Properties, Weight}, - geometry::vector::vec2f, keymap::{self, Binding}, - AppContext, Axis, Border, Entity, MutableAppContext, Task, View, ViewContext, ViewHandle, - WeakViewHandle, + AppContext, Axis, Entity, MutableAppContext, RenderContext, Task, View, ViewContext, + ViewHandle, WeakViewHandle, }; use postage::watch; use std::{ @@ -45,7 +42,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action("file_finder:select", FileFinder::select); cx.add_action("menu:select_prev", FileFinder::select_prev); cx.add_action("menu:select_next", FileFinder::select_next); - cx.add_action("uniform_list:scroll", FileFinder::scroll); cx.add_bindings(vec![ Binding::new("cmd-p", "file_finder:toggle", None), @@ -68,7 +64,7 @@ impl View for FileFinder { "FileFinder" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { let settings = self.settings.borrow(); Align::new( @@ -79,11 +75,7 @@ impl View for FileFinder { .with_child(Expanded::new(1.0, self.render_matches()).boxed()) .boxed(), ) - .with_margin_top(12.0) - .with_uniform_padding(6.0) - .with_corner_radius(6.0) - .with_background_color(settings.theme.ui.modal_background) - .with_shadow(vec2f(0., 4.), 12., ColorF::new(0.0, 0.0, 0.0, 0.5).to_u8()) + .with_style(&settings.theme.ui.selector.container) .boxed(), ) .with_max_width(600.0) @@ -115,7 +107,7 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(settings.theme.editor.default_text.0) + .with_style(&settings.theme.ui.selector.label) .boxed(), ) .with_margin_top(6.0) @@ -147,20 +139,25 @@ impl FileFinder { } fn render_match(&self, path_match: &PathMatch, index: usize) -> ElementBox { + let selected_index = self.selected_index(); let settings = self.settings.borrow(); - let theme = &settings.theme.ui; + let style = if index == selected_index { + &settings.theme.ui.selector.active_item + } else { + &settings.theme.ui.selector.item + }; let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match); - let bold = *Properties::new().weight(Weight::BOLD); - let selected_index = self.selected_index(); - let mut container = Container::new( + let container = Container::new( Flex::row() .with_child( Container::new( LineBox::new( settings.ui_font_family, settings.ui_font_size, - Svg::new("icons/file-16.svg").boxed(), + Svg::new("icons/file-16.svg") + .with_color(style.label.text.color) + .boxed(), ) .boxed(), ) @@ -177,12 +174,8 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(theme.modal_match_text.0) - .with_highlights( - theme.modal_match_text_highlight.0, - bold, - file_name_positions, - ) + .with_style(&style.label) + .with_highlights(file_name_positions) .boxed(), ) .with_child( @@ -191,12 +184,8 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(theme.modal_match_text.0) - .with_highlights( - theme.modal_match_text_highlight.0, - bold, - full_path_positions, - ) + .with_style(&style.label) + .with_highlights(full_path_positions) .boxed(), ) .boxed(), @@ -205,16 +194,7 @@ impl FileFinder { ) .boxed(), ) - .with_uniform_padding(6.0) - .with_background_color(if index == selected_index { - theme.modal_match_background_active.0 - } else { - theme.modal_match_background.0 - }); - - if index == selected_index || index < self.matches.len() - 1 { - container = container.with_border(Border::bottom(1.0, theme.modal_match_border)); - } + .with_style(&style.container); let entry = (path_match.tree_id, path_match.path.clone()); EventHandler::new(container.boxed()) @@ -250,31 +230,30 @@ impl FileFinder { (file_name, file_name_positions, full_path, path_positions) } - fn toggle(workspace_view: &mut Workspace, _: &(), cx: &mut ViewContext) { - workspace_view.toggle_modal(cx, |cx, workspace_view| { - let workspace = cx.handle(); - let finder = - cx.add_view(|cx| Self::new(workspace_view.settings.clone(), workspace, cx)); + fn toggle(workspace: &mut Workspace, _: &(), cx: &mut ViewContext) { + workspace.toggle_modal(cx, |cx, workspace| { + let handle = cx.handle(); + let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx)); cx.subscribe_to_view(&finder, Self::on_event); finder }); } fn on_event( - workspace_view: &mut Workspace, + workspace: &mut Workspace, _: ViewHandle, event: &Event, cx: &mut ViewContext, ) { match event { Event::Selected(tree_id, path) => { - workspace_view + workspace .open_entry((*tree_id, path.clone()), cx) .map(|d| d.detach()); - workspace_view.dismiss_modal(cx); + workspace.dismiss_modal(cx); } Event::Dismissed => { - workspace_view.dismiss_modal(cx); + workspace.dismiss_modal(cx); } } } @@ -301,7 +280,7 @@ impl FileFinder { matches: Vec::new(), selected: None, cancel_flag: Arc::new(AtomicBool::new(false)), - list_state: UniformListState::new(), + list_state: Default::default(), } } @@ -371,10 +350,6 @@ impl FileFinder { cx.notify(); } - fn scroll(&mut self, _: &f32, cx: &mut ViewContext) { - cx.notify(); - } - fn confirm(&mut self, _: &(), cx: &mut ViewContext) { if let Some(m) = self.matches.get(self.selected_index()) { cx.emit(Event::Selected(m.tree_id, m.path.clone())); @@ -407,7 +382,7 @@ impl FileFinder { false, false, 100, - cancel_flag.clone(), + cancel_flag.as_ref(), background, ) .await; diff --git a/zed/src/fuzzy.rs b/zed/src/fuzzy.rs new file mode 100644 index 0000000000000000000000000000000000000000..dde7f8fd7aff2fd198816f6115119e6128172978 --- /dev/null +++ b/zed/src/fuzzy.rs @@ -0,0 +1,785 @@ +mod char_bag; + +use crate::{ + util, + worktree::{EntryKind, Snapshot}, +}; +use gpui::executor; +use std::{ + borrow::Cow, + cmp::{max, min, Ordering}, + path::Path, + sync::atomic::{self, AtomicBool}, + sync::Arc, +}; + +pub use char_bag::CharBag; + +const BASE_DISTANCE_PENALTY: f64 = 0.6; +const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; +const MIN_DISTANCE_PENALTY: f64 = 0.2; + +struct Matcher<'a> { + query: &'a [char], + lowercase_query: &'a [char], + query_char_bag: CharBag, + smart_case: bool, + max_results: usize, + min_score: f64, + match_positions: Vec, + last_positions: Vec, + score_matrix: Vec>, + best_position_matrix: Vec, +} + +trait Match: Ord { + fn score(&self) -> f64; + fn set_positions(&mut self, positions: Vec); +} + +trait MatchCandidate { + fn has_chars(&self, bag: CharBag) -> bool; + fn to_string<'a>(&'a self) -> Cow<'a, str>; +} + +#[derive(Clone, Debug)] +pub struct PathMatchCandidate<'a> { + pub path: &'a Arc, + pub char_bag: CharBag, +} + +#[derive(Clone, Debug)] +pub struct PathMatch { + pub score: f64, + pub positions: Vec, + pub tree_id: usize, + pub path: Arc, + pub path_prefix: Arc, +} + +#[derive(Clone, Debug)] +pub struct StringMatchCandidate { + pub string: String, + pub char_bag: CharBag, +} + +impl Match for PathMatch { + fn score(&self) -> f64 { + self.score + } + + fn set_positions(&mut self, positions: Vec) { + self.positions = positions; + } +} + +impl Match for StringMatch { + fn score(&self) -> f64 { + self.score + } + + fn set_positions(&mut self, positions: Vec) { + self.positions = positions; + } +} + +impl<'a> MatchCandidate for PathMatchCandidate<'a> { + fn has_chars(&self, bag: CharBag) -> bool { + self.char_bag.is_superset(bag) + } + + fn to_string(&self) -> Cow<'a, str> { + self.path.to_string_lossy() + } +} + +impl<'a> MatchCandidate for &'a StringMatchCandidate { + fn has_chars(&self, bag: CharBag) -> bool { + self.char_bag.is_superset(bag) + } + + fn to_string(&self) -> Cow<'a, str> { + self.string.as_str().into() + } +} + +#[derive(Clone, Debug)] +pub struct StringMatch { + pub score: f64, + pub positions: Vec, + pub string: String, +} + +impl PartialEq for StringMatch { + fn eq(&self, other: &Self) -> bool { + self.score.eq(&other.score) + } +} + +impl Eq for StringMatch {} + +impl PartialOrd for StringMatch { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for StringMatch { + fn cmp(&self, other: &Self) -> Ordering { + self.score + .partial_cmp(&other.score) + .unwrap_or(Ordering::Equal) + .then_with(|| self.string.cmp(&other.string)) + } +} + +impl PartialEq for PathMatch { + fn eq(&self, other: &Self) -> bool { + self.score.eq(&other.score) + } +} + +impl Eq for PathMatch {} + +impl PartialOrd for PathMatch { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PathMatch { + fn cmp(&self, other: &Self) -> Ordering { + self.score + .partial_cmp(&other.score) + .unwrap_or(Ordering::Equal) + .then_with(|| self.tree_id.cmp(&other.tree_id)) + .then_with(|| Arc::as_ptr(&self.path).cmp(&Arc::as_ptr(&other.path))) + } +} + +pub async fn match_strings( + candidates: &[StringMatchCandidate], + query: &str, + smart_case: bool, + max_results: usize, + cancel_flag: &AtomicBool, + background: Arc, +) -> Vec { + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + + let lowercase_query = &lowercase_query; + let query = &query; + let query_char_bag = CharBag::from(&lowercase_query[..]); + + let num_cpus = background.num_cpus().min(candidates.len()); + let segment_size = (candidates.len() + num_cpus - 1) / num_cpus; + let mut segment_results = (0..num_cpus) + .map(|_| Vec::with_capacity(max_results)) + .collect::>(); + + background + .scoped(|scope| { + for (segment_idx, results) in segment_results.iter_mut().enumerate() { + let cancel_flag = &cancel_flag; + scope.spawn(async move { + let segment_start = segment_idx * segment_size; + let segment_end = segment_start + segment_size; + let mut matcher = Matcher::new( + query, + lowercase_query, + query_char_bag, + smart_case, + max_results, + ); + matcher.match_strings( + &candidates[segment_start..segment_end], + results, + cancel_flag, + ); + }); + } + }) + .await; + + let mut results = Vec::new(); + for segment_result in segment_results { + if results.is_empty() { + results = segment_result; + } else { + util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(&a)); + } + } + results +} + +pub async fn match_paths( + snapshots: &[Snapshot], + query: &str, + include_ignored: bool, + smart_case: bool, + max_results: usize, + cancel_flag: &AtomicBool, + background: Arc, +) -> Vec { + let path_count: usize = if include_ignored { + snapshots.iter().map(Snapshot::file_count).sum() + } else { + snapshots.iter().map(Snapshot::visible_file_count).sum() + }; + if path_count == 0 { + return Vec::new(); + } + + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + + let lowercase_query = &lowercase_query; + let query = &query; + let query_char_bag = CharBag::from(&lowercase_query[..]); + + let num_cpus = background.num_cpus().min(path_count); + let segment_size = (path_count + num_cpus - 1) / num_cpus; + let mut segment_results = (0..num_cpus) + .map(|_| Vec::with_capacity(max_results)) + .collect::>(); + + background + .scoped(|scope| { + for (segment_idx, results) in segment_results.iter_mut().enumerate() { + scope.spawn(async move { + let segment_start = segment_idx * segment_size; + let segment_end = segment_start + segment_size; + let mut matcher = Matcher::new( + query, + lowercase_query, + query_char_bag, + smart_case, + max_results, + ); + + let mut tree_start = 0; + for snapshot in snapshots { + let tree_end = if include_ignored { + tree_start + snapshot.file_count() + } else { + tree_start + snapshot.visible_file_count() + }; + + if tree_start < segment_end && segment_start < tree_end { + let path_prefix: Arc = + if snapshot.root_entry().map_or(false, |e| e.is_file()) { + snapshot.root_name().into() + } else if snapshots.len() > 1 { + format!("{}/", snapshot.root_name()).into() + } else { + "".into() + }; + + let start = max(tree_start, segment_start) - tree_start; + let end = min(tree_end, segment_end) - tree_start; + let entries = if include_ignored { + snapshot.files(start).take(end - start) + } else { + snapshot.visible_files(start).take(end - start) + }; + let paths = entries.map(|entry| { + if let EntryKind::File(char_bag) = entry.kind { + PathMatchCandidate { + path: &entry.path, + char_bag, + } + } else { + unreachable!() + } + }); + + matcher.match_paths( + snapshot.id(), + path_prefix, + paths, + results, + &cancel_flag, + ); + } + if tree_end >= segment_end { + break; + } + tree_start = tree_end; + } + }) + } + }) + .await; + + let mut results = Vec::new(); + for segment_result in segment_results { + if results.is_empty() { + results = segment_result; + } else { + util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(&a)); + } + } + results +} + +impl<'a> Matcher<'a> { + fn new( + query: &'a [char], + lowercase_query: &'a [char], + query_char_bag: CharBag, + smart_case: bool, + max_results: usize, + ) -> Self { + Self { + query, + lowercase_query, + query_char_bag, + min_score: 0.0, + last_positions: vec![0; query.len()], + match_positions: vec![0; query.len()], + score_matrix: Vec::new(), + best_position_matrix: Vec::new(), + smart_case, + max_results, + } + } + + fn match_strings( + &mut self, + candidates: &[StringMatchCandidate], + results: &mut Vec, + cancel_flag: &AtomicBool, + ) { + self.match_internal( + &[], + &[], + candidates.iter(), + results, + cancel_flag, + |candidate, score| StringMatch { + score, + positions: Vec::new(), + string: candidate.string.to_string(), + }, + ) + } + + fn match_paths( + &mut self, + tree_id: usize, + path_prefix: Arc, + path_entries: impl Iterator>, + results: &mut Vec, + cancel_flag: &AtomicBool, + ) { + let prefix = path_prefix.chars().collect::>(); + let lowercase_prefix = prefix + .iter() + .map(|c| c.to_ascii_lowercase()) + .collect::>(); + self.match_internal( + &prefix, + &lowercase_prefix, + path_entries, + results, + cancel_flag, + |candidate, score| PathMatch { + score, + tree_id, + positions: Vec::new(), + path: candidate.path.clone(), + path_prefix: path_prefix.clone(), + }, + ) + } + + fn match_internal( + &mut self, + prefix: &[char], + lowercase_prefix: &[char], + candidates: impl Iterator, + results: &mut Vec, + cancel_flag: &AtomicBool, + build_match: F, + ) where + R: Match, + F: Fn(&C, f64) -> R, + { + let mut candidate_chars = Vec::new(); + let mut lowercase_candidate_chars = Vec::new(); + + for candidate in candidates { + if !candidate.has_chars(self.query_char_bag) { + continue; + } + + if cancel_flag.load(atomic::Ordering::Relaxed) { + break; + } + + candidate_chars.clear(); + lowercase_candidate_chars.clear(); + for c in candidate.to_string().chars() { + candidate_chars.push(c); + lowercase_candidate_chars.push(c.to_ascii_lowercase()); + } + + if !self.find_last_positions(&lowercase_prefix, &lowercase_candidate_chars) { + continue; + } + + let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len()); + self.score_matrix.clear(); + self.score_matrix.resize(matrix_len, None); + self.best_position_matrix.clear(); + self.best_position_matrix.resize(matrix_len, 0); + + let score = self.score_match( + &candidate_chars, + &lowercase_candidate_chars, + &prefix, + &lowercase_prefix, + ); + + if score > 0.0 { + let mut mat = build_match(&candidate, score); + if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) { + if results.len() < self.max_results { + mat.set_positions(self.match_positions.clone()); + results.insert(i, mat); + } else if i < results.len() { + results.pop(); + mat.set_positions(self.match_positions.clone()); + results.insert(i, mat); + } + if results.len() == self.max_results { + self.min_score = results.last().unwrap().score(); + } + } + } + } + } + + fn find_last_positions(&mut self, prefix: &[char], path: &[char]) -> bool { + let mut path = path.iter(); + let mut prefix_iter = prefix.iter(); + for (i, char) in self.query.iter().enumerate().rev() { + if let Some(j) = path.rposition(|c| c == char) { + self.last_positions[i] = j + prefix.len(); + } else if let Some(j) = prefix_iter.rposition(|c| c == char) { + self.last_positions[i] = j; + } else { + return false; + } + } + true + } + + fn score_match( + &mut self, + path: &[char], + path_cased: &[char], + prefix: &[char], + lowercase_prefix: &[char], + ) -> f64 { + let score = self.recursive_score_match( + path, + path_cased, + prefix, + lowercase_prefix, + 0, + 0, + self.query.len() as f64, + ) * self.query.len() as f64; + + if score <= 0.0 { + return 0.0; + } + + let path_len = prefix.len() + path.len(); + let mut cur_start = 0; + let mut byte_ix = 0; + let mut char_ix = 0; + for i in 0..self.query.len() { + let match_char_ix = self.best_position_matrix[i * path_len + cur_start]; + while char_ix < match_char_ix { + let ch = prefix + .get(char_ix) + .or_else(|| path.get(char_ix - prefix.len())) + .unwrap(); + byte_ix += ch.len_utf8(); + char_ix += 1; + } + cur_start = match_char_ix + 1; + self.match_positions[i] = byte_ix; + } + + score + } + + fn recursive_score_match( + &mut self, + path: &[char], + path_cased: &[char], + prefix: &[char], + lowercase_prefix: &[char], + query_idx: usize, + path_idx: usize, + cur_score: f64, + ) -> f64 { + if query_idx == self.query.len() { + return 1.0; + } + + let path_len = prefix.len() + path.len(); + + if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] { + return memoized; + } + + let mut score = 0.0; + let mut best_position = 0; + + let query_char = self.lowercase_query[query_idx]; + let limit = self.last_positions[query_idx]; + + let mut last_slash = 0; + for j in path_idx..=limit { + let path_char = if j < prefix.len() { + lowercase_prefix[j] + } else { + path_cased[j - prefix.len()] + }; + let is_path_sep = path_char == '/' || path_char == '\\'; + + if query_idx == 0 && is_path_sep { + last_slash = j; + } + + if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { + let curr = if j < prefix.len() { + prefix[j] + } else { + path[j - prefix.len()] + }; + + let mut char_score = 1.0; + if j > path_idx { + let last = if j - 1 < prefix.len() { + prefix[j - 1] + } else { + path[j - 1 - prefix.len()] + }; + + if last == '/' { + char_score = 0.9; + } else if last == '-' || last == '_' || last == ' ' || last.is_numeric() { + char_score = 0.8; + } else if last.is_lowercase() && curr.is_uppercase() { + char_score = 0.8; + } else if last == '.' { + char_score = 0.7; + } else if query_idx == 0 { + char_score = BASE_DISTANCE_PENALTY; + } else { + char_score = MIN_DISTANCE_PENALTY.max( + BASE_DISTANCE_PENALTY + - (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY, + ); + } + } + + // Apply a severe penalty if the case doesn't match. + // This will make the exact matches have higher score than the case-insensitive and the + // path insensitive matches. + if (self.smart_case || curr == '/') && self.query[query_idx] != curr { + char_score *= 0.001; + } + + let mut multiplier = char_score; + + // Scale the score based on how deep within the path we found the match. + if query_idx == 0 { + multiplier /= ((prefix.len() + path.len()) - last_slash) as f64; + } + + let mut next_score = 1.0; + if self.min_score > 0.0 { + next_score = cur_score * multiplier; + // Scores only decrease. If we can't pass the previous best, bail + if next_score < self.min_score { + // Ensure that score is non-zero so we use it in the memo table. + if score == 0.0 { + score = 1e-18; + } + continue; + } + } + + let new_score = self.recursive_score_match( + path, + path_cased, + prefix, + lowercase_prefix, + query_idx + 1, + j + 1, + next_score, + ) * multiplier; + + if new_score > score { + score = new_score; + best_position = j; + // Optimization: can't score better than 1. + if new_score == 1.0 { + break; + } + } + } + } + + if best_position != 0 { + self.best_position_matrix[query_idx * path_len + path_idx] = best_position; + } + + self.score_matrix[query_idx * path_len + path_idx] = Some(score); + score + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn test_get_last_positions() { + let mut query: &[char] = &['d', 'c']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']); + assert_eq!(result, false); + + query = &['c', 'd']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']); + assert_eq!(result, true); + assert_eq!(matcher.last_positions, vec![2, 4]); + + query = &['z', '/', 'z', 'f']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']); + assert_eq!(result, true); + assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]); + } + + #[test] + fn test_match_path_entries() { + let paths = vec![ + "", + "a", + "ab", + "abC", + "abcd", + "alphabravocharlie", + "AlphaBravoCharlie", + "thisisatestdir", + "/////ThisIsATestDir", + "/this/is/a/test/dir", + "/test/tiatd", + ]; + + assert_eq!( + match_query("abc", false, &paths), + vec![ + ("abC", vec![0, 1, 2]), + ("abcd", vec![0, 1, 2]), + ("AlphaBravoCharlie", vec![0, 5, 10]), + ("alphabravocharlie", vec![4, 5, 10]), + ] + ); + assert_eq!( + match_query("t/i/a/t/d", false, &paths), + vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),] + ); + + assert_eq!( + match_query("tiatd", false, &paths), + vec![ + ("/test/tiatd", vec![6, 7, 8, 9, 10]), + ("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]), + ("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]), + ("thisisatestdir", vec![0, 2, 6, 7, 11]), + ] + ); + } + + #[test] + fn test_match_multibyte_path_entries() { + let paths = vec!["aαbβ/cγdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"]; + assert_eq!("1️⃣".len(), 7); + assert_eq!( + match_query("bcd", false, &paths), + vec![ + ("αβγδ/bcde", vec![9, 10, 11]), + ("aαbβ/cγdδ", vec![3, 7, 10]), + ] + ); + assert_eq!( + match_query("cde", false, &paths), + vec![ + ("αβγδ/bcde", vec![10, 11, 12]), + ("c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", vec![0, 23, 46]), + ] + ); + } + + fn match_query<'a>( + query: &str, + smart_case: bool, + paths: &Vec<&'a str>, + ) -> Vec<(&'a str, Vec)> { + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + let query_chars = CharBag::from(&lowercase_query[..]); + + let path_arcs = paths + .iter() + .map(|path| Arc::from(PathBuf::from(path))) + .collect::>(); + let mut path_entries = Vec::new(); + for (i, path) in paths.iter().enumerate() { + let lowercase_path = path.to_lowercase().chars().collect::>(); + let char_bag = CharBag::from(lowercase_path.as_slice()); + path_entries.push(PathMatchCandidate { + char_bag, + path: path_arcs.get(i).unwrap(), + }); + } + + let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100); + + let cancel_flag = AtomicBool::new(false); + let mut results = Vec::new(); + matcher.match_paths( + 0, + "".into(), + path_entries.into_iter(), + &mut results, + &cancel_flag, + ); + + results + .into_iter() + .map(|result| { + ( + paths + .iter() + .copied() + .find(|p| result.path.as_ref() == Path::new(p)) + .unwrap(), + result.positions, + ) + }) + .collect() + } +} diff --git a/zed/src/worktree/char_bag.rs b/zed/src/fuzzy/char_bag.rs similarity index 100% rename from zed/src/worktree/char_bag.rs rename to zed/src/fuzzy/char_bag.rs diff --git a/zed/src/language.rs b/zed/src/language.rs index 7d286dfbe3e9ae3fbf2df2fd671550885e517455..886befe595e551befa25c78348ef75f6c5db1cf2 100644 --- a/zed/src/language.rs +++ b/zed/src/language.rs @@ -1,4 +1,4 @@ -use crate::settings::{Theme, ThemeMap}; +use crate::settings::{HighlightMap, Theme}; use parking_lot::Mutex; use rust_embed::RustEmbed; use serde::Deserialize; @@ -27,7 +27,7 @@ pub struct Language { pub grammar: Grammar, pub highlight_query: Query, pub brackets_query: Query, - pub theme_mapping: Mutex, + pub highlight_map: Mutex, } pub struct LanguageRegistry { @@ -35,12 +35,12 @@ pub struct LanguageRegistry { } impl Language { - pub fn theme_mapping(&self) -> ThemeMap { - self.theme_mapping.lock().clone() + pub fn highlight_map(&self) -> HighlightMap { + self.highlight_map.lock().clone() } pub fn set_theme(&self, theme: &Theme) { - *self.theme_mapping.lock() = ThemeMap::new(self.highlight_query.capture_names(), theme); + *self.highlight_map.lock() = HighlightMap::new(self.highlight_query.capture_names(), theme); } } @@ -53,7 +53,7 @@ impl LanguageRegistry { grammar, highlight_query: Self::load_query(grammar, "rust/highlights.scm"), brackets_query: Self::load_query(grammar, "rust/brackets.scm"), - theme_mapping: Mutex::new(ThemeMap::default()), + highlight_map: Mutex::new(HighlightMap::default()), }; Self { @@ -114,7 +114,7 @@ mod tests { grammar, highlight_query: Query::new(grammar, "").unwrap(), brackets_query: Query::new(grammar, "").unwrap(), - theme_mapping: Default::default(), + highlight_map: Default::default(), }), Arc::new(Language { config: LanguageConfig { @@ -125,7 +125,7 @@ mod tests { grammar, highlight_query: Query::new(grammar, "").unwrap(), brackets_query: Query::new(grammar, "").unwrap(), - theme_mapping: Default::default(), + highlight_map: Default::default(), }), ], }; diff --git a/zed/src/lib.rs b/zed/src/lib.rs index b8c84feb1a1079bb786aab564c6f8d34a8ca1f47..e59fb3fd67a246e4f3f32aba96ad1b38f6b3a80e 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -1,9 +1,8 @@ -use zrpc::ForegroundRouter; - pub mod assets; pub mod editor; pub mod file_finder; pub mod fs; +mod fuzzy; pub mod language; pub mod menus; mod operation_queue; @@ -12,18 +11,28 @@ pub mod settings; mod sum_tree; #[cfg(any(test, feature = "test-support"))] pub mod test; +pub mod theme; +pub mod theme_selector; mod time; mod util; pub mod workspace; pub mod worktree; pub use settings::Settings; + +use parking_lot::Mutex; +use postage::watch; +use std::sync::Arc; +use zrpc::ForegroundRouter; + pub struct AppState { - pub settings: postage::watch::Receiver, - pub languages: std::sync::Arc, - pub rpc_router: std::sync::Arc, + pub settings_tx: Arc>>, + pub settings: watch::Receiver, + pub languages: Arc, + pub themes: Arc, + pub rpc_router: Arc, pub rpc: rpc::Client, - pub fs: std::sync::Arc, + pub fs: Arc, } pub fn init(cx: &mut gpui::MutableAppContext) { diff --git a/zed/src/main.rs b/zed/src/main.rs index d69818a54100faa3b1aa89f13179b1aa950fbc53..b488dbe95b5c9947fec80bc76e430203657dcd52 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -3,12 +3,13 @@ use fs::OpenOptions; use log::LevelFilter; +use parking_lot::Mutex; use simplelog::SimpleLogger; use std::{fs, path::PathBuf, sync::Arc}; use zed::{ self, assets, editor, file_finder, fs::RealFs, - language, menus, rpc, settings, + language, menus, rpc, settings, theme_selector, workspace::{self, OpenParams}, worktree::{self}, AppState, @@ -20,13 +21,17 @@ fn main() { let app = gpui::App::new(assets::Assets).unwrap(); - let (_, settings) = settings::channel(&app.font_cache()).unwrap(); + let themes = settings::ThemeRegistry::new(assets::Assets); + let (settings_tx, settings) = + settings::channel_with_themes(&app.font_cache(), &themes).unwrap(); let languages = Arc::new(language::LanguageRegistry::new()); languages.set_theme(&settings.borrow().theme); let mut app_state = AppState { languages: languages.clone(), + settings_tx: Arc::new(Mutex::new(settings_tx)), settings, + themes, rpc_router: Arc::new(ForegroundRouter::new()), rpc: rpc::Client::new(languages), fs: Arc::new(RealFs), @@ -38,12 +43,14 @@ fn main() { &app_state.rpc, Arc::get_mut(&mut app_state.rpc_router).unwrap(), ); + let app_state = Arc::new(app_state); + zed::init(cx); workspace::init(cx); editor::init(cx); file_finder::init(cx); + theme_selector::init(cx, &app_state); - let app_state = Arc::new(app_state); cx.set_menus(menus::menus(&app_state.clone())); if stdout_is_a_pty() { diff --git a/zed/src/settings.rs b/zed/src/settings.rs index 54621b905b59812306a1475ce36ce5935d52568a..a6b9e667c1d848e431a9805b3f7d36f385800520 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -1,20 +1,10 @@ -use super::assets::Assets; -use anyhow::{anyhow, Context, Result}; -use gpui::{ - color::ColorU, - font_cache::{FamilyId, FontCache}, - fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight}, -}; +use crate::theme::{self, DEFAULT_THEME_NAME}; +use anyhow::Result; +use gpui::font_cache::{FamilyId, FontCache}; use postage::watch; -use serde::Deserialize; -use std::{ - collections::HashMap, - fmt, - ops::{Deref, DerefMut}, - sync::Arc, -}; +use std::sync::Arc; -const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX); +pub use theme::{HighlightId, HighlightMap, Theme, ThemeRegistry}; #[derive(Clone)] pub struct Settings { @@ -26,71 +16,19 @@ pub struct Settings { pub theme: Arc, } -#[derive(Clone, Default)] -pub struct Theme { - pub ui: UiTheme, - pub editor: EditorTheme, - syntax: Vec<(String, ColorU, FontProperties)>, -} - -#[derive(Clone, Default, Deserialize)] -#[serde(default)] -pub struct UiTheme { - pub tab_background: Color, - pub tab_background_active: Color, - pub tab_text: Color, - pub tab_text_active: Color, - pub tab_border: Color, - pub tab_icon_close: Color, - pub tab_icon_dirty: Color, - pub tab_icon_conflict: Color, - pub modal_background: Color, - pub modal_match_background: Color, - pub modal_match_background_active: Color, - pub modal_match_border: Color, - pub modal_match_text: Color, - pub modal_match_text_highlight: Color, -} - -#[derive(Clone, Default, Deserialize)] -#[serde(default)] -pub struct EditorTheme { - pub background: Color, - pub gutter_background: Color, - pub active_line_background: Color, - pub line_number: Color, - pub line_number_active: Color, - pub default_text: Color, - pub replicas: Vec, -} - -#[derive(Clone, Copy, Deserialize)] -pub struct ReplicaTheme { - pub cursor: Color, - pub selection: Color, -} - -#[derive(Clone, Copy, Default)] -pub struct Color(pub ColorU); - -#[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::new_with_theme(font_cache, Arc::new(Theme::default())) + } + + pub fn new_with_theme(font_cache: &FontCache, theme: Arc) -> Result { Ok(Self { buffer_font_family: font_cache.load_family(&["Fira Code", "Monaco"])?, buffer_font_size: 14.0, 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/dark.toml").unwrap()) - .expect("Failed to parse built-in theme"), - ), + theme, }) } @@ -100,275 +38,23 @@ impl Settings { } } -impl Theme { - pub fn parse(source: impl AsRef<[u8]>) -> Result { - #[derive(Deserialize)] - struct ThemeToml { - #[serde(default)] - ui: UiTheme, - #[serde(default)] - editor: EditorTheme, - #[serde(default)] - syntax: HashMap, - } - - #[derive(Deserialize)] - #[serde(untagged)] - enum StyleToml { - Color(Color), - Full { - color: Option, - weight: Option, - #[serde(default)] - italic: bool, - }, - } - - let theme_toml: ThemeToml = - toml::from_slice(source.as_ref()).context("failed to parse theme TOML")?; - - let mut syntax = Vec::<(String, ColorU, FontProperties)>::new(); - for (key, style) in theme_toml.syntax { - let (color, weight, italic) = match style { - StyleToml::Color(color) => (color, None, false), - StyleToml::Full { - color, - weight, - italic, - } => (color.unwrap_or(Color::default()), weight, italic), - }; - match syntax.binary_search_by_key(&&key, |e| &e.0) { - Ok(i) | Err(i) => { - let mut properties = FontProperties::new(); - properties.weight = deserialize_weight(weight)?; - if italic { - properties.style = FontStyle::Italic; - } - syntax.insert(i, (key, color.0, properties)); - } - } - } - - Ok(Theme { - ui: theme_toml.ui, - editor: theme_toml.editor, - syntax, - }) - } - - pub fn syntax_style(&self, id: StyleId) -> (ColorU, FontProperties) { - self.syntax.get(id.0 as usize).map_or( - (self.editor.default_text.0, 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 - } -} - -impl<'de> Deserialize<'de> for Color { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let rgba_value = u32::deserialize(deserializer)?; - Ok(Self(ColorU::from_u32((rgba_value << 8) + 0xFF))) - } -} - -impl Into for Color { - fn into(self) -> ColorU { - self.0 - } -} - -impl Deref for Color { - type Target = ColorU; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Color { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl fmt::Debug for Color { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl PartialEq for Color { - fn eq(&self, other: &ColorU) -> bool { - self.0.eq(other) - } -} - pub fn channel( font_cache: &FontCache, ) -> Result<(watch::Sender, watch::Receiver)> { Ok(watch::channel_with(Settings::new(font_cache)?)) } -fn deserialize_weight(weight: Option) -> Result { - match &weight { - None => return Ok(FontWeight::NORMAL), - Some(toml::Value::Integer(i)) => return Ok(FontWeight(*i as f32)), - Some(toml::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())) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_theme() { - let theme = Theme::parse( - r#" - [ui] - tab_background_active = 0x100000 - - [editor] - background = 0x00ed00 - line_number = 0xdddddd - - [syntax] - "beta.two" = 0xAABBCC - "alpha.one" = {color = 0x112233, weight = "bold"} - "gamma.three" = {weight = "light", italic = true} - "#, - ) - .unwrap(); - - assert_eq!(theme.ui.tab_background_active, ColorU::from_u32(0x100000ff)); - assert_eq!(theme.editor.background, ColorU::from_u32(0x00ed00ff)); - assert_eq!(theme.editor.line_number, ColorU::from_u32(0xddddddff)); - assert_eq!( - theme.syntax, - &[ - ( - "alpha.one".to_string(), - ColorU::from_u32(0x112233ff), - *FontProperties::new().weight(FontWeight::BOLD) - ), - ( - "beta.two".to_string(), - ColorU::from_u32(0xaabbccff), - *FontProperties::new().weight(FontWeight::NORMAL) - ), - ( - "gamma.three".to_string(), - ColorU::from_u32(0x00000000), - *FontProperties::new() - .weight(FontWeight::LIGHT) - .style(FontStyle::Italic), - ), - ] - ); - } - - #[test] - fn test_parse_empty_theme() { - Theme::parse("").unwrap(); - } - - #[test] - fn test_theme_map() { - let theme = Theme { - ui: Default::default(), - editor: Default::default(), - syntax: [ - ("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, 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") - ); - } +pub fn channel_with_themes( + font_cache: &FontCache, + themes: &ThemeRegistry, +) -> Result<(watch::Sender, watch::Receiver)> { + let theme = match themes.get(DEFAULT_THEME_NAME) { + Ok(theme) => theme, + Err(err) => { + panic!("failed to deserialize default theme: {:?}", err) + } + }; + Ok(watch::channel_with(Settings::new_with_theme( + font_cache, theme, + )?)) } diff --git a/zed/src/test.rs b/zed/src/test.rs index a3e5914b4ea94254a166b54a723b8da07795179b..e2ddff054ea1b288404bbcc4bfc75d462e540b43 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -1,5 +1,13 @@ -use crate::{fs::RealFs, language::LanguageRegistry, rpc, settings, time::ReplicaId, AppState}; +use crate::{ + fs::RealFs, + language::LanguageRegistry, + rpc, + settings::{self, ThemeRegistry}, + time::ReplicaId, + AppState, +}; use gpui::{AppContext, Entity, ModelHandle}; +use parking_lot::Mutex; use smol::channel; use std::{ marker::PhantomData, @@ -147,10 +155,13 @@ fn write_tree(path: &Path, tree: serde_json::Value) { } pub fn build_app_state(cx: &AppContext) -> Arc { - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let (settings_tx, settings) = settings::channel(&cx.font_cache()).unwrap(); let languages = Arc::new(LanguageRegistry::new()); + let themes = ThemeRegistry::new(()); Arc::new(AppState { + settings_tx: Arc::new(Mutex::new(settings_tx)), settings, + themes, languages: languages.clone(), rpc_router: Arc::new(ForegroundRouter::new()), rpc: rpc::Client::new(languages), diff --git a/zed/src/theme.rs b/zed/src/theme.rs new file mode 100644 index 0000000000000000000000000000000000000000..b1c09bf7c862fe1cc6e5384defafb223a46507a1 --- /dev/null +++ b/zed/src/theme.rs @@ -0,0 +1,626 @@ +use anyhow::{anyhow, Context, Result}; +use gpui::{ + color::Color, + elements::{ContainerStyle, LabelStyle}, + fonts::TextStyle, + AssetSource, +}; +use json::{Map, Value}; +use parking_lot::Mutex; +use serde::{Deserialize, Deserializer}; +use serde_json as json; +use std::{cmp::Ordering, collections::HashMap, sync::Arc}; + +const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); +pub const DEFAULT_THEME_NAME: &'static str = "dark"; + +pub struct ThemeRegistry { + assets: Box, + themes: Mutex>>, + theme_data: Mutex>>, +} + +#[derive(Clone, Debug)] +pub struct HighlightMap(Arc<[HighlightId]>); + +#[derive(Clone, Copy, Debug)] +pub struct HighlightId(u32); + +#[derive(Debug, Default, Deserialize)] +pub struct Theme { + #[serde(default)] + pub name: String, + pub ui: Ui, + pub editor: Editor, + #[serde(deserialize_with = "deserialize_syntax_theme")] + pub syntax: Vec<(String, TextStyle)>, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Ui { + pub background: Color, + pub tab: Tab, + pub active_tab: Tab, + pub selector: Selector, +} + +#[derive(Debug, Deserialize)] +pub struct Editor { + pub background: Color, + pub gutter_background: Color, + pub active_line_background: Color, + pub line_number: Color, + pub line_number_active: Color, + pub text: Color, + pub replicas: Vec, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +pub struct Replica { + pub cursor: Color, + pub selection: Color, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Tab { + #[serde(flatten)] + pub container: ContainerStyle, + #[serde(flatten)] + pub label: LabelStyle, + pub icon_close: Color, + pub icon_dirty: Color, + pub icon_conflict: Color, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Selector { + #[serde(flatten)] + pub container: ContainerStyle, + #[serde(flatten)] + pub label: LabelStyle, + + pub item: SelectorItem, + pub active_item: SelectorItem, +} + +#[derive(Debug, Default, Deserialize)] +pub struct SelectorItem { + #[serde(flatten)] + pub container: ContainerStyle, + #[serde(flatten)] + pub label: LabelStyle, +} + +impl Default for Editor { + fn default() -> Self { + Self { + background: Default::default(), + gutter_background: Default::default(), + active_line_background: Default::default(), + line_number: Default::default(), + line_number_active: Default::default(), + text: Default::default(), + replicas: vec![Replica::default()], + } + } +} + +impl ThemeRegistry { + pub fn new(source: impl AssetSource) -> Arc { + Arc::new(Self { + assets: Box::new(source), + themes: Default::default(), + theme_data: Default::default(), + }) + } + + pub fn list(&self) -> impl Iterator { + 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 clear(&self) { + self.theme_data.lock().clear(); + self.themes.lock().clear(); + } + + pub fn get(&self, name: &str) -> Result> { + if let Some(theme) = self.themes.lock().get(name) { + return Ok(theme.clone()); + } + + let theme_data = self.load(name)?; + let mut theme = serde_json::from_value::(theme_data.as_ref().clone())?; + theme.name = name.into(); + let theme = Arc::new(theme); + self.themes.lock().insert(name.to_string(), theme.clone()); + Ok(theme) + } + + fn load(&self, name: &str) -> Result> { + 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_data: Map = toml::from_slice(source_code.as_ref()) + .with_context(|| format!("failed to parse {}.toml", name))?; + + // If this theme extends another base theme, deeply merge it into the base theme's data + if let Some(base_name) = theme_data + .get("extends") + .and_then(|name| name.as_str()) + .map(str::to_string) + { + let base_theme_data = self + .load(&base_name) + .with_context(|| format!("failed to load base theme {}", base_name))? + .as_ref() + .clone(); + if let Value::Object(mut base_theme_object) = base_theme_data { + deep_merge_json(&mut base_theme_object, theme_data); + theme_data = base_theme_object; + } + } + + // Evaluate `extends` fields in styles + // First, find the key paths of all objects with `extends` directives + let mut directives = Vec::new(); + let mut key_path = Vec::new(); + for (key, value) in theme_data.iter() { + if value.is_array() || value.is_object() { + key_path.push(Key::Object(key.clone())); + find_extensions(value, &mut key_path, &mut directives); + key_path.pop(); + } + } + // If you extend something with an extend directive, process the source's extend directive first + directives.sort_unstable(); + + // Now update objects to include the fields of objects they extend + for ExtendDirective { + source_path, + target_path, + } in directives + { + let source = value_at(&mut theme_data, &source_path)?.clone(); + let target = value_at(&mut theme_data, &target_path)?; + if let (Value::Object(mut source_object), Value::Object(target_object)) = + (source, target.take()) + { + deep_merge_json(&mut source_object, target_object); + *target = Value::Object(source_object); + } + } + + // Evaluate any variables + if let Some((key, variables)) = theme_data.remove_entry("variables") { + if let Some(variables) = variables.as_object() { + for value in theme_data.values_mut() { + evaluate_variables(value, &variables, &mut Vec::new())?; + } + } + theme_data.insert(key, variables); + } + + let result = Arc::new(Value::Object(theme_data)); + self.theme_data + .lock() + .insert(name.to_string(), result.clone()); + + Ok(result) + } +} + +impl Theme { + pub fn highlight_style(&self, id: HighlightId) -> TextStyle { + self.syntax + .get(id.0 as usize) + .map(|entry| entry.1.clone()) + .unwrap_or_else(|| TextStyle { + color: self.editor.text, + font_properties: Default::default(), + }) + } + + #[cfg(test)] + pub fn highlight_name(&self, id: HighlightId) -> Option<&str> { + self.syntax.get(id.0 as usize).map(|e| e.0.as_str()) + } +} + +impl HighlightMap { + 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. + HighlightMap( + 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_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32)) + }) + .collect(), + ) + } + + pub fn get(&self, capture_id: u32) -> HighlightId { + self.0 + .get(capture_id as usize) + .copied() + .unwrap_or(DEFAULT_HIGHLIGHT_ID) + } +} + +impl Default for HighlightMap { + fn default() -> Self { + Self(Arc::new([])) + } +} + +impl Default for HighlightId { + fn default() -> Self { + DEFAULT_HIGHLIGHT_ID + } +} + +fn deep_merge_json(base: &mut Map, extension: Map) { + for (key, extension_value) in extension { + if let Value::Object(extension_object) = extension_value { + if let Some(base_object) = base.get_mut(&key).and_then(|value| value.as_object_mut()) { + deep_merge_json(base_object, extension_object); + } else { + base.insert(key, Value::Object(extension_object)); + } + } else { + base.insert(key, extension_value); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum Key { + Array(usize), + Object(String), +} + +#[derive(Debug, PartialEq, Eq)] +struct ExtendDirective { + source_path: Vec, + target_path: Vec, +} + +impl Ord for ExtendDirective { + fn cmp(&self, other: &Self) -> Ordering { + if self.target_path.starts_with(&other.source_path) + || other.source_path.starts_with(&self.target_path) + { + Ordering::Less + } else if other.target_path.starts_with(&self.source_path) + || self.source_path.starts_with(&other.target_path) + { + Ordering::Greater + } else { + Ordering::Equal + } + } +} + +impl PartialOrd for ExtendDirective { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +fn find_extensions(value: &Value, key_path: &mut Vec, directives: &mut Vec) { + match value { + Value::Array(vec) => { + for (ix, value) in vec.iter().enumerate() { + key_path.push(Key::Array(ix)); + find_extensions(value, key_path, directives); + key_path.pop(); + } + } + Value::Object(map) => { + for (key, value) in map.iter() { + if key == "extends" { + if let Some(source_path) = value.as_str() { + directives.push(ExtendDirective { + source_path: source_path + .split(".") + .map(|key| Key::Object(key.to_string())) + .collect(), + target_path: key_path.clone(), + }); + } + } else if value.is_array() || value.is_object() { + key_path.push(Key::Object(key.to_string())); + find_extensions(value, key_path, directives); + key_path.pop(); + } + } + } + _ => {} + } +} + +fn value_at<'a>(object: &'a mut Map, key_path: &Vec) -> Result<&'a mut Value> { + let mut key_path = key_path.iter(); + if let Some(Key::Object(first_key)) = key_path.next() { + let mut cur_value = object.get_mut(first_key); + for key in key_path { + if let Some(value) = cur_value { + match key { + Key::Array(ix) => cur_value = value.get_mut(ix), + Key::Object(key) => cur_value = value.get_mut(key), + } + } else { + return Err(anyhow!("invalid key path")); + } + } + cur_value.ok_or_else(|| anyhow!("invalid key path")) + } else { + Err(anyhow!("invalid key path")) + } +} + +fn evaluate_variables( + value: &mut Value, + variables: &Map, + stack: &mut Vec, +) -> Result<()> { + match value { + 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() { + *value = definition; + evaluate_variables(value, 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 +} + +pub fn deserialize_syntax_theme<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let mut result = Vec::<(String, TextStyle)>::new(); + + let syntax_data: HashMap = Deserialize::deserialize(deserializer)?; + for (key, style) in syntax_data { + match result.binary_search_by(|(needle, _)| needle.cmp(&key)) { + Ok(i) | Err(i) => { + result.insert(i, (key, style)); + } + } + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use crate::assets::Assets; + + use super::*; + + #[test] + fn test_bundled_themes() { + let registry = ThemeRegistry::new(Assets); + let mut has_default_theme = false; + for theme_name in registry.list() { + let theme = registry.get(&theme_name).unwrap(); + if theme.name == DEFAULT_THEME_NAME { + has_default_theme = true; + } + assert_eq!(theme.name, theme_name); + } + assert!(has_default_theme); + } + + #[test] + fn test_theme_extension() { + let assets = TestAssets(&[ + ( + "themes/_base.toml", + r##" + [ui.active_tab] + extends = "ui.tab" + border.color = "#666666" + text = "$bright_text" + + [ui.tab] + extends = "ui.element" + text = "$dull_text" + + [ui.element] + background = "#111111" + border = {width = 2.0, color = "#00000000"} + + [editor] + background = "#222222" + default_text = "$regular_text" + "##, + ), + ( + "themes/light.toml", + r##" + extends = "_base" + + [variables] + bright_text = "#ffffff" + regular_text = "#eeeeee" + dull_text = "#dddddd" + + [editor] + background = "#232323" + "##, + ), + ]); + + let registry = ThemeRegistry::new(assets); + let theme_data = registry.load("light").unwrap(); + assert_eq!( + theme_data.as_ref(), + &serde_json::json!({ + "ui": { + "active_tab": { + "background": "#111111", + "border": { + "width": 2.0, + "color": "#666666" + }, + "extends": "ui.tab", + "text": "#ffffff" + }, + "tab": { + "background": "#111111", + "border": { + "width": 2.0, + "color": "#00000000" + }, + "extends": "ui.element", + "text": "#dddddd" + }, + "element": { + "background": "#111111", + "border": { + "width": 2.0, + "color": "#00000000" + } + } + }, + "editor": { + "background": "#232323", + "default_text": "#eeeeee" + }, + "extends": "_base", + "variables": { + "bright_text": "#ffffff", + "regular_text": "#eeeeee", + "dull_text": "#dddddd" + } + }) + ); + } + + #[test] + fn test_highlight_map() { + let theme = Theme { + name: "test".into(), + 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(|(name, color)| (name.to_string(), (*color).into())) + .collect(), + }; + + let capture_names = &[ + "function.special".to_string(), + "function.async.rust".to_string(), + "variable.builtin.self".to_string(), + ]; + + let map = HighlightMap::new(capture_names, &theme); + assert_eq!(theme.highlight_name(map.get(0)), Some("function")); + assert_eq!(theme.highlight_name(map.get(1)), Some("function.async")); + assert_eq!(theme.highlight_name(map.get(2)), Some("variable.builtin")); + } + + struct TestAssets(&'static [(&'static str, &'static str)]); + + impl AssetSource for TestAssets { + fn load(&self, path: &str) -> Result> { + 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> { + self.0 + .iter() + .copied() + .filter_map(|(path, _)| { + if path.starts_with(prefix) { + Some(path.into()) + } else { + None + } + }) + .collect() + } + } +} diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs new file mode 100644 index 0000000000000000000000000000000000000000..7b4456fd39f75ab91cbf9efa59ffaf50cb764d73 --- /dev/null +++ b/zed/src/theme_selector.rs @@ -0,0 +1,306 @@ +use std::{cmp, sync::Arc}; + +use crate::{ + editor::{self, Editor}, + fuzzy::{match_strings, StringMatch, StringMatchCandidate}, + settings::ThemeRegistry, + workspace::Workspace, + AppState, Settings, +}; +use gpui::{ + elements::{ + Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement, + UniformList, UniformListState, + }, + keymap::{self, Binding}, + AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, + ViewContext, ViewHandle, +}; +use parking_lot::Mutex; +use postage::watch; + +pub struct ThemeSelector { + settings_tx: Arc>>, + settings: watch::Receiver, + registry: Arc, + matches: Vec, + query_buffer: ViewHandle, + list_state: UniformListState, + selected_index: usize, +} + +pub fn init(cx: &mut MutableAppContext, app_state: &Arc) { + cx.add_action("theme_selector:confirm", ThemeSelector::confirm); + cx.add_action("menu:select_prev", ThemeSelector::select_prev); + cx.add_action("menu:select_next", ThemeSelector::select_next); + cx.add_action("theme_selector:toggle", ThemeSelector::toggle); + cx.add_action("theme_selector:reload", ThemeSelector::reload); + + cx.add_bindings(vec![ + Binding::new("cmd-k cmd-t", "theme_selector:toggle", None).with_arg(app_state.clone()), + Binding::new("cmd-k t", "theme_selector:reload", None).with_arg(app_state.clone()), + Binding::new("escape", "theme_selector:toggle", Some("ThemeSelector")) + .with_arg(app_state.clone()), + Binding::new("enter", "theme_selector:confirm", Some("ThemeSelector")), + ]); +} + +pub enum Event { + Dismissed, +} + +impl ThemeSelector { + fn new( + settings_tx: Arc>>, + settings: watch::Receiver, + registry: Arc, + cx: &mut ViewContext, + ) -> Self { + let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx)); + cx.subscribe_to_view(&query_buffer, Self::on_query_editor_event); + + let mut this = Self { + settings, + settings_tx, + registry, + query_buffer, + matches: Vec::new(), + list_state: Default::default(), + selected_index: 0, + }; + this.update_matches(cx); + this + } + + fn toggle( + workspace: &mut Workspace, + app_state: &Arc, + cx: &mut ViewContext, + ) { + workspace.toggle_modal(cx, |cx, _| { + let selector = cx.add_view(|cx| { + Self::new( + app_state.settings_tx.clone(), + app_state.settings.clone(), + app_state.themes.clone(), + cx, + ) + }); + cx.subscribe_to_view(&selector, Self::on_event); + selector + }); + } + + fn reload(_: &mut Workspace, app_state: &Arc, cx: &mut ViewContext) { + let current_theme_name = app_state.settings.borrow().theme.name.clone(); + app_state.themes.clear(); + match app_state.themes.get(¤t_theme_name) { + Ok(theme) => { + cx.notify_all(); + app_state.settings_tx.lock().borrow_mut().theme = theme; + } + Err(error) => { + log::error!("failed to load theme {}: {:?}", current_theme_name, error) + } + } + } + + fn confirm(&mut self, _: &(), cx: &mut ViewContext) { + if let Some(mat) = self.matches.get(self.selected_index) { + if let Ok(theme) = self.registry.get(&mat.string) { + self.settings_tx.lock().borrow_mut().theme = theme; + cx.notify_all(); + cx.emit(Event::Dismissed); + } + } + } + + fn select_prev(&mut self, _: &(), cx: &mut ViewContext) { + if self.selected_index > 0 { + self.selected_index -= 1; + } + self.list_state.scroll_to(self.selected_index); + cx.notify(); + } + + fn select_next(&mut self, _: &(), cx: &mut ViewContext) { + if self.selected_index + 1 < self.matches.len() { + self.selected_index += 1; + } + self.list_state.scroll_to(self.selected_index); + cx.notify(); + } + + // fn select(&mut self, selected_index: &usize, cx: &mut ViewContext) { + // self.selected_index = *selected_index; + // self.confirm(&(), cx); + // } + + fn update_matches(&mut self, cx: &mut ViewContext) { + let background = cx.background().clone(); + let candidates = self + .registry + .list() + .map(|name| StringMatchCandidate { + char_bag: name.as_str().into(), + string: name, + }) + .collect::>(); + let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx)); + + self.matches = if query.is_empty() { + candidates + .into_iter() + .map(|candidate| StringMatch { + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + smol::block_on(match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + )) + }; + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => { + workspace.dismiss_modal(cx); + } + } + } + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => self.update_matches(cx), + editor::Event::Blurred => cx.emit(Event::Dismissed), + _ => {} + } + } + + fn render_matches(&self, cx: &RenderContext) -> ElementBox { + if self.matches.is_empty() { + let settings = self.settings.borrow(); + return Container::new( + Label::new( + "No matches".into(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_style(&settings.theme.ui.selector.label) + .boxed(), + ) + .with_margin_top(6.0) + .named("empty matches"); + } + + let handle = cx.handle(); + let list = UniformList::new( + self.list_state.clone(), + self.matches.len(), + move |mut range, items, cx| { + let cx = cx.as_ref(); + let selector = handle.upgrade(cx).unwrap(); + let selector = selector.read(cx); + let start = range.start; + range.end = cmp::min(range.end, selector.matches.len()); + items.extend( + selector.matches[range] + .iter() + .enumerate() + .map(move |(i, path_match)| selector.render_match(path_match, start + i)), + ); + }, + ); + + Container::new(list.boxed()) + .with_margin_top(6.0) + .named("matches") + } + + fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox { + let settings = self.settings.borrow(); + let theme = &settings.theme.ui; + + let container = Container::new( + Label::new( + theme_match.string.clone(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_style(if index == self.selected_index { + &theme.selector.active_item.label + } else { + &theme.selector.item.label + }) + .with_highlights(theme_match.positions.clone()) + .boxed(), + ) + .with_style(if index == self.selected_index { + &theme.selector.active_item.container + } else { + &theme.selector.item.container + }); + + container.boxed() + } +} + +impl Entity for ThemeSelector { + type Event = Event; +} + +impl View for ThemeSelector { + fn ui_name() -> &'static str { + "ThemeSelector" + } + + fn render(&self, cx: &RenderContext) -> ElementBox { + let settings = self.settings.borrow(); + + Align::new( + ConstrainedBox::new( + Container::new( + Flex::new(Axis::Vertical) + .with_child(ChildView::new(self.query_buffer.id()).boxed()) + .with_child(Expanded::new(1.0, self.render_matches(cx)).boxed()) + .boxed(), + ) + .with_style(&settings.theme.ui.selector.container) + .boxed(), + ) + .with_max_width(600.0) + .with_max_height(400.0) + .boxed(), + ) + .top() + .named("theme selector") + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_buffer); + } + + fn keymap_context(&self, _: &AppContext) -> keymap::Context { + let mut cx = Self::default_keymap_context(); + cx.set.insert("menu".into()); + cx + } +} diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 682206f5e6ece1d97fbb16d5d8f881ce54c27f1e..cb543522d8d348e9dd1b39eb2034ddb3974ceb33 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -13,8 +13,8 @@ use crate::{ use anyhow::{anyhow, Result}; use gpui::{ elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, ClipboardItem, - Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, Task, View, - ViewContext, ViewHandle, WeakModelHandle, + Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, + View, ViewContext, ViewHandle, WeakModelHandle, }; use log::error; pub use pane::*; @@ -879,7 +879,7 @@ impl View for Workspace { "Workspace" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { let settings = self.settings.borrow(); Container::new( Stack::new() @@ -887,7 +887,7 @@ impl View for Workspace { .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed())) .boxed(), ) - .with_background_color(settings.theme.editor.background) + .with_background_color(settings.theme.ui.background) .named("workspace") } @@ -911,7 +911,7 @@ impl WorkspaceHandle for ViewHandle { let tree_id = tree.id(); tree.read(cx) .files(0) - .map(move |f| (tree_id, f.path().clone())) + .map(move |f| (tree_id, f.path.clone())) }) .collect::>() } @@ -974,8 +974,8 @@ mod tests { }) .await; assert_eq!(cx.window_ids().len(), 1); - let workspace_view_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); - workspace_view_1.read_with(&cx, |workspace, _| { + let workspace_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); + workspace_1.read_with(&cx, |workspace, _| { assert_eq!(workspace.worktrees().len(), 2) }); @@ -1397,9 +1397,9 @@ mod tests { assert_eq!(pane2_item.entry_id(cx.as_ref()), Some(file1.clone())); cx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); - let workspace_view = workspace.read(cx); - assert_eq!(workspace_view.panes.len(), 1); - assert_eq!(workspace_view.active_pane(), &pane_1); + let workspace = workspace.read(cx); + assert_eq!(workspace.panes.len(), 1); + assert_eq!(workspace.active_pane(), &pane_1); }); } } diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 2b152a6996ac5b4e8ccfa323a518f638083fa6ba..511e48dfd41dc4e7a5a5b5382e82208477606525 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -1,11 +1,12 @@ use super::{ItemViewHandle, SplitDirection}; -use crate::settings::{Settings, UiTheme}; +use crate::{settings::Settings, theme}; use gpui::{ - color::ColorU, + color::Color, elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, - AppContext, Border, Entity, MutableAppContext, Quad, View, ViewContext, ViewHandle, + AppContext, Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, + ViewHandle, }; use postage::watch; use std::{cmp, path::Path, sync::Arc}; @@ -192,6 +193,7 @@ impl Pane { let is_active = ix == self.active_item; enum Tab {} + let border = &theme.tab.container.border; row.add_child( Expanded::new( @@ -199,10 +201,10 @@ impl Pane { MouseEventHandler::new::(item.id(), cx, |mouse_state| { let title = item.title(cx); - let mut border = Border::new(1.0, theme.tab_border.0); + let mut border = border.clone(); border.left = ix > 0; border.right = ix == last_item_ix; - border.bottom = ix != self.active_item; + border.bottom = !is_active; let mut container = Container::new( Stack::new() @@ -213,10 +215,10 @@ impl Pane { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(if is_active { - theme.tab_text_active.0 + .with_style(if is_active { + &theme.active_tab.label } else { - theme.tab_text.0 + &theme.tab.label }) .boxed(), ) @@ -237,15 +239,15 @@ impl Pane { ) .boxed(), ) - .with_horizontal_padding(10.) + .with_style(if is_active { + &theme.active_tab.container + } else { + &theme.tab.container + }) .with_border(border); if is_active { - container = container - .with_background_color(theme.tab_background_active) - .with_padding_bottom(border.width); - } else { - container = container.with_background_color(theme.tab_background); + container = container.with_padding_bottom(border.width); } ConstrainedBox::new( @@ -268,10 +270,13 @@ impl Pane { // Ensure there's always a minimum amount of space after the last tab, // so that the tab's border doesn't abut the window's border. + let mut border = Border::bottom(1.0, Color::default()); + border.color = theme.tab.container.border.color; + row.add_child( ConstrainedBox::new( Container::new(Empty::new().boxed()) - .with_border(Border::bottom(1.0, theme.tab_border)) + .with_border(border) .boxed(), ) .with_min_width(20.) @@ -282,7 +287,7 @@ impl Pane { Expanded::new( 0.0, Container::new(Empty::new().boxed()) - .with_border(Border::bottom(1.0, theme.tab_border)) + .with_border(border) .boxed(), ) .named("filler"), @@ -299,33 +304,33 @@ impl Pane { tab_hovered: bool, is_dirty: bool, has_conflict: bool, - theme: &UiTheme, + theme: &theme::Ui, cx: &AppContext, ) -> ElementBox { enum TabCloseButton {} - let mut clicked_color = theme.tab_icon_dirty; + let mut clicked_color = theme.tab.icon_dirty; clicked_color.a = 180; let current_color = if has_conflict { - Some(theme.tab_icon_conflict) + Some(theme.tab.icon_conflict) } else if is_dirty { - Some(theme.tab_icon_dirty) + Some(theme.tab.icon_dirty) } else { None }; let icon = if tab_hovered { - let close_color = current_color.unwrap_or(theme.tab_icon_close).0; + let close_color = current_color.unwrap_or(theme.tab.icon_close); let icon = Svg::new("icons/x.svg").with_color(close_color); MouseEventHandler::new::(item_id, cx, |mouse_state| { if mouse_state.hovered { - Container::new(icon.with_color(ColorU::white()).boxed()) + Container::new(icon.with_color(Color::white()).boxed()) .with_background_color(if mouse_state.clicked { clicked_color } else { - theme.tab_icon_dirty + theme.tab.icon_dirty }) .with_corner_radius(close_icon_size / 2.) .boxed() @@ -343,7 +348,7 @@ impl Pane { let square = RectF::new(bounds.origin(), vec2f(diameter, diameter)); cx.scene.push_quad(Quad { bounds: square, - background: Some(current_color.0), + background: Some(current_color), border: Default::default(), corner_radius: diameter / 2., }); @@ -371,7 +376,7 @@ impl View for Pane { "Pane" } - fn render<'a>(&self, cx: &AppContext) -> ElementBox { + fn render<'a>(&self, cx: &RenderContext) -> ElementBox { if let Some(active_item) = self.active_item() { Flex::column() .with_child(self.render_tabs(cx)) diff --git a/zed/src/workspace/pane_group.rs b/zed/src/workspace/pane_group.rs index 46a00b3f2a0e63b7adde8fb44a1ed5b514a65da3..d5d0040009fba87bfd9bc0bb29acb6a533457336 100644 --- a/zed/src/workspace/pane_group.rs +++ b/zed/src/workspace/pane_group.rs @@ -1,9 +1,5 @@ use anyhow::{anyhow, Result}; -use gpui::{ - color::{rgbu, ColorU}, - elements::*, - Axis, Border, -}; +use gpui::{color::Color, elements::*, Axis, Border}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct PaneGroup { @@ -388,6 +384,6 @@ fn border_width() -> f32 { } #[inline(always)] -fn border_color() -> ColorU { - rgbu(0xdb, 0xdb, 0xdc) +fn border_color() -> Color { + Color::new(0xdb, 0xdb, 0xdc, 0xff) } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 03032c91169fa87dc2f741b48a81fcb02980fa72..f8566cd8c40b67fcca79d6401f1476058fbea620 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1,11 +1,11 @@ -mod char_bag; -mod fuzzy; mod ignore; -use self::{char_bag::CharBag, ignore::IgnoreStack}; +use self::ignore::IgnoreStack; use crate::{ editor::{self, Buffer, History, Operation, Rope}, fs::{self, Fs}, + fuzzy, + fuzzy::CharBag, language::LanguageRegistry, rpc::{self, proto}, sum_tree::{self, Cursor, Edit, SumTree}, @@ -1116,6 +1116,10 @@ pub struct Snapshot { } impl Snapshot { + pub fn id(&self) -> usize { + self.id + } + pub fn build_update(&self, other: &Self, worktree_id: u64) -> proto::UpdateWorktree { let mut updated_entries = Vec::new(); let mut removed_entries = Vec::new(); @@ -1214,7 +1218,7 @@ impl Snapshot { self.entries_by_path .cursor::<(), ()>() .filter(move |entry| entry.path.as_ref() != empty_path) - .map(|entry| entry.path()) + .map(|entry| &entry.path) } pub fn visible_files(&self, start: usize) -> FileIter { @@ -1248,17 +1252,17 @@ impl Snapshot { } pub fn inode_for_path(&self, path: impl AsRef) -> Option { - self.entry_for_path(path.as_ref()).map(|e| e.inode()) + self.entry_for_path(path.as_ref()).map(|e| e.inode) } fn insert_entry(&mut self, mut entry: Entry) -> Entry { - if !entry.is_dir() && entry.path().file_name() == Some(&GITIGNORE) { - let (ignore, err) = Gitignore::new(self.abs_path.join(entry.path())); + if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) { + let (ignore, err) = Gitignore::new(self.abs_path.join(&entry.path)); if let Some(err) = err { - log::error!("error in ignore file {:?} - {:?}", entry.path(), err); + log::error!("error in ignore file {:?} - {:?}", &entry.path, err); } - let ignore_dir_path = entry.path().parent().unwrap(); + let ignore_dir_path = entry.path.parent().unwrap(); self.ignores .insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id)); } @@ -1381,10 +1385,10 @@ impl Snapshot { impl fmt::Debug for Snapshot { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for entry in self.entries_by_path.cursor::<(), ()>() { - for _ in entry.path().ancestors().skip(1) { + for _ in entry.path.ancestors().skip(1) { write!(f, " ")?; } - writeln!(f, "{:?} (inode: {})", entry.path(), entry.inode())?; + writeln!(f, "{:?} (inode: {})", entry.path, entry.inode)?; } Ok(()) } @@ -1535,19 +1539,19 @@ impl File { } pub fn entry_id(&self) -> (usize, Arc) { - (self.worktree.id(), self.path()) + (self.worktree.id(), self.path.clone()) } } #[derive(Clone, Debug)] pub struct Entry { - id: usize, - kind: EntryKind, - path: Arc, - inode: u64, - mtime: SystemTime, - is_symlink: bool, - is_ignored: bool, + pub id: usize, + pub kind: EntryKind, + pub path: Arc, + pub inode: u64, + pub mtime: SystemTime, + pub is_symlink: bool, + pub is_ignored: bool, } #[derive(Clone, Debug)] @@ -1579,23 +1583,11 @@ impl Entry { } } - pub fn path(&self) -> &Arc { - &self.path - } - - pub fn inode(&self) -> u64 { - self.inode - } - - pub fn is_ignored(&self) -> bool { - self.is_ignored - } - - fn is_dir(&self) -> bool { + pub fn is_dir(&self) -> bool { matches!(self.kind, EntryKind::Dir | EntryKind::PendingDir) } - fn is_file(&self) -> bool { + pub fn is_file(&self) -> bool { matches!(self.kind, EntryKind::File(_)) } } @@ -1619,7 +1611,7 @@ impl sum_tree::Item for Entry { } EntrySummary { - max_path: self.path().clone(), + max_path: self.path.clone(), file_count, visible_file_count, } @@ -1630,7 +1622,7 @@ impl sum_tree::KeyedItem for Entry { type Key = PathKey; fn key(&self) -> Self::Key { - PathKey(self.path().clone()) + PathKey(self.path.clone()) } } @@ -2147,7 +2139,7 @@ impl BackgroundScanner { let mut edits = Vec::new(); for mut entry in snapshot.child_entries(&job.path).cloned() { let was_ignored = entry.is_ignored; - entry.is_ignored = ignore_stack.is_path_ignored(entry.path(), entry.is_dir()); + entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir()); if entry.is_dir() { let child_ignore_stack = if entry.is_ignored { IgnoreStack::all() @@ -2156,7 +2148,7 @@ impl BackgroundScanner { }; job.ignore_queue .send(UpdateIgnoreStatusJob { - path: entry.path().clone(), + path: entry.path.clone(), ignore_stack: child_ignore_stack, ignore_queue: job.ignore_queue.clone(), }) @@ -2333,9 +2325,9 @@ impl<'a> Iterator for ChildEntriesIter<'a> { fn next(&mut self) -> Option { if let Some(item) = self.cursor.item() { - if item.path().starts_with(self.parent_path) { + if item.path.starts_with(self.parent_path) { self.cursor - .seek_forward(&PathSearch::Successor(item.path()), Bias::Left, &()); + .seek_forward(&PathSearch::Successor(&item.path), Bias::Left, &()); Some(item) } else { None @@ -2608,6 +2600,7 @@ mod tests { ); tree.snapshot() })]; + let cancel_flag = Default::default(); let results = cx .read(|cx| { match_paths( @@ -2616,7 +2609,7 @@ mod tests { false, false, 10, - Default::default(), + &cancel_flag, cx.background().clone(), ) }) @@ -2659,6 +2652,7 @@ mod tests { assert_eq!(tree.file_count(), 0); tree.snapshot() })]; + let cancel_flag = Default::default(); let results = cx .read(|cx| { match_paths( @@ -2667,7 +2661,7 @@ mod tests { false, false, 10, - Default::default(), + &cancel_flag, cx.background().clone(), ) }) @@ -2928,8 +2922,8 @@ mod tests { let tree = tree.read(cx); let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap(); let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap(); - assert_eq!(tracked.is_ignored(), false); - assert_eq!(ignored.is_ignored(), true); + assert_eq!(tracked.is_ignored, false); + assert_eq!(ignored.is_ignored, true); }); std::fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); @@ -2940,9 +2934,9 @@ mod tests { let dot_git = tree.entry_for_path(".git").unwrap(); let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap(); let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap(); - assert_eq!(tracked.is_ignored(), false); - assert_eq!(ignored.is_ignored(), true); - assert_eq!(dot_git.is_ignored(), true); + assert_eq!(tracked.is_ignored, false); + assert_eq!(ignored.is_ignored, true); + assert_eq!(dot_git.is_ignored, true); }); } @@ -3175,9 +3169,9 @@ mod tests { let mut visible_files = self.visible_files(0); for entry in self.entries_by_path.cursor::<(), ()>() { if entry.is_file() { - assert_eq!(files.next().unwrap().inode(), entry.inode); + assert_eq!(files.next().unwrap().inode, entry.inode); if !entry.is_ignored { - assert_eq!(visible_files.next().unwrap().inode(), entry.inode); + assert_eq!(visible_files.next().unwrap().inode, entry.inode); } } } @@ -3190,14 +3184,14 @@ mod tests { bfs_paths.push(path); let ix = stack.len(); for child_entry in self.child_entries(path) { - stack.insert(ix, child_entry.path()); + stack.insert(ix, &child_entry.path); } } let dfs_paths = self .entries_by_path .cursor::<(), ()>() - .map(|e| e.path().as_ref()) + .map(|e| e.path.as_ref()) .collect::>(); assert_eq!(bfs_paths, dfs_paths); @@ -3212,7 +3206,7 @@ mod tests { fn to_vec(&self) -> Vec<(&Path, u64, bool)> { let mut paths = Vec::new(); for entry in self.entries_by_path.cursor::<(), ()>() { - paths.push((entry.path().as_ref(), entry.inode(), entry.is_ignored())); + paths.push((entry.path.as_ref(), entry.inode, entry.is_ignored)); } paths.sort_by(|a, b| a.0.cmp(&b.0)); paths diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs deleted file mode 100644 index 8018aa829dbe1ca0b0ca23132a1451beb737f760..0000000000000000000000000000000000000000 --- a/zed/src/worktree/fuzzy.rs +++ /dev/null @@ -1,659 +0,0 @@ -use super::{char_bag::CharBag, EntryKind, Snapshot}; -use crate::util; -use gpui::executor; -use std::{ - cmp::{max, min, Ordering}, - path::Path, - sync::atomic::{self, AtomicBool}, - sync::Arc, -}; - -const BASE_DISTANCE_PENALTY: f64 = 0.6; -const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; -const MIN_DISTANCE_PENALTY: f64 = 0.2; - -#[derive(Clone, Debug)] -pub struct MatchCandidate<'a> { - pub path: &'a Arc, - pub char_bag: CharBag, -} - -#[derive(Clone, Debug)] -pub struct PathMatch { - pub score: f64, - pub positions: Vec, - pub tree_id: usize, - pub path: Arc, - pub path_prefix: Arc, -} - -impl PartialEq for PathMatch { - fn eq(&self, other: &Self) -> bool { - self.score.eq(&other.score) - } -} - -impl Eq for PathMatch {} - -impl PartialOrd for PathMatch { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for PathMatch { - fn cmp(&self, other: &Self) -> Ordering { - self.score - .partial_cmp(&other.score) - .unwrap_or(Ordering::Equal) - .then_with(|| self.tree_id.cmp(&other.tree_id)) - .then_with(|| Arc::as_ptr(&self.path).cmp(&Arc::as_ptr(&other.path))) - } -} - -pub async fn match_paths( - snapshots: &[Snapshot], - query: &str, - include_ignored: bool, - smart_case: bool, - max_results: usize, - cancel_flag: Arc, - background: Arc, -) -> Vec { - let path_count: usize = if include_ignored { - snapshots.iter().map(Snapshot::file_count).sum() - } else { - snapshots.iter().map(Snapshot::visible_file_count).sum() - }; - if path_count == 0 { - return Vec::new(); - } - - let lowercase_query = query.to_lowercase().chars().collect::>(); - let query = query.chars().collect::>(); - - let lowercase_query = &lowercase_query; - let query = &query; - let query_chars = CharBag::from(&lowercase_query[..]); - - let num_cpus = background.num_cpus().min(path_count); - let segment_size = (path_count + num_cpus - 1) / num_cpus; - let mut segment_results = (0..num_cpus) - .map(|_| Vec::with_capacity(max_results)) - .collect::>(); - - background - .scoped(|scope| { - for (segment_idx, results) in segment_results.iter_mut().enumerate() { - let cancel_flag = &cancel_flag; - scope.spawn(async move { - let segment_start = segment_idx * segment_size; - let segment_end = segment_start + segment_size; - - let mut min_score = 0.0; - let mut last_positions = Vec::new(); - last_positions.resize(query.len(), 0); - let mut match_positions = Vec::new(); - match_positions.resize(query.len(), 0); - let mut score_matrix = Vec::new(); - let mut best_position_matrix = Vec::new(); - - let mut tree_start = 0; - for snapshot in snapshots { - let tree_end = if include_ignored { - tree_start + snapshot.file_count() - } else { - tree_start + snapshot.visible_file_count() - }; - - if tree_start < segment_end && segment_start < tree_end { - let path_prefix: Arc = - if snapshot.root_entry().map_or(false, |e| e.is_file()) { - snapshot.root_name().into() - } else if snapshots.len() > 1 { - format!("{}/", snapshot.root_name()).into() - } else { - "".into() - }; - - let start = max(tree_start, segment_start) - tree_start; - let end = min(tree_end, segment_end) - tree_start; - let entries = if include_ignored { - snapshot.files(start).take(end - start) - } else { - snapshot.visible_files(start).take(end - start) - }; - let paths = entries.map(|entry| { - if let EntryKind::File(char_bag) = entry.kind { - MatchCandidate { - path: &entry.path, - char_bag, - } - } else { - unreachable!() - } - }); - - match_single_tree_paths( - snapshot, - path_prefix, - paths, - query, - lowercase_query, - query_chars, - smart_case, - results, - max_results, - &mut min_score, - &mut match_positions, - &mut last_positions, - &mut score_matrix, - &mut best_position_matrix, - &cancel_flag, - ); - } - if tree_end >= segment_end { - break; - } - tree_start = tree_end; - } - }) - } - }) - .await; - - let mut results = Vec::new(); - for segment_result in segment_results { - if results.is_empty() { - results = segment_result; - } else { - util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(&a)); - } - } - results -} - -fn match_single_tree_paths<'a>( - snapshot: &Snapshot, - path_prefix: Arc, - path_entries: impl Iterator>, - query: &[char], - lowercase_query: &[char], - query_chars: CharBag, - smart_case: bool, - results: &mut Vec, - max_results: usize, - min_score: &mut f64, - match_positions: &mut Vec, - last_positions: &mut Vec, - score_matrix: &mut Vec>, - best_position_matrix: &mut Vec, - cancel_flag: &AtomicBool, -) { - let mut path_chars = Vec::new(); - let mut lowercase_path_chars = Vec::new(); - - let prefix = path_prefix.chars().collect::>(); - let lowercase_prefix = prefix - .iter() - .map(|c| c.to_ascii_lowercase()) - .collect::>(); - - for candidate in path_entries { - if !candidate.char_bag.is_superset(query_chars) { - continue; - } - - if cancel_flag.load(atomic::Ordering::Relaxed) { - break; - } - - path_chars.clear(); - lowercase_path_chars.clear(); - for c in candidate.path.to_string_lossy().chars() { - path_chars.push(c); - lowercase_path_chars.push(c.to_ascii_lowercase()); - } - - if !find_last_positions( - last_positions, - &lowercase_prefix, - &lowercase_path_chars, - lowercase_query, - ) { - continue; - } - - let matrix_len = query.len() * (path_chars.len() + prefix.len()); - score_matrix.clear(); - score_matrix.resize(matrix_len, None); - best_position_matrix.clear(); - best_position_matrix.resize(matrix_len, 0); - - let score = score_match( - query, - lowercase_query, - &path_chars, - &lowercase_path_chars, - &prefix, - &lowercase_prefix, - smart_case, - &last_positions, - score_matrix, - best_position_matrix, - match_positions, - *min_score, - ); - - if score > 0.0 { - let mat = PathMatch { - tree_id: snapshot.id, - path: candidate.path.clone(), - path_prefix: path_prefix.clone(), - score, - positions: match_positions.clone(), - }; - if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) { - if results.len() < max_results { - results.insert(i, mat); - } else if i < results.len() { - results.pop(); - results.insert(i, mat); - } - if results.len() == max_results { - *min_score = results.last().unwrap().score; - } - } - } - } -} - -fn find_last_positions( - last_positions: &mut Vec, - prefix: &[char], - path: &[char], - query: &[char], -) -> bool { - let mut path = path.iter(); - let mut prefix_iter = prefix.iter(); - for (i, char) in query.iter().enumerate().rev() { - if let Some(j) = path.rposition(|c| c == char) { - last_positions[i] = j + prefix.len(); - } else if let Some(j) = prefix_iter.rposition(|c| c == char) { - last_positions[i] = j; - } else { - return false; - } - } - true -} - -fn score_match( - query: &[char], - query_cased: &[char], - path: &[char], - path_cased: &[char], - prefix: &[char], - lowercase_prefix: &[char], - smart_case: bool, - last_positions: &[usize], - score_matrix: &mut [Option], - best_position_matrix: &mut [usize], - match_positions: &mut [usize], - min_score: f64, -) -> f64 { - let score = recursive_score_match( - query, - query_cased, - path, - path_cased, - prefix, - lowercase_prefix, - smart_case, - last_positions, - score_matrix, - best_position_matrix, - min_score, - 0, - 0, - query.len() as f64, - ) * query.len() as f64; - - if score <= 0.0 { - return 0.0; - } - - let path_len = prefix.len() + path.len(); - let mut cur_start = 0; - let mut byte_ix = 0; - let mut char_ix = 0; - for i in 0..query.len() { - let match_char_ix = best_position_matrix[i * path_len + cur_start]; - while char_ix < match_char_ix { - let ch = prefix - .get(char_ix) - .or_else(|| path.get(char_ix - prefix.len())) - .unwrap(); - byte_ix += ch.len_utf8(); - char_ix += 1; - } - cur_start = match_char_ix + 1; - match_positions[i] = byte_ix; - } - - score -} - -fn recursive_score_match( - query: &[char], - query_cased: &[char], - path: &[char], - path_cased: &[char], - prefix: &[char], - lowercase_prefix: &[char], - smart_case: bool, - last_positions: &[usize], - score_matrix: &mut [Option], - best_position_matrix: &mut [usize], - min_score: f64, - query_idx: usize, - path_idx: usize, - cur_score: f64, -) -> f64 { - if query_idx == query.len() { - return 1.0; - } - - let path_len = prefix.len() + path.len(); - - if let Some(memoized) = score_matrix[query_idx * path_len + path_idx] { - return memoized; - } - - let mut score = 0.0; - let mut best_position = 0; - - let query_char = query_cased[query_idx]; - let limit = last_positions[query_idx]; - - let mut last_slash = 0; - for j in path_idx..=limit { - let path_char = if j < prefix.len() { - lowercase_prefix[j] - } else { - path_cased[j - prefix.len()] - }; - let is_path_sep = path_char == '/' || path_char == '\\'; - - if query_idx == 0 && is_path_sep { - last_slash = j; - } - - if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { - let curr = if j < prefix.len() { - prefix[j] - } else { - path[j - prefix.len()] - }; - - let mut char_score = 1.0; - if j > path_idx { - let last = if j - 1 < prefix.len() { - prefix[j - 1] - } else { - path[j - 1 - prefix.len()] - }; - - if last == '/' { - char_score = 0.9; - } else if last == '-' || last == '_' || last == ' ' || last.is_numeric() { - char_score = 0.8; - } else if last.is_lowercase() && curr.is_uppercase() { - char_score = 0.8; - } else if last == '.' { - char_score = 0.7; - } else if query_idx == 0 { - char_score = BASE_DISTANCE_PENALTY; - } else { - char_score = MIN_DISTANCE_PENALTY.max( - BASE_DISTANCE_PENALTY - - (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY, - ); - } - } - - // Apply a severe penalty if the case doesn't match. - // This will make the exact matches have higher score than the case-insensitive and the - // path insensitive matches. - if (smart_case || curr == '/') && query[query_idx] != curr { - char_score *= 0.001; - } - - let mut multiplier = char_score; - - // Scale the score based on how deep within the path we found the match. - if query_idx == 0 { - multiplier /= ((prefix.len() + path.len()) - last_slash) as f64; - } - - let mut next_score = 1.0; - if min_score > 0.0 { - next_score = cur_score * multiplier; - // Scores only decrease. If we can't pass the previous best, bail - if next_score < min_score { - // Ensure that score is non-zero so we use it in the memo table. - if score == 0.0 { - score = 1e-18; - } - continue; - } - } - - let new_score = recursive_score_match( - query, - query_cased, - path, - path_cased, - prefix, - lowercase_prefix, - smart_case, - last_positions, - score_matrix, - best_position_matrix, - min_score, - query_idx + 1, - j + 1, - next_score, - ) * multiplier; - - if new_score > score { - score = new_score; - best_position = j; - // Optimization: can't score better than 1. - if new_score == 1.0 { - break; - } - } - } - } - - if best_position != 0 { - best_position_matrix[query_idx * path_len + path_idx] = best_position; - } - - score_matrix[query_idx * path_len + path_idx] = Some(score); - score -} - -#[cfg(test)] -mod tests { - use super::*; - use std::path::PathBuf; - - #[test] - fn test_get_last_positions() { - let mut last_positions = vec![0; 2]; - let result = find_last_positions( - &mut last_positions, - &['a', 'b', 'c'], - &['b', 'd', 'e', 'f'], - &['d', 'c'], - ); - assert_eq!(result, false); - - last_positions.resize(2, 0); - let result = find_last_positions( - &mut last_positions, - &['a', 'b', 'c'], - &['b', 'd', 'e', 'f'], - &['c', 'd'], - ); - assert_eq!(result, true); - assert_eq!(last_positions, vec![2, 4]); - - last_positions.resize(4, 0); - let result = find_last_positions( - &mut last_positions, - &['z', 'e', 'd', '/'], - &['z', 'e', 'd', '/', 'f'], - &['z', '/', 'z', 'f'], - ); - assert_eq!(result, true); - assert_eq!(last_positions, vec![0, 3, 4, 8]); - } - - #[test] - fn test_match_path_entries() { - let paths = vec![ - "", - "a", - "ab", - "abC", - "abcd", - "alphabravocharlie", - "AlphaBravoCharlie", - "thisisatestdir", - "/////ThisIsATestDir", - "/this/is/a/test/dir", - "/test/tiatd", - ]; - - assert_eq!( - match_query("abc", false, &paths), - vec![ - ("abC", vec![0, 1, 2]), - ("abcd", vec![0, 1, 2]), - ("AlphaBravoCharlie", vec![0, 5, 10]), - ("alphabravocharlie", vec![4, 5, 10]), - ] - ); - assert_eq!( - match_query("t/i/a/t/d", false, &paths), - vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),] - ); - - assert_eq!( - match_query("tiatd", false, &paths), - vec![ - ("/test/tiatd", vec![6, 7, 8, 9, 10]), - ("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]), - ("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]), - ("thisisatestdir", vec![0, 2, 6, 7, 11]), - ] - ); - } - - #[test] - fn test_match_multibyte_path_entries() { - let paths = vec!["aαbβ/cγdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"]; - assert_eq!("1️⃣".len(), 7); - assert_eq!( - match_query("bcd", false, &paths), - vec![ - ("αβγδ/bcde", vec![9, 10, 11]), - ("aαbβ/cγdδ", vec![3, 7, 10]), - ] - ); - assert_eq!( - match_query("cde", false, &paths), - vec![ - ("αβγδ/bcde", vec![10, 11, 12]), - ("c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", vec![0, 23, 46]), - ] - ); - } - - fn match_query<'a>( - query: &str, - smart_case: bool, - paths: &Vec<&'a str>, - ) -> Vec<(&'a str, Vec)> { - let lowercase_query = query.to_lowercase().chars().collect::>(); - let query = query.chars().collect::>(); - let query_chars = CharBag::from(&lowercase_query[..]); - - let path_arcs = paths - .iter() - .map(|path| Arc::from(PathBuf::from(path))) - .collect::>(); - let mut path_entries = Vec::new(); - for (i, path) in paths.iter().enumerate() { - let lowercase_path = path.to_lowercase().chars().collect::>(); - let char_bag = CharBag::from(lowercase_path.as_slice()); - path_entries.push(MatchCandidate { - char_bag, - path: path_arcs.get(i).unwrap(), - }); - } - - let mut match_positions = Vec::new(); - let mut last_positions = Vec::new(); - match_positions.resize(query.len(), 0); - last_positions.resize(query.len(), 0); - - let cancel_flag = AtomicBool::new(false); - let mut results = Vec::new(); - match_single_tree_paths( - &Snapshot { - id: 0, - scan_id: 0, - abs_path: PathBuf::new().into(), - ignores: Default::default(), - entries_by_path: Default::default(), - entries_by_id: Default::default(), - removed_entry_ids: Default::default(), - root_name: Default::default(), - root_char_bag: Default::default(), - next_entry_id: Default::default(), - }, - "".into(), - path_entries.into_iter(), - &query[..], - &lowercase_query[..], - query_chars, - smart_case, - &mut results, - 100, - &mut 0.0, - &mut match_positions, - &mut last_positions, - &mut Vec::new(), - &mut Vec::new(), - &cancel_flag, - ); - - results - .into_iter() - .map(|result| { - ( - paths - .iter() - .copied() - .find(|p| result.path.as_ref() == Path::new(p)) - .unwrap(), - result.positions, - ) - }) - .collect() - } -}