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

Add flexible theme system
This commit is contained in:
Nathan Sobo 2021-08-05 08:57:52 -06:00 committed by GitHub
commit 01fcec53d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2645 additions and 1550 deletions

7
Cargo.lock generated
View File

@ -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"

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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 {
fn to_json(&self) -> serde_json::Value {
json!(format!("0x{:x}{:x}{:x}", self.r, self.g, self.b))
#[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}{: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)
}
}

View File

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

View File

@ -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 {

View File

@ -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);
) -> SmallVec<[(usize, FontId, Color); 8]> {
if self.highlight_indices.is_empty() {
return smallvec![(self.text.len(), font_id, self.style.text.color)];
}
let mut highlight_indices = highlights.indices.iter().copied().peekable();
let mut runs = SmallVec::new();
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);
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();
}
}
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
let mut runs = SmallVec::new();
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));
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 black = Color::black();
let red = Color::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 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!(

View File

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

View File

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

View File

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

View File

@ -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()])

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
use serde::Deserialize;
use serde_json::json;
use std::borrow::Cow;
use serde_json::json;
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

View File

@ -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();
}
}

View File

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

View File

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

View File

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

View File

@ -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"

View File

@ -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"

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

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

View File

@ -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 {

View File

@ -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)]

View File

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

785
zed/src/fuzzy.rs Normal file
View File

@ -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γ", "αβγδ/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γ", 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()
}
}

View File

@ -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(),
}),
],
};

View File

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

View File

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

View File

@ -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,
)?))
}

View File

@ -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),

626
zed/src/theme.rs Normal file
View File

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

306
zed/src/theme_selector.rs Normal file
View File

