From 96ade8668f8317a44a539e21c959454f6e1aadc4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 16:48:44 +0200 Subject: [PATCH 01/55] Start on image rendering --- Cargo.lock | 65 +++++++- gpui/Cargo.toml | 1 + gpui/src/elements.rs | 24 +-- gpui/src/elements/image.rs | 65 ++++++++ gpui/src/image_data.rs | 31 ++++ gpui/src/lib.rs | 2 + gpui/src/platform/mac/atlas.rs | 84 ++++++++-- gpui/src/platform/mac/renderer.rs | 173 +++++++++++++++++--- gpui/src/platform/mac/shaders/shaders.h | 51 ++++-- gpui/src/platform/mac/shaders/shaders.metal | 33 ++++ gpui/src/scene.rs | 22 ++- zed/Cargo.toml | 1 + zed/src/workspace.rs | 33 ++-- 13 files changed, 510 insertions(+), 75 deletions(-) create mode 100644 gpui/src/elements/image.rs create mode 100644 gpui/src/image_data.rs diff --git a/Cargo.lock b/Cargo.lock index 562da86feec01872e2eb71718d775b41f454dde7..c749b35f2dc5e329fea6b282a3cab690f7156e6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -814,7 +814,7 @@ dependencies = [ "error-chain", "glob 0.2.11", "icns", - "image", + "image 0.12.4", "libflate", "md5", "msi", @@ -2102,6 +2102,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 +2177,7 @@ dependencies = [ "font-kit", "foreign-types", "gpui_macros", + "image 0.23.14", "lazy_static", "log", "metal", @@ -2462,15 +2473,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" @@ -3014,6 +3044,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" @@ -5129,6 +5170,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" @@ -5694,6 +5746,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" @@ -5836,6 +5894,7 @@ dependencies = [ "gpui", "http-auth-basic", "ignore", + "image 0.23.14", "lazy_static", "libc", "log", diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 11a855cd40c087c4aed6fc278adfbdb7ded92952..7cc202e0b642dd6a22caa58b6008b8f9dee7b03c 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -11,6 +11,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/elements.rs b/gpui/src/elements.rs index 6d7429222c7c1bf74d259932cbc5b3b614892ce3..252cebd7334f57874a8fd6164da15ad0ba3c2986 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,25 +17,12 @@ 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}, json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, diff --git a/gpui/src/elements/image.rs b/gpui/src/elements/image.rs new file mode 100644 index 0000000000000000000000000000000000000000..44f39d8a3078173ac64f946f6c6a18cf4b4d5684 --- /dev/null +++ b/gpui/src/elements/image.rs @@ -0,0 +1,65 @@ +use crate::{ + geometry::{rect::RectF, vector::Vector2F}, + json::{json, ToJson}, + scene, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, PaintContext, + SizeConstraint, +}; +use std::sync::Arc; + +pub struct Image(Arc); + +impl Image { + pub fn new(data: Arc) -> Self { + Self(data) + } +} + +impl Element for Image { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + _: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + (constraint.max, ()) + } + + fn paint( + &mut self, + bounds: RectF, + _: RectF, + _: &mut Self::LayoutState, + cx: &mut PaintContext, + ) -> Self::PaintState { + cx.scene.push_image(scene::Image { + bounds, + data: self.0.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/image_data.rs b/gpui/src/image_data.rs new file mode 100644 index 0000000000000000000000000000000000000000..352393e3b57328f8373134c23edcb9b9ed5790ae --- /dev/null +++ b/gpui/src/image_data.rs @@ -0,0 +1,31 @@ +use crate::geometry::vector::{vec2i, Vector2I}; +use image::{Bgra, ImageBuffer}; +use std::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) + } +} 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/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/renderer.rs b/gpui/src/platform/mac/renderer.rs index e12a52d6134dbd2c5d110a570f52920c449a287d..215863d881c9f2bce84e6b6cbb20fe16b0481447 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -1,13 +1,15 @@ -use super::{atlas::AtlasAllocator, sprite_cache::SpriteCache}; +use super::{ + atlas::{self, AtlasAllocator}, + sprite_cache::SpriteCache, +}; use crate::{ color::Color, geometry::{ - rect::RectF, + rect::{RectF, RectI}, 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}; @@ -21,9 +23,13 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio pub struct Renderer { sprite_cache: SpriteCache, path_atlases: AtlasAllocator, + image_atlases: AtlasAllocator, + prev_rendered_images: HashMap, + curr_rendered_images: HashMap, 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 +70,10 @@ impl Renderer { ); let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts); - let path_atlases = build_path_atlas_allocator(MTLPixelFormat::R8Unorm, &device); + let path_atlases = + AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor()); + let image_atlases = + AtlasAllocator::new(device.clone(), build_image_atlas_texture_descriptor()); let quad_pipeline_state = build_pipeline_state( &device, &library, @@ -89,6 +98,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, @@ -100,9 +117,13 @@ impl Renderer { Self { sprite_cache, path_atlases, + image_atlases, + prev_rendered_images: Default::default(), + curr_rendered_images: Default::default(), quad_pipeline_state, shadow_pipeline_state, sprite_pipeline_state, + image_pipeline_state, path_atlas_pipeline_state, unit_vertices, instances, @@ -117,6 +138,12 @@ impl Renderer { output: &metal::TextureRef, ) { let mut offset = 0; + + mem::swap( + &mut self.curr_rendered_images, + &mut self.prev_rendered_images, + ); + let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer); self.render_layers( scene, @@ -130,6 +157,11 @@ impl Renderer { location: 0, length: offset as NSUInteger, }); + + for (id, _) in self.prev_rendered_images.values() { + self.image_atlases.deallocate(*id); + } + self.prev_rendered_images.clear(); } fn render_path_atlases( @@ -146,11 +178,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 +194,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 +205,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 +348,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, @@ -602,6 +641,97 @@ 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 (alloc_id, atlas_bounds) = self + .prev_rendered_images + .remove(&image.data.id) + .or_else(|| self.curr_rendered_images.get(&image.data.id).copied()) + .unwrap_or_else(|| { + self.image_atlases + .upload(image.data.size(), image.data.as_bytes()) + }); + self.curr_rendered_images + .insert(image.data.id, (alloc_id, atlas_bounds)); + 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(), + }); + } + + 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_atlases.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 +838,23 @@ 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 build_image_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(MTLPixelFormat::BGRA8Unorm); + texture_descriptor } fn align_offset(offset: &mut usize) { @@ -803,9 +937,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..b06e2e565fbb88ec7b2c4e3b580fd6d222b9cb1b 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,37 @@ 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; +} GPUIImage; diff --git a/gpui/src/platform/mac/shaders/shaders.metal b/gpui/src/platform/mac/shaders/shaders.metal index 91e5ea129577d9443ee0f395c0e01df72f37b702..c83bac43ad59e3cb0d1acde7347aa55cab0962db 100644 --- a/gpui/src/platform/mac/shaders/shaders.metal +++ b/gpui/src/platform/mac/shaders/shaders.metal @@ -217,6 +217,39 @@ fragment float4 sprite_fragment( return color; } +struct ImageFragmentInput { + float4 position [[position]]; + float2 atlas_position; +}; + +vertex ImageFragmentInput 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 ImageFragmentInput { + device_position, + atlas_position, + }; +} + +fragment float4 image_fragment( + ImageFragmentInput input [[stage_in]], + texture2d atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]] +) { + constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear); + return atlas.sample(atlas_sampler, input.atlas_position); +} + struct PathAtlasVertexOutput { float4 position [[position]]; float2 st_position; diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 401918c5fe014426a98496d7f4e2d31b903bb274..ab6ca71a06465d9c1ef2a02260f543730734349c 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,11 @@ pub struct PathVertex { pub st_position: Vector2F, } +pub struct Image { + pub bounds: RectF, + pub data: Arc, +} + impl Scene { pub fn new(scale_factor: f32) -> Self { let stacking_context = StackingContext::new(None); @@ -166,6 +173,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 +251,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 +279,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/zed/Cargo.toml b/zed/Cargo.toml index 985901c50cf8a14d4331ed73f89a98fd1323d28d..2b36db9ba86152b040fa6edcf74da5c6bc68a0d2 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -30,6 +30,7 @@ futures = "0.3" gpui = { path = "../gpui" } http-auth-basic = "0.1.3" ignore = "0.4" +image = "0.23" lazy_static = "1.4.0" libc = "0.2" log = "0.4" diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index df10aef54d641b316c8fc567963ee249f1f4abd6..92e05d7a8e75ac9c2d52dc2834b731127a257e6c 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -21,7 +21,7 @@ use gpui::{ json::to_string_pretty, keymap::Binding, platform::WindowOptions, - AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, + AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle, }; @@ -354,10 +354,19 @@ pub struct Workspace { (usize, Arc), postage::watch::Receiver, Arc>>>, >, + image: Arc, } impl Workspace { pub fn new(app_state: &AppState, cx: &mut ViewContext) -> Self { + let image_bytes = crate::assets::Assets::get("images/as-cii.jpeg").unwrap(); + let image = image::io::Reader::new(std::io::Cursor::new(&*image_bytes.data)) + .with_guessed_format() + .unwrap() + .decode() + .unwrap() + .into_bgra8(); + let pane = cx.add_view(|_| Pane::new(app_state.settings.clone())); let pane_id = pane.id(); cx.subscribe(&pane, move |me, _, event, cx| { @@ -401,6 +410,7 @@ impl Workspace { worktrees: Default::default(), items: Default::default(), loading_items: Default::default(), + image: ImageData::new(image), } } @@ -954,17 +964,16 @@ impl View for Workspace { Flex::column() .with_child( ConstrainedBox::new( - Container::new( - Align::new( - Label::new( - "zed".into(), - theme.workspace.titlebar.label.clone() - ).boxed() - ) - .boxed() - ) - .with_style(&theme.workspace.titlebar.container) - .boxed(), + Image::new(self.image.clone()).boxed() + // Container::new( + // Align::new( + // Label::new("zed".into(), theme.workspace.titlebar.label.clone()) + // .boxed(), + // ) + // .boxed(), + // ) + // .with_style(&theme.workspace.titlebar.container) + // .boxed(), ) .with_height(32.) .named("titlebar"), From 95da665095297f5f58a09a3eb57045bdfcfec2a8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 17:49:11 +0200 Subject: [PATCH 02/55] Allow passing a corner radius and borders to rendered images Co-Authored-By: Nathan Sobo --- gpui/src/color.rs | 4 + gpui/src/elements/image.rs | 30 +++++- gpui/src/platform/mac/renderer.rs | 8 ++ gpui/src/platform/mac/shaders/shaders.h | 6 ++ gpui/src/platform/mac/shaders/shaders.metal | 101 +++++++++++--------- gpui/src/scene.rs | 2 + 6 files changed, 101 insertions(+), 50 deletions(-) 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/image.rs b/gpui/src/elements/image.rs index 44f39d8a3078173ac64f946f6c6a18cf4b4d5684..2611478fe0b85e93ee9f55147e0b758e53427153 100644 --- a/gpui/src/elements/image.rs +++ b/gpui/src/elements/image.rs @@ -1,16 +1,34 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::{json, ToJson}, - scene, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, PaintContext, - SizeConstraint, + scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, + PaintContext, SizeConstraint, }; use std::sync::Arc; -pub struct Image(Arc); +pub struct Image { + data: Arc, + border: Border, + corner_radius: f32, +} impl Image { pub fn new(data: Arc) -> Self { - Self(data) + Self { + data, + border: Default::default(), + corner_radius: Default::default(), + } + } + + pub fn with_corner_radius(mut self, corner_radius: f32) -> Self { + self.corner_radius = corner_radius; + self + } + + pub fn with_border(mut self, border: Border) -> Self { + self.border = border; + self } } @@ -35,7 +53,9 @@ impl Element for Image { ) -> Self::PaintState { cx.scene.push_image(scene::Image { bounds, - data: self.0.clone(), + border: self.border, + corner_radius: self.corner_radius, + data: self.data.clone(), }); } diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index 215863d881c9f2bce84e6b6cbb20fe16b0481447..6f712f29b3bbbfdf647e7b0ddeaebbcd43594b0c 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -657,6 +657,8 @@ impl Renderer { 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 .prev_rendered_images .remove(&image.data.id) @@ -675,6 +677,12 @@ impl Renderer { 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, }); } diff --git a/gpui/src/platform/mac/shaders/shaders.h b/gpui/src/platform/mac/shaders/shaders.h index b06e2e565fbb88ec7b2c4e3b580fd6d222b9cb1b..1b6ad3f26f98122b11afeaff02006dd2ef0e8daa 100644 --- a/gpui/src/platform/mac/shaders/shaders.h +++ b/gpui/src/platform/mac/shaders/shaders.h @@ -97,4 +97,10 @@ typedef struct 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 c83bac43ad59e3cb0d1acde7347aa55cab0962db..00338b30e71c234147f88916f011f923221e11fc 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,12 @@ fragment float4 quad_fragment( float4 color; if (border_width == 0.) { - color = coloru_to_colorf(input.background_color); + color = input.background_color; } else { float inset_distance = distance + border_width; color = mix( - coloru_to_colorf(input.border_color), - coloru_to_colorf(input.background_color), + input.border_color, + input.background_color, saturate(0.5 - inset_distance) ); } @@ -109,6 +82,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,12 +223,7 @@ fragment float4 sprite_fragment( return color; } -struct ImageFragmentInput { - float4 position [[position]]; - float2 atlas_position; -}; - -vertex ImageFragmentInput image_vertex( +vertex QuadFragmentInput image_vertex( uint unit_vertex_id [[vertex_id]], uint image_id [[instance_id]], constant float2 *unit_vertices [[buffer(GPUIImageVertexInputIndexVertices)]], @@ -236,18 +237,28 @@ vertex ImageFragmentInput image_vertex( float4 device_position = to_device_position(position, *viewport_size); float2 atlas_position = (unit_vertex * image.source_size + image.atlas_origin) / *atlas_size; - return ImageFragmentInput { + 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( - ImageFragmentInput input [[stage_in]], + QuadFragmentInput input [[stage_in]], texture2d atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]] ) { constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear); - return atlas.sample(atlas_sampler, input.atlas_position); + input.background_color = atlas.sample(atlas_sampler, input.atlas_position); + return quad_sdf(input); } struct PathAtlasVertexOutput { diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index ab6ca71a06465d9c1ef2a02260f543730734349c..1b9c863647205a09fcafde18c2abdda1aa36e922 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -128,6 +128,8 @@ pub struct PathVertex { pub struct Image { pub bounds: RectF, + pub border: Border, + pub corner_radius: f32, pub data: Arc, } From d15eda53f67ed6489be55f458a336c7d6a53d7df Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 17:57:01 +0200 Subject: [PATCH 03/55] Use `AtlasAllocator` in `SpriteCache` Co-Authored-By: Nathan Sobo --- gpui/src/platform/mac/renderer.rs | 13 ++-- gpui/src/platform/mac/sprite_cache.rs | 101 ++++---------------------- 2 files changed, 20 insertions(+), 94 deletions(-) diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index 6f712f29b3bbbfdf647e7b0ddeaebbcd43594b0c..d650c2d9170ee98d6fdad9fe11d1040b1900ee04 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -598,11 +598,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); @@ -612,13 +607,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), 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) } } From bd4d73bb2714f9ad4d41110c4e7b67c08612408d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 18:11:59 +0200 Subject: [PATCH 04/55] Extract image rasterization into `ImageCache` Co-Authored-By: Nathan Sobo --- gpui/src/platform/mac.rs | 1 + gpui/src/platform/mac/image_cache.rs | 49 +++++++++++++++++++++++++++ gpui/src/platform/mac/renderer.rs | 50 +++++----------------------- 3 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 gpui/src/platform/mac/image_cache.rs 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/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/renderer.rs b/gpui/src/platform/mac/renderer.rs index d650c2d9170ee98d6fdad9fe11d1040b1900ee04..369696d47838d25afc105dcfa2426e905c19b5c2 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -1,11 +1,8 @@ -use super::{ - atlas::{self, AtlasAllocator}, - sprite_cache::SpriteCache, -}; +use super::{atlas::AtlasAllocator, image_cache::ImageCache, sprite_cache::SpriteCache}; use crate::{ color::Color, geometry::{ - rect::{RectF, RectI}, + rect::RectF, vector::{vec2f, vec2i, Vector2F}, }, platform, @@ -22,10 +19,8 @@ const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decisio pub struct Renderer { sprite_cache: SpriteCache, + image_cache: ImageCache, path_atlases: AtlasAllocator, - image_atlases: AtlasAllocator, - prev_rendered_images: HashMap, - curr_rendered_images: HashMap, quad_pipeline_state: metal::RenderPipelineState, shadow_pipeline_state: metal::RenderPipelineState, sprite_pipeline_state: metal::RenderPipelineState, @@ -70,10 +65,9 @@ impl Renderer { ); let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts); + let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768)); let path_atlases = AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor()); - let image_atlases = - AtlasAllocator::new(device.clone(), build_image_atlas_texture_descriptor()); let quad_pipeline_state = build_pipeline_state( &device, &library, @@ -116,10 +110,8 @@ impl Renderer { ); Self { sprite_cache, + image_cache, path_atlases, - image_atlases, - prev_rendered_images: Default::default(), - curr_rendered_images: Default::default(), quad_pipeline_state, shadow_pipeline_state, sprite_pipeline_state, @@ -139,11 +131,6 @@ impl Renderer { ) { let mut offset = 0; - mem::swap( - &mut self.curr_rendered_images, - &mut self.prev_rendered_images, - ); - let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer); self.render_layers( scene, @@ -157,11 +144,7 @@ impl Renderer { location: 0, length: offset as NSUInteger, }); - - for (id, _) in self.prev_rendered_images.values() { - self.image_atlases.deallocate(*id); - } - self.prev_rendered_images.clear(); + self.image_cache.finish_frame(); } fn render_path_atlases( @@ -660,16 +643,7 @@ impl Renderer { 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 - .prev_rendered_images - .remove(&image.data.id) - .or_else(|| self.curr_rendered_images.get(&image.data.id).copied()) - .unwrap_or_else(|| { - self.image_atlases - .upload(image.data.size(), image.data.as_bytes()) - }); - self.curr_rendered_images - .insert(image.data.id, (alloc_id, atlas_bounds)); + let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data); images_by_atlas .entry(alloc_id.atlas_id) .or_insert_with(Vec::new) @@ -707,7 +681,7 @@ impl Renderer { "instance buffer exhausted" ); - let texture = self.image_atlases.texture(atlas_id).unwrap(); + let texture = self.image_cache.atlas_texture(atlas_id).unwrap(); command_encoder.set_vertex_buffer( shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexImages as u64, Some(&self.instances), @@ -858,14 +832,6 @@ fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor { texture_descriptor } -fn build_image_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(MTLPixelFormat::BGRA8Unorm); - texture_descriptor -} - fn align_offset(offset: &mut usize) { let r = *offset % 256; if r > 0 { From 2cf1c697c2120146d29e6aea625f8c4edc1e471b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 18:53:41 +0200 Subject: [PATCH 05/55] Render a signed out icon in titlebar Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- zed/assets/icons/signed-out-12.svg | 3 ++ zed/assets/themes/_base.toml | 8 ++-- zed/src/theme.rs | 12 +++++- zed/src/workspace.rs | 59 +++++++++++++++++---------- zed/src/workspace/sidebar.rs | 64 +++++++++++++++++------------- 5 files changed, 94 insertions(+), 52 deletions(-) create mode 100644 zed/assets/icons/signed-out-12.svg 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..124aa178909a107dae4ab26d0c2296566db3e97c 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -7,7 +7,9 @@ pane_divider = { width = 1, color = "$border.0" } [workspace.titlebar] border = { width = 1, bottom = true, color = "$border.0" } -text = { extends = "$text.0" } +title = "$text.0" +icon_width = 16 +icon_signed_out = "$text.2.color" [workspace.tab] text = "$text.2" @@ -26,7 +28,7 @@ background = "$surface.1" text = "$text.0" [workspace.sidebar] -padding = { left = 12, right = 12 } +width = 36 border = { right = true, width = 1, color = "$border.0" } [workspace.sidebar.resize_handle] @@ -35,7 +37,7 @@ background = "$border.0" [workspace.sidebar.icon] color = "$text.2.color" -height = 18 +height = 16 [workspace.sidebar.active_icon] extends = "$workspace.sidebar.icon" diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 12496251f4c006de4db4841dd927d51cad1bef07..f0c17a1cb8876811177f0de76311cd9d818a83a4 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -34,7 +34,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 +42,15 @@ pub struct Workspace { pub right_sidebar: Sidebar, } +#[derive(Clone, Deserialize)] +pub struct Titlebar { + #[serde(flatten)] + pub container: ContainerStyle, + pub title: TextStyle, + pub icon_width: f32, + pub icon_signed_out: Color, +} + #[derive(Clone, Deserialize)] pub struct Tab { #[serde(flatten)] @@ -60,6 +69,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, diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 92e05d7a8e75ac9c2d52dc2834b731127a257e6c..1dc672f728c3055ce046e3798836b1fd7f97ddb2 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -21,7 +21,7 @@ use gpui::{ json::to_string_pretty, keymap::Binding, platform::WindowOptions, - AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, MutableAppContext, + AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle, }; @@ -354,19 +354,10 @@ pub struct Workspace { (usize, Arc), postage::watch::Receiver, Arc>>>, >, - image: Arc, } impl Workspace { pub fn new(app_state: &AppState, cx: &mut ViewContext) -> Self { - let image_bytes = crate::assets::Assets::get("images/as-cii.jpeg").unwrap(); - let image = image::io::Reader::new(std::io::Cursor::new(&*image_bytes.data)) - .with_guessed_format() - .unwrap() - .decode() - .unwrap() - .into_bgra8(); - let pane = cx.add_view(|_| Pane::new(app_state.settings.clone())); let pane_id = pane.id(); cx.subscribe(&pane, move |me, _, event, cx| { @@ -410,7 +401,6 @@ impl Workspace { worktrees: Default::default(), items: Default::default(), loading_items: Default::default(), - image: ImageData::new(image), } } @@ -946,6 +936,24 @@ impl Workspace { pub fn active_pane(&self) -> &ViewHandle { &self.active_pane } + + fn render_account_status(&self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme; + ConstrainedBox::new( + Align::new( + ConstrainedBox::new( + Svg::new("icons/signed-out-12.svg") + .with_color(theme.workspace.titlebar.icon_signed_out) + .boxed(), + ) + .with_width(theme.workspace.titlebar.icon_width) + .boxed(), + ) + .boxed(), + ) + .with_width(theme.workspace.right_sidebar.width) + .boxed() + } } impl Entity for Workspace { @@ -964,16 +972,25 @@ impl View for Workspace { Flex::column() .with_child( ConstrainedBox::new( - Image::new(self.image.clone()).boxed() - // Container::new( - // Align::new( - // Label::new("zed".into(), theme.workspace.titlebar.label.clone()) - // .boxed(), - // ) - // .boxed(), - // ) - // .with_style(&theme.workspace.titlebar.container) - // .boxed(), + Container::new( + Stack::new() + .with_child( + Align::new( + Label::new( + "zed".into(), + theme.workspace.titlebar.title.clone(), + ) + .boxed(), + ) + .boxed(), + ) + .with_child( + Align::new(self.render_account_status(cx)).right().boxed(), + ) + .boxed(), + ) + .with_style(&theme.workspace.titlebar.container) + .boxed(), ) .with_height(32.) .named("titlebar"), diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 9ee1190eefadb869e631bd18d079ae3a0395574d..7cdb6447a7ff55f8deece33ab5dcbf9738badeb0 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() } From 428c491542b32061910013b8d9576991ce4c92ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Sep 2021 19:21:03 +0200 Subject: [PATCH 06/55] WIP: Start on rendering avatars Co-Authored-By: Max Brunsfeld Co-Authored-By: Nathan Sobo --- zed/src/channel.rs | 2 +- zed/src/lib.rs | 1 + zed/src/main.rs | 3 ++- zed/src/test.rs | 3 ++- zed/src/user.rs | 2 +- zed/src/workspace.rs | 3 +++ 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index aa182c0540997e55ed8b8165b752ebaf7f30ac1a..8e8b2964ff6e7dad478f9dcf109fac049f153649 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -443,7 +443,7 @@ 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, body: message.body, diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 162c69c1db9e7085107d4f252cb7f3b1c6825992..d672d234bfbcce21a64714f13ba46cd23926fe91 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -42,6 +42,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..852bfaec2340d0dbb4635617b4e0b76d4d475080 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -43,8 +43,9 @@ fn main() { 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), }); diff --git a/zed/src/test.rs b/zed/src/test.rs index ce865bbfe58d64c267267cbfbe85321a87a7ca37..3557c0e3c4a6d65f0e85d2a77846a7fa494c41e5 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -170,8 +170,9 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { 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), }) } diff --git a/zed/src/user.rs b/zed/src/user.rs index df98707a8ec907373613d2d6db266934a406149e..9f821f048c57832e3f1e5f371e3bd881f2edce47 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -36,7 +36,7 @@ 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); } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 1dc672f728c3055ce046e3798836b1fd7f97ddb2..c603ec5bd474071336845afe46e5ae3ad6ac1c81 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -10,6 +10,7 @@ use crate::{ project_browser::ProjectBrowser, rpc, settings::Settings, + user, worktree::{File, Worktree}, AppState, }; @@ -341,6 +342,7 @@ pub struct Workspace { pub settings: watch::Receiver, languages: Arc, rpc: Arc, + user_store: Arc, fs: Arc, modal: Option, center: PaneGroup, @@ -395,6 +397,7 @@ 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, From f0019e3725d32f72d7fdbaae956cf1d787b8f19c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 12:29:56 -0600 Subject: [PATCH 07/55] WIP --- gpui/src/app.rs | 10 +++++ server/src/rpc.rs | 10 ++--- zed/src/channel.rs | 4 +- zed/src/main.rs | 2 +- zed/src/rpc.rs | 12 +++--- zed/src/test.rs | 2 +- zed/src/user.rs | 88 +++++++++++++++++++++++++++++++++++++++++--- zed/src/workspace.rs | 38 ++++++++++++++----- 8 files changed, 135 insertions(+), 31 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index c1ce8fdba0d266726a8db83eaba36fe3fe3f4de6..4b18af06b4bdb0fef7c3fb47e819fa3153e6c8f2 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> { diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 2bd0eac625e836d32bf4417be107a11a766dadb0..539068f2d3d55e8cb11c085c52c0a84957ce4b6b 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1512,7 +1512,7 @@ mod tests { .await .unwrap(); - let user_store_a = Arc::new(UserStore::new(client_a.clone())); + let user_store_a = UserStore::new(client_a.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()) @@ -1537,7 +1537,7 @@ mod tests { }) .await; - let user_store_b = Arc::new(UserStore::new(client_b.clone())); + let user_store_b = UserStore::new(client_b.clone(), cx_b.background().as_ref()); 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()) @@ -1637,7 +1637,7 @@ mod tests { .await .unwrap(); - let user_store_a = Arc::new(UserStore::new(client_a.clone())); + let user_store_a = UserStore::new(client_a.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()) @@ -1713,7 +1713,7 @@ mod tests { .await .unwrap(); - let user_store_a = Arc::new(UserStore::new(client_a.clone())); + let user_store_a = UserStore::new(client_a.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()) @@ -1739,7 +1739,7 @@ mod tests { }) .await; - let user_store_b = Arc::new(UserStore::new(client_b.clone())); + let user_store_b = UserStore::new(client_b.clone(), cx_b.background().as_ref()); 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()) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 8e8b2964ff6e7dad478f9dcf109fac049f153649..bf1237d8359f4e4c857b419fbc663b6182325c60 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -118,7 +118,7 @@ impl ChannelList { cx.notify(); }); } - rpc::Status::Disconnected { .. } => { + rpc::Status::SignedOut { .. } => { this.update(&mut cx, |this, cx| { this.available_channels = None; this.channels.clear(); @@ -503,7 +503,7 @@ mod tests { let user_id = 5; let mut client = Client::new(); 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(), 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)); diff --git a/zed/src/main.rs b/zed/src/main.rs index 852bfaec2340d0dbb4635617b4e0b76d4d475080..f585ed5b82e4afc0bd261f244a38504a58e2c5fb 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -37,7 +37,7 @@ fn main() { app.run(move |cx| { let rpc = rpc::Client::new(); - let user_store = Arc::new(UserStore::new(rpc.clone())); + let user_store = UserStore::new(rpc.clone(), cx.background()); let app_state = Arc::new(AppState { languages: languages.clone(), settings_tx: Arc::new(Mutex::new(settings_tx)), diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 501779c2b027913126b09a82f43bbde415bbc6c2..69bc33a62e1c4065f7c946b239e72d7039ef5c49 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -39,7 +39,7 @@ pub struct Client { #[derive(Copy, Clone, Debug)] pub enum Status { - Disconnected, + SignedOut, Authenticating, Connecting { user_id: u64, @@ -73,7 +73,7 @@ struct ClientState { impl Default for ClientState { fn default() -> Self { Self { - status: watch::channel_with(Status::Disconnected), + status: watch::channel_with(Status::SignedOut), entity_id_extractors: Default::default(), model_handlers: Default::default(), _maintain_connection: None, @@ -167,7 +167,7 @@ impl Client { } })); } - Status::Disconnected => { + Status::SignedOut => { state._maintain_connection.take(); } _ => {} @@ -232,7 +232,7 @@ impl Client { cx: &AsyncAppContext, ) -> anyhow::Result<()> { let was_disconnected = match *self.status().borrow() { - Status::Disconnected => true, + Status::SignedOut => true, Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { false } @@ -324,7 +324,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); @@ -470,7 +470,7 @@ impl Client { 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(()) } diff --git a/zed/src/test.rs b/zed/src/test.rs index 3557c0e3c4a6d65f0e85d2a77846a7fa494c41e5..019969ee4f75ee1e463c3abbcc3f75950c9ac635 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -164,7 +164,7 @@ 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 user_store = UserStore::new(rpc.clone(), cx.background()); Arc::new(AppState { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, diff --git a/zed/src/user.rs b/zed/src/user.rs index 9f821f048c57832e3f1e5f371e3bd881f2edce47..6c02c0a284e6dcca3ebd32d3ba601f15808fb948 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -1,22 +1,73 @@ -use crate::rpc::Client; +use crate::{ + rpc::{Client, Status}, + util::TryFutureExt, +}; use anyhow::{anyhow, Result}; +use gpui::{elements::Image, executor, ImageData, Task}; use parking_lot::Mutex; +use postage::{prelude::Stream, sink::Sink, watch}; use std::{collections::HashMap, sync::Arc}; +use surf::{ + http::{Method, Request}, + HttpClient, Url, +}; use zrpc::proto; -pub use proto::User; +pub struct User { + id: u64, + github_login: String, + avatar: Option, +} pub struct UserStore { users: Mutex>>, + current_user: watch::Receiver>>, rpc: Arc, + http: Arc, + _maintain_current_user: Option>, } 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 = Arc::new(Self { users: Default::default(), - rpc, - } + current_user: current_user_rx, + rpc: rpc.clone(), + http, + _maintain_current_user: None, + }); + + let task = { + let this = Arc::downgrade(&this); + executor.spawn(async move { + let mut status = rpc.status(); + while let Some(status) = status.recv().await { + match status { + Status::Connected { user_id, .. } => { + if let Some(this) = this.upgrade() { + current_user_tx + .send(this.fetch_user(user_id).log_err().await) + .await + .ok(); + } + } + Status::SignedOut => { + current_user_tx.send(None).await.ok(); + } + _ => {} + } + } + }) + }; + Arc::get_mut(&mut this).unwrap()._maintain_current_user = Some(task); + + this } pub async fn load_users(&self, mut user_ids: Vec) -> Result<()> { @@ -56,4 +107,29 @@ impl UserStore { Err(anyhow!("server responded with no users")) } } + + pub fn current_user(&self) -> &watch::Receiver>> { + &self.current_user + } +} + +impl User { + async fn new(message: proto::User, http: &dyn HttpClient) -> Self { + let avatar = fetch_avatar(http, &message.avatar_url).await.log_err(); + User { + id: message.id, + github_login: message.github_login, + avatar, + } + } +} + +async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { + let url = Url::parse(url)?; + let request = Request::new(Method::Get, url); + let response = http.send(request).await?; + let bytes = response.body_bytes().await?; + 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 c603ec5bd474071336845afe46e5ae3ad6ac1c81..1410b6ece55ea3a0bee1c87d9b67377ec07cbaa6 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -29,7 +29,7 @@ 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::{ @@ -356,6 +356,7 @@ pub struct Workspace { (usize, Arc), postage::watch::Receiver, Arc>>>, >, + _observe_current_user: Task<()>, } impl Workspace { @@ -389,6 +390,18 @@ impl Workspace { ); right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); + let mut current_user = app_state.user_store.current_user().clone(); + let _observe_current_user = cx.spawn_weak(|this, mut cx| async move { + current_user.recv().await; + while current_user.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()), @@ -404,6 +417,7 @@ impl Workspace { worktrees: Default::default(), items: Default::default(), loading_items: Default::default(), + _observe_current_user, } } @@ -940,17 +954,21 @@ impl Workspace { &self.active_pane } - fn render_account_status(&self, cx: &mut RenderContext) -> ElementBox { + fn render_current_user(&self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; + let avatar = if let Some(current_user) = self.user_store.current_user().borrow().as_ref() { + todo!() + } else { + Svg::new("icons/signed-out-12.svg") + .with_color(theme.workspace.titlebar.icon_signed_out) + .boxed() + }; + ConstrainedBox::new( Align::new( - ConstrainedBox::new( - Svg::new("icons/signed-out-12.svg") - .with_color(theme.workspace.titlebar.icon_signed_out) - .boxed(), - ) - .with_width(theme.workspace.titlebar.icon_width) - .boxed(), + ConstrainedBox::new(avatar) + .with_width(theme.workspace.titlebar.icon_width) + .boxed(), ) .boxed(), ) @@ -988,7 +1006,7 @@ impl View for Workspace { .boxed(), ) .with_child( - Align::new(self.render_account_status(cx)).right().boxed(), + Align::new(self.render_current_user(cx)).right().boxed(), ) .boxed(), ) From 84d4bb6186ad4a1acc4357a2aacc446f92c6edf0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Sep 2021 12:09:49 -0700 Subject: [PATCH 08/55] Introduce HttpClient trait, use it to fetch avatars in UserStore * Add a FakeHttpClient for tests --- gpui/src/image_data.rs | 18 ++++++-- server/src/rpc.rs | 2 + zed/src/channel.rs | 8 ++-- zed/src/http.rs | 26 ++++++++++++ zed/src/lib.rs | 1 + zed/src/main.rs | 5 ++- zed/src/test.rs | 36 +++++++++++++++- zed/src/user.rs | 94 +++++++++++++++++++++++------------------- zed/src/workspace.rs | 10 ++++- 9 files changed, 146 insertions(+), 54 deletions(-) create mode 100644 zed/src/http.rs diff --git a/gpui/src/image_data.rs b/gpui/src/image_data.rs index 352393e3b57328f8373134c23edcb9b9ed5790ae..d97820ab51b3f09ad8f2241243837ce1535dc6af 100644 --- a/gpui/src/image_data.rs +++ b/gpui/src/image_data.rs @@ -1,8 +1,11 @@ use crate::geometry::vector::{vec2i, Vector2I}; use image::{Bgra, ImageBuffer}; -use std::sync::{ - atomic::{AtomicUsize, Ordering::SeqCst}, - Arc, +use std::{ + fmt, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, }; pub struct ImageData { @@ -29,3 +32,12 @@ impl ImageData { 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/server/src/rpc.rs b/server/src/rpc.rs index 539068f2d3d55e8cb11c085c52c0a84957ce4b6b..14e520d58973f09e9850e4b35dc56f8930a7f91d 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1025,6 +1025,7 @@ mod tests { language::LanguageRegistry, rpc::{self, Client}, settings, + test::FakeHttpClient, user::UserStore, worktree::Worktree, }; @@ -1486,6 +1487,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; + let mut http = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) }); 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; diff --git a/zed/src/channel.rs b/zed/src/channel.rs index bf1237d8359f4e4c857b419fbc663b6182325c60..42727ea54b7d001c0df3f1384950b2c3ff1917ec 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -46,7 +46,7 @@ pub struct Channel { _subscription: rpc::Subscription, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct ChannelMessage { pub id: u64, pub body: String, @@ -495,15 +495,17 @@ 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 = UserStore::new(client.clone(), cx.background().as_ref()); + 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)); diff --git a/zed/src/http.rs b/zed/src/http.rs new file mode 100644 index 0000000000000000000000000000000000000000..68f1610c2749c8e374ddd5c8a81beef0281b06f9 --- /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, Request, Response as ServerResponse}, + 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 d672d234bfbcce21a64714f13ba46cd23926fe91..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; diff --git a/zed/src/main.rs b/zed/src/main.rs index f585ed5b82e4afc0bd261f244a38504a58e2c5fb..9e73a961265e7347d652a6d016ece75ea513d3ce 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,7 +37,8 @@ fn main() { app.run(move |cx| { let rpc = rpc::Client::new(); - let user_store = UserStore::new(rpc.clone(), cx.background()); + 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)), diff --git a/zed/src/test.rs b/zed/src/test.rs index 019969ee4f75ee1e463c3abbcc3f75950c9ac635..e8527a4ed762a057fbcd72355fa4783024e57f40 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -2,6 +2,7 @@ use crate::{ assets::Assets, channel::ChannelList, fs::RealFs, + http::{HttpClient, Request, Response, ServerResponse}, language::LanguageRegistry, rpc::{self, Client}, settings::{self, ThemeRegistry}, @@ -10,11 +11,13 @@ use crate::{ 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::{ @@ -164,7 +167,8 @@ 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 = UserStore::new(rpc.clone(), cx.background()); + 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, @@ -313,3 +317,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/user.rs b/zed/src/user.rs index 6c02c0a284e6dcca3ebd32d3ba601f15808fb948..ee9915ac3e6b21215fd98d3fb141e712a01c3907 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -1,22 +1,24 @@ use crate::{ + http::{HttpClient, Method, Request, Url}, rpc::{Client, Status}, util::TryFutureExt, }; -use anyhow::{anyhow, Result}; -use gpui::{elements::Image, executor, ImageData, Task}; +use anyhow::{anyhow, Context, Result}; +use futures::future; +use gpui::{executor, ImageData, Task}; use parking_lot::Mutex; -use postage::{prelude::Stream, sink::Sink, watch}; -use std::{collections::HashMap, sync::Arc}; -use surf::{ - http::{Method, Request}, - HttpClient, Url, +use postage::{oneshot, prelude::Stream, sink::Sink, watch}; +use std::{ + collections::HashMap, + sync::{Arc, Weak}, }; use zrpc::proto; +#[derive(Debug)] pub struct User { - id: u64, - github_login: String, - avatar: Option, + pub id: u64, + pub github_login: String, + pub avatar: Option>, } pub struct UserStore { @@ -24,7 +26,7 @@ pub struct UserStore { current_user: watch::Receiver>>, rpc: Arc, http: Arc, - _maintain_current_user: Option>, + _maintain_current_user: Task<()>, } impl UserStore { @@ -34,18 +36,18 @@ impl UserStore { executor: &executor::Background, ) -> Arc { let (mut current_user_tx, current_user_rx) = watch::channel(); - - let mut this = Arc::new(Self { + let (mut this_tx, mut this_rx) = oneshot::channel::>(); + let this = Arc::new(Self { users: Default::default(), current_user: current_user_rx, rpc: rpc.clone(), http, - _maintain_current_user: None, - }); - - let task = { - let this = Arc::downgrade(&this); - executor.spawn(async move { + _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 { @@ -63,10 +65,12 @@ impl UserStore { _ => {} } } - }) - }; - Arc::get_mut(&mut this).unwrap()._maintain_current_user = Some(task); - + }), + }); + let weak = Arc::downgrade(&this); + executor + .spawn(async move { this_tx.send(weak).await }) + .detach(); this } @@ -78,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)); } } @@ -92,20 +103,12 @@ impl UserStore { 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) -> &watch::Receiver>> { @@ -115,20 +118,25 @@ impl UserStore { impl User { async fn new(message: proto::User, http: &dyn HttpClient) -> Self { - let avatar = fetch_avatar(http, &message.avatar_url).await.log_err(); User { id: message.id, github_login: message.github_login, - avatar, + 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)?; + let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?; let request = Request::new(Method::Get, url); - let response = http.send(request).await?; - let bytes = response.body_bytes().await?; + 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 1410b6ece55ea3a0bee1c87d9b67377ec07cbaa6..c38a2d78bb89acc4e23bccb2209639c59ddf8e0b 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -956,8 +956,14 @@ impl Workspace { fn render_current_user(&self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; - let avatar = if let Some(current_user) = self.user_store.current_user().borrow().as_ref() { - todo!() + let avatar = if let Some(avatar) = self + .user_store + .current_user() + .borrow() + .as_ref() + .and_then(|user| user.avatar.clone()) + { + Image::new(avatar).boxed() } else { Svg::new("icons/signed-out-12.svg") .with_color(theme.workspace.titlebar.icon_signed_out) From e0e0bdbc3aa2d8b35a69616acd169e0cba978d6d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 16:28:26 -0600 Subject: [PATCH 09/55] Synthesize GitHub avatar URL and follow redirects when fetching it Co-Authored-By: Max Brunsfeld --- server/src/rpc.rs | 2 +- zed/src/http.rs | 4 ++-- zed/src/user.rs | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 14e520d58973f09e9850e4b35dc56f8930a7f91d..d4cc596810d51acf40fd2267112c093c3955d2eb 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -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 diff --git a/zed/src/http.rs b/zed/src/http.rs index 68f1610c2749c8e374ddd5c8a81beef0281b06f9..30a7a08a519950beee6f28729f2af2f6c9df363a 100644 --- a/zed/src/http.rs +++ b/zed/src/http.rs @@ -2,8 +2,8 @@ pub use anyhow::{anyhow, Result}; use futures::future::BoxFuture; use std::sync::Arc; pub use surf::{ - http::{Method, Request, Response as ServerResponse}, - Response, Url, + http::{Method, Response as ServerResponse}, + Request, Response, Url, }; pub trait HttpClient: Send + Sync { diff --git a/zed/src/user.rs b/zed/src/user.rs index ee9915ac3e6b21215fd98d3fb141e712a01c3907..3a119474e1e7397b5c5352ffb13cddd27f98ae9c 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -128,7 +128,9 @@ impl User { 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 request = Request::new(Method::Get, url); + let mut request = Request::new(Method::Get, url); + request.middleware(surf::middleware::Redirect::default()); + let mut response = http .send(request) .await From b63b717eacc7ea579c53a382232db12ad800a665 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 16:39:35 -0600 Subject: [PATCH 10/55] Preserve aspect ratio when scaling images Co-Authored-By: Max Brunsfeld --- gpui/src/elements.rs | 15 ++++++++++++++- gpui/src/elements/image.rs | 6 +++++- gpui/src/elements/svg.rs | 21 ++++++--------------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 252cebd7334f57874a8fd6164da15ad0ba3c2986..42e9810cfbff21ea9c96017b54689cb5a77f48cd 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -24,7 +24,10 @@ pub use self::{ }; pub use crate::presenter::ChildView; use crate::{ - geometry::{rect::RectF, vector::Vector2F}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use core::panic; @@ -359,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/image.rs b/gpui/src/elements/image.rs index 2611478fe0b85e93ee9f55147e0b758e53427153..f9908a76fb77b9ce3c14b1fc2f7073401ed84a00 100644 --- a/gpui/src/elements/image.rs +++ b/gpui/src/elements/image.rs @@ -6,6 +6,8 @@ use crate::{ }; use std::sync::Arc; +use super::constrain_size_preserving_aspect_ratio; + pub struct Image { data: Arc, border: Border, @@ -41,7 +43,9 @@ impl Element for Image { constraint: SizeConstraint, _: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - (constraint.max, ()) + let size = + constrain_size_preserving_aspect_ratio(constraint.max, self.data.size().to_f32()); + (size, ()) } fn paint( diff --git a/gpui/src/elements/svg.rs b/gpui/src/elements/svg.rs index 8adb285b99a73c1e2ab661976d4f2d58d4d635db..55e11a92531341262e69e79ff6365a2c20cfa5b6 100644 --- a/gpui/src/elements/svg.rs +++ b/gpui/src/elements/svg.rs @@ -41,21 +41,10 @@ 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) => { @@ -111,6 +100,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), From 426d52d8c1394bf0452d19f8d3534dfe11ad8214 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 16:59:09 -0600 Subject: [PATCH 11/55] Mix quad border color with background color based on its alpha channel Co-Authored-By: Max Brunsfeld --- gpui/src/platform/mac/shaders/shaders.metal | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gpui/src/platform/mac/shaders/shaders.metal b/gpui/src/platform/mac/shaders/shaders.metal index 00338b30e71c234147f88916f011f923221e11fc..13d2720fad7788ff93a58a8143dde44ced25cf41 100644 --- a/gpui/src/platform/mac/shaders/shaders.metal +++ b/gpui/src/platform/mac/shaders/shaders.metal @@ -70,9 +70,10 @@ float4 quad_sdf(QuadFragmentInput input) { if (border_width == 0.) { 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( - input.border_color, + border_color, input.background_color, saturate(0.5 - inset_distance) ); From 0f415a594f86f9da113f69bb4054a2fa2adaa0fe Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 16:59:38 -0600 Subject: [PATCH 12/55] Style avatar image with border and rounded corners Co-Authored-By: Max Brunsfeld --- gpui/src/elements/image.rs | 27 ++++++++++++++------------- zed/assets/themes/_base.toml | 7 ++++--- zed/src/theme.rs | 5 +++-- zed/src/workspace.rs | 6 ++++-- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/gpui/src/elements/image.rs b/gpui/src/elements/image.rs index f9908a76fb77b9ce3c14b1fc2f7073401ed84a00..421e18ec95ce7bc64df77d675797a10c41ff3541 100644 --- a/gpui/src/elements/image.rs +++ b/gpui/src/elements/image.rs @@ -1,16 +1,23 @@ +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; -use super::constrain_size_preserving_aspect_ratio; - pub struct Image { data: Arc, + style: ImageStyle, +} + +#[derive(Copy, Clone, Default, Deserialize)] +pub struct ImageStyle { + #[serde(default)] border: Border, + #[serde(default)] corner_radius: f32, } @@ -18,18 +25,12 @@ impl Image { pub fn new(data: Arc) -> Self { Self { data, - border: Default::default(), - corner_radius: Default::default(), + style: Default::default(), } } - pub fn with_corner_radius(mut self, corner_radius: f32) -> Self { - self.corner_radius = corner_radius; - self - } - - pub fn with_border(mut self, border: Border) -> Self { - self.border = border; + pub fn with_style(mut self, style: ImageStyle) -> Self { + self.style = style; self } } @@ -57,8 +58,8 @@ impl Element for Image { ) -> Self::PaintState { cx.scene.push_image(scene::Image { bounds, - border: self.border, - corner_radius: self.corner_radius, + border: self.style.border, + corner_radius: self.style.corner_radius, data: self.data.clone(), }); } diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 124aa178909a107dae4ab26d0c2296566db3e97c..dfd27f396d35b5f68b0e5744c9ed424103be7839 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -8,8 +8,9 @@ pane_divider = { width = 1, color = "$border.0" } [workspace.titlebar] border = { width = 1, bottom = true, color = "$border.0" } title = "$text.0" -icon_width = 16 +avatar_width = 20 icon_signed_out = "$text.2.color" +avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } } [workspace.tab] text = "$text.2" @@ -28,7 +29,7 @@ background = "$surface.1" text = "$text.0" [workspace.sidebar] -width = 36 +width = 38 border = { right = true, width = 1, color = "$border.0" } [workspace.sidebar.resize_handle] @@ -37,7 +38,7 @@ background = "$border.0" [workspace.sidebar.icon] color = "$text.2.color" -height = 16 +height = 18 [workspace.sidebar.active_icon] extends = "$workspace.sidebar.icon" diff --git a/zed/src/theme.rs b/zed/src/theme.rs index f0c17a1cb8876811177f0de76311cd9d818a83a4..96e562a55ffc45ebb593010699fed06bedf65160 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -4,7 +4,7 @@ mod theme_registry; use anyhow::Result; use gpui::{ color::Color, - elements::{ContainerStyle, LabelStyle}, + elements::{ContainerStyle, ImageStyle, LabelStyle}, fonts::{HighlightStyle, TextStyle}, Border, }; @@ -47,8 +47,9 @@ pub struct Titlebar { #[serde(flatten)] pub container: ContainerStyle, pub title: TextStyle, - pub icon_width: f32, + pub avatar_width: f32, pub icon_signed_out: Color, + pub avatar: ImageStyle, } #[derive(Clone, Deserialize)] diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index c38a2d78bb89acc4e23bccb2209639c59ddf8e0b..1cef8caef082f45462d0a226f49a7a0b6f5bf019 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -963,7 +963,9 @@ impl Workspace { .as_ref() .and_then(|user| user.avatar.clone()) { - Image::new(avatar).boxed() + Image::new(avatar) + .with_style(theme.workspace.titlebar.avatar) + .boxed() } else { Svg::new("icons/signed-out-12.svg") .with_color(theme.workspace.titlebar.icon_signed_out) @@ -973,7 +975,7 @@ impl Workspace { ConstrainedBox::new( Align::new( ConstrainedBox::new(avatar) - .with_width(theme.workspace.titlebar.icon_width) + .with_width(theme.workspace.titlebar.avatar_width) .boxed(), ) .boxed(), From e212461dfee3a9f175ef3dc5ed619ecfc5b82549 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 17:06:40 -0600 Subject: [PATCH 13/55] Authenticate when clicking on unauthenticated avatar in titlebar Co-Authored-By: Max Brunsfeld --- zed/src/workspace.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 1cef8caef082f45462d0a226f49a7a0b6f5bf019..bb69d18424b58e14ebcdf0f3ffd24837d53d48d2 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -12,7 +12,7 @@ use crate::{ settings::Settings, user, worktree::{File, Worktree}, - AppState, + AppState, Authenticate, }; use anyhow::{anyhow, Result}; use gpui::{ @@ -21,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, @@ -967,9 +967,14 @@ impl Workspace { .with_style(theme.workspace.titlebar.avatar) .boxed() } else { - Svg::new("icons/signed-out-12.svg") - .with_color(theme.workspace.titlebar.icon_signed_out) - .boxed() + MouseEventHandler::new::(0, cx, |_, _| { + Svg::new("icons/signed-out-12.svg") + .with_color(theme.workspace.titlebar.icon_signed_out) + .boxed() + }) + .on_click(|cx| cx.dispatch_action(Authenticate)) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() }; ConstrainedBox::new( From 99a2dc4880636900173b400d5405fd4bd27cb51e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 17:47:43 -0600 Subject: [PATCH 14/55] Render an offline icon in titlebar when connection is lost Co-Authored-By: Max Brunsfeld --- gpui/src/elements/container.rs | 12 ++++---- gpui/src/views/select.rs | 4 +-- zed/assets/icons/offline-14.svg | 3 ++ zed/assets/themes/_base.toml | 8 +++-- zed/src/chat_panel.rs | 16 +++++----- zed/src/file_finder.rs | 8 ++--- zed/src/theme.rs | 10 ++++++- zed/src/theme_selector.rs | 8 ++--- zed/src/workspace.rs | 52 ++++++++++++++++++++++++++++----- zed/src/workspace/pane.rs | 17 ++++++++--- zed/src/workspace/sidebar.rs | 4 +-- 11 files changed, 102 insertions(+), 40 deletions(-) create mode 100644 zed/assets/icons/offline-14.svg diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index 48dcfa1b137986df78690a1adaf8e06249dae314..eeda6f206d139c210259744f9604fb30f90d1fae 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, @@ -367,7 +367,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/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/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/themes/_base.toml b/zed/assets/themes/_base.toml index dfd27f396d35b5f68b0e5744c9ed424103be7839..485d3bf2a79e7269fb24462c1160d370c7bf1b89 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -9,9 +9,13 @@ pane_divider = { width = 1, color = "$border.0" } border = { width = 1, bottom = true, color = "$border.0" } title = "$text.0" avatar_width = 20 -icon_signed_out = "$text.2.color" +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 } @@ -29,7 +33,7 @@ background = "$surface.1" text = "$text.0" [workspace.sidebar] -width = 38 +width = 32 border = { right = true, width = 1, color = "$border.0" } [workspace.sidebar.resize_handle] diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 200a35fccae69b89be1e35e5655eb03642cf8aa2..5ccc014ca787119f6866254b20adc1ae79cab98f 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -209,7 +209,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()) @@ -243,7 +243,7 @@ impl ChatPanel { ) .boxed(), ) - .with_style(&theme.sender.container) + .with_style(theme.sender.container) .boxed(), ) .with_child( @@ -254,7 +254,7 @@ impl ChatPanel { ) .boxed(), ) - .with_style(&theme.timestamp.container) + .with_style(theme.timestamp.container) .boxed(), ) .boxed(), @@ -262,14 +262,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 +293,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() } @@ -387,7 +387,7 @@ impl View for ChatPanel { }; 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/file_finder.rs b/zed/src/file_finder.rs index 7e552d4748eef5368ade73f14b279c2ae834de7f..23bec79e0076046f3d483f9baa4972c7e418a2f1 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, diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 96e562a55ffc45ebb593010699fed06bedf65160..88f385a05461b18a0db3f6182e9b3d08effb97b2 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -48,10 +48,18 @@ pub struct Titlebar { pub container: ContainerStyle, pub title: TextStyle, pub avatar_width: f32, - pub icon_signed_out: Color, + 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)] diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 09f259281a45e3451b2d9810230f4bddfa35fb52..6a623b120e6a3fcdc5eb679b2fcdd22c18ba6ec4 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -214,7 +214,7 @@ impl ThemeSelector { ) .boxed(), ) - .with_style(&settings.theme.selector.empty.container) + .with_style(settings.theme.selector.empty.container) .named("empty matches"); } @@ -259,9 +259,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 +288,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/workspace.rs b/zed/src/workspace.rs index bb69d18424b58e14ebcdf0f3ffd24837d53d48d2..cbdf3b149a532421b92c37a39e1c07106cccbcfd 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -31,7 +31,6 @@ pub use pane::*; pub use pane_group::*; use postage::{prelude::Stream, watch}; use sidebar::{Side, Sidebar, ToggleSidebarItem}; -use smol::prelude::*; use std::{ collections::{hash_map::Entry, HashMap, HashSet}, future::Future, @@ -391,9 +390,14 @@ impl Workspace { right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); let mut current_user = app_state.user_store.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; - while current_user.recv().await.is_some() { + 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()); @@ -642,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| { @@ -954,7 +958,34 @@ impl Workspace { &self.active_pane } - fn render_current_user(&self, cx: &mut RenderContext) -> ElementBox { + fn render_connection_status(&self) -> Option { + let theme = &self.settings.borrow().theme; + match dbg!(&*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 @@ -969,7 +1000,7 @@ impl Workspace { } else { MouseEventHandler::new::(0, cx, |_, _| { Svg::new("icons/signed-out-12.svg") - .with_color(theme.workspace.titlebar.icon_signed_out) + .with_color(theme.workspace.titlebar.icon_color) .boxed() }) .on_click(|cx| cx.dispatch_action(Authenticate)) @@ -1019,11 +1050,18 @@ impl View for Workspace { .boxed(), ) .with_child( - Align::new(self.render_current_user(cx)).right().boxed(), + 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.) diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 66a062fb65d5355e3c98f8c62f41153f71fe1f42..c0cd6bb9fd7e47141cc633a55cb9eeb00e48ca81 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}; @@ -256,7 +264,7 @@ impl Pane { ) .boxed(), ) - .with_style(&ContainerStyle { + .with_style(ContainerStyle { margin: Margin { left: style.spacing, right: style.spacing, @@ -283,7 +291,8 @@ impl Pane { icon.with_color(style.icon_close).boxed() } }, - ).with_cursor_style(CursorStyle::PointingHand) + ) + .with_cursor_style(CursorStyle::PointingHand) .on_click(move |cx| { cx.dispatch_action(CloseItem(item_id)) }) @@ -298,7 +307,7 @@ impl Pane { ) .boxed(), ) - .with_style(&style.container) + .with_style(style.container) .boxed(), ) .on_mouse_down(move |cx| { diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 7cdb6447a7ff55f8deece33ab5dcbf9738badeb0..3ceba15df0051701873c472a993ced0fe0b0ed64 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -113,7 +113,7 @@ impl Sidebar { })) .boxed(), ) - .with_style(&theme.container) + .with_style(theme.container) .boxed(), ) .with_width(theme.width) @@ -165,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 { From aa7c1bfa2d2f418a97c8206e19ff46ee78f34afc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Sep 2021 17:15:17 -0700 Subject: [PATCH 15/55] Fix type errors in server tests --- server/src/rpc.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index d4cc596810d51acf40fd2267112c093c3955d2eb..912f57fcf714e8a892c27accf73ee14ee5c76ba3 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1484,10 +1484,10 @@ mod tests { #[gpui::test] async fn test_basic_chat(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 mut http = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) }); 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; @@ -1514,7 +1514,8 @@ mod tests { .await .unwrap(); - let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref()); + 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()) @@ -1539,7 +1540,8 @@ mod tests { }) .await; - let user_store_b = UserStore::new(client_b.clone(), cx_b.background().as_ref()); + let user_store_b = + UserStore::new(client_b.clone(), http.clone(), cx_b.background().as_ref()); 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()) @@ -1627,6 +1629,7 @@ mod tests { #[gpui::test] async fn test_chat_message_validation(mut cx_a: TestAppContext) { cx_a.foreground().forbid_parking(); + let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) }); let mut server = TestServer::start().await; let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await; @@ -1639,7 +1642,7 @@ mod tests { .await .unwrap(); - let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref()); + let user_store_a = UserStore::new(client_a.clone(), http, 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()) @@ -1685,6 +1688,7 @@ 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; @@ -1715,7 +1719,8 @@ mod tests { .await .unwrap(); - let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref()); + 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()) @@ -1741,7 +1746,8 @@ mod tests { }) .await; - let user_store_b = UserStore::new(client_b.clone(), cx_b.background().as_ref()); + let user_store_b = + UserStore::new(client_b.clone(), http.clone(), cx_b.background().as_ref()); 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()) From 44a457e8b6e01fdeca6fbade1bdf2119d657214d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 18:21:46 -0600 Subject: [PATCH 16/55] Cache credentials in memory separately from connection status This prevents us from re-prompting for keychain access when we retry connections after the connection is lost. Co-Authored-By: Max Brunsfeld --- server/src/rpc.rs | 67 ++++++++++----------- zed/src/rpc.rs | 146 ++++++++++++++++++++++++++-------------------- zed/src/test.rs | 39 +++++++------ zed/src/user.rs | 4 +- 4 files changed, 139 insertions(+), 117 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 912f57fcf714e8a892c27accf73ee14ee5c76ba3..86f369fb8a12296569ab8100baa67ce33f9e45d1 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1023,7 +1023,7 @@ mod tests { editor::{Editor, Insert}, fs::{FakeFs, Fs as _}, language::LanguageRegistry, - rpc::{self, Client}, + rpc::{self, Client, Credentials}, settings, test::FakeHttpClient, user::UserStore, @@ -1922,39 +1922,40 @@ 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)) + .override_authenticate(move |cx| { + cx.spawn(|_| async move { + let access_token = "the-token".to_string(); + Ok(Credentials { + user_id: 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_establish_connection(move |credentials, cx| { + assert_eq!(credentials.user_id, client_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(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) + } + }) + }); client .authenticate_and_connect(&cx.to_async()) diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 69bc33a62e1c4065f7c946b239e72d7039ef5c49..bc6b41dd5f523e40e239f78465ed246b38c77bfd 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -29,11 +29,10 @@ 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 Task>>, >, } @@ -41,25 +40,17 @@ pub struct Client { pub enum Status { 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,9 +61,16 @@ 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 { + credentials: None, status: watch::channel_with(Status::SignedOut), entity_id_extractors: Default::default(), model_handlers: Default::default(), @@ -107,22 +105,35 @@ 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 { @@ -249,23 +260,31 @@ 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 credentials = self.state.read().credentials.clone(); + let credentials = if let Some(credentials) = credentials { + credentials + } else { + let credentials = match self.authenticate(&cx).await { + Ok(credentials) => credentials, + Err(err) => { + self.set_status(Status::ConnectionError, cx); + return Err(err); + } + }; + self.state.write().credentials = Some(credentials.clone()); + 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.set_connection(conn, cx).await; Ok(()) } Err(err) => { @@ -275,7 +294,7 @@ impl Client { } } - async fn set_connection(self: &Arc, user_id: u64, conn: Conn, cx: &AsyncAppContext) { + async fn set_connection(self: &Arc, conn: Conn, cx: &AsyncAppContext) { let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await; cx.foreground() .spawn({ @@ -310,13 +329,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(); @@ -334,35 +347,35 @@ 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) + 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)); + 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?; @@ -387,7 +400,7 @@ 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 { @@ -397,7 +410,10 @@ impl Client { .flatten() { log::info!("already signed in. user_id: {}", user_id); - return Ok((user_id.parse()?, String::from_utf8(access_token).unwrap())); + return Ok(Credentials { + user_id: user_id.parse()?, + access_token: String::from_utf8(access_token).unwrap(), + }); } // Generate a pair of asymmetric encryption keys. The public key will be used by the @@ -463,7 +479,11 @@ impl Client { 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, + }) }) } diff --git a/zed/src/test.rs b/zed/src/test.rs index e8527a4ed762a057fbcd72355fa4783024e57f40..b9948cc460f66d7891b3321dc6bb09d34b207c5a 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -4,7 +4,7 @@ use crate::{ fs::RealFs, http::{HttpClient, Request, Response, ServerResponse}, language::LanguageRegistry, - rpc::{self, Client}, + rpc::{self, Client, Credentials}, settings::{self, ThemeRegistry}, time::ReplicaId, user::UserStore, @@ -226,25 +226,26 @@ impl FakeServer { Arc::get_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, access_token)) + .override_authenticate(move |cx| { + cx.spawn(|_| async move { + let access_token = "the-token".to_string(); + Ok(Credentials { + user_id: 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 } - }) - } - }, - ); + }) + }) + .override_establish_connection({ + let server = result.clone(); + move |credentials, cx| { + assert_eq!(credentials.user_id, client_user_id); + assert_eq!(credentials.access_token, "the-token"); + cx.spawn({ + let server = server.clone(); + move |cx| async move { server.connect(&cx).await } + }) + } + }); client .authenticate_and_connect(&cx.to_async()) diff --git a/zed/src/user.rs b/zed/src/user.rs index 3a119474e1e7397b5c5352ffb13cddd27f98ae9c..06aab321934dd1ffb985430157b7cfd02385dbc7 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -51,8 +51,8 @@ impl UserStore { let mut status = rpc.status(); while let Some(status) = status.recv().await { match status { - Status::Connected { user_id, .. } => { - if let Some(this) = this.upgrade() { + 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 From 77a4a36eb3a483e7e4e5a7c91f5b4924db347123 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 18:30:17 -0600 Subject: [PATCH 17/55] Test that we reuse credentials when reconnecting Co-Authored-By: Max Brunsfeld --- zed/src/rpc.rs | 2 ++ zed/src/test.rs | 32 +++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index bc6b41dd5f523e40e239f78465ed246b38c77bfd..8846b02f4bdfaff4adefea9e7060e6fc48480a09 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -581,6 +581,7 @@ mod tests { status.recv().await, Some(Status::Connected { .. }) )); + assert_eq!(server.auth_count(), 1); server.forbid_connections(); server.disconnect().await; @@ -589,6 +590,7 @@ 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 } #[test] diff --git a/zed/src/test.rs b/zed/src/test.rs index b9948cc460f66d7891b3321dc6bb09d34b207c5a..4f28db9d2808874ee336a64ca3f0f496bd9aeaa7 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -21,7 +21,7 @@ use std::{ marker::PhantomData, path::{Path, PathBuf}, sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, }, }; @@ -209,6 +209,7 @@ pub struct FakeServer { incoming: Mutex>>>, connection_id: Mutex>, forbid_connections: AtomicBool, + auth_count: AtomicUsize, } impl FakeServer { @@ -217,26 +218,31 @@ 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(), }); Arc::get_mut(client) .unwrap() - .override_authenticate(move |cx| { - cx.spawn(|_| async move { - let access_token = "the-token".to_string(); - Ok(Credentials { - user_id: client_user_id, - access_token, + .override_authenticate({ + let server = server.clone(); + move |cx| { + server.auth_count.fetch_add(1, SeqCst); + cx.spawn(move |_| async move { + let access_token = "the-token".to_string(); + Ok(Credentials { + user_id: client_user_id, + access_token, + }) }) - }) + } }) .override_establish_connection({ - let server = result.clone(); + let server = server.clone(); move |credentials, cx| { assert_eq!(credentials.user_id, client_user_id); assert_eq!(credentials.access_token, "the-token"); @@ -251,7 +257,7 @@ impl FakeServer { .authenticate_and_connect(&cx.to_async()) .await .unwrap(); - result + server } pub async fn disconnect(&self) { @@ -273,6 +279,10 @@ impl FakeServer { } } + pub fn auth_count(&self) -> usize { + self.auth_count.load(SeqCst) + } + pub fn forbid_connections(&self) { self.forbid_connections.store(true, SeqCst); } From 4a9918979e77ef03357fa3ec76e23a523ca126b0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 19:19:11 -0600 Subject: [PATCH 18/55] WIP: Clear cached credentials if authentication fails Still need to actually handle an HTTP response from the server indicating there was an invalid token. Co-Authored-By: Max Brunsfeld --- Cargo.lock | 9 ++--- server/src/rpc.rs | 20 ++++++----- zed/Cargo.toml | 1 + zed/src/rpc.rs | 91 ++++++++++++++++++++++++++++++++++++++--------- zed/src/test.rs | 49 +++++++++++++++++-------- zrpc/src/conn.rs | 4 +-- zrpc/src/lib.rs | 2 +- zrpc/src/peer.rs | 40 ++++++++++----------- 8 files changed, 149 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c749b35f2dc5e329fea6b282a3cab690f7156e6f..aea0c49da87ae5aeed63e66f65aad561b1471870 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5108,18 +5108,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", @@ -5914,6 +5914,7 @@ dependencies = [ "smol", "surf", "tempdir", + "thiserror", "time 0.3.2", "tiny_http", "toml 0.5.8", diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 86f369fb8a12296569ab8100baa67ce33f9e45d1..1e0fe2465cafbe2d7034e941500145cda25ebd1d 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(), @@ -972,7 +972,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; } }); @@ -1023,7 +1023,7 @@ mod tests { editor::{Editor, Insert}, fs::{FakeFs, Fs as _}, language::LanguageRegistry, - rpc::{self, Client, Credentials}, + rpc::{self, Client, Credentials, EstablishConnectionError}, settings, test::FakeHttpClient, user::UserStore, @@ -1941,9 +1941,11 @@ mod tests { let client_name = client_name.clone(); cx.spawn(move |cx| async move { if forbid_connections.load(SeqCst) { - Err(anyhow!("server is forbidding connections")) + Err(EstablishConnectionError::other(anyhow!( + "server is forbidding connections" + ))) } else { - let (client_conn, server_conn, kill_conn) = Conn::in_memory(); + let (client_conn, server_conn, kill_conn) = Connection::in_memory(); connection_killers.lock().insert(client_user_id, kill_conn); cx.background() .spawn(server.handle_connection( diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 2b36db9ba86152b040fa6edcf74da5c6bc68a0d2..d9c2cc6a584535caace7d267b5a04b10dfffb721 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -50,6 +50,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" diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 8846b02f4bdfaff4adefea9e7060e6fc48480a09..3526381cde572ba127775a01aca2faa194cf1f1e 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -15,10 +15,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! { @@ -32,10 +33,32 @@ pub struct Client { authenticate: Option Task>>>, establish_connection: Option< - Box Task>>, + Box< + dyn 'static + + Send + + Sync + + Fn( + &Credentials, + &AsyncAppContext, + ) -> Task>, + >, >, } +#[derive(Error, Debug)] +pub enum EstablishConnectionError { + #[error("invalid access token")] + InvalidAccessToken, + #[error("{0}")] + Other(anyhow::Error), +} + +impl EstablishConnectionError { + pub fn other(error: impl Into + Send + Sync) -> Self { + Self::Other(error.into()) + } +} + #[derive(Copy, Clone, Debug)] pub enum Status { SignedOut, @@ -122,7 +145,10 @@ impl Client { #[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>, + F: 'static + + Send + + Sync + + Fn(&Credentials, &AsyncAppContext) -> Task>, { self.establish_connection = Some(Box::new(connect)); self @@ -288,13 +314,18 @@ impl Client { Ok(()) } Err(err) => { + eprintln!("error in authenticate and connect {}", err); + if matches!(err, EstablishConnectionError::InvalidAccessToken) { + eprintln!("nuking credentials"); + self.state.write().credentials.take(); + } self.set_status(Status::ConnectionError, cx); - Err(err) + Err(err)? } } } - async fn set_connection(self: &Arc, 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({ @@ -359,7 +390,7 @@ impl Client { self: &Arc, credentials: &Credentials, cx: &AsyncAppContext, - ) -> Task> { + ) -> Task> { if let Some(callback) = self.establish_connection.as_ref() { callback(credentials, cx) } else { @@ -371,28 +402,43 @@ impl Client { self: &Arc, credentials: &Credentials, cx: &AsyncAppContext, - ) -> Task> { + ) -> 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 = smol::net::TcpStream::connect(host) + .await + .map_err(EstablishConnectionError::other)?; + let request = request + .uri(format!("wss://{}/rpc", host)) + .body(()) + .map_err(EstablishConnectionError::other)?; let (stream, _) = async_tungstenite::async_tls::client_async_tls(request, stream) .await - .context("websocket handshake")?; - Ok(Conn::new(stream)) + .context("websocket handshake") + .map_err(EstablishConnectionError::other)?; + 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 = smol::net::TcpStream::connect(host) + .await + .map_err(EstablishConnectionError::other)?; + let request = request + .uri(format!("ws://{}/rpc", host)) + .body(()) + .map_err(EstablishConnectionError::other)?; let (stream, _) = async_tungstenite::client_async(request, stream) .await - .context("websocket handshake")?; - Ok(Conn::new(stream)) + .context("websocket handshake") + .map_err(EstablishConnectionError::other)?; + Ok(Connection::new(stream)) } else { - Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL)) + Err(EstablishConnectionError::other(anyhow!( + "invalid server url: {}", + *ZED_SERVER_URL + ))) } }) } @@ -591,6 +637,19 @@ mod tests { 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 4f28db9d2808874ee336a64ca3f0f496bd9aeaa7..e5ab3154f526e3090774fbf02ae338c6749f03ab 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -4,7 +4,7 @@ use crate::{ fs::RealFs, http::{HttpClient, Request, Response, ServerResponse}, language::LanguageRegistry, - rpc::{self, Client, Credentials}, + rpc::{self, Client, Credentials, EstablishConnectionError}, settings::{self, ThemeRegistry}, time::ReplicaId, user::UserStore, @@ -26,7 +26,7 @@ use std::{ }, }; use tempdir::TempDir; -use zrpc::{proto, Conn, ConnectionId, Peer, Receipt, TypedEnvelope}; +use zrpc::{proto, Connection, ConnectionId, Peer, Receipt, TypedEnvelope}; #[cfg(test)] #[ctor::ctor] @@ -210,6 +210,8 @@ pub struct FakeServer { connection_id: Mutex>, forbid_connections: AtomicBool, auth_count: AtomicUsize, + access_token: AtomicUsize, + user_id: u64, } impl FakeServer { @@ -224,6 +226,8 @@ impl FakeServer { 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) @@ -232,8 +236,8 @@ impl FakeServer { let server = server.clone(); move |cx| { server.auth_count.fetch_add(1, SeqCst); + let access_token = server.access_token.load(SeqCst).to_string(); cx.spawn(move |_| async move { - let access_token = "the-token".to_string(); Ok(Credentials { user_id: client_user_id, access_token, @@ -244,11 +248,10 @@ impl FakeServer { .override_establish_connection({ let server = server.clone(); move |credentials, cx| { - assert_eq!(credentials.user_id, client_user_id); - assert_eq!(credentials.access_token, "the-token"); + let credentials = credentials.clone(); cx.spawn({ let server = server.clone(); - move |cx| async move { server.connect(&cx).await } + move |cx| async move { server.establish_connection(&credentials, &cx).await } }) } }); @@ -266,23 +269,39 @@ 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::InvalidAccessToken)? } + + 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) { self.forbid_connections.store(true, SeqCst); } 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(); From 7e4d5b7d04dba42f3096a2025e719e5881687db4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 14 Sep 2021 20:36:03 -0600 Subject: [PATCH 19/55] Clear cached credentials when establishing a websocket connection with an invalid token --- gpui/src/platform.rs | 1 + gpui/src/platform/mac/platform.rs | 20 ++++++++++ gpui/src/platform/test.rs | 4 ++ server/src/auth.rs | 6 ++- zed/src/rpc.rs | 65 +++++++++++++++---------------- zed/src/test.rs | 2 +- zed/src/workspace.rs | 2 +- 7 files changed, 63 insertions(+), 37 deletions(-) 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/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/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/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/zed/src/rpc.rs b/zed/src/rpc.rs index 3526381cde572ba127775a01aca2faa194cf1f1e..9596b671edee8daea398292d3ac9f662877a6481 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -1,6 +1,9 @@ use crate::util::ResultExt; use anyhow::{anyhow, Context, Result}; -use async_tungstenite::tungstenite::http::Request; +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; @@ -47,10 +50,25 @@ pub struct Client { #[derive(Error, Debug)] pub enum EstablishConnectionError { - #[error("invalid access token")] - InvalidAccessToken, + #[error("unauthorized")] + Unauthorized, + #[error("{0}")] + Other(#[from] anyhow::Error), #[error("{0}")] - Other(anyhow::Error), + 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 { @@ -314,10 +332,9 @@ impl Client { Ok(()) } Err(err) => { - eprintln!("error in authenticate and connect {}", err); - if matches!(err, EstablishConnectionError::InvalidAccessToken) { - eprintln!("nuking credentials"); + if matches!(err, EstablishConnectionError::Unauthorized) { self.state.write().credentials.take(); + cx.platform().delete_credentials(&ZED_SERVER_URL).ok(); } self.set_status(Status::ConnectionError, cx); Err(err)? @@ -409,36 +426,18 @@ impl Client { ); cx.background().spawn(async move { if let Some(host) = ZED_SERVER_URL.strip_prefix("https://") { - let stream = smol::net::TcpStream::connect(host) - .await - .map_err(EstablishConnectionError::other)?; - let request = request - .uri(format!("wss://{}/rpc", host)) - .body(()) - .map_err(EstablishConnectionError::other)?; - let (stream, _) = async_tungstenite::async_tls::client_async_tls(request, stream) - .await - .context("websocket handshake") - .map_err(EstablishConnectionError::other)?; + 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?; Ok(Connection::new(stream)) } else if let Some(host) = ZED_SERVER_URL.strip_prefix("http://") { - let stream = smol::net::TcpStream::connect(host) - .await - .map_err(EstablishConnectionError::other)?; - let request = request - .uri(format!("ws://{}/rpc", host)) - .body(()) - .map_err(EstablishConnectionError::other)?; - let (stream, _) = async_tungstenite::client_async(request, stream) - .await - .context("websocket handshake") - .map_err(EstablishConnectionError::other)?; + 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?; Ok(Connection::new(stream)) } else { - Err(EstablishConnectionError::other(anyhow!( - "invalid server url: {}", - *ZED_SERVER_URL - ))) + Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))? } }) } diff --git a/zed/src/test.rs b/zed/src/test.rs index e5ab3154f526e3090774fbf02ae338c6749f03ab..7d027a8a1771b41fd2b68844802a2f6939b6448b 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -283,7 +283,7 @@ impl FakeServer { } if credentials.access_token != self.access_token.load(SeqCst).to_string() { - Err(EstablishConnectionError::InvalidAccessToken)? + Err(EstablishConnectionError::Unauthorized)? } let (client_conn, server_conn, _) = Connection::in_memory(); diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index cbdf3b149a532421b92c37a39e1c07106cccbcfd..9ce67c2f8a678eb344b91118cde78a5bd0000d3d 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -960,7 +960,7 @@ impl Workspace { fn render_connection_status(&self) -> Option { let theme = &self.settings.borrow().theme; - match dbg!(&*self.rpc.status().borrow()) { + match &*self.rpc.status().borrow() { rpc::Status::ConnectionError | rpc::Status::ConnectionLost | rpc::Status::Reauthenticating From 603f1d820d7164f803118da98296fd775b68031e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Sep 2021 11:45:08 +0200 Subject: [PATCH 20/55] Authenticate via the browser if keychain credentials are invalid Co-Authored-By: Nathan Sobo --- Cargo.lock | 12 +++++++++++ zed/Cargo.toml | 1 + zed/src/rpc.rs | 58 ++++++++++++++++++++++++++++++++++---------------- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aea0c49da87ae5aeed63e66f65aad561b1471870..60b2864c5bd3d70dcbe44bc1547f85d68e852869 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" @@ -5881,6 +5892,7 @@ version = "0.1.0" dependencies = [ "anyhow", "arrayvec 0.7.1", + "async-recursion", "async-trait", "async-tungstenite", "cargo-bundle", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index d9c2cc6a584535caace7d267b5a04b10dfffb721..bb4fab264e589d96a83960db49ad69d6b88488a2 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -18,6 +18,7 @@ test-support = ["tempdir", "zrpc/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"] } diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 9596b671edee8daea398292d3ac9f662877a6481..a01d14193fa833d6e3151eefaff37d555acf4b60 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -1,5 +1,6 @@ use crate::util::ResultExt; use anyhow::{anyhow, Context, Result}; +use async_recursion::async_recursion; use async_tungstenite::tungstenite::{ error::Error as WebsocketError, http::{Request, StatusCode}, @@ -282,6 +283,7 @@ impl Client { } } + #[async_recursion(?Send)] pub async fn authenticate_and_connect( self: &Arc, cx: &AsyncAppContext, @@ -304,9 +306,13 @@ impl Client { self.set_status(Status::Reauthenticating, cx) } + 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, @@ -328,16 +334,27 @@ impl Client { match self.establish_connection(&credentials, cx).await { Ok(conn) => { log::info!("connected to rpc address {}", *ZED_SERVER_URL); + if !read_from_keychain { + write_credentials_to_keychain(&credentials, cx).log_err(); + } self.set_connection(conn, cx).await; Ok(()) } Err(err) => { if matches!(err, EstablishConnectionError::Unauthorized) { self.state.write().credentials.take(); - cx.platform().delete_credentials(&ZED_SERVER_URL).ok(); + 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)? } - self.set_status(Status::ConnectionError, cx); - Err(err)? } } } @@ -449,18 +466,6 @@ impl Client { 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(Credentials { - user_id: user_id.parse()?, - access_token: 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. @@ -521,9 +526,6 @@ 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(Credentials { user_id: user_id.parse()?, @@ -564,6 +566,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 { From 914112f2b57a02a6579a40fef93ac3c0c9393a5f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Sep 2021 12:15:54 +0200 Subject: [PATCH 21/55] Fix `test_channel_messages` unit test Co-Authored-By: Nathan Sobo --- zed/src/channel.rs | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 42727ea54b7d001c0df3f1384950b2c3ff1917ec..0c3cb8bd2496dbdff7c618871237f72ff0027dd7 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -510,6 +510,21 @@ mod tests { 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 @@ -569,23 +584,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; From 7d59b2d86125bea293f583fc968bf221feb620a8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Sep 2021 04:51:22 -0600 Subject: [PATCH 22/55] Log panics when not attached to a pty Hopefully this will give us better forensics if we panic in production. --- Cargo.lock | 11 +++++++++++ zed/Cargo.toml | 1 + zed/src/main.rs | 1 + 3 files changed, 13 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 60b2864c5bd3d70dcbe44bc1547f85d68e852869..1d420bb992eb811f999740b90d6fc9005e030c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2807,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" @@ -5910,6 +5920,7 @@ dependencies = [ "lazy_static", "libc", "log", + "log-panics", "num_cpus", "parking_lot", "postage", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index bb4fab264e589d96a83960db49ad69d6b88488a2..63f64e4a5f26a991231611f678cb0433ca6a8e0b 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -35,6 +35,7 @@ image = "0.23" 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"] } diff --git a/zed/src/main.rs b/zed/src/main.rs index 9e73a961265e7347d652a6d016ece75ea513d3ce..87426c1ca7bac448aa9b0f2d49ee341687f5f525 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -90,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(); } } From ec7c6f3f91c0740fabb06f1fb0b4f91bfc523dc5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Sep 2021 05:13:05 -0600 Subject: [PATCH 23/55] Always assign credentials on rpc::Client after connecting Co-Authored-By: Antonio Scandurra --- zed/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index a01d14193fa833d6e3151eefaff37d555acf4b60..fe1dde4ffba2d2579fcfe036cb3b162126fa1847 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -321,7 +321,6 @@ impl Client { return Err(err); } }; - self.state.write().credentials = Some(credentials.clone()); credentials }; @@ -334,6 +333,7 @@ impl Client { match self.establish_connection(&credentials, cx).await { Ok(conn) => { log::info!("connected to rpc address {}", *ZED_SERVER_URL); + self.state.write().credentials = Some(credentials.clone()); if !read_from_keychain { write_credentials_to_keychain(&credentials, cx).log_err(); } From c2e9aa1b545cbcff8890df105c7e0c47b1a30ff4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Sep 2021 05:18:13 -0600 Subject: [PATCH 24/55] Render chat panel messages even if connection is lost Co-Authored-By: Antonio Scandurra --- zed/src/chat_panel.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 5ccc014ca787119f6866254b20adc1ae79cab98f..7cc152116e00361f9dcfee2e7c1fdbff9b8dcb06 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -381,9 +381,10 @@ 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) From 65b22157e7e00c26d0fb2f1f9d710fc6b4482dd2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Sep 2021 15:12:16 +0200 Subject: [PATCH 25/55] WIP --- zed/src/channel.rs | 102 ++++++++++++++++++++++---------- zed/src/chat_panel.rs | 7 ++- zed/src/theme.rs | 1 + zed/src/theme/theme_registry.rs | 50 ++++++++++++++++ 4 files changed, 128 insertions(+), 32 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 0c3cb8bd2496dbdff7c618871237f72ff0027dd7..11991b371dbbf16353682e7e7bf5ac729279c1af 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::{ @@ -39,8 +39,7 @@ 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, _subscription: rpc::Subscription, @@ -48,20 +47,21 @@ pub struct Channel { #[derive(Clone, Debug)] pub struct ChannelMessage { - pub id: u64, + pub id: ChannelMessageId, pub body: String, pub timestamp: OffsetDateTime, pub sender: Arc, } -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, } @@ -216,9 +216,8 @@ 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, _subscription, } } @@ -236,13 +235,27 @@ impl Channel { Err(anyhow!("message body can't be empty"))?; } + let current_user = self + .user_store + .current_user() + .borrow() + .clone() + .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)); + self.insert_messages( + SumTree::from_item( + ChannelMessage { + id: pending_id, + body: body.clone(), + sender: current_user, + timestamp: OffsetDateTime::now_utc(), + }, + &(), + ), + cx, + ); let user_store = self.user_store.clone(); let rpc = self.rpc.clone(); Ok(cx.spawn(|this, mut cx| async move { @@ -254,13 +267,8 @@ 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.remove_message(pending_id, cx); + this.insert_messages(SumTree::from_item(message, &()), cx); Ok(()) }) })) @@ -271,7 +279,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 @@ -354,10 +367,6 @@ impl Channel { cursor.take(range.len()) } - pub fn pending_messages(&self) -> &[PendingChannelMessage] { - &self.pending_messages - } - fn handle_message_sent( &mut self, message: TypedEnvelope, @@ -384,9 +393,30 @@ impl Channel { Ok(()) } + fn remove_message(&mut self, message_id: ChannelMessageId, cx: &mut ModelContext) { + let mut old_cursor = self.messages.cursor::(); + let mut new_messages = old_cursor.slice(&message_id, Bias::Left, &()); + let start_ix = old_cursor.sum_start().0; + let removed_messages = old_cursor.slice(&message_id, Bias::Right, &()); + let removed_count = removed_messages.summary().count; + new_messages.push_tree(old_cursor.suffix(&()), &()); + + drop(old_cursor); + self.messages = new_messages; + + if removed_count > 0 { + let end_ix = start_ix + removed_count; + cx.emit(ChannelEvent::MessagesUpdated { + old_range: start_ix..end_ix, + new_count: 0, + }); + cx.notify(); + } + } + 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 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, &()); @@ -445,12 +475,16 @@ impl ChannelMessage { ) -> Result { 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, }) } + + pub fn is_pending(&self) -> bool { + matches!(self.id, ChannelMessageId::Pending(_)) + } } impl sum_tree::Item for ChannelMessage { @@ -464,6 +498,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 +513,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; diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 7cc152116e00361f9dcfee2e7c1fdbff9b8dcb06..d7752b9a53e944b6e5c1776ab280fd3ea95f025b 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -230,7 +230,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( diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 88f385a05461b18a0db3f6182e9b3d08effb97b2..ef0798ab24ff843dabd5502fbb201cde36b0ef0b 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -95,6 +95,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, diff --git a/zed/src/theme/theme_registry.rs b/zed/src/theme/theme_registry.rs index cd9781afe942f4e5bee4d1c16b6aa5886ae376a0..18bcc9ebf2e35369200a6ea1dff009769c9da352 100644 --- a/zed/src/theme/theme_registry.rs +++ b/zed/src/theme/theme_registry.rs @@ -327,6 +327,24 @@ impl KeyPathReferenceSet { ); } } + + // If an existing reference's target 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_target, + KeyPathReference::target, + PartialEq::eq, + ) { + Self::add_dependency( + (new_id, id), + &mut self.dependencies, + &mut self.dependency_counts, + ); + } + } } // Find all existing references that satisfy a given predicate with respect @@ -619,6 +637,38 @@ mod tests { ); } + #[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!( + theme_data + .get("b") + .unwrap() + .get("text") + .unwrap() + .get("color") + .unwrap(), + "blue" + ); + } + #[test] fn test_key_path_reference_set_simple() { let input_references = build_refs(&[ From 5ee0e85f0207948afd48b516719df65f6d603b20 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Sep 2021 18:29:41 +0200 Subject: [PATCH 26/55] WIP Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- zed/src/theme.rs | 1 + zed/src/theme/resolve_tree.rs | 275 ++++++++++++++++++++++++++++++++ zed/src/theme/theme_registry.rs | 34 ++-- 3 files changed, 293 insertions(+), 17 deletions(-) create mode 100644 zed/src/theme/resolve_tree.rs diff --git a/zed/src/theme.rs b/zed/src/theme.rs index ef0798ab24ff843dabd5502fbb201cde36b0ef0b..7a84b0e2eb860db2cfae4983166b126454ba8625 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -1,4 +1,5 @@ mod highlight_map; +mod resolve_tree; mod theme_registry; use anyhow::Result; diff --git a/zed/src/theme/resolve_tree.rs b/zed/src/theme/resolve_tree.rs new file mode 100644 index 0000000000000000000000000000000000000000..bf1bad97af89609a9b39802afe5ad4521da65c7d --- /dev/null +++ b/zed/src/theme/resolve_tree.rs @@ -0,0 +1,275 @@ +use anyhow::{anyhow, Result}; +use serde_json::Value; +use std::{ + cell::RefCell, + collections::HashMap, + mem, + rc::{Rc, Weak}, +}; + +pub fn resolve(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: HashMap, + resolved: bool, + parent: Option>>, + }, + String { + value: String, + 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(s) => { + if let Some(path) = s.strip_prefix("$") { + Ok(Self::new(Node::Reference { + path: path.to_string(), + parent: None, + })) + } else { + Ok(Self::new(Node::String { + value: s, + parent: None, + })) + } + } + Value::Object(object) => { + let mut tree = Self::new(Node::Object { + base: Default::default(), + children: Default::default(), + resolved: false, + parent: None, + }); + let mut children = HashMap::new(); + let mut resolved = true; + let mut base = None; + for (key, value) in object.into_iter() { + if key == "extends" { + if let Value::String(s) = value { + base = Some(s); + resolved = false; + } + } else { + let value = 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) + } + _ => return Err(anyhow!("unsupported json type")), + } + } + + fn to_json(&self) -> Result { + match &*self.0.borrow() { + Node::Reference { .. } => Err(anyhow!("unresolved tree")), + Node::String { value, .. } => Ok(Value::String(value.clone())), + 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)) + } + _ => unimplemented!(), + } + } + + fn parent(&self) -> Option { + match &*self.0.borrow() { + Node::Reference { parent, .. } + | Node::Object { parent, .. } + | Node::String { 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")); + } + } + Node::Reference { .. } => return Ok(None), + Node::String { .. } => return Err(anyhow!("component is not an object")), + } + } + + Ok(Some(tree)) + } + + fn is_resolved(&self) -> bool { + match &*self.0.borrow() { + Node::Reference { .. } => false, + Node::Object { resolved, .. } => *resolved, + Node::String { .. } => true, + } + } + + fn update_resolved(&self) { + match &mut *self.0.borrow_mut() { + Node::Object { + resolved, children, .. + } => { + *resolved = children.values().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; + dbg!("==========="); + 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!("could not resolve tree")) + } + } + + fn resolve_subtree(&self, root: &Tree, unresolved: &mut Vec) -> Result { + let mut made_progress = false; + let borrow = self.0.borrow(); + match &*borrow { + Node::Reference { path, parent } => { + print!("entering reference ${}: ", path); + if let Some(subtree) = root.get(&path)? { + if subtree.is_resolved() { + println!("resolved"); + let parent = parent.clone(); + drop(borrow); + let mut new_node = subtree.0.borrow().clone(); + new_node.set_parent(parent); + *self.0.borrow_mut() = new_node; + Ok(true) + } else { + println!("unresolved (but existing)"); + unresolved.push(self.clone()); + Ok(false) + } + } else { + println!("unresolved (referant does not exist)"); + unresolved.push(self.clone()); + Ok(false) + } + } + Node::Object { + base, + children, + resolved, + .. + } => { + if *resolved { + println!("already resolved"); + Ok(false) + } else { + let mut children_resolved = true; + for (key, child) in children.iter() { + println!("resolving subtree {}", key); + made_progress |= child.resolve_subtree(root, unresolved)?; + children_resolved &= child.is_resolved(); + } + + if children_resolved { + drop(borrow); + if let Node::Object { resolved, .. } = &mut *self.0.borrow_mut() { + *resolved = true; + } + } + + Ok(made_progress) + } + } + Node::String { value, .. } => { + println!("terminating at string: {}", value); + return Ok(false); + } + } + } +} + +impl Node { + fn set_parent(&mut self, new_parent: Option>>) { + match self { + Node::Reference { parent, .. } + | Node::Object { parent, .. } + | Node::String { parent, .. } => *parent = new_parent, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_basic() { + let json = serde_json::json!({ + "a": { + "x": "$b.d" + }, + "b": { + "c": "$a", + "d": "$e.f" + }, + "e": { + "f": "1" + } + }); + + dbg!(resolve(json).unwrap()); + } +} diff --git a/zed/src/theme/theme_registry.rs b/zed/src/theme/theme_registry.rs index 18bcc9ebf2e35369200a6ea1dff009769c9da352..ad423042a421254854f6e3384cfc3cf65aaa0588 100644 --- a/zed/src/theme/theme_registry.rs +++ b/zed/src/theme/theme_registry.rs @@ -336,7 +336,7 @@ impl KeyPathReferenceSet { &self.references, &self.reference_ids_by_target, KeyPathReference::target, - PartialEq::eq, + KeyPath::starts_with, ) { Self::add_dependency( (new_id, id), @@ -733,20 +733,20 @@ mod tests { #[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"), - ], + // &[ + // ("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"), @@ -754,13 +754,13 @@ mod tests { ("a.b.c2", "x.b2.c"), ], &[ - ("x.y", "m.n.n.o.q"), ("x.y.z", "m.n.n.o.p"), + ("x.y", "m.n.n.o.q"), ("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"), + ("a.b.c.d", "u.v"), ], ]; From 95ef70e4f4defa0510954368f3d5f6ab14e4a169 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Sep 2021 12:56:20 -0600 Subject: [PATCH 27/55] Switch to new resolution system in ThemeRegistry Co-Authored-By: Max Brunsfeld --- Cargo.lock | 1 + zed/Cargo.toml | 1 + zed/assets/themes/_base.toml | 6 + zed/src/theme.rs | 2 +- zed/src/theme/resolution.rs | 441 +++++++++++++++++++++++++ zed/src/theme/resolve_tree.rs | 275 ---------------- zed/src/theme/theme_registry.rs | 547 +------------------------------- 7 files changed, 464 insertions(+), 809 deletions(-) create mode 100644 zed/src/theme/resolution.rs delete mode 100644 zed/src/theme/resolve_tree.rs diff --git a/Cargo.lock b/Cargo.lock index 1d420bb992eb811f999740b90d6fc9005e030c43..ded74ab07e70086200fd0b565b450d4b3fa7c41c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5917,6 +5917,7 @@ dependencies = [ "http-auth-basic", "ignore", "image 0.23.14", + "indexmap", "lazy_static", "libc", "log", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 63f64e4a5f26a991231611f678cb0433ca6a8e0b..8d27fcd4c540b58544dd6223960b74409afcae86 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -32,6 +32,7 @@ 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" diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 485d3bf2a79e7269fb24462c1160d370c7bf1b89..1a2999379c4e46f82514349cddeee38ecb697467 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -67,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" diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 7a84b0e2eb860db2cfae4983166b126454ba8625..a96945fecc1011d9c1de9ef941560f863350491d 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -1,5 +1,5 @@ mod highlight_map; -mod resolve_tree; +mod resolution; mod theme_registry; use anyhow::Result; diff --git a/zed/src/theme/resolution.rs b/zed/src/theme/resolution.rs new file mode 100644 index 0000000000000000000000000000000000000000..a37726023008e3f6cd6a434023654ecf08f4fd64 --- /dev/null +++ b/zed/src/theme/resolution.rs @@ -0,0 +1,441 @@ +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(String), + Number(serde_json::Number), + Bool(bool), + Null, +} + +#[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))) + } + } + Value::Number(value) => Ok(Self::new(Node::Number(value))), + Value::Bool(value) => Ok(Self::new(Node::Bool(value))), + Value::Null => Ok(Self::new(Node::Null)), + 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)) + } 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, .. } => parent.as_ref().and_then(|p| p.upgrade()).map(Tree), + _ => None, + } + } + + 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 mut made_progress = false; + let borrow = self.0.borrow(); + match &*borrow { + Node::Reference { path, parent } => { + if let Some(subtree) = root.get(&path)? { + if subtree.is_resolved() { + let parent = parent.clone(); + drop(borrow); + 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 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(borrow); + + if let Some(base) = resolved_base.as_ref() { + self.extend_from(&base); + } + + 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 children_resolved = true; + for child in children.iter() { + made_progress |= child.resolve_subtree(root, unresolved)?; + children_resolved &= child.is_resolved(); + } + + if children_resolved { + drop(borrow); + + if let Node::Array { resolved, .. } = &mut *self.0.borrow_mut() { + *resolved = true; + } + } + + Ok(made_progress) + } + } + Node::String(_) | Node::Number(_) | Node::Bool(_) | Node::Null => { + return 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, .. } => *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!({ + "e": { + "f": "1", + "x": "1" + }, + "a": { + "x": "1" + }, + "b": { + "c": { + "x": "1" + }, + "d": "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/resolve_tree.rs b/zed/src/theme/resolve_tree.rs deleted file mode 100644 index bf1bad97af89609a9b39802afe5ad4521da65c7d..0000000000000000000000000000000000000000 --- a/zed/src/theme/resolve_tree.rs +++ /dev/null @@ -1,275 +0,0 @@ -use anyhow::{anyhow, Result}; -use serde_json::Value; -use std::{ - cell::RefCell, - collections::HashMap, - mem, - rc::{Rc, Weak}, -}; - -pub fn resolve(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: HashMap, - resolved: bool, - parent: Option>>, - }, - String { - value: String, - 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(s) => { - if let Some(path) = s.strip_prefix("$") { - Ok(Self::new(Node::Reference { - path: path.to_string(), - parent: None, - })) - } else { - Ok(Self::new(Node::String { - value: s, - parent: None, - })) - } - } - Value::Object(object) => { - let mut tree = Self::new(Node::Object { - base: Default::default(), - children: Default::default(), - resolved: false, - parent: None, - }); - let mut children = HashMap::new(); - let mut resolved = true; - let mut base = None; - for (key, value) in object.into_iter() { - if key == "extends" { - if let Value::String(s) = value { - base = Some(s); - resolved = false; - } - } else { - let value = 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) - } - _ => return Err(anyhow!("unsupported json type")), - } - } - - fn to_json(&self) -> Result { - match &*self.0.borrow() { - Node::Reference { .. } => Err(anyhow!("unresolved tree")), - Node::String { value, .. } => Ok(Value::String(value.clone())), - 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)) - } - _ => unimplemented!(), - } - } - - fn parent(&self) -> Option { - match &*self.0.borrow() { - Node::Reference { parent, .. } - | Node::Object { parent, .. } - | Node::String { 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")); - } - } - Node::Reference { .. } => return Ok(None), - Node::String { .. } => return Err(anyhow!("component is not an object")), - } - } - - Ok(Some(tree)) - } - - fn is_resolved(&self) -> bool { - match &*self.0.borrow() { - Node::Reference { .. } => false, - Node::Object { resolved, .. } => *resolved, - Node::String { .. } => true, - } - } - - fn update_resolved(&self) { - match &mut *self.0.borrow_mut() { - Node::Object { - resolved, children, .. - } => { - *resolved = children.values().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; - dbg!("==========="); - 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!("could not resolve tree")) - } - } - - fn resolve_subtree(&self, root: &Tree, unresolved: &mut Vec) -> Result { - let mut made_progress = false; - let borrow = self.0.borrow(); - match &*borrow { - Node::Reference { path, parent } => { - print!("entering reference ${}: ", path); - if let Some(subtree) = root.get(&path)? { - if subtree.is_resolved() { - println!("resolved"); - let parent = parent.clone(); - drop(borrow); - let mut new_node = subtree.0.borrow().clone(); - new_node.set_parent(parent); - *self.0.borrow_mut() = new_node; - Ok(true) - } else { - println!("unresolved (but existing)"); - unresolved.push(self.clone()); - Ok(false) - } - } else { - println!("unresolved (referant does not exist)"); - unresolved.push(self.clone()); - Ok(false) - } - } - Node::Object { - base, - children, - resolved, - .. - } => { - if *resolved { - println!("already resolved"); - Ok(false) - } else { - let mut children_resolved = true; - for (key, child) in children.iter() { - println!("resolving subtree {}", key); - made_progress |= child.resolve_subtree(root, unresolved)?; - children_resolved &= child.is_resolved(); - } - - if children_resolved { - drop(borrow); - if let Node::Object { resolved, .. } = &mut *self.0.borrow_mut() { - *resolved = true; - } - } - - Ok(made_progress) - } - } - Node::String { value, .. } => { - println!("terminating at string: {}", value); - return Ok(false); - } - } - } -} - -impl Node { - fn set_parent(&mut self, new_parent: Option>>) { - match self { - Node::Reference { parent, .. } - | Node::Object { parent, .. } - | Node::String { parent, .. } => *parent = new_parent, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_basic() { - let json = serde_json::json!({ - "a": { - "x": "$b.d" - }, - "b": { - "c": "$a", - "d": "$e.f" - }, - "e": { - "f": "1" - } - }); - - dbg!(resolve(json).unwrap()); - } -} diff --git a/zed/src/theme/theme_registry.rs b/zed/src/theme/theme_registry.rs index ad423042a421254854f6e3384cfc3cf65aaa0588..64770aedbe51b5d889752502636ada8b3aba2666 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,311 +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, - ); - } - } - - // If an existing reference's target 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_target, - KeyPathReference::target, - KeyPath::starts_with, - ) { - Self::add_dependency( - (new_id, 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 { @@ -473,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) { @@ -593,6 +182,12 @@ mod tests { let registry = ThemeRegistry::new(assets, cx.font_cache().clone()); let theme_data = registry.load("light", true).unwrap(); + + println!( + "{}", + serde_json::to_string_pretty(theme_data.as_ref()).unwrap() + ); + assert_eq!( theme_data.as_ref(), &serde_json::json!({ @@ -669,120 +264,6 @@ 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); - } - - 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"), - ] - ); - } - - #[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.z", "m.n.n.o.p"), - ("x.y", "m.n.n.o.q"), - ("u.v.w", "x.y.z"), - ("a.b.c.d.e", "u.v"), - ("a.b.c.d.f", "u.v"), - ("a.b.c.d.g", "u.v"), - ("a.b.c.d", "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 { From 31b5602dc14fbb744c0ff5d43abdd3db8ed1de12 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 15 Sep 2021 13:40:28 -0700 Subject: [PATCH 28/55] Get server integration tests passing again * Set up UserStore to have the current user, so that channel messages can be sent. This is needed now that pending messages are represented more similarly to regular messages. * Drop buffer inside of an `AppContext.update` block, so that the Buffer's release hook is called in time. Co-Authored-By: Nathan Sobo --- server/src/db.rs | 37 ++++++-- server/src/rpc.rs | 206 ++++++++++++++++++++++--------------------- zed/src/channel.rs | 2 - zed/src/user.rs | 8 +- zed/src/workspace.rs | 4 +- 5 files changed, 140 insertions(+), 117 deletions(-) diff --git a/server/src/db.rs b/server/src/db.rs index 8d2199a9f33a39c2b0f65d0e5eb60560c3ad2ab0..28aadd0d6334793f1307dfded99a550696571ba2 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -133,14 +133,18 @@ impl Db { let query = " SELECT users.* FROM - users, channel_memberships + users LEFT JOIN channel_memberships + ON + channel_memberships.user_id = users.id WHERE - users.id = ANY ($1) AND - channel_memberships.user_id = users.id AND - channel_memberships.channel_id IN ( - SELECT channel_id - FROM channel_memberships - WHERE channel_memberships.user_id = $2 + users.id = $2 OR + ( + users.id = ANY ($1) AND + channel_memberships.channel_id IN ( + SELECT channel_id + FROM channel_memberships + WHERE channel_memberships.user_id = $2 + ) ) "; @@ -455,7 +459,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, @@ -563,6 +567,23 @@ pub mod tests { } } + #[gpui::test] + async fn test_get_users_by_ids() { + let test_db = TestDb::new(); + let db = test_db.db(); + let user_id = db.create_user("user", false).await.unwrap(); + assert_eq!( + db.get_users_by_ids(user_id, Some(user_id).iter().copied()) + .await + .unwrap(), + vec![User { + id: user_id, + github_login: "user".to_string(), + admin: false, + }] + ) + } + #[gpui::test] async fn test_recent_channel_messages() { let test_db = TestDb::new(); diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 1e0fe2465cafbe2d7034e941500145cda25ebd1d..e6a48ae41032af00cc48d6635222c94af21b761e 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1039,8 +1039,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(); @@ -1124,7 +1124,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; @@ -1147,9 +1147,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()); @@ -1288,8 +1288,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()); @@ -1369,8 +1369,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()); @@ -1429,8 +1429,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()); @@ -1484,38 +1484,39 @@ mod tests { #[gpui::test] async fn test_basic_chat(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; // 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(), ) .await .unwrap(); - 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()) @@ -1536,12 +1537,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 = - UserStore::new(client_b.clone(), http.clone(), cx_b.background().as_ref()); 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()) @@ -1563,7 +1562,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; @@ -1575,28 +1574,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; @@ -1616,33 +1612,25 @@ 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] async fn test_chat_message_validation(mut cx_a: TestAppContext) { cx_a.foreground().forbid_parking(); - let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) }); 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 = UserStore::new(client_a.clone(), http, 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()) @@ -1692,27 +1680,31 @@ 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; 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(), ) @@ -1742,13 +1734,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 = - UserStore::new(client_b.clone(), http.clone(), cx_b.background().as_ref()); - 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; @@ -1769,13 +1759,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 { .. }) @@ -1793,7 +1783,7 @@ 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)] ) }); @@ -1806,12 +1796,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 }) @@ -1827,9 +1817,9 @@ 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_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; @@ -1845,10 +1835,10 @@ 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_a".to_string(), "you online?".to_string(), false), ] }) .await; @@ -1863,22 +1853,14 @@ 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_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 { @@ -1913,8 +1895,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(); @@ -1926,13 +1908,13 @@ mod tests { cx.spawn(|_| async move { let access_token = "the-token".to_string(); Ok(Credentials { - user_id: client_user_id.0 as u64, + user_id: user_id.0 as u64, access_token, }) }) }) .override_establish_connection(move |credentials, cx| { - assert_eq!(credentials.user_id, client_user_id.0 as u64); + assert_eq!(credentials.user_id, user_id.0 as u64); assert_eq!(credentials.access_token, "the-token"); let server = server.clone(); @@ -1946,24 +1928,26 @@ mod tests { ))) } else { let (client_conn, server_conn, kill_conn) = Connection::in_memory(); - connection_killers.lock().insert(client_user_id, kill_conn); + connection_killers.lock().insert(user_id, kill_conn); cx.background() - .spawn(server.handle_connection( - server_conn, - client_name, - client_user_id, - )) + .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) { @@ -2019,6 +2003,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/src/channel.rs b/zed/src/channel.rs index 11991b371dbbf16353682e7e7bf5ac729279c1af..ed7fc4d6c966e4c12c0f609e837fd7d63694f269 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -238,8 +238,6 @@ impl Channel { let current_user = self .user_store .current_user() - .borrow() - .clone() .ok_or_else(|| anyhow!("current_user is not present"))?; let channel_id = self.details.id; diff --git a/zed/src/user.rs b/zed/src/user.rs index 06aab321934dd1ffb985430157b7cfd02385dbc7..54e84d756ff81229a44dcb5291e12fec1618da27 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -111,8 +111,12 @@ impl UserStore { .ok_or_else(|| anyhow!("server responded with no users")) } - pub fn current_user(&self) -> &watch::Receiver>> { - &self.current_user + pub fn current_user(&self) -> Option> { + self.current_user.borrow().clone() + } + + pub fn watch_current_user(&self) -> watch::Receiver>> { + self.current_user.clone() } } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 9ce67c2f8a678eb344b91118cde78a5bd0000d3d..ff3666e0de077cc8667716482f2479ba220e0825 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -389,7 +389,7 @@ impl Workspace { ); right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); - let mut current_user = app_state.user_store.current_user().clone(); + 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; @@ -990,8 +990,6 @@ impl Workspace { let avatar = if let Some(avatar) = self .user_store .current_user() - .borrow() - .as_ref() .and_then(|user| user.avatar.clone()) { Image::new(avatar) From caf0f0e4289a40d4eff849c0a6e2e3803bda28c1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 15 Sep 2021 16:40:18 -0700 Subject: [PATCH 29/55] Fix duplicated results in get_users_by_ids --- server/src/db.rs | 121 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 103 insertions(+), 18 deletions(-) diff --git a/server/src/db.rs b/server/src/db.rs index 28aadd0d6334793f1307dfded99a550696571ba2..c3e270bc8759a92b971ce7a73b8914be8c9e7ace 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -128,29 +128,46 @@ 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 LEFT JOIN channel_memberships - ON - channel_memberships.user_id = users.id + users, channel_memberships WHERE - users.id = $2 OR - ( - users.id = ANY ($1) AND - channel_memberships.channel_id IN ( - SELECT channel_id - FROM channel_memberships - WHERE channel_memberships.user_id = $2 - ) + users.id = ANY ($1) AND + channel_memberships.user_id = users.id AND + channel_memberships.channel_id IN ( + SELECT channel_id + 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 }) @@ -571,16 +588,84 @@ pub mod tests { async fn test_get_users_by_ids() { let test_db = TestDb::new(); let db = test_db.db(); - let user_id = db.create_user("user", false).await.unwrap(); + + 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_id, Some(user_id).iter().copied()) + 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: user_id, - github_login: "user".to_string(), + id: friend1, + github_login: "friend-1".to_string(), admin: false, - }] + },] ) } From 23920754675d208baafe1248ea0b639292e6cf4c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 15 Sep 2021 17:49:51 -0700 Subject: [PATCH 30/55] Get resolve_references test passing --- zed/src/theme/resolution.rs | 30 +++++++++++++++++------------- zed/src/theme/theme_registry.rs | 5 ----- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/zed/src/theme/resolution.rs b/zed/src/theme/resolution.rs index a37726023008e3f6cd6a434023654ecf08f4fd64..054ece1ff2cb231c57b16c088dc1afbee9ec877d 100644 --- a/zed/src/theme/resolution.rs +++ b/zed/src/theme/resolution.rs @@ -291,6 +291,8 @@ impl Tree { if let Some(base) = root.get(base)? { if base.is_resolved() { resolved_base = Some(base); + } else { + made_progress |= base.resolve_subtree(root, unresolved)?; } } } @@ -409,19 +411,21 @@ mod test { assert_eq!( resolve_references(json).unwrap(), serde_json::json!({ - "e": { - "f": "1", - "x": "1" - }, - "a": { - "x": "1" - }, - "b": { - "c": { - "x": "1" - }, - "d": "1" - }}) + "a": { + "x": "1" + }, + "b": { + "c": { + "x": "1" + }, + "d": "1" + }, + "e": { + "extends": "$a", + "f": "1", + "x": "1" + }, + }) ) } diff --git a/zed/src/theme/theme_registry.rs b/zed/src/theme/theme_registry.rs index 64770aedbe51b5d889752502636ada8b3aba2666..c5cf8f2fcbd856a7e0a5419f5337e8c198aaca59 100644 --- a/zed/src/theme/theme_registry.rs +++ b/zed/src/theme/theme_registry.rs @@ -183,11 +183,6 @@ mod tests { let registry = ThemeRegistry::new(assets, cx.font_cache().clone()); let theme_data = registry.load("light", true).unwrap(); - println!( - "{}", - serde_json::to_string_pretty(theme_data.as_ref()).unwrap() - ); - assert_eq!( theme_data.as_ref(), &serde_json::json!({ From fbe77e92b48395a40a0118808dff3e6ef6a84e07 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 15 Sep 2021 17:56:08 -0700 Subject: [PATCH 31/55] Run all crates' tests on CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From df4b5890fd59ec1031fbbc9ff6a644db99225621 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 15 Sep 2021 18:06:21 -0700 Subject: [PATCH 32/55] Run fewer iterations of the gpui list test by default --- gpui/src/elements/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 1a86e2935cd774837d2dbf03f12acd089c4e487b..34b8427229c546417abc201a64c7b64869fa92ee 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -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")) From 9402eb3f3ea21f64875a74fef26b7574510ecdad Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Sep 2021 11:15:48 +0200 Subject: [PATCH 33/55] Maintain parent pointers from primitive values too After resolving a reference to a primitive value, we want to set its parent pointer so that `Tree::resolve` can navigate upward and update the cached resolved status of each node. --- zed/src/theme/resolution.rs | 95 ++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/zed/src/theme/resolution.rs b/zed/src/theme/resolution.rs index 054ece1ff2cb231c57b16c088dc1afbee9ec877d..fd3864e274af20f75fbfe4da54f43fcdcdecc6c3 100644 --- a/zed/src/theme/resolution.rs +++ b/zed/src/theme/resolution.rs @@ -30,10 +30,21 @@ enum Node { resolved: bool, parent: Option>>, }, - String(String), - Number(serde_json::Number), - Bool(bool), - Null, + String { + value: String, + parent: Option>>, + }, + Number { + value: serde_json::Number, + parent: Option>>, + }, + Bool { + value: bool, + parent: Option>>, + }, + Null { + parent: Option>>, + }, } #[derive(Clone)] @@ -53,12 +64,21 @@ impl Tree { parent: None, })) } else { - Ok(Self::new(Node::String(value))) + Ok(Self::new(Node::String { + value, + parent: None, + })) } } - Value::Number(value) => Ok(Self::new(Node::Number(value))), - Value::Bool(value) => Ok(Self::new(Node::Bool(value))), - Value::Null => Ok(Self::new(Node::Null)), + 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(), @@ -75,7 +95,10 @@ impl Tree { if let Value::String(value) = value { base = value.strip_prefix("$").map(str::to_string); resolved = false; - Self::new(Node::String(value)) + Self::new(Node::String { + value, + parent: None, + }) } else { unreachable!() } @@ -133,10 +156,10 @@ impl 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::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 { @@ -158,8 +181,11 @@ impl Tree { match &*self.0.borrow() { Node::Reference { parent, .. } | Node::Object { parent, .. } - | Node::Array { parent, .. } => parent.as_ref().and_then(|p| p.upgrade()).map(Tree), - _ => None, + | 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), } } @@ -182,10 +208,10 @@ impl Tree { } Node::Reference { .. } => return Ok(None), Node::Array { .. } - | Node::String(_) - | Node::Number(_) - | Node::Bool(_) - | Node::Null => { + | Node::String { .. } + | Node::Number { .. } + | Node::Bool { .. } + | Node::Null { .. } => { return Err(anyhow!( "key \"{}\" in path \"{}\" is not an object", component, @@ -202,7 +228,9 @@ impl Tree { match &*self.0.borrow() { Node::Reference { .. } => false, Node::Object { resolved, .. } | Node::Array { resolved, .. } => *resolved, - Node::String(_) | Node::Number(_) | Node::Bool(_) | Node::Null => true, + Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => { + true + } } } @@ -247,14 +275,13 @@ impl Tree { } fn resolve_subtree(&self, root: &Tree, unresolved: &mut Vec) -> Result { - let mut made_progress = false; - let borrow = self.0.borrow(); - match &*borrow { + 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(borrow); + drop(node); let mut new_node = subtree.0.borrow().clone(); new_node.set_parent(parent); *self.0.borrow_mut() = new_node; @@ -277,6 +304,7 @@ impl Tree { 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)?; @@ -291,16 +319,15 @@ impl Tree { if let Some(base) = root.get(base)? { if base.is_resolved() { resolved_base = Some(base); - } else { - made_progress |= base.resolve_subtree(root, unresolved)?; } } } - drop(borrow); + 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() { @@ -325,6 +352,7 @@ impl Tree { 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)?; @@ -332,7 +360,7 @@ impl Tree { } if children_resolved { - drop(borrow); + drop(node); if let Node::Array { resolved, .. } = &mut *self.0.borrow_mut() { *resolved = true; @@ -342,8 +370,8 @@ impl Tree { Ok(made_progress) } } - Node::String(_) | Node::Number(_) | Node::Bool(_) | Node::Null => { - return Ok(false); + Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => { + Ok(false) } } } @@ -382,8 +410,11 @@ impl Node { match self { Node::Reference { parent, .. } | Node::Object { parent, .. } - | Node::Array { parent, .. } => *parent = new_parent, - _ => {} + | Node::Array { parent, .. } + | Node::String { parent, .. } + | Node::Number { parent, .. } + | Node::Bool { parent, .. } + | Node::Null { parent } => *parent = new_parent, } } } From 4a96a5c9ff1367e6a10fc5afccab7aaea522ebc7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Sep 2021 11:46:53 +0200 Subject: [PATCH 34/55] Use a negative delta to scroll down in layout unit test for `List` --- gpui/src/elements/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 34b8427229c546417abc201a64c7b64869fa92ee..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), ); From 8973e250caab8d3a4ac26ebb9c25d9b3b11f38fe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Sep 2021 16:23:20 +0200 Subject: [PATCH 35/55] Re-send pending messages after reconnecting --- Cargo.lock | 13 +++-- server/Cargo.toml | 5 +- server/src/bin/seed.rs | 2 +- server/src/db.rs | 44 +++++++++++++-- server/src/rpc.rs | 45 +++++++++++++++- zed/src/channel.rs | 119 ++++++++++++++++++++++++++++++----------- zrpc/proto/zed.proto | 7 +++ zrpc/src/proto.rs | 19 +++++++ 8 files changed, 211 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ded74ab07e70086200fd0b565b450d4b3fa7c41c..7973316c9e337532685bfc546f22dae9d8a7bee1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -836,7 +836,7 @@ dependencies = [ "target_build_utils", "term", "toml 0.4.10", - "uuid", + "uuid 0.5.1", "walkdir", ] @@ -884,7 +884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e7fb075b9b54e939006aa12e1f6cd2d3194041ff4ebe7f2efcbedf18f25b667" dependencies = [ "byteorder", - "uuid", + "uuid 0.5.1", ] [[package]] @@ -2963,7 +2963,7 @@ dependencies = [ "byteorder", "cfb", "encoding", - "uuid", + "uuid 0.5.1", ] [[package]] @@ -4784,6 +4784,7 @@ dependencies = [ "thiserror", "time 0.2.25", "url", + "uuid 0.8.2", "webpki", "webpki-roots", "whoami", @@ -5606,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" 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/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 c3e270bc8759a92b971ce7a73b8914be8c9e7ace..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; @@ -402,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) @@ -414,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) @@ -430,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 @@ -514,6 +517,7 @@ pub struct ChannelMessage { pub sender_id: UserId, pub body: String, pub sent_at: OffsetDateTime, + pub nonce: Uuid, } #[cfg(test)] @@ -677,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(); } @@ -697,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 e6a48ae41032af00cc48d6635222c94af21b761e..debd982366c7a4b9d1339963612d9e101dfcff0f 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -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 @@ -1513,6 +1530,7 @@ mod tests { current_user_id(&user_store_b), "hello A, it's B.", OffsetDateTime::now_utc(), + 1, ) .await .unwrap(); @@ -1707,6 +1725,7 @@ mod tests { current_user_id(&user_store_b), "hello A, it's B.", OffsetDateTime::now_utc(), + 2, ) .await .unwrap(); @@ -1787,6 +1806,24 @@ mod tests { ) }); + // 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| { @@ -1812,7 +1849,8 @@ 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) @@ -1820,6 +1858,7 @@ mod tests { ("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; @@ -1838,6 +1877,7 @@ mod tests { ("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), ] }) @@ -1856,6 +1896,7 @@ mod tests { ("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), ] diff --git a/zed/src/channel.rs b/zed/src/channel.rs index ed7fc4d6c966e4c12c0f609e837fd7d63694f269..c43cf2e6f7b28a45e5a69dfa67c0383e065f6143 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -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, @@ -42,6 +43,7 @@ pub struct Channel { next_pending_message_id: usize, user_store: Arc, rpc: Arc, + rng: StdRng, _subscription: rpc::Subscription, } @@ -51,6 +53,7 @@ pub struct ChannelMessage { pub body: String, pub timestamp: OffsetDateTime, pub sender: Arc, + pub nonce: u128, } #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -218,6 +221,7 @@ impl Channel { messages: Default::default(), loaded_all_messages: false, next_pending_message_id: 0, + rng: StdRng::from_entropy(), _subscription, } } @@ -242,6 +246,7 @@ impl Channel { let channel_id = self.details.id; 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 { @@ -249,6 +254,7 @@ impl Channel { body: body.clone(), sender: current_user, timestamp: OffsetDateTime::now_utc(), + nonce, }, &(), ), @@ -257,7 +263,11 @@ impl Channel { 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"))?, @@ -265,7 +275,6 @@ impl Channel { ) .await?; this.update(&mut cx, |this, cx| { - this.remove_message(pending_id, cx); this.insert_messages(SumTree::from_item(message, &()), cx); Ok(()) }) @@ -312,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() @@ -365,6 +393,12 @@ impl Channel { cursor.take(range.len()) } + 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( &mut self, message: TypedEnvelope, @@ -391,29 +425,13 @@ impl Channel { Ok(()) } - fn remove_message(&mut self, message_id: ChannelMessageId, cx: &mut ModelContext) { - let mut old_cursor = self.messages.cursor::(); - let mut new_messages = old_cursor.slice(&message_id, Bias::Left, &()); - let start_ix = old_cursor.sum_start().0; - let removed_messages = old_cursor.slice(&message_id, Bias::Right, &()); - let removed_count = removed_messages.summary().count; - new_messages.push_tree(old_cursor.suffix(&()), &()); - - drop(old_cursor); - self.messages = new_messages; - - if removed_count > 0 { - let end_ix = start_ix + removed_count; - cx.emit(ChannelEvent::MessagesUpdated { - old_range: start_ix..end_ix, - new_count: 0, - }); - cx.notify(); - } - } - fn insert_messages(&mut self, messages: SumTree, cx: &mut ModelContext) { if let Some((first_message, last_message)) = messages.first().zip(messages.last()) { + 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; @@ -423,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, @@ -477,6 +525,10 @@ impl ChannelMessage { body: message.body, timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?, sender, + nonce: message + .nonce + .ok_or_else(|| anyhow!("nonce is required"))? + .into(), }) } @@ -606,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, @@ -665,6 +719,7 @@ mod tests { body: "c".into(), timestamp: 1002, sender_id: 7, + nonce: Some(3.into()), }), }) .await; @@ -720,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/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/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 + } +} From 02768b7f7bcab53e40c19e0910daca517b04cff6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Sep 2021 17:39:12 +0200 Subject: [PATCH 36/55] Remove duplicated text base definition --- zed/assets/themes/black.toml | 3 +-- zed/assets/themes/dark.toml | 1 - zed/assets/themes/light.toml | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) 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" } From 79fb3aa8afab40255fc514541a1308270108c733 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Sep 2021 17:43:43 +0200 Subject: [PATCH 37/55] Add migration to add a `nonce` column to `channel_messages` Co-Authored-By: Nathan Sobo --- .../20210916123647_add_nonce_to_channel_messages.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 server/migrations/20210916123647_add_nonce_to_channel_messages.sql 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"); From 260114af6c75a4b420fe136c7cee7f9babdad2e0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Sep 2021 17:47:17 +0200 Subject: [PATCH 38/55] Update Inconsolata to not include ligatures Fixes #146 Co-Authored-By: Nathan Sobo --- .../fonts/inconsolata/Inconsolata-Bold.ttf | Bin 120140 -> 107220 bytes .../fonts/inconsolata/Inconsolata-Regular.ttf | Bin 110488 -> 105628 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf b/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf index e8aad4c3cd21d9811f33b08fef26502b36441641..6c930e3bc724408d0e9884aeb91cdbcd4edc98ec 100644 GIT binary patch literal 107220 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 ZZ})}Vj=|aacxWfirgd_jq1*iD{{S;H)C~Xt diff --git a/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf b/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf index 00ffc946a5aaccdbc563b2725b2c921aad1979a0..3b74e08a12d9abf807de632719ab781c809d6bf2 100644 GIT binary patch literal 105628 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 z z?|0Oh6QZM?5Sx!1zJ3)_^L;c*wIqK1aTCdp;ERx-A+!;?LrYmHrUjTzv>P&cZX#N1C9&>~{=s*bok*r0)`;S_&ABQB4=T?)8;D??n(mNwUY;-8t#4mx&qWi$hbOQ|D3NnV||iic8l66;|{qzYId)bYx_V+&?SomWsa060;#8CD~O}zyEio zvy?oK!|BOzI6Pm!rmTrhh(d4PA8a-!CdI}yb@0s%Bhx$P=E^oN8P{grCxR==pMWeo zSRk06WQ~n6z&*D>I;VkG+Tc*3%P=|sa4<)@!+@w?MU}a#1}-N>3#_mSYaC`cNK>k+ zW-1otpe4@mGamTV+LXkb6+@J5$%0KUQU@R=fS>1K|HZQ6U}20!MFarVsbmQ% zTcuKf<*L^i%VLwPNtzK@VY6Y5w3>an*8G|qe^}MJ+r33uGx8!Y|N2V)>c2l9DMJ6| zqkmC87r~uW<>a-RnIg%2EDy%@CNPC9EoN4E#X(qL1 z$r#VyN->${2 z%F&)keBH(lyI^eB>XO|4=3X5_8{O|oLA!iOzT58`tT3o^25Hk zkOx^7`QkFq@o{xfoL9p2IIqMf8%)BV9EVT7IN-vrJ3Y=T^O9XB^XhTl@if4$6S2`< zqa(bMMIknNj8_VGc8pg$v@-lEx=2uGP`ihs*8HMt}1A}iUnh$~E}m*NUk zey+p;E=9B{7NOzNnn^eoAKEz7QmHuc+Ni?xr#n^cj@9&1c=w?0! zA3O>cLRO~B8LOC7#fD&nIxQwz!S`@Dp8_p}v7~K{q?RZuDJ7EYhGe% zo;lW^ky_-gG5_F>AKhxTr;Bx*40}>jK_(y0swrN#wBzVp)d#>O^SMWr*! zN_&c7Ys_pYD{hUb&xUirRl#yIY#-QN9IHeeK^Fs~kl8XZG)>YcV=OJzX~(#>`TbTi zMkZ>edd-e80vuK3w$d_DLl11f?UIPyyC>B&wrz!x!_~N`{?gu3kaF`MTZ%yl{wOSc zbR~9hZNMy_$Ce&Vf)k~Z50wH@$UyA|1$O9Ga6%{u;a5l>S~yT?V;DFljMA2bF%9sZ z94X11WqFhHQu3VkI9L)Kt4CN(GWn~Dl`6TOc*h!A#{?R(o-JNmX)XN2b5)D#N7d=E-}8&%Sg@M(h6bM?T2UEHJh| zQ@o;Y&dSE*&b7fA7t_W$h1FulcnSSW>v9#rQfNhTCctM-d`19vkz_V3Zn+|-t;WS$ zU09D3+!pI`CbP|_M)Y*;vGWEq>T<8_*>~~9ZRqQZl{JfdeVvujBkyyY?C+9E&^Dre zq2F#sMPvF+!pSa`@I!b@`mh#xL2nqY+ockp>0cL5%{G`7Q%q_{pmvI@sbO&H(!PoX-Xaq>#Uxkf zl+~xSwluY;6)udP(Gq1aTVtK6ErIkre~SN{s*bt&IgJKOVyw$ros(8rkdji-kXtyr zTJlx2L-fNz*$QHxk@yF6IQ55EuaWq-N9*OR9*4i* z5&;k6!{!B?+6N(GH7M&P<%jSEuau+S>dQ(@z=9lNrSMa*;RF-Xco=%0q76btKv!`L zo2?KA;~<6-oN{QhL4!$~iGsc}U&ZPXL^2y1qLC?OD`i#2l_PV~}hUxx2a~GA*iq5Ew zjh#Fxqba}c;=;DnhKi9K{zA`!+_K)13!(=Xw81qG+k>!?O~T#)Nnp^6@Q=+5@ptfo z;e0v;QM5=Im@9VQVEYZ}2pKVc=`ar+Q_5OezJ3w!$I3{cwN4{Pgo+{Nv@9YliSU`( zVJ5GBY-d6g?M$GRT�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%0DoA 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 literal 110488 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 WNA1Vt?0zD;&7is6TxR$-|M@>8AH|OV From ad9712db7034308b0c27bf2c6fdc08963c96fa8d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Sep 2021 12:26:18 -0600 Subject: [PATCH 39/55] Move EditorStyle into editor module --- zed/src/editor.rs | 48 +++++++++++++++++++++++++++++++++++++-- zed/src/editor/element.rs | 6 +++-- zed/src/theme.rs | 45 +----------------------------------- 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 3157b0cd2fbb28010d13d2b6ab5da4ec4ba35f6c..8926d5afd5074e5cc34563e0ee571fca3d958a0d 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -5,7 +5,7 @@ pub mod movement; use crate::{ settings::{HighlightId, Settings}, - theme::{EditorStyle, Theme}, + theme::Theme, time::ReplicaId, util::{post_inc, Bias}, workspace, @@ -20,7 +20,7 @@ use gpui::{ action, color::Color, font_cache::FamilyId, - fonts::Properties as FontProperties, + fonts::{HighlightStyle, Properties as FontProperties}, geometry::vector::Vector2F, keymap::Binding, text_layout::{self, RunStyle}, @@ -278,6 +278,26 @@ pub enum EditorMode { Full, } +#[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, Copy, Default, Deserialize)] +pub struct SelectionStyle { + pub cursor: Color, + pub selection: Color, +} + pub struct Editor { handle: WeakViewHandle, buffer: ModelHandle, @@ -2569,6 +2589,30 @@ impl Snapshot { } } +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(), + } + } +} + fn compute_scroll_position( snapshot: &DisplayMapSnapshot, mut scroll_position: Vector2F, diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index 8ab6c52cc5fcf9d15027a0940fbb08cbb6c2414a..2dec968d78d67de9d9e30c20e6a7fd455915ab4a 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -1,5 +1,7 @@ -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, +}; +use crate::time::ReplicaId; use gpui::{ color::Color, geometry::{ diff --git a/zed/src/theme.rs b/zed/src/theme.rs index a96945fecc1011d9c1de9ef941560f863350491d..cf89173d8b847c106e5337ea7abddfd5530b78a7 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -2,6 +2,7 @@ mod highlight_map; mod resolution; mod theme_registry; +use crate::editor::{EditorStyle, SelectionStyle}; use anyhow::Result; use gpui::{ color::Color, @@ -158,20 +159,6 @@ 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)] @@ -181,12 +168,6 @@ pub struct InputEditorStyle { 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 } @@ -204,30 +185,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 { From 606aa148a609f273d3cf10c76260cc35d792f3b4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Sep 2021 13:20:12 -0600 Subject: [PATCH 40/55] Require a build_style callback to be passed to Editor on construction We're going to use this to control the text style, so it really doesn't make sense to allow an editor to be constructed without it. --- server/src/rpc.rs | 4 +- zed/src/chat_panel.rs | 13 +++-- zed/src/editor.rs | 104 +++++++++++++++++++++----------------- zed/src/file_finder.rs | 12 +++-- zed/src/theme_selector.rs | 12 +++-- 5 files changed, 87 insertions(+), 58 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index debd982366c7a4b9d1339963612d9e101dfcff0f..caabd6e5a3309407d6713b1ceed0c45ede6ac0f2 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1121,7 +1121,9 @@ 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, |_| Default::default(), cx) + }); buffer_a .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1) .await; diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index d7752b9a53e944b6e5c1776ab280fd3ea95f025b..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(); diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 8926d5afd5074e5cc34563e0ee571fca3d958a0d..fe4677db5f10b4afe4e883cd45ef094ba183d817 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -310,7 +310,7 @@ 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, @@ -344,9 +344,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 } @@ -354,10 +358,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 } @@ -365,6 +370,16 @@ impl Editor { pub fn for_buffer( buffer: ModelHandle, settings: watch::Receiver, + build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle, + cx: &mut ViewContext, + ) -> Self { + 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 display_map = @@ -396,7 +411,7 @@ 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, @@ -410,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() } @@ -2648,10 +2655,7 @@ 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); EditorElement::new(self.handle.clone(), style).boxed() } @@ -2703,8 +2707,12 @@ 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 |_| settings.borrow().theme.editor.clone(), + cx, + ) } } @@ -2741,10 +2749,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) } @@ -2787,7 +2799,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); editor.update(cx, |view, cx| { @@ -2855,7 +2867,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { @@ -2889,7 +2901,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { @@ -2935,7 +2947,7 @@ mod tests { let settings = settings::test(&cx).1; let (_, editor) = cx.add_window(Default::default(), |cx| { - Editor::for_buffer(buffer.clone(), settings.clone(), cx) + Editor::for_buffer(buffer.clone(), settings.clone(), |_| Default::default(), cx) }); let layouts = editor.update(cx, |editor, cx| { @@ -2981,7 +2993,7 @@ mod tests { }); let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { - Editor::for_buffer(buffer.clone(), settings, cx) + Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { @@ -3049,7 +3061,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) + Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) }); buffer.update(cx, |buffer, cx| { @@ -3126,7 +3138,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) + Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) }); assert_eq!('ⓐ'.len_utf8(), 3); @@ -3184,7 +3196,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) + Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx) @@ -3215,7 +3227,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.select_display_ranges( @@ -3360,7 +3372,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.select_display_ranges( @@ -3554,7 +3566,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { @@ -3616,7 +3628,7 @@ mod tests { }); let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { - Editor::for_buffer(buffer.clone(), settings, cx) + Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { @@ -3652,7 +3664,7 @@ mod tests { }); let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { - Editor::for_buffer(buffer.clone(), settings, cx) + Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { @@ -3682,7 +3694,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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.select_display_ranges( @@ -3708,7 +3720,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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx) @@ -3727,7 +3739,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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.select_display_ranges( @@ -3756,7 +3768,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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.select_display_ranges( @@ -3784,7 +3796,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.fold_ranges( @@ -3884,7 +3896,7 @@ mod tests { let settings = settings::test(&cx).1; let view = cx .add_window(Default::default(), |cx| { - Editor::for_buffer(buffer.clone(), settings, cx) + Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) }) .1; @@ -4018,7 +4030,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.select_all(&SelectAll, cx); @@ -4034,7 +4046,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.select_display_ranges( @@ -4082,7 +4094,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { view.fold_ranges( @@ -4152,7 +4164,7 @@ mod tests { 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) + Editor::for_buffer(buffer, settings, |_| Default::default(), cx) }); view.update(cx, |view, cx| { @@ -4339,7 +4351,9 @@ 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| { + Editor::for_buffer(buffer, settings.clone(), |_| Default::default(), cx) + }); view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing()) .await; diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 23bec79e0076046f3d483f9baa4972c7e418a2f1..8f3217b2e58521b6dcb9f45ac67e992d90c9b420 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -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/theme_selector.rs b/zed/src/theme_selector.rs index 6a623b120e6a3fcdc5eb679b2fcdd22c18ba6ec4..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) From c21b754c4c197e8a888182acb094fd9a0fd427a4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Sep 2021 13:23:42 -0600 Subject: [PATCH 41/55] Make placeholder text style optional --- zed/src/editor.rs | 18 ++++++++++-------- zed/src/theme.rs | 3 ++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index fe4677db5f10b4afe4e883cd45ef094ba183d817..15a312ce5f22122a94b5c9fa862e28e2dfdfb924 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -282,7 +282,7 @@ pub enum EditorMode { pub struct EditorStyle { pub text: HighlightStyle, #[serde(default)] - pub placeholder_text: HighlightStyle, + pub placeholder_text: Option, pub background: Color, pub selection: SelectionStyle, pub gutter_background: Color, @@ -2468,7 +2468,7 @@ impl Snapshot { .skip(rows.start as usize) .take(rows.len()); let font_id = font_cache - .select_font(self.font_family, &style.placeholder_text.font_properties)?; + .select_font(self.font_family, &style.placeholder_text().font_properties)?; return Ok(placeholder_lines .into_iter() .map(|line| { @@ -2479,7 +2479,7 @@ impl Snapshot { line.len(), RunStyle { font_id, - color: style.placeholder_text.color, + color: style.placeholder_text().color, underline: false, }, )], @@ -2596,6 +2596,12 @@ impl Snapshot { } } +impl EditorStyle { + fn placeholder_text(&self) -> &HighlightStyle { + self.placeholder_text.as_ref().unwrap_or(&self.text) + } +} + impl Default for EditorStyle { fn default() -> Self { Self { @@ -2604,11 +2610,7 @@ impl Default for EditorStyle { font_properties: Default::default(), underline: false, }, - placeholder_text: HighlightStyle { - color: Color::from_u32(0x00ff00ff), - font_properties: Default::default(), - underline: false, - }, + placeholder_text: None, background: Default::default(), gutter_background: Default::default(), active_line_background: Default::default(), diff --git a/zed/src/theme.rs b/zed/src/theme.rs index cf89173d8b847c106e5337ea7abddfd5530b78a7..52e1b0d0a8478e63ab507e38a506a5421f69efd9 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -164,7 +164,8 @@ pub struct InputEditorStyle { #[serde(flatten)] pub container: ContainerStyle, pub text: HighlightStyle, - pub placeholder_text: HighlightStyle, + #[serde(default)] + pub placeholder_text: Option, pub selection: SelectionStyle, } From a1f0693599eac138efa5bcc79c92f24b9d01ac32 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Sep 2021 14:12:38 -0600 Subject: [PATCH 42/55] Specify full TextStyles in EditorStyle --- gpui/src/fonts.rs | 10 +++ server/src/rpc.rs | 9 ++- zed/assets/themes/_base.toml | 10 +-- zed/src/editor.rs | 126 ++++++++++++++++------------------- zed/src/theme.rs | 10 ++- 5 files changed, 86 insertions(+), 79 deletions(-) diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index 96248c167577326cb74ed3ee6c1d7934f385f362..3d3ff1efb6897d6a544ace1f78b11c8985b775da 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -126,6 +126,16 @@ impl TextStyle { } } +impl From for HighlightStyle { + fn from(other: TextStyle) -> Self { + Self { + color: other.color, + font_properties: other.font_properties, + underline: other.underline, + } + } +} + impl HighlightStyle { fn from_json(json: HighlightStyleJson) -> Self { let font_properties = properties_from_json(json.weight, json.italic); diff --git a/server/src/rpc.rs b/server/src/rpc.rs index caabd6e5a3309407d6713b1ceed0c45ede6ac0f2..698414851e60fadd301b60cfc19ac31ad425ef6f 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1037,7 +1037,7 @@ mod tests { }; use zed::{ channel::{Channel, ChannelDetails, ChannelList}, - editor::{Editor, Insert}, + editor::{Editor, EditorStyle, Insert}, fs::{FakeFs, Fs as _}, language::LanguageRegistry, rpc::{self, Client, Credentials, EstablishConnectionError}, @@ -1122,7 +1122,12 @@ mod tests { // 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, |_| Default::default(), 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) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 1a2999379c4e46f82514349cddeee38ecb697467..f8f059487a38f9ee5cfc89d76f0f0ec50f9d8a58 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -107,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" } @@ -132,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" } @@ -153,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/src/editor.rs b/zed/src/editor.rs index 15a312ce5f22122a94b5c9fa862e28e2dfdfb924..8be376e987c632b5f6941c23939e3d761d7e2101 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -20,7 +20,7 @@ use gpui::{ action, color::Color, font_cache::FamilyId, - fonts::{HighlightStyle, Properties as FontProperties}, + fonts::{Properties as FontProperties, TextStyle}, geometry::vector::Vector2F, keymap::Binding, text_layout::{self, RunStyle}, @@ -280,9 +280,9 @@ pub enum EditorMode { #[derive(Clone, Deserialize)] pub struct EditorStyle { - pub text: HighlightStyle, + pub text: TextStyle, #[serde(default)] - pub placeholder_text: Option, + pub placeholder_text: Option, pub background: Color, pub selection: SelectionStyle, pub gutter_background: Color, @@ -2520,7 +2520,7 @@ impl Snapshot { .theme .syntax .highlight_style(style_ix) - .unwrap_or(style.text.clone()); + .unwrap_or(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 @@ -2597,17 +2597,19 @@ impl Snapshot { } impl EditorStyle { - fn placeholder_text(&self) -> &HighlightStyle { - self.placeholder_text.as_ref().unwrap_or(&self.text) - } -} - -impl Default for EditorStyle { - fn default() -> Self { + #[cfg(any(test, feature ="test-support"))] + pub fn test(font_cache: &FontCache) -> Self { + let font_family_name = "Monaco"; + let font_properties = Default::default(); + let family_id = font_cache.load_family(&[font_family_name]).unwrap(); + let font_id = font_cache.select_font(family_id, &font_properties).unwrap(); Self { - text: HighlightStyle { + text: TextStyle { + font_family_name: font_family_name.into(), + font_id, + font_size: 14., color: Color::from_u32(0xff0000ff), - font_properties: Default::default(), + font_properties, underline: false, }, placeholder_text: None, @@ -2620,6 +2622,10 @@ impl Default for EditorStyle { guest_selections: Default::default(), } } + + fn placeholder_text(&self) -> &TextStyle { + self.placeholder_text.as_ref().unwrap_or(&self.text) + } } fn compute_scroll_position( @@ -2800,9 +2806,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, |_| Default::default(), 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); @@ -2868,9 +2873,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, |_| Default::default(), 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); @@ -2902,9 +2905,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, |_| Default::default(), 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); @@ -2949,7 +2950,7 @@ mod tests { let settings = settings::test(&cx).1; let (_, editor) = cx.add_window(Default::default(), |cx| { - Editor::for_buffer(buffer.clone(), settings.clone(), |_| Default::default(), cx) + build_editor(buffer, settings.clone(), cx) }); let layouts = editor.update(cx, |editor, cx| { @@ -2995,7 +2996,7 @@ mod tests { }); let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { - Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) + build_editor(buffer.clone(), settings, cx) }); view.update(cx, |view, cx| { @@ -3063,7 +3064,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, |_| Default::default(), cx) + build_editor(buffer.clone(), settings, cx) }); buffer.update(cx, |buffer, cx| { @@ -3140,7 +3141,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, |_| Default::default(), cx) + build_editor(buffer.clone(), settings, cx) }); assert_eq!('ⓐ'.len_utf8(), 3); @@ -3198,7 +3199,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, |_| Default::default(), cx) + build_editor(buffer.clone(), settings, cx) }); view.update(cx, |view, cx| { view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx) @@ -3228,9 +3229,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, |_| Default::default(), cx) - }); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3373,9 +3372,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, |_| Default::default(), cx) - }); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3567,9 +3564,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, |_| Default::default(), 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); @@ -3630,7 +3625,7 @@ mod tests { }); let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { - Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) + build_editor(buffer.clone(), settings, cx) }); view.update(cx, |view, cx| { @@ -3666,7 +3661,7 @@ mod tests { }); let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { - Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) + build_editor(buffer.clone(), settings, cx) }); view.update(cx, |view, cx| { @@ -3695,9 +3690,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, |_| Default::default(), cx) - }); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3721,9 +3714,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, |_| Default::default(), 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(); @@ -3740,9 +3731,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, |_| Default::default(), cx) - }); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3769,9 +3758,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, |_| Default::default(), cx) - }); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3797,9 +3784,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, |_| Default::default(), cx) - }); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.fold_ranges( vec![ @@ -3898,7 +3883,7 @@ mod tests { let settings = settings::test(&cx).1; let view = cx .add_window(Default::default(), |cx| { - Editor::for_buffer(buffer.clone(), settings, |_| Default::default(), cx) + build_editor(buffer.clone(), settings, cx) }) .1; @@ -4031,9 +4016,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, |_| Default::default(), 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!( @@ -4047,9 +4030,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, |_| Default::default(), cx) - }); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -4095,9 +4076,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, |_| Default::default(), cx) - }); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); view.update(cx, |view, cx| { view.fold_ranges( vec![ @@ -4165,9 +4144,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, |_| Default::default(), 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) @@ -4353,9 +4330,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(), |_| Default::default(), 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; @@ -4493,6 +4468,19 @@ 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 { + Editor::for_buffer( + buffer, + settings, + move |cx| EditorStyle::test(cx.font_cache()), + cx, + ) + } } trait RangeExt { diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 52e1b0d0a8478e63ab507e38a506a5421f69efd9..4458183e6dde3ef563cdd3fac1c60d56ce996553 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -163,9 +163,9 @@ pub struct ContainedLabel { pub struct InputEditorStyle { #[serde(flatten)] pub container: ContainerStyle, - pub text: HighlightStyle, + pub text: TextStyle, #[serde(default)] - pub placeholder_text: Option, + pub placeholder_text: Option, pub selection: SelectionStyle, } @@ -196,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(), } } } From 4f0c9a3e317a9a51981a9fc1c6da7a40409f6a90 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Sep 2021 14:43:19 -0600 Subject: [PATCH 43/55] Build workspace editor TextStyle from font fields in settings We'll specify values in the theme but we'll only end up using the color for these editors. --- gpui/src/font_cache.rs | 8 ++++---- gpui/src/fonts.rs | 7 +++++-- zed/src/editor.rs | 37 +++++++++++++++++++++++++++++++------ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/gpui/src/font_cache.rs b/gpui/src/font_cache.rs index 3c11b9659cb26441ab7746bac6a6f306fdbcef42..ddb7c9c28801ee490e03c8bd43c17189742dcf40 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); diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index 3d3ff1efb6897d6a544ace1f78b11c8985b775da..9df36464f6c065206d4538129c3dbe1192281282 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, diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 8be376e987c632b5f6941c23939e3d761d7e2101..3bdb200226f0d9222891c6e7220ca9e6a0cf440e 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -2597,15 +2597,18 @@ impl Snapshot { } impl EditorStyle { - #[cfg(any(test, feature ="test-support"))] + #[cfg(any(test, feature = "test-support"))] pub fn test(font_cache: &FontCache) -> Self { - let font_family_name = "Monaco"; + let font_family_name = Arc::from("Monaco"); let font_properties = Default::default(); - let family_id = font_cache.load_family(&[font_family_name]).unwrap(); - let font_id = font_cache.select_font(family_id, &font_properties).unwrap(); + 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_name.into(), + font_family_name, + font_family_id, font_id, font_size: 14., color: Color::from_u32(0xff0000ff), @@ -2718,7 +2721,29 @@ impl workspace::Item for Buffer { Editor::for_buffer( handle, settings.clone(), - move |_| settings.borrow().theme.editor.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, ) } From f13a3544fcaf4ab87e88fa3f7d81be2fac9f643e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Sep 2021 16:43:43 -0600 Subject: [PATCH 44/55] Move editor layout code into element Now that most of the layout code is based on the EditorStyle struct, I think it makes more sense to put it in the element. --- gpui/src/font_cache.rs | 18 +- gpui/src/fonts.rs | 16 ++ zed/src/editor.rs | 313 ++++---------------------------- zed/src/editor/display_map.rs | 2 +- zed/src/editor/element.rs | 331 +++++++++++++++++++++++++++------- 5 files changed, 330 insertions(+), 350 deletions(-) diff --git a/gpui/src/font_cache.rs b/gpui/src/font_cache.rs index ddb7c9c28801ee490e03c8bd43c17189742dcf40..c0255a7af5f251b9828e4788dba6443f30efcc5b 100644 --- a/gpui/src/font_cache.rs +++ b/gpui/src/font_cache.rs @@ -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 9df36464f6c065206d4538129c3dbe1192281282..3ec8aad9626bf78e4beb79709b89e525be679ad9 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -127,6 +127,22 @@ 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 { diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 3bdb200226f0d9222891c6e7220ca9e6a0cf440e..af7f0e4db339246a96a9e3e21dc48c19222b2ce3 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -4,7 +4,7 @@ mod element; pub mod movement; use crate::{ - settings::{HighlightId, Settings}, + settings::Settings, theme::Theme, time::ReplicaId, util::{post_inc, Bias}, @@ -17,15 +17,10 @@ pub use display_map::DisplayPoint; use display_map::*; pub use element::*; use gpui::{ - action, - color::Color, - font_cache::FamilyId, - fonts::{Properties as FontProperties, TextStyle}, - 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, font_cache::FamilyId, 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}, @@ -2328,263 +2321,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().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 { - 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) { @@ -2598,7 +2388,7 @@ impl Snapshot { impl EditorStyle { #[cfg(any(test, feature = "test-support"))] - pub fn test(font_cache: &FontCache) -> Self { + 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(); @@ -2966,33 +2756,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| { - build_editor(buffer, 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| { diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 16d4da79b4c6446fa02f42c10469297dc397ccb0..cdfc17f46c28f84c14336630142c501e2bb5835c 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -8,8 +8,8 @@ use gpui::{Entity, ModelContext, ModelHandle}; use postage::watch; 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, diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index 2dec968d78d67de9d9e30c20e6a7fd455915ab4a..0912265099191d2aacabaf95bfdaec7d0124c01a 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -1,7 +1,8 @@ use super::{ DisplayPoint, Editor, EditorMode, EditorStyle, Insert, Scroll, Select, SelectPhase, Snapshot, + MAX_LINE_LEN, }; -use crate::time::ReplicaId; +use crate::{theme::HighlightId, time::ReplicaId}; use gpui::{ color::Color, geometry::{ @@ -11,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, }; @@ -20,6 +21,7 @@ use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, collections::{BTreeMap, HashMap}, + fmt::Write, ops::Range, }; @@ -376,6 +378,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 { @@ -392,30 +564,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| { @@ -490,51 +654,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); + 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(); } - Ok(layouts) => { - for line in &layouts { - if line.width() > max_visible_line_width { - max_visible_line_width = line.width(); - } - } - - layouts - } - }; + } let mut layout = LayoutState { size, @@ -544,6 +675,7 @@ impl Element for EditorElement { overscroll, text_offset, snapshot, + style: self.style.clone(), active_rows, line_layouts, line_number_layouts, @@ -553,15 +685,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, ); @@ -661,6 +796,7 @@ pub struct LayoutState { gutter_size: Vector2F, gutter_padding: f32, text_size: Vector2F, + style: EditorStyle, snapshot: Snapshot, active_rows: BTreeMap, line_layouts: Vec, @@ -674,20 +810,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( @@ -695,6 +827,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 { @@ -866,3 +1028,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); + } +} From 68039b9d482be2de595ca057485b9b2a78ba80c4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Sep 2021 16:46:35 -0600 Subject: [PATCH 45/55] Remove font_family and font_size from editor::Snapshot We'll rely on the style struct instead. --- zed/src/editor.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index af7f0e4db339246a96a9e3e21dc48c19222b2ce3..c27d91d3404d211ce43263cbc0896494c2c89258 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -17,10 +17,9 @@ pub use display_map::DisplayPoint; use display_map::*; pub use element::*; use gpui::{ - action, color::Color, font_cache::FamilyId, fonts::TextStyle, geometry::vector::Vector2F, + action, color::Color, fonts::TextStyle, geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, - ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, - WeakViewHandle, + ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, WeakViewHandle, }; use postage::watch; use serde::{Deserialize, Serialize}; @@ -318,8 +317,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, @@ -436,8 +433,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) From 42bf88b52a2355f53eadc5cf26d85ccd2f9eb04a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 17 Sep 2021 15:39:19 -0700 Subject: [PATCH 46/55] Base soft wrapping on TextStyle instead of Settings --- gpui/src/app.rs | 10 ++ zed/src/editor.rs | 55 +++++++-- zed/src/editor/display_map.rs | 159 +++++++++++++++---------- zed/src/editor/display_map/wrap_map.rs | 87 +++++--------- zed/src/editor/movement.rs | 17 ++- 5 files changed, 191 insertions(+), 137 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 4b18af06b4bdb0fef7c3fb47e819fa3153e6c8f2..ebe6c89a8ace2a965138658a8faaccab66b2f1ef 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2335,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/zed/src/editor.rs b/zed/src/editor.rs index c27d91d3404d211ce43263cbc0896494c2c89258..1b0eb4852b8e528c03adcaac7aaaec9498f18b5e 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -17,9 +17,9 @@ pub use display_map::DisplayPoint; use display_map::*; pub use element::*; use gpui::{ - action, color::Color, fonts::TextStyle, geometry::vector::Vector2F, - keymap::Binding, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, - ModelHandle, MutableAppContext, RenderContext, Task, 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}; @@ -372,8 +372,17 @@ impl Editor { build_style: Rc EditorStyle>>, cx: &mut ViewContext, ) -> Self { - let display_map = - cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.clone(), None, cx)); + 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) @@ -2452,6 +2461,9 @@ impl Entity for Editor { impl View for Editor { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { 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() } @@ -4257,12 +4269,33 @@ mod tests { settings: watch::Receiver, cx: &mut ViewContext, ) -> Editor { - Editor::for_buffer( - buffer, - settings, - move |cx| EditorStyle::test(cx.font_cache()), - cx, - ) + 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) } } diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index cdfc17f46c28f84c14336630142c501e2bb5835c..e4b8cc0886603f1257bac097445a08db7f0b9871 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -2,10 +2,9 @@ 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; use wrap_map::WrapMap; @@ -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/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(), From 9a9c8aec3f0342f1c1cec7f25f2af0c25e273304 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 17 Sep 2021 16:12:03 -0700 Subject: [PATCH 47/55] Fix deadlock when handling incoming RPC messages We need to drop the lock on the rpc::ClientState when handling an incoming messages in case those message handlers attempt to interact with the client and grab the lock. --- zed/src/rpc.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index fe1dde4ffba2d2579fcfe036cb3b162126fa1847..d4315a44d42f7de2b40a07eeaabefd7d1994f7c8 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -371,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); @@ -383,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()); } From 9691267dc8e7377086b2c5ebe7ab5e16fc6e65d9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 17 Sep 2021 16:17:47 -0700 Subject: [PATCH 48/55] Only blink local cursors --- gpui/src/presenter.rs | 8 ++++++++ zed/src/editor.rs | 14 +++++++------- zed/src/editor/element.rs | 3 ++- 3 files changed, 17 insertions(+), 8 deletions(-) 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/zed/src/editor.rs b/zed/src/editor.rs index 1b0eb4852b8e528c03adcaac7aaaec9498f18b5e..98f0b4a0231e8d2cfba9eab21f2a88d72ff602e0 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -305,7 +305,7 @@ pub struct Editor { build_style: Rc EditorStyle>>, settings: watch::Receiver, focused: bool, - cursors_visible: bool, + show_local_cursors: bool, blink_epoch: usize, blinking_paused: bool, mode: EditorMode, @@ -416,7 +416,7 @@ impl Editor { autoscroll_requested: false, settings, focused: false, - cursors_visible: false, + show_local_cursors: false, blink_epoch: 0, blinking_paused: false, mode: EditorMode::Full, @@ -2253,7 +2253,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(); @@ -2278,7 +2278,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(); @@ -2295,8 +2295,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) { @@ -2483,7 +2483,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(); }); diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index 0912265099191d2aacabaf95bfdaec7d0124c01a..7c65b1a22d19d498ffd6f58df4db81a78d5fdb82 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -269,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; @@ -338,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 = From b5c76ccc95d6459e2d9f9da22d2af2d8be96c040 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 17 Sep 2021 16:45:09 -0700 Subject: [PATCH 49/55] Render close icons on all tabs when tab bar is hovered --- zed/src/workspace/pane.rs | 69 ++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index c0cd6bb9fd7e47141cc633a55cb9eeb00e48ca81..f4d409ae5df489295d61633a124fc1fe193e8b94 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -185,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; @@ -276,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"); @@ -316,36 +316,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") } From 928779154e484a6f476901d461eb7737c21dd249 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 17 Sep 2021 16:59:46 -0700 Subject: [PATCH 50/55] Tweak spacing so tab close buttons feel more balanced --- zed/assets/themes/_base.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index f8f059487a38f9ee5cfc89d76f0f0ec50f9d8a58..57f25eb26fc331311a2dc93536554471ec549602 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -18,7 +18,7 @@ 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" From c7e2b6dacb541686d2f297e9c9a6e244044eff48 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 18 Sep 2021 10:37:32 -0700 Subject: [PATCH 51/55] Expand the hit area area around tab close icons --- gpui/src/elements/container.rs | 11 +++++++++++ gpui/src/elements/mouse_event_handler.rs | 3 ++- zed/src/workspace/pane.rs | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index eeda6f206d139c210259744f9604fb30f90d1fae..abbafe4d034e76926ff79f794a286a24a727d927 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -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!({}); 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/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index f4d409ae5df489295d61633a124fc1fe193e8b94..31ec57354ad131f8542f211c5ee88cfb2520ad8f 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -292,6 +292,7 @@ impl Pane { } }, ) + .with_padding(Padding::uniform(4.)) .with_cursor_style(CursorStyle::PointingHand) .on_click(move |cx| { cx.dispatch_action(CloseItem(item_id)) From af99d0ef42e6f16232e6eefec8d43e679e0362f7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 18 Sep 2021 11:46:22 -0700 Subject: [PATCH 52/55] Attempt to assign a language when a new buffer is saved --- zed/src/editor.rs | 5 +++++ zed/src/editor/buffer.rs | 13 ++++++++++++- zed/src/worktree.rs | 7 +++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 98f0b4a0231e8d2cfba9eab21f2a88d72ff602e0..25403d3aae44c3ef22c735700f80bfd1ea46f0dd 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -4,6 +4,7 @@ mod element; pub mod movement; use crate::{ + language::Language, settings::Settings, theme::Theme, time::ReplicaId, @@ -449,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>, diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 97e0202cec4a6295e478b2078ad5d393df22ab50..43e5693bd43b2eaf6edbae60ef026648881e6827 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() @@ -871,7 +878,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/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(), From 9e6c54ba0cf804bf3d0a414fd4ccc8688fae4a06 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 19 Sep 2021 17:33:46 -0700 Subject: [PATCH 53/55] Test language assignment when new buffers are saved --- zed/src/editor/buffer.rs | 4 ++++ zed/src/workspace.rs | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 43e5693bd43b2eaf6edbae60ef026648881e6827..82aa10d4383e40157b8550403b5ec230178dac35 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -801,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 } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index ff3666e0de077cc8667716482f2479ba220e0825..1a83c358760fc43755a853068e18c6b40df5cc59 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -1476,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)); @@ -1489,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| { @@ -1504,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. @@ -1512,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| { From 1719d7da2a2674834c43753337f83cd8f172efa2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 19 Sep 2021 17:34:04 -0700 Subject: [PATCH 54/55] Suppress SVG loading errors in tests --- gpui/Cargo.toml | 3 +++ gpui/src/elements/svg.rs | 5 +++-- zed/Cargo.toml | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 7cc202e0b642dd6a22caa58b6008b8f9dee7b03c..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" diff --git a/gpui/src/elements/svg.rs b/gpui/src/elements/svg.rs index 55e11a92531341262e69e79ff6365a2c20cfa5b6..3e93d3adae3cd721a2c6e4ff501bbda0bf6b5f86 100644 --- a/gpui/src/elements/svg.rs +++ b/gpui/src/elements/svg.rs @@ -47,8 +47,9 @@ impl Element for Svg { ); (size, Some(tree)) } - Err(error) => { - log::error!("{}", error); + Err(_error) => { + #[cfg(not(any(test, feature = "test-support")))] + log::error!("{}", _error); (constraint.min, None) } } diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 8d27fcd4c540b58544dd6223960b74409afcae86..17d42a04c95253c5f0943724a7cb52ea238c8faf 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -14,7 +14,7 @@ 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" @@ -69,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"] From cb2d8bac1da9c04bb007dac3edb50401a6cf64ef Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 20 Sep 2021 19:42:24 +0200 Subject: [PATCH 55/55] Use bullseye-slim for migration Dockerfile Closes #154 Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- Dockerfile.migrator | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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