From 06643211252e002dadb82a2a6e21c134868425e7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 6 Apr 2021 23:50:13 -0600 Subject: [PATCH 01/12] Add ability to debug element trees as JSON --- Cargo.lock | 6 ++- gpui/Cargo.toml | 2 + gpui/examples/text.rs | 17 +++++-- gpui/src/color.rs | 9 ++++ gpui/src/elements/align.rs | 21 +++++++- gpui/src/elements/canvas.rs | 16 ++++++- gpui/src/elements/constrained_box.rs | 23 +++++++-- gpui/src/elements/container.rs | 72 ++++++++++++++++++++++++++++ gpui/src/elements/empty.rs | 23 +++++++-- gpui/src/elements/event_handler.rs | 24 ++++++++-- gpui/src/elements/flex.rs | 48 ++++++++++++++++--- gpui/src/elements/label.rs | 34 ++++++++++++- gpui/src/elements/line_box.rs | 26 ++++++++-- gpui/src/elements/new.rs | 28 ++++++++++- gpui/src/elements/stack.rs | 19 +++++++- gpui/src/elements/svg.rs | 23 ++++++++- gpui/src/elements/uniform_list.rs | 19 ++++++++ gpui/src/font_cache.rs | 9 ++++ gpui/src/fonts.rs | 57 +++++++++++++++++++++- gpui/src/geometry.rs | 15 +++++- gpui/src/json.rs | 15 ++++++ gpui/src/lib.rs | 7 +-- gpui/src/presenter.rs | 45 +++++++++++++++++ gpui/src/scene.rs | 22 +++++++++ zed/src/editor/buffer_element.rs | 15 ++++++ 25 files changed, 554 insertions(+), 41 deletions(-) create mode 100644 gpui/src/color.rs create mode 100644 gpui/src/json.rs diff --git a/Cargo.lock b/Cargo.lock index 38d75ed246..b6d5b045e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -930,6 +930,8 @@ dependencies = [ "rand 0.8.3", "replace_with", "resvg", + "serde", + "serde_json", "simplelog", "smallvec", "smol", @@ -1691,9 +1693,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.124" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd761ff957cb2a45fbb9ab3da6512de9de55872866160b23c25f1a841e99d29f" +checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" [[package]] name = "serde_json" diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 2b0e90bedc..7bf1913372 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -18,6 +18,8 @@ pathfinder_geometry = "0.5" rand = "0.8.3" replace_with = "0.1.7" resvg = "0.14" +serde = "1.0.125" +serde_json = "1.0.64" smallvec = "1.6.1" smol = "1.2" tiny-skia = "0.5" diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index e481ce5e2f..14bc198270 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -2,9 +2,10 @@ use gpui::{ color::ColorU, fonts::{Properties, Weight}, platform::{current as platform, Runner}, - Element as _, Quad, + DebugContext, Element as _, Quad, }; use log::LevelFilter; +use pathfinder_geometry::rect::RectF; use simplelog::SimpleLogger; fn main() { @@ -59,7 +60,7 @@ impl gpui::Element for TextElement { fn paint( &mut self, - bounds: pathfinder_geometry::rect::RectF, + bounds: RectF, _: &mut Self::LayoutState, ctx: &mut gpui::PaintContext, ) -> Self::PaintState { @@ -109,11 +110,21 @@ impl gpui::Element for TextElement { fn dispatch_event( &mut self, _: &gpui::Event, - _: pathfinder_geometry::rect::RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, _: &mut gpui::EventContext, ) -> bool { false } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &DebugContext, + ) -> gpui::json::Value { + todo!() + } } diff --git a/gpui/src/color.rs b/gpui/src/color.rs new file mode 100644 index 0000000000..95b966493e --- /dev/null +++ b/gpui/src/color.rs @@ -0,0 +1,9 @@ +use crate::json::ToJson; +pub use pathfinder_color::*; +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)) + } +} diff --git a/gpui/src/elements/align.rs b/gpui/src/elements/align.rs index a8277d2469..9d5c905eb5 100644 --- a/gpui/src/elements/align.rs +++ b/gpui/src/elements/align.rs @@ -1,8 +1,10 @@ use crate::{ - AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + json, AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, + LayoutContext, PaintContext, SizeConstraint, }; +use json::ToJson; use pathfinder_geometry::vector::{vec2f, Vector2F}; +use serde_json::json; pub struct Align { child: ElementBox, @@ -79,4 +81,19 @@ impl Element for Align { ) -> bool { self.child.dispatch_event(event, ctx) } + + fn debug( + &self, + bounds: pathfinder_geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + ctx: &DebugContext, + ) -> json::Value { + json!({ + "type": "Align", + "alignment": self.alignment.to_json(), + "bounds": bounds.to_json(), + "child": self.child.debug(ctx), + }) + } } diff --git a/gpui/src/elements/canvas.rs b/gpui/src/elements/canvas.rs index 4788ab9b9d..a82ef50c90 100644 --- a/gpui/src/elements/canvas.rs +++ b/gpui/src/elements/canvas.rs @@ -1,5 +1,9 @@ use super::Element; -use crate::PaintContext; +use crate::{ + json::{self, json}, + DebugContext, PaintContext, +}; +use json::ToJson; use pathfinder_geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -70,4 +74,14 @@ where ) -> bool { false } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &DebugContext, + ) -> json::Value { + json!({"type": "Canvas", "bounds": bounds.to_json()}) + } } diff --git a/gpui/src/elements/constrained_box.rs b/gpui/src/elements/constrained_box.rs index dc4c0be7fd..95b6d29637 100644 --- a/gpui/src/elements/constrained_box.rs +++ b/gpui/src/elements/constrained_box.rs @@ -1,8 +1,11 @@ +use json::ToJson; +use serde_json::json; + use crate::{ - AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + geometry::{rect::RectF, vector::Vector2F}, + json, AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, + LayoutContext, PaintContext, SizeConstraint, }; -use pathfinder_geometry::vector::Vector2F; pub struct ConstrainedBox { child: ElementBox, @@ -63,7 +66,7 @@ impl Element for ConstrainedBox { fn paint( &mut self, - bounds: pathfinder_geometry::rect::RectF, + bounds: RectF, _: &mut Self::LayoutState, ctx: &mut PaintContext, ) -> Self::PaintState { @@ -73,11 +76,21 @@ impl Element for ConstrainedBox { fn dispatch_event( &mut self, event: &Event, - _: pathfinder_geometry::rect::RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, ctx: &mut EventContext, ) -> bool { self.child.dispatch_event(event, ctx) } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + ctx: &DebugContext, + ) -> json::Value { + json!({"type": "ConstrainedBox", "constraint": self.constraint.to_json(), "child": self.child.debug(ctx)}) + } } diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index 83b3e820e3..dcded86f38 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -1,8 +1,10 @@ use pathfinder_geometry::rect::RectF; +use serde_json::json; use crate::{ color::ColorU, geometry::vector::{vec2f, Vector2F}, + json::ToJson, scene::{self, Border, Quad}, AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, @@ -189,6 +191,28 @@ impl Element for Container { ) -> bool { self.child.dispatch_event(event, ctx) } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + ctx: &crate::DebugContext, + ) -> serde_json::Value { + json!({ + "type": "Container", + "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(), + }, + "bounds": bounds.to_json(), + "child": self.child.debug(ctx), + }) + } } #[derive(Default)] @@ -199,6 +223,25 @@ pub struct Margin { right: f32, } +impl ToJson for Margin { + fn to_json(&self) -> serde_json::Value { + let mut value = json!({}); + if self.top > 0. { + value["top"] = json!(self.top); + } + if self.right > 0. { + value["right"] = json!(self.right); + } + if self.bottom > 0. { + value["bottom"] = json!(self.bottom); + } + if self.left > 0. { + value["left"] = json!(self.left); + } + value + } +} + #[derive(Default)] pub struct Padding { top: f32, @@ -207,9 +250,38 @@ pub struct Padding { right: f32, } +impl ToJson for Padding { + fn to_json(&self) -> serde_json::Value { + let mut value = json!({}); + if self.top > 0. { + value["top"] = json!(self.top); + } + if self.right > 0. { + value["right"] = json!(self.right); + } + if self.bottom > 0. { + value["bottom"] = json!(self.bottom); + } + if self.left > 0. { + value["left"] = json!(self.left); + } + value + } +} + #[derive(Default)] pub struct Shadow { offset: Vector2F, blur: f32, color: ColorU, } + +impl ToJson for Shadow { + fn to_json(&self) -> serde_json::Value { + json!({ + "offset": self.offset.to_json(), + "blur": self.blur, + "color": self.color.to_json() + }) + } +} diff --git a/gpui/src/elements/empty.rs b/gpui/src/elements/empty.rs index f5d038bb50..fe9ff3c9b9 100644 --- a/gpui/src/elements/empty.rs +++ b/gpui/src/elements/empty.rs @@ -1,6 +1,10 @@ -use crate::geometry::{ - rect::RectF, - vector::{vec2f, Vector2F}, +use crate::{ + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::{json, ToJson}, + DebugContext, }; use crate::{ AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, @@ -58,4 +62,17 @@ impl Element for Empty { ) -> bool { false } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &DebugContext, + ) -> serde_json::Value { + json!({ + "type": "Empty", + "bounds": bounds.to_json(), + }) + } } diff --git a/gpui/src/elements/event_handler.rs b/gpui/src/elements/event_handler.rs index b558bd0254..717d7db1f6 100644 --- a/gpui/src/elements/event_handler.rs +++ b/gpui/src/elements/event_handler.rs @@ -1,6 +1,9 @@ +use pathfinder_geometry::rect::RectF; +use serde_json::json; + use crate::{ - geometry::vector::Vector2F, AfterLayoutContext, Element, ElementBox, Event, EventContext, - LayoutContext, PaintContext, SizeConstraint, + geometry::vector::Vector2F, AfterLayoutContext, DebugContext, Element, ElementBox, Event, + EventContext, LayoutContext, PaintContext, SizeConstraint, }; pub struct EventHandler { @@ -49,7 +52,7 @@ impl Element for EventHandler { fn paint( &mut self, - bounds: pathfinder_geometry::rect::RectF, + bounds: RectF, _: &mut Self::LayoutState, ctx: &mut PaintContext, ) -> Self::PaintState { @@ -59,7 +62,7 @@ impl Element for EventHandler { fn dispatch_event( &mut self, event: &Event, - bounds: pathfinder_geometry::rect::RectF, + bounds: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, ctx: &mut EventContext, @@ -80,4 +83,17 @@ impl Element for EventHandler { } } } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + ctx: &DebugContext, + ) -> serde_json::Value { + json!({ + "type": "EventHandler", + "child": self.child.debug(ctx), + }) + } } diff --git a/gpui/src/elements/flex.rs b/gpui/src/elements/flex.rs index 86cc9447c5..abed2aabc5 100644 --- a/gpui/src/elements/flex.rs +++ b/gpui/src/elements/flex.rs @@ -1,10 +1,15 @@ use std::any::Any; use crate::{ - AfterLayoutContext, Axis, Element, ElementBox, Event, EventContext, LayoutContext, - PaintContext, SizeConstraint, Vector2FExt, + json::{self, ToJson, Value}, + AfterLayoutContext, Axis, DebugContext, Element, ElementBox, Event, EventContext, + LayoutContext, PaintContext, SizeConstraint, Vector2FExt, }; -use pathfinder_geometry::vector::{vec2f, Vector2F}; +use pathfinder_geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, +}; +use serde_json::json; pub struct Flex { axis: Axis, @@ -130,7 +135,7 @@ impl Element for Flex { fn paint( &mut self, - bounds: pathfinder_geometry::rect::RectF, + bounds: RectF, _: &mut Self::LayoutState, ctx: &mut PaintContext, ) -> Self::PaintState { @@ -147,7 +152,7 @@ impl Element for Flex { fn dispatch_event( &mut self, event: &Event, - _: pathfinder_geometry::rect::RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, ctx: &mut EventContext, @@ -158,6 +163,21 @@ impl Element for Flex { } handled } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + ctx: &DebugContext, + ) -> json::Value { + json!({ + "type": "Flex", + "axis": self.axis.to_json(), + "bounds": bounds.to_json(), + "children": self.children.iter().map(|child| child.debug(ctx)).collect::>() + }) + } } struct FlexParentData { @@ -202,7 +222,7 @@ impl Element for Expanded { fn paint( &mut self, - bounds: pathfinder_geometry::rect::RectF, + bounds: RectF, _: &mut Self::LayoutState, ctx: &mut PaintContext, ) -> Self::PaintState { @@ -212,7 +232,7 @@ impl Element for Expanded { fn dispatch_event( &mut self, event: &Event, - _: pathfinder_geometry::rect::RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, ctx: &mut EventContext, @@ -223,4 +243,18 @@ impl Element for Expanded { fn metadata(&self) -> Option<&dyn Any> { Some(&self.metadata) } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + ctx: &DebugContext, + ) -> Value { + json!({ + "type": "Expanded", + "flex": self.metadata.flex, + "child": self.child.debug(ctx) + }) + } } diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index bc5281e15a..952bc0eab0 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -1,3 +1,5 @@ +use serde_json::json; + use crate::{ color::ColorU, font_cache::FamilyId, @@ -6,8 +8,10 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, + json::{ToJson, Value}, text_layout::Line, - AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, + AfterLayoutContext, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, }; use std::{ops::Range, sync::Arc}; @@ -152,4 +156,32 @@ impl Element for Label { ) -> bool { false } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + ctx: &DebugContext, + ) -> Value { + json!({ + "type": "Label", + "font_size": self.font_size, + "bounds": bounds.to_json(), + "text": &self.text, + "family_id": ctx.font_cache.family_name(self.family_id).unwrap(), + "font_properties": self.font_properties.to_json(), + "highlights": self.highlights.to_json(), + }) + } +} + +impl ToJson for Highlights { + fn to_json(&self) -> Value { + json!({ + "color": self.color.to_json(), + "indices": self.indices, + "font_properties": self.font_properties.to_json(), + }) + } } diff --git a/gpui/src/elements/line_box.rs b/gpui/src/elements/line_box.rs index 7f1a3bc628..eec76cbeb4 100644 --- a/gpui/src/elements/line_box.rs +++ b/gpui/src/elements/line_box.rs @@ -1,9 +1,13 @@ use crate::{ font_cache::FamilyId, fonts::Properties, - geometry::vector::{vec2f, Vector2F}, - AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::{json, ToJson}, + AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, + PaintContext, SizeConstraint, }; pub struct LineBox { @@ -85,4 +89,20 @@ impl Element for LineBox { ) -> bool { self.child.dispatch_event(event, ctx) } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + ctx: &DebugContext, + ) -> serde_json::Value { + json!({ + "bounds": bounds.to_json(), + "family_id": ctx.font_cache.family_name(self.family_id).unwrap(), + "font_size": self.font_size, + "font_properties": self.font_properties.to_json(), + "child": self.child.debug(ctx), + }) + } } diff --git a/gpui/src/elements/new.rs b/gpui/src/elements/new.rs index 9cde494171..446948e3cc 100644 --- a/gpui/src/elements/new.rs +++ b/gpui/src/elements/new.rs @@ -1,6 +1,7 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, - AfterLayoutContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, + json, AfterLayoutContext, DebugContext, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, }; use core::panic; use replace_with::replace_with_or_abort; @@ -11,6 +12,7 @@ trait AnyElement { fn after_layout(&mut self, _: &mut AfterLayoutContext) {} fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext); fn dispatch_event(&mut self, event: &Event, ctx: &mut EventContext) -> bool; + fn debug(&self, ctx: &DebugContext) -> serde_json::Value; fn size(&self) -> Vector2F; fn metadata(&self) -> Option<&dyn Any>; @@ -53,6 +55,14 @@ pub trait Element { None } + fn debug( + &self, + bounds: RectF, + layout: &Self::LayoutState, + paint: &Self::PaintState, + ctx: &DebugContext, + ) -> serde_json::Value; + fn boxed(self) -> ElementBox where Self: 'static + Sized, @@ -165,6 +175,18 @@ impl AnyElement for Lifecycle { | Lifecycle::PostPaint { element, .. } => element.metadata(), } } + + fn debug(&self, ctx: &DebugContext) -> serde_json::Value { + match self { + Lifecycle::PostPaint { + element, + bounds, + layout, + paint, + } => element.debug(*bounds, layout, paint, ctx), + _ => panic!("invalid element lifecycle state"), + } + } } impl ElementBox { @@ -191,4 +213,8 @@ impl ElementBox { pub fn metadata(&self) -> Option<&dyn Any> { self.0.metadata() } + + pub fn debug(&self, ctx: &DebugContext) -> json::Value { + self.0.debug(ctx) + } } diff --git a/gpui/src/elements/stack.rs b/gpui/src/elements/stack.rs index 0cfcf48cc5..6fdae68010 100644 --- a/gpui/src/elements/stack.rs +++ b/gpui/src/elements/stack.rs @@ -1,7 +1,8 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, - AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + json::{self, json, ToJson}, + AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, + PaintContext, SizeConstraint, }; pub struct Stack { @@ -71,6 +72,20 @@ impl Element for Stack { } false } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + ctx: &DebugContext, + ) -> json::Value { + json!({ + "type": "Stack", + "bounds": bounds.to_json(), + "children": self.children.iter().map(|child| child.debug(ctx)).collect::>() + }) + } } impl Extend for Stack { diff --git a/gpui/src/elements/svg.rs b/gpui/src/elements/svg.rs index 9afc5eeab8..12b08b154c 100644 --- a/gpui/src/elements/svg.rs +++ b/gpui/src/elements/svg.rs @@ -1,11 +1,13 @@ +use serde_json::json; + use crate::{ color::ColorU, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, - scene, AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + scene, AfterLayoutContext, DebugContext, Element, Event, EventContext, LayoutContext, + PaintContext, SizeConstraint, }; pub struct Svg { @@ -86,8 +88,25 @@ impl Element for Svg { ) -> bool { false } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &DebugContext, + ) -> serde_json::Value { + json!({ + "type": "Svg", + "bounds": bounds.to_json(), + "path": self.path, + "color": self.color.to_json(), + }) + } } +use crate::json::ToJson; + fn from_usvg_rect(rect: usvg::Rect) -> RectF { RectF::new( vec2f(rect.x() as f32, rect.y() as f32), diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index 637dfdbd55..c1b9b86ca7 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -7,8 +7,10 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, + json::{self, json}, ElementBox, }; +use json::ToJson; use parking_lot::Mutex; use std::{cmp, ops::Range, sync::Arc}; @@ -236,4 +238,21 @@ where handled } + + fn debug( + &self, + bounds: RectF, + layout: &Self::LayoutState, + _: &Self::PaintState, + ctx: &crate::DebugContext, + ) -> json::Value { + json!({ + "type": "UniformList", + "bounds": bounds.to_json(), + "scroll_max": layout.scroll_max, + "item_height": layout.item_height, + "items": layout.items.iter().map(|item| item.debug(ctx)).collect::>() + + }) + } } diff --git a/gpui/src/font_cache.rs b/gpui/src/font_cache.rs index 205a450670..4e109ef240 100644 --- a/gpui/src/font_cache.rs +++ b/gpui/src/font_cache.rs @@ -36,6 +36,15 @@ impl FontCache { })) } + pub fn family_name(&self, family_id: FamilyId) -> Result { + self.0 + .read() + .families + .get(family_id.0) + .ok_or_else(|| anyhow!("invalid family id")) + .map(|family| family.name.clone()) + } + pub fn load_family(&self, names: &[&str]) -> Result { for name in names { let state = self.0.upgradable_read(); diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index 599f018526..e7e6b5dedb 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -1,7 +1,62 @@ +use crate::json::json; pub use font_kit::metrics::Metrics; -pub use font_kit::properties::{Properties, Weight}; +pub use font_kit::properties::{Properties, Stretch, Style, Weight}; + +use crate::json::ToJson; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct FontId(pub usize); pub type GlyphId = u32; + +impl ToJson for Properties { + fn to_json(&self) -> crate::json::Value { + json!({ + "style": self.style.to_json(), + "weight": self.weight.to_json(), + "stretch": self.stretch.to_json(), + }) + } +} + +impl ToJson for Style { + fn to_json(&self) -> crate::json::Value { + match self { + Style::Normal => json!("normal"), + Style::Italic => json!("italic"), + Style::Oblique => json!("oblique"), + } + } +} + +impl ToJson for Weight { + fn to_json(&self) -> crate::json::Value { + if self.0 == Weight::THIN.0 { + json!("thin") + } else if self.0 == Weight::EXTRA_LIGHT.0 { + json!("extra light") + } else if self.0 == Weight::LIGHT.0 { + json!("light") + } else if self.0 == Weight::NORMAL.0 { + json!("normal") + } else if self.0 == Weight::MEDIUM.0 { + json!("medium") + } else if self.0 == Weight::SEMIBOLD.0 { + json!("semibold") + } else if self.0 == Weight::BOLD.0 { + json!("bold") + } else if self.0 == Weight::EXTRA_BOLD.0 { + json!("extra bold") + } else if self.0 == Weight::BLACK.0 { + json!("black") + } else { + json!(self.0) + } + } +} + +impl ToJson for Stretch { + fn to_json(&self) -> serde_json::Value { + json!(self.0) + } +} diff --git a/gpui/src/geometry.rs b/gpui/src/geometry.rs index ba6cabfea1..cf8ec9cb40 100644 --- a/gpui/src/geometry.rs +++ b/gpui/src/geometry.rs @@ -1,7 +1,8 @@ use super::scene::{Path, PathVertex}; -use crate::color::ColorU; +use crate::{color::ColorU, json::ToJson}; pub use pathfinder_geometry::*; use rect::RectF; +use serde_json::json; use vector::{vec2f, Vector2F}; pub struct PathBuilder { @@ -106,3 +107,15 @@ impl PathBuilder { } } } + +impl ToJson for Vector2F { + fn to_json(&self) -> serde_json::Value { + json!([self.x(), self.y()]) + } +} + +impl ToJson for RectF { + fn to_json(&self) -> serde_json::Value { + json!({"origin": self.origin().to_json(), "size": self.size().to_json()}) + } +} diff --git a/gpui/src/json.rs b/gpui/src/json.rs new file mode 100644 index 0000000000..8d7625acec --- /dev/null +++ b/gpui/src/json.rs @@ -0,0 +1,15 @@ +pub use serde_json::*; + +pub trait ToJson { + fn to_json(&self) -> Value; +} + +impl ToJson for Option { + fn to_json(&self) -> Value { + if let Some(value) = self.as_ref() { + value.to_json() + } else { + json!(null) + } + } +} diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index bdf7096506..7b0ecdddef 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -18,11 +18,12 @@ mod util; pub use elements::{Element, ElementBox}; pub mod executor; pub use executor::Task; +pub mod color; +pub mod json; pub mod keymap; pub mod platform; -pub use pathfinder_color as color; pub use platform::Event; pub use presenter::{ - AfterLayoutContext, Axis, EventContext, LayoutContext, PaintContext, SizeConstraint, - Vector2FExt, + AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext, + SizeConstraint, Vector2FExt, }; diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 197fdaf637..293e528aba 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -2,11 +2,13 @@ use crate::{ app::{AppContext, MutableAppContext, WindowInvalidation}, elements::Element, font_cache::FontCache, + json::ToJson, platform::Event, text_layout::TextLayoutCache, AssetCache, ElementBox, Scene, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; +use serde_json::json; use std::{any::Any, collections::HashMap, sync::Arc}; pub struct Presenter { @@ -224,6 +226,12 @@ impl<'a> EventContext<'a> { } } +pub struct DebugContext<'a> { + rendered_views: &'a mut HashMap, + pub font_cache: &'a FontCache, + pub app: &'a AppContext, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Axis { Horizontal, @@ -239,6 +247,15 @@ impl Axis { } } +impl ToJson for Axis { + fn to_json(&self) -> serde_json::Value { + match self { + Axis::Horizontal => json!("horizontal"), + Axis::Vertical => json!("vertical"), + } + } +} + pub trait Vector2FExt { fn along(self, axis: Axis) -> f32; } @@ -291,6 +308,15 @@ impl SizeConstraint { } } +impl ToJson for SizeConstraint { + fn to_json(&self) -> serde_json::Value { + json!({ + "min": self.min.to_json(), + "max": self.max.to_json(), + }) + } +} + pub struct ChildView { view_id: usize, } @@ -342,6 +368,25 @@ impl Element for ChildView { ) -> bool { ctx.dispatch_event(self.view_id, event) } + + fn debug( + &self, + bounds: pathfinder_geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + ctx: &DebugContext, + ) -> serde_json::Value { + json!({ + "type": "ChildView", + "view_id": self.view_id, + "bounds": bounds.to_json(), + "child": if let Some(view) = ctx.rendered_views.get(&self.view_id) { + view.debug(ctx) + } else { + json!(null) + } + }) + } } #[cfg(test)] diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index cde5dc60d0..44f9fcd1a0 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -1,7 +1,10 @@ +use serde_json::json; + use crate::{ color::ColorU, fonts::{FontId, GlyphId}, geometry::{rect::RectF, vector::Vector2F}, + json::ToJson, }; pub struct Scene { @@ -258,3 +261,22 @@ impl Border { } } } + +impl ToJson for Border { + fn to_json(&self) -> serde_json::Value { + let mut value = json!({}); + if self.top { + value["top"] = json!(self.width); + } + if self.right { + value["right"] = json!(self.width); + } + if self.bottom { + value["bottom"] = json!(self.width); + } + if self.left { + value["left"] = json!(self.width); + } + value + } +} diff --git a/zed/src/editor/buffer_element.rs b/zed/src/editor/buffer_element.rs index a4a9d63a33..e00c11f1fd 100644 --- a/zed/src/editor/buffer_element.rs +++ b/zed/src/editor/buffer_element.rs @@ -6,10 +6,12 @@ use gpui::{ vector::{vec2f, Vector2F}, PathBuilder, }, + json::{self, ToJson}, text_layout::{self, TextLayoutCache}, AfterLayoutContext, AppContext, Border, Element, Event, EventContext, FontCache, LayoutContext, PaintContext, Quad, Scene, SizeConstraint, ViewHandle, }; +use json::json; use smallvec::SmallVec; use std::cmp::Ordering; use std::{ @@ -477,6 +479,19 @@ impl Element for BufferElement { false } } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::DebugContext, + ) -> json::Value { + json!({ + "type": "BufferElement", + "bounds": bounds.to_json() + }) + } } pub struct LayoutState { From cbb23a93a66c70a478d5a4fbec2b16803cbc5373 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 7 Apr 2021 20:51:14 -0600 Subject: [PATCH 02/12] Log prettified element debug JSON to on cmd-alt-i --- gpui/src/app.rs | 47 +++++++++++++++++++++++------ gpui/src/presenter.rs | 16 ++++++++-- zed/src/workspace/workspace_view.rs | 21 +++++++++++-- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 8dbc34b893..b65e631e96 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -310,6 +310,7 @@ pub struct MutableAppContext { window_invalidations: HashMap, invalidation_callbacks: HashMap>, + debug_elements_callbacks: HashMap crate::json::Value>>, foreground: Rc, future_handlers: Rc>>, stream_handlers: Rc>>, @@ -347,6 +348,7 @@ impl MutableAppContext { observations: HashMap::new(), window_invalidations: HashMap::new(), invalidation_callbacks: HashMap::new(), + debug_elements_callbacks: HashMap::new(), foreground, future_handlers: Default::default(), stream_handlers: Default::default(), @@ -373,16 +375,29 @@ impl MutableAppContext { &self.ctx.background } - pub fn on_window_invalidated( - &mut self, - window_id: usize, - callback: F, - ) { + pub fn on_window_invalidated(&mut self, window_id: usize, callback: F) + where + F: 'static + FnMut(WindowInvalidation, &mut MutableAppContext), + { self.invalidation_callbacks .insert(window_id, Box::new(callback)); self.update_windows(); } + pub fn on_debug_elements(&mut self, window_id: usize, callback: F) + where + F: 'static + Fn(&AppContext) -> crate::json::Value, + { + self.debug_elements_callbacks + .insert(window_id, Box::new(callback)); + } + + pub fn debug_elements(&self, window_id: usize) -> Option { + self.debug_elements_callbacks + .get(&window_id) + .map(|debug_elements| debug_elements(&self.ctx)) + } + pub fn add_action(&mut self, name: S, mut handler: F) where S: Into, @@ -692,11 +707,19 @@ impl MutableAppContext { })); } - self.on_window_invalidated(window_id, move |invalidation, ctx| { - let mut presenter = presenter.borrow_mut(); - presenter.invalidate(invalidation, ctx.downgrade()); - let scene = presenter.build_scene(window.size(), window.scale_factor(), ctx); - window.present_scene(scene); + { + let presenter = presenter.clone(); + self.on_window_invalidated(window_id, move |invalidation, ctx| { + let mut presenter = presenter.borrow_mut(); + presenter.invalidate(invalidation, ctx.downgrade()); + let scene = + presenter.build_scene(window.size(), window.scale_factor(), ctx); + window.present_scene(scene); + }); + } + + self.on_debug_elements(window_id, move |ctx| { + presenter.borrow().debug_elements(ctx).unwrap() }); } } @@ -1573,6 +1596,10 @@ impl<'a, T: View> ViewContext<'a, T> { &self.app.ctx.background } + pub fn debug_elements(&self) -> crate::json::Value { + self.app.debug_elements(self.window_id).unwrap() + } + pub fn focus(&mut self, handle: S) where S: Into, diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 293e528aba..7c3be96a72 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -2,7 +2,7 @@ use crate::{ app::{AppContext, MutableAppContext, WindowInvalidation}, elements::Element, font_cache::FontCache, - json::ToJson, + json::{self, ToJson}, platform::Event, text_layout::TextLayoutCache, AssetCache, ElementBox, Scene, @@ -130,6 +130,18 @@ impl Presenter { Vec::new() } } + + pub fn debug_elements(&self, ctx: &AppContext) -> Option { + ctx.root_view_id(self.window_id) + .and_then(|root_view_id| self.rendered_views.get(&root_view_id)) + .map(|root_element| { + root_element.debug(&DebugContext { + rendered_views: &self.rendered_views, + font_cache: &self.font_cache, + app: ctx, + }) + }) + } } pub struct ActionToDispatch { @@ -227,7 +239,7 @@ impl<'a> EventContext<'a> { } pub struct DebugContext<'a> { - rendered_views: &'a mut HashMap, + rendered_views: &'a HashMap, pub font_cache: &'a FontCache, pub app: &'a AppContext, } diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 46cbdb0f28..7cb9fd8b64 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -2,15 +2,19 @@ use super::{pane, Pane, PaneGroup, SplitDirection, Workspace}; use crate::{settings::Settings, watch}; use futures_core::future::LocalBoxFuture; use gpui::{ - color::rgbu, elements::*, keymap::Binding, AnyViewHandle, App, AppContext, Entity, ModelHandle, - MutableAppContext, View, ViewContext, ViewHandle, + color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, App, + AppContext, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, }; use log::{error, info}; use std::{collections::HashSet, path::PathBuf}; pub fn init(app: &mut App) { app.add_action("workspace:save", WorkspaceView::save_active_item); - app.add_bindings(vec![Binding::new("cmd-s", "workspace:save", None)]); + app.add_action("workspace:debug_elements", WorkspaceView::debug_elements); + app.add_bindings(vec![ + Binding::new("cmd-s", "workspace:save", None), + Binding::new("cmd-alt-i", "workspace:debug_elements", None), + ]); } pub trait ItemView: View { @@ -251,6 +255,17 @@ impl WorkspaceView { }); } + pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext) { + match to_string_pretty(&ctx.debug_elements()) { + Ok(json) => { + log::info!("{}", json); + } + Err(error) => { + log::error!("error debugging elements: {}", error); + } + }; + } + fn workspace_updated(&mut self, _: ModelHandle, ctx: &mut ViewContext) { ctx.notify(); } From 9f6f27f305056e85a352f304781eca043728fc61 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 7 Apr 2021 21:54:05 -0600 Subject: [PATCH 03/12] Order debug JSON and allow elements to be named; copy to clipboard --- Cargo.lock | 17 +++++++++++ gpui/src/app.rs | 4 +++ gpui/src/elements/align.rs | 2 +- gpui/src/elements/container.rs | 2 +- gpui/src/elements/flex.rs | 2 +- gpui/src/elements/label.rs | 6 ++-- gpui/src/elements/line_box.rs | 2 +- gpui/src/elements/new.rs | 47 +++++++++++++++++++++++------ gpui/src/platform/mac/app.rs | 21 +++++++++++-- gpui/src/platform/mod.rs | 1 + gpui/src/platform/test.rs | 2 ++ zed/Cargo.toml | 4 +-- zed/src/file_finder.rs | 8 ++--- zed/src/workspace/pane.rs | 10 +++--- zed/src/workspace/workspace_view.rs | 8 +++-- 15 files changed, 104 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6d5b045e2..07e3c4e13d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -940,6 +940,12 @@ dependencies = [ "usvg", ] +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + [[package]] name = "hermit-abi" version = "0.1.18" @@ -973,6 +979,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "instant" version = "0.1.9" @@ -1703,6 +1719,7 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ + "indexmap", "itoa", "ryu", "serde", diff --git a/gpui/src/app.rs b/gpui/src/app.rs index b65e631e96..d6996e0fee 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -1120,6 +1120,10 @@ impl MutableAppContext { } } } + + pub fn copy(&self, text: &str) { + self.platform.copy(text); + } } impl ModelAsRef for MutableAppContext { diff --git a/gpui/src/elements/align.rs b/gpui/src/elements/align.rs index 9d5c905eb5..a879870cab 100644 --- a/gpui/src/elements/align.rs +++ b/gpui/src/elements/align.rs @@ -91,8 +91,8 @@ impl Element for Align { ) -> json::Value { json!({ "type": "Align", - "alignment": self.alignment.to_json(), "bounds": bounds.to_json(), + "alignment": self.alignment.to_json(), "child": self.child.debug(ctx), }) } diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index dcded86f38..9554f3b353 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -201,6 +201,7 @@ impl Element for Container { ) -> serde_json::Value { json!({ "type": "Container", + "bounds": bounds.to_json(), "details": { "margin": self.margin.to_json(), "padding": self.padding.to_json(), @@ -209,7 +210,6 @@ impl Element for Container { "corner_radius": self.corner_radius, "shadow": self.shadow.to_json(), }, - "bounds": bounds.to_json(), "child": self.child.debug(ctx), }) } diff --git a/gpui/src/elements/flex.rs b/gpui/src/elements/flex.rs index abed2aabc5..db87a80dbf 100644 --- a/gpui/src/elements/flex.rs +++ b/gpui/src/elements/flex.rs @@ -173,8 +173,8 @@ impl Element for Flex { ) -> json::Value { json!({ "type": "Flex", - "axis": self.axis.to_json(), "bounds": bounds.to_json(), + "axis": self.axis.to_json(), "children": self.children.iter().map(|child| child.debug(ctx)).collect::>() }) } diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index 952bc0eab0..2ef66bcc9d 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -166,11 +166,11 @@ impl Element for Label { ) -> Value { json!({ "type": "Label", - "font_size": self.font_size, "bounds": bounds.to_json(), - "text": &self.text, - "family_id": ctx.font_cache.family_name(self.family_id).unwrap(), + "font_family": ctx.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(), }) } diff --git a/gpui/src/elements/line_box.rs b/gpui/src/elements/line_box.rs index eec76cbeb4..83f58feea5 100644 --- a/gpui/src/elements/line_box.rs +++ b/gpui/src/elements/line_box.rs @@ -99,7 +99,7 @@ impl Element for LineBox { ) -> serde_json::Value { json!({ "bounds": bounds.to_json(), - "family_id": ctx.font_cache.family_name(self.family_id).unwrap(), + "font_family": ctx.font_cache.family_name(self.family_id).unwrap(), "font_size": self.font_size, "font_properties": self.font_properties.to_json(), "child": self.child.debug(ctx), diff --git a/gpui/src/elements/new.rs b/gpui/src/elements/new.rs index 446948e3cc..3521a5fab0 100644 --- a/gpui/src/elements/new.rs +++ b/gpui/src/elements/new.rs @@ -5,7 +5,7 @@ use crate::{ }; use core::panic; use replace_with::replace_with_or_abort; -use std::any::Any; +use std::{any::Any, borrow::Cow}; trait AnyElement { fn layout(&mut self, constraint: SizeConstraint, ctx: &mut LayoutContext) -> Vector2F; @@ -67,7 +67,20 @@ pub trait Element { where Self: 'static + Sized, { - ElementBox(Box::new(Lifecycle::Init { element: self })) + ElementBox { + name: None, + element: Box::new(Lifecycle::Init { element: self }), + } + } + + fn named(self, name: impl Into>) -> ElementBox + where + Self: 'static + Sized, + { + ElementBox { + name: Some(name.into()), + element: Box::new(Lifecycle::Init { element: self }), + } } } @@ -87,7 +100,10 @@ pub enum Lifecycle { paint: T::PaintState, }, } -pub struct ElementBox(Box); +pub struct ElementBox { + name: Option>, + element: Box, +} impl AnyElement for Lifecycle { fn layout(&mut self, constraint: SizeConstraint, ctx: &mut LayoutContext) -> Vector2F { @@ -191,30 +207,41 @@ impl AnyElement for Lifecycle { impl ElementBox { pub fn layout(&mut self, constraint: SizeConstraint, ctx: &mut LayoutContext) -> Vector2F { - self.0.layout(constraint, ctx) + self.element.layout(constraint, ctx) } pub fn after_layout(&mut self, ctx: &mut AfterLayoutContext) { - self.0.after_layout(ctx); + self.element.after_layout(ctx); } pub fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext) { - self.0.paint(origin, ctx); + self.element.paint(origin, ctx); } pub fn dispatch_event(&mut self, event: &Event, ctx: &mut EventContext) -> bool { - self.0.dispatch_event(event, ctx) + self.element.dispatch_event(event, ctx) } pub fn size(&self) -> Vector2F { - self.0.size() + self.element.size() } pub fn metadata(&self) -> Option<&dyn Any> { - self.0.metadata() + self.element.metadata() } pub fn debug(&self, ctx: &DebugContext) -> json::Value { - self.0.debug(ctx) + let mut value = self.element.debug(ctx); + + if let Some(name) = &self.name { + if let json::Value::Object(map) = &mut value { + let mut new_map: crate::json::Map = Default::default(); + new_map.insert("name".into(), json::Value::String(name.to_string())); + new_map.append(map); + return json::Value::Object(new_map); + } + } + + value } } diff --git a/gpui/src/platform/mac/app.rs b/gpui/src/platform/mac/app.rs index daf8335f60..792639e9d2 100644 --- a/gpui/src/platform/mac/app.rs +++ b/gpui/src/platform/mac/app.rs @@ -1,9 +1,13 @@ use super::{BoolExt as _, Dispatcher, FontSystem, Window}; use crate::{executor, platform}; use anyhow::Result; -use cocoa::base::id; +use cocoa::{ + appkit::{NSPasteboard, NSPasteboardTypeString}, + base::{id, nil}, + foundation::NSData, +}; use objc::{class, msg_send, sel, sel_impl}; -use std::{rc::Rc, sync::Arc}; +use std::{ffi::c_void, rc::Rc, sync::Arc}; pub struct App { dispatcher: Arc, @@ -42,4 +46,17 @@ impl platform::App for App { fn fonts(&self) -> Arc { self.fonts.clone() } + + fn copy(&self, text: &str) { + unsafe { + let data = NSData::dataWithBytes_length_( + nil, + text.as_ptr() as *const c_void, + text.len() as u64, + ); + let pasteboard = NSPasteboard::generalPasteboard(nil); + pasteboard.clearContents(); + pasteboard.setData_forType(data, NSPasteboardTypeString); + } + } } diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index 5bd7e4659e..ce4c27f923 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -40,6 +40,7 @@ pub trait App { executor: Rc, ) -> Result>; fn fonts(&self) -> Arc; + fn copy(&self, text: &str); } pub trait Dispatcher: Send + Sync { diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 545ab10e34..c42b94628a 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -46,6 +46,8 @@ impl super::App for App { fn fonts(&self) -> std::sync::Arc { self.fonts.clone() } + + fn copy(&self, _: &str) {} } impl Window { diff --git a/zed/Cargo.toml b/zed/Cargo.toml index b1cecf1ae0..61864372cd 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -18,9 +18,9 @@ arrayvec = "0.5.2" crossbeam-channel = "0.5.0" dirs = "3.0" easy-parallel = "3.1.0" +futures-core = "0.3" gpui = {path = "../gpui"} ignore = {git = "https://github.com/zed-industries/ripgrep", rev = "1d152118f35b3e3590216709b86277062d79b8a0"} -futures-core = "0.3" lazy_static = "1.4.0" libc = "0.2" log = "0.4" @@ -33,6 +33,6 @@ smallvec = "1.6.1" smol = "1.2.5" [dev-dependencies] -serde_json = "1.0.64" +serde_json = {version = "1.0.64", features = ["preserve_order"]} tempdir = "0.3.7" unindent = "0.1.7" diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 39b497f5c8..c951d12934 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -78,7 +78,7 @@ impl View for FileFinder { .boxed(), ) .top_center() - .boxed() + .named("file finder") } fn on_focus(&mut self, ctx: &mut ViewContext) { @@ -105,7 +105,7 @@ impl FileFinder { .boxed(), ) .with_margin_top(6.0) - .boxed(); + .named("empty matches"); } let handle = self.handle.clone(); @@ -127,7 +127,7 @@ impl FileFinder { .with_background_color(ColorU::from_u32(0xf7f7f7ff)) .with_border(Border::all(1.0, ColorU::from_u32(0xdbdbdcff))) .with_margin_top(6.0) - .boxed() + .named("matches") } fn render_match( @@ -226,7 +226,7 @@ impl FileFinder { ctx.dispatch_action("file_finder:select", (tree_id, entry_id)); true }) - .boxed() + .named("match") }) } diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 845fa0c4a7..fad299aa8b 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -244,7 +244,7 @@ impl Pane { .with_max_width(264.0) .boxed(), ) - .boxed(), + .named("tab"), ); } @@ -263,10 +263,10 @@ impl Pane { .with_border(Border::bottom(1.0, border_color)) .boxed(), ) - .boxed(), + .named("filler"), ); - row.boxed() + row.named("tabs") } fn render_modified_icon(is_modified: bool) -> ElementBox { @@ -304,9 +304,9 @@ impl View for Pane { Flex::column() .with_child(self.render_tabs(app)) .with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed()) - .boxed() + .named("pane") } else { - Empty::new().boxed() + Empty::new().named("pane") } } diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 7cb9fd8b64..5810245be4 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -258,7 +258,11 @@ impl WorkspaceView { pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext) { match to_string_pretty(&ctx.debug_elements()) { Ok(json) => { - log::info!("{}", json); + ctx.app_mut().copy(&json); + log::info!( + "copied {:.1} KiB of element debug JSON to the clipboard", + json.len() as f32 / 1024. + ); } Err(error) => { log::error!("error debugging elements: {}", error); @@ -373,7 +377,7 @@ impl View for WorkspaceView { .boxed(), ) .with_background_color(rgbu(0xea, 0xea, 0xeb)) - .boxed() + .named("workspace") } fn on_focus(&mut self, ctx: &mut ViewContext) { From b1536cca58750e050a4b555ececaeaebb674cb34 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 7 Apr 2021 21:56:45 -0600 Subject: [PATCH 04/12] Add README tip --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a335dbf648..e1a64b0dd8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,11 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true. -Everything is under construction, including this README, but in the meantime, here is a high-level roadmap: +## Development tips + +### Dump element JSON + +If you trigger `cmd-shift-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way. ## Roadmap From 44ab38ac98604b6baaadb6066cc8165ecd71865c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 7 Apr 2021 19:56:15 +0200 Subject: [PATCH 05/12] Introduce undo/redo support for arbitrary operations in `Buffer` --- zed/src/editor/buffer/mod.rs | 355 ++++++++++++++++++++++++++++------- zed/src/time.rs | 8 +- 2 files changed, 287 insertions(+), 76 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 973fbf6a2d..3b1e5466af 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -32,13 +32,51 @@ use std::{ pub type SelectionSetId = time::Lamport; pub type SelectionsVersion = usize; +#[derive(Clone, Default)] +struct UndoMap(HashMap>); + +impl UndoMap { + fn insert(&mut self, undo: UndoOperation) { + self.0.entry(undo.edit_id).or_default().push(undo); + } + + fn is_undone(&self, op_id: time::Local) -> bool { + self.undo_count(op_id) % 2 == 1 + } + + fn was_undone(&self, op_id: time::Local, version: &time::Global) -> bool { + let undo_count = self + .0 + .get(&op_id) + .unwrap_or(&Vec::new()) + .iter() + .filter(|undo| version.observed(undo.id)) + .map(|undo| undo.count) + .max() + .unwrap_or(0); + undo_count % 2 == 1 + } + + fn undo_count(&self, op_id: time::Local) -> u32 { + self.0 + .get(&op_id) + .unwrap_or(&Vec::new()) + .iter() + .map(|undo| undo.count) + .max() + .unwrap_or(0) + } +} + pub struct Buffer { file: Option, fragments: SumTree, insertion_splits: HashMap>, + edit_ops: HashMap, pub version: time::Global, saved_version: time::Global, last_edit: time::Local, + undos: UndoMap, selections: HashMap>, pub selections_last_update: SelectionsVersion, deferred_ops: OperationQueue, @@ -78,6 +116,7 @@ pub struct FragmentIter<'a> { struct Edits<'a, F: Fn(&FragmentSummary) -> bool> { cursor: FilterCursor<'a, F, Fragment, usize>, + undos: &'a UndoMap, since: time::Global, delta: isize, } @@ -114,6 +153,7 @@ struct Fragment { insertion: Insertion, text: Text, deletions: HashSet, + visible: bool, } #[derive(Eq, PartialEq, Clone, Debug)] @@ -143,13 +183,11 @@ struct InsertionSplitSummary { #[derive(Clone, Debug, Eq, PartialEq)] pub enum Operation { Edit { - start_id: time::Local, - start_offset: usize, - end_id: time::Local, - end_offset: usize, - version_in_range: time::Global, - new_text: Option, - local_timestamp: time::Local, + edit: EditOperation, + lamport_timestamp: time::Lamport, + }, + Undo { + undo: UndoOperation, lamport_timestamp: time::Lamport, }, UpdateSelections { @@ -159,6 +197,24 @@ pub enum Operation { }, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EditOperation { + id: time::Local, + start_id: time::Local, + start_offset: usize, + end_id: time::Local, + end_offset: usize, + version_in_range: time::Global, + new_text: Option, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct UndoOperation { + id: time::Local, + edit_id: time::Local, + count: u32, +} + impl Buffer { pub fn new>(replica_id: ReplicaId, base_text: T) -> Self { Self::build(replica_id, None, base_text.into()) @@ -192,6 +248,7 @@ impl Buffer { insertion: base_insertion.clone(), text: base_insertion.text.slice(0..0), deletions: HashSet::new(), + visible: true, }); if base_insertion.text.len() > 0 { @@ -210,6 +267,7 @@ impl Buffer { text: base_insertion.text.clone(), insertion: base_insertion, deletions: HashSet::new(), + visible: true, }); } @@ -217,9 +275,11 @@ impl Buffer { file, fragments, insertion_splits, + edit_ops: HashMap::new(), version: time::Global::new(), saved_version: time::Global::new(), last_edit: time::Local::default(), + undos: Default::default(), selections: HashMap::default(), selections_last_update: 0, deferred_ops: OperationQueue::new(), @@ -391,6 +451,7 @@ impl Buffer { Edits { cursor, + undos: &self.undos, since, delta: 0, } @@ -432,6 +493,12 @@ impl Buffer { new_text.clone(), ); + for op in &ops { + if let Operation::Edit { edit, .. } = op { + self.edit_ops.insert(edit.id, edit.clone()); + } + } + if let Some(op) = ops.last() { if let Some(ctx) = ctx { ctx.notify(); @@ -441,12 +508,9 @@ impl Buffer { } } - if let Operation::Edit { - local_timestamp, .. - } = op - { - self.last_edit = *local_timestamp; - self.version.observe(*local_timestamp); + if let Operation::Edit { edit, .. } = op { + self.last_edit = edit.id; + self.version.observe(edit.id); } else { unreachable!() } @@ -676,29 +740,33 @@ impl Buffer { fn apply_op(&mut self, op: Operation) -> Result<()> { match op { Operation::Edit { - start_id, - start_offset, - end_id, - end_offset, - new_text, - version_in_range, - local_timestamp, + edit, lamport_timestamp, + .. } => { - if !self.version.observed(local_timestamp) { + if !self.version.observed(edit.id) { self.apply_edit( - start_id, - start_offset, - end_id, - end_offset, - new_text.as_ref().cloned(), - &version_in_range, - local_timestamp, + edit.start_id, + edit.start_offset, + edit.end_id, + edit.end_offset, + edit.new_text.as_ref().cloned(), + &edit.version_in_range, + edit.id, lamport_timestamp, )?; - self.version.observe(local_timestamp); + self.version.observe(edit.id); + self.edit_ops.insert(edit.id, edit); } } + Operation::Undo { + undo, + lamport_timestamp, + } => { + self.apply_undo(undo)?; + self.version.observe(undo.id); + self.lamport_clock.observe(lamport_timestamp); + } Operation::UpdateSelections { set_id, selections, @@ -787,6 +855,7 @@ impl Buffer { if let Some(mut fragment) = within_range { if version_in_range.observed(fragment.insertion.id) { fragment.deletions.insert(local_timestamp); + fragment.visible = false; } new_fragments.push(fragment); } @@ -807,6 +876,7 @@ impl Buffer { if fragment.id < end_fragment_id && version_in_range.observed(fragment.insertion.id) { fragment.deletions.insert(local_timestamp); + fragment.visible = false; } new_fragments.push(fragment); } @@ -831,6 +901,88 @@ impl Buffer { Ok(()) } + fn undo_or_redo( + &mut self, + edit_id: time::Local, + ctx: Option<&mut ModelContext>, + ) -> Result { + let was_dirty = self.is_dirty(); + let old_version = self.version.clone(); + let undo = UndoOperation { + id: self.local_clock.tick(), + edit_id, + count: self.undos.undo_count(edit_id) + 1, + }; + self.apply_undo(undo)?; + self.version.observe(undo.id); + + if let Some(ctx) = ctx { + ctx.notify(); + let changes = self.edits_since(old_version).collect::>(); + if !changes.is_empty() { + self.did_edit(changes, was_dirty, ctx); + } + } + + Ok(Operation::Undo { + undo, + lamport_timestamp: self.lamport_clock.tick(), + }) + } + + fn apply_undo(&mut self, undo: UndoOperation) -> Result<()> { + let mut new_fragments; + + self.undos.insert(undo); + let edit = &self.edit_ops[&undo.edit_id]; + let start_fragment_id = self.resolve_fragment_id(edit.start_id, edit.start_offset)?; + let end_fragment_id = self.resolve_fragment_id(edit.end_id, edit.end_offset)?; + let mut cursor = self.fragments.cursor::(); + + if start_fragment_id == end_fragment_id { + let splits = &self.insertion_splits[&undo.edit_id]; + let mut insertion_splits = splits.cursor::<(), ()>().map(|s| &s.fragment_id).peekable(); + + let first_split_id = insertion_splits.next().unwrap(); + new_fragments = cursor.slice(&FragmentIdRef::new(first_split_id), SeekBias::Left); + + loop { + let mut fragment = cursor.item().unwrap().clone(); + fragment.visible = fragment.is_visible(&self.undos); + new_fragments.push(fragment); + cursor.next(); + if let Some(split_id) = insertion_splits.next() { + new_fragments + .push_tree(cursor.slice(&FragmentIdRef::new(split_id), SeekBias::Left)); + } else { + break; + } + } + } else { + new_fragments = cursor.slice(&FragmentIdRef::new(&start_fragment_id), SeekBias::Left); + while let Some(fragment) = cursor.item() { + if fragment.id > end_fragment_id { + break; + } else { + let mut fragment = fragment.clone(); + if edit.version_in_range.observed(fragment.insertion.id) + || fragment.insertion.id == undo.edit_id + { + fragment.visible = fragment.is_visible(&self.undos); + } + new_fragments.push(fragment); + cursor.next(); + } + } + } + + new_fragments.push_tree(cursor.suffix()); + drop(cursor); + self.fragments = new_fragments; + + Ok(()) + } + fn flush_deferred_ops(&mut self) -> Result<()> { self.deferred_replicas.clear(); let mut deferred_ops = Vec::new(); @@ -851,16 +1003,12 @@ impl Buffer { false } else { match op { - Operation::Edit { - start_id, - end_id, - version_in_range, - .. - } => { - self.version.observed(*start_id) - && self.version.observed(*end_id) - && *version_in_range <= self.version + Operation::Edit { edit, .. } => { + self.version.observed(edit.start_id) + && self.version.observed(edit.end_id) + && edit.version_in_range <= self.version } + Operation::Undo { undo, .. } => self.version.observed(undo.edit_id), Operation::UpdateSelections { selections, .. } => { if let Some(selections) = selections { selections.iter().all(|selection| { @@ -986,8 +1134,9 @@ impl Buffer { prefix.set_end_offset(prefix.start_offset() + (range.end - fragment_start)); prefix.id = FragmentId::between(&new_fragments.last().unwrap().id, &fragment.id); - if fragment.is_visible() { + if fragment.visible { prefix.deletions.insert(local_timestamp); + prefix.visible = false; } fragment.set_start_offset(prefix.end_offset()); new_fragments.push(prefix.clone()); @@ -1002,8 +1151,9 @@ impl Buffer { } } else { version_in_range.observe(fragment.insertion.id); - if fragment.is_visible() { + if fragment.visible { fragment.deletions.insert(local_timestamp); + fragment.visible = false; } } @@ -1012,13 +1162,15 @@ impl Buffer { // loop and find the first fragment that the splice does not contain fully. if range.end <= fragment_end { ops.push(Operation::Edit { - start_id: start_id.unwrap(), - start_offset: start_offset.unwrap(), - end_id: end_id.unwrap(), - end_offset: end_offset.unwrap(), - version_in_range, - new_text: new_text.clone(), - local_timestamp, + edit: EditOperation { + id: local_timestamp, + start_id: start_id.unwrap(), + start_offset: start_offset.unwrap(), + end_id: end_id.unwrap(), + end_offset: end_offset.unwrap(), + version_in_range, + new_text: new_text.clone(), + }, lamport_timestamp, }); @@ -1055,8 +1207,9 @@ impl Buffer { fragment_end = fragment_start + fragment.visible_len(); if range.start < fragment_start && range.end >= fragment_end { let mut new_fragment = fragment.clone(); - if new_fragment.is_visible() { + if new_fragment.visible { new_fragment.deletions.insert(local_timestamp); + new_fragment.visible = false; } version_in_range.observe(new_fragment.insertion.id); new_fragments.push(new_fragment); @@ -1066,13 +1219,15 @@ impl Buffer { end_id = Some(fragment.insertion.id); end_offset = Some(fragment.end_offset()); ops.push(Operation::Edit { - start_id: start_id.unwrap(), - start_offset: start_offset.unwrap(), - end_id: end_id.unwrap(), - end_offset: end_offset.unwrap(), - version_in_range, - new_text: new_text.clone(), - local_timestamp, + edit: EditOperation { + id: local_timestamp, + start_id: start_id.unwrap(), + start_offset: start_offset.unwrap(), + end_id: end_id.unwrap(), + end_offset: end_offset.unwrap(), + version_in_range, + new_text: new_text.clone(), + }, lamport_timestamp, }); @@ -1111,13 +1266,15 @@ impl Buffer { debug_assert_eq!(old_ranges.next(), None); let last_fragment = new_fragments.last().unwrap(); ops.push(Operation::Edit { - start_id: last_fragment.insertion.id, - start_offset: last_fragment.end_offset(), - end_id: last_fragment.insertion.id, - end_offset: last_fragment.end_offset(), - version_in_range: time::Global::new(), - new_text: new_text.clone(), - local_timestamp, + edit: EditOperation { + id: local_timestamp, + start_id: last_fragment.insertion.id, + start_offset: last_fragment.end_offset(), + end_id: last_fragment.insertion.id, + end_offset: last_fragment.end_offset(), + version_in_range: time::Global::new(), + new_text: new_text.clone(), + }, lamport_timestamp, }); @@ -1365,7 +1522,7 @@ impl Buffer { .ok_or_else(|| anyhow!("fragment id does not exist"))?; let mut summary = fragments_cursor.start().clone(); - if fragment.is_visible() { + if fragment.visible { summary += fragment .text .slice(..offset - fragment.start_offset()) @@ -1398,9 +1555,11 @@ impl Clone for Buffer { file: self.file.clone(), fragments: self.fragments.clone(), insertion_splits: self.insertion_splits.clone(), + edit_ops: self.edit_ops.clone(), version: self.version.clone(), saved_version: self.saved_version.clone(), last_edit: self.last_edit.clone(), + undos: self.undos.clone(), selections: self.selections.clone(), selections_last_update: self.selections_last_update.clone(), deferred_ops: self.deferred_ops.clone(), @@ -1464,7 +1623,7 @@ impl<'a> Iterator for CharIter<'a> { loop { self.fragments_cursor.next(); if let Some(fragment) = self.fragments_cursor.item() { - if fragment.is_visible() { + if fragment.visible { self.fragment_chars = fragment.text.as_str().chars(); return self.fragment_chars.next(); } @@ -1498,7 +1657,7 @@ impl<'a> Iterator for FragmentIter<'a> { self.started = true; } if let Some(fragment) = self.cursor.item() { - if fragment.is_visible() { + if fragment.visible { return Some(fragment.text.as_str()); } } else { @@ -1518,7 +1677,7 @@ impl<'a, F: Fn(&FragmentSummary) -> bool> Iterator for Edits<'a, F> { let new_offset = *self.cursor.start(); let old_offset = (new_offset as isize - self.delta) as usize; - if !fragment.was_visible(&self.since) && fragment.is_visible() { + if !fragment.was_visible(&self.since, &self.undos) && fragment.visible { if let Some(ref mut change) = change { if change.new_range.end == new_offset { change.new_range.end += fragment.len(); @@ -1533,7 +1692,7 @@ impl<'a, F: Fn(&FragmentSummary) -> bool> Iterator for Edits<'a, F> { }); self.delta += fragment.len() as isize; } - } else if fragment.was_visible(&self.since) && !fragment.is_visible() { + } else if fragment.was_visible(&self.since, &self.undos) && !fragment.visible { if let Some(ref mut change) = change { if change.new_range.end == new_offset { change.old_range.end += fragment.len(); @@ -1733,6 +1892,7 @@ impl Fragment { text: insertion.text.clone(), insertion, deletions: HashSet::new(), + visible: true, } } @@ -1753,7 +1913,7 @@ impl Fragment { } fn visible_len(&self) -> usize { - if self.is_visible() { + if self.visible { self.len() } else { 0 @@ -1764,12 +1924,16 @@ impl Fragment { self.text.len() } - fn is_visible(&self) -> bool { - self.deletions.is_empty() + fn is_visible(&self, undos: &UndoMap) -> bool { + !undos.is_undone(self.insertion.id) && self.deletions.iter().all(|d| undos.is_undone(*d)) } - fn was_visible(&self, version: &time::Global) -> bool { - version.observed(self.insertion.id) && self.deletions.iter().all(|d| !version.observed(*d)) + fn was_visible(&self, version: &time::Global, undos: &UndoMap) -> bool { + (version.observed(self.insertion.id) && !undos.was_undone(self.insertion.id, version)) + && self + .deletions + .iter() + .all(|d| !version.observed(*d) || undos.was_undone(*d, version)) } fn point_for_offset(&self, offset: usize) -> Result { @@ -1791,7 +1955,7 @@ impl sum_tree::Item for Fragment { max_version.observe(*deletion); } - if self.is_visible() { + if self.visible { FragmentSummary { text_summary: self.text.summary(), max_fragment_id: self.id.clone(), @@ -1899,6 +2063,9 @@ impl Operation { Operation::Edit { lamport_timestamp, .. } => *lamport_timestamp, + Operation::Undo { + lamport_timestamp, .. + } => *lamport_timestamp, Operation::UpdateSelections { lamport_timestamp, .. } => *lamport_timestamp, @@ -2607,6 +2774,38 @@ mod tests { Ok(()) } + #[test] + fn test_undo_redo() -> Result<()> { + let mut buffer = Buffer::new(0, ""); + + let edit1 = buffer.edit(vec![0..0], "abx", None)?; + let edit2 = buffer.edit(vec![2..3], "yzef", None)?; + let edit3 = buffer.edit(vec![2..4], "cd", None)?; + + buffer.undo_or_redo(edit1[0].edit_id().unwrap(), None)?; + assert_eq!(buffer.text(), "cdef"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap(), None)?; + assert_eq!(buffer.text(), "abcdef"); + + buffer.undo_or_redo(edit2[0].edit_id().unwrap(), None)?; + assert_eq!(buffer.text(), "abcdx"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap(), None)?; + assert_eq!(buffer.text(), "abx"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap(), None)?; + assert_eq!(buffer.text(), "abyzef"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap(), None)?; + assert_eq!(buffer.text(), "abcdef"); + + buffer.undo_or_redo(edit3[0].edit_id().unwrap(), None)?; + assert_eq!(buffer.text(), "abyzef"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap(), None)?; + assert_eq!(buffer.text(), "yzef"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap(), None)?; + assert_eq!(buffer.text(), ""); + + Ok(()) + } + #[test] fn test_random_concurrent_edits() { use crate::test::Network; @@ -2710,6 +2909,16 @@ mod tests { } } + impl Operation { + fn edit_id(&self) -> Option { + match self { + Operation::Edit { edit, .. } => Some(edit.id), + Operation::Undo { undo, .. } => Some(undo.edit_id), + Operation::UpdateSelections { .. } => None, + } + } + } + fn line_lengths_in_range(buffer: &Buffer, range: Range) -> BTreeMap> { let mut lengths = BTreeMap::new(); for (row, line) in buffer.text()[range].lines().enumerate() { diff --git a/zed/src/time.rs b/zed/src/time.rs index 3c088fc4d1..38f15f2dde 100644 --- a/zed/src/time.rs +++ b/zed/src/time.rs @@ -5,10 +5,12 @@ use std::ops::{Add, AddAssign}; use std::sync::Arc; pub type ReplicaId = u16; +pub type Seq = u64; + #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd)] pub struct Local { pub replica_id: ReplicaId, - pub value: u64, + pub value: Seq, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -16,7 +18,7 @@ pub struct Global(Arc>); #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct Lamport { - pub value: u64, + pub value: Seq, pub replica_id: ReplicaId, } @@ -62,7 +64,7 @@ impl Global { Global(Arc::new(HashMap::new())) } - pub fn get(&self, replica_id: ReplicaId) -> u64 { + pub fn get(&self, replica_id: ReplicaId) -> Seq { *self.0.get(&replica_id).unwrap_or(&0) } From 5e0d5b7f9ab28540414ee8752617c5e5a228487e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 8 Apr 2021 15:03:11 +0200 Subject: [PATCH 06/12] Fix `Debug` impl for `Text` to only display the slice a `Text` refers to ...as opposed to the whole string. --- zed/src/editor/buffer/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/src/editor/buffer/text.rs b/zed/src/editor/buffer/text.rs index 1a074d4a75..147ffdd86a 100644 --- a/zed/src/editor/buffer/text.rs +++ b/zed/src/editor/buffer/text.rs @@ -162,7 +162,7 @@ impl<'a> From<&'a str> for Text { impl Debug for Text { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Text").field(&self.text).finish() + f.debug_tuple("Text").field(&self.as_str()).finish() } } From c20935de9126d2a13a3e2d7a8828ad1776e2b265 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 8 Apr 2021 16:57:46 +0200 Subject: [PATCH 07/12] WIP --- Cargo.lock | 7 ++ zed/Cargo.toml | 1 + zed/src/editor/buffer/mod.rs | 134 +++++++++++++++++++++++++---------- 3 files changed, 105 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38d75ed246..5d83ea1e40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1674,6 +1674,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "semver" version = "0.9.0" @@ -2251,6 +2257,7 @@ dependencies = [ "parking_lot", "rand 0.8.3", "rust-embed", + "seahash", "serde_json", "simplelog", "smallvec", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index b1cecf1ae0..a3d1a0457c 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -28,6 +28,7 @@ num_cpus = "1.13.0" parking_lot = "0.11.1" rand = "0.8.3" rust-embed = "5.9.0" +seahash = "4.1" simplelog = "0.9" smallvec = "1.6.1" smol = "1.2.5" diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 3b1e5466af..e828a3aae1 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -5,6 +5,7 @@ mod text; pub use anchor::*; use futures_core::future::LocalBoxFuture; pub use point::*; +use seahash::SeaHasher; pub use text::*; use crate::{ @@ -20,7 +21,7 @@ use lazy_static::lazy_static; use rand::prelude::*; use std::{ cmp::{self, Ordering}, - collections::{HashMap, HashSet}, + hash::BuildHasher, iter::{self, Iterator}, mem, ops::{AddAssign, Range}, @@ -32,6 +33,29 @@ use std::{ pub type SelectionSetId = time::Lamport; pub type SelectionsVersion = usize; +#[derive(Clone, Default)] +struct DeterministicState; + +impl BuildHasher for DeterministicState { + type Hasher = SeaHasher; + + fn build_hasher(&self) -> Self::Hasher { + SeaHasher::new() + } +} + +#[cfg(test)] +type HashMap = std::collections::HashMap; + +#[cfg(test)] +type HashSet = std::collections::HashSet; + +#[cfg(not(test))] +type HashMap = std::collections::HashMap; + +#[cfg(not(test))] +type HashSet = std::collections::HashSet; + #[derive(Clone, Default)] struct UndoMap(HashMap>); @@ -40,14 +64,14 @@ impl UndoMap { self.0.entry(undo.edit_id).or_default().push(undo); } - fn is_undone(&self, op_id: time::Local) -> bool { - self.undo_count(op_id) % 2 == 1 + fn is_undone(&self, edit_id: time::Local) -> bool { + self.undo_count(edit_id) % 2 == 1 } - fn was_undone(&self, op_id: time::Local, version: &time::Global) -> bool { + fn was_undone(&self, edit_id: time::Local, version: &time::Global) -> bool { let undo_count = self .0 - .get(&op_id) + .get(&edit_id) .unwrap_or(&Vec::new()) .iter() .filter(|undo| version.observed(undo.id)) @@ -57,9 +81,9 @@ impl UndoMap { undo_count % 2 == 1 } - fn undo_count(&self, op_id: time::Local) -> u32 { + fn undo_count(&self, edit_id: time::Local) -> u32 { self.0 - .get(&op_id) + .get(&edit_id) .unwrap_or(&Vec::new()) .iter() .map(|undo| undo.count) @@ -76,7 +100,7 @@ pub struct Buffer { pub version: time::Global, saved_version: time::Global, last_edit: time::Local, - undos: UndoMap, + undo_map: UndoMap, selections: HashMap>, pub selections_last_update: SelectionsVersion, deferred_ops: OperationQueue, @@ -225,7 +249,7 @@ impl Buffer { } fn build(replica_id: ReplicaId, file: Option, base_text: String) -> Self { - let mut insertion_splits = HashMap::new(); + let mut insertion_splits = HashMap::default(); let mut fragments = SumTree::new(); let base_insertion = Insertion { @@ -247,7 +271,7 @@ impl Buffer { id: FragmentId::min_value().clone(), insertion: base_insertion.clone(), text: base_insertion.text.slice(0..0), - deletions: HashSet::new(), + deletions: HashSet::default(), visible: true, }); @@ -266,7 +290,7 @@ impl Buffer { id: base_fragment_id, text: base_insertion.text.clone(), insertion: base_insertion, - deletions: HashSet::new(), + deletions: HashSet::default(), visible: true, }); } @@ -275,15 +299,15 @@ impl Buffer { file, fragments, insertion_splits, - edit_ops: HashMap::new(), + edit_ops: HashMap::default(), version: time::Global::new(), saved_version: time::Global::new(), last_edit: time::Local::default(), - undos: Default::default(), + undo_map: Default::default(), selections: HashMap::default(), selections_last_update: 0, deferred_ops: OperationQueue::new(), - deferred_replicas: HashSet::new(), + deferred_replicas: HashSet::default(), replica_id, local_clock: time::Local::new(replica_id), lamport_clock: time::Lamport::new(replica_id), @@ -451,7 +475,7 @@ impl Buffer { Edits { cursor, - undos: &self.undos, + undos: &self.undo_map, since, delta: 0, } @@ -911,7 +935,7 @@ impl Buffer { let undo = UndoOperation { id: self.local_clock.tick(), edit_id, - count: self.undos.undo_count(edit_id) + 1, + count: self.undo_map.undo_count(edit_id) + 1, }; self.apply_undo(undo)?; self.version.observe(undo.id); @@ -931,15 +955,26 @@ impl Buffer { } fn apply_undo(&mut self, undo: UndoOperation) -> Result<()> { + // let mut new_fragments = SumTree::new(); + + // self.undos.insert(undo); + // let edit = &self.edit_ops[&undo.edit_id]; + // let start_fragment_id = self.resolve_fragment_id(edit.start_id, edit.start_offset)?; + // let end_fragment_id = self.resolve_fragment_id(edit.end_id, edit.end_offset)?; + // let mut cursor = self.fragments.cursor::(); + + // for fragment in cursor {} + // self.fragments = new_fragments; + let mut new_fragments; - self.undos.insert(undo); + self.undo_map.insert(undo); let edit = &self.edit_ops[&undo.edit_id]; let start_fragment_id = self.resolve_fragment_id(edit.start_id, edit.start_offset)?; let end_fragment_id = self.resolve_fragment_id(edit.end_id, edit.end_offset)?; let mut cursor = self.fragments.cursor::(); - if start_fragment_id == end_fragment_id { + if edit.start_id == edit.end_id && edit.start_offset == edit.end_offset { let splits = &self.insertion_splits[&undo.edit_id]; let mut insertion_splits = splits.cursor::<(), ()>().map(|s| &s.fragment_id).peekable(); @@ -948,7 +983,7 @@ impl Buffer { loop { let mut fragment = cursor.item().unwrap().clone(); - fragment.visible = fragment.is_visible(&self.undos); + fragment.visible = fragment.is_visible(&self.undo_map); new_fragments.push(fragment); cursor.next(); if let Some(split_id) = insertion_splits.next() { @@ -968,7 +1003,7 @@ impl Buffer { if edit.version_in_range.observed(fragment.insertion.id) || fragment.insertion.id == undo.edit_id { - fragment.visible = fragment.is_visible(&self.undos); + fragment.visible = fragment.is_visible(&self.undo_map); } new_fragments.push(fragment); cursor.next(); @@ -1559,7 +1594,7 @@ impl Clone for Buffer { version: self.version.clone(), saved_version: self.saved_version.clone(), last_edit: self.last_edit.clone(), - undos: self.undos.clone(), + undo_map: self.undo_map.clone(), selections: self.selections.clone(), selections_last_update: self.selections_last_update.clone(), deferred_ops: self.deferred_ops.clone(), @@ -1891,7 +1926,7 @@ impl Fragment { id, text: insertion.text.clone(), insertion, - deletions: HashSet::new(), + deletions: HashSet::default(), visible: true, } } @@ -2810,9 +2845,9 @@ mod tests { fn test_random_concurrent_edits() { use crate::test::Network; - const PEERS: usize = 3; + const PEERS: usize = 2; - for seed in 0..50 { + for seed in 0..1000 { println!("{:?}", seed); let mut rng = &mut StdRng::seed_from_u64(seed); @@ -2830,19 +2865,29 @@ mod tests { network.add_peer(i as u16); } - let mut mutation_count = 10; + let mut mutation_count = 3; loop { let replica_index = rng.gen_range(0..PEERS); let replica_id = replica_ids[replica_index]; let buffer = &mut buffers[replica_index]; - if mutation_count > 0 && rng.gen() { - let (_, _, ops) = buffer.randomly_mutate(&mut rng, None); - network.broadcast(replica_id, ops, &mut rng); - mutation_count -= 1; - } else if network.has_unreceived(replica_id) { - buffer - .apply_ops(network.receive(replica_id, &mut rng), None) - .unwrap(); + + match rng.gen_range(0..=100) { + 0..=50 if mutation_count != 0 => { + let (_, _, ops) = buffer.randomly_mutate(&mut rng, None); + network.broadcast(replica_id, ops, &mut rng); + mutation_count -= 1; + } + 51..=70 if mutation_count != 0 => { + let ops = buffer.randomly_undo_redo(&mut rng, None); + network.broadcast(replica_id, ops, &mut rng); + mutation_count -= 1; + } + 71..=100 if network.has_unreceived(replica_id) => { + buffer + .apply_ops(network.receive(replica_id, &mut rng), None) + .unwrap(); + } + _ => {} } if mutation_count == 0 && network.is_idle() { @@ -2868,13 +2913,14 @@ mod tests { pub fn randomly_mutate( &mut self, rng: &mut T, - ctx: Option<&mut ModelContext>, + mut ctx: Option<&mut ModelContext>, ) -> (Vec>, String, Vec) where T: Rng, { // Randomly edit - let (old_ranges, new_text, mut operations) = self.randomly_edit(rng, 5, ctx); + let (old_ranges, new_text, mut operations) = + self.randomly_edit(rng, 5, ctx.as_deref_mut()); // Randomly add, remove or mutate selection sets. let replica_selection_sets = &self @@ -2907,6 +2953,20 @@ mod tests { (old_ranges, new_text, operations) } + + pub fn randomly_undo_redo( + &mut self, + rng: &mut impl Rng, + mut ctx: Option<&mut ModelContext>, + ) -> Vec { + let mut ops = Vec::new(); + for _ in 0..rng.gen_range(0..5) { + if let Some(edit_id) = self.edit_ops.keys().choose(rng).copied() { + ops.push(self.undo_or_redo(edit_id, ctx.as_deref_mut()).unwrap()); + } + } + ops + } } impl Operation { @@ -2924,11 +2984,11 @@ mod tests { for (row, line) in buffer.text()[range].lines().enumerate() { lengths .entry(line.len() as u32) - .or_insert(HashSet::new()) + .or_insert(HashSet::default()) .insert(row as u32); } if lengths.is_empty() { - let mut rows = HashSet::new(); + let mut rows = HashSet::default(); rows.insert(0); lengths.insert(0, rows); } From 7b6fbe1d894d278b4f3d9f8528240032af90d8e7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 9 Apr 2021 10:07:58 +0200 Subject: [PATCH 08/12] Compute minimal `version_in_range` on edit and account for undo --- zed/src/editor/buffer/mod.rs | 49 +++++++++++++++++++----------------- zed/src/sum_tree/cursor.rs | 2 +- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index e828a3aae1..a16cf1f87c 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -56,7 +56,7 @@ type HashMap = std::collections::HashMap; #[cfg(not(test))] type HashSet = std::collections::HashSet; -#[derive(Clone, Default)] +#[derive(Clone, Default, Debug)] struct UndoMap(HashMap>); impl UndoMap { @@ -177,6 +177,7 @@ struct Fragment { insertion: Insertion, text: Text, deletions: HashSet, + undos: HashSet, visible: bool, } @@ -272,6 +273,7 @@ impl Buffer { insertion: base_insertion.clone(), text: base_insertion.text.slice(0..0), deletions: HashSet::default(), + undos: HashSet::default(), visible: true, }); @@ -291,6 +293,7 @@ impl Buffer { text: base_insertion.text.clone(), insertion: base_insertion, deletions: HashSet::default(), + undos: HashSet::default(), visible: true, }); } @@ -787,9 +790,11 @@ impl Buffer { undo, lamport_timestamp, } => { - self.apply_undo(undo)?; - self.version.observe(undo.id); - self.lamport_clock.observe(lamport_timestamp); + if !self.version.observed(undo.id) { + self.apply_undo(undo)?; + self.version.observe(undo.id); + self.lamport_clock.observe(lamport_timestamp); + } } Operation::UpdateSelections { set_id, @@ -877,7 +882,7 @@ impl Buffer { new_fragments.push(fragment); } if let Some(mut fragment) = within_range { - if version_in_range.observed(fragment.insertion.id) { + if fragment.was_visible(&version_in_range, &self.undo_map) { fragment.deletions.insert(local_timestamp); fragment.visible = false; } @@ -897,7 +902,8 @@ impl Buffer { )); } - if fragment.id < end_fragment_id && version_in_range.observed(fragment.insertion.id) + if fragment.id < end_fragment_id + && fragment.was_visible(&version_in_range, &self.undo_map) { fragment.deletions.insert(local_timestamp); fragment.visible = false; @@ -955,17 +961,6 @@ impl Buffer { } fn apply_undo(&mut self, undo: UndoOperation) -> Result<()> { - // let mut new_fragments = SumTree::new(); - - // self.undos.insert(undo); - // let edit = &self.edit_ops[&undo.edit_id]; - // let start_fragment_id = self.resolve_fragment_id(edit.start_id, edit.start_offset)?; - // let end_fragment_id = self.resolve_fragment_id(edit.end_id, edit.end_offset)?; - // let mut cursor = self.fragments.cursor::(); - - // for fragment in cursor {} - // self.fragments = new_fragments; - let mut new_fragments; self.undo_map.insert(undo); @@ -984,6 +979,7 @@ impl Buffer { loop { let mut fragment = cursor.item().unwrap().clone(); fragment.visible = fragment.is_visible(&self.undo_map); + fragment.undos.insert(undo.id); new_fragments.push(fragment); cursor.next(); if let Some(split_id) = insertion_splits.next() { @@ -1004,6 +1000,7 @@ impl Buffer { || fragment.insertion.id == undo.edit_id { fragment.visible = fragment.is_visible(&self.undo_map); + fragment.undos.insert(undo.id); } new_fragments.push(fragment); cursor.next(); @@ -1110,6 +1107,7 @@ impl Buffer { while cur_range.is_some() && cursor.item().is_some() { let mut fragment = cursor.item().unwrap().clone(); + let fragment_summary = cursor.item_summary().unwrap(); let mut fragment_start = *cursor.start(); let mut fragment_end = fragment_start + fragment.visible_len(); @@ -1169,6 +1167,7 @@ impl Buffer { prefix.set_end_offset(prefix.start_offset() + (range.end - fragment_start)); prefix.id = FragmentId::between(&new_fragments.last().unwrap().id, &fragment.id); + version_in_range.observe_all(&fragment_summary.max_version); if fragment.visible { prefix.deletions.insert(local_timestamp); prefix.visible = false; @@ -1182,10 +1181,9 @@ impl Buffer { fragment_start = range.end; end_id = Some(fragment.insertion.id); end_offset = Some(fragment.start_offset()); - version_in_range.observe(fragment.insertion.id); } } else { - version_in_range.observe(fragment.insertion.id); + version_in_range.observe_all(&fragment_summary.max_version); if fragment.visible { fragment.deletions.insert(local_timestamp); fragment.visible = false; @@ -1238,15 +1236,16 @@ impl Buffer { cursor.next(); if let Some(range) = cur_range.clone() { while let Some(fragment) = cursor.item() { + let fragment_summary = cursor.item_summary().unwrap(); fragment_start = *cursor.start(); fragment_end = fragment_start + fragment.visible_len(); if range.start < fragment_start && range.end >= fragment_end { let mut new_fragment = fragment.clone(); + version_in_range.observe_all(&fragment_summary.max_version); if new_fragment.visible { new_fragment.deletions.insert(local_timestamp); new_fragment.visible = false; } - version_in_range.observe(new_fragment.insertion.id); new_fragments.push(new_fragment); cursor.next(); @@ -1927,6 +1926,7 @@ impl Fragment { text: insertion.text.clone(), insertion, deletions: HashSet::default(), + undos: HashSet::default(), visible: true, } } @@ -1989,6 +1989,9 @@ impl sum_tree::Item for Fragment { for deletion in &self.deletions { max_version.observe(*deletion); } + for undo in &self.undos { + max_version.observe(*undo); + } if self.visible { FragmentSummary { @@ -2845,9 +2848,9 @@ mod tests { fn test_random_concurrent_edits() { use crate::test::Network; - const PEERS: usize = 2; + const PEERS: usize = 5; - for seed in 0..1000 { + for seed in 0..100 { println!("{:?}", seed); let mut rng = &mut StdRng::seed_from_u64(seed); @@ -2865,7 +2868,7 @@ mod tests { network.add_peer(i as u16); } - let mut mutation_count = 3; + let mut mutation_count = 10; loop { let replica_index = rng.gen_range(0..PEERS); let replica_id = replica_ids[replica_index]; diff --git a/zed/src/sum_tree/cursor.rs b/zed/src/sum_tree/cursor.rs index da33bc1179..440e99ba90 100644 --- a/zed/src/sum_tree/cursor.rs +++ b/zed/src/sum_tree/cursor.rs @@ -77,7 +77,7 @@ where } } - fn item_summary(&self) -> Option<&'a T::Summary> { + pub fn item_summary(&self) -> Option<&'a T::Summary> { assert!(self.did_seek, "Must seek before calling this method"); if let Some(entry) = self.stack.last() { match *entry.tree.0 { From 2e7ca8ea4603151759f866b663598b464a19b9a3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 9 Apr 2021 11:02:46 +0200 Subject: [PATCH 09/12] Store only max undo by replica instead of individual undos in Fragment --- zed/src/editor/buffer/mod.rs | 22 ++++++++++------------ zed/src/time.rs | 20 ++++++++++++++++---- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index a16cf1f87c..3c9b004bbf 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -177,7 +177,7 @@ struct Fragment { insertion: Insertion, text: Text, deletions: HashSet, - undos: HashSet, + max_undos: time::Global, visible: bool, } @@ -272,8 +272,8 @@ impl Buffer { id: FragmentId::min_value().clone(), insertion: base_insertion.clone(), text: base_insertion.text.slice(0..0), - deletions: HashSet::default(), - undos: HashSet::default(), + deletions: Default::default(), + max_undos: Default::default(), visible: true, }); @@ -292,8 +292,8 @@ impl Buffer { id: base_fragment_id, text: base_insertion.text.clone(), insertion: base_insertion, - deletions: HashSet::default(), - undos: HashSet::default(), + deletions: Default::default(), + max_undos: Default::default(), visible: true, }); } @@ -979,7 +979,7 @@ impl Buffer { loop { let mut fragment = cursor.item().unwrap().clone(); fragment.visible = fragment.is_visible(&self.undo_map); - fragment.undos.insert(undo.id); + fragment.max_undos.observe(undo.id); new_fragments.push(fragment); cursor.next(); if let Some(split_id) = insertion_splits.next() { @@ -1000,7 +1000,7 @@ impl Buffer { || fragment.insertion.id == undo.edit_id { fragment.visible = fragment.is_visible(&self.undo_map); - fragment.undos.insert(undo.id); + fragment.max_undos.observe(undo.id); } new_fragments.push(fragment); cursor.next(); @@ -1925,8 +1925,8 @@ impl Fragment { id, text: insertion.text.clone(), insertion, - deletions: HashSet::default(), - undos: HashSet::default(), + deletions: Default::default(), + max_undos: Default::default(), visible: true, } } @@ -1989,9 +1989,7 @@ impl sum_tree::Item for Fragment { for deletion in &self.deletions { max_version.observe(*deletion); } - for undo in &self.undos { - max_version.observe(*undo); - } + max_version.observe_all(&self.max_undos); if self.visible { FragmentSummary { diff --git a/zed/src/time.rs b/zed/src/time.rs index 38f15f2dde..4836ed921c 100644 --- a/zed/src/time.rs +++ b/zed/src/time.rs @@ -4,6 +4,8 @@ use std::mem; use std::ops::{Add, AddAssign}; use std::sync::Arc; +use lazy_static::lazy_static; + pub type ReplicaId = u16; pub type Seq = u64; @@ -13,9 +15,6 @@ pub struct Local { pub value: Seq, } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Global(Arc>); - #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct Lamport { pub value: Seq, @@ -59,9 +58,22 @@ impl<'a> AddAssign<&'a Local> for Local { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Global(Arc>); + +lazy_static! { + static ref DEFAULT_GLOBAL: Global = Global(Arc::new(HashMap::new())); +} + +impl Default for Global { + fn default() -> Self { + DEFAULT_GLOBAL.clone() + } +} + impl Global { pub fn new() -> Self { - Global(Arc::new(HashMap::new())) + Self::default() } pub fn get(&self, replica_id: ReplicaId) -> Seq { From 4d9ff1109ab4b8c49f04d199676c91ecb91028ef Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 9 Apr 2021 11:16:13 +0200 Subject: [PATCH 10/12] Exercise arbitrary undos/redos in single-peer buffer randomized test --- zed/src/editor/buffer/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 3c9b004bbf..77bae2f5fc 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -2280,6 +2280,11 @@ mod tests { } assert_eq!(buffer.text(), reference_string); + if rng.gen_bool(0.25) { + buffer.randomly_undo_redo(rng, None); + reference_string = buffer.text(); + } + { let line_lengths = line_lengths_in_range(&buffer, 0..buffer.len()); @@ -2961,7 +2966,7 @@ mod tests { mut ctx: Option<&mut ModelContext>, ) -> Vec { let mut ops = Vec::new(); - for _ in 0..rng.gen_range(0..5) { + for _ in 0..rng.gen_range(1..5) { if let Some(edit_id) = self.edit_ops.keys().choose(rng).copied() { ops.push(self.undo_or_redo(edit_id, ctx.as_deref_mut()).unwrap()); } From bf3a5d0a0d9800fbce45629e6217cd9cf627502e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 9 Apr 2021 11:25:28 +0200 Subject: [PATCH 11/12] Move UndoMap down --- zed/src/editor/buffer/mod.rs | 68 ++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 77bae2f5fc..951eb6fd10 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -56,6 +56,40 @@ type HashMap = std::collections::HashMap; #[cfg(not(test))] type HashSet = std::collections::HashSet; +pub struct Buffer { + file: Option, + fragments: SumTree, + insertion_splits: HashMap>, + edit_ops: HashMap, + pub version: time::Global, + saved_version: time::Global, + last_edit: time::Local, + undo_map: UndoMap, + selections: HashMap>, + pub selections_last_update: SelectionsVersion, + deferred_ops: OperationQueue, + deferred_replicas: HashSet, + replica_id: ReplicaId, + local_clock: time::Local, + lamport_clock: time::Lamport, +} + +pub struct Snapshot { + fragments: SumTree, +} + +#[derive(Clone)] +pub struct History { + pub base_text: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Selection { + pub start: Anchor, + pub end: Anchor, + pub reversed: bool, +} + #[derive(Clone, Default, Debug)] struct UndoMap(HashMap>); @@ -92,40 +126,6 @@ impl UndoMap { } } -pub struct Buffer { - file: Option, - fragments: SumTree, - insertion_splits: HashMap>, - edit_ops: HashMap, - pub version: time::Global, - saved_version: time::Global, - last_edit: time::Local, - undo_map: UndoMap, - selections: HashMap>, - pub selections_last_update: SelectionsVersion, - deferred_ops: OperationQueue, - deferred_replicas: HashSet, - replica_id: ReplicaId, - local_clock: time::Local, - lamport_clock: time::Lamport, -} - -pub struct Snapshot { - fragments: SumTree, -} - -#[derive(Clone)] -pub struct History { - pub base_text: String, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Selection { - pub start: Anchor, - pub end: Anchor, - pub reversed: bool, -} - #[derive(Clone)] pub struct CharIter<'a> { fragments_cursor: Cursor<'a, Fragment, usize, usize>, From cba8730d5adefc826ff840b62e7ea21cb74dc3b1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 9 Apr 2021 14:06:09 +0200 Subject: [PATCH 12/12] Don't report edits in `Buffer::undo_or_redo` We will report them in public higher-level methods like `Buffer::undo` and `Buffer::redo`. --- zed/src/editor/buffer/mod.rs | 73 ++++++++++++++---------------------- 1 file changed, 28 insertions(+), 45 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 951eb6fd10..827da6be9d 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -931,13 +931,7 @@ impl Buffer { Ok(()) } - fn undo_or_redo( - &mut self, - edit_id: time::Local, - ctx: Option<&mut ModelContext>, - ) -> Result { - let was_dirty = self.is_dirty(); - let old_version = self.version.clone(); + fn undo_or_redo(&mut self, edit_id: time::Local) -> Result { let undo = UndoOperation { id: self.local_clock.tick(), edit_id, @@ -946,14 +940,6 @@ impl Buffer { self.apply_undo(undo)?; self.version.observe(undo.id); - if let Some(ctx) = ctx { - ctx.notify(); - let changes = self.edits_since(old_version).collect::>(); - if !changes.is_empty() { - self.did_edit(changes, was_dirty, ctx); - } - } - Ok(Operation::Undo { undo, lamport_timestamp: self.lamport_clock.tick(), @@ -2281,7 +2267,7 @@ mod tests { assert_eq!(buffer.text(), reference_string); if rng.gen_bool(0.25) { - buffer.randomly_undo_redo(rng, None); + buffer.randomly_undo_redo(rng); reference_string = buffer.text(); } @@ -2817,32 +2803,33 @@ mod tests { #[test] fn test_undo_redo() -> Result<()> { - let mut buffer = Buffer::new(0, ""); + let mut buffer = Buffer::new(0, "1234"); - let edit1 = buffer.edit(vec![0..0], "abx", None)?; - let edit2 = buffer.edit(vec![2..3], "yzef", None)?; - let edit3 = buffer.edit(vec![2..4], "cd", None)?; + let edit1 = buffer.edit(vec![1..1], "abx", None)?; + let edit2 = buffer.edit(vec![3..4], "yzef", None)?; + let edit3 = buffer.edit(vec![3..5], "cd", None)?; + assert_eq!(buffer.text(), "1abcdef234"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap(), None)?; - assert_eq!(buffer.text(), "cdef"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap(), None)?; - assert_eq!(buffer.text(), "abcdef"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap())?; + assert_eq!(buffer.text(), "1cdef234"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap())?; + assert_eq!(buffer.text(), "1abcdef234"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap(), None)?; - assert_eq!(buffer.text(), "abcdx"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap(), None)?; - assert_eq!(buffer.text(), "abx"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap(), None)?; - assert_eq!(buffer.text(), "abyzef"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap(), None)?; - assert_eq!(buffer.text(), "abcdef"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap())?; + assert_eq!(buffer.text(), "1abcdx234"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap())?; + assert_eq!(buffer.text(), "1abx234"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap())?; + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(edit3[0].edit_id().unwrap())?; + assert_eq!(buffer.text(), "1abcdef234"); - buffer.undo_or_redo(edit3[0].edit_id().unwrap(), None)?; - assert_eq!(buffer.text(), "abyzef"); - buffer.undo_or_redo(edit1[0].edit_id().unwrap(), None)?; - assert_eq!(buffer.text(), "yzef"); - buffer.undo_or_redo(edit2[0].edit_id().unwrap(), None)?; - assert_eq!(buffer.text(), ""); + buffer.undo_or_redo(edit3[0].edit_id().unwrap())?; + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(edit1[0].edit_id().unwrap())?; + assert_eq!(buffer.text(), "1yzef234"); + buffer.undo_or_redo(edit2[0].edit_id().unwrap())?; + assert_eq!(buffer.text(), "1234"); Ok(()) } @@ -2884,7 +2871,7 @@ mod tests { mutation_count -= 1; } 51..=70 if mutation_count != 0 => { - let ops = buffer.randomly_undo_redo(&mut rng, None); + let ops = buffer.randomly_undo_redo(&mut rng); network.broadcast(replica_id, ops, &mut rng); mutation_count -= 1; } @@ -2960,15 +2947,11 @@ mod tests { (old_ranges, new_text, operations) } - pub fn randomly_undo_redo( - &mut self, - rng: &mut impl Rng, - mut ctx: Option<&mut ModelContext>, - ) -> Vec { + pub fn randomly_undo_redo(&mut self, rng: &mut impl Rng) -> Vec { let mut ops = Vec::new(); for _ in 0..rng.gen_range(1..5) { if let Some(edit_id) = self.edit_ops.keys().choose(rng).copied() { - ops.push(self.undo_or_redo(edit_id, ctx.as_deref_mut()).unwrap()); + ops.push(self.undo_or_redo(edit_id).unwrap()); } } ops