@ -0,0 +1,306 @@
use std::{cmp, sync::Arc};
use crate::{
editor::{self, Editor},
fuzzy::{match_strings, StringMatch, StringMatchCandidate},
settings::ThemeRegistry,
workspace::Workspace,
AppState, Settings,
};
use gpui::{
elements::{
Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement,
UniformList, UniformListState,
},
keymap::{self, Binding},
AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
ViewContext, ViewHandle,
};
use parking_lot::Mutex;
use postage::watch;
pub struct ThemeSelector {
settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
settings: watch::Receiver<Settings>,
registry: Arc<ThemeRegistry>,
matches: Vec<StringMatch>,
query_buffer: ViewHandle<Editor>,
list_state: UniformListState,
selected_index: usize,
}
pub fn init(cx: &mut MutableAppContext, app_state: &Arc<AppState>) {
cx.add_action("theme_selector:confirm", ThemeSelector::confirm);
cx.add_action("menu:select_prev", ThemeSelector::select_prev);
cx.add_action("menu:select_next", ThemeSelector::select_next);
cx.add_action("theme_selector:toggle", ThemeSelector::toggle);
cx.add_action("theme_selector:reload", ThemeSelector::reload);
cx.add_bindings(vec![
Binding::new("cmd-k cmd-t", "theme_selector:toggle", None).with_arg(app_state.clone()),
Binding::new("cmd-k t", "theme_selector:reload", None).with_arg(app_state.clone()),
Binding::new("escape", "theme_selector:toggle", Some("ThemeSelector"))
.with_arg(app_state.clone()),
Binding::new("enter", "theme_selector:confirm", Some("ThemeSelector")),
]);
}
pub enum Event {
Dismissed,
}
impl ThemeSelector {
fn new(
settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
settings: watch::Receiver<Settings>,
registry: Arc<ThemeRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx));
cx.subscribe_to_view(&query_buffer, Self::on_query_editor_event);
let mut this = Self {
settings,
settings_tx,
registry,
query_buffer,
matches: Vec::new(),
list_state: Default::default(),
selected_index: 0,
};
this.update_matches(cx);
this
}
fn toggle(
workspace: &mut Workspace,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) {
workspace.toggle_modal(cx, |cx, _| {
let selector = cx.add_view(|cx| {
Self::new(
app_state.settings_tx.clone(),
app_state.settings.clone(),
app_state.themes.clone(),
cx,
)
});
cx.subscribe_to_view(&selector, Self::on_event);
selector
});
}
fn reload(_: &mut Workspace, app_state: &Arc<AppState>, cx: &mut ViewContext<Workspace>) {
let current_theme_name = app_state.settings.borrow().theme.name.clone();
app_state.themes.clear();
match app_state.themes.get(&current_theme_name) {
Ok(theme) => {
cx.notify_all();
app_state.settings_tx.lock().borrow_mut().theme = theme;
}
Err(error) => {
log::error!("failed to load theme {}: {:?}", current_theme_name, error)
}
}
}
fn confirm(&mut self, _: &(), cx: &mut ViewContext<Self>) {
if let Some(mat) = self.matches.get(self.selected_index) {
if let Ok(theme) = self.registry.get(&mat.string) {
self.settings_tx.lock().borrow_mut().theme = theme;
cx.notify_all();
cx.emit(Event::Dismissed);
}
}
}
fn select_prev(&mut self, _: &(), cx: &mut ViewContext<Self>) {
if self.selected_index > 0 {
self.selected_index -= 1;
}
self.list_state.scroll_to(self.selected_index);
cx.notify();
}
fn select_next(&mut self, _: &(), cx: &mut ViewContext<Self>) {
if self.selected_index + 1 < self.matches.len() {
self.selected_index += 1;
}
self.list_state.scroll_to(self.selected_index);
cx.notify();
}
// fn select(&mut self, selected_index: &usize, cx: &mut ViewContext<Self>) {
// self.selected_index = *selected_index;
// self.confirm(&(), cx);
// }
fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
let background = cx.background().clone();
let candidates = self
.registry
.list()
.map(|name| StringMatchCandidate {
char_bag: name.as_str().into(),
string: name,
})
.collect::<Vec<_>>();
let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx));
self.matches = if query.is_empty() {
candidates
.into_iter()
.map(|candidate| StringMatch {
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
smol::block_on(match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background,
))
};
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<ThemeSelector>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => {
workspace.dismiss_modal(cx);
}
}
}
fn on_query_editor_event(
&mut self,
_: ViewHandle<Editor>,
event: &editor::Event,
cx: &mut ViewContext<Self>,
) {
match event {
editor::Event::Edited => self.update_matches(cx),
editor::Event::Blurred => cx.emit(Event::Dismissed),
_ => {}
}
}
fn render_matches(&self, cx: &RenderContext<Self>) -> ElementBox {
if self.matches.is_empty() {
let settings = self.settings.borrow();
return Container::new(
Label::new(
"No matches".into(),
settings.ui_font_family,
settings.ui_font_size,
)
.with_style(&settings.theme.ui.selector.label)
.boxed(),
)
.with_margin_top(6.0)
.named("empty matches");
}
let handle = cx.handle();
let list = UniformList::new(
self.list_state.clone(),
self.matches.len(),
move |mut range, items, cx| {
let cx = cx.as_ref();
let selector = handle.upgrade(cx).unwrap();
let selector = selector.read(cx);
let start = range.start;
range.end = cmp::min(range.end, selector.matches.len());
items.extend(
selector.matches[range]
.iter()
.enumerate()
.map(move |(i, path_match)| selector.render_match(path_match, start + i)),
);
},
);
Container::new(list.boxed())
.with_margin_top(6.0)
.named("matches")
}
fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
let settings = self.settings.borrow();
let theme = &settings.theme.ui;
let container = Container::new(
Label::new(
theme_match.string.clone(),
settings.ui_font_family,
settings.ui_font_size,
)
.with_style(if index == self.selected_index {
&theme.selector.active_item.label
} else {
&theme.selector.item.label
})
.with_highlights(theme_match.positions.clone())
.boxed(),
)
.with_style(if index == self.selected_index {
&theme.selector.active_item.container
} else {
&theme.selector.item.container
});
container.boxed()
}
}
impl Entity for ThemeSelector {
type Event = Event;
}
impl View for ThemeSelector {
fn ui_name() -> &'static str {
"ThemeSelector"
}
fn render(&self, cx: &RenderContext<Self>) -> ElementBox {
let settings = self.settings.borrow();
Align::new(
ConstrainedBox::new(
Container::new(
Flex::new(Axis::Vertical)
.with_child(ChildView::new(self.query_buffer.id()).boxed())
.with_child(Expanded::new(1.0, self.render_matches(cx)).boxed())
.boxed(),
)
.with_style(&settings.theme.ui.selector.container)
.boxed(),
)
.with_max_width(600.0)
.with_max_height(400.0)
.boxed(),
)
.top()
.named("theme selector")
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.focus(&self.query_buffer);
}
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx
}
}

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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γ", "αβγδ/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γ", 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()
}
}