Merge branch 'master' into menus

Max Brunsfeld created

Change summary

Cargo.lock                           |  30 +
README.md                            |   6 
gpui/Cargo.toml                      |   2 
gpui/examples/text.rs                |  17 
gpui/src/app.rs                      |  51 ++
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             |  73 ++++
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/platform/mac/app.rs         |  19 +
gpui/src/platform/mod.rs             |   1 
gpui/src/platform/test.rs            |   2 
gpui/src/presenter.rs                |  57 +++
gpui/src/scene.rs                    |  22 +
zed/Cargo.toml                       |   5 
zed/src/editor/buffer/mod.rs         | 454 +++++++++++++++++++++++------
zed/src/editor/buffer/text.rs        |   2 
zed/src/editor/buffer_element.rs     |  15 
zed/src/file_finder.rs               |   8 
zed/src/sum_tree/cursor.rs           |   2 
zed/src/time.rs                      |  28 +
zed/src/workspace/pane.rs            |  10 
zed/src/workspace/workspace_view.rs  |  27 +
38 files changed, 1,105 insertions(+), 186 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -924,6 +924,8 @@ dependencies = [
  "rand 0.8.3",
  "replace_with",
  "resvg",
+ "serde",
+ "serde_json",
  "simplelog",
  "smallvec",
  "smol",
@@ -932,6 +934,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"
@@ -965,6 +973,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"
@@ -1668,6 +1686,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"
@@ -1685,9 +1709,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"
@@ -1695,6 +1719,7 @@ version = "1.0.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
 dependencies = [
+ "indexmap",
  "itoa",
  "ryu",
  "serde",
@@ -2245,6 +2270,7 @@ dependencies = [
  "parking_lot",
  "rand 0.8.3",
  "rust-embed",
+ "seahash",
  "serde_json",
  "simplelog",
  "smallvec",

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
 

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"

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!()
+    }
 }

gpui/src/app.rs 🔗

@@ -324,6 +324,7 @@ pub struct MutableAppContext {
     window_invalidations: HashMap<usize, WindowInvalidation>,
     invalidation_callbacks:
         HashMap<usize, Box<dyn FnMut(WindowInvalidation, &mut MutableAppContext)>>,
+    debug_elements_callbacks: HashMap<usize, Box<dyn Fn(&AppContext) -> crate::json::Value>>,
     foreground: Rc<executor::Foreground>,
     future_handlers: Rc<RefCell<HashMap<usize, FutureHandler>>>,
     stream_handlers: Rc<RefCell<HashMap<usize, StreamHandler>>>,
@@ -361,6 +362,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(),
@@ -391,16 +393,29 @@ impl MutableAppContext {
         &self.ctx.background
     }
 
-    pub fn on_window_invalidated<F: 'static + FnMut(WindowInvalidation, &mut MutableAppContext)>(
-        &mut self,
-        window_id: usize,
-        callback: F,
-    ) {
+    pub fn on_window_invalidated<F>(&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<F>(&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<crate::json::Value> {
+        self.debug_elements_callbacks
+            .get(&window_id)
+            .map(|debug_elements| debug_elements(&self.ctx))
+    }
+
     pub fn add_action<S, V, T, F>(&mut self, name: S, mut handler: F)
     where
         S: Into<String>,
@@ -710,11 +725,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()
                 });
             }
         }
@@ -1115,6 +1138,10 @@ impl MutableAppContext {
             }
         }
     }
+
+    pub fn copy(&self, text: &str) {
+        self.platform.copy(text);
+    }
 }
 
 impl ModelAsRef for MutableAppContext {
@@ -1591,6 +1618,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<S>(&mut self, handle: S)
     where
         S: Into<AnyViewHandle>,

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

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",
+            "bounds": bounds.to_json(),
+            "alignment": self.alignment.to_json(),
+            "child": self.child.debug(ctx),
+        })
+    }
 }

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

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

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

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(),
+        })
+    }
 }

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

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",
+            "bounds": bounds.to_json(),
+            "axis": self.axis.to_json(),
+            "children": self.children.iter().map(|child| child.debug(ctx)).collect::<Vec<json::Value>>()
+        })
+    }
 }
 
 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)
+        })
+    }
 }

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",
+            "bounds": bounds.to_json(),
+            "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(),
+        })
+    }
+}
+
+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(),
+        })
+    }
 }

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

