diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bebd0f0beabb4d6a2670ff7700e863e88e00771..cfbdc2ca02f89119c8953c0f9733daa2b60402ee 100644 --- a/.github/workflows/ci.yml +++ b/.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 diff --git a/Cargo.lock b/Cargo.lock index 562da86feec01872e2eb71718d775b41f454dde7..7973316c9e337532685bfc546f22dae9d8a7bee1 100644 --- a/Cargo.lock +++ b/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", diff --git a/Dockerfile.migrator b/Dockerfile.migrator index 99c21b2230387b2ab1d31016a5c9494d573d21c0..24b58da839e1ca09880574649694c33cb32b1fc1 100644 --- a/Dockerfile.migrator +++ b/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 diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 11a855cd40c087c4aed6fc278adfbdb7ded92952..be86a788d818bec2e565baff397fe0b39167dd04 100644 --- a/gpui/Cargo.toml +++ b/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" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index c1ce8fdba0d266726a8db83eaba36fe3fe3f4de6..ebe6c89a8ace2a965138658a8faaccab66b2f1ef 100644 --- a/gpui/src/app.rs +++ b/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(&self, f: F) -> Task + where + F: FnOnce(WeakViewHandle, AsyncAppContext) -> Fut, + Fut: 'static + Future, + 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 ReadModel for RenderContext<'_, V> { } } +impl UpdateModel for RenderContext<'_, V> { + fn update_model(&mut self, handle: &ModelHandle, update: F) -> S + where + T: Entity, + F: FnOnce(&mut T, &mut ModelContext) -> S, + { + self.app.update_model(handle, update) + } +} + impl AsRef for ViewContext<'_, M> { fn as_ref(&self) -> &AppContext { &self.app.cx diff --git a/gpui/src/color.rs b/gpui/src/color.rs index 9c6de6247a62c5fb96b793c1f6332e495eb442a2..9e31530b27f8b9f93c42df203aa88f99d9b9eff0 100644 --- a/gpui/src/color.rs +++ b/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)) } diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 6d7429222c7c1bf74d259932cbc5b3b614892ce3..42e9810cfbff21ea9c96017b54689cb5a77f48cd 100644 --- a/gpui/src/elements.rs +++ b/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 + Sized { } impl<'a, T> ParentElement<'a> for T where T: Extend {} + +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()) + } +} diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index 48dcfa1b137986df78690a1adaf8e06249dae314..abbafe4d034e76926ff79f794a286a24a727d927 100644 --- a/gpui/src/elements/container.rs +++ b/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, diff --git a/gpui/src/elements/image.rs b/gpui/src/elements/image.rs new file mode 100644 index 0000000000000000000000000000000000000000..421e18ec95ce7bc64df77d675797a10c41ff3541 --- /dev/null +++ b/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, + 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) -> 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(), + }) + } +} diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 1a86e2935cd774837d2dbf03f12acd089c4e487b..3864bf3c80daf4f9e2a6c119351838c2aabb2bb3 100644 --- a/gpui/src/elements/list.rs +++ b/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")) diff --git a/gpui/src/elements/mouse_event_handler.rs b/gpui/src/elements/mouse_event_handler.rs index 3b28409f9fa7ee67d90401bb40357ca012558b24..2cc01c3080f22a7d3587dd4750879ee8287dc5a8 100644 --- a/gpui/src/elements/mouse_event_handler.rs +++ b/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 { diff --git a/gpui/src/elements/svg.rs b/gpui/src/elements/svg.rs index 8adb285b99a73c1e2ab661976d4f2d58d4d635db..3e93d3adae3cd721a2c6e4ff501bbda0bf6b5f86 100644 --- a/gpui/src/elements/svg.rs +++ b/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), diff --git a/gpui/src/font_cache.rs b/gpui/src/font_cache.rs index 3c11b9659cb26441ab7746bac6a6f306fdbcef42..c0255a7af5f251b9828e4788dba6443f30efcc5b 100644 --- a/gpui/src/font_cache.rs +++ b/gpui/src/font_cache.rs @@ -17,7 +17,7 @@ use std::{ pub struct FamilyId(usize); struct Family { - name: String, + name: Arc, font_ids: Vec, } @@ -49,7 +49,7 @@ impl FontCache { })) } - pub fn family_name(&self, family_id: FamilyId) -> Result { + pub fn family_name(&self, family_id: FamilyId) -> Result> { 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, font_id: FontId, font_size: f32) -> LineWrapperHandle { diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index 96248c167577326cb74ed3ee6c1d7934f385f362..3ec8aad9626bf78e4beb79709b89e525be679ad9 100644 --- a/gpui/src/fonts.rs +++ b/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, + 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 { 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 for HighlightStyle { + fn from(other: TextStyle) -> Self { + Self { + color: other.color, + font_properties: other.font_properties, + underline: other.underline, + } + } } impl HighlightStyle { diff --git a/gpui/src/image_data.rs b/gpui/src/image_data.rs new file mode 100644 index 0000000000000000000000000000000000000000..d97820ab51b3f09ad8f2241243837ce1535dc6af --- /dev/null +++ b/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, Vec>, +} + +impl ImageData { + pub fn new(data: ImageBuffer, Vec>) -> Arc { + 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() + } +} diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index 6cb1c6f39d7d5caee1b94516461a371335924231..4b4d5f25d55060585df09f267c9bad55a258ced2 100644 --- a/gpui/src/lib.rs +++ b/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; diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index a4c86eab2f9f696754c11677dabb8d32849ba33b..cd972021a57c0084c38145b7222813be515d8fa2 100644 --- a/gpui/src/platform.rs +++ b/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)>>; + fn delete_credentials(&self, url: &str) -> Result<()>; fn set_cursor_style(&self, style: CursorStyle); diff --git a/gpui/src/platform/mac.rs b/gpui/src/platform/mac.rs index 016d3cb4448b1034f5b56b96140e62e13e027476..8cf3f62874aac87bc6d1841fdfc79d7914a66b50 100644 --- a/gpui/src/platform/mac.rs +++ b/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; diff --git a/gpui/src/platform/mac/atlas.rs b/gpui/src/platform/mac/atlas.rs index c9d910a586d686b3691f8710fa49c85923b67886..e23a045d47e4ea5f28bda6e1c951a2724a9f0258 100644 --- a/gpui/src/platform/mac/atlas.rs +++ b/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, } +#[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 { - 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) { diff --git a/gpui/src/platform/mac/image_cache.rs b/gpui/src/platform/mac/image_cache.rs new file mode 100644 index 0000000000000000000000000000000000000000..dac2e1a38b24051e983f54152a6e57c2926819b3 --- /dev/null +++ b/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, + curr_frame: HashMap, + 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) + } +} diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index 7015cbc713cecc528e742fe902a86c840c25cfd1..c956a199989ea35cfc62a678caac03240d44775d 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/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; } diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index e12a52d6134dbd2c5d110a570f52920c449a287d..369696d47838d25afc105dcfa2426e905c19b5c2 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/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::() 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::() 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::() 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::() 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::(); + 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::() 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")); diff --git a/gpui/src/platform/mac/shaders/shaders.h b/gpui/src/platform/mac/shaders/shaders.h index 5f49bfca64004ec67abc9c58c641b631cfd58375..1b6ad3f26f98122b11afeaff02006dd2ef0e8daa 100644 --- a/gpui/src/platform/mac/shaders/shaders.h +++ b/gpui/src/platform/mac/shaders/shaders.h @@ -1,16 +1,19 @@ #include -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; diff --git a/gpui/src/platform/mac/shaders/shaders.metal b/gpui/src/platform/mac/shaders/shaders.metal index 91e5ea129577d9443ee0f395c0e01df72f37b702..13d2720fad7788ff93a58a8143dde44ced25cf41 100644 --- a/gpui/src/platform/mac/shaders/shaders.metal +++ b/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 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; diff --git a/gpui/src/platform/mac/sprite_cache.rs b/gpui/src/platform/mac/sprite_cache.rs index 4c764ae1ca518a52e6f45b6bd80093345576fe26..7d11a3d2760dc46e42a08747cb922d9a9ef42e75 100644 --- a/gpui/src/platform/mac/sprite_cache.rs +++ b/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, - atlases: Vec, + atlases: AtlasAllocator, glyphs: HashMap>, icons: HashMap, } @@ -56,21 +51,18 @@ impl SpriteCache { size: Vector2I, fonts: Arc, ) -> 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::>(); - 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 { - 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) } } diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 85afff49994607fc2c16fd9705f5c207f6a6969e..d705a277e54f6d278d5d8fb02ad2c9ccf28014fb 100644 --- a/gpui/src/platform/test.rs +++ b/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; } diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index b2fa59b848aa2bb3ca53c9e5565104a6186bb1dc..2062397e9e6547dbe4ea9083485a8b8c0a519475 100644 --- a/gpui/src/presenter.rs +++ b/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, dispatched_actions: Vec, diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 401918c5fe014426a98496d7f4e2d31b903bb274..1b9c863647205a09fcafde18c2abdda1aa36e922 100644 --- a/gpui/src/scene.rs +++ b/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, quads: Vec, underlines: Vec, + images: Vec, shadows: Vec, glyphs: Vec, icons: Vec, @@ -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, +} + 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); } diff --git a/gpui/src/views/select.rs b/gpui/src/views/select.rs index b9e099a75c17f954a294f3150c6e264ea612ad17..e257455a7afc3ede7f02fc3489d3855eb5444438 100644 --- a/gpui/src/views/select.rs +++ b/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(), diff --git a/server/Cargo.toml b/server/Cargo.toml index b73c70102a311ecf1813a5bd85efa315e344dd93..b295ff21acf4d7d578b1f5a2ba4ccfed234684eb 100644 --- a/server/Cargo.toml +++ b/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" } diff --git a/server/migrations/20210916123647_add_nonce_to_channel_messages.sql b/server/migrations/20210916123647_add_nonce_to_channel_messages.sql new file mode 100644 index 0000000000000000000000000000000000000000..ee4d4aa319f6417e854137332011115570153eae --- /dev/null +++ b/server/migrations/20210916123647_add_nonce_to_channel_messages.sql @@ -0,0 +1,4 @@ +ALTER TABLE "channel_messages" +ADD "nonce" UUID NOT NULL DEFAULT gen_random_uuid(); + +CREATE UNIQUE INDEX "index_channel_messages_nonce" ON "channel_messages" ("nonce"); diff --git a/server/src/auth.rs b/server/src/auth.rs index 5a3e301d27537a1e031d804341af29d071afdd95..1f6ec5f1db176638ffc52106d129cc6793f75c6e 100644 --- a/server/src/auth.rs +++ b/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> 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) } } } diff --git a/server/src/bin/seed.rs b/server/src/bin/seed.rs index b259dc4c14b24ea8b1278be56a6610f2e5fa1f64..d2427d495c451497df0644dc0fc4d36e7ecaa4ea 100644 --- a/server/src/bin/seed.rs +++ b/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"); } diff --git a/server/src/db.rs b/server/src/db.rs index 8d2199a9f33a39c2b0f65d0e5eb60560c3ad2ab0..14ad85b68af2e06148c02d12dc74790fa2b5b0c9 100644 --- a/server/src/db.rs +++ b/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, ) -> Result> { + let mut include_requester = false; + let ids = ids + .map(|id| { + if id == requester_id { + include_requester = true; + } + id.0 + }) + .collect::>(); + 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::>()) + .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 { 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); + } } diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 2bd0eac625e836d32bf4417be107a11a766dadb0..698414851e60fadd301b60cfc19ac31ad425ef6f 100644 --- a/server/src/rpc.rs +++ b/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, + connections: HashMap, pub worktrees: HashMap, channels: HashMap, next_worktree_id: u64, } -struct Connection { +struct ConnectionState { user_id: UserId, worktrees: HashSet, channels: HashSet, @@ -133,7 +133,7 @@ impl Server { pub fn handle_connection( self: &Arc, - connection: Conn, + connection: Connection, addr: String, user_id: UserId, ) -> impl Future { @@ -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::>(); 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::>(); self.peer @@ -972,7 +989,7 @@ pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { 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::>(), - &["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::>(), - &["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) { - let client_user_id = self.app_state.db.create_user(name, false).await.unwrap(); + ) -> (Arc, Arc) { + 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) -> 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 { diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 985901c50cf8a14d4331ed73f89a98fd1323d28d..17d42a04c95253c5f0943724a7cb52ea238c8faf 100644 --- a/zed/Cargo.toml +++ b/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"] diff --git a/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf b/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf index e8aad4c3cd21d9811f33b08fef26502b36441641..6c930e3bc724408d0e9884aeb91cdbcd4edc98ec 100644 Binary files a/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf and b/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf differ diff --git a/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf b/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf index 00ffc946a5aaccdbc563b2725b2c921aad1979a0..3b74e08a12d9abf807de632719ab781c809d6bf2 100644 Binary files a/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf and b/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf differ diff --git a/zed/assets/icons/offline-14.svg b/zed/assets/icons/offline-14.svg new file mode 100644 index 0000000000000000000000000000000000000000..5349f65ead5f2ef87331f97352ef770ca0f33656 --- /dev/null +++ b/zed/assets/icons/offline-14.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/icons/signed-out-12.svg b/zed/assets/icons/signed-out-12.svg new file mode 100644 index 0000000000000000000000000000000000000000..3cecfe9dd3dfaee05bf4bab6fab223c729fc4a52 --- /dev/null +++ b/zed/assets/icons/signed-out-12.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 6c0c37fa9876961734c5d56473162edd51fc4d2c..57f25eb26fc331311a2dc93536554471ec549602 100644 --- a/zed/assets/themes/_base.toml +++ b/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" diff --git a/zed/assets/themes/black.toml b/zed/assets/themes/black.toml index 53d9957f4b20b124f01460f5ccdc2a687444db87..3a7319e2a9e6af9b1f101981bfc8f107267b0c50 100644 --- a/zed/assets/themes/black.toml +++ b/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" \ No newline at end of file +constant = "#9cdcfe" diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index cf17c62fdbed355397b727fcad1c5de9e02ec9d3..f9c5a97f2acd9a3b40bf92e254ce9b16ff9b9688 100644 --- a/zed/assets/themes/dark.toml +++ b/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" } diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml index 80f84f998c1981d453b3d793298b3d5afdba0397..fe3262b12ca295168d14fe7e37cea069932562f0 100644 --- a/zed/assets/themes/light.toml +++ b/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" } diff --git a/zed/src/channel.rs b/zed/src/channel.rs index aa182c0540997e55ed8b8165b752ebaf7f30ac1a..c43cf2e6f7b28a45e5a69dfa67c0383e065f6143 100644 --- a/zed/src/channel.rs +++ b/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, loaded_all_messages: bool, - pending_messages: Vec, - next_local_message_id: u64, + next_pending_message_id: usize, user_store: Arc, rpc: Arc, + 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, + 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::>() }); + 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 { + let mut cursor = self.messages.cursor::(); + 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, cx: &mut ModelContext) { if let Some((first_message, last_message)) = messages.first().zip(messages.last()) { - let mut old_cursor = self.messages.cursor::(); + let nonces = messages + .cursor::<(), ()>() + .map(|m| m.nonce) + .collect::>(); + + let mut old_cursor = self.messages.cursor::(); 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::>::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 { - 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::().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::().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::().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()), }, ], }, diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 200a35fccae69b89be1e35e5655eb03642cf8aa2..5b07214efb363a4db206fc71fcd44ec94ee018c7 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -54,10 +54,15 @@ impl ChatPanel { cx: &mut ViewContext, ) -> 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) -> 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.) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 3157b0cd2fbb28010d13d2b6ab5da4ec4ba35f6c..25403d3aae44c3ef22c735700f80bfd1ea46f0dd 100644 --- a/zed/src/editor.rs +++ b/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, + 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, +} + +#[derive(Clone, Copy, Default, Deserialize)] +pub struct SelectionStyle { + pub cursor: Color, + pub selection: Color, +} + pub struct Editor { handle: WeakViewHandle, buffer: ModelHandle, @@ -290,10 +303,10 @@ pub struct Editor { scroll_position: Vector2F, scroll_top_anchor: Anchor, autoscroll_requested: bool, - build_style: Option EditorStyle>>>, + build_style: Rc EditorStyle>>, settings: watch::Receiver, 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>, pub theme: Arc, - 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, cx: &mut ViewContext) -> Self { + pub fn single_line( + settings: watch::Receiver, + build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle, + cx: &mut ViewContext, + ) -> 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, + build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle, cx: &mut ViewContext, ) -> 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, settings: watch::Receiver, + build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle, cx: &mut ViewContext, ) -> 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, + settings: watch::Receiver, + build_style: Rc EditorStyle>>, + cx: &mut ViewContext, + ) -> 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> { + self.buffer.read(cx).language() + } + pub fn set_placeholder_text( &mut self, placeholder_text: impl Into>, @@ -2229,7 +2258,7 @@ impl Editor { } fn pause_cursor_blinking(&mut self, cx: &mut ViewContext) { - 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) { 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, cx: &mut ViewContext) { @@ -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> { + 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, + ) -> 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 { + &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 { - 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, - active_rows: &BTreeMap, - font_cache: &FontCache, - layout_cache: &TextLayoutCache, - theme: &Theme, - ) -> Result>> { - 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, - style: &EditorStyle, - font_cache: &FontCache, - layout_cache: &TextLayoutCache, - ) -> Result> { - 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 { - 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) -> 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.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, cx: &mut ViewContext, ) -> 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, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> 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 { diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 97e0202cec4a6295e478b2078ad5d393df22ab50..82aa10d4383e40157b8550403b5ec230178dac35 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -714,9 +714,16 @@ impl Buffer { path: impl Into>, cx: &mut ModelContext, ) -> Task> { + 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> { + 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, diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 16d4da79b4c6446fa02f42c10469297dc397ccb0..e4b8cc0886603f1257bac097445a08db7f0b9871 100644 --- a/zed/src/editor/display_map.rs +++ b/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, @@ -25,13 +24,16 @@ impl Entity for DisplayMap { impl DisplayMap { pub fn new( buffer: ModelHandle, - settings: watch::Receiver, + tab_size: usize, + font_id: FontId, + font_size: f32, wrap_width: Option, cx: &mut ModelContext, ) -> 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.wrap_map + .update(cx, |map, cx| map.set_font(font_id, font_size, cx)); + } + pub fn set_wrap_width(&self, width: Option, cx: &mut ModelContext) -> 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::(); 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::(), "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::(), "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::(), "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) diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index d648a052aab0c6fb4ffc626d803d47983568d3a7..86e01f7e5695f1e4aa4ae1ac7dfaedd170b5c6a8 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/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)>, wrap_width: Option, background_task: Option>, - _watch_settings: Task<()>, - settings: watch::Receiver, + 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, + font_id: FontId, + font_size: f32, wrap_width: Option, cx: &mut ModelContext, ) -> 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) { + 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, cx: &mut ModelContext) -> 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()) { diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index 8ab6c52cc5fcf9d15027a0940fbb08cbb6c2414a..7c65b1a22d19d498ffd6f58df4db81a78d5fdb82 100644 --- a/zed/src/editor/element.rs +++ b/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, + active_rows: &BTreeMap, + snapshot: &Snapshot, + cx: &LayoutContext, + ) -> Vec> { + 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, + snapshot: &mut Snapshot, + cx: &LayoutContext, + ) -> Vec { + 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, line_layouts: Vec, @@ -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); + } +} diff --git a/zed/src/editor/movement.rs b/zed/src/editor/movement.rs index aec3932a7d9ad96154f44e95783e9d8a70801c61..8f5bc6f20a814536366f9077f1509d611dc1cf29 100644 --- a/zed/src/editor/movement.rs +++ b/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(), diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 7e552d4748eef5368ade73f14b279c2ae834de7f..8f3217b2e58521b6dcb9f45ac67e992d90c9b420 100644 --- a/zed/src/file_finder.rs +++ b/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(); diff --git a/zed/src/http.rs b/zed/src/http.rs new file mode 100644 index 0000000000000000000000000000000000000000..30a7a08a519950beee6f28729f2af2f6c9df363a --- /dev/null +++ b/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>; +} + +pub fn client() -> Arc { + Arc::new(surf::client()) +} + +impl HttpClient for surf::Client { + fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result> { + Box::pin(async move { + Ok(self + .send(req) + .await + .map_err(|e| anyhow!("http request failed: {}", e))?) + }) + } +} diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 162c69c1db9e7085107d4f252cb7f3b1c6825992..c9cec56f46da05851471cd20f9e4adb9133dc18c 100644 --- a/zed/src/lib.rs +++ b/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, pub themes: Arc, pub rpc: Arc, + pub user_store: Arc, pub fs: Arc, pub channel_list: ModelHandle, } diff --git a/zed/src/main.rs b/zed/src/main.rs index a7dc346e367ec96641e6a005a4f2d0244f3a8e6a..87426c1ca7bac448aa9b0f2d49ee341687f5f525 100644 --- a/zed/src/main.rs +++ b/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(); } } diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 501779c2b027913126b09a82f43bbde415bbc6c2..d4315a44d42f7de2b40a07eeaabefd7d1994f7c8 100644 --- a/zed/src/rpc.rs +++ b/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, state: RwLock, - auth_callback: Option< - Box Task>>, - >, - connect_callback: Option< - Box Task>>, + authenticate: + Option Task>>>, + establish_connection: Option< + Box< + dyn 'static + + Send + + Sync + + Fn( + &Credentials, + &AsyncAppContext, + ) -> Task>, + >, >, } +#[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 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 + 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, status: (watch::Sender, watch::Receiver), entity_id_extractors: HashMap 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( - &mut self, - login: Login, - connect: Connect, - ) where - Login: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task>, - Connect: 'static + Send + Sync + Fn(u64, &str, &AsyncAppContext) -> Task>, + pub fn override_authenticate(&mut self, authenticate: F) -> &mut Self + where + F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task>, { - 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(&mut self, connect: F) -> &mut Self + where + F: 'static + + Send + + Sync + + Fn(&Credentials, &AsyncAppContext) -> Task>, + { + self.establish_connection = Some(Box::new(connect)); + self + } + + pub fn user_id(&self) -> Option { + self.state + .read() + .credentials + .as_ref() + .map(|credentials| credentials.user_id) } pub fn status(&self) -> watch::Receiver { @@ -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, 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, user_id: u64, conn: Conn, cx: &AsyncAppContext) { + async fn set_connection(self: &Arc, 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, cx: &AsyncAppContext) -> Task> { - if let Some(callback) = self.auth_callback.as_ref() { + fn authenticate(self: &Arc, cx: &AsyncAppContext) -> Task> { + if let Some(callback) = self.authenticate.as_ref() { callback(cx) } else { self.authenticate_with_browser(cx) } } - fn connect( + fn establish_connection( self: &Arc, - user_id: u64, - access_token: &str, + credentials: &Credentials, cx: &AsyncAppContext, - ) -> Task> { - if let Some(callback) = self.connect_callback.as_ref() { - callback(user_id, access_token, cx) + ) -> Task> { + 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, - user_id: u64, - access_token: &str, + credentials: &Credentials, cx: &AsyncAppContext, - ) -> Task> { - let request = - Request::builder().header("Authorization", format!("{} {}", user_id, access_token)); + ) -> Task> { + 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, cx: &AsyncAppContext, - ) -> Task> { + ) -> Task> { 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, 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 { + 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] diff --git a/zed/src/test.rs b/zed/src/test.rs index ce865bbfe58d64c267267cbfbe85321a87a7ca37..7d027a8a1771b41fd2b68844802a2f6939b6448b 100644 --- a/zed/src/test.rs +++ b/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 { 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>>>, connection_id: Mutex>, forbid_connections: AtomicBool, + auth_count: AtomicUsize, + access_token: AtomicUsize, + user_id: u64, } impl FakeServer { @@ -212,40 +220,47 @@ impl FakeServer { client: &mut Arc, cx: &TestAppContext, ) -> Arc { - 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 { + async fn establish_connection( + &self, + credentials: &Credentials, + cx: &AsyncAppContext, + ) -> Result { + 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 BoxFuture<'static, Result>>, +} + +impl FakeHttpClient { + pub fn new(handler: F) -> Arc + where + Fut: 'static + Send + Future>, + 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> { + let future = (self.handler)(req); + Box::pin(async move { future.await.map(Into::into) }) + } +} diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 12496251f4c006de4db4841dd927d51cad1bef07..4458183e6dde3ef563cdd3fac1c60d56ce996553 100644 --- a/zed/src/theme.rs +++ b/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, -} - #[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, 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(), } } } diff --git a/zed/src/theme/resolution.rs b/zed/src/theme/resolution.rs new file mode 100644 index 0000000000000000000000000000000000000000..fd3864e274af20f75fbfe4da54f43fcdcdecc6c3 --- /dev/null +++ b/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 { + let tree = Tree::from_json(value)?; + tree.resolve()?; + tree.to_json() +} + +#[derive(Clone)] +enum Node { + Reference { + path: String, + parent: Option>>, + }, + Object { + base: Option, + children: IndexMap, + resolved: bool, + parent: Option>>, + }, + Array { + children: Vec, + resolved: bool, + parent: Option>>, + }, + String { + value: String, + parent: Option>>, + }, + Number { + value: serde_json::Number, + parent: Option>>, + }, + Bool { + value: bool, + parent: Option>>, + }, + Null { + parent: Option>>, + }, +} + +#[derive(Clone)] +struct Tree(Rc>); + +impl Tree { + pub fn new(node: Node) -> Self { + Self(Rc::new(RefCell::new(node))) + } + + fn from_json(value: Value) -> Result { + 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 { + 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 { + 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> { + 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) -> Result { + 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>>) { + 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()); + } +} diff --git a/zed/src/theme/theme_registry.rs b/zed/src/theme/theme_registry.rs index cd9781afe942f4e5bee4d1c16b6aa5886ae376a0..c5cf8f2fcbd856a7e0a5419f5337e8c198aaca59 100644 --- a/zed/src/theme/theme_registry.rs +++ b/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, } -#[derive(Default)] -struct KeyPathReferenceSet { - references: Vec, - reference_ids_by_source: Vec, - reference_ids_by_target: Vec, - dependencies: Vec<(usize, usize)>, - dependency_counts: Vec, -} - -#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord)] -struct KeyPathReference { - target: KeyPath, - source: KeyPath, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct KeyPath(Vec); - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -enum Key { - Array(usize), - Object(String), -} - impl ThemeRegistry { pub fn new(source: impl AssetSource, font_cache: Arc) -> Arc { 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> { - 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::>(); - - // 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 + '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, - ) { - 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 { - (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, extension: Map) { 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, extension: Map) } } -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, 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::>(); - - 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::>(); - 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 + '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 { diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 09f259281a45e3451b2d9810230f4bddfa35fb52..4bb657d619a6d47e41cf3ed08c1564277fd6c8df 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -58,10 +58,14 @@ impl ThemeSelector { cx: &mut ViewContext, ) -> 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) diff --git a/zed/src/user.rs b/zed/src/user.rs index df98707a8ec907373613d2d6db266934a406149e..54e84d756ff81229a44dcb5291e12fec1618da27 100644 --- a/zed/src/user.rs +++ b/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>, +} pub struct UserStore { users: Mutex>>, + current_user: watch::Receiver>>, rpc: Arc, + http: Arc, + _maintain_current_user: Task<()>, } impl UserStore { - pub fn new(rpc: Arc) -> Self { - Self { + pub fn new( + rpc: Arc, + http: Arc, + executor: &executor::Background, + ) -> Arc { + let (mut current_user_tx, current_user_rx) = watch::channel(); + let (mut this_tx, mut this_rx) = oneshot::channel::>(); + 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) -> 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> { + pub async fn fetch_user(&self, user_id: u64) -> Result> { 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> { + self.current_user.borrow().clone() + } + + pub fn watch_current_user(&self) -> watch::Receiver>> { + 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> { + 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)) +} diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index df10aef54d641b316c8fc567963ee249f1f4abd6..1a83c358760fc43755a853068e18c6b40df5cc59 100644 --- a/zed/src/workspace.rs +++ b/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, languages: Arc, rpc: Arc, + user_store: Arc, fs: Arc, modal: Option, center: PaneGroup, @@ -354,6 +355,7 @@ pub struct Workspace { (usize, Arc), postage::watch::Receiver, Arc>>>, >, + _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 { &self.active_pane } + + fn render_connection_status(&self) -> Option { + 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) -> 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::(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| { diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 66a062fb65d5355e3c98f8c62f41153f71fe1f42..31ec57354ad131f8542f211c5ee88cfb2520ad8f 100644 --- a/zed/src/workspace/pane.rs +++ b/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::(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::(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") } diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 9ee1190eefadb869e631bd18d079ae3a0395574d..3ceba15df0051701873c472a993ced0fe0b0ed64 100644 --- a/zed/src/workspace/sidebar.rs +++ b/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::(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::( + 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.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 { diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 7b2fea91756c42a5cb20b05a0314d1a40e61029a..f952bef22f44d1a7c420eb97e3ea13922e62078c 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -268,6 +268,13 @@ impl Worktree { } } + pub fn languages(&self) -> &Arc { + match self { + Worktree::Local(worktree) => &worktree.languages, + Worktree::Remote(worktree) => &worktree.languages, + } + } + pub fn snapshot(&self) -> Snapshot { match self { Worktree::Local(worktree) => worktree.snapshot(), diff --git a/zrpc/proto/zed.proto b/zrpc/proto/zed.proto index c9f1dc0f80dbddb01d37769a2cac35d11d455d30..4e42441eb276ad36e71938946f7c229cc9799e5f 100644 --- a/zrpc/proto/zed.proto +++ b/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; } diff --git a/zrpc/src/conn.rs b/zrpc/src/conn.rs index e67b4fa58708232ff7197175f476191ef2153ec5..5ca845d13f1d489861fe076b2258a8d84bf8d615 100644 --- a/zrpc/src/conn.rs +++ b/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>, pub(crate) rx: Box< @@ -13,7 +13,7 @@ pub struct Conn { >, } -impl Conn { +impl Connection { pub fn new(stream: S) -> Self where S: 'static diff --git a/zrpc/src/lib.rs b/zrpc/src/lib.rs index b3973cae19ddf6d1b18ef447547e0bc56b6aa98d..a7bb44774b8e700443f753e3fb47c1176ef80142 100644 --- a/zrpc/src/lib.rs +++ b/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::*; diff --git a/zrpc/src/peer.rs b/zrpc/src/peer.rs index 75db257f55bd23752e0f1e2e72bd616f1df8ed1c..eeda034e9581ce215ee01821cff3e82bab70ed25 100644 --- a/zrpc/src/peer.rs +++ b/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 TypedEnvelope { } pub struct Peer { - connections: RwLock>, + connections: RwLock>, next_connection_id: AtomicU32, } #[derive(Clone)] -struct Connection { +struct ConnectionState { outgoing_tx: mpsc::Sender, next_message_id: Arc, response_channels: Arc>>>, @@ -100,7 +100,7 @@ impl Peer { pub async fn add_connection( self: &Arc, - conn: Conn, + connection: Connection, ) -> ( ConnectionId, impl Future> + 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> { 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> { 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> { 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> { 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, connection_id: ConnectionId, - ) -> impl Future> { + ) -> impl Future> { 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(); diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index af9dbf3abcdf070d757635edc77bc4ebc78ed200..b2d4de3bbf501c2ce5c7e28fb0c7f7355171a790 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -248,3 +248,22 @@ impl From for Timestamp { } } } + +impl From 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 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 + } +}