Merge pull request #125 from zed-industries/theme-variables

Nathan Sobo created

Add flexible theme system

Change summary

Cargo.lock                             |   7 
gpui/Cargo.toml                        |   1 
gpui/examples/text.rs                  |  16 
gpui/src/app.rs                        |  85 ++
gpui/src/assets.rs                     |   7 
gpui/src/color.rs                      |  86 ++
gpui/src/elements.rs                   |  60 +
gpui/src/elements/container.rs         | 161 +++--
gpui/src/elements/label.rs             | 182 +++---
gpui/src/elements/svg.rs               |   8 
gpui/src/elements/uniform_list.rs      |  16 
gpui/src/fonts.rs                      | 105 +++
gpui/src/geometry.rs                   |  13 
gpui/src/platform.rs                   |   4 
gpui/src/platform/mac/fonts.rs         |  12 
gpui/src/platform/mac/renderer.rs      |  22 
gpui/src/scene.rs                      |  72 +
gpui/src/text_layout.rs                |  16 
server/src/tests.rs                    |   2 
zed/Cargo.toml                         |   6 
zed/assets/themes/_base.toml           |  47 +
zed/assets/themes/dark.toml            |  53 -
zed/assets/themes/light.toml           |  21 
zed/src/assets.rs                      |   4 
zed/src/editor.rs                      |  30 
zed/src/editor/buffer.rs               |  16 
zed/src/editor/display_map.rs          |  36 
zed/src/editor/display_map/fold_map.rs |   8 
zed/src/editor/display_map/tab_map.rs  |   6 
zed/src/editor/display_map/wrap_map.rs |   8 
zed/src/editor/element.rs              |  22 
zed/src/file_finder.rs                 |  85 +-
zed/src/fuzzy.rs                       | 785 ++++++++++++++++++++++++++++
zed/src/fuzzy/char_bag.rs              |   0 
zed/src/language.rs                    |  16 
zed/src/lib.rs                         |  21 
zed/src/main.rs                        |  13 
zed/src/settings.rs                    | 360 ------------
zed/src/test.rs                        |  15 
zed/src/theme.rs                       | 626 ++++++++++++++++++++++
zed/src/theme_selector.rs              | 306 ++++++++++
zed/src/workspace.rs                   |  20 
zed/src/workspace/pane.rs              |  55 +
zed/src/workspace/pane_group.rs        |  10 
zed/src/worktree.rs                    |  96 +-
zed/src/worktree/fuzzy.rs              | 659 -----------------------
46 files changed, 2,647 insertions(+), 1,552 deletions(-)

Detailed changes

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"

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"] }

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<Self>) -> 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);

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<Self>) {}
     fn on_blur(&mut self, _: &mut ViewContext<Self>) {}
     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::<Vec<_>>();
+        self.pending_effects.extend(notifications);
+    }
+
     pub fn dispatch_action<T: 'static + Any>(
         &mut self,
         window_id: usize,
@@ -1503,7 +1513,7 @@ impl AppContext {
     pub fn render_view(&self, window_id: usize, view_id: usize) -> Result<ElementBox> {
         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::<T>,
+            },
+        )
     }
 
     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<T>,
+}
+
+impl<'a, T: View> RenderContext<'a, T> {
+    pub fn handle(&self) -> WeakViewHandle<T> {
+        WeakViewHandle::new(self.window_id, self.view_id)
+    }
+}
+
 impl AsRef<AppContext> for &AppContext {
     fn as_ref(&self) -> &AppContext {
         self
     }
 }
 
+impl<V: View> Deref for RenderContext<'_, V> {
+    type Target = AppContext;
+
+    fn deref(&self) -> &Self::Target {
+        &self.app
+    }
+}
+
 impl<M> AsRef<AppContext> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> ElementBox {
                 Empty::new().boxed()
             }
 
@@ -3674,7 +3717,7 @@ mod tests {
                 "test view"
             }
 
-            fn render(&self, _: &AppContext) -> ElementBox {
+            fn render(&self, _: &RenderContext<Self>) -> ElementBox {
                 Empty::new().boxed()
             }
         }
@@ -3719,7 +3762,7 @@ mod tests {
                 "test view"
             }
 
-            fn render(&self, _: &AppContext) -> ElementBox {
+            fn render(&self, _: &RenderContext<Self>) -> ElementBox {
                 Empty::new().boxed()
             }
         }
@@ -3742,7 +3785,7 @@ mod tests {
                 "test view"
             }
 