gpui/src/elements/new.rs 🔗

@@ -1,16 +1,18 @@
 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;
-use std::any::Any;
+use std::{any::Any, borrow::Cow};
 
 trait AnyElement {
     fn layout(&mut self, constraint: SizeConstraint, ctx: &mut LayoutContext) -> Vector2F;
     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,11 +55,32 @@ 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,
     {
-        ElementBox(Box::new(Lifecycle::Init { element: self }))
+        ElementBox {
+            name: None,
+            element: Box::new(Lifecycle::Init { element: self }),
+        }
+    }
+
+    fn named(self, name: impl Into<Cow<'static, str>>) -> ElementBox
+    where
+        Self: 'static + Sized,
+    {
+        ElementBox {
+            name: Some(name.into()),
+            element: Box::new(Lifecycle::Init { element: self }),
+        }
     }
 }
 
@@ -77,7 +100,10 @@ pub enum Lifecycle<T: Element> {
         paint: T::PaintState,
     },
 }
-pub struct ElementBox(Box<dyn AnyElement>);
+pub struct ElementBox {
+    name: Option<Cow<'static, str>>,
+    element: Box<dyn AnyElement>,
+}
 
 impl<T: Element> AnyElement for Lifecycle<T> {
     fn layout(&mut self, constraint: SizeConstraint, ctx: &mut LayoutContext) -> Vector2F {
@@ -165,30 +191,57 @@ impl<T: Element> AnyElement for Lifecycle<T> {
             | 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 {
     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 {
+        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<String, serde_json::Value> = Default::default();
+                new_map.insert("name".into(), json::Value::String(name.to_string()));
+                new_map.append(map);
+                return json::Value::Object(new_map);
+            }
+        }
+
+        value
     }
 }

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::<Vec<json::Value>>()
+        })
+    }
 }
 
 impl Extend<ElementBox> for Stack {

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

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::<Vec<json::Value>>()
+
+        })
+    }
 }

gpui/src/font_cache.rs 🔗

@@ -36,6 +36,15 @@ impl FontCache {
         }))
     }
 
+    pub fn family_name(&self, family_id: FamilyId) -> Result<String> {
+        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<FamilyId> {
         for name in names {
             let state = self.0.upgradable_read();

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

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

gpui/src/json.rs 🔗

@@ -0,0 +1,15 @@
+pub use serde_json::*;
+
+pub trait ToJson {
+    fn to_json(&self) -> Value;
+}
+
+impl<T: ToJson> ToJson for Option<T> {
+    fn to_json(&self) -> Value {
+        if let Some(value) = self.as_ref() {
+            value.to_json()
+        } else {
+            json!(null)
+        }
+    }
+}

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

gpui/src/platform/mac/app.rs 🔗

@@ -2,12 +2,12 @@ use super::{BoolExt as _, Dispatcher, FontSystem, Window};
 use crate::{executor, platform};
 use anyhow::Result;
 use cocoa::{
-    appkit::{NSApplication, NSOpenPanel, NSModalResponse},
+    appkit::{NSApplication, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString},
     base::nil,
-    foundation::{NSArray, NSString, NSURL},
+    foundation::{NSArray, NSData, NSString, NSURL},
 };
 use objc::{msg_send, sel, sel_impl};
-use std::{path::PathBuf, rc::Rc, sync::Arc};
+use std::{ffi::c_void, path::PathBuf, rc::Rc, sync::Arc};
 
 pub struct App {
     dispatcher: Arc<Dispatcher>,
@@ -84,4 +84,17 @@ impl platform::App for App {
             let _: () = msg_send![app, terminate: nil];
         }
     }
+
+    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);
+        }
+    }
 }

gpui/src/platform/mod.rs 🔗

