Merge branch 'main' into site-v2

Nate created

Change summary

.github/workflows/ci.yml                                           |   2 
Cargo.lock                                                         | 111 
Dockerfile.migrator                                                |   4 
gpui/Cargo.toml                                                    |   4 
gpui/src/app.rs                                                    |  20 
gpui/src/color.rs                                                  |   4 
gpui/src/elements.rs                                               |  39 
gpui/src/elements/container.rs                                     |  23 
gpui/src/elements/image.rs                                         |  90 
gpui/src/elements/list.rs                                          |   4 
gpui/src/elements/mouse_event_handler.rs                           |   3 
gpui/src/elements/svg.rs                                           |  26 
gpui/src/font_cache.rs                                             |  26 
gpui/src/fonts.rs                                                  |  33 
gpui/src/image_data.rs                                             |  43 
gpui/src/lib.rs                                                    |   2 
gpui/src/platform.rs                                               |   1 
gpui/src/platform/mac.rs                                           |   1 
gpui/src/platform/mac/atlas.rs                                     |  84 
gpui/src/platform/mac/image_cache.rs                               |  49 
gpui/src/platform/mac/platform.rs                                  |  20 
gpui/src/platform/mac/renderer.rs                                  | 158 
gpui/src/platform/mac/shaders/shaders.h                            |  57 
gpui/src/platform/mac/shaders/shaders.metal                        | 117 
gpui/src/platform/mac/sprite_cache.rs                              | 101 
gpui/src/platform/test.rs                                          |   4 
gpui/src/presenter.rs                                              |   8 
gpui/src/scene.rs                                                  |  24 
gpui/src/views/select.rs                                           |   4 
server/Cargo.toml                                                  |   5 
server/migrations/20210916123647_add_nonce_to_channel_messages.sql |   4 
server/src/auth.rs                                                 |   6 
server/src/bin/seed.rs                                             |   2 
server/src/db.rs                                                   | 156 
server/src/rpc.rs                                                  | 329 
zed/Cargo.toml                                                     |   8 
zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf                  |   0 
zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf               |   0 
zed/assets/icons/offline-14.svg                                    |   1 
zed/assets/icons/signed-out-12.svg                                 |   1 
zed/assets/themes/_base.toml                                       |  29 
zed/assets/themes/black.toml                                       |   3 
zed/assets/themes/dark.toml                                        |   1 
zed/assets/themes/light.toml                                       |   1 
zed/src/channel.rs                                                 | 217 
zed/src/chat_panel.rs                                              |  43 
zed/src/editor.rs                                                  | 593 
zed/src/editor/buffer.rs                                           |  17 
zed/src/editor/display_map.rs                                      | 161 
zed/src/editor/display_map/wrap_map.rs                             |  87 
zed/src/editor/element.rs                                          | 338 
zed/src/editor/movement.rs                                         |  17 
zed/src/file_finder.rs                                             |  20 
zed/src/http.rs                                                    |  26 
zed/src/lib.rs                                                     |   2 
zed/src/main.rs                                                    |   9 
zed/src/rpc.rs                                                     | 304 
zed/src/test.rs                                                    | 131 
zed/src/theme.rs                                                   |  81 
zed/src/theme/resolution.rs                                        | 476 
zed/src/theme/theme_registry.rs                                    | 546 
zed/src/theme_selector.rs                                          |  20 
zed/src/user.rs                                                    | 136 
zed/src/workspace.rs                                               | 132 
zed/src/workspace/pane.rs                                          |  87 
zed/src/workspace/sidebar.rs                                       |  66 
zed/src/worktree.rs                                                |   7 
zrpc/proto/zed.proto                                               |   7 
zrpc/src/conn.rs                                                   |   4 
zrpc/src/lib.rs                                                    |   2 
zrpc/src/peer.rs                                                   |  40 
zrpc/src/proto.rs                                                  |  19 
72 files changed, 3,323 insertions(+), 1,873 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -33,7 +33,7 @@ jobs:
           clean: false
 
       - name: Run tests
-        run: cargo test --no-fail-fast
+        run: cargo test --workspace --no-fail-fast
 
   bundle:
     name: Bundle app

Cargo.lock 🔗

@@ -344,6 +344,17 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "async-recursion"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "async-rustls"
 version = "0.1.2"
@@ -814,7 +825,7 @@ dependencies = [
  "error-chain",
  "glob 0.2.11",
  "icns",
- "image",
+ "image 0.12.4",
  "libflate",
  "md5",
  "msi",
@@ -825,7 +836,7 @@ dependencies = [
  "target_build_utils",
  "term",
  "toml 0.4.10",
- "uuid",
+ "uuid 0.5.1",
  "walkdir",
 ]
 
@@ -873,7 +884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e7fb075b9b54e939006aa12e1f6cd2d3194041ff4ebe7f2efcbedf18f25b667"
 dependencies = [
  "byteorder",
- "uuid",
+ "uuid 0.5.1",
 ]
 
 [[package]]
@@ -2102,6 +2113,16 @@ dependencies = [
  "lzw",
 ]
 
+[[package]]
+name = "gif"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
 [[package]]
 name = "gimli"
 version = "0.23.0"
@@ -2167,6 +2188,7 @@ dependencies = [
  "font-kit",
  "foreign-types",
  "gpui_macros",
+ "image 0.23.14",
  "lazy_static",
  "log",
  "metal",
@@ -2462,15 +2484,34 @@ checksum = "d95816db758249fe16f23a4e23f1a3a817fe11892dbfd1c5836f625324702158"
 dependencies = [
  "byteorder",
  "enum_primitive",
- "gif",
+ "gif 0.9.2",
  "jpeg-decoder",
  "num-iter",
- "num-rational",
+ "num-rational 0.1.42",
  "num-traits 0.1.43",
  "png 0.6.2",
  "scoped_threadpool",
 ]
 
+[[package]]
+name = "image"
+version = "0.23.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "gif 0.11.2",
+ "jpeg-decoder",
+ "num-iter",
+ "num-rational 0.3.2",
+ "num-traits 0.2.14",
+ "png 0.16.8",
+ "scoped_threadpool",
+ "tiff",
+]
+
 [[package]]
 name = "indexmap"
 version = "1.6.2"
@@ -2766,6 +2807,16 @@ dependencies = [
  "value-bag",
 ]
 
+[[package]]
+name = "log-panics"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae0136257df209261daa18d6c16394757c63e032e27aafd8b07788b051082bef"
+dependencies = [
+ "backtrace",
+ "log",
+]
+
 [[package]]
 name = "loom"
 version = "0.4.0"
@@ -2912,7 +2963,7 @@ dependencies = [
  "byteorder",
  "cfb",
  "encoding",
- "uuid",
+ "uuid 0.5.1",
 ]
 
 [[package]]
@@ -3014,6 +3065,17 @@ dependencies = [
  "num-traits 0.2.14",
 ]
 
+[[package]]
+name = "num-rational"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
+dependencies = [
+ "autocfg 1.0.1",
+ "num-integer",
+ "num-traits 0.2.14",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.1.43"
@@ -4722,6 +4784,7 @@ dependencies = [
  "thiserror",
  "time 0.2.25",
  "url",
+ "uuid 0.8.2",
  "webpki",
  "webpki-roots",
  "whoami",
@@ -5067,18 +5130,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.24"
+version = "1.0.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
+checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.24"
+version = "1.0.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
+checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5129,6 +5192,17 @@ dependencies = [
  "tide",
 ]
 
+[[package]]
+name = "tiff"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
+dependencies = [
+ "jpeg-decoder",
+ "miniz_oxide 0.4.4",
+ "weezl",
+]
+
 [[package]]
 name = "time"
 version = "0.1.44"
@@ -5533,6 +5607,12 @@ dependencies = [
  "sha1 0.2.0",
 ]
 
+[[package]]
+name = "uuid"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
+
 [[package]]
 name = "value-bag"
 version = "1.0.0-alpha.7"
@@ -5694,6 +5774,12 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "weezl"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e"
+
 [[package]]
 name = "wepoll-sys"
 version = "3.0.1"
@@ -5823,6 +5909,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "arrayvec 0.7.1",
+ "async-recursion",
  "async-trait",
  "async-tungstenite",
  "cargo-bundle",
@@ -5836,9 +5923,12 @@ dependencies = [
  "gpui",
  "http-auth-basic",
  "ignore",
+ "image 0.23.14",
+ "indexmap",
  "lazy_static",
  "libc",
  "log",
+ "log-panics",
  "num_cpus",
  "parking_lot",
  "postage",
@@ -5855,6 +5945,7 @@ dependencies = [
  "smol",
  "surf",
  "tempdir",
+ "thiserror",
  "time 0.3.2",
  "tiny_http",
  "toml 0.5.8",

Dockerfile.migrator 🔗

@@ -1,12 +1,12 @@
 # syntax = docker/dockerfile:1.2
 
-FROM rust as builder
+FROM rust:1.55-bullseye as builder
 WORKDIR app
 RUN --mount=type=cache,target=/usr/local/cargo/registry \
     --mount=type=cache,target=./target \
     cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.7
 
-FROM debian:buster-slim as runtime
+FROM debian:bullseye-slim as runtime
 RUN apt-get update; \
     apt-get install -y --no-install-recommends libssl1.1
 WORKDIR app

gpui/Cargo.toml 🔗

@@ -4,6 +4,9 @@ edition = "2018"
 name = "gpui"
 version = "0.1.0"
 
+[features]
+test-support = []
+
 [dependencies]
 arrayvec = "0.7.1"
 async-task = "4.0.3"
@@ -11,6 +14,7 @@ backtrace = "0.3"
 ctor = "0.1"
 etagere = "0.2"
 gpui_macros = { path = "../gpui_macros" }
+image = "0.23"
 lazy_static = "1.4.0"
 log = "0.4"
 num_cpus = "1.13"

gpui/src/app.rs 🔗

@@ -2282,6 +2282,16 @@ impl<'a, T: View> ViewContext<'a, T> {
         let handle = self.handle();
         self.app.spawn(|cx| f(handle, cx))
     }
+
+    pub fn spawn_weak<F, Fut, S>(&self, f: F) -> Task<S>
+    where
+        F: FnOnce(WeakViewHandle<T>, AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = S>,
+        S: 'static,
+    {
+        let handle = self.handle().downgrade();
+        self.app.spawn(|cx| f(handle, cx))
+    }
 }
 
 pub struct RenderContext<'a, T: View> {
@@ -2325,6 +2335,16 @@ impl<V: View> ReadModel for RenderContext<'_, V> {
     }
 }
 
+impl<V: View> UpdateModel for RenderContext<'_, V> {
+    fn update_model<T, F, S>(&mut self, handle: &ModelHandle<T>, update: F) -> S
+    where
+        T: Entity,
+        F: FnOnce(&mut T, &mut ModelContext<T>) -> S,
+    {
+        self.app.update_model(handle, update)
+    }
+}
+
 impl<M> AsRef<AppContext> for ViewContext<'_, M> {
     fn as_ref(&self) -> &AppContext {
         &self.app.cx

gpui/src/color.rs 🔗

@@ -29,6 +29,10 @@ impl Color {
         Self(ColorU::white())
     }
 
+    pub fn red() -> Self {
+        Self(ColorU::from_u32(0xff0000ff))
+    }
+
     pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
         Self(ColorU::new(r, g, b, a))
     }

gpui/src/elements.rs 🔗

@@ -6,6 +6,7 @@ mod empty;
 mod event_handler;
 mod flex;
 mod hook;
+mod image;
 mod label;
 mod line_box;
 mod list;
@@ -16,27 +17,17 @@ mod svg;
 mod text;
 mod uniform_list;
 
+pub use self::{
+    align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
+    hook::*, image::*, label::*, line_box::*, list::*, mouse_event_handler::*, overlay::*,
+    stack::*, svg::*, text::*, uniform_list::*,
+};
 pub use crate::presenter::ChildView;
-pub use align::*;
-pub use canvas::*;
-pub use constrained_box::*;
-pub use container::*;
-pub use empty::*;
-pub use event_handler::*;
-pub use flex::*;
-pub use hook::*;
-pub use label::*;
-pub use line_box::*;
-pub use list::*;
-pub use mouse_event_handler::*;
-pub use overlay::*;
-pub use stack::*;
-pub use svg::*;
-pub use text::*;
-pub use uniform_list::*;
-
 use crate::{
-    geometry::{rect::RectF, vector::Vector2F},
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
     json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
 use core::panic;
@@ -371,3 +362,13 @@ pub trait ParentElement<'a>: Extend<ElementBox> + Sized {
 }
 
 impl<'a, T> ParentElement<'a> for T where T: Extend<ElementBox> {}
+
+fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
+    if max_size.x().is_infinite() && max_size.y().is_infinite() {
+        size
+    } else if max_size.x().is_infinite() || max_size.x() / max_size.y() > size.x() / size.y() {
+        vec2f(size.x() * max_size.y() / size.y(), max_size.y())
+    } else {
+        vec2f(max_size.x(), size.y() * max_size.x() / size.x())
+    }
+}

gpui/src/elements/container.rs 🔗

@@ -13,7 +13,7 @@ use crate::{
     Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
 
-#[derive(Clone, Debug, Default, Deserialize)]
+#[derive(Clone, Copy, Debug, Default, Deserialize)]
 pub struct ContainerStyle {
     #[serde(default)]
     pub margin: Margin,
@@ -42,8 +42,8 @@ impl Container {
         }
     }
 
-    pub fn with_style(mut self, style: &ContainerStyle) -> Self {
-        self.style = style.clone();
+    pub fn with_style(mut self, style: ContainerStyle) -> Self {
+        self.style = style;
         self
     }
 
@@ -242,7 +242,7 @@ impl ToJson for ContainerStyle {
     }
 }
 
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default)]
 pub struct Margin {
     pub top: f32,
     pub left: f32,
@@ -269,7 +269,7 @@ impl ToJson for Margin {
     }
 }
 
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default)]
 pub struct Padding {
     pub top: f32,
     pub left: f32,
@@ -348,6 +348,17 @@ enum Spacing {
     },
 }
 
+impl Padding {
+    pub fn uniform(padding: f32) -> Self {
+        Self {
+            top: padding,
+            left: padding,
+            bottom: padding,
+            right: padding,
+        }
+    }
+}
+
 impl ToJson for Padding {
     fn to_json(&self) -> serde_json::Value {
         let mut value = json!({});
@@ -367,7 +378,7 @@ impl ToJson for Padding {
     }
 }
 
-#[derive(Clone, Debug, Default, Deserialize)]
+#[derive(Clone, Copy, Debug, Default, Deserialize)]
 pub struct Shadow {
     #[serde(default, deserialize_with = "deserialize_vec2f")]
     offset: Vector2F,

gpui/src/elements/image.rs 🔗

@@ -0,0 +1,90 @@
+use super::constrain_size_preserving_aspect_ratio;
+use crate::{
+    geometry::{rect::RectF, vector::Vector2F},
+    json::{json, ToJson},
+    scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext,
+    PaintContext, SizeConstraint,
+};
+use serde::Deserialize;
+use std::sync::Arc;
+
+pub struct Image {
+    data: Arc<ImageData>,
+    style: ImageStyle,
+}
+
+#[derive(Copy, Clone, Default, Deserialize)]
+pub struct ImageStyle {
+    #[serde(default)]
+    border: Border,
+    #[serde(default)]
+    corner_radius: f32,
+}
+
+impl Image {
+    pub fn new(data: Arc<ImageData>) -> Self {
+        Self {
+            data,
+            style: Default::default(),
+        }
+    }
+
+    pub fn with_style(mut self, style: ImageStyle) -> Self {
+        self.style = style;
+        self
+    }
+}
+
+impl Element for Image {
+    type LayoutState = ();
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: SizeConstraint,
+        _: &mut LayoutContext,
+    ) -> (Vector2F, Self::LayoutState) {
+        let size =
+            constrain_size_preserving_aspect_ratio(constraint.max, self.data.size().to_f32());
+        (size, ())
+    }
+
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        _: RectF,
+        _: &mut Self::LayoutState,
+        cx: &mut PaintContext,
+    ) -> Self::PaintState {
+        cx.scene.push_image(scene::Image {
+            bounds,
+            border: self.style.border,
+            corner_radius: self.style.corner_radius,
+            data: self.data.clone(),
+        });
+    }
+
+    fn dispatch_event(
+        &mut self,
+        _: &Event,
+        _: RectF,
+        _: &mut Self::LayoutState,
+        _: &mut Self::PaintState,
+        _: &mut EventContext,
+    ) -> bool {
+        false
+    }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "Image",
+            "bounds": bounds.to_json(),
+        })
+    }
+}

gpui/src/elements/list.rs 🔗

@@ -603,7 +603,7 @@ mod tests {
                 offset_in_item: 0.,
             },
             40.,
-            vec2f(0., 54.),
+            vec2f(0., -54.),
             true,
             &mut presenter.build_event_context(cx),
         );
@@ -654,7 +654,7 @@ mod tests {
         assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
     }
 
-    #[crate::test(self, iterations = 10000, seed = 0)]
+    #[crate::test(self, iterations = 10, seed = 0)]
     fn test_random(cx: &mut crate::MutableAppContext, mut rng: StdRng) {
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))

gpui/src/elements/mouse_event_handler.rs 🔗