-            fn render(&self, _: &AppContext) -> ElementBox {
+            fn render(&self, _: &RenderContext<Self>) -> ElementBox {
                 Empty::new().boxed()
             }
         }

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<Cow<[u8]>>;
+    fn list(&self, path: &str) -> Vec<Cow<'static, str>>;
 }
 
 impl AssetSource for () {
@@ -12,6 +13,10 @@ impl AssetSource for () {
             path
         ))
     }
+
+    fn list(&self, _: &str) -> Vec<Cow<'static, str>> {
+        vec![]
+    }
 }
 
 pub struct AssetCache {

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<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let literal: Cow<str> = 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)
     }
 }

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<T: Element> {
+    Empty,
     Init {
         element: T,
     },
@@ -139,8 +139,9 @@ pub struct ElementBox {
 
 impl<T: Element> AnyElement for Lifecycle<T> {
     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<T: Element> AnyElement for Lifecycle<T> {
                 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<T: Element> AnyElement for Lifecycle<T> {
                     layout,
                 }
             }
-        });
-        result.unwrap()
+        };
+        result
     }
 
     fn after_layout(&mut self, cx: &mut AfterLayoutContext) {
@@ -175,27 +176,25 @@ impl<T: Element> AnyElement for Lifecycle<T> {
     }
 
     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<T: Element> AnyElement for Lifecycle<T> {
 
     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<T: Element> AnyElement for Lifecycle<T> {
 
     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<T: Element> AnyElement for Lifecycle<T> {
     }
 }
 
+impl<T: Element> Default for Lifecycle<T> {
+    fn default() -> Self {
+        Self::Empty
+    }
+}
+
 impl ElementBox {
     pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F {
         self.element.layout(constraint, cx)

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<Color>,
+    #[serde(default)]
+    pub border: Border,
+    #[serde(default)]
+    pub corner_radius: f32,
+    #[serde(default)]
+    pub shadow: Option<Shadow>,
+}
+
 pub struct Container {
-    margin: Margin,
-    padding: Padding,
-    background_color: Option<ColorU>,
-    border: Border,
-    corner_radius: f32,
-    shadow: Option<Shadow>,
     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<ColorU>) -> 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<ColorU>) -> 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 {

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<Highlights>,
+    style: LabelStyle,
+    highlight_indices: Vec<usize>,
 }
 
-pub struct Highlights {
-    color: ColorU,
-    indices: Vec<usize>,
-    font_properties: Properties,
+#[derive(Clone, Debug, Default, Deserialize)]
+pub struct LabelStyle {
+    pub text: TextStyle,
+    pub highlight_text: Option<TextStyle>,
 }
 
 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<usize>,
-    ) -> 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<usize>) -> 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!(

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<Cow<'static, str>>) -> 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
     }

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<Mutex<StateInner>>);
 
 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<usize>,
@@ -57,11 +51,11 @@ impl<F> UniformList<F>
 where
     F: Fn(Range<usize>, &mut Vec<ElementBox>, &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
     }

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<WeightJson>,
+    #[serde(default)]
+    italic: bool,
+}
+
+impl<'de> Deserialize<'de> for TextStyle {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    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<Color> 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<TextStyle> 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!({

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<RectF>) -> Path {
+    pub fn build(mut self, color: Color, clip_bounds: Option<RectF>) -> 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<Vector2F, D::Error>
+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()])

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<usize>;
 }

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!(

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;

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<ColorU>,
+    pub background: Option<Color>,
     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<ColorU>,
+    pub color: Color,
     pub top: bool,
     pub right: bool,
     pub bottom: bool,
     pub left: bool,
 }
 
+impl<'de> Deserialize<'de> for Border {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    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<PathVertex>,
 }
 
@@ -193,10 +231,10 @@ impl Layer {
 }
 
 impl Border {
-    pub fn new(width: f32, color: impl Into<ColorU>) -> 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<ColorU>) -> 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<ColorU>) -> 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<ColorU>) -> 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<ColorU>) -> 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<ColorU>) -> Self {
+    pub fn right(width: f32, color: Color) -> Self {
         let mut border = Self::new(width, color);
         border.right = true;
         border

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<f32>,
-    runs: SmallVec<[(usize, FontId, ColorU); 1]>,
+    runs: SmallVec<[(usize, FontId, Color); 1]>,
 }
 
 impl CacheKey for CacheKeyValue {
@@ -123,7 +123,7 @@ impl<'a> Borrow<dyn CacheKey + 'a> for CacheKeyValue {
 struct CacheKeyRef<'a> {
     text: &'a str,
     font_size: OrderedFloat<f32>,
-    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<LineLayout>,
-    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<LineLayout>, runs: &[(usize, FontId, ColorU)]) -> Self {
+    fn new(layout: Arc<LineLayout>, 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();
                     }
                 }
 

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<Self>) -> gpui::ElementBox {
         gpui::Element::boxed(gpui::elements::Empty)
     }
 }

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"] }

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" },
+]

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"

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"