@@ -44,6 +44,7 @@ pub trait App {
     fn prompt_for_paths(&self, options: PathPromptOptions) -> Option<Vec<PathBuf>>;
     fn fonts(&self) -> Arc<dyn FontSystem>;
     fn quit(&self);
+    fn copy(&self, text: &str);
 }
 
 pub trait Dispatcher: Send + Sync {

gpui/src/platform/test.rs 🔗

@@ -52,6 +52,8 @@ impl super::App for App {
     fn prompt_for_paths(&self, _: super::PathPromptOptions) -> Option<Vec<std::path::PathBuf>> {
         None
     }
+
+    fn copy(&self, _: &str) {}
 }
 
 impl Window {

gpui/src/presenter.rs 🔗

@@ -2,11 +2,13 @@ use crate::{
     app::{AppContext, MutableAppContext, WindowInvalidation},
     elements::Element,
     font_cache::FontCache,
+    json::{self, 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 {
@@ -128,6 +130,18 @@ impl Presenter {
             Vec::new()
         }
     }
+
+    pub fn debug_elements(&self, ctx: &AppContext) -> Option<json::Value> {
+        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 {
@@ -224,6 +238,12 @@ impl<'a> EventContext<'a> {
     }
 }
 
+pub struct DebugContext<'a> {
+    rendered_views: &'a HashMap<usize, ElementBox>,
+    pub font_cache: &'a FontCache,
+    pub app: &'a AppContext,
+}
+
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub enum Axis {
     Horizontal,
@@ -239,6 +259,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 +320,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 +380,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)]

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

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"
@@ -28,11 +28,12 @@ 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"
 
 [dev-dependencies]
-serde_json = "1.0.64"
+serde_json = {version = "1.0.64", features = ["preserve_order"]}
 tempdir = "0.3.7"
 unindent = "0.1.7"

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,13 +33,38 @@ 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<K, V> = std::collections::HashMap<K, V, DeterministicState>;
+
+#[cfg(test)]
+type HashSet<T> = std::collections::HashSet<T, DeterministicState>;
+
+#[cfg(not(test))]
+type HashMap<K, V> = std::collections::HashMap<K, V>;
+
+#[cfg(not(test))]
+type HashSet<T> = std::collections::HashSet<T>;
+
 pub struct Buffer {
     file: Option<FileHandle>,
     fragments: SumTree<Fragment>,
     insertion_splits: HashMap<time::Local, SumTree<InsertionSplit>>,
+    edit_ops: HashMap<time::Local, EditOperation>,
     pub version: time::Global,
     saved_version: time::Global,
     last_edit: time::Local,
+    undo_map: UndoMap,
     selections: HashMap<SelectionSetId, Vec<Selection>>,
     pub selections_last_update: SelectionsVersion,
     deferred_ops: OperationQueue<Operation>,
@@ -64,6 +90,42 @@ pub struct Selection {
     pub reversed: bool,
 }
 
+#[derive(Clone, Default, Debug)]
+struct UndoMap(HashMap<time::Local, Vec<UndoOperation>>);
+
+impl UndoMap {
+    fn insert(&mut self, undo: UndoOperation) {
+        self.0.entry(undo.edit_id).or_default().push(undo);
+    }
+
+    fn is_undone(&self, edit_id: time::Local) -> bool {
+        self.undo_count(edit_id) % 2 == 1
+    }
+
+    fn was_undone(&self, edit_id: time::Local, version: &time::Global) -> bool {
+        let undo_count = self
+            .0
+            .get(&edit_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, edit_id: time::Local) -> u32 {
+        self.0
+            .get(&edit_id)
+            .unwrap_or(&Vec::new())
+            .iter()
+            .map(|undo| undo.count)
+            .max()
+            .unwrap_or(0)
+    }
+}
+
 #[derive(Clone)]
 pub struct CharIter<'a> {
     fragments_cursor: Cursor<'a, Fragment, usize, usize>,
@@ -78,6 +140,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 +177,8 @@ struct Fragment {
     insertion: Insertion,
     text: Text,
     deletions: HashSet<time::Local>,
+    max_undos: time::Global,
+    visible: bool,
 }
 
 #[derive(Eq, PartialEq, Clone, Debug)]
@@ -143,13 +208,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<Text>,
-        local_timestamp: time::Local,
+        edit: EditOperation,
+        lamport_timestamp: time::Lamport,
+    },
+    Undo {
+        undo: UndoOperation,
         lamport_timestamp: time::Lamport,
     },
     UpdateSelections {
@@ -159,6 +222,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<Text>,
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub struct UndoOperation {
+    id: time::Local,
+    edit_id: time::Local,
+    count: u32,
+}
+
 impl Buffer {
     pub fn new<T: Into<String>>(replica_id: ReplicaId, base_text: T) -> Self {
         Self::build(replica_id, None, base_text.into())
@@ -169,7 +250,7 @@ impl Buffer {
     }
 
     fn build(replica_id: ReplicaId, file: Option<FileHandle>, 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 {
@@ -191,7 +272,9 @@ impl Buffer {
             id: FragmentId::min_value().clone(),
             insertion: base_insertion.clone(),
             text: base_insertion.text.slice(0..0),
-            deletions: HashSet::new(),
+            deletions: Default::default(),
+            max_undos: Default::default(),
+            visible: true,
         });
 
         if base_insertion.text.len() > 0 {
@@ -209,7 +292,9 @@ impl Buffer {
                 id: base_fragment_id,
                 text: base_insertion.text.clone(),
                 insertion: base_insertion,
-                deletions: HashSet::new(),
+                deletions: Default::default(),
+                max_undos: Default::default(),
+                visible: true,
             });
         }
 
@@ -217,13 +302,15 @@ impl Buffer {
             file,
             fragments,
             insertion_splits,
+            edit_ops: HashMap::default(),
             version: time::Global::new(),
             saved_version: time::Global::new(),
             last_edit: time::Local::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),
@@ -391,6 +478,7 @@ impl Buffer {
 
         Edits {
             cursor,
+            undos: &self.undo_map,
             since,
             delta: 0,
         }
@@ -432,6 +520,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 +535,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,27 +767,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,
+            } => {
+                if !self.version.observed(undo.id) {
+                    self.apply_undo(undo)?;
+                    self.version.observe(undo.id);
+                    self.lamport_clock.observe(lamport_timestamp);
                 }
             }
             Operation::UpdateSelections {
@@ -785,8 +882,9 @@ 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;
                     }
                     new_fragments.push(fragment);
                 }
@@ -804,9 +902,11 @@ 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;
                 }
                 new_fragments.push(fragment);
             }
@@ -831,6 +931,76 @@ impl Buffer {
         Ok(())
     }
 
+    fn undo_or_redo(&mut self, edit_id: time::Local) -> Result<Operation> {
+        let undo = UndoOperation {
+            id: self.local_clock.tick(),
+            edit_id,
+            count: self.undo_map.undo_count(edit_id) + 1,
+        };
+        self.apply_undo(undo)?;
+        self.version.observe(undo.id);
+
+        Ok(Operation::Undo {
+            undo,
+            lamport_timestamp: self.lamport_clock.tick(),
+        })
+    }
+
+    fn apply_undo(&mut self, undo: UndoOperation) -> Result<()> {
+        let mut new_fragments;
+
+        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::<FragmentIdRef, ()>();
+
+        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();
+
+            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.undo_map);
+                fragment.max_undos.observe(undo.id);
+                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.undo_map);
+                        fragment.max_undos.observe(undo.id);
+                    }
+                    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 +1021,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| {
@@ -927,6 +1093,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();
 
@@ -986,8 +1153,10 @@ 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() {
+                        version_in_range.observe_all(&fragment_summary.max_version);
+                        if fragment.visible {
                             prefix.deletions.insert(local_timestamp);
+                            prefix.visible = false;
                         }
                         fragment.set_start_offset(prefix.end_offset());
                         new_fragments.push(prefix.clone());
@@ -998,12 +1167,12 @@ 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);
-                    if fragment.is_visible() {
+                    version_in_range.observe_all(&fragment_summary.max_version);
+                    if fragment.visible {
                         fragment.deletions.insert(local_timestamp);
+                        fragment.visible = false;
                     }
                 }
 
@@ -1012,13 +1181,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,
                     });
 
@@ -1051,14 +1222,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();
-                        if new_fragment.is_visible() {
+                        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();
 
@@ -1066,13 +1239,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 +1286,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 +1542,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 +1575,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(),
+            undo_map: self.undo_map.clone(),
             selections: self.selections.clone(),
             selections_last_update: self.selections_last_update.clone(),
             deferred_ops: self.deferred_ops.clone(),
@@ -1464,7 +1643,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 +1677,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 +1697,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 +1712,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();
@@ -1732,7 +1911,9 @@ impl Fragment {
             id,
             text: insertion.text.clone(),
             insertion,
-            deletions: HashSet::new(),
+            deletions: Default::default(),
+            max_undos: Default::default(),
+            visible: true,
         }
     }
 
@@ -1753,7 +1934,7 @@ impl Fragment {
     }
 
     fn visible_len(&self) -> usize {
-        if self.is_visible() {
+        if self.visible {
             self.len()
         } else {
             0
@@ -1764,12 +1945,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<Point> {
@@ -1790,8 +1975,9 @@ impl sum_tree::Item for Fragment {
         for deletion in &self.deletions {
             max_version.observe(*deletion);
         }
+        max_version.observe_all(&self.max_undos);
 
-        if self.is_visible() {
+        if self.visible {
             FragmentSummary {
                 text_summary: self.text.summary(),
                 max_fragment_id: self.id.clone(),
@@ -1899,6 +2085,9 @@ impl Operation {
             Operation::Edit {
                 lamport_timestamp, ..
             } => *lamport_timestamp,
+            Operation::Undo {
+                lamport_timestamp, ..
+            } => *lamport_timestamp,
             Operation::UpdateSelections {
                 lamport_timestamp, ..
             } => *lamport_timestamp,
@@ -2077,6 +2266,11 @@ mod tests {
                 }
                 assert_eq!(buffer.text(), reference_string);
 
+                if rng.gen_bool(0.25) {
+                    buffer.randomly_undo_redo(rng);
+                    reference_string = buffer.text();
+                }
+
                 {
                     let line_lengths = line_lengths_in_range(&buffer, 0..buffer.len());
 
@@ -2607,13 +2801,46 @@ mod tests {
         Ok(())
     }
 
+    #[test]
+    fn test_undo_redo() -> Result<()> {
+        let mut buffer = Buffer::new(0, "1234");
+
+        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())?;
+        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())?;
+        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())?;
+        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(())
+    }
+
     #[test]
     fn test_random_concurrent_edits() {
         use crate::test::Network;
 
-        const PEERS: usize = 3;
+        const PEERS: usize = 5;
 
-        for seed in 0..50 {
+        for seed in 0..100 {
             println!("{:?}", seed);
             let mut rng = &mut StdRng::seed_from_u64(seed);
 
@@ -2636,14 +2863,24 @@ mod tests {
                 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);
+                        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() {
@@ -2669,13 +2906,14 @@ mod tests {
         pub fn randomly_mutate<T>(
             &mut self,
             rng: &mut T,
-            ctx: Option<&mut ModelContext<Self>>,
+            mut ctx: Option<&mut ModelContext<Self>>,
         ) -> (Vec<Range<usize>>, String, Vec<Operation>)
         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
@@ -2708,6 +2946,26 @@ mod tests {
 
             (old_ranges, new_text, operations)
         }
+
+        pub fn randomly_undo_redo(&mut self, rng: &mut impl Rng) -> Vec<Operation> {
+            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).unwrap());
+                }
+            }
+            ops
+        }
+    }
+
+    impl Operation {
+        fn edit_id(&self) -> Option<time::Local> {
+            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<usize>) -> BTreeMap<u32, HashSet<u32>> {
@@ -2715,11 +2973,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);
         }

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

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 {

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<Self>) {
@@ -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")
         })
     }
 

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 {

zed/src/time.rs 🔗

@@ -4,19 +4,20 @@ 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;
+
 #[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)]
-pub struct Global(Arc<HashMap<ReplicaId, u64>>);
-
 #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
 pub struct Lamport {
-    pub value: u64,
+    pub value: Seq,
     pub replica_id: ReplicaId,
 }
 
@@ -57,12 +58,25 @@ impl<'a> AddAssign<&'a Local> for Local {
     }
 }
 
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Global(Arc<HashMap<ReplicaId, u64>>);
+
+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) -> u64 {
+    pub fn get(&self, replica_id: ReplicaId) -> Seq {
         *self.0.get(&replica_id).unwrap_or(&0)
     }
 

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

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,21 @@ impl WorkspaceView {
         });
     }
 
+    pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        match to_string_pretty(&ctx.debug_elements()) {
+            Ok(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);
+            }
+        };
+    }
+
     fn workspace_updated(&mut self, _: ModelHandle<Workspace>, ctx: &mut ViewContext<Self>) {
         ctx.notify();
     }
@@ -358,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<Self>) {