Detailed changes
@@ -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"
@@ -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"] }
@@ -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);
@@ -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()
}
}
@@ -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 {
@@ -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)
}
}
@@ -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)
@@ -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 {
@@ -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!(
@@ -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
}
@@ -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
}
@@ -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!({
@@ -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()])
@@ -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>;
}
@@ -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!(
@@ -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;
@@ -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
@@ -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();
}
}
@@ -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)
}
}
@@ -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"] }
@@ -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" },
+]
@@ -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"
@@ -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"
@@ -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()
+ }
}
@@ -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()
}
@@ -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())
{
@@ -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);
@@ -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.
@@ -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() {
@@ -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 {
@@ -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)]
@@ -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;
@@ -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()
+ }
+}
@@ -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(),
}),
],
};
@@ -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) {
@@ -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() {
@@ -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,
+ )?))
}
@@ -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),
@@ -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()
+ }
+ }
+}
@@ -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(¤t_theme_name) {
+ Ok(theme) => {
+ cx.notify_all();
+ app_state.settings_tx.lock().borrow_mut().theme = theme;
+ }
+ Err(error) => {
+ log::error!("failed to load theme {}: {:?}", current_theme_name, error)
+ }
+ }
+ }
+
+ fn confirm(&mut self, _: &(), cx: &mut ViewContext<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
+ }
+}
@@ -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);
});
}
}
@@ -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))
@@ -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)
}
@@ -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
@@ -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()
- }
-}