zed/src/assets.rs 🔗

@@ -10,4 +10,8 @@ impl AssetSource for Assets {
     fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
         Self::get(path).ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
     }
+
+    fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
+        Self::iter().filter(|p| p.starts_with(path)).collect()
+    }
 }

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<Self>) -> ElementBox {
         EditorElement::new(self.handle.clone()).boxed()
     }
 

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<Self::Item> {
         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())
             {

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);

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<Self::Item> {
         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.

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<Snapshot>);
@@ -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<Self::Item> {
         if self.chunk.is_empty() {

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<Self::Item> {
         if self.output_position.row() >= self.max_output_row {

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<SelectionLine>,
-    color: ColorU,
+    color: Color,
 }
 
 #[derive(Debug)]

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<Self>) -> 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>) {
-        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>) {
+        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<FileFinder>,
         event: &Event,
         cx: &mut ViewContext<Workspace>,
     ) {
         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<Self>) {
-        cx.notify();
-    }
-
     fn confirm(&mut self, _: &(), cx: &mut ViewContext<Self>) {
         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;

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<usize>,
+    last_positions: Vec<usize>,
+    score_matrix: Vec<Option<f64>>,
+    best_position_matrix: Vec<usize>,
+}
+
+trait Match: Ord {
+    fn score(&self) -> f64;
+    fn set_positions(&mut self, positions: Vec<usize>);
+}
+
+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<Path>,
+    pub char_bag: CharBag,
+}
+
+#[derive(Clone, Debug)]
+pub struct PathMatch {
+    pub score: f64,
+    pub positions: Vec<usize>,
+    pub tree_id: usize,
+    pub path: Arc<Path>,
+    pub path_prefix: Arc<str>,
+}
+
+#[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<usize>) {
+        self.positions = positions;
+    }
+}
+
+impl Match for StringMatch {
+    fn score(&self) -> f64 {
+        self.score
+    }
+
+    fn set_positions(&mut self, positions: Vec<usize>) {
+        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<usize>,
+    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<Ordering> {
+        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<Ordering> {
+        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<executor::Background>,
+) -> Vec<StringMatch> {
+    let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
+    let query = query.chars().collect::<Vec<_>>();
+
+    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::<Vec<_>>();
+
+    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<executor::Background>,
+) -> Vec<PathMatch> {
+    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::<Vec<_>>();
+    let query = query.chars().collect::<Vec<_>>();
+
+    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::<Vec<_>>();
+
+    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<str> =
+                                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<StringMatch>,
+        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<str>,
+        path_entries: impl Iterator<Item = PathMatchCandidate<'a>>,
+        results: &mut Vec<PathMatch>,
+        cancel_flag: &AtomicBool,
+    ) {
+        let prefix = path_prefix.chars().collect::<Vec<_>>();
+        let lowercase_prefix = prefix
+            .iter()
+            .map(|c| c.to_ascii_lowercase())
+            .collect::<Vec<_>>();
+        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<C: MatchCandidate, R, F>(
+        &mut self,
+        prefix: &[char],
+        lowercase_prefix: &[char],
+        candidates: impl Iterator<Item = C>,
+        results: &mut Vec<R>,
+        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<usize>)> {
+        let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
+        let query = query.chars().collect::<Vec<_>>();
+        let query_chars = CharBag::from(&lowercase_query[..]);
+
+        let path_arcs = paths
+            .iter()
+            .map(|path| Arc::from(PathBuf::from(path)))
+            .collect::<Vec<_>>();
+        let mut path_entries = Vec::new();
+        for (i, path) in paths.iter().enumerate() {
+            let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
+            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()
+    }
+}

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<ThemeMap>,
+    pub highlight_map: Mutex<HighlightMap>,
 }
 
 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(),
                 }),
             ],
         };

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<Settings>,
-    pub languages: std::sync::Arc<language::LanguageRegistry>,
-    pub rpc_router: std::sync::Arc<ForegroundRouter>,
+    pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
+    pub settings: watch::Receiver<Settings>,
+    pub languages: Arc<language::LanguageRegistry>,
+    pub themes: Arc<settings::ThemeRegistry>,
+    pub rpc_router: Arc<ForegroundRouter>,
     pub rpc: rpc::Client,