@@ -116,7 +116,8 @@ impl Element for MouseEventHandler {
         let hit_bounds = RectF::from_points(
             bounds.origin() - vec2f(self.padding.left, self.padding.top),
             bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
-        );
+        )
+        .round_out();
 
         self.state.update(cx, |state, cx| match event {
             Event::MouseMoved {

gpui/src/elements/svg.rs 🔗

@@ -41,25 +41,15 @@ impl Element for Svg {
     ) -> (Vector2F, Self::LayoutState) {
         match cx.asset_cache.svg(&self.path) {
             Ok(tree) => {
-                let size = if constraint.max.x().is_infinite() && constraint.max.y().is_infinite() {
-                    let rect = from_usvg_rect(tree.svg_node().view_box.rect);
-                    rect.size()
-                } else {
-                    let max_size = constraint.max;
-                    let svg_size = from_usvg_rect(tree.svg_node().view_box.rect).size();
-
-                    if max_size.x().is_infinite()
-                        || max_size.x() / max_size.y() > svg_size.x() / svg_size.y()
-                    {
-                        vec2f(svg_size.x() * max_size.y() / svg_size.y(), max_size.y())
-                    } else {
-                        vec2f(max_size.x(), svg_size.y() * max_size.x() / svg_size.x())
-                    }
-                };
+                let size = constrain_size_preserving_aspect_ratio(
+                    constraint.max,
+                    from_usvg_rect(tree.svg_node().view_box.rect).size(),
+                );
                 (size, Some(tree))
             }
-            Err(error) => {
-                log::error!("{}", error);
+            Err(_error) => {
+                #[cfg(not(any(test, feature = "test-support")))]
+                log::error!("{}", _error);
                 (constraint.min, None)
             }
         }
@@ -111,6 +101,8 @@ impl Element for Svg {
 
 use crate::json::ToJson;
 
+use super::constrain_size_preserving_aspect_ratio;
+
 fn from_usvg_rect(rect: usvg::Rect) -> RectF {
     RectF::new(
         vec2f(rect.x() as f32, rect.y() as f32),

gpui/src/font_cache.rs 🔗

@@ -17,7 +17,7 @@ use std::{
 pub struct FamilyId(usize);
 
 struct Family {
-    name: String,
+    name: Arc<str>,
     font_ids: Vec<FontId>,
 }
 
@@ -49,7 +49,7 @@ impl FontCache {
         }))
     }
 
-    pub fn family_name(&self, family_id: FamilyId) -> Result<String> {
+    pub fn family_name(&self, family_id: FamilyId) -> Result<Arc<str>> {
         self.0
             .read()
             .families
@@ -62,7 +62,7 @@ impl FontCache {
         for name in names {
             let state = self.0.upgradable_read();
 
-            if let Some(ix) = state.families.iter().position(|f| f.name == *name) {
+            if let Some(ix) = state.families.iter().position(|f| f.name.as_ref() == *name) {
                 return Ok(FamilyId(ix));
             }
 
@@ -81,7 +81,7 @@ impl FontCache {
                 }
 
                 state.families.push(Family {
-                    name: String::from(*name),
+                    name: Arc::from(*name),
                     font_ids,
                 });
                 return Ok(family_id);
@@ -141,8 +141,8 @@ impl FontCache {
 
     pub fn bounding_box(&self, font_id: FontId, font_size: f32) -> Vector2F {
         let bounding_box = self.metric(font_id, |m| m.bounding_box);
-        let width = self.scale_metric(bounding_box.width(), font_id, font_size);
-        let height = self.scale_metric(bounding_box.height(), font_id, font_size);
+        let width = bounding_box.width() * self.em_scale(font_id, font_size);
+        let height = bounding_box.height() * self.em_scale(font_id, font_size);
         vec2f(width, height)
     }
 
@@ -154,28 +154,28 @@ impl FontCache {
             glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
             bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
         }
-        self.scale_metric(bounds.width(), font_id, font_size)
+        bounds.width() * self.em_scale(font_id, font_size)
     }
 
     pub fn line_height(&self, font_id: FontId, font_size: f32) -> f32 {
         let height = self.metric(font_id, |m| m.bounding_box.height());
-        self.scale_metric(height, font_id, font_size)
+        (height * self.em_scale(font_id, font_size)).ceil()
     }
 
     pub fn cap_height(&self, font_id: FontId, font_size: f32) -> f32 {
-        self.scale_metric(self.metric(font_id, |m| m.cap_height), font_id, font_size)
+        self.metric(font_id, |m| m.cap_height) * self.em_scale(font_id, font_size)
     }
 
     pub fn ascent(&self, font_id: FontId, font_size: f32) -> f32 {
-        self.scale_metric(self.metric(font_id, |m| m.ascent), font_id, font_size)
+        self.metric(font_id, |m| m.ascent) * self.em_scale(font_id, font_size)
     }
 
     pub fn descent(&self, font_id: FontId, font_size: f32) -> f32 {
-        self.scale_metric(self.metric(font_id, |m| -m.descent), font_id, font_size)
+        self.metric(font_id, |m| -m.descent) * self.em_scale(font_id, font_size)
     }
 
-    pub fn scale_metric(&self, metric: f32, font_id: FontId, font_size: f32) -> f32 {
-        metric * font_size / self.metric(font_id, |m| m.units_per_em as f32)
+    pub fn em_scale(&self, font_id: FontId, font_size: f32) -> f32 {
+        font_size / self.metric(font_id, |m| m.units_per_em as f32)
     }
 
     pub fn line_wrapper(self: &Arc<Self>, font_id: FontId, font_size: f32) -> LineWrapperHandle {

gpui/src/fonts.rs 🔗

@@ -1,5 +1,6 @@
 use crate::{
     color::Color,
+    font_cache::FamilyId,
     json::{json, ToJson},
     text_layout::RunStyle,
     FontCache,
@@ -22,6 +23,7 @@ pub type GlyphId = u32;
 pub struct TextStyle {
     pub color: Color,
     pub font_family_name: Arc<str>,
+    pub font_family_id: FamilyId,
     pub font_id: FontId,
     pub font_size: f32,
     pub font_properties: Properties,
@@ -85,11 +87,12 @@ impl TextStyle {
         font_cache: &FontCache,
     ) -> anyhow::Result<Self> {
         let font_family_name = font_family_name.into();
-        let family_id = font_cache.load_family(&[&font_family_name])?;
-        let font_id = font_cache.select_font(family_id, &font_properties)?;
+        let font_family_id = font_cache.load_family(&[&font_family_name])?;
+        let font_id = font_cache.select_font(font_family_id, &font_properties)?;
         Ok(Self {
             color,
             font_family_name,
+            font_family_id,
             font_id,
             font_size,
             font_properties,
@@ -124,6 +127,32 @@ impl TextStyle {
             }
         })
     }
+
+    pub fn line_height(&self, font_cache: &FontCache) -> f32 {
+        font_cache.line_height(self.font_id, self.font_size)
+    }
+
+    pub fn em_width(&self, font_cache: &FontCache) -> f32 {
+        font_cache.em_width(self.font_id, self.font_size)
+    }
+
+    pub fn descent(&self, font_cache: &FontCache) -> f32 {
+        font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
+    }
+
+    fn em_scale(&self, font_cache: &FontCache) -> f32 {
+        font_cache.em_scale(self.font_id, self.font_size)
+    }
+}
+
+impl From<TextStyle> for HighlightStyle {
+    fn from(other: TextStyle) -> Self {
+        Self {
+            color: other.color,
+            font_properties: other.font_properties,
+            underline: other.underline,
+        }
+    }
 }
 
 impl HighlightStyle {

gpui/src/image_data.rs 🔗

@@ -0,0 +1,43 @@
+use crate::geometry::vector::{vec2i, Vector2I};
+use image::{Bgra, ImageBuffer};
+use std::{
+    fmt,
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
+};
+
+pub struct ImageData {
+    pub id: usize,
+    data: ImageBuffer<Bgra<u8>, Vec<u8>>,
+}
+
+impl ImageData {
+    pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Arc<Self> {
+        static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
+
+        Arc::new(Self {
+            id: NEXT_ID.fetch_add(1, SeqCst),
+            data,
+        })
+    }
+
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.data
+    }
+
+    pub fn size(&self) -> Vector2I {
+        let (width, height) = self.data.dimensions();
+        vec2i(width as i32, height as i32)
+    }
+}
+
+impl fmt::Debug for ImageData {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("ImageData")
+            .field("id", &self.id)
+            .field("size", &self.data.dimensions())
+            .finish()
+    }
+}

gpui/src/lib.rs 🔗

@@ -7,6 +7,8 @@ mod test;
 pub use assets::*;
 pub mod elements;
 pub mod font_cache;
+mod image_data;
+pub use crate::image_data::ImageData;
 pub mod views;
 pub use font_cache::FontCache;
 mod clipboard;

gpui/src/platform.rs 🔗

@@ -48,6 +48,7 @@ pub trait Platform: Send + Sync {
 
     fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
     fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
+    fn delete_credentials(&self, url: &str) -> Result<()>;
 
     fn set_cursor_style(&self, style: CursorStyle);
 

gpui/src/platform/mac.rs 🔗

@@ -3,6 +3,7 @@ mod dispatcher;
 mod event;
 mod fonts;
 mod geometry;
+mod image_cache;
 mod platform;
 mod renderer;
 mod sprite_cache;

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

@@ -1,4 +1,7 @@
-use crate::geometry::vector::{vec2i, Vector2I};
+use crate::geometry::{
+    rect::RectI,
+    vector::{vec2i, Vector2I},
+};
 use etagere::BucketedAtlasAllocator;
 use foreign_types::ForeignType;
 use metal::{self, Device, TextureDescriptor};
@@ -11,6 +14,12 @@ pub struct AtlasAllocator {
     free_atlases: Vec<Atlas>,
 }
 
+#[derive(Copy, Clone)]
+pub struct AllocId {
+    pub atlas_id: usize,
+    alloc_id: etagere::AllocId,
+}
+
 impl AtlasAllocator {
     pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self {
         let mut me = Self {
@@ -31,20 +40,40 @@ impl AtlasAllocator {
         )
     }
 
-    pub fn allocate(&mut self, requested_size: Vector2I) -> anyhow::Result<(usize, Vector2I)> {
-        let origin = self
+    pub fn allocate(&mut self, requested_size: Vector2I) -> (AllocId, Vector2I) {
+        let (alloc_id, origin) = self
             .atlases
             .last_mut()
             .unwrap()
             .allocate(requested_size)
             .unwrap_or_else(|| {
                 let mut atlas = self.new_atlas(requested_size);
-                let origin = atlas.allocate(requested_size).unwrap();
+                let (id, origin) = atlas.allocate(requested_size).unwrap();
                 self.atlases.push(atlas);
-                origin
+                (id, origin)
             });
 
-        Ok((self.atlases.len() - 1, origin))
+        let id = AllocId {
+            atlas_id: self.atlases.len() - 1,
+            alloc_id,
+        };
+        (id, origin)
+    }
+
+    pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> (AllocId, RectI) {
+        let (alloc_id, origin) = self.allocate(size);
+        let bounds = RectI::new(origin, size);
+        self.atlases[alloc_id.atlas_id].upload(bounds, bytes);
+        (alloc_id, bounds)
+    }
+
+    pub fn deallocate(&mut self, id: AllocId) {
+        if let Some(atlas) = self.atlases.get_mut(id.atlas_id) {
+            atlas.deallocate(id.alloc_id);
+            if atlas.is_empty() {
+                self.free_atlases.push(self.atlases.remove(id.atlas_id));
+            }
+        }
     }
 
     pub fn clear(&mut self) {
@@ -102,13 +131,44 @@ impl Atlas {
         vec2i(size.width, size.height)
     }
 
-    fn allocate(&mut self, size: Vector2I) -> Option<Vector2I> {
-        let origin = self
+    fn allocate(&mut self, size: Vector2I) -> Option<(etagere::AllocId, Vector2I)> {
+        let alloc = self
             .allocator
-            .allocate(etagere::Size::new(size.x(), size.y()))?
-            .rectangle
-            .min;
-        Some(vec2i(origin.x, origin.y))
+            .allocate(etagere::Size::new(size.x(), size.y()))?;
+        let origin = alloc.rectangle.min;
+        Some((alloc.id, vec2i(origin.x, origin.y)))
+    }
+
+    fn upload(&mut self, bounds: RectI, bytes: &[u8]) {
+        let region = metal::MTLRegion::new_2d(
+            bounds.origin().x() as u64,
+            bounds.origin().y() as u64,
+            bounds.size().x() as u64,
+            bounds.size().y() as u64,
+        );
+        self.texture.replace_region(
+            region,
+            0,
+            bytes.as_ptr() as *const _,
+            (bounds.size().x() * self.bytes_per_pixel() as i32) as u64,
+        );
+    }
+
+    fn bytes_per_pixel(&self) -> u8 {
+        use metal::MTLPixelFormat::*;
+        match self.texture.pixel_format() {
+            A8Unorm | R8Unorm => 1,
+            RGBA8Unorm | BGRA8Unorm => 4,
+            _ => unimplemented!(),
+        }
+    }
+
+    fn deallocate(&mut self, id: etagere::AllocId) {
+        self.allocator.deallocate(id);
+    }
+
+    fn is_empty(&self) -> bool {
+        self.allocator.is_empty()
     }
 
     fn clear(&mut self) {

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

@@ -0,0 +1,49 @@
+use metal::{MTLPixelFormat, TextureDescriptor, TextureRef};
+
+use super::atlas::{AllocId, AtlasAllocator};
+use crate::{
+    geometry::{rect::RectI, vector::Vector2I},
+    ImageData,
+};
+use std::{collections::HashMap, mem};
+
+pub struct ImageCache {
+    prev_frame: HashMap<usize, (AllocId, RectI)>,
+    curr_frame: HashMap<usize, (AllocId, RectI)>,
+    atlases: AtlasAllocator,
+}
+
+impl ImageCache {
+    pub fn new(device: metal::Device, size: Vector2I) -> Self {
+        let descriptor = TextureDescriptor::new();
+        descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
+        descriptor.set_width(size.x() as u64);
+        descriptor.set_height(size.y() as u64);
+        Self {
+            prev_frame: Default::default(),
+            curr_frame: Default::default(),
+            atlases: AtlasAllocator::new(device, descriptor),
+        }
+    }
+
+    pub fn render(&mut self, image: &ImageData) -> (AllocId, RectI) {
+        let (alloc_id, atlas_bounds) = self
+            .prev_frame
+            .remove(&image.id)
+            .or_else(|| self.curr_frame.get(&image.id).copied())
+            .unwrap_or_else(|| self.atlases.upload(image.size(), image.as_bytes()));
+        self.curr_frame.insert(image.id, (alloc_id, atlas_bounds));
+        (alloc_id, atlas_bounds)
+    }
+
+    pub fn finish_frame(&mut self) {
+        mem::swap(&mut self.prev_frame, &mut self.curr_frame);
+        for (_, (id, _)) in self.curr_frame.drain() {
+            self.atlases.deallocate(id);
+        }
+    }
+
+    pub fn atlas_texture(&self, atlas_id: usize) -> Option<&TextureRef> {
+        self.atlases.texture(atlas_id)
+    }
+}

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

@@ -551,6 +551,25 @@ impl platform::Platform for MacPlatform {
         }
     }
 
+    fn delete_credentials(&self, url: &str) -> Result<()> {
+        let url = CFString::from(url);
+
+        unsafe {
+            use security::*;
+
+            let mut query_attrs = CFMutableDictionary::with_capacity(2);
+            query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+            query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+
+            let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
+
+            if status != errSecSuccess {
+                return Err(anyhow!("delete password failed: {}", status));
+            }
+        }
+        Ok(())
+    }
+
     fn set_cursor_style(&self, style: CursorStyle) {
         unsafe {
             let cursor: id = match style {
@@ -676,6 +695,7 @@ mod security {
 
         pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
         pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus;
+        pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus;
         pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
     }
 

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

@@ -1,4 +1,4 @@
-use super::{atlas::AtlasAllocator, sprite_cache::SpriteCache};
+use super::{atlas::AtlasAllocator, image_cache::ImageCache, sprite_cache::SpriteCache};
 use crate::{
     color::Color,
     geometry::{
@@ -6,8 +6,7 @@ use crate::{
         vector::{vec2f, vec2i, Vector2F},
     },
     platform,
-    scene::{Glyph, Icon, Layer, Quad, Shadow},
-    Scene,
+    scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow},
 };
 use cocoa::foundation::NSUInteger;
 use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
@@ -20,10 +19,12 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio
 
 pub struct Renderer {
     sprite_cache: SpriteCache,
+    image_cache: ImageCache,
     path_atlases: AtlasAllocator,
     quad_pipeline_state: metal::RenderPipelineState,
     shadow_pipeline_state: metal::RenderPipelineState,
     sprite_pipeline_state: metal::RenderPipelineState,
+    image_pipeline_state: metal::RenderPipelineState,
     path_atlas_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
     instances: metal::Buffer,
@@ -64,7 +65,9 @@ impl Renderer {
         );
 
         let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts);
-        let path_atlases = build_path_atlas_allocator(MTLPixelFormat::R8Unorm, &device);
+        let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768));
+        let path_atlases =
+            AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
         let quad_pipeline_state = build_pipeline_state(
             &device,
             &library,
@@ -89,6 +92,14 @@ impl Renderer {
             "sprite_fragment",
             pixel_format,
         );
+        let image_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "image",
+            "image_vertex",
+            "image_fragment",
+            pixel_format,
+        );
         let path_atlas_pipeline_state = build_path_atlas_pipeline_state(
             &device,
             &library,
@@ -99,10 +110,12 @@ impl Renderer {
         );
         Self {
             sprite_cache,
+            image_cache,
             path_atlases,
             quad_pipeline_state,
             shadow_pipeline_state,
             sprite_pipeline_state,
+            image_pipeline_state,
             path_atlas_pipeline_state,
             unit_vertices,
             instances,
@@ -117,6 +130,7 @@ impl Renderer {
         output: &metal::TextureRef,
     ) {
         let mut offset = 0;
+
         let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer);
         self.render_layers(
             scene,
@@ -130,6 +144,7 @@ impl Renderer {
             location: 0,
             length: offset as NSUInteger,
         });
+        self.image_cache.finish_frame();
     }
 
     fn render_path_atlases(
@@ -146,11 +161,11 @@ impl Renderer {
             for path in layer.paths() {
                 let origin = path.bounds.origin() * scene.scale_factor();
                 let size = (path.bounds.size() * scene.scale_factor()).ceil();
-                let (atlas_id, atlas_origin) = self.path_atlases.allocate(size.to_i32()).unwrap();
+                let (alloc_id, atlas_origin) = self.path_atlases.allocate(size.to_i32());
                 let atlas_origin = atlas_origin.to_f32();
                 sprites.push(PathSprite {
                     layer_id,
-                    atlas_id,
+                    atlas_id: alloc_id.atlas_id,
                     shader_data: shaders::GPUISprite {
                         origin: origin.floor().to_float2(),
                         target_size: size.to_float2(),
@@ -162,7 +177,7 @@ impl Renderer {
                 });
 
                 if let Some(current_atlas_id) = current_atlas_id {
-                    if atlas_id != current_atlas_id {
+                    if alloc_id.atlas_id != current_atlas_id {
                         self.render_paths_to_atlas(
                             offset,
                             &vertices,
@@ -173,7 +188,7 @@ impl Renderer {
                     }
                 }
 
-                current_atlas_id = Some(atlas_id);
+                current_atlas_id = Some(alloc_id.atlas_id);
 
                 for vertex in &path.vertices {
                     let xy_position =
@@ -316,6 +331,13 @@ impl Renderer {
                 drawable_size,
                 command_encoder,
             );
+            self.render_images(
+                layer.images(),
+                scale_factor,
+                offset,
+                drawable_size,
+                command_encoder,
+            );
             self.render_quads(
                 layer.underlines(),
                 scale_factor,
@@ -559,11 +581,6 @@ impl Renderer {
             mem::size_of::<shaders::vector_float2>() as u64,
             [drawable_size.to_float2()].as_ptr() as *const c_void,
         );
-        command_encoder.set_vertex_bytes(
-            shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64,
-            mem::size_of::<shaders::vector_float2>() as u64,
-            [self.sprite_cache.atlas_size().to_float2()].as_ptr() as *const c_void,
-        );
 
         for (atlas_id, sprites) in sprites_by_atlas {
             align_offset(offset);
@@ -573,13 +590,19 @@ impl Renderer {
                 "instance buffer exhausted"
             );
 
+            let texture = self.sprite_cache.atlas_texture(atlas_id).unwrap();
             command_encoder.set_vertex_buffer(
                 shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexSprites as u64,
                 Some(&self.instances),
                 *offset as u64,
             );
+            command_encoder.set_vertex_bytes(
+                shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64,
+                mem::size_of::<shaders::vector_float2>() as u64,
+                [vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
+                    as *const c_void,
+            );
 
-            let texture = self.sprite_cache.atlas_texture(atlas_id).unwrap();
             command_encoder.set_fragment_texture(
                 shaders::GPUISpriteFragmentInputIndex_GPUISpriteFragmentInputIndexAtlas as u64,
                 Some(texture),
@@ -602,6 +625,96 @@ impl Renderer {
         }
     }
 
+    fn render_images(
+        &mut self,
+        images: &[Image],
+        scale_factor: f32,
+        offset: &mut usize,
+        drawable_size: Vector2F,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if images.is_empty() {
+            return;
+        }
+
+        let mut images_by_atlas = HashMap::new();
+        for image in images {
+            let origin = image.bounds.origin() * scale_factor;
+            let target_size = image.bounds.size() * scale_factor;
+            let corner_radius = image.corner_radius * scale_factor;
+            let border_width = image.border.width * scale_factor;
+            let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data);
+            images_by_atlas
+                .entry(alloc_id.atlas_id)
+                .or_insert_with(Vec::new)
+                .push(shaders::GPUIImage {
+                    origin: origin.to_float2(),
+                    target_size: target_size.to_float2(),
+                    source_size: atlas_bounds.size().to_float2(),
+                    atlas_origin: atlas_bounds.origin().to_float2(),
+                    border_top: border_width * (image.border.top as usize as f32),
+                    border_right: border_width * (image.border.right as usize as f32),
+                    border_bottom: border_width * (image.border.bottom as usize as f32),
+                    border_left: border_width * (image.border.left as usize as f32),
+                    border_color: image.border.color.to_uchar4(),
+                    corner_radius,
+                });
+        }
+
+        command_encoder.set_render_pipeline_state(&self.image_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexVertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_bytes(
+            shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexViewportSize as u64,
+            mem::size_of::<shaders::vector_float2>() as u64,
+            [drawable_size.to_float2()].as_ptr() as *const c_void,
+        );
+
+        for (atlas_id, images) in images_by_atlas {
+            align_offset(offset);
+            let next_offset = *offset + images.len() * mem::size_of::<shaders::GPUIImage>();
+            assert!(
+                next_offset <= INSTANCE_BUFFER_SIZE,
+                "instance buffer exhausted"
+            );
+
+            let texture = self.image_cache.atlas_texture(atlas_id).unwrap();
+            command_encoder.set_vertex_buffer(
+                shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexImages as u64,
+                Some(&self.instances),
+                *offset as u64,
+            );
+            command_encoder.set_vertex_bytes(
+                shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexAtlasSize as u64,
+                mem::size_of::<shaders::vector_float2>() as u64,
+                [vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
+                    as *const c_void,
+            );
+            command_encoder.set_fragment_texture(
+                shaders::GPUIImageFragmentInputIndex_GPUIImageFragmentInputIndexAtlas as u64,
+                Some(texture),
+            );
+
+            unsafe {
+                let buffer_contents = (self.instances.contents() as *mut u8)
+                    .offset(*offset as isize)
+                    as *mut shaders::GPUIImage;
+                std::ptr::copy_nonoverlapping(images.as_ptr(), buffer_contents, images.len());
+            }
+
+            command_encoder.draw_primitives_instanced(
+                metal::MTLPrimitiveType::Triangle,
+                0,
+                6,
+                images.len() as u64,
+            );
+            *offset = next_offset;
+        }
+    }
+
     fn render_path_sprites(
         &mut self,
         layer_id: usize,
@@ -708,19 +821,15 @@ impl Renderer {
     }
 }
 
-fn build_path_atlas_allocator(
-    pixel_format: MTLPixelFormat,
-    device: &metal::Device,
-) -> AtlasAllocator {
+fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor {
     let texture_descriptor = metal::TextureDescriptor::new();
     texture_descriptor.set_width(2048);
     texture_descriptor.set_height(2048);
-    texture_descriptor.set_pixel_format(pixel_format);
+    texture_descriptor.set_pixel_format(MTLPixelFormat::R8Unorm);
     texture_descriptor
         .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
     texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
-    let path_atlases = AtlasAllocator::new(device.clone(), texture_descriptor);
-    path_atlases
+    texture_descriptor
 }
 
 fn align_offset(offset: &mut usize) {
@@ -803,9 +912,10 @@ mod shaders {
     #![allow(non_camel_case_types)]
     #![allow(non_snake_case)]
 
-    use pathfinder_geometry::vector::Vector2I;
-
-    use crate::{color::Color, geometry::vector::Vector2F};
+    use crate::{
+        color::Color,
+        geometry::vector::{Vector2F, Vector2I},
+    };
     use std::mem;
 
     include!(concat!(env!("OUT_DIR"), "/shaders.rs"));

gpui/src/platform/mac/shaders/shaders.h 🔗

@@ -1,16 +1,19 @@
 #include <simd/simd.h>
 
-typedef struct {
+typedef struct
+{
     vector_float2 viewport_size;
 } GPUIUniforms;
 
-typedef enum {
+typedef enum
+{
     GPUIQuadInputIndexVertices = 0,
     GPUIQuadInputIndexQuads = 1,
     GPUIQuadInputIndexUniforms = 2,
 } GPUIQuadInputIndex;
 
-typedef struct {
+typedef struct
+{
     vector_float2 origin;
     vector_float2 size;
     vector_uchar4 background_color;
@@ -22,13 +25,15 @@ typedef struct {
     float corner_radius;
 } GPUIQuad;
 
-typedef enum {
+typedef enum
+{
     GPUIShadowInputIndexVertices = 0,
     GPUIShadowInputIndexShadows = 1,
     GPUIShadowInputIndexUniforms = 2,
 } GPUIShadowInputIndex;
 
-typedef struct {
+typedef struct
+{
     vector_float2 origin;
     vector_float2 size;
     float corner_radius;
@@ -36,18 +41,21 @@ typedef struct {
     vector_uchar4 color;
 } GPUIShadow;
 
-typedef enum {
+typedef enum
+{
     GPUISpriteVertexInputIndexVertices = 0,
     GPUISpriteVertexInputIndexSprites = 1,
     GPUISpriteVertexInputIndexViewportSize = 2,
     GPUISpriteVertexInputIndexAtlasSize = 3,
 } GPUISpriteVertexInputIndex;
 
-typedef enum {
+typedef enum
+{
     GPUISpriteFragmentInputIndexAtlas = 0,
 } GPUISpriteFragmentInputIndex;
 
-typedef struct {
+typedef struct
+{
     vector_float2 origin;
     vector_float2 target_size;
     vector_float2 source_size;
@@ -56,14 +64,43 @@ typedef struct {
     uint8_t compute_winding;
 } GPUISprite;
 
-typedef enum {
+typedef enum
+{
     GPUIPathAtlasVertexInputIndexVertices = 0,
     GPUIPathAtlasVertexInputIndexAtlasSize = 1,
 } GPUIPathAtlasVertexInputIndex;
 
-typedef struct {
+typedef struct
+{
     vector_float2 xy_position;
     vector_float2 st_position;
     vector_float2 clip_rect_origin;
     vector_float2 clip_rect_size;
 } GPUIPathVertex;
+
+typedef enum
+{
+    GPUIImageVertexInputIndexVertices = 0,
+    GPUIImageVertexInputIndexImages = 1,
+    GPUIImageVertexInputIndexViewportSize = 2,
+    GPUIImageVertexInputIndexAtlasSize = 3,
+} GPUIImageVertexInputIndex;
+
+typedef enum
+{
+    GPUIImageFragmentInputIndexAtlas = 0,
+} GPUIImageFragmentInputIndex;
+
+typedef struct
+{
+    vector_float2 origin;
+    vector_float2 target_size;
+    vector_float2 source_size;
+    vector_float2 atlas_origin;
+    float border_top;
+    float border_right;
+    float border_bottom;
+    float border_left;
+    vector_uchar4 border_color;
+    float corner_radius;
+} GPUIImage;

gpui/src/platform/mac/shaders/shaders.metal 🔗

@@ -34,46 +34,19 @@ float blur_along_x(float x, float y, float sigma, float corner, float2 halfSize)
 
 struct QuadFragmentInput {
     float4 position [[position]];
-    vector_float2 origin;
-    vector_float2 size;
-    vector_uchar4 background_color;
+    float2 atlas_position; // only used in the image shader
+    float2 origin;
+    float2 size;
+    float4 background_color;
     float border_top;
     float border_right;
     float border_bottom;
     float border_left;
-    vector_uchar4 border_color;
+    float4 border_color;
     float corner_radius;
 };
 
-vertex QuadFragmentInput quad_vertex(
-    uint unit_vertex_id [[vertex_id]],
-    uint quad_id [[instance_id]],
-    constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
-    constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
-    constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
-) {
-    float2 unit_vertex = unit_vertices[unit_vertex_id];
-    GPUIQuad quad = quads[quad_id];
-    float2 position = unit_vertex * quad.size + quad.origin;
-    float4 device_position = to_device_position(position, uniforms->viewport_size);
-
-    return QuadFragmentInput {
-        device_position,
-        quad.origin,
-        quad.size,
-        quad.background_color,
-        quad.border_top,
-        quad.border_right,
-        quad.border_bottom,
-        quad.border_left,
-        quad.border_color,
-        quad.corner_radius,
-    };
-}
-
-fragment float4 quad_fragment(
-    QuadFragmentInput input [[stage_in]]
-) {
+float4 quad_sdf(QuadFragmentInput input) {
     float2 half_size = input.size / 2.;
     float2 center = input.origin + half_size;
     float2 center_to_point = input.position.xy - center;
@@ -95,12 +68,13 @@ fragment float4 quad_fragment(
 
     float4 color;
     if (border_width == 0.) {
-        color = coloru_to_colorf(input.background_color);
+        color = input.background_color;
     } else {
+        float4 border_color = float4(mix(float3(input.background_color), float3(input.border_color), input.border_color.a), 1.);
         float inset_distance = distance + border_width;
         color = mix(
-            coloru_to_colorf(input.border_color),
-            coloru_to_colorf(input.background_color),
+            border_color,
+            input.background_color,
             saturate(0.5 - inset_distance)
         );
     }
@@ -109,6 +83,39 @@ fragment float4 quad_fragment(
     return coverage * color;
 }
 
+vertex QuadFragmentInput quad_vertex(
+    uint unit_vertex_id [[vertex_id]],
+    uint quad_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
+    constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
+    constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
+) {
+    float2 unit_vertex = unit_vertices[unit_vertex_id];
+    GPUIQuad quad = quads[quad_id];
+    float2 position = unit_vertex * quad.size + quad.origin;
+    float4 device_position = to_device_position(position, uniforms->viewport_size);
+
+    return QuadFragmentInput {
+        device_position,
+        float2(0., 0.),
+        quad.origin,
+        quad.size,
+        coloru_to_colorf(quad.background_color),
+        quad.border_top,
+        quad.border_right,
+        quad.border_bottom,
+        quad.border_left,
+        coloru_to_colorf(quad.border_color),
+        quad.corner_radius,
+    };
+}
+
+fragment float4 quad_fragment(
+    QuadFragmentInput input [[stage_in]]
+) {
+    return quad_sdf(input);
+}
+
 struct ShadowFragmentInput {
     float4 position [[position]];
     vector_float2 origin;
@@ -217,6 +224,44 @@ fragment float4 sprite_fragment(
     return color;
 }
 
+vertex QuadFragmentInput image_vertex(
+    uint unit_vertex_id [[vertex_id]],
+    uint image_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(GPUIImageVertexInputIndexVertices)]],
+    constant GPUIImage *images [[buffer(GPUIImageVertexInputIndexImages)]],
+    constant float2 *viewport_size [[buffer(GPUIImageVertexInputIndexViewportSize)]],
+    constant float2 *atlas_size [[buffer(GPUIImageVertexInputIndexAtlasSize)]]
+) {
+    float2 unit_vertex = unit_vertices[unit_vertex_id];
+    GPUIImage image = images[image_id];
+    float2 position = unit_vertex * image.target_size + image.origin;
+    float4 device_position = to_device_position(position, *viewport_size);
+    float2 atlas_position = (unit_vertex * image.source_size + image.atlas_origin) / *atlas_size;
+
+    return QuadFragmentInput {
+        device_position,
+        atlas_position,
+        image.origin,
+        image.target_size,
+        float4(0.),
+        image.border_top,
+        image.border_right,
+        image.border_bottom,
+        image.border_left,
+        coloru_to_colorf(image.border_color),
+        image.corner_radius,
+    };
+}
+
+fragment float4 image_fragment(
+    QuadFragmentInput input [[stage_in]],
+    texture2d<float> atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]]
+) {
+    constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
+    input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
+    return quad_sdf(input);
+}
+
 struct PathAtlasVertexOutput {
     float4 position [[position]];
     float2 st_position;

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

@@ -1,12 +1,9 @@
+use super::atlas::AtlasAllocator;
 use crate::{
     fonts::{FontId, GlyphId},
-    geometry::{
-        rect::RectI,
-        vector::{vec2f, vec2i, Vector2F, Vector2I},
-    },
+    geometry::vector::{vec2f, Vector2F, Vector2I},
     platform,
 };
-use etagere::BucketedAtlasAllocator;
 use metal::{MTLPixelFormat, TextureDescriptor};
 use ordered_float::OrderedFloat;
 use std::{borrow::Cow, collections::HashMap, sync::Arc};
@@ -42,10 +39,8 @@ pub struct IconSprite {
 }
 
 pub struct SpriteCache {
-    device: metal::Device,
-    atlas_size: Vector2I,
     fonts: Arc<dyn platform::FontSystem>,
-    atlases: Vec<Atlas>,
+    atlases: AtlasAllocator,
     glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
     icons: HashMap<IconDescriptor, IconSprite>,
 }
@@ -56,21 +51,18 @@ impl SpriteCache {
         size: Vector2I,
         fonts: Arc<dyn platform::FontSystem>,
     ) -> Self {
-        let atlases = vec![Atlas::new(&device, size)];
+        let descriptor = TextureDescriptor::new();
+        descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
+        descriptor.set_width(size.x() as u64);
+        descriptor.set_height(size.y() as u64);
         Self {
-            device,
-            atlas_size: size,
             fonts,
-            atlases,
+            atlases: AtlasAllocator::new(device, descriptor),
             glyphs: Default::default(),
             icons: Default::default(),
         }
     }
 
-    pub fn atlas_size(&self) -> Vector2I {
-        self.atlas_size
-    }
-
     pub fn render_glyph(
         &mut self,
         font_id: FontId,
@@ -84,8 +76,6 @@ impl SpriteCache {
         let target_position = target_position * scale_factor;
         let fonts = &self.fonts;
         let atlases = &mut self.atlases;
-        let atlas_size = self.atlas_size;
-        let device = &self.device;
         let subpixel_variant = (
             (target_position.x().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
                 % SUBPIXEL_VARIANTS,
@@ -111,22 +101,10 @@ impl SpriteCache {
                     subpixel_shift,
                     scale_factor,
                 )?;
-                assert!(glyph_bounds.width() < atlas_size.x());
-                assert!(glyph_bounds.height() < atlas_size.y());
-
-                let atlas_bounds = atlases
-                    .last_mut()
-                    .unwrap()
-                    .try_insert(glyph_bounds.size(), &mask)
-                    .unwrap_or_else(|| {
-                        let mut atlas = Atlas::new(device, atlas_size);
-                        let bounds = atlas.try_insert(glyph_bounds.size(), &mask).unwrap();
-                        atlases.push(atlas);
-                        bounds
-                    });
 
+                let (alloc_id, atlas_bounds) = atlases.upload(glyph_bounds.size(), &mask);
                 Some(GlyphSprite {
-                    atlas_id: atlases.len() - 1,
+                    atlas_id: alloc_id.atlas_id,
                     atlas_origin: atlas_bounds.origin(),
                     offset: glyph_bounds.origin(),
                     size: glyph_bounds.size(),
@@ -142,10 +120,6 @@ impl SpriteCache {
         svg: usvg::Tree,
     ) -> IconSprite {
         let atlases = &mut self.atlases;
-        let atlas_size = self.atlas_size;
-        let device = &self.device;
-        assert!(size.x() < atlas_size.x());
-        assert!(size.y() < atlas_size.y());
         self.icons
             .entry(IconDescriptor {
                 path,
@@ -161,19 +135,9 @@ impl SpriteCache {
                     .map(|a| a.alpha())
                     .collect::<Vec<_>>();
 
-                let atlas_bounds = atlases
-                    .last_mut()
-                    .unwrap()
-                    .try_insert(size, &mask)
-                    .unwrap_or_else(|| {
-                        let mut atlas = Atlas::new(device, atlas_size);
-                        let bounds = atlas.try_insert(size, &mask).unwrap();
-                        atlases.push(atlas);
-                        bounds
-                    });
-
+                let (alloc_id, atlas_bounds) = atlases.upload(size, &mask);
                 IconSprite {
-                    atlas_id: atlases.len() - 1,
+                    atlas_id: alloc_id.atlas_id,
                     atlas_origin: atlas_bounds.origin(),
                     size,
                 }
@@ -182,45 +146,6 @@ impl SpriteCache {
     }
 
     pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
-        self.atlases.get(atlas_id).map(|a| a.texture.as_ref())
-    }
-}
-
-struct Atlas {
-    allocator: BucketedAtlasAllocator,
-    texture: metal::Texture,
-}
-
-impl Atlas {
-    fn new(device: &metal::DeviceRef, size: Vector2I) -> Self {
-        let descriptor = TextureDescriptor::new();
-        descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
-        descriptor.set_width(size.x() as u64);
-        descriptor.set_height(size.y() as u64);
-
-        Self {
-            allocator: BucketedAtlasAllocator::new(etagere::Size::new(size.x(), size.y())),
-            texture: device.new_texture(&descriptor),
-        }
-    }
-
-    fn try_insert(&mut self, size: Vector2I, mask: &[u8]) -> Option<RectI> {
-        let allocation = self
-            .allocator
-            .allocate(etagere::size2(size.x() + 1, size.y() + 1))?;
-
-        let bounds = allocation.rectangle;
-        let region = metal::MTLRegion::new_2d(
-            bounds.min.x as u64,
-            bounds.min.y as u64,
-            size.x() as u64,
-            size.y() as u64,
-        );
-        self.texture
-            .replace_region(region, 0, mask.as_ptr() as *const _, size.x() as u64);
-        Some(RectI::from_points(
-            vec2i(bounds.min.x, bounds.min.y),
-            vec2i(bounds.max.x, bounds.max.y),
-        ))
+        self.atlases.texture(atlas_id)
     }
 }

gpui/src/platform/test.rs 🔗

@@ -137,6 +137,10 @@ impl super::Platform for Platform {
         Ok(None)
     }
 
+    fn delete_credentials(&self, _: &str) -> Result<()> {
+        Ok(())
+    }
+
     fn set_cursor_style(&self, style: CursorStyle) {
         *self.cursor.lock() = style;
     }

gpui/src/presenter.rs 🔗

@@ -286,6 +286,14 @@ impl<'a> PaintContext<'a> {
     }
 }
 
+impl<'a> Deref for PaintContext<'a> {
+    type Target = AppContext;
+
+    fn deref(&self) -> &Self::Target {
+        self.app
+    }
+}
+
 pub struct EventContext<'a> {
     rendered_views: &'a mut HashMap<usize, ElementBox>,
     dispatched_actions: Vec<DispatchDirective>,

gpui/src/scene.rs 🔗

@@ -1,12 +1,13 @@
 use serde::Deserialize;
 use serde_json::json;
-use std::borrow::Cow;
+use std::{borrow::Cow, sync::Arc};
 
 use crate::{
     color::Color,
     fonts::{FontId, GlyphId},
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
+    ImageData,
 };
 
 pub struct Scene {
@@ -25,6 +26,7 @@ pub struct Layer {
     clip_bounds: Option<RectF>,
     quads: Vec<Quad>,
     underlines: Vec<Quad>,
+    images: Vec<Image>,
     shadows: Vec<Shadow>,
     glyphs: Vec<Glyph>,
     icons: Vec<Icon>,
@@ -124,6 +126,13 @@ pub struct PathVertex {
     pub st_position: Vector2F,
 }
 
+pub struct Image {
+    pub bounds: RectF,
+    pub border: Border,
+    pub corner_radius: f32,
+    pub data: Arc<ImageData>,
+}
+
 impl Scene {
     pub fn new(scale_factor: f32) -> Self {
         let stacking_context = StackingContext::new(None);
@@ -166,6 +175,10 @@ impl Scene {
         self.active_layer().push_quad(quad)
     }
 
+    pub fn push_image(&mut self, image: Image) {
+        self.active_layer().push_image(image)
+    }
+
     pub fn push_underline(&mut self, underline: Quad) {
         self.active_layer().push_underline(underline)
     }
@@ -240,6 +253,7 @@ impl Layer {
             clip_bounds,
             quads: Vec::new(),
             underlines: Vec::new(),
+            images: Vec::new(),
             shadows: Vec::new(),
             glyphs: Vec::new(),
             icons: Vec::new(),
@@ -267,6 +281,14 @@ impl Layer {
         self.underlines.as_slice()
     }
 
+    fn push_image(&mut self, image: Image) {
+        self.images.push(image);
+    }
+
+    pub fn images(&self) -> &[Image] {
+        self.images.as_slice()
+    }
+
     fn push_shadow(&mut self, shadow: Shadow) {
         self.shadows.push(shadow);
     }

gpui/src/views/select.rs 🔗

@@ -111,7 +111,7 @@ impl View for Select {
                     mouse_state.hovered,
                     cx,
                 ))
-                .with_style(&style.header)
+                .with_style(style.header)
                 .boxed()
             })
             .on_click(move |cx| cx.dispatch_action(ToggleSelect))
@@ -158,7 +158,7 @@ impl View for Select {
                         .with_max_height(200.)
                         .boxed(),
                     )
-                    .with_style(&style.menu)
+                    .with_style(style.menu)
                     .boxed(),
                 )
                 .boxed(),

server/Cargo.toml 🔗

@@ -5,6 +5,9 @@ edition = "2018"
 name = "zed-server"
 version = "0.1.0"
 
+[[bin]]
+name = "zed-server"
+
 [[bin]]
 name = "seed"
 required-features = ["seed-support"]
@@ -47,7 +50,7 @@ default-features = false
 
 [dependencies.sqlx]
 version = "0.5.2"
-features = ["runtime-async-std-rustls", "postgres", "time"]
+features = ["runtime-async-std-rustls", "postgres", "time", "uuid"]
 
 [dev-dependencies]
 gpui = { path = "../gpui" }

server/src/auth.rs 🔗

@@ -17,7 +17,7 @@ use scrypt::{
 };
 use serde::{Deserialize, Serialize};
 use std::{borrow::Cow, convert::TryFrom, sync::Arc};
-use surf::Url;
+use surf::{StatusCode, Url};
 use tide::Server;
 use zrpc::auth as zed_auth;
 
@@ -73,7 +73,9 @@ impl tide::Middleware<Arc<AppState>> for VerifyToken {
             request.set_ext(user_id);
             Ok(next.run(request).await)
         } else {
-            Err(anyhow!("invalid credentials").into())
+            let mut response = tide::Response::new(StatusCode::Unauthorized);
+            response.set_body("invalid credentials");
+            Ok(response)
         }
     }
 }

server/src/bin/seed.rs 🔗

@@ -73,7 +73,7 @@ async fn main() {
         for timestamp in timestamps {
             let sender_id = *zed_user_ids.choose(&mut rng).unwrap();
             let body = lipsum::lipsum_words(rng.gen_range(1..=50));
-            db.create_channel_message(channel_id, sender_id, &body, timestamp)
+            db.create_channel_message(channel_id, sender_id, &body, timestamp, rng.gen())
                 .await
                 .expect("failed to insert message");
         }

server/src/db.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::Context;
 use async_std::task::{block_on, yield_now};
 use serde::Serialize;
-use sqlx::{FromRow, Result};
+use sqlx::{types::Uuid, FromRow, Result};
 use time::OffsetDateTime;
 
 pub use async_sqlx_session::PostgresSessionStore as SessionStore;
@@ -128,10 +128,23 @@ impl Db {
         requester_id: UserId,
         ids: impl Iterator<Item = UserId>,
     ) -> Result<Vec<User>> {
+        let mut include_requester = false;
+        let ids = ids
+            .map(|id| {
+                if id == requester_id {
+                    include_requester = true;
+                }
+                id.0
+            })
+            .collect::<Vec<_>>();
+
         test_support!(self, {
             // Only return users that are in a common channel with the requesting user.
+            // Also allow the requesting user to return their own data, even if they aren't
+            // in any channels.
             let query = "
-                SELECT users.*
+                SELECT
+                    users.*
                 FROM
                     users, channel_memberships
                 WHERE
@@ -142,11 +155,19 @@ impl Db {
                         FROM channel_memberships
                         WHERE channel_memberships.user_id = $2
                     )
+                UNION
+                SELECT
+                    users.*
+                FROM
+                    users
+                WHERE
+                    $3 AND users.id = $2
             ";
 
             sqlx::query_as(query)
-                .bind(&ids.map(|id| id.0).collect::<Vec<_>>())
+                .bind(&ids)
                 .bind(requester_id)
+                .bind(include_requester)
                 .fetch_all(&self.pool)
                 .await
         })
@@ -381,11 +402,13 @@ impl Db {
         sender_id: UserId,
         body: &str,
         timestamp: OffsetDateTime,
+        nonce: u128,
     ) -> Result<MessageId> {
         test_support!(self, {
             let query = "
-                INSERT INTO channel_messages (channel_id, sender_id, body, sent_at)
-                VALUES ($1, $2, $3, $4)
+                INSERT INTO channel_messages (channel_id, sender_id, body, sent_at, nonce)
+                VALUES ($1, $2, $3, $4, $5)
+                ON CONFLICT (nonce) DO UPDATE SET nonce = excluded.nonce
                 RETURNING id
             ";
             sqlx::query_scalar(query)
@@ -393,6 +416,7 @@ impl Db {
                 .bind(sender_id.0)
                 .bind(body)
                 .bind(timestamp)
+                .bind(Uuid::from_u128(nonce))
                 .fetch_one(&self.pool)
                 .await
                 .map(MessageId)
@@ -409,7 +433,7 @@ impl Db {
             let query = r#"
                 SELECT * FROM (
                     SELECT
-                        id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at
+                        id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at, nonce
                     FROM
                         channel_messages
                     WHERE
@@ -455,7 +479,7 @@ macro_rules! id_type {
 }
 
 id_type!(UserId);
-#[derive(Debug, FromRow, Serialize)]
+#[derive(Debug, FromRow, Serialize, PartialEq)]
 pub struct User {
     pub id: UserId,
     pub github_login: String,
@@ -493,6 +517,7 @@ pub struct ChannelMessage {
     pub sender_id: UserId,
     pub body: String,
     pub sent_at: OffsetDateTime,
+    pub nonce: Uuid,
 }
 
 #[cfg(test)]
@@ -563,6 +588,91 @@ pub mod tests {
         }
     }
 
+    #[gpui::test]
+    async fn test_get_users_by_ids() {
+        let test_db = TestDb::new();
+        let db = test_db.db();
+
+        let user = db.create_user("user", false).await.unwrap();
+        let friend1 = db.create_user("friend-1", false).await.unwrap();
+        let friend2 = db.create_user("friend-2", false).await.unwrap();
+        let friend3 = db.create_user("friend-3", false).await.unwrap();
+        let stranger = db.create_user("stranger", false).await.unwrap();
+
+        // A user can read their own info, even if they aren't in any channels.
+        assert_eq!(
+            db.get_users_by_ids(
+                user,
+                [user, friend1, friend2, friend3, stranger].iter().copied()
+            )
+            .await
+            .unwrap(),
+            vec![User {
+                id: user,
+                github_login: "user".to_string(),
+                admin: false,
+            },],
+        );
+
+        // A user can read the info of any other user who is in a shared channel
+        // with them.
+        let org = db.create_org("test org", "test-org").await.unwrap();
+        let chan1 = db.create_org_channel(org, "channel-1").await.unwrap();
+        let chan2 = db.create_org_channel(org, "channel-2").await.unwrap();
+        let chan3 = db.create_org_channel(org, "channel-3").await.unwrap();
+
+        db.add_channel_member(chan1, user, false).await.unwrap();
+        db.add_channel_member(chan2, user, false).await.unwrap();
+        db.add_channel_member(chan1, friend1, false).await.unwrap();
+        db.add_channel_member(chan1, friend2, false).await.unwrap();
+        db.add_channel_member(chan2, friend2, false).await.unwrap();
+        db.add_channel_member(chan2, friend3, false).await.unwrap();
+        db.add_channel_member(chan3, stranger, false).await.unwrap();
+
+        assert_eq!(
+            db.get_users_by_ids(
+                user,
+                [user, friend1, friend2, friend3, stranger].iter().copied()
+            )
+            .await
+            .unwrap(),
+            vec![
+                User {
+                    id: user,
+                    github_login: "user".to_string(),
+                    admin: false,
+                },
+                User {
+                    id: friend1,
+                    github_login: "friend-1".to_string(),
+                    admin: false,
+                },
+                User {
+                    id: friend2,
+                    github_login: "friend-2".to_string(),
+                    admin: false,
+                },
+                User {
+                    id: friend3,
+                    github_login: "friend-3".to_string(),
+                    admin: false,
+                }
+            ]
+        );
+
+        // The user's own info is only returned if they request it.
+        assert_eq!(
+            db.get_users_by_ids(user, [friend1].iter().copied())
+                .await
+                .unwrap(),
+            vec![User {
+                id: friend1,
+                github_login: "friend-1".to_string(),
+                admin: false,
+            },]
+        )
+    }
+
     #[gpui::test]
     async fn test_recent_channel_messages() {
         let test_db = TestDb::new();
@@ -571,7 +681,7 @@ pub mod tests {
         let org = db.create_org("org", "org").await.unwrap();
         let channel = db.create_org_channel(org, "channel").await.unwrap();
         for i in 0..10 {
-            db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc())
+            db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
                 .await
                 .unwrap();
         }
@@ -591,4 +701,34 @@ pub mod tests {
             ["1", "2", "3", "4"]
         );
     }
+
+    #[gpui::test]
+    async fn test_channel_message_nonces() {
+        let test_db = TestDb::new();
+        let db = test_db.db();
+        let user = db.create_user("user", false).await.unwrap();
+        let org = db.create_org("org", "org").await.unwrap();
+        let channel = db.create_org_channel(org, "channel").await.unwrap();
+
+        let msg1_id = db
+            .create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
+            .await
+            .unwrap();
+        let msg2_id = db
+            .create_channel_message(channel, user, "2", OffsetDateTime::now_utc(), 2)
+            .await
+            .unwrap();
+        let msg3_id = db
+            .create_channel_message(channel, user, "3", OffsetDateTime::now_utc(), 1)
+            .await
+            .unwrap();
+        let msg4_id = db
+            .create_channel_message(channel, user, "4", OffsetDateTime::now_utc(), 2)
+            .await
+            .unwrap();
+
+        assert_ne!(msg1_id, msg2_id);
+        assert_eq!(msg1_id, msg3_id);
+        assert_eq!(msg2_id, msg4_id);
+    }
 }

server/src/rpc.rs 🔗

@@ -27,7 +27,7 @@ use time::OffsetDateTime;
 use zrpc::{
     auth::random_token,
     proto::{self, AnyTypedEnvelope, EnvelopedMessage},
-    Conn, ConnectionId, Peer, TypedEnvelope,
+    Connection, ConnectionId, Peer, TypedEnvelope,
 };
 
 type ReplicaId = u16;
@@ -48,13 +48,13 @@ pub struct Server {
 
 #[derive(Default)]
 struct ServerState {
-    connections: HashMap<ConnectionId, Connection>,
+    connections: HashMap<ConnectionId, ConnectionState>,
     pub worktrees: HashMap<u64, Worktree>,
     channels: HashMap<ChannelId, Channel>,
     next_worktree_id: u64,
 }
 
-struct Connection {
+struct ConnectionState {
     user_id: UserId,
     worktrees: HashSet<u64>,
     channels: HashSet<ChannelId>,
@@ -133,7 +133,7 @@ impl Server {
 
     pub fn handle_connection(
         self: &Arc<Self>,
-        connection: Conn,
+        connection: Connection,
         addr: String,
         user_id: UserId,
     ) -> impl Future<Output = ()> {
@@ -211,7 +211,7 @@ impl Server {
     async fn add_connection(&self, connection_id: ConnectionId, user_id: UserId) {
         self.state.write().await.connections.insert(
             connection_id,
-            Connection {
+            ConnectionState {
                 user_id,
                 worktrees: Default::default(),
                 channels: Default::default(),
@@ -558,8 +558,8 @@ impl Server {
             .into_iter()
             .map(|user| proto::User {
                 id: user.id.to_proto(),
+                avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),
                 github_login: user.github_login,
-                avatar_url: String::new(),
             })
             .collect();
         self.peer
@@ -602,6 +602,7 @@ impl Server {
                 body: msg.body,
                 timestamp: msg.sent_at.unix_timestamp() as u64,
                 sender_id: msg.sender_id.to_proto(),
+                nonce: Some(msg.nonce.as_u128().into()),
             })
             .collect::<Vec<_>>();
         self.peer
@@ -687,10 +688,24 @@ impl Server {
         }
 
         let timestamp = OffsetDateTime::now_utc();
+        let nonce = if let Some(nonce) = request.payload.nonce {
+            nonce
+        } else {
+            self.peer
+                .respond_with_error(
+                    receipt,
+                    proto::Error {
+                        message: "nonce can't be blank".to_string(),
+                    },
+                )
+                .await?;
+            return Ok(());
+        };
+
         let message_id = self
             .app_state
             .db
-            .create_channel_message(channel_id, user_id, &body, timestamp)
+            .create_channel_message(channel_id, user_id, &body, timestamp, nonce.clone().into())
             .await?
             .to_proto();
         let message = proto::ChannelMessage {
@@ -698,6 +713,7 @@ impl Server {
             id: message_id,
             body,
             timestamp: timestamp.unix_timestamp() as u64,
+            nonce: Some(nonce),
         };
         broadcast(request.sender_id, connection_ids, |conn_id| {
             self.peer.send(
@@ -754,6 +770,7 @@ impl Server {
                 body: msg.body,
                 timestamp: msg.sent_at.unix_timestamp() as u64,
                 sender_id: msg.sender_id.to_proto(),
+                nonce: Some(msg.nonce.as_u128().into()),
             })
             .collect::<Vec<_>>();
         self.peer
@@ -972,7 +989,7 @@ pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
             let user_id = user_id.ok_or_else(|| anyhow!("user_id is not present on request. ensure auth::VerifyToken middleware is present"))?;
             task::spawn(async move {
                 if let Some(stream) = upgrade_receiver.await {
-                    server.handle_connection(Conn::new(WebSocketStream::from_raw_socket(stream, Role::Server, None).await), addr, user_id).await;
+                    server.handle_connection(Connection::new(WebSocketStream::from_raw_socket(stream, Role::Server, None).await), addr, user_id).await;
                 }
             });
 
@@ -1020,11 +1037,12 @@ mod tests {
     };
     use zed::{
         channel::{Channel, ChannelDetails, ChannelList},
-        editor::{Editor, Insert},
+        editor::{Editor, EditorStyle, Insert},
         fs::{FakeFs, Fs as _},
         language::LanguageRegistry,
-        rpc::{self, Client},
+        rpc::{self, Client, Credentials, EstablishConnectionError},
         settings,
+        test::FakeHttpClient,
         user::UserStore,
         worktree::Worktree,
     };
@@ -1038,8 +1056,8 @@ mod tests {
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start().await;
-        let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
-        let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
+        let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
+        let (client_b, _) = server.create_client(&mut cx_b, "user_b").await;
 
         cx_a.foreground().forbid_parking();
 
@@ -1103,7 +1121,14 @@ mod tests {
             .unwrap();
 
         // Create a selection set as client B and see that selection set as client A.
-        let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, settings, cx));
+        let editor_b = cx_b.add_view(window_b, |cx| {
+            Editor::for_buffer(
+                buffer_b,
+                settings,
+                |cx| EditorStyle::test(cx.font_cache()),
+                cx,
+            )
+        });
         buffer_a
             .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
             .await;
@@ -1123,7 +1148,7 @@ mod tests {
             .await;
 
         // Close the buffer as client A, see that the buffer is closed.
-        drop(buffer_a);
+        cx_a.update(move |_| drop(buffer_a));
         worktree_a
             .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx))
             .await;
@@ -1146,9 +1171,9 @@ mod tests {
 
         // Connect to a server as 3 clients.
         let mut server = TestServer::start().await;
-        let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
-        let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
-        let (_, client_c) = server.create_client(&mut cx_c, "user_c").await;
+        let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
+        let (client_b, _) = server.create_client(&mut cx_b, "user_b").await;
+        let (client_c, _) = server.create_client(&mut cx_c, "user_c").await;
 
         let fs = Arc::new(FakeFs::new());
 
@@ -1287,8 +1312,8 @@ mod tests {
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start().await;
-        let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
-        let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
+        let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
+        let (client_b, _) = server.create_client(&mut cx_b, "user_b").await;
 
         // Share a local worktree as client A
         let fs = Arc::new(FakeFs::new());
@@ -1368,8 +1393,8 @@ mod tests {
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start().await;
-        let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
-        let (_, client_b) = server.create_client(&mut cx_b, "user_b").await;
+        let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
+        let (client_b, _) = server.create_client(&mut cx_b, "user_b").await;
 
         // Share a local worktree as client A
         let fs = Arc::new(FakeFs::new());
@@ -1428,8 +1453,8 @@ mod tests {
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start().await;
-        let (_, client_a) = server.create_client(&mut cx_a, "user_a").await;
-        let (_, client_b) = server.create_client(&mut cx_a, "user_b").await;
+        let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
+        let (client_b, _) = server.create_client(&mut cx_a, "user_b").await;
 
         // Share a local worktree as client A
         let fs = Arc::new(FakeFs::new());
@@ -1486,33 +1511,37 @@ mod tests {
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start().await;
-        let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await;
-        let (user_id_b, client_b) = server.create_client(&mut cx_b, "user_b").await;
+        let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await;
+        let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await;
 
         // Create an org that includes these 2 users.
         let db = &server.app_state.db;
         let org_id = db.create_org("Test Org", "test-org").await.unwrap();
-        db.add_org_member(org_id, user_id_a, false).await.unwrap();
-        db.add_org_member(org_id, user_id_b, false).await.unwrap();
+        db.add_org_member(org_id, current_user_id(&user_store_a), false)
+            .await
+            .unwrap();
+        db.add_org_member(org_id, current_user_id(&user_store_b), false)
+            .await
+            .unwrap();
 
         // Create a channel that includes all the users.
         let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
-        db.add_channel_member(channel_id, user_id_a, false)
+        db.add_channel_member(channel_id, current_user_id(&user_store_a), false)
             .await
             .unwrap();
-        db.add_channel_member(channel_id, user_id_b, false)
+        db.add_channel_member(channel_id, current_user_id(&user_store_b), false)
             .await
             .unwrap();
         db.create_channel_message(
             channel_id,
-            user_id_b,
+            current_user_id(&user_store_b),
             "hello A, it's B.",
             OffsetDateTime::now_utc(),
+            1,
         )
         .await
         .unwrap();
 
-        let user_store_a = Arc::new(UserStore::new(client_a.clone()));
         let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx));
         channels_a
             .condition(&mut cx_a, |list, _| list.available_channels().is_some())
@@ -1533,11 +1562,10 @@ mod tests {
         channel_a
             .condition(&cx_a, |channel, _| {
                 channel_messages(channel)
-                    == [("user_b".to_string(), "hello A, it's B.".to_string())]
+                    == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
             })
             .await;
 
-        let user_store_b = Arc::new(UserStore::new(client_b.clone()));
         let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b, client_b, cx));
         channels_b
             .condition(&mut cx_b, |list, _| list.available_channels().is_some())
@@ -1559,7 +1587,7 @@ mod tests {
         channel_b
             .condition(&cx_b, |channel, _| {
                 channel_messages(channel)
-                    == [("user_b".to_string(), "hello A, it's B.".to_string())]
+                    == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
             })
             .await;
 
@@ -1571,28 +1599,25 @@ mod tests {
                     .detach();
                 let task = channel.send_message("sup".to_string(), cx).unwrap();
                 assert_eq!(
-                    channel
-                        .pending_messages()
-                        .iter()
-                        .map(|m| &m.body)
-                        .collect::<Vec<_>>(),
-                    &["oh, hi B.", "sup"]
+                    channel_messages(channel),
+                    &[
+                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                        ("user_a".to_string(), "oh, hi B.".to_string(), true),
+                        ("user_a".to_string(), "sup".to_string(), true)
+                    ]
                 );
                 task
             })
             .await
             .unwrap();
 
-        channel_a
-            .condition(&cx_a, |channel, _| channel.pending_messages().is_empty())
-            .await;
         channel_b
             .condition(&cx_b, |channel, _| {
                 channel_messages(channel)
                     == [
-                        ("user_b".to_string(), "hello A, it's B.".to_string()),
-                        ("user_a".to_string(), "oh, hi B.".to_string()),
-                        ("user_a".to_string(), "sup".to_string()),
+                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                        ("user_a".to_string(), "oh, hi B.".to_string(), false),
+                        ("user_a".to_string(), "sup".to_string(), false),
                     ]
             })
             .await;
@@ -1612,14 +1637,6 @@ mod tests {
         server
             .condition(|state| !state.channels.contains_key(&channel_id))
             .await;
-
-        fn channel_messages(channel: &Channel) -> Vec<(String, String)> {
-            channel
-                .messages()
-                .cursor::<(), ()>()
-                .map(|m| (m.sender.github_login.clone(), m.body.clone()))
-                .collect()
-        }
     }
 
     #[gpui::test]
@@ -1627,17 +1644,18 @@ mod tests {
         cx_a.foreground().forbid_parking();
 
         let mut server = TestServer::start().await;
-        let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await;
+        let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await;
 
         let db = &server.app_state.db;
         let org_id = db.create_org("Test Org", "test-org").await.unwrap();
         let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
-        db.add_org_member(org_id, user_id_a, false).await.unwrap();
-        db.add_channel_member(channel_id, user_id_a, false)
+        db.add_org_member(org_id, current_user_id(&user_store_a), false)
+            .await
+            .unwrap();
+        db.add_channel_member(channel_id, current_user_id(&user_store_a), false)
             .await
             .unwrap();
 
-        let user_store_a = Arc::new(UserStore::new(client_a.clone()));
         let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx));
         channels_a
             .condition(&mut cx_a, |list, _| list.available_channels().is_some())
@@ -1683,37 +1701,44 @@ mod tests {
     #[gpui::test]
     async fn test_chat_reconnection(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         cx_a.foreground().forbid_parking();
+        let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) });
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start().await;
-        let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await;
-        let (user_id_b, client_b) = server.create_client(&mut cx_b, "user_b").await;
+        let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await;
+        let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await;
         let mut status_b = client_b.status();
 
         // Create an org that includes these 2 users.
         let db = &server.app_state.db;
         let org_id = db.create_org("Test Org", "test-org").await.unwrap();
-        db.add_org_member(org_id, user_id_a, false).await.unwrap();
-        db.add_org_member(org_id, user_id_b, false).await.unwrap();
+        db.add_org_member(org_id, current_user_id(&user_store_a), false)
+            .await
+            .unwrap();
+        db.add_org_member(org_id, current_user_id(&user_store_b), false)
+            .await
+            .unwrap();
 
         // Create a channel that includes all the users.
         let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
-        db.add_channel_member(channel_id, user_id_a, false)
+        db.add_channel_member(channel_id, current_user_id(&user_store_a), false)
             .await
             .unwrap();
-        db.add_channel_member(channel_id, user_id_b, false)
+        db.add_channel_member(channel_id, current_user_id(&user_store_b), false)
             .await
             .unwrap();
         db.create_channel_message(
             channel_id,
-            user_id_b,
+            current_user_id(&user_store_b),
             "hello A, it's B.",
             OffsetDateTime::now_utc(),
+            2,
         )
         .await
         .unwrap();
 
-        let user_store_a = Arc::new(UserStore::new(client_a.clone()));
+        let user_store_a =
+            UserStore::new(client_a.clone(), http.clone(), cx_a.background().as_ref());
         let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx));
         channels_a
             .condition(&mut cx_a, |list, _| list.available_channels().is_some())
@@ -1735,12 +1760,11 @@ mod tests {
         channel_a
             .condition(&cx_a, |channel, _| {
                 channel_messages(channel)
-                    == [("user_b".to_string(), "hello A, it's B.".to_string())]
+                    == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
             })
             .await;
 
-        let user_store_b = Arc::new(UserStore::new(client_b.clone()));
-        let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b, client_b, cx));
+        let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b.clone(), client_b, cx));
         channels_b
             .condition(&mut cx_b, |list, _| list.available_channels().is_some())
             .await;
@@ -1761,13 +1785,13 @@ mod tests {
         channel_b
             .condition(&cx_b, |channel, _| {
                 channel_messages(channel)
-                    == [("user_b".to_string(), "hello A, it's B.".to_string())]
+                    == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
             })
             .await;
 
         // Disconnect client B, ensuring we can still access its cached channel data.
         server.forbid_connections();
-        server.disconnect_client(user_id_b);
+        server.disconnect_client(current_user_id(&user_store_b));
         while !matches!(
             status_b.recv().await,
             Some(rpc::Status::ReconnectionError { .. })
@@ -1785,10 +1809,28 @@ mod tests {
         channel_b.read_with(&cx_b, |channel, _| {
             assert_eq!(
                 channel_messages(channel),
-                [("user_b".to_string(), "hello A, it's B.".to_string())]
+                [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
             )
         });
 
+        // Send a message from client B while it is disconnected.
+        channel_b
+            .update(&mut cx_b, |channel, cx| {
+                let task = channel
+                    .send_message("can you see this?".to_string(), cx)
+                    .unwrap();
+                assert_eq!(
+                    channel_messages(channel),
+                    &[
+                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                        ("user_b".to_string(), "can you see this?".to_string(), true)
+                    ]
+                );
+                task
+            })
+            .await
+            .unwrap_err();
+
         // Send a message from client A while B is disconnected.
         channel_a
             .update(&mut cx_a, |channel, cx| {
@@ -1798,12 +1840,12 @@ mod tests {
                     .detach();
                 let task = channel.send_message("sup".to_string(), cx).unwrap();
                 assert_eq!(
-                    channel
-                        .pending_messages()
-                        .iter()
-                        .map(|m| &m.body)
-                        .collect::<Vec<_>>(),
-                    &["oh, hi B.", "sup"]
+                    channel_messages(channel),
+                    &[
+                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                        ("user_a".to_string(), "oh, hi B.".to_string(), true),
+                        ("user_a".to_string(), "sup".to_string(), true)
+                    ]
                 );
                 task
             })
@@ -1814,14 +1856,16 @@ mod tests {
         server.allow_connections();
         cx_b.foreground().advance_clock(Duration::from_secs(10));
 
-        // Verify that B sees the new messages upon reconnection.
+        // Verify that B sees the new messages upon reconnection, as well as the message client B
+        // sent while offline.
         channel_b
             .condition(&cx_b, |channel, _| {
                 channel_messages(channel)
                     == [
-                        ("user_b".to_string(), "hello A, it's B.".to_string()),
-                        ("user_a".to_string(), "oh, hi B.".to_string()),
-                        ("user_a".to_string(), "sup".to_string()),
+                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                        ("user_a".to_string(), "oh, hi B.".to_string(), false),
+                        ("user_a".to_string(), "sup".to_string(), false),
+                        ("user_b".to_string(), "can you see this?".to_string(), false),
                     ]
             })
             .await;
@@ -1837,10 +1881,11 @@ mod tests {
             .condition(&cx_b, |channel, _| {
                 channel_messages(channel)
                     == [
-                        ("user_b".to_string(), "hello A, it's B.".to_string()),
-                        ("user_a".to_string(), "oh, hi B.".to_string()),
-                        ("user_a".to_string(), "sup".to_string()),
-                        ("user_a".to_string(), "you online?".to_string()),
+                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                        ("user_a".to_string(), "oh, hi B.".to_string(), false),
+                        ("user_a".to_string(), "sup".to_string(), false),
+                        ("user_b".to_string(), "can you see this?".to_string(), false),
+                        ("user_a".to_string(), "you online?".to_string(), false),
                     ]
             })
             .await;
@@ -1855,22 +1900,15 @@ mod tests {
             .condition(&cx_a, |channel, _| {
                 channel_messages(channel)
                     == [
-                        ("user_b".to_string(), "hello A, it's B.".to_string()),
-                        ("user_a".to_string(), "oh, hi B.".to_string()),
-                        ("user_a".to_string(), "sup".to_string()),
-                        ("user_a".to_string(), "you online?".to_string()),
-                        ("user_b".to_string(), "yep".to_string()),
+                        ("user_b".to_string(), "hello A, it's B.".to_string(), false),
+                        ("user_a".to_string(), "oh, hi B.".to_string(), false),
+                        ("user_a".to_string(), "sup".to_string(), false),
+                        ("user_b".to_string(), "can you see this?".to_string(), false),
+                        ("user_a".to_string(), "you online?".to_string(), false),
+                        ("user_b".to_string(), "yep".to_string(), false),
                     ]
             })
             .await;
-
-        fn channel_messages(channel: &Channel) -> Vec<(String, String)> {
-            channel
-                .messages()
-                .cursor::<(), ()>()
-                .map(|m| (m.sender.github_login.clone(), m.body.clone()))
-                .collect()
-        }
     }
 
     struct TestServer {
@@ -1905,8 +1943,8 @@ mod tests {
             &mut self,
             cx: &mut TestAppContext,
             name: &str,
-        ) -> (UserId, Arc<Client>) {
-            let client_user_id = self.app_state.db.create_user(name, false).await.unwrap();
+        ) -> (Arc<Client>, Arc<UserStore>) {
+            let user_id = self.app_state.db.create_user(name, false).await.unwrap();
             let client_name = name.to_string();
             let mut client = Client::new();
             let server = self.server.clone();
@@ -1914,45 +1952,50 @@ mod tests {
             let forbid_connections = self.forbid_connections.clone();
             Arc::get_mut(&mut client)
                 .unwrap()
-                .set_login_and_connect_callbacks(
-                    move |cx| {
-                        cx.spawn(|_| async move {
-                            let access_token = "the-token".to_string();
-                            Ok((client_user_id.0 as u64, access_token))
-                        })
-                    },
-                    move |user_id, access_token, cx| {
-                        assert_eq!(user_id, client_user_id.0 as u64);
-                        assert_eq!(access_token, "the-token");
-
-                        let server = server.clone();
-                        let connection_killers = connection_killers.clone();
-                        let forbid_connections = forbid_connections.clone();
-                        let client_name = client_name.clone();
-                        cx.spawn(move |cx| async move {
-                            if forbid_connections.load(SeqCst) {
-                                Err(anyhow!("server is forbidding connections"))
-                            } else {
-                                let (client_conn, server_conn, kill_conn) = Conn::in_memory();
-                                connection_killers.lock().insert(client_user_id, kill_conn);
-                                cx.background()
-                                    .spawn(server.handle_connection(
-                                        server_conn,
-                                        client_name,
-                                        client_user_id,
-                                    ))
-                                    .detach();
-                                Ok(client_conn)
-                            }
+                .override_authenticate(move |cx| {
+                    cx.spawn(|_| async move {
+                        let access_token = "the-token".to_string();
+                        Ok(Credentials {
+                            user_id: user_id.0 as u64,
+                            access_token,
                         })
-                    },
-                );
+                    })
+                })
+                .override_establish_connection(move |credentials, cx| {
+                    assert_eq!(credentials.user_id, user_id.0 as u64);
+                    assert_eq!(credentials.access_token, "the-token");
+
+                    let server = server.clone();
+                    let connection_killers = connection_killers.clone();
+                    let forbid_connections = forbid_connections.clone();
+                    let client_name = client_name.clone();
+                    cx.spawn(move |cx| async move {
+                        if forbid_connections.load(SeqCst) {
+                            Err(EstablishConnectionError::other(anyhow!(
+                                "server is forbidding connections"
+                            )))
+                        } else {
+                            let (client_conn, server_conn, kill_conn) = Connection::in_memory();
+                            connection_killers.lock().insert(user_id, kill_conn);
+                            cx.background()
+                                .spawn(server.handle_connection(server_conn, client_name, user_id))
+                                .detach();
+                            Ok(client_conn)
+                        }
+                    })
+                });
 
+            let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) });
             client
                 .authenticate_and_connect(&cx.to_async())
                 .await
                 .unwrap();
-            (client_user_id, client)
+
+            let user_store = UserStore::new(client.clone(), http, &cx.background());
+            let mut authed_user = user_store.watch_current_user();
+            while authed_user.recv().await.unwrap().is_none() {}
+
+            (client, user_store)
         }
 
         fn disconnect_client(&self, user_id: UserId) {
@@ -2008,6 +2051,24 @@ mod tests {
         }
     }
 
+    fn current_user_id(user_store: &Arc<UserStore>) -> UserId {
+        UserId::from_proto(user_store.current_user().unwrap().id)
+    }
+
+    fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
+        channel
+            .messages()
+            .cursor::<(), ()>()
+            .map(|m| {
+                (
+                    m.sender.github_login.clone(),
+                    m.body.clone(),
+                    m.is_pending(),
+                )
+            })
+            .collect()
+    }
+
     struct EmptyView;
 
     impl gpui::Entity for EmptyView {

zed/Cargo.toml 🔗

@@ -14,10 +14,11 @@ name = "Zed"
 path = "src/main.rs"
 
 [features]
-test-support = ["tempdir", "zrpc/test-support"]
+test-support = ["tempdir", "zrpc/test-support", "gpui/test-support"]
 
 [dependencies]
 anyhow = "1.0.38"
+async-recursion = "0.3"
 async-trait = "0.1"
 arrayvec = "0.7.1"
 async-tungstenite = { version = "0.14", features = ["async-tls"] }
@@ -30,9 +31,12 @@ futures = "0.3"
 gpui = { path = "../gpui" }
 http-auth-basic = "0.1.3"
 ignore = "0.4"
+image = "0.23"
+indexmap = "1.6.2"
 lazy_static = "1.4.0"
 libc = "0.2"
 log = "0.4"
+log-panics = { version = "2.0", features = ["with-backtrace"] }
 num_cpus = "1.13.0"
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
@@ -49,6 +53,7 @@ smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2.5"
 surf = "2.2"
 tempdir = { version = "0.3.7", optional = true }
+thiserror = "1.0.29"
 time = { version = "0.3" }
 tiny_http = "0.8"
 toml = "0.5"
@@ -64,6 +69,7 @@ serde_json = { version = "1.0.64", features = ["preserve_order"] }
 tempdir = { version = "0.3.7" }
 unindent = "0.1.7"
 zrpc = { path = "../zrpc", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
 
 [package.metadata.bundle]
 icon = ["app-icon@2x.png", "app-icon.png"]

zed/assets/themes/_base.toml 🔗

@@ -7,11 +7,18 @@ pane_divider = { width = 1, color = "$border.0" }
 
 [workspace.titlebar]
 border = { width = 1, bottom = true, color = "$border.0" }
-text = { extends = "$text.0" }
+title = "$text.0"
+avatar_width = 20
+icon_color = "$text.2.color"
+avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } }
+
+[workspace.titlebar.offline_icon]
+padding = { right = 4 }
+width = 16
 
 [workspace.tab]
 text = "$text.2"
-padding = { left = 10, right = 10 }
+padding = { left = 12, right = 12 }
 icon_width = 8
 spacing = 10
 icon_close = "$text.2.color"
@@ -26,7 +33,7 @@ background = "$surface.1"
 text = "$text.0"
 
 [workspace.sidebar]
-padding = { left = 12, right = 12 }
+width = 32
 border = { right = true, width = 1, color = "$border.0" }
 
 [workspace.sidebar.resize_handle]
@@ -60,6 +67,12 @@ sender = { extends = "$text.0", weight = "bold", margin.right = 8 }
 timestamp = "$text.2"
 padding.bottom = 6
 
+[chat_panel.pending_message]
+extends = "$chat_panel.message"
+body = { color = "$text.3.color" }
+sender = { color = "$text.3.color" }
+timestamp = { color = "$text.3.color" }
+
 [chat_panel.channel_select.item]
 padding = 4
 name = "$text.1"
@@ -94,8 +107,8 @@ shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
 background = "$surface.1"
 corner_radius = 6
 padding = { left = 8, right = 8, top = 7, bottom = 7 }
-text = "$text.0.color"
-placeholder_text = "$text.2.color"
+text = "$text.0"
+placeholder_text = "$text.2"
 selection = "$selection.host"
 border = { width = 1, color = "$border.0" }
 
@@ -119,8 +132,8 @@ border = { width = 1, color = "$border.0" }
 background = "$surface.1"
 corner_radius = 6
 padding = { left = 16, right = 16, top = 7, bottom = 7 }
-text = "$text.0.color"
-placeholder_text = "$text.2.color"
+text = "$text.0"
+placeholder_text = "$text.2"
 selection = "$selection.host"
 border = { width = 1, color = "$border.0" }
 
@@ -140,7 +153,7 @@ background = "$state.hover"
 text = "$text.0"
 
 [editor]
-text = "$text.1.color"
+text = "$text.1"
 background = "$surface.1"
 gutter_background = "$surface.1"
 active_line_background = "$state.active_line"

zed/assets/themes/black.toml 🔗

@@ -9,7 +9,6 @@ extends = "_base"
 0 = "#0F1011"
 
 [text]
-base = { family = "Inconsolata", size = 15 }
 0 = { extends = "$text.base", color = "#ffffff" }
 1 = { extends = "$text.base", color = "#b3b3b3" }
 2 = { extends = "$text.base", color = "#7b7d80" }
@@ -49,4 +48,4 @@ number = "#b5cea8"
 comment = "#6a9955"
 property = "#4e94ce"
 variant = "#4fc1ff"
-constant = "#9cdcfe"
+constant = "#9cdcfe"

zed/assets/themes/dark.toml 🔗

@@ -9,7 +9,6 @@ extends = "_base"
 0 = "#1B222B"
 
 [text]
-base = { family = "Inconsolata", size = 15 }
 0 = { extends = "$text.base", color = "#FFFFFF" }
 1 = { extends = "$text.base", color = "#CDD1E2" }
 2 = { extends = "$text.base", color = "#9BA8BE" }

zed/assets/themes/light.toml 🔗

@@ -9,7 +9,6 @@ extends = "_base"
 0 = "#DDDDDC"
 
 [text]
-base = { family = "Inconsolata", size = 15 }
 0 = { extends = "$text.base", color = "#000000" }
 1 = { extends = "$text.base", color = "#29292B" }
 2 = { extends = "$text.base", color = "#7E7E83" }

zed/src/channel.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     rpc::{self, Client},
     user::{User, UserStore},
-    util::TryFutureExt,
+    util::{post_inc, TryFutureExt},
 };
 use anyhow::{anyhow, Context, Result};
 use gpui::{
@@ -9,6 +9,7 @@ use gpui::{
     Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
 };
 use postage::prelude::Stream;
+use rand::prelude::*;
 use std::{
     collections::{HashMap, HashSet},
     mem,
@@ -39,29 +40,31 @@ pub struct Channel {
     details: ChannelDetails,
     messages: SumTree<ChannelMessage>,
     loaded_all_messages: bool,
-    pending_messages: Vec<PendingChannelMessage>,
-    next_local_message_id: u64,
+    next_pending_message_id: usize,
     user_store: Arc<UserStore>,
     rpc: Arc<Client>,
+    rng: StdRng,
     _subscription: rpc::Subscription,
 }
 
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
 pub struct ChannelMessage {
-    pub id: u64,
+    pub id: ChannelMessageId,
     pub body: String,
     pub timestamp: OffsetDateTime,
     pub sender: Arc<User>,
+    pub nonce: u128,
 }
 
-pub struct PendingChannelMessage {
-    pub body: String,
-    local_id: u64,
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ChannelMessageId {
+    Saved(u64),
+    Pending(usize),
 }
 
 #[derive(Clone, Debug, Default)]
 pub struct ChannelMessageSummary {
-    max_id: u64,
+    max_id: ChannelMessageId,
     count: usize,
 }
 
@@ -118,7 +121,7 @@ impl ChannelList {
                                 cx.notify();
                             });
                         }
-                        rpc::Status::Disconnected { .. } => {
+                        rpc::Status::SignedOut { .. } => {
                             this.update(&mut cx, |this, cx| {
                                 this.available_channels = None;
                                 this.channels.clear();
@@ -216,9 +219,9 @@ impl Channel {
             user_store,
             rpc,
             messages: Default::default(),
-            pending_messages: Default::default(),
             loaded_all_messages: false,
-            next_local_message_id: 0,
+            next_pending_message_id: 0,
+            rng: StdRng::from_entropy(),
             _subscription,
         }
     }
@@ -236,17 +239,35 @@ impl Channel {
             Err(anyhow!("message body can't be empty"))?;
         }
 
+        let current_user = self
+            .user_store
+            .current_user()
+            .ok_or_else(|| anyhow!("current_user is not present"))?;
+
         let channel_id = self.details.id;
-        let local_id = self.next_local_message_id;
-        self.next_local_message_id += 1;
-        self.pending_messages.push(PendingChannelMessage {
-            local_id,
-            body: body.clone(),
-        });
+        let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
+        let nonce = self.rng.gen();
+        self.insert_messages(
+            SumTree::from_item(
+                ChannelMessage {
+                    id: pending_id,
+                    body: body.clone(),
+                    sender: current_user,
+                    timestamp: OffsetDateTime::now_utc(),
+                    nonce,
+                },
+                &(),
+            ),
+            cx,
+        );
         let user_store = self.user_store.clone();
         let rpc = self.rpc.clone();
         Ok(cx.spawn(|this, mut cx| async move {
-            let request = rpc.request(proto::SendChannelMessage { channel_id, body });
+            let request = rpc.request(proto::SendChannelMessage {
+                channel_id,
+                body,
+                nonce: Some(nonce.into()),
+            });
             let response = request.await?;
             let message = ChannelMessage::from_proto(
                 response.message.ok_or_else(|| anyhow!("invalid message"))?,
@@ -254,13 +275,7 @@ impl Channel {
             )
             .await?;
             this.update(&mut cx, |this, cx| {
-                if let Ok(i) = this
-                    .pending_messages
-                    .binary_search_by_key(&local_id, |msg| msg.local_id)
-                {
-                    this.pending_messages.remove(i);
-                    this.insert_messages(SumTree::from_item(message, &()), cx);
-                }
+                this.insert_messages(SumTree::from_item(message, &()), cx);
                 Ok(())
             })
         }))
@@ -271,7 +286,12 @@ impl Channel {
             let rpc = self.rpc.clone();
             let user_store = self.user_store.clone();
             let channel_id = self.details.id;
-            if let Some(before_message_id) = self.messages.first().map(|message| message.id) {
+            if let Some(before_message_id) =
+                self.messages.first().and_then(|message| match message.id {
+                    ChannelMessageId::Saved(id) => Some(id),
+                    ChannelMessageId::Pending(_) => None,
+                })
+            {
                 cx.spawn(|this, mut cx| {
                     async move {
                         let response = rpc
@@ -301,32 +321,51 @@ impl Channel {
         let user_store = self.user_store.clone();
         let rpc = self.rpc.clone();
         let channel_id = self.details.id;
-        cx.spawn(|channel, mut cx| {
+        cx.spawn(|this, mut cx| {
             async move {
                 let response = rpc.request(proto::JoinChannel { channel_id }).await?;
                 let messages = messages_from_proto(response.messages, &user_store).await?;
                 let loaded_all_messages = response.done;
 
-                channel.update(&mut cx, |channel, cx| {
+                let pending_messages = this.update(&mut cx, |this, cx| {
                     if let Some((first_new_message, last_old_message)) =
-                        messages.first().zip(channel.messages.last())
+                        messages.first().zip(this.messages.last())
                     {
                         if first_new_message.id > last_old_message.id {
-                            let old_messages = mem::take(&mut channel.messages);
+                            let old_messages = mem::take(&mut this.messages);
                             cx.emit(ChannelEvent::MessagesUpdated {
                                 old_range: 0..old_messages.summary().count,
                                 new_count: 0,
                             });
-                            channel.loaded_all_messages = loaded_all_messages;
+                            this.loaded_all_messages = loaded_all_messages;
                         }
                     }
 
-                    channel.insert_messages(messages, cx);
+                    this.insert_messages(messages, cx);
                     if loaded_all_messages {
-                        channel.loaded_all_messages = loaded_all_messages;
+                        this.loaded_all_messages = loaded_all_messages;
                     }
+
+                    this.pending_messages().cloned().collect::<Vec<_>>()
                 });
 
+                for pending_message in pending_messages {
+                    let request = rpc.request(proto::SendChannelMessage {
+                        channel_id,
+                        body: pending_message.body,
+                        nonce: Some(pending_message.nonce.into()),
+                    });
+                    let response = request.await?;
+                    let message = ChannelMessage::from_proto(
+                        response.message.ok_or_else(|| anyhow!("invalid message"))?,
+                        &user_store,
+                    )
+                    .await?;
+                    this.update(&mut cx, |this, cx| {
+                        this.insert_messages(SumTree::from_item(message, &()), cx);
+                    });
+                }
+
                 Ok(())
             }
             .log_err()
@@ -354,8 +393,10 @@ impl Channel {
         cursor.take(range.len())
     }
 
-    pub fn pending_messages(&self) -> &[PendingChannelMessage] {
-        &self.pending_messages
+    pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
+        let mut cursor = self.messages.cursor::<ChannelMessageId, ()>();
+        cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
+        cursor
     }
 
     fn handle_message_sent(
@@ -386,7 +427,12 @@ impl Channel {
 
     fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
         if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
-            let mut old_cursor = self.messages.cursor::<u64, Count>();
+            let nonces = messages
+                .cursor::<(), ()>()
+                .map(|m| m.nonce)
+                .collect::<HashSet<_>>();
+
+            let mut old_cursor = self.messages.cursor::<ChannelMessageId, Count>();
             let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
             let start_ix = old_cursor.sum_start().0;
             let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
@@ -395,10 +441,40 @@ impl Channel {
             let end_ix = start_ix + removed_count;
 
             new_messages.push_tree(messages, &());
-            new_messages.push_tree(old_cursor.suffix(&()), &());
+
+            let mut ranges = Vec::<Range<usize>>::new();
+            if new_messages.last().unwrap().is_pending() {
+                new_messages.push_tree(old_cursor.suffix(&()), &());
+            } else {
+                new_messages.push_tree(
+                    old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
+                    &(),
+                );
+
+                while let Some(message) = old_cursor.item() {
+                    let message_ix = old_cursor.sum_start().0;
+                    if nonces.contains(&message.nonce) {
+                        if ranges.last().map_or(false, |r| r.end == message_ix) {
+                            ranges.last_mut().unwrap().end += 1;
+                        } else {
+                            ranges.push(message_ix..message_ix + 1);
+                        }
+                    } else {
+                        new_messages.push(message.clone(), &());
+                    }
+                    old_cursor.next(&());
+                }
+            }
+
             drop(old_cursor);
             self.messages = new_messages;
 
+            for range in ranges.into_iter().rev() {
+                cx.emit(ChannelEvent::MessagesUpdated {
+                    old_range: range,
+                    new_count: 0,
+                });
+            }
             cx.emit(ChannelEvent::MessagesUpdated {
                 old_range: start_ix..end_ix,
                 new_count,
@@ -443,14 +519,22 @@ impl ChannelMessage {
         message: proto::ChannelMessage,
         user_store: &UserStore,
     ) -> Result<Self> {
-        let sender = user_store.get_user(message.sender_id).await?;
+        let sender = user_store.fetch_user(message.sender_id).await?;
         Ok(ChannelMessage {
-            id: message.id,
+            id: ChannelMessageId::Saved(message.id),
             body: message.body,
             timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
             sender,
+            nonce: message
+                .nonce
+                .ok_or_else(|| anyhow!("nonce is required"))?
+                .into(),
         })
     }
+
+    pub fn is_pending(&self) -> bool {
+        matches!(self.id, ChannelMessageId::Pending(_))
+    }
 }
 
 impl sum_tree::Item for ChannelMessage {
@@ -464,6 +548,12 @@ impl sum_tree::Item for ChannelMessage {
     }
 }
 
+impl Default for ChannelMessageId {
+    fn default() -> Self {
+        Self::Saved(0)
+    }
+}
+
 impl sum_tree::Summary for ChannelMessageSummary {
     type Context = ();
 
@@ -473,7 +563,7 @@ impl sum_tree::Summary for ChannelMessageSummary {
     }
 }
 
-impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for u64 {
+impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
     fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
         debug_assert!(summary.max_id > *self);
         *self = summary.max_id;
@@ -495,19 +585,36 @@ impl<'a> sum_tree::SeekDimension<'a, ChannelMessageSummary> for Count {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test::FakeServer;
+    use crate::test::{FakeHttpClient, FakeServer};
     use gpui::TestAppContext;
+    use surf::http::Response;
 
     #[gpui::test]
     async fn test_channel_messages(mut cx: TestAppContext) {
         let user_id = 5;
         let mut client = Client::new();
+        let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) });
         let server = FakeServer::for_client(user_id, &mut client, &cx).await;
-        let user_store = Arc::new(UserStore::new(client.clone()));
+        let user_store = UserStore::new(client.clone(), http_client, cx.background().as_ref());
 
         let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx));
         channel_list.read_with(&cx, |list, _| assert_eq!(list.available_channels(), None));
 
+        let get_users = server.receive::<proto::GetUsers>().await.unwrap();
+        assert_eq!(get_users.payload.user_ids, vec![5]);
+        server
+            .respond(
+                get_users.receipt(),
+                proto::GetUsersResponse {
+                    users: vec![proto::User {
+                        id: 5,
+                        github_login: "nathansobo".into(),
+                        avatar_url: "http://avatar.com/nathansobo".into(),
+                    }],
+                },
+            )
+            .await;
+
         // Get the available channels.
         let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
         server
@@ -551,12 +658,14 @@ mod tests {
                             body: "a".into(),
                             timestamp: 1000,
                             sender_id: 5,
+                            nonce: Some(1.into()),
                         },
                         proto::ChannelMessage {
                             id: 11,
                             body: "b".into(),
                             timestamp: 1001,
                             sender_id: 6,
+                            nonce: Some(2.into()),
                         },
                     ],
                     done: false,
@@ -567,23 +676,16 @@ mod tests {
         // Client requests all users for the received messages
         let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
         get_users.payload.user_ids.sort();
-        assert_eq!(get_users.payload.user_ids, vec![5, 6]);
+        assert_eq!(get_users.payload.user_ids, vec![6]);
         server
             .respond(
                 get_users.receipt(),
                 proto::GetUsersResponse {
-                    users: vec![
-                        proto::User {
-                            id: 5,
-                            github_login: "nathansobo".into(),
-                            avatar_url: "http://avatar.com/nathansobo".into(),
-                        },
-                        proto::User {
-                            id: 6,
-                            github_login: "maxbrunsfeld".into(),
-                            avatar_url: "http://avatar.com/maxbrunsfeld".into(),
-                        },
-                    ],
+                    users: vec![proto::User {
+                        id: 6,
+                        github_login: "maxbrunsfeld".into(),
+                        avatar_url: "http://avatar.com/maxbrunsfeld".into(),
+                    }],
                 },
             )
             .await;
@@ -617,6 +719,7 @@ mod tests {
                     body: "c".into(),
                     timestamp: 1002,
                     sender_id: 7,
+                    nonce: Some(3.into()),
                 }),
             })
             .await;
@@ -672,12 +775,14 @@ mod tests {
                             body: "y".into(),
                             timestamp: 998,
                             sender_id: 5,
+                            nonce: Some(4.into()),
                         },
                         proto::ChannelMessage {
                             id: 9,
                             body: "z".into(),
                             timestamp: 999,
                             sender_id: 6,
+                            nonce: Some(5.into()),
                         },
                     ],
                 },

zed/src/chat_panel.rs 🔗

@@ -54,10 +54,15 @@ impl ChatPanel {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let input_editor = cx.add_view(|cx| {
-            Editor::auto_height(4, settings.clone(), cx).with_style({
-                let settings = settings.clone();
-                move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
-            })
+            Editor::auto_height(
+                4,
+                settings.clone(),
+                {
+                    let settings = settings.clone();
+                    move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
+                },
+                cx,
+            )
         });
         let channel_select = cx.add_view(|cx| {
             let channel_list = channel_list.clone();
@@ -209,7 +214,7 @@ impl ChatPanel {
         Flex::column()
             .with_child(
                 Container::new(ChildView::new(self.channel_select.id()).boxed())
-                    .with_style(&theme.chat_panel.channel_select.container)
+                    .with_style(theme.chat_panel.channel_select.container)
                     .boxed(),
             )
             .with_child(self.render_active_channel_messages())
@@ -230,7 +235,12 @@ impl ChatPanel {
     fn render_message(&self, message: &ChannelMessage) -> ElementBox {
         let now = OffsetDateTime::now_utc();
         let settings = self.settings.borrow();
-        let theme = &settings.theme.chat_panel.message;
+        let theme = if message.is_pending() {
+            &settings.theme.chat_panel.pending_message
+        } else {
+            &settings.theme.chat_panel.message
+        };
+
         Container::new(
             Flex::column()
                 .with_child(
@@ -243,7 +253,7 @@ impl ChatPanel {
                                 )
                                 .boxed(),
                             )
-                            .with_style(&theme.sender.container)
+                            .with_style(theme.sender.container)
                             .boxed(),
                         )
                         .with_child(
@@ -254,7 +264,7 @@ impl ChatPanel {
                                 )
                                 .boxed(),
                             )
-                            .with_style(&theme.timestamp.container)
+                            .with_style(theme.timestamp.container)
                             .boxed(),
                         )
                         .boxed(),
@@ -262,14 +272,14 @@ impl ChatPanel {
                 .with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
                 .boxed(),
         )
-        .with_style(&theme.container)
+        .with_style(theme.container)
         .boxed()
     }
 
     fn render_input_box(&self) -> ElementBox {
         let theme = &self.settings.borrow().theme;
         Container::new(ChildView::new(self.input_editor.id()).boxed())
-            .with_style(&theme.chat_panel.input_editor.container)
+            .with_style(theme.chat_panel.input_editor.container)
             .boxed()
     }
 
@@ -293,13 +303,13 @@ impl ChatPanel {
             Flex::row()
                 .with_child(
                     Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
-                        .with_style(&theme.hash.container)
+                        .with_style(theme.hash.container)
                         .boxed(),
                 )
                 .with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
                 .boxed(),
         )
-        .with_style(&theme.container)
+        .with_style(theme.container)
         .boxed()
     }
 
@@ -381,13 +391,14 @@ impl View for ChatPanel {
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         let theme = &self.settings.borrow().theme;
-        let element = match *self.rpc.status().borrow() {
-            rpc::Status::Connected { .. } => self.render_channel(),
-            _ => self.render_sign_in_prompt(cx),
+        let element = if self.rpc.user_id().is_some() {
+            self.render_channel()
+        } else {
+            self.render_sign_in_prompt(cx)
         };
         ConstrainedBox::new(
             Container::new(element)
-                .with_style(&theme.chat_panel.container)
+                .with_style(theme.chat_panel.container)
                 .boxed(),
         )
         .with_min_width(150.)

zed/src/editor.rs 🔗

@@ -4,8 +4,9 @@ mod element;
 pub mod movement;
 
 use crate::{
-    settings::{HighlightId, Settings},
-    theme::{EditorStyle, Theme},
+    language::Language,
+    settings::Settings,
+    theme::Theme,
     time::ReplicaId,
     util::{post_inc, Bias},
     workspace,
@@ -17,15 +18,9 @@ pub use display_map::DisplayPoint;
 use display_map::*;
 pub use element::*;
 use gpui::{
-    action,
-    color::Color,
-    font_cache::FamilyId,
-    fonts::Properties as FontProperties,
-    geometry::vector::Vector2F,
-    keymap::Binding,
-    text_layout::{self, RunStyle},
-    AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle,
-    MutableAppContext, RenderContext, Task, TextLayoutCache, View, ViewContext, WeakViewHandle,
+    action, color::Color, fonts::TextStyle, geometry::vector::Vector2F, keymap::Binding,
+    text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
+    MutableAppContext, RenderContext, Task, View, ViewContext, WeakViewHandle,
 };
 use postage::watch;
 use serde::{Deserialize, Serialize};
@@ -34,8 +29,6 @@ use smol::Timer;
 use std::{
     cell::RefCell,
     cmp::{self, Ordering},
-    collections::BTreeMap,
-    fmt::Write,
     iter::FromIterator,
     mem,
     ops::{Range, RangeInclusive},
@@ -278,6 +271,26 @@ pub enum EditorMode {
     Full,
 }
 
+#[derive(Clone, Deserialize)]
+pub struct EditorStyle {
+    pub text: TextStyle,
+    #[serde(default)]
+    pub placeholder_text: Option<TextStyle>,
+    pub background: Color,
+    pub selection: SelectionStyle,
+    pub gutter_background: Color,
+    pub active_line_background: Color,
+    pub line_number: Color,
+    pub line_number_active: Color,
+    pub guest_selections: Vec<SelectionStyle>,
+}
+
+#[derive(Clone, Copy, Default, Deserialize)]
+pub struct SelectionStyle {
+    pub cursor: Color,
+    pub selection: Color,
+}
+
 pub struct Editor {
     handle: WeakViewHandle<Self>,
     buffer: ModelHandle<Buffer>,
@@ -290,10 +303,10 @@ pub struct Editor {
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
     autoscroll_requested: bool,
-    build_style: Option<Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>>,
+    build_style: Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>,
     settings: watch::Receiver<Settings>,
     focused: bool,
-    cursors_visible: bool,
+    show_local_cursors: bool,
     blink_epoch: usize,
     blinking_paused: bool,
     mode: EditorMode,
@@ -305,8 +318,6 @@ pub struct Snapshot {
     pub display_snapshot: DisplayMapSnapshot,
     pub placeholder_text: Option<Arc<str>>,
     pub theme: Arc<Theme>,
-    pub font_family: FamilyId,
-    pub font_size: f32,
     is_focused: bool,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
@@ -324,9 +335,13 @@ struct ClipboardSelection {
 }
 
 impl Editor {
-    pub fn single_line(settings: watch::Receiver<Settings>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn single_line(
+        settings: watch::Receiver<Settings>,
+        build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
-        let mut view = Self::for_buffer(buffer, settings, cx);
+        let mut view = Self::for_buffer(buffer, settings, build_style, cx);
         view.mode = EditorMode::SingleLine;
         view
     }
@@ -334,10 +349,11 @@ impl Editor {
     pub fn auto_height(
         max_lines: usize,
         settings: watch::Receiver<Settings>,
+        build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
-        let mut view = Self::for_buffer(buffer, settings, cx);
+        let mut view = Self::for_buffer(buffer, settings, build_style, cx);
         view.mode = EditorMode::AutoHeight { max_lines };
         view
     }
@@ -345,10 +361,29 @@ impl Editor {
     pub fn for_buffer(
         buffer: ModelHandle<Buffer>,
         settings: watch::Receiver<Settings>,
+        build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let display_map =
-            cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.clone(), None, cx));
+        Self::new(buffer, settings, Rc::new(RefCell::new(build_style)), cx)
+    }
+
+    fn new(
+        buffer: ModelHandle<Buffer>,
+        settings: watch::Receiver<Settings>,
+        build_style: Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let style = build_style.borrow_mut()(cx);
+        let display_map = cx.add_model(|cx| {
+            DisplayMap::new(
+                buffer.clone(),
+                settings.borrow().tab_size,
+                style.text.font_id,
+                style.text.font_size,
+                None,
+                cx,
+            )
+        });
         cx.observe(&buffer, Self::on_buffer_changed).detach();
         cx.subscribe(&buffer, Self::on_buffer_event).detach();
         cx.observe(&display_map, Self::on_display_map_changed)
@@ -376,13 +411,13 @@ impl Editor {
             next_selection_id,
             add_selections_state: None,
             select_larger_syntax_node_stack: Vec::new(),
-            build_style: None,
+            build_style,
             scroll_position: Vector2F::zero(),
             scroll_top_anchor: Anchor::min(),
             autoscroll_requested: false,
             settings,
             focused: false,
-            cursors_visible: false,
+            show_local_cursors: false,
             blink_epoch: 0,
             blinking_paused: false,
             mode: EditorMode::Full,
@@ -390,14 +425,6 @@ impl Editor {
         }
     }
 
-    pub fn with_style(
-        mut self,
-        f: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
-    ) -> Self {
-        self.build_style = Some(Rc::new(RefCell::new(f)));
-        self
-    }
-
     pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
         self.buffer.read(cx).replica_id()
     }
@@ -416,8 +443,6 @@ impl Editor {
             scroll_top_anchor: self.scroll_top_anchor.clone(),
             theme: settings.theme.clone(),
             placeholder_text: self.placeholder_text.clone(),
-            font_family: settings.buffer_font_family,
-            font_size: settings.buffer_font_size,
             is_focused: self
                 .handle
                 .upgrade(cx)
@@ -425,6 +450,10 @@ impl Editor {
         }
     }
 
+    pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
+        self.buffer.read(cx).language()
+    }
+
     pub fn set_placeholder_text(
         &mut self,
         placeholder_text: impl Into<Arc<str>>,
@@ -2229,7 +2258,7 @@ impl Editor {
     }
 
     fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
-        self.cursors_visible = true;
+        self.show_local_cursors = true;
         cx.notify();
 
         let epoch = self.next_blink_epoch();
@@ -2254,7 +2283,7 @@ impl Editor {
 
     fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
         if epoch == self.blink_epoch && self.focused && !self.blinking_paused {
-            self.cursors_visible = !self.cursors_visible;
+            self.show_local_cursors = !self.show_local_cursors;
             cx.notify();
 
             let epoch = self.next_blink_epoch();
@@ -2271,8 +2300,8 @@ impl Editor {
         }
     }
 
-    pub fn cursors_visible(&self) -> bool {
-        self.cursors_visible
+    pub fn show_local_cursors(&self) -> bool {
+        self.show_local_cursors
     }
 
     fn on_buffer_changed(&mut self, _: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
@@ -2301,263 +2330,60 @@ impl Editor {
 }
 
 impl Snapshot {
-    pub fn scroll_position(&self) -> Vector2F {
-        compute_scroll_position(
-            &self.display_snapshot,
-            self.scroll_position,
-            &self.scroll_top_anchor,
-        )
+    pub fn is_empty(&self) -> bool {
+        self.display_snapshot.is_empty()
     }
 
-    pub fn max_point(&self) -> DisplayPoint {
-        self.display_snapshot.max_point()
+    pub fn is_focused(&self) -> bool {
+        self.is_focused
     }
 
-    pub fn longest_row(&self) -> u32 {
-        self.display_snapshot.longest_row()
+    pub fn placeholder_text(&self) -> Option<&Arc<str>> {
+        self.placeholder_text.as_ref()
     }
 
-    pub fn line_len(&self, display_row: u32) -> u32 {
-        self.display_snapshot.line_len(display_row)
+    pub fn buffer_row_count(&self) -> u32 {
+        self.display_snapshot.buffer_row_count()
     }
 
-    pub fn font_ascent(&self, font_cache: &FontCache) -> f32 {
-        let font_id = font_cache.default_font(self.font_family);
-        let ascent = font_cache.metric(font_id, |m| m.ascent);
-        font_cache.scale_metric(ascent, font_id, self.font_size)
+    pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
+        self.display_snapshot.buffer_rows(start_row)
     }
 
-    pub fn font_descent(&self, font_cache: &FontCache) -> f32 {
-        let font_id = font_cache.default_font(self.font_family);
-        let descent = font_cache.metric(font_id, |m| m.descent);
-        font_cache.scale_metric(descent, font_id, self.font_size)
+    pub fn highlighted_chunks_for_rows(
+        &mut self,
+        display_rows: Range<u32>,
+    ) -> display_map::HighlightedChunks {
+        self.display_snapshot
+            .highlighted_chunks_for_rows(display_rows)
     }
 
-    pub fn line_height(&self, font_cache: &FontCache) -> f32 {
-        let font_id = font_cache.default_font(self.font_family);
-        font_cache.line_height(font_id, self.font_size).ceil()
+    pub fn theme(&self) -> &Arc<Theme> {
+        &self.theme
     }
 
-    pub fn em_width(&self, font_cache: &FontCache) -> f32 {
-        let font_id = font_cache.default_font(self.font_family);
-        font_cache.em_width(font_id, self.font_size)
+    pub fn scroll_position(&self) -> Vector2F {
+        compute_scroll_position(
+            &self.display_snapshot,
+            self.scroll_position,
+            &self.scroll_top_anchor,
+        )
     }
 
-    // TODO: Can we make this not return a result?
-    pub fn max_line_number_width(
-        &self,
-        font_cache: &FontCache,
-        layout_cache: &TextLayoutCache,
-    ) -> Result<f32> {
-        let font_size = self.font_size;
-        let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
-        let digit_count = (self.display_snapshot.buffer_row_count() as f32)
-            .log10()
-            .floor() as usize
-            + 1;
-
-        Ok(layout_cache
-            .layout_str(
-                "1".repeat(digit_count).as_str(),
-                font_size,
-                &[(
-                    digit_count,
-                    RunStyle {
-                        font_id,
-                        color: Color::black(),
-                        underline: false,
-                    },
-                )],
-            )
-            .width())
+    pub fn max_point(&self) -> DisplayPoint {
+        self.display_snapshot.max_point()
     }
 
-    pub fn layout_line_numbers(
-        &self,
-        rows: Range<u32>,
-        active_rows: &BTreeMap<u32, bool>,
-        font_cache: &FontCache,
-        layout_cache: &TextLayoutCache,
-        theme: &Theme,
-    ) -> Result<Vec<Option<text_layout::Line>>> {
-        let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
-
-        let mut layouts = Vec::with_capacity(rows.len());
-        let mut line_number = String::new();
-        for (ix, (buffer_row, soft_wrapped)) in self
-            .display_snapshot
-            .buffer_rows(rows.start)
-            .take((rows.end - rows.start) as usize)
-            .enumerate()
-        {
-            let display_row = rows.start + ix as u32;
-            let color = if active_rows.contains_key(&display_row) {
-                theme.editor.line_number_active
-            } else {
-                theme.editor.line_number
-            };
-            if soft_wrapped {
-                layouts.push(None);
-            } else {
-                line_number.clear();
-                write!(&mut line_number, "{}", buffer_row + 1).unwrap();
-                layouts.push(Some(layout_cache.layout_str(
-                    &line_number,
-                    self.font_size,
-                    &[(
-                        line_number.len(),
-                        RunStyle {
-                            font_id,
-                            color,
-                            underline: false,
-                        },
-                    )],
-                )));
-            }
-        }
-
-        Ok(layouts)
+    pub fn longest_row(&self) -> u32 {
+        self.display_snapshot.longest_row()
     }
 
-    pub fn layout_lines(
-        &mut self,
-        mut rows: Range<u32>,
-        style: &EditorStyle,
-        font_cache: &FontCache,
-        layout_cache: &TextLayoutCache,
-    ) -> Result<Vec<text_layout::Line>> {
-        rows.end = cmp::min(rows.end, self.display_snapshot.max_point().row() + 1);
-        if rows.start >= rows.end {
-            return Ok(Vec::new());
-        }
-
-        // When the editor is empty and unfocused, then show the placeholder.
-        if self.display_snapshot.is_empty() && !self.is_focused {
-            let placeholder_lines = self
-                .placeholder_text
-                .as_ref()
-                .map_or("", AsRef::as_ref)
-                .split('\n')
-                .skip(rows.start as usize)
-                .take(rows.len());
-            let font_id = font_cache
-                .select_font(self.font_family, &style.placeholder_text.font_properties)?;
-            return Ok(placeholder_lines
-                .into_iter()
-                .map(|line| {
-                    layout_cache.layout_str(
-                        line,
-                        self.font_size,
-                        &[(
-                            line.len(),
-                            RunStyle {
-                                font_id,
-                                color: style.placeholder_text.color,
-                                underline: false,
-                            },
-                        )],
-                    )
-                })
-                .collect());
-        }
-
-        let mut prev_font_properties = FontProperties::new();
-        let mut prev_font_id = font_cache
-            .select_font(self.font_family, &prev_font_properties)
-            .unwrap();
-
-        let mut layouts = Vec::with_capacity(rows.len());
-        let mut line = String::new();
-        let mut styles = Vec::new();
-        let mut row = rows.start;
-        let mut line_exceeded_max_len = false;
-        let chunks = self
-            .display_snapshot
-            .highlighted_chunks_for_rows(rows.clone());
-
-        'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
-            for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
-                if ix > 0 {
-                    layouts.push(layout_cache.layout_str(&line, self.font_size, &styles));
-                    line.clear();
-                    styles.clear();
-                    row += 1;
-                    line_exceeded_max_len = false;
-                    if row == rows.end {
-                        break 'outer;
-                    }
-                }
-
-                if !line_chunk.is_empty() && !line_exceeded_max_len {
-                    let style = self
-                        .theme
-                        .syntax
-                        .highlight_style(style_ix)
-                        .unwrap_or(style.text.clone());
-                    // Avoid a lookup if the font properties match the previous ones.
-                    let font_id = if style.font_properties == prev_font_properties {
-                        prev_font_id
-                    } else {
-                        font_cache.select_font(self.font_family, &style.font_properties)?
-                    };
-
-                    if line.len() + line_chunk.len() > MAX_LINE_LEN {
-                        let mut chunk_len = MAX_LINE_LEN - line.len();
-                        while !line_chunk.is_char_boundary(chunk_len) {
-                            chunk_len -= 1;
-                        }
-                        line_chunk = &line_chunk[..chunk_len];
-                        line_exceeded_max_len = true;
-                    }
-
-                    line.push_str(line_chunk);
-                    styles.push((
-                        line_chunk.len(),
-                        RunStyle {
-                            font_id,
-                            color: style.color,
-                            underline: style.underline,
-                        },
-                    ));
-                    prev_font_id = font_id;
-                    prev_font_properties = style.font_properties;
-                }
-            }
-        }
-
-        Ok(layouts)
+    pub fn line_len(&self, display_row: u32) -> u32 {
+        self.display_snapshot.line_len(display_row)
     }
 
-    pub fn layout_line(
-        &self,
-        row: u32,
-        font_cache: &FontCache,
-        layout_cache: &TextLayoutCache,
-    ) -> Result<text_layout::Line> {
-        let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
-
-        let mut line = self.display_snapshot.line(row);
-
-        if line.len() > MAX_LINE_LEN {
-            let mut len = MAX_LINE_LEN;
-            while !line.is_char_boundary(len) {
-                len -= 1;
-            }
-            line.truncate(len);
-        }
-
-        Ok(layout_cache.layout_str(
-            &line,
-            self.font_size,
-            &[(
-                self.display_snapshot.line_len(row) as usize,
-                RunStyle {
-                    font_id,
-                    color: Color::black(),
-                    underline: false,
-                },
-            )],
-        ))
+    pub fn line(&self, display_row: u32) -> String {
+        self.display_snapshot.line(display_row)
     }
 
     pub fn prev_row_boundary(&self, point: DisplayPoint) -> (DisplayPoint, Point) {
@@ -2569,6 +2395,41 @@ impl Snapshot {
     }
 }
 
+impl EditorStyle {
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test(font_cache: &gpui::FontCache) -> Self {
+        let font_family_name = Arc::from("Monaco");
+        let font_properties = Default::default();
+        let font_family_id = font_cache.load_family(&[&font_family_name]).unwrap();
+        let font_id = font_cache
+            .select_font(font_family_id, &font_properties)
+            .unwrap();
+        Self {
+            text: TextStyle {
+                font_family_name,
+                font_family_id,
+                font_id,
+                font_size: 14.,
+                color: Color::from_u32(0xff0000ff),
+                font_properties,
+                underline: false,
+            },
+            placeholder_text: None,
+            background: Default::default(),
+            gutter_background: Default::default(),
+            active_line_background: Default::default(),
+            line_number: Default::default(),
+            line_number_active: Default::default(),
+            selection: Default::default(),
+            guest_selections: Default::default(),
+        }
+    }
+
+    fn placeholder_text(&self) -> &TextStyle {
+        self.placeholder_text.as_ref().unwrap_or(&self.text)
+    }
+}
+
 fn compute_scroll_position(
     snapshot: &DisplayMapSnapshot,
     mut scroll_position: Vector2F,
@@ -2604,10 +2465,10 @@ impl Entity for Editor {
 
 impl View for Editor {
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let style = self
-            .build_style
-            .as_ref()
-            .map_or(Default::default(), |build| (build.borrow_mut())(cx));
+        let style = self.build_style.borrow_mut()(cx);
+        self.display_map.update(cx, |map, cx| {
+            map.set_font(style.text.font_id, style.text.font_size, cx)
+        });
         EditorElement::new(self.handle.clone(), style).boxed()
     }
 
@@ -2627,7 +2488,7 @@ impl View for Editor {
 
     fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
         self.focused = false;
-        self.cursors_visible = false;
+        self.show_local_cursors = false;
         self.buffer.update(cx, |buffer, cx| {
             buffer.set_active_selection_set(None, cx).unwrap();
         });
@@ -2659,8 +2520,34 @@ impl workspace::Item for Buffer {
         settings: watch::Receiver<Settings>,
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View {
-        Editor::for_buffer(handle, settings.clone(), cx)
-            .with_style(move |_| settings.borrow().theme.editor.clone())
+        Editor::for_buffer(
+            handle,
+            settings.clone(),
+            move |cx| {
+                let settings = settings.borrow();
+                let font_cache = cx.font_cache();
+                let font_family_id = settings.buffer_font_family;
+                let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
+                let font_properties = Default::default();
+                let font_id = font_cache
+                    .select_font(font_family_id, &font_properties)
+                    .unwrap();
+                let font_size = settings.buffer_font_size;
+
+                let mut theme = settings.theme.editor.clone();
+                theme.text = TextStyle {
+                    color: theme.text.color,
+                    font_family_name,
+                    font_family_id,
+                    font_id,
+                    font_size,
+                    font_properties,
+                    underline: false,
+                };
+                theme
+            },
+            cx,
+        )
     }
 }
 
@@ -2697,10 +2584,14 @@ impl workspace::ItemView for Editor {
     where
         Self: Sized,
     {
-        let mut clone = Editor::for_buffer(self.buffer.clone(), self.settings.clone(), cx);
+        let mut clone = Editor::new(
+            self.buffer.clone(),
+            self.settings.clone(),
+            self.build_style.clone(),
+            cx,
+        );
         clone.scroll_position = self.scroll_position;
         clone.scroll_top_anchor = self.scroll_top_anchor.clone();
-        clone.build_style = self.build_style.clone();
         Some(clone)
     }
 
@@ -2742,9 +2633,8 @@ mod tests {
     fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
         let settings = settings::test(&cx).1;
-        let (_, editor) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, editor) =
+            cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         editor.update(cx, |view, cx| {
             view.begin_selection(DisplayPoint::new(2, 2), false, cx);
@@ -2810,9 +2700,7 @@ mod tests {
     fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
         let settings = settings::test(&cx).1;
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
             view.begin_selection(DisplayPoint::new(2, 2), false, cx);
@@ -2844,9 +2732,7 @@ mod tests {
     fn test_cancel(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
         let settings = settings::test(&cx).1;
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
             view.begin_selection(DisplayPoint::new(3, 4), false, cx);
@@ -2882,33 +2768,6 @@ mod tests {
         });
     }
 
-    #[gpui::test]
-    fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
-        let layout_cache = TextLayoutCache::new(cx.platform().fonts());
-        let font_cache = cx.font_cache().clone();
-
-        let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
-
-        let settings = settings::test(&cx).1;
-        let (_, editor) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings.clone(), cx)
-        });
-
-        let layouts = editor.update(cx, |editor, cx| {
-            editor
-                .snapshot(cx)
-                .layout_line_numbers(
-                    0..6,
-                    &Default::default(),
-                    &font_cache,
-                    &layout_cache,
-                    &settings.borrow().theme,
-                )
-                .unwrap()
-        });
-        assert_eq!(layouts.len(), 6);
-    }
-
     #[gpui::test]
     fn test_fold(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| {
@@ -2937,7 +2796,7 @@ mod tests {
         });
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
 
         view.update(cx, |view, cx| {
@@ -3005,7 +2864,7 @@ mod tests {
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
 
         buffer.update(cx, |buffer, cx| {
@@ -3082,7 +2941,7 @@ mod tests {
         let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx));
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
 
         assert_eq!('ⓐ'.len_utf8(), 3);
@@ -3140,7 +2999,7 @@ mod tests {
         let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx));
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
         view.update(cx, |view, cx| {
             view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx)
@@ -3170,9 +3029,7 @@ mod tests {
     fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\n  def", cx));
         let settings = settings::test(&cx).1;
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -3315,9 +3172,7 @@ mod tests {
         let buffer =
             cx.add_model(|cx| Buffer::new(0, "use std::str::{foo, bar}\n\n  {baz.qux()}", cx));
         let settings = settings::test(&cx).1;
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -3509,9 +3364,7 @@ mod tests {
         let buffer =
             cx.add_model(|cx| Buffer::new(0, "use one::{\n    two::three::four::five\n};", cx));
         let settings = settings::test(&cx).1;
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
             view.set_wrap_width(130., cx);
@@ -3572,7 +3425,7 @@ mod tests {
         });
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
 
         view.update(cx, |view, cx| {
@@ -3608,7 +3461,7 @@ mod tests {
         });
         let settings = settings::test(&cx).1;
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer.clone(), settings, cx)
+            build_editor(buffer.clone(), settings, cx)
         });
 
         view.update(cx, |view, cx| {
@@ -3637,9 +3490,7 @@ mod tests {
     fn test_delete_line(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -3663,9 +3514,7 @@ mod tests {
 
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx)
                 .unwrap();
@@ -3682,9 +3531,7 @@ mod tests {
     fn test_duplicate_line(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -3711,9 +3558,7 @@ mod tests {
 
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -3739,9 +3584,7 @@ mod tests {
     fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(10, 5), cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.fold_ranges(
                 vec![
@@ -3840,7 +3683,7 @@ mod tests {
         let settings = settings::test(&cx).1;
         let view = cx
             .add_window(Default::default(), |cx| {
-                Editor::for_buffer(buffer.clone(), settings, cx)
+                build_editor(buffer.clone(), settings, cx)
             })
             .1;
 
@@ -3973,9 +3816,7 @@ mod tests {
     fn test_select_all(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\nde\nfgh", cx));
         let settings = settings::test(&cx).1;
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_all(&SelectAll, cx);
             assert_eq!(
@@ -3989,9 +3830,7 @@ mod tests {
     fn test_select_line(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 5), cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.select_display_ranges(
                 &[
@@ -4037,9 +3876,7 @@ mod tests {
     fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(9, 5), cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
         view.update(cx, |view, cx| {
             view.fold_ranges(
                 vec![
@@ -4107,9 +3944,7 @@ mod tests {
     fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) {
         let settings = settings::test(&cx).1;
         let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", cx));
-        let (_, view) = cx.add_window(Default::default(), |cx| {
-            Editor::for_buffer(buffer, settings, cx)
-        });
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
             view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], cx)
@@ -4295,7 +4130,7 @@ mod tests {
             let history = History::new(text.into());
             Buffer::from_history(0, history, None, lang.cloned(), cx)
         });
-        let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings.clone(), cx));
+        let (_, view) = cx.add_window(|cx| build_editor(buffer, settings.clone(), cx));
         view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing())
             .await;
 
@@ -4433,6 +4268,40 @@ mod tests {
         let point = DisplayPoint::new(row as u32, column as u32);
         point..point
     }
+
+    fn build_editor(
+        buffer: ModelHandle<Buffer>,
+        settings: watch::Receiver<Settings>,
+        cx: &mut ViewContext<Editor>,
+    ) -> Editor {
+        let style = {
+            let font_cache = cx.font_cache();
+            let settings = settings.borrow();
+            EditorStyle {
+                text: TextStyle {
+                    color: Default::default(),
+                    font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
+                    font_family_id: settings.buffer_font_family,
+                    font_id: font_cache
+                        .select_font(settings.buffer_font_family, &Default::default())
+                        .unwrap(),
+                    font_size: settings.buffer_font_size,
+                    font_properties: Default::default(),
+                    underline: false,
+                },
+                placeholder_text: None,
+                background: Default::default(),
+                selection: Default::default(),
+                gutter_background: Default::default(),
+                active_line_background: Default::default(),
+                line_number: Default::default(),
+                line_number_active: Default::default(),
+                guest_selections: Default::default(),
+            }
+        };
+
+        Editor::for_buffer(buffer, settings, move |_| style.clone(), cx)
+    }
 }
 
 trait RangeExt<T> {

zed/src/editor/buffer.rs 🔗

@@ -714,9 +714,16 @@ impl Buffer {
         path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
+        let path = path.into();
         let handle = cx.handle();
         let text = self.visible_text.clone();
         let version = self.version.clone();
+
+        if let Some(language) = worktree.read(cx).languages().select_language(&path).cloned() {
+            self.language = Some(language);
+            self.reparse(cx);    
+        }
+
         let save_as = worktree.update(cx, |worktree, cx| {
             worktree
                 .as_local_mut()
@@ -794,6 +801,10 @@ impl Buffer {
         cx.emit(Event::FileHandleChanged);
     }
 
+    pub fn language(&self) -> Option<&Arc<Language>> {
+        self.language.as_ref()
+    }
+
     pub fn parse_count(&self) -> usize {
         self.parse_count
     }
@@ -871,7 +882,11 @@ impl Buffer {
                     cx.spawn(move |this, mut cx| async move {
                         let new_tree = parse_task.await;
                         this.update(&mut cx, move |this, cx| {
-                            let parse_again = this.version > parsed_version;
+                            let language_changed =
+                                this.language.as_ref().map_or(true, |curr_language| {
+                                    !Arc::ptr_eq(curr_language, &language)
+                                });
+                            let parse_again = this.version > parsed_version || language_changed;
                             *this.syntax_tree.lock() = Some(SyntaxTree {
                                 tree: new_tree,
                                 dirty: false,

zed/src/editor/display_map.rs 🔗

@@ -2,14 +2,13 @@ mod fold_map;
 mod tab_map;
 mod wrap_map;
 
-use super::{buffer, Anchor, Bias, Buffer, Point, Settings, ToOffset, ToPoint};
+use super::{buffer, Anchor, Bias, Buffer, Point, ToOffset, ToPoint};
 use fold_map::FoldMap;
-use gpui::{Entity, ModelContext, ModelHandle};
-use postage::watch;
+use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
 use std::ops::Range;
 use tab_map::TabMap;
-pub use wrap_map::BufferRows;
 use wrap_map::WrapMap;
+pub use wrap_map::{BufferRows, HighlightedChunks};
 
 pub struct DisplayMap {
     buffer: ModelHandle<Buffer>,
@@ -25,13 +24,16 @@ impl Entity for DisplayMap {
 impl DisplayMap {
     pub fn new(
         buffer: ModelHandle<Buffer>,
-        settings: watch::Receiver<Settings>,
+        tab_size: usize,
+        font_id: FontId,
+        font_size: f32,
         wrap_width: Option<f32>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
-        let (tab_map, snapshot) = TabMap::new(snapshot, settings.borrow().tab_size);
-        let wrap_map = cx.add_model(|cx| WrapMap::new(snapshot, settings, wrap_width, cx));
+        let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
+        let wrap_map =
+            cx.add_model(|cx| WrapMap::new(snapshot, font_id, font_size, wrap_width, cx));
         cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
         DisplayMap {
             buffer,
@@ -85,6 +87,11 @@ impl DisplayMap {
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
     }
 
+    pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
+        self.wrap_map
+            .update(cx, |map, cx| map.set_font(font_id, font_size, cx));
+    }
+
     pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
         self.wrap_map
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -367,12 +374,12 @@ mod tests {
             .unwrap_or(10);
 
         let font_cache = cx.font_cache().clone();
-        let settings = Settings {
-            tab_size: rng.gen_range(1..=4),
-            buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
-            buffer_font_size: 14.0,
-            ..cx.read(Settings::test)
-        };
+        let tab_size = rng.gen_range(1..=4);
+        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
         let max_wrap_width = 300.0;
         let mut wrap_width = if rng.gen_bool(0.1) {
             None
@@ -380,7 +387,7 @@ mod tests {
             Some(rng.gen_range(0.0..=max_wrap_width))
         };
 
-        log::info!("tab size: {}", settings.tab_size);
+        log::info!("tab size: {}", tab_size);
         log::info!("wrap width: {:?}", wrap_width);
 
         let buffer = cx.add_model(|cx| {
@@ -388,9 +395,10 @@ mod tests {
             let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
             Buffer::new(0, text, cx)
         });
-        let settings = watch::channel_with(settings).1;
 
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings, wrap_width, cx));
+        let map = cx.add_model(|cx| {
+            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
+        });
         let (_observer, notifications) = Observer::new(&map, &mut cx);
         let mut fold_count = 0;
 
@@ -529,26 +537,27 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_soft_wraps(mut cx: gpui::TestAppContext) {
+    fn test_soft_wraps(cx: &mut MutableAppContext) {
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
         cx.foreground().forbid_parking();
 
         let font_cache = cx.font_cache();
 
-        let settings = Settings {
-            buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
-            buffer_font_size: 12.0,
-            tab_size: 4,
-            ..cx.read(Settings::test)
-        };
+        let tab_size = 4;
+        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 12.0;
         let wrap_width = Some(64.);
 
         let text = "one two three four five\nsix seven eight";
         let buffer = cx.add_model(|cx| Buffer::new(0, text.to_string(), cx));
-        let (mut settings_tx, settings_rx) = watch::channel_with(settings);
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings_rx, wrap_width, cx));
+        let map = cx.add_model(|cx| {
+            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
+        });
 
-        let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
             snapshot.chunks_at(0).collect::<String>(),
             "one two \nthree four \nfive\nsix seven \neight"
@@ -592,23 +601,21 @@ mod tests {
             (DisplayPoint::new(2, 4), SelectionGoal::Column(10))
         );
 
-        buffer.update(&mut cx, |buffer, cx| {
+        buffer.update(cx, |buffer, cx| {
             let ix = buffer.text().find("seven").unwrap();
             buffer.edit(vec![ix..ix], "and ", cx);
         });
 
-        let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
             snapshot.chunks_at(1).collect::<String>(),
             "three four \nfive\nsix and \nseven eight"
         );
 
         // Re-wrap on font size changes
-        settings_tx.borrow_mut().buffer_font_size += 3.;
-
-        map.next_notification(&mut cx).await;
+        map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
 
-        let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
             snapshot.chunks_at(1).collect::<String>(),
             "three \nfour five\nsix and \nseven \neight"
@@ -619,11 +626,16 @@ mod tests {
     fn test_chunks_at(cx: &mut gpui::MutableAppContext) {
         let text = sample_text(6, 6);
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        let settings = watch::channel_with(Settings {
-            tab_size: 4,
-            ..Settings::test(cx)
+        let tab_size = 4;
+        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let map = cx.add_model(|cx| {
+            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
         });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         buffer.update(cx, |buffer, cx| {
             buffer.edit(
                 vec![
@@ -695,13 +707,16 @@ mod tests {
         });
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
 
-        let settings = cx.update(|cx| {
-            watch::channel_with(Settings {
-                tab_size: 2,
-                ..Settings::test(cx)
-            })
-        });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer, settings.1, None, cx));
+        let tab_size = 2;
+        let font_cache = cx.font_cache();
+        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
         assert_eq!(
             cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
             vec![
@@ -782,15 +797,16 @@ mod tests {
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
 
         let font_cache = cx.font_cache();
-        let settings = cx.update(|cx| {
-            watch::channel_with(Settings {
-                tab_size: 4,
-                buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(),
-                buffer_font_size: 16.0,
-                ..Settings::test(cx)
-            })
-        });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer, settings.1, Some(40.0), cx));
+
+        let tab_size = 4;
+        let family_id = font_cache.load_family(&["Courier"]).unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 16.0;
+
+        let map = cx
+            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), cx));
         assert_eq!(
             cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
             [
@@ -825,11 +841,17 @@ mod tests {
         let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
         let display_text = "\n'a', 'α',   '✋',    '❎', '🍐'\n";
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        let settings = watch::channel_with(Settings {
-            tab_size: 4,
-            ..Settings::test(cx)
+
+        let tab_size = 4;
+        let font_cache = cx.font_cache();
+        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let map = cx.add_model(|cx| {
+            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
         });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         let map = map.update(cx, |map, cx| map.snapshot(cx));
 
         assert_eq!(map.text(), display_text);
@@ -863,11 +885,17 @@ mod tests {
     fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
         let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
-        let settings = watch::channel_with(Settings {
-            tab_size: 4,
-            ..Settings::test(cx)
+        let tab_size = 4;
+        let font_cache = cx.font_cache();
+        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+
+        let map = cx.add_model(|cx| {
+            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
         });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         let map = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(map.text(), "✅       α\nβ   \n🏀β      γ");
         assert_eq!(
@@ -924,11 +952,16 @@ mod tests {
     #[gpui::test]
     fn test_max_point(cx: &mut gpui::MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "aaa\n\t\tbbb", cx));
-        let settings = watch::channel_with(Settings {
-            tab_size: 4,
-            ..Settings::test(cx)
+        let tab_size = 4;
+        let font_cache = cx.font_cache();
+        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let map = cx.add_model(|cx| {
+            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
         });
-        let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
         assert_eq!(
             map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
             DisplayPoint::new(1, 11)

zed/src/editor/display_map/wrap_map.rs 🔗

@@ -2,14 +2,14 @@ use super::{
     fold_map,
     tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
 };
-use crate::{editor::Point, settings::HighlightId, util::Bias, Settings};
+use crate::{editor::Point, settings::HighlightId, util::Bias};
 use gpui::{
+    fonts::FontId,
     sum_tree::{self, Cursor, SumTree},
     text_layout::LineWrapper,
     Entity, ModelContext, Task,
 };
 use lazy_static::lazy_static;
-use postage::{prelude::Stream, watch};
 use smol::future::yield_now;
 use std::{collections::VecDeque, ops::Range, time::Duration};
 
@@ -18,8 +18,7 @@ pub struct WrapMap {
     pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
     wrap_width: Option<f32>,
     background_task: Option<Task<()>>,
-    _watch_settings: Task<()>,
-    settings: watch::Receiver<Settings>,
+    font: (FontId, f32),
 }
 
 impl Entity for WrapMap {
@@ -76,36 +75,17 @@ pub struct BufferRows<'a> {
 impl WrapMap {
     pub fn new(
         tab_snapshot: TabSnapshot,
-        settings: watch::Receiver<Settings>,
+        font_id: FontId,
+        font_size: f32,
         wrap_width: Option<f32>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        let _watch_settings = cx.spawn_weak({
-            let mut prev_font = (
-                settings.borrow().buffer_font_size,
-                settings.borrow().buffer_font_family,
-            );
-            let mut settings = settings.clone();
-            move |this, mut cx| async move {
-                while let Some(settings) = settings.recv().await {
-                    if let Some(this) = this.upgrade(&cx) {
-                        let font = (settings.buffer_font_size, settings.buffer_font_family);
-                        if font != prev_font {
-                            prev_font = font;
-                            this.update(&mut cx, |this, cx| this.rewrap(cx));
-                        }
-                    }
-                }
-            }
-        });
-
         let mut this = Self {
+            font: (font_id, font_size),
             wrap_width: None,
             pending_edits: Default::default(),
             snapshot: Snapshot::new(tab_snapshot),
-            settings,
             background_task: None,
-            _watch_settings,
         };
         this.set_wrap_width(wrap_width, cx);
 
@@ -128,6 +108,13 @@ impl WrapMap {
         self.snapshot.clone()
     }
 
+    pub fn set_font(&mut self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
+        if (font_id, font_size) != self.font {
+            self.font = (font_id, font_size);
+            self.rewrap(cx)
+        }
+    }
+
     pub fn set_wrap_width(&mut self, wrap_width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
         if wrap_width == self.wrap_width {
             return false;
@@ -144,15 +131,9 @@ impl WrapMap {
         if let Some(wrap_width) = self.wrap_width {
             let mut new_snapshot = self.snapshot.clone();
             let font_cache = cx.font_cache().clone();
-            let settings = self.settings.clone();
+            let (font_id, font_size) = self.font;
             let task = cx.background().spawn(async move {
-                let mut line_wrapper = {
-                    let settings = settings.borrow();
-                    let font_id = font_cache
-                        .select_font(settings.buffer_font_family, &Default::default())
-                        .unwrap();
-                    font_cache.line_wrapper(font_id, settings.buffer_font_size)
-                };
+                let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
                 let tab_snapshot = new_snapshot.tab_snapshot.clone();
                 let range = TabPoint::zero()..tab_snapshot.max_point();
                 new_snapshot
@@ -222,15 +203,9 @@ impl WrapMap {
                 let pending_edits = self.pending_edits.clone();
                 let mut snapshot = self.snapshot.clone();
                 let font_cache = cx.font_cache().clone();
-                let settings = self.settings.clone();
+                let (font_id, font_size) = self.font;
                 let update_task = cx.background().spawn(async move {
-                    let mut line_wrapper = {
-                        let settings = settings.borrow();
-                        let font_id = font_cache
-                            .select_font(settings.buffer_font_family, &Default::default())
-                            .unwrap();
-                        font_cache.line_wrapper(font_id, settings.buffer_font_size)
-                    };
+                    let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
 
                     for (tab_snapshot, edits) in pending_edits {
                         snapshot
@@ -950,13 +925,14 @@ mod tests {
         } else {
             Some(rng.gen_range(0.0..=1000.0))
         };
-        let settings = Settings {
-            tab_size: rng.gen_range(1..=4),
-            buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
-            buffer_font_size: 14.0,
-            ..cx.read(Settings::test)
-        };
-        log::info!("Tab size: {}", settings.tab_size);
+        let tab_size = rng.gen_range(1..=4);
+        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let font_id = font_cache
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+
+        log::info!("Tab size: {}", tab_size);
         log::info!("Wrap width: {:?}", wrap_width);
 
         let buffer = cx.add_model(|cx| {
@@ -965,7 +941,7 @@ mod tests {
             Buffer::new(0, text, cx)
         });
         let (mut fold_map, folds_snapshot) = cx.read(|cx| FoldMap::new(buffer.clone(), cx));
-        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), settings.tab_size);
+        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
         log::info!(
             "Unwrapped text (no folds): {:?}",
             buffer.read_with(&cx, |buf, _| buf.text())
@@ -976,16 +952,13 @@ mod tests {
         );
         log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
 
-        let font_id = font_cache
-            .select_font(settings.buffer_font_family, &Default::default())
-            .unwrap();
-        let mut line_wrapper = LineWrapper::new(font_id, settings.buffer_font_size, font_system);
+        let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
         let unwrapped_text = tabs_snapshot.text();
         let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
 
-        let settings = watch::channel_with(settings).1;
-        let wrap_map = cx
-            .add_model(|cx| WrapMap::new(tabs_snapshot.clone(), settings.clone(), wrap_width, cx));
+        let wrap_map = cx.add_model(|cx| {
+            WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)
+        });
         let (_observer, notifications) = Observer::new(&wrap_map, &mut cx);
 
         if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {

zed/src/editor/element.rs 🔗

@@ -1,5 +1,8 @@
-use super::{DisplayPoint, Editor, EditorMode, Insert, Scroll, Select, SelectPhase, Snapshot};
-use crate::{theme::EditorStyle, time::ReplicaId};
+use super::{
+    DisplayPoint, Editor, EditorMode, EditorStyle, Insert, Scroll, Select, SelectPhase, Snapshot,
+    MAX_LINE_LEN,
+};
+use crate::{theme::HighlightId, time::ReplicaId};
 use gpui::{
     color::Color,
     geometry::{
@@ -9,7 +12,7 @@ use gpui::{
     },
     json::{self, ToJson},
     keymap::Keystroke,
-    text_layout::{self, TextLayoutCache},
+    text_layout::{self, RunStyle, TextLayoutCache},
     AppContext, Axis, Border, Element, Event, EventContext, FontCache, LayoutContext,
     MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
 };
@@ -18,6 +21,7 @@ use smallvec::SmallVec;
 use std::{
     cmp::{self, Ordering},
     collections::{BTreeMap, HashMap},
+    fmt::Write,
     ops::Range,
 };
 
@@ -265,6 +269,7 @@ impl EditorElement {
         let view = self.view(cx.app);
         let settings = self.view(cx.app).settings.borrow();
         let theme = &settings.theme.editor;
+        let local_replica_id = view.replica_id(cx);
         let scroll_position = layout.snapshot.scroll_position();
         let start_row = scroll_position.y() as u32;
         let scroll_top = scroll_position.y() * layout.line_height;
@@ -334,7 +339,7 @@ impl EditorElement {
                     selection.paint(bounds, cx.scene);
                 }
 
-                if view.cursors_visible() {
+                if view.show_local_cursors() || *replica_id != local_replica_id {
                     let cursor_position = selection.end;
                     if (start_row..end_row).contains(&cursor_position.row()) {
                         let cursor_row_layout =
@@ -374,6 +379,176 @@ impl EditorElement {
 
         cx.scene.pop_layer();
     }
+
+    fn max_line_number_width(&self, snapshot: &Snapshot, cx: &LayoutContext) -> f32 {
+        let digit_count = (snapshot.buffer_row_count() as f32).log10().floor() as usize + 1;
+
+        cx.text_layout_cache
+            .layout_str(
+                "1".repeat(digit_count).as_str(),
+                self.style.text.font_size,
+                &[(
+                    digit_count,
+                    RunStyle {
+                        font_id: self.style.text.font_id,
+                        color: Color::black(),
+                        underline: false,
+                    },
+                )],
+            )
+            .width()
+    }
+
+    fn layout_line_numbers(
+        &self,
+        rows: Range<u32>,
+        active_rows: &BTreeMap<u32, bool>,
+        snapshot: &Snapshot,
+        cx: &LayoutContext,
+    ) -> Vec<Option<text_layout::Line>> {
+        let mut layouts = Vec::with_capacity(rows.len());
+        let mut line_number = String::new();
+        for (ix, (buffer_row, soft_wrapped)) in snapshot
+            .buffer_rows(rows.start)
+            .take((rows.end - rows.start) as usize)
+            .enumerate()
+        {
+            let display_row = rows.start + ix as u32;
+            let color = if active_rows.contains_key(&display_row) {
+                self.style.line_number_active
+            } else {
+                self.style.line_number
+            };
+            if soft_wrapped {
+                layouts.push(None);
+            } else {
+                line_number.clear();
+                write!(&mut line_number, "{}", buffer_row + 1).unwrap();
+                layouts.push(Some(cx.text_layout_cache.layout_str(
+                    &line_number,
+                    self.style.text.font_size,
+                    &[(
+                        line_number.len(),
+                        RunStyle {
+                            font_id: self.style.text.font_id,
+                            color,
+                            underline: false,
+                        },
+                    )],
+                )));
+            }
+        }
+
+        layouts
+    }
+
+    fn layout_lines(
+        &mut self,
+        mut rows: Range<u32>,
+        snapshot: &mut Snapshot,
+        cx: &LayoutContext,
+    ) -> Vec<text_layout::Line> {
+        rows.end = cmp::min(rows.end, snapshot.max_point().row() + 1);
+        if rows.start >= rows.end {
+            return Vec::new();
+        }
+
+        // When the editor is empty and unfocused, then show the placeholder.
+        if snapshot.is_empty() && !snapshot.is_focused() {
+            let placeholder_style = self.style.placeholder_text();
+            let placeholder_text = snapshot.placeholder_text();
+            let placeholder_lines = placeholder_text
+                .as_ref()
+                .map_or("", AsRef::as_ref)
+                .split('\n')
+                .skip(rows.start as usize)
+                .take(rows.len());
+            return placeholder_lines
+                .map(|line| {
+                    cx.text_layout_cache.layout_str(
+                        line,
+                        placeholder_style.font_size,
+                        &[(
+                            line.len(),
+                            RunStyle {
+                                font_id: placeholder_style.font_id,
+                                color: placeholder_style.color,
+                                underline: false,
+                            },
+                        )],
+                    )
+                })
+                .collect();
+        }
+
+        let mut prev_font_properties = self.style.text.font_properties.clone();
+        let mut prev_font_id = self.style.text.font_id;
+
+        let theme = snapshot.theme().clone();
+        let mut layouts = Vec::with_capacity(rows.len());
+        let mut line = String::new();
+        let mut styles = Vec::new();
+        let mut row = rows.start;
+        let mut line_exceeded_max_len = false;
+        let chunks = snapshot.highlighted_chunks_for_rows(rows.clone());
+
+        'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
+            for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
+                if ix > 0 {
+                    layouts.push(cx.text_layout_cache.layout_str(
+                        &line,
+                        self.style.text.font_size,
+                        &styles,
+                    ));
+                    line.clear();
+                    styles.clear();
+                    row += 1;
+                    line_exceeded_max_len = false;
+                    if row == rows.end {
+                        break 'outer;
+                    }
+                }
+
+                if !line_chunk.is_empty() && !line_exceeded_max_len {
+                    let style = theme
+                        .syntax
+                        .highlight_style(style_ix)
+                        .unwrap_or(self.style.text.clone().into());
+                    // Avoid a lookup if the font properties match the previous ones.
+                    let font_id = if style.font_properties == prev_font_properties {
+                        prev_font_id
+                    } else {
+                        cx.font_cache
+                            .select_font(self.style.text.font_family_id, &style.font_properties)
+                            .unwrap_or(self.style.text.font_id)
+                    };
+
+                    if line.len() + line_chunk.len() > MAX_LINE_LEN {
+                        let mut chunk_len = MAX_LINE_LEN - line.len();
+                        while !line_chunk.is_char_boundary(chunk_len) {
+                            chunk_len -= 1;
+                        }
+                        line_chunk = &line_chunk[..chunk_len];
+                        line_exceeded_max_len = true;
+                    }
+
+                    line.push_str(line_chunk);
+                    styles.push((
+                        line_chunk.len(),
+                        RunStyle {
+                            font_id,
+                            color: style.color,
+                            underline: style.underline,
+                        },
+                    ));
+                    prev_font_id = font_id;
+                    prev_font_properties = style.font_properties;
+                }
+            }
+        }
+
+        layouts
+    }
 }
 
 impl Element for EditorElement {
@@ -390,30 +565,22 @@ impl Element for EditorElement {
             unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
         }
 
-        let font_cache = &cx.font_cache;
-        let layout_cache = &cx.text_layout_cache;
         let snapshot = self.snapshot(cx.app);
-        let line_height = snapshot.line_height(font_cache);
+        let line_height = self.style.text.line_height(cx.font_cache);
 
         let gutter_padding;
         let gutter_width;
         if snapshot.mode == EditorMode::Full {
-            gutter_padding = snapshot.em_width(cx.font_cache);
-            match snapshot.max_line_number_width(cx.font_cache, cx.text_layout_cache) {
-                Err(error) => {
-                    log::error!("error computing max line number width: {}", error);
-                    return (size, None);
-                }
-                Ok(width) => gutter_width = width + gutter_padding * 2.0,
-            }
+            gutter_padding = self.style.text.em_width(cx.font_cache);
+            gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
         } else {
             gutter_padding = 0.0;
             gutter_width = 0.0
         };
 
         let text_width = size.x() - gutter_width;
-        let text_offset = vec2f(-snapshot.font_descent(cx.font_cache), 0.);
-        let em_width = snapshot.em_width(font_cache);
+        let text_offset = vec2f(-self.style.text.descent(cx.font_cache), 0.);
+        let em_width = self.style.text.em_width(cx.font_cache);
         let overscroll = vec2f(em_width, 0.);
         let wrap_width = text_width - text_offset.x() - overscroll.x() - em_width;
         let snapshot = self.update_view(cx.app, |view, cx| {
@@ -488,51 +655,18 @@ impl Element for EditorElement {
         });
 
         let line_number_layouts = if snapshot.mode == EditorMode::Full {
-            let settings = self
-                .view
-                .upgrade(cx.app)
-                .unwrap()
-                .read(cx.app)
-                .settings
-                .borrow();
-            match snapshot.layout_line_numbers(
-                start_row..end_row,
-                &active_rows,
-                cx.font_cache,
-                cx.text_layout_cache,
-                &settings.theme,
-            ) {
-                Err(error) => {
-                    log::error!("error laying out line numbers: {}", error);
-                    return (size, None);
-                }
-                Ok(layouts) => layouts,
-            }
+            self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx)
         } else {
             Vec::new()
         };
 
         let mut max_visible_line_width = 0.0;
-        let line_layouts = match snapshot.layout_lines(
-            start_row..end_row,
-            &self.style,
-            font_cache,
-            layout_cache,
-        ) {
-            Err(error) => {
-                log::error!("error laying out lines: {}", error);
-                return (size, None);
-            }
-            Ok(layouts) => {
-                for line in &layouts {
-                    if line.width() > max_visible_line_width {
-                        max_visible_line_width = line.width();
-                    }
-                }
-
-                layouts
+        let line_layouts = self.layout_lines(start_row..end_row, &mut snapshot, cx);
+        for line in &line_layouts {
+            if line.width() > max_visible_line_width {
+                max_visible_line_width = line.width();
             }
-        };
+        }
 
         let mut layout = LayoutState {
             size,
@@ -542,6 +676,7 @@ impl Element for EditorElement {
             overscroll,
             text_offset,
             snapshot,
+            style: self.style.clone(),
             active_rows,
             line_layouts,
             line_number_layouts,
@@ -551,15 +686,18 @@ impl Element for EditorElement {
             max_visible_line_width,
         };
 
+        let scroll_max = layout.scroll_max(cx.font_cache, cx.text_layout_cache).x();
+        let scroll_width = layout.scroll_width(cx.text_layout_cache);
+        let max_glyph_width = self.style.text.em_width(&cx.font_cache);
         self.update_view(cx.app, |view, cx| {
-            let clamped = view.clamp_scroll_left(layout.scroll_max(font_cache, layout_cache).x());
+            let clamped = view.clamp_scroll_left(scroll_max);
             let autoscrolled;
             if autoscroll_horizontally {
                 autoscrolled = view.autoscroll_horizontally(
                     start_row,
                     layout.text_size.x(),
-                    layout.scroll_width(font_cache, layout_cache),
-                    layout.snapshot.em_width(font_cache),
+                    scroll_width,
+                    max_glyph_width,
                     &layout.line_layouts,
                     cx,
                 );
@@ -659,6 +797,7 @@ pub struct LayoutState {
     gutter_size: Vector2F,
     gutter_padding: f32,
     text_size: Vector2F,
+    style: EditorStyle,
     snapshot: Snapshot,
     active_rows: BTreeMap<u32, bool>,
     line_layouts: Vec<text_layout::Line>,
@@ -672,20 +811,16 @@ pub struct LayoutState {
 }
 
 impl LayoutState {
-    fn scroll_width(&self, font_cache: &FontCache, layout_cache: &TextLayoutCache) -> f32 {
+    fn scroll_width(&self, layout_cache: &TextLayoutCache) -> f32 {
         let row = self.snapshot.longest_row();
-        let longest_line_width = self
-            .snapshot
-            .layout_line(row, font_cache, layout_cache)
-            .unwrap()
-            .width();
+        let longest_line_width = self.layout_line(row, &self.snapshot, layout_cache).width();
         longest_line_width.max(self.max_visible_line_width) + self.overscroll.x()
     }
 
     fn scroll_max(&self, font_cache: &FontCache, layout_cache: &TextLayoutCache) -> Vector2F {
         let text_width = self.text_size.x();
-        let scroll_width = self.scroll_width(font_cache, layout_cache);
-        let em_width = self.snapshot.em_width(font_cache);
+        let scroll_width = self.scroll_width(layout_cache);
+        let em_width = self.style.text.em_width(font_cache);
         let max_row = self.snapshot.max_point().row();
 
         vec2f(
@@ -693,6 +828,36 @@ impl LayoutState {
             max_row.saturating_sub(1) as f32,
         )
     }
+
+    pub fn layout_line(
+        &self,
+        row: u32,
+        snapshot: &Snapshot,
+        layout_cache: &TextLayoutCache,
+    ) -> text_layout::Line {
+        let mut line = snapshot.line(row);
+
+        if line.len() > MAX_LINE_LEN {
+            let mut len = MAX_LINE_LEN;
+            while !line.is_char_boundary(len) {
+                len -= 1;
+            }
+            line.truncate(len);
+        }
+
+        layout_cache.layout_str(
+            &line,
+            self.style.text.font_size,
+            &[(
+                snapshot.line_len(row) as usize,
+                RunStyle {
+                    font_id: self.style.text.font_id,
+                    color: Color::black(),
+                    underline: false,
+                },
+            )],
+        )
+    }
 }
 
 pub struct PaintState {
@@ -864,3 +1029,42 @@ fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
 fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 {
     delta.powf(1.2) / 300.0
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        editor::{Buffer, Editor, EditorStyle},
+        settings,
+        test::sample_text,
+    };
+
+    #[gpui::test]
+    fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
+        let font_cache = cx.font_cache().clone();
+        let settings = settings::test(&cx).1;
+        let style = EditorStyle::test(&font_cache);
+
+        let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
+        let (window_id, editor) = cx.add_window(Default::default(), |cx| {
+            Editor::for_buffer(
+                buffer,
+                settings.clone(),
+                {
+                    let style = style.clone();
+                    move |_| style.clone()
+                },
+                cx,
+            )
+        });
+        let element = EditorElement::new(editor.downgrade(), style);
+
+        let layouts = editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let mut presenter = cx.build_presenter(window_id, 30.);
+            let mut layout_cx = presenter.build_layout_context(false, cx);
+            element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx)
+        });
+        assert_eq!(layouts.len(), 6);
+    }
+}

zed/src/editor/movement.rs 🔗

@@ -180,16 +180,21 @@ fn char_kind(c: char) -> CharKind {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{
-        editor::{display_map::DisplayMap, Buffer},
-        test::test_app_state,
-    };
+    use crate::editor::{display_map::DisplayMap, Buffer};
 
     #[gpui::test]
     fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
-        let settings = test_app_state(cx).settings.clone();
+        let tab_size = 4;
+        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+
         let buffer = cx.add_model(|cx| Buffer::new(0, "a bcΔ defγ", cx));
-        let display_map = cx.add_model(|cx| DisplayMap::new(buffer, settings, None, cx));
+        let display_map =
+            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
         let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
             prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)).unwrap(),

zed/src/file_finder.rs 🔗

@@ -88,13 +88,13 @@ impl View for FileFinder {
                     Flex::new(Axis::Vertical)
                         .with_child(
                             Container::new(ChildView::new(self.query_editor.id()).boxed())
-                                .with_style(&settings.theme.selector.input_editor.container)
+                                .with_style(settings.theme.selector.input_editor.container)
                                 .boxed(),
                         )
                         .with_child(Flexible::new(1.0, self.render_matches()).boxed())
                         .boxed(),
                 )
-                .with_style(&settings.theme.selector.container)
+                .with_style(settings.theme.selector.container)
                 .boxed(),
             )
             .with_max_width(500.0)
@@ -127,7 +127,7 @@ impl FileFinder {
                 )
                 .boxed(),
             )
-            .with_style(&settings.theme.selector.empty.container)
+            .with_style(settings.theme.selector.empty.container)
             .named("empty matches");
         }
 
@@ -200,7 +200,7 @@ impl FileFinder {
                 )
                 .boxed(),
         )
-        .with_style(&style.container);
+        .with_style(style.container);
 
         let action = Select(Entry {
             worktree_id: path_match.tree_id,
@@ -275,10 +275,14 @@ impl FileFinder {
         cx.observe(&workspace, Self::workspace_updated).detach();
 
         let query_editor = cx.add_view(|cx| {
-            Editor::single_line(settings.clone(), cx).with_style({
-                let settings = settings.clone();
-                move |_| settings.borrow().theme.selector.input_editor.as_editor()
-            })
+            Editor::single_line(
+                settings.clone(),
+                {
+                    let settings = settings.clone();
+                    move |_| settings.borrow().theme.selector.input_editor.as_editor()
+                },
+                cx,
+            )
         });
         cx.subscribe(&query_editor, Self::on_query_editor_event)
             .detach();

zed/src/http.rs 🔗

@@ -0,0 +1,26 @@
+pub use anyhow::{anyhow, Result};
+use futures::future::BoxFuture;
+use std::sync::Arc;
+pub use surf::{
+    http::{Method, Response as ServerResponse},
+    Request, Response, Url,
+};
+
+pub trait HttpClient: Send + Sync {
+    fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>>;
+}
+
+pub fn client() -> Arc<dyn HttpClient> {
+    Arc::new(surf::client())
+}
+
+impl HttpClient for surf::Client {
+    fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
+        Box::pin(async move {
+            Ok(self
+                .send(req)
+                .await
+                .map_err(|e| anyhow!("http request failed: {}", e))?)
+        })
+    }
+}

zed/src/lib.rs 🔗

@@ -5,6 +5,7 @@ pub mod editor;
 pub mod file_finder;
 pub mod fs;
 mod fuzzy;
+pub mod http;
 pub mod language;
 pub mod menus;
 pub mod project_browser;
@@ -42,6 +43,7 @@ pub struct AppState {
     pub languages: Arc<language::LanguageRegistry>,
     pub themes: Arc<settings::ThemeRegistry>,
     pub rpc: Arc<rpc::Client>,
+    pub user_store: Arc<user::UserStore>,
     pub fs: Arc<dyn fs::Fs>,
     pub channel_list: ModelHandle<ChannelList>,
 }

zed/src/main.rs 🔗

@@ -13,7 +13,7 @@ use zed::{
     channel::ChannelList,
     chat_panel, editor, file_finder,
     fs::RealFs,
-    language, menus, rpc, settings, theme_selector,
+    http, language, menus, rpc, settings, theme_selector,
     user::UserStore,
     workspace::{self, OpenNew, OpenParams, OpenPaths},
     AppState,
@@ -37,14 +37,16 @@ fn main() {
 
     app.run(move |cx| {
         let rpc = rpc::Client::new();
-        let user_store = Arc::new(UserStore::new(rpc.clone()));
+        let http = http::client();
+        let user_store = UserStore::new(rpc.clone(), http.clone(), cx.background());
         let app_state = Arc::new(AppState {
             languages: languages.clone(),
             settings_tx: Arc::new(Mutex::new(settings_tx)),
             settings,
             themes,
-            channel_list: cx.add_model(|cx| ChannelList::new(user_store, rpc.clone(), cx)),
+            channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)),
             rpc,
+            user_store,
             fs: Arc::new(RealFs),
         });
 
@@ -88,6 +90,7 @@ fn init_logger() {
             .expect("could not open logfile");
         simplelog::WriteLogger::init(level, simplelog::Config::default(), log_file)
             .expect("could not initialize logger");
+        log_panics::init();
     }
 }
 

zed/src/rpc.rs 🔗

@@ -1,6 +1,10 @@
 use crate::util::ResultExt;
 use anyhow::{anyhow, Context, Result};
-use async_tungstenite::tungstenite::http::Request;
+use async_recursion::async_recursion;
+use async_tungstenite::tungstenite::{
+    error::Error as WebsocketError,
+    http::{Request, StatusCode},
+};
 use gpui::{AsyncAppContext, Entity, ModelContext, Task};
 use lazy_static::lazy_static;
 use parking_lot::RwLock;
@@ -15,10 +19,11 @@ use std::{
     time::{Duration, Instant},
 };
 use surf::Url;
+use thiserror::Error;
 pub use zrpc::{proto, ConnectionId, PeerId, TypedEnvelope};
 use zrpc::{
     proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
-    Conn, Peer, Receipt,
+    Connection, Peer, Receipt,
 };
 
 lazy_static! {
@@ -29,37 +34,65 @@ lazy_static! {
 pub struct Client {
     peer: Arc<Peer>,
     state: RwLock<ClientState>,
-    auth_callback: Option<
-        Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<(u64, String)>>>,
-    >,
-    connect_callback: Option<
-        Box<dyn 'static + Send + Sync + Fn(u64, &str, &AsyncAppContext) -> Task<Result<Conn>>>,
+    authenticate:
+        Option<Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>>>,
+    establish_connection: Option<
+        Box<
+            dyn 'static
+                + Send
+                + Sync
+                + Fn(
+                    &Credentials,
+                    &AsyncAppContext,
+                ) -> Task<Result<Connection, EstablishConnectionError>>,
+        >,
     >,
 }
 
+#[derive(Error, Debug)]
+pub enum EstablishConnectionError {
+    #[error("unauthorized")]
+    Unauthorized,
+    #[error("{0}")]
+    Other(#[from] anyhow::Error),
+    #[error("{0}")]
+    Io(#[from] std::io::Error),
+    #[error("{0}")]
+    Http(#[from] async_tungstenite::tungstenite::http::Error),
+}
+
+impl From<WebsocketError> for EstablishConnectionError {
+    fn from(error: WebsocketError) -> Self {
+        if let WebsocketError::Http(response) = &error {
+            if response.status() == StatusCode::UNAUTHORIZED {
+                return EstablishConnectionError::Unauthorized;
+            }
+        }
+        EstablishConnectionError::Other(error.into())
+    }
+}
+
+impl EstablishConnectionError {
+    pub fn other(error: impl Into<anyhow::Error> + Send + Sync) -> Self {
+        Self::Other(error.into())
+    }
+}
+
 #[derive(Copy, Clone, Debug)]
 pub enum Status {
-    Disconnected,
+    SignedOut,
     Authenticating,
-    Connecting {
-        user_id: u64,
-    },
+    Connecting,
     ConnectionError,
-    Connected {
-        connection_id: ConnectionId,
-        user_id: u64,
-    },
+    Connected { connection_id: ConnectionId },
     ConnectionLost,
     Reauthenticating,
-    Reconnecting {
-        user_id: u64,
-    },
-    ReconnectionError {
-        next_reconnection: Instant,
-    },
+    Reconnecting,
+    ReconnectionError { next_reconnection: Instant },
 }
 
 struct ClientState {
+    credentials: Option<Credentials>,
     status: (watch::Sender<Status>, watch::Receiver<Status>),
     entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
     model_handlers: HashMap<
@@ -70,10 +103,17 @@ struct ClientState {
     heartbeat_interval: Duration,
 }
 
+#[derive(Clone)]
+pub struct Credentials {
+    pub user_id: u64,
+    pub access_token: String,
+}
+
 impl Default for ClientState {
     fn default() -> Self {
         Self {
-            status: watch::channel_with(Status::Disconnected),
+            credentials: None,
+            status: watch::channel_with(Status::SignedOut),
             entity_id_extractors: Default::default(),
             model_handlers: Default::default(),
             _maintain_connection: None,
@@ -107,22 +147,38 @@ impl Client {
         Arc::new(Self {
             peer: Peer::new(),
             state: Default::default(),
-            auth_callback: None,
-            connect_callback: None,
+            authenticate: None,
+            establish_connection: None,
         })
     }
 
     #[cfg(any(test, feature = "test-support"))]
-    pub fn set_login_and_connect_callbacks<Login, Connect>(
-        &mut self,
-        login: Login,
-        connect: Connect,
-    ) where
-        Login: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<(u64, String)>>,
-        Connect: 'static + Send + Sync + Fn(u64, &str, &AsyncAppContext) -> Task<Result<Conn>>,
+    pub fn override_authenticate<F>(&mut self, authenticate: F) -> &mut Self
+    where
+        F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>,
     {
-        self.auth_callback = Some(Box::new(login));
-        self.connect_callback = Some(Box::new(connect));
+        self.authenticate = Some(Box::new(authenticate));
+        self
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn override_establish_connection<F>(&mut self, connect: F) -> &mut Self
+    where
+        F: 'static
+            + Send
+            + Sync
+            + Fn(&Credentials, &AsyncAppContext) -> Task<Result<Connection, EstablishConnectionError>>,
+    {
+        self.establish_connection = Some(Box::new(connect));
+        self
+    }
+
+    pub fn user_id(&self) -> Option<u64> {
+        self.state
+            .read()
+            .credentials
+            .as_ref()
+            .map(|credentials| credentials.user_id)
     }
 
     pub fn status(&self) -> watch::Receiver<Status> {
@@ -167,7 +223,7 @@ impl Client {
                     }
                 }));
             }
-            Status::Disconnected => {
+            Status::SignedOut => {
                 state._maintain_connection.take();
             }
             _ => {}
@@ -227,12 +283,13 @@ impl Client {
         }
     }
 
+    #[async_recursion(?Send)]
     pub async fn authenticate_and_connect(
         self: &Arc<Self>,
         cx: &AsyncAppContext,
     ) -> anyhow::Result<()> {
         let was_disconnected = match *self.status().borrow() {
-            Status::Disconnected => true,
+            Status::SignedOut => true,
             Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
                 false
             }
@@ -249,33 +306,60 @@ impl Client {
             self.set_status(Status::Reauthenticating, cx)
         }
 
-        let (user_id, access_token) = match self.authenticate(&cx).await {
-            Ok(result) => result,
-            Err(err) => {
-                self.set_status(Status::ConnectionError, cx);
-                return Err(err);
-            }
+        let mut read_from_keychain = false;
+        let credentials = self.state.read().credentials.clone();
+        let credentials = if let Some(credentials) = credentials {
+            credentials
+        } else if let Some(credentials) = read_credentials_from_keychain(cx) {
+            read_from_keychain = true;
+            credentials
+        } else {
+            let credentials = match self.authenticate(&cx).await {
+                Ok(credentials) => credentials,
+                Err(err) => {
+                    self.set_status(Status::ConnectionError, cx);
+                    return Err(err);
+                }
+            };
+            credentials
         };
 
         if was_disconnected {
-            self.set_status(Status::Connecting { user_id }, cx);
+            self.set_status(Status::Connecting, cx);
         } else {
-            self.set_status(Status::Reconnecting { user_id }, cx);
+            self.set_status(Status::Reconnecting, cx);
         }
-        match self.connect(user_id, &access_token, cx).await {
+
+        match self.establish_connection(&credentials, cx).await {
             Ok(conn) => {
                 log::info!("connected to rpc address {}", *ZED_SERVER_URL);
-                self.set_connection(user_id, conn, cx).await;
+                self.state.write().credentials = Some(credentials.clone());
+                if !read_from_keychain {
+                    write_credentials_to_keychain(&credentials, cx).log_err();
+                }
+                self.set_connection(conn, cx).await;
                 Ok(())
             }
             Err(err) => {
-                self.set_status(Status::ConnectionError, cx);
-                Err(err)
+                if matches!(err, EstablishConnectionError::Unauthorized) {
+                    self.state.write().credentials.take();
+                    cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
+                    if read_from_keychain {
+                        self.set_status(Status::SignedOut, cx);
+                        self.authenticate_and_connect(cx).await
+                    } else {
+                        self.set_status(Status::ConnectionError, cx);
+                        Err(err)?
+                    }
+                } else {
+                    self.set_status(Status::ConnectionError, cx);
+                    Err(err)?
+                }
             }
         }
     }
 
-    async fn set_connection(self: &Arc<Self>, user_id: u64, conn: Conn, cx: &AsyncAppContext) {
+    async fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
         let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await;
         cx.foreground()
             .spawn({
@@ -287,11 +371,11 @@ impl Client {
                         if let Some(extract_entity_id) =
                             state.entity_id_extractors.get(&message.payload_type_id())
                         {
+                            let payload_type_id = message.payload_type_id();
                             let entity_id = (extract_entity_id)(message.as_ref());
-                            if let Some(handler) = state
-                                .model_handlers
-                                .get_mut(&(message.payload_type_id(), entity_id))
-                            {
+                            let handler_key = (payload_type_id, entity_id);
+                            if let Some(mut handler) = state.model_handlers.remove(&handler_key) {
+                                drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
                                 let start_time = Instant::now();
                                 log::info!("RPC client message {}", message.payload_type_name());
                                 (handler)(message, &mut cx);
@@ -299,6 +383,10 @@ impl Client {
                                     "RPC message handled. duration:{:?}",
                                     start_time.elapsed()
                                 );
+                                this.state
+                                    .write()
+                                    .model_handlers
+                                    .insert(handler_key, handler);
                             } else {
                                 log::info!("unhandled message {}", message.payload_type_name());
                             }
@@ -310,13 +398,7 @@ impl Client {
             })
             .detach();
 
-        self.set_status(
-            Status::Connected {
-                connection_id,
-                user_id,
-            },
-            cx,
-        );
+        self.set_status(Status::Connected { connection_id }, cx);
 
         let handle_io = cx.background().spawn(handle_io);
         let this = self.clone();
@@ -324,7 +406,7 @@ impl Client {
         cx.foreground()
             .spawn(async move {
                 match handle_io.await {
-                    Ok(()) => this.set_status(Status::Disconnected, &cx),
+                    Ok(()) => this.set_status(Status::SignedOut, &cx),
                     Err(err) => {
                         log::error!("connection error: {:?}", err);
                         this.set_status(Status::ConnectionLost, &cx);
@@ -334,52 +416,49 @@ impl Client {
             .detach();
     }
 
-    fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<(u64, String)>> {
-        if let Some(callback) = self.auth_callback.as_ref() {
+    fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<Credentials>> {
+        if let Some(callback) = self.authenticate.as_ref() {
             callback(cx)
         } else {
             self.authenticate_with_browser(cx)
         }
     }
 
-    fn connect(
+    fn establish_connection(
         self: &Arc<Self>,
-        user_id: u64,
-        access_token: &str,
+        credentials: &Credentials,
         cx: &AsyncAppContext,
-    ) -> Task<Result<Conn>> {
-        if let Some(callback) = self.connect_callback.as_ref() {
-            callback(user_id, access_token, cx)
+    ) -> Task<Result<Connection, EstablishConnectionError>> {
+        if let Some(callback) = self.establish_connection.as_ref() {
+            callback(credentials, cx)
         } else {
-            self.connect_with_websocket(user_id, access_token, cx)
+            self.establish_websocket_connection(credentials, cx)
         }
     }
 
-    fn connect_with_websocket(
+    fn establish_websocket_connection(
         self: &Arc<Self>,
-        user_id: u64,
-        access_token: &str,
+        credentials: &Credentials,
         cx: &AsyncAppContext,
-    ) -> Task<Result<Conn>> {
-        let request =
-            Request::builder().header("Authorization", format!("{} {}", user_id, access_token));
+    ) -> Task<Result<Connection, EstablishConnectionError>> {
+        let request = Request::builder().header(
+            "Authorization",
+            format!("{} {}", credentials.user_id, credentials.access_token),
+        );
         cx.background().spawn(async move {
             if let Some(host) = ZED_SERVER_URL.strip_prefix("https://") {
                 let stream = smol::net::TcpStream::connect(host).await?;
                 let request = request.uri(format!("wss://{}/rpc", host)).body(())?;
-                let (stream, _) = async_tungstenite::async_tls::client_async_tls(request, stream)
-                    .await
-                    .context("websocket handshake")?;
-                Ok(Conn::new(stream))
+                let (stream, _) =
+                    async_tungstenite::async_tls::client_async_tls(request, stream).await?;
+                Ok(Connection::new(stream))
             } else if let Some(host) = ZED_SERVER_URL.strip_prefix("http://") {
                 let stream = smol::net::TcpStream::connect(host).await?;
                 let request = request.uri(format!("ws://{}/rpc", host)).body(())?;
-                let (stream, _) = async_tungstenite::client_async(request, stream)
-                    .await
-                    .context("websocket handshake")?;
-                Ok(Conn::new(stream))
+                let (stream, _) = async_tungstenite::client_async(request, stream).await?;
+                Ok(Connection::new(stream))
             } else {
-                Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))
+                Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))?
             }
         })
     }
@@ -387,19 +466,10 @@ impl Client {
     pub fn authenticate_with_browser(
         self: &Arc<Self>,
         cx: &AsyncAppContext,
-    ) -> Task<Result<(u64, String)>> {
+    ) -> Task<Result<Credentials>> {
         let platform = cx.platform();
         let executor = cx.background();
         executor.clone().spawn(async move {
-            if let Some((user_id, access_token)) = platform
-                .read_credentials(&ZED_SERVER_URL)
-                .log_err()
-                .flatten()
-            {
-                log::info!("already signed in. user_id: {}", user_id);
-                return Ok((user_id.parse()?, String::from_utf8(access_token).unwrap()));
-            }
-
             // Generate a pair of asymmetric encryption keys. The public key will be used by the
             // zed server to encrypt the user's access token, so that it can'be intercepted by
             // any other app running on the user's device.
@@ -460,17 +530,18 @@ impl Client {
                 .decrypt_string(&access_token)
                 .context("failed to decrypt access token")?;
             platform.activate(true);
-            platform
-                .write_credentials(&ZED_SERVER_URL, &user_id, access_token.as_bytes())
-                .log_err();
-            Ok((user_id.parse()?, access_token))
+
+            Ok(Credentials {
+                user_id: user_id.parse()?,
+                access_token,
+            })
         })
     }
 
     pub async fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
         let conn_id = self.connection_id()?;
         self.peer.disconnect(conn_id).await;
-        self.set_status(Status::Disconnected, cx);
+        self.set_status(Status::SignedOut, cx);
         Ok(())
     }
 
@@ -499,6 +570,26 @@ impl Client {
     }
 }
 
+fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
+    let (user_id, access_token) = cx
+        .platform()
+        .read_credentials(&ZED_SERVER_URL)
+        .log_err()
+        .flatten()?;
+    Some(Credentials {
+        user_id: user_id.parse().ok()?,
+        access_token: String::from_utf8(access_token).ok()?,
+    })
+}
+
+fn write_credentials_to_keychain(credentials: &Credentials, cx: &AsyncAppContext) -> Result<()> {
+    cx.platform().write_credentials(
+        &ZED_SERVER_URL,
+        &credentials.user_id.to_string(),
+        credentials.access_token.as_bytes(),
+    )
+}
+
 const WORKTREE_URL_PREFIX: &'static str = "zed://worktrees/";
 
 pub fn encode_worktree_url(id: u64, access_token: &str) -> String {
@@ -561,6 +652,7 @@ mod tests {
             status.recv().await,
             Some(Status::Connected { .. })
         ));
+        assert_eq!(server.auth_count(), 1);
 
         server.forbid_connections();
         server.disconnect().await;
@@ -569,6 +661,20 @@ mod tests {
         server.allow_connections();
         cx.foreground().advance_clock(Duration::from_secs(10));
         while !matches!(status.recv().await, Some(Status::Connected { .. })) {}
+        assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting
+
+        server.forbid_connections();
+        server.disconnect().await;
+        while !matches!(status.recv().await, Some(Status::ReconnectionError { .. })) {}
+
+        // Clear cached credentials after authentication fails
+        server.roll_access_token();
+        server.allow_connections();
+        cx.foreground().advance_clock(Duration::from_secs(10));
+        assert_eq!(server.auth_count(), 1);
+        cx.foreground().advance_clock(Duration::from_secs(10));
+        while !matches!(status.recv().await, Some(Status::Connected { .. })) {}
+        assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
     }
 
     #[test]

zed/src/test.rs 🔗

@@ -2,28 +2,31 @@ use crate::{
     assets::Assets,
     channel::ChannelList,
     fs::RealFs,
+    http::{HttpClient, Request, Response, ServerResponse},
     language::LanguageRegistry,
-    rpc::{self, Client},
+    rpc::{self, Client, Credentials, EstablishConnectionError},
     settings::{self, ThemeRegistry},
     time::ReplicaId,
     user::UserStore,
     AppState,
 };
 use anyhow::{anyhow, Result};
+use futures::{future::BoxFuture, Future};
 use gpui::{AsyncAppContext, Entity, ModelHandle, MutableAppContext, TestAppContext};
 use parking_lot::Mutex;
 use postage::{mpsc, prelude::Stream as _};
 use smol::channel;
 use std::{
+    fmt,
     marker::PhantomData,
     path::{Path, PathBuf},
     sync::{
-        atomic::{AtomicBool, Ordering::SeqCst},
+        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
         Arc,
     },
 };
 use tempdir::TempDir;
-use zrpc::{proto, Conn, ConnectionId, Peer, Receipt, TypedEnvelope};
+use zrpc::{proto, Connection, ConnectionId, Peer, Receipt, TypedEnvelope};
 
 #[cfg(test)]
 #[ctor::ctor]
@@ -164,14 +167,16 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
     let languages = Arc::new(LanguageRegistry::new());
     let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
     let rpc = rpc::Client::new();
-    let user_store = Arc::new(UserStore::new(rpc.clone()));
+    let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
+    let user_store = UserStore::new(rpc.clone(), http, cx.background());
     Arc::new(AppState {
         settings_tx: Arc::new(Mutex::new(settings_tx)),
         settings,
         themes,
         languages: languages.clone(),
-        channel_list: cx.add_model(|cx| ChannelList::new(user_store, rpc.clone(), cx)),
+        channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)),
         rpc,
+        user_store,
         fs: Arc::new(RealFs),
     })
 }
@@ -204,6 +209,9 @@ pub struct FakeServer {
     incoming: Mutex<Option<mpsc::Receiver<Box<dyn proto::AnyTypedEnvelope>>>>,
     connection_id: Mutex<Option<ConnectionId>>,
     forbid_connections: AtomicBool,
+    auth_count: AtomicUsize,
+    access_token: AtomicUsize,
+    user_id: u64,
 }
 
 impl FakeServer {
@@ -212,40 +220,47 @@ impl FakeServer {
         client: &mut Arc<Client>,
         cx: &TestAppContext,
     ) -> Arc<Self> {
-        let result = Arc::new(Self {
+        let server = Arc::new(Self {
             peer: Peer::new(),
             incoming: Default::default(),
             connection_id: Default::default(),
             forbid_connections: Default::default(),
+            auth_count: Default::default(),
+            access_token: Default::default(),
+            user_id: client_user_id,
         });
 
         Arc::get_mut(client)
             .unwrap()
-            .set_login_and_connect_callbacks(
+            .override_authenticate({
+                let server = server.clone();
                 move |cx| {
-                    cx.spawn(|_| async move {
-                        let access_token = "the-token".to_string();
-                        Ok((client_user_id, access_token))
-                    })
-                },
-                {
-                    let server = result.clone();
-                    move |user_id, access_token, cx| {
-                        assert_eq!(user_id, client_user_id);
-                        assert_eq!(access_token, "the-token");
-                        cx.spawn({
-                            let server = server.clone();
-                            move |cx| async move { server.connect(&cx).await }
+                    server.auth_count.fetch_add(1, SeqCst);
+                    let access_token = server.access_token.load(SeqCst).to_string();
+                    cx.spawn(move |_| async move {
+                        Ok(Credentials {
+                            user_id: client_user_id,
+                            access_token,
                         })
-                    }
-                },
-            );
+                    })
+                }
+            })
+            .override_establish_connection({
+                let server = server.clone();
+                move |credentials, cx| {
+                    let credentials = credentials.clone();
+                    cx.spawn({
+                        let server = server.clone();
+                        move |cx| async move { server.establish_connection(&credentials, &cx).await }
+                    })
+                }
+            });
 
         client
             .authenticate_and_connect(&cx.to_async())
             .await
             .unwrap();
-        result
+        server
     }
 
     pub async fn disconnect(&self) {
@@ -254,17 +269,37 @@ impl FakeServer {
         self.incoming.lock().take();
     }
 
-    async fn connect(&self, cx: &AsyncAppContext) -> Result<Conn> {
+    async fn establish_connection(
+        &self,
+        credentials: &Credentials,
+        cx: &AsyncAppContext,
+    ) -> Result<Connection, EstablishConnectionError> {
+        assert_eq!(credentials.user_id, self.user_id);
+
         if self.forbid_connections.load(SeqCst) {
-            Err(anyhow!("server is forbidding connections"))
-        } else {
-            let (client_conn, server_conn, _) = Conn::in_memory();
-            let (connection_id, io, incoming) = self.peer.add_connection(server_conn).await;
-            cx.background().spawn(io).detach();
-            *self.incoming.lock() = Some(incoming);
-            *self.connection_id.lock() = Some(connection_id);
-            Ok(client_conn)
+            Err(EstablishConnectionError::Other(anyhow!(
+                "server is forbidding connections"
+            )))?
+        }
+
+        if credentials.access_token != self.access_token.load(SeqCst).to_string() {
+            Err(EstablishConnectionError::Unauthorized)?
         }
+
+        let (client_conn, server_conn, _) = Connection::in_memory();
+        let (connection_id, io, incoming) = self.peer.add_connection(server_conn).await;
+        cx.background().spawn(io).detach();
+        *self.incoming.lock() = Some(incoming);
+        *self.connection_id.lock() = Some(connection_id);
+        Ok(client_conn)
+    }
+
+    pub fn auth_count(&self) -> usize {
+        self.auth_count.load(SeqCst)
+    }
+
+    pub fn roll_access_token(&self) {
+        self.access_token.fetch_add(1, SeqCst);
     }
 
     pub fn forbid_connections(&self) {
@@ -312,3 +347,33 @@ impl FakeServer {
         self.connection_id.lock().expect("not connected")
     }
 }
+
+pub struct FakeHttpClient {
+    handler:
+        Box<dyn 'static + Send + Sync + Fn(Request) -> BoxFuture<'static, Result<ServerResponse>>>,
+}
+
+impl FakeHttpClient {
+    pub fn new<Fut, F>(handler: F) -> Arc<dyn HttpClient>
+    where
+        Fut: 'static + Send + Future<Output = Result<ServerResponse>>,
+        F: 'static + Send + Sync + Fn(Request) -> Fut,
+    {
+        Arc::new(Self {
+            handler: Box::new(move |req| Box::pin(handler(req))),
+        })
+    }
+}
+
+impl fmt::Debug for FakeHttpClient {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("FakeHttpClient").finish()
+    }
+}
+
+impl HttpClient for FakeHttpClient {
+    fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
+        let future = (self.handler)(req);
+        Box::pin(async move { future.await.map(Into::into) })
+    }
+}

zed/src/theme.rs 🔗

@@ -1,10 +1,12 @@
 mod highlight_map;
+mod resolution;
 mod theme_registry;
 
+use crate::editor::{EditorStyle, SelectionStyle};
 use anyhow::Result;
 use gpui::{
     color::Color,
-    elements::{ContainerStyle, LabelStyle},
+    elements::{ContainerStyle, ImageStyle, LabelStyle},
     fonts::{HighlightStyle, TextStyle},
     Border,
 };
@@ -34,7 +36,7 @@ pub struct SyntaxTheme {
 #[derive(Deserialize)]
 pub struct Workspace {
     pub background: Color,
-    pub titlebar: ContainedLabel,
+    pub titlebar: Titlebar,
     pub tab: Tab,
     pub active_tab: Tab,
     pub pane_divider: Border,
@@ -42,6 +44,24 @@ pub struct Workspace {
     pub right_sidebar: Sidebar,
 }
 
+#[derive(Clone, Deserialize)]
+pub struct Titlebar {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
+    pub title: TextStyle,
+    pub avatar_width: f32,
+    pub offline_icon: OfflineIcon,
+    pub icon_color: Color,
+    pub avatar: ImageStyle,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct OfflineIcon {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
+    pub width: f32,
+}
+
 #[derive(Clone, Deserialize)]
 pub struct Tab {
     #[serde(flatten)]
@@ -60,6 +80,7 @@ pub struct Tab {
 pub struct Sidebar {
     #[serde(flatten)]
     pub container: ContainerStyle,
+    pub width: f32,
     pub icon: SidebarIcon,
     pub active_icon: SidebarIcon,
     pub resize_handle: ContainerStyle,
@@ -76,6 +97,7 @@ pub struct ChatPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub message: ChatMessage,
+    pub pending_message: ChatMessage,
     pub channel_select: ChannelSelect,
     pub input_editor: InputEditorStyle,
     pub sign_in_prompt: TextStyle,
@@ -137,35 +159,16 @@ pub struct ContainedLabel {
     pub label: LabelStyle,
 }
 
-#[derive(Clone, Deserialize)]
-pub struct EditorStyle {
-    pub text: HighlightStyle,
-    #[serde(default)]
-    pub placeholder_text: HighlightStyle,
-    pub background: Color,
-    pub selection: SelectionStyle,
-    pub gutter_background: Color,
-    pub active_line_background: Color,
-    pub line_number: Color,
-    pub line_number_active: Color,
-    pub guest_selections: Vec<SelectionStyle>,
-}
-
 #[derive(Clone, Deserialize)]
 pub struct InputEditorStyle {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub text: HighlightStyle,
-    pub placeholder_text: HighlightStyle,
+    pub text: TextStyle,
+    #[serde(default)]
+    pub placeholder_text: Option<TextStyle>,
     pub selection: SelectionStyle,
 }
 
-#[derive(Clone, Copy, Default, Deserialize)]
-pub struct SelectionStyle {
-    pub cursor: Color,
-    pub selection: Color,
-}
-
 impl SyntaxTheme {
     pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
         Self { highlights }
@@ -183,30 +186,6 @@ impl SyntaxTheme {
     }
 }
 
-impl Default for EditorStyle {
-    fn default() -> Self {
-        Self {
-            text: HighlightStyle {
-                color: Color::from_u32(0xff0000ff),
-                font_properties: Default::default(),
-                underline: false,
-            },
-            placeholder_text: HighlightStyle {
-                color: Color::from_u32(0x00ff00ff),
-                font_properties: Default::default(),
-                underline: false,
-            },
-            background: Default::default(),
-            gutter_background: Default::default(),
-            active_line_background: Default::default(),
-            line_number: Default::default(),
-            line_number_active: Default::default(),
-            selection: Default::default(),
-            guest_selections: Default::default(),
-        }
-    }
-}
-
 impl InputEditorStyle {
     pub fn as_editor(&self) -> EditorStyle {
         EditorStyle {
@@ -217,7 +196,11 @@ impl InputEditorStyle {
                 .background_color
                 .unwrap_or(Color::transparent_black()),
             selection: self.selection,
-            ..Default::default()
+            gutter_background: Default::default(),
+            active_line_background: Default::default(),
+            line_number: Default::default(),
+            line_number_active: Default::default(),
+            guest_selections: Default::default(),
         }
     }
 }

zed/src/theme/resolution.rs 🔗

@@ -0,0 +1,476 @@
+use anyhow::{anyhow, Result};
+use indexmap::IndexMap;
+use serde_json::Value;
+use std::{
+    cell::RefCell,
+    mem,
+    rc::{Rc, Weak},
+};
+
+pub fn resolve_references(value: Value) -> Result<Value> {
+    let tree = Tree::from_json(value)?;
+    tree.resolve()?;
+    tree.to_json()
+}
+
+#[derive(Clone)]
+enum Node {
+    Reference {
+        path: String,
+        parent: Option<Weak<RefCell<Node>>>,
+    },
+    Object {
+        base: Option<String>,
+        children: IndexMap<String, Tree>,
+        resolved: bool,
+        parent: Option<Weak<RefCell<Node>>>,
+    },
+    Array {
+        children: Vec<Tree>,
+        resolved: bool,
+        parent: Option<Weak<RefCell<Node>>>,
+    },
+    String {
+        value: String,
+        parent: Option<Weak<RefCell<Node>>>,
+    },
+    Number {
+        value: serde_json::Number,
+        parent: Option<Weak<RefCell<Node>>>,
+    },
+    Bool {
+        value: bool,
+        parent: Option<Weak<RefCell<Node>>>,
+    },
+    Null {
+        parent: Option<Weak<RefCell<Node>>>,
+    },
+}
+
+#[derive(Clone)]
+struct Tree(Rc<RefCell<Node>>);
+
+impl Tree {
+    pub fn new(node: Node) -> Self {
+        Self(Rc::new(RefCell::new(node)))
+    }
+
+    fn from_json(value: Value) -> Result<Self> {
+        match value {
+            Value::String(value) => {
+                if let Some(path) = value.strip_prefix("$") {
+                    Ok(Self::new(Node::Reference {
+                        path: path.to_string(),
+                        parent: None,
+                    }))
+                } else {
+                    Ok(Self::new(Node::String {
+                        value,
+                        parent: None,
+                    }))
+                }
+            }
+            Value::Number(value) => Ok(Self::new(Node::Number {
+                value,
+                parent: None,
+            })),
+            Value::Bool(value) => Ok(Self::new(Node::Bool {
+                value,
+                parent: None,
+            })),
+            Value::Null => Ok(Self::new(Node::Null { parent: None })),
+            Value::Object(object) => {
+                let tree = Self::new(Node::Object {
+                    base: Default::default(),
+                    children: Default::default(),
+                    resolved: false,
+                    parent: None,
+                });
+                let mut children = IndexMap::new();
+                let mut resolved = true;
+                let mut base = None;
+                for (key, value) in object.into_iter() {
+                    let value = if key == "extends" {
+                        if value.is_string() {
+                            if let Value::String(value) = value {
+                                base = value.strip_prefix("$").map(str::to_string);
+                                resolved = false;
+                                Self::new(Node::String {
+                                    value,
+                                    parent: None,
+                                })
+                            } else {
+                                unreachable!()
+                            }
+                        } else {
+                            Tree::from_json(value)?
+                        }
+                    } else {
+                        Tree::from_json(value)?
+                    };
+                    value
+                        .0
+                        .borrow_mut()
+                        .set_parent(Some(Rc::downgrade(&tree.0)));
+                    resolved &= value.is_resolved();
+                    children.insert(key.clone(), value);
+                }
+
+                *tree.0.borrow_mut() = Node::Object {
+                    base,
+                    children,
+                    resolved,
+                    parent: None,
+                };
+                Ok(tree)
+            }
+            Value::Array(elements) => {
+                let tree = Self::new(Node::Array {
+                    children: Default::default(),
+                    resolved: false,
+                    parent: None,
+                });
+
+                let mut children = Vec::new();
+                let mut resolved = true;
+                for element in elements {
+                    let child = Tree::from_json(element)?;
+                    child
+                        .0
+                        .borrow_mut()
+                        .set_parent(Some(Rc::downgrade(&tree.0)));
+                    resolved &= child.is_resolved();
+                    children.push(child);
+                }
+
+                *tree.0.borrow_mut() = Node::Array {
+                    children,
+                    resolved,
+                    parent: None,
+                };
+                Ok(tree)
+            }
+        }
+    }
+
+    fn to_json(&self) -> Result<Value> {
+        match &*self.0.borrow() {
+            Node::Reference { .. } => Err(anyhow!("unresolved tree")),
+            Node::String { value, .. } => Ok(Value::String(value.clone())),
+            Node::Number { value, .. } => Ok(Value::Number(value.clone())),
+            Node::Bool { value, .. } => Ok(Value::Bool(*value)),
+            Node::Null { .. } => Ok(Value::Null),
+            Node::Object { children, .. } => {
+                let mut json_children = serde_json::Map::new();
+                for (key, value) in children {
+                    json_children.insert(key.clone(), value.to_json()?);
+                }
+                Ok(Value::Object(json_children))
+            }
+            Node::Array { children, .. } => {
+                let mut json_children = Vec::new();
+                for child in children {
+                    json_children.push(child.to_json()?);
+                }
+                Ok(Value::Array(json_children))
+            }
+        }
+    }
+
+    fn parent(&self) -> Option<Tree> {
+        match &*self.0.borrow() {
+            Node::Reference { parent, .. }
+            | Node::Object { parent, .. }
+            | Node::Array { parent, .. }
+            | Node::String { parent, .. }
+            | Node::Number { parent, .. }
+            | Node::Bool { parent, .. }
+            | Node::Null { parent } => parent.as_ref().and_then(|p| p.upgrade()).map(Tree),
+        }
+    }
+
+    fn get(&self, path: &str) -> Result<Option<Tree>> {
+        let mut tree = self.clone();
+        for component in path.split('.') {
+            let node = tree.0.borrow();
+            match &*node {
+                Node::Object { children, .. } => {
+                    if let Some(subtree) = children.get(component).cloned() {
+                        drop(node);
+                        tree = subtree;
+                    } else {
+                        return Err(anyhow!(
+                            "key \"{}\" does not exist in path \"{}\"",
+                            component,
+                            path
+                        ));
+                    }
+                }
+                Node::Reference { .. } => return Ok(None),
+                Node::Array { .. }
+                | Node::String { .. }
+                | Node::Number { .. }
+                | Node::Bool { .. }
+                | Node::Null { .. } => {
+                    return Err(anyhow!(
+                        "key \"{}\" in path \"{}\" is not an object",
+                        component,
+                        path
+                    ))
+                }
+            }
+        }
+
+        Ok(Some(tree))
+    }
+
+    fn is_resolved(&self) -> bool {
+        match &*self.0.borrow() {
+            Node::Reference { .. } => false,
+            Node::Object { resolved, .. } | Node::Array { resolved, .. } => *resolved,
+            Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
+                true
+            }
+        }
+    }
+
+    fn update_resolved(&self) {
+        match &mut *self.0.borrow_mut() {
+            Node::Object {
+                resolved, children, ..
+            } => {
+                *resolved = children.values().all(|c| c.is_resolved());
+            }
+            Node::Array {
+                resolved, children, ..
+            } => {
+                *resolved = children.iter().all(|c| c.is_resolved());
+            }
+            _ => {}
+        }
+    }
+
+    pub fn resolve(&self) -> Result<()> {
+        let mut unresolved = vec![self.clone()];
+        let mut made_progress = true;
+
+        while made_progress && !unresolved.is_empty() {
+            made_progress = false;
+            for mut tree in mem::take(&mut unresolved) {
+                made_progress |= tree.resolve_subtree(self, &mut unresolved)?;
+                if tree.is_resolved() {
+                    while let Some(parent) = tree.parent() {
+                        parent.update_resolved();
+                        tree = parent;
+                    }
+                }
+            }
+        }
+
+        if unresolved.is_empty() {
+            Ok(())
+        } else {
+            Err(anyhow!("tree contains cycles"))
+        }
+    }
+
+    fn resolve_subtree(&self, root: &Tree, unresolved: &mut Vec<Tree>) -> Result<bool> {
+        let node = self.0.borrow();
+        match &*node {
+            Node::Reference { path, parent } => {
+                if let Some(subtree) = root.get(&path)? {
+                    if subtree.is_resolved() {
+                        let parent = parent.clone();
+                        drop(node);
+                        let mut new_node = subtree.0.borrow().clone();
+                        new_node.set_parent(parent);
+                        *self.0.borrow_mut() = new_node;
+                        Ok(true)
+                    } else {
+                        unresolved.push(self.clone());
+                        Ok(false)
+                    }
+                } else {
+                    unresolved.push(self.clone());
+                    Ok(false)
+                }
+            }
+            Node::Object {
+                base,
+                children,
+                resolved,
+                ..
+            } => {
+                if *resolved {
+                    Ok(false)
+                } else {
+                    let mut made_progress = false;
+                    let mut children_resolved = true;
+                    for child in children.values() {
+                        made_progress |= child.resolve_subtree(root, unresolved)?;
+                        children_resolved &= child.is_resolved();
+                    }
+
+                    if children_resolved {
+                        let mut has_base = false;
+                        let mut resolved_base = None;
+                        if let Some(base) = base {
+                            has_base = true;
+                            if let Some(base) = root.get(base)? {
+                                if base.is_resolved() {
+                                    resolved_base = Some(base);
+                                }
+                            }
+                        }
+
+                        drop(node);
+
+                        if let Some(base) = resolved_base.as_ref() {
+                            self.extend_from(&base);
+                            made_progress = true;
+                        }
+
+                        if let Node::Object { resolved, .. } = &mut *self.0.borrow_mut() {
+                            if has_base {
+                                if resolved_base.is_some() {
+                                    *resolved = true;
+                                } else {
+                                    unresolved.push(self.clone());
+                                }
+                            } else {
+                                *resolved = true;
+                            }
+                        }
+                    }
+
+                    Ok(made_progress)
+                }
+            }
+            Node::Array {
+                children, resolved, ..
+            } => {
+                if *resolved {
+                    Ok(false)
+                } else {
+                    let mut made_progress = false;
+                    let mut children_resolved = true;
+                    for child in children.iter() {
+                        made_progress |= child.resolve_subtree(root, unresolved)?;
+                        children_resolved &= child.is_resolved();
+                    }
+
+                    if children_resolved {
+                        drop(node);
+
+                        if let Node::Array { resolved, .. } = &mut *self.0.borrow_mut() {
+                            *resolved = true;
+                        }
+                    }
+
+                    Ok(made_progress)
+                }
+            }
+            Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
+                Ok(false)
+            }
+        }
+    }
+
+    fn extend_from(&self, base: &Tree) {
+        if Rc::ptr_eq(&self.0, &base.0) {
+            return;
+        }
+
+        if let (
+            Node::Object { children, .. },
+            Node::Object {
+                children: base_children,
+                ..
+            },
+        ) = (&mut *self.0.borrow_mut(), &*base.0.borrow())
+        {
+            for (key, base_value) in base_children {
+                if let Some(value) = children.get(key) {
+                    value.extend_from(base_value);
+                } else {
+                    let base_value = base_value.clone();
+                    base_value
+                        .0
+                        .borrow_mut()
+                        .set_parent(Some(Rc::downgrade(&self.0)));
+                    children.insert(key.clone(), base_value);
+                }
+            }
+        }
+    }
+}
+
+impl Node {
+    fn set_parent(&mut self, new_parent: Option<Weak<RefCell<Node>>>) {
+        match self {
+            Node::Reference { parent, .. }
+            | Node::Object { parent, .. }
+            | Node::Array { parent, .. }
+            | Node::String { parent, .. }
+            | Node::Number { parent, .. }
+            | Node::Bool { parent, .. }
+            | Node::Null { parent } => *parent = new_parent,
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_references() {
+        let json = serde_json::json!({
+            "a": {
+                "x": "$b.d"
+            },
+            "b": {
+                "c": "$a",
+                "d": "$e.f"
+            },
+            "e": {
+                "extends": "$a",
+                "f": "1"
+            }
+        });
+
+        assert_eq!(
+            resolve_references(json).unwrap(),
+            serde_json::json!({
+                "a": {
+                    "x": "1"
+                },
+                "b": {
+                    "c": {
+                        "x": "1"
+                    },
+                    "d": "1"
+                },
+                "e": {
+                    "extends": "$a",
+                    "f": "1",
+                    "x": "1"
+                },
+            })
+        )
+    }
+
+    #[test]
+    fn test_cycles() {
+        let json = serde_json::json!({
+            "a": {
+                "b": "$c.d"
+            },
+            "c": {
+                "d": "$a.b",
+            },
+        });
+
+        assert!(resolve_references(json).is_err());
+    }
+}

zed/src/theme/theme_registry.rs 🔗

@@ -1,8 +1,9 @@
-use anyhow::{anyhow, Context, Result};
+use super::resolution::resolve_references;
+use anyhow::{Context, Result};
 use gpui::{fonts, AssetSource, FontCache};
 use parking_lot::Mutex;
 use serde_json::{Map, Value};
-use std::{collections::HashMap, fmt, mem, sync::Arc};
+use std::{collections::HashMap, sync::Arc};
 
 use super::Theme;
 
@@ -13,30 +14,6 @@ pub struct ThemeRegistry {
     font_cache: Arc<FontCache>,
 }
 
-#[derive(Default)]
-struct KeyPathReferenceSet {
-    references: Vec<KeyPathReference>,
-    reference_ids_by_source: Vec<usize>,
-    reference_ids_by_target: Vec<usize>,
-    dependencies: Vec<(usize, usize)>,
-    dependency_counts: Vec<usize>,
-}
-
-#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
-struct KeyPathReference {
-    target: KeyPath,
-    source: KeyPath,
-}
-
-#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
-struct KeyPath(Vec<Key>);
-
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-enum Key {
-    Array(usize),
-    Object(String),
-}
-
 impl ThemeRegistry {
     pub fn new(source: impl AssetSource, font_cache: Arc<FontCache>) -> Arc<Self> {
         Arc::new(Self {
@@ -111,41 +88,15 @@ impl ThemeRegistry {
             }
         }
 
+        let mut theme_data = Value::Object(theme_data);
+
         // Find all of the key path references in the object, and then sort them according
         // to their dependencies.
         if evaluate_references {
-            let mut key_path = KeyPath::default();
-            let mut references = KeyPathReferenceSet::default();
-            for (key, value) in theme_data.iter() {
-                key_path.0.push(Key::Object(key.clone()));
-                find_references(value, &mut key_path, &mut references);
-                key_path.0.pop();
-            }
-            let sorted_references = references
-                .top_sort()
-                .map_err(|key_paths| anyhow!("cycle for key paths: {:?}", key_paths))?;
-
-            // Now update objects to include the fields of objects they extend
-            for KeyPathReference { source, target } in sorted_references {
-                if let Some(source) = value_at(&mut theme_data, &source).cloned() {
-                    let target = value_at(&mut theme_data, &target).unwrap();
-                    if let Value::Object(target_object) = target.take() {
-                        if let Value::Object(mut source_object) = source {
-                            deep_merge_json(&mut source_object, target_object);
-                            *target = Value::Object(source_object);
-                        } else {
-                            Err(anyhow!("extended key path {} is not an object", source))?;
-                        }
-                    } else {
-                        *target = source;
-                    }
-                } else {
-                    Err(anyhow!("invalid key path '{}'", source))?;
-                }
-            }
+            theme_data = resolve_references(theme_data)?;
         }
 
-        let result = Arc::new(Value::Object(theme_data));
+        let result = Arc::new(theme_data);
         self.theme_data
             .lock()
             .insert(name.to_string(), result.clone());
@@ -154,293 +105,6 @@ impl ThemeRegistry {
     }
 }
 
-impl KeyPathReferenceSet {
-    fn insert(&mut self, reference: KeyPathReference) {
-        let id = self.references.len();
-        let source_ix = self
-            .reference_ids_by_source
-            .binary_search_by_key(&&reference.source, |id| &self.references[*id].source)
-            .unwrap_or_else(|i| i);
-        let target_ix = self
-            .reference_ids_by_target
-            .binary_search_by_key(&&reference.target, |id| &self.references[*id].target)
-            .unwrap_or_else(|i| i);
-
-        self.populate_dependencies(id, &reference);
-        self.reference_ids_by_source.insert(source_ix, id);
-        self.reference_ids_by_target.insert(target_ix, id);
-        self.references.push(reference);
-    }
-
-    fn top_sort(mut self) -> Result<Vec<KeyPathReference>, Vec<KeyPath>> {
-        let mut results = Vec::with_capacity(self.references.len());
-        let mut root_ids = Vec::with_capacity(self.references.len());
-
-        // Find the initial set of references that have no dependencies.
-        for (id, dep_count) in self.dependency_counts.iter().enumerate() {
-            if *dep_count == 0 {
-                root_ids.push(id);
-            }
-        }
-
-        while results.len() < root_ids.len() {
-            // Just to guarantee a stable result when the inputs are randomized,
-            // sort references lexicographically in absence of any dependency relationship.
-            root_ids[results.len()..].sort_by_key(|id| &self.references[*id]);
-
-            let root_id = root_ids[results.len()];
-            let root = mem::take(&mut self.references[root_id]);
-            results.push(root);
-
-            // Remove this reference as a dependency from any of its dependent references.
-            if let Ok(dep_ix) = self
-                .dependencies
-                .binary_search_by_key(&root_id, |edge| edge.0)
-            {
-                let mut first_dep_ix = dep_ix;
-                let mut last_dep_ix = dep_ix + 1;
-                while first_dep_ix > 0 && self.dependencies[first_dep_ix - 1].0 == root_id {
-                    first_dep_ix -= 1;
-                }
-                while last_dep_ix < self.dependencies.len()
-                    && self.dependencies[last_dep_ix].0 == root_id
-                {
-                    last_dep_ix += 1;
-                }
-
-                // If any reference no longer has any dependencies, then then mark it as a root.
-                // Preserve the references' original order where possible.
-                for (_, successor_id) in self.dependencies.drain(first_dep_ix..last_dep_ix) {
-                    self.dependency_counts[successor_id] -= 1;
-                    if self.dependency_counts[successor_id] == 0 {
-                        root_ids.push(successor_id);
-                    }
-                }
-            }
-        }
-
-        // If any references never became roots, then there are reference cycles
-        // in the set. Return an error containing all of the key paths that are
-        // directly involved in cycles.
-        if results.len() < self.references.len() {
-            let mut cycle_ref_ids = (0..self.references.len())
-                .filter(|id| !root_ids.contains(id))
-                .collect::<Vec<_>>();
-
-            // Iteratively remove any references that have no dependencies,
-            // so that the error will only indicate which key paths are directly
-            // involved in the cycles.
-            let mut done = false;
-            while !done {
-                done = true;
-                cycle_ref_ids.retain(|id| {
-                    if self.dependencies.iter().any(|dep| dep.0 == *id) {
-                        true
-                    } else {
-                        done = false;
-                        self.dependencies.retain(|dep| dep.1 != *id);
-                        false
-                    }
-                });
-            }
-
-            let mut cycle_key_paths = Vec::new();
-            for id in cycle_ref_ids {
-                let reference = &self.references[id];
-                cycle_key_paths.push(reference.target.clone());
-                cycle_key_paths.push(reference.source.clone());
-            }
-            cycle_key_paths.sort_unstable();
-            return Err(cycle_key_paths);
-        }
-
-        Ok(results)
-    }
-
-    fn populate_dependencies(&mut self, new_id: usize, new_reference: &KeyPathReference) {
-        self.dependency_counts.push(0);
-
-        // If an existing reference's source path starts with the new reference's
-        // target path, then insert this new reference before that existing reference.
-        for id in Self::reference_ids_for_key_path(
-            &new_reference.target.0,
-            &self.references,
-            &self.reference_ids_by_source,
-            KeyPathReference::source,
-            KeyPath::starts_with,
-        ) {
-            Self::add_dependency(
-                (new_id, id),
-                &mut self.dependencies,
-                &mut self.dependency_counts,
-            );
-        }
-
-        // If an existing reference's target path starts with the new reference's
-        // source path, then insert this new reference after that existing reference.
-        for id in Self::reference_ids_for_key_path(
-            &new_reference.source.0,
-            &self.references,
-            &self.reference_ids_by_target,
-            KeyPathReference::target,
-            KeyPath::starts_with,
-        ) {
-            Self::add_dependency(
-                (id, new_id),
-                &mut self.dependencies,
-                &mut self.dependency_counts,
-            );
-        }
-
-        // If an existing reference's source path is a prefix of the new reference's
-        // target path, then insert this new reference before that existing reference.
-        for prefix in new_reference.target.prefixes() {
-            for id in Self::reference_ids_for_key_path(
-                prefix,
-                &self.references,
-                &self.reference_ids_by_source,
-                KeyPathReference::source,
-                PartialEq::eq,
-            ) {
-                Self::add_dependency(
-                    (new_id, id),
-                    &mut self.dependencies,
-                    &mut self.dependency_counts,
-                );
-            }
-        }
-
-        // If an existing reference's target path is a prefix of the new reference's
-        // source path, then insert this new reference after that existing reference.
-        for prefix in new_reference.source.prefixes() {
-            for id in Self::reference_ids_for_key_path(
-                prefix,
-                &self.references,
-                &self.reference_ids_by_target,
-                KeyPathReference::target,
-                PartialEq::eq,
-            ) {
-                Self::add_dependency(
-                    (id, new_id),
-                    &mut self.dependencies,
-                    &mut self.dependency_counts,
-                );
-            }
-        }
-    }
-
-    // Find all existing references that satisfy a given predicate with respect
-    // to a given key path. Use a sorted array of reference ids in order to avoid
-    // performing unnecessary comparisons.
-    fn reference_ids_for_key_path<'a>(
-        key_path: &[Key],
-        references: &[KeyPathReference],
-        sorted_reference_ids: &'a [usize],
-        reference_attribute: impl Fn(&KeyPathReference) -> &KeyPath,
-        predicate: impl Fn(&KeyPath, &[Key]) -> bool,
-    ) -> impl Iterator<Item = usize> + 'a {
-        let ix = sorted_reference_ids
-            .binary_search_by_key(&key_path, |id| &reference_attribute(&references[*id]).0)
-            .unwrap_or_else(|i| i);
-
-        let mut start_ix = ix;
-        while start_ix > 0 {
-            let reference_id = sorted_reference_ids[start_ix - 1];
-            let reference = &references[reference_id];
-            if !predicate(&reference_attribute(reference), key_path) {
-                break;
-            }
-            start_ix -= 1;
-        }
-
-        let mut end_ix = ix;
-        while end_ix < sorted_reference_ids.len() {
-            let reference_id = sorted_reference_ids[end_ix];
-            let reference = &references[reference_id];
-            if !predicate(&reference_attribute(reference), key_path) {
-                break;
-            }
-            end_ix += 1;
-        }
-
-        sorted_reference_ids[start_ix..end_ix].iter().copied()
-    }
-
-    fn add_dependency(
-        (predecessor, successor): (usize, usize),
-        dependencies: &mut Vec<(usize, usize)>,
-        dependency_counts: &mut Vec<usize>,
-    ) {
-        let dependency = (predecessor, successor);
-        if let Err(i) = dependencies.binary_search(&dependency) {
-            dependencies.insert(i, dependency);
-        }
-        dependency_counts[successor] += 1;
-    }
-}
-
-impl KeyPathReference {
-    fn source(&self) -> &KeyPath {
-        &self.source
-    }
-
-    fn target(&self) -> &KeyPath {
-        &self.target
-    }
-}
-
-impl KeyPath {
-    fn new(string: &str) -> Self {
-        Self(
-            string
-                .split(".")
-                .map(|key| Key::Object(key.to_string()))
-                .collect(),
-        )
-    }
-
-    fn starts_with(&self, other: &[Key]) -> bool {
-        self.0.starts_with(&other)
-    }
-
-    fn prefixes(&self) -> impl Iterator<Item = &[Key]> {
-        (1..self.0.len()).map(move |end_ix| &self.0[0..end_ix])
-    }
-}
-
-impl PartialEq<[Key]> for KeyPath {
-    fn eq(&self, other: &[Key]) -> bool {
-        self.0.eq(other)
-    }
-}
-
-impl fmt::Debug for KeyPathReference {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "KeyPathReference {{ {} <- {} }}",
-            self.target, self.source
-        )
-    }
-}
-
-impl fmt::Display for KeyPath {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        for (i, key) in self.0.iter().enumerate() {
-            match key {
-                Key::Array(index) => write!(f, "[{}]", index)?,
-                Key::Object(key) => {
-                    if i > 0 {
-                        ".".fmt(f)?;
-                    }
-                    key.fmt(f)?;
-                }
-            }
-        }
-        Ok(())
-    }
-}
-
 fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>) {
     for (key, extension_value) in extension {
         if let Value::Object(extension_object) = extension_value {
@@ -455,69 +119,12 @@ fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>)
     }
 }
 
-fn find_references(value: &Value, key_path: &mut KeyPath, references: &mut KeyPathReferenceSet) {
-    match value {
-        Value::Array(vec) => {
-            for (ix, value) in vec.iter().enumerate() {
-                key_path.0.push(Key::Array(ix));
-                find_references(value, key_path, references);
-                key_path.0.pop();
-            }
-        }
-        Value::Object(map) => {
-            for (key, value) in map.iter() {
-                if key == "extends" {
-                    if let Some(source_path) = value.as_str().and_then(|s| s.strip_prefix("$")) {
-                        references.insert(KeyPathReference {
-                            source: KeyPath::new(source_path),
-                            target: key_path.clone(),
-                        });
-                    }
-                } else {
-                    key_path.0.push(Key::Object(key.to_string()));
-                    find_references(value, key_path, references);
-                    key_path.0.pop();
-                }
-            }
-        }
-        Value::String(string) => {
-            if let Some(source_path) = string.strip_prefix("$") {
-                references.insert(KeyPathReference {
-                    source: KeyPath::new(source_path),
-                    target: key_path.clone(),
-                });
-            }
-        }
-        _ => {}
-    }
-}
-
-fn value_at<'a>(object: &'a mut Map<String, Value>, key_path: &KeyPath) -> Option<&'a mut Value> {
-    let mut key_path = key_path.0.iter();
-    if let Some(Key::Object(first_key)) = key_path.next() {
-        let mut cur_value = object.get_mut(first_key);
-        for key in key_path {
-            if let Some(value) = cur_value {
-                match key {
-                    Key::Array(ix) => cur_value = value.get_mut(ix),
-                    Key::Object(key) => cur_value = value.get_mut(key),
-                }
-            } else {
-                return None;
-            }
-        }
-        cur_value
-    } else {
-        None
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
     use crate::{test::test_app_state, theme::DEFAULT_THEME_NAME};
+    use anyhow::anyhow;
     use gpui::MutableAppContext;
-    use rand::{prelude::StdRng, Rng};
 
     #[gpui::test]
     fn test_bundled_themes(cx: &mut MutableAppContext) {
@@ -575,6 +182,7 @@ mod tests {
 
         let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
         let theme_data = registry.load("light", true).unwrap();
+
         assert_eq!(
             theme_data.as_ref(),
             &serde_json::json!({
@@ -619,120 +227,38 @@ mod tests {
         );
     }
 
-    #[test]
-    fn test_key_path_reference_set_simple() {
-        let input_references = build_refs(&[
-            ("r", "a"),
-            ("a.b.c", "d"),
-            ("d.e", "f"),
-            ("t.u", "v"),
-            ("v.w", "x"),
-            ("v.y", "x"),
-            ("d.h", "i"),
-            ("v.z", "x"),
-            ("f.g", "d.h"),
-        ]);
-        let expected_references = build_refs(&[
-            ("d.h", "i"),
-            ("f.g", "d.h"),
-            ("d.e", "f"),
-            ("a.b.c", "d"),
-            ("r", "a"),
-            ("v.w", "x"),
-            ("v.y", "x"),
-            ("v.z", "x"),
-            ("t.u", "v"),
-        ])
-        .collect::<Vec<_>>();
-
-        let mut reference_set = KeyPathReferenceSet::default();
-        for reference in input_references {
-            reference_set.insert(reference);
-        }
-        assert_eq!(reference_set.top_sort().unwrap(), expected_references);
-    }
-
-    #[test]
-    fn test_key_path_reference_set_with_cycles() {
-        let input_references = build_refs(&[
-            ("x", "a.b"),
-            ("y", "x.c"),
-            ("a.b.c", "d.e"),
-            ("d.e.f", "g.h"),
-            ("g.h.i", "a"),
-        ]);
-
-        let mut reference_set = KeyPathReferenceSet::default();
-        for reference in input_references {
-            reference_set.insert(reference);
-        }
+    #[gpui::test]
+    fn test_nested_extension(cx: &mut MutableAppContext) {
+        let assets = TestAssets(&[(
+            "themes/theme.toml",
+            r##"
+                [a]
+                text = { extends = "$text.0" }
+
+                [b]
+                extends = "$a"
+                text = { extends = "$text.1" }
+
+                [text]
+                0 = { color = "red" }
+                1 = { color = "blue" }
+            "##,
+        )]);
 
+        let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
+        let theme_data = registry.load("theme", true).unwrap();
         assert_eq!(
-            reference_set.top_sort().unwrap_err(),
-            &[
-                KeyPath::new("a"),
-                KeyPath::new("a.b.c"),
-                KeyPath::new("d.e"),
-                KeyPath::new("d.e.f"),
-                KeyPath::new("g.h"),
-                KeyPath::new("g.h.i"),
-            ]
+            theme_data
+                .get("b")
+                .unwrap()
+                .get("text")
+                .unwrap()
+                .get("color")
+                .unwrap(),
+            "blue"
         );
     }
 
-    #[gpui::test(iterations = 20)]
-    async fn test_key_path_reference_set_random(mut rng: StdRng) {
-        let examples: &[&[_]] = &[
-            &[
-                ("n.d.h", "i"),
-                ("f.g", "n.d.h"),
-                ("n.d.e", "f"),
-                ("a.b.c", "n.d"),
-                ("r", "a"),
-                ("q.q.q", "r.s"),
-                ("r.t", "q"),
-                ("x.x", "r.r"),
-                ("v.w", "x"),
-                ("v.y", "x"),
-                ("v.z", "x"),
-                ("t.u", "v"),
-            ],
-            &[
-                ("w.x.y.z", "t.u.z"),
-                ("x", "w.x"),
-                ("a.b.c1", "x.b1.c"),
-                ("a.b.c2", "x.b2.c"),
-            ],
-            &[
-                ("x.y", "m.n.n.o.q"),
-                ("x.y.z", "m.n.n.o.p"),
-                ("u.v.w", "x.y.z"),
-                ("a.b.c.d", "u.v"),
-                ("a.b.c.d.e", "u.v"),
-                ("a.b.c.d.f", "u.v"),
-                ("a.b.c.d.g", "u.v"),
-            ],
-        ];
-
-        for example in examples {
-            let expected_references = build_refs(example).collect::<Vec<_>>();
-            let mut input_references = expected_references.clone();
-            input_references.sort_by_key(|_| rng.gen_range(0..1000));
-            let mut reference_set = KeyPathReferenceSet::default();
-            for reference in input_references {
-                reference_set.insert(reference);
-            }
-            assert_eq!(reference_set.top_sort().unwrap(), expected_references);
-        }
-    }
-
-    fn build_refs<'a>(rows: &'a [(&str, &str)]) -> impl Iterator<Item = KeyPathReference> + 'a {
-        rows.iter().map(|(target, source)| KeyPathReference {
-            target: KeyPath::new(target),
-            source: KeyPath::new(source),
-        })
-    }
-
     struct TestAssets(&'static [(&'static str, &'static str)]);
 
     impl AssetSource for TestAssets {

zed/src/theme_selector.rs 🔗

@@ -58,10 +58,14 @@ impl ThemeSelector {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let query_editor = cx.add_view(|cx| {
-            Editor::single_line(settings.clone(), cx).with_style({
-                let settings = settings.clone();
-                move |_| settings.borrow().theme.selector.input_editor.as_editor()
-            })
+            Editor::single_line(
+                settings.clone(),
+                {
+                    let settings = settings.clone();
+                    move |_| settings.borrow().theme.selector.input_editor.as_editor()
+                },
+                cx,
+            )
         });
 
         cx.subscribe(&query_editor, Self::on_query_editor_event)
@@ -214,7 +218,7 @@ impl ThemeSelector {
                 )
                 .boxed(),
             )
-            .with_style(&settings.theme.selector.empty.container)
+            .with_style(settings.theme.selector.empty.container)
             .named("empty matches");
         }
 
@@ -259,9 +263,9 @@ impl ThemeSelector {
             .boxed(),
         )
         .with_style(if index == self.selected_index {
-            &theme.selector.active_item.container
+            theme.selector.active_item.container
         } else {
-            &theme.selector.item.container
+            theme.selector.item.container
         });
 
         container.boxed()
@@ -288,7 +292,7 @@ impl View for ThemeSelector {
                         .with_child(Flexible::new(1.0, self.render_matches(cx)).boxed())
                         .boxed(),
                 )
-                .with_style(&settings.theme.selector.container)
+                .with_style(settings.theme.selector.container)
                 .boxed(),
             )
             .with_max_width(600.0)

zed/src/user.rs 🔗

@@ -1,22 +1,77 @@
-use crate::rpc::Client;
-use anyhow::{anyhow, Result};
+use crate::{
+    http::{HttpClient, Method, Request, Url},
+    rpc::{Client, Status},
+    util::TryFutureExt,
+};
+use anyhow::{anyhow, Context, Result};
+use futures::future;
+use gpui::{executor, ImageData, Task};
 use parking_lot::Mutex;
-use std::{collections::HashMap, sync::Arc};
+use postage::{oneshot, prelude::Stream, sink::Sink, watch};
+use std::{
+    collections::HashMap,
+    sync::{Arc, Weak},
+};
 use zrpc::proto;
 
-pub use proto::User;
+#[derive(Debug)]
+pub struct User {
+    pub id: u64,
+    pub github_login: String,
+    pub avatar: Option<Arc<ImageData>>,
+}
 
 pub struct UserStore {
     users: Mutex<HashMap<u64, Arc<User>>>,
+    current_user: watch::Receiver<Option<Arc<User>>>,
     rpc: Arc<Client>,
+    http: Arc<dyn HttpClient>,
+    _maintain_current_user: Task<()>,
 }
 
 impl UserStore {
-    pub fn new(rpc: Arc<Client>) -> Self {
-        Self {
+    pub fn new(
+        rpc: Arc<Client>,
+        http: Arc<dyn HttpClient>,
+        executor: &executor::Background,
+    ) -> Arc<Self> {
+        let (mut current_user_tx, current_user_rx) = watch::channel();
+        let (mut this_tx, mut this_rx) = oneshot::channel::<Weak<Self>>();
+        let this = Arc::new(Self {
             users: Default::default(),
-            rpc,
-        }
+            current_user: current_user_rx,
+            rpc: rpc.clone(),
+            http,
+            _maintain_current_user: executor.spawn(async move {
+                let this = if let Some(this) = this_rx.recv().await {
+                    this
+                } else {
+                    return;
+                };
+                let mut status = rpc.status();
+                while let Some(status) = status.recv().await {
+                    match status {
+                        Status::Connected { .. } => {
+                            if let Some((this, user_id)) = this.upgrade().zip(rpc.user_id()) {
+                                current_user_tx
+                                    .send(this.fetch_user(user_id).log_err().await)
+                                    .await
+                                    .ok();
+                            }
+                        }
+                        Status::SignedOut => {
+                            current_user_tx.send(None).await.ok();
+                        }
+                        _ => {}
+                    }
+                }
+            }),
+        });
+        let weak = Arc::downgrade(&this);
+        executor
+            .spawn(async move { this_tx.send(weak).await })
+            .detach();
+        this
     }
 
     pub async fn load_users(&self, mut user_ids: Vec<u64>) -> Result<()> {
@@ -27,8 +82,15 @@ impl UserStore {
 
         if !user_ids.is_empty() {
             let response = self.rpc.request(proto::GetUsers { user_ids }).await?;
+            let new_users = future::join_all(
+                response
+                    .users
+                    .into_iter()
+                    .map(|user| User::new(user, self.http.as_ref())),
+            )
+            .await;
             let mut users = self.users.lock();
-            for user in response.users {
+            for user in new_users {
                 users.insert(user.id, Arc::new(user));
             }
         }
@@ -36,24 +98,52 @@ impl UserStore {
         Ok(())
     }
 
-    pub async fn get_user(&self, user_id: u64) -> Result<Arc<User>> {
+    pub async fn fetch_user(&self, user_id: u64) -> Result<Arc<User>> {
         if let Some(user) = self.users.lock().get(&user_id).cloned() {
             return Ok(user);
         }
 
-        let response = self
-            .rpc
-            .request(proto::GetUsers {
-                user_ids: vec![user_id],
-            })
-            .await?;
-
-        if let Some(user) = response.users.into_iter().next() {
-            let user = Arc::new(user);
-            self.users.lock().insert(user_id, user.clone());
-            Ok(user)
-        } else {
-            Err(anyhow!("server responded with no users"))
+        self.load_users(vec![user_id]).await?;
+        self.users
+            .lock()
+            .get(&user_id)
+            .cloned()
+            .ok_or_else(|| anyhow!("server responded with no users"))
+    }
+
+    pub fn current_user(&self) -> Option<Arc<User>> {
+        self.current_user.borrow().clone()
+    }
+
+    pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
+        self.current_user.clone()
+    }
+}
+
+impl User {
+    async fn new(message: proto::User, http: &dyn HttpClient) -> Self {
+        User {
+            id: message.id,
+            github_login: message.github_login,
+            avatar: fetch_avatar(http, &message.avatar_url).log_err().await,
         }
     }
 }
+
+async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
+    let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?;
+    let mut request = Request::new(Method::Get, url);
+    request.middleware(surf::middleware::Redirect::default());
+
+    let mut response = http
+        .send(request)
+        .await
+        .map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
+    let bytes = response
+        .body_bytes()
+        .await
+        .map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
+    let format = image::guess_format(&bytes)?;
+    let image = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
+    Ok(ImageData::new(image))
+}

zed/src/workspace.rs 🔗

@@ -10,8 +10,9 @@ use crate::{
     project_browser::ProjectBrowser,
     rpc,
     settings::Settings,
+    user,
     worktree::{File, Worktree},
-    AppState,
+    AppState, Authenticate,
 };
 use anyhow::{anyhow, Result};
 use gpui::{
@@ -20,7 +21,7 @@ use gpui::{
     geometry::{rect::RectF, vector::vec2f},
     json::to_string_pretty,
     keymap::Binding,
-    platform::WindowOptions,
+    platform::{CursorStyle, WindowOptions},
     AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext,
     PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
     WeakModelHandle,
@@ -28,9 +29,8 @@ use gpui::{
 use log::error;
 pub use pane::*;
 pub use pane_group::*;
-use postage::watch;
+use postage::{prelude::Stream, watch};
 use sidebar::{Side, Sidebar, ToggleSidebarItem};
-use smol::prelude::*;
 use std::{
     collections::{hash_map::Entry, HashMap, HashSet},
     future::Future,
@@ -341,6 +341,7 @@ pub struct Workspace {
     pub settings: watch::Receiver<Settings>,
     languages: Arc<LanguageRegistry>,
     rpc: Arc<rpc::Client>,
+    user_store: Arc<user::UserStore>,
     fs: Arc<dyn Fs>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
@@ -354,6 +355,7 @@ pub struct Workspace {
         (usize, Arc<Path>),
         postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
     >,
+    _observe_current_user: Task<()>,
 }
 
 impl Workspace {
@@ -387,6 +389,23 @@ impl Workspace {
         );
         right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into());
 
+        let mut current_user = app_state.user_store.watch_current_user().clone();
+        let mut connection_status = app_state.rpc.status().clone();
+        let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
+            current_user.recv().await;
+            connection_status.recv().await;
+            let mut stream =
+                Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
+
+            while stream.recv().await.is_some() {
+                cx.update(|cx| {
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(cx, |_, cx| cx.notify());
+                    }
+                })
+            }
+        });
+
         Workspace {
             modal: None,
             center: PaneGroup::new(pane.id()),
@@ -395,12 +414,14 @@ impl Workspace {
             settings: app_state.settings.clone(),
             languages: app_state.languages.clone(),
             rpc: app_state.rpc.clone(),
+            user_store: app_state.user_store.clone(),
             fs: app_state.fs.clone(),
             left_sidebar,
             right_sidebar,
             worktrees: Default::default(),
             items: Default::default(),
             loading_items: Default::default(),
+            _observe_current_user,
         }
     }
 
@@ -625,7 +646,7 @@ impl Workspace {
                 if let Some(load_result) = watch.borrow().as_ref() {
                     break load_result.clone();
                 }
-                watch.next().await;
+                watch.recv().await;
             };
 
             this.update(&mut cx, |this, cx| {
@@ -936,6 +957,66 @@ impl Workspace {
     pub fn active_pane(&self) -> &ViewHandle<Pane> {
         &self.active_pane
     }
+
+    fn render_connection_status(&self) -> Option<ElementBox> {
+        let theme = &self.settings.borrow().theme;
+        match &*self.rpc.status().borrow() {
+            rpc::Status::ConnectionError
+            | rpc::Status::ConnectionLost
+            | rpc::Status::Reauthenticating
+            | rpc::Status::Reconnecting { .. }
+            | rpc::Status::ReconnectionError { .. } => Some(
+                Container::new(
+                    Align::new(
+                        ConstrainedBox::new(
+                            Svg::new("icons/offline-14.svg")
+                                .with_color(theme.workspace.titlebar.icon_color)
+                                .boxed(),
+                        )
+                        .with_width(theme.workspace.titlebar.offline_icon.width)
+                        .boxed(),
+                    )
+                    .boxed(),
+                )
+                .with_style(theme.workspace.titlebar.offline_icon.container)
+                .boxed(),
+            ),
+            _ => None,
+        }
+    }
+
+    fn render_avatar(&self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = &self.settings.borrow().theme;
+        let avatar = if let Some(avatar) = self
+            .user_store
+            .current_user()
+            .and_then(|user| user.avatar.clone())
+        {
+            Image::new(avatar)
+                .with_style(theme.workspace.titlebar.avatar)
+                .boxed()
+        } else {
+            MouseEventHandler::new::<Authenticate, _, _, _>(0, cx, |_, _| {
+                Svg::new("icons/signed-out-12.svg")
+                    .with_color(theme.workspace.titlebar.icon_color)
+                    .boxed()
+            })
+            .on_click(|cx| cx.dispatch_action(Authenticate))
+            .with_cursor_style(CursorStyle::PointingHand)
+            .boxed()
+        };
+
+        ConstrainedBox::new(
+            Align::new(
+                ConstrainedBox::new(avatar)
+                    .with_width(theme.workspace.titlebar.avatar_width)
+                    .boxed(),
+            )
+            .boxed(),
+        )
+        .with_width(theme.workspace.right_sidebar.width)
+        .boxed()
+    }
 }
 
 impl Entity for Workspace {
@@ -955,15 +1036,30 @@ impl View for Workspace {
                 .with_child(
                     ConstrainedBox::new(
                         Container::new(
-                            Align::new(
-                                Label::new(
-                                    "zed".into(), 
-                                    theme.workspace.titlebar.label.clone()
-                                ).boxed()
-                            )
-                            .boxed()
+                            Stack::new()
+                                .with_child(
+                                    Align::new(
+                                        Label::new(
+                                            "zed".into(),
+                                            theme.workspace.titlebar.title.clone(),
+                                        )
+                                        .boxed(),
+                                    )
+                                    .boxed(),
+                                )
+                                .with_child(
+                                    Align::new(
+                                        Flex::row()
+                                            .with_children(self.render_connection_status())
+                                            .with_child(self.render_avatar(cx))
+                                            .boxed(),
+                                    )
+                                    .right()
+                                    .boxed(),
+                                )
+                                .boxed(),
                         )
-                        .with_style(&theme.workspace.titlebar.container)
+                        .with_style(theme.workspace.titlebar.container)
                         .boxed(),
                     )
                     .with_height(32.)
@@ -1380,7 +1476,7 @@ mod tests {
         });
         cx.simulate_new_path_selection(|parent_dir| {
             assert_eq!(parent_dir, dir.path());
-            Some(parent_dir.join("the-new-name"))
+            Some(parent_dir.join("the-new-name.rs"))
         });
         cx.read(|cx| {
             assert!(editor.is_dirty(cx));
@@ -1393,8 +1489,10 @@ mod tests {
             .await;
         cx.read(|cx| {
             assert!(!editor.is_dirty(cx));
-            assert_eq!(editor.title(cx), "the-new-name");
+            assert_eq!(editor.title(cx), "the-new-name.rs");
         });
+        // The language is assigned based on the path
+        editor.read_with(&cx, |editor, cx| assert!(editor.language(cx).is_some()));
 
         // Edit the file and save it again. This time, there is no filename prompt.
         editor.update(&mut cx, |editor, cx| {
@@ -1408,7 +1506,7 @@ mod tests {
         editor
             .condition(&cx, |editor, cx| !editor.is_dirty(cx))
             .await;
-        cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name"));
+        cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs"));
 
         // Open the same newly-created file in another pane item. The new editor should reuse
         // the same buffer.
@@ -1416,7 +1514,7 @@ mod tests {
             workspace.open_new_file(&OpenNew(app_state.clone()), cx);
             workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
             assert!(workspace
-                .open_entry((tree.id(), Path::new("the-new-name").into()), cx)
+                .open_entry((tree.id(), Path::new("the-new-name.rs").into()), cx)
                 .is_none());
         });
         let editor2 = workspace.update(&mut cx, |workspace, cx| {

zed/src/workspace/pane.rs 🔗

@@ -1,6 +1,14 @@
 use super::{ItemViewHandle, SplitDirection};
 use crate::settings::Settings;
-use gpui::{Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle, action, color::Color, elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::CursorStyle};
+use gpui::{
+    action,
+    color::Color,
+    elements::*,
+    geometry::{rect::RectF, vector::vec2f},
+    keymap::Binding,
+    platform::CursorStyle,
+    Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
+};
 use postage::watch;
 use std::{cmp, path::Path, sync::Arc};
 
@@ -177,13 +185,13 @@ impl Pane {
             theme.workspace.tab.label.text.font_size,
         );
 
-        let mut row = Flex::row();
-        for (ix, item) in self.items.iter().enumerate() {
-            let is_active = ix == self.active_item;
+        enum Tabs {}
+        let tabs = MouseEventHandler::new::<Tabs, _, _, _>(0, cx, |mouse_state, cx| {
+            let mut row = Flex::row();
+            for (ix, item) in self.items.iter().enumerate() {
+                let is_active = ix == self.active_item;
 
-            enum Tab {}
-            row.add_child(
-                MouseEventHandler::new::<Tab, _, _, _>(item.id(), cx, |mouse_state, cx| {
+                row.add_child({
                     let mut title = item.title(cx);
                     if title.len() > MAX_TAB_TITLE_LEN {
                         let mut truncated_len = MAX_TAB_TITLE_LEN;
@@ -256,7 +264,7 @@ impl Pane {
                                         )
                                         .boxed(),
                                     )
-                                    .with_style(&ContainerStyle {
+                                    .with_style(ContainerStyle {
                                         margin: Margin {
                                             left: style.spacing,
                                             right: style.spacing,
@@ -268,7 +276,7 @@ impl Pane {
                                 )
                                 .with_child(
                                     Align::new(
-                                        ConstrainedBox::new(if is_active || mouse_state.hovered {
+                                        ConstrainedBox::new(if mouse_state.hovered {
                                             let item_id = item.id();
                                             enum TabCloseButton {}
                                             let icon = Svg::new("icons/x.svg");
@@ -283,7 +291,9 @@ impl Pane {
                                                         icon.with_color(style.icon_close).boxed()
                                                     }
                                                 },
-                                            ).with_cursor_style(CursorStyle::PointingHand)
+                                            )
+                                            .with_padding(Padding::uniform(4.))
+                                            .with_cursor_style(CursorStyle::PointingHand)
                                             .on_click(move |cx| {
                                                 cx.dispatch_action(CloseItem(item_id))
                                             })
@@ -298,7 +308,7 @@ impl Pane {
                                 )
                                 .boxed(),
                         )
-                        .with_style(&style.container)
+                        .with_style(style.container)
                         .boxed(),
                     )
                     .on_mouse_down(move |cx| {
@@ -307,36 +317,37 @@ impl Pane {
                     })
                     .boxed()
                 })
-                .boxed(),
-            )
-        }
+            }
 
-        // Ensure there's always a minimum amount of space after the last tab,
-        // so that the tab's border doesn't abut the window's border.
-        let mut border = Border::bottom(1.0, Color::default());
-        border.color = theme.workspace.tab.container.border.color;
-
-        row.add_child(
-            ConstrainedBox::new(
-                Container::new(Empty::new().boxed())
-                    .with_border(border)
-                    .boxed(),
-            )
-            .with_min_width(20.)
-            .named("fixed-filler"),
-        );
+            // Ensure there's always a minimum amount of space after the last tab,
+            // so that the tab's border doesn't abut the window's border.
+            let mut border = Border::bottom(1.0, Color::default());
+            border.color = theme.workspace.tab.container.border.color;
 
-        row.add_child(
-            Expanded::new(
-                0.0,
-                Container::new(Empty::new().boxed())
-                    .with_border(border)
-                    .boxed(),
-            )
-            .named("filler"),
-        );
+            row.add_child(
+                ConstrainedBox::new(
+                    Container::new(Empty::new().boxed())
+                        .with_border(border)
+                        .boxed(),
+                )
+                .with_min_width(20.)
+                .named("fixed-filler"),
+            );
+
+            row.add_child(
+                Expanded::new(
+                    0.0,
+                    Container::new(Empty::new().boxed())
+                        .with_border(border)
+                        .boxed(),
+                )
+                .named("filler"),
+            );
+
+            row.boxed()
+        });
 
-        ConstrainedBox::new(row.boxed())
+        ConstrainedBox::new(tabs.boxed())
             .with_height(line_height + 16.)
             .named("tabs")
     }

zed/src/workspace/sidebar.rs 🔗

@@ -75,38 +75,48 @@ impl Sidebar {
         );
         let theme = self.theme(settings);
 
-        Container::new(
-            Flex::column()
-                .with_children(self.items.iter().enumerate().map(|(item_index, item)| {
-                    let theme = if Some(item_index) == self.active_item_ix {
-                        &theme.active_icon
-                    } else {
-                        &theme.icon
-                    };
-                    enum SidebarButton {}
-                    MouseEventHandler::new::<SidebarButton, _, _, _>(item.view.id(), cx, |_, _| {
-                        ConstrainedBox::new(
-                            Align::new(
+        ConstrainedBox::new(
+            Container::new(
+                Flex::column()
+                    .with_children(self.items.iter().enumerate().map(|(item_index, item)| {
+                        let theme = if Some(item_index) == self.active_item_ix {
+                            &theme.active_icon
+                        } else {
+                            &theme.icon
+                        };
+                        enum SidebarButton {}
+                        MouseEventHandler::new::<SidebarButton, _, _, _>(
+                            item.view.id(),
+                            cx,
+                            |_, _| {
                                 ConstrainedBox::new(
-                                    Svg::new(item.icon_path).with_color(theme.color).boxed(),
+                                    Align::new(
+                                        ConstrainedBox::new(
+                                            Svg::new(item.icon_path)
+                                                .with_color(theme.color)
+                                                .boxed(),
+                                        )
+                                        .with_height(theme.height)
+                                        .boxed(),
+                                    )
+                                    .boxed(),
                                 )
-                                .with_height(theme.height)
-                                .boxed(),
-                            )
-                            .boxed(),
+                                .with_height(line_height + 16.0)
+                                .boxed()
+                            },
                         )
-                        .with_height(line_height + 16.0)
+                        .with_cursor_style(CursorStyle::PointingHand)
+                        .on_mouse_down(move |cx| {
+                            cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index }))
+                        })
                         .boxed()
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_mouse_down(move |cx| {
-                        cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index }))
-                    })
-                    .boxed()
-                }))
-                .boxed(),
+                    }))
+                    .boxed(),
+            )
+            .with_style(theme.container)
+            .boxed(),
         )
-        .with_style(&theme.container)
+        .with_width(theme.width)
         .boxed()
     }
 
@@ -155,7 +165,7 @@ impl Sidebar {
         let side = self.side;
         MouseEventHandler::new::<Self, _, _, _>(self.side.id(), &mut cx, |_, _| {
             Container::new(Empty::new().boxed())
-                .with_style(&self.theme(settings).resize_handle)
+                .with_style(self.theme(settings).resize_handle)
                 .boxed()
         })
         .with_padding(Padding {

zed/src/worktree.rs 🔗

@@ -268,6 +268,13 @@ impl Worktree {
         }
     }
 
+    pub fn languages(&self) -> &Arc<LanguageRegistry> {
+        match self {
+            Worktree::Local(worktree) => &worktree.languages,
+            Worktree::Remote(worktree) => &worktree.languages,
+        }
+    }
+
     pub fn snapshot(&self) -> Snapshot {
         match self {
             Worktree::Local(worktree) => worktree.snapshot(),

zrpc/proto/zed.proto 🔗

@@ -151,6 +151,7 @@ message GetUsersResponse {
 message SendChannelMessage {
     uint64 channel_id = 1;
     string body = 2;
+    Nonce nonce = 3;
 }
 
 message SendChannelMessageResponse {
@@ -296,6 +297,11 @@ message Range {
     uint64 end = 2;
 }
 
+message Nonce {
+    uint64 upper_half = 1;
+    uint64 lower_half = 2;
+}
+
 message Channel {
     uint64 id = 1;
     string name = 2;
@@ -306,4 +312,5 @@ message ChannelMessage {
     string body = 2;
     uint64 timestamp = 3;
     uint64 sender_id = 4;
+    Nonce nonce = 5;
 }

zrpc/src/conn.rs 🔗

@@ -2,7 +2,7 @@ use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSock
 use futures::{channel::mpsc, SinkExt as _, Stream, StreamExt as _};
 use std::{io, task::Poll};
 
-pub struct Conn {
+pub struct Connection {
     pub(crate) tx:
         Box<dyn 'static + Send + Unpin + futures::Sink<WebSocketMessage, Error = WebSocketError>>,
     pub(crate) rx: Box<
@@ -13,7 +13,7 @@ pub struct Conn {
     >,
 }
 
-impl Conn {
+impl Connection {
     pub fn new<S>(stream: S) -> Self
     where
         S: 'static

zrpc/src/lib.rs 🔗

@@ -2,5 +2,5 @@ pub mod auth;
 mod conn;
 mod peer;
 pub mod proto;
-pub use conn::Conn;
+pub use conn::Connection;
 pub use peer::*;

zrpc/src/peer.rs 🔗

@@ -1,5 +1,5 @@
 use super::proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage};
-use super::Conn;
+use super::Connection;
 use anyhow::{anyhow, Context, Result};
 use async_lock::{Mutex, RwLock};
 use futures::FutureExt as _;
@@ -79,12 +79,12 @@ impl<T: RequestMessage> TypedEnvelope<T> {
 }
 
 pub struct Peer {
-    connections: RwLock<HashMap<ConnectionId, Connection>>,
+    connections: RwLock<HashMap<ConnectionId, ConnectionState>>,
     next_connection_id: AtomicU32,
 }
 
 #[derive(Clone)]
-struct Connection {
+struct ConnectionState {
     outgoing_tx: mpsc::Sender<proto::Envelope>,
     next_message_id: Arc<AtomicU32>,
     response_channels: Arc<Mutex<HashMap<u32, mpsc::Sender<proto::Envelope>>>>,
@@ -100,7 +100,7 @@ impl Peer {
 
     pub async fn add_connection(
         self: &Arc<Self>,
-        conn: Conn,
+        connection: Connection,
     ) -> (
         ConnectionId,
         impl Future<Output = anyhow::Result<()>> + Send,
@@ -112,16 +112,16 @@ impl Peer {
         );
         let (mut incoming_tx, incoming_rx) = mpsc::channel(64);
         let (outgoing_tx, mut outgoing_rx) = mpsc::channel(64);
-        let connection = Connection {
+        let connection_state = ConnectionState {
             outgoing_tx,
             next_message_id: Default::default(),
             response_channels: Default::default(),
         };
-        let mut writer = MessageStream::new(conn.tx);
-        let mut reader = MessageStream::new(conn.rx);
+        let mut writer = MessageStream::new(connection.tx);
+        let mut reader = MessageStream::new(connection.rx);
 
         let this = self.clone();
-        let response_channels = connection.response_channels.clone();
+        let response_channels = connection_state.response_channels.clone();
         let handle_io = async move {
             loop {
                 let read_message = reader.read_message().fuse();
@@ -179,7 +179,7 @@ impl Peer {
         self.connections
             .write()
             .await
-            .insert(connection_id, connection);
+            .insert(connection_id, connection_state);
 
         (connection_id, handle_io, incoming_rx)
     }
@@ -218,7 +218,7 @@ impl Peer {
         let this = self.clone();
         let (tx, mut rx) = mpsc::channel(1);
         async move {
-            let mut connection = this.connection(receiver_id).await?;
+            let mut connection = this.connection_state(receiver_id).await?;
             let message_id = connection
                 .next_message_id
                 .fetch_add(1, atomic::Ordering::SeqCst);
@@ -252,7 +252,7 @@ impl Peer {
     ) -> impl Future<Output = Result<()>> {
         let this = self.clone();
         async move {
-            let mut connection = this.connection(receiver_id).await?;
+            let mut connection = this.connection_state(receiver_id).await?;
             let message_id = connection
                 .next_message_id
                 .fetch_add(1, atomic::Ordering::SeqCst);
@@ -272,7 +272,7 @@ impl Peer {
     ) -> impl Future<Output = Result<()>> {
         let this = self.clone();
         async move {
-            let mut connection = this.connection(receiver_id).await?;
+            let mut connection = this.connection_state(receiver_id).await?;
             let message_id = connection
                 .next_message_id
                 .fetch_add(1, atomic::Ordering::SeqCst);
@@ -291,7 +291,7 @@ impl Peer {
     ) -> impl Future<Output = Result<()>> {
         let this = self.clone();
         async move {
-            let mut connection = this.connection(receipt.sender_id).await?;
+            let mut connection = this.connection_state(receipt.sender_id).await?;
             let message_id = connection
                 .next_message_id
                 .fetch_add(1, atomic::Ordering::SeqCst);
@@ -310,7 +310,7 @@ impl Peer {
     ) -> impl Future<Output = Result<()>> {
         let this = self.clone();
         async move {
-            let mut connection = this.connection(receipt.sender_id).await?;
+            let mut connection = this.connection_state(receipt.sender_id).await?;
             let message_id = connection
                 .next_message_id
                 .fetch_add(1, atomic::Ordering::SeqCst);
@@ -322,10 +322,10 @@ impl Peer {
         }
     }
 
-    fn connection(
+    fn connection_state(
         self: &Arc<Self>,
         connection_id: ConnectionId,
-    ) -> impl Future<Output = Result<Connection>> {
+    ) -> impl Future<Output = Result<ConnectionState>> {
         let this = self.clone();
         async move {
             let connections = this.connections.read().await;
@@ -352,12 +352,12 @@ mod tests {
             let client1 = Peer::new();
             let client2 = Peer::new();
 
-            let (client1_to_server_conn, server_to_client_1_conn, _) = Conn::in_memory();
+            let (client1_to_server_conn, server_to_client_1_conn, _) = Connection::in_memory();
             let (client1_conn_id, io_task1, _) =
                 client1.add_connection(client1_to_server_conn).await;
             let (_, io_task2, incoming1) = server.add_connection(server_to_client_1_conn).await;
 
-            let (client2_to_server_conn, server_to_client_2_conn, _) = Conn::in_memory();
+            let (client2_to_server_conn, server_to_client_2_conn, _) = Connection::in_memory();
             let (client2_conn_id, io_task3, _) =
                 client2.add_connection(client2_to_server_conn).await;
             let (_, io_task4, incoming2) = server.add_connection(server_to_client_2_conn).await;
@@ -486,7 +486,7 @@ mod tests {
     #[test]
     fn test_disconnect() {
         smol::block_on(async move {
-            let (client_conn, mut server_conn, _) = Conn::in_memory();
+            let (client_conn, mut server_conn, _) = Connection::in_memory();
 
             let client = Peer::new();
             let (connection_id, io_handler, mut incoming) =
@@ -520,7 +520,7 @@ mod tests {
     #[test]
     fn test_io_error() {
         smol::block_on(async move {
-            let (client_conn, server_conn, _) = Conn::in_memory();
+            let (client_conn, server_conn, _) = Connection::in_memory();
             drop(server_conn);
 
             let client = Peer::new();

zrpc/src/proto.rs 🔗

@@ -248,3 +248,22 @@ impl From<SystemTime> for Timestamp {
         }
     }
 }
+
+impl From<u128> for Nonce {
+    fn from(nonce: u128) -> Self {
+        let upper_half = (nonce >> 64) as u64;
+        let lower_half = nonce as u64;
+        Self {
+            upper_half,
+            lower_half,
+        }
+    }
+}
+
+impl From<Nonce> for u128 {
+    fn from(nonce: Nonce) -> Self {
+        let upper_half = (nonce.upper_half as u128) << 64;
+        let lower_half = nonce.lower_half as u128;
+        upper_half | lower_half
+    }
+}