mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Merge pull request #125 from zed-industries/theme-variables
Add flexible theme system
This commit is contained in:
commit
01fcec53d7
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -2172,7 +2172,6 @@ dependencies = [
|
||||
"png 0.16.8",
|
||||
"postage",
|
||||
"rand 0.8.3",
|
||||
"replace_with",
|
||||
"resvg",
|
||||
"seahash",
|
||||
"serde 1.0.125",
|
||||
@ -3927,12 +3926,6 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "replace_with"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690"
|
||||
|
||||
[[package]]
|
||||
name = "resvg"
|
||||
version = "0.14.0"
|
||||
|
@ -19,7 +19,6 @@ pathfinder_color = "0.5"
|
||||
pathfinder_geometry = "0.5"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
rand = "0.8.3"
|
||||
replace_with = "0.1.7"
|
||||
resvg = "0.14"
|
||||
seahash = "4.1"
|
||||
serde = { version = "1.0.125", features = ["derive"] }
|
||||
|
@ -1,5 +1,5 @@
|
||||
use gpui::{
|
||||
color::ColorU,
|
||||
color::Color,
|
||||
fonts::{Properties, Weight},
|
||||
DebugContext, Element as _, Quad,
|
||||
};
|
||||
@ -28,7 +28,7 @@ impl gpui::View for TextView {
|
||||
"View"
|
||||
}
|
||||
|
||||
fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox {
|
||||
fn render(&self, _: &gpui::RenderContext<Self>) -> gpui::ElementBox {
|
||||
TextElement.boxed()
|
||||
}
|
||||
}
|
||||
@ -82,17 +82,17 @@ impl gpui::Element for TextElement {
|
||||
text,
|
||||
font_size,
|
||||
&[
|
||||
(1, normal, ColorU::default()),
|
||||
(1, bold, ColorU::default()),
|
||||
(1, normal, ColorU::default()),
|
||||
(1, bold, ColorU::default()),
|
||||
(text.len() - 4, normal, ColorU::default()),
|
||||
(1, normal, Color::default()),
|
||||
(1, bold, Color::default()),
|
||||
(1, normal, Color::default()),
|
||||
(1, bold, Color::default()),
|
||||
(text.len() - 4, normal, Color::default()),
|
||||
],
|
||||
);
|
||||
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: bounds,
|
||||
background: Some(ColorU::white()),
|
||||
background: Some(Color::white()),
|
||||
..Default::default()
|
||||
});
|
||||
line.paint(bounds.origin(), bounds, cx);
|
||||
|
@ -36,9 +36,9 @@ pub trait Entity: 'static + Send + Sync {
|
||||
fn release(&mut self, _: &mut MutableAppContext) {}
|
||||
}
|
||||
|
||||
pub trait View: Entity {
|
||||
pub trait View: Entity + Sized {
|
||||
fn ui_name() -> &'static str;
|
||||
fn render<'a>(&self, cx: &AppContext) -> ElementBox;
|
||||
fn render(&self, cx: &RenderContext<'_, Self>) -> ElementBox;
|
||||
fn on_focus(&mut self, _: &mut ViewContext<Self>) {}
|
||||
fn on_blur(&mut self, _: &mut ViewContext<Self>) {}
|
||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||
@ -813,6 +813,16 @@ impl MutableAppContext {
|
||||
.push_back(Effect::ViewNotification { window_id, view_id });
|
||||
}
|
||||
|
||||
pub(crate) fn notify_all_views(&mut self) {
|
||||
let notifications = self
|
||||
.views
|
||||
.keys()
|
||||
.copied()
|
||||
.map(|(window_id, view_id)| Effect::ViewNotification { window_id, view_id })
|
||||
.collect::<Vec<_>>();
|
||||
self.pending_effects.extend(notifications);
|
||||
}
|
||||
|
||||
pub fn dispatch_action<T: 'static + Any>(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
@ -1503,7 +1513,7 @@ impl AppContext {
|
||||
pub fn render_view(&self, window_id: usize, view_id: usize) -> Result<ElementBox> {
|
||||
self.views
|
||||
.get(&(window_id, view_id))
|
||||
.map(|v| v.render(self))
|
||||
.map(|v| v.render(window_id, view_id, self))
|
||||
.ok_or(anyhow!("view not found"))
|
||||
}
|
||||
|
||||
@ -1512,7 +1522,7 @@ impl AppContext {
|
||||
.iter()
|
||||
.filter_map(|((win_id, view_id), view)| {
|
||||
if *win_id == window_id {
|
||||
Some((*view_id, view.render(self)))
|
||||
Some((*view_id, view.render(*win_id, *view_id, self)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -1650,7 +1660,7 @@ pub trait AnyView: Send + Sync {
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn release(&mut self, cx: &mut MutableAppContext);
|
||||
fn ui_name(&self) -> &'static str;
|
||||
fn render<'a>(&self, cx: &AppContext) -> ElementBox;
|
||||
fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox;
|
||||
fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
|
||||
fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
|
||||
fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
|
||||
@ -1676,8 +1686,16 @@ where
|
||||
T::ui_name()
|
||||
}
|
||||
|
||||
fn render<'a>(&self, cx: &AppContext) -> ElementBox {
|
||||
View::render(self, cx)
|
||||
fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox {
|
||||
View::render(
|
||||
self,
|
||||
&RenderContext {
|
||||
window_id,
|
||||
view_id,
|
||||
app: cx,
|
||||
view_type: PhantomData::<T>,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize) {
|
||||
@ -2079,6 +2097,10 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
self.app.notify_view(self.window_id, self.view_id);
|
||||
}
|
||||
|
||||
pub fn notify_all(&mut self) {
|
||||
self.app.notify_all_views();
|
||||
}
|
||||
|
||||
pub fn propagate_action(&mut self) {
|
||||
self.halt_action_dispatch = false;
|
||||
}
|
||||
@ -2094,12 +2116,33 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderContext<'a, T: View> {
|
||||
pub app: &'a AppContext,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
view_type: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: View> RenderContext<'a, T> {
|
||||
pub fn handle(&self) -> WeakViewHandle<T> {
|
||||
WeakViewHandle::new(self.window_id, self.view_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<AppContext> for &AppContext {
|
||||
fn as_ref(&self) -> &AppContext {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> Deref for RenderContext<'_, V> {
|
||||
type Target = AppContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.app
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> AsRef<AppContext> for ViewContext<'_, M> {
|
||||
fn as_ref(&self) -> &AppContext {
|
||||
&self.app.cx
|
||||
@ -3004,7 +3047,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl super::View for View {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
@ -3067,7 +3110,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl super::View for View {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
let mouse_down_count = self.mouse_down_count.clone();
|
||||
EventHandler::new(Empty::new().boxed())
|
||||
.on_mouse_down(move |_| {
|
||||
@ -3129,7 +3172,7 @@ mod tests {
|
||||
"View"
|
||||
}
|
||||
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
@ -3169,7 +3212,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl super::View for View {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
@ -3222,7 +3265,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl super::View for View {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
@ -3272,7 +3315,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl super::View for View {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
@ -3315,7 +3358,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl super::View for View {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
@ -3362,7 +3405,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl super::View for View {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
@ -3420,7 +3463,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl View for ViewA {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
@ -3438,7 +3481,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl View for ViewB {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
@ -3541,7 +3584,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl super::View for View {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
@ -3674,7 +3717,7 @@ mod tests {
|
||||
"test view"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> ElementBox {
|
||||
fn render(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
@ -3719,7 +3762,7 @@ mod tests {
|
||||
"test view"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> ElementBox {
|
||||
fn render(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
@ -3742,7 +3785,7 @@ mod tests {
|
||||
"test view"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> ElementBox {
|
||||
fn render(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashMap};
|
||||
|
||||
pub trait AssetSource: 'static {
|
||||
pub trait AssetSource: 'static + Send + Sync {
|
||||
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
|
||||
fn list(&self, path: &str) -> Vec<Cow<'static, str>>;
|
||||
}
|
||||
|
||||
impl AssetSource for () {
|
||||
@ -12,6 +13,10 @@ impl AssetSource for () {
|
||||
path
|
||||
))
|
||||
}
|
||||
|
||||
fn list(&self, _: &str) -> Vec<Cow<'static, str>> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AssetCache {
|
||||
|
@ -1,9 +1,89 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use crate::json::ToJson;
|
||||
pub use pathfinder_color::*;
|
||||
use pathfinder_color::ColorU;
|
||||
use serde::{
|
||||
de::{self, Unexpected},
|
||||
Deserialize, Deserializer,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
impl ToJson for ColorU {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -34,8 +34,7 @@ use crate::{
|
||||
};
|
||||
use core::panic;
|
||||
use json::ToJson;
|
||||
use replace_with::replace_with_or_abort;
|
||||
use std::{any::Any, borrow::Cow};
|
||||
use std::{any::Any, borrow::Cow, mem};
|
||||
|
||||
trait AnyElement {
|
||||
fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F;
|
||||
@ -115,6 +114,7 @@ pub trait Element {
|
||||
}
|
||||
|
||||
pub enum Lifecycle<T: Element> {
|
||||
Empty,
|
||||
Init {
|
||||
element: T,
|
||||
},
|
||||
@ -139,8 +139,9 @@ pub struct ElementBox {
|
||||
|
||||
impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F {
|
||||
let mut result = None;
|
||||
replace_with_or_abort(self, |me| match me {
|
||||
let result;
|
||||
*self = match mem::take(self) {
|
||||
Lifecycle::Empty => unreachable!(),
|
||||
Lifecycle::Init { mut element }
|
||||
| Lifecycle::PostLayout { mut element, .. }
|
||||
| Lifecycle::PostPaint { mut element, .. } => {
|
||||
@ -148,7 +149,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
debug_assert!(size.x().is_finite());
|
||||
debug_assert!(size.y().is_finite());
|
||||
|
||||
result = Some(size);
|
||||
result = size;
|
||||
Lifecycle::PostLayout {
|
||||
element,
|
||||
constraint,
|
||||
@ -156,8 +157,8 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
layout,
|
||||
}
|
||||
}
|
||||
});
|
||||
result.unwrap()
|
||||
};
|
||||
result
|
||||
}
|
||||
|
||||
fn after_layout(&mut self, cx: &mut AfterLayoutContext) {
|
||||
@ -175,27 +176,25 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
}
|
||||
|
||||
fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) {
|
||||
replace_with_or_abort(self, |me| {
|
||||
if let Lifecycle::PostLayout {
|
||||
mut element,
|
||||
*self = if let Lifecycle::PostLayout {
|
||||
mut element,
|
||||
constraint,
|
||||
size,
|
||||
mut layout,
|
||||
} = mem::take(self)
|
||||
{
|
||||
let bounds = RectF::new(origin, size);
|
||||
let paint = element.paint(bounds, &mut layout, cx);
|
||||
Lifecycle::PostPaint {
|
||||
element,
|
||||
constraint,
|
||||
size,
|
||||
mut layout,
|
||||
} = me
|
||||
{
|
||||
let bounds = RectF::new(origin, size);
|
||||
let paint = element.paint(bounds, &mut layout, cx);
|
||||
Lifecycle::PostPaint {
|
||||
element,
|
||||
constraint,
|
||||
bounds,
|
||||
layout,
|
||||
paint,
|
||||
}
|
||||
} else {
|
||||
panic!("invalid element lifecycle state");
|
||||
bounds,
|
||||
layout,
|
||||
paint,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
panic!("invalid element lifecycle state");
|
||||
};
|
||||
}
|
||||
|
||||
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
|
||||
@ -215,7 +214,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
|
||||
fn size(&self) -> Vector2F {
|
||||
match self {
|
||||
Lifecycle::Init { .. } => panic!("invalid element lifecycle state"),
|
||||
Lifecycle::Empty | Lifecycle::Init { .. } => panic!("invalid element lifecycle state"),
|
||||
Lifecycle::PostLayout { size, .. } => *size,
|
||||
Lifecycle::PostPaint { bounds, .. } => bounds.size(),
|
||||
}
|
||||
@ -223,6 +222,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
|
||||
fn metadata(&self) -> Option<&dyn Any> {
|
||||
match self {
|
||||
Lifecycle::Empty => unreachable!(),
|
||||
Lifecycle::Init { element }
|
||||
| Lifecycle::PostLayout { element, .. }
|
||||
| Lifecycle::PostPaint { element, .. } => element.metadata(),
|
||||
@ -257,6 +257,12 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Element> Default for Lifecycle<T> {
|
||||
fn default() -> Self {
|
||||
Self::Empty
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementBox {
|
||||
pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F {
|
||||
self.element.layout(constraint, cx)
|
||||
|
@ -1,62 +1,77 @@
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
color::ColorU,
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
color::Color,
|
||||
geometry::{
|
||||
deserialize_vec2f,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::ToJson,
|
||||
scene::{self, Border, Quad},
|
||||
AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
||||
SizeConstraint,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct ContainerStyle {
|
||||
#[serde(default)]
|
||||
pub margin: Margin,
|
||||
#[serde(default)]
|
||||
pub padding: Padding,
|
||||
#[serde(rename = "background")]
|
||||
pub background_color: Option<Color>,
|
||||
#[serde(default)]
|
||||
pub border: Border,
|
||||
#[serde(default)]
|
||||
pub corner_radius: f32,
|
||||
#[serde(default)]
|
||||
pub shadow: Option<Shadow>,
|
||||
}
|
||||
|
||||
pub struct Container {
|
||||
margin: Margin,
|
||||
padding: Padding,
|
||||
background_color: Option<ColorU>,
|
||||
border: Border,
|
||||
corner_radius: f32,
|
||||
shadow: Option<Shadow>,
|
||||
child: ElementBox,
|
||||
style: ContainerStyle,
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn new(child: ElementBox) -> Self {
|
||||
Self {
|
||||
margin: Margin::default(),
|
||||
padding: Padding::default(),
|
||||
background_color: None,
|
||||
border: Border::default(),
|
||||
corner_radius: 0.0,
|
||||
shadow: None,
|
||||
child,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_style(mut self, style: &ContainerStyle) -> Self {
|
||||
self.style = style.clone();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_margin_top(mut self, margin: f32) -> Self {
|
||||
self.margin.top = margin;
|
||||
self.style.margin.top = margin;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_margin_left(mut self, margin: f32) -> Self {
|
||||
self.margin.left = margin;
|
||||
self.style.margin.left = margin;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_horizontal_padding(mut self, padding: f32) -> Self {
|
||||
self.padding.left = padding;
|
||||
self.padding.right = padding;
|
||||
self.style.padding.left = padding;
|
||||
self.style.padding.right = padding;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_vertical_padding(mut self, padding: f32) -> Self {
|
||||
self.padding.top = padding;
|
||||
self.padding.bottom = padding;
|
||||
self.style.padding.top = padding;
|
||||
self.style.padding.bottom = padding;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_uniform_padding(mut self, padding: f32) -> Self {
|
||||
self.padding = Padding {
|
||||
self.style.padding = Padding {
|
||||
top: padding,
|
||||
left: padding,
|
||||
bottom: padding,
|
||||
@ -66,68 +81,68 @@ impl Container {
|
||||
}
|
||||
|
||||
pub fn with_padding_right(mut self, padding: f32) -> Self {
|
||||
self.padding.right = padding;
|
||||
self.style.padding.right = padding;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_padding_bottom(mut self, padding: f32) -> Self {
|
||||
self.padding.bottom = padding;
|
||||
self.style.padding.bottom = padding;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_background_color(mut self, color: impl Into<ColorU>) -> Self {
|
||||
self.background_color = Some(color.into());
|
||||
pub fn with_background_color(mut self, color: Color) -> Self {
|
||||
self.style.background_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_border(mut self, border: Border) -> Self {
|
||||
self.border = border;
|
||||
self.style.border = border;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_corner_radius(mut self, radius: f32) -> Self {
|
||||
self.corner_radius = radius;
|
||||
self.style.corner_radius = radius;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: impl Into<ColorU>) -> Self {
|
||||
self.shadow = Some(Shadow {
|
||||
pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: Color) -> Self {
|
||||
self.style.shadow = Some(Shadow {
|
||||
offset,
|
||||
blur,
|
||||
color: color.into(),
|
||||
color,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
fn margin_size(&self) -> Vector2F {
|
||||
vec2f(
|
||||
self.margin.left + self.margin.right,
|
||||
self.margin.top + self.margin.bottom,
|
||||
self.style.margin.left + self.style.margin.right,
|
||||
self.style.margin.top + self.style.margin.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
fn padding_size(&self) -> Vector2F {
|
||||
vec2f(
|
||||
self.padding.left + self.padding.right,
|
||||
self.padding.top + self.padding.bottom,
|
||||
self.style.padding.left + self.style.padding.right,
|
||||
self.style.padding.top + self.style.padding.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
fn border_size(&self) -> Vector2F {
|
||||
let mut x = 0.0;
|
||||
if self.border.left {
|
||||
x += self.border.width;
|
||||
if self.style.border.left {
|
||||
x += self.style.border.width;
|
||||
}
|
||||
if self.border.right {
|
||||
x += self.border.width;
|
||||
if self.style.border.right {
|
||||
x += self.style.border.width;
|
||||
}
|
||||
|
||||
let mut y = 0.0;
|
||||
if self.border.top {
|
||||
y += self.border.width;
|
||||
if self.style.border.top {
|
||||
y += self.style.border.width;
|
||||
}
|
||||
if self.border.bottom {
|
||||
y += self.border.width;
|
||||
if self.style.border.bottom {
|
||||
y += self.style.border.width;
|
||||
}
|
||||
|
||||
vec2f(x, y)
|
||||
@ -168,28 +183,31 @@ impl Element for Container {
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
let quad_bounds = RectF::from_points(
|
||||
bounds.origin() + vec2f(self.margin.left, self.margin.top),
|
||||
bounds.lower_right() - vec2f(self.margin.right, self.margin.bottom),
|
||||
bounds.origin() + vec2f(self.style.margin.left, self.style.margin.top),
|
||||
bounds.lower_right() - vec2f(self.style.margin.right, self.style.margin.bottom),
|
||||
);
|
||||
|
||||
if let Some(shadow) = self.shadow.as_ref() {
|
||||
if let Some(shadow) = self.style.shadow.as_ref() {
|
||||
cx.scene.push_shadow(scene::Shadow {
|
||||
bounds: quad_bounds + shadow.offset,
|
||||
corner_radius: self.corner_radius,
|
||||
corner_radius: self.style.corner_radius,
|
||||
sigma: shadow.blur,
|
||||
color: shadow.color,
|
||||
});
|
||||
}
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: quad_bounds,
|
||||
background: self.background_color,
|
||||
border: self.border,
|
||||
corner_radius: self.corner_radius,
|
||||
background: self.style.background_color,
|
||||
border: self.style.border,
|
||||
corner_radius: self.style.corner_radius,
|
||||
});
|
||||
|
||||
let child_origin = quad_bounds.origin()
|
||||
+ vec2f(self.padding.left, self.padding.top)
|
||||
+ vec2f(self.border.left_width(), self.border.top_width());
|
||||
+ vec2f(self.style.padding.left, self.style.padding.top)
|
||||
+ vec2f(
|
||||
self.style.border.left_width(),
|
||||
self.style.border.top_width(),
|
||||
);
|
||||
self.child.paint(child_origin, cx);
|
||||
}
|
||||
|
||||
@ -214,24 +232,34 @@ impl Element for Container {
|
||||
json!({
|
||||
"type": "Container",
|
||||
"bounds": bounds.to_json(),
|
||||
"details": {
|
||||
"margin": self.margin.to_json(),
|
||||
"padding": self.padding.to_json(),
|
||||
"background_color": self.background_color.to_json(),
|
||||
"border": self.border.to_json(),
|
||||
"corner_radius": self.corner_radius,
|
||||
"shadow": self.shadow.to_json(),
|
||||
},
|
||||
"details": self.style.to_json(),
|
||||
"child": self.child.debug(cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
impl ToJson for ContainerStyle {
|
||||
fn to_json(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"margin": self.margin.to_json(),
|
||||
"padding": self.padding.to_json(),
|
||||
"background_color": self.background_color.to_json(),
|
||||
"border": self.border.to_json(),
|
||||
"corner_radius": self.corner_radius,
|
||||
"shadow": self.shadow.to_json(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct Margin {
|
||||
#[serde(default)]
|
||||
top: f32,
|
||||
#[serde(default)]
|
||||
left: f32,
|
||||
#[serde(default)]
|
||||
bottom: f32,
|
||||
#[serde(default)]
|
||||
right: f32,
|
||||
}
|
||||
|
||||
@ -254,11 +282,15 @@ impl ToJson for Margin {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct Padding {
|
||||
#[serde(default)]
|
||||
top: f32,
|
||||
#[serde(default)]
|
||||
left: f32,
|
||||
#[serde(default)]
|
||||
bottom: f32,
|
||||
#[serde(default)]
|
||||
right: f32,
|
||||
}
|
||||
|
||||
@ -281,11 +313,14 @@ impl ToJson for Padding {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct Shadow {
|
||||
#[serde(default, deserialize_with = "deserialize_vec2f")]
|
||||
offset: Vector2F,
|
||||
#[serde(default)]
|
||||
blur: f32,
|
||||
color: ColorU,
|
||||
#[serde(default)]
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl ToJson for Shadow {
|
||||
|
@ -1,10 +1,7 @@
|
||||
use serde_json::json;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
use crate::{
|
||||
color::ColorU,
|
||||
color::Color,
|
||||
font_cache::FamilyId,
|
||||
fonts::{FontId, Properties},
|
||||
fonts::{FontId, TextStyle},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
@ -14,20 +11,22 @@ use crate::{
|
||||
AfterLayoutContext, DebugContext, Element, Event, EventContext, FontCache, LayoutContext,
|
||||
PaintContext, SizeConstraint,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
pub struct Label {
|
||||
text: String,
|
||||
family_id: FamilyId,
|
||||
font_properties: Properties,
|
||||
font_size: f32,
|
||||
default_color: ColorU,
|
||||
highlights: Option<Highlights>,
|
||||
style: LabelStyle,
|
||||
highlight_indices: Vec<usize>,
|
||||
}
|
||||
|
||||
pub struct Highlights {
|
||||
color: ColorU,
|
||||
indices: Vec<usize>,
|
||||
font_properties: Properties,
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct LabelStyle {
|
||||
pub text: TextStyle,
|
||||
pub highlight_text: Option<TextStyle>,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
@ -35,29 +34,24 @@ impl Label {
|
||||
Self {
|
||||
text,
|
||||
family_id,
|
||||
font_properties: Properties::new(),
|
||||
font_size,
|
||||
default_color: ColorU::black(),
|
||||
highlights: None,
|
||||
highlight_indices: Default::default(),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_default_color(mut self, color: ColorU) -> Self {
|
||||
self.default_color = color;
|
||||
pub fn with_style(mut self, style: &LabelStyle) -> Self {
|
||||
self.style = style.clone();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_highlights(
|
||||
mut self,
|
||||
color: ColorU,
|
||||
font_properties: Properties,
|
||||
indices: Vec<usize>,
|
||||
) -> Self {
|
||||
self.highlights = Some(Highlights {
|
||||
color,
|
||||
font_properties,
|
||||
indices,
|
||||
});
|
||||
pub fn with_default_color(mut self, color: Color) -> Self {
|
||||
self.style.text.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
|
||||
self.highlight_indices = indices;
|
||||
self
|
||||
}
|
||||
|
||||
@ -65,47 +59,58 @@ impl Label {
|
||||
&self,
|
||||
font_cache: &FontCache,
|
||||
font_id: FontId,
|
||||
) -> SmallVec<[(usize, FontId, ColorU); 8]> {
|
||||
if let Some(highlights) = self.highlights.as_ref() {
|
||||
let highlight_font_id = font_cache
|
||||
.select_font(self.family_id, &highlights.font_properties)
|
||||
.unwrap_or(font_id);
|
||||
) -> 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!(
|
||||
|
@ -3,7 +3,7 @@ use std::borrow::Cow;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
color::ColorU,
|
||||
color::Color,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
@ -14,18 +14,18 @@ use crate::{
|
||||
|
||||
pub struct Svg {
|
||||
path: Cow<'static, str>,
|
||||
color: ColorU,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl Svg {
|
||||
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
color: ColorU::black(),
|
||||
color: Color::black(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_color(mut self, color: ColorU) -> Self {
|
||||
pub fn with_color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
@ -13,17 +13,10 @@ use json::ToJson;
|
||||
use parking_lot::Mutex;
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UniformListState(Arc<Mutex<StateInner>>);
|
||||
|
||||
impl UniformListState {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(Mutex::new(StateInner {
|
||||
scroll_top: 0.0,
|
||||
scroll_to: None,
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn scroll_to(&self, item_ix: usize) {
|
||||
self.0.lock().scroll_to = Some(item_ix);
|
||||
}
|
||||
@ -33,6 +26,7 @@ impl UniformListState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StateInner {
|
||||
scroll_top: f32,
|
||||
scroll_to: Option<usize>,
|
||||
@ -57,11 +51,11 @@ impl<F> UniformList<F>
|
||||
where
|
||||
F: Fn(Range<usize>, &mut Vec<ElementBox>, &AppContext),
|
||||
{
|
||||
pub fn new(state: UniformListState, item_count: usize, build_items: F) -> Self {
|
||||
pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self {
|
||||
Self {
|
||||
state,
|
||||
item_count,
|
||||
append_items: build_items,
|
||||
append_items,
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +73,7 @@ where
|
||||
|
||||
let mut state = self.state.0.lock();
|
||||
state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
|
||||
cx.dispatch_action("uniform_list:scroll", state.scroll_top);
|
||||
cx.notify();
|
||||
|
||||
true
|
||||
}
|
||||
|
@ -1,14 +1,109 @@
|
||||
use crate::json::json;
|
||||
pub use font_kit::metrics::Metrics;
|
||||
pub use font_kit::properties::{Properties, Stretch, Style, Weight};
|
||||
|
||||
use crate::json::ToJson;
|
||||
use crate::{
|
||||
color::Color,
|
||||
json::{json, ToJson},
|
||||
};
|
||||
pub use font_kit::{
|
||||
metrics::Metrics,
|
||||
properties::{Properties, Stretch, Style, Weight},
|
||||
};
|
||||
use serde::{de, Deserialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct FontId(pub usize);
|
||||
|
||||
pub type GlyphId = u32;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct TextStyle {
|
||||
pub color: Color,
|
||||
pub font_properties: Properties,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Deserialize)]
|
||||
enum WeightJson {
|
||||
thin,
|
||||
extra_light,
|
||||
light,
|
||||
normal,
|
||||
medium,
|
||||
semibold,
|
||||
bold,
|
||||
extra_bold,
|
||||
black,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TextStyleJson {
|
||||
color: Color,
|
||||
weight: Option<WeightJson>,
|
||||
#[serde(default)]
|
||||
italic: bool,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TextStyle {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let json = Value::deserialize(deserializer)?;
|
||||
if json.is_object() {
|
||||
let style_json: TextStyleJson =
|
||||
serde_json::from_value(json).map_err(de::Error::custom)?;
|
||||
Ok(style_json.into())
|
||||
} else {
|
||||
Ok(Self {
|
||||
color: serde_json::from_value(json).map_err(de::Error::custom)?,
|
||||
font_properties: Properties::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for TextStyle {
|
||||
fn from(color: Color) -> Self {
|
||||
Self {
|
||||
color,
|
||||
font_properties: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToJson for TextStyle {
|
||||
fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"color": self.color.to_json(),
|
||||
"font_properties": self.font_properties.to_json(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<TextStyle> for TextStyleJson {
|
||||
fn into(self) -> TextStyle {
|
||||
let weight = match self.weight.unwrap_or(WeightJson::normal) {
|
||||
WeightJson::thin => Weight::THIN,
|
||||
WeightJson::extra_light => Weight::EXTRA_LIGHT,
|
||||
WeightJson::light => Weight::LIGHT,
|
||||
WeightJson::normal => Weight::NORMAL,
|
||||
WeightJson::medium => Weight::MEDIUM,
|
||||
WeightJson::semibold => Weight::SEMIBOLD,
|
||||
WeightJson::bold => Weight::BOLD,
|
||||
WeightJson::extra_bold => Weight::EXTRA_BOLD,
|
||||
WeightJson::black => Weight::BLACK,
|
||||
};
|
||||
let style = if self.italic {
|
||||
Style::Italic
|
||||
} else {
|
||||
Style::Normal
|
||||
};
|
||||
TextStyle {
|
||||
color: self.color,
|
||||
font_properties: *Properties::new().weight(weight).style(style),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToJson for Properties {
|
||||
fn to_json(&self) -> crate::json::Value {
|
||||
json!({
|
||||
|
@ -1,7 +1,8 @@
|
||||
use super::scene::{Path, PathVertex};
|
||||
use crate::{color::ColorU, json::ToJson};
|
||||
use crate::{color::Color, json::ToJson};
|
||||
pub use pathfinder_geometry::*;
|
||||
use rect::RectF;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use serde_json::json;
|
||||
use vector::{vec2f, Vector2F};
|
||||
|
||||
@ -55,7 +56,7 @@ impl PathBuilder {
|
||||
self.current = point;
|
||||
}
|
||||
|
||||
pub fn build(mut self, color: ColorU, clip_bounds: Option<RectF>) -> Path {
|
||||
pub fn build(mut self, color: Color, clip_bounds: Option<RectF>) -> Path {
|
||||
if let Some(clip_bounds) = clip_bounds {
|
||||
self.bounds = self
|
||||
.bounds
|
||||
@ -108,6 +109,14 @@ impl PathBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_vec2f<'de, D>(deserializer: D) -> Result<Vector2F, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let [x, y]: [f32; 2] = Deserialize::deserialize(deserializer)?;
|
||||
Ok(vec2f(x, y))
|
||||
}
|
||||
|
||||
impl ToJson for Vector2F {
|
||||
fn to_json(&self) -> serde_json::Value {
|
||||
json!([self.x(), self.y()])
|
||||
|
@ -8,7 +8,7 @@ pub mod current {
|
||||
}
|
||||
|
||||
use crate::{
|
||||
color::ColorU,
|
||||
color::Color,
|
||||
executor,
|
||||
fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties},
|
||||
geometry::{
|
||||
@ -134,7 +134,7 @@ pub trait FontSystem: Send + Sync {
|
||||
&self,
|
||||
text: &str,
|
||||
font_size: f32,
|
||||
runs: &[(usize, FontId, ColorU)],
|
||||
runs: &[(usize, FontId, Color)],
|
||||
) -> LineLayout;
|
||||
fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize>;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
color::ColorU,
|
||||
color::Color,
|
||||
fonts::{FontId, GlyphId, Metrics, Properties},
|
||||
geometry::{
|
||||
rect::{RectF, RectI},
|
||||
@ -82,7 +82,7 @@ impl platform::FontSystem for FontSystem {
|
||||
&self,
|
||||
text: &str,
|
||||
font_size: f32,
|
||||
runs: &[(usize, FontId, ColorU)],
|
||||
runs: &[(usize, FontId, Color)],
|
||||
) -> LineLayout {
|
||||
self.0.read().layout_line(text, font_size, runs)
|
||||
}
|
||||
@ -191,7 +191,7 @@ impl FontSystemState {
|
||||
&self,
|
||||
text: &str,
|
||||
font_size: f32,
|
||||
runs: &[(usize, FontId, ColorU)],
|
||||
runs: &[(usize, FontId, Color)],
|
||||
) -> LineLayout {
|
||||
let font_id_attr_name = CFString::from_static_string("zed_font_id");
|
||||
|
||||
@ -445,9 +445,9 @@ mod tests {
|
||||
text,
|
||||
16.0,
|
||||
&[
|
||||
(9, zapfino_regular, ColorU::default()),
|
||||
(13, menlo_regular, ColorU::default()),
|
||||
(text.len() - 22, zapfino_regular, ColorU::default()),
|
||||
(9, zapfino_regular, Color::default()),
|
||||
(13, menlo_regular, Color::default()),
|
||||
(text.len() - 22, zapfino_regular, Color::default()),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::{atlas::AtlasAllocator, sprite_cache::SpriteCache};
|
||||
use crate::{
|
||||
color::ColorU,
|
||||
color::Color,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, vec2i, Vector2F},
|
||||
@ -11,7 +11,7 @@ use crate::{
|
||||
};
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
|
||||
use shaders::{ToFloat2 as _, ToUchar4 as _};
|
||||
use shaders::ToFloat2 as _;
|
||||
use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec};
|
||||
|
||||
const SHADERS_METALLIB: &'static [u8] =
|
||||
@ -438,17 +438,13 @@ impl Renderer {
|
||||
size: bounds.size().round().to_float2(),
|
||||
background_color: quad
|
||||
.background
|
||||
.unwrap_or(ColorU::transparent_black())
|
||||
.unwrap_or(Color::transparent_black())
|
||||
.to_uchar4(),
|
||||
border_top: border_width * (quad.border.top as usize as f32),
|
||||
border_right: border_width * (quad.border.right as usize as f32),
|
||||
border_bottom: border_width * (quad.border.bottom as usize as f32),
|
||||
border_left: border_width * (quad.border.left as usize as f32),
|
||||
border_color: quad
|
||||
.border
|
||||
.color
|
||||
.unwrap_or(ColorU::transparent_black())
|
||||
.to_uchar4(),
|
||||
border_color: quad.border.color.to_uchar4(),
|
||||
corner_radius: quad.corner_radius * scene.scale_factor(),
|
||||
};
|
||||
unsafe {
|
||||
@ -782,7 +778,7 @@ mod shaders {
|
||||
|
||||
use pathfinder_geometry::vector::Vector2I;
|
||||
|
||||
use crate::{color::ColorU, geometry::vector::Vector2F};
|
||||
use crate::{color::Color, geometry::vector::Vector2F};
|
||||
use std::mem;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/shaders.rs"));
|
||||
@ -791,10 +787,6 @@ mod shaders {
|
||||
fn to_float2(&self) -> vector_float2;
|
||||
}
|
||||
|
||||
pub trait ToUchar4 {
|
||||
fn to_uchar4(&self) -> vector_uchar4;
|
||||
}
|
||||
|
||||
impl ToFloat2 for (f32, f32) {
|
||||
fn to_float2(&self) -> vector_float2 {
|
||||
unsafe {
|
||||
@ -823,8 +815,8 @@ mod shaders {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToUchar4 for ColorU {
|
||||
fn to_uchar4(&self) -> vector_uchar4 {
|
||||
impl Color {
|
||||
pub fn to_uchar4(&self) -> vector_uchar4 {
|
||||
let mut vec = self.a as vector_uchar4;
|
||||
vec <<= 8;
|
||||
vec |= self.b as vector_uchar4;
|
||||
|
@ -1,9 +1,9 @@
|
||||
use 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
color::ColorU,
|
||||
color::Color,
|
||||
fonts::{FontId, GlyphId},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
@ -43,7 +43,7 @@ impl TextLayoutCache {
|
||||
&'a self,
|
||||
text: &'a str,
|
||||
font_size: f32,
|
||||
runs: &'a [(usize, FontId, ColorU)],
|
||||
runs: &'a [(usize, FontId, Color)],
|
||||
) -> Line {
|
||||
let key = &CacheKeyRef {
|
||||
text,
|
||||
@ -94,7 +94,7 @@ impl<'a> Hash for (dyn CacheKey + 'a) {
|
||||
struct CacheKeyValue {
|
||||
text: String,
|
||||
font_size: OrderedFloat<f32>,
|
||||
runs: SmallVec<[(usize, FontId, ColorU); 1]>,
|
||||
runs: SmallVec<[(usize, FontId, Color); 1]>,
|
||||
}
|
||||
|
||||
impl CacheKey for CacheKeyValue {
|
||||
@ -123,7 +123,7 @@ impl<'a> Borrow<dyn CacheKey + 'a> for CacheKeyValue {
|
||||
struct CacheKeyRef<'a> {
|
||||
text: &'a str,
|
||||
font_size: OrderedFloat<f32>,
|
||||
runs: &'a [(usize, FontId, ColorU)],
|
||||
runs: &'a [(usize, FontId, Color)],
|
||||
}
|
||||
|
||||
impl<'a> CacheKey for CacheKeyRef<'a> {
|
||||
@ -135,7 +135,7 @@ impl<'a> CacheKey for CacheKeyRef<'a> {
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Line {
|
||||
layout: Arc<LineLayout>,
|
||||
color_runs: SmallVec<[(u32, ColorU); 32]>,
|
||||
color_runs: SmallVec<[(u32, Color); 32]>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@ -162,7 +162,7 @@ pub struct Glyph {
|
||||
}
|
||||
|
||||
impl Line {
|
||||
fn new(layout: Arc<LineLayout>, runs: &[(usize, FontId, ColorU)]) -> Self {
|
||||
fn new(layout: Arc<LineLayout>, runs: &[(usize, FontId, Color)]) -> Self {
|
||||
let mut color_runs = SmallVec::new();
|
||||
for (len, _, color) in runs {
|
||||
color_runs.push((*len as u32, *color));
|
||||
@ -206,7 +206,7 @@ impl Line {
|
||||
|
||||
let mut color_runs = self.color_runs.iter();
|
||||
let mut color_end = 0;
|
||||
let mut color = ColorU::black();
|
||||
let mut color = Color::black();
|
||||
|
||||
for run in &self.layout.runs {
|
||||
let max_glyph_width = cx
|
||||
@ -230,7 +230,7 @@ impl Line {
|
||||
color = next_run.1;
|
||||
} else {
|
||||
color_end = self.layout.len;
|
||||
color = ColorU::black();
|
||||
color = Color::black();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -607,7 +607,7 @@ impl gpui::View for EmptyView {
|
||||
"empty view"
|
||||
}
|
||||
|
||||
fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox {
|
||||
fn render<'a>(&self, _: &gpui::RenderContext<Self>) -> gpui::ElementBox {
|
||||
gpui::Element::boxed(gpui::elements::Empty)
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ name = "Zed"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
test-support = ["tempdir", "serde_json", "zrpc/test-support"]
|
||||
test-support = ["tempdir", "zrpc/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
@ -41,9 +41,7 @@ rsa = "0.4"
|
||||
rust-embed = "5.9.0"
|
||||
seahash = "4.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1.0.64", features = [
|
||||
"preserve_order",
|
||||
], optional = true }
|
||||
serde_json = { version = "1.0.64", features = ["preserve_order"] }
|
||||
similar = "1.3"
|
||||
simplelog = "0.9"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
|
47
zed/assets/themes/_base.toml
Normal file
47
zed/assets/themes/_base.toml
Normal 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" },
|
||||
]
|
@ -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"
|
||||
|
21
zed/assets/themes/light.toml
Normal file
21
zed/assets/themes/light.toml
Normal 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"
|
@ -10,4 +10,8 @@ impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
|
||||
Self::get(path).ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
|
||||
Self::iter().filter(|p| p.starts_with(path)).collect()
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ mod element;
|
||||
pub mod movement;
|
||||
|
||||
use crate::{
|
||||
settings::{Settings, StyleId, Theme},
|
||||
settings::{HighlightId, Settings, Theme},
|
||||
time::ReplicaId,
|
||||
util::{post_inc, Bias},
|
||||
workspace,
|
||||
@ -16,10 +16,10 @@ pub use display_map::DisplayPoint;
|
||||
use display_map::*;
|
||||
pub use element::*;
|
||||
use gpui::{
|
||||
color::ColorU, font_cache::FamilyId, fonts::Properties as FontProperties,
|
||||
color::Color, font_cache::FamilyId, fonts::Properties as FontProperties,
|
||||
geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element,
|
||||
ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, Task, TextLayoutCache, View,
|
||||
ViewContext, WeakViewHandle,
|
||||
ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, RenderContext, Task,
|
||||
TextLayoutCache, View, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -2349,7 +2349,7 @@ impl Snapshot {
|
||||
.layout_str(
|
||||
"1".repeat(digit_count).as_str(),
|
||||
font_size,
|
||||
&[(digit_count, font_id, ColorU::black())],
|
||||
&[(digit_count, font_id, Color::black())],
|
||||
)
|
||||
.width())
|
||||
}
|
||||
@ -2374,9 +2374,9 @@ impl Snapshot {
|
||||
{
|
||||
let display_row = rows.start + ix as u32;
|
||||
let color = if active_rows.contains_key(&display_row) {
|
||||
theme.editor.line_number_active.0
|
||||
theme.editor.line_number_active
|
||||
} else {
|
||||
theme.editor.line_number.0
|
||||
theme.editor.line_number
|
||||
};
|
||||
if soft_wrapped {
|
||||
layouts.push(None);
|
||||
@ -2419,7 +2419,7 @@ impl Snapshot {
|
||||
.display_snapshot
|
||||
.highlighted_chunks_for_rows(rows.clone());
|
||||
|
||||
'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", StyleId::default()))) {
|
||||
'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
|
||||
for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
|
||||
if ix > 0 {
|
||||
layouts.push(layout_cache.layout_str(&line, self.font_size, &styles));
|
||||
@ -2433,12 +2433,12 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
if !line_chunk.is_empty() && !line_exceeded_max_len {
|
||||
let (color, font_properties) = self.theme.syntax_style(style_ix);
|
||||
let style = self.theme.highlight_style(style_ix);
|
||||
// Avoid a lookup if the font properties match the previous ones.
|
||||
let font_id = if font_properties == prev_font_properties {
|
||||
let font_id = if style.font_properties == prev_font_properties {
|
||||
prev_font_id
|
||||
} else {
|
||||
font_cache.select_font(self.font_family, &font_properties)?
|
||||
font_cache.select_font(self.font_family, &style.font_properties)?
|
||||
};
|
||||
|
||||
if line.len() + line_chunk.len() > MAX_LINE_LEN {
|
||||
@ -2451,9 +2451,9 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
line.push_str(line_chunk);
|
||||
styles.push((line_chunk.len(), font_id, color));
|
||||
styles.push((line_chunk.len(), font_id, style.color));
|
||||
prev_font_id = font_id;
|
||||
prev_font_properties = font_properties;
|
||||
prev_font_properties = style.font_properties;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2485,7 +2485,7 @@ impl Snapshot {
|
||||
&[(
|
||||
self.display_snapshot.line_len(row) as usize,
|
||||
font_id,
|
||||
ColorU::black(),
|
||||
Color::black(),
|
||||
)],
|
||||
))
|
||||
}
|
||||
@ -2533,7 +2533,7 @@ impl Entity for Editor {
|
||||
}
|
||||
|
||||
impl View for Editor {
|
||||
fn render<'a>(&self, _: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
EditorElement::new(self.handle.clone()).boxed()
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ use zrpc::proto;
|
||||
use crate::{
|
||||
language::{Language, Tree},
|
||||
operation_queue::{self, OperationQueue},
|
||||
settings::{StyleId, ThemeMap},
|
||||
settings::{HighlightId, HighlightMap},
|
||||
sum_tree::{self, FilterCursor, SumTree},
|
||||
time::{self, ReplicaId},
|
||||
util::Bias,
|
||||
@ -1985,7 +1985,7 @@ impl Snapshot {
|
||||
captures,
|
||||
next_capture: None,
|
||||
stack: Default::default(),
|
||||
theme_mapping: language.theme_mapping(),
|
||||
highlight_map: language.highlight_map(),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
@ -2316,8 +2316,8 @@ impl<'a> tree_sitter::TextProvider<'a> for TextProvider<'a> {
|
||||
struct Highlights<'a> {
|
||||
captures: tree_sitter::QueryCaptures<'a, 'a, TextProvider<'a>>,
|
||||
next_capture: Option<(tree_sitter::QueryMatch<'a, 'a>, usize)>,
|
||||
stack: Vec<(usize, StyleId)>,
|
||||
theme_mapping: ThemeMap,
|
||||
stack: Vec<(usize, HighlightId)>,
|
||||
highlight_map: HighlightMap,
|
||||
}
|
||||
|
||||
pub struct HighlightedChunks<'a> {
|
||||
@ -2341,7 +2341,7 @@ impl<'a> HighlightedChunks<'a> {
|
||||
if offset < next_capture_end {
|
||||
highlights.stack.push((
|
||||
next_capture_end,
|
||||
highlights.theme_mapping.get(capture.index),
|
||||
highlights.highlight_map.get(capture.index),
|
||||
));
|
||||
}
|
||||
highlights.next_capture.take();
|
||||
@ -2357,7 +2357,7 @@ impl<'a> HighlightedChunks<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
type Item = (&'a str, StyleId);
|
||||
type Item = (&'a str, HighlightId);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut next_capture_start = usize::MAX;
|
||||
@ -2381,7 +2381,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
next_capture_start = capture.node.start_byte();
|
||||
break;
|
||||
} else {
|
||||
let style_id = highlights.theme_mapping.get(capture.index);
|
||||
let style_id = highlights.highlight_map.get(capture.index);
|
||||
highlights.stack.push((capture.node.end_byte(), style_id));
|
||||
highlights.next_capture = highlights.captures.next();
|
||||
}
|
||||
@ -2391,7 +2391,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
if let Some(chunk) = self.chunks.peek() {
|
||||
let chunk_start = self.range.start;
|
||||
let mut chunk_end = (self.chunks.offset() + chunk.len()).min(next_capture_start);
|
||||
let mut style_id = StyleId::default();
|
||||
let mut style_id = HighlightId::default();
|
||||
if let Some((parent_capture_end, parent_style_id)) =
|
||||
self.highlights.as_ref().and_then(|h| h.stack.last())
|
||||
{
|
||||
|
@ -340,7 +340,7 @@ mod tests {
|
||||
util::RandomCharIter,
|
||||
};
|
||||
use buffer::{History, SelectionGoal};
|
||||
use gpui::MutableAppContext;
|
||||
use gpui::{color::Color, MutableAppContext};
|
||||
use rand::{prelude::StdRng, Rng};
|
||||
use std::{env, sync::Arc};
|
||||
use Bias::*;
|
||||
@ -652,13 +652,13 @@ mod tests {
|
||||
(function_item name: (identifier) @fn.name)"#,
|
||||
)
|
||||
.unwrap();
|
||||
let theme = Theme::parse(
|
||||
r#"
|
||||
[syntax]
|
||||
"mod.body" = 0xff0000
|
||||
"fn.name" = 0x00ff00"#,
|
||||
)
|
||||
.unwrap();
|
||||
let theme = Theme {
|
||||
syntax: vec![
|
||||
("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
|
||||
("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let lang = Arc::new(Language {
|
||||
config: LanguageConfig {
|
||||
name: "Test".to_string(),
|
||||
@ -668,7 +668,7 @@ mod tests {
|
||||
grammar: grammar.clone(),
|
||||
highlight_query,
|
||||
brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
|
||||
theme_mapping: Default::default(),
|
||||
highlight_map: Default::default(),
|
||||
});
|
||||
lang.set_theme(&theme);
|
||||
|
||||
@ -742,13 +742,13 @@ mod tests {
|
||||
(function_item name: (identifier) @fn.name)"#,
|
||||
)
|
||||
.unwrap();
|
||||
let theme = Theme::parse(
|
||||
r#"
|
||||
[syntax]
|
||||
"mod.body" = 0xff0000
|
||||
"fn.name" = 0x00ff00"#,
|
||||
)
|
||||
.unwrap();
|
||||
let theme = Theme {
|
||||
syntax: vec![
|
||||
("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
|
||||
("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let lang = Arc::new(Language {
|
||||
config: LanguageConfig {
|
||||
name: "Test".to_string(),
|
||||
@ -758,7 +758,7 @@ mod tests {
|
||||
grammar: grammar.clone(),
|
||||
highlight_query,
|
||||
brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
|
||||
theme_mapping: Default::default(),
|
||||
highlight_map: Default::default(),
|
||||
});
|
||||
lang.set_theme(&theme);
|
||||
|
||||
@ -937,7 +937,7 @@ mod tests {
|
||||
let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let mut chunks: Vec<(String, Option<&str>)> = Vec::new();
|
||||
for (chunk, style_id) in snapshot.highlighted_chunks_for_rows(rows) {
|
||||
let style_name = theme.syntax_style_name(style_id);
|
||||
let style_name = theme.highlight_name(style_id);
|
||||
if let Some((last_chunk, last_style_name)) = chunks.last_mut() {
|
||||
if style_name == *last_style_name {
|
||||
last_chunk.push_str(chunk);
|
||||
|
@ -4,7 +4,7 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
editor::buffer,
|
||||
settings::StyleId,
|
||||
settings::HighlightId,
|
||||
sum_tree::{self, Cursor, FilterCursor, SumTree},
|
||||
time,
|
||||
util::Bias,
|
||||
@ -1004,12 +1004,12 @@ impl<'a> Iterator for Chunks<'a> {
|
||||
pub struct HighlightedChunks<'a> {
|
||||
transform_cursor: Cursor<'a, Transform, FoldOffset, usize>,
|
||||
buffer_chunks: buffer::HighlightedChunks<'a>,
|
||||
buffer_chunk: Option<(usize, &'a str, StyleId)>,
|
||||
buffer_chunk: Option<(usize, &'a str, HighlightId)>,
|
||||
buffer_offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
type Item = (&'a str, StyleId);
|
||||
type Item = (&'a str, HighlightId);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let transform = if let Some(item) = self.transform_cursor.item() {
|
||||
@ -1031,7 +1031,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
self.transform_cursor.next(&());
|
||||
}
|
||||
|
||||
return Some((output_text, StyleId::default()));
|
||||
return Some((output_text, HighlightId::default()));
|
||||
}
|
||||
|
||||
// Retrieve a chunk from the current location in the buffer.
|
||||
|
@ -1,7 +1,7 @@
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot};
|
||||
use crate::{editor::rope, settings::StyleId, util::Bias};
|
||||
use crate::{editor::rope, settings::HighlightId, util::Bias};
|
||||
use std::{mem, ops::Range};
|
||||
|
||||
pub struct TabMap(Mutex<Snapshot>);
|
||||
@ -416,14 +416,14 @@ impl<'a> Iterator for Chunks<'a> {
|
||||
pub struct HighlightedChunks<'a> {
|
||||
fold_chunks: fold_map::HighlightedChunks<'a>,
|
||||
chunk: &'a str,
|
||||
style_id: StyleId,
|
||||
style_id: HighlightId,
|
||||
column: usize,
|
||||
tab_size: usize,
|
||||
skip_leading_tab: bool,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
type Item = (&'a str, StyleId);
|
||||
type Item = (&'a str, HighlightId);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.chunk.is_empty() {
|
||||
|
@ -5,7 +5,7 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
editor::Point,
|
||||
settings::StyleId,
|
||||
settings::HighlightId,
|
||||
sum_tree::{self, Cursor, SumTree},
|
||||
util::Bias,
|
||||
Settings,
|
||||
@ -59,7 +59,7 @@ pub struct Chunks<'a> {
|
||||
pub struct HighlightedChunks<'a> {
|
||||
input_chunks: tab_map::HighlightedChunks<'a>,
|
||||
input_chunk: &'a str,
|
||||
style_id: StyleId,
|
||||
style_id: HighlightId,
|
||||
output_position: WrapPoint,
|
||||
max_output_row: u32,
|
||||
transforms: Cursor<'a, Transform, WrapPoint, TabPoint>,
|
||||
@ -487,7 +487,7 @@ impl Snapshot {
|
||||
HighlightedChunks {
|
||||
input_chunks: self.tab_snapshot.highlighted_chunks(input_start..input_end),
|
||||
input_chunk: "",
|
||||
style_id: StyleId::default(),
|
||||
style_id: HighlightId::default(),
|
||||
output_position: output_start,
|
||||
max_output_row: rows.end,
|
||||
transforms,
|
||||
@ -670,7 +670,7 @@ impl<'a> Iterator for Chunks<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
type Item = (&'a str, StyleId);
|
||||
type Item = (&'a str, HighlightId);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.output_position.row() >= self.max_output_row {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use super::{DisplayPoint, Editor, SelectAction, Snapshot};
|
||||
use crate::time::ReplicaId;
|
||||
use gpui::{
|
||||
color::ColorU,
|
||||
color::Color,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
@ -196,14 +196,14 @@ impl EditorElement {
|
||||
let theme = &settings.theme;
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: gutter_bounds,
|
||||
background: Some(theme.editor.gutter_background.0),
|
||||
border: Border::new(0., ColorU::transparent_black()),
|
||||
background: Some(theme.editor.gutter_background),
|
||||
border: Border::new(0., Color::transparent_black()),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: text_bounds,
|
||||
background: Some(theme.editor.background.0),
|
||||
border: Border::new(0., ColorU::transparent_black()),
|
||||
background: Some(theme.editor.background),
|
||||
border: Border::new(0., Color::transparent_black()),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
|
||||
@ -229,7 +229,7 @@ impl EditorElement {
|
||||
);
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: RectF::new(origin, size),
|
||||
background: Some(theme.editor.active_line_background.0),
|
||||
background: Some(theme.editor.active_line_background),
|
||||
border: Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
@ -290,7 +290,7 @@ impl EditorElement {
|
||||
};
|
||||
|
||||
let selection = Selection {
|
||||
color: replica_theme.selection.0,
|
||||
color: replica_theme.selection,
|
||||
line_height: layout.line_height,
|
||||
start_y: content_origin.y() + row_range.start as f32 * layout.line_height
|
||||
- scroll_top,
|
||||
@ -333,7 +333,7 @@ impl EditorElement {
|
||||
- scroll_left;
|
||||
let y = selection.end.row() as f32 * layout.line_height - scroll_top;
|
||||
cursors.push(Cursor {
|
||||
color: replica_theme.cursor.0,
|
||||
color: replica_theme.cursor,
|
||||
origin: content_origin + vec2f(x, y),
|
||||
line_height: layout.line_height,
|
||||
});
|
||||
@ -707,7 +707,7 @@ impl PaintState {
|
||||
struct Cursor {
|
||||
origin: Vector2F,
|
||||
line_height: f32,
|
||||
color: ColorU,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
@ -715,7 +715,7 @@ impl Cursor {
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: RectF::new(self.origin, vec2f(2.0, self.line_height)),
|
||||
background: Some(self.color),
|
||||
border: Border::new(0., ColorU::black()),
|
||||
border: Border::new(0., Color::black()),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
}
|
||||
@ -726,7 +726,7 @@ struct Selection {
|
||||
start_y: f32,
|
||||
line_height: f32,
|
||||
lines: Vec<SelectionLine>,
|
||||
color: ColorU,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -6,13 +6,10 @@ use crate::{
|
||||
worktree::{match_paths, PathMatch},
|
||||
};
|
||||
use gpui::{
|
||||
color::ColorF,
|
||||
elements::*,
|
||||
fonts::{Properties, Weight},
|
||||
geometry::vector::vec2f,
|
||||
keymap::{self, Binding},
|
||||
AppContext, Axis, Border, Entity, MutableAppContext, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
AppContext, Axis, Entity, MutableAppContext, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use std::{
|
||||
@ -45,7 +42,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action("file_finder:select", FileFinder::select);
|
||||
cx.add_action("menu:select_prev", FileFinder::select_prev);
|
||||
cx.add_action("menu:select_next", FileFinder::select_next);
|
||||
cx.add_action("uniform_list:scroll", FileFinder::scroll);
|
||||
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("cmd-p", "file_finder:toggle", None),
|
||||
@ -68,7 +64,7 @@ impl View for FileFinder {
|
||||
"FileFinder"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> ElementBox {
|
||||
fn render(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
|
||||
Align::new(
|
||||
@ -79,11 +75,7 @@ impl View for FileFinder {
|
||||
.with_child(Expanded::new(1.0, self.render_matches()).boxed())
|
||||
.boxed(),
|
||||
)
|
||||
.with_margin_top(12.0)
|
||||
.with_uniform_padding(6.0)
|
||||
.with_corner_radius(6.0)
|
||||
.with_background_color(settings.theme.ui.modal_background)
|
||||
.with_shadow(vec2f(0., 4.), 12., ColorF::new(0.0, 0.0, 0.0, 0.5).to_u8())
|
||||
.with_style(&settings.theme.ui.selector.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_max_width(600.0)
|
||||
@ -115,7 +107,7 @@ impl FileFinder {
|
||||
settings.ui_font_family,
|
||||
settings.ui_font_size,
|
||||
)
|
||||
.with_default_color(settings.theme.editor.default_text.0)
|
||||
.with_style(&settings.theme.ui.selector.label)
|
||||
.boxed(),
|
||||
)
|
||||
.with_margin_top(6.0)
|
||||
@ -147,20 +139,25 @@ impl FileFinder {
|
||||
}
|
||||
|
||||
fn render_match(&self, path_match: &PathMatch, index: usize) -> ElementBox {
|
||||
let selected_index = self.selected_index();
|
||||
let settings = self.settings.borrow();
|
||||
let theme = &settings.theme.ui;
|
||||
let style = if index == selected_index {
|
||||
&settings.theme.ui.selector.active_item
|
||||
} else {
|
||||
&settings.theme.ui.selector.item
|
||||
};
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
self.labels_for_match(path_match);
|
||||
let bold = *Properties::new().weight(Weight::BOLD);
|
||||
let selected_index = self.selected_index();
|
||||
let mut container = Container::new(
|
||||
let container = Container::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Container::new(
|
||||
LineBox::new(
|
||||
settings.ui_font_family,
|
||||
settings.ui_font_size,
|
||||
Svg::new("icons/file-16.svg").boxed(),
|
||||
Svg::new("icons/file-16.svg")
|
||||
.with_color(style.label.text.color)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
@ -177,12 +174,8 @@ impl FileFinder {
|
||||
settings.ui_font_family,
|
||||
settings.ui_font_size,
|
||||
)
|
||||
.with_default_color(theme.modal_match_text.0)
|
||||
.with_highlights(
|
||||
theme.modal_match_text_highlight.0,
|
||||
bold,
|
||||
file_name_positions,
|
||||
)
|
||||
.with_style(&style.label)
|
||||
.with_highlights(file_name_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
@ -191,12 +184,8 @@ impl FileFinder {
|
||||
settings.ui_font_family,
|
||||
settings.ui_font_size,
|
||||
)
|
||||
.with_default_color(theme.modal_match_text.0)
|
||||
.with_highlights(
|
||||
theme.modal_match_text_highlight.0,
|
||||
bold,
|
||||
full_path_positions,
|
||||
)
|
||||
.with_style(&style.label)
|
||||
.with_highlights(full_path_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
@ -205,16 +194,7 @@ impl FileFinder {
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_uniform_padding(6.0)
|
||||
.with_background_color(if index == selected_index {
|
||||
theme.modal_match_background_active.0
|
||||
} else {
|
||||
theme.modal_match_background.0
|
||||
});
|
||||
|
||||
if index == selected_index || index < self.matches.len() - 1 {
|
||||
container = container.with_border(Border::bottom(1.0, theme.modal_match_border));
|
||||
}
|
||||
.with_style(&style.container);
|
||||
|
||||
let entry = (path_match.tree_id, path_match.path.clone());
|
||||
EventHandler::new(container.boxed())
|
||||
@ -250,31 +230,30 @@ impl FileFinder {
|
||||
(file_name, file_name_positions, full_path, path_positions)
|
||||
}
|
||||
|
||||
fn toggle(workspace_view: &mut Workspace, _: &(), cx: &mut ViewContext<Workspace>) {
|
||||
workspace_view.toggle_modal(cx, |cx, workspace_view| {
|
||||
let workspace = cx.handle();
|
||||
let finder =
|
||||
cx.add_view(|cx| Self::new(workspace_view.settings.clone(), workspace, cx));
|
||||
fn toggle(workspace: &mut Workspace, _: &(), cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |cx, workspace| {
|
||||
let handle = cx.handle();
|
||||
let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx));
|
||||
cx.subscribe_to_view(&finder, Self::on_event);
|
||||
finder
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
workspace_view: &mut Workspace,
|
||||
workspace: &mut Workspace,
|
||||
_: ViewHandle<FileFinder>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
Event::Selected(tree_id, path) => {
|
||||
workspace_view
|
||||
workspace
|
||||
.open_entry((*tree_id, path.clone()), cx)
|
||||
.map(|d| d.detach());
|
||||
workspace_view.dismiss_modal(cx);
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
Event::Dismissed => {
|
||||
workspace_view.dismiss_modal(cx);
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -301,7 +280,7 @@ impl FileFinder {
|
||||
matches: Vec::new(),
|
||||
selected: None,
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
list_state: UniformListState::new(),
|
||||
list_state: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -371,10 +350,6 @@ impl FileFinder {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn scroll(&mut self, _: &f32, cx: &mut ViewContext<Self>) {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &(), cx: &mut ViewContext<Self>) {
|
||||
if let Some(m) = self.matches.get(self.selected_index()) {
|
||||
cx.emit(Event::Selected(m.tree_id, m.path.clone()));
|
||||
@ -407,7 +382,7 @@ impl FileFinder {
|
||||
false,
|
||||
false,
|
||||
100,
|
||||
cancel_flag.clone(),
|
||||
cancel_flag.as_ref(),
|
||||
background,
|
||||
)
|
||||
.await;
|
||||
|
785
zed/src/fuzzy.rs
Normal file
785
zed/src/fuzzy.rs
Normal 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γdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"];
|
||||
assert_eq!("1️⃣".len(), 7);
|
||||
assert_eq!(
|
||||
match_query("bcd", false, &paths),
|
||||
vec![
|
||||
("αβγδ/bcde", vec![9, 10, 11]),
|
||||
("aαbβ/cγdδ", vec![3, 7, 10]),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
match_query("cde", false, &paths),
|
||||
vec![
|
||||
("αβγδ/bcde", vec![10, 11, 12]),
|
||||
("c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", vec![0, 23, 46]),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn match_query<'a>(
|
||||
query: &str,
|
||||
smart_case: bool,
|
||||
paths: &Vec<&'a str>,
|
||||
) -> Vec<(&'a str, Vec<usize>)> {
|
||||
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
||||
let query = query.chars().collect::<Vec<_>>();
|
||||
let query_chars = CharBag::from(&lowercase_query[..]);
|
||||
|
||||
let path_arcs = paths
|
||||
.iter()
|
||||
.map(|path| Arc::from(PathBuf::from(path)))
|
||||
.collect::<Vec<_>>();
|
||||
let mut path_entries = Vec::new();
|
||||
for (i, path) in paths.iter().enumerate() {
|
||||
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
|
||||
let char_bag = CharBag::from(lowercase_path.as_slice());
|
||||
path_entries.push(PathMatchCandidate {
|
||||
char_bag,
|
||||
path: path_arcs.get(i).unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
|
||||
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
let mut results = Vec::new();
|
||||
matcher.match_paths(
|
||||
0,
|
||||
"".into(),
|
||||
path_entries.into_iter(),
|
||||
&mut results,
|
||||
&cancel_flag,
|
||||
);
|
||||
|
||||
results
|
||||
.into_iter()
|
||||
.map(|result| {
|
||||
(
|
||||
paths
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|p| result.path.as_ref() == Path::new(p))
|
||||
.unwrap(),
|
||||
result.positions,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use crate::settings::{Theme, ThemeMap};
|
||||
use crate::settings::{HighlightMap, Theme};
|
||||
use parking_lot::Mutex;
|
||||
use rust_embed::RustEmbed;
|
||||
use serde::Deserialize;
|
||||
@ -27,7 +27,7 @@ pub struct Language {
|
||||
pub grammar: Grammar,
|
||||
pub highlight_query: Query,
|
||||
pub brackets_query: Query,
|
||||
pub theme_mapping: Mutex<ThemeMap>,
|
||||
pub highlight_map: Mutex<HighlightMap>,
|
||||
}
|
||||
|
||||
pub struct LanguageRegistry {
|
||||
@ -35,12 +35,12 @@ pub struct LanguageRegistry {
|
||||
}
|
||||
|
||||
impl Language {
|
||||
pub fn theme_mapping(&self) -> ThemeMap {
|
||||
self.theme_mapping.lock().clone()
|
||||
pub fn highlight_map(&self) -> HighlightMap {
|
||||
self.highlight_map.lock().clone()
|
||||
}
|
||||
|
||||
pub fn set_theme(&self, theme: &Theme) {
|
||||
*self.theme_mapping.lock() = ThemeMap::new(self.highlight_query.capture_names(), theme);
|
||||
*self.highlight_map.lock() = HighlightMap::new(self.highlight_query.capture_names(), theme);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ impl LanguageRegistry {
|
||||
grammar,
|
||||
highlight_query: Self::load_query(grammar, "rust/highlights.scm"),
|
||||
brackets_query: Self::load_query(grammar, "rust/brackets.scm"),
|
||||
theme_mapping: Mutex::new(ThemeMap::default()),
|
||||
highlight_map: Mutex::new(HighlightMap::default()),
|
||||
};
|
||||
|
||||
Self {
|
||||
@ -114,7 +114,7 @@ mod tests {
|
||||
grammar,
|
||||
highlight_query: Query::new(grammar, "").unwrap(),
|
||||
brackets_query: Query::new(grammar, "").unwrap(),
|
||||
theme_mapping: Default::default(),
|
||||
highlight_map: Default::default(),
|
||||
}),
|
||||
Arc::new(Language {
|
||||
config: LanguageConfig {
|
||||
@ -125,7 +125,7 @@ mod tests {
|
||||
grammar,
|
||||
highlight_query: Query::new(grammar, "").unwrap(),
|
||||
brackets_query: Query::new(grammar, "").unwrap(),
|
||||
theme_mapping: Default::default(),
|
||||
highlight_map: Default::default(),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
use zrpc::ForegroundRouter;
|
||||
|
||||
pub mod assets;
|
||||
pub mod editor;
|
||||
pub mod file_finder;
|
||||
pub mod fs;
|
||||
mod fuzzy;
|
||||
pub mod language;
|
||||
pub mod menus;
|
||||
mod operation_queue;
|
||||
@ -12,18 +11,28 @@ pub mod settings;
|
||||
mod sum_tree;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
pub mod theme;
|
||||
pub mod theme_selector;
|
||||
mod time;
|
||||
mod util;
|
||||
pub mod workspace;
|
||||
pub mod worktree;
|
||||
|
||||
pub use settings::Settings;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use postage::watch;
|
||||
use std::sync::Arc;
|
||||
use zrpc::ForegroundRouter;
|
||||
|
||||
pub struct AppState {
|
||||
pub settings: postage::watch::Receiver<Settings>,
|
||||
pub languages: std::sync::Arc<language::LanguageRegistry>,
|
||||
pub rpc_router: std::sync::Arc<ForegroundRouter>,
|
||||
pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
|
||||
pub settings: watch::Receiver<Settings>,
|
||||
pub languages: Arc<language::LanguageRegistry>,
|
||||
pub themes: Arc<settings::ThemeRegistry>,
|
||||
pub rpc_router: Arc<ForegroundRouter>,
|
||||
pub rpc: rpc::Client,
|
||||
pub fs: std::sync::Arc<dyn fs::Fs>,
|
||||
pub fs: Arc<dyn fs::Fs>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut gpui::MutableAppContext) {
|
||||
|
@ -3,12 +3,13 @@
|
||||
|
||||
use fs::OpenOptions;
|
||||
use log::LevelFilter;
|
||||
use parking_lot::Mutex;
|
||||
use simplelog::SimpleLogger;
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use zed::{
|
||||
self, assets, editor, file_finder,
|
||||
fs::RealFs,
|
||||
language, menus, rpc, settings,
|
||||
language, menus, rpc, settings, theme_selector,
|
||||
workspace::{self, OpenParams},
|
||||
worktree::{self},
|
||||
AppState,
|
||||
@ -20,13 +21,17 @@ fn main() {
|
||||
|
||||
let app = gpui::App::new(assets::Assets).unwrap();
|
||||
|
||||
let (_, settings) = settings::channel(&app.font_cache()).unwrap();
|
||||
let themes = settings::ThemeRegistry::new(assets::Assets);
|
||||
let (settings_tx, settings) =
|
||||
settings::channel_with_themes(&app.font_cache(), &themes).unwrap();
|
||||
let languages = Arc::new(language::LanguageRegistry::new());
|
||||
languages.set_theme(&settings.borrow().theme);
|
||||
|
||||
let mut app_state = AppState {
|
||||
languages: languages.clone(),
|
||||
settings_tx: Arc::new(Mutex::new(settings_tx)),
|
||||
settings,
|
||||
themes,
|
||||
rpc_router: Arc::new(ForegroundRouter::new()),
|
||||
rpc: rpc::Client::new(languages),
|
||||
fs: Arc::new(RealFs),
|
||||
@ -38,12 +43,14 @@ fn main() {
|
||||
&app_state.rpc,
|
||||
Arc::get_mut(&mut app_state.rpc_router).unwrap(),
|
||||
);
|
||||
let app_state = Arc::new(app_state);
|
||||
|
||||
zed::init(cx);
|
||||
workspace::init(cx);
|
||||
editor::init(cx);
|
||||
file_finder::init(cx);
|
||||
theme_selector::init(cx, &app_state);
|
||||
|
||||
let app_state = Arc::new(app_state);
|
||||
cx.set_menus(menus::menus(&app_state.clone()));
|
||||
|
||||
if stdout_is_a_pty() {
|
||||
|
@ -1,20 +1,10 @@
|
||||
use super::assets::Assets;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use gpui::{
|
||||
color::ColorU,
|
||||
font_cache::{FamilyId, FontCache},
|
||||
fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight},
|
||||
};
|
||||
use crate::theme::{self, DEFAULT_THEME_NAME};
|
||||
use anyhow::Result;
|
||||
use gpui::font_cache::{FamilyId, FontCache};
|
||||
use postage::watch;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX);
|
||||
pub use theme::{HighlightId, HighlightMap, Theme, ThemeRegistry};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Settings {
|
||||
@ -26,71 +16,19 @@ pub struct Settings {
|
||||
pub theme: Arc<Theme>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Theme {
|
||||
pub ui: UiTheme,
|
||||
pub editor: EditorTheme,
|
||||
syntax: Vec<(String, ColorU, FontProperties)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct UiTheme {
|
||||
pub tab_background: Color,
|
||||
pub tab_background_active: Color,
|
||||
pub tab_text: Color,
|
||||
pub tab_text_active: Color,
|
||||
pub tab_border: Color,
|
||||
pub tab_icon_close: Color,
|
||||
pub tab_icon_dirty: Color,
|
||||
pub tab_icon_conflict: Color,
|
||||
pub modal_background: Color,
|
||||
pub modal_match_background: Color,
|
||||
pub modal_match_background_active: Color,
|
||||
pub modal_match_border: Color,
|
||||
pub modal_match_text: Color,
|
||||
pub modal_match_text_highlight: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct EditorTheme {
|
||||
pub background: Color,
|
||||
pub gutter_background: Color,
|
||||
pub active_line_background: Color,
|
||||
pub line_number: Color,
|
||||
pub line_number_active: Color,
|
||||
pub default_text: Color,
|
||||
pub replicas: Vec<ReplicaTheme>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Deserialize)]
|
||||
pub struct ReplicaTheme {
|
||||
pub cursor: Color,
|
||||
pub selection: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Color(pub ColorU);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ThemeMap(Arc<[StyleId]>);
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct StyleId(u32);
|
||||
|
||||
impl Settings {
|
||||
pub fn new(font_cache: &FontCache) -> Result<Self> {
|
||||
Self::new_with_theme(font_cache, Arc::new(Theme::default()))
|
||||
}
|
||||
|
||||
pub fn new_with_theme(font_cache: &FontCache, theme: Arc<Theme>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
buffer_font_family: font_cache.load_family(&["Fira Code", "Monaco"])?,
|
||||
buffer_font_size: 14.0,
|
||||
tab_size: 4,
|
||||
ui_font_family: font_cache.load_family(&["SF Pro", "Helvetica"])?,
|
||||
ui_font_size: 12.0,
|
||||
theme: Arc::new(
|
||||
Theme::parse(Assets::get("themes/dark.toml").unwrap())
|
||||
.expect("Failed to parse built-in theme"),
|
||||
),
|
||||
theme,
|
||||
})
|
||||
}
|
||||
|
||||
@ -100,275 +38,23 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn parse(source: impl AsRef<[u8]>) -> Result<Self> {
|
||||
#[derive(Deserialize)]
|
||||
struct ThemeToml {
|
||||
#[serde(default)]
|
||||
ui: UiTheme,
|
||||
#[serde(default)]
|
||||
editor: EditorTheme,
|
||||
#[serde(default)]
|
||||
syntax: HashMap<String, StyleToml>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum StyleToml {
|
||||
Color(Color),
|
||||
Full {
|
||||
color: Option<Color>,
|
||||
weight: Option<toml::Value>,
|
||||
#[serde(default)]
|
||||
italic: bool,
|
||||
},
|
||||
}
|
||||
|
||||
let theme_toml: ThemeToml =
|
||||
toml::from_slice(source.as_ref()).context("failed to parse theme TOML")?;
|
||||
|
||||
let mut syntax = Vec::<(String, ColorU, FontProperties)>::new();
|
||||
for (key, style) in theme_toml.syntax {
|
||||
let (color, weight, italic) = match style {
|
||||
StyleToml::Color(color) => (color, None, false),
|
||||
StyleToml::Full {
|
||||
color,
|
||||
weight,
|
||||
italic,
|
||||
} => (color.unwrap_or(Color::default()), weight, italic),
|
||||
};
|
||||
match syntax.binary_search_by_key(&&key, |e| &e.0) {
|
||||
Ok(i) | Err(i) => {
|
||||
let mut properties = FontProperties::new();
|
||||
properties.weight = deserialize_weight(weight)?;
|
||||
if italic {
|
||||
properties.style = FontStyle::Italic;
|
||||
}
|
||||
syntax.insert(i, (key, color.0, properties));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Theme {
|
||||
ui: theme_toml.ui,
|
||||
editor: theme_toml.editor,
|
||||
syntax,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn syntax_style(&self, id: StyleId) -> (ColorU, FontProperties) {
|
||||
self.syntax.get(id.0 as usize).map_or(
|
||||
(self.editor.default_text.0, FontProperties::new()),
|
||||
|entry| (entry.1, entry.2),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> {
|
||||
self.syntax.get(id.0 as usize).map(|e| e.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ThemeMap {
|
||||
pub fn new(capture_names: &[String], theme: &Theme) -> Self {
|
||||
// For each capture name in the highlight query, find the longest
|
||||
// key in the theme's syntax styles that matches all of the
|
||||
// dot-separated components of the capture name.
|
||||
ThemeMap(
|
||||
capture_names
|
||||
.iter()
|
||||
.map(|capture_name| {
|
||||
theme
|
||||
.syntax
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, (key, _, _))| {
|
||||
let mut len = 0;
|
||||
let capture_parts = capture_name.split('.');
|
||||
for key_part in key.split('.') {
|
||||
if capture_parts.clone().any(|part| part == key_part) {
|
||||
len += 1;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some((i, len))
|
||||
})
|
||||
.max_by_key(|(_, len)| *len)
|
||||
.map_or(DEFAULT_STYLE_ID, |(i, _)| StyleId(i as u32))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get(&self, capture_id: u32) -> StyleId {
|
||||
self.0
|
||||
.get(capture_id as usize)
|
||||
.copied()
|
||||
.unwrap_or(DEFAULT_STYLE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThemeMap {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new([]))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StyleId {
|
||||
fn default() -> Self {
|
||||
DEFAULT_STYLE_ID
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Color {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let rgba_value = u32::deserialize(deserializer)?;
|
||||
Ok(Self(ColorU::from_u32((rgba_value << 8) + 0xFF)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<ColorU> for Color {
|
||||
fn into(self) -> ColorU {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Color {
|
||||
type Target = ColorU;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Color {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Color {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<ColorU> for Color {
|
||||
fn eq(&self, other: &ColorU) -> bool {
|
||||
self.0.eq(other)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel(
|
||||
font_cache: &FontCache,
|
||||
) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
|
||||
Ok(watch::channel_with(Settings::new(font_cache)?))
|
||||
}
|
||||
|
||||
fn deserialize_weight(weight: Option<toml::Value>) -> Result<FontWeight> {
|
||||
match &weight {
|
||||
None => return Ok(FontWeight::NORMAL),
|
||||
Some(toml::Value::Integer(i)) => return Ok(FontWeight(*i as f32)),
|
||||
Some(toml::Value::String(s)) => match s.as_str() {
|
||||
"normal" => return Ok(FontWeight::NORMAL),
|
||||
"bold" => return Ok(FontWeight::BOLD),
|
||||
"light" => return Ok(FontWeight::LIGHT),
|
||||
"semibold" => return Ok(FontWeight::SEMIBOLD),
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Err(anyhow!("Invalid weight {}", weight.unwrap()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_theme() {
|
||||
let theme = Theme::parse(
|
||||
r#"
|
||||
[ui]
|
||||
tab_background_active = 0x100000
|
||||
|
||||
[editor]
|
||||
background = 0x00ed00
|
||||
line_number = 0xdddddd
|
||||
|
||||
[syntax]
|
||||
"beta.two" = 0xAABBCC
|
||||
"alpha.one" = {color = 0x112233, weight = "bold"}
|
||||
"gamma.three" = {weight = "light", italic = true}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(theme.ui.tab_background_active, ColorU::from_u32(0x100000ff));
|
||||
assert_eq!(theme.editor.background, ColorU::from_u32(0x00ed00ff));
|
||||
assert_eq!(theme.editor.line_number, ColorU::from_u32(0xddddddff));
|
||||
assert_eq!(
|
||||
theme.syntax,
|
||||
&[
|
||||
(
|
||||
"alpha.one".to_string(),
|
||||
ColorU::from_u32(0x112233ff),
|
||||
*FontProperties::new().weight(FontWeight::BOLD)
|
||||
),
|
||||
(
|
||||
"beta.two".to_string(),
|
||||
ColorU::from_u32(0xaabbccff),
|
||||
*FontProperties::new().weight(FontWeight::NORMAL)
|
||||
),
|
||||
(
|
||||
"gamma.three".to_string(),
|
||||
ColorU::from_u32(0x00000000),
|
||||
*FontProperties::new()
|
||||
.weight(FontWeight::LIGHT)
|
||||
.style(FontStyle::Italic),
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty_theme() {
|
||||
Theme::parse("").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_map() {
|
||||
let theme = Theme {
|
||||
ui: Default::default(),
|
||||
editor: Default::default(),
|
||||
syntax: [
|
||||
("function", ColorU::from_u32(0x100000ff)),
|
||||
("function.method", ColorU::from_u32(0x200000ff)),
|
||||
("function.async", ColorU::from_u32(0x300000ff)),
|
||||
("variable.builtin.self.rust", ColorU::from_u32(0x400000ff)),
|
||||
("variable.builtin", ColorU::from_u32(0x500000ff)),
|
||||
("variable", ColorU::from_u32(0x600000ff)),
|
||||
]
|
||||
.iter()
|
||||
.map(|e| (e.0.to_string(), e.1, FontProperties::new()))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let capture_names = &[
|
||||
"function.special".to_string(),
|
||||
"function.async.rust".to_string(),
|
||||
"variable.builtin.self".to_string(),
|
||||
];
|
||||
|
||||
let map = ThemeMap::new(capture_names, &theme);
|
||||
assert_eq!(theme.syntax_style_name(map.get(0)), Some("function"));
|
||||
assert_eq!(theme.syntax_style_name(map.get(1)), Some("function.async"));
|
||||
assert_eq!(
|
||||
theme.syntax_style_name(map.get(2)),
|
||||
Some("variable.builtin")
|
||||
);
|
||||
}
|
||||
pub fn channel_with_themes(
|
||||
font_cache: &FontCache,
|
||||
themes: &ThemeRegistry,
|
||||
) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
|
||||
let theme = match themes.get(DEFAULT_THEME_NAME) {
|
||||
Ok(theme) => theme,
|
||||
Err(err) => {
|
||||
panic!("failed to deserialize default theme: {:?}", err)
|
||||
}
|
||||
};
|
||||
Ok(watch::channel_with(Settings::new_with_theme(
|
||||
font_cache, theme,
|
||||
)?))
|
||||
}
|
||||
|
@ -1,5 +1,13 @@
|
||||
use crate::{fs::RealFs, language::LanguageRegistry, rpc, settings, time::ReplicaId, AppState};
|
||||
use crate::{
|
||||
fs::RealFs,
|
||||
language::LanguageRegistry,
|
||||
rpc,
|
||||
settings::{self, ThemeRegistry},
|
||||
time::ReplicaId,
|
||||
AppState,
|
||||
};
|
||||
use gpui::{AppContext, Entity, ModelHandle};
|
||||
use parking_lot::Mutex;
|
||||
use smol::channel;
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
@ -147,10 +155,13 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
|
||||
}
|
||||
|
||||
pub fn build_app_state(cx: &AppContext) -> Arc<AppState> {
|
||||
let settings = settings::channel(&cx.font_cache()).unwrap().1;
|
||||
let (settings_tx, settings) = settings::channel(&cx.font_cache()).unwrap();
|
||||
let languages = Arc::new(LanguageRegistry::new());
|
||||
let themes = ThemeRegistry::new(());
|
||||
Arc::new(AppState {
|
||||
settings_tx: Arc::new(Mutex::new(settings_tx)),
|
||||
settings,
|
||||
themes,
|
||||
languages: languages.clone(),
|
||||
rpc_router: Arc::new(ForegroundRouter::new()),
|
||||
rpc: rpc::Client::new(languages),
|
||||
|
626
zed/src/theme.rs
Normal file
626
zed/src/theme.rs
Normal 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
306
zed/src/theme_selector.rs
Normal 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(¤t_theme_name) {
|
||||
Ok(theme) => {
|
||||
cx.notify_all();
|
||||
app_state.settings_tx.lock().borrow_mut().theme = theme;
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("failed to load theme {}: {:?}", current_theme_name, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &(), cx: &mut ViewContext<Self>) {
|
||||
if let Some(mat) = self.matches.get(self.selected_index) {
|
||||
if let Ok(theme) = self.registry.get(&mat.string) {
|
||||
self.settings_tx.lock().borrow_mut().theme = theme;
|
||||
cx.notify_all();
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &(), cx: &mut ViewContext<Self>) {
|
||||
if self.selected_index > 0 {
|
||||
self.selected_index -= 1;
|
||||
}
|
||||
self.list_state.scroll_to(self.selected_index);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &(), cx: &mut ViewContext<Self>) {
|
||||
if self.selected_index + 1 < self.matches.len() {
|
||||
self.selected_index += 1;
|
||||
}
|
||||
self.list_state.scroll_to(self.selected_index);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// fn select(&mut self, selected_index: &usize, cx: &mut ViewContext<Self>) {
|
||||
// self.selected_index = *selected_index;
|
||||
// self.confirm(&(), cx);
|
||||
// }
|
||||
|
||||
fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let background = cx.background().clone();
|
||||
let candidates = self
|
||||
.registry
|
||||
.list()
|
||||
.map(|name| StringMatchCandidate {
|
||||
char_bag: name.as_str().into(),
|
||||
string: name,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx));
|
||||
|
||||
self.matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.map(|candidate| StringMatch {
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
smol::block_on(match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
workspace: &mut Workspace,
|
||||
_: ViewHandle<ThemeSelector>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
Event::Dismissed => {
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_query_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
event: &editor::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Edited => self.update_matches(cx),
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_matches(&self, cx: &RenderContext<Self>) -> ElementBox {
|
||||
if self.matches.is_empty() {
|
||||
let settings = self.settings.borrow();
|
||||
return Container::new(
|
||||
Label::new(
|
||||
"No matches".into(),
|
||||
settings.ui_font_family,
|
||||
settings.ui_font_size,
|
||||
)
|
||||
.with_style(&settings.theme.ui.selector.label)
|
||||
.boxed(),
|
||||
)
|
||||
.with_margin_top(6.0)
|
||||
.named("empty matches");
|
||||
}
|
||||
|
||||
let handle = cx.handle();
|
||||
let list = UniformList::new(
|
||||
self.list_state.clone(),
|
||||
self.matches.len(),
|
||||
move |mut range, items, cx| {
|
||||
let cx = cx.as_ref();
|
||||
let selector = handle.upgrade(cx).unwrap();
|
||||
let selector = selector.read(cx);
|
||||
let start = range.start;
|
||||
range.end = cmp::min(range.end, selector.matches.len());
|
||||
items.extend(
|
||||
selector.matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(i, path_match)| selector.render_match(path_match, start + i)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Container::new(list.boxed())
|
||||
.with_margin_top(6.0)
|
||||
.named("matches")
|
||||
}
|
||||
|
||||
fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
let theme = &settings.theme.ui;
|
||||
|
||||
let container = Container::new(
|
||||
Label::new(
|
||||
theme_match.string.clone(),
|
||||
settings.ui_font_family,
|
||||
settings.ui_font_size,
|
||||
)
|
||||
.with_style(if index == self.selected_index {
|
||||
&theme.selector.active_item.label
|
||||
} else {
|
||||
&theme.selector.item.label
|
||||
})
|
||||
.with_highlights(theme_match.positions.clone())
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(if index == self.selected_index {
|
||||
&theme.selector.active_item.container
|
||||
} else {
|
||||
&theme.selector.item.container
|
||||
});
|
||||
|
||||
container.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ThemeSelector {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for ThemeSelector {
|
||||
fn ui_name() -> &'static str {
|
||||
"ThemeSelector"
|
||||
}
|
||||
|
||||
fn render(&self, cx: &RenderContext<Self>) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(ChildView::new(self.query_buffer.id()).boxed())
|
||||
.with_child(Expanded::new(1.0, self.render_matches(cx)).boxed())
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(&settings.theme.ui.selector.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_max_width(600.0)
|
||||
.with_max_height(400.0)
|
||||
.boxed(),
|
||||
)
|
||||
.top()
|
||||
.named("theme selector")
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.focus(&self.query_buffer);
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx
|
||||
}
|
||||
}
|
@ -13,8 +13,8 @@ use crate::{
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{
|
||||
elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, ClipboardItem,
|
||||
Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, Task, View,
|
||||
ViewContext, ViewHandle, WeakModelHandle,
|
||||
Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task,
|
||||
View, ViewContext, ViewHandle, WeakModelHandle,
|
||||
};
|
||||
use log::error;
|
||||
pub use pane::*;
|
||||
@ -879,7 +879,7 @@ impl View for Workspace {
|
||||
"Workspace"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> ElementBox {
|
||||
fn render(&self, _: &RenderContext<Self>) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
Container::new(
|
||||
Stack::new()
|
||||
@ -887,7 +887,7 @@ impl View for Workspace {
|
||||
.with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
|
||||
.boxed(),
|
||||
)
|
||||
.with_background_color(settings.theme.editor.background)
|
||||
.with_background_color(settings.theme.ui.background)
|
||||
.named("workspace")
|
||||
}
|
||||
|
||||
@ -911,7 +911,7 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
|
||||
let tree_id = tree.id();
|
||||
tree.read(cx)
|
||||
.files(0)
|
||||
.map(move |f| (tree_id, f.path().clone()))
|
||||
.map(move |f| (tree_id, f.path.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
@ -974,8 +974,8 @@ mod tests {
|
||||
})
|
||||
.await;
|
||||
assert_eq!(cx.window_ids().len(), 1);
|
||||
let workspace_view_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
|
||||
workspace_view_1.read_with(&cx, |workspace, _| {
|
||||
let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
|
||||
workspace_1.read_with(&cx, |workspace, _| {
|
||||
assert_eq!(workspace.worktrees().len(), 2)
|
||||
});
|
||||
|
||||
@ -1397,9 +1397,9 @@ mod tests {
|
||||
assert_eq!(pane2_item.entry_id(cx.as_ref()), Some(file1.clone()));
|
||||
|
||||
cx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
|
||||
let workspace_view = workspace.read(cx);
|
||||
assert_eq!(workspace_view.panes.len(), 1);
|
||||
assert_eq!(workspace_view.active_pane(), &pane_1);
|
||||
let workspace = workspace.read(cx);
|
||||
assert_eq!(workspace.panes.len(), 1);
|
||||
assert_eq!(workspace.active_pane(), &pane_1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
use super::{ItemViewHandle, SplitDirection};
|
||||
use crate::settings::{Settings, UiTheme};
|
||||
use crate::{settings::Settings, theme};
|
||||
use gpui::{
|
||||
color::ColorU,
|
||||
color::Color,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
keymap::Binding,
|
||||
AppContext, Border, Entity, MutableAppContext, Quad, View, ViewContext, ViewHandle,
|
||||
AppContext, Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use std::{cmp, path::Path, sync::Arc};
|
||||
@ -192,6 +193,7 @@ impl Pane {
|
||||
let is_active = ix == self.active_item;
|
||||
|
||||
enum Tab {}
|
||||
let border = &theme.tab.container.border;
|
||||
|
||||
row.add_child(
|
||||
Expanded::new(
|
||||
@ -199,10 +201,10 @@ impl Pane {
|
||||
MouseEventHandler::new::<Tab, _>(item.id(), cx, |mouse_state| {
|
||||
let title = item.title(cx);
|
||||
|
||||
let mut border = Border::new(1.0, theme.tab_border.0);
|
||||
let mut border = border.clone();
|
||||
border.left = ix > 0;
|
||||
border.right = ix == last_item_ix;
|
||||
border.bottom = ix != self.active_item;
|
||||
border.bottom = !is_active;
|
||||
|
||||
let mut container = Container::new(
|
||||
Stack::new()
|
||||
@ -213,10 +215,10 @@ impl Pane {
|
||||
settings.ui_font_family,
|
||||
settings.ui_font_size,
|
||||
)
|
||||
.with_default_color(if is_active {
|
||||
theme.tab_text_active.0
|
||||
.with_style(if is_active {
|
||||
&theme.active_tab.label
|
||||
} else {
|
||||
theme.tab_text.0
|
||||
&theme.tab.label
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
@ -237,15 +239,15 @@ impl Pane {
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_horizontal_padding(10.)
|
||||
.with_style(if is_active {
|
||||
&theme.active_tab.container
|
||||
} else {
|
||||
&theme.tab.container
|
||||
})
|
||||
.with_border(border);
|
||||
|
||||
if is_active {
|
||||
container = container
|
||||
.with_background_color(theme.tab_background_active)
|
||||
.with_padding_bottom(border.width);
|
||||
} else {
|
||||
container = container.with_background_color(theme.tab_background);
|
||||
container = container.with_padding_bottom(border.width);
|
||||
}
|
||||
|
||||
ConstrainedBox::new(
|
||||
@ -268,10 +270,13 @@ impl Pane {
|
||||
|
||||
// Ensure there's always a minimum amount of space after the last tab,
|
||||
// so that the tab's border doesn't abut the window's border.
|
||||
let mut border = Border::bottom(1.0, Color::default());
|
||||
border.color = theme.tab.container.border.color;
|
||||
|
||||
row.add_child(
|
||||
ConstrainedBox::new(
|
||||
Container::new(Empty::new().boxed())
|
||||
.with_border(Border::bottom(1.0, theme.tab_border))
|
||||
.with_border(border)
|
||||
.boxed(),
|
||||
)
|
||||
.with_min_width(20.)
|
||||
@ -282,7 +287,7 @@ impl Pane {
|
||||
Expanded::new(
|
||||
0.0,
|
||||
Container::new(Empty::new().boxed())
|
||||
.with_border(Border::bottom(1.0, theme.tab_border))
|
||||
.with_border(border)
|
||||
.boxed(),
|
||||
)
|
||||
.named("filler"),
|
||||
@ -299,33 +304,33 @@ impl Pane {
|
||||
tab_hovered: bool,
|
||||
is_dirty: bool,
|
||||
has_conflict: bool,
|
||||
theme: &UiTheme,
|
||||
theme: &theme::Ui,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
enum TabCloseButton {}
|
||||
|
||||
let mut clicked_color = theme.tab_icon_dirty;
|
||||
let mut clicked_color = theme.tab.icon_dirty;
|
||||
clicked_color.a = 180;
|
||||
|
||||
let current_color = if has_conflict {
|
||||
Some(theme.tab_icon_conflict)
|
||||
Some(theme.tab.icon_conflict)
|
||||
} else if is_dirty {
|
||||
Some(theme.tab_icon_dirty)
|
||||
Some(theme.tab.icon_dirty)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let icon = if tab_hovered {
|
||||
let close_color = current_color.unwrap_or(theme.tab_icon_close).0;
|
||||
let close_color = current_color.unwrap_or(theme.tab.icon_close);
|
||||
let icon = Svg::new("icons/x.svg").with_color(close_color);
|
||||
|
||||
MouseEventHandler::new::<TabCloseButton, _>(item_id, cx, |mouse_state| {
|
||||
if mouse_state.hovered {
|
||||
Container::new(icon.with_color(ColorU::white()).boxed())
|
||||
Container::new(icon.with_color(Color::white()).boxed())
|
||||
.with_background_color(if mouse_state.clicked {
|
||||
clicked_color
|
||||
} else {
|
||||
theme.tab_icon_dirty
|
||||
theme.tab.icon_dirty
|
||||
})
|
||||
.with_corner_radius(close_icon_size / 2.)
|
||||
.boxed()
|
||||
@ -343,7 +348,7 @@ impl Pane {
|
||||
let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: square,
|
||||
background: Some(current_color.0),
|
||||
background: Some(current_color),
|
||||
border: Default::default(),
|
||||
corner_radius: diameter / 2.,
|
||||
});
|
||||
@ -371,7 +376,7 @@ impl View for Pane {
|
||||
"Pane"
|
||||
}
|
||||
|
||||
fn render<'a>(&self, cx: &AppContext) -> ElementBox {
|
||||
fn render<'a>(&self, cx: &RenderContext<Self>) -> ElementBox {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
Flex::column()
|
||||
.with_child(self.render_tabs(cx))
|
||||
|
@ -1,9 +1,5 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{
|
||||
color::{rgbu, ColorU},
|
||||
elements::*,
|
||||
Axis, Border,
|
||||
};
|
||||
use gpui::{color::Color, elements::*, Axis, Border};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PaneGroup {
|
||||
@ -388,6 +384,6 @@ fn border_width() -> f32 {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn border_color() -> ColorU {
|
||||
rgbu(0xdb, 0xdb, 0xdc)
|
||||
fn border_color() -> Color {
|
||||
Color::new(0xdb, 0xdb, 0xdc, 0xff)
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
mod char_bag;
|
||||
mod fuzzy;
|
||||
mod ignore;
|
||||
|
||||
use self::{char_bag::CharBag, ignore::IgnoreStack};
|
||||
use self::ignore::IgnoreStack;
|
||||
use crate::{
|
||||
editor::{self, Buffer, History, Operation, Rope},
|
||||
fs::{self, Fs},
|
||||
fuzzy,
|
||||
fuzzy::CharBag,
|
||||
language::LanguageRegistry,
|
||||
rpc::{self, proto},
|
||||
sum_tree::{self, Cursor, Edit, SumTree},
|
||||
@ -1116,6 +1116,10 @@ pub struct Snapshot {
|
||||
}
|
||||
|
||||
impl Snapshot {
|
||||
pub fn id(&self) -> usize {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn build_update(&self, other: &Self, worktree_id: u64) -> proto::UpdateWorktree {
|
||||
let mut updated_entries = Vec::new();
|
||||
let mut removed_entries = Vec::new();
|
||||
@ -1214,7 +1218,7 @@ impl Snapshot {
|
||||
self.entries_by_path
|
||||
.cursor::<(), ()>()
|
||||
.filter(move |entry| entry.path.as_ref() != empty_path)
|
||||
.map(|entry| entry.path())
|
||||
.map(|entry| &entry.path)
|
||||
}
|
||||
|
||||
pub fn visible_files(&self, start: usize) -> FileIter {
|
||||
@ -1248,17 +1252,17 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
pub fn inode_for_path(&self, path: impl AsRef<Path>) -> Option<u64> {
|
||||
self.entry_for_path(path.as_ref()).map(|e| e.inode())
|
||||
self.entry_for_path(path.as_ref()).map(|e| e.inode)
|
||||
}
|
||||
|
||||
fn insert_entry(&mut self, mut entry: Entry) -> Entry {
|
||||
if !entry.is_dir() && entry.path().file_name() == Some(&GITIGNORE) {
|
||||
let (ignore, err) = Gitignore::new(self.abs_path.join(entry.path()));
|
||||
if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) {
|
||||
let (ignore, err) = Gitignore::new(self.abs_path.join(&entry.path));
|
||||
if let Some(err) = err {
|
||||
log::error!("error in ignore file {:?} - {:?}", entry.path(), err);
|
||||
log::error!("error in ignore file {:?} - {:?}", &entry.path, err);
|
||||
}
|
||||
|
||||
let ignore_dir_path = entry.path().parent().unwrap();
|
||||
let ignore_dir_path = entry.path.parent().unwrap();
|
||||
self.ignores
|
||||
.insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id));
|
||||
}
|
||||
@ -1381,10 +1385,10 @@ impl Snapshot {
|
||||
impl fmt::Debug for Snapshot {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for entry in self.entries_by_path.cursor::<(), ()>() {
|
||||
for _ in entry.path().ancestors().skip(1) {
|
||||
for _ in entry.path.ancestors().skip(1) {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
writeln!(f, "{:?} (inode: {})", entry.path(), entry.inode())?;
|
||||
writeln!(f, "{:?} (inode: {})", entry.path, entry.inode)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -1535,19 +1539,19 @@ impl File {
|
||||
}
|
||||
|
||||
pub fn entry_id(&self) -> (usize, Arc<Path>) {
|
||||
(self.worktree.id(), self.path())
|
||||
(self.worktree.id(), self.path.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Entry {
|
||||
id: usize,
|
||||
kind: EntryKind,
|
||||
path: Arc<Path>,
|
||||
inode: u64,
|
||||
mtime: SystemTime,
|
||||
is_symlink: bool,
|
||||
is_ignored: bool,
|
||||
pub id: usize,
|
||||
pub kind: EntryKind,
|
||||
pub path: Arc<Path>,
|
||||
pub inode: u64,
|
||||
pub mtime: SystemTime,
|
||||
pub is_symlink: bool,
|
||||
pub is_ignored: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -1579,23 +1583,11 @@ impl Entry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Arc<Path> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn inode(&self) -> u64 {
|
||||
self.inode
|
||||
}
|
||||
|
||||
pub fn is_ignored(&self) -> bool {
|
||||
self.is_ignored
|
||||
}
|
||||
|
||||
fn is_dir(&self) -> bool {
|
||||
pub fn is_dir(&self) -> bool {
|
||||
matches!(self.kind, EntryKind::Dir | EntryKind::PendingDir)
|
||||
}
|
||||
|
||||
fn is_file(&self) -> bool {
|
||||
pub fn is_file(&self) -> bool {
|
||||
matches!(self.kind, EntryKind::File(_))
|
||||
}
|
||||
}
|
||||
@ -1619,7 +1611,7 @@ impl sum_tree::Item for Entry {
|
||||
}
|
||||
|
||||
EntrySummary {
|
||||
max_path: self.path().clone(),
|
||||
max_path: self.path.clone(),
|
||||
file_count,
|
||||
visible_file_count,
|
||||
}
|
||||
@ -1630,7 +1622,7 @@ impl sum_tree::KeyedItem for Entry {
|
||||
type Key = PathKey;
|
||||
|
||||
fn key(&self) -> Self::Key {
|
||||
PathKey(self.path().clone())
|
||||
PathKey(self.path.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@ -2147,7 +2139,7 @@ impl BackgroundScanner {
|
||||
let mut edits = Vec::new();
|
||||
for mut entry in snapshot.child_entries(&job.path).cloned() {
|
||||
let was_ignored = entry.is_ignored;
|
||||
entry.is_ignored = ignore_stack.is_path_ignored(entry.path(), entry.is_dir());
|
||||
entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir());
|
||||
if entry.is_dir() {
|
||||
let child_ignore_stack = if entry.is_ignored {
|
||||
IgnoreStack::all()
|
||||
@ -2156,7 +2148,7 @@ impl BackgroundScanner {
|
||||
};
|
||||
job.ignore_queue
|
||||
.send(UpdateIgnoreStatusJob {
|
||||
path: entry.path().clone(),
|
||||
path: entry.path.clone(),
|
||||
ignore_stack: child_ignore_stack,
|
||||
ignore_queue: job.ignore_queue.clone(),
|
||||
})
|
||||
@ -2333,9 +2325,9 @@ impl<'a> Iterator for ChildEntriesIter<'a> {
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(item) = self.cursor.item() {
|
||||
if item.path().starts_with(self.parent_path) {
|
||||
if item.path.starts_with(self.parent_path) {
|
||||
self.cursor
|
||||
.seek_forward(&PathSearch::Successor(item.path()), Bias::Left, &());
|
||||
.seek_forward(&PathSearch::Successor(&item.path), Bias::Left, &());
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
@ -2608,6 +2600,7 @@ mod tests {
|
||||
);
|
||||
tree.snapshot()
|
||||
})];
|
||||
let cancel_flag = Default::default();
|
||||
let results = cx
|
||||
.read(|cx| {
|
||||
match_paths(
|
||||
@ -2616,7 +2609,7 @@ mod tests {
|
||||
false,
|
||||
false,
|
||||
10,
|
||||
Default::default(),
|
||||
&cancel_flag,
|
||||
cx.background().clone(),
|
||||
)
|
||||
})
|
||||
@ -2659,6 +2652,7 @@ mod tests {
|
||||
assert_eq!(tree.file_count(), 0);
|
||||
tree.snapshot()
|
||||
})];
|
||||
let cancel_flag = Default::default();
|
||||
let results = cx
|
||||
.read(|cx| {
|
||||
match_paths(
|
||||
@ -2667,7 +2661,7 @@ mod tests {
|
||||
false,
|
||||
false,
|
||||
10,
|
||||
Default::default(),
|
||||
&cancel_flag,
|
||||
cx.background().clone(),
|
||||
)
|
||||
})
|
||||
@ -2928,8 +2922,8 @@ mod tests {
|
||||
let tree = tree.read(cx);
|
||||
let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap();
|
||||
let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap();
|
||||
assert_eq!(tracked.is_ignored(), false);
|
||||
assert_eq!(ignored.is_ignored(), true);
|
||||
assert_eq!(tracked.is_ignored, false);
|
||||
assert_eq!(ignored.is_ignored, true);
|
||||
});
|
||||
|
||||
std::fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
|
||||
@ -2940,9 +2934,9 @@ mod tests {
|
||||
let dot_git = tree.entry_for_path(".git").unwrap();
|
||||
let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap();
|
||||
let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap();
|
||||
assert_eq!(tracked.is_ignored(), false);
|
||||
assert_eq!(ignored.is_ignored(), true);
|
||||
assert_eq!(dot_git.is_ignored(), true);
|
||||
assert_eq!(tracked.is_ignored, false);
|
||||
assert_eq!(ignored.is_ignored, true);
|
||||
assert_eq!(dot_git.is_ignored, true);
|
||||
});
|
||||
}
|
||||
|
||||
@ -3175,9 +3169,9 @@ mod tests {
|
||||
let mut visible_files = self.visible_files(0);
|
||||
for entry in self.entries_by_path.cursor::<(), ()>() {
|
||||
if entry.is_file() {
|
||||
assert_eq!(files.next().unwrap().inode(), entry.inode);
|
||||
assert_eq!(files.next().unwrap().inode, entry.inode);
|
||||
if !entry.is_ignored {
|
||||
assert_eq!(visible_files.next().unwrap().inode(), entry.inode);
|
||||
assert_eq!(visible_files.next().unwrap().inode, entry.inode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3190,14 +3184,14 @@ mod tests {
|
||||
bfs_paths.push(path);
|
||||
let ix = stack.len();
|
||||
for child_entry in self.child_entries(path) {
|
||||
stack.insert(ix, child_entry.path());
|
||||
stack.insert(ix, &child_entry.path);
|
||||
}
|
||||
}
|
||||
|
||||
let dfs_paths = self
|
||||
.entries_by_path
|
||||
.cursor::<(), ()>()
|
||||
.map(|e| e.path().as_ref())
|
||||
.map(|e| e.path.as_ref())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(bfs_paths, dfs_paths);
|
||||
|
||||
@ -3212,7 +3206,7 @@ mod tests {
|
||||
fn to_vec(&self) -> Vec<(&Path, u64, bool)> {
|
||||
let mut paths = Vec::new();
|
||||
for entry in self.entries_by_path.cursor::<(), ()>() {
|
||||
paths.push((entry.path().as_ref(), entry.inode(), entry.is_ignored()));
|
||||
paths.push((entry.path.as_ref(), entry.inode, entry.is_ignored));
|
||||
}
|
||||
paths.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
paths
|
||||
|
@ -1,659 +0,0 @@
|
||||
use super::{char_bag::CharBag, EntryKind, Snapshot};
|
||||
use crate::util;
|
||||
use gpui::executor;
|
||||
use std::{
|
||||
cmp::{max, min, Ordering},
|
||||
path::Path,
|
||||
sync::atomic::{self, AtomicBool},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
const BASE_DISTANCE_PENALTY: f64 = 0.6;
|
||||
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
|
||||
const MIN_DISTANCE_PENALTY: f64 = 0.2;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MatchCandidate<'a> {
|
||||
pub path: &'a Arc<Path>,
|
||||
pub char_bag: CharBag,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathMatch {
|
||||
pub score: f64,
|
||||
pub positions: Vec<usize>,
|
||||
pub tree_id: usize,
|
||||
pub path: Arc<Path>,
|
||||
pub path_prefix: Arc<str>,
|
||||
}
|
||||
|
||||
impl PartialEq for PathMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.score.eq(&other.score)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PathMatch {}
|
||||
|
||||
impl PartialOrd for PathMatch {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for PathMatch {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.score
|
||||
.partial_cmp(&other.score)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
.then_with(|| self.tree_id.cmp(&other.tree_id))
|
||||
.then_with(|| Arc::as_ptr(&self.path).cmp(&Arc::as_ptr(&other.path)))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn match_paths(
|
||||
snapshots: &[Snapshot],
|
||||
query: &str,
|
||||
include_ignored: bool,
|
||||
smart_case: bool,
|
||||
max_results: usize,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
background: Arc<executor::Background>,
|
||||
) -> Vec<PathMatch> {
|
||||
let path_count: usize = if include_ignored {
|
||||
snapshots.iter().map(Snapshot::file_count).sum()
|
||||
} else {
|
||||
snapshots.iter().map(Snapshot::visible_file_count).sum()
|
||||
};
|
||||
if path_count == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
||||
let query = query.chars().collect::<Vec<_>>();
|
||||
|
||||
let lowercase_query = &lowercase_query;
|
||||
let query = &query;
|
||||
let query_chars = CharBag::from(&lowercase_query[..]);
|
||||
|
||||
let num_cpus = background.num_cpus().min(path_count);
|
||||
let segment_size = (path_count + num_cpus - 1) / num_cpus;
|
||||
let mut segment_results = (0..num_cpus)
|
||||
.map(|_| Vec::with_capacity(max_results))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
background
|
||||
.scoped(|scope| {
|
||||
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
||||
let cancel_flag = &cancel_flag;
|
||||
scope.spawn(async move {
|
||||
let segment_start = segment_idx * segment_size;
|
||||
let segment_end = segment_start + segment_size;
|
||||
|
||||
let mut min_score = 0.0;
|
||||
let mut last_positions = Vec::new();
|
||||
last_positions.resize(query.len(), 0);
|
||||
let mut match_positions = Vec::new();
|
||||
match_positions.resize(query.len(), 0);
|
||||
let mut score_matrix = Vec::new();
|
||||
let mut best_position_matrix = Vec::new();
|
||||
|
||||
let mut tree_start = 0;
|
||||
for snapshot in snapshots {
|
||||
let tree_end = if include_ignored {
|
||||
tree_start + snapshot.file_count()
|
||||
} else {
|
||||
tree_start + snapshot.visible_file_count()
|
||||
};
|
||||
|
||||
if tree_start < segment_end && segment_start < tree_end {
|
||||
let path_prefix: Arc<str> =
|
||||
if snapshot.root_entry().map_or(false, |e| e.is_file()) {
|
||||
snapshot.root_name().into()
|
||||
} else if snapshots.len() > 1 {
|
||||
format!("{}/", snapshot.root_name()).into()
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
|
||||
let start = max(tree_start, segment_start) - tree_start;
|
||||
let end = min(tree_end, segment_end) - tree_start;
|
||||
let entries = if include_ignored {
|
||||
snapshot.files(start).take(end - start)
|
||||
} else {
|
||||
snapshot.visible_files(start).take(end - start)
|
||||
};
|
||||
let paths = entries.map(|entry| {
|
||||
if let EntryKind::File(char_bag) = entry.kind {
|
||||
MatchCandidate {
|
||||
path: &entry.path,
|
||||
char_bag,
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
|
||||
match_single_tree_paths(
|
||||
snapshot,
|
||||
path_prefix,
|
||||
paths,
|
||||
query,
|
||||
lowercase_query,
|
||||
query_chars,
|
||||
smart_case,
|
||||
results,
|
||||
max_results,
|
||||
&mut min_score,
|
||||
&mut match_positions,
|
||||
&mut last_positions,
|
||||
&mut score_matrix,
|
||||
&mut best_position_matrix,
|
||||
&cancel_flag,
|
||||
);
|
||||
}
|
||||
if tree_end >= segment_end {
|
||||
break;
|
||||
}
|
||||
tree_start = tree_end;
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for segment_result in segment_results {
|
||||
if results.is_empty() {
|
||||
results = segment_result;
|
||||
} else {
|
||||
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(&a));
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
fn match_single_tree_paths<'a>(
|
||||
snapshot: &Snapshot,
|
||||
path_prefix: Arc<str>,
|
||||
path_entries: impl Iterator<Item = MatchCandidate<'a>>,
|
||||
query: &[char],
|
||||
lowercase_query: &[char],
|
||||
query_chars: CharBag,
|
||||
smart_case: bool,
|
||||
results: &mut Vec<PathMatch>,
|
||||
max_results: usize,
|
||||
min_score: &mut f64,
|
||||
match_positions: &mut Vec<usize>,
|
||||
last_positions: &mut Vec<usize>,
|
||||
score_matrix: &mut Vec<Option<f64>>,
|
||||
best_position_matrix: &mut Vec<usize>,
|
||||
cancel_flag: &AtomicBool,
|
||||
) {
|
||||
let mut path_chars = Vec::new();
|
||||
let mut lowercase_path_chars = Vec::new();
|
||||
|
||||
let prefix = path_prefix.chars().collect::<Vec<_>>();
|
||||
let lowercase_prefix = prefix
|
||||
.iter()
|
||||
.map(|c| c.to_ascii_lowercase())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for candidate in path_entries {
|
||||
if !candidate.char_bag.is_superset(query_chars) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if cancel_flag.load(atomic::Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
path_chars.clear();
|
||||
lowercase_path_chars.clear();
|
||||
for c in candidate.path.to_string_lossy().chars() {
|
||||
path_chars.push(c);
|
||||
lowercase_path_chars.push(c.to_ascii_lowercase());
|
||||
}
|
||||
|
||||
if !find_last_positions(
|
||||
last_positions,
|
||||
&lowercase_prefix,
|
||||
&lowercase_path_chars,
|
||||
lowercase_query,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let matrix_len = query.len() * (path_chars.len() + prefix.len());
|
||||
score_matrix.clear();
|
||||
score_matrix.resize(matrix_len, None);
|
||||
best_position_matrix.clear();
|
||||
best_position_matrix.resize(matrix_len, 0);
|
||||
|
||||
let score = score_match(
|
||||
query,
|
||||
lowercase_query,
|
||||
&path_chars,
|
||||
&lowercase_path_chars,
|
||||
&prefix,
|
||||
&lowercase_prefix,
|
||||
smart_case,
|
||||
&last_positions,
|
||||
score_matrix,
|
||||
best_position_matrix,
|
||||
match_positions,
|
||||
*min_score,
|
||||
);
|
||||
|
||||
if score > 0.0 {
|
||||
let mat = PathMatch {
|
||||
tree_id: snapshot.id,
|
||||
path: candidate.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
score,
|
||||
positions: match_positions.clone(),
|
||||
};
|
||||
if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) {
|
||||
if results.len() < max_results {
|
||||
results.insert(i, mat);
|
||||
} else if i < results.len() {
|
||||
results.pop();
|
||||
results.insert(i, mat);
|
||||
}
|
||||
if results.len() == max_results {
|
||||
*min_score = results.last().unwrap().score;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_last_positions(
|
||||
last_positions: &mut Vec<usize>,
|
||||
prefix: &[char],
|
||||
path: &[char],
|
||||
query: &[char],
|
||||
) -> bool {
|
||||
let mut path = path.iter();
|
||||
let mut prefix_iter = prefix.iter();
|
||||
for (i, char) in query.iter().enumerate().rev() {
|
||||
if let Some(j) = path.rposition(|c| c == char) {
|
||||
last_positions[i] = j + prefix.len();
|
||||
} else if let Some(j) = prefix_iter.rposition(|c| c == char) {
|
||||
last_positions[i] = j;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn score_match(
|
||||
query: &[char],
|
||||
query_cased: &[char],
|
||||
path: &[char],
|
||||
path_cased: &[char],
|
||||
prefix: &[char],
|
||||
lowercase_prefix: &[char],
|
||||
smart_case: bool,
|
||||
last_positions: &[usize],
|
||||
score_matrix: &mut [Option<f64>],
|
||||
best_position_matrix: &mut [usize],
|
||||
match_positions: &mut [usize],
|
||||
min_score: f64,
|
||||
) -> f64 {
|
||||
let score = recursive_score_match(
|
||||
query,
|
||||
query_cased,
|
||||
path,
|
||||
path_cased,
|
||||
prefix,
|
||||
lowercase_prefix,
|
||||
smart_case,
|
||||
last_positions,
|
||||
score_matrix,
|
||||
best_position_matrix,
|
||||
min_score,
|
||||
0,
|
||||
0,
|
||||
query.len() as f64,
|
||||
) * query.len() as f64;
|
||||
|
||||
if score <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let path_len = prefix.len() + path.len();
|
||||
let mut cur_start = 0;
|
||||
let mut byte_ix = 0;
|
||||
let mut char_ix = 0;
|
||||
for i in 0..query.len() {
|
||||
let match_char_ix = best_position_matrix[i * path_len + cur_start];
|
||||
while char_ix < match_char_ix {
|
||||
let ch = prefix
|
||||
.get(char_ix)
|
||||
.or_else(|| path.get(char_ix - prefix.len()))
|
||||
.unwrap();
|
||||
byte_ix += ch.len_utf8();
|
||||
char_ix += 1;
|
||||
}
|
||||
cur_start = match_char_ix + 1;
|
||||
match_positions[i] = byte_ix;
|
||||
}
|
||||
|
||||
score
|
||||
}
|
||||
|
||||
fn recursive_score_match(
|
||||
query: &[char],
|
||||
query_cased: &[char],
|
||||
path: &[char],
|
||||
path_cased: &[char],
|
||||
prefix: &[char],
|
||||
lowercase_prefix: &[char],
|
||||
smart_case: bool,
|
||||
last_positions: &[usize],
|
||||
score_matrix: &mut [Option<f64>],
|
||||
best_position_matrix: &mut [usize],
|
||||
min_score: f64,
|
||||
query_idx: usize,
|
||||
path_idx: usize,
|
||||
cur_score: f64,
|
||||
) -> f64 {
|
||||
if query_idx == query.len() {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
let path_len = prefix.len() + path.len();
|
||||
|
||||
if let Some(memoized) = score_matrix[query_idx * path_len + path_idx] {
|
||||
return memoized;
|
||||
}
|
||||
|
||||
let mut score = 0.0;
|
||||
let mut best_position = 0;
|
||||
|
||||
let query_char = query_cased[query_idx];
|
||||
let limit = last_positions[query_idx];
|
||||
|
||||
let mut last_slash = 0;
|
||||
for j in path_idx..=limit {
|
||||
let path_char = if j < prefix.len() {
|
||||
lowercase_prefix[j]
|
||||
} else {
|
||||
path_cased[j - prefix.len()]
|
||||
};
|
||||
let is_path_sep = path_char == '/' || path_char == '\\';
|
||||
|
||||
if query_idx == 0 && is_path_sep {
|
||||
last_slash = j;
|
||||
}
|
||||
|
||||
if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
|
||||
let curr = if j < prefix.len() {
|
||||
prefix[j]
|
||||
} else {
|
||||
path[j - prefix.len()]
|
||||
};
|
||||
|
||||
let mut char_score = 1.0;
|
||||
if j > path_idx {
|
||||
let last = if j - 1 < prefix.len() {
|
||||
prefix[j - 1]
|
||||
} else {
|
||||
path[j - 1 - prefix.len()]
|
||||
};
|
||||
|
||||
if last == '/' {
|
||||
char_score = 0.9;
|
||||
} else if last == '-' || last == '_' || last == ' ' || last.is_numeric() {
|
||||
char_score = 0.8;
|
||||
} else if last.is_lowercase() && curr.is_uppercase() {
|
||||
char_score = 0.8;
|
||||
} else if last == '.' {
|
||||
char_score = 0.7;
|
||||
} else if query_idx == 0 {
|
||||
char_score = BASE_DISTANCE_PENALTY;
|
||||
} else {
|
||||
char_score = MIN_DISTANCE_PENALTY.max(
|
||||
BASE_DISTANCE_PENALTY
|
||||
- (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a severe penalty if the case doesn't match.
|
||||
// This will make the exact matches have higher score than the case-insensitive and the
|
||||
// path insensitive matches.
|
||||
if (smart_case || curr == '/') && query[query_idx] != curr {
|
||||
char_score *= 0.001;
|
||||
}
|
||||
|
||||
let mut multiplier = char_score;
|
||||
|
||||
// Scale the score based on how deep within the path we found the match.
|
||||
if query_idx == 0 {
|
||||
multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
|
||||
}
|
||||
|
||||
let mut next_score = 1.0;
|
||||
if min_score > 0.0 {
|
||||
next_score = cur_score * multiplier;
|
||||
// Scores only decrease. If we can't pass the previous best, bail
|
||||
if next_score < min_score {
|
||||
// Ensure that score is non-zero so we use it in the memo table.
|
||||
if score == 0.0 {
|
||||
score = 1e-18;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let new_score = recursive_score_match(
|
||||
query,
|
||||
query_cased,
|
||||
path,
|
||||
path_cased,
|
||||
prefix,
|
||||
lowercase_prefix,
|
||||
smart_case,
|
||||
last_positions,
|
||||
score_matrix,
|
||||
best_position_matrix,
|
||||
min_score,
|
||||
query_idx + 1,
|
||||
j + 1,
|
||||
next_score,
|
||||
) * multiplier;
|
||||
|
||||
if new_score > score {
|
||||
score = new_score;
|
||||
best_position = j;
|
||||
// Optimization: can't score better than 1.
|
||||
if new_score == 1.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best_position != 0 {
|
||||
best_position_matrix[query_idx * path_len + path_idx] = best_position;
|
||||
}
|
||||
|
||||
score_matrix[query_idx * path_len + path_idx] = Some(score);
|
||||
score
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_get_last_positions() {
|
||||
let mut last_positions = vec![0; 2];
|
||||
let result = find_last_positions(
|
||||
&mut last_positions,
|
||||
&['a', 'b', 'c'],
|
||||
&['b', 'd', 'e', 'f'],
|
||||
&['d', 'c'],
|
||||
);
|
||||
assert_eq!(result, false);
|
||||
|
||||
last_positions.resize(2, 0);
|
||||
let result = find_last_positions(
|
||||
&mut last_positions,
|
||||
&['a', 'b', 'c'],
|
||||
&['b', 'd', 'e', 'f'],
|
||||
&['c', 'd'],
|
||||
);
|
||||
assert_eq!(result, true);
|
||||
assert_eq!(last_positions, vec![2, 4]);
|
||||
|
||||
last_positions.resize(4, 0);
|
||||
let result = find_last_positions(
|
||||
&mut last_positions,
|
||||
&['z', 'e', 'd', '/'],
|
||||
&['z', 'e', 'd', '/', 'f'],
|
||||
&['z', '/', 'z', 'f'],
|
||||
);
|
||||
assert_eq!(result, true);
|
||||
assert_eq!(last_positions, vec![0, 3, 4, 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_path_entries() {
|
||||
let paths = vec![
|
||||
"",
|
||||
"a",
|
||||
"ab",
|
||||
"abC",
|
||||
"abcd",
|
||||
"alphabravocharlie",
|
||||
"AlphaBravoCharlie",
|
||||
"thisisatestdir",
|
||||
"/////ThisIsATestDir",
|
||||
"/this/is/a/test/dir",
|
||||
"/test/tiatd",
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
match_query("abc", false, &paths),
|
||||
vec![
|
||||
("abC", vec![0, 1, 2]),
|
||||
("abcd", vec![0, 1, 2]),
|
||||
("AlphaBravoCharlie", vec![0, 5, 10]),
|
||||
("alphabravocharlie", vec![4, 5, 10]),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
match_query("t/i/a/t/d", false, &paths),
|
||||
vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_query("tiatd", false, &paths),
|
||||
vec![
|
||||
("/test/tiatd", vec![6, 7, 8, 9, 10]),
|
||||
("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
|
||||
("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
|
||||
("thisisatestdir", vec![0, 2, 6, 7, 11]),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_multibyte_path_entries() {
|
||||
let paths = vec!["aαbβ/cγdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"];
|
||||
assert_eq!("1️⃣".len(), 7);
|
||||
assert_eq!(
|
||||
match_query("bcd", false, &paths),
|
||||
vec![
|
||||
("αβγδ/bcde", vec![9, 10, 11]),
|
||||
("aαbβ/cγdδ", vec![3, 7, 10]),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
match_query("cde", false, &paths),
|
||||
vec![
|
||||
("αβγδ/bcde", vec![10, 11, 12]),
|
||||
("c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", vec![0, 23, 46]),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn match_query<'a>(
|
||||
query: &str,
|
||||
smart_case: bool,
|
||||
paths: &Vec<&'a str>,
|
||||
) -> Vec<(&'a str, Vec<usize>)> {
|
||||
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
||||
let query = query.chars().collect::<Vec<_>>();
|
||||
let query_chars = CharBag::from(&lowercase_query[..]);
|
||||
|
||||
let path_arcs = paths
|
||||
.iter()
|
||||
.map(|path| Arc::from(PathBuf::from(path)))
|
||||
.collect::<Vec<_>>();
|
||||
let mut path_entries = Vec::new();
|
||||
for (i, path) in paths.iter().enumerate() {
|
||||
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
|
||||
let char_bag = CharBag::from(lowercase_path.as_slice());
|
||||
path_entries.push(MatchCandidate {
|
||||
char_bag,
|
||||
path: path_arcs.get(i).unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut match_positions = Vec::new();
|
||||
let mut last_positions = Vec::new();
|
||||
match_positions.resize(query.len(), 0);
|
||||
last_positions.resize(query.len(), 0);
|
||||
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
let mut results = Vec::new();
|
||||
match_single_tree_paths(
|
||||
&Snapshot {
|
||||
id: 0,
|
||||
scan_id: 0,
|
||||
abs_path: PathBuf::new().into(),
|
||||
ignores: Default::default(),
|
||||
entries_by_path: Default::default(),
|
||||
entries_by_id: Default::default(),
|
||||
removed_entry_ids: Default::default(),
|
||||
root_name: Default::default(),
|
||||
root_char_bag: Default::default(),
|
||||
next_entry_id: Default::default(),
|
||||
},
|
||||
"".into(),
|
||||
path_entries.into_iter(),
|
||||
&query[..],
|
||||
&lowercase_query[..],
|
||||
query_chars,
|
||||
smart_case,
|
||||
&mut results,
|
||||
100,
|
||||
&mut 0.0,
|
||||
&mut match_positions,
|
||||
&mut last_positions,
|
||||
&mut Vec::new(),
|
||||
&mut Vec::new(),
|
||||
&cancel_flag,
|
||||
);
|
||||
|
||||
results
|
||||
.into_iter()
|
||||
.map(|result| {
|
||||
(
|
||||
paths
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|p| result.path.as_ref() == Path::new(p))
|
||||
.unwrap(),
|
||||
result.positions,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user