-    pub fs: std::sync::Arc<dyn fs::Fs>,
+    pub fs: Arc<dyn fs::Fs>,
 }
 
 pub fn init(cx: &mut gpui::MutableAppContext) {

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() {

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<Theme>,
 }
 
-#[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<ReplicaTheme>,
-}
-
-#[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> {
+        Self::new_with_theme(font_cache, Arc::new(Theme::default()))
+    }
+
+    pub fn new_with_theme(font_cache: &FontCache, theme: Arc<Theme>) -> Result<Self> {
         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<Self> {
-        #[derive(Deserialize)]
-        struct ThemeToml {
-            #[serde(default)]
-            ui: UiTheme,
-            #[serde(default)]
-            editor: EditorTheme,
-            #[serde(default)]
-            syntax: HashMap<String, StyleToml>,
-        }
-
-        #[derive(Deserialize)]
-        #[serde(untagged)]
-        enum StyleToml {
-            Color(Color),
-            Full {
-                color: Option<Color>,
-                weight: Option<toml::Value>,
-                #[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<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        let rgba_value = u32::deserialize(deserializer)?;
-        Ok(Self(ColorU::from_u32((rgba_value << 8) + 0xFF)))
-    }
-}
-
-impl Into<ColorU> 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<ColorU> for Color {
-    fn eq(&self, other: &ColorU) -> bool {
-        self.0.eq(other)
-    }
-}
-
 pub fn channel(
     font_cache: &FontCache,
 ) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
     Ok(watch::channel_with(Settings::new(font_cache)?))
 }
 
-fn deserialize_weight(weight: Option<toml::Value>) -> Result<FontWeight> {
-    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<Settings>, watch::Receiver<Settings>)> {
+    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,
+    )?))
 }

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<AppState> {
-    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),

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<dyn AssetSource>,
+    themes: Mutex<HashMap<String, Arc<Theme>>>,
+    theme_data: Mutex<HashMap<String, Arc<Value>>>,
+}
+
+#[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<Replica>,
+}
+
+#[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<Self> {
+        Arc::new(Self {
+            assets: Box::new(source),
+            themes: Default::default(),
+            theme_data: Default::default(),
+        })
+    }
+
+    pub fn list(&self) -> impl Iterator<Item = String> {
+        self.assets.list("themes/").into_iter().filter_map(|path| {
+            let filename = path.strip_prefix("themes/")?;
+            let theme_name = filename.strip_suffix(".toml")?;
+            if theme_name.starts_with('_') {
+                None
+            } else {
+                Some(theme_name.to_string())
+            }
+        })
+    }
+
+    pub fn clear(&self) {
+        self.theme_data.lock().clear();
+        self.themes.lock().clear();
+    }
+
+    pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
+        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>(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<Arc<Value>> {
+        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<String, Value> = 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<String, Value>, extension: Map<String, Value>) {
+    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<Key>,
+    target_path: Vec<Key>,
+}
+
+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<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+fn find_extensions(value: &Value, key_path: &mut Vec<Key>, directives: &mut Vec<ExtendDirective>) {
+    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<String, Value>, key_path: &Vec<Key>) -> 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<String, Value>,
+    stack: &mut Vec<String>,
+) -> 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<Vec<(String, TextStyle)>, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let mut result = Vec::<(String, TextStyle)>::new();
+
+    let syntax_data: HashMap<String, TextStyle> = 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<std::borrow::Cow<[u8]>> {
+            if let Some(row) = self.0.iter().find(|e| e.0 == path) {
+                Ok(row.1.as_bytes().into())
+            } else {
+                Err(anyhow!("no such path {}", path))
+            }
+        }
+
+        fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
+            self.0
+                .iter()
+                .copied()
+                .filter_map(|(path, _)| {
+                    if path.starts_with(prefix) {
+                        Some(path.into())
+                    } else {
+                        None
+                    }
+                })
+                .collect()
+        }
+    }
+}

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<Mutex<watch::Sender<Settings>>>,
+    settings: watch::Receiver<Settings>,
+    registry: Arc<ThemeRegistry>,
+    matches: Vec<StringMatch>,
+    query_buffer: ViewHandle<Editor>,
+    list_state: UniformListState,
+    selected_index: usize,
+}
+
+pub fn init(cx: &mut MutableAppContext, app_state: &Arc<AppState>) {
+    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<Mutex<watch::Sender<Settings>>>,
+        settings: watch::Receiver<Settings>,
+        registry: Arc<ThemeRegistry>,
+        cx: &mut ViewContext<Self>,
+    ) -> 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<AppState>,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        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<AppState>, cx: &mut ViewContext<Workspace>) {
+        let current_theme_name = app_state.settings.borrow().theme.name.clone();
+        app_state.themes.clear();
+        match app_state.themes.get(&current_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<Self>) {
+        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<Self>) {
+        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<Self>) {
+        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>) {
+    //     self.selected_index = *selected_index;
+    //     self.confirm(&(), cx);
+    // }
+
+    fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
+        let background = cx.background().clone();
+        let candidates = self
+            .registry
+            .list()
+            .map(|name| StringMatchCandidate {
+                char_bag: name.as_str().into(),
+                string: name,
+            })
+            .collect::<Vec<_>>();
+        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<ThemeSelector>,
+        event: &Event,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        match event {
+            Event::Dismissed => {
+                workspace.dismiss_modal(cx);
+            }
+        }
+    }
+
+    fn on_query_editor_event(
+        &mut self,
+        _: ViewHandle<Editor>,
+        event: &editor::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            editor::Event::Edited => self.update_matches(cx),
+            editor::Event::Blurred => cx.emit(Event::Dismissed),
+            _ => {}
+        }
+    }
+
+    fn render_matches(&self, cx: &RenderContext<Self>) -> 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<Self>) -> 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<Self>) {
+        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
+    }
+}

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<Self>) -> 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<Workspace> {
                 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::<Vec<_>>()
     }
@@ -974,8 +974,8 @@ mod tests {
         })
         .await;
         assert_eq!(cx.window_ids().len(), 1);
-        let workspace_view_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
-        workspace_view_1.read_with(&cx, |workspace, _| {
+        let workspace_1 = cx.root_view::<Workspace>(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);
         });
     }
 }

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::<Tab, _>(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::<TabCloseButton, _>(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<Self>) -> ElementBox {
         if let Some(active_item) = self.active_item() {
             Flex::column()
                 .with_child(self.render_tabs(cx))

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)
 }

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<Path>) -> Option<u64> {
-        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<Path>) {
-        (self.worktree.id(), self.path())
+        (self.worktree.id(), self.path.clone())
     }
 }
 
 #[derive(Clone, Debug)]
 pub struct Entry {
-    id: usize,
-    kind: EntryKind,
-    path: Arc<Path>,
-    inode: u64,
-    mtime: SystemTime,
-    is_symlink: bool,
-    is_ignored: bool,
+    pub id: usize,
+    pub kind: EntryKind,
+    pub path: Arc<Path>,
+    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<Path> {
-        &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<Self::Item> {
         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::<Vec<_>>();
             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

zed/src/worktree/fuzzy.rs 🔗

@@ -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<Path>,
-    pub char_bag: CharBag,
-}
-
-#[derive(Clone, Debug)]
-pub struct PathMatch {
-    pub score: f64,
-    pub positions: Vec<usize>,
-    pub tree_id: usize,
-    pub path: Arc<Path>,
-    pub path_prefix: Arc<str>,
-}
-
-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<Ordering> {
-        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<AtomicBool>,
-    background: Arc<executor::Background>,
-) -> Vec<PathMatch> {
-    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::<Vec<_>>();
-    let query = query.chars().collect::<Vec<_>>();
-
-    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::<Vec<_>>();
-
-    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<str> =
-                                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<str>,
-    path_entries: impl Iterator<Item = MatchCandidate<'a>>,
-    query: &[char],
-    lowercase_query: &[char],
-    query_chars: CharBag,
-    smart_case: bool,
-    results: &mut Vec<PathMatch>,
-    max_results: usize,
-    min_score: &mut f64,
-    match_positions: &mut Vec<usize>,
-    last_positions: &mut Vec<usize>,
-    score_matrix: &mut Vec<Option<f64>>,
-    best_position_matrix: &mut Vec<usize>,
-    cancel_flag: &AtomicBool,
-) {
-    let mut path_chars = Vec::new();
-    let mut lowercase_path_chars = Vec::new();
-
-    let prefix = path_prefix.chars().collect::<Vec<_>>();
-    let lowercase_prefix = prefix
-        .iter()
-        .map(|c| c.to_ascii_lowercase())
-        .collect::<Vec<_>>();
-
-    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<usize>,
-    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<f64>],
-    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<f64>],
-    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<usize>)> {
-        let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
-        let query = query.chars().collect::<Vec<_>>();
-        let query_chars = CharBag::from(&lowercase_query[..]);
-
-        let path_arcs = paths
-            .iter()
-            .map(|path| Arc::from(PathBuf::from(path)))
-            .collect::<Vec<_>>();
-        let mut path_entries = Vec::new();
-        for (i, path) in paths.iter().enumerate() {
-            let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
-            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()
-    }
-}