From 34eb2c446f5b4653ecac4a628c338543d3580ff4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 5 Aug 2021 11:48:35 -0600 Subject: [PATCH 001/204] Start on custom titlebar --- gpui/examples/text.rs | 2 +- gpui/src/app.rs | 47 +++++++++-------- gpui/src/platform.rs | 2 + gpui/src/platform/mac/window.rs | 9 +++- zed/src/editor.rs | 93 ++++++++++++++++++++++++--------- zed/src/workspace.rs | 27 +++++++--- 6 files changed, 125 insertions(+), 55 deletions(-) diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index 45aa5931129ed43f9e6bf76a1063392827063ed1..08959b77a2f72652ed609e95c92d77be075e4c71 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -12,7 +12,7 @@ fn main() { gpui::App::new(()).unwrap().run(|cx| { cx.platform().activate(true); - cx.add_window(|_| TextView); + cx.add_window(Default::default(), |_| TextView); }); } diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 579bf5c4682dba2fcfbe15d48166396f3a9ea81d..8df38af0d47300d37bef7b1a6206dfbb95fd9c69 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -12,7 +12,6 @@ use anyhow::{anyhow, Result}; use async_task::Task; use keymap::MatchResult; use parking_lot::{Mutex, RwLock}; -use pathfinder_geometry::{rect::RectF, vector::vec2f}; use platform::Event; use postage::{mpsc, sink::Sink as _, stream::Stream as _}; use smol::prelude::*; @@ -300,7 +299,9 @@ impl TestAppContext { T: View, F: FnOnce(&mut ViewContext) -> T, { - self.cx.borrow_mut().add_window(build_root_view) + self.cx + .borrow_mut() + .add_window(Default::default(), build_root_view) } pub fn window_ids(&self) -> Vec { @@ -959,7 +960,11 @@ impl MutableAppContext { handle } - pub fn add_window(&mut self, build_root_view: F) -> (usize, ViewHandle) + pub fn add_window( + &mut self, + window_options: WindowOptions, + build_root_view: F, + ) -> (usize, ViewHandle) where T: View, F: FnOnce(&mut ViewContext) -> T, @@ -976,7 +981,7 @@ impl MutableAppContext { invalidation: None, }, ); - self.open_platform_window(window_id); + self.open_platform_window(window_id, window_options); root_view.update(self, |view, cx| { view.on_focus(cx); cx.notify(); @@ -992,15 +997,11 @@ impl MutableAppContext { self.remove_dropped_entities(); } - fn open_platform_window(&mut self, window_id: usize) { - let mut window = self.cx.platform.open_window( - window_id, - WindowOptions { - bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)), - title: "Zed".into(), - }, - self.foreground.clone(), - ); + fn open_platform_window(&mut self, window_id: usize, window_options: WindowOptions) { + let mut window = + self.cx + .platform + .open_window(window_id, window_options, self.foreground.clone()); let text_layout_cache = TextLayoutCache::new(self.cx.platform.fonts()); let presenter = Rc::new(RefCell::new(Presenter::new( window_id, @@ -3070,7 +3071,7 @@ mod tests { } } - let (window_id, _) = cx.add_window(|cx| View::new(None, cx)); + let (window_id, _) = cx.add_window(Default::default(), |cx| View::new(None, cx)); let handle_1 = cx.add_view(window_id, |cx| View::new(None, cx)); let handle_2 = cx.add_view(window_id, |cx| View::new(Some(handle_1.clone()), cx)); assert_eq!(cx.cx.views.len(), 3); @@ -3126,7 +3127,7 @@ mod tests { } let mouse_down_count = Arc::new(AtomicUsize::new(0)); - let (window_id, _) = cx.add_window(|_| View { + let (window_id, _) = cx.add_window(Default::default(), |_| View { mouse_down_count: mouse_down_count.clone(), }); let presenter = cx.presenters_and_platform_windows[&window_id].0.clone(); @@ -3184,7 +3185,7 @@ mod tests { released: model_released.clone(), }); - let (window_id, _) = cx.add_window(|_| View { + let (window_id, _) = cx.add_window(Default::default(), |_| View { released: view_released.clone(), }); @@ -3227,7 +3228,7 @@ mod tests { type Event = usize; } - let (window_id, handle_1) = cx.add_window(|_| View::default()); + let (window_id, handle_1) = cx.add_window(Default::default(), |_| View::default()); let handle_2 = cx.add_view(window_id, |_| View::default()); let handle_2b = handle_2.clone(); let handle_3 = cx.add_model(|_| Model); @@ -3280,7 +3281,7 @@ mod tests { type Event = (); } - let (window_id, _) = cx.add_window(|_| View); + let (window_id, _) = cx.add_window(Default::default(), |_| View); let observing_view = cx.add_view(window_id, |_| View); let emitting_view = cx.add_view(window_id, |_| View); let observing_model = cx.add_model(|_| Model); @@ -3333,7 +3334,7 @@ mod tests { type Event = (); } - let (_, view) = cx.add_window(|_| View::default()); + let (_, view) = cx.add_window(Default::default(), |_| View::default()); let model = cx.add_model(|_| Model::default()); view.update(cx, |_, c| { @@ -3373,7 +3374,7 @@ mod tests { type Event = (); } - let (window_id, _) = cx.add_window(|_| View); + let (window_id, _) = cx.add_window(Default::default(), |_| View); let observing_view = cx.add_view(window_id, |_| View); let observing_model = cx.add_model(|_| Model); let observed_model = cx.add_model(|_| Model); @@ -3423,7 +3424,7 @@ mod tests { } let events: Arc>> = Default::default(); - let (window_id, view_1) = cx.add_window(|_| View { + let (window_id, view_1) = cx.add_window(Default::default(), |_| View { events: events.clone(), name: "view 1".to_string(), }); @@ -3533,7 +3534,7 @@ mod tests { actions_clone.borrow_mut().push(format!("{} d", view.id)); }); - let (window_id, view_1) = cx.add_window(|_| ViewA { id: 1 }); + let (window_id, view_1) = cx.add_window(Default::default(), |_| ViewA { id: 1 }); let view_2 = cx.add_view(window_id, |_| ViewB { id: 2 }); let view_3 = cx.add_view(window_id, |_| ViewA { id: 3 }); let view_4 = cx.add_view(window_id, |_| ViewB { id: 4 }); @@ -3613,7 +3614,7 @@ mod tests { view_2.keymap_context.set.insert("b".into()); view_3.keymap_context.set.insert("c".into()); - let (window_id, view_1) = cx.add_window(|_| view_1); + let (window_id, view_1) = cx.add_window(Default::default(), |_| view_1); let view_2 = cx.add_view(window_id, |_| view_2); let view_3 = cx.add_view(window_id, |_| view_3); diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 7107d7763dd373383d874cb90421c4eedd2d0ca2..62225d66e8343a4587abe7690a212227f9c3927f 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -95,9 +95,11 @@ pub trait WindowContext { fn present_scene(&mut self, scene: Scene); } +#[derive(Default)] pub struct WindowOptions<'a> { pub bounds: RectF, pub title: Option<&'a str>, + pub titlebar_appears_transparent: bool, } pub struct PathPromptOptions { diff --git a/gpui/src/platform/mac/window.rs b/gpui/src/platform/mac/window.rs index b1d86acb12e7b358ce747a22f0d32e5c4ff0e270..56969e80892f34266d72ec8cd166825f3a61a658 100644 --- a/gpui/src/platform/mac/window.rs +++ b/gpui/src/platform/mac/window.rs @@ -153,11 +153,15 @@ impl Window { let pool = NSAutoreleasePool::new(nil); let frame = options.bounds.to_ns_rect(); - let style_mask = NSWindowStyleMask::NSClosableWindowMask + let mut style_mask = NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSMiniaturizableWindowMask | NSWindowStyleMask::NSResizableWindowMask | NSWindowStyleMask::NSTitledWindowMask; + if options.titlebar_appears_transparent { + style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask; + } + let native_window: id = msg_send![WINDOW_CLASS, alloc]; let native_window = native_window.initWithContentRect_styleMask_backing_defer_( frame, @@ -213,6 +217,9 @@ impl Window { if let Some(title) = options.title.as_ref() { native_window.setTitle_(NSString::alloc(nil).init_str(title)); } + if options.titlebar_appears_transparent { + native_window.setTitlebarAppearsTransparent_(YES); + } native_window.setAcceptsMouseMovedEvents_(YES); native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); diff --git a/zed/src/editor.rs b/zed/src/editor.rs index d5507f38c9a17b2be23a38e24c0c3182dd3809f1..b3dc4f8a2b860389c1d816a7744ec154cb0b6ec5 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -2659,7 +2659,9 @@ 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::channel(&cx.font_cache()).unwrap().1; - let (_, editor) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, editor) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); editor.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(2, 2), false, cx); @@ -2725,7 +2727,9 @@ 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::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(2, 2), false, cx); @@ -2757,7 +2761,9 @@ 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::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(3, 4), false, cx); @@ -2801,8 +2807,9 @@ mod tests { let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx)); let settings = settings::channel(&font_cache).unwrap().1; - let (_, editor) = - cx.add_window(|cx| Editor::for_buffer(buffer.clone(), settings.clone(), cx)); + let (_, editor) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer.clone(), settings.clone(), cx) + }); let layouts = editor.update(cx, |editor, cx| { editor @@ -2846,7 +2853,9 @@ mod tests { ) }); let settings = settings::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer.clone(), settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer.clone(), settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], cx) @@ -2912,7 +2921,9 @@ mod tests { fn test_move_cursor(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx)); let settings = settings::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer.clone(), settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer.clone(), settings, cx) + }); buffer.update(cx, |buffer, cx| { buffer.edit( @@ -2987,7 +2998,9 @@ mod tests { fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx)); let settings = settings::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer.clone(), settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer.clone(), settings, cx) + }); assert_eq!('ⓐ'.len_utf8(), 3); assert_eq!('α'.len_utf8(), 2); @@ -3043,7 +3056,9 @@ mod tests { fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx)); let settings = settings::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer.clone(), settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer.clone(), settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx) .unwrap(); @@ -3072,7 +3087,9 @@ 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::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3215,7 +3232,9 @@ mod tests { let buffer = cx.add_model(|cx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", cx)); let settings = settings::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3401,7 +3420,9 @@ mod tests { let buffer = cx.add_model(|cx| Buffer::new(0, "use one::{\n two::three::four::five\n};", cx)); let settings = settings::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.set_wrap_width(140., cx); @@ -3461,7 +3482,9 @@ mod tests { ) }); let settings = settings::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer.clone(), settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer.clone(), settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges( @@ -3495,7 +3518,9 @@ mod tests { ) }); let settings = settings::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer.clone(), settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer.clone(), settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges( @@ -3523,7 +3548,9 @@ mod tests { fn test_delete_line(cx: &mut gpui::MutableAppContext) { let settings = settings::channel(&cx.font_cache()).unwrap().1; let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx)); - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3547,7 +3574,9 @@ mod tests { let settings = settings::channel(&cx.font_cache()).unwrap().1; let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx)); - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx) .unwrap(); @@ -3564,7 +3593,9 @@ mod tests { fn test_duplicate_line(cx: &mut gpui::MutableAppContext) { let settings = settings::channel(&cx.font_cache()).unwrap().1; let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx)); - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3591,7 +3622,9 @@ mod tests { let settings = settings::channel(&cx.font_cache()).unwrap().1; let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx)); - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3617,7 +3650,9 @@ mod tests { fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { let settings = settings::channel(&cx.font_cache()).unwrap().1; let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(10, 5), cx)); - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.fold_ranges( vec![ @@ -3700,7 +3735,9 @@ mod tests { let buffer = cx.add_model(|cx| Buffer::new(0, "one two three four five six ", cx)); let settings = settings::channel(&cx.font_cache()).unwrap().1; let view = cx - .add_window(|cx| Editor::for_buffer(buffer.clone(), settings, cx)) + .add_window(Default::default(), |cx| { + Editor::for_buffer(buffer.clone(), settings, cx) + }) .1; // Cut with three selections. Clipboard text is divided into three slices. @@ -3832,7 +3869,9 @@ 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::channel(&cx.font_cache()).unwrap().1; - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.select_all(&(), cx); assert_eq!( @@ -3846,7 +3885,9 @@ mod tests { fn test_select_line(cx: &mut gpui::MutableAppContext) { let settings = settings::channel(&cx.font_cache()).unwrap().1; let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 5), cx)); - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges( &[ @@ -3892,7 +3933,9 @@ mod tests { fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { let settings = settings::channel(&cx.font_cache()).unwrap().1; let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(9, 5), cx)); - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.fold_ranges( vec![ @@ -3957,7 +4000,9 @@ mod tests { fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) { let settings = settings::channel(&cx.font_cache()).unwrap().1; let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", cx)); - let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings, cx)); + let (_, view) = cx.add_window(Default::default(), |cx| { + Editor::for_buffer(buffer, settings, cx) + }); view.update(cx, |view, cx| { view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], cx) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index cb543522d8d348e9dd1b39eb2034ddb3974ceb33..9badef458e00559eb808144d4b67777ecaf8893b 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -12,9 +12,14 @@ use crate::{ }; use anyhow::{anyhow, Result}; use gpui::{ - elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, ClipboardItem, - Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, - View, ViewContext, ViewHandle, WeakModelHandle, + elements::*, + geometry::{rect::RectF, vector::vec2f}, + json::to_string_pretty, + keymap::Binding, + platform::WindowOptions, + AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, + PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, + WeakModelHandle, }; use log::error; pub use pane::*; @@ -93,12 +98,14 @@ fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) -> Task<()> { log::info!("open new workspace"); // Add a new workspace if necessary - let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms.app_state, cx)); + + let (_, workspace) = + cx.add_window(window_options(), |cx| Workspace::new(¶ms.app_state, cx)); workspace.update(cx, |workspace, cx| workspace.open_paths(¶ms.paths, cx)) } fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { - cx.add_window(|cx| { + cx.add_window(window_options(), |cx| { let mut view = Workspace::new(app_state.as_ref(), cx); view.open_new_file(&app_state, cx); view @@ -106,13 +113,21 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { } fn join_worktree(app_state: &Arc, cx: &mut MutableAppContext) { - cx.add_window(|cx| { + cx.add_window(window_options(), |cx| { let mut view = Workspace::new(app_state.as_ref(), cx); view.join_worktree(&app_state, cx); view }); } +fn window_options() -> WindowOptions<'static> { + WindowOptions { + bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)), + title: None, + titlebar_appears_transparent: true, + } +} + pub trait Item: Entity + Sized { type View: ItemView; From 1910a4c1be86ef5c442e8612512cacf53245ca5e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Aug 2021 11:56:24 -0700 Subject: [PATCH 002/204] Add DB migration for chat tables Co-Authored-By: Nathan Sobo --- script/sqlx | 2 + .../20210805175147_create_chat_tables.sql | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 server/migrations/20210805175147_create_chat_tables.sql diff --git a/script/sqlx b/script/sqlx index 080e0d843a213dc1ac4e5dbddf5c89df8ff4c086..3765c806c84d81a8e42301740a860b403c0863b0 100755 --- a/script/sqlx +++ b/script/sqlx @@ -5,6 +5,8 @@ set -e # Install sqlx-cli if needed [[ "$(sqlx --version)" == "sqlx-cli 0.5.5" ]] || cargo install sqlx-cli --version 0.5.5 +cd server + # Export contents of .env.toml eval "$(cargo run --bin dotenv)" diff --git a/server/migrations/20210805175147_create_chat_tables.sql b/server/migrations/20210805175147_create_chat_tables.sql new file mode 100644 index 0000000000000000000000000000000000000000..c79c8e39b4719e608d0e76f0dd50116cf4069a53 --- /dev/null +++ b/server/migrations/20210805175147_create_chat_tables.sql @@ -0,0 +1,58 @@ +CREATE TABLE IF NOT EXISTS "orgs" ( + "id" SERIAL PRIMARY KEY, + "name" VARCHAR NOT NULL, + "slug" VARCHAR NOT NULL +); + +CREATE TABLE IF NOT EXISTS "org_memberships" ( + "id" SERIAL PRIMARY KEY, + "org_id" INTEGER REFERENCES orgs (id) NOT NULL, + "user_id" INTEGER REFERENCES users (id) NOT NULL, + "admin" BOOLEAN NOT NULL +); + +CREATE UNIQUE INDEX "index_org_memberships_user_id" ON "org_memberships" ("user_id"); +CREATE UNIQUE INDEX "index_org_memberships_org_id" ON "org_memberships" ("org_id"); + +CREATE TABLE IF NOT EXISTS "channels" ( + "id" SERIAL PRIMARY KEY, + "owner_id" INTEGER NOT NULL, + "owner_is_user" BOOLEAN NOT NULL, + "name" VARCHAR NOT NULL +); + +CREATE UNIQUE INDEX "index_channels_owner" ON "channels" ("owner_is_user", "owner_id"); + +CREATE TABLE IF NOT EXISTS "channel_memberships" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER REFERENCES channels (id) NOT NULL, + "user_id" INTEGER REFERENCES users (id) NOT NULL, + "admin" BOOLEAN NOT NULL +); + +CREATE UNIQUE INDEX "index_channel_memberships_user_id" ON "channel_memberships" ("user_id"); +CREATE UNIQUE INDEX "index_channel_memberships_channel_id" ON "channel_memberships" ("channel_id"); + +CREATE TABLE IF NOT EXISTS "channel_messages" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER REFERENCES channels (id) NOT NULL, + "sender_id" INTEGER REFERENCES users (id) NOT NULL, + "content" TEXT NOT NULL, + "sent_at" TIMESTAMP +); + +CREATE UNIQUE INDEX "index_channel_messages_channel_id" ON "channel_messages" ("channel_id"); + +INSERT INTO users (github_login, admin) VALUES ('iamnbutler', true); + +DO $$ +DECLARE + zed_org_id INTEGER; + max_id INTEGER; + nathan_id INTEGER; + antonio_id INTEGER; + nate_id INTEGER; +BEGIN + INSERT INTO "orgs" (name, slug) VALUES ('Zed', 'zed') RETURNING id into zed_org_id; +END $$; + From 775bf8dd33f3919eea17cfbaf391136e982cd70d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Aug 2021 11:56:36 -0700 Subject: [PATCH 003/204] Add seed target for inserting seed data Co-Authored-By: Nathan Sobo --- .../20210805175147_create_chat_tables.sql | 28 ++--- server/src/bin/seed.rs | 107 ++++++++++++++++++ 2 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 server/src/bin/seed.rs diff --git a/server/migrations/20210805175147_create_chat_tables.sql b/server/migrations/20210805175147_create_chat_tables.sql index c79c8e39b4719e608d0e76f0dd50116cf4069a53..9e9d47b731bea5694c96cb672df2b7e5650533e8 100644 --- a/server/migrations/20210805175147_create_chat_tables.sql +++ b/server/migrations/20210805175147_create_chat_tables.sql @@ -4,6 +4,8 @@ CREATE TABLE IF NOT EXISTS "orgs" ( "slug" VARCHAR NOT NULL ); +CREATE UNIQUE INDEX "index_orgs_slug" ON "orgs" ("slug"); + CREATE TABLE IF NOT EXISTS "org_memberships" ( "id" SERIAL PRIMARY KEY, "org_id" INTEGER REFERENCES orgs (id) NOT NULL, @@ -11,8 +13,8 @@ CREATE TABLE IF NOT EXISTS "org_memberships" ( "admin" BOOLEAN NOT NULL ); -CREATE UNIQUE INDEX "index_org_memberships_user_id" ON "org_memberships" ("user_id"); -CREATE UNIQUE INDEX "index_org_memberships_org_id" ON "org_memberships" ("org_id"); +CREATE INDEX "index_org_memberships_user_id" ON "org_memberships" ("user_id"); +CREATE UNIQUE INDEX "index_org_memberships_org_id_and_user_id" ON "org_memberships" ("org_id", "user_id"); CREATE TABLE IF NOT EXISTS "channels" ( "id" SERIAL PRIMARY KEY, @@ -21,7 +23,7 @@ CREATE TABLE IF NOT EXISTS "channels" ( "name" VARCHAR NOT NULL ); -CREATE UNIQUE INDEX "index_channels_owner" ON "channels" ("owner_is_user", "owner_id"); +CREATE UNIQUE INDEX "index_channels_owner_and_name" ON "channels" ("owner_is_user", "owner_id", "name"); CREATE TABLE IF NOT EXISTS "channel_memberships" ( "id" SERIAL PRIMARY KEY, @@ -30,8 +32,8 @@ CREATE TABLE IF NOT EXISTS "channel_memberships" ( "admin" BOOLEAN NOT NULL ); -CREATE UNIQUE INDEX "index_channel_memberships_user_id" ON "channel_memberships" ("user_id"); -CREATE UNIQUE INDEX "index_channel_memberships_channel_id" ON "channel_memberships" ("channel_id"); +CREATE INDEX "index_channel_memberships_user_id" ON "channel_memberships" ("user_id"); +CREATE UNIQUE INDEX "index_channel_memberships_channel_id_and_user_id" ON "channel_memberships" ("channel_id", "user_id"); CREATE TABLE IF NOT EXISTS "channel_messages" ( "id" SERIAL PRIMARY KEY, @@ -41,18 +43,4 @@ CREATE TABLE IF NOT EXISTS "channel_messages" ( "sent_at" TIMESTAMP ); -CREATE UNIQUE INDEX "index_channel_messages_channel_id" ON "channel_messages" ("channel_id"); - -INSERT INTO users (github_login, admin) VALUES ('iamnbutler', true); - -DO $$ -DECLARE - zed_org_id INTEGER; - max_id INTEGER; - nathan_id INTEGER; - antonio_id INTEGER; - nate_id INTEGER; -BEGIN - INSERT INTO "orgs" (name, slug) VALUES ('Zed', 'zed') RETURNING id into zed_org_id; -END $$; - +CREATE INDEX "index_channel_messages_channel_id" ON "channel_messages" ("channel_id"); diff --git a/server/src/bin/seed.rs b/server/src/bin/seed.rs new file mode 100644 index 0000000000000000000000000000000000000000..61af3bcd88a261cb27609d725299aae0e9dc550e --- /dev/null +++ b/server/src/bin/seed.rs @@ -0,0 +1,107 @@ +use sqlx::postgres::PgPoolOptions; +use tide::log; + +#[path = "../env.rs"] +mod env; + +#[async_std::main] +async fn main() { + if let Err(error) = env::load_dotenv() { + log::error!( + "error loading .env.toml (this is expected in production): {}", + error + ); + } + + let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var"); + let db = PgPoolOptions::new() + .max_connections(5) + .connect(&database_url) + .await + .expect("failed to connect to postgres database"); + + let zed_users = ["nathansobo", "maxbrunsfeld", "as-cii", "iamnbutler"]; + let mut zed_user_ids = Vec::::new(); + for zed_user in zed_users { + zed_user_ids.push( + sqlx::query_scalar( + r#" + INSERT INTO users + (github_login, admin) + VALUES + ($1, true) + ON CONFLICT (github_login) DO UPDATE SET + github_login=EXCLUDED.github_login + RETURNING id + "#, + ) + .bind(zed_user) + .fetch_one(&db) + .await + .expect("failed to insert user"), + ) + } + + let zed_org_id: i32 = sqlx::query_scalar( + r#" + INSERT INTO orgs + (name, slug) + VALUES + ('Zed', 'zed') + ON CONFLICT (slug) DO UPDATE SET + slug=EXCLUDED.slug + RETURNING id + "#, + ) + .fetch_one(&db) + .await + .expect("failed to insert org"); + + let general_channel_id: i32 = sqlx::query_scalar( + r#" + INSERT INTO channels + (owner_is_user, owner_id, name) + VALUES + (false, $1, 'General') + ON CONFLICT (owner_is_user, owner_id, name) DO UPDATE SET + name=EXCLUDED.name + RETURNING id + "#, + ) + .bind(zed_org_id) + .fetch_one(&db) + .await + .expect("failed to insert channel"); + + for user_id in zed_user_ids { + sqlx::query( + r#" + INSERT INTO org_memberships + (org_id, user_id, admin) + VALUES + ($1, $2, true) + ON CONFLICT DO NOTHING + "#, + ) + .bind(zed_org_id) + .bind(user_id) + .execute(&db) + .await + .expect("failed to insert org membership"); + + sqlx::query( + r#" + INSERT INTO channel_memberships + (channel_id, user_id, admin) + VALUES + ($1, $2, true) + ON CONFLICT DO NOTHING + "#, + ) + .bind(general_channel_id) + .bind(user_id) + .execute(&db) + .await + .expect("failed to insert channel membership"); + } +} From 0b6376bdda5de4475dbb25f487ca11f051f3e56c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Aug 2021 19:05:58 -0700 Subject: [PATCH 004/204] Add several proto messages for chat Co-Authored-By: Nathan Sobo --- .../20210805175147_create_chat_tables.sql | 2 +- zrpc/proto/zed.proto | 66 +++++++++++++++++-- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/server/migrations/20210805175147_create_chat_tables.sql b/server/migrations/20210805175147_create_chat_tables.sql index 9e9d47b731bea5694c96cb672df2b7e5650533e8..5bba4689d9c21e65d989cf05e2e1eedb0151621d 100644 --- a/server/migrations/20210805175147_create_chat_tables.sql +++ b/server/migrations/20210805175147_create_chat_tables.sql @@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS "channel_messages" ( "id" SERIAL PRIMARY KEY, "channel_id" INTEGER REFERENCES channels (id) NOT NULL, "sender_id" INTEGER REFERENCES users (id) NOT NULL, - "content" TEXT NOT NULL, + "body" TEXT NOT NULL, "sent_at" TIMESTAMP ); diff --git a/zrpc/proto/zed.proto b/zrpc/proto/zed.proto index d4d4406e0d594704844a1b0cd612ef6f3df01e11..65360ae8853f329c0e208d0cf0e956ff221d44f9 100644 --- a/zrpc/proto/zed.proto +++ b/zrpc/proto/zed.proto @@ -22,9 +22,19 @@ message Envelope { BufferSaved buffer_saved = 17; AddPeer add_peer = 18; RemovePeer remove_peer = 19; + GetChannels get_channels = 20; + GetChannelsResponse get_channels_response = 21; + JoinChannel join_channel = 22; + JoinChannelResponse join_channel_response = 23; + GetUsers get_users = 24; + GetUsersResponse get_users_response = 25; + SendChannelMessage send_channel_message = 26; + ChannelMessageSent channel_message_sent = 27; } } +// Messages + message Auth { int32 user_id = 1; string access_token = 2; @@ -75,11 +85,6 @@ message RemovePeer { uint32 peer_id = 2; } -message Peer { - uint32 peer_id = 1; - uint32 replica_id = 2; -} - message OpenBuffer { uint64 worktree_id = 1; string path = 2; @@ -112,6 +117,45 @@ message BufferSaved { Timestamp mtime = 4; } +message GetChannels {} + +message GetChannelsResponse { + repeated Channel channels = 1; +} + +message JoinChannel { + uint64 channel_id = 1; +} + +message JoinChannelResponse { + repeated ChannelMessage messages = 1; +} + +message GetUsers { + repeated uint64 user_ids = 1; +} + +message GetUsersResponse { + repeated User users = 1; +} + +message SendChannelMessage { + uint64 channel_id = 1; + string body = 2; +} + +message ChannelMessageSent { + uint64 channel_id = 1; + ChannelMessage message = 2; +} + +// Entities + +message Peer { + uint32 peer_id = 1; + uint32 replica_id = 2; +} + message User { uint64 id = 1; string github_login = 2; @@ -228,3 +272,15 @@ message Range { uint64 start = 1; uint64 end = 2; } + +message Channel { + uint64 id = 1; + string name = 2; +} + +message ChannelMessage { + uint64 id = 1; + string body = 2; + uint64 timestamp = 3; + uint64 sender_id = 4; +} \ No newline at end of file From 2b9b9b8f1f237e5541fbf814b49f6d39ad281e6c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Aug 2021 19:06:13 -0700 Subject: [PATCH 005/204] Add seed-db script --- script/seed-db | 5 +++++ 1 file changed, 5 insertions(+) create mode 100755 script/seed-db diff --git a/script/seed-db b/script/seed-db new file mode 100755 index 0000000000000000000000000000000000000000..0bdfc0a5e33d839bb09e1a5884d598dde1c9a5a5 --- /dev/null +++ b/script/seed-db @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e +cd server +cargo run --bin seed From 109d8271e01eb816abb02156b512cb34cf183d37 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Aug 2021 19:06:50 -0700 Subject: [PATCH 006/204] Add server methods for creating chat domain objects Also, consolidate all sql into a `db` module --- server/src/admin.rs | 65 ++--------- server/src/auth.rs | 79 +++++-------- server/src/db.rs | 276 ++++++++++++++++++++++++++++++++++++++++++++ server/src/home.rs | 10 +- server/src/main.rs | 17 ++- server/src/rpc.rs | 16 +-- server/src/tests.rs | 21 ++-- 7 files changed, 344 insertions(+), 140 deletions(-) create mode 100644 server/src/db.rs diff --git a/server/src/admin.rs b/server/src/admin.rs index 3f379ff56f9a1e2f2c5d34d41b20a24dfa683c7a..d6e3f8161589e6b2420cc9a7d54bd212ec8d76b1 100644 --- a/server/src/admin.rs +++ b/server/src/admin.rs @@ -1,7 +1,6 @@ -use crate::{auth::RequestExt as _, AppState, DbPool, LayoutData, Request, RequestExt as _}; +use crate::{auth::RequestExt as _, db, AppState, LayoutData, Request, RequestExt as _}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use sqlx::{Executor, FromRow}; use std::sync::Arc; use surf::http::mime; @@ -41,23 +40,8 @@ pub fn add_routes(app: &mut tide::Server>) { struct AdminData { #[serde(flatten)] layout: Arc, - users: Vec, - signups: Vec, -} - -#[derive(Debug, FromRow, Serialize)] -pub struct User { - pub id: i32, - pub github_login: String, - pub admin: bool, -} - -#[derive(Debug, FromRow, Serialize)] -pub struct Signup { - pub id: i32, - pub github_login: String, - pub email_address: String, - pub about: String, + users: Vec, + signups: Vec, } async fn get_admin_page(mut request: Request) -> tide::Result { @@ -65,12 +49,8 @@ async fn get_admin_page(mut request: Request) -> tide::Result { let data = AdminData { layout: request.layout_data().await?, - users: sqlx::query_as("SELECT * FROM users ORDER BY github_login ASC") - .fetch_all(request.db()) - .await?, - signups: sqlx::query_as("SELECT * FROM signups ORDER BY id DESC") - .fetch_all(request.db()) - .await?, + users: request.db().get_all_users().await?, + signups: request.db().get_all_signups().await?, }; Ok(tide::Response::builder(200) @@ -96,7 +76,7 @@ async fn post_user(mut request: Request) -> tide::Result { .unwrap_or(&form.github_login); if !github_login.is_empty() { - create_user(request.db(), github_login, form.admin).await?; + request.db().create_user(github_login, form.admin).await?; } Ok(tide::Redirect::new("/admin").into()) @@ -116,11 +96,7 @@ async fn put_user(mut request: Request) -> tide::Result { request .db() - .execute( - sqlx::query("UPDATE users SET admin = $1 WHERE id = $2;") - .bind(body.admin) - .bind(user_id), - ) + .set_user_is_admin(db::UserId(user_id), body.admin) .await?; Ok(tide::Response::builder(200).build()) @@ -128,33 +104,14 @@ async fn put_user(mut request: Request) -> tide::Result { async fn delete_user(request: Request) -> tide::Result { request.require_admin().await?; - - let user_id = request.param("id")?.parse::()?; - request - .db() - .execute(sqlx::query("DELETE FROM users WHERE id = $1;").bind(user_id)) - .await?; - + let user_id = db::UserId(request.param("id")?.parse::()?); + request.db().delete_user(user_id).await?; Ok(tide::Redirect::new("/admin").into()) } -pub async fn create_user(db: &DbPool, github_login: &str, admin: bool) -> tide::Result { - let id: i32 = - sqlx::query_scalar("INSERT INTO users (github_login, admin) VALUES ($1, $2) RETURNING id;") - .bind(github_login) - .bind(admin) - .fetch_one(db) - .await?; - Ok(id) -} - async fn delete_signup(request: Request) -> tide::Result { request.require_admin().await?; - let signup_id = request.param("id")?.parse::()?; - request - .db() - .execute(sqlx::query("DELETE FROM signups WHERE id = $1;").bind(signup_id)) - .await?; - + let signup_id = db::SignupId(request.param("id")?.parse::()?); + request.db().delete_signup(signup_id).await?; Ok(tide::Redirect::new("/admin").into()) } diff --git a/server/src/auth.rs b/server/src/auth.rs index 4a7107e550c2adc4838a703308554c93ea21464d..9dde8212ff2a848257dfafc2512443b4a0218c33 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -1,7 +1,9 @@ -use super::errors::TideResultExt; -use crate::{github, rpc, AppState, DbPool, Request, RequestExt as _}; +use super::{ + db::{self, UserId}, + errors::TideResultExt, +}; +use crate::{github, rpc, AppState, Request, RequestExt as _}; use anyhow::{anyhow, Context}; -use async_std::stream::StreamExt; use async_trait::async_trait; pub use oauth2::basic::BasicClient as Client; use oauth2::{ @@ -14,7 +16,6 @@ use scrypt::{ Scrypt, }; use serde::{Deserialize, Serialize}; -use sqlx::FromRow; use std::{borrow::Cow, convert::TryFrom, sync::Arc}; use surf::Url; use tide::Server; @@ -34,9 +35,6 @@ pub struct User { pub struct VerifyToken; -#[derive(Clone, Copy)] -pub struct UserId(pub i32); - #[async_trait] impl tide::Middleware> for VerifyToken { async fn handle( @@ -51,33 +49,28 @@ impl tide::Middleware> for VerifyToken { .as_str() .split_whitespace(); - let user_id: i32 = auth_header - .next() - .ok_or_else(|| anyhow!("missing user id in authorization header"))? - .parse()?; + let user_id = UserId( + auth_header + .next() + .ok_or_else(|| anyhow!("missing user id in authorization header"))? + .parse()?, + ); let access_token = auth_header .next() .ok_or_else(|| anyhow!("missing access token in authorization header"))?; let state = request.state().clone(); - let mut password_hashes = - sqlx::query_scalar::<_, String>("SELECT hash FROM access_tokens WHERE user_id = $1") - .bind(&user_id) - .fetch_many(&state.db); - let mut credentials_valid = false; - while let Some(password_hash) = password_hashes.next().await { - if let either::Either::Right(password_hash) = password_hash? { - if verify_access_token(&access_token, &password_hash)? { - credentials_valid = true; - break; - } + for password_hash in state.db.get_access_token_hashes(user_id).await? { + if verify_access_token(&access_token, &password_hash)? { + credentials_valid = true; + break; } } if credentials_valid { - request.set_ext(UserId(user_id)); + request.set_ext(user_id); Ok(next.run(request).await) } else { Err(anyhow!("invalid credentials").into()) @@ -94,25 +87,12 @@ pub trait RequestExt { impl RequestExt for Request { async fn current_user(&self) -> tide::Result> { if let Some(details) = self.session().get::(CURRENT_GITHUB_USER) { - #[derive(FromRow)] - struct UserRow { - admin: bool, - } - - let user_row: Option = - sqlx::query_as("SELECT admin FROM users WHERE github_login = $1") - .bind(&details.login) - .fetch_optional(self.db()) - .await?; - - let is_insider = user_row.is_some(); - let is_admin = user_row.map_or(false, |row| row.admin); - + let user = self.db().get_user_by_github_login(&details.login).await?; Ok(Some(User { github_login: details.login, avatar_url: details.avatar_url, - is_insider, - is_admin, + is_insider: user.is_some(), + is_admin: user.map_or(false, |user| user.admin), })) } else { Ok(None) @@ -265,9 +245,9 @@ async fn get_auth_callback(mut request: Request) -> tide::Result { .await .context("failed to fetch user")?; - let user_id: Option = sqlx::query_scalar("SELECT id from users where github_login = $1") - .bind(&user_details.login) - .fetch_optional(request.db()) + let user = request + .db() + .get_user_by_github_login(&user_details.login) .await?; request @@ -276,8 +256,8 @@ async fn get_auth_callback(mut request: Request) -> tide::Result { // When signing in from the native app, generate a new access token for the current user. Return // a redirect so that the user's browser sends this access token to the locally-running app. - if let Some((user_id, app_sign_in_params)) = user_id.zip(query.native_app_sign_in_params) { - let access_token = create_access_token(request.db(), user_id).await?; + if let Some((user, app_sign_in_params)) = user.zip(query.native_app_sign_in_params) { + let access_token = create_access_token(request.db(), user.id()).await?; let native_app_public_key = zed_auth::PublicKey::try_from(app_sign_in_params.native_app_public_key.clone()) .context("failed to parse app public key")?; @@ -287,7 +267,9 @@ async fn get_auth_callback(mut request: Request) -> tide::Result { return Ok(tide::Redirect::new(&format!( "http://127.0.0.1:{}?user_id={}&access_token={}", - app_sign_in_params.native_app_port, user_id, encrypted_access_token, + app_sign_in_params.native_app_port, + user.id().0, + encrypted_access_token, )) .into()); } @@ -300,14 +282,11 @@ async fn post_sign_out(mut request: Request) -> tide::Result { Ok(tide::Redirect::new("/").into()) } -pub async fn create_access_token(db: &DbPool, user_id: i32) -> tide::Result { +pub async fn create_access_token(db: &db::Db, user_id: UserId) -> tide::Result { let access_token = zed_auth::random_token(); let access_token_hash = hash_access_token(&access_token).context("failed to hash access token")?; - sqlx::query("INSERT INTO access_tokens (user_id, hash) values ($1, $2)") - .bind(user_id) - .bind(access_token_hash) - .fetch_optional(db) + db.create_access_token_hash(user_id, access_token_hash) .await?; Ok(access_token) } diff --git a/server/src/db.rs b/server/src/db.rs new file mode 100644 index 0000000000000000000000000000000000000000..9b610187012f781b384a6b7af50c4a54b790396e --- /dev/null +++ b/server/src/db.rs @@ -0,0 +1,276 @@ +use serde::Serialize; +use sqlx::{FromRow, Result}; + +pub use async_sqlx_session::PostgresSessionStore as SessionStore; +pub use sqlx::postgres::PgPoolOptions as DbOptions; + +pub struct Db(pub sqlx::PgPool); + +#[derive(Debug, FromRow, Serialize)] +pub struct User { + id: i32, + pub github_login: String, + pub admin: bool, +} + +#[derive(Debug, FromRow, Serialize)] +pub struct Signup { + id: i32, + pub github_login: String, + pub email_address: String, + pub about: String, +} + +#[derive(Debug, FromRow)] +pub struct ChannelMessage { + id: i32, + sender_id: i32, + body: String, + sent_at: i64, +} + +#[derive(Clone, Copy)] +pub struct UserId(pub i32); + +#[derive(Clone, Copy)] +pub struct OrgId(pub i32); + +#[derive(Clone, Copy)] +pub struct ChannelId(pub i32); + +#[derive(Clone, Copy)] +pub struct SignupId(pub i32); + +#[derive(Clone, Copy)] +pub struct MessageId(pub i32); + +impl Db { + // signups + + pub async fn create_signup( + &self, + github_login: &str, + email_address: &str, + about: &str, + ) -> Result { + let query = " + INSERT INTO signups (github_login, email_address, about) + VALUES ($1, $2, $3) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(github_login) + .bind(email_address) + .bind(about) + .fetch_one(&self.0) + .await + .map(SignupId) + } + + pub async fn get_all_signups(&self) -> Result> { + let query = "SELECT * FROM users ORDER BY github_login ASC"; + sqlx::query_as(query).fetch_all(&self.0).await + } + + pub async fn delete_signup(&self, id: SignupId) -> Result<()> { + let query = "DELETE FROM signups WHERE id = $1"; + sqlx::query(query) + .bind(id.0) + .execute(&self.0) + .await + .map(drop) + } + + // users + + pub async fn create_user(&self, github_login: &str, admin: bool) -> Result { + let query = " + INSERT INTO users (github_login, admin) + VALUES ($1, $2) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(github_login) + .bind(admin) + .fetch_one(&self.0) + .await + .map(UserId) + } + + pub async fn get_all_users(&self) -> Result> { + let query = "SELECT * FROM users ORDER BY github_login ASC"; + sqlx::query_as(query).fetch_all(&self.0).await + } + + pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { + let query = "SELECT * FROM users WHERE github_login = $1 LIMIT 1"; + sqlx::query_as(query) + .bind(github_login) + .fetch_optional(&self.0) + .await + } + + pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> { + let query = "UPDATE users SET admin = $1 WHERE id = $2"; + sqlx::query(query) + .bind(is_admin) + .bind(id.0) + .execute(&self.0) + .await + .map(drop) + } + + pub async fn delete_user(&self, id: UserId) -> Result<()> { + let query = "DELETE FROM users WHERE id = $1;"; + sqlx::query(query) + .bind(id.0) + .execute(&self.0) + .await + .map(drop) + } + + // access tokens + + pub async fn create_access_token_hash( + &self, + user_id: UserId, + access_token_hash: String, + ) -> Result<()> { + let query = " + INSERT INTO access_tokens (user_id, hash) + VALUES ($1, $2) + "; + sqlx::query(query) + .bind(user_id.0 as i32) + .bind(access_token_hash) + .execute(&self.0) + .await + .map(drop) + } + + pub async fn get_access_token_hashes(&self, user_id: UserId) -> Result> { + let query = "SELECT hash FROM access_tokens WHERE user_id = $1"; + sqlx::query_scalar::<_, String>(query) + .bind(user_id.0 as i32) + .fetch_all(&self.0) + .await + } + + // orgs + + pub async fn create_org(&self, name: &str, slug: &str) -> Result { + let query = " + INSERT INTO orgs (name, slug) + VALUES ($1, $2) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(name) + .bind(slug) + .fetch_one(&self.0) + .await + .map(OrgId) + } + + pub async fn add_org_member(&self, org_id: OrgId, user_id: UserId) -> Result<()> { + let query = " + INSERT INTO org_memberships (org_id, user_id) + VALUES ($1, $2) + "; + sqlx::query(query) + .bind(org_id.0) + .bind(user_id.0) + .execute(&self.0) + .await + .map(drop) + } + + // channels + + pub async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result { + let query = " + INSERT INTO channels (owner_id, owner_is_user, name) + VALUES ($1, false, $2) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(org_id.0) + .bind(name) + .fetch_one(&self.0) + .await + .map(ChannelId) + } + + pub async fn add_channel_member( + &self, + channel_id: ChannelId, + user_id: UserId, + is_admin: bool, + ) -> Result<()> { + let query = " + INSERT INTO channel_memberships (channel_id, user_id, admin) + VALUES ($1, $2, $3) + "; + sqlx::query(query) + .bind(channel_id.0) + .bind(user_id.0) + .bind(is_admin) + .execute(&self.0) + .await + .map(drop) + } + + // messages + + pub async fn create_channel_message( + &self, + channel_id: ChannelId, + sender_id: UserId, + body: &str, + ) -> Result { + let query = " + INSERT INTO channel_messages (channel_id, sender_id, body, sent_at) + VALUES ($1, $2, $3, NOW()::timestamp) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(channel_id.0) + .bind(sender_id.0) + .bind(body) + .fetch_one(&self.0) + .await + .map(MessageId) + } + + pub async fn get_recent_channel_messages( + &self, + channel_id: ChannelId, + count: usize, + ) -> Result> { + let query = " + SELECT id, sender_id, body, sent_at + FROM channel_messages + WHERE channel_id = $1 + LIMIT $2 + "; + sqlx::query_as(query) + .bind(channel_id.0) + .bind(count as i64) + .fetch_all(&self.0) + .await + } +} + +impl std::ops::Deref for Db { + type Target = sqlx::PgPool; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl User { + pub fn id(&self) -> UserId { + UserId(self.id) + } +} diff --git a/server/src/home.rs b/server/src/home.rs index b4b8c24bf607302db7c15b109bd784bf38e667e2..25adde3a0f2fed3d7b11c107e23dcbfdf1d67bac 100644 --- a/server/src/home.rs +++ b/server/src/home.rs @@ -3,7 +3,6 @@ use crate::{ }; use comrak::ComrakOptions; use serde::{Deserialize, Serialize}; -use sqlx::Executor as _; use std::sync::Arc; use tide::{http::mime, log, Server}; @@ -76,14 +75,7 @@ async fn post_signup(mut request: Request) -> tide::Result { // Save signup in the database request .db() - .execute( - sqlx::query( - "INSERT INTO signups (github_login, email_address, about) VALUES ($1, $2, $3);", - ) - .bind(&form.github_login) - .bind(&form.email_address) - .bind(&form.about), - ) + .create_signup(&form.github_login, &form.email_address, &form.about) .await?; let layout_data = request.layout_data().await?; diff --git a/server/src/main.rs b/server/src/main.rs index ebd52b0a8bd0fd1e42beeaa505245852f545dd6f..ec153bea8fd535d513be81aab3f0b0bfc862d3b8 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,7 @@ mod admin; mod assets; mod auth; +mod db; mod env; mod errors; mod expiring; @@ -13,15 +14,14 @@ mod tests; use self::errors::TideResultExt as _; use anyhow::{Context, Result}; -use async_sqlx_session::PostgresSessionStore; use async_std::{net::TcpListener, sync::RwLock as AsyncRwLock}; use async_trait::async_trait; use auth::RequestExt as _; +use db::{Db, DbOptions}; use handlebars::{Handlebars, TemplateRenderError}; use parking_lot::RwLock; use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; -use sqlx::postgres::{PgPool, PgPoolOptions}; use std::sync::Arc; use surf::http::cookies::SameSite; use tide::{log, sessions::SessionMiddleware}; @@ -29,7 +29,6 @@ use tide_compress::CompressMiddleware; use zrpc::Peer; type Request = tide::Request>; -type DbPool = PgPool; #[derive(RustEmbed)] #[folder = "templates"] @@ -47,7 +46,7 @@ pub struct Config { } pub struct AppState { - db: sqlx::PgPool, + db: Db, handlebars: RwLock>, auth_client: auth::Client, github_client: Arc, @@ -58,11 +57,11 @@ pub struct AppState { impl AppState { async fn new(config: Config) -> tide::Result> { - let db = PgPoolOptions::new() + let db = Db(DbOptions::new() .max_connections(5) .connect(&config.database_url) .await - .context("failed to connect to postgres database")?; + .context("failed to connect to postgres database")?); let github_client = github::AppClient::new(config.github_app_id, config.github_private_key.clone()); @@ -117,7 +116,7 @@ impl AppState { #[async_trait] trait RequestExt { async fn layout_data(&mut self) -> tide::Result>; - fn db(&self) -> &DbPool; + fn db(&self) -> &Db; } #[async_trait] @@ -131,7 +130,7 @@ impl RequestExt for Request { Ok(self.ext::>().unwrap().clone()) } - fn db(&self) -> &DbPool { + fn db(&self) -> &Db { &self.state().db } } @@ -173,7 +172,7 @@ pub async fn run_server( web.with(CompressMiddleware::new()); web.with( SessionMiddleware::new( - PostgresSessionStore::new_with_table_name(&state.config.database_url, "sessions") + db::SessionStore::new_with_table_name(&state.config.database_url, "sessions") .await .unwrap(), state.config.session_secret.as_bytes(), diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 3c189833b252e2354e43683e63f4e2fba6db54c6..cd229a3c670f681a7c435635353af63bc2606305 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1,6 +1,8 @@ -use crate::auth::{self, UserId}; - -use super::{auth::PeerExt as _, AppState}; +use super::{ + auth::{self, PeerExt as _}, + db::UserId, + AppState, +}; use anyhow::anyhow; use async_std::task; use async_tungstenite::{ @@ -37,7 +39,7 @@ pub struct State { } struct ConnectionState { - _user_id: i32, + _user_id: UserId, worktrees: HashSet, } @@ -68,7 +70,7 @@ impl WorktreeState { impl State { // Add a new connection associated with a given user. - pub fn add_connection(&mut self, connection_id: ConnectionId, _user_id: i32) { + pub fn add_connection(&mut self, connection_id: ConnectionId, _user_id: UserId) { self.connections.insert( connection_id, ConnectionState { @@ -291,7 +293,7 @@ pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { let upgrade_receiver = http_res.recv_upgrade().await; let addr = request.remote().unwrap_or("unknown").to_string(); let state = request.state().clone(); - let user_id = user_id.ok_or_else(|| anyhow!("user_id is not present on request. ensure auth::VerifyToken middleware is present"))?.0; + 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 { let stream = WebSocketStream::from_raw_socket(stream, Role::Server, None).await; @@ -310,7 +312,7 @@ pub async fn handle_connection( state: Arc, addr: String, stream: Conn, - user_id: i32, + user_id: UserId, ) where Conn: 'static + futures::Sink diff --git a/server/src/tests.rs b/server/src/tests.rs index 66d904746772c5c4d0813ac85ab33b108c074207..d0257e9f4184027561fc15b78b4bc02b2998c541 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -1,5 +1,5 @@ use crate::{ - admin, auth, github, + auth, db, github, rpc::{self, add_rpc_routes}, AppState, Config, }; @@ -9,7 +9,6 @@ use rand::prelude::*; use serde_json::json; use sqlx::{ migrate::{MigrateDatabase, Migrator}, - postgres::PgPoolOptions, Executor as _, Postgres, }; use std::{path::Path, sync::Arc}; @@ -499,9 +498,7 @@ impl TestServer { } async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> Client { - let user_id = admin::create_user(&self.app_state.db, name, false) - .await - .unwrap(); + let user_id = self.app_state.db.create_user(name, false).await.unwrap(); let lang_registry = Arc::new(LanguageRegistry::new()); let client = Client::new(lang_registry.clone()); let mut client_router = ForegroundRouter::new(); @@ -532,18 +529,20 @@ impl TestServer { config.database_url = format!("postgres://postgres@localhost/{}", db_name); Self::create_db(&config.database_url).await; - let db = PgPoolOptions::new() - .max_connections(5) - .connect(&config.database_url) - .await - .expect("failed to connect to postgres database"); + let db = db::Db( + db::DbOptions::new() + .max_connections(5) + .connect(&config.database_url) + .await + .expect("failed to connect to postgres database"), + ); let migrator = Migrator::new(Path::new(concat!( env!("CARGO_MANIFEST_DIR"), "/migrations" ))) .await .unwrap(); - migrator.run(&db).await.unwrap(); + migrator.run(&db.0).await.unwrap(); let github_client = github::AppClient::test(); Arc::new(AppState { From 13ee9c228620d34b5c7cc91180584e0c17f35ee3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Aug 2021 19:59:54 -0700 Subject: [PATCH 007/204] Start work on server-side RPC handling for chat --- server/src/db.rs | 51 +++++++++++++++++++++++++++++++++++ server/src/rpc.rs | 66 ++++++++++++++++++++++++++++++++++++++++++--- server/src/tests.rs | 60 +++++++++++++++++++++++++++++++---------- zrpc/src/proto.rs | 5 ++++ 4 files changed, 164 insertions(+), 18 deletions(-) diff --git a/server/src/db.rs b/server/src/db.rs index 9b610187012f781b384a6b7af50c4a54b790396e..300c8de6d5526039150f63d3e0b296487be68e9f 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -21,6 +21,12 @@ pub struct Signup { pub about: String, } +#[derive(Debug, FromRow, Serialize)] +pub struct Channel { + id: i32, + pub name: String, +} + #[derive(Debug, FromRow)] pub struct ChannelMessage { id: i32, @@ -158,6 +164,7 @@ impl Db { // orgs + #[cfg(test)] pub async fn create_org(&self, name: &str, slug: &str) -> Result { let query = " INSERT INTO orgs (name, slug) @@ -172,6 +179,7 @@ impl Db { .map(OrgId) } + #[cfg(test)] pub async fn add_org_member(&self, org_id: OrgId, user_id: UserId) -> Result<()> { let query = " INSERT INTO org_memberships (org_id, user_id) @@ -187,6 +195,7 @@ impl Db { // channels + #[cfg(test)] pub async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result { let query = " INSERT INTO channels (owner_id, owner_is_user, name) @@ -201,6 +210,42 @@ impl Db { .map(ChannelId) } + pub async fn get_channels_for_user(&self, user_id: UserId) -> Result> { + let query = " + SELECT + channels.id, channels.name + FROM + channel_memberships, channels + WHERE + channel_memberships.user_id = $1 AND + channel_memberships.channel_id = channels.id + "; + sqlx::query_as(query) + .bind(user_id.0) + .fetch_all(&self.0) + .await + } + + pub async fn can_user_access_channel( + &self, + user_id: UserId, + channel_id: ChannelId, + ) -> Result { + let query = " + SELECT id + FROM channel_memberships + WHERE user_id = $1 AND channel_id = $2 + LIMIT 1 + "; + sqlx::query_scalar::<_, i32>(query) + .bind(user_id.0) + .bind(channel_id.0) + .fetch_optional(&self.0) + .await + .map(|e| e.is_some()) + } + + #[cfg(test)] pub async fn add_channel_member( &self, channel_id: ChannelId, @@ -269,6 +314,12 @@ impl std::ops::Deref for Db { } } +impl Channel { + pub fn id(&self) -> ChannelId { + ChannelId(self.id) + } +} + impl User { pub fn id(&self) -> UserId { UserId(self.id) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index cd229a3c670f681a7c435635353af63bc2606305..f1ebf605a25fd2bab1aa01883f1decd2ca68d2b2 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1,6 +1,6 @@ use super::{ auth::{self, PeerExt as _}, - db::UserId, + db::{ChannelId, UserId}, AppState, }; use anyhow::anyhow; @@ -39,7 +39,7 @@ pub struct State { } struct ConnectionState { - _user_id: UserId, + user_id: UserId, worktrees: HashSet, } @@ -70,11 +70,11 @@ impl WorktreeState { impl State { // Add a new connection associated with a given user. - pub fn add_connection(&mut self, connection_id: ConnectionId, _user_id: UserId) { + pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId) { self.connections.insert( connection_id, ConnectionState { - _user_id, + user_id, worktrees: Default::default(), }, ); @@ -130,6 +130,14 @@ impl State { } } + fn user_id_for_connection(&self, connection_id: ConnectionId) -> tide::Result { + Ok(self + .connections + .get(&connection_id) + .ok_or_else(|| anyhow!("unknown connection"))? + .user_id) + } + fn read_worktree( &self, worktree_id: u64, @@ -254,6 +262,8 @@ pub fn add_rpc_routes(router: &mut Router, state: &Arc, rpc: &Arc>, rpc: &Arc) { @@ -600,6 +610,54 @@ async fn buffer_saved( broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await } +async fn get_channels( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + let user_id = state + .rpc + .read() + .await + .user_id_for_connection(request.sender_id)?; + let channels = state.db.get_channels_for_user(user_id).await?; + rpc.respond( + request.receipt(), + proto::GetChannelsResponse { + channels: channels + .into_iter() + .map(|chan| proto::Channel { + id: chan.id().0 as u64, + name: chan.name, + }) + .collect(), + }, + ) + .await?; + Ok(()) +} + +async fn join_channel( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + let user_id = state + .rpc + .read() + .await + .user_id_for_connection(request.sender_id)?; + if !state + .db + .can_user_access_channel(user_id, ChannelId(request.payload.channel_id as i32)) + .await? + { + Err(anyhow!("access denied"))?; + } + + Ok(()) +} + async fn broadcast_in_worktree( worktree_id: u64, request: TypedEnvelope, diff --git a/server/src/tests.rs b/server/src/tests.rs index d0257e9f4184027561fc15b78b4bc02b2998c541..653e2ae59a16bf6e376e7a2e14b11de43af6e7b4 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -1,5 +1,7 @@ use crate::{ - auth, db, github, + auth, + db::{self, UserId}, + github, rpc::{self, add_rpc_routes}, AppState, Config, }; @@ -31,8 +33,8 @@ async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) // 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(); @@ -138,9 +140,9 @@ async fn test_propagate_saves_and_fs_changes_in_shared_worktree( // 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; cx_a.foreground().forbid_parking(); @@ -280,8 +282,8 @@ async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: Tes // 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(); @@ -359,8 +361,8 @@ async fn test_editing_while_guest_opens_buffer(mut cx_a: TestAppContext, mut cx_ // 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(); @@ -420,8 +422,8 @@ async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) // 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; cx_a.foreground().forbid_parking(); @@ -474,6 +476,36 @@ async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) .await; } +#[gpui::test] +async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { + let lang_registry = Arc::new(LanguageRegistry::new()); + + // 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_a, "user_b").await; + + // Create a channel that includes these 2 users and 1 other user. + let db = &server.app_state.db; + let user_id_c = db.create_user("user_c", false).await.unwrap(); + 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_channel_member(channel_id, user_id_a, false) + .await + .unwrap(); + db.add_channel_member(channel_id, user_id_b, false) + .await + .unwrap(); + db.add_channel_member(channel_id, user_id_c, false) + .await + .unwrap(); + db.create_channel_message(channel_id, user_id_c, "first message!") + .await + .unwrap(); + + // let chatroom_a = ChatRoom:: +} + struct TestServer { peer: Arc, app_state: Arc, @@ -497,7 +529,7 @@ impl TestServer { } } - async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> Client { + async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> (UserId, Client) { let user_id = self.app_state.db.create_user(name, false).await.unwrap(); let lang_registry = Arc::new(LanguageRegistry::new()); let client = Client::new(lang_registry.clone()); @@ -520,7 +552,7 @@ impl TestServer { .await .unwrap(); - client + (user_id, client) } async fn build_app_state(db_name: &str) -> Arc { diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index bb082c17837030c8974822b74833b1b9ade0ed55..77390cbb175b062aa2ae5b8b6cc74e0dcda74172 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -79,6 +79,11 @@ message!(UpdateBuffer); request_message!(SaveBuffer, BufferSaved); message!(AddPeer); message!(RemovePeer); +request_message!(GetChannels, GetChannelsResponse); +request_message!(JoinChannel, JoinChannelResponse); +request_message!(GetUsers, GetUsersResponse); +message!(SendChannelMessage); +message!(ChannelMessageSent); /// A stream of protobuf messages. pub struct MessageStream { From e16c62ed0e2ad6186c6474ea456bcd74e7c61bfd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 6 Aug 2021 09:08:29 -0600 Subject: [PATCH 008/204] Add platform::Window::titlebar_height --- gpui/src/platform.rs | 1 + gpui/src/platform/mac/window.rs | 13 +++++++++++++ gpui/src/platform/test.rs | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 62225d66e8343a4587abe7690a212227f9c3927f..05c03811feb0013660dd7f89c291aa104ec33ca9 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -92,6 +92,7 @@ pub trait Window: WindowContext { pub trait WindowContext { fn size(&self) -> Vector2F; fn scale_factor(&self) -> f32; + fn titlebar_height(&self) -> f32; fn present_scene(&mut self, scene: Scene); } diff --git a/gpui/src/platform/mac/window.rs b/gpui/src/platform/mac/window.rs index 56969e80892f34266d72ec8cd166825f3a61a658..f12fe38ce6bd0184f46eb6f147e55f540c8231dd 100644 --- a/gpui/src/platform/mac/window.rs +++ b/gpui/src/platform/mac/window.rs @@ -15,6 +15,7 @@ use cocoa::{ foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString}, quartzcore::AutoresizingMask, }; +use core_graphics::display::CGRect; use ctor::ctor; use foreign_types::ForeignType as _; use objc::{ @@ -336,6 +337,10 @@ impl platform::WindowContext for Window { fn present_scene(&mut self, scene: Scene) { self.0.as_ref().borrow_mut().present_scene(scene); } + + fn titlebar_height(&self) -> f32 { + self.0.as_ref().borrow().titlebar_height() + } } impl platform::WindowContext for WindowState { @@ -352,6 +357,14 @@ impl platform::WindowContext for WindowState { } } + fn titlebar_height(&self) -> f32 { + unsafe { + let frame = NSWindow::frame(self.native_window); + let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect]; + (frame.size.height - content_layout_rect.size.height) as f32 + } + } + fn present_scene(&mut self, scene: Scene) { self.scene_to_render = Some(scene); unsafe { diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 77e23d5aa6773c5790545f04c47dca4291f22b40..86b1153424bc7bb9937ec66cd9047a44aa8f1cf1 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -164,6 +164,10 @@ impl super::WindowContext for Window { self.scale_factor } + fn titlebar_height(&self) -> f32 { + 24. + } + fn present_scene(&mut self, scene: crate::Scene) { self.current_scene = Some(scene); } From 149fb574c7e9e44f88741b4d1d4348d58eb9c034 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 6 Aug 2021 09:11:16 -0600 Subject: [PATCH 009/204] Expose titlebar_height in LayoutContext --- gpui/src/app.rs | 8 +++++++- gpui/src/presenter.rs | 7 +++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 8df38af0d47300d37bef7b1a6206dfbb95fd9c69..8d626d2d626af2c8ea7153826c3a4d9efcbabd45 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -1041,6 +1041,7 @@ impl MutableAppContext { app.update(|cx| { let scene = presenter.borrow_mut().build_scene( window.size(), + window.titlebar_height(), window.scale_factor(), cx, ); @@ -1197,7 +1198,12 @@ impl MutableAppContext { { let mut presenter = presenter.borrow_mut(); presenter.invalidate(invalidation, self.as_ref()); - let scene = presenter.build_scene(window.size(), window.scale_factor(), self); + let scene = presenter.build_scene( + window.size(), + window.titlebar_height(), + window.scale_factor(), + self, + ); window.present_scene(scene); } self.presenters_and_platform_windows diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 844dc0502432dc2c96fabca95a92aac36ac202dd..05aa2acc46c906c02fd8fc8d6374d84e30b0fe6b 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -70,13 +70,14 @@ impl Presenter { pub fn build_scene( &mut self, window_size: Vector2F, + titlebar_height: f32, scale_factor: f32, cx: &mut MutableAppContext, ) -> Scene { let mut scene = Scene::new(scale_factor); if let Some(root_view_id) = cx.root_view_id(self.window_id) { - self.layout(window_size, cx); + self.layout(window_size, titlebar_height, cx); self.after_layout(cx); let mut paint_cx = PaintContext { scene: &mut scene, @@ -98,7 +99,7 @@ impl Presenter { scene } - fn layout(&mut self, size: Vector2F, cx: &mut MutableAppContext) { + fn layout(&mut self, size: Vector2F, titlebar_height: f32, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { let mut layout_ctx = LayoutContext { rendered_views: &mut self.rendered_views, @@ -108,6 +109,7 @@ impl Presenter { asset_cache: &self.asset_cache, view_stack: Vec::new(), app: cx, + titlebar_height, }; layout_ctx.layout(root_view_id, SizeConstraint::strict(size)); } @@ -186,6 +188,7 @@ pub struct LayoutContext<'a> { pub asset_cache: &'a AssetCache, pub app: &'a mut MutableAppContext, view_stack: Vec, + pub titlebar_height: f32, } impl<'a> LayoutContext<'a> { From 4a32bd6bb0c4e519009ef2a15688166854e9b825 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 6 Aug 2021 13:43:06 -0700 Subject: [PATCH 010/204] Implement initial RPC endpoints for chat Co-Authored-By: Nathan Sobo --- Cargo.lock | 2 + server/Cargo.toml | 3 +- server/src/auth.rs | 6 +- server/src/db.rs | 125 +++++++++++++++++++++----------- server/src/rpc.rs | 165 +++++++++++++++++++++++++++++++++++++------ server/src/tests.rs | 30 ++++++-- zrpc/proto/zed.proto | 8 +-- 7 files changed, 258 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a24b7b81edf21c93cbd8cd71fa934dc8daf79f50..817a0e7c917c857773e2ce2e105e1d133c3ebe02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4699,6 +4699,7 @@ dependencies = [ "sqlx-rt 0.5.5", "stringprep", "thiserror", + "time 0.2.25", "url", "webpki", "webpki-roots", @@ -5866,6 +5867,7 @@ dependencies = [ "surf", "tide", "tide-compress", + "time 0.2.25", "toml 0.5.8", "zed", "zrpc", diff --git a/server/Cargo.toml b/server/Cargo.toml index 6d26f66054912768dc708b1d56ccbe8a25791614..aad43e5b6e25805ef53d06a85823f66ede415c17 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -31,6 +31,7 @@ sha-1 = "0.9" surf = "2.2.0" tide = "0.16.0" tide-compress = "0.9.0" +time = "0.2" toml = "0.5.8" zrpc = { path = "../zrpc" } @@ -41,7 +42,7 @@ default-features = false [dependencies.sqlx] version = "0.5.2" -features = ["runtime-async-std-rustls", "postgres"] +features = ["runtime-async-std-rustls", "postgres", "time"] [dev-dependencies] gpui = { path = "../gpui" } diff --git a/server/src/auth.rs b/server/src/auth.rs index 9dde8212ff2a848257dfafc2512443b4a0218c33..d61428fa371a0f26bded6a42250aab37a1e1d9f5 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -257,7 +257,7 @@ async fn get_auth_callback(mut request: Request) -> tide::Result { // When signing in from the native app, generate a new access token for the current user. Return // a redirect so that the user's browser sends this access token to the locally-running app. if let Some((user, app_sign_in_params)) = user.zip(query.native_app_sign_in_params) { - let access_token = create_access_token(request.db(), user.id()).await?; + let access_token = create_access_token(request.db(), user.id).await?; let native_app_public_key = zed_auth::PublicKey::try_from(app_sign_in_params.native_app_public_key.clone()) .context("failed to parse app public key")?; @@ -267,9 +267,7 @@ async fn get_auth_callback(mut request: Request) -> tide::Result { return Ok(tide::Redirect::new(&format!( "http://127.0.0.1:{}?user_id={}&access_token={}", - app_sign_in_params.native_app_port, - user.id().0, - encrypted_access_token, + app_sign_in_params.native_app_port, user.id.0, encrypted_access_token, )) .into()); } diff --git a/server/src/db.rs b/server/src/db.rs index 300c8de6d5526039150f63d3e0b296487be68e9f..de196766e103ea42d4124cf4f01e652028554387 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -1,5 +1,6 @@ use serde::Serialize; use sqlx::{FromRow, Result}; +use time::OffsetDateTime; pub use async_sqlx_session::PostgresSessionStore as SessionStore; pub use sqlx::postgres::PgPoolOptions as DbOptions; @@ -8,14 +9,14 @@ pub struct Db(pub sqlx::PgPool); #[derive(Debug, FromRow, Serialize)] pub struct User { - id: i32, + pub id: UserId, pub github_login: String, pub admin: bool, } #[derive(Debug, FromRow, Serialize)] pub struct Signup { - id: i32, + pub id: SignupId, pub github_login: String, pub email_address: String, pub about: String, @@ -23,33 +24,18 @@ pub struct Signup { #[derive(Debug, FromRow, Serialize)] pub struct Channel { - id: i32, + pub id: ChannelId, pub name: String, } #[derive(Debug, FromRow)] pub struct ChannelMessage { - id: i32, - sender_id: i32, - body: String, - sent_at: i64, + pub id: MessageId, + pub sender_id: UserId, + pub body: String, + pub sent_at: OffsetDateTime, } -#[derive(Clone, Copy)] -pub struct UserId(pub i32); - -#[derive(Clone, Copy)] -pub struct OrgId(pub i32); - -#[derive(Clone, Copy)] -pub struct ChannelId(pub i32); - -#[derive(Clone, Copy)] -pub struct SignupId(pub i32); - -#[derive(Clone, Copy)] -pub struct MessageId(pub i32); - impl Db { // signups @@ -108,6 +94,33 @@ impl Db { sqlx::query_as(query).fetch_all(&self.0).await } + pub async fn get_users_by_ids( + &self, + requester_id: UserId, + ids: impl Iterator, + ) -> Result> { + // Only return users that are in a common channel with the requesting user. + let query = " + SELECT users.* + FROM + users, channel_memberships + WHERE + users.id IN $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 + ) + "; + + sqlx::query_as(query) + .bind(&ids.map(|id| id.0).collect::>()) + .bind(requester_id) + .fetch_all(&self.0) + .await + } + pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { let query = "SELECT * FROM users WHERE github_login = $1 LIMIT 1"; sqlx::query_as(query) @@ -147,7 +160,7 @@ impl Db { VALUES ($1, $2) "; sqlx::query(query) - .bind(user_id.0 as i32) + .bind(user_id.0) .bind(access_token_hash) .execute(&self.0) .await @@ -156,8 +169,8 @@ impl Db { pub async fn get_access_token_hashes(&self, user_id: UserId) -> Result> { let query = "SELECT hash FROM access_tokens WHERE user_id = $1"; - sqlx::query_scalar::<_, String>(query) - .bind(user_id.0 as i32) + sqlx::query_scalar(query) + .bind(user_id.0) .fetch_all(&self.0) .await } @@ -180,14 +193,20 @@ impl Db { } #[cfg(test)] - pub async fn add_org_member(&self, org_id: OrgId, user_id: UserId) -> Result<()> { + pub async fn add_org_member( + &self, + org_id: OrgId, + user_id: UserId, + is_admin: bool, + ) -> Result<()> { let query = " - INSERT INTO org_memberships (org_id, user_id) - VALUES ($1, $2) + INSERT INTO org_memberships (org_id, user_id, admin) + VALUES ($1, $2, $3) "; sqlx::query(query) .bind(org_id.0) .bind(user_id.0) + .bind(is_admin) .execute(&self.0) .await .map(drop) @@ -272,16 +291,18 @@ impl Db { channel_id: ChannelId, sender_id: UserId, body: &str, + timestamp: OffsetDateTime, ) -> Result { let query = " INSERT INTO channel_messages (channel_id, sender_id, body, sent_at) - VALUES ($1, $2, $3, NOW()::timestamp) + VALUES ($1, $2, $3, $4) RETURNING id "; sqlx::query_scalar(query) .bind(channel_id.0) .bind(sender_id.0) .bind(body) + .bind(timestamp) .fetch_one(&self.0) .await .map(MessageId) @@ -292,12 +313,15 @@ impl Db { channel_id: ChannelId, count: usize, ) -> Result> { - let query = " - SELECT id, sender_id, body, sent_at - FROM channel_messages - WHERE channel_id = $1 + let query = r#" + SELECT + id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at + FROM + channel_messages + WHERE + channel_id = $1 LIMIT $2 - "; + "#; sqlx::query_as(query) .bind(channel_id.0) .bind(count as i64) @@ -314,14 +338,29 @@ impl std::ops::Deref for Db { } } -impl Channel { - pub fn id(&self) -> ChannelId { - ChannelId(self.id) - } +macro_rules! id_type { + ($name:ident) => { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, sqlx::Type, Serialize)] + #[sqlx(transparent)] + #[serde(transparent)] + pub struct $name(pub i32); + + impl $name { + #[allow(unused)] + pub fn from_proto(value: u64) -> Self { + Self(value as i32) + } + + #[allow(unused)] + pub fn to_proto(&self) -> u64 { + self.0 as u64 + } + } + }; } -impl User { - pub fn id(&self) -> UserId { - UserId(self.id) - } -} +id_type!(UserId); +id_type!(OrgId); +id_type!(ChannelId); +id_type!(SignupId); +id_type!(MessageId); diff --git a/server/src/rpc.rs b/server/src/rpc.rs index f1ebf605a25fd2bab1aa01883f1decd2ca68d2b2..8696f0369130ef71b0f5e1f2ab9c9c7ed931091d 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -23,6 +23,7 @@ use tide::{ http::headers::{HeaderName, CONNECTION, UPGRADE}, Request, Response, }; +use time::OffsetDateTime; use zrpc::{ auth::random_token, proto::{self, EnvelopedMessage}, @@ -33,17 +34,19 @@ type ReplicaId = u16; #[derive(Default)] pub struct State { - connections: HashMap, - pub worktrees: HashMap, + connections: HashMap, + pub worktrees: HashMap, + channels: HashMap, next_worktree_id: u64, } -struct ConnectionState { +struct Connection { user_id: UserId, worktrees: HashSet, + channels: HashSet, } -pub struct WorktreeState { +pub struct Worktree { host_connection_id: Option, guest_connection_ids: HashMap, active_replica_ids: HashSet, @@ -52,7 +55,12 @@ pub struct WorktreeState { entries: HashMap, } -impl WorktreeState { +#[derive(Default)] +struct Channel { + connection_ids: HashSet, +} + +impl Worktree { pub fn connection_ids(&self) -> Vec { self.guest_connection_ids .keys() @@ -68,14 +76,21 @@ impl WorktreeState { } } +impl Channel { + fn connection_ids(&self) -> Vec { + self.connection_ids.iter().copied().collect() + } +} + impl State { // Add a new connection associated with a given user. pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId) { self.connections.insert( connection_id, - ConnectionState { + Connection { user_id, worktrees: Default::default(), + channels: Default::default(), }, ); } @@ -83,8 +98,13 @@ impl State { // Remove the given connection and its association with any worktrees. pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Vec { let mut worktree_ids = Vec::new(); - if let Some(connection_state) = self.connections.remove(&connection_id) { - for worktree_id in connection_state.worktrees { + if let Some(connection) = self.connections.remove(&connection_id) { + for channel_id in connection.channels { + if let Some(channel) = self.channels.get_mut(&channel_id) { + channel.connection_ids.remove(&connection_id); + } + } + for worktree_id in connection.worktrees { if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { if worktree.host_connection_id == Some(connection_id) { worktree_ids.push(worktree_id); @@ -100,28 +120,39 @@ impl State { worktree_ids } + fn join_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) { + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.channels.insert(channel_id); + self.channels + .entry(channel_id) + .or_default() + .connection_ids + .insert(connection_id); + } + } + // Add the given connection as a guest of the given worktree pub fn join_worktree( &mut self, connection_id: ConnectionId, worktree_id: u64, access_token: &str, - ) -> Option<(ReplicaId, &WorktreeState)> { - if let Some(worktree_state) = self.worktrees.get_mut(&worktree_id) { - if access_token == worktree_state.access_token { - if let Some(connection_state) = self.connections.get_mut(&connection_id) { - connection_state.worktrees.insert(worktree_id); + ) -> Option<(ReplicaId, &Worktree)> { + if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { + if access_token == worktree.access_token { + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.worktrees.insert(worktree_id); } let mut replica_id = 1; - while worktree_state.active_replica_ids.contains(&replica_id) { + while worktree.active_replica_ids.contains(&replica_id) { replica_id += 1; } - worktree_state.active_replica_ids.insert(replica_id); - worktree_state + worktree.active_replica_ids.insert(replica_id); + worktree .guest_connection_ids .insert(connection_id, replica_id); - Some((replica_id, worktree_state)) + Some((replica_id, worktree)) } else { None } @@ -142,7 +173,7 @@ impl State { &self, worktree_id: u64, connection_id: ConnectionId, - ) -> tide::Result<&WorktreeState> { + ) -> tide::Result<&Worktree> { let worktree = self .worktrees .get(&worktree_id) @@ -165,7 +196,7 @@ impl State { &mut self, worktree_id: u64, connection_id: ConnectionId, - ) -> tide::Result<&mut WorktreeState> { + ) -> tide::Result<&mut Worktree> { let worktree = self .worktrees .get_mut(&worktree_id) @@ -263,7 +294,9 @@ pub fn add_rpc_routes(router: &mut Router, state: &Arc, rpc: &Arc>, rpc: &Arc) { @@ -373,7 +406,7 @@ async fn share_worktree( .collect(); state.worktrees.insert( worktree_id, - WorktreeState { + Worktree { host_connection_id: Some(request.sender_id), guest_connection_ids: Default::default(), active_replica_ids: Default::default(), @@ -627,7 +660,7 @@ async fn get_channels( channels: channels .into_iter() .map(|chan| proto::Channel { - id: chan.id().0 as u64, + id: chan.id.to_proto(), name: chan.name, }) .collect(), @@ -637,6 +670,34 @@ async fn get_channels( Ok(()) } +async fn get_users( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + let user_id = state + .rpc + .read() + .await + .user_id_for_connection(request.sender_id)?; + let receipt = request.receipt(); + let user_ids = request.payload.user_ids.into_iter().map(UserId::from_proto); + let users = state + .db + .get_users_by_ids(user_id, user_ids) + .await? + .into_iter() + .map(|user| proto::User { + id: user.id.to_proto(), + github_login: user.github_login, + avatar_url: String::new(), + }) + .collect(); + rpc.respond(receipt, proto::GetUsersResponse { users }) + .await?; + Ok(()) +} + async fn join_channel( request: TypedEnvelope, rpc: &Arc, @@ -647,14 +708,74 @@ async fn join_channel( .read() .await .user_id_for_connection(request.sender_id)?; + let channel_id = ChannelId::from_proto(request.payload.channel_id); if !state .db - .can_user_access_channel(user_id, ChannelId(request.payload.channel_id as i32)) + .can_user_access_channel(user_id, channel_id) .await? { Err(anyhow!("access denied"))?; } + state + .rpc + .write() + .await + .join_channel(request.sender_id, channel_id); + let messages = state + .db + .get_recent_channel_messages(channel_id, 50) + .await? + .into_iter() + .map(|msg| proto::ChannelMessage { + id: msg.id.to_proto(), + body: msg.body, + timestamp: msg.sent_at.unix_timestamp() as u64, + sender_id: msg.sender_id.to_proto(), + }) + .collect(); + rpc.respond(request.receipt(), proto::JoinChannelResponse { messages }) + .await?; + Ok(()) +} + +async fn send_channel_message( + request: TypedEnvelope, + peer: &Arc, + app: &Arc, +) -> tide::Result<()> { + let channel_id = ChannelId::from_proto(request.payload.channel_id); + let user_id; + let connection_ids; + { + let state = app.rpc.read().await; + user_id = state.user_id_for_connection(request.sender_id)?; + if let Some(channel) = state.channels.get(&channel_id) { + connection_ids = channel.connection_ids(); + } else { + return Ok(()); + } + } + + let timestamp = OffsetDateTime::now_utc(); + let message_id = app + .db + .create_channel_message(channel_id, user_id, &request.payload.body, timestamp) + .await?; + let message = proto::ChannelMessageSent { + channel_id: channel_id.to_proto(), + message: Some(proto::ChannelMessage { + sender_id: user_id.to_proto(), + id: message_id.to_proto(), + body: request.payload.body, + timestamp: timestamp.unix_timestamp() as u64, + }), + }; + broadcast(request.sender_id, connection_ids, |conn_id| { + peer.send(conn_id, message.clone()) + }) + .await?; + Ok(()) } diff --git a/server/src/tests.rs b/server/src/tests.rs index 653e2ae59a16bf6e376e7a2e14b11de43af6e7b4..86980e8673a01020efdcbea7719689c36d2a37cd 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -11,9 +11,10 @@ use rand::prelude::*; use serde_json::json; use sqlx::{ migrate::{MigrateDatabase, Migrator}, + types::time::OffsetDateTime, Executor as _, Postgres, }; -use std::{path::Path, sync::Arc}; +use std::{path::Path, sync::Arc, time::SystemTime}; use zed::{ editor::Editor, fs::{FakeFs, Fs as _}, @@ -485,10 +486,15 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { 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_a, "user_b").await; - // Create a channel that includes these 2 users and 1 other user. + // Create an org that includes these 2 users and 1 other user. let db = &server.app_state.db; let user_id_c = db.create_user("user_c", false).await.unwrap(); 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, user_id_c, 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) .await @@ -499,11 +505,21 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { db.add_channel_member(channel_id, user_id_c, false) .await .unwrap(); - db.create_channel_message(channel_id, user_id_c, "first message!") - .await - .unwrap(); - - // let chatroom_a = ChatRoom:: + db.create_channel_message( + channel_id, + user_id_c, + "first message!", + OffsetDateTime::now_utc(), + ) + .await + .unwrap(); + assert_eq!( + db.get_recent_channel_messages(channel_id, 50) + .await + .unwrap()[0] + .body, + "first message!" + ); } struct TestServer { diff --git a/zrpc/proto/zed.proto b/zrpc/proto/zed.proto index 65360ae8853f329c0e208d0cf0e956ff221d44f9..7d8e3ff7425b30cd573ceb3ee510833bdddf1a70 100644 --- a/zrpc/proto/zed.proto +++ b/zrpc/proto/zed.proto @@ -24,10 +24,10 @@ message Envelope { RemovePeer remove_peer = 19; GetChannels get_channels = 20; GetChannelsResponse get_channels_response = 21; - JoinChannel join_channel = 22; - JoinChannelResponse join_channel_response = 23; - GetUsers get_users = 24; - GetUsersResponse get_users_response = 25; + GetUsers get_users = 22; + GetUsersResponse get_users_response = 23; + JoinChannel join_channel = 24; + JoinChannelResponse join_channel_response = 25; SendChannelMessage send_channel_message = 26; ChannelMessageSent channel_message_sent = 27; } From ff822c9158a56e23cc8d9b9a813d4649ef424516 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 6 Aug 2021 14:47:18 -0700 Subject: [PATCH 011/204] wip --- server/src/tests.rs | 19 ++++++++++++------- zed/src/channel.rs | 31 +++++++++++++++++++++++++++++++ zed/src/lib.rs | 1 + zed/src/rpc.rs | 1 + 4 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 zed/src/channel.rs diff --git a/server/src/tests.rs b/server/src/tests.rs index 86980e8673a01020efdcbea7719689c36d2a37cd..257bdfb8d8633f567921836026602555fafee73c 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -513,13 +513,18 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { ) .await .unwrap(); - assert_eq!( - db.get_recent_channel_messages(channel_id, 50) - .await - .unwrap()[0] - .body, - "first message!" - ); + + let channels_a = client_a.get_channels().await; + assert_eq!(channels_a.len(), 1); + assert_eq!(channels_a[0].read(&cx_a).name(), "test-channel"); + + // assert_eq!( + // db.get_recent_channel_messages(channel_id, 50) + // .await + // .unwrap()[0] + // .body, + // "first message!" + // ); } struct TestServer { diff --git a/zed/src/channel.rs b/zed/src/channel.rs new file mode 100644 index 0000000000000000000000000000000000000000..c846999e8ece2d2e1cadc92742e9f055dfb2bc2a --- /dev/null +++ b/zed/src/channel.rs @@ -0,0 +1,31 @@ +use crate::rpc::Client; +use gpui::{Entity, ModelHandle, WeakModelHandle}; +use std::{ + collections::{HashMap, VecDeque}, + sync::Arc, +}; + +pub struct ChannelList { + channels: HashMap>, + rpc: Arc, +} + +pub struct Channel { + id: u64, + name: String, + first_message_id: Option, + messages: Option>, + rpc: Arc, +} + +pub struct ChannelMessage { + id: u64, +} + +enum Event {} + +impl Entity for ChannelList { + type Event = Event; +} + +impl ChannelList {} diff --git a/zed/src/lib.rs b/zed/src/lib.rs index e59fb3fd67a246e4f3f32aba96ad1b38f6b3a80e..f283584c0e1966fe2206aa367c88ca3094b61ad5 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -1,4 +1,5 @@ pub mod assets; +pub mod channel; pub mod editor; pub mod file_finder; pub mod fs; diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index f455376f2092b99d314089eb6df0f6f2e850352e..7c1640ce82c809ba90a092cfd233d8713813ee53 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -29,6 +29,7 @@ pub struct Client { pub struct ClientState { connection_id: Option, pub shared_worktrees: HashMap>, + pub channel_list: Option>, pub languages: Arc, } From 5b599a32b83e7729d3a6585f0b755fd9a02f4eab Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 6 Aug 2021 16:06:03 -0600 Subject: [PATCH 012/204] WIP --- zed/src/channel.rs | 53 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index c846999e8ece2d2e1cadc92742e9f055dfb2bc2a..2ec8b8c2acfab903137693aae8a04b2c0ff7bfd4 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,18 +1,26 @@ -use crate::rpc::Client; -use gpui::{Entity, ModelHandle, WeakModelHandle}; +use crate::rpc::{self, Client}; +use anyhow::{anyhow, Result}; +use gpui::{ + AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle, +}; use std::{ collections::{HashMap, VecDeque}, sync::Arc, }; +use zrpc::{proto::ChannelMessageSent, ForegroundRouter, Router, TypedEnvelope}; pub struct ChannelList { + available_channels: Vec, channels: HashMap>, rpc: Arc, } -pub struct Channel { +pub struct ChannelDetails { id: u64, name: String, +} +pub struct Channel { + details: ChannelDetails, first_message_id: Option, messages: Option>, rpc: Arc, @@ -21,11 +29,46 @@ pub struct Channel { pub struct ChannelMessage { id: u64, } - enum Event {} impl Entity for ChannelList { type Event = Event; } -impl ChannelList {} +impl ChannelList { + fn new( + rpc: Arc, + router: &mut ForegroundRouter, + cx: &mut ModelContext, + ) -> Self { + // Subscribe to messages. + let this = cx.handle().downgrade(); + rpc.on_message( + router, + |envelope, rpc, cx: &mut AsyncAppContext| async move { + cx.update(|cx| { + if let Some(this) = this.upgrade(cx) { + this.update(cx, |this, cx| this.receive_message(envelope, cx)) + } else { + Err(anyhow!("can't upgrade ChannelList handle")) + } + }) + }, + cx, + ); + + Self { + available_channels: Default::default(), + channels: Default::default(), + rpc, + } + } + + fn receive_message( + &mut self, + envelope: TypedEnvelope, + cx: &mut ModelContext, + ) -> Result<()> { + Ok(()) + } +} From 541f58e12ce5e0d360d666c7366e95f406afb06b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 18 Aug 2021 11:45:29 -0600 Subject: [PATCH 013/204] Start on subscribing to messages in channel entity instances Co-Authored-By: Max Brunsfeld Co-Authored-By: Antonio Scandurra --- gpui/src/app.rs | 66 ++++++++++++++++++++++++++++++++++++++++------ zed/src/channel.rs | 63 +++++++++++++++++++++++++++++++++---------- zed/src/rpc.rs | 6 ++++- zrpc/src/peer.rs | 18 +++++++++---- zrpc/src/proto.rs | 2 +- 5 files changed, 126 insertions(+), 29 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 579bf5c4682dba2fcfbe15d48166396f3a9ea81d..a2fa8188246bed0c3b2dab51a266b48926502d2c 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -70,6 +70,11 @@ pub trait UpdateModel { F: FnOnce(&mut T, &mut ModelContext) -> S; } +pub trait UpgradeModelHandle { + fn upgrade_model_handle(&self, handle: WeakModelHandle) + -> Option>; +} + pub trait ReadView { fn read_view(&self, handle: &ViewHandle) -> &T; } @@ -457,6 +462,15 @@ impl UpdateModel for AsyncAppContext { } } +impl UpgradeModelHandle for AsyncAppContext { + fn upgrade_model_handle( + &self, + handle: WeakModelHandle, + ) -> Option> { + self.0.borrow_mut().upgrade_model_handle(handle) + } +} + impl ReadModelWith for AsyncAppContext { fn read_model_with T, T>( &self, @@ -1434,6 +1448,15 @@ impl UpdateModel for MutableAppContext { } } +impl UpgradeModelHandle for MutableAppContext { + fn upgrade_model_handle( + &self, + handle: WeakModelHandle, + ) -> Option> { + self.cx.upgrade_model_handle(handle) + } +} + impl ReadView for MutableAppContext { fn read_view(&self, handle: &ViewHandle) -> &T { if let Some(view) = self.cx.views.get(&(handle.window_id, handle.view_id)) { @@ -1565,6 +1588,19 @@ impl ReadModel for AppContext { } } +impl UpgradeModelHandle for AppContext { + fn upgrade_model_handle( + &self, + handle: WeakModelHandle, + ) -> Option> { + if self.models.contains_key(&handle.model_id) { + Some(ModelHandle::new(handle.model_id, &self.ref_counts)) + } else { + None + } + } +} + impl ReadView for AppContext { fn read_view(&self, handle: &ViewHandle) -> &T { if let Some(view) = self.views.get(&(handle.window_id, handle.view_id)) { @@ -1858,6 +1894,15 @@ impl UpdateModel for ModelContext<'_, M> { } } +impl UpgradeModelHandle for ModelContext<'_, M> { + fn upgrade_model_handle( + &self, + handle: WeakModelHandle, + ) -> Option> { + self.cx.upgrade_model_handle(handle) + } +} + impl Deref for ModelContext<'_, M> { type Target = MutableAppContext; @@ -2175,6 +2220,15 @@ impl ReadModel for ViewContext<'_, V> { } } +impl UpgradeModelHandle for ViewContext<'_, V> { + fn upgrade_model_handle( + &self, + handle: WeakModelHandle, + ) -> Option> { + self.cx.upgrade_model_handle(handle) + } +} + impl UpdateModel for ViewContext<'_, V> { fn update_model(&mut self, handle: &ModelHandle, update: F) -> S where @@ -2372,7 +2426,6 @@ impl Handle for ModelHandle { EntityLocation::Model(self.model_id) } } - pub struct WeakModelHandle { model_id: usize, model_type: PhantomData, @@ -2386,13 +2439,8 @@ impl WeakModelHandle { } } - pub fn upgrade(&self, cx: impl AsRef) -> Option> { - let cx = cx.as_ref(); - if cx.models.contains_key(&self.model_id) { - Some(ModelHandle::new(self.model_id, &cx.ref_counts)) - } else { - None - } + pub fn upgrade(self, cx: &impl UpgradeModelHandle) -> Option> { + cx.upgrade_model_handle(self) } } @@ -2419,6 +2467,8 @@ impl Clone for WeakModelHandle { } } +impl Copy for WeakModelHandle {} + pub struct ViewHandle { window_id: usize, view_id: usize, diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 2ec8b8c2acfab903137693aae8a04b2c0ff7bfd4..0554f2b1838e11dca3f9f5185c699360059f499d 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,7 +1,8 @@ use crate::rpc::{self, Client}; use anyhow::{anyhow, Result}; +use futures::StreamExt; use gpui::{ - AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle, + AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle, }; use std::{ collections::{HashMap, VecDeque}, @@ -24,6 +25,7 @@ pub struct Channel { first_message_id: Option, messages: Option>, rpc: Arc, + _receive_messages: Task<()>, } pub struct ChannelMessage { @@ -43,19 +45,20 @@ impl ChannelList { ) -> Self { // Subscribe to messages. let this = cx.handle().downgrade(); - rpc.on_message( - router, - |envelope, rpc, cx: &mut AsyncAppContext| async move { - cx.update(|cx| { - if let Some(this) = this.upgrade(cx) { - this.update(cx, |this, cx| this.receive_message(envelope, cx)) - } else { - Err(anyhow!("can't upgrade ChannelList handle")) - } - }) - }, - cx, - ); + + // rpc.on_message( + // router, + // |envelope, rpc, cx: &mut AsyncAppContext| async move { + // cx.update(|cx| { + // if let Some(this) = this.upgrade(cx) { + // this.update(cx, |this, cx| this.receive_message(envelope, cx)) + // } else { + // Err(anyhow!("can't upgrade ChannelList handle")) + // } + // }) + // }, + // cx, + // ); Self { available_channels: Default::default(), @@ -72,3 +75,35 @@ impl ChannelList { Ok(()) } } + +impl Entity for Channel { + type Event = (); +} + +impl Channel { + pub fn new(details: ChannelDetails, rpc: Arc, cx: &mut ModelContext) -> Self { + let messages = rpc.subscribe(); + let receive_messages = cx.spawn_weak(|this, cx| async move { + while let Some(message) = messages.next().await { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.message_received(&message, cx)); + } + } + }); + + Self { + details, + rpc, + first_message_id: None, + messages: None, + _receive_messages: receive_messages, + } + } + + fn message_received( + &mut self, + message: &TypedEnvelope, + cx: &mut ModelContext, + ) { + } +} diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 7c1640ce82c809ba90a092cfd233d8713813ee53..c987e34b1657cfb2fe23e0af54b75b7a9792078b 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -2,6 +2,7 @@ use crate::{language::LanguageRegistry, worktree::Worktree}; use anyhow::{anyhow, Context, Result}; use async_tungstenite::tungstenite::http::Request; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; +use futures::Stream; use gpui::{AsyncAppContext, ModelHandle, Task, WeakModelHandle}; use lazy_static::lazy_static; use smol::lock::RwLock; @@ -29,7 +30,6 @@ pub struct Client { pub struct ClientState { connection_id: Option, pub shared_worktrees: HashMap>, - pub channel_list: Option>, pub languages: Arc, } @@ -82,6 +82,10 @@ impl Client { }); } + pub fn subscribe(&self) -> impl Stream>> { + self.peer.subscribe() + } + pub async fn log_in_and_connect( &self, router: Arc, diff --git a/zrpc/src/peer.rs b/zrpc/src/peer.rs index 2093d6a73215a5463502b28935a258ddb3de1751..37a8bc3efbb90a93e9d5d3f2d9b683a9545ca162 100644 --- a/zrpc/src/peer.rs +++ b/zrpc/src/peer.rs @@ -3,15 +3,15 @@ use anyhow::{anyhow, Context, Result}; use async_lock::{Mutex, RwLock}; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; use futures::{ - future::{BoxFuture, LocalBoxFuture}, - FutureExt, StreamExt, + future::{self, BoxFuture, LocalBoxFuture}, + FutureExt, Stream, StreamExt, }; use postage::{ - mpsc, - prelude::{Sink, Stream}, + broadcast, mpsc, + prelude::{Sink as _, Stream as _}, }; use std::{ - any::TypeId, + any::{Any, TypeId}, collections::{HashMap, HashSet}, fmt, future::Future, @@ -77,6 +77,7 @@ pub struct RouterInternal { pub struct Peer { connections: RwLock>, next_connection_id: AtomicU32, + incoming_messages: broadcast::Sender>, } #[derive(Clone)] @@ -91,6 +92,7 @@ impl Peer { Arc::new(Self { connections: Default::default(), next_connection_id: Default::default(), + incoming_messages: broadcast::channel(256).0, }) } @@ -189,6 +191,12 @@ impl Peer { self.connections.write().await.clear(); } + pub fn subscribe(&self) -> impl Stream>> { + self.incoming_messages + .subscribe() + .filter_map(|envelope| future::ready(Arc::downcast(envelope).ok())) + } + pub fn request( self: &Arc, receiver_id: ConnectionId, diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index 77390cbb175b062aa2ae5b8b6cc74e0dcda74172..9d44b4da384d89a29ed1be827cd214f981f87889 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -8,7 +8,7 @@ use std::{ include!(concat!(env!("OUT_DIR"), "/zed.messages.rs")); -pub trait EnvelopedMessage: Clone + Sized + Send + 'static { +pub trait EnvelopedMessage: Clone + Sized + Send + Sync + 'static { const NAME: &'static str; fn into_envelope( self, From ef421d735d4f5eb69ea0128e5c0934c4e5bcd087 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 18 Aug 2021 13:12:27 -0600 Subject: [PATCH 014/204] Implement broadcast of typed envelopes This required a rework of the macro so that we can always construct a typed envelope from our list of available message types from incoming protobuf envelopes. Co-Authored-By: Max Brunsfeld --- zed/src/channel.rs | 44 +++--------------- zed/src/worktree.rs | 18 +++---- zrpc/src/peer.rs | 17 +++++-- zrpc/src/proto.rs | 111 ++++++++++++++++++++++++++++++++++---------- 4 files changed, 113 insertions(+), 77 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 0554f2b1838e11dca3f9f5185c699360059f499d..9ce1f7a97cd201e88a517a65193a966a6b669665 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,14 +1,11 @@ use crate::rpc::{self, Client}; -use anyhow::{anyhow, Result}; use futures::StreamExt; -use gpui::{ - AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle, -}; +use gpui::{Entity, ModelContext, Task, WeakModelHandle}; use std::{ collections::{HashMap, VecDeque}, sync::Arc, }; -use zrpc::{proto::ChannelMessageSent, ForegroundRouter, Router, TypedEnvelope}; +use zrpc::{proto::ChannelMessageSent, TypedEnvelope}; pub struct ChannelList { available_channels: Vec, @@ -31,49 +28,20 @@ pub struct Channel { pub struct ChannelMessage { id: u64, } -enum Event {} +pub enum Event {} impl Entity for ChannelList { type Event = Event; } impl ChannelList { - fn new( - rpc: Arc, - router: &mut ForegroundRouter, - cx: &mut ModelContext, - ) -> Self { - // Subscribe to messages. - let this = cx.handle().downgrade(); - - // rpc.on_message( - // router, - // |envelope, rpc, cx: &mut AsyncAppContext| async move { - // cx.update(|cx| { - // if let Some(this) = this.upgrade(cx) { - // this.update(cx, |this, cx| this.receive_message(envelope, cx)) - // } else { - // Err(anyhow!("can't upgrade ChannelList handle")) - // } - // }) - // }, - // cx, - // ); - + fn new(rpc: Arc) -> Self { Self { available_channels: Default::default(), channels: Default::default(), rpc, } } - - fn receive_message( - &mut self, - envelope: TypedEnvelope, - cx: &mut ModelContext, - ) -> Result<()> { - Ok(()) - } } impl Entity for Channel { @@ -82,8 +50,8 @@ impl Entity for Channel { impl Channel { pub fn new(details: ChannelDetails, rpc: Arc, cx: &mut ModelContext) -> Self { - let messages = rpc.subscribe(); - let receive_messages = cx.spawn_weak(|this, cx| async move { + let mut messages = rpc.subscribe(); + let receive_messages = cx.spawn_weak(|this, mut cx| async move { while let Some(message) = messages.next().await { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| this.message_received(&message, cx)); diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index f8566cd8c40b67fcca79d6401f1476058fbea620..1bfe2cd1a2e0819bcecc8df974276438dc19c486 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -18,7 +18,7 @@ use futures::{Stream, StreamExt}; pub use fuzzy::{match_paths, PathMatch}; use gpui::{ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, - Task, WeakModelHandle, + Task, UpgradeModelHandle, WeakModelHandle, }; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -373,7 +373,7 @@ impl Worktree { let buffer = worktree .open_buffers .get(&buffer_id) - .and_then(|buf| buf.upgrade(&cx)) + .and_then(|buf| buf.upgrade(cx)) .ok_or_else(|| { anyhow!("invalid buffer {} in update buffer message", buffer_id) })?; @@ -382,7 +382,7 @@ impl Worktree { Worktree::Remote(worktree) => match worktree.open_buffers.get_mut(&buffer_id) { Some(RemoteBuffer::Operations(pending_ops)) => pending_ops.extend(ops), Some(RemoteBuffer::Loaded(buffer)) => { - if let Some(buffer) = buffer.upgrade(&cx) { + if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))?; } else { worktree @@ -410,7 +410,7 @@ impl Worktree { if let Some(buffer) = worktree .open_buffers .get(&(message.buffer_id as usize)) - .and_then(|buf| buf.upgrade(&cx)) + .and_then(|buf| buf.upgrade(cx)) { buffer.update(cx, |buffer, cx| { let version = message.version.try_into()?; @@ -480,7 +480,7 @@ impl Worktree { let mut buffers_to_delete = Vec::new(); for (buffer_id, buffer) in open_buffers { - if let Some(buffer) = buffer.upgrade(&cx) { + if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| { let buffer_is_clean = !buffer.is_dirty(); @@ -633,7 +633,7 @@ impl LocalWorktree { cx.spawn_weak(|this, mut cx| async move { while let Ok(scan_state) = scan_states_rx.recv().await { - if let Some(handle) = cx.read(|cx| this.upgrade(&cx)) { + if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { let to_send = handle.update(&mut cx, |this, cx| { last_scan_state_tx.blocking_send(scan_state).ok(); this.poll_snapshot(cx); @@ -778,7 +778,7 @@ impl LocalWorktree { .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?; self.shared_buffers.remove(&peer_id); for (_, buffer) in &self.open_buffers { - if let Some(buffer) = buffer.upgrade(&cx) { + if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } @@ -1078,7 +1078,7 @@ impl RemoteWorktree { .remove(&peer_id) .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?; for (_, buffer) in &self.open_buffers { - if let Some(buffer) = buffer.upgrade(&cx) { + if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } @@ -1093,7 +1093,7 @@ enum RemoteBuffer { } impl RemoteBuffer { - fn upgrade(&self, cx: impl AsRef) -> Option> { + fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option> { match self { Self::Operations(_) => None, Self::Loaded(buffer) => buffer.upgrade(cx), diff --git a/zrpc/src/peer.rs b/zrpc/src/peer.rs index 37a8bc3efbb90a93e9d5d3f2d9b683a9545ca162..ef3dd1d9564c31f067a327980798af642ff60cbc 100644 --- a/zrpc/src/peer.rs +++ b/zrpc/src/peer.rs @@ -45,7 +45,7 @@ pub struct Receipt { pub struct TypedEnvelope { pub sender_id: ConnectionId, - original_sender_id: Option, + pub original_sender_id: Option, pub message_id: u32, pub payload: T, } @@ -158,18 +158,25 @@ impl Peer { } }; + let mut broadcast_incoming_messages = self.incoming_messages.clone(); let response_channels = connection.response_channels.clone(); let handle_messages = async move { - while let Some(message) = incoming_rx.recv().await { - if let Some(responding_to) = message.responding_to { + while let Some(envelope) = incoming_rx.recv().await { + if let Some(responding_to) = envelope.responding_to { let channel = response_channels.lock().await.remove(&responding_to); if let Some(mut tx) = channel { - tx.send(message).await.ok(); + tx.send(envelope).await.ok(); } else { log::warn!("received RPC response to unknown request {}", responding_to); } } else { - router.handle(connection_id, message).await; + router.handle(connection_id, envelope.clone()).await; + match proto::build_typed_envelope(connection_id, envelope) { + Ok(envelope) => { + broadcast_incoming_messages.send(envelope).await.ok(); + } + Err(error) => log::error!("{}", error), + } } } response_channels.lock().await.clear(); diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index 9d44b4da384d89a29ed1be827cd214f981f87889..7d4458f1d713796c26150d20052d4da2a793a0d3 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -1,6 +1,10 @@ +use super::{ConnectionId, PeerId, TypedEnvelope}; +use anyhow::{anyhow, Result}; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; use futures::{SinkExt as _, StreamExt as _}; use prost::Message; +use std::any::Any; +use std::sync::Arc; use std::{ io, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -24,6 +28,59 @@ pub trait RequestMessage: EnvelopedMessage { type Response: EnvelopedMessage; } +macro_rules! messages { + ($($name:ident),*) => { + fn unicast_message_into_typed_envelope(sender_id: ConnectionId, envelope: &mut Envelope) -> Option> { + match &mut envelope.payload { + $(payload @ Some(envelope::Payload::$name(_)) => Some(Arc::new(TypedEnvelope { + sender_id, + original_sender_id: envelope.original_sender_id.map(PeerId), + message_id: envelope.id, + payload: payload.take().unwrap(), + })), )* + _ => None + } + } + + $( + message!($name); + )* + }; +} + +macro_rules! request_messages { + ($(($request_name:ident, $response_name:ident)),*) => { + fn request_message_into_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { + match envelope.payload { + $( + Some(envelope::Payload::$request_name(payload)) => Some(Arc::new(TypedEnvelope { + sender_id, + original_sender_id: envelope.original_sender_id.map(PeerId), + message_id: envelope.id, + payload, + })), + Some(envelope::Payload::$response_name(payload)) => Some(Arc::new(TypedEnvelope { + sender_id, + original_sender_id: envelope.original_sender_id.map(PeerId), + message_id: envelope.id, + payload, + })), + )* + _ => None + } + } + + $( + message!($request_name); + message!($response_name); + )* + + $(impl RequestMessage for $request_name { + type Response = $response_name; + })* + }; +} + macro_rules! message { ($name:ident) => { impl EnvelopedMessage for $name { @@ -58,33 +115,37 @@ macro_rules! message { }; } -macro_rules! request_message { - ($req:ident, $resp:ident) => { - message!($req); - message!($resp); - impl RequestMessage for $req { - type Response = $resp; - } - }; +messages!( + UpdateWorktree, + CloseWorktree, + CloseBuffer, + UpdateBuffer, + AddPeer, + RemovePeer, + SendChannelMessage, + ChannelMessageSent +); + +request_messages!( + (Auth, AuthResponse), + (ShareWorktree, ShareWorktreeResponse), + (OpenWorktree, OpenWorktreeResponse), + (OpenBuffer, OpenBufferResponse), + (SaveBuffer, BufferSaved), + (GetChannels, GetChannelsResponse), + (JoinChannel, JoinChannelResponse), + (GetUsers, GetUsersResponse) +); + +pub fn build_typed_envelope( + sender_id: ConnectionId, + mut envelope: Envelope, +) -> Result> { + unicast_message_into_typed_envelope(sender_id, &mut envelope) + .or_else(|| request_message_into_typed_envelope(sender_id, envelope)) + .ok_or_else(|| anyhow!("unrecognized payload type")) } -request_message!(Auth, AuthResponse); -request_message!(ShareWorktree, ShareWorktreeResponse); -request_message!(OpenWorktree, OpenWorktreeResponse); -message!(UpdateWorktree); -message!(CloseWorktree); -request_message!(OpenBuffer, OpenBufferResponse); -message!(CloseBuffer); -message!(UpdateBuffer); -request_message!(SaveBuffer, BufferSaved); -message!(AddPeer); -message!(RemovePeer); -request_message!(GetChannels, GetChannelsResponse); -request_message!(JoinChannel, JoinChannelResponse); -request_message!(GetUsers, GetUsersResponse); -message!(SendChannelMessage); -message!(ChannelMessageSent); - /// A stream of protobuf messages. pub struct MessageStream { stream: S, From 37f24d10ece3693c6ece2674f484fb9e2687963c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 18 Aug 2021 19:06:46 -0600 Subject: [PATCH 015/204] Subscribe to worktree messages at the entity level Co-Authored-By: Max Brunsfeld --- server/src/tests.rs | 2 +- zed/src/channel.rs | 21 ++-- zed/src/rpc.rs | 33 ++++- zed/src/util.rs | 12 ++ zed/src/worktree.rs | 293 +++++++++++++++++++++++++++++++++----------- zrpc/src/peer.rs | 9 +- zrpc/src/proto.rs | 168 +++++++++++++------------ 7 files changed, 364 insertions(+), 174 deletions(-) diff --git a/server/src/tests.rs b/server/src/tests.rs index 257bdfb8d8633f567921836026602555fafee73c..cce311c5f3242f97837fe289439f268347751eaf 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -14,7 +14,7 @@ use sqlx::{ types::time::OffsetDateTime, Executor as _, Postgres, }; -use std::{path::Path, sync::Arc, time::SystemTime}; +use std::{path::Path, sync::Arc}; use zed::{ editor::Editor, fs::{FakeFs, Fs as _}, diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 9ce1f7a97cd201e88a517a65193a966a6b669665..2aa2a966eea928ed1bc835de45385d7daed0fdd9 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,5 +1,5 @@ use crate::rpc::{self, Client}; -use futures::StreamExt; +use anyhow::Result; use gpui::{Entity, ModelContext, Task, WeakModelHandle}; use std::{ collections::{HashMap, VecDeque}, @@ -22,7 +22,7 @@ pub struct Channel { first_message_id: Option, messages: Option>, rpc: Arc, - _receive_messages: Task<()>, + _message_handler: Task<()>, } pub struct ChannelMessage { @@ -50,28 +50,23 @@ impl Entity for Channel { impl Channel { pub fn new(details: ChannelDetails, rpc: Arc, cx: &mut ModelContext) -> Self { - let mut messages = rpc.subscribe(); - let receive_messages = cx.spawn_weak(|this, mut cx| async move { - while let Some(message) = messages.next().await { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.message_received(&message, cx)); - } - } - }); + let _message_handler = rpc.subscribe_from_model(details.id, cx, Self::handle_message_sent); Self { details, rpc, first_message_id: None, messages: None, - _receive_messages: receive_messages, + _message_handler, } } - fn message_received( + fn handle_message_sent( &mut self, message: &TypedEnvelope, + rpc: rpc::Client, cx: &mut ModelContext, - ) { + ) -> Result<()> { + Ok(()) } } diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index c987e34b1657cfb2fe23e0af54b75b7a9792078b..07c6cb6fb1a018cf7ae5f06f12d49cefa2edd197 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -2,14 +2,15 @@ use crate::{language::LanguageRegistry, worktree::Worktree}; use anyhow::{anyhow, Context, Result}; use async_tungstenite::tungstenite::http::Request; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; -use futures::Stream; -use gpui::{AsyncAppContext, ModelHandle, Task, WeakModelHandle}; +use futures::StreamExt; +use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use lazy_static::lazy_static; use smol::lock::RwLock; use std::collections::HashMap; use std::time::Duration; use std::{convert::TryFrom, future::Future, sync::Arc}; use surf::Url; +use zrpc::proto::EntityMessage; pub use zrpc::{proto, ConnectionId, PeerId, TypedEnvelope}; use zrpc::{ proto::{EnvelopedMessage, RequestMessage}, @@ -82,8 +83,32 @@ impl Client { }); } - pub fn subscribe(&self) -> impl Stream>> { - self.peer.subscribe() + pub fn subscribe_from_model( + &self, + remote_id: u64, + cx: &mut ModelContext, + mut handler: F, + ) -> Task<()> + where + T: EntityMessage, + M: Entity, + F: 'static + FnMut(&mut M, &TypedEnvelope, Client, &mut ModelContext) -> Result<()>, + { + let rpc = self.clone(); + let mut incoming = self.peer.subscribe::(); + cx.spawn_weak(|model, mut cx| async move { + while let Some(envelope) = incoming.next().await { + if envelope.payload.remote_entity_id() == remote_id { + if let Some(model) = model.upgrade(&cx) { + model.update(&mut cx, |model, cx| { + if let Err(error) = handler(model, &envelope, rpc.clone(), cx) { + log::error!("error handling message: {}", error) + } + }); + } + } + } + }) } pub async fn log_in_and_connect( diff --git a/zed/src/util.rs b/zed/src/util.rs index dbbb1c45987b57e72fcca630dee7f44f48b5c75d..ea9b544f9a3f9961b1efd8f077690f19aece6fdb 100644 --- a/zed/src/util.rs +++ b/zed/src/util.rs @@ -1,3 +1,4 @@ +use futures::Future; use rand::prelude::*; use std::cmp::Ordering; @@ -81,6 +82,17 @@ impl Iterator for RandomCharIter { } } +pub async fn log_async_errors(f: F) -> impl Future +where + F: Future>, +{ + async { + if let Err(error) = f.await { + log::error!("{}", error) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 1bfe2cd1a2e0819bcecc8df974276438dc19c486..a483a06647d2a741e2c56dcd4ccf268d8a831508 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -10,7 +10,7 @@ use crate::{ rpc::{self, proto}, sum_tree::{self, Cursor, Edit, SumTree}, time::{self, ReplicaId}, - util::Bias, + util::{log_async_errors, Bias}, }; use ::ignore::gitignore::Gitignore; use anyhow::{anyhow, Result}; @@ -76,7 +76,10 @@ impl Entity for Worktree { fn release(&mut self, cx: &mut MutableAppContext) { let rpc = match self { - Self::Local(tree) => tree.rpc.clone(), + Self::Local(tree) => tree + .share + .as_ref() + .map(|share| (share.rpc.clone(), share.remote_id)), Self::Remote(tree) => Some((tree.rpc.clone(), tree.remote_id)), }; @@ -226,6 +229,14 @@ impl Worktree { .detach(); } + let _message_handlers = vec![ + rpc.subscribe_from_model(remote_id, cx, Self::handle_add_peer), + rpc.subscribe_from_model(remote_id, cx, Self::handle_remove_peer), + rpc.subscribe_from_model(remote_id, cx, Self::handle_update), + rpc.subscribe_from_model(remote_id, cx, Self::handle_update_buffer), + rpc.subscribe_from_model(remote_id, cx, Self::handle_buffer_saved), + ]; + Worktree::Remote(RemoteWorktree { remote_id, replica_id, @@ -239,6 +250,7 @@ impl Worktree { .map(|p| (PeerId(p.peer_id), p.replica_id as ReplicaId)) .collect(), languages, + _message_handlers, }) }) }); @@ -289,10 +301,11 @@ impl Worktree { } } - pub fn add_peer( + pub fn handle_add_peer( &mut self, - envelope: TypedEnvelope, - cx: &mut ModelContext, + envelope: &TypedEnvelope, + _: rpc::Client, + cx: &mut ModelContext, ) -> Result<()> { match self { Worktree::Local(worktree) => worktree.add_peer(envelope, cx), @@ -300,10 +313,11 @@ impl Worktree { } } - pub fn remove_peer( + pub fn handle_remove_peer( &mut self, - envelope: TypedEnvelope, - cx: &mut ModelContext, + envelope: &TypedEnvelope, + _: rpc::Client, + cx: &mut ModelContext, ) -> Result<()> { match self { Worktree::Local(worktree) => worktree.remove_peer(envelope, cx), @@ -311,6 +325,51 @@ impl Worktree { } } + pub fn handle_update( + &mut self, + envelope: &TypedEnvelope, + _: rpc::Client, + cx: &mut ModelContext, + ) -> anyhow::Result<()> { + self.as_remote_mut() + .unwrap() + .update_from_remote(envelope, cx) + } + + pub fn handle_open_buffer( + &mut self, + envelope: &TypedEnvelope, + rpc: rpc::Client, + cx: &mut ModelContext, + ) -> anyhow::Result<()> { + let receipt = envelope.receipt(); + + let response = self + .as_local_mut() + .unwrap() + .open_remote_buffer(envelope, cx); + + cx.background() + .spawn(log_async_errors(async move { + rpc.respond(receipt, response.await?).await?; + Ok(()) + })) + .detach(); + + Ok(()) + } + + pub fn handle_close_buffer( + &mut self, + envelope: &TypedEnvelope, + _: rpc::Client, + cx: &mut ModelContext, + ) -> anyhow::Result<()> { + self.as_local_mut() + .unwrap() + .close_remote_buffer(envelope, cx) + } + pub fn peers(&self) -> &HashMap { match self { Worktree::Local(worktree) => &worktree.peers, @@ -356,13 +415,15 @@ impl Worktree { .is_some() } - pub fn update_buffer( + pub fn handle_update_buffer( &mut self, - envelope: proto::UpdateBuffer, + envelope: &TypedEnvelope, + _: rpc::Client, cx: &mut ModelContext, ) -> Result<()> { - let buffer_id = envelope.buffer_id as usize; - let ops = envelope + let payload = envelope.payload.clone(); + let buffer_id = payload.buffer_id as usize; + let ops = payload .operations .into_iter() .map(|op| op.try_into()) @@ -401,34 +462,72 @@ impl Worktree { Ok(()) } - pub fn buffer_saved( + pub fn handle_save_buffer( &mut self, - message: proto::BufferSaved, + envelope: &TypedEnvelope, + rpc: rpc::Client, cx: &mut ModelContext, ) -> Result<()> { - if let Worktree::Remote(worktree) = self { - if let Some(buffer) = worktree - .open_buffers - .get(&(message.buffer_id as usize)) - .and_then(|buf| buf.upgrade(cx)) - { - buffer.update(cx, |buffer, cx| { - let version = message.version.try_into()?; - let mtime = message - .mtime - .ok_or_else(|| anyhow!("missing mtime"))? - .into(); - buffer.did_save(version, mtime, cx); - Result::<_, anyhow::Error>::Ok(()) - })?; - } - Ok(()) - } else { - Err(anyhow!( - "invalid buffer {} in buffer saved message", - message.buffer_id - )) + let sender_id = envelope.original_sender_id()?; + let buffer = self + .as_local() + .unwrap() + .shared_buffers + .get(&sender_id) + .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; + + let receipt = envelope.receipt(); + let worktree_id = envelope.payload.worktree_id; + let buffer_id = envelope.payload.buffer_id; + let save = buffer.update(cx, |buffer, cx| buffer.save(cx))?; + + cx.background() + .spawn(log_async_errors(async move { + let (version, mtime) = save.await?; + + rpc.respond( + receipt, + proto::BufferSaved { + worktree_id, + buffer_id, + version: (&version).into(), + mtime: Some(mtime.into()), + }, + ) + .await?; + + Ok(()) + })) + .detach(); + + Ok(()) + } + + pub fn handle_buffer_saved( + &mut self, + envelope: &TypedEnvelope, + _: rpc::Client, + cx: &mut ModelContext, + ) -> Result<()> { + let payload = envelope.payload.clone(); + let worktree = self.as_remote_mut().unwrap(); + if let Some(buffer) = worktree + .open_buffers + .get(&(payload.buffer_id as usize)) + .and_then(|buf| buf.upgrade(cx)) + { + buffer.update(cx, |buffer, cx| { + let version = payload.version.try_into()?; + let mtime = payload + .mtime + .ok_or_else(|| anyhow!("missing mtime"))? + .into(); + buffer.did_save(version, mtime, cx); + Result::<_, anyhow::Error>::Ok(()) + })?; } + Ok(()) } fn poll_snapshot(&mut self, cx: &mut ModelContext) { @@ -561,11 +660,10 @@ impl Deref for Worktree { pub struct LocalWorktree { snapshot: Snapshot, background_snapshot: Arc>, - snapshots_to_send_tx: Option>, last_scan_state_rx: watch::Receiver, _background_scanner_task: Option>, poll_task: Option>, - rpc: Option<(rpc::Client, u64)>, + share: Option, open_buffers: HashMap>, shared_buffers: HashMap>>, peers: HashMap, @@ -619,14 +717,13 @@ impl LocalWorktree { let tree = Self { snapshot: snapshot.clone(), background_snapshot: Arc::new(Mutex::new(snapshot)), - snapshots_to_send_tx: None, last_scan_state_rx, _background_scanner_task: None, + share: None, poll_task: None, open_buffers: Default::default(), shared_buffers: Default::default(), peers: Default::default(), - rpc: None, languages, fs, }; @@ -639,10 +736,8 @@ impl LocalWorktree { this.poll_snapshot(cx); let tree = this.as_local_mut().unwrap(); if !tree.is_scanning() { - if let Some(snapshots_to_send_tx) = - tree.snapshots_to_send_tx.clone() - { - Some((tree.snapshot(), snapshots_to_send_tx)) + if let Some(share) = tree.share.as_ref() { + Some((tree.snapshot(), share.snapshots_tx.clone())) } else { None } @@ -717,7 +812,7 @@ impl LocalWorktree { pub fn open_remote_buffer( &mut self, - envelope: TypedEnvelope, + envelope: &TypedEnvelope, cx: &mut ModelContext, ) -> Task> { let peer_id = envelope.original_sender_id(); @@ -744,7 +839,7 @@ impl LocalWorktree { pub fn close_remote_buffer( &mut self, - envelope: TypedEnvelope, + envelope: &TypedEnvelope, _: &mut ModelContext, ) -> Result<()> { if let Some(shared_buffers) = self.shared_buffers.get_mut(&envelope.original_sender_id()?) { @@ -756,19 +851,24 @@ impl LocalWorktree { pub fn add_peer( &mut self, - envelope: TypedEnvelope, + envelope: &TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { - let peer = envelope.payload.peer.ok_or_else(|| anyhow!("empty peer"))?; + let peer = envelope + .payload + .peer + .as_ref() + .ok_or_else(|| anyhow!("empty peer"))?; self.peers .insert(PeerId(peer.peer_id), peer.replica_id as ReplicaId); cx.notify(); + Ok(()) } pub fn remove_peer( &mut self, - envelope: TypedEnvelope, + envelope: &TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { let peer_id = PeerId(envelope.payload.peer_id); @@ -783,6 +883,7 @@ impl LocalWorktree { } } cx.notify(); + Ok(()) } @@ -892,6 +993,7 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { let share_request = share_request.await; let share_response = rpc.request(share_request).await?; + let remote_id = share_response.worktree_id; rpc.state .write() @@ -906,11 +1008,10 @@ impl LocalWorktree { cx.background() .spawn({ let rpc = rpc.clone(); - let worktree_id = share_response.worktree_id; async move { let mut prev_snapshot = snapshot; while let Ok(snapshot) = snapshots_to_send_rx.recv().await { - let message = snapshot.build_update(&prev_snapshot, worktree_id); + let message = snapshot.build_update(&prev_snapshot, remote_id); match rpc.send(message).await { Ok(()) => prev_snapshot = snapshot, Err(err) => log::error!("error sending snapshot diff {}", err), @@ -920,13 +1021,26 @@ impl LocalWorktree { }) .detach(); - this.update(&mut cx, |worktree, _| { + this.update(&mut cx, |worktree, cx| { + let _message_handlers = vec![ + rpc.subscribe_from_model(remote_id, cx, Worktree::handle_add_peer), + rpc.subscribe_from_model(remote_id, cx, Worktree::handle_remove_peer), + rpc.subscribe_from_model(remote_id, cx, Worktree::handle_open_buffer), + rpc.subscribe_from_model(remote_id, cx, Worktree::handle_close_buffer), + rpc.subscribe_from_model(remote_id, cx, Worktree::handle_update_buffer), + rpc.subscribe_from_model(remote_id, cx, Worktree::handle_save_buffer), + ]; + let worktree = worktree.as_local_mut().unwrap(); - worktree.rpc = Some((rpc, share_response.worktree_id)); - worktree.snapshots_to_send_tx = Some(snapshots_to_send_tx); + worktree.share = Some(ShareState { + rpc, + remote_id: share_response.worktree_id, + snapshots_tx: snapshots_to_send_tx, + _message_handlers, + }); }); - Ok((share_response.worktree_id, share_response.access_token)) + Ok((remote_id, share_response.access_token)) }) } @@ -978,6 +1092,13 @@ impl fmt::Debug for LocalWorktree { } } +struct ShareState { + rpc: rpc::Client, + remote_id: u64, + snapshots_tx: Sender, + _message_handlers: Vec>, +} + pub struct RemoteWorktree { remote_id: u64, snapshot: Snapshot, @@ -988,6 +1109,7 @@ pub struct RemoteWorktree { open_buffers: HashMap, peers: HashMap, languages: Arc, + _message_handlers: Vec>, } impl RemoteWorktree { @@ -1055,12 +1177,32 @@ impl RemoteWorktree { self.snapshot.clone() } + fn update_from_remote( + &mut self, + envelope: &TypedEnvelope, + cx: &mut ModelContext, + ) -> Result<()> { + let mut tx = self.updates_tx.clone(); + let payload = envelope.payload.clone(); + cx.background() + .spawn(async move { + tx.send(payload).await.expect("receiver runs to completion"); + }) + .detach(); + + Ok(()) + } + pub fn add_peer( &mut self, - envelope: TypedEnvelope, + envelope: &TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { - let peer = envelope.payload.peer.ok_or_else(|| anyhow!("empty peer"))?; + let peer = envelope + .payload + .peer + .as_ref() + .ok_or_else(|| anyhow!("empty peer"))?; self.peers .insert(PeerId(peer.peer_id), peer.replica_id as ReplicaId); cx.notify(); @@ -1069,7 +1211,7 @@ impl RemoteWorktree { pub fn remove_peer( &mut self, - envelope: TypedEnvelope, + envelope: &TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { let peer_id = PeerId(envelope.payload.peer_id); @@ -1420,7 +1562,10 @@ impl File { pub fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) { self.worktree.update(cx, |worktree, cx| { if let Some((rpc, remote_id)) = match worktree { - Worktree::Local(worktree) => worktree.rpc.clone(), + Worktree::Local(worktree) => worktree + .share + .as_ref() + .map(|share| (share.rpc.clone(), share.remote_id)), Worktree::Remote(worktree) => Some((worktree.rpc.clone(), worktree.remote_id)), } { cx.spawn(|_, _| async move { @@ -1497,9 +1642,12 @@ impl File { ) -> Task> { self.worktree.update(cx, |worktree, cx| match worktree { Worktree::Local(worktree) => { - let rpc = worktree.rpc.clone(); + let rpc = worktree + .share + .as_ref() + .map(|share| (share.rpc.clone(), share.remote_id)); let save = worktree.save(self.path.clone(), text, cx); - cx.spawn(|_, _| async move { + cx.background().spawn(async move { let entry = save.await?; if let Some((rpc, worktree_id)) = rpc { rpc.send(proto::BufferSaved { @@ -1516,7 +1664,7 @@ impl File { Worktree::Remote(worktree) => { let rpc = worktree.rpc.clone(); let worktree_id = worktree.remote_id; - cx.spawn(|_, _| async move { + cx.background().spawn(async move { let response = rpc .request(proto::SaveBuffer { worktree_id, @@ -2395,7 +2543,9 @@ mod remote { .read() .await .shared_worktree(envelope.payload.worktree_id, cx)? - .update(cx, |worktree, cx| worktree.add_peer(envelope, cx)) + .update(cx, |worktree, cx| { + worktree.handle_add_peer(&envelope, rpc.clone(), cx) + }) } pub async fn remove_peer( @@ -2407,7 +2557,9 @@ mod remote { .read() .await .shared_worktree(envelope.payload.worktree_id, cx)? - .update(cx, |worktree, cx| worktree.remove_peer(envelope, cx)) + .update(cx, |worktree, cx| { + worktree.handle_remove_peer(&envelope, rpc.clone(), cx) + }) } pub async fn update_worktree( @@ -2456,7 +2608,7 @@ mod remote { worktree .as_local_mut() .unwrap() - .open_remote_buffer(envelope, cx) + .open_remote_buffer(&envelope, cx) }) .await?; @@ -2480,7 +2632,7 @@ mod remote { worktree .as_local_mut() .unwrap() - .close_remote_buffer(envelope, cx) + .close_remote_buffer(&envelope, cx) }) } @@ -2489,12 +2641,13 @@ mod remote { rpc: &rpc::Client, cx: &mut AsyncAppContext, ) -> anyhow::Result<()> { - let message = envelope.payload; rpc.state .read() .await - .shared_worktree(message.worktree_id, cx)? - .update(cx, |tree, cx| tree.update_buffer(message, cx))?; + .shared_worktree(envelope.payload.worktree_id, cx)? + .update(cx, |tree, cx| { + tree.handle_update_buffer(&envelope, rpc.clone(), cx) + })?; Ok(()) } @@ -2538,7 +2691,7 @@ mod remote { .await .shared_worktree(envelope.payload.worktree_id, cx)? .update(cx, |worktree, cx| { - worktree.buffer_saved(envelope.payload, cx) + worktree.handle_buffer_saved(&envelope, rpc.clone(), cx) })?; Ok(()) } diff --git a/zrpc/src/peer.rs b/zrpc/src/peer.rs index ef3dd1d9564c31f067a327980798af642ff60cbc..c377ad1309ccc0e6eae58457769ecaa49c520a54 100644 --- a/zrpc/src/peer.rs +++ b/zrpc/src/peer.rs @@ -171,11 +171,10 @@ impl Peer { } } else { router.handle(connection_id, envelope.clone()).await; - match proto::build_typed_envelope(connection_id, envelope) { - Ok(envelope) => { - broadcast_incoming_messages.send(envelope).await.ok(); - } - Err(error) => log::error!("{}", error), + if let Some(envelope) = proto::build_typed_envelope(connection_id, envelope) { + broadcast_incoming_messages.send(envelope).await.ok(); + } else { + log::error!("unable to construct a typed envelope"); } } } diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index 7d4458f1d713796c26150d20052d4da2a793a0d3..1c799ebe21d544af919c30beb60d20fa2a15c291 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -1,5 +1,5 @@ use super::{ConnectionId, PeerId, TypedEnvelope}; -use anyhow::{anyhow, Result}; +use anyhow::Result; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; use futures::{SinkExt as _, StreamExt as _}; use prost::Message; @@ -24,127 +24,133 @@ pub trait EnvelopedMessage: Clone + Sized + Send + Sync + 'static { fn from_envelope(envelope: Envelope) -> Option; } +pub trait EntityMessage: EnvelopedMessage { + fn remote_entity_id(&self) -> u64; +} + pub trait RequestMessage: EnvelopedMessage { type Response: EnvelopedMessage; } macro_rules! messages { - ($($name:ident),*) => { - fn unicast_message_into_typed_envelope(sender_id: ConnectionId, envelope: &mut Envelope) -> Option> { - match &mut envelope.payload { - $(payload @ Some(envelope::Payload::$name(_)) => Some(Arc::new(TypedEnvelope { + ($($name:ident),* $(,)?) => { + pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { + match envelope.payload { + $(Some(envelope::Payload::$name(payload)) => Some(Arc::new(TypedEnvelope { sender_id, original_sender_id: envelope.original_sender_id.map(PeerId), message_id: envelope.id, - payload: payload.take().unwrap(), + payload, })), )* _ => None } } $( - message!($name); + impl EnvelopedMessage for $name { + const NAME: &'static str = std::stringify!($name); + + fn into_envelope( + self, + id: u32, + responding_to: Option, + original_sender_id: Option, + ) -> Envelope { + Envelope { + id, + responding_to, + original_sender_id, + payload: Some(envelope::Payload::$name(self)), + } + } + + fn matches_envelope(envelope: &Envelope) -> bool { + matches!(&envelope.payload, Some(envelope::Payload::$name(_))) + } + + fn from_envelope(envelope: Envelope) -> Option { + if let Some(envelope::Payload::$name(msg)) = envelope.payload { + Some(msg) + } else { + None + } + } + } )* }; } macro_rules! request_messages { - ($(($request_name:ident, $response_name:ident)),*) => { - fn request_message_into_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { - match envelope.payload { - $( - Some(envelope::Payload::$request_name(payload)) => Some(Arc::new(TypedEnvelope { - sender_id, - original_sender_id: envelope.original_sender_id.map(PeerId), - message_id: envelope.id, - payload, - })), - Some(envelope::Payload::$response_name(payload)) => Some(Arc::new(TypedEnvelope { - sender_id, - original_sender_id: envelope.original_sender_id.map(PeerId), - message_id: envelope.id, - payload, - })), - )* - _ => None - } - } - - $( - message!($request_name); - message!($response_name); - )* - + ($(($request_name:ident, $response_name:ident)),* $(,)?) => { $(impl RequestMessage for $request_name { type Response = $response_name; })* }; } -macro_rules! message { - ($name:ident) => { - impl EnvelopedMessage for $name { - const NAME: &'static str = std::stringify!($name); - - fn into_envelope( - self, - id: u32, - responding_to: Option, - original_sender_id: Option, - ) -> Envelope { - Envelope { - id, - responding_to, - original_sender_id, - payload: Some(envelope::Payload::$name(self)), - } - } - - fn matches_envelope(envelope: &Envelope) -> bool { - matches!(&envelope.payload, Some(envelope::Payload::$name(_))) - } - - fn from_envelope(envelope: Envelope) -> Option { - if let Some(envelope::Payload::$name(msg)) = envelope.payload { - Some(msg) - } else { - None - } +macro_rules! entity_messages { + ($id_field:ident, $($name:ident),* $(,)?) => { + $(impl EntityMessage for $name { + fn remote_entity_id(&self) -> u64 { + self.$id_field } - } + })* }; } messages!( - UpdateWorktree, - CloseWorktree, - CloseBuffer, - UpdateBuffer, AddPeer, + Auth, + AuthResponse, + BufferSaved, + ChannelMessageSent, + CloseBuffer, + CloseWorktree, + GetChannels, + GetChannelsResponse, + GetUsers, + GetUsersResponse, + JoinChannel, + JoinChannelResponse, + OpenBuffer, + OpenBufferResponse, + OpenWorktree, + OpenWorktreeResponse, RemovePeer, + SaveBuffer, SendChannelMessage, - ChannelMessageSent + ShareWorktree, + ShareWorktreeResponse, + UpdateBuffer, + UpdateWorktree, ); request_messages!( (Auth, AuthResponse), - (ShareWorktree, ShareWorktreeResponse), - (OpenWorktree, OpenWorktreeResponse), - (OpenBuffer, OpenBufferResponse), - (SaveBuffer, BufferSaved), (GetChannels, GetChannelsResponse), + (GetUsers, GetUsersResponse), (JoinChannel, JoinChannelResponse), - (GetUsers, GetUsersResponse) + (OpenBuffer, OpenBufferResponse), + (OpenWorktree, OpenWorktreeResponse), + (SaveBuffer, BufferSaved), + (ShareWorktree, ShareWorktreeResponse), ); -pub fn build_typed_envelope( - sender_id: ConnectionId, - mut envelope: Envelope, -) -> Result> { - unicast_message_into_typed_envelope(sender_id, &mut envelope) - .or_else(|| request_message_into_typed_envelope(sender_id, envelope)) - .ok_or_else(|| anyhow!("unrecognized payload type")) -} +entity_messages!( + worktree_id, + AddPeer, + BufferSaved, + CloseBuffer, + CloseWorktree, + OpenBuffer, + OpenWorktree, + RemovePeer, + SaveBuffer, + UpdateBuffer, + UpdateWorktree, +); + +entity_messages!(channel_id, ChannelMessageSent); /// A stream of protobuf messages. pub struct MessageStream { From 3f5db7284d3f391fcc95fe464591206cf13146f3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 18 Aug 2021 19:16:40 -0600 Subject: [PATCH 016/204] Remove shared_worktrees map from ClientState Each worktree instance now handles its own messages. --- zed/src/main.rs | 8 +- zed/src/rpc.rs | 25 +----- zed/src/worktree.rs | 196 +------------------------------------------- 3 files changed, 4 insertions(+), 225 deletions(-) diff --git a/zed/src/main.rs b/zed/src/main.rs index b488dbe95b5c9947fec80bc76e430203657dcd52..a831893614ebfd4891ab656d60313d16eac30f34 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -11,7 +11,6 @@ use zed::{ fs::RealFs, language, menus, rpc, settings, theme_selector, workspace::{self, OpenParams}, - worktree::{self}, AppState, }; use zrpc::ForegroundRouter; @@ -27,7 +26,7 @@ fn main() { let languages = Arc::new(language::LanguageRegistry::new()); languages.set_theme(&settings.borrow().theme); - let mut app_state = AppState { + let app_state = AppState { languages: languages.clone(), settings_tx: Arc::new(Mutex::new(settings_tx)), settings, @@ -38,11 +37,6 @@ fn main() { }; app.run(move |cx| { - worktree::init( - cx, - &app_state.rpc, - Arc::get_mut(&mut app_state.rpc_router).unwrap(), - ); let app_state = Arc::new(app_state); zed::init(cx); diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 07c6cb6fb1a018cf7ae5f06f12d49cefa2edd197..00f5e2dd7446564ce75213b4ce499fa98c0c368e 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -1,12 +1,11 @@ -use crate::{language::LanguageRegistry, worktree::Worktree}; +use crate::language::LanguageRegistry; use anyhow::{anyhow, Context, Result}; use async_tungstenite::tungstenite::http::Request; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; use futures::StreamExt; -use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; +use gpui::{AsyncAppContext, Entity, ModelContext, Task}; use lazy_static::lazy_static; use smol::lock::RwLock; -use std::collections::HashMap; use std::time::Duration; use std::{convert::TryFrom, future::Future, sync::Arc}; use surf::Url; @@ -30,35 +29,15 @@ pub struct Client { pub struct ClientState { connection_id: Option, - pub shared_worktrees: HashMap>, pub languages: Arc, } -impl ClientState { - pub fn shared_worktree( - &self, - id: u64, - cx: &mut AsyncAppContext, - ) -> Result> { - if let Some(worktree) = self.shared_worktrees.get(&id) { - if let Some(worktree) = cx.read(|cx| worktree.upgrade(cx)) { - Ok(worktree) - } else { - Err(anyhow!("worktree {} was dropped", id)) - } - } else { - Err(anyhow!("worktree {} does not exist", id)) - } - } -} - impl Client { pub fn new(languages: Arc) -> Self { Self { peer: Peer::new(), state: Arc::new(RwLock::new(ClientState { connection_id: None, - shared_worktrees: Default::default(), languages, })), } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index a483a06647d2a741e2c56dcd4ccf268d8a831508..cf60eb87cd3346236ec4f7bacfcfd48071589cab 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -42,23 +42,12 @@ use std::{ }, time::{Duration, SystemTime}, }; -use zrpc::{ForegroundRouter, PeerId, TypedEnvelope}; +use zrpc::{PeerId, TypedEnvelope}; lazy_static! { static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); } -pub fn init(cx: &mut MutableAppContext, rpc: &rpc::Client, router: &mut ForegroundRouter) { - rpc.on_message(router, remote::add_peer, cx); - rpc.on_message(router, remote::remove_peer, cx); - rpc.on_message(router, remote::update_worktree, cx); - rpc.on_message(router, remote::open_buffer, cx); - rpc.on_message(router, remote::close_buffer, cx); - rpc.on_message(router, remote::update_buffer, cx); - rpc.on_message(router, remote::buffer_saved, cx); - rpc.on_message(router, remote::save_buffer, cx); -} - #[derive(Clone, Debug)] enum ScanState { Idle, @@ -85,11 +74,6 @@ impl Entity for Worktree { if let Some((rpc, worktree_id)) = rpc { cx.spawn(|_| async move { - rpc.state - .write() - .await - .shared_worktrees - .remove(&worktree_id); if let Err(err) = rpc.send(proto::CloseWorktree { worktree_id }).await { log::error!("error closing worktree {}: {}", worktree_id, err); } @@ -254,11 +238,6 @@ impl Worktree { }) }) }); - rpc.state - .write() - .await - .shared_worktrees - .insert(open_response.worktree_id, worktree.downgrade()); Ok(worktree) } @@ -989,18 +968,11 @@ impl LocalWorktree { ) -> Task> { let snapshot = self.snapshot(); let share_request = self.share_request(cx); - let handle = cx.handle(); cx.spawn(|this, mut cx| async move { let share_request = share_request.await; let share_response = rpc.request(share_request).await?; let remote_id = share_response.worktree_id; - rpc.state - .write() - .await - .shared_worktrees - .insert(share_response.worktree_id, handle.downgrade()); - log::info!("sharing worktree {:?}", share_response); let (snapshots_to_send_tx, snapshots_to_send_rx) = smol::channel::unbounded::(); @@ -2531,172 +2503,6 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { } } -mod remote { - use super::*; - - pub async fn add_peer( - envelope: TypedEnvelope, - rpc: &rpc::Client, - cx: &mut AsyncAppContext, - ) -> anyhow::Result<()> { - rpc.state - .read() - .await - .shared_worktree(envelope.payload.worktree_id, cx)? - .update(cx, |worktree, cx| { - worktree.handle_add_peer(&envelope, rpc.clone(), cx) - }) - } - - pub async fn remove_peer( - envelope: TypedEnvelope, - rpc: &rpc::Client, - cx: &mut AsyncAppContext, - ) -> anyhow::Result<()> { - rpc.state - .read() - .await - .shared_worktree(envelope.payload.worktree_id, cx)? - .update(cx, |worktree, cx| { - worktree.handle_remove_peer(&envelope, rpc.clone(), cx) - }) - } - - pub async fn update_worktree( - envelope: TypedEnvelope, - rpc: &rpc::Client, - cx: &mut AsyncAppContext, - ) -> anyhow::Result<()> { - rpc.state - .read() - .await - .shared_worktree(envelope.payload.worktree_id, cx)? - .update(cx, |worktree, _| { - if let Some(worktree) = worktree.as_remote_mut() { - let mut tx = worktree.updates_tx.clone(); - Ok(async move { - tx.send(envelope.payload) - .await - .expect("receiver runs to completion"); - }) - } else { - Err(anyhow!( - "invalid update message for local worktree {}", - envelope.payload.worktree_id - )) - } - })? - .await; - - Ok(()) - } - - pub async fn open_buffer( - envelope: TypedEnvelope, - rpc: &rpc::Client, - cx: &mut AsyncAppContext, - ) -> anyhow::Result<()> { - let receipt = envelope.receipt(); - let worktree = rpc - .state - .read() - .await - .shared_worktree(envelope.payload.worktree_id, cx)?; - - let response = worktree - .update(cx, |worktree, cx| { - worktree - .as_local_mut() - .unwrap() - .open_remote_buffer(&envelope, cx) - }) - .await?; - - rpc.respond(receipt, response).await?; - - Ok(()) - } - - pub async fn close_buffer( - envelope: TypedEnvelope, - rpc: &rpc::Client, - cx: &mut AsyncAppContext, - ) -> anyhow::Result<()> { - let worktree = rpc - .state - .read() - .await - .shared_worktree(envelope.payload.worktree_id, cx)?; - - worktree.update(cx, |worktree, cx| { - worktree - .as_local_mut() - .unwrap() - .close_remote_buffer(&envelope, cx) - }) - } - - pub async fn update_buffer( - envelope: TypedEnvelope, - rpc: &rpc::Client, - cx: &mut AsyncAppContext, - ) -> anyhow::Result<()> { - rpc.state - .read() - .await - .shared_worktree(envelope.payload.worktree_id, cx)? - .update(cx, |tree, cx| { - tree.handle_update_buffer(&envelope, rpc.clone(), cx) - })?; - Ok(()) - } - - pub async fn save_buffer( - envelope: TypedEnvelope, - rpc: &rpc::Client, - cx: &mut AsyncAppContext, - ) -> anyhow::Result<()> { - let state = rpc.state.read().await; - let worktree = state.shared_worktree(envelope.payload.worktree_id, cx)?; - let sender_id = envelope.original_sender_id()?; - let buffer = worktree.read_with(cx, |tree, _| { - tree.as_local() - .unwrap() - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) - .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) - })?; - let (version, mtime) = buffer.update(cx, |buffer, cx| buffer.save(cx))?.await?; - rpc.respond( - envelope.receipt(), - proto::BufferSaved { - worktree_id: envelope.payload.worktree_id, - buffer_id: envelope.payload.buffer_id, - version: (&version).into(), - mtime: Some(mtime.into()), - }, - ) - .await?; - Ok(()) - } - - pub async fn buffer_saved( - envelope: TypedEnvelope, - rpc: &rpc::Client, - cx: &mut AsyncAppContext, - ) -> anyhow::Result<()> { - rpc.state - .read() - .await - .shared_worktree(envelope.payload.worktree_id, cx)? - .update(cx, |worktree, cx| { - worktree.handle_buffer_saved(&envelope, rpc.clone(), cx) - })?; - Ok(()) - } -} - #[cfg(test)] mod tests { use super::*; From 9336c02867335a0dfc16650708da2b5dfbf87a4e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 18 Aug 2021 21:59:13 -0600 Subject: [PATCH 017/204] Start on a peer2 module with an alternative implementation --- zrpc/proto/zed.proto | 58 +++--- zrpc/src/lib.rs | 1 + zrpc/src/peer.rs | 6 +- zrpc/src/peer2.rs | 470 +++++++++++++++++++++++++++++++++++++++++++ zrpc/src/proto.rs | 20 +- 5 files changed, 520 insertions(+), 35 deletions(-) create mode 100644 zrpc/src/peer2.rs diff --git a/zrpc/proto/zed.proto b/zrpc/proto/zed.proto index 7d8e3ff7425b30cd573ceb3ee510833bdddf1a70..3a0b7aabb6a341cffc58d57e28dd1b0e454fd252 100644 --- a/zrpc/proto/zed.proto +++ b/zrpc/proto/zed.proto @@ -6,35 +6,45 @@ message Envelope { optional uint32 responding_to = 2; optional uint32 original_sender_id = 3; oneof payload { - Auth auth = 4; - AuthResponse auth_response = 5; - ShareWorktree share_worktree = 6; - ShareWorktreeResponse share_worktree_response = 7; - OpenWorktree open_worktree = 8; - OpenWorktreeResponse open_worktree_response = 9; - UpdateWorktree update_worktree = 10; - CloseWorktree close_worktree = 11; - OpenBuffer open_buffer = 12; - OpenBufferResponse open_buffer_response = 13; - CloseBuffer close_buffer = 14; - UpdateBuffer update_buffer = 15; - SaveBuffer save_buffer = 16; - BufferSaved buffer_saved = 17; - AddPeer add_peer = 18; - RemovePeer remove_peer = 19; - GetChannels get_channels = 20; - GetChannelsResponse get_channels_response = 21; - GetUsers get_users = 22; - GetUsersResponse get_users_response = 23; - JoinChannel join_channel = 24; - JoinChannelResponse join_channel_response = 25; - SendChannelMessage send_channel_message = 26; - ChannelMessageSent channel_message_sent = 27; + Ping ping = 4; + Pong pong = 5; + Auth auth = 6; + AuthResponse auth_response = 7; + ShareWorktree share_worktree = 8; + ShareWorktreeResponse share_worktree_response = 9; + OpenWorktree open_worktree = 10; + OpenWorktreeResponse open_worktree_response = 11; + UpdateWorktree update_worktree = 12; + CloseWorktree close_worktree = 13; + OpenBuffer open_buffer = 14; + OpenBufferResponse open_buffer_response = 15; + CloseBuffer close_buffer = 16; + UpdateBuffer update_buffer = 17; + SaveBuffer save_buffer = 18; + BufferSaved buffer_saved = 19; + AddPeer add_peer = 20; + RemovePeer remove_peer = 21; + GetChannels get_channels = 22; + GetChannelsResponse get_channels_response = 23; + GetUsers get_users = 24; + GetUsersResponse get_users_response = 25; + JoinChannel join_channel = 26; + JoinChannelResponse join_channel_response = 27; + SendChannelMessage send_channel_message = 28; + ChannelMessageSent channel_message_sent = 29; } } // Messages +message Ping { + int32 id = 1; +} + +message Pong { + int32 id = 2; +} + message Auth { int32 user_id = 1; string access_token = 2; diff --git a/zrpc/src/lib.rs b/zrpc/src/lib.rs index 8cafad9f1f57b9e38af28cbf08c95de66183cc84..be3625e51f23765effb83b9079a774597553761f 100644 --- a/zrpc/src/lib.rs +++ b/zrpc/src/lib.rs @@ -1,5 +1,6 @@ pub mod auth; mod peer; +mod peer2; pub mod proto; #[cfg(any(test, feature = "test-support"))] pub mod test; diff --git a/zrpc/src/peer.rs b/zrpc/src/peer.rs index c377ad1309ccc0e6eae58457769ecaa49c520a54..d0dcf836a5680e9d78a46fb088cd0a86d9c746bf 100644 --- a/zrpc/src/peer.rs +++ b/zrpc/src/peer.rs @@ -38,8 +38,8 @@ type ForegroundMessageHandler = Box, ConnectionId) -> Option>>; pub struct Receipt { - sender_id: ConnectionId, - message_id: u32, + pub sender_id: ConnectionId, + pub message_id: u32, payload_type: PhantomData, } @@ -172,7 +172,7 @@ impl Peer { } else { router.handle(connection_id, envelope.clone()).await; if let Some(envelope) = proto::build_typed_envelope(connection_id, envelope) { - broadcast_incoming_messages.send(envelope).await.ok(); + broadcast_incoming_messages.send(Arc::from(envelope)).await.ok(); } else { log::error!("unable to construct a typed envelope"); } diff --git a/zrpc/src/peer2.rs b/zrpc/src/peer2.rs new file mode 100644 index 0000000000000000000000000000000000000000..7ead744bdd1394fcd3dd39ffb4f0e0382b026122 --- /dev/null +++ b/zrpc/src/peer2.rs @@ -0,0 +1,470 @@ +use crate::{ + proto::{self, EnvelopedMessage, MessageStream, RequestMessage}, + ConnectionId, PeerId, Receipt, +}; +use anyhow::{anyhow, Context, Result}; +use async_lock::{Mutex, RwLock}; +use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; +use futures::{FutureExt, StreamExt}; +use postage::{ + mpsc, + prelude::{Sink as _, Stream as _}, +}; +use std::{ + any::Any, + collections::HashMap, + future::Future, + sync::{ + atomic::{self, AtomicU32}, + Arc, + }, +}; + +pub struct Peer { + connections: RwLock>, + next_connection_id: AtomicU32, +} + +#[derive(Clone)] +struct Connection { + outgoing_tx: mpsc::Sender, + next_message_id: Arc, + response_channels: Arc>>>, +} + +impl Peer { + pub fn new() -> Arc { + Arc::new(Self { + connections: Default::default(), + next_connection_id: Default::default(), + }) + } + + pub async fn add_connection( + self: &Arc, + conn: Conn, + ) -> ( + ConnectionId, + impl Future> + Send, + mpsc::Receiver>, + ) + where + Conn: futures::Sink + + futures::Stream> + + Send + + Unpin, + { + let (tx, rx) = conn.split(); + let connection_id = ConnectionId( + self.next_connection_id + .fetch_add(1, atomic::Ordering::SeqCst), + ); + let (mut incoming_tx, incoming_rx) = mpsc::channel(64); + let (outgoing_tx, mut outgoing_rx) = mpsc::channel(64); + let connection = Connection { + outgoing_tx, + next_message_id: Default::default(), + response_channels: Default::default(), + }; + let mut writer = MessageStream::new(tx); + let mut reader = MessageStream::new(rx); + + let response_channels = connection.response_channels.clone(); + let handle_io = async move { + loop { + let read_message = reader.read_message().fuse(); + futures::pin_mut!(read_message); + loop { + futures::select_biased! { + incoming = read_message => match incoming { + Ok(incoming) => { + if let Some(responding_to) = incoming.responding_to { + let channel = response_channels.lock().await.remove(&responding_to); + if let Some(mut tx) = channel { + tx.send(incoming).await.ok(); + } else { + log::warn!("received RPC response to unknown request {}", responding_to); + } + } else { + if let Some(envelope) = proto::build_typed_envelope(connection_id, incoming) { + if incoming_tx.send(envelope).await.is_err() { + response_channels.lock().await.clear(); + return Ok(()) + } + } else { + log::error!("unable to construct a typed envelope"); + } + } + + break; + } + Err(error) => { + response_channels.lock().await.clear(); + Err(error).context("received invalid RPC message")?; + } + }, + outgoing = outgoing_rx.recv().fuse() => match outgoing { + Some(outgoing) => { + if let Err(result) = writer.write_message(&outgoing).await { + response_channels.lock().await.clear(); + Err(result).context("failed to write RPC message")?; + } + } + None => { + response_channels.lock().await.clear(); + return Ok(()) + } + } + } + } + } + }; + + self.connections + .write() + .await + .insert(connection_id, connection); + + (connection_id, handle_io, incoming_rx) + } + + pub async fn disconnect(&self, connection_id: ConnectionId) { + self.connections.write().await.remove(&connection_id); + } + + pub async fn reset(&self) { + self.connections.write().await.clear(); + } + + pub fn request( + self: &Arc, + receiver_id: ConnectionId, + request: T, + ) -> impl Future> { + self.request_internal(None, receiver_id, request) + } + + pub fn forward_request( + self: &Arc, + sender_id: ConnectionId, + receiver_id: ConnectionId, + request: T, + ) -> impl Future> { + self.request_internal(Some(sender_id), receiver_id, request) + } + + pub fn request_internal( + self: &Arc, + original_sender_id: Option, + receiver_id: ConnectionId, + request: T, + ) -> impl Future> { + let this = self.clone(); + let (tx, mut rx) = mpsc::channel(1); + async move { + let mut connection = this.connection(receiver_id).await?; + let message_id = connection + .next_message_id + .fetch_add(1, atomic::Ordering::SeqCst); + connection + .response_channels + .lock() + .await + .insert(message_id, tx); + connection + .outgoing_tx + .send(request.into_envelope(message_id, None, original_sender_id.map(|id| id.0))) + .await + .map_err(|_| anyhow!("connection was closed"))?; + let response = rx + .recv() + .await + .ok_or_else(|| anyhow!("connection was closed"))?; + T::Response::from_envelope(response) + .ok_or_else(|| anyhow!("received response of the wrong type")) + } + } + + pub fn send( + self: &Arc, + receiver_id: ConnectionId, + message: T, + ) -> impl Future> { + let this = self.clone(); + async move { + let mut connection = this.connection(receiver_id).await?; + let message_id = connection + .next_message_id + .fetch_add(1, atomic::Ordering::SeqCst); + connection + .outgoing_tx + .send(message.into_envelope(message_id, None, None)) + .await?; + Ok(()) + } + } + + pub fn forward_send( + self: &Arc, + sender_id: ConnectionId, + receiver_id: ConnectionId, + message: T, + ) -> impl Future> { + let this = self.clone(); + async move { + let mut connection = this.connection(receiver_id).await?; + let message_id = connection + .next_message_id + .fetch_add(1, atomic::Ordering::SeqCst); + connection + .outgoing_tx + .send(message.into_envelope(message_id, None, Some(sender_id.0))) + .await?; + Ok(()) + } + } + + pub fn respond( + self: &Arc, + receipt: Receipt, + response: T::Response, + ) -> impl Future> { + let this = self.clone(); + async move { + let mut connection = this.connection(receipt.sender_id).await?; + let message_id = connection + .next_message_id + .fetch_add(1, atomic::Ordering::SeqCst); + connection + .outgoing_tx + .send(response.into_envelope(message_id, Some(receipt.message_id), None)) + .await?; + Ok(()) + } + } + + fn connection( + self: &Arc, + connection_id: ConnectionId, + ) -> impl Future> { + let this = self.clone(); + async move { + let connections = this.connections.read().await; + let connection = connections + .get(&connection_id) + .ok_or_else(|| anyhow!("no such connection: {}", connection_id))?; + Ok(connection.clone()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test, TypedEnvelope}; + + #[test] + fn test_request_response() { + smol::block_on(async move { + // create 2 clients connected to 1 server + let server = Peer::new(); + let client1 = Peer::new(); + let client2 = Peer::new(); + + let (client1_to_server_conn, server_to_client_1_conn) = test::Channel::bidirectional(); + 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) = test::Channel::bidirectional(); + 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; + + smol::spawn(io_task1).detach(); + smol::spawn(io_task2).detach(); + smol::spawn(io_task3).detach(); + smol::spawn(io_task4).detach(); + smol::spawn(handle_messages(incoming1, server.clone())).detach(); + smol::spawn(handle_messages(incoming2, server.clone())).detach(); + + assert_eq!( + client1 + .request(client1_conn_id, proto::Ping { id: 1 },) + .await + .unwrap(), + proto::Pong { id: 1 } + ); + + assert_eq!( + client2 + .request(client2_conn_id, proto::Ping { id: 2 },) + .await + .unwrap(), + proto::Pong { id: 2 } + ); + + assert_eq!( + client1 + .request( + client1_conn_id, + proto::OpenBuffer { + worktree_id: 1, + path: "path/one".to_string(), + }, + ) + .await + .unwrap(), + proto::OpenBufferResponse { + buffer: Some(proto::Buffer { + id: 101, + content: "path/one content".to_string(), + history: vec![], + selections: vec![], + }), + } + ); + + assert_eq!( + client2 + .request( + client2_conn_id, + proto::OpenBuffer { + worktree_id: 2, + path: "path/two".to_string(), + }, + ) + .await + .unwrap(), + proto::OpenBufferResponse { + buffer: Some(proto::Buffer { + id: 102, + content: "path/two content".to_string(), + history: vec![], + selections: vec![], + }), + } + ); + + client1.disconnect(client1_conn_id).await; + client2.disconnect(client1_conn_id).await; + + async fn handle_messages( + mut messages: mpsc::Receiver>, + peer: Arc, + ) -> Result<()> { + while let Some(envelope) = messages.next().await { + if let Some(envelope) = envelope.downcast_ref::>() { + let receipt = envelope.receipt(); + peer.respond( + receipt, + proto::Pong { + id: envelope.payload.id, + }, + ) + .await? + } else if let Some(envelope) = + envelope.downcast_ref::>() + { + let message = &envelope.payload; + let receipt = envelope.receipt(); + let response = match message.path.as_str() { + "path/one" => { + assert_eq!(message.worktree_id, 1); + proto::OpenBufferResponse { + buffer: Some(proto::Buffer { + id: 101, + content: "path/one content".to_string(), + history: vec![], + selections: vec![], + }), + } + } + "path/two" => { + assert_eq!(message.worktree_id, 2); + proto::OpenBufferResponse { + buffer: Some(proto::Buffer { + id: 102, + content: "path/two content".to_string(), + history: vec![], + selections: vec![], + }), + } + } + _ => { + panic!("unexpected path {}", message.path); + } + }; + + peer.respond(receipt, response).await? + } else { + panic!("unknown message type"); + } + } + + Ok(()) + } + }); + } + + #[test] + fn test_disconnect() { + smol::block_on(async move { + let (client_conn, mut server_conn) = test::Channel::bidirectional(); + + let client = Peer::new(); + let (connection_id, io_handler, mut incoming) = + client.add_connection(client_conn).await; + + let (mut io_ended_tx, mut io_ended_rx) = postage::barrier::channel(); + smol::spawn(async move { + io_handler.await.ok(); + io_ended_tx.send(()).await.unwrap(); + }) + .detach(); + + let (mut messages_ended_tx, mut messages_ended_rx) = postage::barrier::channel(); + smol::spawn(async move { + incoming.next().await; + messages_ended_tx.send(()).await.unwrap(); + }) + .detach(); + + client.disconnect(connection_id).await; + + io_ended_rx.recv().await; + messages_ended_rx.recv().await; + assert!( + futures::SinkExt::send(&mut server_conn, WebSocketMessage::Binary(vec![])) + .await + .is_err() + ); + }); + } + + #[test] + fn test_io_error() { + smol::block_on(async move { + let (client_conn, server_conn) = test::Channel::bidirectional(); + drop(server_conn); + + let client = Peer::new(); + let (connection_id, io_handler, mut incoming) = + client.add_connection(client_conn).await; + smol::spawn(io_handler).detach(); + smol::spawn(async move { incoming.next().await }).detach(); + + let err = client + .request( + connection_id, + proto::Auth { + user_id: 42, + access_token: "token".to_string(), + }, + ) + .await + .unwrap_err(); + assert_eq!(err.to_string(), "connection was closed"); + }); + } +} diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index 1c799ebe21d544af919c30beb60d20fa2a15c291..d8c794fd63ab8cef1b1f2c213ae6242906be1850 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -4,7 +4,6 @@ use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSock use futures::{SinkExt as _, StreamExt as _}; use prost::Message; use std::any::Any; -use std::sync::Arc; use std::{ io, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -34,14 +33,16 @@ pub trait RequestMessage: EnvelopedMessage { macro_rules! messages { ($($name:ident),* $(,)?) => { - pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { + pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { match envelope.payload { - $(Some(envelope::Payload::$name(payload)) => Some(Arc::new(TypedEnvelope { - sender_id, - original_sender_id: envelope.original_sender_id.map(PeerId), - message_id: envelope.id, - payload, - })), )* + $(Some(envelope::Payload::$name(payload)) => { + Some(Box::new(TypedEnvelope { + sender_id, + original_sender_id: envelope.original_sender_id.map(PeerId), + message_id: envelope.id, + payload, + })) + }, )* _ => None } } @@ -116,6 +117,8 @@ messages!( OpenBufferResponse, OpenWorktree, OpenWorktreeResponse, + Ping, + Pong, RemovePeer, SaveBuffer, SendChannelMessage, @@ -132,6 +135,7 @@ request_messages!( (JoinChannel, JoinChannelResponse), (OpenBuffer, OpenBufferResponse), (OpenWorktree, OpenWorktreeResponse), + (Ping, Pong), (SaveBuffer, BufferSaved), (ShareWorktree, ShareWorktreeResponse), ); From 5dee7ecf5b3cbe077645cfd8e2ae17b046436780 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Aug 2021 15:01:57 +0200 Subject: [PATCH 018/204] Start using the new `zrpc::peer2::Peer` in Server --- server/src/auth.rs | 28 +++++++++++ server/src/rpc.rs | 116 +++++++++++++++++++++++++++++++++++++++++++++ zrpc/src/lib.rs | 2 +- 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/server/src/auth.rs b/server/src/auth.rs index d61428fa371a0f26bded6a42250aab37a1e1d9f5..ac326b15defe9cc89ae67886e6d0add79e9c111f 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -137,6 +137,34 @@ impl PeerExt for Peer { } } +#[async_trait] +impl PeerExt for zrpc::peer2::Peer { + async fn sign_out( + self: &Arc, + connection_id: zrpc::ConnectionId, + state: &AppState, + ) -> tide::Result<()> { + self.disconnect(connection_id).await; + let worktree_ids = state.rpc.write().await.remove_connection(connection_id); + for worktree_id in worktree_ids { + let state = state.rpc.read().await; + if let Some(worktree) = state.worktrees.get(&worktree_id) { + rpc::broadcast(connection_id, worktree.connection_ids(), |conn_id| { + self.send( + conn_id, + proto::RemovePeer { + worktree_id, + peer_id: connection_id.0, + }, + ) + }) + .await?; + } + } + Ok(()) + } +} + pub fn build_client(client_id: &str, client_secret: &str) -> Client { Client::new( ClientId::new(client_id.to_string()), diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 8696f0369130ef71b0f5e1f2ab9c9c7ed931091d..b7be90d3485cbd8e2e0a611e327efe1b5e02dd05 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -9,8 +9,11 @@ use async_tungstenite::{ tungstenite::{protocol::Role, Error as WebSocketError, Message as WebSocketMessage}, WebSocketStream, }; +use futures::{future::BoxFuture, FutureExt}; +use postage::prelude::Stream as _; use sha1::{Digest as _, Sha1}; use std::{ + any::{Any, TypeId}, collections::{HashMap, HashSet}, future::Future, mem, @@ -32,6 +35,119 @@ use zrpc::{ type ReplicaId = u16; +type Handler = Box< + dyn Fn(&mut Option>, Arc) -> Option>, +>; + +#[derive(Default)] +struct ServerBuilder { + handlers: Vec, + handler_types: HashSet, +} + +impl ServerBuilder { + pub fn on_message(&mut self, handler: F) -> &mut Self + where + F: 'static + Fn(Box>, Arc) -> Fut, + Fut: 'static + Send + Future, + M: EnvelopedMessage, + { + if self.handler_types.insert(TypeId::of::()) { + panic!("registered a handler for the same message twice"); + } + + self.handlers + .push(Box::new(move |untyped_envelope, server| { + if let Some(typed_envelope) = untyped_envelope.take() { + match typed_envelope.downcast::>() { + Ok(typed_envelope) => Some((handler)(typed_envelope, server).boxed()), + Err(envelope) => { + *untyped_envelope = Some(envelope); + None + } + } + } else { + None + } + })); + self + } + + pub fn build(self, rpc: Arc, state: Arc) -> Arc { + Arc::new(Server { + rpc, + state, + handlers: self.handlers, + }) + } +} + +struct Server { + rpc: Arc, + state: Arc, + handlers: Vec, +} + +impl Server { + pub async fn add_connection( + self: &Arc, + connection: Conn, + addr: String, + user_id: UserId, + ) where + Conn: 'static + + futures::Sink + + futures::Stream> + + Send + + Unpin, + { + let this = self.clone(); + let (connection_id, handle_io, mut incoming_rx) = this.rpc.add_connection(connection).await; + this.state + .rpc + .write() + .await + .add_connection(connection_id, user_id); + + let handle_io = handle_io.fuse(); + futures::pin_mut!(handle_io); + loop { + let next_message = incoming_rx.recv().fuse(); + futures::pin_mut!(next_message); + futures::select_biased! { + message = next_message => { + if let Some(message) = message { + let mut message = Some(message); + for handler in &this.handlers { + if let Some(future) = (handler)(&mut message, this.clone()) { + future.await; + break; + } + } + + if let Some(message) = message { + log::warn!("unhandled message: {:?}", message); + } + } else { + log::info!("rpc connection closed {:?}", addr); + break; + } + } + handle_io = handle_io => { + if let Err(err) = handle_io { + log::error!("error handling rpc connection {:?} - {:?}", addr, err); + } + break; + } + } + } + + if let Err(err) = this.rpc.sign_out(connection_id, &this.state).await { + log::error!("error signing out connection {:?} - {:?}", addr, err); + } + } +} + #[derive(Default)] pub struct State { connections: HashMap, diff --git a/zrpc/src/lib.rs b/zrpc/src/lib.rs index be3625e51f23765effb83b9079a774597553761f..67132cf299253bd63737bd0ad7474cde8ff9e3e9 100644 --- a/zrpc/src/lib.rs +++ b/zrpc/src/lib.rs @@ -1,6 +1,6 @@ pub mod auth; mod peer; -mod peer2; +pub mod peer2; pub mod proto; #[cfg(any(test, feature = "test-support"))] pub mod test; From d6412fdbde188ac59b69ae270eb67aac6c0b6e8f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Aug 2021 15:02:30 +0200 Subject: [PATCH 019/204] Delete old `Peer` implementation and adapt previous code paths This still doesn't compile but should be close. --- server/src/auth.rs | 28 --- server/src/rpc.rs | 164 +++------------ server/src/tests.rs | 6 +- zed/src/lib.rs | 2 - zed/src/rpc.rs | 41 +--- zed/src/test.rs | 2 - zed/src/workspace.rs | 6 +- zrpc/src/lib.rs | 1 - zrpc/src/peer.rs | 448 ++++++++++++----------------------------- zrpc/src/peer2.rs | 470 ------------------------------------------- 10 files changed, 171 insertions(+), 997 deletions(-) delete mode 100644 zrpc/src/peer2.rs diff --git a/server/src/auth.rs b/server/src/auth.rs index ac326b15defe9cc89ae67886e6d0add79e9c111f..d61428fa371a0f26bded6a42250aab37a1e1d9f5 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -137,34 +137,6 @@ impl PeerExt for Peer { } } -#[async_trait] -impl PeerExt for zrpc::peer2::Peer { - async fn sign_out( - self: &Arc, - connection_id: zrpc::ConnectionId, - state: &AppState, - ) -> tide::Result<()> { - self.disconnect(connection_id).await; - let worktree_ids = state.rpc.write().await.remove_connection(connection_id); - for worktree_id in worktree_ids { - let state = state.rpc.read().await; - if let Some(worktree) = state.worktrees.get(&worktree_id) { - rpc::broadcast(connection_id, worktree.connection_ids(), |conn_id| { - self.send( - conn_id, - proto::RemovePeer { - worktree_id, - peer_id: connection_id.0, - }, - ) - }) - .await?; - } - } - Ok(()) - } -} - pub fn build_client(client_id: &str, client_secret: &str) -> Client { Client::new( ClientId::new(client_id.to_string()), diff --git a/server/src/rpc.rs b/server/src/rpc.rs index b7be90d3485cbd8e2e0a611e327efe1b5e02dd05..e628ef2c8131ce7b07d0db1a7e9b4d1605e20917 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -30,13 +30,15 @@ use time::OffsetDateTime; use zrpc::{ auth::random_token, proto::{self, EnvelopedMessage}, - ConnectionId, Peer, Router, TypedEnvelope, + ConnectionId, Peer, TypedEnvelope, }; type ReplicaId = u16; type Handler = Box< - dyn Fn(&mut Option>, Arc) -> Option>, + dyn Send + + Sync + + Fn(&mut Option>, Arc) -> Option>, >; #[derive(Default)] @@ -48,7 +50,7 @@ struct ServerBuilder { impl ServerBuilder { pub fn on_message(&mut self, handler: F) -> &mut Self where - F: 'static + Fn(Box>, Arc) -> Fut, + F: 'static + Send + Sync + Fn(Box>, Arc) -> Fut, Fut: 'static + Send + Future, M: EnvelopedMessage, { @@ -73,23 +75,23 @@ impl ServerBuilder { self } - pub fn build(self, rpc: Arc, state: Arc) -> Arc { + pub fn build(self, rpc: &Arc, state: &Arc) -> Arc { Arc::new(Server { - rpc, - state, + rpc: rpc.clone(), + state: state.clone(), handlers: self.handlers, }) } } -struct Server { - rpc: Arc, +pub struct Server { + rpc: Arc, state: Arc, handlers: Vec, } impl Server { - pub async fn add_connection( + pub async fn handle_connection( self: &Arc, connection: Conn, addr: String, @@ -332,99 +334,31 @@ impl State { } } -trait MessageHandler<'a, M: proto::EnvelopedMessage> { - type Output: 'a + Send + Future>; - - fn handle( - &self, - message: TypedEnvelope, - rpc: &'a Arc, - app_state: &'a Arc, - ) -> Self::Output; -} - -impl<'a, M, F, Fut> MessageHandler<'a, M> for F -where - M: proto::EnvelopedMessage, - F: Fn(TypedEnvelope, &'a Arc, &'a Arc) -> Fut, - Fut: 'a + Send + Future>, -{ - type Output = Fut; - - fn handle( - &self, - message: TypedEnvelope, - rpc: &'a Arc, - app_state: &'a Arc, - ) -> Self::Output { - (self)(message, rpc, app_state) - } -} - -fn on_message(router: &mut Router, rpc: &Arc, app_state: &Arc, handler: H) -where - M: EnvelopedMessage, - H: 'static + Clone + Send + Sync + for<'a> MessageHandler<'a, M>, -{ - let rpc = rpc.clone(); - let handler = handler.clone(); - let app_state = app_state.clone(); - router.add_message_handler(move |message| { - let rpc = rpc.clone(); - let handler = handler.clone(); - let app_state = app_state.clone(); - async move { - let sender_id = message.sender_id; - let message_id = message.message_id; - let start_time = Instant::now(); - log::info!( - "RPC message received. id: {}.{}, type:{}", - sender_id, - message_id, - M::NAME - ); - if let Err(err) = handler.handle(message, &rpc, &app_state).await { - log::error!("error handling message: {:?}", err); - } else { - log::info!( - "RPC message handled. id:{}.{}, duration:{:?}", - sender_id, - message_id, - start_time.elapsed() - ); - } - - Ok(()) - } - }); -} - -pub fn add_rpc_routes(router: &mut Router, state: &Arc, rpc: &Arc) { - on_message(router, rpc, state, share_worktree); - on_message(router, rpc, state, join_worktree); - on_message(router, rpc, state, update_worktree); - on_message(router, rpc, state, close_worktree); - on_message(router, rpc, state, open_buffer); - on_message(router, rpc, state, close_buffer); - on_message(router, rpc, state, update_buffer); - on_message(router, rpc, state, buffer_saved); - on_message(router, rpc, state, save_buffer); - on_message(router, rpc, state, get_channels); - on_message(router, rpc, state, get_users); - on_message(router, rpc, state, join_channel); - on_message(router, rpc, state, send_channel_message); +pub fn build_server(state: &Arc, rpc: &Arc) -> Arc { + ServerBuilder::default() + // .on_message(share_worktree) + // .on_message(join_worktree) + // .on_message(update_worktree) + // .on_message(close_worktree) + // .on_message(open_buffer) + // .on_message(close_buffer) + // .on_message(update_buffer) + // .on_message(buffer_saved) + // .on_message(save_buffer) + // .on_message(get_channels) + // .on_message(get_users) + // .on_message(join_channel) + // .on_message(send_channel_message) + .build(rpc, state) } pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { - let mut router = Router::new(); - add_rpc_routes(&mut router, app.state(), rpc); - let router = Arc::new(router); + let server = build_server(app.state(), rpc); let rpc = rpc.clone(); app.at("/rpc").with(auth::VerifyToken).get(move |request: Request>| { let user_id = request.ext::().copied(); - let rpc = rpc.clone(); - let router = router.clone(); + let server = server.clone(); async move { const WEBSOCKET_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; @@ -451,12 +385,11 @@ pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { let http_res: &mut tide::http::Response = response.as_mut(); let upgrade_receiver = http_res.recv_upgrade().await; let addr = request.remote().unwrap_or("unknown").to_string(); - let state = request.state().clone(); 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 { let stream = WebSocketStream::from_raw_socket(stream, Role::Server, None).await; - handle_connection(rpc, router, state, addr, stream, user_id).await; + server.handle_connection(stream, addr, user_id).await; } }); @@ -465,43 +398,6 @@ pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { }); } -pub async fn handle_connection( - rpc: Arc, - router: Arc, - state: Arc, - addr: String, - stream: Conn, - user_id: UserId, -) where - Conn: 'static - + futures::Sink - + futures::Stream> - + Send - + Unpin, -{ - log::info!("accepted rpc connection: {:?}", addr); - let (connection_id, handle_io, handle_messages) = rpc.add_connection(stream, router).await; - state - .rpc - .write() - .await - .add_connection(connection_id, user_id); - - let handle_messages = async move { - handle_messages.await; - Ok(()) - }; - - if let Err(e) = futures::try_join!(handle_messages, handle_io) { - log::error!("error handling rpc connection {:?} - {:?}", addr, e); - } - - log::info!("closing connection to {:?}", addr); - if let Err(e) = rpc.sign_out(connection_id, &state).await { - log::error!("error signing out connection {:?} - {:?}", addr, e); - } -} - async fn share_worktree( mut request: TypedEnvelope, rpc: &Arc, diff --git a/server/src/tests.rs b/server/src/tests.rs index cce311c5f3242f97837fe289439f268347751eaf..18a7375f862489b0c4b323c762b2ef08fe1da26b 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -2,7 +2,7 @@ use crate::{ auth, db::{self, UserId}, github, - rpc::{self, add_rpc_routes}, + rpc::{self, build_server}, AppState, Config, }; use async_std::task; @@ -24,7 +24,7 @@ use zed::{ test::Channel, worktree::Worktree, }; -use zrpc::{ForegroundRouter, Peer, Router}; +use zrpc::Peer; #[gpui::test] async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { @@ -541,7 +541,7 @@ impl TestServer { let app_state = Self::build_app_state(&db_name).await; let peer = Peer::new(); let mut router = Router::new(); - add_rpc_routes(&mut router, &app_state, &peer); + build_server(&mut router, &app_state, &peer); Self { peer, router: Arc::new(router), diff --git a/zed/src/lib.rs b/zed/src/lib.rs index f283584c0e1966fe2206aa367c88ca3094b61ad5..90e68c698def7aed60438fdd597c445044840da9 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -24,14 +24,12 @@ pub use settings::Settings; use parking_lot::Mutex; use postage::watch; use std::sync::Arc; -use zrpc::ForegroundRouter; pub struct AppState { pub settings_tx: Arc>>, pub settings: watch::Receiver, pub languages: Arc, pub themes: Arc, - pub rpc_router: Arc, pub rpc: rpc::Client, pub fs: Arc, } diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 00f5e2dd7446564ce75213b4ce499fa98c0c368e..2bd9fd7f819e4bbe7faf3b73f2e98d8b748c35d6 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -13,7 +13,7 @@ use zrpc::proto::EntityMessage; pub use zrpc::{proto, ConnectionId, PeerId, TypedEnvelope}; use zrpc::{ proto::{EnvelopedMessage, RequestMessage}, - ForegroundRouter, Peer, Receipt, + Peer, Receipt, }; lazy_static! { @@ -43,25 +43,6 @@ impl Client { } } - pub fn on_message( - &self, - router: &mut ForegroundRouter, - handler: H, - cx: &mut gpui::MutableAppContext, - ) where - H: 'static + Clone + for<'a> MessageHandler<'a, M>, - M: proto::EnvelopedMessage, - { - let this = self.clone(); - let cx = cx.to_async(); - router.add_message_handler(move |message| { - let this = this.clone(); - let mut cx = cx.clone(); - let handler = handler.clone(); - async move { handler.handle(message, &this, &mut cx).await } - }); - } - pub fn subscribe_from_model( &self, remote_id: u64, @@ -90,11 +71,7 @@ impl Client { }) } - pub async fn log_in_and_connect( - &self, - router: Arc, - cx: AsyncAppContext, - ) -> surf::Result<()> { + pub async fn log_in_and_connect(&self, cx: AsyncAppContext) -> surf::Result<()> { if self.state.read().await.connection_id.is_some() { return Ok(()); } @@ -111,13 +88,13 @@ impl Client { .await .context("websocket handshake")?; log::info!("connected to rpc address {}", *ZED_SERVER_URL); - self.add_connection(stream, router, cx).await?; + self.add_connection(stream, cx).await?; } else if let Some(host) = ZED_SERVER_URL.strip_prefix("http://") { let stream = smol::net::TcpStream::connect(host).await?; let request = request.uri(format!("ws://{}/rpc", host)).body(())?; let (stream, _) = async_tungstenite::client_async(request, stream).await?; log::info!("connected to rpc address {}", *ZED_SERVER_URL); - self.add_connection(stream, router, cx).await?; + self.add_connection(stream, cx).await?; } else { return Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))?; }; @@ -125,12 +102,7 @@ impl Client { Ok(()) } - pub async fn add_connection( - &self, - conn: Conn, - router: Arc, - cx: AsyncAppContext, - ) -> surf::Result<()> + pub async fn add_connection(&self, conn: Conn, cx: AsyncAppContext) -> surf::Result<()> where Conn: 'static + futures::Sink @@ -138,8 +110,7 @@ impl Client { + Unpin + Send, { - let (connection_id, handle_io, handle_messages) = - self.peer.add_connection(conn, router).await; + let (connection_id, handle_io, handle_messages) = self.peer.add_connection(conn).await; cx.foreground().spawn(handle_messages).detach(); cx.background() .spawn(async move { diff --git a/zed/src/test.rs b/zed/src/test.rs index e2ddff054ea1b288404bbcc4bfc75d462e540b43..ce223691266c9a863a9ef9bcf5b3e3f863f4f364 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -15,7 +15,6 @@ use std::{ sync::Arc, }; use tempdir::TempDir; -use zrpc::ForegroundRouter; #[cfg(feature = "test-support")] pub use zrpc::test::Channel; @@ -163,7 +162,6 @@ pub fn build_app_state(cx: &AppContext) -> Arc { settings, themes, languages: languages.clone(), - rpc_router: Arc::new(ForegroundRouter::new()), rpc: rpc::Client::new(languages), fs: Arc::new(RealFs), }) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index cb543522d8d348e9dd1b39eb2034ddb3974ceb33..e074d57a64b49f92f63b875888770c64361801bd 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -728,10 +728,9 @@ impl Workspace { fn share_worktree(&mut self, app_state: &Arc, cx: &mut ViewContext) { let rpc = self.rpc.clone(); let platform = cx.platform(); - let router = app_state.rpc_router.clone(); let task = cx.spawn(|this, mut cx| async move { - rpc.log_in_and_connect(router, cx.clone()).await?; + rpc.log_in_and_connect(cx.clone()).await?; let share_task = this.update(&mut cx, |this, cx| { let worktree = this.worktrees.iter().next()?; @@ -761,10 +760,9 @@ impl Workspace { fn join_worktree(&mut self, app_state: &Arc, cx: &mut ViewContext) { let rpc = self.rpc.clone(); let languages = self.languages.clone(); - let router = app_state.rpc_router.clone(); let task = cx.spawn(|this, mut cx| async move { - rpc.log_in_and_connect(router, cx.clone()).await?; + rpc.log_in_and_connect(cx.clone()).await?; let worktree_url = cx .platform() diff --git a/zrpc/src/lib.rs b/zrpc/src/lib.rs index 67132cf299253bd63737bd0ad7474cde8ff9e3e9..8cafad9f1f57b9e38af28cbf08c95de66183cc84 100644 --- a/zrpc/src/lib.rs +++ b/zrpc/src/lib.rs @@ -1,6 +1,5 @@ pub mod auth; mod peer; -pub mod peer2; pub mod proto; #[cfg(any(test, feature = "test-support"))] pub mod test; diff --git a/zrpc/src/peer.rs b/zrpc/src/peer.rs index d0dcf836a5680e9d78a46fb088cd0a86d9c746bf..315f56b316c05ad27cdbb22ea92676db0823654d 100644 --- a/zrpc/src/peer.rs +++ b/zrpc/src/peer.rs @@ -2,17 +2,14 @@ use crate::proto::{self, EnvelopedMessage, MessageStream, RequestMessage}; use anyhow::{anyhow, Context, Result}; use async_lock::{Mutex, RwLock}; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; -use futures::{ - future::{self, BoxFuture, LocalBoxFuture}, - FutureExt, Stream, StreamExt, -}; +use futures::{FutureExt, StreamExt}; use postage::{ - broadcast, mpsc, + mpsc, prelude::{Sink as _, Stream as _}, }; use std::{ - any::{Any, TypeId}, - collections::{HashMap, HashSet}, + any::Any, + collections::HashMap, fmt, future::Future, marker::PhantomData, @@ -25,17 +22,20 @@ use std::{ #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct ConnectionId(pub u32); +impl fmt::Display for ConnectionId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct PeerId(pub u32); -type MessageHandler = Box< - dyn Send - + Sync - + Fn(&mut Option, ConnectionId) -> Option>, ->; - -type ForegroundMessageHandler = - Box, ConnectionId) -> Option>>; +impl fmt::Display for PeerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} pub struct Receipt { pub sender_id: ConnectionId, @@ -43,6 +43,18 @@ pub struct Receipt { payload_type: PhantomData, } +impl Clone for Receipt { + fn clone(&self) -> Self { + Self { + sender_id: self.sender_id, + message_id: self.message_id, + payload_type: PhantomData, + } + } +} + +impl Copy for Receipt {} + pub struct TypedEnvelope { pub sender_id: ConnectionId, pub original_sender_id: Option, @@ -67,17 +79,9 @@ impl TypedEnvelope { } } -pub type Router = RouterInternal; -pub type ForegroundRouter = RouterInternal; -pub struct RouterInternal { - message_handlers: Vec, - handler_types: HashSet, -} - pub struct Peer { connections: RwLock>, next_connection_id: AtomicU32, - incoming_messages: broadcast::Sender>, } #[derive(Clone)] @@ -92,22 +96,18 @@ impl Peer { Arc::new(Self { connections: Default::default(), next_connection_id: Default::default(), - incoming_messages: broadcast::channel(256).0, }) } - pub async fn add_connection( + pub async fn add_connection( self: &Arc, conn: Conn, - router: Arc>, ) -> ( ConnectionId, impl Future> + Send, - impl Future, + mpsc::Receiver>, ) where - H: Fn(&mut Option, ConnectionId) -> Option, - Fut: Future, Conn: futures::Sink + futures::Stream> + Send @@ -118,7 +118,7 @@ impl Peer { self.next_connection_id .fetch_add(1, atomic::Ordering::SeqCst), ); - let (mut incoming_tx, mut incoming_rx) = mpsc::channel(64); + let (mut incoming_tx, incoming_rx) = mpsc::channel(64); let (outgoing_tx, mut outgoing_rx) = mpsc::channel(64); let connection = Connection { outgoing_tx, @@ -128,6 +128,7 @@ impl Peer { let mut writer = MessageStream::new(tx); let mut reader = MessageStream::new(rx); + let response_channels = connection.response_channels.clone(); let handle_io = async move { loop { let read_message = reader.read_message().fuse(); @@ -136,57 +137,54 @@ impl Peer { futures::select_biased! { incoming = read_message => match incoming { Ok(incoming) => { - if incoming_tx.send(incoming).await.is_err() { - return Ok(()); + if let Some(responding_to) = incoming.responding_to { + let channel = response_channels.lock().await.remove(&responding_to); + if let Some(mut tx) = channel { + tx.send(incoming).await.ok(); + } else { + log::warn!("received RPC response to unknown request {}", responding_to); + } + } else { + if let Some(envelope) = proto::build_typed_envelope(connection_id, incoming) { + if incoming_tx.send(envelope).await.is_err() { + response_channels.lock().await.clear(); + return Ok(()) + } + } else { + log::error!("unable to construct a typed envelope"); + } } + break; } Err(error) => { + response_channels.lock().await.clear(); Err(error).context("received invalid RPC message")?; } }, outgoing = outgoing_rx.recv().fuse() => match outgoing { Some(outgoing) => { if let Err(result) = writer.write_message(&outgoing).await { + response_channels.lock().await.clear(); Err(result).context("failed to write RPC message")?; } } - None => return Ok(()), + None => { + response_channels.lock().await.clear(); + return Ok(()) + } } } } } }; - let mut broadcast_incoming_messages = self.incoming_messages.clone(); - let response_channels = connection.response_channels.clone(); - let handle_messages = async move { - while let Some(envelope) = incoming_rx.recv().await { - if let Some(responding_to) = envelope.responding_to { - let channel = response_channels.lock().await.remove(&responding_to); - if let Some(mut tx) = channel { - tx.send(envelope).await.ok(); - } else { - log::warn!("received RPC response to unknown request {}", responding_to); - } - } else { - router.handle(connection_id, envelope.clone()).await; - if let Some(envelope) = proto::build_typed_envelope(connection_id, envelope) { - broadcast_incoming_messages.send(Arc::from(envelope)).await.ok(); - } else { - log::error!("unable to construct a typed envelope"); - } - } - } - response_channels.lock().await.clear(); - }; - self.connections .write() .await .insert(connection_id, connection); - (connection_id, handle_io, handle_messages) + (connection_id, handle_io, incoming_rx) } pub async fn disconnect(&self, connection_id: ConnectionId) { @@ -197,12 +195,6 @@ impl Peer { self.connections.write().await.clear(); } - pub fn subscribe(&self) -> impl Stream>> { - self.incoming_messages - .subscribe() - .filter_map(|envelope| future::ready(Arc::downcast(envelope).ok())) - } - pub fn request( self: &Arc, receiver_id: ConnectionId, @@ -325,142 +317,10 @@ impl Peer { } } -impl RouterInternal -where - H: Fn(&mut Option, ConnectionId) -> Option, - Fut: Future, -{ - pub fn new() -> Self { - Self { - message_handlers: Default::default(), - handler_types: Default::default(), - } - } - - async fn handle(&self, connection_id: ConnectionId, message: proto::Envelope) { - let mut envelope = Some(message); - for handler in self.message_handlers.iter() { - if let Some(future) = handler(&mut envelope, connection_id) { - future.await; - return; - } - } - log::warn!("unhandled message: {:?}", envelope.unwrap().payload); - } -} - -impl Router { - pub fn add_message_handler(&mut self, handler: F) - where - T: EnvelopedMessage, - Fut: 'static + Send + Future>, - F: 'static + Send + Sync + Fn(TypedEnvelope) -> Fut, - { - if !self.handler_types.insert(TypeId::of::()) { - panic!("duplicate handler type"); - } - - self.message_handlers - .push(Box::new(move |envelope, connection_id| { - if envelope.as_ref().map_or(false, T::matches_envelope) { - let envelope = Option::take(envelope).unwrap(); - let message_id = envelope.id; - let future = handler(TypedEnvelope { - sender_id: connection_id, - original_sender_id: envelope.original_sender_id.map(PeerId), - message_id, - payload: T::from_envelope(envelope).unwrap(), - }); - Some( - async move { - if let Err(error) = future.await { - log::error!( - "error handling message {} {}: {:?}", - T::NAME, - message_id, - error - ); - } - } - .boxed(), - ) - } else { - None - } - })); - } -} - -impl ForegroundRouter { - pub fn add_message_handler(&mut self, handler: F) - where - T: EnvelopedMessage, - Fut: 'static + Future>, - F: 'static + Fn(TypedEnvelope) -> Fut, - { - if !self.handler_types.insert(TypeId::of::()) { - panic!("duplicate handler type"); - } - - self.message_handlers - .push(Box::new(move |envelope, connection_id| { - if envelope.as_ref().map_or(false, T::matches_envelope) { - let envelope = Option::take(envelope).unwrap(); - let message_id = envelope.id; - let future = handler(TypedEnvelope { - sender_id: connection_id, - original_sender_id: envelope.original_sender_id.map(PeerId), - message_id, - payload: T::from_envelope(envelope).unwrap(), - }); - Some( - async move { - if let Err(error) = future.await { - log::error!( - "error handling message {} {}: {:?}", - T::NAME, - message_id, - error - ); - } - } - .boxed_local(), - ) - } else { - None - } - })); - } -} - -impl Clone for Receipt { - fn clone(&self) -> Self { - Self { - sender_id: self.sender_id, - message_id: self.message_id, - payload_type: PhantomData, - } - } -} - -impl Copy for Receipt {} - -impl fmt::Display for ConnectionId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Display for PeerId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - #[cfg(test)] mod tests { use super::*; - use crate::test; + use crate::{test, TypedEnvelope}; #[test] fn test_request_response() { @@ -470,139 +330,37 @@ mod tests { let client1 = Peer::new(); let client2 = Peer::new(); - let mut router = Router::new(); - router.add_message_handler({ - let server = server.clone(); - move |envelope: TypedEnvelope| { - let server = server.clone(); - async move { - let receipt = envelope.receipt(); - let message = envelope.payload; - server - .respond( - receipt, - match message.user_id { - 1 => { - assert_eq!(message.access_token, "access-token-1"); - proto::AuthResponse { - credentials_valid: true, - } - } - 2 => { - assert_eq!(message.access_token, "access-token-2"); - proto::AuthResponse { - credentials_valid: false, - } - } - _ => { - panic!("unexpected user id {}", message.user_id); - } - }, - ) - .await - } - } - }); - - router.add_message_handler({ - let server = server.clone(); - move |envelope: TypedEnvelope| { - let server = server.clone(); - async move { - let receipt = envelope.receipt(); - let message = envelope.payload; - server - .respond( - receipt, - match message.path.as_str() { - "path/one" => { - assert_eq!(message.worktree_id, 1); - proto::OpenBufferResponse { - buffer: Some(proto::Buffer { - id: 101, - content: "path/one content".to_string(), - history: vec![], - selections: vec![], - }), - } - } - "path/two" => { - assert_eq!(message.worktree_id, 2); - proto::OpenBufferResponse { - buffer: Some(proto::Buffer { - id: 102, - content: "path/two content".to_string(), - history: vec![], - selections: vec![], - }), - } - } - _ => { - panic!("unexpected path {}", message.path); - } - }, - ) - .await - } - } - }); - let router = Arc::new(router); - let (client1_to_server_conn, server_to_client_1_conn) = test::Channel::bidirectional(); - let (client1_conn_id, io_task1, msg_task1) = client1 - .add_connection(client1_to_server_conn, router.clone()) - .await; - let (_, io_task2, msg_task2) = server - .add_connection(server_to_client_1_conn, router.clone()) - .await; + 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) = test::Channel::bidirectional(); - let (client2_conn_id, io_task3, msg_task3) = client2 - .add_connection(client2_to_server_conn, router.clone()) - .await; - let (_, io_task4, msg_task4) = server - .add_connection(server_to_client_2_conn, router.clone()) - .await; + 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; smol::spawn(io_task1).detach(); smol::spawn(io_task2).detach(); smol::spawn(io_task3).detach(); smol::spawn(io_task4).detach(); - smol::spawn(msg_task1).detach(); - smol::spawn(msg_task2).detach(); - smol::spawn(msg_task3).detach(); - smol::spawn(msg_task4).detach(); + smol::spawn(handle_messages(incoming1, server.clone())).detach(); + smol::spawn(handle_messages(incoming2, server.clone())).detach(); assert_eq!( client1 - .request( - client1_conn_id, - proto::Auth { - user_id: 1, - access_token: "access-token-1".to_string(), - }, - ) + .request(client1_conn_id, proto::Ping { id: 1 },) .await .unwrap(), - proto::AuthResponse { - credentials_valid: true, - } + proto::Pong { id: 1 } ); assert_eq!( client2 - .request( - client2_conn_id, - proto::Auth { - user_id: 2, - access_token: "access-token-2".to_string(), - }, - ) + .request(client2_conn_id, proto::Ping { id: 2 },) .await .unwrap(), - proto::AuthResponse { - credentials_valid: false, - } + proto::Pong { id: 2 } ); assert_eq!( @@ -649,6 +407,62 @@ mod tests { client1.disconnect(client1_conn_id).await; client2.disconnect(client1_conn_id).await; + + async fn handle_messages( + mut messages: mpsc::Receiver>, + peer: Arc, + ) -> Result<()> { + while let Some(envelope) = messages.next().await { + if let Some(envelope) = envelope.downcast_ref::>() { + let receipt = envelope.receipt(); + peer.respond( + receipt, + proto::Pong { + id: envelope.payload.id, + }, + ) + .await? + } else if let Some(envelope) = + envelope.downcast_ref::>() + { + let message = &envelope.payload; + let receipt = envelope.receipt(); + let response = match message.path.as_str() { + "path/one" => { + assert_eq!(message.worktree_id, 1); + proto::OpenBufferResponse { + buffer: Some(proto::Buffer { + id: 101, + content: "path/one content".to_string(), + history: vec![], + selections: vec![], + }), + } + } + "path/two" => { + assert_eq!(message.worktree_id, 2); + proto::OpenBufferResponse { + buffer: Some(proto::Buffer { + id: 102, + content: "path/two content".to_string(), + history: vec![], + selections: vec![], + }), + } + } + _ => { + panic!("unexpected path {}", message.path); + } + }; + + peer.respond(receipt, response).await? + } else { + panic!("unknown message type"); + } + } + + Ok(()) + } }); } @@ -658,9 +472,8 @@ mod tests { let (client_conn, mut server_conn) = test::Channel::bidirectional(); let client = Peer::new(); - let router = Arc::new(Router::new()); - let (connection_id, io_handler, message_handler) = - client.add_connection(client_conn, router).await; + let (connection_id, io_handler, mut incoming) = + client.add_connection(client_conn).await; let (mut io_ended_tx, mut io_ended_rx) = postage::barrier::channel(); smol::spawn(async move { @@ -671,7 +484,7 @@ mod tests { let (mut messages_ended_tx, mut messages_ended_rx) = postage::barrier::channel(); smol::spawn(async move { - message_handler.await; + incoming.next().await; messages_ended_tx.send(()).await.unwrap(); }) .detach(); @@ -695,11 +508,10 @@ mod tests { drop(server_conn); let client = Peer::new(); - let router = Arc::new(Router::new()); - let (connection_id, io_handler, message_handler) = - client.add_connection(client_conn, router).await; + let (connection_id, io_handler, mut incoming) = + client.add_connection(client_conn).await; smol::spawn(io_handler).detach(); - smol::spawn(message_handler).detach(); + smol::spawn(async move { incoming.next().await }).detach(); let err = client .request( diff --git a/zrpc/src/peer2.rs b/zrpc/src/peer2.rs deleted file mode 100644 index 7ead744bdd1394fcd3dd39ffb4f0e0382b026122..0000000000000000000000000000000000000000 --- a/zrpc/src/peer2.rs +++ /dev/null @@ -1,470 +0,0 @@ -use crate::{ - proto::{self, EnvelopedMessage, MessageStream, RequestMessage}, - ConnectionId, PeerId, Receipt, -}; -use anyhow::{anyhow, Context, Result}; -use async_lock::{Mutex, RwLock}; -use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; -use futures::{FutureExt, StreamExt}; -use postage::{ - mpsc, - prelude::{Sink as _, Stream as _}, -}; -use std::{ - any::Any, - collections::HashMap, - future::Future, - sync::{ - atomic::{self, AtomicU32}, - Arc, - }, -}; - -pub struct Peer { - connections: RwLock>, - next_connection_id: AtomicU32, -} - -#[derive(Clone)] -struct Connection { - outgoing_tx: mpsc::Sender, - next_message_id: Arc, - response_channels: Arc>>>, -} - -impl Peer { - pub fn new() -> Arc { - Arc::new(Self { - connections: Default::default(), - next_connection_id: Default::default(), - }) - } - - pub async fn add_connection( - self: &Arc, - conn: Conn, - ) -> ( - ConnectionId, - impl Future> + Send, - mpsc::Receiver>, - ) - where - Conn: futures::Sink - + futures::Stream> - + Send - + Unpin, - { - let (tx, rx) = conn.split(); - let connection_id = ConnectionId( - self.next_connection_id - .fetch_add(1, atomic::Ordering::SeqCst), - ); - let (mut incoming_tx, incoming_rx) = mpsc::channel(64); - let (outgoing_tx, mut outgoing_rx) = mpsc::channel(64); - let connection = Connection { - outgoing_tx, - next_message_id: Default::default(), - response_channels: Default::default(), - }; - let mut writer = MessageStream::new(tx); - let mut reader = MessageStream::new(rx); - - let response_channels = connection.response_channels.clone(); - let handle_io = async move { - loop { - let read_message = reader.read_message().fuse(); - futures::pin_mut!(read_message); - loop { - futures::select_biased! { - incoming = read_message => match incoming { - Ok(incoming) => { - if let Some(responding_to) = incoming.responding_to { - let channel = response_channels.lock().await.remove(&responding_to); - if let Some(mut tx) = channel { - tx.send(incoming).await.ok(); - } else { - log::warn!("received RPC response to unknown request {}", responding_to); - } - } else { - if let Some(envelope) = proto::build_typed_envelope(connection_id, incoming) { - if incoming_tx.send(envelope).await.is_err() { - response_channels.lock().await.clear(); - return Ok(()) - } - } else { - log::error!("unable to construct a typed envelope"); - } - } - - break; - } - Err(error) => { - response_channels.lock().await.clear(); - Err(error).context("received invalid RPC message")?; - } - }, - outgoing = outgoing_rx.recv().fuse() => match outgoing { - Some(outgoing) => { - if let Err(result) = writer.write_message(&outgoing).await { - response_channels.lock().await.clear(); - Err(result).context("failed to write RPC message")?; - } - } - None => { - response_channels.lock().await.clear(); - return Ok(()) - } - } - } - } - } - }; - - self.connections - .write() - .await - .insert(connection_id, connection); - - (connection_id, handle_io, incoming_rx) - } - - pub async fn disconnect(&self, connection_id: ConnectionId) { - self.connections.write().await.remove(&connection_id); - } - - pub async fn reset(&self) { - self.connections.write().await.clear(); - } - - pub fn request( - self: &Arc, - receiver_id: ConnectionId, - request: T, - ) -> impl Future> { - self.request_internal(None, receiver_id, request) - } - - pub fn forward_request( - self: &Arc, - sender_id: ConnectionId, - receiver_id: ConnectionId, - request: T, - ) -> impl Future> { - self.request_internal(Some(sender_id), receiver_id, request) - } - - pub fn request_internal( - self: &Arc, - original_sender_id: Option, - receiver_id: ConnectionId, - request: T, - ) -> impl Future> { - let this = self.clone(); - let (tx, mut rx) = mpsc::channel(1); - async move { - let mut connection = this.connection(receiver_id).await?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .response_channels - .lock() - .await - .insert(message_id, tx); - connection - .outgoing_tx - .send(request.into_envelope(message_id, None, original_sender_id.map(|id| id.0))) - .await - .map_err(|_| anyhow!("connection was closed"))?; - let response = rx - .recv() - .await - .ok_or_else(|| anyhow!("connection was closed"))?; - T::Response::from_envelope(response) - .ok_or_else(|| anyhow!("received response of the wrong type")) - } - } - - pub fn send( - self: &Arc, - receiver_id: ConnectionId, - message: T, - ) -> impl Future> { - let this = self.clone(); - async move { - let mut connection = this.connection(receiver_id).await?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .send(message.into_envelope(message_id, None, None)) - .await?; - Ok(()) - } - } - - pub fn forward_send( - self: &Arc, - sender_id: ConnectionId, - receiver_id: ConnectionId, - message: T, - ) -> impl Future> { - let this = self.clone(); - async move { - let mut connection = this.connection(receiver_id).await?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .send(message.into_envelope(message_id, None, Some(sender_id.0))) - .await?; - Ok(()) - } - } - - pub fn respond( - self: &Arc, - receipt: Receipt, - response: T::Response, - ) -> impl Future> { - let this = self.clone(); - async move { - let mut connection = this.connection(receipt.sender_id).await?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .send(response.into_envelope(message_id, Some(receipt.message_id), None)) - .await?; - Ok(()) - } - } - - fn connection( - self: &Arc, - connection_id: ConnectionId, - ) -> impl Future> { - let this = self.clone(); - async move { - let connections = this.connections.read().await; - let connection = connections - .get(&connection_id) - .ok_or_else(|| anyhow!("no such connection: {}", connection_id))?; - Ok(connection.clone()) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{test, TypedEnvelope}; - - #[test] - fn test_request_response() { - smol::block_on(async move { - // create 2 clients connected to 1 server - let server = Peer::new(); - let client1 = Peer::new(); - let client2 = Peer::new(); - - let (client1_to_server_conn, server_to_client_1_conn) = test::Channel::bidirectional(); - 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) = test::Channel::bidirectional(); - 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; - - smol::spawn(io_task1).detach(); - smol::spawn(io_task2).detach(); - smol::spawn(io_task3).detach(); - smol::spawn(io_task4).detach(); - smol::spawn(handle_messages(incoming1, server.clone())).detach(); - smol::spawn(handle_messages(incoming2, server.clone())).detach(); - - assert_eq!( - client1 - .request(client1_conn_id, proto::Ping { id: 1 },) - .await - .unwrap(), - proto::Pong { id: 1 } - ); - - assert_eq!( - client2 - .request(client2_conn_id, proto::Ping { id: 2 },) - .await - .unwrap(), - proto::Pong { id: 2 } - ); - - assert_eq!( - client1 - .request( - client1_conn_id, - proto::OpenBuffer { - worktree_id: 1, - path: "path/one".to_string(), - }, - ) - .await - .unwrap(), - proto::OpenBufferResponse { - buffer: Some(proto::Buffer { - id: 101, - content: "path/one content".to_string(), - history: vec![], - selections: vec![], - }), - } - ); - - assert_eq!( - client2 - .request( - client2_conn_id, - proto::OpenBuffer { - worktree_id: 2, - path: "path/two".to_string(), - }, - ) - .await - .unwrap(), - proto::OpenBufferResponse { - buffer: Some(proto::Buffer { - id: 102, - content: "path/two content".to_string(), - history: vec![], - selections: vec![], - }), - } - ); - - client1.disconnect(client1_conn_id).await; - client2.disconnect(client1_conn_id).await; - - async fn handle_messages( - mut messages: mpsc::Receiver>, - peer: Arc, - ) -> Result<()> { - while let Some(envelope) = messages.next().await { - if let Some(envelope) = envelope.downcast_ref::>() { - let receipt = envelope.receipt(); - peer.respond( - receipt, - proto::Pong { - id: envelope.payload.id, - }, - ) - .await? - } else if let Some(envelope) = - envelope.downcast_ref::>() - { - let message = &envelope.payload; - let receipt = envelope.receipt(); - let response = match message.path.as_str() { - "path/one" => { - assert_eq!(message.worktree_id, 1); - proto::OpenBufferResponse { - buffer: Some(proto::Buffer { - id: 101, - content: "path/one content".to_string(), - history: vec![], - selections: vec![], - }), - } - } - "path/two" => { - assert_eq!(message.worktree_id, 2); - proto::OpenBufferResponse { - buffer: Some(proto::Buffer { - id: 102, - content: "path/two content".to_string(), - history: vec![], - selections: vec![], - }), - } - } - _ => { - panic!("unexpected path {}", message.path); - } - }; - - peer.respond(receipt, response).await? - } else { - panic!("unknown message type"); - } - } - - Ok(()) - } - }); - } - - #[test] - fn test_disconnect() { - smol::block_on(async move { - let (client_conn, mut server_conn) = test::Channel::bidirectional(); - - let client = Peer::new(); - let (connection_id, io_handler, mut incoming) = - client.add_connection(client_conn).await; - - let (mut io_ended_tx, mut io_ended_rx) = postage::barrier::channel(); - smol::spawn(async move { - io_handler.await.ok(); - io_ended_tx.send(()).await.unwrap(); - }) - .detach(); - - let (mut messages_ended_tx, mut messages_ended_rx) = postage::barrier::channel(); - smol::spawn(async move { - incoming.next().await; - messages_ended_tx.send(()).await.unwrap(); - }) - .detach(); - - client.disconnect(connection_id).await; - - io_ended_rx.recv().await; - messages_ended_rx.recv().await; - assert!( - futures::SinkExt::send(&mut server_conn, WebSocketMessage::Binary(vec![])) - .await - .is_err() - ); - }); - } - - #[test] - fn test_io_error() { - smol::block_on(async move { - let (client_conn, server_conn) = test::Channel::bidirectional(); - drop(server_conn); - - let client = Peer::new(); - let (connection_id, io_handler, mut incoming) = - client.add_connection(client_conn).await; - smol::spawn(io_handler).detach(); - smol::spawn(async move { incoming.next().await }).detach(); - - let err = client - .request( - connection_id, - proto::Auth { - user_id: 42, - access_token: "token".to_string(), - }, - ) - .await - .unwrap_err(); - assert_eq!(err.to_string(), "connection was closed"); - }); - } -} From d398b96f564cf80b87c667e7330630ac804ee823 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Aug 2021 15:35:03 +0200 Subject: [PATCH 020/204] Re-register message handlers in RPC server --- server/src/rpc.rs | 305 +++++++++++++++++++++++++--------------------- 1 file changed, 164 insertions(+), 141 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index e628ef2c8131ce7b07d0db1a7e9b4d1605e20917..465cd96a9845fb27a1fc4def458f9351df3ec6f6 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -35,23 +35,26 @@ use zrpc::{ type ReplicaId = u16; -type Handler = Box< +type MessageHandler = Box< dyn Send + Sync - + Fn(&mut Option>, Arc) -> Option>, + + Fn( + &mut Option>, + Arc, + ) -> Option>>, >; #[derive(Default)] struct ServerBuilder { - handlers: Vec, + handlers: Vec, handler_types: HashSet, } impl ServerBuilder { - pub fn on_message(&mut self, handler: F) -> &mut Self + pub fn on_message(mut self, handler: F) -> Self where F: 'static + Send + Sync + Fn(Box>, Arc) -> Fut, - Fut: 'static + Send + Future, + Fut: 'static + Send + Future>, M: EnvelopedMessage, { if self.handler_types.insert(TypeId::of::()) { @@ -87,7 +90,7 @@ impl ServerBuilder { pub struct Server { rpc: Arc, state: Arc, - handlers: Vec, + handlers: Vec, } impl Server { @@ -119,10 +122,16 @@ impl Server { futures::select_biased! { message = next_message => { if let Some(message) = message { + let start_time = Instant::now(); + log::info!("RPC message received"); let mut message = Some(message); for handler in &this.handlers { if let Some(future) = (handler)(&mut message, this.clone()) { - future.await; + if let Err(err) = future.await { + log::error!("error handling message: {:?}", err); + } else { + log::info!("RPC message handled. duration:{:?}", start_time.elapsed()); + } break; } } @@ -336,26 +345,24 @@ impl State { pub fn build_server(state: &Arc, rpc: &Arc) -> Arc { ServerBuilder::default() - // .on_message(share_worktree) - // .on_message(join_worktree) - // .on_message(update_worktree) - // .on_message(close_worktree) - // .on_message(open_buffer) - // .on_message(close_buffer) - // .on_message(update_buffer) - // .on_message(buffer_saved) - // .on_message(save_buffer) - // .on_message(get_channels) - // .on_message(get_users) - // .on_message(join_channel) - // .on_message(send_channel_message) + .on_message(share_worktree) + .on_message(join_worktree) + .on_message(update_worktree) + .on_message(close_worktree) + .on_message(open_buffer) + .on_message(close_buffer) + .on_message(update_buffer) + .on_message(buffer_saved) + .on_message(save_buffer) + .on_message(get_channels) + .on_message(get_users) + .on_message(join_channel) + .on_message(send_channel_message) .build(rpc, state) } pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { let server = build_server(app.state(), rpc); - - let rpc = rpc.clone(); app.at("/rpc").with(auth::VerifyToken).get(move |request: Request>| { let user_id = request.ext::().copied(); let server = server.clone(); @@ -399,11 +406,10 @@ pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { } async fn share_worktree( - mut request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + mut request: Box>, + server: Arc, ) -> tide::Result<()> { - let mut state = state.rpc.write().await; + let mut state = server.state.rpc.write().await; let worktree_id = state.next_worktree_id; state.next_worktree_id += 1; let access_token = random_token(); @@ -428,26 +434,27 @@ async fn share_worktree( }, ); - rpc.respond( - request.receipt(), - proto::ShareWorktreeResponse { - worktree_id, - access_token, - }, - ) - .await?; + server + .rpc + .respond( + request.receipt(), + proto::ShareWorktreeResponse { + worktree_id, + access_token, + }, + ) + .await?; Ok(()) } async fn join_worktree( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { let worktree_id = request.payload.worktree_id; let access_token = &request.payload.access_token; - let mut state = state.rpc.write().await; + let mut state = server.state.rpc.write().await; if let Some((peer_replica_id, worktree)) = state.join_worktree(request.sender_id, worktree_id, access_token) { @@ -468,7 +475,7 @@ async fn join_worktree( } broadcast(request.sender_id, worktree.connection_ids(), |conn_id| { - rpc.send( + server.rpc.send( conn_id, proto::AddPeer { worktree_id, @@ -480,42 +487,45 @@ async fn join_worktree( ) }) .await?; - rpc.respond( - request.receipt(), - proto::OpenWorktreeResponse { - worktree_id, - worktree: Some(proto::Worktree { - root_name: worktree.root_name.clone(), - entries: worktree.entries.values().cloned().collect(), - }), - replica_id: peer_replica_id as u32, - peers, - }, - ) - .await?; + server + .rpc + .respond( + request.receipt(), + proto::OpenWorktreeResponse { + worktree_id, + worktree: Some(proto::Worktree { + root_name: worktree.root_name.clone(), + entries: worktree.entries.values().cloned().collect(), + }), + replica_id: peer_replica_id as u32, + peers, + }, + ) + .await?; } else { - rpc.respond( - request.receipt(), - proto::OpenWorktreeResponse { - worktree_id, - worktree: None, - replica_id: 0, - peers: Vec::new(), - }, - ) - .await?; + server + .rpc + .respond( + request.receipt(), + proto::OpenWorktreeResponse { + worktree_id, + worktree: None, + replica_id: 0, + peers: Vec::new(), + }, + ) + .await?; } Ok(()) } async fn update_worktree( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { { - let mut state = state.rpc.write().await; + let mut state = server.state.rpc.write().await; let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; for entry_id in &request.payload.removed_entries { worktree.entries.remove(&entry_id); @@ -526,18 +536,17 @@ async fn update_worktree( } } - broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await?; + broadcast_in_worktree(request.payload.worktree_id, &request, &server).await?; Ok(()) } async fn close_worktree( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { let connection_ids; { - let mut state = state.rpc.write().await; + let mut state = server.state.rpc.write().await; let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; connection_ids = worktree.connection_ids(); if worktree.host_connection_id == Some(request.sender_id) { @@ -548,7 +557,7 @@ async fn close_worktree( } broadcast(request.sender_id, connection_ids, |conn_id| { - rpc.send( + server.rpc.send( conn_id, proto::RemovePeer { worktree_id: request.payload.worktree_id, @@ -562,53 +571,55 @@ async fn close_worktree( } async fn open_buffer( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { let receipt = request.receipt(); let worktree_id = request.payload.worktree_id; - let host_connection_id = state + let host_connection_id = server + .state .rpc .read() .await .read_worktree(worktree_id, request.sender_id)? .host_connection_id()?; - let response = rpc + let response = server + .rpc .forward_request(request.sender_id, host_connection_id, request.payload) .await?; - rpc.respond(receipt, response).await?; + server.rpc.respond(receipt, response).await?; Ok(()) } async fn close_buffer( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { - let host_connection_id = state + let host_connection_id = server + .state .rpc .read() .await .read_worktree(request.payload.worktree_id, request.sender_id)? .host_connection_id()?; - rpc.forward_send(request.sender_id, host_connection_id, request.payload) + server + .rpc + .forward_send(request.sender_id, host_connection_id, request.payload) .await?; Ok(()) } async fn save_buffer( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { let host; let guests; { - let state = state.rpc.read().await; + let state = server.state.rpc.read().await; let worktree = state.read_worktree(request.payload.worktree_id, request.sender_id)?; host = worktree.host_connection_id()?; guests = worktree @@ -620,17 +631,19 @@ async fn save_buffer( let sender = request.sender_id; let receipt = request.receipt(); - let response = rpc + let response = server + .rpc .forward_request(sender, host, request.payload.clone()) .await?; broadcast(host, guests, |conn_id| { let response = response.clone(); + let server = &server; async move { if conn_id == sender { - rpc.respond(receipt, response).await + server.rpc.respond(receipt, response).await } else { - rpc.forward_send(host, conn_id, response).await + server.rpc.forward_send(host, conn_id, response).await } } }) @@ -640,61 +653,62 @@ async fn save_buffer( } async fn update_buffer( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { - broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await + broadcast_in_worktree(request.payload.worktree_id, &request, &server).await } async fn buffer_saved( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { - broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await + broadcast_in_worktree(request.payload.worktree_id, &request, &server).await } async fn get_channels( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { - let user_id = state + let user_id = server + .state .rpc .read() .await .user_id_for_connection(request.sender_id)?; - let channels = state.db.get_channels_for_user(user_id).await?; - rpc.respond( - request.receipt(), - proto::GetChannelsResponse { - channels: channels - .into_iter() - .map(|chan| proto::Channel { - id: chan.id.to_proto(), - name: chan.name, - }) - .collect(), - }, - ) - .await?; + let channels = server.state.db.get_channels_for_user(user_id).await?; + server + .rpc + .respond( + request.receipt(), + proto::GetChannelsResponse { + channels: channels + .into_iter() + .map(|chan| proto::Channel { + id: chan.id.to_proto(), + name: chan.name, + }) + .collect(), + }, + ) + .await?; Ok(()) } async fn get_users( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { - let user_id = state + let user_id = server + .state .rpc .read() .await .user_id_for_connection(request.sender_id)?; let receipt = request.receipt(); let user_ids = request.payload.user_ids.into_iter().map(UserId::from_proto); - let users = state + let users = server + .state .db .get_users_by_ids(user_id, user_ids) .await? @@ -705,23 +719,26 @@ async fn get_users( avatar_url: String::new(), }) .collect(); - rpc.respond(receipt, proto::GetUsersResponse { users }) + server + .rpc + .respond(receipt, proto::GetUsersResponse { users }) .await?; Ok(()) } async fn join_channel( - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { - let user_id = state + let user_id = server + .state .rpc .read() .await .user_id_for_connection(request.sender_id)?; let channel_id = ChannelId::from_proto(request.payload.channel_id); - if !state + if !server + .state .db .can_user_access_channel(user_id, channel_id) .await? @@ -729,12 +746,14 @@ async fn join_channel( Err(anyhow!("access denied"))?; } - state + server + .state .rpc .write() .await .join_channel(request.sender_id, channel_id); - let messages = state + let messages = server + .state .db .get_recent_channel_messages(channel_id, 50) .await? @@ -746,21 +765,22 @@ async fn join_channel( sender_id: msg.sender_id.to_proto(), }) .collect(); - rpc.respond(request.receipt(), proto::JoinChannelResponse { messages }) + server + .rpc + .respond(request.receipt(), proto::JoinChannelResponse { messages }) .await?; Ok(()) } async fn send_channel_message( - request: TypedEnvelope, - peer: &Arc, - app: &Arc, + request: Box>, + server: Arc, ) -> tide::Result<()> { let channel_id = ChannelId::from_proto(request.payload.channel_id); let user_id; let connection_ids; { - let state = app.rpc.read().await; + let state = server.state.rpc.read().await; user_id = state.user_id_for_connection(request.sender_id)?; if let Some(channel) = state.channels.get(&channel_id) { connection_ids = channel.connection_ids(); @@ -770,7 +790,8 @@ async fn send_channel_message( } let timestamp = OffsetDateTime::now_utc(); - let message_id = app + let message_id = server + .state .db .create_channel_message(channel_id, user_id, &request.payload.body, timestamp) .await?; @@ -784,7 +805,7 @@ async fn send_channel_message( }), }; broadcast(request.sender_id, connection_ids, |conn_id| { - peer.send(conn_id, message.clone()) + server.rpc.send(conn_id, message.clone()) }) .await?; @@ -793,11 +814,11 @@ async fn send_channel_message( async fn broadcast_in_worktree( worktree_id: u64, - request: TypedEnvelope, - rpc: &Arc, - state: &Arc, + request: &TypedEnvelope, + server: &Arc, ) -> tide::Result<()> { - let connection_ids = state + let connection_ids = server + .state .rpc .read() .await @@ -805,7 +826,9 @@ async fn broadcast_in_worktree( .connection_ids(); broadcast(request.sender_id, connection_ids, |conn_id| { - rpc.forward_send(request.sender_id, conn_id, request.payload.clone()) + server + .rpc + .forward_send(request.sender_id, conn_id, request.payload.clone()) }) .await?; From 5338b30c0090d6e2a5ffb3938efe6ace93e6e2dc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Aug 2021 19:38:17 +0200 Subject: [PATCH 021/204] Remove remaining instances of router Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- server/src/rpc.rs | 109 ++++++++++++++----------------- server/src/tests.rs | 39 +++++------- zed/src/channel.rs | 10 +-- zed/src/main.rs | 4 +- zed/src/menus.rs | 4 +- zed/src/rpc.rs | 148 +++++++++++++++++++++++++++++++------------ zed/src/test.rs | 2 +- zed/src/util.rs | 8 +-- zed/src/workspace.rs | 6 +- zed/src/worktree.rs | 47 +++++++------- zrpc/src/peer.rs | 8 +-- zrpc/src/proto.rs | 29 ++++++++- 12 files changed, 242 insertions(+), 172 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 465cd96a9845fb27a1fc4def458f9351df3ec6f6..77a50aceaca1770588011e625688cbf3575f8a2b 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -29,7 +29,7 @@ use tide::{ use time::OffsetDateTime; use zrpc::{ auth::random_token, - proto::{self, EnvelopedMessage}, + proto::{self, AnyTypedEnvelope, EnvelopedMessage}, ConnectionId, Peer, TypedEnvelope, }; @@ -38,16 +38,12 @@ type ReplicaId = u16; type MessageHandler = Box< dyn Send + Sync - + Fn( - &mut Option>, - Arc, - ) -> Option>>, + + Fn(Box, Arc) -> BoxFuture<'static, tide::Result<()>>, >; #[derive(Default)] struct ServerBuilder { - handlers: Vec, - handler_types: HashSet, + handlers: HashMap, } impl ServerBuilder { @@ -57,24 +53,17 @@ impl ServerBuilder { Fut: 'static + Send + Future>, M: EnvelopedMessage, { - if self.handler_types.insert(TypeId::of::()) { + let prev_handler = self.handlers.insert( + TypeId::of::(), + Box::new(move |envelope, server| { + let envelope = envelope.into_any().downcast::>().unwrap(); + (handler)(envelope, server).boxed() + }), + ); + if prev_handler.is_some() { panic!("registered a handler for the same message twice"); } - self.handlers - .push(Box::new(move |untyped_envelope, server| { - if let Some(typed_envelope) = untyped_envelope.take() { - match typed_envelope.downcast::>() { - Ok(typed_envelope) => Some((handler)(typed_envelope, server).boxed()), - Err(envelope) => { - *untyped_envelope = Some(envelope); - None - } - } - } else { - None - } - })); self } @@ -90,16 +79,17 @@ impl ServerBuilder { pub struct Server { rpc: Arc, state: Arc, - handlers: Vec, + handlers: HashMap, } impl Server { - pub async fn handle_connection( + pub fn handle_connection( self: &Arc, connection: Conn, addr: String, user_id: UserId, - ) where + ) -> impl Future + where Conn: 'static + futures::Sink + futures::Stream> @@ -107,54 +97,51 @@ impl Server { + Unpin, { let this = self.clone(); - let (connection_id, handle_io, mut incoming_rx) = this.rpc.add_connection(connection).await; - this.state - .rpc - .write() - .await - .add_connection(connection_id, user_id); - - let handle_io = handle_io.fuse(); - futures::pin_mut!(handle_io); - loop { - let next_message = incoming_rx.recv().fuse(); - futures::pin_mut!(next_message); - futures::select_biased! { - message = next_message => { - if let Some(message) = message { - let start_time = Instant::now(); - log::info!("RPC message received"); - let mut message = Some(message); - for handler in &this.handlers { - if let Some(future) = (handler)(&mut message, this.clone()) { - if let Err(err) = future.await { + async move { + let (connection_id, handle_io, mut incoming_rx) = + this.rpc.add_connection(connection).await; + this.state + .rpc + .write() + .await + .add_connection(connection_id, user_id); + + let handle_io = handle_io.fuse(); + futures::pin_mut!(handle_io); + loop { + let next_message = incoming_rx.recv().fuse(); + futures::pin_mut!(next_message); + futures::select_biased! { + message = next_message => { + if let Some(message) = message { + let start_time = Instant::now(); + log::info!("RPC message received: {}", message.payload_type_name()); + if let Some(handler) = this.handlers.get(&message.payload_type_id()) { + if let Err(err) = (handler)(message, this.clone()).await { log::error!("error handling message: {:?}", err); } else { log::info!("RPC message handled. duration:{:?}", start_time.elapsed()); } - break; + } else { + log::warn!("unhandled message: {}", message.payload_type_name()); } + } else { + log::info!("rpc connection closed {:?}", addr); + break; } - - if let Some(message) = message { - log::warn!("unhandled message: {:?}", message); + } + handle_io = handle_io => { + if let Err(err) = handle_io { + log::error!("error handling rpc connection {:?} - {:?}", addr, err); } - } else { - log::info!("rpc connection closed {:?}", addr); break; } } - handle_io = handle_io => { - if let Err(err) = handle_io { - log::error!("error handling rpc connection {:?} - {:?}", addr, err); - } - break; - } } - } - if let Err(err) = this.rpc.sign_out(connection_id, &this.state).await { - log::error!("error signing out connection {:?} - {:?}", addr, err); + if let Err(err) = this.rpc.sign_out(connection_id, &this.state).await { + log::error!("error signing out connection {:?} - {:?}", addr, err); + } } } } diff --git a/server/src/tests.rs b/server/src/tests.rs index 18a7375f862489b0c4b323c762b2ef08fe1da26b..5df19aa530726d3867d86f3cc7a7dc8aa3b56746 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -1,9 +1,7 @@ use crate::{ auth, db::{self, UserId}, - github, - rpc::{self, build_server}, - AppState, Config, + github, rpc, AppState, Config, }; use async_std::task; use gpui::TestAppContext; @@ -28,6 +26,8 @@ use zrpc::Peer; #[gpui::test] async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + tide::log::start(); + let (window_b, _) = cx_b.add_window(|_| EmptyView); let settings = settings::channel(&cx_b.font_cache()).unwrap().1; let lang_registry = Arc::new(LanguageRegistry::new()); @@ -514,9 +514,9 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { .await .unwrap(); - let channels_a = client_a.get_channels().await; - assert_eq!(channels_a.len(), 1); - assert_eq!(channels_a[0].read(&cx_a).name(), "test-channel"); + // let channels_a = client_a.get_channels().await; + // assert_eq!(channels_a.len(), 1); + // assert_eq!(channels_a[0].read(&cx_a).name(), "test-channel"); // assert_eq!( // db.get_recent_channel_messages(channel_id, 50) @@ -530,8 +530,8 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { struct TestServer { peer: Arc, app_state: Arc, + server: Arc, db_name: String, - router: Arc, } impl TestServer { @@ -540,36 +540,27 @@ impl TestServer { let db_name = format!("zed-test-{}", rng.gen::()); let app_state = Self::build_app_state(&db_name).await; let peer = Peer::new(); - let mut router = Router::new(); - build_server(&mut router, &app_state, &peer); + let server = rpc::build_server(&app_state, &peer); Self { peer, - router: Arc::new(router), app_state, + server, db_name, } } async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> (UserId, Client) { let user_id = self.app_state.db.create_user(name, false).await.unwrap(); - let lang_registry = Arc::new(LanguageRegistry::new()); - let client = Client::new(lang_registry.clone()); - let mut client_router = ForegroundRouter::new(); - cx.update(|cx| zed::worktree::init(cx, &client, &mut client_router)); - + let client = Client::new(); let (client_conn, server_conn) = Channel::bidirectional(); cx.background() - .spawn(rpc::handle_connection( - self.peer.clone(), - self.router.clone(), - self.app_state.clone(), - name.to_string(), - server_conn, - user_id, - )) + .spawn( + self.server + .handle_connection(server_conn, name.to_string(), user_id), + ) .detach(); client - .add_connection(client_conn, Arc::new(client_router), cx.to_async()) + .add_connection(client_conn, cx.to_async()) .await .unwrap(); diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 2aa2a966eea928ed1bc835de45385d7daed0fdd9..60ff50489c5bc18a94a04b87496a6c95d24eea3a 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,6 +1,6 @@ use crate::rpc::{self, Client}; use anyhow::Result; -use gpui::{Entity, ModelContext, Task, WeakModelHandle}; +use gpui::{Entity, ModelContext, WeakModelHandle}; use std::{ collections::{HashMap, VecDeque}, sync::Arc, @@ -22,7 +22,7 @@ pub struct Channel { first_message_id: Option, messages: Option>, rpc: Arc, - _message_handler: Task<()>, + _subscription: rpc::Subscription, } pub struct ChannelMessage { @@ -50,20 +50,20 @@ impl Entity for Channel { impl Channel { pub fn new(details: ChannelDetails, rpc: Arc, cx: &mut ModelContext) -> Self { - let _message_handler = rpc.subscribe_from_model(details.id, cx, Self::handle_message_sent); + let _subscription = rpc.subscribe_from_model(details.id, cx, Self::handle_message_sent); Self { details, rpc, first_message_id: None, messages: None, - _message_handler, + _subscription, } } fn handle_message_sent( &mut self, - message: &TypedEnvelope, + message: TypedEnvelope, rpc: rpc::Client, cx: &mut ModelContext, ) -> Result<()> { diff --git a/zed/src/main.rs b/zed/src/main.rs index a831893614ebfd4891ab656d60313d16eac30f34..f087109c9938ac22446037d2425c36feff7dbe5a 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -13,7 +13,6 @@ use zed::{ workspace::{self, OpenParams}, AppState, }; -use zrpc::ForegroundRouter; fn main() { init_logger(); @@ -31,8 +30,7 @@ fn main() { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, themes, - rpc_router: Arc::new(ForegroundRouter::new()), - rpc: rpc::Client::new(languages), + rpc: rpc::Client::new(), fs: Arc::new(RealFs), }; diff --git a/zed/src/menus.rs b/zed/src/menus.rs index 86839e4d3f26e39e5a938a7b8e80af0920f4e010..227f0b9efcbb825ba6a6bd2545cc780676b2ff25 100644 --- a/zed/src/menus.rs +++ b/zed/src/menus.rs @@ -19,13 +19,13 @@ pub fn menus(state: &Arc) -> Vec> { name: "Share", keystroke: None, action: "workspace:share_worktree", - arg: Some(Box::new(state.clone())), + arg: None, }, MenuItem::Action { name: "Join", keystroke: None, action: "workspace:join_worktree", - arg: Some(Box::new(state.clone())), + arg: None, }, MenuItem::Action { name: "Quit", diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 2bd9fd7f819e4bbe7faf3b73f2e98d8b748c35d6..1c0d6d0894fcaf78e7416a47c06356cedc1fea4d 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -1,15 +1,17 @@ -use crate::language::LanguageRegistry; use anyhow::{anyhow, Context, Result}; use async_tungstenite::tungstenite::http::Request; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; -use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, Task}; use lazy_static::lazy_static; -use smol::lock::RwLock; -use std::time::Duration; +use parking_lot::RwLock; +use postage::prelude::Stream; +use std::any::TypeId; +use std::collections::HashMap; +use std::sync::Weak; +use std::time::{Duration, Instant}; use std::{convert::TryFrom, future::Future, sync::Arc}; use surf::Url; -use zrpc::proto::EntityMessage; +use zrpc::proto::{AnyTypedEnvelope, EntityMessage}; pub use zrpc::{proto, ConnectionId, PeerId, TypedEnvelope}; use zrpc::{ proto::{EnvelopedMessage, RequestMessage}, @@ -24,22 +26,37 @@ lazy_static! { #[derive(Clone)] pub struct Client { peer: Arc, - pub state: Arc>, + state: Arc>, } +#[derive(Default)] pub struct ClientState { connection_id: Option, - pub languages: Arc, + entity_id_extractors: HashMap u64>>, + model_handlers: HashMap< + (TypeId, u64), + Box, &mut AsyncAppContext)>, + >, +} + +pub struct Subscription { + state: Weak>, + id: (TypeId, u64), +} + +impl Drop for Subscription { + fn drop(&mut self) { + if let Some(state) = self.state.upgrade() { + let _ = state.write().model_handlers.remove(&self.id).unwrap(); + } + } } impl Client { - pub fn new(languages: Arc) -> Self { + pub fn new() -> Self { Self { peer: Peer::new(), - state: Arc::new(RwLock::new(ClientState { - connection_id: None, - languages, - })), + state: Default::default(), } } @@ -48,31 +65,56 @@ impl Client { remote_id: u64, cx: &mut ModelContext, mut handler: F, - ) -> Task<()> + ) -> Subscription where T: EntityMessage, M: Entity, - F: 'static + FnMut(&mut M, &TypedEnvelope, Client, &mut ModelContext) -> Result<()>, + F: 'static + + Send + + Sync + + FnMut(&mut M, TypedEnvelope, Client, &mut ModelContext) -> Result<()>, { - let rpc = self.clone(); - let mut incoming = self.peer.subscribe::(); - cx.spawn_weak(|model, mut cx| async move { - while let Some(envelope) = incoming.next().await { - if envelope.payload.remote_entity_id() == remote_id { - if let Some(model) = model.upgrade(&cx) { - model.update(&mut cx, |model, cx| { - if let Err(error) = handler(model, &envelope, rpc.clone(), cx) { - log::error!("error handling message: {}", error) - } - }); - } + let subscription_id = (TypeId::of::(), remote_id); + let client = self.clone(); + let mut state = self.state.write(); + let model = cx.handle().downgrade(); + state + .entity_id_extractors + .entry(subscription_id.0) + .or_insert_with(|| { + Box::new(|envelope| { + let envelope = envelope + .as_any() + .downcast_ref::>() + .unwrap(); + envelope.payload.remote_entity_id() + }) + }); + let prev_handler = state.model_handlers.insert( + subscription_id, + Box::new(move |envelope, cx| { + if let Some(model) = model.upgrade(cx) { + let envelope = envelope.into_any().downcast::>().unwrap(); + model.update(cx, |model, cx| { + if let Err(error) = handler(model, *envelope, client.clone(), cx) { + log::error!("error handling message: {}", error) + } + }); } - } - }) + }), + ); + if prev_handler.is_some() { + panic!("registered a handler for the same entity twice") + } + + Subscription { + state: Arc::downgrade(&self.state), + id: subscription_id, + } } pub async fn log_in_and_connect(&self, cx: AsyncAppContext) -> surf::Result<()> { - if self.state.read().await.connection_id.is_some() { + if self.state.read().connection_id.is_some() { return Ok(()); } @@ -110,8 +152,39 @@ impl Client { + Unpin + Send, { - let (connection_id, handle_io, handle_messages) = self.peer.add_connection(conn).await; - cx.foreground().spawn(handle_messages).detach(); + let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await; + { + let mut cx = cx.clone(); + let state = self.state.clone(); + cx.foreground() + .spawn(async move { + while let Some(message) = incoming.recv().await { + let mut state = state.write(); + if let Some(extract_entity_id) = + state.entity_id_extractors.get(&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 start_time = Instant::now(); + log::info!("RPC client message {}", message.payload_type_name()); + (handler)(message, &mut cx); + log::info!( + "RPC message handled. duration:{:?}", + start_time.elapsed() + ); + } else { + log::info!("unhandled message {}", message.payload_type_name()); + } + } else { + log::info!("unhandled message {}", message.payload_type_name()); + } + } + }) + .detach(); + } cx.background() .spawn(async move { if let Err(error) = handle_io.await { @@ -119,7 +192,7 @@ impl Client { } }) .detach(); - self.state.write().await.connection_id = Some(connection_id); + self.state.write().connection_id = Some(connection_id); Ok(()) } @@ -200,27 +273,24 @@ impl Client { } pub async fn disconnect(&self) -> Result<()> { - let conn_id = self.connection_id().await?; + let conn_id = self.connection_id()?; self.peer.disconnect(conn_id).await; Ok(()) } - async fn connection_id(&self) -> Result { + fn connection_id(&self) -> Result { self.state .read() - .await .connection_id .ok_or_else(|| anyhow!("not connected")) } pub async fn send(&self, message: T) -> Result<()> { - self.peer.send(self.connection_id().await?, message).await + self.peer.send(self.connection_id()?, message).await } pub async fn request(&self, request: T) -> Result { - self.peer - .request(self.connection_id().await?, request) - .await + self.peer.request(self.connection_id()?, request).await } pub fn respond( diff --git a/zed/src/test.rs b/zed/src/test.rs index ce223691266c9a863a9ef9bcf5b3e3f863f4f364..3576681cd33dac3c17e3a4f5d1953bfa41e1450e 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -162,7 +162,7 @@ pub fn build_app_state(cx: &AppContext) -> Arc { settings, themes, languages: languages.clone(), - rpc: rpc::Client::new(languages), + rpc: rpc::Client::new(), fs: Arc::new(RealFs), }) } diff --git a/zed/src/util.rs b/zed/src/util.rs index ea9b544f9a3f9961b1efd8f077690f19aece6fdb..5bae8b7a99c69c318576f7528262bdbb9b1f34db 100644 --- a/zed/src/util.rs +++ b/zed/src/util.rs @@ -82,14 +82,12 @@ impl Iterator for RandomCharIter { } } -pub async fn log_async_errors(f: F) -> impl Future +pub async fn log_async_errors(f: F) where F: Future>, { - async { - if let Err(error) = f.await { - log::error!("{}", error) - } + if let Err(error) = f.await { + log::error!("{}", error) } } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index e074d57a64b49f92f63b875888770c64361801bd..8dfa7fc4da604eb63978c55e344283892d8a28de 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -108,7 +108,7 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { fn join_worktree(app_state: &Arc, cx: &mut MutableAppContext) { cx.add_window(|cx| { let mut view = Workspace::new(app_state.as_ref(), cx); - view.join_worktree(&app_state, cx); + view.join_worktree(&(), cx); view }); } @@ -725,7 +725,7 @@ impl Workspace { }; } - fn share_worktree(&mut self, app_state: &Arc, cx: &mut ViewContext) { + fn share_worktree(&mut self, _: &(), cx: &mut ViewContext) { let rpc = self.rpc.clone(); let platform = cx.platform(); @@ -757,7 +757,7 @@ impl Workspace { .detach(); } - fn join_worktree(&mut self, app_state: &Arc, cx: &mut ViewContext) { + fn join_worktree(&mut self, _: &(), cx: &mut ViewContext) { let rpc = self.rpc.clone(); let languages = self.languages.clone(); diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index cf60eb87cd3346236ec4f7bacfcfd48071589cab..18f057dbab0a5e818813422270689d238fcb7bfc 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -213,7 +213,7 @@ impl Worktree { .detach(); } - let _message_handlers = vec![ + let _subscriptions = vec![ rpc.subscribe_from_model(remote_id, cx, Self::handle_add_peer), rpc.subscribe_from_model(remote_id, cx, Self::handle_remove_peer), rpc.subscribe_from_model(remote_id, cx, Self::handle_update), @@ -234,7 +234,7 @@ impl Worktree { .map(|p| (PeerId(p.peer_id), p.replica_id as ReplicaId)) .collect(), languages, - _message_handlers, + _subscriptions, }) }) }); @@ -282,7 +282,7 @@ impl Worktree { pub fn handle_add_peer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, _: rpc::Client, cx: &mut ModelContext, ) -> Result<()> { @@ -294,7 +294,7 @@ impl Worktree { pub fn handle_remove_peer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, _: rpc::Client, cx: &mut ModelContext, ) -> Result<()> { @@ -306,7 +306,7 @@ impl Worktree { pub fn handle_update( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, _: rpc::Client, cx: &mut ModelContext, ) -> anyhow::Result<()> { @@ -317,7 +317,7 @@ impl Worktree { pub fn handle_open_buffer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, rpc: rpc::Client, cx: &mut ModelContext, ) -> anyhow::Result<()> { @@ -340,7 +340,7 @@ impl Worktree { pub fn handle_close_buffer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, _: rpc::Client, cx: &mut ModelContext, ) -> anyhow::Result<()> { @@ -396,7 +396,7 @@ impl Worktree { pub fn handle_update_buffer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, _: rpc::Client, cx: &mut ModelContext, ) -> Result<()> { @@ -443,7 +443,7 @@ impl Worktree { pub fn handle_save_buffer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, rpc: rpc::Client, cx: &mut ModelContext, ) -> Result<()> { @@ -485,7 +485,7 @@ impl Worktree { pub fn handle_buffer_saved( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, _: rpc::Client, cx: &mut ModelContext, ) -> Result<()> { @@ -791,7 +791,7 @@ impl LocalWorktree { pub fn open_remote_buffer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, cx: &mut ModelContext, ) -> Task> { let peer_id = envelope.original_sender_id(); @@ -818,11 +818,12 @@ impl LocalWorktree { pub fn close_remote_buffer( &mut self, - envelope: &TypedEnvelope, - _: &mut ModelContext, + envelope: TypedEnvelope, + cx: &mut ModelContext, ) -> Result<()> { if let Some(shared_buffers) = self.shared_buffers.get_mut(&envelope.original_sender_id()?) { shared_buffers.remove(&envelope.payload.buffer_id); + cx.notify(); } Ok(()) @@ -830,7 +831,7 @@ impl LocalWorktree { pub fn add_peer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { let peer = envelope @@ -847,7 +848,7 @@ impl LocalWorktree { pub fn remove_peer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { let peer_id = PeerId(envelope.payload.peer_id); @@ -994,7 +995,7 @@ impl LocalWorktree { .detach(); this.update(&mut cx, |worktree, cx| { - let _message_handlers = vec![ + let _subscriptions = vec![ rpc.subscribe_from_model(remote_id, cx, Worktree::handle_add_peer), rpc.subscribe_from_model(remote_id, cx, Worktree::handle_remove_peer), rpc.subscribe_from_model(remote_id, cx, Worktree::handle_open_buffer), @@ -1008,7 +1009,7 @@ impl LocalWorktree { rpc, remote_id: share_response.worktree_id, snapshots_tx: snapshots_to_send_tx, - _message_handlers, + _subscriptions, }); }); @@ -1068,7 +1069,7 @@ struct ShareState { rpc: rpc::Client, remote_id: u64, snapshots_tx: Sender, - _message_handlers: Vec>, + _subscriptions: Vec, } pub struct RemoteWorktree { @@ -1081,7 +1082,7 @@ pub struct RemoteWorktree { open_buffers: HashMap, peers: HashMap, languages: Arc, - _message_handlers: Vec>, + _subscriptions: Vec, } impl RemoteWorktree { @@ -1151,7 +1152,7 @@ impl RemoteWorktree { fn update_from_remote( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { let mut tx = self.updates_tx.clone(); @@ -1167,7 +1168,7 @@ impl RemoteWorktree { pub fn add_peer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { let peer = envelope @@ -1183,7 +1184,7 @@ impl RemoteWorktree { pub fn remove_peer( &mut self, - envelope: &TypedEnvelope, + envelope: TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { let peer_id = PeerId(envelope.payload.peer_id); @@ -2761,7 +2762,7 @@ mod tests { replica_id: 1, peers: Vec::new(), }, - rpc::Client::new(Default::default()), + rpc::Client::new(), Default::default(), &mut cx.to_async(), ) diff --git a/zrpc/src/peer.rs b/zrpc/src/peer.rs index 315f56b316c05ad27cdbb22ea92676db0823654d..7048fcd0a1825ad1c91a38a63cacf8a62fc8ca0a 100644 --- a/zrpc/src/peer.rs +++ b/zrpc/src/peer.rs @@ -1,4 +1,4 @@ -use crate::proto::{self, EnvelopedMessage, MessageStream, RequestMessage}; +use crate::proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage}; use anyhow::{anyhow, Context, Result}; use async_lock::{Mutex, RwLock}; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; @@ -8,7 +8,6 @@ use postage::{ prelude::{Sink as _, Stream as _}, }; use std::{ - any::Any, collections::HashMap, fmt, future::Future, @@ -105,7 +104,7 @@ impl Peer { ) -> ( ConnectionId, impl Future> + Send, - mpsc::Receiver>, + mpsc::Receiver>, ) where Conn: futures::Sink @@ -409,10 +408,11 @@ mod tests { client2.disconnect(client1_conn_id).await; async fn handle_messages( - mut messages: mpsc::Receiver>, + mut messages: mpsc::Receiver>, peer: Arc, ) -> Result<()> { while let Some(envelope) = messages.next().await { + let envelope = envelope.into_any(); if let Some(envelope) = envelope.downcast_ref::>() { let receipt = envelope.receipt(); peer.respond( diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index d8c794fd63ab8cef1b1f2c213ae6242906be1850..271fa8e29e5a63d7f7ae0a5e3b09b21a022fdbc9 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -3,7 +3,7 @@ use anyhow::Result; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; use futures::{SinkExt as _, StreamExt as _}; use prost::Message; -use std::any::Any; +use std::any::{Any, TypeId}; use std::{ io, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -31,9 +31,34 @@ pub trait RequestMessage: EnvelopedMessage { type Response: EnvelopedMessage; } +pub trait AnyTypedEnvelope: 'static + Send + Sync { + fn payload_type_id(&self) -> TypeId; + fn payload_type_name(&self) -> &'static str; + fn as_any(&self) -> &dyn Any; + fn into_any(self: Box) -> Box; +} + +impl AnyTypedEnvelope for TypedEnvelope { + fn payload_type_id(&self) -> TypeId { + TypeId::of::() + } + + fn payload_type_name(&self) -> &'static str { + T::NAME + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn into_any(self: Box) -> Box { + self + } +} + macro_rules! messages { ($($name:ident),* $(,)?) => { - pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { + pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { match envelope.payload { $(Some(envelope::Payload::$name(payload)) => { Some(Box::new(TypedEnvelope { From 6288ac2e21f1af405297fad02284c2f07990d122 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Aug 2021 19:58:13 +0200 Subject: [PATCH 022/204] Fix double borrow panic Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- zed/src/worktree.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 18f057dbab0a5e818813422270689d238fcb7bfc..d24c1b412eb8596dd30088a21f8eb2b1d5b497df 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -459,7 +459,9 @@ impl Worktree { let receipt = envelope.receipt(); let worktree_id = envelope.payload.worktree_id; let buffer_id = envelope.payload.buffer_id; - let save = buffer.update(cx, |buffer, cx| buffer.save(cx))?; + let save = cx.spawn(|_, mut cx| async move { + buffer.update(&mut cx, |buffer, cx| buffer.save(cx))?.await + }); cx.background() .spawn(log_async_errors(async move { From 266867b516e70bba225987dd3a17081f8daa9c84 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 19 Aug 2021 12:17:32 -0700 Subject: [PATCH 023/204] Send SaveBuffer message on foreground thread This ensures that it gets sent *after* any other messages for which we have already spawned a foreground send. Co-Authored-By: Nathan Sobo --- zed/src/worktree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index d24c1b412eb8596dd30088a21f8eb2b1d5b497df..723487ad4ae00640bddb032520374a3668199653 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1639,7 +1639,7 @@ impl File { Worktree::Remote(worktree) => { let rpc = worktree.rpc.clone(); let worktree_id = worktree.remote_id; - cx.background().spawn(async move { + cx.foreground().spawn(async move { let response = rpc .request(proto::SaveBuffer { worktree_id, From 3631fbd874a0bf3b788d8b5f7581fa7fb0e51171 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 19 Aug 2021 12:17:52 -0700 Subject: [PATCH 024/204] Consolidate server's rpc state into the rpc::Server struct Co-Authored-By: Nathan Sobo --- server/src/auth.rs | 41 +- server/src/main.rs | 4 +- server/src/rpc.rs | 1130 +++++++++++++++++++++---------------------- server/src/tests.rs | 3 +- 4 files changed, 569 insertions(+), 609 deletions(-) diff --git a/server/src/auth.rs b/server/src/auth.rs index d61428fa371a0f26bded6a42250aab37a1e1d9f5..5a3e301d27537a1e031d804341af29d071afdd95 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -2,7 +2,7 @@ use super::{ db::{self, UserId}, errors::TideResultExt, }; -use crate::{github, rpc, AppState, Request, RequestExt as _}; +use crate::{github, AppState, Request, RequestExt as _}; use anyhow::{anyhow, Context}; use async_trait::async_trait; pub use oauth2::basic::BasicClient as Client; @@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize}; use std::{borrow::Cow, convert::TryFrom, sync::Arc}; use surf::Url; use tide::Server; -use zrpc::{auth as zed_auth, proto, Peer}; +use zrpc::auth as zed_auth; static CURRENT_GITHUB_USER: &'static str = "current_github_user"; static GITHUB_AUTH_URL: &'static str = "https://github.com/login/oauth/authorize"; @@ -100,43 +100,6 @@ impl RequestExt for Request { } } -#[async_trait] -pub trait PeerExt { - async fn sign_out( - self: &Arc, - connection_id: zrpc::ConnectionId, - state: &AppState, - ) -> tide::Result<()>; -} - -#[async_trait] -impl PeerExt for Peer { - async fn sign_out( - self: &Arc, - connection_id: zrpc::ConnectionId, - state: &AppState, - ) -> tide::Result<()> { - self.disconnect(connection_id).await; - let worktree_ids = state.rpc.write().await.remove_connection(connection_id); - for worktree_id in worktree_ids { - let state = state.rpc.read().await; - if let Some(worktree) = state.worktrees.get(&worktree_id) { - rpc::broadcast(connection_id, worktree.connection_ids(), |conn_id| { - self.send( - conn_id, - proto::RemovePeer { - worktree_id, - peer_id: connection_id.0, - }, - ) - }) - .await?; - } - } - Ok(()) - } -} - pub fn build_client(client_id: &str, client_secret: &str) -> Client { Client::new( ClientId::new(client_id.to_string()), diff --git a/server/src/main.rs b/server/src/main.rs index ec153bea8fd535d513be81aab3f0b0bfc862d3b8..b98c8b0d04e8cdda766f95d3d2ed37d9bad9aad1 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -14,7 +14,7 @@ mod tests; use self::errors::TideResultExt as _; use anyhow::{Context, Result}; -use async_std::{net::TcpListener, sync::RwLock as AsyncRwLock}; +use async_std::net::TcpListener; use async_trait::async_trait; use auth::RequestExt as _; use db::{Db, DbOptions}; @@ -51,7 +51,6 @@ pub struct AppState { auth_client: auth::Client, github_client: Arc, repo_client: github::RepoClient, - rpc: AsyncRwLock, config: Config, } @@ -76,7 +75,6 @@ impl AppState { auth_client: auth::build_client(&config.github_client_id, &config.github_client_secret), github_client, repo_client, - rpc: Default::default(), config, }; this.register_partials(); diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 77a50aceaca1770588011e625688cbf3575f8a2b..d107e3606b0ba6097bcc5013011c5b8800a9b70e 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1,10 +1,10 @@ use super::{ - auth::{self, PeerExt as _}, + auth, db::{ChannelId, UserId}, AppState, }; use anyhow::anyhow; -use async_std::task; +use async_std::{sync::RwLock, task}; use async_tungstenite::{ tungstenite::{protocol::Role, Error as WebSocketError, Message as WebSocketMessage}, WebSocketStream, @@ -13,7 +13,7 @@ use futures::{future::BoxFuture, FutureExt}; use postage::prelude::Stream as _; use sha1::{Digest as _, Sha1}; use std::{ - any::{Any, TypeId}, + any::TypeId, collections::{HashMap, HashSet}, future::Future, mem, @@ -38,51 +38,90 @@ type ReplicaId = u16; type MessageHandler = Box< dyn Send + Sync - + Fn(Box, Arc) -> BoxFuture<'static, tide::Result<()>>, + + Fn(Arc, Box) -> BoxFuture<'static, tide::Result<()>>, >; -#[derive(Default)] -struct ServerBuilder { +pub struct Server { + peer: Arc, + state: RwLock, + app_state: Arc, handlers: HashMap, } -impl ServerBuilder { - pub fn on_message(mut self, handler: F) -> Self +#[derive(Default)] +struct ServerState { + connections: HashMap, + pub worktrees: HashMap, + channels: HashMap, + next_worktree_id: u64, +} + +struct Connection { + user_id: UserId, + worktrees: HashSet, + channels: HashSet, +} + +struct Worktree { + host_connection_id: Option, + guest_connection_ids: HashMap, + active_replica_ids: HashSet, + access_token: String, + root_name: String, + entries: HashMap, +} + +#[derive(Default)] +struct Channel { + connection_ids: HashSet, +} + +impl Server { + pub fn new(app_state: Arc, peer: Arc) -> Arc { + let mut server = Server { + peer, + app_state, + state: Default::default(), + handlers: Default::default(), + }; + + server + .add_handler(Server::share_worktree) + .add_handler(Server::join_worktree) + .add_handler(Server::update_worktree) + .add_handler(Server::close_worktree) + .add_handler(Server::open_buffer) + .add_handler(Server::close_buffer) + .add_handler(Server::update_buffer) + .add_handler(Server::buffer_saved) + .add_handler(Server::save_buffer) + .add_handler(Server::get_channels) + .add_handler(Server::get_users) + .add_handler(Server::join_channel) + .add_handler(Server::send_channel_message); + + Arc::new(server) + } + + fn add_handler(&mut self, handler: F) -> &mut Self where - F: 'static + Send + Sync + Fn(Box>, Arc) -> Fut, + F: 'static + Send + Sync + Fn(Arc, TypedEnvelope) -> Fut, Fut: 'static + Send + Future>, M: EnvelopedMessage, { let prev_handler = self.handlers.insert( TypeId::of::(), - Box::new(move |envelope, server| { + Box::new(move |server, envelope| { let envelope = envelope.into_any().downcast::>().unwrap(); - (handler)(envelope, server).boxed() + (handler)(server, *envelope).boxed() }), ); if prev_handler.is_some() { panic!("registered a handler for the same message twice"); } - self } - pub fn build(self, rpc: &Arc, state: &Arc) -> Arc { - Arc::new(Server { - rpc: rpc.clone(), - state: state.clone(), - handlers: self.handlers, - }) - } -} - -pub struct Server { - rpc: Arc, - state: Arc, - handlers: HashMap, -} - -impl Server { pub fn handle_connection( self: &Arc, connection: Conn, @@ -99,12 +138,8 @@ impl Server { let this = self.clone(); async move { let (connection_id, handle_io, mut incoming_rx) = - this.rpc.add_connection(connection).await; - this.state - .rpc - .write() - .await - .add_connection(connection_id, user_id); + this.peer.add_connection(connection).await; + this.add_connection(connection_id, user_id).await; let handle_io = handle_io.fuse(); futures::pin_mut!(handle_io); @@ -117,7 +152,7 @@ impl Server { let start_time = Instant::now(); log::info!("RPC message received: {}", message.payload_type_name()); if let Some(handler) = this.handlers.get(&message.payload_type_id()) { - if let Err(err) = (handler)(message, this.clone()).await { + if let Err(err) = (handler)(this.clone(), message).await { log::error!("error handling message: {:?}", err); } else { log::info!("RPC message handled. duration:{:?}", start_time.elapsed()); @@ -139,67 +174,36 @@ impl Server { } } - if let Err(err) = this.rpc.sign_out(connection_id, &this.state).await { + if let Err(err) = this.sign_out(connection_id).await { log::error!("error signing out connection {:?} - {:?}", addr, err); } } } -} - -#[derive(Default)] -pub struct State { - connections: HashMap, - pub worktrees: HashMap, - channels: HashMap, - next_worktree_id: u64, -} - -struct Connection { - user_id: UserId, - worktrees: HashSet, - channels: HashSet, -} - -pub struct Worktree { - host_connection_id: Option, - guest_connection_ids: HashMap, - active_replica_ids: HashSet, - access_token: String, - root_name: String, - entries: HashMap, -} - -#[derive(Default)] -struct Channel { - connection_ids: HashSet, -} - -impl Worktree { - pub fn connection_ids(&self) -> Vec { - self.guest_connection_ids - .keys() - .copied() - .chain(self.host_connection_id) - .collect() - } - - fn host_connection_id(&self) -> tide::Result { - Ok(self - .host_connection_id - .ok_or_else(|| anyhow!("host disconnected from worktree"))?) - } -} -impl Channel { - fn connection_ids(&self) -> Vec { - self.connection_ids.iter().copied().collect() + async fn sign_out(self: &Arc, connection_id: zrpc::ConnectionId) -> tide::Result<()> { + self.peer.disconnect(connection_id).await; + let worktree_ids = self.remove_connection(connection_id).await; + for worktree_id in worktree_ids { + let state = self.state.read().await; + if let Some(worktree) = state.worktrees.get(&worktree_id) { + broadcast(connection_id, worktree.connection_ids(), |conn_id| { + self.peer.send( + conn_id, + proto::RemovePeer { + worktree_id, + peer_id: connection_id.0, + }, + ) + }) + .await?; + } + } + Ok(()) } -} -impl State { // Add a new connection associated with a given user. - pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId) { - self.connections.insert( + async fn add_connection(&self, connection_id: ConnectionId, user_id: UserId) { + self.state.write().await.connections.insert( connection_id, Connection { user_id, @@ -210,16 +214,17 @@ impl State { } // Remove the given connection and its association with any worktrees. - pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Vec { + async fn remove_connection(&self, connection_id: ConnectionId) -> Vec { let mut worktree_ids = Vec::new(); - if let Some(connection) = self.connections.remove(&connection_id) { + let mut state = self.state.write().await; + if let Some(connection) = state.connections.remove(&connection_id) { for channel_id in connection.channels { - if let Some(channel) = self.channels.get_mut(&channel_id) { + if let Some(channel) = state.channels.get_mut(&channel_id) { channel.connection_ids.remove(&connection_id); } } for worktree_id in connection.worktrees { - if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { + if let Some(worktree) = state.worktrees.get_mut(&worktree_id) { if worktree.host_connection_id == Some(connection_id) { worktree_ids.push(worktree_id); } else if let Some(replica_id) = @@ -234,6 +239,444 @@ impl State { worktree_ids } + async fn share_worktree( + self: Arc, + mut request: TypedEnvelope, + ) -> tide::Result<()> { + let mut state = self.state.write().await; + let worktree_id = state.next_worktree_id; + state.next_worktree_id += 1; + let access_token = random_token(); + let worktree = request + .payload + .worktree + .as_mut() + .ok_or_else(|| anyhow!("missing worktree"))?; + let entries = mem::take(&mut worktree.entries) + .into_iter() + .map(|entry| (entry.id, entry)) + .collect(); + state.worktrees.insert( + worktree_id, + Worktree { + host_connection_id: Some(request.sender_id), + guest_connection_ids: Default::default(), + active_replica_ids: Default::default(), + access_token: access_token.clone(), + root_name: mem::take(&mut worktree.root_name), + entries, + }, + ); + + self.peer + .respond( + request.receipt(), + proto::ShareWorktreeResponse { + worktree_id, + access_token, + }, + ) + .await?; + Ok(()) + } + + async fn join_worktree( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let worktree_id = request.payload.worktree_id; + let access_token = &request.payload.access_token; + + let mut state = self.state.write().await; + if let Some((peer_replica_id, worktree)) = + state.join_worktree(request.sender_id, worktree_id, access_token) + { + let mut peers = Vec::new(); + if let Some(host_connection_id) = worktree.host_connection_id { + peers.push(proto::Peer { + peer_id: host_connection_id.0, + replica_id: 0, + }); + } + for (peer_conn_id, peer_replica_id) in &worktree.guest_connection_ids { + if *peer_conn_id != request.sender_id { + peers.push(proto::Peer { + peer_id: peer_conn_id.0, + replica_id: *peer_replica_id as u32, + }); + } + } + + broadcast(request.sender_id, worktree.connection_ids(), |conn_id| { + self.peer.send( + conn_id, + proto::AddPeer { + worktree_id, + peer: Some(proto::Peer { + peer_id: request.sender_id.0, + replica_id: peer_replica_id as u32, + }), + }, + ) + }) + .await?; + self.peer + .respond( + request.receipt(), + proto::OpenWorktreeResponse { + worktree_id, + worktree: Some(proto::Worktree { + root_name: worktree.root_name.clone(), + entries: worktree.entries.values().cloned().collect(), + }), + replica_id: peer_replica_id as u32, + peers, + }, + ) + .await?; + } else { + self.peer + .respond( + request.receipt(), + proto::OpenWorktreeResponse { + worktree_id, + worktree: None, + replica_id: 0, + peers: Vec::new(), + }, + ) + .await?; + } + + Ok(()) + } + + async fn update_worktree( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + { + let mut state = self.state.write().await; + let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; + for entry_id in &request.payload.removed_entries { + worktree.entries.remove(&entry_id); + } + + for entry in &request.payload.updated_entries { + worktree.entries.insert(entry.id, entry.clone()); + } + } + + self.broadcast_in_worktree(request.payload.worktree_id, &request) + .await?; + Ok(()) + } + + async fn close_worktree( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let connection_ids; + { + let mut state = self.state.write().await; + let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; + connection_ids = worktree.connection_ids(); + if worktree.host_connection_id == Some(request.sender_id) { + worktree.host_connection_id = None; + } else if let Some(replica_id) = + worktree.guest_connection_ids.remove(&request.sender_id) + { + worktree.active_replica_ids.remove(&replica_id); + } + } + + broadcast(request.sender_id, connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::RemovePeer { + worktree_id: request.payload.worktree_id, + peer_id: request.sender_id.0, + }, + ) + }) + .await?; + + Ok(()) + } + + async fn open_buffer( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receipt = request.receipt(); + let worktree_id = request.payload.worktree_id; + let host_connection_id = self + .state + .read() + .await + .read_worktree(worktree_id, request.sender_id)? + .host_connection_id()?; + + let response = self + .peer + .forward_request(request.sender_id, host_connection_id, request.payload) + .await?; + self.peer.respond(receipt, response).await?; + Ok(()) + } + + async fn close_buffer( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let host_connection_id = self + .state + .read() + .await + .read_worktree(request.payload.worktree_id, request.sender_id)? + .host_connection_id()?; + + self.peer + .forward_send(request.sender_id, host_connection_id, request.payload) + .await?; + + Ok(()) + } + + async fn save_buffer( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let host; + let guests; + { + let state = self.state.read().await; + let worktree = state.read_worktree(request.payload.worktree_id, request.sender_id)?; + host = worktree.host_connection_id()?; + guests = worktree + .guest_connection_ids + .keys() + .copied() + .collect::>(); + } + + let sender = request.sender_id; + let receipt = request.receipt(); + let response = self + .peer + .forward_request(sender, host, request.payload.clone()) + .await?; + + broadcast(host, guests, |conn_id| { + let response = response.clone(); + let peer = &self.peer; + async move { + if conn_id == sender { + peer.respond(receipt, response).await + } else { + peer.forward_send(host, conn_id, response).await + } + } + }) + .await?; + + Ok(()) + } + + async fn update_buffer( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + self.broadcast_in_worktree(request.payload.worktree_id, &request) + .await + } + + async fn buffer_saved( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + self.broadcast_in_worktree(request.payload.worktree_id, &request) + .await + } + + async fn get_channels( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let user_id = self + .state + .read() + .await + .user_id_for_connection(request.sender_id)?; + let channels = self.app_state.db.get_channels_for_user(user_id).await?; + self.peer + .respond( + request.receipt(), + proto::GetChannelsResponse { + channels: channels + .into_iter() + .map(|chan| proto::Channel { + id: chan.id.to_proto(), + name: chan.name, + }) + .collect(), + }, + ) + .await?; + Ok(()) + } + + async fn get_users( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let user_id = self + .state + .read() + .await + .user_id_for_connection(request.sender_id)?; + let receipt = request.receipt(); + let user_ids = request.payload.user_ids.into_iter().map(UserId::from_proto); + let users = self + .app_state + .db + .get_users_by_ids(user_id, user_ids) + .await? + .into_iter() + .map(|user| proto::User { + id: user.id.to_proto(), + github_login: user.github_login, + avatar_url: String::new(), + }) + .collect(); + self.peer + .respond(receipt, proto::GetUsersResponse { users }) + .await?; + Ok(()) + } + + async fn join_channel( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let user_id = self + .state + .read() + .await + .user_id_for_connection(request.sender_id)?; + let channel_id = ChannelId::from_proto(request.payload.channel_id); + if !self + .app_state + .db + .can_user_access_channel(user_id, channel_id) + .await? + { + Err(anyhow!("access denied"))?; + } + + self.state + .write() + .await + .join_channel(request.sender_id, channel_id); + let messages = self + .app_state + .db + .get_recent_channel_messages(channel_id, 50) + .await? + .into_iter() + .map(|msg| proto::ChannelMessage { + id: msg.id.to_proto(), + body: msg.body, + timestamp: msg.sent_at.unix_timestamp() as u64, + sender_id: msg.sender_id.to_proto(), + }) + .collect(); + self.peer + .respond(request.receipt(), proto::JoinChannelResponse { messages }) + .await?; + Ok(()) + } + + async fn send_channel_message( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let channel_id = ChannelId::from_proto(request.payload.channel_id); + let user_id; + let connection_ids; + { + let state = self.state.read().await; + user_id = state.user_id_for_connection(request.sender_id)?; + if let Some(channel) = state.channels.get(&channel_id) { + connection_ids = channel.connection_ids(); + } else { + return Ok(()); + } + } + + let timestamp = OffsetDateTime::now_utc(); + let message_id = self + .app_state + .db + .create_channel_message(channel_id, user_id, &request.payload.body, timestamp) + .await?; + let message = proto::ChannelMessageSent { + channel_id: channel_id.to_proto(), + message: Some(proto::ChannelMessage { + sender_id: user_id.to_proto(), + id: message_id.to_proto(), + body: request.payload.body, + timestamp: timestamp.unix_timestamp() as u64, + }), + }; + broadcast(request.sender_id, connection_ids, |conn_id| { + self.peer.send(conn_id, message.clone()) + }) + .await?; + + Ok(()) + } + + async fn broadcast_in_worktree( + &self, + worktree_id: u64, + message: &TypedEnvelope, + ) -> tide::Result<()> { + let connection_ids = self + .state + .read() + .await + .read_worktree(worktree_id, message.sender_id)? + .connection_ids(); + + broadcast(message.sender_id, connection_ids, |conn_id| { + self.peer + .forward_send(message.sender_id, conn_id, message.payload.clone()) + }) + .await?; + + Ok(()) + } +} + +pub async fn broadcast( + sender_id: ConnectionId, + receiver_ids: Vec, + mut f: F, +) -> anyhow::Result<()> +where + F: FnMut(ConnectionId) -> T, + T: Future>, +{ + let futures = receiver_ids + .into_iter() + .filter(|id| *id != sender_id) + .map(|id| f(id)); + futures::future::try_join_all(futures).await?; + Ok(()) +} + +impl ServerState { fn join_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) { if let Some(connection) = self.connections.get_mut(&connection_id) { connection.channels.insert(channel_id); @@ -245,8 +688,16 @@ impl State { } } + fn user_id_for_connection(&self, connection_id: ConnectionId) -> tide::Result { + Ok(self + .connections + .get(&connection_id) + .ok_or_else(|| anyhow!("unknown connection"))? + .user_id) + } + // Add the given connection as a guest of the given worktree - pub fn join_worktree( + fn join_worktree( &mut self, connection_id: ConnectionId, worktree_id: u64, @@ -275,14 +726,6 @@ impl State { } } - fn user_id_for_connection(&self, connection_id: ConnectionId) -> tide::Result { - Ok(self - .connections - .get(&connection_id) - .ok_or_else(|| anyhow!("unknown connection"))? - .user_id) - } - fn read_worktree( &self, worktree_id: u64, @@ -330,26 +773,30 @@ impl State { } } -pub fn build_server(state: &Arc, rpc: &Arc) -> Arc { - ServerBuilder::default() - .on_message(share_worktree) - .on_message(join_worktree) - .on_message(update_worktree) - .on_message(close_worktree) - .on_message(open_buffer) - .on_message(close_buffer) - .on_message(update_buffer) - .on_message(buffer_saved) - .on_message(save_buffer) - .on_message(get_channels) - .on_message(get_users) - .on_message(join_channel) - .on_message(send_channel_message) - .build(rpc, state) +impl Worktree { + pub fn connection_ids(&self) -> Vec { + self.guest_connection_ids + .keys() + .copied() + .chain(self.host_connection_id) + .collect() + } + + fn host_connection_id(&self) -> tide::Result { + Ok(self + .host_connection_id + .ok_or_else(|| anyhow!("host disconnected from worktree"))?) + } +} + +impl Channel { + fn connection_ids(&self) -> Vec { + self.connection_ids.iter().copied().collect() + } } pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { - let server = build_server(app.state(), rpc); + let server = Server::new(app.state().clone(), rpc.clone()); app.at("/rpc").with(auth::VerifyToken).get(move |request: Request>| { let user_id = request.ext::().copied(); let server = server.clone(); @@ -392,453 +839,6 @@ pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { }); } -async fn share_worktree( - mut request: Box>, - server: Arc, -) -> tide::Result<()> { - let mut state = server.state.rpc.write().await; - let worktree_id = state.next_worktree_id; - state.next_worktree_id += 1; - let access_token = random_token(); - let worktree = request - .payload - .worktree - .as_mut() - .ok_or_else(|| anyhow!("missing worktree"))?; - let entries = mem::take(&mut worktree.entries) - .into_iter() - .map(|entry| (entry.id, entry)) - .collect(); - state.worktrees.insert( - worktree_id, - Worktree { - host_connection_id: Some(request.sender_id), - guest_connection_ids: Default::default(), - active_replica_ids: Default::default(), - access_token: access_token.clone(), - root_name: mem::take(&mut worktree.root_name), - entries, - }, - ); - - server - .rpc - .respond( - request.receipt(), - proto::ShareWorktreeResponse { - worktree_id, - access_token, - }, - ) - .await?; - Ok(()) -} - -async fn join_worktree( - request: Box>, - server: Arc, -) -> tide::Result<()> { - let worktree_id = request.payload.worktree_id; - let access_token = &request.payload.access_token; - - let mut state = server.state.rpc.write().await; - if let Some((peer_replica_id, worktree)) = - state.join_worktree(request.sender_id, worktree_id, access_token) - { - let mut peers = Vec::new(); - if let Some(host_connection_id) = worktree.host_connection_id { - peers.push(proto::Peer { - peer_id: host_connection_id.0, - replica_id: 0, - }); - } - for (peer_conn_id, peer_replica_id) in &worktree.guest_connection_ids { - if *peer_conn_id != request.sender_id { - peers.push(proto::Peer { - peer_id: peer_conn_id.0, - replica_id: *peer_replica_id as u32, - }); - } - } - - broadcast(request.sender_id, worktree.connection_ids(), |conn_id| { - server.rpc.send( - conn_id, - proto::AddPeer { - worktree_id, - peer: Some(proto::Peer { - peer_id: request.sender_id.0, - replica_id: peer_replica_id as u32, - }), - }, - ) - }) - .await?; - server - .rpc - .respond( - request.receipt(), - proto::OpenWorktreeResponse { - worktree_id, - worktree: Some(proto::Worktree { - root_name: worktree.root_name.clone(), - entries: worktree.entries.values().cloned().collect(), - }), - replica_id: peer_replica_id as u32, - peers, - }, - ) - .await?; - } else { - server - .rpc - .respond( - request.receipt(), - proto::OpenWorktreeResponse { - worktree_id, - worktree: None, - replica_id: 0, - peers: Vec::new(), - }, - ) - .await?; - } - - Ok(()) -} - -async fn update_worktree( - request: Box>, - server: Arc, -) -> tide::Result<()> { - { - let mut state = server.state.rpc.write().await; - let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; - for entry_id in &request.payload.removed_entries { - worktree.entries.remove(&entry_id); - } - - for entry in &request.payload.updated_entries { - worktree.entries.insert(entry.id, entry.clone()); - } - } - - broadcast_in_worktree(request.payload.worktree_id, &request, &server).await?; - Ok(()) -} - -async fn close_worktree( - request: Box>, - server: Arc, -) -> tide::Result<()> { - let connection_ids; - { - let mut state = server.state.rpc.write().await; - let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; - connection_ids = worktree.connection_ids(); - if worktree.host_connection_id == Some(request.sender_id) { - worktree.host_connection_id = None; - } else if let Some(replica_id) = worktree.guest_connection_ids.remove(&request.sender_id) { - worktree.active_replica_ids.remove(&replica_id); - } - } - - broadcast(request.sender_id, connection_ids, |conn_id| { - server.rpc.send( - conn_id, - proto::RemovePeer { - worktree_id: request.payload.worktree_id, - peer_id: request.sender_id.0, - }, - ) - }) - .await?; - - Ok(()) -} - -async fn open_buffer( - request: Box>, - server: Arc, -) -> tide::Result<()> { - let receipt = request.receipt(); - let worktree_id = request.payload.worktree_id; - let host_connection_id = server - .state - .rpc - .read() - .await - .read_worktree(worktree_id, request.sender_id)? - .host_connection_id()?; - - let response = server - .rpc - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?; - server.rpc.respond(receipt, response).await?; - Ok(()) -} - -async fn close_buffer( - request: Box>, - server: Arc, -) -> tide::Result<()> { - let host_connection_id = server - .state - .rpc - .read() - .await - .read_worktree(request.payload.worktree_id, request.sender_id)? - .host_connection_id()?; - - server - .rpc - .forward_send(request.sender_id, host_connection_id, request.payload) - .await?; - - Ok(()) -} - -async fn save_buffer( - request: Box>, - server: Arc, -) -> tide::Result<()> { - let host; - let guests; - { - let state = server.state.rpc.read().await; - let worktree = state.read_worktree(request.payload.worktree_id, request.sender_id)?; - host = worktree.host_connection_id()?; - guests = worktree - .guest_connection_ids - .keys() - .copied() - .collect::>(); - } - - let sender = request.sender_id; - let receipt = request.receipt(); - let response = server - .rpc - .forward_request(sender, host, request.payload.clone()) - .await?; - - broadcast(host, guests, |conn_id| { - let response = response.clone(); - let server = &server; - async move { - if conn_id == sender { - server.rpc.respond(receipt, response).await - } else { - server.rpc.forward_send(host, conn_id, response).await - } - } - }) - .await?; - - Ok(()) -} - -async fn update_buffer( - request: Box>, - server: Arc, -) -> tide::Result<()> { - broadcast_in_worktree(request.payload.worktree_id, &request, &server).await -} - -async fn buffer_saved( - request: Box>, - server: Arc, -) -> tide::Result<()> { - broadcast_in_worktree(request.payload.worktree_id, &request, &server).await -} - -async fn get_channels( - request: Box>, - server: Arc, -) -> tide::Result<()> { - let user_id = server - .state - .rpc - .read() - .await - .user_id_for_connection(request.sender_id)?; - let channels = server.state.db.get_channels_for_user(user_id).await?; - server - .rpc - .respond( - request.receipt(), - proto::GetChannelsResponse { - channels: channels - .into_iter() - .map(|chan| proto::Channel { - id: chan.id.to_proto(), - name: chan.name, - }) - .collect(), - }, - ) - .await?; - Ok(()) -} - -async fn get_users( - request: Box>, - server: Arc, -) -> tide::Result<()> { - let user_id = server - .state - .rpc - .read() - .await - .user_id_for_connection(request.sender_id)?; - let receipt = request.receipt(); - let user_ids = request.payload.user_ids.into_iter().map(UserId::from_proto); - let users = server - .state - .db - .get_users_by_ids(user_id, user_ids) - .await? - .into_iter() - .map(|user| proto::User { - id: user.id.to_proto(), - github_login: user.github_login, - avatar_url: String::new(), - }) - .collect(); - server - .rpc - .respond(receipt, proto::GetUsersResponse { users }) - .await?; - Ok(()) -} - -async fn join_channel( - request: Box>, - server: Arc, -) -> tide::Result<()> { - let user_id = server - .state - .rpc - .read() - .await - .user_id_for_connection(request.sender_id)?; - let channel_id = ChannelId::from_proto(request.payload.channel_id); - if !server - .state - .db - .can_user_access_channel(user_id, channel_id) - .await? - { - Err(anyhow!("access denied"))?; - } - - server - .state - .rpc - .write() - .await - .join_channel(request.sender_id, channel_id); - let messages = server - .state - .db - .get_recent_channel_messages(channel_id, 50) - .await? - .into_iter() - .map(|msg| proto::ChannelMessage { - id: msg.id.to_proto(), - body: msg.body, - timestamp: msg.sent_at.unix_timestamp() as u64, - sender_id: msg.sender_id.to_proto(), - }) - .collect(); - server - .rpc - .respond(request.receipt(), proto::JoinChannelResponse { messages }) - .await?; - Ok(()) -} - -async fn send_channel_message( - request: Box>, - server: Arc, -) -> tide::Result<()> { - let channel_id = ChannelId::from_proto(request.payload.channel_id); - let user_id; - let connection_ids; - { - let state = server.state.rpc.read().await; - user_id = state.user_id_for_connection(request.sender_id)?; - if let Some(channel) = state.channels.get(&channel_id) { - connection_ids = channel.connection_ids(); - } else { - return Ok(()); - } - } - - let timestamp = OffsetDateTime::now_utc(); - let message_id = server - .state - .db - .create_channel_message(channel_id, user_id, &request.payload.body, timestamp) - .await?; - let message = proto::ChannelMessageSent { - channel_id: channel_id.to_proto(), - message: Some(proto::ChannelMessage { - sender_id: user_id.to_proto(), - id: message_id.to_proto(), - body: request.payload.body, - timestamp: timestamp.unix_timestamp() as u64, - }), - }; - broadcast(request.sender_id, connection_ids, |conn_id| { - server.rpc.send(conn_id, message.clone()) - }) - .await?; - - Ok(()) -} - -async fn broadcast_in_worktree( - worktree_id: u64, - request: &TypedEnvelope, - server: &Arc, -) -> tide::Result<()> { - let connection_ids = server - .state - .rpc - .read() - .await - .read_worktree(worktree_id, request.sender_id)? - .connection_ids(); - - broadcast(request.sender_id, connection_ids, |conn_id| { - server - .rpc - .forward_send(request.sender_id, conn_id, request.payload.clone()) - }) - .await?; - - Ok(()) -} - -pub async fn broadcast( - sender_id: ConnectionId, - receiver_ids: Vec, - mut f: F, -) -> anyhow::Result<()> -where - F: FnMut(ConnectionId) -> T, - T: Future>, -{ - let futures = receiver_ids - .into_iter() - .filter(|id| *id != sender_id) - .map(|id| f(id)); - futures::future::try_join_all(futures).await?; - Ok(()) -} - fn header_contains_ignore_case( request: &tide::Request, header_name: HeaderName, diff --git a/server/src/tests.rs b/server/src/tests.rs index 5df19aa530726d3867d86f3cc7a7dc8aa3b56746..1cc7bf18ba53d165c9b1d54d8d964b3efa6cc6ed 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -540,7 +540,7 @@ impl TestServer { let db_name = format!("zed-test-{}", rng.gen::()); let app_state = Self::build_app_state(&db_name).await; let peer = Peer::new(); - let server = rpc::build_server(&app_state, &peer); + let server = rpc::Server::new(app_state.clone(), peer.clone()); Self { peer, app_state, @@ -595,7 +595,6 @@ impl TestServer { auth_client: auth::build_client("", ""), repo_client: github::RepoClient::test(&github_client), github_client, - rpc: Default::default(), config, }) } From 86c819757d8bd9a5a6d59bfc1c32c51a0487cf70 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 19 Aug 2021 12:56:50 -0700 Subject: [PATCH 025/204] Move Arc outside of rpc::Client Co-Authored-By: Nathan Sobo --- server/src/tests.rs | 6 +++++- zed/src/channel.rs | 2 +- zed/src/lib.rs | 2 +- zed/src/rpc.rs | 40 ++++++++++++++++++++++++---------------- zed/src/workspace.rs | 2 +- zed/src/worktree.rs | 26 +++++++++++++------------- 6 files changed, 45 insertions(+), 33 deletions(-) diff --git a/server/src/tests.rs b/server/src/tests.rs index 1cc7bf18ba53d165c9b1d54d8d964b3efa6cc6ed..3846f687944797705b0bef88edeae159f0cedc02 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -549,7 +549,11 @@ impl TestServer { } } - async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> (UserId, Client) { + async fn create_client( + &mut self, + cx: &mut TestAppContext, + name: &str, + ) -> (UserId, Arc) { let user_id = self.app_state.db.create_user(name, false).await.unwrap(); let client = Client::new(); let (client_conn, server_conn) = Channel::bidirectional(); diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 60ff50489c5bc18a94a04b87496a6c95d24eea3a..5fc21206b20c5b7dd6733f60283756b620945f1d 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -64,7 +64,7 @@ impl Channel { fn handle_message_sent( &mut self, message: TypedEnvelope, - rpc: rpc::Client, + rpc: Arc, cx: &mut ModelContext, ) -> Result<()> { Ok(()) diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 90e68c698def7aed60438fdd597c445044840da9..492eaaf1048b3a113dcc372b1a6984953bc731c5 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -30,7 +30,7 @@ pub struct AppState { pub settings: watch::Receiver, pub languages: Arc, pub themes: Arc, - pub rpc: rpc::Client, + pub rpc: Arc, pub fs: Arc, } diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 1c0d6d0894fcaf78e7416a47c06356cedc1fea4d..902f17eb9e95fa3db1df9aec6b64ff3e254e0f58 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -23,14 +23,13 @@ lazy_static! { std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev:443".to_string()); } -#[derive(Clone)] pub struct Client { peer: Arc, - state: Arc>, + state: RwLock, } #[derive(Default)] -pub struct ClientState { +struct ClientState { connection_id: Option, entity_id_extractors: HashMap u64>>, model_handlers: HashMap< @@ -40,28 +39,33 @@ pub struct ClientState { } pub struct Subscription { - state: Weak>, + client: Weak, id: (TypeId, u64), } impl Drop for Subscription { fn drop(&mut self) { - if let Some(state) = self.state.upgrade() { - let _ = state.write().model_handlers.remove(&self.id).unwrap(); + if let Some(client) = self.client.upgrade() { + client + .state + .write() + .model_handlers + .remove(&self.id) + .unwrap(); } } } impl Client { - pub fn new() -> Self { - Self { + pub fn new() -> Arc { + Arc::new(Self { peer: Peer::new(), state: Default::default(), - } + }) } pub fn subscribe_from_model( - &self, + self: &Arc, remote_id: u64, cx: &mut ModelContext, mut handler: F, @@ -72,7 +76,7 @@ impl Client { F: 'static + Send + Sync - + FnMut(&mut M, TypedEnvelope, Client, &mut ModelContext) -> Result<()>, + + FnMut(&mut M, TypedEnvelope, Arc, &mut ModelContext) -> Result<()>, { let subscription_id = (TypeId::of::(), remote_id); let client = self.clone(); @@ -108,12 +112,12 @@ impl Client { } Subscription { - state: Arc::downgrade(&self.state), + client: Arc::downgrade(self), id: subscription_id, } } - pub async fn log_in_and_connect(&self, cx: AsyncAppContext) -> surf::Result<()> { + pub async fn log_in_and_connect(self: &Arc, cx: AsyncAppContext) -> surf::Result<()> { if self.state.read().connection_id.is_some() { return Ok(()); } @@ -144,7 +148,11 @@ impl Client { Ok(()) } - pub async fn add_connection(&self, conn: Conn, cx: AsyncAppContext) -> surf::Result<()> + pub async fn add_connection( + self: &Arc, + conn: Conn, + cx: AsyncAppContext, + ) -> surf::Result<()> where Conn: 'static + futures::Sink @@ -155,11 +163,11 @@ impl Client { let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await; { let mut cx = cx.clone(); - let state = self.state.clone(); + let this = self.clone(); cx.foreground() .spawn(async move { while let Some(message) = incoming.recv().await { - let mut state = state.write(); + let mut state = this.state.write(); if let Some(extract_entity_id) = state.entity_id_extractors.get(&message.payload_type_id()) { diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 8dfa7fc4da604eb63978c55e344283892d8a28de..663ca5f75e9feb674c2b4c9cf3618e8b763e63ca 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -312,7 +312,7 @@ pub struct State { pub struct Workspace { pub settings: watch::Receiver, languages: Arc, - rpc: rpc::Client, + rpc: Arc, fs: Arc, modal: Option, center: PaneGroup, diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 723487ad4ae00640bddb032520374a3668199653..d9a8aa889b456485c6ff874962492f0faaa35ba0 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -107,7 +107,7 @@ impl Worktree { } pub async fn open_remote( - rpc: rpc::Client, + rpc: Arc, id: u64, access_token: String, languages: Arc, @@ -125,7 +125,7 @@ impl Worktree { async fn remote( open_response: proto::OpenWorktreeResponse, - rpc: rpc::Client, + rpc: Arc, languages: Arc, cx: &mut AsyncAppContext, ) -> Result> { @@ -283,7 +283,7 @@ impl Worktree { pub fn handle_add_peer( &mut self, envelope: TypedEnvelope, - _: rpc::Client, + _: Arc, cx: &mut ModelContext, ) -> Result<()> { match self { @@ -295,7 +295,7 @@ impl Worktree { pub fn handle_remove_peer( &mut self, envelope: TypedEnvelope, - _: rpc::Client, + _: Arc, cx: &mut ModelContext, ) -> Result<()> { match self { @@ -307,7 +307,7 @@ impl Worktree { pub fn handle_update( &mut self, envelope: TypedEnvelope, - _: rpc::Client, + _: Arc, cx: &mut ModelContext, ) -> anyhow::Result<()> { self.as_remote_mut() @@ -318,7 +318,7 @@ impl Worktree { pub fn handle_open_buffer( &mut self, envelope: TypedEnvelope, - rpc: rpc::Client, + rpc: Arc, cx: &mut ModelContext, ) -> anyhow::Result<()> { let receipt = envelope.receipt(); @@ -341,7 +341,7 @@ impl Worktree { pub fn handle_close_buffer( &mut self, envelope: TypedEnvelope, - _: rpc::Client, + _: Arc, cx: &mut ModelContext, ) -> anyhow::Result<()> { self.as_local_mut() @@ -397,7 +397,7 @@ impl Worktree { pub fn handle_update_buffer( &mut self, envelope: TypedEnvelope, - _: rpc::Client, + _: Arc, cx: &mut ModelContext, ) -> Result<()> { let payload = envelope.payload.clone(); @@ -444,7 +444,7 @@ impl Worktree { pub fn handle_save_buffer( &mut self, envelope: TypedEnvelope, - rpc: rpc::Client, + rpc: Arc, cx: &mut ModelContext, ) -> Result<()> { let sender_id = envelope.original_sender_id()?; @@ -488,7 +488,7 @@ impl Worktree { pub fn handle_buffer_saved( &mut self, envelope: TypedEnvelope, - _: rpc::Client, + _: Arc, cx: &mut ModelContext, ) -> Result<()> { let payload = envelope.payload.clone(); @@ -966,7 +966,7 @@ impl LocalWorktree { pub fn share( &mut self, - rpc: rpc::Client, + rpc: Arc, cx: &mut ModelContext, ) -> Task> { let snapshot = self.snapshot(); @@ -1068,7 +1068,7 @@ impl fmt::Debug for LocalWorktree { } struct ShareState { - rpc: rpc::Client, + rpc: Arc, remote_id: u64, snapshots_tx: Sender, _subscriptions: Vec, @@ -1078,7 +1078,7 @@ pub struct RemoteWorktree { remote_id: u64, snapshot: Snapshot, snapshot_rx: watch::Receiver, - rpc: rpc::Client, + rpc: Arc, updates_tx: postage::mpsc::Sender, replica_id: ReplicaId, open_buffers: HashMap, From 620b988e229e5d5f99851f93a731cd79d3a23892 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 19 Aug 2021 13:07:28 -0700 Subject: [PATCH 026/204] WIP --- server/src/tests.rs | 31 ++++++++++++++++++----------- zed/src/channel.rs | 48 ++++++++++++++++++++++++++++++++++++--------- zed/src/rpc.rs | 14 +++++++------ 3 files changed, 67 insertions(+), 26 deletions(-) diff --git a/server/src/tests.rs b/server/src/tests.rs index 3846f687944797705b0bef88edeae159f0cedc02..607929327fc17fd7225874f4e1a31efacaea7e50 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -14,6 +14,7 @@ use sqlx::{ }; use std::{path::Path, sync::Arc}; use zed::{ + channel::{ChannelDetails, ChannelList}, editor::Editor, fs::{FakeFs, Fs as _}, language::LanguageRegistry, @@ -514,17 +515,25 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { .await .unwrap(); - // let channels_a = client_a.get_channels().await; - // assert_eq!(channels_a.len(), 1); - // assert_eq!(channels_a[0].read(&cx_a).name(), "test-channel"); - - // assert_eq!( - // db.get_recent_channel_messages(channel_id, 50) - // .await - // .unwrap()[0] - // .body, - // "first message!" - // ); + let channels_a = ChannelList::new(client_a, &mut cx_a.to_async()) + .await + .unwrap(); + let channels_b = ChannelList::new(client_b, &mut cx_b.to_async()) + .await + .unwrap(); + channels_a.read_with(&cx_a, |list, _| { + assert_eq!( + list.available_channels(), + &[ChannelDetails { + id: channel_id.to_proto(), + name: "test-channel".to_string() + }] + ) + }); + + let channel_a = channels_a.read_with(&cx_a, |this, cx| { + this.get_channel(channel_id.to_proto(), &cx).unwrap() + }); } struct TestServer { diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 5fc21206b20c5b7dd6733f60283756b620945f1d..1c7d576c65c1fbb6bb9bedab389aff8a87a5cf13 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,11 +1,14 @@ use crate::rpc::{self, Client}; -use anyhow::Result; -use gpui::{Entity, ModelContext, WeakModelHandle}; +use anyhow::{Context, Result}; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, WeakModelHandle}; use std::{ collections::{HashMap, VecDeque}, sync::Arc, }; -use zrpc::{proto::ChannelMessageSent, TypedEnvelope}; +use zrpc::{ + proto::{self, ChannelMessageSent}, + TypedEnvelope, +}; pub struct ChannelList { available_channels: Vec, @@ -13,10 +16,12 @@ pub struct ChannelList { rpc: Arc, } +#[derive(Debug, PartialEq)] pub struct ChannelDetails { - id: u64, - name: String, + pub id: u64, + pub name: String, } + pub struct Channel { details: ChannelDetails, first_message_id: Option, @@ -35,12 +40,28 @@ impl Entity for ChannelList { } impl ChannelList { - fn new(rpc: Arc) -> Self { - Self { - available_channels: Default::default(), + pub async fn new(rpc: Arc, cx: &mut AsyncAppContext) -> Result> { + let response = rpc + .request(proto::GetChannels {}) + .await + .context("failed to fetch available channels")?; + + Ok(cx.add_model(|_| Self { + available_channels: response.channels.into_iter().map(Into::into).collect(), channels: Default::default(), rpc, - } + })) + } + + pub fn available_channels(&self) -> &[ChannelDetails] { + &self.available_channels + } + + pub fn get_channel(&self, id: u64, cx: &AppContext) -> Option> { + self.channels + .get(&id) + .cloned() + .and_then(|handle| handle.upgrade(cx)) } } @@ -70,3 +91,12 @@ impl Channel { Ok(()) } } + +impl From for ChannelDetails { + fn from(message: proto::Channel) -> Self { + Self { + id: message.id, + name: message.name, + } + } +} diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 902f17eb9e95fa3db1df9aec6b64ff3e254e0f58..a65551a7ffb69a1aeb5774d0bb07f2ba96eaaacc 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -46,12 +46,14 @@ pub struct Subscription { impl Drop for Subscription { fn drop(&mut self) { if let Some(client) = self.client.upgrade() { - client - .state - .write() - .model_handlers - .remove(&self.id) - .unwrap(); + drop( + client + .state + .write() + .model_handlers + .remove(&self.id) + .unwrap(), + ); } } } From a4882169c250a5ffb78767ec06b981ad989d34ed Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 19 Aug 2021 15:25:20 -0600 Subject: [PATCH 027/204] Fetch messages when joining a channel --- gpui/src/app.rs | 20 ++++++++++- server/src/tests.rs | 12 ++++--- zed/src/channel.rs | 81 +++++++++++++++++++++++++++++++++++++++----- zrpc/proto/zed.proto | 9 +++-- zrpc/src/proto.rs | 1 + 5 files changed, 108 insertions(+), 15 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index a2fa8188246bed0c3b2dab51a266b48926502d2c..221419341e93c63b8e9c661e37e58faecb68f86d 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -14,7 +14,7 @@ use keymap::MatchResult; use parking_lot::{Mutex, RwLock}; use pathfinder_geometry::{rect::RectF, vector::vec2f}; use platform::Event; -use postage::{mpsc, sink::Sink as _, stream::Stream as _}; +use postage::{mpsc, oneshot, sink::Sink as _, stream::Stream as _}; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, @@ -2310,6 +2310,24 @@ impl ModelHandle { cx.update_model(self, update) } + pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { + let (tx, mut rx) = oneshot::channel(); + let mut tx = Some(tx); + + let mut cx = cx.cx.borrow_mut(); + self.update(&mut *cx, |_, cx| { + cx.observe(self, move |_, _, _| { + if let Some(mut tx) = tx.take() { + tx.blocking_send(()).ok(); + } + }); + }); + + async move { + rx.recv().await; + } + } + pub fn condition( &self, cx: &TestAppContext, diff --git a/server/src/tests.rs b/server/src/tests.rs index 607929327fc17fd7225874f4e1a31efacaea7e50..dd899e9fed7885e031cdd33dae194922517ad50c 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -480,8 +480,6 @@ async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) #[gpui::test] async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { - let lang_registry = Arc::new(LanguageRegistry::new()); - // 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; @@ -531,8 +529,14 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { ) }); - let channel_a = channels_a.read_with(&cx_a, |this, cx| { - this.get_channel(channel_id.to_proto(), &cx).unwrap() + let channel_a = channels_a.update(&mut cx_a, |this, cx| { + this.get_channel(channel_id.to_proto(), cx).unwrap() + }); + + channel_a.read_with(&cx_a, |channel, _| assert!(channel.messages().is_none())); + channel_a.next_notification(&cx_a).await; + channel_a.read_with(&cx_a, |channel, _| { + assert_eq!(channel.messages().unwrap().len(), 1); }); } diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 1c7d576c65c1fbb6bb9bedab389aff8a87a5cf13..3161592f4d3e8b340aa3e45001c21e82f9cfc5e6 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,8 +1,11 @@ use crate::rpc::{self, Client}; use anyhow::{Context, Result}; -use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, WeakModelHandle}; +use gpui::{ + executor, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, + WeakModelHandle, +}; use std::{ - collections::{HashMap, VecDeque}, + collections::{hash_map, HashMap, VecDeque}, sync::Arc, }; use zrpc::{ @@ -16,7 +19,7 @@ pub struct ChannelList { rpc: Arc, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct ChannelDetails { pub id: u64, pub name: String, @@ -28,6 +31,7 @@ pub struct Channel { messages: Option>, rpc: Arc, _subscription: rpc::Subscription, + background: Arc, } pub struct ChannelMessage { @@ -57,11 +61,28 @@ impl ChannelList { &self.available_channels } - pub fn get_channel(&self, id: u64, cx: &AppContext) -> Option> { - self.channels - .get(&id) - .cloned() - .and_then(|handle| handle.upgrade(cx)) + pub fn get_channel( + &mut self, + id: u64, + cx: &mut MutableAppContext, + ) -> Option> { + match self.channels.entry(id) { + hash_map::Entry::Occupied(entry) => entry.get().upgrade(cx), + hash_map::Entry::Vacant(entry) => { + if let Some(details) = self + .available_channels + .iter() + .find(|details| details.id == id) + { + let rpc = self.rpc.clone(); + let channel = cx.add_model(|cx| Channel::new(details.clone(), rpc, cx)); + entry.insert(channel.downgrade()); + Some(channel) + } else { + None + } + } + } } } @@ -73,12 +94,31 @@ impl Channel { pub fn new(details: ChannelDetails, rpc: Arc, cx: &mut ModelContext) -> Self { let _subscription = rpc.subscribe_from_model(details.id, cx, Self::handle_message_sent); + { + let rpc = rpc.clone(); + let channel_id = details.id; + cx.spawn(|channel, mut cx| async move { + match rpc.request(proto::JoinChannel { channel_id }).await { + Ok(response) => { + let messages = response.messages.into_iter().map(Into::into).collect(); + channel.update(&mut cx, |channel, cx| { + channel.messages = Some(messages); + cx.notify(); + }) + } + Err(error) => log::error!("error joining channel: {}", error), + } + }) + .detach(); + } + Self { details, rpc, first_message_id: None, messages: None, _subscription, + background: cx.background().clone(), } } @@ -90,6 +130,25 @@ impl Channel { ) -> Result<()> { Ok(()) } + + pub fn messages(&self) -> Option<&VecDeque> { + self.messages.as_ref() + } +} + +// TODO: Implement the server side of leaving a channel +impl Drop for Channel { + fn drop(&mut self) { + let rpc = self.rpc.clone(); + let channel_id = self.details.id; + self.background + .spawn(async move { + if let Err(error) = rpc.send(proto::LeaveChannel { channel_id }).await { + log::error!("error leaving channel: {}", error); + }; + }) + .detach() + } } impl From for ChannelDetails { @@ -100,3 +159,9 @@ impl From for ChannelDetails { } } } + +impl From for ChannelMessage { + fn from(message: proto::ChannelMessage) -> Self { + ChannelMessage { id: message.id } + } +} diff --git a/zrpc/proto/zed.proto b/zrpc/proto/zed.proto index 3a0b7aabb6a341cffc58d57e28dd1b0e454fd252..cbced82e7b05a3c4caffd0653688f1ac0134752a 100644 --- a/zrpc/proto/zed.proto +++ b/zrpc/proto/zed.proto @@ -30,8 +30,9 @@ message Envelope { GetUsersResponse get_users_response = 25; JoinChannel join_channel = 26; JoinChannelResponse join_channel_response = 27; - SendChannelMessage send_channel_message = 28; - ChannelMessageSent channel_message_sent = 29; + LeaveChannel leave_channel = 28; + SendChannelMessage send_channel_message = 29; + ChannelMessageSent channel_message_sent = 30; } } @@ -141,6 +142,10 @@ message JoinChannelResponse { repeated ChannelMessage messages = 1; } +message LeaveChannel { + uint64 channel_id = 1; +} + message GetUsers { repeated uint64 user_ids = 1; } diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index 271fa8e29e5a63d7f7ae0a5e3b09b21a022fdbc9..173e802f00138fa5a909998ca6d1161b7c021aa6 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -138,6 +138,7 @@ messages!( GetUsersResponse, JoinChannel, JoinChannelResponse, + LeaveChannel, OpenBuffer, OpenBufferResponse, OpenWorktree, From 467512eedd60f68a67cd23fa21b88bc5d2d29bd8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 19 Aug 2021 17:24:54 -0700 Subject: [PATCH 028/204] Remove Auth and AuthResponse protobuf messages --- zrpc/proto/zed.proto | 57 ++++++++++++++++++-------------------------- zrpc/src/peer.rs | 8 +------ zrpc/src/proto.rs | 12 +++------- 3 files changed, 27 insertions(+), 50 deletions(-) diff --git a/zrpc/proto/zed.proto b/zrpc/proto/zed.proto index cbced82e7b05a3c4caffd0653688f1ac0134752a..48b481c5f4548a7cb6bc1dd780e4151c8fc6f200 100644 --- a/zrpc/proto/zed.proto +++ b/zrpc/proto/zed.proto @@ -8,31 +8,29 @@ message Envelope { oneof payload { Ping ping = 4; Pong pong = 5; - Auth auth = 6; - AuthResponse auth_response = 7; - ShareWorktree share_worktree = 8; - ShareWorktreeResponse share_worktree_response = 9; - OpenWorktree open_worktree = 10; - OpenWorktreeResponse open_worktree_response = 11; - UpdateWorktree update_worktree = 12; - CloseWorktree close_worktree = 13; - OpenBuffer open_buffer = 14; - OpenBufferResponse open_buffer_response = 15; - CloseBuffer close_buffer = 16; - UpdateBuffer update_buffer = 17; - SaveBuffer save_buffer = 18; - BufferSaved buffer_saved = 19; - AddPeer add_peer = 20; - RemovePeer remove_peer = 21; - GetChannels get_channels = 22; - GetChannelsResponse get_channels_response = 23; - GetUsers get_users = 24; - GetUsersResponse get_users_response = 25; - JoinChannel join_channel = 26; - JoinChannelResponse join_channel_response = 27; - LeaveChannel leave_channel = 28; - SendChannelMessage send_channel_message = 29; - ChannelMessageSent channel_message_sent = 30; + ShareWorktree share_worktree = 6; + ShareWorktreeResponse share_worktree_response = 7; + OpenWorktree open_worktree = 8; + OpenWorktreeResponse open_worktree_response = 9; + UpdateWorktree update_worktree = 10; + CloseWorktree close_worktree = 11; + OpenBuffer open_buffer = 12; + OpenBufferResponse open_buffer_response = 13; + CloseBuffer close_buffer = 14; + UpdateBuffer update_buffer = 15; + SaveBuffer save_buffer = 16; + BufferSaved buffer_saved = 17; + AddPeer add_peer = 18; + RemovePeer remove_peer = 19; + GetChannels get_channels = 20; + GetChannelsResponse get_channels_response = 21; + GetUsers get_users = 22; + GetUsersResponse get_users_response = 23; + JoinChannel join_channel = 24; + JoinChannelResponse join_channel_response = 25; + LeaveChannel leave_channel = 26; + SendChannelMessage send_channel_message = 27; + ChannelMessageSent channel_message_sent = 28; } } @@ -46,15 +44,6 @@ message Pong { int32 id = 2; } -message Auth { - int32 user_id = 1; - string access_token = 2; -} - -message AuthResponse { - bool credentials_valid = 1; -} - message ShareWorktree { Worktree worktree = 1; } diff --git a/zrpc/src/peer.rs b/zrpc/src/peer.rs index 7048fcd0a1825ad1c91a38a63cacf8a62fc8ca0a..06d4b01ae06140c417270a992e3d23142bc65498 100644 --- a/zrpc/src/peer.rs +++ b/zrpc/src/peer.rs @@ -514,13 +514,7 @@ mod tests { smol::spawn(async move { incoming.next().await }).detach(); let err = client - .request( - connection_id, - proto::Auth { - user_id: 42, - access_token: "token".to_string(), - }, - ) + .request(connection_id, proto::Ping { id: 42 }) .await .unwrap_err(); assert_eq!(err.to_string(), "connection was closed"); diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index 173e802f00138fa5a909998ca6d1161b7c021aa6..0bcbfd4d65ba1152fb089691948f0e09bbbb747c 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -126,8 +126,6 @@ macro_rules! entity_messages { messages!( AddPeer, - Auth, - AuthResponse, BufferSaved, ChannelMessageSent, CloseBuffer, @@ -148,6 +146,7 @@ messages!( RemovePeer, SaveBuffer, SendChannelMessage, + SendChannelMessageResponse, ShareWorktree, ShareWorktreeResponse, UpdateBuffer, @@ -155,7 +154,6 @@ messages!( ); request_messages!( - (Auth, AuthResponse), (GetChannels, GetChannelsResponse), (GetUsers, GetUsersResponse), (JoinChannel, JoinChannelResponse), @@ -164,6 +162,7 @@ request_messages!( (Ping, Pong), (SaveBuffer, BufferSaved), (ShareWorktree, ShareWorktreeResponse), + (SendChannelMessage, SendChannelMessageResponse), ); entity_messages!( @@ -259,12 +258,7 @@ mod tests { fn test_round_trip_message() { smol::block_on(async { let stream = test::Channel::new(); - let message1 = Auth { - user_id: 5, - access_token: "the-access-token".into(), - } - .into_envelope(3, None, None); - + let message1 = Ping { id: 5 }.into_envelope(3, None, None); let message2 = OpenBuffer { worktree_id: 0, path: "some/path".to_string(), From c28a366e4a0c9db27e3000c1de39ac148b845d2a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 19 Aug 2021 17:27:21 -0700 Subject: [PATCH 029/204] Send LeaveChannel message in Entity::release instead of Drop::drop --- zed/src/channel.rs | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 3161592f4d3e8b340aa3e45001c21e82f9cfc5e6..d3fc64a505dd7b6ed0dffa0f20b8aa00bdf14f07 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,8 +1,7 @@ use crate::rpc::{self, Client}; use anyhow::{Context, Result}; use gpui::{ - executor, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, - WeakModelHandle, + AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle, }; use std::{ collections::{hash_map, HashMap, VecDeque}, @@ -31,7 +30,6 @@ pub struct Channel { messages: Option>, rpc: Arc, _subscription: rpc::Subscription, - background: Arc, } pub struct ChannelMessage { @@ -88,6 +86,19 @@ impl ChannelList { impl Entity for Channel { type Event = (); + + // TODO: Implement the server side of leaving a channel + fn release(&mut self, cx: &mut MutableAppContext) { + let rpc = self.rpc.clone(); + let channel_id = self.details.id; + cx.foreground() + .spawn(async move { + if let Err(error) = rpc.send(proto::LeaveChannel { channel_id }).await { + log::error!("error leaving channel: {}", error); + }; + }) + .detach() + } } impl Channel { @@ -118,10 +129,14 @@ impl Channel { first_message_id: None, messages: None, _subscription, - background: cx.background().clone(), } } + + pub fn messages(&self) -> Option<&VecDeque> { + self.messages.as_ref() + } + fn handle_message_sent( &mut self, message: TypedEnvelope, @@ -130,25 +145,6 @@ impl Channel { ) -> Result<()> { Ok(()) } - - pub fn messages(&self) -> Option<&VecDeque> { - self.messages.as_ref() - } -} - -// TODO: Implement the server side of leaving a channel -impl Drop for Channel { - fn drop(&mut self) { - let rpc = self.rpc.clone(); - let channel_id = self.details.id; - self.background - .spawn(async move { - if let Err(error) = rpc.send(proto::LeaveChannel { channel_id }).await { - log::error!("error leaving channel: {}", error); - }; - }) - .detach() - } } impl From for ChannelDetails { From 10868cf0ea985f12dea761268e72b13a587ec747 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 19 Aug 2021 18:04:49 -0700 Subject: [PATCH 030/204] Start work on sending channel messages --- server/src/rpc.rs | 16 +++++++-- server/src/tests.rs | 44 ++++++++++++++--------- zed/src/channel.rs | 84 ++++++++++++++++++++++++++++++++++++-------- zed/src/rpc.rs | 23 ++++++++---- zrpc/proto/zed.proto | 8 ++++- 5 files changed, 133 insertions(+), 42 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index d107e3606b0ba6097bcc5013011c5b8800a9b70e..e5086d870cba3b57bfd7922c37d50395a73cef5e 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -619,12 +619,14 @@ impl Server { .app_state .db .create_channel_message(channel_id, user_id, &request.payload.body, timestamp) - .await?; + .await? + .to_proto(); + let receipt = request.receipt(); let message = proto::ChannelMessageSent { channel_id: channel_id.to_proto(), message: Some(proto::ChannelMessage { sender_id: user_id.to_proto(), - id: message_id.to_proto(), + id: message_id, body: request.payload.body, timestamp: timestamp.unix_timestamp() as u64, }), @@ -633,7 +635,15 @@ impl Server { self.peer.send(conn_id, message.clone()) }) .await?; - + self.peer + .respond( + receipt, + proto::SendChannelMessageResponse { + message_id, + timestamp: timestamp.unix_timestamp() as u64, + }, + ) + .await?; Ok(()) } diff --git a/server/src/tests.rs b/server/src/tests.rs index dd899e9fed7885e031cdd33dae194922517ad50c..b301d7b2d658c015b71c9f17e16a5223778ac190 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -485,13 +485,11 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { 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_a, "user_b").await; - // Create an org that includes these 2 users and 1 other user. + // Create an org that includes these 2 users. let db = &server.app_state.db; - let user_id_c = db.create_user("user_c", false).await.unwrap(); 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, user_id_c, false).await.unwrap(); // Create a channel that includes all the users. let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap(); @@ -501,13 +499,10 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { db.add_channel_member(channel_id, user_id_b, false) .await .unwrap(); - db.add_channel_member(channel_id, user_id_c, false) - .await - .unwrap(); db.create_channel_message( channel_id, - user_id_c, - "first message!", + user_id_b, + "hello A, it's B.", OffsetDateTime::now_utc(), ) .await @@ -516,9 +511,6 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { let channels_a = ChannelList::new(client_a, &mut cx_a.to_async()) .await .unwrap(); - let channels_b = ChannelList::new(client_b, &mut cx_b.to_async()) - .await - .unwrap(); channels_a.read_with(&cx_a, |list, _| { assert_eq!( list.available_channels(), @@ -532,12 +524,33 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { let channel_a = channels_a.update(&mut cx_a, |this, cx| { this.get_channel(channel_id.to_proto(), cx).unwrap() }); - - channel_a.read_with(&cx_a, |channel, _| assert!(channel.messages().is_none())); + channel_a.read_with(&cx_a, |channel, _| assert!(channel.messages().is_empty())); channel_a.next_notification(&cx_a).await; channel_a.read_with(&cx_a, |channel, _| { - assert_eq!(channel.messages().unwrap().len(), 1); + assert_eq!( + channel + .messages() + .iter() + .map(|m| (m.sender_id, m.body.as_ref())) + .collect::>(), + &[(user_id_b.to_proto(), "hello A, it's B.")] + ); }); + + channel_a.update(&mut cx_a, |channel, cx| { + channel.send_message("oh, hi B.".to_string(), cx).unwrap(); + channel.send_message("sup".to_string(), cx).unwrap(); + assert_eq!( + channel + .pending_messages() + .iter() + .map(|m| &m.body) + .collect::>(), + &["oh, hi B.", "sup"] + ) + }); + + channel_a.next_notification(&cx_a).await; } struct TestServer { @@ -577,10 +590,9 @@ impl TestServer { ) .detach(); client - .add_connection(client_conn, cx.to_async()) + .add_connection(user_id.to_proto(), client_conn, cx.to_async()) .await .unwrap(); - (user_id, client) } diff --git a/zed/src/channel.rs b/zed/src/channel.rs index d3fc64a505dd7b6ed0dffa0f20b8aa00bdf14f07..cbbdec6015565b636d4a1172d81202d14ef05d61 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,5 +1,8 @@ -use crate::rpc::{self, Client}; -use anyhow::{Context, Result}; +use crate::{ + rpc::{self, Client}, + util::log_async_errors, +}; +use anyhow::{anyhow, Context, Result}; use gpui::{ AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle, }; @@ -27,14 +30,24 @@ pub struct ChannelDetails { pub struct Channel { details: ChannelDetails, first_message_id: Option, - messages: Option>, + messages: VecDeque, + pending_messages: Vec, + next_local_message_id: u64, rpc: Arc, _subscription: rpc::Subscription, } pub struct ChannelMessage { - id: u64, + pub id: u64, + pub sender_id: u64, + pub body: String, +} + +pub struct PendingChannelMessage { + pub body: String, + local_id: u64, } + pub enum Event {} impl Entity for ChannelList { @@ -110,13 +123,10 @@ impl Channel { let channel_id = details.id; cx.spawn(|channel, mut cx| async move { match rpc.request(proto::JoinChannel { channel_id }).await { - Ok(response) => { - let messages = response.messages.into_iter().map(Into::into).collect(); - channel.update(&mut cx, |channel, cx| { - channel.messages = Some(messages); - cx.notify(); - }) - } + Ok(response) => channel.update(&mut cx, |channel, cx| { + channel.messages = response.messages.into_iter().map(Into::into).collect(); + cx.notify(); + }), Err(error) => log::error!("error joining channel: {}", error), } }) @@ -127,14 +137,54 @@ impl Channel { details, rpc, first_message_id: None, - messages: None, + messages: Default::default(), + pending_messages: Default::default(), + next_local_message_id: 0, _subscription, } } + pub fn send_message(&mut self, body: String, cx: &mut ModelContext) -> Result<()> { + let channel_id = self.details.id; + let current_user_id = self.rpc.user_id().ok_or_else(|| anyhow!("not logged in"))?; + 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 rpc = self.rpc.clone(); + cx.spawn(|this, mut cx| { + log_async_errors(async move { + let request = rpc.request(proto::SendChannelMessage { channel_id, body }); + let response = request.await?; + this.update(&mut cx, |this, cx| { + if let Ok(i) = this + .pending_messages + .binary_search_by_key(&local_id, |msg| msg.local_id) + { + let body = this.pending_messages.remove(i).body; + this.messages.push_back(ChannelMessage { + id: response.message_id, + sender_id: current_user_id, + body, + }); + cx.notify(); + } + }); + Ok(()) + }) + }) + .detach(); + Ok(()) + } + + pub fn messages(&self) -> &VecDeque { + &self.messages + } - pub fn messages(&self) -> Option<&VecDeque> { - self.messages.as_ref() + pub fn pending_messages(&self) -> &[PendingChannelMessage] { + &self.pending_messages } fn handle_message_sent( @@ -158,6 +208,10 @@ impl From for ChannelDetails { impl From for ChannelMessage { fn from(message: proto::ChannelMessage) -> Self { - ChannelMessage { id: message.id } + ChannelMessage { + id: message.id, + sender_id: message.sender_id, + body: message.body, + } } } diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index a65551a7ffb69a1aeb5774d0bb07f2ba96eaaacc..b8909a988a5d1bbf4adfa08a1114d52092d0888e 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -31,6 +31,7 @@ pub struct Client { #[derive(Default)] struct ClientState { connection_id: Option, + user_id: Option, entity_id_extractors: HashMap u64>>, model_handlers: HashMap< (TypeId, u64), @@ -66,6 +67,10 @@ impl Client { }) } + pub fn user_id(&self) -> Option { + self.state.read().user_id + } + pub fn subscribe_from_model( self: &Arc, remote_id: u64, @@ -125,7 +130,7 @@ impl Client { } let (user_id, access_token) = Self::login(cx.platform(), &cx.background()).await?; - let user_id: i32 = user_id.parse()?; + let user_id = user_id.parse::()?; let request = Request::builder().header("Authorization", format!("{} {}", user_id, access_token)); @@ -135,23 +140,25 @@ impl Client { let (stream, _) = async_tungstenite::async_tls::client_async_tls(request, stream) .await .context("websocket handshake")?; - log::info!("connected to rpc address {}", *ZED_SERVER_URL); - self.add_connection(stream, cx).await?; + self.add_connection(user_id, stream, cx).await?; } else if let Some(host) = ZED_SERVER_URL.strip_prefix("http://") { let stream = smol::net::TcpStream::connect(host).await?; let request = request.uri(format!("ws://{}/rpc", host)).body(())?; - let (stream, _) = async_tungstenite::client_async(request, stream).await?; - log::info!("connected to rpc address {}", *ZED_SERVER_URL); - self.add_connection(stream, cx).await?; + let (stream, _) = async_tungstenite::client_async(request, stream) + .await + .context("websocket handshake")?; + self.add_connection(user_id, stream, cx).await?; } else { return Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))?; }; + log::info!("connected to rpc address {}", *ZED_SERVER_URL); Ok(()) } pub async fn add_connection( self: &Arc, + user_id: u64, conn: Conn, cx: AsyncAppContext, ) -> surf::Result<()> @@ -202,7 +209,9 @@ impl Client { } }) .detach(); - self.state.write().connection_id = Some(connection_id); + let mut state = self.state.write(); + state.connection_id = Some(connection_id); + state.user_id = Some(user_id); Ok(()) } diff --git a/zrpc/proto/zed.proto b/zrpc/proto/zed.proto index 48b481c5f4548a7cb6bc1dd780e4151c8fc6f200..f368cb2d475da9dab1e651a8d29bc961dbec1bb8 100644 --- a/zrpc/proto/zed.proto +++ b/zrpc/proto/zed.proto @@ -30,7 +30,8 @@ message Envelope { JoinChannelResponse join_channel_response = 25; LeaveChannel leave_channel = 26; SendChannelMessage send_channel_message = 27; - ChannelMessageSent channel_message_sent = 28; + SendChannelMessageResponse send_channel_message_response = 28; + ChannelMessageSent channel_message_sent = 29; } } @@ -148,6 +149,11 @@ message SendChannelMessage { string body = 2; } +message SendChannelMessageResponse { + uint64 message_id = 1; + uint64 timestamp = 2; +} + message ChannelMessageSent { uint64 channel_id = 1; ChannelMessage message = 2; From 9ed4176e4d9691b826ca961ded55b08cf9738049 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 20 Aug 2021 11:16:59 +0200 Subject: [PATCH 031/204] Save messages received from the server --- server/src/tests.rs | 74 ++++++++++++++++++++++++++++++++++----------- zed/src/channel.rs | 14 +++++++-- 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/server/src/tests.rs b/server/src/tests.rs index b301d7b2d658c015b71c9f17e16a5223778ac190..b6b9045ecbfb5751395de23af9610fccfad85f3c 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -4,7 +4,7 @@ use crate::{ github, rpc, AppState, Config, }; use async_std::task; -use gpui::TestAppContext; +use gpui::{ModelHandle, TestAppContext}; use rand::prelude::*; use serde_json::json; use sqlx::{ @@ -14,13 +14,12 @@ use sqlx::{ }; use std::{path::Path, sync::Arc}; use zed::{ - channel::{ChannelDetails, ChannelList}, + channel::{Channel, ChannelDetails, ChannelList}, editor::Editor, fs::{FakeFs, Fs as _}, language::LanguageRegistry, rpc::Client, - settings, - test::Channel, + settings, test, worktree::Worktree, }; use zrpc::Peer; @@ -479,11 +478,11 @@ async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) } #[gpui::test] -async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { +async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { // 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_a, "user_b").await; + let (user_id_b, client_b) = server.create_client(&mut cx_b, "user_b").await; // Create an org that includes these 2 users. let db = &server.app_state.db; @@ -520,22 +519,37 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { }] ) }); - let channel_a = channels_a.update(&mut cx_a, |this, cx| { this.get_channel(channel_id.to_proto(), cx).unwrap() }); channel_a.read_with(&cx_a, |channel, _| assert!(channel.messages().is_empty())); channel_a.next_notification(&cx_a).await; - channel_a.read_with(&cx_a, |channel, _| { + assert_eq!( + channel_messages(&channel_a, &cx_a), + &[(user_id_b.to_proto(), "hello A, it's B.".to_string())] + ); + + let channels_b = ChannelList::new(client_b, &mut cx_b.to_async()) + .await + .unwrap(); + channels_b.read_with(&cx_b, |list, _| { assert_eq!( - channel - .messages() - .iter() - .map(|m| (m.sender_id, m.body.as_ref())) - .collect::>(), - &[(user_id_b.to_proto(), "hello A, it's B.")] - ); + list.available_channels(), + &[ChannelDetails { + id: channel_id.to_proto(), + name: "test-channel".to_string() + }] + ) }); + let channel_b = channels_b.update(&mut cx_b, |this, cx| { + this.get_channel(channel_id.to_proto(), cx).unwrap() + }); + channel_b.read_with(&cx_b, |channel, _| assert!(channel.messages().is_empty())); + channel_b.next_notification(&cx_b).await; + assert_eq!( + channel_messages(&channel_b, &cx_b), + &[(user_id_b.to_proto(), "hello A, it's B.".to_string())] + ); channel_a.update(&mut cx_a, |channel, cx| { channel.send_message("oh, hi B.".to_string(), cx).unwrap(); @@ -549,8 +563,34 @@ async fn test_basic_chat(mut cx_a: TestAppContext, cx_b: TestAppContext) { &["oh, hi B.", "sup"] ) }); - channel_a.next_notification(&cx_a).await; + channel_a.read_with(&cx_a, |channel, _| { + assert_eq!(channel.pending_messages().len(), 1); + }); + channel_a.next_notification(&cx_a).await; + channel_a.read_with(&cx_a, |channel, _| { + assert_eq!(channel.pending_messages().len(), 0); + }); + + channel_b.next_notification(&cx_b).await; + assert_eq!( + channel_messages(&channel_b, &cx_b), + &[ + (user_id_b.to_proto(), "hello A, it's B.".to_string()), + (user_id_a.to_proto(), "oh, hi B.".to_string()), + (user_id_a.to_proto(), "sup".to_string()), + ] + ); + + fn channel_messages(channel: &ModelHandle, cx: &TestAppContext) -> Vec<(u64, String)> { + channel.read_with(cx, |channel, _| { + channel + .messages() + .iter() + .map(|m| (m.sender_id, m.body.clone())) + .collect() + }) + } } struct TestServer { @@ -582,7 +622,7 @@ impl TestServer { ) -> (UserId, Arc) { let user_id = self.app_state.db.create_user(name, false).await.unwrap(); let client = Client::new(); - let (client_conn, server_conn) = Channel::bidirectional(); + let (client_conn, server_conn) = test::Channel::bidirectional(); cx.background() .spawn( self.server diff --git a/zed/src/channel.rs b/zed/src/channel.rs index cbbdec6015565b636d4a1172d81202d14ef05d61..9390d8bac4431fc56d8c35e95e7e19e7954527cf 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -146,7 +146,7 @@ impl Channel { pub fn send_message(&mut self, body: String, cx: &mut ModelContext) -> Result<()> { let channel_id = self.details.id; - let current_user_id = self.rpc.user_id().ok_or_else(|| anyhow!("not logged in"))?; + let current_user_id = self.current_user_id()?; let local_id = self.next_local_message_id; self.next_local_message_id += 1; self.pending_messages.push(PendingChannelMessage { @@ -187,12 +187,22 @@ impl Channel { &self.pending_messages } + fn current_user_id(&self) -> Result { + self.rpc.user_id().ok_or_else(|| anyhow!("not logged in")) + } + fn handle_message_sent( &mut self, message: TypedEnvelope, - rpc: Arc, + _: Arc, cx: &mut ModelContext, ) -> Result<()> { + let message = message + .payload + .message + .ok_or_else(|| anyhow!("empty message"))?; + self.messages.push_back(message.into()); + cx.notify(); Ok(()) } } From 5f3e6f35d48a21b1af3ce1300161aa8ccfe13687 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 20 Aug 2021 14:31:59 +0200 Subject: [PATCH 032/204] Implement and test channel cleanup as clients leave Co-Authored-By: Nathan Sobo --- server/src/main.rs | 2 - server/src/rpc.rs | 830 +++++++++++++++++++++++++++++++++++++++++++- server/src/tests.rs | 724 -------------------------------------- zed/src/channel.rs | 1 - 4 files changed, 825 insertions(+), 732 deletions(-) delete mode 100644 server/src/tests.rs diff --git a/server/src/main.rs b/server/src/main.rs index b98c8b0d04e8cdda766f95d3d2ed37d9bad9aad1..a49705e2493c20e7841c70c1c5ffc6359befaa83 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -9,8 +9,6 @@ mod github; mod home; mod rpc; mod team; -#[cfg(test)] -mod tests; use self::errors::TideResultExt as _; use anyhow::{Context, Result}; diff --git a/server/src/rpc.rs b/server/src/rpc.rs index e5086d870cba3b57bfd7922c37d50395a73cef5e..377cf5af4ced52b69a380dee9c8dfff80f2dd46b 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -10,11 +10,11 @@ use async_tungstenite::{ WebSocketStream, }; use futures::{future::BoxFuture, FutureExt}; -use postage::prelude::Stream as _; +use postage::{mpsc, prelude::Sink as _, prelude::Stream as _}; use sha1::{Digest as _, Sha1}; use std::{ any::TypeId, - collections::{HashMap, HashSet}, + collections::{hash_map, HashMap, HashSet}, future::Future, mem, sync::Arc, @@ -46,6 +46,7 @@ pub struct Server { state: RwLock, app_state: Arc, handlers: HashMap, + notifications: Option>, } #[derive(Default)] @@ -77,12 +78,17 @@ struct Channel { } impl Server { - pub fn new(app_state: Arc, peer: Arc) -> Arc { - let mut server = Server { + pub fn new( + app_state: Arc, + peer: Arc, + notifications: Option>, + ) -> Arc { + let mut server = Self { peer, app_state, state: Default::default(), handlers: Default::default(), + notifications, }; server @@ -98,6 +104,7 @@ impl Server { .add_handler(Server::get_channels) .add_handler(Server::get_users) .add_handler(Server::join_channel) + .add_handler(Server::leave_channel) .add_handler(Server::send_channel_message); Arc::new(server) @@ -157,6 +164,10 @@ impl Server { } else { log::info!("RPC message handled. duration:{:?}", start_time.elapsed()); } + + if let Some(mut notifications) = this.notifications.clone() { + let _ = notifications.send(()).await; + } } else { log::warn!("unhandled message: {}", message.payload_type_name()); } @@ -597,6 +608,33 @@ impl Server { Ok(()) } + async fn leave_channel( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let user_id = self + .state + .read() + .await + .user_id_for_connection(request.sender_id)?; + let channel_id = ChannelId::from_proto(request.payload.channel_id); + if !self + .app_state + .db + .can_user_access_channel(user_id, channel_id) + .await? + { + Err(anyhow!("access denied"))?; + } + + self.state + .write() + .await + .leave_channel(request.sender_id, channel_id); + + Ok(()) + } + async fn send_channel_message( self: Arc, request: TypedEnvelope, @@ -698,6 +736,18 @@ impl ServerState { } } + fn leave_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) { + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.channels.remove(&channel_id); + if let hash_map::Entry::Occupied(mut entry) = self.channels.entry(channel_id) { + entry.get_mut().connection_ids.remove(&connection_id); + if entry.get_mut().connection_ids.is_empty() { + entry.remove(); + } + } + } + } + fn user_id_for_connection(&self, connection_id: ConnectionId) -> tide::Result { Ok(self .connections @@ -806,7 +856,7 @@ impl Channel { } pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { - let server = Server::new(app.state().clone(), rpc.clone()); + let server = Server::new(app.state().clone(), rpc.clone(), None); app.at("/rpc").with(auth::VerifyToken).get(move |request: Request>| { let user_id = request.ext::().copied(); let server = server.clone(); @@ -863,3 +913,773 @@ fn header_contains_ignore_case( }) .unwrap_or(false) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + auth, + db::{self, UserId}, + github, rpc, AppState, Config, + }; + use async_std::{sync::RwLockReadGuard, task}; + use gpui::{ModelHandle, TestAppContext}; + use postage::mpsc; + use rand::prelude::*; + use serde_json::json; + use sqlx::{ + migrate::{MigrateDatabase, Migrator}, + types::time::OffsetDateTime, + Executor as _, Postgres, + }; + use std::{path::Path, sync::Arc, time::Duration}; + use zed::{ + channel::{Channel, ChannelDetails, ChannelList}, + editor::Editor, + fs::{FakeFs, Fs as _}, + language::LanguageRegistry, + rpc::Client, + settings, test, + worktree::Worktree, + }; + use zrpc::Peer; + + #[gpui::test] + async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + let (window_b, _) = cx_b.add_window(|_| EmptyView); + let settings = settings::channel(&cx_b.font_cache()).unwrap().1; + let lang_registry = Arc::new(LanguageRegistry::new()); + + // 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; + + cx_a.foreground().forbid_parking(); + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.insert_tree( + "/a", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let worktree_a = Worktree::open_local( + "/a".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id()); + worktree_a + .condition(&cx_a, |tree, _| { + tree.peers() + .values() + .any(|replica_id| *replica_id == replica_id_b) + }) + .await; + + // Open the same file as client B and client A. + let buffer_b = worktree_b + .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx)) + .await + .unwrap(); + buffer_b.read_with(&cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); + worktree_a.read_with(&cx_a, |tree, cx| assert!(tree.has_open_buffer("b.txt", cx))); + let buffer_a = worktree_a + .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx)) + .await + .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)); + buffer_a + .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1) + .await; + + // Edit the buffer as client B and see that edit as client A. + editor_b.update(&mut cx_b, |editor, cx| { + editor.insert(&"ok, ".to_string(), cx) + }); + buffer_a + .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents") + .await; + + // Remove the selection set as client B, see those selections disappear as client A. + cx_b.update(move |_| drop(editor_b)); + buffer_a + .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) + .await; + + // Close the buffer as client A, see that the buffer is closed. + drop(buffer_a); + worktree_a + .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx)) + .await; + + // Dropping the worktree removes client B from client A's peers. + cx_b.update(move |_| drop(worktree_b)); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().is_empty()) + .await; + } + + #[gpui::test] + async fn test_propagate_saves_and_fs_changes_in_shared_worktree( + mut cx_a: TestAppContext, + mut cx_b: TestAppContext, + mut cx_c: TestAppContext, + ) { + let lang_registry = Arc::new(LanguageRegistry::new()); + + // 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; + + cx_a.foreground().forbid_parking(); + + let fs = Arc::new(FakeFs::new()); + + // Share a worktree as client A. + fs.insert_tree( + "/a", + json!({ + "file1": "", + "file2": "" + }), + ) + .await; + + let worktree_a = Worktree::open_local( + "/a".as_ref(), + lang_registry.clone(), + fs.clone(), + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as clients B and C. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token.clone(), + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let worktree_c = Worktree::open_remote( + client_c.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_c.to_async(), + ) + .await + .unwrap(); + + // Open and edit a buffer as both guests B and C. + let buffer_b = worktree_b + .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx)) + .await + .unwrap(); + let buffer_c = worktree_c + .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx)) + .await + .unwrap(); + buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx)); + buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx)); + + // Open and edit that buffer as the host. + let buffer_a = worktree_a + .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx)) + .await + .unwrap(); + + buffer_a + .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") + .await; + buffer_a.update(&mut cx_a, |buf, cx| { + buf.edit([buf.len()..buf.len()], "i-am-a", cx) + }); + + // Wait for edits to propagate + buffer_a + .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") + .await; + buffer_b + .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") + .await; + buffer_c + .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") + .await; + + // Edit the buffer as the host and concurrently save as guest B. + let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap()); + buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx)); + save_b.await.unwrap(); + assert_eq!( + fs.load("/a/file1".as_ref()).await.unwrap(), + "hi-a, i-am-c, i-am-b, i-am-a" + ); + buffer_a.read_with(&cx_a, |buf, _| assert!(!buf.is_dirty())); + buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty())); + buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await; + + // Make changes on host's file system, see those changes on the guests. + fs.rename("/a/file2".as_ref(), "/a/file3".as_ref()) + .await + .unwrap(); + fs.insert_file(Path::new("/a/file4"), "4".into()) + .await + .unwrap(); + + worktree_b + .condition(&cx_b, |tree, _| tree.file_count() == 3) + .await; + worktree_c + .condition(&cx_c, |tree, _| tree.file_count() == 3) + .await; + worktree_b.read_with(&cx_b, |tree, _| { + assert_eq!( + tree.paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + &["file1", "file3", "file4"] + ) + }); + worktree_c.read_with(&cx_c, |tree, _| { + assert_eq!( + tree.paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + &["file1", "file3", "file4"] + ) + }); + } + + #[gpui::test] + async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + let lang_registry = Arc::new(LanguageRegistry::new()); + + // 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; + + cx_a.foreground().forbid_parking(); + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.save(Path::new("/a.txt"), &"a-contents".into()) + .await + .unwrap(); + let worktree_a = Worktree::open_local( + "/".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + let buffer_b = worktree_b + .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)) + .await + .unwrap(); + let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime); + + buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx)); + buffer_b.read_with(&cx_b, |buf, _| { + assert!(buf.is_dirty()); + assert!(!buf.has_conflict()); + }); + + buffer_b + .update(&mut cx_b, |buf, cx| buf.save(cx)) + .unwrap() + .await + .unwrap(); + worktree_b + .condition(&cx_b, |_, cx| { + buffer_b.read(cx).file().unwrap().mtime != mtime + }) + .await; + buffer_b.read_with(&cx_b, |buf, _| { + assert!(!buf.is_dirty()); + assert!(!buf.has_conflict()); + }); + + buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "hello ", cx)); + buffer_b.read_with(&cx_b, |buf, _| { + assert!(buf.is_dirty()); + assert!(!buf.has_conflict()); + }); + } + + #[gpui::test] + async fn test_editing_while_guest_opens_buffer( + mut cx_a: TestAppContext, + mut cx_b: TestAppContext, + ) { + let lang_registry = Arc::new(LanguageRegistry::new()); + + // 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; + + cx_a.foreground().forbid_parking(); + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.save(Path::new("/a.txt"), &"a-contents".into()) + .await + .unwrap(); + let worktree_a = Worktree::open_local( + "/".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + let buffer_a = worktree_a + .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx)) + .await + .unwrap(); + let buffer_b = cx_b + .background() + .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))); + + task::yield_now().await; + buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx)); + + let text = buffer_a.read_with(&cx_a, |buf, _| buf.text()); + let buffer_b = buffer_b.await.unwrap(); + buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await; + } + + #[gpui::test] + async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) { + let lang_registry = Arc::new(LanguageRegistry::new()); + + // 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; + + cx_a.foreground().forbid_parking(); + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.insert_tree( + "/a", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let worktree_a = Worktree::open_local( + "/a".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let _worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().len() == 1) + .await; + + // Drop client B's connection and ensure client A observes client B leaving the worktree. + client_b.disconnect().await.unwrap(); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().len() == 0) + .await; + } + + #[gpui::test] + async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + // 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; + + // 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(); + + // 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) + .await + .unwrap(); + db.add_channel_member(channel_id, user_id_b, false) + .await + .unwrap(); + db.create_channel_message( + channel_id, + user_id_b, + "hello A, it's B.", + OffsetDateTime::now_utc(), + ) + .await + .unwrap(); + + let channels_a = ChannelList::new(client_a, &mut cx_a.to_async()) + .await + .unwrap(); + channels_a.read_with(&cx_a, |list, _| { + assert_eq!( + list.available_channels(), + &[ChannelDetails { + id: channel_id.to_proto(), + name: "test-channel".to_string() + }] + ) + }); + let channel_a = channels_a.update(&mut cx_a, |this, cx| { + this.get_channel(channel_id.to_proto(), cx).unwrap() + }); + channel_a.read_with(&cx_a, |channel, _| assert!(channel.messages().is_empty())); + channel_a.next_notification(&cx_a).await; + assert_eq!( + channel_messages(&channel_a, &cx_a), + &[(user_id_b.to_proto(), "hello A, it's B.".to_string())] + ); + + let channels_b = ChannelList::new(client_b, &mut cx_b.to_async()) + .await + .unwrap(); + channels_b.read_with(&cx_b, |list, _| { + assert_eq!( + list.available_channels(), + &[ChannelDetails { + id: channel_id.to_proto(), + name: "test-channel".to_string() + }] + ) + }); + let channel_b = channels_b.update(&mut cx_b, |this, cx| { + this.get_channel(channel_id.to_proto(), cx).unwrap() + }); + channel_b.read_with(&cx_b, |channel, _| assert!(channel.messages().is_empty())); + channel_b.next_notification(&cx_b).await; + assert_eq!( + channel_messages(&channel_b, &cx_b), + &[(user_id_b.to_proto(), "hello A, it's B.".to_string())] + ); + + channel_a.update(&mut cx_a, |channel, cx| { + channel.send_message("oh, hi B.".to_string(), cx).unwrap(); + channel.send_message("sup".to_string(), cx).unwrap(); + assert_eq!( + channel + .pending_messages() + .iter() + .map(|m| &m.body) + .collect::>(), + &["oh, hi B.", "sup"] + ) + }); + channel_a.next_notification(&cx_a).await; + channel_a.read_with(&cx_a, |channel, _| { + assert_eq!(channel.pending_messages().len(), 1); + }); + channel_a.next_notification(&cx_a).await; + channel_a.read_with(&cx_a, |channel, _| { + assert_eq!(channel.pending_messages().len(), 0); + }); + + channel_b.next_notification(&cx_b).await; + assert_eq!( + channel_messages(&channel_b, &cx_b), + &[ + (user_id_b.to_proto(), "hello A, it's B.".to_string()), + (user_id_a.to_proto(), "oh, hi B.".to_string()), + (user_id_a.to_proto(), "sup".to_string()), + ] + ); + + assert_eq!( + server.state().await.channels[&channel_id] + .connection_ids + .len(), + 2 + ); + cx_b.update(|_| drop(channel_b)); + server + .condition(|state| state.channels[&channel_id].connection_ids.len() == 1) + .await; + + cx_a.update(|_| drop(channel_a)); + server + .condition(|state| !state.channels.contains_key(&channel_id)) + .await; + + fn channel_messages( + channel: &ModelHandle, + cx: &TestAppContext, + ) -> Vec<(u64, String)> { + channel.read_with(cx, |channel, _| { + channel + .messages() + .iter() + .map(|m| (m.sender_id, m.body.clone())) + .collect() + }) + } + } + + struct TestServer { + peer: Arc, + app_state: Arc, + server: Arc, + db_name: String, + notifications: mpsc::Receiver<()>, + } + + impl TestServer { + async fn start() -> Self { + let mut rng = StdRng::from_entropy(); + let db_name = format!("zed-test-{}", rng.gen::()); + let app_state = Self::build_app_state(&db_name).await; + let peer = Peer::new(); + let notifications = mpsc::channel(128); + let server = Server::new(app_state.clone(), peer.clone(), Some(notifications.0)); + Self { + peer, + app_state, + server, + db_name, + notifications: notifications.1, + } + } + + async fn create_client( + &mut self, + cx: &mut TestAppContext, + name: &str, + ) -> (UserId, Arc) { + let user_id = self.app_state.db.create_user(name, false).await.unwrap(); + let client = Client::new(); + let (client_conn, server_conn) = test::Channel::bidirectional(); + cx.background() + .spawn( + self.server + .handle_connection(server_conn, name.to_string(), user_id), + ) + .detach(); + client + .add_connection(user_id.to_proto(), client_conn, cx.to_async()) + .await + .unwrap(); + (user_id, client) + } + + async fn build_app_state(db_name: &str) -> Arc { + let mut config = Config::default(); + config.session_secret = "a".repeat(32); + config.database_url = format!("postgres://postgres@localhost/{}", db_name); + + Self::create_db(&config.database_url).await; + let db = db::Db( + db::DbOptions::new() + .max_connections(5) + .connect(&config.database_url) + .await + .expect("failed to connect to postgres database"), + ); + let migrator = Migrator::new(Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/migrations" + ))) + .await + .unwrap(); + migrator.run(&db.0).await.unwrap(); + + let github_client = github::AppClient::test(); + Arc::new(AppState { + db, + handlebars: Default::default(), + auth_client: auth::build_client("", ""), + repo_client: github::RepoClient::test(&github_client), + github_client, + config, + }) + } + + async fn create_db(url: &str) { + // Enable tests to run in parallel by serializing the creation of each test database. + lazy_static::lazy_static! { + static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(()); + } + + let _lock = DB_CREATION.lock().await; + Postgres::create_database(url) + .await + .expect("failed to create test database"); + } + + async fn state<'a>(&'a self) -> RwLockReadGuard<'a, ServerState> { + self.server.state.read().await + } + + async fn condition(&mut self, mut predicate: F) + where + F: FnMut(&ServerState) -> bool, + { + async_std::future::timeout(Duration::from_millis(500), async { + while !(predicate)(&*self.server.state.read().await) { + self.notifications.recv().await; + } + }) + .await + .expect("condition timed out"); + } + } + + impl Drop for TestServer { + fn drop(&mut self) { + task::block_on(async { + self.peer.reset().await; + self.app_state + .db + .execute( + format!( + " + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();", + self.db_name, + ) + .as_str(), + ) + .await + .unwrap(); + self.app_state.db.close().await; + Postgres::drop_database(&self.app_state.config.database_url) + .await + .unwrap(); + }); + } + } + + struct EmptyView; + + impl gpui::Entity for EmptyView { + type Event = (); + } + + impl gpui::View for EmptyView { + fn ui_name() -> &'static str { + "empty view" + } + + fn render<'a>(&self, _: &gpui::RenderContext) -> gpui::ElementBox { + gpui::Element::boxed(gpui::elements::Empty) + } + } +} diff --git a/server/src/tests.rs b/server/src/tests.rs deleted file mode 100644 index b6b9045ecbfb5751395de23af9610fccfad85f3c..0000000000000000000000000000000000000000 --- a/server/src/tests.rs +++ /dev/null @@ -1,724 +0,0 @@ -use crate::{ - auth, - db::{self, UserId}, - github, rpc, AppState, Config, -}; -use async_std::task; -use gpui::{ModelHandle, TestAppContext}; -use rand::prelude::*; -use serde_json::json; -use sqlx::{ - migrate::{MigrateDatabase, Migrator}, - types::time::OffsetDateTime, - Executor as _, Postgres, -}; -use std::{path::Path, sync::Arc}; -use zed::{ - channel::{Channel, ChannelDetails, ChannelList}, - editor::Editor, - fs::{FakeFs, Fs as _}, - language::LanguageRegistry, - rpc::Client, - settings, test, - worktree::Worktree, -}; -use zrpc::Peer; - -#[gpui::test] -async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { - tide::log::start(); - - let (window_b, _) = cx_b.add_window(|_| EmptyView); - let settings = settings::channel(&cx_b.font_cache()).unwrap().1; - let lang_registry = Arc::new(LanguageRegistry::new()); - - // 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; - - cx_a.foreground().forbid_parking(); - - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); - fs.insert_tree( - "/a", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - let worktree_a = Worktree::open_local( - "/a".as_ref(), - lang_registry.clone(), - fs, - &mut cx_a.to_async(), - ) - .await - .unwrap(); - worktree_a - .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) - .await - .unwrap(); - - // Join that worktree as client B, and see that a guest has joined as client A. - let worktree_b = Worktree::open_remote( - client_b.clone(), - worktree_id, - worktree_token, - lang_registry.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id()); - worktree_a - .condition(&cx_a, |tree, _| { - tree.peers() - .values() - .any(|replica_id| *replica_id == replica_id_b) - }) - .await; - - // Open the same file as client B and client A. - let buffer_b = worktree_b - .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx)) - .await - .unwrap(); - buffer_b.read_with(&cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); - worktree_a.read_with(&cx_a, |tree, cx| assert!(tree.has_open_buffer("b.txt", cx))); - let buffer_a = worktree_a - .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx)) - .await - .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)); - buffer_a - .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1) - .await; - - // Edit the buffer as client B and see that edit as client A. - editor_b.update(&mut cx_b, |editor, cx| { - editor.insert(&"ok, ".to_string(), cx) - }); - buffer_a - .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents") - .await; - - // Remove the selection set as client B, see those selections disappear as client A. - cx_b.update(move |_| drop(editor_b)); - buffer_a - .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) - .await; - - // Close the buffer as client A, see that the buffer is closed. - drop(buffer_a); - worktree_a - .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx)) - .await; - - // Dropping the worktree removes client B from client A's peers. - cx_b.update(move |_| drop(worktree_b)); - worktree_a - .condition(&cx_a, |tree, _| tree.peers().is_empty()) - .await; -} - -#[gpui::test] -async fn test_propagate_saves_and_fs_changes_in_shared_worktree( - mut cx_a: TestAppContext, - mut cx_b: TestAppContext, - mut cx_c: TestAppContext, -) { - let lang_registry = Arc::new(LanguageRegistry::new()); - - // 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; - - cx_a.foreground().forbid_parking(); - - let fs = Arc::new(FakeFs::new()); - - // Share a worktree as client A. - fs.insert_tree( - "/a", - json!({ - "file1": "", - "file2": "" - }), - ) - .await; - - let worktree_a = Worktree::open_local( - "/a".as_ref(), - lang_registry.clone(), - fs.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); - worktree_a - .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) - .await - .unwrap(); - - // Join that worktree as clients B and C. - let worktree_b = Worktree::open_remote( - client_b.clone(), - worktree_id, - worktree_token.clone(), - lang_registry.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - let worktree_c = Worktree::open_remote( - client_c.clone(), - worktree_id, - worktree_token, - lang_registry.clone(), - &mut cx_c.to_async(), - ) - .await - .unwrap(); - - // Open and edit a buffer as both guests B and C. - let buffer_b = worktree_b - .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx)) - .await - .unwrap(); - let buffer_c = worktree_c - .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx)) - .await - .unwrap(); - buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx)); - buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx)); - - // Open and edit that buffer as the host. - let buffer_a = worktree_a - .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx)) - .await - .unwrap(); - - buffer_a - .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") - .await; - buffer_a.update(&mut cx_a, |buf, cx| { - buf.edit([buf.len()..buf.len()], "i-am-a", cx) - }); - - // Wait for edits to propagate - buffer_a - .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") - .await; - buffer_b - .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") - .await; - buffer_c - .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") - .await; - - // Edit the buffer as the host and concurrently save as guest B. - let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap()); - buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx)); - save_b.await.unwrap(); - assert_eq!( - fs.load("/a/file1".as_ref()).await.unwrap(), - "hi-a, i-am-c, i-am-b, i-am-a" - ); - buffer_a.read_with(&cx_a, |buf, _| assert!(!buf.is_dirty())); - buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty())); - buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await; - - // Make changes on host's file system, see those changes on the guests. - fs.rename("/a/file2".as_ref(), "/a/file3".as_ref()) - .await - .unwrap(); - fs.insert_file(Path::new("/a/file4"), "4".into()) - .await - .unwrap(); - - worktree_b - .condition(&cx_b, |tree, _| tree.file_count() == 3) - .await; - worktree_c - .condition(&cx_c, |tree, _| tree.file_count() == 3) - .await; - worktree_b.read_with(&cx_b, |tree, _| { - assert_eq!( - tree.paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - &["file1", "file3", "file4"] - ) - }); - worktree_c.read_with(&cx_c, |tree, _| { - assert_eq!( - tree.paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - &["file1", "file3", "file4"] - ) - }); -} - -#[gpui::test] -async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { - let lang_registry = Arc::new(LanguageRegistry::new()); - - // 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; - - cx_a.foreground().forbid_parking(); - - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); - fs.save(Path::new("/a.txt"), &"a-contents".into()) - .await - .unwrap(); - let worktree_a = Worktree::open_local( - "/".as_ref(), - lang_registry.clone(), - fs, - &mut cx_a.to_async(), - ) - .await - .unwrap(); - worktree_a - .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) - .await - .unwrap(); - - // Join that worktree as client B, and see that a guest has joined as client A. - let worktree_b = Worktree::open_remote( - client_b.clone(), - worktree_id, - worktree_token, - lang_registry.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - - let buffer_b = worktree_b - .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)) - .await - .unwrap(); - let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime); - - buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx)); - buffer_b.read_with(&cx_b, |buf, _| { - assert!(buf.is_dirty()); - assert!(!buf.has_conflict()); - }); - - buffer_b - .update(&mut cx_b, |buf, cx| buf.save(cx)) - .unwrap() - .await - .unwrap(); - worktree_b - .condition(&cx_b, |_, cx| { - buffer_b.read(cx).file().unwrap().mtime != mtime - }) - .await; - buffer_b.read_with(&cx_b, |buf, _| { - assert!(!buf.is_dirty()); - assert!(!buf.has_conflict()); - }); - - buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "hello ", cx)); - buffer_b.read_with(&cx_b, |buf, _| { - assert!(buf.is_dirty()); - assert!(!buf.has_conflict()); - }); -} - -#[gpui::test] -async fn test_editing_while_guest_opens_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { - let lang_registry = Arc::new(LanguageRegistry::new()); - - // 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; - - cx_a.foreground().forbid_parking(); - - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); - fs.save(Path::new("/a.txt"), &"a-contents".into()) - .await - .unwrap(); - let worktree_a = Worktree::open_local( - "/".as_ref(), - lang_registry.clone(), - fs, - &mut cx_a.to_async(), - ) - .await - .unwrap(); - worktree_a - .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) - .await - .unwrap(); - - // Join that worktree as client B, and see that a guest has joined as client A. - let worktree_b = Worktree::open_remote( - client_b.clone(), - worktree_id, - worktree_token, - lang_registry.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - - let buffer_a = worktree_a - .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx)) - .await - .unwrap(); - let buffer_b = cx_b - .background() - .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))); - - task::yield_now().await; - buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx)); - - let text = buffer_a.read_with(&cx_a, |buf, _| buf.text()); - let buffer_b = buffer_b.await.unwrap(); - buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await; -} - -#[gpui::test] -async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) { - let lang_registry = Arc::new(LanguageRegistry::new()); - - // 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; - - cx_a.foreground().forbid_parking(); - - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); - fs.insert_tree( - "/a", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - let worktree_a = Worktree::open_local( - "/a".as_ref(), - lang_registry.clone(), - fs, - &mut cx_a.to_async(), - ) - .await - .unwrap(); - worktree_a - .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) - .await - .unwrap(); - - // Join that worktree as client B, and see that a guest has joined as client A. - let _worktree_b = Worktree::open_remote( - client_b.clone(), - worktree_id, - worktree_token, - lang_registry.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - worktree_a - .condition(&cx_a, |tree, _| tree.peers().len() == 1) - .await; - - // Drop client B's connection and ensure client A observes client B leaving the worktree. - client_b.disconnect().await.unwrap(); - worktree_a - .condition(&cx_a, |tree, _| tree.peers().len() == 0) - .await; -} - -#[gpui::test] -async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { - // 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; - - // 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(); - - // 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) - .await - .unwrap(); - db.add_channel_member(channel_id, user_id_b, false) - .await - .unwrap(); - db.create_channel_message( - channel_id, - user_id_b, - "hello A, it's B.", - OffsetDateTime::now_utc(), - ) - .await - .unwrap(); - - let channels_a = ChannelList::new(client_a, &mut cx_a.to_async()) - .await - .unwrap(); - channels_a.read_with(&cx_a, |list, _| { - assert_eq!( - list.available_channels(), - &[ChannelDetails { - id: channel_id.to_proto(), - name: "test-channel".to_string() - }] - ) - }); - let channel_a = channels_a.update(&mut cx_a, |this, cx| { - this.get_channel(channel_id.to_proto(), cx).unwrap() - }); - channel_a.read_with(&cx_a, |channel, _| assert!(channel.messages().is_empty())); - channel_a.next_notification(&cx_a).await; - assert_eq!( - channel_messages(&channel_a, &cx_a), - &[(user_id_b.to_proto(), "hello A, it's B.".to_string())] - ); - - let channels_b = ChannelList::new(client_b, &mut cx_b.to_async()) - .await - .unwrap(); - channels_b.read_with(&cx_b, |list, _| { - assert_eq!( - list.available_channels(), - &[ChannelDetails { - id: channel_id.to_proto(), - name: "test-channel".to_string() - }] - ) - }); - let channel_b = channels_b.update(&mut cx_b, |this, cx| { - this.get_channel(channel_id.to_proto(), cx).unwrap() - }); - channel_b.read_with(&cx_b, |channel, _| assert!(channel.messages().is_empty())); - channel_b.next_notification(&cx_b).await; - assert_eq!( - channel_messages(&channel_b, &cx_b), - &[(user_id_b.to_proto(), "hello A, it's B.".to_string())] - ); - - channel_a.update(&mut cx_a, |channel, cx| { - channel.send_message("oh, hi B.".to_string(), cx).unwrap(); - channel.send_message("sup".to_string(), cx).unwrap(); - assert_eq!( - channel - .pending_messages() - .iter() - .map(|m| &m.body) - .collect::>(), - &["oh, hi B.", "sup"] - ) - }); - channel_a.next_notification(&cx_a).await; - channel_a.read_with(&cx_a, |channel, _| { - assert_eq!(channel.pending_messages().len(), 1); - }); - channel_a.next_notification(&cx_a).await; - channel_a.read_with(&cx_a, |channel, _| { - assert_eq!(channel.pending_messages().len(), 0); - }); - - channel_b.next_notification(&cx_b).await; - assert_eq!( - channel_messages(&channel_b, &cx_b), - &[ - (user_id_b.to_proto(), "hello A, it's B.".to_string()), - (user_id_a.to_proto(), "oh, hi B.".to_string()), - (user_id_a.to_proto(), "sup".to_string()), - ] - ); - - fn channel_messages(channel: &ModelHandle, cx: &TestAppContext) -> Vec<(u64, String)> { - channel.read_with(cx, |channel, _| { - channel - .messages() - .iter() - .map(|m| (m.sender_id, m.body.clone())) - .collect() - }) - } -} - -struct TestServer { - peer: Arc, - app_state: Arc, - server: Arc, - db_name: String, -} - -impl TestServer { - async fn start() -> Self { - let mut rng = StdRng::from_entropy(); - let db_name = format!("zed-test-{}", rng.gen::()); - let app_state = Self::build_app_state(&db_name).await; - let peer = Peer::new(); - let server = rpc::Server::new(app_state.clone(), peer.clone()); - Self { - peer, - app_state, - server, - db_name, - } - } - - async fn create_client( - &mut self, - cx: &mut TestAppContext, - name: &str, - ) -> (UserId, Arc) { - let user_id = self.app_state.db.create_user(name, false).await.unwrap(); - let client = Client::new(); - let (client_conn, server_conn) = test::Channel::bidirectional(); - cx.background() - .spawn( - self.server - .handle_connection(server_conn, name.to_string(), user_id), - ) - .detach(); - client - .add_connection(user_id.to_proto(), client_conn, cx.to_async()) - .await - .unwrap(); - (user_id, client) - } - - async fn build_app_state(db_name: &str) -> Arc { - let mut config = Config::default(); - config.session_secret = "a".repeat(32); - config.database_url = format!("postgres://postgres@localhost/{}", db_name); - - Self::create_db(&config.database_url).await; - let db = db::Db( - db::DbOptions::new() - .max_connections(5) - .connect(&config.database_url) - .await - .expect("failed to connect to postgres database"), - ); - let migrator = Migrator::new(Path::new(concat!( - env!("CARGO_MANIFEST_DIR"), - "/migrations" - ))) - .await - .unwrap(); - migrator.run(&db.0).await.unwrap(); - - let github_client = github::AppClient::test(); - Arc::new(AppState { - db, - handlebars: Default::default(), - auth_client: auth::build_client("", ""), - repo_client: github::RepoClient::test(&github_client), - github_client, - config, - }) - } - - async fn create_db(url: &str) { - // Enable tests to run in parallel by serializing the creation of each test database. - lazy_static::lazy_static! { - static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(()); - } - - let _lock = DB_CREATION.lock().await; - Postgres::create_database(url) - .await - .expect("failed to create test database"); - } -} - -impl Drop for TestServer { - fn drop(&mut self) { - task::block_on(async { - self.peer.reset().await; - self.app_state - .db - .execute( - format!( - " - SELECT pg_terminate_backend(pg_stat_activity.pid) - FROM pg_stat_activity - WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();", - self.db_name, - ) - .as_str(), - ) - .await - .unwrap(); - self.app_state.db.close().await; - Postgres::drop_database(&self.app_state.config.database_url) - .await - .unwrap(); - }); - } -} - -struct EmptyView; - -impl gpui::Entity for EmptyView { - type Event = (); -} - -impl gpui::View for EmptyView { - fn ui_name() -> &'static str { - "empty view" - } - - fn render<'a>(&self, _: &gpui::RenderContext) -> gpui::ElementBox { - gpui::Element::boxed(gpui::elements::Empty) - } -} diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 9390d8bac4431fc56d8c35e95e7e19e7954527cf..0ce147113cff9a4246627a49dcdcdea043891a6a 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -100,7 +100,6 @@ impl ChannelList { impl Entity for Channel { type Event = (); - // TODO: Implement the server side of leaving a channel fn release(&mut self, cx: &mut MutableAppContext) { let rpc = self.rpc.clone(); let channel_id = self.details.id; From 0b9767651bd28b0570f49b97b5e8185e56014ce4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 20 Aug 2021 14:44:36 +0200 Subject: [PATCH 033/204] Provide a consistent ordering for channel messages based on their ID Co-Authored-By: Nathan Sobo --- zed/src/channel.rs | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 0ce147113cff9a4246627a49dcdcdea043891a6a..fe683b728c035fa18d5cb071351f51cc17ef4baf 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -7,7 +7,8 @@ use gpui::{ AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle, }; use std::{ - collections::{hash_map, HashMap, VecDeque}, + cmp::Ordering, + collections::{hash_map, BTreeSet, HashMap}, sync::Arc, }; use zrpc::{ @@ -29,8 +30,7 @@ pub struct ChannelDetails { pub struct Channel { details: ChannelDetails, - first_message_id: Option, - messages: VecDeque, + messages: BTreeSet, pending_messages: Vec, next_local_message_id: u64, rpc: Arc, @@ -135,7 +135,6 @@ impl Channel { Self { details, rpc, - first_message_id: None, messages: Default::default(), pending_messages: Default::default(), next_local_message_id: 0, @@ -163,7 +162,7 @@ impl Channel { .binary_search_by_key(&local_id, |msg| msg.local_id) { let body = this.pending_messages.remove(i).body; - this.messages.push_back(ChannelMessage { + this.messages.insert(ChannelMessage { id: response.message_id, sender_id: current_user_id, body, @@ -178,7 +177,7 @@ impl Channel { Ok(()) } - pub fn messages(&self) -> &VecDeque { + pub fn messages(&self) -> &BTreeSet { &self.messages } @@ -200,7 +199,7 @@ impl Channel { .payload .message .ok_or_else(|| anyhow!("empty message"))?; - self.messages.push_back(message.into()); + self.messages.insert(message.into()); cx.notify(); Ok(()) } @@ -224,3 +223,23 @@ impl From for ChannelMessage { } } } + +impl PartialOrd for ChannelMessage { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ChannelMessage { + fn cmp(&self, other: &Self) -> Ordering { + self.id.cmp(&other.id) + } +} + +impl PartialEq for ChannelMessage { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for ChannelMessage {} From b13da81a84113adc705c8c152de0adcf44596c1e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 20 Aug 2021 14:58:14 +0200 Subject: [PATCH 034/204] Extract a `Db::close` method and remove deref to `PgPool` Co-Authored-By: Nathan Sobo --- server/src/db.rs | 19 +++++++++++++------ server/src/rpc.rs | 20 +++----------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/server/src/db.rs b/server/src/db.rs index de196766e103ea42d4124cf4f01e652028554387..8351b9d224d92b6b8a79cc95240640f32882d5b9 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -328,13 +328,20 @@ impl Db { .fetch_all(&self.0) .await } -} - -impl std::ops::Deref for Db { - type Target = sqlx::PgPool; - fn deref(&self) -> &Self::Target { - &self.0 + #[cfg(test)] + pub async fn close(&self, db_name: &str) { + let query = " + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid(); + "; + sqlx::query(query) + .bind(db_name) + .execute(&self.0) + .await + .unwrap(); + self.0.close().await; } } diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 377cf5af4ced52b69a380dee9c8dfff80f2dd46b..91dda0964f7bc097b10c85b8b7edb28304e0144f 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -920,7 +920,7 @@ mod tests { use crate::{ auth, db::{self, UserId}, - github, rpc, AppState, Config, + github, AppState, Config, }; use async_std::{sync::RwLockReadGuard, task}; use gpui::{ModelHandle, TestAppContext}; @@ -930,7 +930,7 @@ mod tests { use sqlx::{ migrate::{MigrateDatabase, Migrator}, types::time::OffsetDateTime, - Executor as _, Postgres, + Postgres, }; use std::{path::Path, sync::Arc, time::Duration}; use zed::{ @@ -1645,21 +1645,7 @@ mod tests { fn drop(&mut self) { task::block_on(async { self.peer.reset().await; - self.app_state - .db - .execute( - format!( - " - SELECT pg_terminate_backend(pg_stat_activity.pid) - FROM pg_stat_activity - WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();", - self.db_name, - ) - .as_str(), - ) - .await - .unwrap(); - self.app_state.db.close().await; + self.app_state.db.close(&self.db_name).await; Postgres::drop_database(&self.app_state.config.database_url) .await .unwrap(); From 3ba530bca1ded8436267355c2a4d3d2befb2231f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 20 Aug 2021 15:00:06 +0200 Subject: [PATCH 035/204] :lipstick: Co-Authored-By: Nathan Sobo --- server/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 91dda0964f7bc097b10c85b8b7edb28304e0144f..e5a8d848e478411931bf058ef42b16303858819e 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1536,7 +1536,7 @@ mod tests { struct TestServer { peer: Arc, app_state: Arc, - server: Arc, + server: Arc, db_name: String, notifications: mpsc::Receiver<()>, } From 98f691d16d01c5680ecd41021c7853b5a2dc22a7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 20 Aug 2021 16:24:33 +0200 Subject: [PATCH 036/204] Make database interactions deterministic in test Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 20 +- gpui/src/executor.rs | 14 +- server/src/db.rs | 468 +++++++++++++++++++++++++------------------ server/src/main.rs | 11 +- server/src/rpc.rs | 112 +++++------ 5 files changed, 339 insertions(+), 286 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 221419341e93c63b8e9c661e37e58faecb68f86d..a2fa8188246bed0c3b2dab51a266b48926502d2c 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -14,7 +14,7 @@ use keymap::MatchResult; use parking_lot::{Mutex, RwLock}; use pathfinder_geometry::{rect::RectF, vector::vec2f}; use platform::Event; -use postage::{mpsc, oneshot, sink::Sink as _, stream::Stream as _}; +use postage::{mpsc, sink::Sink as _, stream::Stream as _}; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, @@ -2310,24 +2310,6 @@ impl ModelHandle { cx.update_model(self, update) } - pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { - let (tx, mut rx) = oneshot::channel(); - let mut tx = Some(tx); - - let mut cx = cx.cx.borrow_mut(); - self.update(&mut *cx, |_, cx| { - cx.observe(self, move |_, _, _| { - if let Some(mut tx) = tx.take() { - tx.blocking_send(()).ok(); - } - }); - }); - - async move { - rx.recv().await; - } - } - pub fn condition( &self, cx: &TestAppContext, diff --git a/gpui/src/executor.rs b/gpui/src/executor.rs index 78cb77c6b9521779228633f0a7f604febce63420..c848cff9c59bae7566baee89e47f94817e6df058 100644 --- a/gpui/src/executor.rs +++ b/gpui/src/executor.rs @@ -122,9 +122,14 @@ impl Deterministic { smol::pin!(future); let unparker = self.parker.lock().unparker(); - let waker = waker_fn(move || { - unparker.unpark(); - }); + let woken = Arc::new(AtomicBool::new(false)); + let waker = { + let woken = woken.clone(); + waker_fn(move || { + woken.store(true, SeqCst); + unparker.unpark(); + }) + }; let mut cx = Context::from_waker(&waker); let mut trace = Trace::default(); @@ -166,10 +171,11 @@ impl Deterministic { && state.scheduled_from_background.is_empty() && state.spawned_from_foreground.is_empty() { - if state.forbid_parking { + if state.forbid_parking && !woken.load(SeqCst) { panic!("deterministic executor parked after a call to forbid_parking"); } drop(state); + woken.store(false, SeqCst); self.parker.lock().park(); } diff --git a/server/src/db.rs b/server/src/db.rs index 8351b9d224d92b6b8a79cc95240640f32882d5b9..1e489aae3642cfd896b4449f97af1eb8c10449cf 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -1,3 +1,5 @@ +use anyhow::Context; +use async_std::task::{block_on, yield_now}; use serde::Serialize; use sqlx::{FromRow, Result}; use time::OffsetDateTime; @@ -5,7 +7,24 @@ use time::OffsetDateTime; pub use async_sqlx_session::PostgresSessionStore as SessionStore; pub use sqlx::postgres::PgPoolOptions as DbOptions; -pub struct Db(pub sqlx::PgPool); +macro_rules! test_support { + ($self:ident, { $($token:tt)* }) => {{ + let body = async { + $($token)* + }; + if $self.test_mode { + yield_now().await; + block_on(body) + } else { + body.await + } + }}; +} + +pub struct Db { + db: sqlx::PgPool, + test_mode: bool, +} #[derive(Debug, FromRow, Serialize)] pub struct User { @@ -37,6 +56,33 @@ pub struct ChannelMessage { } impl Db { + pub async fn new(url: &str, max_connections: u32) -> tide::Result { + let db = DbOptions::new() + .max_connections(max_connections) + .connect(url) + .await + .context("failed to connect to postgres database")?; + Ok(Self { + db, + test_mode: false, + }) + } + + #[cfg(test)] + pub fn test(url: &str, max_connections: u32) -> Self { + let mut db = block_on(Self::new(url, max_connections)).unwrap(); + db.test_mode = true; + db + } + + #[cfg(test)] + pub fn migrate(&self, path: &std::path::Path) { + block_on(async { + let migrator = sqlx::migrate::Migrator::new(path).await.unwrap(); + migrator.run(&self.db).await.unwrap(); + }); + } + // signups pub async fn create_signup( @@ -45,53 +91,63 @@ impl Db { email_address: &str, about: &str, ) -> Result { - let query = " - INSERT INTO signups (github_login, email_address, about) - VALUES ($1, $2, $3) - RETURNING id - "; - sqlx::query_scalar(query) - .bind(github_login) - .bind(email_address) - .bind(about) - .fetch_one(&self.0) - .await - .map(SignupId) + test_support!(self, { + let query = " + INSERT INTO signups (github_login, email_address, about) + VALUES ($1, $2, $3) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(github_login) + .bind(email_address) + .bind(about) + .fetch_one(&self.db) + .await + .map(SignupId) + }) } pub async fn get_all_signups(&self) -> Result> { - let query = "SELECT * FROM users ORDER BY github_login ASC"; - sqlx::query_as(query).fetch_all(&self.0).await + test_support!(self, { + let query = "SELECT * FROM users ORDER BY github_login ASC"; + sqlx::query_as(query).fetch_all(&self.db).await + }) } pub async fn delete_signup(&self, id: SignupId) -> Result<()> { - let query = "DELETE FROM signups WHERE id = $1"; - sqlx::query(query) - .bind(id.0) - .execute(&self.0) - .await - .map(drop) + test_support!(self, { + let query = "DELETE FROM signups WHERE id = $1"; + sqlx::query(query) + .bind(id.0) + .execute(&self.db) + .await + .map(drop) + }) } // users pub async fn create_user(&self, github_login: &str, admin: bool) -> Result { - let query = " - INSERT INTO users (github_login, admin) - VALUES ($1, $2) - RETURNING id - "; - sqlx::query_scalar(query) - .bind(github_login) - .bind(admin) - .fetch_one(&self.0) - .await - .map(UserId) + test_support!(self, { + let query = " + INSERT INTO users (github_login, admin) + VALUES ($1, $2) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(github_login) + .bind(admin) + .fetch_one(&self.db) + .await + .map(UserId) + }) } pub async fn get_all_users(&self) -> Result> { - let query = "SELECT * FROM users ORDER BY github_login ASC"; - sqlx::query_as(query).fetch_all(&self.0).await + test_support!(self, { + let query = "SELECT * FROM users ORDER BY github_login ASC"; + sqlx::query_as(query).fetch_all(&self.db).await + }) } pub async fn get_users_by_ids( @@ -99,53 +155,61 @@ impl Db { requester_id: UserId, ids: impl Iterator, ) -> Result> { - // Only return users that are in a common channel with the requesting user. - let query = " - SELECT users.* - FROM - users, channel_memberships - WHERE - users.id IN $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 - ) - "; - - sqlx::query_as(query) - .bind(&ids.map(|id| id.0).collect::>()) - .bind(requester_id) - .fetch_all(&self.0) - .await + test_support!(self, { + // Only return users that are in a common channel with the requesting user. + let query = " + SELECT users.* + FROM + users, channel_memberships + WHERE + users.id IN $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 + ) + "; + + sqlx::query_as(query) + .bind(&ids.map(|id| id.0).collect::>()) + .bind(requester_id) + .fetch_all(&self.db) + .await + }) } pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { - let query = "SELECT * FROM users WHERE github_login = $1 LIMIT 1"; - sqlx::query_as(query) - .bind(github_login) - .fetch_optional(&self.0) - .await + test_support!(self, { + let query = "SELECT * FROM users WHERE github_login = $1 LIMIT 1"; + sqlx::query_as(query) + .bind(github_login) + .fetch_optional(&self.db) + .await + }) } pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> { - let query = "UPDATE users SET admin = $1 WHERE id = $2"; - sqlx::query(query) - .bind(is_admin) - .bind(id.0) - .execute(&self.0) - .await - .map(drop) + test_support!(self, { + let query = "UPDATE users SET admin = $1 WHERE id = $2"; + sqlx::query(query) + .bind(is_admin) + .bind(id.0) + .execute(&self.db) + .await + .map(drop) + }) } pub async fn delete_user(&self, id: UserId) -> Result<()> { - let query = "DELETE FROM users WHERE id = $1;"; - sqlx::query(query) - .bind(id.0) - .execute(&self.0) - .await - .map(drop) + test_support!(self, { + let query = "DELETE FROM users WHERE id = $1;"; + sqlx::query(query) + .bind(id.0) + .execute(&self.db) + .await + .map(drop) + }) } // access tokens @@ -155,41 +219,47 @@ impl Db { user_id: UserId, access_token_hash: String, ) -> Result<()> { - let query = " + test_support!(self, { + let query = " INSERT INTO access_tokens (user_id, hash) VALUES ($1, $2) "; - sqlx::query(query) - .bind(user_id.0) - .bind(access_token_hash) - .execute(&self.0) - .await - .map(drop) + sqlx::query(query) + .bind(user_id.0) + .bind(access_token_hash) + .execute(&self.db) + .await + .map(drop) + }) } pub async fn get_access_token_hashes(&self, user_id: UserId) -> Result> { - let query = "SELECT hash FROM access_tokens WHERE user_id = $1"; - sqlx::query_scalar(query) - .bind(user_id.0) - .fetch_all(&self.0) - .await + test_support!(self, { + let query = "SELECT hash FROM access_tokens WHERE user_id = $1"; + sqlx::query_scalar(query) + .bind(user_id.0) + .fetch_all(&self.db) + .await + }) } // orgs #[cfg(test)] pub async fn create_org(&self, name: &str, slug: &str) -> Result { - let query = " - INSERT INTO orgs (name, slug) - VALUES ($1, $2) - RETURNING id - "; - sqlx::query_scalar(query) - .bind(name) - .bind(slug) - .fetch_one(&self.0) - .await - .map(OrgId) + test_support!(self, { + let query = " + INSERT INTO orgs (name, slug) + VALUES ($1, $2) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(name) + .bind(slug) + .fetch_one(&self.db) + .await + .map(OrgId) + }) } #[cfg(test)] @@ -199,50 +269,56 @@ impl Db { user_id: UserId, is_admin: bool, ) -> Result<()> { - let query = " - INSERT INTO org_memberships (org_id, user_id, admin) - VALUES ($1, $2, $3) - "; - sqlx::query(query) - .bind(org_id.0) - .bind(user_id.0) - .bind(is_admin) - .execute(&self.0) - .await - .map(drop) + test_support!(self, { + let query = " + INSERT INTO org_memberships (org_id, user_id, admin) + VALUES ($1, $2, $3) + "; + sqlx::query(query) + .bind(org_id.0) + .bind(user_id.0) + .bind(is_admin) + .execute(&self.db) + .await + .map(drop) + }) } // channels #[cfg(test)] pub async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result { - let query = " - INSERT INTO channels (owner_id, owner_is_user, name) - VALUES ($1, false, $2) - RETURNING id - "; - sqlx::query_scalar(query) - .bind(org_id.0) - .bind(name) - .fetch_one(&self.0) - .await - .map(ChannelId) + test_support!(self, { + let query = " + INSERT INTO channels (owner_id, owner_is_user, name) + VALUES ($1, false, $2) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(org_id.0) + .bind(name) + .fetch_one(&self.db) + .await + .map(ChannelId) + }) } pub async fn get_channels_for_user(&self, user_id: UserId) -> Result> { - let query = " - SELECT - channels.id, channels.name - FROM - channel_memberships, channels - WHERE - channel_memberships.user_id = $1 AND - channel_memberships.channel_id = channels.id - "; - sqlx::query_as(query) - .bind(user_id.0) - .fetch_all(&self.0) - .await + test_support!(self, { + let query = " + SELECT + channels.id, channels.name + FROM + channel_memberships, channels + WHERE + channel_memberships.user_id = $1 AND + channel_memberships.channel_id = channels.id + "; + sqlx::query_as(query) + .bind(user_id.0) + .fetch_all(&self.db) + .await + }) } pub async fn can_user_access_channel( @@ -250,18 +326,20 @@ impl Db { user_id: UserId, channel_id: ChannelId, ) -> Result { - let query = " - SELECT id - FROM channel_memberships - WHERE user_id = $1 AND channel_id = $2 - LIMIT 1 - "; - sqlx::query_scalar::<_, i32>(query) - .bind(user_id.0) - .bind(channel_id.0) - .fetch_optional(&self.0) - .await - .map(|e| e.is_some()) + test_support!(self, { + let query = " + SELECT id + FROM channel_memberships + WHERE user_id = $1 AND channel_id = $2 + LIMIT 1 + "; + sqlx::query_scalar::<_, i32>(query) + .bind(user_id.0) + .bind(channel_id.0) + .fetch_optional(&self.db) + .await + .map(|e| e.is_some()) + }) } #[cfg(test)] @@ -271,17 +349,19 @@ impl Db { user_id: UserId, is_admin: bool, ) -> Result<()> { - let query = " - INSERT INTO channel_memberships (channel_id, user_id, admin) - VALUES ($1, $2, $3) - "; - sqlx::query(query) - .bind(channel_id.0) - .bind(user_id.0) - .bind(is_admin) - .execute(&self.0) - .await - .map(drop) + test_support!(self, { + let query = " + INSERT INTO channel_memberships (channel_id, user_id, admin) + VALUES ($1, $2, $3) + "; + sqlx::query(query) + .bind(channel_id.0) + .bind(user_id.0) + .bind(is_admin) + .execute(&self.db) + .await + .map(drop) + }) } // messages @@ -293,19 +373,21 @@ impl Db { body: &str, timestamp: OffsetDateTime, ) -> Result { - let query = " - INSERT INTO channel_messages (channel_id, sender_id, body, sent_at) - VALUES ($1, $2, $3, $4) - RETURNING id - "; - sqlx::query_scalar(query) - .bind(channel_id.0) - .bind(sender_id.0) - .bind(body) - .bind(timestamp) - .fetch_one(&self.0) - .await - .map(MessageId) + test_support!(self, { + let query = " + INSERT INTO channel_messages (channel_id, sender_id, body, sent_at) + VALUES ($1, $2, $3, $4) + RETURNING id + "; + sqlx::query_scalar(query) + .bind(channel_id.0) + .bind(sender_id.0) + .bind(body) + .bind(timestamp) + .fetch_one(&self.db) + .await + .map(MessageId) + }) } pub async fn get_recent_channel_messages( @@ -313,35 +395,39 @@ impl Db { channel_id: ChannelId, count: usize, ) -> Result> { - let query = r#" - SELECT - id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at - FROM - channel_messages - WHERE - channel_id = $1 - LIMIT $2 - "#; - sqlx::query_as(query) - .bind(channel_id.0) - .bind(count as i64) - .fetch_all(&self.0) - .await + test_support!(self, { + let query = r#" + SELECT + id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at + FROM + channel_messages + WHERE + channel_id = $1 + LIMIT $2 + "#; + sqlx::query_as(query) + .bind(channel_id.0) + .bind(count as i64) + .fetch_all(&self.db) + .await + }) } #[cfg(test)] pub async fn close(&self, db_name: &str) { - let query = " - SELECT pg_terminate_backend(pg_stat_activity.pid) - FROM pg_stat_activity - WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid(); - "; - sqlx::query(query) - .bind(db_name) - .execute(&self.0) - .await - .unwrap(); - self.0.close().await; + test_support!(self, { + let query = " + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid(); + "; + sqlx::query(query) + .bind(db_name) + .execute(&self.db) + .await + .unwrap(); + self.db.close().await; + }) } } diff --git a/server/src/main.rs b/server/src/main.rs index a49705e2493c20e7841c70c1c5ffc6359befaa83..41f2638027c591dd354202b46bf2d53c08decce3 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -11,11 +11,11 @@ mod rpc; mod team; use self::errors::TideResultExt as _; -use anyhow::{Context, Result}; +use anyhow::Result; use async_std::net::TcpListener; use async_trait::async_trait; use auth::RequestExt as _; -use db::{Db, DbOptions}; +use db::Db; use handlebars::{Handlebars, TemplateRenderError}; use parking_lot::RwLock; use rust_embed::RustEmbed; @@ -54,12 +54,7 @@ pub struct AppState { impl AppState { async fn new(config: Config) -> tide::Result> { - let db = Db(DbOptions::new() - .max_connections(5) - .connect(&config.database_url) - .await - .context("failed to connect to postgres database")?); - + let db = Db::new(&config.database_url, 5).await?; let github_client = github::AppClient::new(config.github_app_id, config.github_private_key.clone()); let repo_client = github_client diff --git a/server/src/rpc.rs b/server/src/rpc.rs index e5a8d848e478411931bf058ef42b16303858819e..1dc372af0158286be9de25b14f5e69906f3c37d2 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -922,16 +922,15 @@ mod tests { db::{self, UserId}, github, AppState, Config, }; - use async_std::{sync::RwLockReadGuard, task}; - use gpui::{ModelHandle, TestAppContext}; + use async_std::{ + sync::RwLockReadGuard, + task::{self, block_on}, + }; + use gpui::TestAppContext; use postage::mpsc; use rand::prelude::*; use serde_json::json; - use sqlx::{ - migrate::{MigrateDatabase, Migrator}, - types::time::OffsetDateTime, - Postgres, - }; + use sqlx::{migrate::MigrateDatabase, types::time::OffsetDateTime, Postgres}; use std::{path::Path, sync::Arc, time::Duration}; use zed::{ channel::{Channel, ChannelDetails, ChannelList}, @@ -1400,6 +1399,8 @@ mod tests { #[gpui::test] async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); + // 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; @@ -1444,11 +1445,12 @@ mod tests { this.get_channel(channel_id.to_proto(), cx).unwrap() }); channel_a.read_with(&cx_a, |channel, _| assert!(channel.messages().is_empty())); - channel_a.next_notification(&cx_a).await; - assert_eq!( - channel_messages(&channel_a, &cx_a), - &[(user_id_b.to_proto(), "hello A, it's B.".to_string())] - ); + channel_a + .condition(&cx_a, |channel, _| { + channel_messages(channel) + == [(user_id_b.to_proto(), "hello A, it's B.".to_string())] + }) + .await; let channels_b = ChannelList::new(client_b, &mut cx_b.to_async()) .await @@ -1462,15 +1464,17 @@ mod tests { }] ) }); + let channel_b = channels_b.update(&mut cx_b, |this, cx| { this.get_channel(channel_id.to_proto(), cx).unwrap() }); channel_b.read_with(&cx_b, |channel, _| assert!(channel.messages().is_empty())); - channel_b.next_notification(&cx_b).await; - assert_eq!( - channel_messages(&channel_b, &cx_b), - &[(user_id_b.to_proto(), "hello A, it's B.".to_string())] - ); + channel_b + .condition(&cx_b, |channel, _| { + channel_messages(channel) + == [(user_id_b.to_proto(), "hello A, it's B.".to_string())] + }) + .await; channel_a.update(&mut cx_a, |channel, cx| { channel.send_message("oh, hi B.".to_string(), cx).unwrap(); @@ -1484,24 +1488,20 @@ mod tests { &["oh, hi B.", "sup"] ) }); - channel_a.next_notification(&cx_a).await; - channel_a.read_with(&cx_a, |channel, _| { - assert_eq!(channel.pending_messages().len(), 1); - }); - channel_a.next_notification(&cx_a).await; - channel_a.read_with(&cx_a, |channel, _| { - assert_eq!(channel.pending_messages().len(), 0); - }); - channel_b.next_notification(&cx_b).await; - assert_eq!( - channel_messages(&channel_b, &cx_b), - &[ - (user_id_b.to_proto(), "hello A, it's B.".to_string()), - (user_id_a.to_proto(), "oh, hi B.".to_string()), - (user_id_a.to_proto(), "sup".to_string()), - ] - ); + channel_a + .condition(&cx_a, |channel, _| channel.pending_messages().is_empty()) + .await; + channel_b + .condition(&cx_b, |channel, _| { + channel_messages(channel) + == [ + (user_id_b.to_proto(), "hello A, it's B.".to_string()), + (user_id_a.to_proto(), "oh, hi B.".to_string()), + (user_id_a.to_proto(), "sup".to_string()), + ] + }) + .await; assert_eq!( server.state().await.channels[&channel_id] @@ -1519,17 +1519,12 @@ mod tests { .condition(|state| !state.channels.contains_key(&channel_id)) .await; - fn channel_messages( - channel: &ModelHandle, - cx: &TestAppContext, - ) -> Vec<(u64, String)> { - channel.read_with(cx, |channel, _| { - channel - .messages() - .iter() - .map(|m| (m.sender_id, m.body.clone())) - .collect() - }) + fn channel_messages(channel: &Channel) -> Vec<(u64, String)> { + channel + .messages() + .iter() + .map(|m| (m.sender_id, m.body.clone())) + .collect() } } @@ -1584,21 +1579,12 @@ mod tests { config.session_secret = "a".repeat(32); config.database_url = format!("postgres://postgres@localhost/{}", db_name); - Self::create_db(&config.database_url).await; - let db = db::Db( - db::DbOptions::new() - .max_connections(5) - .connect(&config.database_url) - .await - .expect("failed to connect to postgres database"), - ); - let migrator = Migrator::new(Path::new(concat!( + Self::create_db(&config.database_url); + let db = db::Db::test(&config.database_url, 5); + db.migrate(Path::new(concat!( env!("CARGO_MANIFEST_DIR"), "/migrations" - ))) - .await - .unwrap(); - migrator.run(&db.0).await.unwrap(); + ))); let github_client = github::AppClient::test(); Arc::new(AppState { @@ -1611,16 +1597,14 @@ mod tests { }) } - async fn create_db(url: &str) { + fn create_db(url: &str) { // Enable tests to run in parallel by serializing the creation of each test database. lazy_static::lazy_static! { - static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(()); + static ref DB_CREATION: std::sync::Mutex<()> = std::sync::Mutex::new(()); } - let _lock = DB_CREATION.lock().await; - Postgres::create_database(url) - .await - .expect("failed to create test database"); + let _lock = DB_CREATION.lock(); + block_on(Postgres::create_database(url)).expect("failed to create test database"); } async fn state<'a>(&'a self) -> RwLockReadGuard<'a, ServerState> { From f463b2ed50b175d3d47907394557f61b452198a0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 20 Aug 2021 16:41:10 +0200 Subject: [PATCH 037/204] Forbid parking earlier in server integration tests Co-Authored-By: Nathan Sobo --- server/src/rpc.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 1dc372af0158286be9de25b14f5e69906f3c37d2..9742ec015f8fa2368397fd26471fd627655d9c1e 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1054,6 +1054,7 @@ mod tests { mut cx_b: TestAppContext, mut cx_c: TestAppContext, ) { + cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); // Connect to a server as 3 clients. @@ -1062,8 +1063,6 @@ mod tests { let (_, client_b) = server.create_client(&mut cx_b, "user_b").await; let (_, client_c) = server.create_client(&mut cx_c, "user_c").await; - cx_a.foreground().forbid_parking(); - let fs = Arc::new(FakeFs::new()); // Share a worktree as client A. @@ -1196,6 +1195,7 @@ mod tests { #[gpui::test] async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); // Connect to a server as 2 clients. @@ -1203,8 +1203,6 @@ mod tests { 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(); - // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); fs.save(Path::new("/a.txt"), &"a-contents".into()) @@ -1278,6 +1276,7 @@ mod tests { mut cx_a: TestAppContext, mut cx_b: TestAppContext, ) { + cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); // Connect to a server as 2 clients. @@ -1285,8 +1284,6 @@ mod tests { 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(); - // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); fs.save(Path::new("/a.txt"), &"a-contents".into()) @@ -1339,6 +1336,7 @@ mod tests { #[gpui::test] async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); // Connect to a server as 2 clients. @@ -1346,8 +1344,6 @@ mod tests { let (_, client_a) = server.create_client(&mut cx_a, "user_a").await; let (_, client_b) = server.create_client(&mut cx_a, "user_b").await; - cx_a.foreground().forbid_parking(); - // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); fs.insert_tree( From 9b7756f566590dc3929ec7a1757f5c3fcbb15efe Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Aug 2021 10:18:05 -0600 Subject: [PATCH 038/204] Use singular names in theme and number surfaces from 0 --- zed/assets/themes/_base.toml | 28 ++++++++++++++-------------- zed/assets/themes/dark.toml | 12 ++++++------ zed/assets/themes/light.toml | 12 ++++++------ 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 50a1b15e0c504d36d32c4fb312b9fb7d7d2b5236..a6663ec9c35f12e56b493e051f5ac8ec7f86fbdd 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -1,9 +1,9 @@ [ui] -background = "$surfaces.1" +background = "$surface.0" [ui.tab] -background = "$surfaces.2" -text = "$text_colors.dull" +background = "$surface.1" +text = "$text_color.dull" border = { color = "#000000", width = 1.0 } padding = { left = 10, right = 10 } icon_close = "#383839" @@ -12,12 +12,12 @@ icon_conflict = "#e45349" [ui.active_tab] extends = "$ui.tab" -background = "$surfaces.3" -text = "$text_colors.bright" +background = "$surface.2" +text = "$text_color.bright" [ui.selector] -background = "$surfaces.4" -text = "$text_colors.bright" +background = "$surface.3" +text = "$text_color.bright" padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } margin.top = 12.0 corner_radius = 6.0 @@ -35,13 +35,13 @@ extends = "$ui.selector.item" background = "#094771" [editor] -background = "$surfaces.3" -gutter_background = "$surfaces.3" -active_line_background = "$surfaces.4" -line_number = "$text_colors.dull" -line_number_active = "$text_colors.bright" -text = "$text_colors.normal" +background = "$surface.2" +gutter_background = "$surface.2" +active_line_background = "$surface.3" +line_number = "$text_color.dull" +line_number_active = "$text_color.bright" +text = "$text_color.normal" replicas = [ - { selection = "#264f78", cursor = "$text_colors.bright" }, + { selection = "#264f78", cursor = "$text_color.bright" }, { selection = "#504f31", cursor = "#fcf154" }, ] diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index 58872cc75970df0d62878017e0d4bd98a4e52285..2bbd37e9344eedf1d56aa30bbac0b4866a2c2406 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -1,12 +1,12 @@ extends = "_base" -[surfaces] -1 = "#050101" -2 = "#131415" -3 = "#1c1d1e" -4 = "#3a3b3c" +[surface] +0 = "#050101" +1 = "#131415" +2 = "#1c1d1e" +3 = "#3a3b3c" -[text_colors] +[text_color] dull = "#5a5a5b" bright = "#ffffff" normal = "#d4d4d4" diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml index 1cdb2c51ddf9010f41c2283ac03d57a295aa6edf..c384e27f27ef32668bff0696f2879412368084f9 100644 --- a/zed/assets/themes/light.toml +++ b/zed/assets/themes/light.toml @@ -1,12 +1,12 @@ extends = "_base" -[surfaces] -1 = "#ffffff" -2 = "#f3f3f3" -3 = "#ececec" -4 = "#3a3b3c" +[surface] +0 = "#ffffff" +1 = "#f3f3f3" +2 = "#ececec" +3 = "#3a3b3c" -[text_colors] +[text_color] dull = "#acacac" bright = "#111111" normal = "#333333" From 9ff764983d5b75c77695cc562fe14678da3a8a8f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Aug 2021 10:44:34 -0600 Subject: [PATCH 039/204] Flatten theme styles by eliminating top-level ui key Co-Authored-By: Max Brunsfeld Co-Authored-By: Antonio Scandurra --- zed/assets/themes/_base.toml | 16 ++++++++-------- zed/src/file_finder.rs | 8 ++++---- zed/src/theme.rs | 13 ++++++------- zed/src/theme_selector.rs | 6 +++--- zed/src/workspace.rs | 2 +- zed/src/workspace/pane.rs | 4 ++-- 6 files changed, 24 insertions(+), 25 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index a6663ec9c35f12e56b493e051f5ac8ec7f86fbdd..2ad222c1749dfae19a2cbee042a5519e901e8a4a 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -1,7 +1,7 @@ -[ui] +[workspace] background = "$surface.0" -[ui.tab] +[tab] background = "$surface.1" text = "$text_color.dull" border = { color = "#000000", width = 1.0 } @@ -10,12 +10,12 @@ icon_close = "#383839" icon_dirty = "#556de8" icon_conflict = "#e45349" -[ui.active_tab] -extends = "$ui.tab" +[active_tab] +extends = "$tab" background = "$surface.2" text = "$text_color.bright" -[ui.selector] +[selector] background = "$surface.3" text = "$text_color.bright" padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } @@ -23,15 +23,15 @@ margin.top = 12.0 corner_radius = 6.0 shadow = { offset = [0.0, 0.0], blur = 12.0, color = "#00000088" } -[ui.selector.item] +[selector.item] background = "#424344" text = "#cccccc" highlight_text = { color = "#18a3ff", weight = "bold" } border = { color = "#000000", width = 1.0 } padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } -[ui.selector.active_item] -extends = "$ui.selector.item" +[selector.active_item] +extends = "$selector.item" background = "#094771" [editor] diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 7c9900c3f4133b84b9b1063f5d6efabf6b96f6d2..d370f85a081d55155996dd891ba5af47b4a0d480 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -75,7 +75,7 @@ impl View for FileFinder { .with_child(Expanded::new(1.0, self.render_matches()).boxed()) .boxed(), ) - .with_style(&settings.theme.ui.selector.container) + .with_style(&settings.theme.selector.container) .boxed(), ) .with_max_width(600.0) @@ -107,7 +107,7 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_style(&settings.theme.ui.selector.label) + .with_style(&settings.theme.selector.label) .boxed(), ) .with_margin_top(6.0) @@ -142,9 +142,9 @@ impl FileFinder { let selected_index = self.selected_index(); let settings = self.settings.borrow(); let style = if index == selected_index { - &settings.theme.ui.selector.active_item + &settings.theme.selector.active_item } else { - &settings.theme.ui.selector.item + &settings.theme.selector.item }; let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match); diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 39412804d895776366b4384f1aaa95dd2017cb79..52b87860c7c871131e2181a004683a4d2a732f13 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -30,18 +30,18 @@ pub struct HighlightId(u32); pub struct Theme { #[serde(default)] pub name: String, - pub ui: Ui, + pub workspace: Workspace, + pub tab: Tab, + pub active_tab: Tab, + pub selector: Selector, pub editor: Editor, #[serde(deserialize_with = "deserialize_syntax_theme")] pub syntax: Vec<(String, TextStyle)>, } #[derive(Debug, Default, Deserialize)] -pub struct Ui { +pub struct Workspace { pub background: Color, - pub tab: Tab, - pub active_tab: Tab, - pub selector: Selector, } #[derive(Debug, Deserialize)] @@ -800,8 +800,6 @@ mod tests { fn test_highlight_map() { let theme = Theme { name: "test".into(), - ui: Default::default(), - editor: Default::default(), syntax: [ ("function", Color::from_u32(0x100000ff)), ("function.method", Color::from_u32(0x200000ff)), @@ -813,6 +811,7 @@ mod tests { .iter() .map(|(name, color)| (name.to_string(), (*color).into())) .collect(), + ..Default::default() }; let capture_names = &[ diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 25816a41d2d6bb6459e28deffa4d6e989dd3cf06..2853f938fa8327ac977271e834d2e35acf85b8c9 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -207,7 +207,7 @@ impl ThemeSelector { settings.ui_font_family, settings.ui_font_size, ) - .with_style(&settings.theme.ui.selector.label) + .with_style(&settings.theme.selector.label) .boxed(), ) .with_margin_top(6.0) @@ -240,7 +240,7 @@ impl ThemeSelector { fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox { let settings = self.settings.borrow(); - let theme = &settings.theme.ui; + let theme = &settings.theme; let container = Container::new( Label::new( @@ -286,7 +286,7 @@ impl View for ThemeSelector { .with_child(Expanded::new(1.0, self.render_matches(cx)).boxed()) .boxed(), ) - .with_style(&settings.theme.ui.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 663ca5f75e9feb674c2b4c9cf3618e8b763e63ca..55f9efd75a6a52153c8036f461a096fd738460b3 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -885,7 +885,7 @@ impl View for Workspace { .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed())) .boxed(), ) - .with_background_color(settings.theme.ui.background) + .with_background_color(settings.theme.workspace.background) .named("workspace") } diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 511e48dfd41dc4e7a5a5b5382e82208477606525..f99e2a3fd6102b61a0d2f12bada0dcc97fb135ec 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -181,7 +181,7 @@ impl Pane { fn render_tabs(&self, cx: &AppContext) -> ElementBox { let settings = self.settings.borrow(); - let theme = &settings.theme.ui; + let theme = &settings.theme; let line_height = cx.font_cache().line_height( cx.font_cache().default_font(settings.ui_font_family), settings.ui_font_size, @@ -304,7 +304,7 @@ impl Pane { tab_hovered: bool, is_dirty: bool, has_conflict: bool, - theme: &theme::Ui, + theme: &theme::Theme, cx: &AppContext, ) -> ElementBox { enum TabCloseButton {} From 5cfb948b2b30bfb1a24f999198ba3ef1d0322f78 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 20 Aug 2021 10:01:24 -0700 Subject: [PATCH 040/204] Make the default window size non-zero Co-Authored-By: Antonio Scandurra --- gpui/src/platform.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 05c03811feb0013660dd7f89c291aa104ec33ca9..f6930c1ae8a24144c224f60a72255e6e9d10f383 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -13,7 +13,7 @@ use crate::{ fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties}, geometry::{ rect::{RectF, RectI}, - vector::Vector2F, + vector::{vec2f, Vector2F}, }, text_layout::LineLayout, ClipboardItem, Menu, Scene, @@ -96,7 +96,6 @@ pub trait WindowContext { fn present_scene(&mut self, scene: Scene); } -#[derive(Default)] pub struct WindowOptions<'a> { pub bounds: RectF, pub title: Option<&'a str>, @@ -141,3 +140,13 @@ pub trait FontSystem: Send + Sync { ) -> LineLayout; fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec; } + +impl<'a> Default for WindowOptions<'a> { + fn default() -> Self { + Self { + bounds: RectF::new(Default::default(), vec2f(1024.0, 768.0)), + title: Default::default(), + titlebar_appears_transparent: Default::default(), + } + } +} From a7ac37a31899aca76ccafc689f2046f6e4c77fdf Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 20 Aug 2021 10:45:42 -0700 Subject: [PATCH 041/204] Make titlebar_height available during render Co-Authored-By: Antonio Scandurra --- gpui/src/app.rs | 56 ++++++++++++++++++++++++++++++++----------- gpui/src/presenter.rs | 17 +++++++++---- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 3f05ec969deeda30a0909b6d6c24ad83ce3cc0c3..0bcc0a4c29fc503687ed8e3fed1f58986989b803 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -747,12 +747,12 @@ impl MutableAppContext { self.cx.focused_view_id(window_id) } - pub fn render_view(&self, window_id: usize, view_id: usize) -> Result { - self.cx.render_view(window_id, view_id) - } - - pub fn render_views(&self, window_id: usize) -> HashMap { - self.cx.render_views(window_id) + pub fn render_views( + &self, + window_id: usize, + titlebar_height: f32, + ) -> HashMap { + self.cx.render_views(window_id, titlebar_height) } pub fn update T>(&mut self, callback: F) -> T { @@ -1019,6 +1019,7 @@ impl MutableAppContext { let text_layout_cache = TextLayoutCache::new(self.cx.platform.fonts()); let presenter = Rc::new(RefCell::new(Presenter::new( window_id, + window.titlebar_height(), self.cx.font_cache.clone(), text_layout_cache, self.assets.clone(), @@ -1211,10 +1212,11 @@ impl MutableAppContext { { { let mut presenter = presenter.borrow_mut(); - presenter.invalidate(invalidation, self.as_ref()); + let titlebar_height = window.titlebar_height(); + presenter.invalidate(invalidation, titlebar_height, self.as_ref()); let scene = presenter.build_scene( window.size(), - window.titlebar_height(), + titlebar_height, window.scale_factor(), self, ); @@ -1540,19 +1542,31 @@ impl AppContext { .map(|window| window.focused_view_id) } - pub fn render_view(&self, window_id: usize, view_id: usize) -> Result { + pub fn render_view( + &self, + window_id: usize, + view_id: usize, + titlebar_height: f32, + ) -> Result { self.views .get(&(window_id, view_id)) - .map(|v| v.render(window_id, view_id, self)) + .map(|v| v.render(window_id, view_id, titlebar_height, self)) .ok_or(anyhow!("view not found")) } - pub fn render_views(&self, window_id: usize) -> HashMap { + pub fn render_views( + &self, + window_id: usize, + titlebar_height: f32, + ) -> HashMap { self.views .iter() .filter_map(|((win_id, view_id), view)| { if *win_id == window_id { - Some((*view_id, view.render(*win_id, *view_id, self))) + Some(( + *view_id, + view.render(*win_id, *view_id, titlebar_height, self), + )) } else { None } @@ -1703,7 +1717,13 @@ pub trait AnyView: Send + Sync { fn as_any_mut(&mut self) -> &mut dyn Any; fn release(&mut self, cx: &mut MutableAppContext); fn ui_name(&self) -> &'static str; - fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox; + fn render<'a>( + &self, + window_id: usize, + view_id: usize, + titlebar_height: f32, + cx: &AppContext, + ) -> ElementBox; fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn keymap_context(&self, cx: &AppContext) -> keymap::Context; @@ -1729,7 +1749,13 @@ where T::ui_name() } - fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox { + fn render<'a>( + &self, + window_id: usize, + view_id: usize, + titlebar_height: f32, + cx: &AppContext, + ) -> ElementBox { View::render( self, &RenderContext { @@ -1737,6 +1763,7 @@ where view_id, app: cx, view_type: PhantomData::, + titlebar_height, }, ) } @@ -2170,6 +2197,7 @@ impl<'a, T: View> ViewContext<'a, T> { pub struct RenderContext<'a, T: View> { pub app: &'a AppContext, + pub titlebar_height: f32, window_id: usize, view_id: usize, view_type: PhantomData, diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 05aa2acc46c906c02fd8fc8d6374d84e30b0fe6b..a2047a254071a745cd866232d0ecf17a4f09321d 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -28,6 +28,7 @@ pub struct Presenter { impl Presenter { pub fn new( window_id: usize, + titlebar_height: f32, font_cache: Arc, text_layout_cache: TextLayoutCache, asset_cache: Arc, @@ -35,7 +36,7 @@ impl Presenter { ) -> Self { Self { window_id, - rendered_views: cx.render_views(window_id), + rendered_views: cx.render_views(window_id, titlebar_height), parents: HashMap::new(), font_cache, text_layout_cache, @@ -55,15 +56,23 @@ impl Presenter { path } - pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &AppContext) { + pub fn invalidate( + &mut self, + mut invalidation: WindowInvalidation, + titlebar_height: f32, + cx: &AppContext, + ) { for view_id in invalidation.removed { invalidation.updated.remove(&view_id); self.rendered_views.remove(&view_id); self.parents.remove(&view_id); } for view_id in invalidation.updated { - self.rendered_views - .insert(view_id, cx.render_view(self.window_id, view_id).unwrap()); + self.rendered_views.insert( + view_id, + cx.render_view(self.window_id, view_id, titlebar_height) + .unwrap(), + ); } } From b88b3e765747f5a734931a16b775533be2b93c83 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 20 Aug 2021 13:51:52 -0700 Subject: [PATCH 042/204] Add sidebars Co-Authored-By: Nathan Sobo --- zed/assets/icons/comment-16.svg | 3 + zed/assets/icons/folder-tree-16.svg | 3 + zed/assets/icons/user-16.svg | 3 + zed/assets/themes/_base.toml | 41 +++++++------ zed/assets/themes/dark.toml | 21 ++++--- zed/src/lib.rs | 1 + zed/src/project_browser.rs | 19 ++++++ zed/src/theme.rs | 8 +++ zed/src/workspace.rs | 86 ++++++++++++++++++++++---- zed/src/workspace/sidebar.rs | 93 +++++++++++++++++++++++++++++ 10 files changed, 243 insertions(+), 35 deletions(-) create mode 100644 zed/assets/icons/comment-16.svg create mode 100644 zed/assets/icons/folder-tree-16.svg create mode 100644 zed/assets/icons/user-16.svg create mode 100644 zed/src/project_browser.rs create mode 100644 zed/src/workspace/sidebar.rs diff --git a/zed/assets/icons/comment-16.svg b/zed/assets/icons/comment-16.svg new file mode 100644 index 0000000000000000000000000000000000000000..6316d3a4a9033d8c799f28ff0a2336f072fbcb2b --- /dev/null +++ b/zed/assets/icons/comment-16.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/icons/folder-tree-16.svg b/zed/assets/icons/folder-tree-16.svg new file mode 100644 index 0000000000000000000000000000000000000000..f22773b159ccf24fa410f37be92123fc2fa30601 --- /dev/null +++ b/zed/assets/icons/folder-tree-16.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/icons/user-16.svg b/zed/assets/icons/user-16.svg new file mode 100644 index 0000000000000000000000000000000000000000..4ec153f5380a62e4529dc1e38e47d8531a0b07cb --- /dev/null +++ b/zed/assets/icons/user-16.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 2ad222c1749dfae19a2cbee042a5519e901e8a4a..cf340282d5287f3833bee7809f45b8e3e9717c1c 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -2,22 +2,29 @@ background = "$surface.0" [tab] -background = "$surface.1" -text = "$text_color.dull" -border = { color = "#000000", width = 1.0 } +text = "$text.2" padding = { left = 10, right = 10 } -icon_close = "#383839" -icon_dirty = "#556de8" -icon_conflict = "#e45349" +icon_close = "$text.0" +icon_dirty = "$status.info" +icon_conflict = "$status.warn" [active_tab] extends = "$tab" -background = "$surface.2" -text = "$text_color.bright" +background = "$surface.1" +text = "$text.0" + +[sidebar] +padding = { left = 10, right = 10 } + +[sidebar_icon] +color = "$text.2" + +[active_sidebar_icon] +color = "$text.0" [selector] -background = "$surface.3" -text = "$text_color.bright" +background = "$surface.2" +text = "$text.0" padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } margin.top = 12.0 corner_radius = 6.0 @@ -35,13 +42,13 @@ extends = "$selector.item" background = "#094771" [editor] -background = "$surface.2" -gutter_background = "$surface.2" -active_line_background = "$surface.3" -line_number = "$text_color.dull" -line_number_active = "$text_color.bright" -text = "$text_color.normal" +background = "$surface.1" +gutter_background = "$surface.1" +active_line_background = "$surface.2" +line_number = "$text.2" +line_number_active = "$text.0" +text = "$text.1" replicas = [ - { selection = "#264f78", cursor = "$text_color.bright" }, + { selection = "#264f78", cursor = "$text.0" }, { selection = "#504f31", cursor = "#fcf154" }, ] diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index 2bbd37e9344eedf1d56aa30bbac0b4866a2c2406..b019057f651f7b63e3620a6fdf652beb29d9efbd 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -1,15 +1,20 @@ extends = "_base" [surface] -0 = "#050101" -1 = "#131415" -2 = "#1c1d1e" -3 = "#3a3b3c" +0 = "#222324" +1 = "#141516" +2 = "#131415" -[text_color] -dull = "#5a5a5b" -bright = "#ffffff" -normal = "#d4d4d4" +[text] +0 = "#ffffff" +1 = "#b3b3b3" +2 = "#7b7d80" + +[status] +good = "#4fac63" +info = "#3c5dd4" +warn = "#faca50" +bad = "#b7372e" [syntax] keyword = { color = "#0086c0", weight = "bold" } diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 492eaaf1048b3a113dcc372b1a6984953bc731c5..4cec473dfc923898a408faf8f95639c12504e349 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -7,6 +7,7 @@ mod fuzzy; pub mod language; pub mod menus; mod operation_queue; +pub mod project_browser; pub mod rpc; pub mod settings; mod sum_tree; diff --git a/zed/src/project_browser.rs b/zed/src/project_browser.rs new file mode 100644 index 0000000000000000000000000000000000000000..552eab851cb143b17070ba6e058bbe141ef4118c --- /dev/null +++ b/zed/src/project_browser.rs @@ -0,0 +1,19 @@ +use gpui::{elements::Empty, Element, Entity, View}; + +pub struct ProjectBrowser; + +pub enum Event {} + +impl Entity for ProjectBrowser { + type Event = Event; +} + +impl View for ProjectBrowser { + fn ui_name() -> &'static str { + "ProjectBrowser" + } + + fn render(&self, _: &gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + Empty::new().boxed() + } +} diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 52b87860c7c871131e2181a004683a4d2a732f13..a8111d8b0a12d767a4ec5b3c3386458d0562ab56 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -33,6 +33,9 @@ pub struct Theme { pub workspace: Workspace, pub tab: Tab, pub active_tab: Tab, + pub sidebar: ContainerStyle, + pub sidebar_icon: SidebarIcon, + pub active_sidebar_icon: SidebarIcon, pub selector: Selector, pub editor: Editor, #[serde(deserialize_with = "deserialize_syntax_theme")] @@ -72,6 +75,11 @@ pub struct Tab { pub icon_conflict: Color, } +#[derive(Debug, Default, Deserialize)] +pub struct SidebarIcon { + pub color: Color, +} + #[derive(Debug, Default, Deserialize)] pub struct Selector { #[serde(flatten)] diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 3169e9e58d7d79f67fa5a5216c077e468c1608f5..7c133944f1c79adec4d2d9d7549f483d1af98f6e 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -1,10 +1,12 @@ pub mod pane; pub mod pane_group; +pub mod sidebar; use crate::{ editor::{Buffer, Editor}, fs::Fs, language::LanguageRegistry, + project_browser::ProjectBrowser, rpc, settings::Settings, worktree::{File, Worktree}, @@ -25,6 +27,7 @@ use log::error; pub use pane::*; pub use pane_group::*; use postage::watch; +use sidebar::{Side, Sidebar}; use smol::prelude::*; use std::{ collections::{hash_map::Entry, HashMap, HashSet}, @@ -46,6 +49,10 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action("workspace:new_file", Workspace::open_new_file); cx.add_action("workspace:share_worktree", Workspace::share_worktree); cx.add_action("workspace:join_worktree", Workspace::join_worktree); + cx.add_action( + "workspace:toggle_sidebar_item", + Workspace::toggle_sidebar_item, + ); cx.add_bindings(vec![ Binding::new("cmd-s", "workspace:save", None), Binding::new("cmd-alt-i", "workspace:debug_elements", None), @@ -318,12 +325,6 @@ impl Clone for Box { } } -#[derive(Debug)] -pub struct State { - pub modal: Option, - pub center: PaneGroup, -} - pub struct Workspace { pub settings: watch::Receiver, languages: Arc, @@ -331,6 +332,8 @@ pub struct Workspace { fs: Arc, modal: Option, center: PaneGroup, + left_sidebar: Sidebar, + right_sidebar: Sidebar, panes: Vec>, active_pane: ViewHandle, worktrees: HashSet>, @@ -350,6 +353,19 @@ impl Workspace { }); cx.focus(&pane); + let mut left_sidebar = Sidebar::new(Side::Left); + left_sidebar.add_item( + "icons/folder-tree-16.svg", + cx.add_view(|_| ProjectBrowser).into(), + ); + + let mut right_sidebar = Sidebar::new(Side::Right); + right_sidebar.add_item( + "icons/comment-16.svg", + cx.add_view(|_| ProjectBrowser).into(), + ); + right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); + Workspace { modal: None, center: PaneGroup::new(pane.id()), @@ -359,6 +375,8 @@ impl Workspace { languages: app_state.languages.clone(), rpc: app_state.rpc.clone(), fs: app_state.fs.clone(), + left_sidebar, + right_sidebar, worktrees: Default::default(), items: Default::default(), loading_items: Default::default(), @@ -724,6 +742,19 @@ impl Workspace { } } + pub fn toggle_sidebar_item( + &mut self, + (side, item_ix): &(Side, usize), + cx: &mut ViewContext, + ) { + let sidebar = match side { + Side::Left => &mut self.left_sidebar, + Side::Right => &mut self.right_sidebar, + }; + sidebar.toggle_item(*item_ix); + cx.notify(); + } + pub fn debug_elements(&mut self, _: &(), cx: &mut ViewContext) { match to_string_pretty(&cx.debug_elements()) { Ok(json) => { @@ -892,12 +923,47 @@ impl View for Workspace { "Workspace" } - fn render(&self, _: &RenderContext) -> ElementBox { + fn render(&self, cx: &RenderContext) -> ElementBox { let settings = self.settings.borrow(); Container::new( - Stack::new() - .with_child(self.center.render()) - .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed())) + Flex::column() + .with_child( + ConstrainedBox::new(Empty::new().boxed()) + .with_height(cx.titlebar_height) + .named("titlebar"), + ) + .with_child( + Expanded::new( + 1.0, + Stack::new() + .with_child({ + let mut content = Flex::row(); + content.add_child(self.left_sidebar.render(&settings, cx)); + if let Some(panel) = self.left_sidebar.active_item() { + content.add_child( + ConstrainedBox::new(ChildView::new(panel.id()).boxed()) + .with_width(200.0) + .named("left panel"), + ); + } + content.add_child(Expanded::new(1.0, self.center.render()).boxed()); + if let Some(panel) = self.right_sidebar.active_item() { + content.add_child( + ConstrainedBox::new(ChildView::new(panel.id()).boxed()) + .with_width(200.0) + .named("right panel"), + ); + } + content.add_child(self.right_sidebar.render(&settings, cx)); + content.boxed() + }) + .with_children( + self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()), + ) + .boxed(), + ) + .boxed(), + ) .boxed(), ) .with_background_color(settings.theme.workspace.background) diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs new file mode 100644 index 0000000000000000000000000000000000000000..7d7e1f7c6ea5d7ad23244b50816be6ef51d5c04e --- /dev/null +++ b/zed/src/workspace/sidebar.rs @@ -0,0 +1,93 @@ +use crate::Settings; +use gpui::{ + elements::{ + Align, ConstrainedBox, Container, Flex, MouseEventHandler, ParentElement as _, Svg, + }, + AnyViewHandle, AppContext, Element as _, ElementBox, +}; + +pub struct Sidebar { + side: Side, + items: Vec, + active_item_ix: Option, +} + +#[derive(Clone, Copy)] +pub enum Side { + Left, + Right, +} + +struct Item { + icon_path: &'static str, + view: AnyViewHandle, +} + +impl Sidebar { + pub fn new(side: Side) -> Self { + Self { + side, + items: Default::default(), + active_item_ix: None, + } + } + + pub fn add_item(&mut self, icon_path: &'static str, view: AnyViewHandle) { + self.items.push(Item { icon_path, view }); + } + + pub fn toggle_item(&mut self, item_ix: usize) { + if self.active_item_ix == Some(item_ix) { + self.active_item_ix = None; + } else { + self.active_item_ix = Some(item_ix) + } + } + + pub fn active_item(&self) -> Option<&AnyViewHandle> { + self.active_item_ix + .and_then(|ix| self.items.get(ix)) + .map(|item| &item.view) + } + + pub fn render(&self, settings: &Settings, cx: &AppContext) -> ElementBox { + let side = self.side; + let line_height = cx.font_cache().line_height( + cx.font_cache().default_font(settings.ui_font_family), + settings.ui_font_size, + ); + + Container::new( + Flex::column() + .with_children(self.items.iter().enumerate().map(|(item_ix, item)| { + let theme = if Some(item_ix) == self.active_item_ix { + &settings.theme.active_sidebar_icon + } else { + &settings.theme.sidebar_icon + }; + enum SidebarButton {} + MouseEventHandler::new::(item.view.id(), cx, |_| { + ConstrainedBox::new( + Align::new( + ConstrainedBox::new( + Svg::new(item.icon_path).with_color(theme.color).boxed(), + ) + .with_height(line_height) + .boxed(), + ) + .boxed(), + ) + .with_height(line_height + 16.0) + .boxed() + }) + .on_click(move |cx| { + cx.dispatch_action("workspace:toggle_sidebar_item", (side, item_ix)) + }) + .boxed() + })) + .boxed(), + ) + .with_style(&settings.theme.sidebar) + .boxed() + } +} From 2507f9b4d4ebb0ec975b5f8544a051786701de8c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 20 Aug 2021 14:28:45 -0700 Subject: [PATCH 043/204] Stub in a ChatPanel --- zed/src/chat_panel.rs | 23 +++++++++++++++++++++++ zed/src/lib.rs | 1 + 2 files changed, 24 insertions(+) create mode 100644 zed/src/chat_panel.rs diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..ec9f3f486ff6234e09dab63e865b474cd94819b1 --- /dev/null +++ b/zed/src/chat_panel.rs @@ -0,0 +1,23 @@ +use super::channel::{Channel, ChannelList}; +use gpui::{Entity, ModelHandle, View}; + +pub struct ChatPanel { + channel_list: ModelHandle, + active_channel: Option>, +} + +pub enum Event {} + +impl Entity for ChatPanel { + type Event = Event; +} + +impl View for ChatPanel { + fn ui_name() -> &'static str { + "ChatPanel" + } + + fn render(&self, cx: &gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + todo!() + } +} diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 4cec473dfc923898a408faf8f95639c12504e349..445647010c82c90778413aea0af5d0386a3abbdf 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -1,5 +1,6 @@ pub mod assets; pub mod channel; +pub mod chat_panel; pub mod editor; pub mod file_finder; pub mod fs; From c3dda14490e7fe707c6d48b61543f1204c5e6296 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Aug 2021 16:18:39 -0600 Subject: [PATCH 044/204] WIP: Move sum_tree module into gpui so we can use it in List --- Cargo.lock | 19 +++-- gpui/Cargo.toml | 1 + gpui/src/elements.rs | 2 + gpui/src/elements/list.rs | 45 ++++++++++ gpui/src/lib.rs | 1 + {zed => gpui}/src/sum_tree.rs | 48 ++++++++--- {zed => gpui}/src/sum_tree/cursor.rs | 6 +- zed/Cargo.toml | 2 +- zed/src/editor/buffer.rs | 46 +++++----- .../{ => editor/buffer}/operation_queue.rs | 85 +++++++++---------- zed/src/editor/buffer/rope.rs | 10 +-- zed/src/editor/display_map/fold_map.rs | 9 +- zed/src/editor/display_map/wrap_map.rs | 17 ++-- zed/src/lib.rs | 2 - zed/src/util.rs | 24 +----- zed/src/worktree.rs | 7 +- 16 files changed, 188 insertions(+), 136 deletions(-) create mode 100644 gpui/src/elements/list.rs rename {zed => gpui}/src/sum_tree.rs (95%) rename {zed => gpui}/src/sum_tree/cursor.rs (99%) rename zed/src/{ => editor/buffer}/operation_queue.rs (68%) diff --git a/Cargo.lock b/Cargo.lock index 817a0e7c917c857773e2ce2e105e1d133c3ebe02..426b99d93224a109bb77470b7cc4eed0851db14c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd" + [[package]] name = "ascii" version = "1.0.0" @@ -626,7 +632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "constant_time_eq", ] @@ -637,7 +643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "cc", "cfg-if 0.1.10", "constant_time_eq", @@ -2145,6 +2151,7 @@ name = "gpui" version = "0.1.0" dependencies = [ "anyhow", + "arrayvec 0.7.1", "async-task", "backtrace", "bindgen", @@ -2632,7 +2639,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e30b1df631d23875f230ed3ddd1a88c231f269a04b2044eb6ca87e763b5f4c42" dependencies = [ - "arrayvec", + "arrayvec 0.5.2", ] [[package]] @@ -2665,7 +2672,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ - "arrayvec", + "arrayvec 0.5.2", "bitflags 1.2.1", "cfg-if 1.0.0", "ryu", @@ -5164,7 +5171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf81f2900d2e235220e6f31ec9f63ade6a7f59090c556d74fe949bb3b15e9fe" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "bytemuck", "cfg-if 1.0.0", "png 0.16.8", @@ -5792,7 +5799,7 @@ name = "zed" version = "0.1.0" dependencies = [ "anyhow", - "arrayvec", + "arrayvec 0.7.1", "async-trait", "async-tungstenite", "cargo-bundle", diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 535fe16155ba9f2a1b16397cf2dcf27f050578cd..be3d47e42aed08c25653146aa91b3a951cedbf79 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -5,6 +5,7 @@ name = "gpui" version = "0.1.0" [dependencies] +arrayvec = "0.7.1" async-task = "4.0.3" backtrace = "0.3" ctor = "0.1" diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 3d0357409484aafb8916bcc70b3c3bf561f2fa17..14d33b8e9028e4ab3b1fe21ceab88a68ee58bafc 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -7,6 +7,7 @@ mod event_handler; mod flex; mod label; mod line_box; +mod list; mod mouse_event_handler; mod stack; mod svg; @@ -22,6 +23,7 @@ pub use event_handler::*; pub use flex::*; pub use label::*; pub use line_box::*; +pub use list::*; pub use mouse_event_handler::*; pub use stack::*; pub use svg::*; diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs new file mode 100644 index 0000000000000000000000000000000000000000..fd3e06f2954a8f7d262f61fc30241d343ad38947 --- /dev/null +++ b/gpui/src/elements/list.rs @@ -0,0 +1,45 @@ +use crate::sum_tree::{self, SumTree}; +use parking_lot::Mutex; +use std::sync::Arc; + +use crate::ElementBox; + +pub struct List { + state: ListState, +} + +pub struct ListState(Arc>); + +struct StateInner { + elements: Vec, + element_heights: SumTree, +} + +#[derive(Clone, Debug)] +enum ElementHeight { + Pending, + Ready(f32), +} + +#[derive(Clone, Debug, Default)] +struct ElementHeightSummary { + pending_count: usize, + height: f32, +} + +impl sum_tree::Item for ElementHeight { + type Summary = ElementHeightSummary; + + fn summary(&self) -> Self::Summary { + todo!() + } +} + +impl sum_tree::Summary for ElementHeightSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, cx: &Self::Context) { + self.pending_count += summary.pending_count; + self.height += summary.height; + } +} diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index f2f935d142719da38d7ccfe6d6c57193e136646e..308674d45dd726ed70a07d79dc20824aa07521c2 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -1,6 +1,7 @@ mod app; pub use app::*; mod assets; +pub mod sum_tree; #[cfg(test)] mod test; pub use assets::*; diff --git a/zed/src/sum_tree.rs b/gpui/src/sum_tree.rs similarity index 95% rename from zed/src/sum_tree.rs rename to gpui/src/sum_tree.rs index eb13c2a86c5d19b5ee286e9113fb7787a01dd801..eeb53cc1881f769123c2e9b965a5617bfda38a6b 100644 --- a/zed/src/sum_tree.rs +++ b/gpui/src/sum_tree.rs @@ -1,6 +1,5 @@ mod cursor; -use crate::util::Bias; use arrayvec::ArrayVec; pub use cursor::Cursor; pub use cursor::FilterCursor; @@ -47,6 +46,29 @@ impl<'a, S: Summary, T: Dimension<'a, S> + Ord> SeekDimension<'a, S> for T { } } +#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] +pub enum Bias { + Left, + Right, +} + +impl PartialOrd for Bias { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Bias { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (Self::Left, Self::Left) => Ordering::Equal, + (Self::Left, Self::Right) => Ordering::Less, + (Self::Right, Self::Right) => Ordering::Equal, + (Self::Right, Self::Left) => Ordering::Greater, + } + } +} + #[derive(Debug, Clone)] pub struct SumTree(Arc>); @@ -253,8 +275,8 @@ impl SumTree { summary.add_summary(other_node.summary(), cx); let height_delta = *height - other_node.height(); - let mut summaries_to_append = ArrayVec::<[T::Summary; 2 * TREE_BASE]>::new(); - let mut trees_to_append = ArrayVec::<[SumTree; 2 * TREE_BASE]>::new(); + let mut summaries_to_append = ArrayVec::::new(); + let mut trees_to_append = ArrayVec::, { 2 * TREE_BASE }>::new(); if height_delta == 0 { summaries_to_append.extend(other_node.child_summaries().iter().cloned()); trees_to_append.extend(other_node.child_trees().iter().cloned()); @@ -277,8 +299,8 @@ impl SumTree { let child_count = child_trees.len() + trees_to_append.len(); if child_count > 2 * TREE_BASE { - let left_summaries: ArrayVec<_>; - let right_summaries: ArrayVec<_>; + let left_summaries: ArrayVec<_, { 2 * TREE_BASE }>; + let right_summaries: ArrayVec<_, { 2 * TREE_BASE }>; let left_trees; let right_trees; @@ -323,7 +345,7 @@ impl SumTree { let left_items; let right_items; let left_summaries; - let right_summaries: ArrayVec<[T::Summary; 2 * TREE_BASE]>; + let right_summaries: ArrayVec; let midpoint = (child_count + child_count % 2) / 2; { @@ -491,13 +513,13 @@ pub enum Node { Internal { height: u8, summary: T::Summary, - child_summaries: ArrayVec<[T::Summary; 2 * TREE_BASE]>, - child_trees: ArrayVec<[SumTree; 2 * TREE_BASE]>, + child_summaries: ArrayVec, + child_trees: ArrayVec, { 2 * TREE_BASE }>, }, Leaf { summary: T::Summary, - items: ArrayVec<[T; 2 * TREE_BASE]>, - item_summaries: ArrayVec<[T::Summary; 2 * TREE_BASE]>, + items: ArrayVec, + item_summaries: ArrayVec, }, } @@ -532,14 +554,14 @@ impl Node { } } - fn child_trees(&self) -> &ArrayVec<[SumTree; 2 * TREE_BASE]> { + fn child_trees(&self) -> &ArrayVec, { 2 * TREE_BASE }> { match self { Node::Internal { child_trees, .. } => child_trees, Node::Leaf { .. } => panic!("Leaf nodes have no child trees"), } } - fn items(&self) -> &ArrayVec<[T; 2 * TREE_BASE]> { + fn items(&self) -> &ArrayVec { match self { Node::Leaf { items, .. } => items, Node::Internal { .. } => panic!("Internal nodes have no items"), @@ -603,7 +625,7 @@ mod tests { ); } - #[gpui::test(iterations = 100)] + #[crate::test(self, iterations = 100)] fn test_random(mut rng: StdRng) { let rng = &mut rng; let mut tree = SumTree::::new(); diff --git a/zed/src/sum_tree/cursor.rs b/gpui/src/sum_tree/cursor.rs similarity index 99% rename from zed/src/sum_tree/cursor.rs rename to gpui/src/sum_tree/cursor.rs index 8a62cf4b489d59e0a33b474199d8daf18713e3ca..990fe3090051a3280ecaa2ec28c928fcb6c257d4 100644 --- a/zed/src/sum_tree/cursor.rs +++ b/gpui/src/sum_tree/cursor.rs @@ -13,7 +13,7 @@ struct StackEntry<'a, T: Item, S, U> { #[derive(Clone)] pub struct Cursor<'a, T: Item, S, U> { tree: &'a SumTree, - stack: ArrayVec<[StackEntry<'a, T, S, U>; 16]>, + stack: ArrayVec, 16>, seek_dimension: S, sum_dimension: U, did_seek: bool, @@ -495,8 +495,8 @@ where ref item_summaries, .. } => { - let mut slice_items = ArrayVec::<[T; 2 * TREE_BASE]>::new(); - let mut slice_item_summaries = ArrayVec::<[T::Summary; 2 * TREE_BASE]>::new(); + let mut slice_items = ArrayVec::::new(); + let mut slice_item_summaries = ArrayVec::::new(); let mut slice_items_summary = match aggregate { SeekAggregate::Slice(_) => Some(T::Summary::default()), _ => None, diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 25aafaac5f9f71adecd8b4f99bc1425194cbc755..03bcdd3a56fa2c9549f1b4fd61dbaba2568b6300 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -18,8 +18,8 @@ test-support = ["tempdir", "zrpc/test-support"] [dependencies] anyhow = "1.0.38" -arrayvec = "0.5.2" async-trait = "0.1" +arrayvec = "0.7.1" async-tungstenite = { version = "0.14", features = ["async-tls"] } crossbeam-channel = "0.5.0" ctor = "0.1.20" diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 9b1f169e48c71a10e37cb60ae12ef7e6a0e2d49b..eb57328a1e3affecebff7a0d4b09a6da8c930704 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -1,30 +1,30 @@ mod anchor; +mod operation_queue; mod point; pub mod rope; mod selection; -pub use anchor::*; -use parking_lot::Mutex; -pub use point::*; -pub use rope::{Chunks, Rope, TextSummary}; -use seahash::SeaHasher; -pub use selection::*; -use similar::{ChangeTag, TextDiff}; -use tree_sitter::{InputEdit, Parser, QueryCursor}; -use zrpc::proto; - use crate::{ language::{Language, Tree}, - operation_queue::{self, OperationQueue}, settings::{HighlightId, HighlightMap}, - sum_tree::{self, FilterCursor, SumTree}, time::{self, ReplicaId}, util::Bias, worktree::{File, Worktree}, }; +pub use anchor::*; use anyhow::{anyhow, Result}; -use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{ + sum_tree::{self, FilterCursor, SumTree}, + AppContext, Entity, ModelContext, ModelHandle, Task, +}; use lazy_static::lazy_static; +use operation_queue::OperationQueue; +use parking_lot::Mutex; +pub use point::*; +pub use rope::{Chunks, Rope, TextSummary}; +use seahash::SeaHasher; +pub use selection::*; +use similar::{ChangeTag, TextDiff}; use std::{ cell::RefCell, cmp, @@ -37,6 +37,8 @@ use std::{ sync::Arc, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; +use tree_sitter::{InputEdit, Parser, QueryCursor}; +use zrpc::proto; #[derive(Clone, Default)] struct DeterministicState; @@ -120,7 +122,7 @@ pub struct Buffer { syntax_tree: Mutex>, is_parsing: bool, selections: HashMap, - deferred_ops: OperationQueue, + deferred_ops: OperationQueue, deferred_replicas: HashSet, replica_id: ReplicaId, remote_id: u64, @@ -483,6 +485,8 @@ pub enum Operation { set_id: Option, lamport_timestamp: time::Lamport, }, + #[cfg(test)] + Test(time::Lamport), } #[derive(Clone, Debug, Eq, PartialEq)] @@ -1390,6 +1394,8 @@ impl Buffer { } self.lamport_clock.observe(lamport_timestamp); } + #[cfg(test)] + Operation::Test(_) => {} } Ok(()) } @@ -1731,6 +1737,8 @@ impl Buffer { Operation::SetActiveSelections { set_id, .. } => { set_id.map_or(true, |set_id| self.selections.contains_key(&set_id)) } + #[cfg(test)] + Operation::Test(_) => true, } } } @@ -2564,6 +2572,8 @@ impl Operation { Operation::SetActiveSelections { lamport_timestamp, .. } => *lamport_timestamp, + #[cfg(test)] + Operation::Test(lamport_timestamp) => *lamport_timestamp, } } @@ -2630,6 +2640,8 @@ impl<'a> Into for &'a Operation { lamport_timestamp: lamport_timestamp.value, }, ), + #[cfg(test)] + Operation::Test(_) => unimplemented!() }), } } @@ -2834,12 +2846,6 @@ impl TryFrom for Selection { } } -impl operation_queue::Operation for Operation { - fn timestamp(&self) -> time::Lamport { - self.lamport_timestamp() - } -} - pub trait ToOffset { fn to_offset<'a>(&self, content: impl Into>) -> usize; } diff --git a/zed/src/operation_queue.rs b/zed/src/editor/buffer/operation_queue.rs similarity index 68% rename from zed/src/operation_queue.rs rename to zed/src/editor/buffer/operation_queue.rs index 681d13587033abba5c6b6fb5da67f163d427c7af..b6efc3fc9ce12cc55cdfa0c5d7620d23141be182 100644 --- a/zed/src/operation_queue.rs +++ b/zed/src/editor/buffer/operation_queue.rs @@ -1,26 +1,27 @@ -use crate::{ - sum_tree::{Cursor, Dimension, Edit, Item, KeyedItem, SumTree, Summary}, - time, -}; +use super::Operation; +use crate::time; +use gpui::sum_tree::{Cursor, Dimension, Edit, Item, KeyedItem, SumTree, Summary}; use std::{fmt::Debug, ops::Add}; -pub trait Operation: Clone + Debug + Eq { - fn timestamp(&self) -> time::Lamport; -} - #[derive(Clone, Debug)] -pub struct OperationQueue(SumTree); +pub struct OperationQueue(SumTree); #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] pub struct OperationKey(time::Lamport); #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct OperationSummary { - key: OperationKey, - len: usize, + pub key: OperationKey, + pub len: usize, +} + +impl OperationKey { + pub fn new(timestamp: time::Lamport) -> Self { + Self(timestamp) + } } -impl OperationQueue { +impl OperationQueue { pub fn new() -> Self { OperationQueue(SumTree::new()) } @@ -29,9 +30,9 @@ impl OperationQueue { self.0.summary().len } - pub fn insert(&mut self, mut ops: Vec) { - ops.sort_by_key(|op| op.timestamp()); - ops.dedup_by_key(|op| op.timestamp()); + pub fn insert(&mut self, mut ops: Vec) { + ops.sort_by_key(|op| op.lamport_timestamp()); + ops.dedup_by_key(|op| op.lamport_timestamp()); self.0 .edit(ops.into_iter().map(Edit::Insert).collect(), &()); } @@ -42,30 +43,11 @@ impl OperationQueue { clone } - pub fn cursor(&self) -> Cursor { + pub fn cursor(&self) -> Cursor { self.0.cursor() } } -impl Item for T { - type Summary = OperationSummary; - - fn summary(&self) -> Self::Summary { - OperationSummary { - key: OperationKey(self.timestamp()), - len: 1, - } - } -} - -impl KeyedItem for T { - type Key = OperationKey; - - fn key(&self) -> Self::Key { - OperationKey(self.timestamp()) - } -} - impl Summary for OperationSummary { type Context = (); @@ -95,6 +77,25 @@ impl<'a> Dimension<'a, OperationSummary> for OperationKey { } } +impl Item for Operation { + type Summary = OperationSummary; + + fn summary(&self) -> Self::Summary { + OperationSummary { + key: OperationKey::new(self.lamport_timestamp()), + len: 1, + } + } +} + +impl KeyedItem for Operation { + type Key = OperationKey; + + fn key(&self) -> Self::Key { + OperationKey::new(self.lamport_timestamp()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -107,27 +108,21 @@ mod tests { assert_eq!(queue.len(), 0); queue.insert(vec![ - TestOperation(clock.tick()), - TestOperation(clock.tick()), + Operation::Test(clock.tick()), + Operation::Test(clock.tick()), ]); assert_eq!(queue.len(), 2); - queue.insert(vec![TestOperation(clock.tick())]); + queue.insert(vec![Operation::Test(clock.tick())]); assert_eq!(queue.len(), 3); drop(queue.drain()); assert_eq!(queue.len(), 0); - queue.insert(vec![TestOperation(clock.tick())]); + queue.insert(vec![Operation::Test(clock.tick())]); assert_eq!(queue.len(), 1); } #[derive(Clone, Debug, Eq, PartialEq)] struct TestOperation(time::Lamport); - - impl Operation for TestOperation { - fn timestamp(&self) -> time::Lamport { - self.0 - } - } } diff --git a/zed/src/editor/buffer/rope.rs b/zed/src/editor/buffer/rope.rs index 58cd58fd3caef37fbbe018f5b42b2c38ab574bb0..e9732e92c2e3028d757595d2c1a07c9f25d90806 100644 --- a/zed/src/editor/buffer/rope.rs +++ b/zed/src/editor/buffer/rope.rs @@ -1,9 +1,7 @@ use super::Point; -use crate::{ - sum_tree::{self, SumTree}, - util::Bias, -}; +use crate::util::Bias; use arrayvec::ArrayString; +use gpui::sum_tree::{self, SumTree}; use smallvec::SmallVec; use std::{cmp, ops::Range, str}; @@ -61,7 +59,7 @@ impl Rope { if last_chunk.0.len() + first_new_chunk_ref.0.len() <= 2 * CHUNK_BASE { last_chunk.0.push_str(&first_new_chunk.take().unwrap().0); } else { - let mut text = ArrayString::<[_; 4 * CHUNK_BASE]>::new(); + let mut text = ArrayString::<{ 4 * CHUNK_BASE }>::new(); text.push_str(&last_chunk.0); text.push_str(&first_new_chunk_ref.0); let (left, right) = text.split_at(find_split_ix(&text)); @@ -330,7 +328,7 @@ impl<'a> Iterator for Chunks<'a> { } #[derive(Clone, Debug, Default)] -struct Chunk(ArrayString<[u8; 2 * CHUNK_BASE]>); +struct Chunk(ArrayString<{ 2 * CHUNK_BASE }>); impl Chunk { fn to_point(&self, target: usize) -> Point { diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index 3bfa1f3d240429e74f7ed9532dca9bf8eda04be2..1c24bacafc0179646778a60db0789d5e04ffa325 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/zed/src/editor/display_map/fold_map.rs @@ -2,14 +2,11 @@ use super::{ buffer::{AnchorRangeExt, TextSummary}, Anchor, Buffer, Point, ToOffset, }; -use crate::{ - editor::buffer, - settings::HighlightId, +use crate::{editor::buffer, settings::HighlightId, time, util::Bias}; +use gpui::{ sum_tree::{self, Cursor, FilterCursor, SumTree}, - time, - util::Bias, + AppContext, ModelHandle, }; -use gpui::{AppContext, ModelHandle}; use parking_lot::Mutex; use std::{ cmp::{self, Ordering}, diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index 358730748b89173c361decf4bbe9db23864da7d3..e831d800429ceab9d584626fd5827eddefae6070 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/zed/src/editor/display_map/wrap_map.rs @@ -3,14 +3,11 @@ use super::{ line_wrapper::LineWrapper, tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary}, }; -use crate::{ - editor::Point, - settings::HighlightId, +use crate::{editor::Point, settings::HighlightId, util::Bias, Settings}; +use gpui::{ sum_tree::{self, Cursor, SumTree}, - util::Bias, - Settings, + Entity, ModelContext, Task, }; -use gpui::{Entity, ModelContext, Task}; use lazy_static::lazy_static; use smol::future::yield_now; use std::{collections::VecDeque, ops::Range, time::Duration}; @@ -816,8 +813,12 @@ fn push_isomorphic(transforms: &mut Vec, summary: TextSummary) { transforms.push(Transform::isomorphic(summary)); } -impl SumTree { - pub fn push_or_extend(&mut self, transform: Transform) { +trait SumTreeExt { + fn push_or_extend(&mut self, transform: Transform); +} + +impl SumTreeExt for SumTree { + fn push_or_extend(&mut self, transform: Transform) { let mut transform = Some(transform); self.update_last( |last_transform| { diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 445647010c82c90778413aea0af5d0386a3abbdf..45af7e355c6a426ba6638f307dcba59f0fb15f25 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -7,11 +7,9 @@ pub mod fs; mod fuzzy; pub mod language; pub mod menus; -mod operation_queue; pub mod project_browser; pub mod rpc; pub mod settings; -mod sum_tree; #[cfg(any(test, feature = "test-support"))] pub mod test; pub mod theme; diff --git a/zed/src/util.rs b/zed/src/util.rs index 5bae8b7a99c69c318576f7528262bdbb9b1f34db..8ca2cfd9b6a897d3eb0e2271b2446194f0e4ee03 100644 --- a/zed/src/util.rs +++ b/zed/src/util.rs @@ -1,30 +1,8 @@ use futures::Future; +pub use gpui::sum_tree::Bias; use rand::prelude::*; use std::cmp::Ordering; -#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] -pub enum Bias { - Left, - Right, -} - -impl PartialOrd for Bias { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Bias { - fn cmp(&self, other: &Self) -> Ordering { - match (self, other) { - (Self::Left, Self::Left) => Ordering::Equal, - (Self::Left, Self::Right) => Ordering::Less, - (Self::Right, Self::Right) => Ordering::Equal, - (Self::Right, Self::Left) => Ordering::Greater, - } - } -} - pub fn post_inc(value: &mut usize) -> usize { let prev = *value; *value += 1; diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index d9a8aa889b456485c6ff874962492f0faaa35ba0..6c7ba34a15914e3bc75c3b44fc7220e73e928b46 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -8,7 +8,6 @@ use crate::{ fuzzy::CharBag, language::LanguageRegistry, rpc::{self, proto}, - sum_tree::{self, Cursor, Edit, SumTree}, time::{self, ReplicaId}, util::{log_async_errors, Bias}, }; @@ -17,8 +16,10 @@ use anyhow::{anyhow, Result}; use futures::{Stream, StreamExt}; pub use fuzzy::{match_paths, PathMatch}; use gpui::{ - executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, - Task, UpgradeModelHandle, WeakModelHandle, + executor, + sum_tree::{self, Cursor, Edit, SumTree}, + AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, + UpgradeModelHandle, WeakModelHandle, }; use lazy_static::lazy_static; use parking_lot::Mutex; From d0a5bc694c7e359c45a2c9c67ffa799cbabcab13 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Aug 2021 16:34:46 -0600 Subject: [PATCH 045/204] WIP --- gpui/src/elements/list.rs | 69 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index fd3e06f2954a8f7d262f61fc30241d343ad38947..16531144b88e6f287a3da15235009c9a393ca71c 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -1,4 +1,8 @@ -use crate::sum_tree::{self, SumTree}; +use crate::{ + geometry::{rect::RectF, vector::Vector2F}, + sum_tree::{self, SumTree}, + Element, +}; use parking_lot::Mutex; use std::sync::Arc; @@ -12,7 +16,7 @@ pub struct ListState(Arc>); struct StateInner { elements: Vec, - element_heights: SumTree, + heights: SumTree, } #[derive(Clone, Debug)] @@ -27,6 +31,67 @@ struct ElementHeightSummary { height: f32, } +impl Element for List { + type LayoutState = (); + + type PaintState = (); + + fn layout( + &mut self, + constraint: crate::SizeConstraint, + cx: &mut crate::LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + todo!() + } + + fn after_layout( + &mut self, + size: Vector2F, + layout: &mut Self::LayoutState, + cx: &mut crate::AfterLayoutContext, + ) { + todo!() + } + + fn paint( + &mut self, + bounds: RectF, + layout: &mut Self::LayoutState, + cx: &mut crate::PaintContext, + ) -> Self::PaintState { + todo!() + } + + fn dispatch_event( + &mut self, + event: &crate::Event, + bounds: RectF, + layout: &mut Self::LayoutState, + paint: &mut Self::PaintState, + cx: &mut crate::EventContext, + ) -> bool { + todo!() + } + + fn debug( + &self, + bounds: RectF, + layout: &Self::LayoutState, + paint: &Self::PaintState, + cx: &crate::DebugContext, + ) -> serde_json::Value { + todo!() + } +} + +impl ListState { + pub fn new(elements: Vec) -> Self { + let mut heights = SumTree::new(); + heights.extend(elements.iter().map(|_| ElementHeight::Pending), &()); + Self(Arc::new(Mutex::new(StateInner { elements, heights }))) + } +} + impl sum_tree::Item for ElementHeight { type Summary = ElementHeightSummary; From d68e0b0b974b3cc1bf985b2032e1d98705730fd7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Aug 2021 16:40:45 -0600 Subject: [PATCH 046/204] Remove after_layout from Element trait Now that layout takes a MutableAppContext we don't need an after_layout phase. --- gpui/examples/text.rs | 8 -------- gpui/src/elements.rs | 21 --------------------- gpui/src/elements/align.rs | 13 ++----------- gpui/src/elements/canvas.rs | 8 -------- gpui/src/elements/constrained_box.rs | 13 ++----------- gpui/src/elements/container.rs | 12 +----------- gpui/src/elements/empty.rs | 7 +------ gpui/src/elements/event_handler.rs | 13 ++----------- gpui/src/elements/flex.rs | 24 ++---------------------- gpui/src/elements/label.rs | 7 ++----- gpui/src/elements/line_box.rs | 13 ++----------- gpui/src/elements/list.rs | 9 --------- gpui/src/elements/mouse_event_handler.rs | 13 ++----------- gpui/src/elements/stack.rs | 15 ++------------- gpui/src/elements/svg.rs | 6 +----- gpui/src/elements/uniform_list.rs | 15 +-------------- gpui/src/presenter.rs | 9 --------- zed/src/editor/element.rs | 7 ++----- 18 files changed, 22 insertions(+), 191 deletions(-) diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index 08959b77a2f72652ed609e95c92d77be075e4c71..1d092a0da7eb7baea312ff8b09ad78a3acbcf457 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -46,14 +46,6 @@ impl gpui::Element for TextElement { (constraint.max, ()) } - fn after_layout( - &mut self, - _: pathfinder_geometry::vector::Vector2F, - _: &mut Self::LayoutState, - _: &mut gpui::AfterLayoutContext, - ) { - } - fn paint( &mut self, bounds: RectF, diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 14d33b8e9028e4ab3b1fe21ceab88a68ee58bafc..e390e84653981daf0b06ce8c0edd19eda259123b 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -59,13 +59,6 @@ pub trait Element { cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState); - fn after_layout( - &mut self, - size: Vector2F, - layout: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ); - fn paint( &mut self, bounds: RectF, @@ -163,20 +156,6 @@ impl AnyElement for Lifecycle { result } - fn after_layout(&mut self, cx: &mut AfterLayoutContext) { - if let Lifecycle::PostLayout { - element, - size, - layout, - .. - } = self - { - element.after_layout(*size, layout, cx); - } else { - panic!("invalid element lifecycle state"); - } - } - fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) { *self = if let Lifecycle::PostLayout { mut element, diff --git a/gpui/src/elements/align.rs b/gpui/src/elements/align.rs index 5b3fd5d0b51f62c7ca36ab85afe2900d783efb60..8963e063b6b7bbb3efc9d7a24f18d25be3a407a2 100644 --- a/gpui/src/elements/align.rs +++ b/gpui/src/elements/align.rs @@ -1,6 +1,6 @@ use crate::{ - json, AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, - LayoutContext, PaintContext, SizeConstraint, + json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, }; use json::ToJson; use pathfinder_geometry::vector::Vector2F; @@ -51,15 +51,6 @@ impl Element for Align { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: pathfinder_geometry::rect::RectF, diff --git a/gpui/src/elements/canvas.rs b/gpui/src/elements/canvas.rs index e90c377be13c1285e5ff508e042089b6984efb46..2e9d5d9e883dfacd533423970b88d338809b51d3 100644 --- a/gpui/src/elements/canvas.rs +++ b/gpui/src/elements/canvas.rs @@ -56,14 +56,6 @@ where self.0(bounds, cx) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - _: &mut crate::AfterLayoutContext, - ) { - } - fn dispatch_event( &mut self, _: &crate::Event, diff --git a/gpui/src/elements/constrained_box.rs b/gpui/src/elements/constrained_box.rs index 3d50b70a57fb8a74c25a8b68f001acb7aa7ddf55..f7949274020748e2ed5f537ff5136cce03eceb1c 100644 --- a/gpui/src/elements/constrained_box.rs +++ b/gpui/src/elements/constrained_box.rs @@ -3,8 +3,8 @@ use serde_json::json; use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, - LayoutContext, PaintContext, SizeConstraint, + json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, }; pub struct ConstrainedBox { @@ -67,15 +67,6 @@ impl Element for ConstrainedBox { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: RectF, diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index ae13b5d82116539ac1cefc2ef5c8b66bbf1ff644..fc7fc941cf7edd63cd8ef4a7bd446b48195b23ca 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -10,8 +10,7 @@ use crate::{ }, json::ToJson, scene::{self, Border, Quad}, - AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; #[derive(Clone, Debug, Default, Deserialize)] @@ -167,15 +166,6 @@ impl Element for Container { (child_size + size_buffer, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: RectF, diff --git a/gpui/src/elements/empty.rs b/gpui/src/elements/empty.rs index fe9ff3c9b90862443be9b7c6eb17ff7d23251808..3c706cc379133cd88c78f2faaf5c2e4190e78431 100644 --- a/gpui/src/elements/empty.rs +++ b/gpui/src/elements/empty.rs @@ -6,9 +6,7 @@ use crate::{ json::{json, ToJson}, DebugContext, }; -use crate::{ - AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, -}; +use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint}; pub struct Empty; @@ -41,9 +39,6 @@ impl Element for Empty { (vec2f(x, y), ()) } - fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) { - } - fn paint( &mut self, _: RectF, diff --git a/gpui/src/elements/event_handler.rs b/gpui/src/elements/event_handler.rs index a66778f8b7c91d979ee13eb4fd1de804702f4eb0..686bfd7acce704aaad3b08098adbc1e8b7d26f91 100644 --- a/gpui/src/elements/event_handler.rs +++ b/gpui/src/elements/event_handler.rs @@ -2,8 +2,8 @@ use pathfinder_geometry::rect::RectF; use serde_json::json; use crate::{ - geometry::vector::Vector2F, AfterLayoutContext, DebugContext, Element, ElementBox, Event, - EventContext, LayoutContext, PaintContext, SizeConstraint, + geometry::vector::Vector2F, DebugContext, Element, ElementBox, Event, EventContext, + LayoutContext, PaintContext, SizeConstraint, }; pub struct EventHandler { @@ -41,15 +41,6 @@ impl Element for EventHandler { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: RectF, diff --git a/gpui/src/elements/flex.rs b/gpui/src/elements/flex.rs index 1ed9204de4796fc45056acaf16f550de7a181287..8bec507d9f692e104c50b3c419b005b8605d546a 100644 --- a/gpui/src/elements/flex.rs +++ b/gpui/src/elements/flex.rs @@ -2,8 +2,8 @@ use std::{any::Any, f32::INFINITY}; use crate::{ json::{self, ToJson, Value}, - AfterLayoutContext, Axis, DebugContext, Element, ElementBox, Event, EventContext, - LayoutContext, PaintContext, SizeConstraint, Vector2FExt, + Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, Vector2FExt, }; use pathfinder_geometry::{ rect::RectF, @@ -134,17 +134,6 @@ impl Element for Flex { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - for child in &mut self.children { - child.after_layout(cx); - } - } - fn paint( &mut self, bounds: RectF, @@ -223,15 +212,6 @@ impl Element for Expanded { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: RectF, diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index 72f755905cad91b179b9b9421c5feeec0a3b7c5f..5c0e7e247f8f6384efbdd3e51e7a080ffbdcf9b1 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -8,8 +8,8 @@ use crate::{ }, json::{ToJson, Value}, text_layout::Line, - AfterLayoutContext, DebugContext, Element, Event, EventContext, FontCache, LayoutContext, - PaintContext, SizeConstraint, + DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext, + SizeConstraint, }; use serde::Deserialize; use serde_json::json; @@ -140,9 +140,6 @@ impl Element for Label { (size, line) } - fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) { - } - fn paint( &mut self, bounds: RectF, diff --git a/gpui/src/elements/line_box.rs b/gpui/src/elements/line_box.rs index 16baf6e00ba5d483891a41088248bf32fd4b1270..b6ce6d9e9544e274a60aa67783cad25577da2f9d 100644 --- a/gpui/src/elements/line_box.rs +++ b/gpui/src/elements/line_box.rs @@ -6,8 +6,8 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{json, ToJson}, - AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, - PaintContext, SizeConstraint, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, }; pub struct LineBox { @@ -60,15 +60,6 @@ impl Element for LineBox { } } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: pathfinder_geometry::rect::RectF, diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 16531144b88e6f287a3da15235009c9a393ca71c..d96637ea864574ced7b7df3b5a71a1f281e0f152 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -44,15 +44,6 @@ impl Element for List { todo!() } - fn after_layout( - &mut self, - size: Vector2F, - layout: &mut Self::LayoutState, - cx: &mut crate::AfterLayoutContext, - ) { - todo!() - } - fn paint( &mut self, bounds: RectF, diff --git a/gpui/src/elements/mouse_event_handler.rs b/gpui/src/elements/mouse_event_handler.rs index 2fc310e8252b88d79217e5783dea6ba3ca53a207..629a9824ff59041295b685f77918ccd92271e3f1 100644 --- a/gpui/src/elements/mouse_event_handler.rs +++ b/gpui/src/elements/mouse_event_handler.rs @@ -1,7 +1,7 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, - AfterLayoutContext, AppContext, DebugContext, Element, ElementBox, Event, EventContext, - LayoutContext, PaintContext, SizeConstraint, ValueHandle, + AppContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, + PaintContext, SizeConstraint, ValueHandle, }; use serde_json::json; @@ -51,15 +51,6 @@ impl Element for MouseEventHandler { (self.child.layout(constraint, cx), ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - self.child.after_layout(cx); - } - fn paint( &mut self, bounds: RectF, diff --git a/gpui/src/elements/stack.rs b/gpui/src/elements/stack.rs index cfc4d9cc6cfc486a55d26f9c4df333bfe47b346b..5c4f91e0ab5c7ded935f7adcbdf7a29ab5bda52b 100644 --- a/gpui/src/elements/stack.rs +++ b/gpui/src/elements/stack.rs @@ -1,8 +1,8 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::{self, json, ToJson}, - AfterLayoutContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, - PaintContext, SizeConstraint, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, }; pub struct Stack { @@ -33,17 +33,6 @@ impl Element for Stack { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - for child in &mut self.children { - child.after_layout(cx); - } - } - fn paint( &mut self, bounds: RectF, diff --git a/gpui/src/elements/svg.rs b/gpui/src/elements/svg.rs index 93d26f9656a45ba2328972c99226cc6a3c58588b..2cf8f07b6038b9bfc2ee3b202590cba33f7481ac 100644 --- a/gpui/src/elements/svg.rs +++ b/gpui/src/elements/svg.rs @@ -8,8 +8,7 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - scene, AfterLayoutContext, DebugContext, Element, Event, EventContext, LayoutContext, - PaintContext, SizeConstraint, + scene, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; pub struct Svg { @@ -66,9 +65,6 @@ impl Element for Svg { } } - fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) { - } - fn paint(&mut self, bounds: RectF, svg: &mut Self::LayoutState, cx: &mut PaintContext) { if let Some(svg) = svg.clone() { cx.scene.push_icon(scene::Icon { diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index 74ebccdf379080beb603f1464d19a08e132154a0..04567d4223a7d12e28ab846bf97619ad36eaa4fa 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -1,6 +1,4 @@ -use super::{ - AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, -}; +use super::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint}; use crate::{ geometry::{ rect::RectF, @@ -164,17 +162,6 @@ where ) } - fn after_layout( - &mut self, - _: Vector2F, - layout: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - for item in &mut layout.items { - item.after_layout(cx); - } - } - fn paint( &mut self, bounds: RectF, diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index a2047a254071a745cd866232d0ecf17a4f09321d..57e40fff20a23fecd0e47970492dd15959eaaa67 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -398,15 +398,6 @@ impl Element for ChildView { (size, ()) } - fn after_layout( - &mut self, - _: Vector2F, - _: &mut Self::LayoutState, - cx: &mut AfterLayoutContext, - ) { - cx.after_layout(self.view_id); - } - fn paint( &mut self, bounds: pathfinder_geometry::rect::RectF, diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index 44b283dee1d7bf7108be123c535578b33a402560..297f04cb560e71b758c9108b130606e861d8fbf1 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -9,8 +9,8 @@ use gpui::{ }, json::{self, ToJson}, text_layout::{self, TextLayoutCache}, - AfterLayoutContext, AppContext, Border, Element, Event, EventContext, FontCache, LayoutContext, - MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, + AppContext, Border, Element, Event, EventContext, FontCache, LayoutContext, MutableAppContext, + PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, }; use json::json; use smallvec::SmallVec; @@ -552,9 +552,6 @@ impl Element for EditorElement { (size, Some(layout)) } - fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) { - } - fn paint( &mut self, bounds: RectF, From 030de803fed3205fbadef057f943945d657cb74c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 21 Aug 2021 13:40:45 -0600 Subject: [PATCH 047/204] WIP: Start on List::layout Perform layout on any elements that we haven't yet laid out. --- gpui/src/elements/list.rs | 101 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 4 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index d96637ea864574ced7b7df3b5a71a1f281e0f152..82b3819149b3759f7153fd8f4bea419a9f1c775e 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -1,6 +1,6 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, - sum_tree::{self, SumTree}, + sum_tree::{self, Bias, SumTree}, Element, }; use parking_lot::Mutex; @@ -15,6 +15,7 @@ pub struct List { pub struct ListState(Arc>); struct StateInner { + last_layout_width: f32, elements: Vec, heights: SumTree, } @@ -27,10 +28,20 @@ enum ElementHeight { #[derive(Clone, Debug, Default)] struct ElementHeightSummary { + count: usize, pending_count: usize, height: f32, } +#[derive(Clone, Debug, Default)] +struct Count(usize); + +#[derive(Clone, Debug, Default)] +struct PendingCount(usize); + +#[derive(Clone, Debug, Default)] +struct Height(f32); + impl Element for List { type LayoutState = (); @@ -41,6 +52,37 @@ impl Element for List { constraint: crate::SizeConstraint, cx: &mut crate::LayoutContext, ) -> (Vector2F, Self::LayoutState) { + // TODO: Fully invalidate if width has changed since the last layout. + + let state = &mut *self.state.0.lock(); + let mut old_heights = state.heights.cursor::(); + let mut new_heights = old_heights.slice(&PendingCount(1), sum_tree::Bias::Left, &()); + + let mut item_constraint = constraint; + item_constraint.min.set_y(0.); + item_constraint.max.set_y(f32::INFINITY); + + while let Some(height) = old_heights.item() { + if height.is_pending() { + let size = + state.elements[old_heights.sum_start().count].layout(item_constraint, cx); + new_heights.push(ElementHeight::Ready(size.y()), &()); + old_heights.next(&()); + } else { + new_heights.push_tree( + old_heights.slice( + &PendingCount(old_heights.sum_start().pending_count + 1), + Bias::Left, + &(), + ), + &(), + ); + } + } + + drop(old_heights); + state.heights = new_heights; + todo!() } @@ -79,7 +121,17 @@ impl ListState { pub fn new(elements: Vec) -> Self { let mut heights = SumTree::new(); heights.extend(elements.iter().map(|_| ElementHeight::Pending), &()); - Self(Arc::new(Mutex::new(StateInner { elements, heights }))) + Self(Arc::new(Mutex::new(StateInner { + last_layout_width: 0., + elements, + heights, + }))) + } +} + +impl ElementHeight { + fn is_pending(&self) -> bool { + matches!(self, ElementHeight::Pending) } } @@ -87,15 +139,56 @@ impl sum_tree::Item for ElementHeight { type Summary = ElementHeightSummary; fn summary(&self) -> Self::Summary { - todo!() + match self { + ElementHeight::Pending => ElementHeightSummary { + count: 1, + pending_count: 1, + height: 0., + }, + ElementHeight::Ready(height) => ElementHeightSummary { + count: 1, + pending_count: 0, + height: *height, + }, + } } } impl sum_tree::Summary for ElementHeightSummary { type Context = (); - fn add_summary(&mut self, summary: &Self, cx: &Self::Context) { + fn add_summary(&mut self, summary: &Self, _: &()) { self.pending_count += summary.pending_count; self.height += summary.height; } } + +impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for ElementHeightSummary { + fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) { + sum_tree::Summary::add_summary(self, summary, &()); + } +} + +impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Count { + fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) { + self.0 += summary.count; + } +} + +impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for PendingCount { + fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) { + self.0 += summary.pending_count; + } +} + +impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for PendingCount { + fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } +} + +impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Height { + fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) { + self.0 += summary.height; + } +} From 24639ec90097ce06a8fe5312699d3f05984b0589 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 22 Aug 2021 11:58:19 -0600 Subject: [PATCH 048/204] WIP --- gpui/src/app.rs | 279 ++++++++++++++---------- gpui/src/keymap.rs | 85 ++++---- gpui/src/platform.rs | 4 +- gpui/src/platform/mac/platform.rs | 17 +- gpui/src/platform/test.rs | 4 +- gpui/src/presenter.rs | 30 +-- zed/src/editor.rs | 339 +++++++++++++----------------- zed/src/editor/element.rs | 8 +- 8 files changed, 378 insertions(+), 388 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 0bcc0a4c29fc503687ed8e3fed1f58986989b803..f4ecdf48ff68729edc77e46bfae502ad94308641 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -92,6 +92,104 @@ pub trait UpdateView { F: FnOnce(&mut T, &mut ViewContext) -> S; } +pub trait Action: 'static + AnyAction { + type Argument: 'static + Clone; + + const NAME: &'static str; +} + +pub trait AnyAction { + fn id(&self) -> TypeId; + fn arg_as_any(&self) -> &dyn Any; + fn boxed_clone(&self) -> Box; + fn boxed_clone_as_any(&self) -> Box; +} + +impl Action for () { + type Argument = (); + + const NAME: &'static str = "()"; +} + +impl AnyAction for () { + fn id(&self) -> TypeId { + TypeId::of::<()>() + } + + fn arg_as_any(&self) -> &dyn Any { + &() + } + + fn boxed_clone(&self) -> Box { + Box::new(()) + } + + fn boxed_clone_as_any(&self) -> Box { + Box::new(()) + } +} + +#[macro_export] +macro_rules! action { + ($name:ident, $arg:ty) => { + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct $name(pub $arg); + + impl $crate::Action for $name { + type Argument = $arg; + + const NAME: &'static str = stringify!($name); + } + + impl $crate::AnyAction for $name { + fn id(&self) -> std::any::TypeId { + std::any::TypeId::of::<$name>() + } + + fn arg_as_any(&self) -> &dyn std::any::Any { + &self.0 + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn boxed_clone_as_any(&self) -> Box { + Box::new(self.clone()) + } + } + }; + + ($name:ident) => { + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct $name; + + impl $crate::Action for $name { + type Argument = (); + + const NAME: &'static str = stringify!($name); + } + + impl $crate::AnyAction for $name { + fn id(&self) -> std::any::TypeId { + std::any::TypeId::of::<$name>() + } + + fn arg_as_any(&self) -> &dyn std::any::Any { + &() + } + + fn boxed_clone(&self) -> Box { + Box::new(()) + } + + fn boxed_clone_as_any(&self) -> Box { + Box::new(()) + } + } + }; +} + pub struct Menu<'a> { pub name: &'a str, pub items: Vec>, @@ -101,8 +199,7 @@ pub enum MenuItem<'a> { Action { name: &'a str, keystroke: Option<&'a str>, - action: &'a str, - arg: Option>, + action: Box, }, Separator, } @@ -136,19 +233,19 @@ impl App { )))); let cx = app.0.clone(); - foreground_platform.on_menu_command(Box::new(move |command, arg| { + foreground_platform.on_menu_command(Box::new(move |action| { let mut cx = cx.borrow_mut(); if let Some(key_window_id) = cx.cx.platform.key_window_id() { if let Some((presenter, _)) = cx.presenters_and_platform_windows.get(&key_window_id) { let presenter = presenter.clone(); let path = presenter.borrow().dispatch_path(cx.as_ref()); - cx.dispatch_action_any(key_window_id, &path, command, arg.unwrap_or(&())); + cx.dispatch_action_any(key_window_id, &path, action); } else { - cx.dispatch_global_action_any(command, arg.unwrap_or(&())); + cx.dispatch_global_action_any(action); } } else { - cx.dispatch_global_action_any(command, arg.unwrap_or(&())); + cx.dispatch_global_action_any(action); } })); @@ -258,23 +355,19 @@ impl TestAppContext { cx } - pub fn dispatch_action( + pub fn dispatch_action( &self, window_id: usize, responder_chain: Vec, - name: &str, - arg: T, + action: A, ) { - self.cx.borrow_mut().dispatch_action_any( - window_id, - &responder_chain, - name, - Box::new(arg).as_ref(), - ); + self.cx + .borrow_mut() + .dispatch_action_any(window_id, &responder_chain, &action); } - pub fn dispatch_global_action(&self, name: &str, arg: T) { - self.cx.borrow_mut().dispatch_global_action(name, arg); + pub fn dispatch_global_action(&self, action: A) { + self.cx.borrow_mut().dispatch_global_action(action); } pub fn dispatch_keystroke( @@ -563,17 +656,17 @@ impl ReadViewWith for TestAppContext { } type ActionCallback = - dyn FnMut(&mut dyn AnyView, &dyn Any, &mut MutableAppContext, usize, usize) -> bool; + dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize) -> bool; -type GlobalActionCallback = dyn FnMut(&dyn Any, &mut MutableAppContext); +type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); pub struct MutableAppContext { weak_self: Option>>, foreground_platform: Rc, assets: Arc, cx: AppContext, - actions: HashMap>>>, - global_actions: HashMap>>, + actions: HashMap>>>, + global_actions: HashMap>>, keystroke_matcher: keymap::Matcher, next_entity_id: usize, next_window_id: usize, @@ -663,69 +756,53 @@ impl MutableAppContext { .map(|debug_elements| debug_elements(&self.cx)) } - pub fn add_action(&mut self, name: S, mut handler: F) + pub fn add_action(&mut self, mut handler: F) where - S: Into, + A: Action, V: View, - T: Any, - F: 'static + FnMut(&mut V, &T, &mut ViewContext), + F: 'static + FnMut(&mut V, &A, &mut ViewContext), { - let name = name.into(); - let name_clone = name.clone(); let handler = Box::new( move |view: &mut dyn AnyView, - arg: &dyn Any, + action: &dyn AnyAction, cx: &mut MutableAppContext, window_id: usize, view_id: usize| { - match arg.downcast_ref() { - Some(arg) => { - let mut cx = ViewContext::new(cx, window_id, view_id); - handler( - view.as_any_mut() - .downcast_mut() - .expect("downcast is type safe"), - arg, - &mut cx, - ); - cx.halt_action_dispatch - } - None => { - log::error!("Could not downcast argument for action {}", name_clone); - false - } - } + let arg = action.arg_as_any().downcast_ref().unwrap(); + let mut cx = ViewContext::new(cx, window_id, view_id); + handler( + view.as_any_mut() + .downcast_mut() + .expect("downcast is type safe"), + arg, + &mut cx, + ); + cx.halt_action_dispatch }, ); self.actions .entry(TypeId::of::()) .or_default() - .entry(name) + .entry(TypeId::of::()) .or_default() .push(handler); } - pub fn add_global_action(&mut self, name: S, mut handler: F) + pub fn add_global_action(&mut self, mut handler: F) where - S: Into, - T: 'static + Any, - F: 'static + FnMut(&T, &mut MutableAppContext), + A: Action, + F: 'static + FnMut(&A, &mut MutableAppContext), { - let name = name.into(); - let name_clone = name.clone(); - let handler = Box::new(move |arg: &dyn Any, cx: &mut MutableAppContext| { - if let Some(arg) = arg.downcast_ref() { - handler(arg, cx); - } else { - log::error!( - "Could not downcast argument for global action {}", - name_clone - ); - } + let handler = Box::new(move |action: &dyn AnyAction, cx: &mut MutableAppContext| { + let arg = action.arg_as_any().downcast_ref().unwrap(); + handler(arg, cx); }); - self.global_actions.entry(name).or_default().push(handler); + self.global_actions + .entry(TypeId::of::()) + .or_default() + .push(handler); } pub fn window_ids(&self) -> impl Iterator + '_ { @@ -838,22 +915,20 @@ impl MutableAppContext { self.pending_effects.extend(notifications); } - pub fn dispatch_action( + pub fn dispatch_action( &mut self, window_id: usize, responder_chain: Vec, - name: &str, - arg: T, + action: &A, ) { - self.dispatch_action_any(window_id, &responder_chain, name, Box::new(arg).as_ref()); + self.dispatch_action_any(window_id, &responder_chain, action); } pub(crate) fn dispatch_action_any( &mut self, window_id: usize, path: &[usize], - name: &str, - arg: &dyn Any, + action: &dyn AnyAction, ) -> bool { self.pending_flushes += 1; let mut halted_dispatch = false; @@ -865,10 +940,11 @@ impl MutableAppContext { if let Some((name, mut handlers)) = self .actions .get_mut(&type_id) - .and_then(|h| h.remove_entry(name)) + .and_then(|h| h.remove_entry(&action.id())) { for handler in handlers.iter_mut().rev() { - let halt_dispatch = handler(view.as_mut(), arg, self, window_id, *view_id); + let halt_dispatch = + handler(view.as_mut(), action, self, window_id, *view_id); if halt_dispatch { halted_dispatch = true; break; @@ -889,22 +965,22 @@ impl MutableAppContext { } if !halted_dispatch { - self.dispatch_global_action_any(name, arg); + self.dispatch_global_action_any(action); } self.flush_effects(); halted_dispatch } - pub fn dispatch_global_action(&mut self, name: &str, arg: T) { - self.dispatch_global_action_any(name, Box::new(arg).as_ref()); + pub fn dispatch_global_action(&mut self, action: A) { + self.dispatch_global_action_any(&action); } - fn dispatch_global_action_any(&mut self, name: &str, arg: &dyn Any) { - if let Some((name, mut handlers)) = self.global_actions.remove_entry(name) { + fn dispatch_global_action_any(&mut self, action: &dyn AnyAction) { + if let Some((name, mut handlers)) = self.global_actions.remove_entry(&action.id()) { self.pending_flushes += 1; for handler in handlers.iter_mut().rev() { - handler(arg, self); + handler(action, self); } self.global_actions.insert(name, handlers); self.flush_effects(); @@ -943,13 +1019,9 @@ impl MutableAppContext { { MatchResult::None => {} MatchResult::Pending => pending = true, - MatchResult::Action { name, arg } => { - if self.dispatch_action_any( - window_id, - &responder_chain[0..=i], - &name, - arg.as_ref().map(|arg| arg.as_ref()).unwrap_or(&()), - ) { + MatchResult::Action(action) => { + if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref()) + { return Ok(true); } } @@ -3575,31 +3647,29 @@ mod tests { } } - struct ActionArg { - foo: String, - } + action!(Action, &'static str); let actions = Rc::new(RefCell::new(Vec::new())); let actions_clone = actions.clone(); - cx.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { + cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| { actions_clone.borrow_mut().push("global a".to_string()); }); let actions_clone = actions.clone(); - cx.add_global_action("action", move |_: &ActionArg, _: &mut MutableAppContext| { + cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| { actions_clone.borrow_mut().push("global b".to_string()); }); let actions_clone = actions.clone(); - cx.add_action("action", move |view: &mut ViewA, arg: &ActionArg, cx| { - assert_eq!(arg.foo, "bar"); + cx.add_action(move |view: &mut ViewA, action: &Action, cx| { + assert_eq!(action.0, "bar"); cx.propagate_action(); actions_clone.borrow_mut().push(format!("{} a", view.id)); }); let actions_clone = actions.clone(); - cx.add_action("action", move |view: &mut ViewA, _: &ActionArg, cx| { + cx.add_action(move |view: &mut ViewA, _: &Action, cx| { if view.id != 1 { cx.propagate_action(); } @@ -3607,13 +3677,13 @@ mod tests { }); let actions_clone = actions.clone(); - cx.add_action("action", move |view: &mut ViewB, _: &ActionArg, cx| { + cx.add_action(move |view: &mut ViewB, action: &Action, cx| { cx.propagate_action(); actions_clone.borrow_mut().push(format!("{} c", view.id)); }); let actions_clone = actions.clone(); - cx.add_action("action", move |view: &mut ViewB, _: &ActionArg, cx| { + cx.add_action(move |view: &mut ViewB, action: &Action, cx| { cx.propagate_action(); actions_clone.borrow_mut().push(format!("{} d", view.id)); }); @@ -3626,8 +3696,7 @@ mod tests { cx.dispatch_action( window_id, vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()], - "action", - ActionArg { foo: "bar".into() }, + &Action("bar"), ); assert_eq!( @@ -3640,8 +3709,7 @@ mod tests { cx.dispatch_action( window_id, vec![view_2.id(), view_3.id(), view_4.id()], - "action", - ActionArg { foo: "bar".into() }, + &Action("bar"), ); assert_eq!( @@ -3654,10 +3722,7 @@ mod tests { fn test_dispatch_keystroke(cx: &mut MutableAppContext) { use std::cell::Cell; - #[derive(Clone)] - struct ActionArg { - key: String, - } + action!(Action, &'static str); struct View { id: usize, @@ -3704,16 +3769,18 @@ mod tests { // This keymap's only binding dispatches an action on view 2 because that view will have // "a" and "b" in its context, but not "c". - let binding = keymap::Binding::new("a", "action", Some("a && b && !c")) - .with_arg(ActionArg { key: "a".into() }); - cx.add_bindings(vec![binding]); + cx.add_bindings(vec![keymap::Binding::new( + "a", + Action("a"), + Some("a && b && !c"), + )]); let handled_action = Rc::new(Cell::new(false)); let handled_action_clone = handled_action.clone(); - cx.add_action("action", move |view: &mut View, arg: &ActionArg, _| { + cx.add_action(move |view: &mut View, action: &Action, _| { handled_action_clone.set(true); assert_eq!(view.id, 2); - assert_eq!(arg.key, "a"); + assert_eq!(action.0, "a"); }); cx.dispatch_keystroke( diff --git a/gpui/src/keymap.rs b/gpui/src/keymap.rs index a3a2cbf58c1078d9e20a9b2975f4bf7cdc8c9a25..38a32e5e8ce88636a38ed2e47b5f0e83f1fdf595 100644 --- a/gpui/src/keymap.rs +++ b/gpui/src/keymap.rs @@ -5,6 +5,8 @@ use std::{ }; use tree_sitter::{Language, Node, Parser}; +use crate::{Action, AnyAction}; + extern "C" { fn tree_sitter_context_predicate() -> Language; } @@ -24,8 +26,7 @@ pub struct Keymap(Vec); pub struct Binding { keystrokes: Vec, - action: String, - action_arg: Option>, + action: Box, context: Option, } @@ -70,10 +71,7 @@ where pub enum MatchResult { None, Pending, - Action { - name: String, - arg: Option>, - }, + Action(Box), } impl Matcher { @@ -117,10 +115,7 @@ impl Matcher { { if binding.keystrokes.len() == pending.keystrokes.len() { self.pending.remove(&view_id); - return MatchResult::Action { - name: binding.action.clone(), - arg: binding.action_arg.as_ref().map(|arg| (*arg).boxed_clone()), - }; + return MatchResult::Action(binding.action.boxed_clone()); } else { retain_pending = true; pending.context = Some(cx.clone()); @@ -153,19 +148,26 @@ impl Keymap { } } +mod menu { + use crate::action; + + action!(SelectPrev); + action!(SelectNext); +} + impl Default for Keymap { fn default() -> Self { Self(vec![ - Binding::new("up", "menu:select_prev", Some("menu")), - Binding::new("ctrl-p", "menu:select_prev", Some("menu")), - Binding::new("down", "menu:select_next", Some("menu")), - Binding::new("ctrl-n", "menu:select_next", Some("menu")), + Binding::new("up", menu::SelectPrev, Some("menu")), + Binding::new("ctrl-p", menu::SelectPrev, Some("menu")), + Binding::new("down", menu::SelectNext, Some("menu")), + Binding::new("ctrl-n", menu::SelectNext, Some("menu")), ]) } } impl Binding { - pub fn new>(keystrokes: &str, action: S, context: Option<&str>) -> Self { + pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { let context = if let Some(context) = context { Some(ContextPredicate::parse(context).unwrap()) } else { @@ -177,16 +179,10 @@ impl Binding { .split_whitespace() .map(|key| Keystroke::parse(key).unwrap()) .collect(), - action: action.into(), - action_arg: None, + action: Box::new(action), context, } } - - pub fn with_arg(mut self, arg: T) -> Self { - self.action_arg = Some(Box::new(arg)); - self - } } impl Keystroke { @@ -328,6 +324,8 @@ impl ContextPredicate { #[cfg(test)] mod tests { + use crate::action; + use super::*; #[test] @@ -417,15 +415,19 @@ mod tests { #[test] fn test_matcher() -> anyhow::Result<()> { + action!(A, &'static str); + action!(B); + action!(Ab); + #[derive(Clone, Debug, Eq, PartialEq)] struct ActionArg { a: &'static str, } let keymap = Keymap(vec![ - Binding::new("a", "a", Some("a")).with_arg(ActionArg { a: "b" }), - Binding::new("b", "b", Some("a")), - Binding::new("a b", "a_b", Some("a || b")), + Binding::new("a", A("x"), Some("a")), + Binding::new("b", B, Some("a")), + Binding::new("a b", Ab, Some("a || b")), ]); let mut ctx_a = Context::default(); @@ -437,31 +439,19 @@ mod tests { let mut matcher = Matcher::new(keymap); // Basic match - assert_eq!( - matcher.test_keystroke("a", 1, &ctx_a), - Some(("a".to_string(), Some(ActionArg { a: "b" }))) - ); + assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x"))); // Multi-keystroke match assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None); - assert_eq!( - matcher.test_keystroke::<()>("b", 1, &ctx_b), - Some(("a_b".to_string(), None)) - ); + assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab)); // Failed matches don't interfere with matching subsequent keys assert_eq!(matcher.test_keystroke::<()>("x", 1, &ctx_a), None); - assert_eq!( - matcher.test_keystroke("a", 1, &ctx_a), - Some(("a".to_string(), Some(ActionArg { a: "b" }))) - ); + assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x"))); // Pending keystrokes are cleared when the context changes assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None); - assert_eq!( - matcher.test_keystroke::<()>("b", 1, &ctx_a), - Some(("b".to_string(), None)) - ); + assert_eq!(matcher.test_keystroke("b", 1, &ctx_a), Some(B)); let mut ctx_c = Context::default(); ctx_c.set.insert("c".into()); @@ -469,25 +459,22 @@ mod tests { // Pending keystrokes are maintained per-view assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None); assert_eq!(matcher.test_keystroke::<()>("a", 2, &ctx_c), None); - assert_eq!( - matcher.test_keystroke::<()>("b", 1, &ctx_b), - Some(("a_b".to_string(), None)) - ); + assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab)); Ok(()) } impl Matcher { - fn test_keystroke( + fn test_keystroke( &mut self, keystroke: &str, view_id: usize, cx: &Context, - ) -> Option<(String, Option)> { - if let MatchResult::Action { name, arg } = + ) -> Option { + if let MatchResult::Action(action) = self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, cx) { - Some((name, arg.and_then(|arg| arg.downcast_ref::().cloned()))) + Some(*action.boxed_clone_as_any().downcast().unwrap()) } else { None } diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index f6930c1ae8a24144c224f60a72255e6e9d10f383..449f6bb962ea696e6a0b0b455febb4426b0ec38c 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -16,7 +16,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, text_layout::LineLayout, - ClipboardItem, Menu, Scene, + AnyAction, ClipboardItem, Menu, Scene, }; use async_task::Runnable; pub use event::Event; @@ -56,7 +56,7 @@ pub(crate) trait ForegroundPlatform { fn on_open_files(&self, callback: Box)>); fn run(&self, on_finish_launching: Box ()>); - fn on_menu_command(&self, callback: Box)>); + fn on_menu_command(&self, callback: Box); fn set_menus(&self, menus: Vec); fn prompt_for_paths( &self, diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index 794debe4b162d499ff404ad37880cbc9739413de..1b4331bedce42de4b645bac85f83783dcedc5782 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -1,5 +1,7 @@ use super::{BoolExt as _, Dispatcher, FontSystem, Window}; -use crate::{executor, keymap::Keystroke, platform, ClipboardItem, Event, Menu, MenuItem}; +use crate::{ + executor, keymap::Keystroke, platform, AnyAction, ClipboardItem, Event, Menu, MenuItem, +}; use block::ConcreteBlock; use cocoa::{ appkit::{ @@ -90,10 +92,10 @@ pub struct MacForegroundPlatformState { become_active: Option>, resign_active: Option>, event: Option bool>>, - menu_command: Option)>>, + menu_command: Option>, open_files: Option)>>, finish_launching: Option ()>>, - menu_actions: Vec<(String, Option>)>, + menu_actions: Vec>, } impl MacForegroundPlatform { @@ -121,7 +123,6 @@ impl MacForegroundPlatform { name, keystroke, action, - arg, } => { if let Some(keystroke) = keystroke { let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| { @@ -162,7 +163,7 @@ impl MacForegroundPlatform { let tag = state.menu_actions.len() as NSInteger; let _: () = msg_send![item, setTag: tag]; - state.menu_actions.push((action.to_string(), arg)); + state.menu_actions.push(action); } } @@ -215,7 +216,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { } } - fn on_menu_command(&self, callback: Box)>) { + fn on_menu_command(&self, callback: Box) { self.0.borrow_mut().menu_command = Some(callback); } @@ -623,8 +624,8 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { if let Some(mut callback) = platform.menu_command.take() { let tag: NSInteger = msg_send![item, tag]; let index = tag as usize; - if let Some((action, arg)) = platform.menu_actions.get(index) { - callback(action, arg.as_ref().map(Box::as_ref)); + if let Some(action) = platform.menu_actions.get(index) { + callback(action.as_ref()); } platform.menu_command = Some(callback); } diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 86b1153424bc7bb9937ec66cd9047a44aa8f1cf1..40c9304a7aa1ebe4ed2cc6e2521ee849d6fd3b80 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -1,4 +1,4 @@ -use crate::ClipboardItem; +use crate::{AnyAction, ClipboardItem}; use parking_lot::Mutex; use pathfinder_geometry::vector::Vector2F; use std::{ @@ -62,7 +62,7 @@ impl super::ForegroundPlatform for ForegroundPlatform { unimplemented!() } - fn on_menu_command(&self, _: Box)>) {} + fn on_menu_command(&self, _: Box) {} fn set_menus(&self, _: Vec) {} diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 57e40fff20a23fecd0e47970492dd15959eaaa67..4e548ad03b95c8980938334a6c75f4230af59361 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -5,12 +5,11 @@ use crate::{ json::{self, ToJson}, platform::Event, text_layout::TextLayoutCache, - AssetCache, ElementBox, Scene, + Action, AnyAction, AssetCache, ElementBox, Scene, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; use std::{ - any::Any, collections::{HashMap, HashSet}, sync::Arc, }; @@ -144,7 +143,7 @@ impl Presenter { let mut event_cx = EventContext { rendered_views: &mut self.rendered_views, - actions: Default::default(), + dispatched_actions: Default::default(), font_cache: &self.font_cache, text_layout_cache: &self.text_layout_cache, view_stack: Default::default(), @@ -154,18 +153,13 @@ impl Presenter { event_cx.dispatch_event(root_view_id, &event); let invalidated_views = event_cx.invalidated_views; - let actions = event_cx.actions; + let dispatch_directives = event_cx.dispatched_actions; for view_id in invalidated_views { cx.notify_view(self.window_id, view_id); } - for action in actions { - cx.dispatch_action_any( - self.window_id, - &action.path, - action.name, - action.arg.as_ref(), - ); + for directive in dispatch_directives { + cx.dispatch_action_any(self.window_id, &directive.path, directive.action.as_ref()); } } } @@ -183,10 +177,9 @@ impl Presenter { } } -pub struct ActionToDispatch { +pub struct DispatchDirective { pub path: Vec, - pub name: &'static str, - pub arg: Box, + pub action: Box, } pub struct LayoutContext<'a> { @@ -249,7 +242,7 @@ impl<'a> PaintContext<'a> { pub struct EventContext<'a> { rendered_views: &'a mut HashMap, - actions: Vec, + dispatched_actions: Vec, pub font_cache: &'a FontCache, pub text_layout_cache: &'a TextLayoutCache, pub app: &'a mut MutableAppContext, @@ -270,11 +263,10 @@ impl<'a> EventContext<'a> { } } - pub fn dispatch_action(&mut self, name: &'static str, arg: A) { - self.actions.push(ActionToDispatch { + pub fn dispatch_action(&mut self, action: A) { + self.dispatched_actions.push(DispatchDirective { path: self.view_stack.clone(), - name, - arg: Box::new(arg), + action: Box::new(action), }); } diff --git a/zed/src/editor.rs b/zed/src/editor.rs index b3dc4f8a2b860389c1d816a7744ec154cb0b6ec5..f7ba4c1cb0bc7981d00166c82e2fd044091ba2de 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -16,7 +16,7 @@ pub use display_map::DisplayPoint; use display_map::*; pub use element::*; use gpui::{ - color::Color, font_cache::FamilyId, fonts::Properties as FontProperties, + action, color::Color, font_cache::FamilyId, fonts::Properties as FontProperties, geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, RenderContext, Task, TextLayoutCache, View, ViewContext, WeakViewHandle, @@ -40,224 +40,167 @@ use std::{ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; +action!(Cancel); +action!(Backspace); +action!(Delete); +action!(Newline); +action!(Insert, String); +action!(DeleteLine); +action!(DeleteToPreviousWordBoundary); +action!(DeleteToNextWordBoundary); +action!(DeleteToBeginningOfLine); +action!(DeleteToEndOfLine); +action!(CutToEndOfLine); +action!(DuplicateLine); +action!(MoveLineUp); +action!(MoveLineDown); +action!(Cut); +action!(Copy); +action!(Paste); +action!(Undo); +action!(Redo); +action!(MoveUp); +action!(MoveDown); +action!(MoveLeft); +action!(MoveRight); +action!(MoveToPreviousWordBoundary); +action!(MoveToNextWordBoundary); +action!(MoveToBeginningOfLine); +action!(MoveToEndOfLine); +action!(MoveToBeginning); +action!(MoveToEnd); +action!(SelectUp); +action!(SelectDown); +action!(SelectLeft); +action!(SelectRight); +action!(SelectToPreviousWordBoundary); +action!(SelectToNextWordBoundary); +action!(SelectToBeginningOfLine, bool); +action!(SelectToEndOfLine); +action!(SelectToBeginning); +action!(SelectToEnd); +action!(SelectAll); +action!(SelectLine); +action!(SplitSelectionIntoLines); +action!(AddSelectionAbove); +action!(AddSelectionBelow); +action!(SelectLargerSyntaxNode); +action!(SelectSmallerSyntaxNode); +action!(MoveToEnclosingBracket); +action!(PageUp); +action!(PageDown); +action!(Fold); +action!(Unfold); +action!(FoldSelectedRanges); +action!(Scroll, Vector2F); +action!(Select, SelectPhase); + pub fn init(cx: &mut MutableAppContext) { cx.add_bindings(vec![ - Binding::new("escape", "buffer:cancel", Some("BufferView")), - Binding::new("backspace", "buffer:backspace", Some("BufferView")), - Binding::new("ctrl-h", "buffer:backspace", Some("BufferView")), - Binding::new("delete", "buffer:delete", Some("BufferView")), - Binding::new("ctrl-d", "buffer:delete", Some("BufferView")), - Binding::new("enter", "buffer:newline", Some("BufferView")), - Binding::new("tab", "buffer:insert", Some("BufferView")).with_arg("\t".to_string()), - Binding::new("ctrl-shift-K", "buffer:delete_line", Some("BufferView")), + Binding::new("escape", Cancel, Some("BufferView")), + Binding::new("backspace", Backspace, Some("BufferView")), + Binding::new("ctrl-h", Backspace, Some("BufferView")), + Binding::new("delete", Delete, Some("BufferView")), + Binding::new("ctrl-d", Delete, Some("BufferView")), + Binding::new("enter", Newline, Some("BufferView")), + Binding::new("tab", Insert("\t".into()), Some("BufferView")), + Binding::new("ctrl-shift-K", DeleteLine, Some("BufferView")), Binding::new( "alt-backspace", - "buffer:delete_to_previous_word_boundary", - Some("BufferView"), - ), - Binding::new( - "alt-h", - "buffer:delete_to_previous_word_boundary", - Some("BufferView"), - ), - Binding::new( - "alt-delete", - "buffer:delete_to_next_word_boundary", - Some("BufferView"), - ), - Binding::new( - "alt-d", - "buffer:delete_to_next_word_boundary", - Some("BufferView"), - ), - Binding::new( - "cmd-backspace", - "buffer:delete_to_beginning_of_line", - Some("BufferView"), - ), - Binding::new( - "cmd-delete", - "buffer:delete_to_end_of_line", - Some("BufferView"), - ), - Binding::new("ctrl-k", "buffer:cut_to_end_of_line", Some("BufferView")), - Binding::new("cmd-shift-D", "buffer:duplicate_line", Some("BufferView")), - Binding::new("ctrl-cmd-up", "buffer:move_line_up", Some("BufferView")), - Binding::new("ctrl-cmd-down", "buffer:move_line_down", Some("BufferView")), - Binding::new("cmd-x", "buffer:cut", Some("BufferView")), - Binding::new("cmd-c", "buffer:copy", Some("BufferView")), - Binding::new("cmd-v", "buffer:paste", Some("BufferView")), - Binding::new("cmd-z", "buffer:undo", Some("BufferView")), - Binding::new("cmd-shift-Z", "buffer:redo", Some("BufferView")), - Binding::new("up", "buffer:move_up", Some("BufferView")), - Binding::new("down", "buffer:move_down", Some("BufferView")), - Binding::new("left", "buffer:move_left", Some("BufferView")), - Binding::new("right", "buffer:move_right", Some("BufferView")), - Binding::new("ctrl-p", "buffer:move_up", Some("BufferView")), - Binding::new("ctrl-n", "buffer:move_down", Some("BufferView")), - Binding::new("ctrl-b", "buffer:move_left", Some("BufferView")), - Binding::new("ctrl-f", "buffer:move_right", Some("BufferView")), - Binding::new( - "alt-left", - "buffer:move_to_previous_word_boundary", - Some("BufferView"), - ), - Binding::new( - "alt-b", - "buffer:move_to_previous_word_boundary", - Some("BufferView"), - ), - Binding::new( - "alt-right", - "buffer:move_to_next_word_boundary", + DeleteToPreviousWordBoundary, Some("BufferView"), ), - Binding::new( - "alt-f", - "buffer:move_to_next_word_boundary", - Some("BufferView"), - ), - Binding::new( - "cmd-left", - "buffer:move_to_beginning_of_line", - Some("BufferView"), - ), - Binding::new( - "ctrl-a", - "buffer:move_to_beginning_of_line", - Some("BufferView"), - ), - Binding::new( - "cmd-right", - "buffer:move_to_end_of_line", - Some("BufferView"), - ), - Binding::new("ctrl-e", "buffer:move_to_end_of_line", Some("BufferView")), - Binding::new("cmd-up", "buffer:move_to_beginning", Some("BufferView")), - Binding::new("cmd-down", "buffer:move_to_end", Some("BufferView")), - Binding::new("shift-up", "buffer:select_up", Some("BufferView")), - Binding::new("ctrl-shift-P", "buffer:select_up", Some("BufferView")), - Binding::new("shift-down", "buffer:select_down", Some("BufferView")), - Binding::new("ctrl-shift-N", "buffer:select_down", Some("BufferView")), - Binding::new("shift-left", "buffer:select_left", Some("BufferView")), - Binding::new("ctrl-shift-B", "buffer:select_left", Some("BufferView")), - Binding::new("shift-right", "buffer:select_right", Some("BufferView")), - Binding::new("ctrl-shift-F", "buffer:select_right", Some("BufferView")), + Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("BufferView")), + Binding::new("alt-delete", DeleteToNextWordBoundary, Some("BufferView")), + Binding::new("alt-d", DeleteToNextWordBoundary, Some("BufferView")), + Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("BufferView")), + Binding::new("cmd-delete", DeleteToEndOfLine, Some("BufferView")), + Binding::new("ctrl-k", CutToEndOfLine, Some("BufferView")), + Binding::new("cmd-shift-D", DuplicateLine, Some("BufferView")), + Binding::new("ctrl-cmd-up", MoveLineUp, Some("BufferView")), + Binding::new("ctrl-cmd-down", MoveLineDown, Some("BufferView")), + Binding::new("cmd-x", Cut, Some("BufferView")), + Binding::new("cmd-c", Copy, Some("BufferView")), + Binding::new("cmd-v", Paste, Some("BufferView")), + Binding::new("cmd-z", Undo, Some("BufferView")), + Binding::new("cmd-shift-Z", Redo, Some("BufferView")), + Binding::new("up", MoveUp, Some("BufferView")), + Binding::new("down", MoveDown, Some("BufferView")), + Binding::new("left", MoveLeft, Some("BufferView")), + Binding::new("right", MoveRight, Some("BufferView")), + Binding::new("ctrl-p", MoveUp, Some("BufferView")), + Binding::new("ctrl-n", MoveDown, Some("BufferView")), + Binding::new("ctrl-b", MoveLeft, Some("BufferView")), + Binding::new("ctrl-f", MoveRight, Some("BufferView")), + Binding::new("alt-left", MoveToPreviousWordBoundary, Some("BufferView")), + Binding::new("alt-b", MoveToPreviousWordBoundary, Some("BufferView")), + Binding::new("alt-right", MoveToNextWordBoundary, Some("BufferView")), + Binding::new("alt-f", MoveToNextWordBoundary, Some("BufferView")), + Binding::new("cmd-left", MoveToBeginningOfLine, Some("BufferView")), + Binding::new("ctrl-a", MoveToBeginningOfLine, Some("BufferView")), + Binding::new("cmd-right", MoveToEndOfLine, Some("BufferView")), + Binding::new("ctrl-e", MoveToEndOfLine, Some("BufferView")), + Binding::new("cmd-up", MoveToBeginning, Some("BufferView")), + Binding::new("cmd-down", MoveToEnd, Some("BufferView")), + Binding::new("shift-up", SelectUp, Some("BufferView")), + Binding::new("ctrl-shift-P", SelectUp, Some("BufferView")), + Binding::new("shift-down", SelectDown, Some("BufferView")), + Binding::new("ctrl-shift-N", SelectDown, Some("BufferView")), + Binding::new("shift-left", SelectLeft, Some("BufferView")), + Binding::new("ctrl-shift-B", SelectLeft, Some("BufferView")), + Binding::new("shift-right", SelectRight, Some("BufferView")), + Binding::new("ctrl-shift-F", SelectRight, Some("BufferView")), Binding::new( "alt-shift-left", - "buffer:select_to_previous_word_boundary", + SelectToPreviousWordBoundary, Some("BufferView"), ), Binding::new( "alt-shift-B", - "buffer:select_to_previous_word_boundary", + SelectToPreviousWordBoundary, Some("BufferView"), ), Binding::new( "alt-shift-right", - "buffer:select_to_next_word_boundary", - Some("BufferView"), - ), - Binding::new( - "alt-shift-F", - "buffer:select_to_next_word_boundary", + SelectToNextWordBoundary, Some("BufferView"), ), + Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("BufferView")), Binding::new( "cmd-shift-left", - "buffer:select_to_beginning_of_line", - Some("BufferView"), - ) - .with_arg(true), - Binding::new( - "ctrl-shift-A", - "buffer:select_to_beginning_of_line", - Some("BufferView"), - ) - .with_arg(true), - Binding::new( - "cmd-shift-right", - "buffer:select_to_end_of_line", - Some("BufferView"), - ), - Binding::new( - "ctrl-shift-E", - "buffer:select_to_end_of_line", - Some("BufferView"), - ), - Binding::new( - "cmd-shift-up", - "buffer:select_to_beginning", - Some("BufferView"), - ), - Binding::new("cmd-shift-down", "buffer:select_to_end", Some("BufferView")), - Binding::new("cmd-a", "buffer:select_all", Some("BufferView")), - Binding::new("cmd-l", "buffer:select_line", Some("BufferView")), - Binding::new( - "cmd-shift-L", - "buffer:split_selection_into_lines", - Some("BufferView"), - ), - Binding::new( - "cmd-alt-up", - "buffer:add_selection_above", - Some("BufferView"), - ), - Binding::new( - "cmd-ctrl-p", - "buffer:add_selection_above", - Some("BufferView"), - ), - Binding::new( - "cmd-alt-down", - "buffer:add_selection_below", - Some("BufferView"), - ), - Binding::new( - "cmd-ctrl-n", - "buffer:add_selection_below", - Some("BufferView"), - ), - Binding::new( - "alt-up", - "buffer:select_larger_syntax_node", - Some("BufferView"), - ), - Binding::new( - "ctrl-w", - "buffer:select_larger_syntax_node", + SelectToBeginningOfLine(true), Some("BufferView"), ), Binding::new( - "alt-down", - "buffer:select_smaller_syntax_node", - Some("BufferView"), - ), - Binding::new( - "ctrl-shift-W", - "buffer:select_smaller_syntax_node", - Some("BufferView"), - ), - Binding::new( - "ctrl-m", - "buffer:move_to_enclosing_bracket", - Some("BufferView"), - ), - Binding::new("pageup", "buffer:page_up", Some("BufferView")), - Binding::new("pagedown", "buffer:page_down", Some("BufferView")), - Binding::new("alt-cmd-[", "buffer:fold", Some("BufferView")), - Binding::new("alt-cmd-]", "buffer:unfold", Some("BufferView")), - Binding::new( - "alt-cmd-f", - "buffer:fold_selected_ranges", + "ctrl-shift-A", + SelectToBeginningOfLine(true), Some("BufferView"), ), + Binding::new("cmd-shift-right", SelectToEndOfLine, Some("BufferView")), + Binding::new("ctrl-shift-E", SelectToEndOfLine, Some("BufferView")), + Binding::new("cmd-shift-up", SelectToBeginning, Some("BufferView")), + Binding::new("cmd-shift-down", SelectToEnd, Some("BufferView")), + Binding::new("cmd-a", SelectAll, Some("BufferView")), + Binding::new("cmd-l", SelectLine, Some("BufferView")), + Binding::new("cmd-shift-L", SplitSelectionIntoLines, Some("BufferView")), + Binding::new("cmd-alt-up", AddSelectionAbove, Some("BufferView")), + Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("BufferView")), + Binding::new("cmd-alt-down", AddSelectionBelow, Some("BufferView")), + Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("BufferView")), + Binding::new("alt-up", SelectLargerSyntaxNode, Some("BufferView")), + Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("BufferView")), + Binding::new("alt-down", SelectSmallerSyntaxNode, Some("BufferView")), + Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("BufferView")), + Binding::new("ctrl-m", MoveToEnclosingBracket, Some("BufferView")), + Binding::new("pageup", PageUp, Some("BufferView")), + Binding::new("pagedown", PageDown, Some("BufferView")), + Binding::new("alt-cmd-[", Fold, Some("BufferView")), + Binding::new("alt-cmd-]", Unfold, Some("BufferView")), + Binding::new("alt-cmd-f", FoldSelectedRanges, Some("BufferView")), ]); - cx.add_action("buffer:scroll", |this: &mut Editor, scroll_position, cx| { - this.set_scroll_position(*scroll_position, cx) - }); - cx.add_action("buffer:select", Editor::select); + cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); + cx.add_action(Editor::select); cx.add_action("buffer:cancel", Editor::cancel); cx.add_action("buffer:insert", Editor::insert); cx.add_action("buffer:newline", Editor::newline); @@ -357,7 +300,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action("buffer:fold_selected_ranges", Editor::fold_selected_ranges); } -pub enum SelectAction { +pub enum SelectPhase { Begin { position: DisplayPoint, add: bool, @@ -612,14 +555,14 @@ impl Editor { } } - fn select(&mut self, arg: &SelectAction, cx: &mut ViewContext) { - match arg { - SelectAction::Begin { position, add } => self.begin_selection(*position, *add, cx), - SelectAction::Update { + fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { + match phase { + SelectPhase::Begin { position, add } => self.begin_selection(*position, *add, cx), + SelectPhase::Update { position, scroll_position, } => self.update_selection(*position, *scroll_position, cx), - SelectAction::End => self.end_selection(cx), + SelectPhase::End => self.end_selection(cx), } } diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index 297f04cb560e71b758c9108b130606e861d8fbf1..a3da9363690e3dd8eceef8ad459c8d1f5ec438b6 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -1,4 +1,4 @@ -use super::{DisplayPoint, Editor, SelectAction, Snapshot}; +use super::{DisplayPoint, Editor, SelectPhase, Snapshot}; use crate::time::ReplicaId; use gpui::{ color::Color, @@ -55,7 +55,7 @@ impl EditorElement { if paint.text_bounds.contains_point(position) { let snapshot = self.snapshot(cx.app); let position = paint.point_for_position(&snapshot, layout, position); - cx.dispatch_action("buffer:select", SelectAction::Begin { position, add: cmd }); + cx.dispatch_action("buffer:select", SelectPhase::Begin { position, add: cmd }); true } else { false @@ -64,7 +64,7 @@ impl EditorElement { fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool { if self.view(cx.app.as_ref()).is_selecting() { - cx.dispatch_action("buffer:select", SelectAction::End); + cx.dispatch_action("buffer:select", SelectPhase::End); true } else { false @@ -115,7 +115,7 @@ impl EditorElement { cx.dispatch_action( "buffer:select", - SelectAction::Update { + SelectPhase::Update { position, scroll_position: (snapshot.scroll_position() + scroll_delta).clamp( Vector2F::zero(), From 638b533fc7ab70ed1ec667fcc945a3220986c2c8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 22 Aug 2021 13:29:54 -0600 Subject: [PATCH 049/204] WIP --- gpui/src/app.rs | 6 +- gpui/src/keymap.rs | 9 +- zed/src/editor.rs | 574 +++++++++++++++++++------------------- zed/src/editor/element.rs | 27 +- 4 files changed, 312 insertions(+), 304 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index f4ecdf48ff68729edc77e46bfae502ad94308641..281ad99145933507abe1df24e6d044b56a047935 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -132,7 +132,7 @@ impl AnyAction for () { #[macro_export] macro_rules! action { ($name:ident, $arg:ty) => { - #[derive(Clone, Debug, Eq, PartialEq)] + #[derive(Clone, Debug)] pub struct $name(pub $arg); impl $crate::Action for $name { @@ -3677,13 +3677,13 @@ mod tests { }); let actions_clone = actions.clone(); - cx.add_action(move |view: &mut ViewB, action: &Action, cx| { + cx.add_action(move |view: &mut ViewB, _: &Action, cx| { cx.propagate_action(); actions_clone.borrow_mut().push(format!("{} c", view.id)); }); let actions_clone = actions.clone(); - cx.add_action(move |view: &mut ViewB, action: &Action, cx| { + cx.add_action(move |view: &mut ViewB, _: &Action, cx| { cx.propagate_action(); actions_clone.borrow_mut().push(format!("{} d", view.id)); }); diff --git a/gpui/src/keymap.rs b/gpui/src/keymap.rs index 38a32e5e8ce88636a38ed2e47b5f0e83f1fdf595..da4bfa1ced67338fa06766e7259ae6c2983e9242 100644 --- a/gpui/src/keymap.rs +++ b/gpui/src/keymap.rs @@ -419,6 +419,13 @@ mod tests { action!(B); action!(Ab); + impl PartialEq for A { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } + } + impl Eq for A {} + #[derive(Clone, Debug, Eq, PartialEq)] struct ActionArg { a: &'static str, @@ -465,7 +472,7 @@ mod tests { } impl Matcher { - fn test_keystroke( + fn test_keystroke( &mut self, keystroke: &str, view_id: usize, diff --git a/zed/src/editor.rs b/zed/src/editor.rs index f7ba4c1cb0bc7981d00166c82e2fd044091ba2de..829233b3cf74a19d4579a2ba9dfadf283ba1c7de 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -201,105 +201,61 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); cx.add_action(Editor::select); - cx.add_action("buffer:cancel", Editor::cancel); - cx.add_action("buffer:insert", Editor::insert); - cx.add_action("buffer:newline", Editor::newline); - cx.add_action("buffer:backspace", Editor::backspace); - cx.add_action("buffer:delete", Editor::delete); - cx.add_action("buffer:delete_line", Editor::delete_line); - cx.add_action( - "buffer:delete_to_previous_word_boundary", - Editor::delete_to_previous_word_boundary, - ); - cx.add_action( - "buffer:delete_to_next_word_boundary", - Editor::delete_to_next_word_boundary, - ); - cx.add_action( - "buffer:delete_to_beginning_of_line", - Editor::delete_to_beginning_of_line, - ); - cx.add_action( - "buffer:delete_to_end_of_line", - Editor::delete_to_end_of_line, - ); - cx.add_action("buffer:cut_to_end_of_line", Editor::cut_to_end_of_line); - cx.add_action("buffer:duplicate_line", Editor::duplicate_line); - cx.add_action("buffer:move_line_up", Editor::move_line_up); - cx.add_action("buffer:move_line_down", Editor::move_line_down); - cx.add_action("buffer:cut", Editor::cut); - cx.add_action("buffer:copy", Editor::copy); - cx.add_action("buffer:paste", Editor::paste); - cx.add_action("buffer:undo", Editor::undo); - cx.add_action("buffer:redo", Editor::redo); - cx.add_action("buffer:move_up", Editor::move_up); - cx.add_action("buffer:move_down", Editor::move_down); - cx.add_action("buffer:move_left", Editor::move_left); - cx.add_action("buffer:move_right", Editor::move_right); - cx.add_action( - "buffer:move_to_previous_word_boundary", - Editor::move_to_previous_word_boundary, - ); - cx.add_action( - "buffer:move_to_next_word_boundary", - Editor::move_to_next_word_boundary, - ); - cx.add_action( - "buffer:move_to_beginning_of_line", - Editor::move_to_beginning_of_line, - ); - cx.add_action("buffer:move_to_end_of_line", Editor::move_to_end_of_line); - cx.add_action("buffer:move_to_beginning", Editor::move_to_beginning); - cx.add_action("buffer:move_to_end", Editor::move_to_end); - cx.add_action("buffer:select_up", Editor::select_up); - cx.add_action("buffer:select_down", Editor::select_down); - cx.add_action("buffer:select_left", Editor::select_left); - cx.add_action("buffer:select_right", Editor::select_right); - cx.add_action( - "buffer:select_to_previous_word_boundary", - Editor::select_to_previous_word_boundary, - ); - cx.add_action( - "buffer:select_to_next_word_boundary", - Editor::select_to_next_word_boundary, - ); - cx.add_action( - "buffer:select_to_beginning_of_line", - Editor::select_to_beginning_of_line, - ); - cx.add_action( - "buffer:select_to_end_of_line", - Editor::select_to_end_of_line, - ); - cx.add_action("buffer:select_to_beginning", Editor::select_to_beginning); - cx.add_action("buffer:select_to_end", Editor::select_to_end); - cx.add_action("buffer:select_all", Editor::select_all); - cx.add_action("buffer:select_line", Editor::select_line); - cx.add_action( - "buffer:split_selection_into_lines", - Editor::split_selection_into_lines, - ); - cx.add_action("buffer:add_selection_above", Editor::add_selection_above); - cx.add_action("buffer:add_selection_below", Editor::add_selection_below); - cx.add_action( - "buffer:select_larger_syntax_node", - Editor::select_larger_syntax_node, - ); - cx.add_action( - "buffer:select_smaller_syntax_node", - Editor::select_smaller_syntax_node, - ); - cx.add_action( - "buffer:move_to_enclosing_bracket", - Editor::move_to_enclosing_bracket, - ); - cx.add_action("buffer:page_up", Editor::page_up); - cx.add_action("buffer:page_down", Editor::page_down); - cx.add_action("buffer:fold", Editor::fold); - cx.add_action("buffer:unfold", Editor::unfold); - cx.add_action("buffer:fold_selected_ranges", Editor::fold_selected_ranges); + cx.add_action(Editor::cancel); + cx.add_action(Editor::insert); + cx.add_action(Editor::newline); + cx.add_action(Editor::backspace); + cx.add_action(Editor::delete); + cx.add_action(Editor::delete_line); + cx.add_action(Editor::delete_to_previous_word_boundary); + cx.add_action(Editor::delete_to_next_word_boundary); + cx.add_action(Editor::delete_to_beginning_of_line); + cx.add_action(Editor::delete_to_end_of_line); + cx.add_action(Editor::cut_to_end_of_line); + cx.add_action(Editor::duplicate_line); + cx.add_action(Editor::move_line_up); + cx.add_action(Editor::move_line_down); + cx.add_action(Editor::cut); + cx.add_action(Editor::copy); + cx.add_action(Editor::paste); + cx.add_action(Editor::undo); + cx.add_action(Editor::redo); + cx.add_action(Editor::move_up); + cx.add_action(Editor::move_down); + cx.add_action(Editor::move_left); + cx.add_action(Editor::move_right); + cx.add_action(Editor::move_to_previous_word_boundary); + cx.add_action(Editor::move_to_next_word_boundary); + cx.add_action(Editor::move_to_beginning_of_line); + cx.add_action(Editor::move_to_end_of_line); + cx.add_action(Editor::move_to_beginning); + cx.add_action(Editor::move_to_end); + cx.add_action(Editor::select_up); + cx.add_action(Editor::select_down); + cx.add_action(Editor::select_left); + cx.add_action(Editor::select_right); + cx.add_action(Editor::select_to_previous_word_boundary); + cx.add_action(Editor::select_to_next_word_boundary); + cx.add_action(Editor::select_to_beginning_of_line); + cx.add_action(Editor::select_to_end_of_line); + cx.add_action(Editor::select_to_beginning); + cx.add_action(Editor::select_to_end); + cx.add_action(Editor::select_all); + cx.add_action(Editor::select_line); + cx.add_action(Editor::split_selection_into_lines); + cx.add_action(Editor::add_selection_above); + cx.add_action(Editor::add_selection_below); + cx.add_action(Editor::select_larger_syntax_node); + cx.add_action(Editor::select_smaller_syntax_node); + cx.add_action(Editor::move_to_enclosing_bracket); + cx.add_action(Editor::page_up); + cx.add_action(Editor::page_down); + cx.add_action(Editor::fold); + cx.add_action(Editor::unfold); + cx.add_action(Editor::fold_selected_ranges); } +#[derive(Clone, Debug)] pub enum SelectPhase { Begin { position: DisplayPoint, @@ -625,7 +581,7 @@ impl Editor { self.pending_selection.is_some() } - pub fn cancel(&mut self, _: &(), cx: &mut ViewContext) { + pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { let selections = self.selections(cx.as_ref()); if let Some(pending_selection) = self.pending_selection.take() { if selections.is_empty() { @@ -697,7 +653,7 @@ impl Editor { Ok(()) } - pub fn insert(&mut self, text: &String, cx: &mut ViewContext) { + pub fn insert(&mut self, action: &Insert, cx: &mut ViewContext) { let mut old_selections = SmallVec::<[_; 32]>::new(); { let buffer = self.buffer.read(cx); @@ -712,8 +668,8 @@ impl Editor { let mut new_selections = Vec::new(); self.buffer.update(cx, |buffer, cx| { let edit_ranges = old_selections.iter().map(|(_, range)| range.clone()); - buffer.edit(edit_ranges, text.as_str(), cx); - let text_len = text.len() as isize; + buffer.edit(edit_ranges, action.0.as_str(), cx); + let text_len = action.0.len() as isize; let mut delta = 0_isize; new_selections = old_selections .into_iter() @@ -738,15 +694,15 @@ impl Editor { self.end_transaction(cx); } - fn newline(&mut self, _: &(), cx: &mut ViewContext) { + fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { if self.single_line { cx.propagate_action(); } else { - self.insert(&"\n".into(), cx); + self.insert(&Insert("\n".into()), cx); } } - pub fn backspace(&mut self, _: &(), cx: &mut ViewContext) { + pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { self.start_transaction(cx); let mut selections = self.selections(cx.as_ref()).to_vec(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -765,11 +721,11 @@ impl Editor { } self.update_selections(selections, true, cx); - self.insert(&String::new(), cx); + self.insert(&Insert(String::new()), cx); self.end_transaction(cx); } - pub fn delete(&mut self, _: &(), cx: &mut ViewContext) { + pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { self.start_transaction(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx.as_ref()).to_vec(); @@ -788,11 +744,11 @@ impl Editor { } self.update_selections(selections, true, cx); - self.insert(&String::new(), cx); + self.insert(&Insert(String::new()), cx); self.end_transaction(cx); } - pub fn delete_line(&mut self, _: &(), cx: &mut ViewContext) { + pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { self.start_transaction(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -869,7 +825,7 @@ impl Editor { self.end_transaction(cx); } - pub fn duplicate_line(&mut self, _: &(), cx: &mut ViewContext) { + pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { self.start_transaction(cx); let mut selections = self.selections(cx.as_ref()).to_vec(); @@ -929,7 +885,7 @@ impl Editor { self.end_transaction(cx); } - pub fn move_line_up(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext) { self.start_transaction(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -1019,7 +975,7 @@ impl Editor { self.end_transaction(cx); } - pub fn move_line_down(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext) { self.start_transaction(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -1106,7 +1062,7 @@ impl Editor { self.end_transaction(cx); } - pub fn cut(&mut self, _: &(), cx: &mut ViewContext) { + pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { self.start_transaction(cx); let mut text = String::new(); let mut selections = self.selections(cx.as_ref()).to_vec(); @@ -1136,14 +1092,14 @@ impl Editor { } } self.update_selections(selections, true, cx); - self.insert(&String::new(), cx); + self.insert(&Insert(String::new()), cx); self.end_transaction(cx); cx.as_mut() .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); } - pub fn copy(&mut self, _: &(), cx: &mut ViewContext) { + pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { let buffer = self.buffer.read(cx); let max_point = buffer.max_point(); let mut text = String::new(); @@ -1172,7 +1128,7 @@ impl Editor { .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); } - pub fn paste(&mut self, _: &(), cx: &mut ViewContext) { + pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { if let Some(item) = cx.as_mut().read_from_clipboard() { let clipboard_text = item.text(); if let Some(mut clipboard_selections) = item.metadata::>() { @@ -1224,20 +1180,20 @@ impl Editor { self.update_selections(new_selections, true, cx); self.end_transaction(cx); } else { - self.insert(clipboard_text, cx); + self.insert(&Insert(clipboard_text.into()), cx); } } } - pub fn undo(&mut self, _: &(), cx: &mut ViewContext) { + pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { self.buffer.update(cx, |buffer, cx| buffer.undo(cx)); } - pub fn redo(&mut self, _: &(), cx: &mut ViewContext) { + pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { self.buffer.update(cx, |buffer, cx| buffer.redo(cx)); } - pub fn move_left(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let app = cx.as_ref(); let mut selections = self.selections(app).to_vec(); @@ -1261,7 +1217,7 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn select_left(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx.as_ref()).to_vec(); { @@ -1277,7 +1233,7 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn move_right(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx.as_ref()).to_vec(); { @@ -1300,7 +1256,7 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn select_right(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx.as_ref()).to_vec(); { @@ -1317,7 +1273,7 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn move_up(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); if self.single_line { cx.propagate_action(); @@ -1343,7 +1299,7 @@ impl Editor { } } - pub fn select_up(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx.as_ref()).to_vec(); { @@ -1359,7 +1315,7 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn move_down(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { if self.single_line { cx.propagate_action(); } else { @@ -1385,7 +1341,7 @@ impl Editor { } } - pub fn select_down(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx).to_vec(); { @@ -1401,7 +1357,11 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn move_to_previous_word_boundary(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_to_previous_word_boundary( + &mut self, + _: &MoveToPreviousWordBoundary, + cx: &mut ViewContext, + ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx).to_vec(); { @@ -1418,7 +1378,11 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn select_to_previous_word_boundary(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_to_previous_word_boundary( + &mut self, + _: &SelectToPreviousWordBoundary, + cx: &mut ViewContext, + ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx).to_vec(); { @@ -1434,14 +1398,22 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn delete_to_previous_word_boundary(&mut self, _: &(), cx: &mut ViewContext) { + pub fn delete_to_previous_word_boundary( + &mut self, + _: &DeleteToPreviousWordBoundary, + cx: &mut ViewContext, + ) { self.start_transaction(cx); - self.select_to_previous_word_boundary(&(), cx); - self.backspace(&(), cx); + self.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); + self.backspace(&Backspace, cx); self.end_transaction(cx); } - pub fn move_to_next_word_boundary(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_to_next_word_boundary( + &mut self, + _: &MoveToNextWordBoundary, + cx: &mut ViewContext, + ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx).to_vec(); { @@ -1458,7 +1430,11 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn select_to_next_word_boundary(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_to_next_word_boundary( + &mut self, + _: &SelectToNextWordBoundary, + cx: &mut ViewContext, + ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx).to_vec(); { @@ -1474,14 +1450,22 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn delete_to_next_word_boundary(&mut self, _: &(), cx: &mut ViewContext) { + pub fn delete_to_next_word_boundary( + &mut self, + _: &DeleteToNextWordBoundary, + cx: &mut ViewContext, + ) { self.start_transaction(cx); - self.select_to_next_word_boundary(&(), cx); - self.delete(&(), cx); + self.select_to_next_word_boundary(&SelectToNextWordBoundary, cx); + self.delete(&Delete, cx); self.end_transaction(cx); } - pub fn move_to_beginning_of_line(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_to_beginning_of_line( + &mut self, + _: &MoveToBeginningOfLine, + cx: &mut ViewContext, + ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx).to_vec(); { @@ -1500,7 +1484,7 @@ impl Editor { pub fn select_to_beginning_of_line( &mut self, - toggle_indent: &bool, + SelectToBeginningOfLine(toggle_indent): &SelectToBeginningOfLine, cx: &mut ViewContext, ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -1519,14 +1503,18 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn delete_to_beginning_of_line(&mut self, _: &(), cx: &mut ViewContext) { + pub fn delete_to_beginning_of_line( + &mut self, + _: &DeleteToBeginningOfLine, + cx: &mut ViewContext, + ) { self.start_transaction(cx); - self.select_to_beginning_of_line(&false, cx); - self.backspace(&(), cx); + self.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx); + self.backspace(&Backspace, cx); self.end_transaction(cx); } - pub fn move_to_end_of_line(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx).to_vec(); { @@ -1543,7 +1531,7 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn select_to_end_of_line(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_to_end_of_line(&mut self, _: &SelectToEndOfLine, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections(cx).to_vec(); { @@ -1559,21 +1547,21 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn delete_to_end_of_line(&mut self, _: &(), cx: &mut ViewContext) { + pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext) { self.start_transaction(cx); - self.select_to_end_of_line(&(), cx); - self.delete(&(), cx); + self.select_to_end_of_line(&SelectToEndOfLine, cx); + self.delete(&Delete, cx); self.end_transaction(cx); } - pub fn cut_to_end_of_line(&mut self, _: &(), cx: &mut ViewContext) { + pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext) { self.start_transaction(cx); - self.select_to_end_of_line(&(), cx); - self.cut(&(), cx); + self.select_to_end_of_line(&SelectToEndOfLine, cx); + self.cut(&Cut, cx); self.end_transaction(cx); } - pub fn move_to_beginning(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { let buffer = self.buffer.read(cx); let cursor = buffer.anchor_before(Point::new(0, 0)); let selection = Selection { @@ -1586,13 +1574,13 @@ impl Editor { self.update_selections(vec![selection], true, cx); } - pub fn select_to_beginning(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext) { let mut selection = self.selections(cx.as_ref()).last().unwrap().clone(); selection.set_head(self.buffer.read(cx), Anchor::min()); self.update_selections(vec![selection], true, cx); } - pub fn move_to_end(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext) { let buffer = self.buffer.read(cx); let cursor = buffer.anchor_before(buffer.max_point()); let selection = Selection { @@ -1605,13 +1593,13 @@ impl Editor { self.update_selections(vec![selection], true, cx); } - pub fn select_to_end(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { let mut selection = self.selections(cx.as_ref()).last().unwrap().clone(); selection.set_head(self.buffer.read(cx), Anchor::max()); self.update_selections(vec![selection], true, cx); } - pub fn select_all(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext) { let selection = Selection { id: post_inc(&mut self.next_selection_id), start: Anchor::min(), @@ -1622,7 +1610,7 @@ impl Editor { self.update_selections(vec![selection], false, cx); } - pub fn select_line(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_line(&mut self, _: &SelectLine, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx); let mut selections = self.selections(cx).to_vec(); @@ -1636,7 +1624,11 @@ impl Editor { self.update_selections(selections, true, cx); } - pub fn split_selection_into_lines(&mut self, _: &(), cx: &mut ViewContext) { + pub fn split_selection_into_lines( + &mut self, + _: &SplitSelectionIntoLines, + cx: &mut ViewContext, + ) { let app = cx.as_ref(); let buffer = self.buffer.read(app); @@ -1676,11 +1668,11 @@ impl Editor { self.update_selections(new_selections, true, cx); } - pub fn add_selection_above(&mut self, _: &(), cx: &mut ViewContext) { + pub fn add_selection_above(&mut self, _: &AddSelectionAbove, cx: &mut ViewContext) { self.add_selection(true, cx); } - pub fn add_selection_below(&mut self, _: &(), cx: &mut ViewContext) { + pub fn add_selection_below(&mut self, _: &AddSelectionBelow, cx: &mut ViewContext) { self.add_selection(false, cx); } @@ -1777,7 +1769,11 @@ impl Editor { } } - pub fn select_larger_syntax_node(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_larger_syntax_node( + &mut self, + _: &SelectLargerSyntaxNode, + cx: &mut ViewContext, + ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx); @@ -1819,7 +1815,11 @@ impl Editor { self.select_larger_syntax_node_stack = stack; } - pub fn select_smaller_syntax_node(&mut self, _: &(), cx: &mut ViewContext) { + pub fn select_smaller_syntax_node( + &mut self, + _: &SelectSmallerSyntaxNode, + cx: &mut ViewContext, + ) { let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); if let Some(selections) = stack.pop() { self.update_selections(selections, true, cx); @@ -1827,7 +1827,11 @@ impl Editor { self.select_larger_syntax_node_stack = stack; } - pub fn move_to_enclosing_bracket(&mut self, _: &(), cx: &mut ViewContext) { + pub fn move_to_enclosing_bracket( + &mut self, + _: &MoveToEnclosingBracket, + cx: &mut ViewContext, + ) { let buffer = self.buffer.read(cx.as_ref()); let mut selections = self.selections(cx.as_ref()).to_vec(); for selection in &mut selections { @@ -2008,15 +2012,15 @@ impl Editor { }); } - pub fn page_up(&mut self, _: &(), _: &mut ViewContext) { + pub fn page_up(&mut self, _: &PageUp, _: &mut ViewContext) { log::info!("BufferView::page_up"); } - pub fn page_down(&mut self, _: &(), _: &mut ViewContext) { + pub fn page_down(&mut self, _: &PageDown, _: &mut ViewContext) { log::info!("BufferView::page_down"); } - pub fn fold(&mut self, _: &(), cx: &mut ViewContext) { + pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext) { let mut fold_ranges = Vec::new(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -2040,7 +2044,7 @@ impl Editor { self.fold_ranges(fold_ranges, cx); } - pub fn unfold(&mut self, _: &(), cx: &mut ViewContext) { + pub fn unfold(&mut self, _: &Unfold, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx); let ranges = self @@ -2101,7 +2105,7 @@ impl Editor { ..end.to_buffer_point(display_map, Bias::Left); } - pub fn fold_selected_ranges(&mut self, _: &(), cx: &mut ViewContext) { + pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { let buffer = self.buffer.read(cx); let ranges = self .selections(cx.as_ref()) @@ -2691,7 +2695,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.cancel(&(), cx); + view.cancel(&Cancel, cx); view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), cx); assert_eq!( view.selection_ranges(cx), @@ -2726,7 +2730,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.cancel(&(), cx); + view.cancel(&Cancel, cx); assert_eq!( view.selection_ranges(cx), [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] @@ -2734,7 +2738,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.cancel(&(), cx); + view.cancel(&Cancel, cx); assert_eq!( view.selection_ranges(cx), [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] @@ -2803,7 +2807,7 @@ mod tests { view.update(cx, |view, cx| { view.select_display_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], cx) .unwrap(); - view.fold(&(), cx); + view.fold(&Fold, cx); assert_eq!( view.text(cx), " @@ -2824,7 +2828,7 @@ mod tests { .unindent(), ); - view.fold(&(), cx); + view.fold(&Fold, cx); assert_eq!( view.text(cx), " @@ -2834,7 +2838,7 @@ mod tests { .unindent(), ); - view.unfold(&(), cx); + view.unfold(&Unfold, cx); assert_eq!( view.text(cx), " @@ -2855,7 +2859,7 @@ mod tests { .unindent(), ); - view.unfold(&(), cx); + view.unfold(&Unfold, cx); assert_eq!(view.text(cx), buffer.read(cx).text()); }); } @@ -2885,37 +2889,37 @@ mod tests { &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] ); - view.move_down(&(), cx); + view.move_down(&MoveDown, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] ); - view.move_right(&(), cx); + view.move_right(&MoveRight, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] ); - view.move_left(&(), cx); + view.move_left(&MoveLeft, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] ); - view.move_up(&(), cx); + view.move_up(&MoveUp, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] ); - view.move_to_end(&(), cx); + view.move_to_end(&MoveToEnd, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] ); - view.move_to_beginning(&(), cx); + view.move_to_beginning(&MoveToBeginning, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] @@ -2923,13 +2927,13 @@ mod tests { view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], cx) .unwrap(); - view.select_to_beginning(&(), cx); + view.select_to_beginning(&SelectToBeginning, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] ); - view.select_to_end(&(), cx); + view.select_to_end(&SelectToEnd, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] @@ -2959,38 +2963,38 @@ mod tests { ); assert_eq!(view.text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\n"); - view.move_right(&(), cx); + view.move_right(&MoveRight, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(0, "ⓐ".len())]); - view.move_right(&(), cx); + view.move_right(&MoveRight, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(0, "ⓐⓑ".len())]); - view.move_right(&(), cx); + view.move_right(&MoveRight, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(0, "ⓐⓑ…".len())]); - view.move_down(&(), cx); + view.move_down(&MoveDown, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(1, "ab…".len())]); - view.move_left(&(), cx); + view.move_left(&MoveLeft, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(1, "ab".len())]); - view.move_left(&(), cx); + view.move_left(&MoveLeft, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(1, "a".len())]); - view.move_down(&(), cx); + view.move_down(&MoveDown, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(2, "α".len())]); - view.move_right(&(), cx); + view.move_right(&MoveRight, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(2, "αβ".len())]); - view.move_right(&(), cx); + view.move_right(&MoveRight, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(2, "αβ…".len())]); - view.move_right(&(), cx); + view.move_right(&MoveRight, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(2, "αβ…ε".len())]); - view.move_up(&(), cx); + view.move_up(&MoveUp, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(1, "ab…e".len())]); - view.move_up(&(), cx); + view.move_up(&MoveUp, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(0, "ⓐⓑ…ⓔ".len())]); - view.move_left(&(), cx); + view.move_left(&MoveLeft, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(0, "ⓐⓑ…".len())]); - view.move_left(&(), cx); + view.move_left(&MoveLeft, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(0, "ⓐⓑ".len())]); - view.move_left(&(), cx); + view.move_left(&MoveLeft, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(0, "ⓐ".len())]); }); } @@ -3006,22 +3010,22 @@ mod tests { view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx) .unwrap(); - view.move_down(&(), cx); + view.move_down(&MoveDown, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(1, "abcd".len())]); - view.move_down(&(), cx); + view.move_down(&MoveDown, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(2, "αβγ".len())]); - view.move_down(&(), cx); + view.move_down(&MoveDown, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(3, "abcd".len())]); - view.move_down(&(), cx); + view.move_down(&MoveDown, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]); - view.move_up(&(), cx); + view.move_up(&MoveUp, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(3, "abcd".len())]); - view.move_up(&(), cx); + view.move_up(&MoveUp, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(2, "αβγ".len())]); }); } @@ -3045,7 +3049,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&(), cx); + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3056,7 +3060,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&(), cx); + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3067,7 +3071,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&(), cx); + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3078,7 +3082,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_end_of_line(&(), cx); + view.move_to_end_of_line(&MoveToEndOfLine, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3090,7 +3094,7 @@ mod tests { // Moving to the end of line again is a no-op. view.update(cx, |view, cx| { - view.move_to_end_of_line(&(), cx); + view.move_to_end_of_line(&MoveToEndOfLine, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3101,8 +3105,8 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_left(&(), cx); - view.select_to_beginning_of_line(&true, cx); + view.move_left(&MoveLeft, cx); + view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3113,7 +3117,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.select_to_beginning_of_line(&true, cx); + view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3124,7 +3128,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.select_to_beginning_of_line(&true, cx); + view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3135,7 +3139,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.select_to_end_of_line(&(), cx); + view.select_to_end_of_line(&SelectToEndOfLine, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3146,7 +3150,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.delete_to_end_of_line(&(), cx); + view.delete_to_end_of_line(&DeleteToEndOfLine, cx); assert_eq!(view.text(cx), "ab\n de"); assert_eq!( view.selection_ranges(cx), @@ -3158,7 +3162,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.delete_to_beginning_of_line(&(), cx); + view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); assert_eq!(view.text(cx), "\n"); assert_eq!( view.selection_ranges(cx), @@ -3190,7 +3194,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&(), cx); + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3201,7 +3205,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&(), cx); + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3212,7 +3216,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&(), cx); + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3223,7 +3227,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&(), cx); + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3234,7 +3238,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&(), cx); + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3245,7 +3249,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&(), cx); + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3256,7 +3260,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&(), cx); + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3267,7 +3271,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&(), cx); + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3278,7 +3282,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&(), cx); + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3289,7 +3293,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&(), cx); + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3300,8 +3304,8 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_right(&(), cx); - view.select_to_previous_word_boundary(&(), cx); + view.move_right(&MoveRight, cx); + view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3312,7 +3316,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.select_to_previous_word_boundary(&(), cx); + view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3323,7 +3327,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.select_to_next_word_boundary(&(), cx); + view.select_to_next_word_boundary(&SelectToNextWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[ @@ -3334,7 +3338,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.delete_to_next_word_boundary(&(), cx); + view.delete_to_next_word_boundary(&DeleteToNextWordBoundary, cx); assert_eq!(view.text(cx), "use std::s::{foo, bar}\n\n {az.qux()}"); assert_eq!( view.selection_ranges(cx), @@ -3346,7 +3350,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.delete_to_previous_word_boundary(&(), cx); + view.delete_to_previous_word_boundary(&DeleteToPreviousWordBoundary, cx); assert_eq!(view.text(cx), "use std::::{foo, bar}\n\n az.qux()}"); assert_eq!( view.selection_ranges(cx), @@ -3377,37 +3381,37 @@ mod tests { view.select_display_ranges(&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)], cx) .unwrap(); - view.move_to_next_word_boundary(&(), cx); + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] ); - view.move_to_next_word_boundary(&(), cx); + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] ); - view.move_to_next_word_boundary(&(), cx); + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] ); - view.move_to_next_word_boundary(&(), cx); + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] ); - view.move_to_previous_word_boundary(&(), cx); + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] ); - view.move_to_previous_word_boundary(&(), cx); + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(1, 15)..DisplayPoint::new(1, 15)] @@ -3442,7 +3446,7 @@ mod tests { cx, ) .unwrap(); - view.backspace(&(), cx); + view.backspace(&Backspace, cx); }); assert_eq!( @@ -3478,7 +3482,7 @@ mod tests { cx, ) .unwrap(); - view.delete(&(), cx); + view.delete(&Delete, cx); }); assert_eq!( @@ -3504,7 +3508,7 @@ mod tests { cx, ) .unwrap(); - view.delete_line(&(), cx); + view.delete_line(&DeleteLine, cx); assert_eq!(view.text(cx), "ghi"); assert_eq!( view.selection_ranges(cx), @@ -3523,7 +3527,7 @@ mod tests { view.update(cx, |view, cx| { view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx) .unwrap(); - view.delete_line(&(), cx); + view.delete_line(&DeleteLine, cx); assert_eq!(view.text(cx), "ghi\n"); assert_eq!( view.selection_ranges(cx), @@ -3550,7 +3554,7 @@ mod tests { cx, ) .unwrap(); - view.duplicate_line(&(), cx); + view.duplicate_line(&DuplicateLine, cx); assert_eq!(view.text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); assert_eq!( view.selection_ranges(cx), @@ -3577,7 +3581,7 @@ mod tests { cx, ) .unwrap(); - view.duplicate_line(&(), cx); + view.duplicate_line(&DuplicateLine, cx); assert_eq!(view.text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); assert_eq!( view.selection_ranges(cx), @@ -3617,7 +3621,7 @@ mod tests { .unwrap(); assert_eq!(view.text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj"); - view.move_line_up(&(), cx); + view.move_line_up(&MoveLineUp, cx); assert_eq!(view.text(cx), "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff"); assert_eq!( view.selection_ranges(cx), @@ -3631,7 +3635,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_line_down(&(), cx); + view.move_line_down(&MoveLineDown, cx); assert_eq!(view.text(cx), "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj"); assert_eq!( view.selection_ranges(cx), @@ -3645,7 +3649,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_line_down(&(), cx); + view.move_line_down(&MoveLineDown, cx); assert_eq!(view.text(cx), "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj"); assert_eq!( view.selection_ranges(cx), @@ -3659,7 +3663,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.move_line_up(&(), cx); + view.move_line_up(&MoveLineUp, cx); assert_eq!(view.text(cx), "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff"); assert_eq!( view.selection_ranges(cx), @@ -3686,14 +3690,14 @@ mod tests { // Cut with three selections. Clipboard text is divided into three slices. view.update(cx, |view, cx| { view.select_ranges(vec![0..4, 8..14, 19..24], false, cx); - view.cut(&(), cx); + view.cut(&Cut, cx); assert_eq!(view.text(cx), "two four six "); }); // Paste with three cursors. Each cursor pastes one slice of the clipboard text. view.update(cx, |view, cx| { view.select_ranges(vec![4..4, 9..9, 13..13], false, cx); - view.paste(&(), cx); + view.paste(&Paste, cx); assert_eq!(view.text(cx), "two one four three six five "); assert_eq!( view.selection_ranges(cx), @@ -3710,9 +3714,9 @@ mod tests { // is pasted at each cursor. view.update(cx, |view, cx| { view.select_ranges(vec![0..0, 28..28], false, cx); - view.insert(&"( ".to_string(), cx); - view.paste(&(), cx); - view.insert(&") ".to_string(), cx); + view.insert(&Insert("( ".into()), cx); + view.paste(&Paste, cx); + view.insert(&Insert(") ".into()), cx); assert_eq!( view.text(cx), "( one three five ) two one four three six five ( one three five ) " @@ -3721,7 +3725,7 @@ mod tests { view.update(cx, |view, cx| { view.select_ranges(vec![0..0], false, cx); - view.insert(&"123\n4567\n89\n".to_string(), cx); + view.insert(&Insert("123\n4567\n89\n".into()), cx); assert_eq!( view.text(cx), "123\n4567\n89\n( one three five ) two one four three six five ( one three five ) " @@ -3739,7 +3743,7 @@ mod tests { cx, ) .unwrap(); - view.cut(&(), cx); + view.cut(&Cut, cx); assert_eq!( view.text(cx), "13\n9\n( one three five ) two one four three six five ( one three five ) " @@ -3758,7 +3762,7 @@ mod tests { cx, ) .unwrap(); - view.paste(&(), cx); + view.paste(&Paste, cx); assert_eq!( view.text(cx), "123\n4567\n9\n( 8ne three five ) two one four three six five ( one three five ) " @@ -3777,7 +3781,7 @@ mod tests { view.update(cx, |view, cx| { view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], cx) .unwrap(); - view.copy(&(), cx); + view.copy(&Copy, cx); }); // Paste with three selections, noticing how the copied full-line selection is inserted @@ -3792,7 +3796,7 @@ mod tests { cx, ) .unwrap(); - view.paste(&(), cx); + view.paste(&Paste, cx); assert_eq!( view.text(cx), "123\n123\n123\n67\n123\n9\n( 8ne three five ) two one four three six five ( one three five ) " @@ -3816,7 +3820,7 @@ mod tests { Editor::for_buffer(buffer, settings, cx) }); view.update(cx, |view, cx| { - view.select_all(&(), cx); + view.select_all(&SelectAll, cx); assert_eq!( view.selection_ranges(cx), &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] @@ -3842,7 +3846,7 @@ mod tests { cx, ) .unwrap(); - view.select_line(&(), cx); + view.select_line(&SelectLine, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -3853,7 +3857,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.select_line(&(), cx); + view.select_line(&SelectLine, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -3864,7 +3868,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.select_line(&(), cx); + view.select_line(&SelectLine, cx); assert_eq!( view.selection_ranges(cx), vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] @@ -3902,7 +3906,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.split_selection_into_lines(&(), cx); + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); assert_eq!(view.text(cx), "aaaaa\nbbbbb\nccc…eeee\nfffff\nggggg\n…i"); assert_eq!( view.selection_ranges(cx), @@ -3918,7 +3922,7 @@ mod tests { view.update(cx, |view, cx| { view.select_display_ranges(&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)], cx) .unwrap(); - view.split_selection_into_lines(&(), cx); + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); assert_eq!( view.text(cx), "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" @@ -3952,7 +3956,7 @@ mod tests { .unwrap(); }); view.update(cx, |view, cx| { - view.add_selection_above(&(), cx); + view.add_selection_above(&AddSelectionAbove, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -3963,7 +3967,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.add_selection_above(&(), cx); + view.add_selection_above(&AddSelectionAbove, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -3974,7 +3978,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.add_selection_below(&(), cx); + view.add_selection_below(&AddSelectionBelow, cx); assert_eq!( view.selection_ranges(cx), vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] @@ -3982,7 +3986,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.add_selection_below(&(), cx); + view.add_selection_below(&AddSelectionBelow, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -3993,7 +3997,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.add_selection_below(&(), cx); + view.add_selection_below(&AddSelectionBelow, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -4008,7 +4012,7 @@ mod tests { .unwrap(); }); view.update(cx, |view, cx| { - view.add_selection_below(&(), cx); + view.add_selection_below(&AddSelectionBelow, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -4019,7 +4023,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.add_selection_below(&(), cx); + view.add_selection_below(&AddSelectionBelow, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -4030,7 +4034,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.add_selection_above(&(), cx); + view.add_selection_above(&AddSelectionAbove, cx); assert_eq!( view.selection_ranges(cx), vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] @@ -4038,7 +4042,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.add_selection_above(&(), cx); + view.add_selection_above(&AddSelectionAbove, cx); assert_eq!( view.selection_ranges(cx), vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] @@ -4048,7 +4052,7 @@ mod tests { view.update(cx, |view, cx| { view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)], cx) .unwrap(); - view.add_selection_below(&(), cx); + view.add_selection_below(&AddSelectionBelow, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -4060,7 +4064,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.add_selection_below(&(), cx); + view.add_selection_below(&AddSelectionBelow, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -4073,7 +4077,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.add_selection_above(&(), cx); + view.add_selection_above(&AddSelectionAbove, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -4089,7 +4093,7 @@ mod tests { .unwrap(); }); view.update(cx, |view, cx| { - view.add_selection_above(&(), cx); + view.add_selection_above(&AddSelectionAbove, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -4102,7 +4106,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.add_selection_below(&(), cx); + view.add_selection_below(&AddSelectionBelow, cx); assert_eq!( view.selection_ranges(cx), vec![ @@ -4145,7 +4149,7 @@ mod tests { cx, ) .unwrap(); - view.select_larger_syntax_node(&(), cx); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( view.update(&mut cx, |view, cx| view.selection_ranges(cx)), @@ -4157,7 +4161,7 @@ mod tests { ); view.update(&mut cx, |view, cx| { - view.select_larger_syntax_node(&(), cx); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( view.update(&mut cx, |view, cx| view.selection_ranges(cx)), @@ -4168,7 +4172,7 @@ mod tests { ); view.update(&mut cx, |view, cx| { - view.select_larger_syntax_node(&(), cx); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( view.update(&mut cx, |view, cx| view.selection_ranges(cx)), @@ -4177,7 +4181,7 @@ mod tests { // Trying to expand the selected syntax node one more time has no effect. view.update(&mut cx, |view, cx| { - view.select_larger_syntax_node(&(), cx); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( view.update(&mut cx, |view, cx| view.selection_ranges(cx)), @@ -4185,7 +4189,7 @@ mod tests { ); view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&(), cx); + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( view.update(&mut cx, |view, cx| view.selection_ranges(cx)), @@ -4196,7 +4200,7 @@ mod tests { ); view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&(), cx); + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( view.update(&mut cx, |view, cx| view.selection_ranges(cx)), @@ -4208,7 +4212,7 @@ mod tests { ); view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&(), cx); + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( view.update(&mut cx, |view, cx| view.selection_ranges(cx)), @@ -4221,7 +4225,7 @@ mod tests { // Trying to shrink the selected syntax node one more time has no effect. view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&(), cx); + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( view.update(&mut cx, |view, cx| view.selection_ranges(cx)), @@ -4242,7 +4246,7 @@ mod tests { ], cx, ); - view.select_larger_syntax_node(&(), cx); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( view.update(&mut cx, |view, cx| view.selection_ranges(cx)), diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index a3da9363690e3dd8eceef8ad459c8d1f5ec438b6..812bd795a7d0f6c96de59cc0b69ddc374a91e89a 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -1,4 +1,4 @@ -use super::{DisplayPoint, Editor, SelectPhase, Snapshot}; +use super::{DisplayPoint, Editor, Select, SelectPhase, Snapshot, Insert, Scroll}; use crate::time::ReplicaId; use gpui::{ color::Color, @@ -55,7 +55,7 @@ impl EditorElement { if paint.text_bounds.contains_point(position) { let snapshot = self.snapshot(cx.app); let position = paint.point_for_position(&snapshot, layout, position); - cx.dispatch_action("buffer:select", SelectPhase::Begin { position, add: cmd }); + cx.dispatch_action(Select(SelectPhase::Begin { position, add: cmd })); true } else { false @@ -64,7 +64,7 @@ impl EditorElement { fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool { if self.view(cx.app.as_ref()).is_selecting() { - cx.dispatch_action("buffer:select", SelectPhase::End); + cx.dispatch_action(Select(SelectPhase::End)); true } else { false @@ -113,16 +113,13 @@ impl EditorElement { let snapshot = self.snapshot(cx.app); let position = paint.point_for_position(&snapshot, layout, position); - cx.dispatch_action( - "buffer:select", - SelectPhase::Update { - position, - scroll_position: (snapshot.scroll_position() + scroll_delta).clamp( - Vector2F::zero(), - layout.scroll_max(&font_cache, &text_layout_cache), - ), - }, - ); + cx.dispatch_action(Select(SelectPhase::Update { + position, + scroll_position: (snapshot.scroll_position() + scroll_delta).clamp( + Vector2F::zero(), + layout.scroll_max(&font_cache, &text_layout_cache), + ), + })); true } else { false @@ -139,7 +136,7 @@ impl EditorElement { if chars.chars().any(|c| c.is_control()) { false } else { - cx.dispatch_action("buffer:insert", chars.to_string()); + cx.dispatch_action(Insert(chars.to_string())); true } } @@ -177,7 +174,7 @@ impl EditorElement { layout.scroll_max(font_cache, layout_cache), ); - cx.dispatch_action("buffer:scroll", scroll_position); + cx.dispatch_action(Scroll(scroll_position)); true } From 86effd64a2e2fd1e9c13663bf44c40c85aedc117 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 22 Aug 2021 21:02:48 -0600 Subject: [PATCH 050/204] Get project compiling with type-safe actions --- gpui/src/app.rs | 2 +- gpui/src/keymap.rs | 18 ++-- gpui/src/platform/mac/platform.rs | 1 - server/src/rpc.rs | 4 +- zed/src/file_finder.rs | 85 +++++++++--------- zed/src/lib.rs | 8 +- zed/src/main.rs | 13 ++- zed/src/menus.rs | 37 +++----- zed/src/theme_selector.rs | 54 ++++++------ zed/src/workspace.rs | 138 +++++++++++++++++------------- zed/src/workspace/pane.rs | 58 ++++++------- zed/src/workspace/pane_group.rs | 2 +- zed/src/workspace/sidebar.rs | 15 +++- 13 files changed, 229 insertions(+), 206 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 281ad99145933507abe1df24e6d044b56a047935..093f5c7d4d6b834b72a30c33edd3328698bed442 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -132,7 +132,7 @@ impl AnyAction for () { #[macro_export] macro_rules! action { ($name:ident, $arg:ty) => { - #[derive(Clone, Debug)] + #[derive(Clone)] pub struct $name(pub $arg); impl $crate::Action for $name { diff --git a/gpui/src/keymap.rs b/gpui/src/keymap.rs index da4bfa1ced67338fa06766e7259ae6c2983e9242..409366419394ba07be9230b70670b40be9d79157 100644 --- a/gpui/src/keymap.rs +++ b/gpui/src/keymap.rs @@ -2,6 +2,7 @@ use anyhow::anyhow; use std::{ any::Any, collections::{HashMap, HashSet}, + fmt::Debug, }; use tree_sitter::{Language, Node, Parser}; @@ -148,7 +149,7 @@ impl Keymap { } } -mod menu { +pub mod menu { use crate::action; action!(SelectPrev); @@ -425,6 +426,11 @@ mod tests { } } impl Eq for A {} + impl Debug for A { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "A({:?})", &self.0) + } + } #[derive(Clone, Debug, Eq, PartialEq)] struct ActionArg { @@ -472,12 +478,10 @@ mod tests { } impl Matcher { - fn test_keystroke( - &mut self, - keystroke: &str, - view_id: usize, - cx: &Context, - ) -> Option { + fn test_keystroke(&mut self, keystroke: &str, view_id: usize, cx: &Context) -> Option + where + A: Action + Debug + Eq, + { if let MatchResult::Action(action) = self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, cx) { diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index 1b4331bedce42de4b645bac85f83783dcedc5782..6593f9e3efa27db8dcd63fb59ea19601040f6ab6 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -29,7 +29,6 @@ use objc::{ }; use ptr::null_mut; use std::{ - any::Any, cell::{Cell, RefCell}, convert::TryInto, ffi::{c_void, CStr}, diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 9742ec015f8fa2368397fd26471fd627655d9c1e..5b907df01ec9337684723bb82e83b9d76e3c32d0 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -934,7 +934,7 @@ mod tests { use std::{path::Path, sync::Arc, time::Duration}; use zed::{ channel::{Channel, ChannelDetails, ChannelList}, - editor::Editor, + editor::{Editor, Insert}, fs::{FakeFs, Fs as _}, language::LanguageRegistry, rpc::Client, @@ -1023,7 +1023,7 @@ mod tests { // Edit the buffer as client B and see that edit as client A. editor_b.update(&mut cx_b, |editor, cx| { - editor.insert(&"ok, ".to_string(), cx) + editor.insert(&Insert("ok, ".into()), cx) }); buffer_a .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents") diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index d370f85a081d55155996dd891ba5af47b4a0d480..4d4cf13013d3fcc89fc5bc4a16cd658bee186ea4 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -6,8 +6,13 @@ use crate::{ worktree::{match_paths, PathMatch}, }; use gpui::{ + action, elements::*, - keymap::{self, Binding}, + keymap::{ + self, + menu::{SelectNext, SelectPrev}, + Binding, + }, AppContext, Axis, Entity, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; @@ -36,17 +41,27 @@ pub struct FileFinder { list_state: UniformListState, } +action!(Toggle); +action!(Confirm); +action!(Select, Entry); + +#[derive(Clone)] +pub struct Entry { + worktree_id: usize, + path: Arc, +} + pub fn init(cx: &mut MutableAppContext) { - cx.add_action("file_finder:toggle", FileFinder::toggle); - cx.add_action("file_finder:confirm", FileFinder::confirm); - cx.add_action("file_finder:select", FileFinder::select); - cx.add_action("menu:select_prev", FileFinder::select_prev); - cx.add_action("menu:select_next", FileFinder::select_next); + cx.add_action(FileFinder::toggle); + cx.add_action(FileFinder::confirm); + cx.add_action(FileFinder::select); + cx.add_action(FileFinder::select_prev); + cx.add_action(FileFinder::select_next); cx.add_bindings(vec![ - Binding::new("cmd-p", "file_finder:toggle", None), - Binding::new("escape", "file_finder:toggle", Some("FileFinder")), - Binding::new("enter", "file_finder:confirm", Some("FileFinder")), + Binding::new("cmd-p", Toggle, None), + Binding::new("escape", Toggle, Some("FileFinder")), + Binding::new("enter", Confirm, Some("FileFinder")), ]); } @@ -196,10 +211,13 @@ impl FileFinder { ) .with_style(&style.container); - let entry = (path_match.tree_id, path_match.path.clone()); + let action = Select(Entry { + worktree_id: path_match.tree_id, + path: path_match.path.clone(), + }); EventHandler::new(container.boxed()) .on_mouse_down(move |cx| { - cx.dispatch_action("file_finder:select", entry.clone()); + cx.dispatch_action(action.clone()); true }) .named("match") @@ -230,7 +248,7 @@ impl FileFinder { (file_name, file_name_positions, full_path, path_positions) } - fn toggle(workspace: &mut Workspace, _: &(), cx: &mut ViewContext) { + fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { workspace.toggle_modal(cx, |cx, workspace| { let handle = cx.handle(); let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx)); @@ -328,7 +346,7 @@ impl FileFinder { 0 } - fn select_prev(&mut self, _: &(), cx: &mut ViewContext) { + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { let mut selected_index = self.selected_index(); if selected_index > 0 { selected_index -= 1; @@ -339,7 +357,7 @@ impl FileFinder { cx.notify(); } - fn select_next(&mut self, _: &(), cx: &mut ViewContext) { + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { let mut selected_index = self.selected_index(); if selected_index + 1 < self.matches.len() { selected_index += 1; @@ -350,14 +368,14 @@ impl FileFinder { cx.notify(); } - fn confirm(&mut self, _: &(), cx: &mut ViewContext) { + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(m) = self.matches.get(self.selected_index()) { cx.emit(Event::Selected(m.tree_id, m.path.clone())); } } - fn select(&mut self, (tree_id, path): &(usize, Arc), cx: &mut ViewContext) { - cx.emit(Event::Selected(*tree_id, path.clone())); + fn select(&mut self, Select(entry): &Select, cx: &mut ViewContext) { + cx.emit(Event::Selected(entry.worktree_id, entry.path.clone())); } #[must_use] @@ -417,7 +435,7 @@ impl FileFinder { mod tests { use super::*; use crate::{ - editor, + editor::{self, Insert}, fs::FakeFs, test::{build_app_state, temp_tree}, workspace::Workspace, @@ -447,12 +465,7 @@ mod tests { .unwrap(); cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) .await; - cx.dispatch_action( - window_id, - vec![workspace.id()], - "file_finder:toggle".into(), - (), - ); + cx.dispatch_action(window_id, vec![workspace.id()], Toggle); let finder = cx.read(|cx| { workspace @@ -466,26 +479,16 @@ mod tests { let query_buffer = cx.read(|cx| finder.read(cx).query_buffer.clone()); let chain = vec![finder.id(), query_buffer.id()]; - cx.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string()); - cx.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string()); - cx.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string()); + cx.dispatch_action(window_id, chain.clone(), Insert("b".into())); + cx.dispatch_action(window_id, chain.clone(), Insert("n".into())); + cx.dispatch_action(window_id, chain.clone(), Insert("a".into())); finder .condition(&cx, |finder, _| finder.matches.len() == 2) .await; let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action( - window_id, - vec![workspace.id(), finder.id()], - "menu:select_next", - (), - ); - cx.dispatch_action( - window_id, - vec![workspace.id(), finder.id()], - "file_finder:confirm", - (), - ); + cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], SelectNext); + cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], Confirm); active_pane .condition(&cx, |pane, _| pane.active_item().is_some()) .await; @@ -648,9 +651,9 @@ mod tests { finder.update(&mut cx, |f, cx| { assert_eq!(f.matches.len(), 2); assert_eq!(f.selected_index(), 0); - f.select_next(&(), cx); + f.select_next(&SelectNext, cx); assert_eq!(f.selected_index(), 1); - f.select_prev(&(), cx); + f.select_prev(&SelectPrev, cx); assert_eq!(f.selected_index(), 0); }); } diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 45af7e355c6a426ba6638f307dcba59f0fb15f25..d76a308e386d9dc8f133d805d91777e69ad0e94d 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -19,12 +19,16 @@ mod util; pub mod workspace; pub mod worktree; +use gpui::action; pub use settings::Settings; use parking_lot::Mutex; use postage::watch; use std::sync::Arc; +action!(About); +action!(Quit); + pub struct AppState { pub settings_tx: Arc>>, pub settings: watch::Receiver, @@ -35,9 +39,9 @@ pub struct AppState { } pub fn init(cx: &mut gpui::MutableAppContext) { - cx.add_global_action("app:quit", quit); + cx.add_global_action(quit); } -fn quit(_: &(), cx: &mut gpui::MutableAppContext) { +fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) { cx.platform().quit(); } diff --git a/zed/src/main.rs b/zed/src/main.rs index f087109c9938ac22446037d2425c36feff7dbe5a..c35b2bf91507f7723e6f8ea40d3973e3c3d5183a 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -10,7 +10,7 @@ use zed::{ self, assets, editor, file_finder, fs::RealFs, language, menus, rpc, settings, theme_selector, - workspace::{self, OpenParams}, + workspace::{self, OpenParams, OpenPaths}, AppState, }; @@ -51,13 +51,10 @@ fn main() { let paths = collect_path_args(); if !paths.is_empty() { - cx.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths, - app_state: app_state.clone(), - }, - ); + cx.dispatch_global_action(OpenPaths(OpenParams { + paths, + app_state: app_state.clone(), + })); } }); } diff --git a/zed/src/menus.rs b/zed/src/menus.rs index 227f0b9efcbb825ba6a6bd2545cc780676b2ff25..c43d72a87dfb83fd8d6dc2332fa183001bb16f8f 100644 --- a/zed/src/menus.rs +++ b/zed/src/menus.rs @@ -1,9 +1,11 @@ -use crate::AppState; +use crate::{workspace, AppState}; use gpui::{Menu, MenuItem}; use std::sync::Arc; #[cfg(target_os = "macos")] pub fn menus(state: &Arc) -> Vec> { + use crate::editor; + vec![ Menu { name: "Zed", @@ -11,27 +13,23 @@ pub fn menus(state: &Arc) -> Vec> { MenuItem::Action { name: "About Zed…", keystroke: None, - action: "app:about-zed", - arg: None, + action: Box::new(super::About), }, MenuItem::Separator, MenuItem::Action { name: "Share", keystroke: None, - action: "workspace:share_worktree", - arg: None, + action: Box::new(workspace::ShareWorktree), }, MenuItem::Action { name: "Join", keystroke: None, - action: "workspace:join_worktree", - arg: None, + action: Box::new(workspace::JoinWorktree(state.clone())), }, MenuItem::Action { name: "Quit", keystroke: Some("cmd-q"), - action: "app:quit", - arg: None, + action: Box::new(super::Quit), }, ], }, @@ -41,15 +39,13 @@ pub fn menus(state: &Arc) -> Vec> { MenuItem::Action { name: "New", keystroke: Some("cmd-n"), - action: "workspace:new_file", - arg: Some(Box::new(state.clone())), + action: Box::new(workspace::OpenNew(state.clone())), }, MenuItem::Separator, MenuItem::Action { name: "Open…", keystroke: Some("cmd-o"), - action: "workspace:open", - arg: Some(Box::new(state.clone())), + action: Box::new(workspace::Open(state.clone())), }, ], }, @@ -59,33 +55,28 @@ pub fn menus(state: &Arc) -> Vec> { MenuItem::Action { name: "Undo", keystroke: Some("cmd-z"), - action: "buffer:undo", - arg: None, + action: Box::new(editor::Undo), }, MenuItem::Action { name: "Redo", keystroke: Some("cmd-Z"), - action: "buffer:redo", - arg: None, + action: Box::new(editor::Redo), }, MenuItem::Separator, MenuItem::Action { name: "Cut", keystroke: Some("cmd-x"), - action: "buffer:cut", - arg: None, + action: Box::new(editor::Cut), }, MenuItem::Action { name: "Copy", keystroke: Some("cmd-c"), - action: "buffer:copy", - arg: None, + action: Box::new(editor::Copy), }, MenuItem::Action { name: "Paste", keystroke: Some("cmd-v"), - action: "buffer:paste", - arg: None, + action: Box::new(editor::Paste), }, ], }, diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 2853f938fa8327ac977271e834d2e35acf85b8c9..506d11a278cfd13ce71d10e586cd8b778cf55135 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -8,11 +8,12 @@ use crate::{ AppState, Settings, }; use gpui::{ + action, elements::{ Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement, UniformList, UniformListState, }, - keymap::{self, Binding}, + keymap::{self, menu, Binding}, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; @@ -29,19 +30,22 @@ pub struct ThemeSelector { selected_index: usize, } +action!(Confirm); +action!(Toggle, Arc); +action!(Reload, Arc); + pub fn init(cx: &mut MutableAppContext, app_state: &Arc) { - cx.add_action("theme_selector:confirm", ThemeSelector::confirm); - cx.add_action("menu:select_prev", ThemeSelector::select_prev); - cx.add_action("menu:select_next", ThemeSelector::select_next); - cx.add_action("theme_selector:toggle", ThemeSelector::toggle); - cx.add_action("theme_selector:reload", ThemeSelector::reload); + cx.add_action(ThemeSelector::confirm); + cx.add_action(ThemeSelector::select_prev); + cx.add_action(ThemeSelector::select_next); + cx.add_action(ThemeSelector::toggle); + cx.add_action(ThemeSelector::reload); cx.add_bindings(vec![ - Binding::new("cmd-k cmd-t", "theme_selector:toggle", None).with_arg(app_state.clone()), - Binding::new("cmd-k t", "theme_selector:reload", None).with_arg(app_state.clone()), - Binding::new("escape", "theme_selector:toggle", Some("ThemeSelector")) - .with_arg(app_state.clone()), - Binding::new("enter", "theme_selector:confirm", Some("ThemeSelector")), + Binding::new("cmd-k cmd-t", Toggle(app_state.clone()), None), + Binding::new("cmd-k t", Reload(app_state.clone()), None), + Binding::new("escape", Toggle(app_state.clone()), Some("ThemeSelector")), + Binding::new("enter", Confirm, Some("ThemeSelector")), ]); } @@ -72,17 +76,13 @@ impl ThemeSelector { this } - fn toggle( - workspace: &mut Workspace, - app_state: &Arc, - cx: &mut ViewContext, - ) { + fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext) { workspace.toggle_modal(cx, |cx, _| { let selector = cx.add_view(|cx| { Self::new( - app_state.settings_tx.clone(), - app_state.settings.clone(), - app_state.themes.clone(), + action.0.settings_tx.clone(), + action.0.settings.clone(), + action.0.themes.clone(), cx, ) }); @@ -91,13 +91,13 @@ impl ThemeSelector { }); } - fn reload(_: &mut Workspace, app_state: &Arc, cx: &mut ViewContext) { - let current_theme_name = app_state.settings.borrow().theme.name.clone(); - app_state.themes.clear(); - match app_state.themes.get(¤t_theme_name) { + fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext) { + let current_theme_name = action.0.settings.borrow().theme.name.clone(); + action.0.themes.clear(); + match action.0.themes.get(¤t_theme_name) { Ok(theme) => { cx.notify_all(); - app_state.settings_tx.lock().borrow_mut().theme = theme; + action.0.settings_tx.lock().borrow_mut().theme = theme; } Err(error) => { log::error!("failed to load theme {}: {:?}", current_theme_name, error) @@ -105,7 +105,7 @@ impl ThemeSelector { } } - fn confirm(&mut self, _: &(), cx: &mut ViewContext) { + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(mat) = self.matches.get(self.selected_index) { match self.registry.get(&mat.string) { Ok(theme) => { @@ -118,7 +118,7 @@ impl ThemeSelector { } } - fn select_prev(&mut self, _: &(), cx: &mut ViewContext) { + fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext) { if self.selected_index > 0 { self.selected_index -= 1; } @@ -126,7 +126,7 @@ impl ThemeSelector { cx.notify(); } - fn select_next(&mut self, _: &(), cx: &mut ViewContext) { + fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { if self.selected_index + 1 < self.matches.len() { self.selected_index += 1; } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 7c133944f1c79adec4d2d9d7549f483d1af98f6e..e5f4c95495530afe4bdbb1e31fa6e0836ca2bf48 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -14,6 +14,7 @@ use crate::{ }; use anyhow::{anyhow, Result}; use gpui::{ + action, elements::*, geometry::{rect::RectF, vector::vec2f}, json::to_string_pretty, @@ -36,37 +37,43 @@ use std::{ sync::Arc, }; +action!(Open, Arc); +action!(OpenPaths, OpenParams); +action!(OpenNew, Arc); +action!(ShareWorktree); +action!(JoinWorktree, Arc); +action!(Save); +action!(DebugElements); +action!(ToggleSidebarItem, (Side, usize)); + pub fn init(cx: &mut MutableAppContext) { - cx.add_global_action("workspace:open", open); - cx.add_global_action( - "workspace:open_paths", - |params: &OpenParams, cx: &mut MutableAppContext| open_paths(params, cx).detach(), - ); - cx.add_global_action("workspace:new_file", open_new); - cx.add_global_action("workspace:join_worktree", join_worktree); - cx.add_action("workspace:save", Workspace::save_active_item); - cx.add_action("workspace:debug_elements", Workspace::debug_elements); - cx.add_action("workspace:new_file", Workspace::open_new_file); - cx.add_action("workspace:share_worktree", Workspace::share_worktree); - cx.add_action("workspace:join_worktree", Workspace::join_worktree); - cx.add_action( - "workspace:toggle_sidebar_item", - Workspace::toggle_sidebar_item, - ); + cx.add_global_action(open); + cx.add_global_action(|action: &OpenPaths, cx: &mut MutableAppContext| { + open_paths(action, cx).detach() + }); + cx.add_global_action(open_new); + cx.add_global_action(join_worktree); + cx.add_action(Workspace::save_active_item); + cx.add_action(Workspace::debug_elements); + cx.add_action(Workspace::open_new_file); + cx.add_action(Workspace::share_worktree); + cx.add_action(Workspace::join_worktree); + cx.add_action(Workspace::toggle_sidebar_item); cx.add_bindings(vec![ - Binding::new("cmd-s", "workspace:save", None), - Binding::new("cmd-alt-i", "workspace:debug_elements", None), + Binding::new("cmd-s", Save, None), + Binding::new("cmd-alt-i", DebugElements, None), ]); pane::init(cx); } +#[derive(Clone)] pub struct OpenParams { pub paths: Vec, pub app_state: Arc, } -fn open(app_state: &Arc, cx: &mut MutableAppContext) { - let app_state = app_state.clone(); +fn open(action: &Open, cx: &mut MutableAppContext) { + let app_state = action.0.clone(); cx.prompt_for_paths( PathPromptOptions { files: true, @@ -75,22 +82,22 @@ fn open(app_state: &Arc, cx: &mut MutableAppContext) { }, move |paths, cx| { if let Some(paths) = paths { - cx.dispatch_global_action("workspace:open_paths", OpenParams { paths, app_state }); + cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state })); } }, ); } -fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) -> Task<()> { - log::info!("open paths {:?}", params.paths); +fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> { + log::info!("open paths {:?}", action.0.paths); // Open paths in existing workspace if possible for window_id in cx.window_ids().collect::>() { if let Some(handle) = cx.root_view::(window_id) { let task = handle.update(cx, |view, cx| { - if view.contains_paths(¶ms.paths, cx.as_ref()) { + if view.contains_paths(&action.0.paths, cx.as_ref()) { log::info!("open paths on existing workspace"); - Some(view.open_paths(¶ms.paths, cx)) + Some(view.open_paths(&action.0.paths, cx)) } else { None } @@ -106,23 +113,26 @@ fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) -> Task<()> { // Add a new workspace if necessary - let (_, workspace) = - cx.add_window(window_options(), |cx| Workspace::new(¶ms.app_state, cx)); - workspace.update(cx, |workspace, cx| workspace.open_paths(¶ms.paths, cx)) + let (_, workspace) = cx.add_window(window_options(), |cx| { + Workspace::new(&action.0.app_state, cx) + }); + workspace.update(cx, |workspace, cx| { + workspace.open_paths(&action.0.paths, cx) + }) } -fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { +fn open_new(action: &OpenNew, cx: &mut MutableAppContext) { cx.add_window(window_options(), |cx| { - let mut view = Workspace::new(app_state.as_ref(), cx); - view.open_new_file(&app_state, cx); + let mut view = Workspace::new(action.0.as_ref(), cx); + view.open_new_file(&action, cx); view }); } -fn join_worktree(app_state: &Arc, cx: &mut MutableAppContext) { +fn join_worktree(action: &JoinWorktree, cx: &mut MutableAppContext) { cx.add_window(window_options(), |cx| { - let mut view = Workspace::new(app_state.as_ref(), cx); - view.join_worktree(&(), cx); + let mut view = Workspace::new(action.0.as_ref(), cx); + view.join_worktree(action, cx); view }); } @@ -544,7 +554,7 @@ impl Workspace { } } - pub fn open_new_file(&mut self, _: &Arc, cx: &mut ViewContext) { + pub fn open_new_file(&mut self, _: &OpenNew, cx: &mut ViewContext) { let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); let buffer_view = cx.add_view(|cx| Editor::for_buffer(buffer.clone(), self.settings.clone(), cx)); @@ -677,7 +687,7 @@ impl Workspace { self.active_pane().read(cx).active_item() } - pub fn save_active_item(&mut self, _: &(), cx: &mut ViewContext) { + pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext) { if let Some(item) = self.active_item(cx) { let handle = cx.handle(); if item.entry_id(cx.as_ref()).is_none() { @@ -744,7 +754,7 @@ impl Workspace { pub fn toggle_sidebar_item( &mut self, - (side, item_ix): &(Side, usize), + ToggleSidebarItem((side, item_ix)): &ToggleSidebarItem, cx: &mut ViewContext, ) { let sidebar = match side { @@ -755,7 +765,7 @@ impl Workspace { cx.notify(); } - pub fn debug_elements(&mut self, _: &(), cx: &mut ViewContext) { + pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext) { match to_string_pretty(&cx.debug_elements()) { Ok(json) => { let kib = json.len() as f32 / 1024.; @@ -771,7 +781,7 @@ impl Workspace { }; } - fn share_worktree(&mut self, _: &(), cx: &mut ViewContext) { + fn share_worktree(&mut self, _: &ShareWorktree, cx: &mut ViewContext) { let rpc = self.rpc.clone(); let platform = cx.platform(); @@ -803,7 +813,7 @@ impl Workspace { .detach(); } - fn join_worktree(&mut self, _: &(), cx: &mut ViewContext) { + fn join_worktree(&mut self, _: &JoinWorktree, cx: &mut ViewContext) { let rpc = self.rpc.clone(); let languages = self.languages.clone(); @@ -1000,7 +1010,7 @@ impl WorkspaceHandle for ViewHandle { mod tests { use super::*; use crate::{ - editor::Editor, + editor::{Editor, Insert}, fs::FakeFs, test::{build_app_state, temp_tree}, worktree::WorktreeHandle, @@ -1029,13 +1039,13 @@ mod tests { cx.update(|cx| { open_paths( - &OpenParams { + &OpenPaths(OpenParams { paths: vec![ dir.path().join("a").to_path_buf(), dir.path().join("b").to_path_buf(), ], app_state: app_state.clone(), - }, + }), cx, ) }) @@ -1044,10 +1054,10 @@ mod tests { cx.update(|cx| { open_paths( - &OpenParams { + &OpenPaths(OpenParams { paths: vec![dir.path().join("a").to_path_buf()], app_state: app_state.clone(), - }, + }), cx, ) }) @@ -1060,13 +1070,13 @@ mod tests { cx.update(|cx| { open_paths( - &OpenParams { + &OpenPaths(OpenParams { paths: vec![ dir.path().join("b").to_path_buf(), dir.path().join("c").to_path_buf(), ], app_state: app_state.clone(), - }, + }), cx, ) }) @@ -1284,14 +1294,14 @@ mod tests { item.to_any().downcast::().unwrap() }); - cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&"x".to_string(), cx))); + cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&Insert("x".into()), cx))); fs::write(dir.path().join("a.txt"), "changed").unwrap(); editor .condition(&cx, |editor, cx| editor.has_conflict(cx)) .await; cx.read(|cx| assert!(editor.is_dirty(cx))); - cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&(), cx))); + cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&Save, cx))); cx.simulate_prompt_answer(window_id, 0); editor .condition(&cx, |editor, cx| !editor.is_dirty(cx)) @@ -1323,7 +1333,7 @@ mod tests { // Create a new untitled buffer let editor = workspace.update(&mut cx, |workspace, cx| { - workspace.open_new_file(&app_state, cx); + workspace.open_new_file(&OpenNew(app_state.clone()), cx); workspace .active_item(cx) .unwrap() @@ -1335,12 +1345,14 @@ mod tests { editor.update(&mut cx, |editor, cx| { assert!(!editor.is_dirty(cx.as_ref())); assert_eq!(editor.title(cx.as_ref()), "untitled"); - editor.insert(&"hi".to_string(), cx); + editor.insert(&Insert("hi".into()), cx); assert!(editor.is_dirty(cx.as_ref())); }); // Save the buffer. This prompts for a filename. - workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx)); + workspace.update(&mut cx, |workspace, cx| { + workspace.save_active_item(&Save, cx) + }); cx.simulate_new_path_selection(|parent_dir| { assert_eq!(parent_dir, dir.path()); Some(parent_dir.join("the-new-name")) @@ -1361,10 +1373,12 @@ mod tests { // Edit the file and save it again. This time, there is no filename prompt. editor.update(&mut cx, |editor, cx| { - editor.insert(&" there".to_string(), cx); + editor.insert(&Insert(" there".into()), cx); assert_eq!(editor.is_dirty(cx.as_ref()), true); }); - workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx)); + workspace.update(&mut cx, |workspace, cx| { + workspace.save_active_item(&Save, cx) + }); assert!(!cx.did_prompt_for_new_path()); editor .condition(&cx, |editor, cx| !editor.is_dirty(cx)) @@ -1374,7 +1388,7 @@ mod tests { // Open the same newly-created file in another pane item. The new editor should reuse // the same buffer. workspace.update(&mut cx, |workspace, cx| { - workspace.open_new_file(&app_state, cx); + 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) @@ -1398,7 +1412,7 @@ mod tests { cx.update(init); let app_state = cx.read(build_app_state); - cx.dispatch_global_action("workspace:new_file", app_state); + cx.dispatch_global_action(OpenNew(app_state)); let window_id = *cx.window_ids().first().unwrap(); let workspace = cx.root_view::(window_id).unwrap(); let editor = workspace.update(&mut cx, |workspace, cx| { @@ -1414,7 +1428,9 @@ mod tests { assert!(editor.text(cx).is_empty()); }); - workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx)); + workspace.update(&mut cx, |workspace, cx| { + workspace.save_active_item(&Save, cx) + }); let dir = TempDir::new("test-new-empty-workspace").unwrap(); cx.simulate_new_path_selection(|_| { @@ -1467,7 +1483,11 @@ mod tests { ); }); - cx.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ()); + cx.dispatch_action( + window_id, + vec![pane_1.id()], + pane::Split(SplitDirection::Right), + ); cx.update(|cx| { let pane_2 = workspace.read(cx).active_pane().clone(); assert_ne!(pane_1, pane_2); @@ -1475,7 +1495,7 @@ mod tests { let pane2_item = pane_2.read(cx).active_item().unwrap(); assert_eq!(pane2_item.entry_id(cx.as_ref()), Some(file1.clone())); - cx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); + cx.dispatch_action(window_id, vec![pane_2.id()], &CloseActiveItem); let workspace = workspace.read(cx); assert_eq!(workspace.panes.len(), 1); assert_eq!(workspace.active_pane(), &pane_1); diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index f99e2a3fd6102b61a0d2f12bada0dcc97fb135ec..845f6e2521fdf367c6e7dc9496bca2ecdbd3aab9 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -1,6 +1,7 @@ use super::{ItemViewHandle, SplitDirection}; use crate::{settings::Settings, theme}; use gpui::{ + action, color::Color, elements::*, geometry::{rect::RectF, vector::vec2f}, @@ -11,46 +12,41 @@ use gpui::{ use postage::watch; use std::{cmp, path::Path, sync::Arc}; +action!(Split, SplitDirection); +action!(ActivateItem, usize); +action!(ActivatePrevItem); +action!(ActivateNextItem); +action!(CloseActiveItem); +action!(CloseItem, usize); + pub fn init(cx: &mut MutableAppContext) { - cx.add_action( - "pane:activate_item", - |pane: &mut Pane, index: &usize, cx| { - pane.activate_item(*index, cx); - }, - ); - cx.add_action("pane:activate_prev_item", |pane: &mut Pane, _: &(), cx| { + cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { + pane.activate_item(action.0, cx); + }); + cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { pane.activate_prev_item(cx); }); - cx.add_action("pane:activate_next_item", |pane: &mut Pane, _: &(), cx| { + cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| { pane.activate_next_item(cx); }); - cx.add_action("pane:close_active_item", |pane: &mut Pane, _: &(), cx| { + cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| { pane.close_active_item(cx); }); - cx.add_action("pane:close_item", |pane: &mut Pane, item_id: &usize, cx| { - pane.close_item(*item_id, cx); - }); - cx.add_action("pane:split_up", |pane: &mut Pane, _: &(), cx| { - pane.split(SplitDirection::Up, cx); - }); - cx.add_action("pane:split_down", |pane: &mut Pane, _: &(), cx| { - pane.split(SplitDirection::Down, cx); - }); - cx.add_action("pane:split_left", |pane: &mut Pane, _: &(), cx| { - pane.split(SplitDirection::Left, cx); + cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| { + pane.close_item(action.0, cx); }); - cx.add_action("pane:split_right", |pane: &mut Pane, _: &(), cx| { - pane.split(SplitDirection::Right, cx); + cx.add_action(|pane: &mut Pane, action: &Split, cx| { + pane.split(action.0, cx); }); cx.add_bindings(vec![ - Binding::new("shift-cmd-{", "pane:activate_prev_item", Some("Pane")), - Binding::new("shift-cmd-}", "pane:activate_next_item", Some("Pane")), - Binding::new("cmd-w", "pane:close_active_item", Some("Pane")), - Binding::new("cmd-k up", "pane:split_up", Some("Pane")), - Binding::new("cmd-k down", "pane:split_down", Some("Pane")), - Binding::new("cmd-k left", "pane:split_left", Some("Pane")), - Binding::new("cmd-k right", "pane:split_right", Some("Pane")), + Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")), + Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")), + Binding::new("cmd-w", CloseActiveItem, Some("Pane")), + Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")), + Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")), + Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")), + Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")), ]); } @@ -253,7 +249,7 @@ impl Pane { ConstrainedBox::new( EventHandler::new(container.boxed()) .on_mouse_down(move |cx| { - cx.dispatch_action("pane:activate_item", ix); + cx.dispatch_action(ActivateItem(ix)); true }) .boxed(), @@ -338,7 +334,7 @@ impl Pane { icon.boxed() } }) - .on_click(move |cx| cx.dispatch_action("pane:close_item", item_id)) + .on_click(move |cx| cx.dispatch_action(CloseItem(item_id))) .named("close-tab-icon") } else { let diameter = 8.; diff --git a/zed/src/workspace/pane_group.rs b/zed/src/workspace/pane_group.rs index d5d0040009fba87bfd9bc0bb29acb6a533457336..d6bf99277663e66cda8a2d519d51cbc4476714cc 100644 --- a/zed/src/workspace/pane_group.rs +++ b/zed/src/workspace/pane_group.rs @@ -184,7 +184,7 @@ impl PaneAxis { } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum SplitDirection { Up, Down, diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 7d7e1f7c6ea5d7ad23244b50816be6ef51d5c04e..1cd63bb2fb8fc458a264cb9681f222f95bd8d468 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -1,5 +1,6 @@ use crate::Settings; use gpui::{ + action, elements::{ Align, ConstrainedBox, Container, Flex, MouseEventHandler, ParentElement as _, Svg, }, @@ -23,6 +24,14 @@ struct Item { view: AnyViewHandle, } +action!(ToggleSidebarItem, ToggleArg); + +#[derive(Clone)] +pub struct ToggleArg { + side: Side, + item_index: usize, +} + impl Sidebar { pub fn new(side: Side) -> Self { Self { @@ -59,8 +68,8 @@ impl Sidebar { Container::new( Flex::column() - .with_children(self.items.iter().enumerate().map(|(item_ix, item)| { - let theme = if Some(item_ix) == self.active_item_ix { + .with_children(self.items.iter().enumerate().map(|(item_index, item)| { + let theme = if Some(item_index) == self.active_item_ix { &settings.theme.active_sidebar_icon } else { &settings.theme.sidebar_icon @@ -81,7 +90,7 @@ impl Sidebar { .boxed() }) .on_click(move |cx| { - cx.dispatch_action("workspace:toggle_sidebar_item", (side, item_ix)) + cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index })) }) .boxed() })) From ab2977c65cc3023811893f018d0d04bdc6246c4f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Aug 2021 12:00:31 +0200 Subject: [PATCH 051/204] Pass action instead of action argument to handlers --- gpui/src/app.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 093f5c7d4d6b834b72a30c33edd3328698bed442..6148531e6e01bf7fd68d1816e13e0e91689a7b47 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -100,7 +100,7 @@ pub trait Action: 'static + AnyAction { pub trait AnyAction { fn id(&self) -> TypeId; - fn arg_as_any(&self) -> &dyn Any; + fn as_any(&self) -> &dyn Any; fn boxed_clone(&self) -> Box; fn boxed_clone_as_any(&self) -> Box; } @@ -116,8 +116,8 @@ impl AnyAction for () { TypeId::of::<()>() } - fn arg_as_any(&self) -> &dyn Any { - &() + fn as_any(&self) -> &dyn Any { + self } fn boxed_clone(&self) -> Box { @@ -146,8 +146,8 @@ macro_rules! action { std::any::TypeId::of::<$name>() } - fn arg_as_any(&self) -> &dyn std::any::Any { - &self.0 + fn as_any(&self) -> &dyn std::any::Any { + self } fn boxed_clone(&self) -> Box { @@ -175,8 +175,8 @@ macro_rules! action { std::any::TypeId::of::<$name>() } - fn arg_as_any(&self) -> &dyn std::any::Any { - &() + fn as_any(&self) -> &dyn std::any::Any { + self } fn boxed_clone(&self) -> Box { @@ -768,13 +768,13 @@ impl MutableAppContext { cx: &mut MutableAppContext, window_id: usize, view_id: usize| { - let arg = action.arg_as_any().downcast_ref().unwrap(); + let action = action.as_any().downcast_ref().unwrap(); let mut cx = ViewContext::new(cx, window_id, view_id); handler( view.as_any_mut() .downcast_mut() .expect("downcast is type safe"), - arg, + action, &mut cx, ); cx.halt_action_dispatch @@ -795,8 +795,8 @@ impl MutableAppContext { F: 'static + FnMut(&A, &mut MutableAppContext), { let handler = Box::new(move |action: &dyn AnyAction, cx: &mut MutableAppContext| { - let arg = action.arg_as_any().downcast_ref().unwrap(); - handler(arg, cx); + let action = action.as_any().downcast_ref().unwrap(); + handler(action, cx); }); self.global_actions From 6129bda06822361db968d95ddcd08a9d9d50fcda Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Aug 2021 14:45:13 +0200 Subject: [PATCH 052/204] Fix boxed cloning of `AnyAction` Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 43 +++++++++++-------------------------------- gpui/src/keymap.rs | 10 +++++----- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 6148531e6e01bf7fd68d1816e13e0e91689a7b47..d21a1059efa4ab746ff4e775cd36e5a29988f4d4 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -94,41 +94,16 @@ pub trait UpdateView { pub trait Action: 'static + AnyAction { type Argument: 'static + Clone; - - const NAME: &'static str; } pub trait AnyAction { fn id(&self) -> TypeId; + fn name(&self) -> &'static str; fn as_any(&self) -> &dyn Any; fn boxed_clone(&self) -> Box; fn boxed_clone_as_any(&self) -> Box; } -impl Action for () { - type Argument = (); - - const NAME: &'static str = "()"; -} - -impl AnyAction for () { - fn id(&self) -> TypeId { - TypeId::of::<()>() - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn boxed_clone(&self) -> Box { - Box::new(()) - } - - fn boxed_clone_as_any(&self) -> Box { - Box::new(()) - } -} - #[macro_export] macro_rules! action { ($name:ident, $arg:ty) => { @@ -137,8 +112,6 @@ macro_rules! action { impl $crate::Action for $name { type Argument = $arg; - - const NAME: &'static str = stringify!($name); } impl $crate::AnyAction for $name { @@ -146,6 +119,10 @@ macro_rules! action { std::any::TypeId::of::<$name>() } + fn name(&self) -> &'static str { + stringify!($name) + } + fn as_any(&self) -> &dyn std::any::Any { self } @@ -166,8 +143,6 @@ macro_rules! action { impl $crate::Action for $name { type Argument = (); - - const NAME: &'static str = stringify!($name); } impl $crate::AnyAction for $name { @@ -175,16 +150,20 @@ macro_rules! action { std::any::TypeId::of::<$name>() } + fn name(&self) -> &'static str { + stringify!($name) + } + fn as_any(&self) -> &dyn std::any::Any { self } fn boxed_clone(&self) -> Box { - Box::new(()) + Box::new(self.clone()) } fn boxed_clone_as_any(&self) -> Box { - Box::new(()) + Box::new(self.clone()) } } }; diff --git a/gpui/src/keymap.rs b/gpui/src/keymap.rs index 409366419394ba07be9230b70670b40be9d79157..b1082123bccc772a8f820744a427e491db06bd19 100644 --- a/gpui/src/keymap.rs +++ b/gpui/src/keymap.rs @@ -455,23 +455,23 @@ mod tests { assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x"))); // Multi-keystroke match - assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None); + assert_eq!(matcher.test_keystroke::("a", 1, &ctx_b), None); assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab)); // Failed matches don't interfere with matching subsequent keys - assert_eq!(matcher.test_keystroke::<()>("x", 1, &ctx_a), None); + assert_eq!(matcher.test_keystroke::("x", 1, &ctx_a), None); assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x"))); // Pending keystrokes are cleared when the context changes - assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None); + assert_eq!(matcher.test_keystroke::("a", 1, &ctx_b), None); assert_eq!(matcher.test_keystroke("b", 1, &ctx_a), Some(B)); let mut ctx_c = Context::default(); ctx_c.set.insert("c".into()); // Pending keystrokes are maintained per-view - assert_eq!(matcher.test_keystroke::<()>("a", 1, &ctx_b), None); - assert_eq!(matcher.test_keystroke::<()>("a", 2, &ctx_c), None); + assert_eq!(matcher.test_keystroke::("a", 1, &ctx_b), None); + assert_eq!(matcher.test_keystroke::("a", 2, &ctx_c), None); assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab)); Ok(()) From 6dddb72e827e268326173de49048e405b9cf1ef9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Aug 2021 15:20:23 +0200 Subject: [PATCH 053/204] WIP: Create infrastructure for testing element layout Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 34 ++++++++++++++---------------- gpui/src/elements/list.rs | 11 ++++++++++ gpui/src/presenter.rs | 44 +++++++++++++++++++-------------------- 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index d21a1059efa4ab746ff4e775cd36e5a29988f4d4..9c96b41097e55bef3364c978ff1c01aa4694ecce 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -1067,15 +1067,9 @@ impl MutableAppContext { self.cx .platform .open_window(window_id, window_options, self.foreground.clone()); - let text_layout_cache = TextLayoutCache::new(self.cx.platform.fonts()); - let presenter = Rc::new(RefCell::new(Presenter::new( - window_id, - window.titlebar_height(), - self.cx.font_cache.clone(), - text_layout_cache, - self.assets.clone(), - self, - ))); + let presenter = Rc::new(RefCell::new( + self.build_presenter(window_id, window.titlebar_height()), + )); { let mut app = self.upgrade(); @@ -1107,7 +1101,6 @@ impl MutableAppContext { app.update(|cx| { let scene = presenter.borrow_mut().build_scene( window.size(), - window.titlebar_height(), window.scale_factor(), cx, ); @@ -1131,6 +1124,17 @@ impl MutableAppContext { }); } + pub fn build_presenter(&self, window_id: usize, titlebar_height: f32) -> Presenter { + Presenter::new( + window_id, + titlebar_height, + self.cx.font_cache.clone(), + TextLayoutCache::new(self.cx.platform.fonts()), + self.assets.clone(), + self, + ) + } + pub fn add_view(&mut self, window_id: usize, build_view: F) -> ViewHandle where T: View, @@ -1263,14 +1267,8 @@ impl MutableAppContext { { { let mut presenter = presenter.borrow_mut(); - let titlebar_height = window.titlebar_height(); - presenter.invalidate(invalidation, titlebar_height, self.as_ref()); - let scene = presenter.build_scene( - window.size(), - titlebar_height, - window.scale_factor(), - self, - ); + presenter.invalidate(invalidation, self.as_ref()); + let scene = presenter.build_scene(window.size(), window.scale_factor(), self); window.present_scene(scene); } self.presenters_and_platform_windows diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 82b3819149b3759f7153fd8f4bea419a9f1c775e..d64b10cf4c7fbab7b4a54905857ad2a45e3deaee 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -192,3 +192,14 @@ impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Height { self.0 += summary.height; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[crate::test(self)] + fn test_layout(cx: &mut crate::MutableAppContext) { + let mut presenter = cx.build_presenter(0, 20.0); + let layout_cx = presenter.layout_cx(cx); + } +} diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 4e548ad03b95c8980938334a6c75f4230af59361..ebffa9653a76fbb28210aabc4c768a8900f1548f 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -22,6 +22,7 @@ pub struct Presenter { text_layout_cache: TextLayoutCache, asset_cache: Arc, last_mouse_moved_event: Option, + titlebar_height: f32, } impl Presenter { @@ -41,6 +42,7 @@ impl Presenter { text_layout_cache, asset_cache, last_mouse_moved_event: None, + titlebar_height, } } @@ -55,12 +57,7 @@ impl Presenter { path } - pub fn invalidate( - &mut self, - mut invalidation: WindowInvalidation, - titlebar_height: f32, - cx: &AppContext, - ) { + pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &AppContext) { for view_id in invalidation.removed { invalidation.updated.remove(&view_id); self.rendered_views.remove(&view_id); @@ -69,7 +66,7 @@ impl Presenter { for view_id in invalidation.updated { self.rendered_views.insert( view_id, - cx.render_view(self.window_id, view_id, titlebar_height) + cx.render_view(self.window_id, view_id, self.titlebar_height) .unwrap(), ); } @@ -78,14 +75,13 @@ impl Presenter { pub fn build_scene( &mut self, window_size: Vector2F, - titlebar_height: f32, scale_factor: f32, cx: &mut MutableAppContext, ) -> Scene { let mut scene = Scene::new(scale_factor); if let Some(root_view_id) = cx.root_view_id(self.window_id) { - self.layout(window_size, titlebar_height, cx); + self.layout(window_size, cx); self.after_layout(cx); let mut paint_cx = PaintContext { scene: &mut scene, @@ -107,19 +103,22 @@ impl Presenter { scene } - fn layout(&mut self, size: Vector2F, titlebar_height: f32, cx: &mut MutableAppContext) { + fn layout(&mut self, size: Vector2F, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { - let mut layout_ctx = LayoutContext { - rendered_views: &mut self.rendered_views, - parents: &mut self.parents, - font_cache: &self.font_cache, - text_layout_cache: &self.text_layout_cache, - asset_cache: &self.asset_cache, - view_stack: Vec::new(), - app: cx, - titlebar_height, - }; - layout_ctx.layout(root_view_id, SizeConstraint::strict(size)); + self.layout_cx(cx) + .layout(root_view_id, SizeConstraint::strict(size)); + } + } + + pub fn layout_cx<'a>(&'a mut self, cx: &'a mut MutableAppContext) -> LayoutContext<'a> { + LayoutContext { + rendered_views: &mut self.rendered_views, + parents: &mut self.parents, + font_cache: &self.font_cache, + text_layout_cache: &self.text_layout_cache, + asset_cache: &self.asset_cache, + view_stack: Vec::new(), + app: cx, } } @@ -185,12 +184,11 @@ pub struct DispatchDirective { pub struct LayoutContext<'a> { rendered_views: &'a mut HashMap, parents: &'a mut HashMap, + view_stack: Vec, pub font_cache: &'a FontCache, pub text_layout_cache: &'a TextLayoutCache, pub asset_cache: &'a AssetCache, pub app: &'a mut MutableAppContext, - view_stack: Vec, - pub titlebar_height: f32, } impl<'a> LayoutContext<'a> { From 2c3ba00d3e99238b171233939bc885262165faf9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Aug 2021 15:23:06 +0200 Subject: [PATCH 054/204] Remove vestiges of `after_layout` Co-Authored-By: Nathan Sobo --- gpui/src/elements.rs | 8 +------- gpui/src/lib.rs | 3 +-- gpui/src/presenter.rs | 29 ----------------------------- 3 files changed, 2 insertions(+), 38 deletions(-) diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index e390e84653981daf0b06ce8c0edd19eda259123b..1156a434e1243c185ca92de4b12a8a6103e4162f 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -31,8 +31,7 @@ pub use uniform_list::*; use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, AfterLayoutContext, DebugContext, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, + json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use core::panic; use json::ToJson; @@ -40,7 +39,6 @@ use std::{any::Any, borrow::Cow, mem}; trait AnyElement { fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F; - fn after_layout(&mut self, _: &mut AfterLayoutContext) {} fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext); fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool; fn debug(&self, cx: &DebugContext) -> serde_json::Value; @@ -249,10 +247,6 @@ impl ElementBox { self.element.layout(constraint, cx) } - pub fn after_layout(&mut self, cx: &mut AfterLayoutContext) { - self.element.after_layout(cx); - } - pub fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) { self.element.paint(origin, cx); } diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index 308674d45dd726ed70a07d79dc20824aa07521c2..e7cfa3b1776ab416be5df8fed36f98aac36080bf 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -29,6 +29,5 @@ pub use gpui_macros::test; pub use platform::FontSystem; pub use platform::{Event, PathPromptOptions, Platform, PromptLevel}; pub use presenter::{ - AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext, - SizeConstraint, Vector2FExt, + Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, }; diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index ebffa9653a76fbb28210aabc4c768a8900f1548f..f282e4405ea2e5ca2606a96dea9ca0cef3f9d8fc 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -82,7 +82,6 @@ impl Presenter { if let Some(root_view_id) = cx.root_view_id(self.window_id) { self.layout(window_size, cx); - self.after_layout(cx); let mut paint_cx = PaintContext { scene: &mut scene, font_cache: &self.font_cache, @@ -122,18 +121,6 @@ impl Presenter { } } - fn after_layout(&mut self, cx: &mut MutableAppContext) { - if let Some(root_view_id) = cx.root_view_id(self.window_id) { - let mut layout_cx = AfterLayoutContext { - rendered_views: &mut self.rendered_views, - font_cache: &self.font_cache, - text_layout_cache: &self.text_layout_cache, - app: cx, - }; - layout_cx.after_layout(root_view_id); - } - } - pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { if matches!(event, Event::MouseMoved { .. }) { @@ -205,22 +192,6 @@ impl<'a> LayoutContext<'a> { } } -pub struct AfterLayoutContext<'a> { - rendered_views: &'a mut HashMap, - pub font_cache: &'a FontCache, - pub text_layout_cache: &'a TextLayoutCache, - pub app: &'a mut MutableAppContext, -} - -impl<'a> AfterLayoutContext<'a> { - fn after_layout(&mut self, view_id: usize) { - if let Some(mut view) = self.rendered_views.remove(&view_id) { - view.after_layout(self); - self.rendered_views.insert(view_id, view); - } - } -} - pub struct PaintContext<'a> { rendered_views: &'a mut HashMap, pub scene: &'a mut Scene, From 03b7c3c8c68822380277a9ffbb63c7e98f1fe491 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Aug 2021 16:00:05 +0200 Subject: [PATCH 055/204] Implement and test `splice` for `ListState` Co-Authored-By: Nathan Sobo --- gpui/src/elements/list.rs | 98 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index d64b10cf4c7fbab7b4a54905857ad2a45e3deaee..ecf407946b519522798c817dea605e7d77a0b38c 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -4,7 +4,7 @@ use crate::{ Element, }; use parking_lot::Mutex; -use std::sync::Arc; +use std::{ops::Range, sync::Arc}; use crate::ElementBox; @@ -12,6 +12,7 @@ pub struct List { state: ListState, } +#[derive(Clone)] pub struct ListState(Arc>); struct StateInner { @@ -26,7 +27,7 @@ enum ElementHeight { Ready(f32), } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] struct ElementHeightSummary { count: usize, pending_count: usize, @@ -42,6 +43,12 @@ struct PendingCount(usize); #[derive(Clone, Debug, Default)] struct Height(f32); +impl List { + pub fn new(state: ListState) -> Self { + Self { state } + } +} + impl Element for List { type LayoutState = (); @@ -82,8 +89,7 @@ impl Element for List { drop(old_heights); state.heights = new_heights; - - todo!() + (constraint.max, ()) } fn paint( @@ -127,6 +133,33 @@ impl ListState { heights, }))) } + + pub fn splice( + &self, + old_range: Range, + new_elements: impl IntoIterator, + ) { + let state = &mut *self.0.lock(); + + let mut old_heights = state.heights.cursor::(); + let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &()); + old_heights.seek_forward(&Count(old_range.end), Bias::Right, &()); + + let mut len = 0; + let old_elements = state.elements.splice( + old_range, + new_elements.into_iter().map(|e| { + len += 1; + e + }), + ); + drop(old_elements); + + new_heights.extend((0..len).map(|_| ElementHeight::Pending), &()); + new_heights.push_tree(old_heights.suffix(&()), &()); + drop(old_heights); + state.heights = new_heights; + } } impl ElementHeight { @@ -158,6 +191,7 @@ impl sum_tree::Summary for ElementHeightSummary { type Context = (); fn add_summary(&mut self, summary: &Self, _: &()) { + self.count += summary.count; self.pending_count += summary.pending_count; self.height += summary.height; } @@ -175,6 +209,12 @@ impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Count { } } +impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Count { + fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } +} + impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for PendingCount { fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) { self.0 += summary.pending_count; @@ -196,10 +236,58 @@ impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Height { #[cfg(test)] mod tests { use super::*; + use crate::{elements::*, geometry::vector::vec2f}; #[crate::test(self)] fn test_layout(cx: &mut crate::MutableAppContext) { let mut presenter = cx.build_presenter(0, 20.0); - let layout_cx = presenter.layout_cx(cx); + let mut layout_cx = presenter.layout_cx(cx); + let state = ListState::new(vec![item(20.), item(30.), item(10.)]); + let mut list = List::new(state.clone()).boxed(); + + let size = list.layout( + SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)), + &mut layout_cx, + ); + assert_eq!(size, vec2f(100., 40.)); + assert_eq!( + state.0.lock().heights.summary(), + ElementHeightSummary { + count: 3, + pending_count: 0, + height: 60. + } + ); + + state.splice(1..2, vec![item(40.), item(50.)]); + state.splice(3..3, vec![item(60.)]); + assert_eq!( + state.0.lock().heights.summary(), + ElementHeightSummary { + count: 5, + pending_count: 3, + height: 30. + } + ); + let size = list.layout( + SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)), + &mut layout_cx, + ); + assert_eq!(size, vec2f(100., 40.)); + assert_eq!( + state.0.lock().heights.summary(), + ElementHeightSummary { + count: 5, + pending_count: 0, + height: 180. + } + ); + } + + fn item(height: f32) -> ElementBox { + ConstrainedBox::new(Empty::new().boxed()) + .with_height(height) + .with_width(100.) + .boxed() } } From bd89dc41140187efb76ff00f32b745b90b13aa3c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Aug 2021 16:04:23 +0200 Subject: [PATCH 056/204] Perform a full layout of `List` when width changes Co-Authored-By: Nathan Sobo --- gpui/src/elements/list.rs | 50 +++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index ecf407946b519522798c817dea605e7d77a0b38c..c506ef484a2932992de6ca9521d5d34c913d0ac2 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -59,36 +59,44 @@ impl Element for List { constraint: crate::SizeConstraint, cx: &mut crate::LayoutContext, ) -> (Vector2F, Self::LayoutState) { - // TODO: Fully invalidate if width has changed since the last layout. - let state = &mut *self.state.0.lock(); - let mut old_heights = state.heights.cursor::(); - let mut new_heights = old_heights.slice(&PendingCount(1), sum_tree::Bias::Left, &()); - let mut item_constraint = constraint; item_constraint.min.set_y(0.); item_constraint.max.set_y(f32::INFINITY); - while let Some(height) = old_heights.item() { - if height.is_pending() { - let size = - state.elements[old_heights.sum_start().count].layout(item_constraint, cx); - new_heights.push(ElementHeight::Ready(size.y()), &()); - old_heights.next(&()); - } else { - new_heights.push_tree( - old_heights.slice( - &PendingCount(old_heights.sum_start().pending_count + 1), - Bias::Left, + if state.last_layout_width == constraint.max.x() { + let mut old_heights = state.heights.cursor::(); + let mut new_heights = old_heights.slice(&PendingCount(1), sum_tree::Bias::Left, &()); + + while let Some(height) = old_heights.item() { + if height.is_pending() { + let size = + state.elements[old_heights.sum_start().count].layout(item_constraint, cx); + new_heights.push(ElementHeight::Ready(size.y()), &()); + old_heights.next(&()); + } else { + new_heights.push_tree( + old_heights.slice( + &PendingCount(old_heights.sum_start().pending_count + 1), + Bias::Left, + &(), + ), &(), - ), - &(), - ); + ); + } + } + + drop(old_heights); + state.heights = new_heights; + } else { + state.heights = SumTree::new(); + for element in &mut state.elements { + let size = element.layout(item_constraint, cx); + state.heights.push(ElementHeight::Ready(size.y()), &()); } + state.last_layout_width = constraint.max.x(); } - drop(old_heights); - state.heights = new_heights; (constraint.max, ()) } From 3543aceff36b076d8df981474fb6eba66038cf24 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Aug 2021 16:41:48 +0200 Subject: [PATCH 057/204] Implement scrolling and painting for `List` Co-Authored-By: Nathan Sobo --- gpui/src/elements/list.rs | 129 +++++++++++++++++++++++++++++++------- 1 file changed, 105 insertions(+), 24 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index c506ef484a2932992de6ca9521d5d34c913d0ac2..1adf10abcf340e2ad279bfaf4a520b67f2587c05 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -1,7 +1,11 @@ use crate::{ - geometry::{rect::RectF, vector::Vector2F}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::json, sum_tree::{self, Bias, SumTree}, - Element, + DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use parking_lot::Mutex; use std::{ops::Range, sync::Arc}; @@ -19,6 +23,7 @@ struct StateInner { last_layout_width: f32, elements: Vec, heights: SumTree, + scroll_top: f32, } #[derive(Clone, Debug)] @@ -47,6 +52,25 @@ impl List { pub fn new(state: ListState) -> Self { Self { state } } + + fn scroll( + &self, + _: Vector2F, + delta: Vector2F, + precise: bool, + scroll_max: f32, + cx: &mut EventContext, + ) -> bool { + if !precise { + todo!("still need to handle non-precise scroll events from a mouse wheel"); + } + + let mut state = self.state.0.lock(); + state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max); + cx.notify(); + + true + } } impl Element for List { @@ -56,8 +80,8 @@ impl Element for List { fn layout( &mut self, - constraint: crate::SizeConstraint, - cx: &mut crate::LayoutContext, + constraint: SizeConstraint, + cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { let state = &mut *self.state.0.lock(); let mut item_constraint = constraint; @@ -100,34 +124,69 @@ impl Element for List { (constraint.max, ()) } - fn paint( - &mut self, - bounds: RectF, - layout: &mut Self::LayoutState, - cx: &mut crate::PaintContext, - ) -> Self::PaintState { - todo!() + fn paint(&mut self, bounds: RectF, _: &mut (), cx: &mut PaintContext) { + let state = &mut *self.state.0.lock(); + let visible_range = state.visible_range(bounds.height()); + + let mut item_top = { + let mut cursor = state.heights.cursor::(); + cursor.seek(&Count(visible_range.start), Bias::Right, &()); + cursor.sum_start().0 + }; + for element in &mut state.elements[visible_range] { + let origin = bounds.origin() + vec2f(0., item_top) - state.scroll_top; + element.paint(origin, cx); + item_top += element.size().y(); + } } fn dispatch_event( &mut self, - event: &crate::Event, + event: &Event, bounds: RectF, - layout: &mut Self::LayoutState, - paint: &mut Self::PaintState, - cx: &mut crate::EventContext, + _: &mut (), + _: &mut (), + cx: &mut EventContext, ) -> bool { - todo!() + let mut handled = false; + + let mut state = self.state.0.lock(); + let visible_range = state.visible_range(bounds.height()); + for item in &mut state.elements[visible_range] { + handled = item.dispatch_event(event, cx) || handled; + } + + match event { + Event::ScrollWheel { + position, + delta, + precise, + } => { + if bounds.contains_point(*position) { + let scroll_max = state.scroll_max(bounds.height()); + if self.scroll(*position, *delta, *precise, scroll_max, cx) { + handled = true; + } + } + } + _ => {} + } + + handled } - fn debug( - &self, - bounds: RectF, - layout: &Self::LayoutState, - paint: &Self::PaintState, - cx: &crate::DebugContext, - ) -> serde_json::Value { - todo!() + fn debug(&self, bounds: RectF, _: &(), _: &(), cx: &DebugContext) -> serde_json::Value { + let state = self.state.0.lock(); + let visible_range = state.visible_range(bounds.height()); + let visible_elements = state.elements[visible_range.clone()] + .iter() + .map(|e| e.debug(cx)) + .collect::>(); + json!({ + "visible_range": visible_range, + "visible_elements": visible_elements, + "scroll_top": state.scroll_top, + }) } } @@ -139,6 +198,7 @@ impl ListState { last_layout_width: 0., elements, heights, + scroll_top: 0., }))) } @@ -170,6 +230,21 @@ impl ListState { } } +impl StateInner { + fn visible_range(&self, height: f32) -> Range { + let mut cursor = self.heights.cursor::(); + cursor.seek(&Height(self.scroll_top), Bias::Right, &()); + let start_ix = cursor.sum_start().0; + cursor.seek(&Height(self.scroll_top + height), Bias::Left, &()); + let end_ix = cursor.sum_start().0; + start_ix..end_ix + 1 + } + + fn scroll_max(&self, height: f32) -> f32 { + self.heights.summary().height - height + } +} + impl ElementHeight { fn is_pending(&self) -> bool { matches!(self, ElementHeight::Pending) @@ -241,6 +316,12 @@ impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Height { } } +impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Height { + fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering { + self.0.partial_cmp(&other.0).unwrap() + } +} + #[cfg(test)] mod tests { use super::*; From 94e9a8332695fa71de0343552e5f7b97862020fc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Aug 2021 17:04:32 +0200 Subject: [PATCH 058/204] Give up on entities being `Send` and `Sync` Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 9 ++++++--- zed/src/chat_panel.rs | 3 ++- zed/src/test.rs | 2 +- zed/src/workspace.rs | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 9c96b41097e55bef3364c978ff1c01aa4694ecce..30783b35f58e69ad1efc2bc2abe25fb540e8f864 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -29,7 +29,7 @@ use std::{ time::Duration, }; -pub trait Entity: 'static + Send + Sync { +pub trait Entity: 'static { type Event; fn release(&mut self, _: &mut MutableAppContext) {} @@ -1738,7 +1738,7 @@ impl Debug for Effect { } } -pub trait AnyModel: Send + Sync { +pub trait AnyModel { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; fn release(&mut self, cx: &mut MutableAppContext); @@ -1761,7 +1761,7 @@ where } } -pub trait AnyView: Send + Sync { +pub trait AnyView { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; fn release(&mut self, cx: &mut MutableAppContext); @@ -2515,6 +2515,9 @@ pub struct WeakModelHandle { model_type: PhantomData, } +unsafe impl Send for WeakModelHandle {} +unsafe impl Sync for WeakModelHandle {} + impl WeakModelHandle { fn new(model_id: usize) -> Self { Self { diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index ec9f3f486ff6234e09dab63e865b474cd94819b1..57c2e79bd650173bf4e30a51a18cac7439ae028a 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -1,9 +1,10 @@ use super::channel::{Channel, ChannelList}; -use gpui::{Entity, ModelHandle, View}; +use gpui::{elements::*, Entity, ModelHandle, View}; pub struct ChatPanel { channel_list: ModelHandle, active_channel: Option>, + messages: ListState, } pub enum Event {} diff --git a/zed/src/test.rs b/zed/src/test.rs index 3576681cd33dac3c17e3a4f5d1953bfa41e1450e..6c861a31e5c846be04f2a79ac5e6315d4f420563 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -169,7 +169,7 @@ pub fn build_app_state(cx: &AppContext) -> Arc { pub struct Observer(PhantomData); -impl Entity for Observer { +impl Entity for Observer { type Event = (); } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index e5f4c95495530afe4bdbb1e31fa6e0836ca2bf48..35ed7fc467ddaca4685b3f8648f73f2c68d497a6 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -192,7 +192,7 @@ pub trait ItemHandle: Send + Sync { fn downgrade(&self) -> Box; } -pub trait WeakItemHandle: Send + Sync { +pub trait WeakItemHandle { fn file<'a>(&'a self, cx: &'a AppContext) -> Option<&'a File>; fn add_view( &self, @@ -203,7 +203,7 @@ pub trait WeakItemHandle: Send + Sync { fn alive(&self, cx: &AppContext) -> bool; } -pub trait ItemViewHandle: Send + Sync { +pub trait ItemViewHandle { fn title(&self, cx: &AppContext) -> String; fn entry_id(&self, cx: &AppContext) -> Option<(usize, Arc)>; fn boxed_clone(&self) -> Box; From a182db863f012c59207c6b5b1214ff0e6fc257a3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Aug 2021 17:29:46 +0200 Subject: [PATCH 059/204] WIP: Render dummy chat messages to test `List` Co-Authored-By: Nathan Sobo --- gpui/src/elements.rs | 47 ++++++++++++++++++++++++------------ zed/src/chat_panel.rs | 37 ++++++++++++++++++++++++---- zed/src/workspace.rs | 17 ++++++------- zed/src/workspace/sidebar.rs | 4 +-- 4 files changed, 73 insertions(+), 32 deletions(-) diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 1156a434e1243c185ca92de4b12a8a6103e4162f..c3053c9fa1655882dfbc75e3fb1c0a02132ec82c 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -155,25 +155,42 @@ impl AnyElement for Lifecycle { } fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) { - *self = if let Lifecycle::PostLayout { - mut element, - constraint, - size, - mut layout, - } = mem::take(self) - { - let bounds = RectF::new(origin, size); - let paint = element.paint(bounds, &mut layout, cx); + *self = match mem::take(self) { + Lifecycle::PostLayout { + mut element, + constraint, + size, + mut layout, + } => { + let bounds = RectF::new(origin, size); + let paint = element.paint(bounds, &mut layout, cx); + Lifecycle::PostPaint { + element, + constraint, + bounds, + layout, + paint, + } + } Lifecycle::PostPaint { - element, + mut element, constraint, bounds, - layout, - paint, + mut layout, + .. + } => { + let bounds = RectF::new(origin, bounds.size()); + let paint = element.paint(bounds, &mut layout, cx); + Lifecycle::PostPaint { + element, + constraint, + bounds, + layout, + paint, + } } - } else { - panic!("invalid element lifecycle state"); - }; + _ => panic!("invalid element lifecycle state"), + } } fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool { diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 57c2e79bd650173bf4e30a51a18cac7439ae028a..3ef6ed904ae0cf94c8f7deb2032d0bbf1740d1d9 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -1,14 +1,41 @@ +use crate::Settings; + use super::channel::{Channel, ChannelList}; -use gpui::{elements::*, Entity, ModelHandle, View}; +use gpui::{elements::*, Entity, ModelHandle, RenderContext, View, ViewContext}; +use postage::watch; pub struct ChatPanel { - channel_list: ModelHandle, - active_channel: Option>, + // channel_list: ModelHandle, + // active_channel: Option>, messages: ListState, } pub enum Event {} +impl ChatPanel { + pub fn new(settings: watch::Receiver) -> Self { + let settings = settings.borrow(); + let mut messages = Vec::new(); + for i in 0..1000 { + messages.push( + Container::new( + Label::new( + format!("This is message {}", i), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_style(&settings.theme.selector.label) + .boxed(), + ) + .boxed(), + ); + } + Self { + messages: ListState::new(messages), + } + } +} + impl Entity for ChatPanel { type Event = Event; } @@ -18,7 +45,7 @@ impl View for ChatPanel { "ChatPanel" } - fn render(&self, cx: &gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - todo!() + fn render(&self, cx: &RenderContext) -> gpui::ElementBox { + List::new(self.messages.clone()).boxed() } } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 35ed7fc467ddaca4685b3f8648f73f2c68d497a6..69baa2283d1d31249a44e68c531f7c48ef1c84ad 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -3,6 +3,7 @@ pub mod pane_group; pub mod sidebar; use crate::{ + chat_panel::ChatPanel, editor::{Buffer, Editor}, fs::Fs, language::LanguageRegistry, @@ -28,7 +29,7 @@ use log::error; pub use pane::*; pub use pane_group::*; use postage::watch; -use sidebar::{Side, Sidebar}; +use sidebar::{Side, Sidebar, ToggleSidebarItem}; use smol::prelude::*; use std::{ collections::{hash_map::Entry, HashMap, HashSet}, @@ -44,7 +45,6 @@ action!(ShareWorktree); action!(JoinWorktree, Arc); action!(Save); action!(DebugElements); -action!(ToggleSidebarItem, (Side, usize)); pub fn init(cx: &mut MutableAppContext) { cx.add_global_action(open); @@ -372,7 +372,8 @@ impl Workspace { let mut right_sidebar = Sidebar::new(Side::Right); right_sidebar.add_item( "icons/comment-16.svg", - cx.add_view(|_| ProjectBrowser).into(), + cx.add_view(|_| ChatPanel::new(app_state.settings.clone())) + .into(), ); right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); @@ -752,16 +753,12 @@ impl Workspace { } } - pub fn toggle_sidebar_item( - &mut self, - ToggleSidebarItem((side, item_ix)): &ToggleSidebarItem, - cx: &mut ViewContext, - ) { - let sidebar = match side { + pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext) { + let sidebar = match action.0.side { Side::Left => &mut self.left_sidebar, Side::Right => &mut self.right_sidebar, }; - sidebar.toggle_item(*item_ix); + sidebar.toggle_item(action.0.item_index); cx.notify(); } diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 1cd63bb2fb8fc458a264cb9681f222f95bd8d468..81807fd01a5ca621da7575065152b6cc14730cb0 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -28,8 +28,8 @@ action!(ToggleSidebarItem, ToggleArg); #[derive(Clone)] pub struct ToggleArg { - side: Side, - item_index: usize, + pub side: Side, + pub item_index: usize, } impl Sidebar { From d02eaf9e3a0cb66cb223994c3966e72572f665b8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Aug 2021 11:02:30 -0700 Subject: [PATCH 060/204] Fix scrolling in List element Co-Authored-By: Nathan Sobo --- gpui/src/elements/list.rs | 43 +++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 1adf10abcf340e2ad279bfaf4a520b67f2587c05..467f53fd4fdd64dbe74060a3682250eb4e696e2c 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -52,25 +52,6 @@ impl List { pub fn new(state: ListState) -> Self { Self { state } } - - fn scroll( - &self, - _: Vector2F, - delta: Vector2F, - precise: bool, - scroll_max: f32, - cx: &mut EventContext, - ) -> bool { - if !precise { - todo!("still need to handle non-precise scroll events from a mouse wheel"); - } - - let mut state = self.state.0.lock(); - state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max); - cx.notify(); - - true - } } impl Element for List { @@ -134,7 +115,7 @@ impl Element for List { cursor.sum_start().0 }; for element in &mut state.elements[visible_range] { - let origin = bounds.origin() + vec2f(0., item_top) - state.scroll_top; + let origin = bounds.origin() + vec2f(0., item_top - state.scroll_top); element.paint(origin, cx); item_top += element.size().y(); } @@ -163,8 +144,7 @@ impl Element for List { precise, } => { if bounds.contains_point(*position) { - let scroll_max = state.scroll_max(bounds.height()); - if self.scroll(*position, *delta, *precise, scroll_max, cx) { + if state.scroll(*position, *delta, *precise, bounds.height(), cx) { handled = true; } } @@ -240,8 +220,23 @@ impl StateInner { start_ix..end_ix + 1 } - fn scroll_max(&self, height: f32) -> f32 { - self.heights.summary().height - height + fn scroll( + &mut self, + _: Vector2F, + delta: Vector2F, + precise: bool, + height: f32, + cx: &mut EventContext, + ) -> bool { + if !precise { + todo!("still need to handle non-precise scroll events from a mouse wheel"); + } + + let scroll_max = self.heights.summary().height - height; + self.scroll_top = (self.scroll_top - delta.y()).max(0.0).min(scroll_max); + cx.notify(); + + true } } From 8e191f27d91fe90fccd324a55da342fd05412503 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Aug 2021 15:02:30 -0700 Subject: [PATCH 061/204] Simplify state associated with observations Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 293 +++++++++++++++++++++++++++++++----------------- 1 file changed, 192 insertions(+), 101 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 30783b35f58e69ad1efc2bc2abe25fb540e8f864..a40698a4dc6dfdbcb19e4c1f50c097a5b805ff6a 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -650,8 +650,7 @@ pub struct MutableAppContext { next_entity_id: usize, next_window_id: usize, subscriptions: HashMap>, - model_observations: HashMap>, - view_observations: HashMap>, + observations: HashMap>, presenters_and_platform_windows: HashMap>, Box)>, debug_elements_callbacks: HashMap crate::json::Value>>, @@ -690,8 +689,7 @@ impl MutableAppContext { next_entity_id: 0, next_window_id: 0, subscriptions: HashMap::new(), - model_observations: HashMap::new(), - view_observations: HashMap::new(), + observations: HashMap::new(), presenters_and_platform_windows: HashMap::new(), debug_elements_callbacks: HashMap::new(), foreground, @@ -879,6 +877,93 @@ impl MutableAppContext { ); } + pub fn subscribe_to_model(&mut self, handle: &ModelHandle, mut callback: F) + where + E: Entity, + E::Event: 'static, + F: 'static + FnMut(ModelHandle, &E::Event, &mut Self), + { + let emitter_handle = handle.downgrade(); + self.subscribe(handle, move |payload, cx| { + if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { + callback(emitter_handle, payload, cx); + } + }); + } + + pub fn subscribe_to_view(&mut self, handle: &ViewHandle, mut callback: F) + where + V: View, + V::Event: 'static, + F: 'static + FnMut(ViewHandle, &V::Event, &mut Self), + { + let emitter_handle = handle.downgrade(); + self.subscribe(handle, move |payload, cx| { + if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { + callback(emitter_handle, payload, cx); + } + }); + } + + pub fn observe_model(&mut self, handle: &ModelHandle, mut callback: F) + where + E: Entity, + E::Event: 'static, + F: 'static + FnMut(ModelHandle, &mut Self), + { + let emitter_handle = handle.downgrade(); + self.observe(handle, move |cx| { + if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { + callback(emitter_handle, cx); + } + }); + } + + pub fn observe_view(&mut self, handle: &ViewHandle, mut callback: F) + where + V: View, + V::Event: 'static, + F: 'static + FnMut(ViewHandle, &mut Self), + { + let emitter_handle = handle.downgrade(); + self.observe(handle, move |cx| { + if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { + callback(emitter_handle, cx); + } + }); + } + + pub fn subscribe(&mut self, handle: &impl Handle, mut callback: F) + where + E: Entity, + E::Event: 'static, + F: 'static + FnMut(&E::Event, &mut Self), + { + self.subscriptions + .entry(handle.id()) + .or_default() + .push(Subscription::Global { + callback: Box::new(move |payload, cx| { + let payload = payload.downcast_ref().expect("downcast is type safe"); + callback(payload, cx); + }), + }); + } + + pub fn observe(&mut self, handle: &impl Handle, callback: F) + where + E: Entity, + E::Event: 'static, + F: 'static + FnMut(&mut Self), + { + self.observations + .entry(handle.id()) + .or_default() + .push(Observation::Global { + callback: Box::new(callback), + }); + } + pub(crate) fn notify_view(&mut self, window_id: usize, view_id: usize) { self.pending_effects .push_back(Effect::ViewNotification { window_id, view_id }); @@ -1184,14 +1269,14 @@ impl MutableAppContext { for model_id in dropped_models { self.subscriptions.remove(&model_id); - self.model_observations.remove(&model_id); + self.observations.remove(&model_id); let mut model = self.cx.models.remove(&model_id).unwrap(); model.release(self); } for (window_id, view_id) in dropped_views { self.subscriptions.remove(&view_id); - self.model_observations.remove(&view_id); + self.observations.remove(&view_id); let mut view = self.cx.views.remove(&(window_id, view_id)).unwrap(); view.release(self); let change_focus_to = self.cx.windows.get_mut(&window_id).and_then(|window| { @@ -1281,6 +1366,10 @@ impl MutableAppContext { if let Some(subscriptions) = self.subscriptions.remove(&entity_id) { for mut subscription in subscriptions { let alive = match &mut subscription { + Subscription::Global { callback } => { + callback(payload.as_ref(), self); + true + } Subscription::FromModel { model_id, callback } => { if let Some(mut model) = self.cx.models.remove(model_id) { callback(model.as_any_mut(), payload.as_ref(), self, *model_id); @@ -1322,32 +1411,30 @@ impl MutableAppContext { } fn notify_model_observers(&mut self, observed_id: usize) { - if let Some(observations) = self.model_observations.remove(&observed_id) { + if let Some(observations) = self.observations.remove(&observed_id) { if self.cx.models.contains_key(&observed_id) { for mut observation in observations { let alive = match &mut observation { - ModelObservation::FromModel { model_id, callback } => { + Observation::Global { callback } => { + callback(self); + true + } + Observation::FromModel { model_id, callback } => { if let Some(mut model) = self.cx.models.remove(model_id) { - callback(model.as_any_mut(), observed_id, self, *model_id); + callback(model.as_any_mut(), self, *model_id); self.cx.models.insert(*model_id, model); true } else { false } } - ModelObservation::FromView { + Observation::FromView { window_id, view_id, callback, } => { if let Some(mut view) = self.cx.views.remove(&(*window_id, *view_id)) { - callback( - view.as_any_mut(), - observed_id, - self, - *window_id, - *view_id, - ); + callback(view.as_any_mut(), self, *window_id, *view_id); self.cx.views.insert((*window_id, *view_id), view); true } else { @@ -1357,7 +1444,7 @@ impl MutableAppContext { }; if alive { - self.model_observations + self.observations .entry(observed_id) .or_default() .push(observation); @@ -1367,44 +1454,55 @@ impl MutableAppContext { } } - fn notify_view_observers(&mut self, window_id: usize, view_id: usize) { - if let Some(window) = self.cx.windows.get_mut(&window_id) { + fn notify_view_observers(&mut self, observed_window_id: usize, observed_view_id: usize) { + if let Some(window) = self.cx.windows.get_mut(&observed_window_id) { window .invalidation .get_or_insert_with(Default::default) .updated - .insert(view_id); + .insert(observed_view_id); } - if let Some(observations) = self.view_observations.remove(&view_id) { - if self.cx.views.contains_key(&(window_id, view_id)) { + if let Some(observations) = self.observations.remove(&observed_view_id) { + if self + .cx + .views + .contains_key(&(observed_window_id, observed_view_id)) + { for mut observation in observations { - let alive = if let Some(mut view) = self - .cx - .views - .remove(&(observation.window_id, observation.view_id)) + if let Observation::FromView { + window_id: observing_window_id, + view_id: observing_view_id, + callback, + } = &mut observation { - (observation.callback)( - view.as_any_mut(), - view_id, - window_id, - self, - observation.window_id, - observation.view_id, - ); - self.cx + let alive = if let Some(mut view) = self + .cx .views - .insert((observation.window_id, observation.view_id), view); - true - } else { - false - }; + .remove(&(*observing_window_id, *observing_view_id)) + { + (callback)( + view.as_any_mut(), + self, + *observing_window_id, + *observing_view_id, + ); + self.cx + .views + .insert((*observing_window_id, *observing_view_id), view); + true + } else { + false + }; - if alive { - self.view_observations - .entry(view_id) - .or_default() - .push(observation); + if alive { + self.observations + .entry(observed_view_id) + .or_default() + .push(observation); + } + } else { + unreachable!() } } } @@ -1901,17 +1999,19 @@ impl<'a, T: Entity> ModelContext<'a, T> { S: Entity, F: 'static + FnMut(&mut T, ModelHandle, &mut ModelContext), { + let observed_handle = handle.downgrade(); self.app - .model_observations + .observations .entry(handle.model_id) .or_default() - .push(ModelObservation::FromModel { + .push(Observation::FromModel { model_id: self.model_id, - callback: Box::new(move |model, observed_id, app, model_id| { - let model = model.downcast_mut().expect("downcast is type safe"); - let observed = ModelHandle::new(observed_id, &app.cx.ref_counts); - let mut cx = ModelContext::new(app, model_id); - callback(model, observed, &mut cx); + callback: Box::new(move |model, app, model_id| { + if let Some(observed) = observed_handle.upgrade(app) { + let model = model.downcast_mut().expect("downcast is type safe"); + let mut cx = ModelContext::new(app, model_id); + callback(model, observed, &mut cx); + } }), }); } @@ -2173,18 +2273,20 @@ impl<'a, T: View> ViewContext<'a, T> { S: Entity, F: 'static + FnMut(&mut T, ModelHandle, &mut ViewContext), { + let observed_handle = handle.downgrade(); self.app - .model_observations + .observations .entry(handle.id()) .or_default() - .push(ModelObservation::FromView { + .push(Observation::FromView { window_id: self.window_id, view_id: self.view_id, - callback: Box::new(move |view, observed_id, app, window_id, view_id| { - let view = view.downcast_mut().expect("downcast is type safe"); - let observed = ModelHandle::new(observed_id, &app.cx.ref_counts); - let mut cx = ViewContext::new(app, window_id, view_id); - callback(view, observed, &mut cx); + callback: Box::new(move |view, app, window_id, view_id| { + if let Some(observed) = observed_handle.upgrade(app) { + let view = view.downcast_mut().expect("downcast is type safe"); + let mut cx = ViewContext::new(app, window_id, view_id); + callback(view, observed, &mut cx); + } }), }); } @@ -2194,30 +2296,21 @@ impl<'a, T: View> ViewContext<'a, T> { S: View, F: 'static + FnMut(&mut T, ViewHandle, &mut ViewContext), { + let observed_handle = handle.downgrade(); self.app - .view_observations + .observations .entry(handle.id()) .or_default() - .push(ViewObservation { + .push(Observation::FromView { window_id: self.window_id, view_id: self.view_id, - callback: Box::new( - move |view, - observed_view_id, - observed_window_id, - app, - observing_window_id, - observing_view_id| { + callback: Box::new(move |view, app, observing_window_id, observing_view_id| { + if let Some(observed) = observed_handle.upgrade(app) { let view = view.downcast_mut().expect("downcast is type safe"); - let observed_handle = ViewHandle::new( - observed_view_id, - observed_window_id, - &app.cx.ref_counts, - ); let mut cx = ViewContext::new(app, observing_window_id, observing_view_id); - callback(view, observed_handle, &mut cx); - }, - ), + callback(view, observed, &mut cx); + } + }), }); } @@ -2402,19 +2495,17 @@ impl ModelHandle { let (tx, mut rx) = mpsc::channel(1024); let mut cx = cx.cx.borrow_mut(); - self.update(&mut *cx, |_, cx| { - cx.observe(self, { - let mut tx = tx.clone(); - move |_, _, _| { - tx.blocking_send(()).ok(); - } - }); - cx.subscribe(self, { - let mut tx = tx.clone(); - move |_, _, _| { - tx.blocking_send(()).ok(); - } - }) + cx.observe_model(self, { + let mut tx = tx.clone(); + move |_, _| { + tx.blocking_send(()).ok(); + } + }); + cx.subscribe_to_model(self, { + let mut tx = tx.clone(); + move |_, _, _| { + tx.blocking_send(()).ok(); + } }); let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); @@ -3007,6 +3098,9 @@ impl RefCounts { } enum Subscription { + Global { + callback: Box, + }, FromModel { model_id: usize, callback: Box, @@ -3018,24 +3112,21 @@ enum Subscription { }, } -enum ModelObservation { +enum Observation { + Global { + callback: Box, + }, FromModel { model_id: usize, - callback: Box, + callback: Box, }, FromView { window_id: usize, view_id: usize, - callback: Box, + callback: Box, }, } -struct ViewObservation { - window_id: usize, - view_id: usize, - callback: Box, -} - #[cfg(test)] mod tests { use super::*; @@ -3099,7 +3190,7 @@ mod tests { assert_eq!(cx.cx.models.len(), 1); assert!(cx.subscriptions.is_empty()); - assert!(cx.model_observations.is_empty()); + assert!(cx.observations.is_empty()); } #[crate::test(self)] @@ -3233,7 +3324,7 @@ mod tests { assert_eq!(cx.cx.views.len(), 2); assert!(cx.subscriptions.is_empty()); - assert!(cx.model_observations.is_empty()); + assert!(cx.observations.is_empty()); } #[crate::test(self)] From 43bb38206f391a3f6f5721f69b01437bc296ad3c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Aug 2021 14:58:37 -0700 Subject: [PATCH 062/204] Add generic subscribe and observe methods to contexts Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 500 ++++++++++++++------------------------- zed/src/editor/buffer.rs | 10 +- 2 files changed, 188 insertions(+), 322 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index a40698a4dc6dfdbcb19e4c1f50c097a5b805ff6a..70116be8a8341fc84aaec8b666c2964f85264af4 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -649,8 +649,8 @@ pub struct MutableAppContext { keystroke_matcher: keymap::Matcher, next_entity_id: usize, next_window_id: usize, - subscriptions: HashMap>, - observations: HashMap>, + subscriptions: HashMap bool>>>, + observations: HashMap bool>>>, presenters_and_platform_windows: HashMap>, Box)>, debug_elements_callbacks: HashMap crate::json::Value>>, @@ -877,91 +877,71 @@ impl MutableAppContext { ); } - pub fn subscribe_to_model(&mut self, handle: &ModelHandle, mut callback: F) + pub fn subscribe(&mut self, handle: &H, mut callback: F) where E: Entity, E::Event: 'static, - F: 'static + FnMut(ModelHandle, &E::Event, &mut Self), + H: Handle, + F: 'static + FnMut(H, &E::Event, &mut Self), { - let emitter_handle = handle.downgrade(); - self.subscribe(handle, move |payload, cx| { - if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { - callback(emitter_handle, payload, cx); - } - }); - } - - pub fn subscribe_to_view(&mut self, handle: &ViewHandle, mut callback: F) - where - V: View, - V::Event: 'static, - F: 'static + FnMut(ViewHandle, &V::Event, &mut Self), - { - let emitter_handle = handle.downgrade(); - self.subscribe(handle, move |payload, cx| { - if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { - callback(emitter_handle, payload, cx); - } - }); + self.subscribe_internal(handle, move |handle, event, cx| { + callback(handle, event, cx); + true + }) } - pub fn observe_model(&mut self, handle: &ModelHandle, mut callback: F) + fn observe(&mut self, handle: &H, mut callback: F) where E: Entity, E::Event: 'static, - F: 'static + FnMut(ModelHandle, &mut Self), + H: Handle, + F: 'static + FnMut(H, &mut Self), { - let emitter_handle = handle.downgrade(); - self.observe(handle, move |cx| { - if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { - callback(emitter_handle, cx); - } - }); + self.observe_internal(handle, move |handle, cx| { + callback(handle, cx); + true + }) } - pub fn observe_view(&mut self, handle: &ViewHandle, mut callback: F) - where - V: View, - V::Event: 'static, - F: 'static + FnMut(ViewHandle, &mut Self), - { - let emitter_handle = handle.downgrade(); - self.observe(handle, move |cx| { - if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { - callback(emitter_handle, cx); - } - }); - } - - pub fn subscribe(&mut self, handle: &impl Handle, mut callback: F) + pub fn subscribe_internal(&mut self, handle: &H, mut callback: F) where E: Entity, E::Event: 'static, - F: 'static + FnMut(&E::Event, &mut Self), + H: Handle, + F: 'static + FnMut(H, &E::Event, &mut Self) -> bool, { + let emitter = handle.downgrade(); self.subscriptions .entry(handle.id()) .or_default() - .push(Subscription::Global { - callback: Box::new(move |payload, cx| { + .push(Box::new(move |payload, cx| { + if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { let payload = payload.downcast_ref().expect("downcast is type safe"); - callback(payload, cx); - }), - }); + callback(emitter, payload, cx) + } else { + false + } + })) } - pub fn observe(&mut self, handle: &impl Handle, callback: F) + fn observe_internal(&mut self, handle: &H, mut callback: F) where E: Entity, E::Event: 'static, - F: 'static + FnMut(&mut Self), + H: Handle, + F: 'static + FnMut(H, &mut Self) -> bool, { + let observed = handle.downgrade(); self.observations .entry(handle.id()) .or_default() - .push(Observation::Global { - callback: Box::new(callback), - }); + .push(Box::new(move |cx| { + if let Some(observed) = H::upgrade_from(&observed, cx) { + callback(observed, cx) + } else { + false + } + })) } pub(crate) fn notify_view(&mut self, window_id: usize, view_id: usize) { @@ -1363,91 +1343,29 @@ impl MutableAppContext { } fn emit_event(&mut self, entity_id: usize, payload: Box) { - if let Some(subscriptions) = self.subscriptions.remove(&entity_id) { - for mut subscription in subscriptions { - let alive = match &mut subscription { - Subscription::Global { callback } => { - callback(payload.as_ref(), self); - true - } - Subscription::FromModel { model_id, callback } => { - if let Some(mut model) = self.cx.models.remove(model_id) { - callback(model.as_any_mut(), payload.as_ref(), self, *model_id); - self.cx.models.insert(*model_id, model); - true - } else { - false - } - } - Subscription::FromView { - window_id, - view_id, - callback, - } => { - if let Some(mut view) = self.cx.views.remove(&(*window_id, *view_id)) { - callback( - view.as_any_mut(), - payload.as_ref(), - self, - *window_id, - *view_id, - ); - self.cx.views.insert((*window_id, *view_id), view); - true - } else { - false - } - } - }; - + if let Some(callbacks) = self.subscriptions.remove(&entity_id) { + for mut callback in callbacks { + let alive = callback(payload.as_ref(), self); if alive { self.subscriptions .entry(entity_id) .or_default() - .push(subscription); + .push(callback); } } } } fn notify_model_observers(&mut self, observed_id: usize) { - if let Some(observations) = self.observations.remove(&observed_id) { + if let Some(callbacks) = self.observations.remove(&observed_id) { if self.cx.models.contains_key(&observed_id) { - for mut observation in observations { - let alive = match &mut observation { - Observation::Global { callback } => { - callback(self); - true - } - Observation::FromModel { model_id, callback } => { - if let Some(mut model) = self.cx.models.remove(model_id) { - callback(model.as_any_mut(), self, *model_id); - self.cx.models.insert(*model_id, model); - true - } else { - false - } - } - Observation::FromView { - window_id, - view_id, - callback, - } => { - if let Some(mut view) = self.cx.views.remove(&(*window_id, *view_id)) { - callback(view.as_any_mut(), self, *window_id, *view_id); - self.cx.views.insert((*window_id, *view_id), view); - true - } else { - false - } - } - }; - + for mut callback in callbacks { + let alive = callback(self); if alive { self.observations .entry(observed_id) .or_default() - .push(observation); + .push(callback); } } } @@ -1463,46 +1381,19 @@ impl MutableAppContext { .insert(observed_view_id); } - if let Some(observations) = self.observations.remove(&observed_view_id) { + if let Some(callbacks) = self.observations.remove(&observed_view_id) { if self .cx .views .contains_key(&(observed_window_id, observed_view_id)) { - for mut observation in observations { - if let Observation::FromView { - window_id: observing_window_id, - view_id: observing_view_id, - callback, - } = &mut observation - { - let alive = if let Some(mut view) = self - .cx - .views - .remove(&(*observing_window_id, *observing_view_id)) - { - (callback)( - view.as_any_mut(), - self, - *observing_window_id, - *observing_view_id, - ); - self.cx - .views - .insert((*observing_window_id, *observing_view_id), view); - true - } else { - false - }; - - if alive { - self.observations - .entry(observed_view_id) - .or_default() - .push(observation); - } - } else { - unreachable!() + for mut callback in callbacks { + let alive = callback(self); + if alive { + self.observations + .entry(observed_view_id) + .or_default() + .push(callback); } } } @@ -1967,26 +1858,6 @@ impl<'a, T: Entity> ModelContext<'a, T> { self.app.add_model(build_model) } - pub fn subscribe(&mut self, handle: &ModelHandle, mut callback: F) - where - S::Event: 'static, - F: 'static + FnMut(&mut T, &S::Event, &mut ModelContext), - { - self.app - .subscriptions - .entry(handle.model_id) - .or_default() - .push(Subscription::FromModel { - model_id: self.model_id, - callback: Box::new(move |model, payload, app, model_id| { - let model = model.downcast_mut().expect("downcast is type safe"); - let payload = payload.downcast_ref().expect("downcast is type safe"); - let mut cx = ModelContext::new(app, model_id); - callback(model, payload, &mut cx); - }), - }); - } - pub fn emit(&mut self, payload: T::Event) { self.app.pending_effects.push_back(Effect::Event { entity_id: self.model_id, @@ -1994,36 +1865,51 @@ impl<'a, T: Entity> ModelContext<'a, T> { }); } - pub fn observe(&mut self, handle: &ModelHandle, mut callback: F) - where - S: Entity, - F: 'static + FnMut(&mut T, ModelHandle, &mut ModelContext), - { - let observed_handle = handle.downgrade(); + pub fn notify(&mut self) { self.app - .observations - .entry(handle.model_id) - .or_default() - .push(Observation::FromModel { + .pending_effects + .push_back(Effect::ModelNotification { model_id: self.model_id, - callback: Box::new(move |model, app, model_id| { - if let Some(observed) = observed_handle.upgrade(app) { - let model = model.downcast_mut().expect("downcast is type safe"); - let mut cx = ModelContext::new(app, model_id); - callback(model, observed, &mut cx); - } - }), }); } - pub fn notify(&mut self) { + pub fn subscribe(&mut self, handle: &ModelHandle, mut callback: F) + where + S::Event: 'static, + F: 'static + FnMut(&mut T, ModelHandle, &S::Event, &mut ModelContext), + { + let subscriber = self.handle().downgrade(); self.app - .pending_effects - .push_back(Effect::ModelNotification { - model_id: self.model_id, + .subscribe_internal(handle, move |emitter, event, cx| { + if let Some(subscriber) = subscriber.upgrade(cx) { + subscriber.update(cx, |subscriber, cx| { + callback(subscriber, emitter, event, cx); + }); + true + } else { + false + } }); } + pub fn observe(&mut self, handle: &ModelHandle, mut callback: F) + where + S: Entity, + F: 'static + FnMut(&mut T, ModelHandle, &mut ModelContext), + { + let observer = self.handle().downgrade(); + self.app.observe_internal(handle, move |observed, cx| { + if let Some(observer) = observer.upgrade(cx) { + observer.update(cx, |observer, cx| { + callback(observer, observed, cx); + }); + true + } else { + false + } + }); + } + pub fn handle(&self) -> ModelHandle { ModelHandle::new(self.model_id, &self.app.cx.ref_counts) } @@ -2211,107 +2097,85 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.add_option_view(self.window_id, build_view) } - pub fn subscribe_to_model(&mut self, handle: &ModelHandle, mut callback: F) + pub fn subscribe_to_model(&mut self, handle: &ModelHandle, callback: F) where E: Entity, E::Event: 'static, F: 'static + FnMut(&mut T, ModelHandle, &E::Event, &mut ViewContext), { - let emitter_handle = handle.downgrade(); - self.subscribe(handle, move |model, payload, cx| { - if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { - callback(model, emitter_handle, payload, cx); - } - }); + self.subscribe(handle, callback) } - pub fn subscribe_to_view(&mut self, handle: &ViewHandle, mut callback: F) + pub fn subscribe_to_view(&mut self, handle: &ViewHandle, callback: F) where V: View, V::Event: 'static, F: 'static + FnMut(&mut T, ViewHandle, &V::Event, &mut ViewContext), { - let emitter_handle = handle.downgrade(); - self.subscribe(handle, move |view, payload, cx| { - if let Some(emitter_handle) = emitter_handle.upgrade(cx.as_ref()) { - callback(view, emitter_handle, payload, cx); - } - }); + self.subscribe(handle, callback) } - pub fn subscribe(&mut self, handle: &impl Handle, mut callback: F) + pub fn observe_model(&mut self, handle: &ModelHandle, callback: F) where - E: Entity, - E::Event: 'static, - F: 'static + FnMut(&mut T, &E::Event, &mut ViewContext), + S: Entity, + F: 'static + FnMut(&mut T, ModelHandle, &mut ViewContext), { - self.app - .subscriptions - .entry(handle.id()) - .or_default() - .push(Subscription::FromView { - window_id: self.window_id, - view_id: self.view_id, - callback: Box::new(move |entity, payload, app, window_id, view_id| { - let entity = entity.downcast_mut().expect("downcast is type safe"); - let payload = payload.downcast_ref().expect("downcast is type safe"); - let mut cx = ViewContext::new(app, window_id, view_id); - callback(entity, payload, &mut cx); - }), - }); + self.observe(handle, callback) } - pub fn emit(&mut self, payload: T::Event) { - self.app.pending_effects.push_back(Effect::Event { - entity_id: self.view_id, - payload: Box::new(payload), - }); + pub fn observe_view(&mut self, handle: &ViewHandle, callback: F) + where + S: View, + F: 'static + FnMut(&mut T, ViewHandle, &mut ViewContext), + { + self.observe(handle, callback) } - pub fn observe_model(&mut self, handle: &ModelHandle, mut callback: F) + pub fn subscribe(&mut self, handle: &H, mut callback: F) where - S: Entity, - F: 'static + FnMut(&mut T, ModelHandle, &mut ViewContext), + E: Entity, + E::Event: 'static, + H: Handle, + F: 'static + FnMut(&mut T, H, &E::Event, &mut ViewContext), { - let observed_handle = handle.downgrade(); + let subscriber = self.handle().downgrade(); self.app - .observations - .entry(handle.id()) - .or_default() - .push(Observation::FromView { - window_id: self.window_id, - view_id: self.view_id, - callback: Box::new(move |view, app, window_id, view_id| { - if let Some(observed) = observed_handle.upgrade(app) { - let view = view.downcast_mut().expect("downcast is type safe"); - let mut cx = ViewContext::new(app, window_id, view_id); - callback(view, observed, &mut cx); - } - }), + .subscribe_internal(handle, move |emitter, event, cx| { + if let Some(subscriber) = subscriber.upgrade(cx) { + subscriber.update(cx, |subscriber, cx| { + callback(subscriber, emitter, event, cx); + }); + true + } else { + false + } }); } - pub fn observe_view(&mut self, handle: &ViewHandle, mut callback: F) + fn observe(&mut self, handle: &H, mut callback: F) where - S: View, - F: 'static + FnMut(&mut T, ViewHandle, &mut ViewContext), + E: Entity, + H: Handle, + F: 'static + FnMut(&mut T, H, &mut ViewContext), { - let observed_handle = handle.downgrade(); - self.app - .observations - .entry(handle.id()) - .or_default() - .push(Observation::FromView { - window_id: self.window_id, - view_id: self.view_id, - callback: Box::new(move |view, app, observing_window_id, observing_view_id| { - if let Some(observed) = observed_handle.upgrade(app) { - let view = view.downcast_mut().expect("downcast is type safe"); - let mut cx = ViewContext::new(app, observing_window_id, observing_view_id); - callback(view, observed, &mut cx); - } - }), - }); + let observer = self.handle().downgrade(); + self.app.observe_internal(handle, move |observed, cx| { + if let Some(observer) = observer.upgrade(cx) { + observer.update(cx, |observer, cx| { + callback(observer, observed, cx); + }); + true + } else { + false + } + }); + } + + pub fn emit(&mut self, payload: T::Event) { + self.app.pending_effects.push_back(Effect::Event { + entity_id: self.view_id, + payload: Box::new(payload), + }); } pub fn notify(&mut self) { @@ -2433,8 +2297,13 @@ impl UpdateView for ViewContext<'_, V> { } pub trait Handle { + type Weak: 'static; fn id(&self) -> usize; fn location(&self) -> EntityLocation; + fn downgrade(&self) -> Self::Weak; + fn upgrade_from(weak: &Self::Weak, cx: &AppContext) -> Option + where + Self: Sized; } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] @@ -2495,13 +2364,13 @@ impl ModelHandle { let (tx, mut rx) = mpsc::channel(1024); let mut cx = cx.cx.borrow_mut(); - cx.observe_model(self, { + cx.observe(self, { let mut tx = tx.clone(); move |_, _| { tx.blocking_send(()).ok(); } }); - cx.subscribe_to_model(self, { + cx.subscribe(self, { let mut tx = tx.clone(); move |_, _, _| { tx.blocking_send(()).ok(); @@ -2592,7 +2461,9 @@ impl Drop for ModelHandle { } } -impl Handle for ModelHandle { +impl Handle for ModelHandle { + type Weak = WeakModelHandle; + fn id(&self) -> usize { self.model_id } @@ -2600,7 +2471,19 @@ impl Handle for ModelHandle { fn location(&self) -> EntityLocation { EntityLocation::Model(self.model_id) } + + fn downgrade(&self) -> Self::Weak { + self.downgrade() + } + + fn upgrade_from(weak: &Self::Weak, cx: &AppContext) -> Option + where + Self: Sized, + { + weak.upgrade(cx) + } } + pub struct WeakModelHandle { model_id: usize, model_type: PhantomData, @@ -2718,9 +2601,9 @@ impl ViewHandle { } }); - cx.subscribe(self, { + cx.subscribe_to_view(self, { let mut tx = tx.clone(); - move |_, _, _| { + move |_, _, _, _| { tx.blocking_send(()).ok(); } }) @@ -2801,7 +2684,9 @@ impl Drop for ViewHandle { } } -impl Handle for ViewHandle { +impl Handle for ViewHandle { + type Weak = WeakViewHandle; + fn id(&self) -> usize { self.view_id } @@ -2809,6 +2694,17 @@ impl Handle for ViewHandle { fn location(&self) -> EntityLocation { EntityLocation::View(self.window_id, self.view_id) } + + fn downgrade(&self) -> Self::Weak { + self.downgrade() + } + + fn upgrade_from(weak: &Self::Weak, cx: &AppContext) -> Option + where + Self: Sized, + { + weak.upgrade(cx) + } } pub struct AnyViewHandle { @@ -3097,36 +2993,6 @@ impl RefCounts { } } -enum Subscription { - Global { - callback: Box, - }, - FromModel { - model_id: usize, - callback: Box, - }, - FromView { - window_id: usize, - view_id: usize, - callback: Box, - }, -} - -enum Observation { - Global { - callback: Box, - }, - FromModel { - model_id: usize, - callback: Box, - }, - FromView { - window_id: usize, - view_id: usize, - callback: Box, - }, -} - #[cfg(test)] mod tests { use super::*; @@ -3151,7 +3017,7 @@ mod tests { cx.observe(other, |me, _, _| { me.events.push("notified".into()); }); - cx.subscribe(other, |me, event, _| { + cx.subscribe(other, |me, _, event, _| { me.events.push(format!("observed event {}", event)); }); } @@ -3209,10 +3075,10 @@ mod tests { let handle_2b = handle_2.clone(); handle_1.update(cx, |_, c| { - c.subscribe(&handle_2, move |model: &mut Model, event, c| { + c.subscribe(&handle_2, move |model: &mut Model, _, event, c| { model.events.push(*event); - c.subscribe(&handle_2b, |model, event, _| { + c.subscribe(&handle_2b, |model, _, event, _| { model.events.push(*event * 2); }); }); @@ -3519,7 +3385,7 @@ mod tests { cx.subscribe_to_model(&observed_model, |_, _, _, _| {}); }); observing_model.update(cx, |_, cx| { - cx.subscribe(&observed_model, |_, _, _| {}); + cx.subscribe(&observed_model, |_, _, _, _| {}); }); cx.update(|| { diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index eb57328a1e3affecebff7a0d4b09a6da8c930704..624c84697aebab163a5457f87a8d247f64a4997d 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -2940,11 +2940,11 @@ mod tests { let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); let buffer_ops = buffer1.update(cx, |buffer, cx| { let buffer_1_events = buffer_1_events.clone(); - cx.subscribe(&buffer1, move |_, event, _| { + cx.subscribe(&buffer1, move |_, _, event, _| { buffer_1_events.borrow_mut().push(event.clone()) }); let buffer_2_events = buffer_2_events.clone(); - cx.subscribe(&buffer2, move |_, event, _| { + cx.subscribe(&buffer2, move |_, _, event, _| { buffer_2_events.borrow_mut().push(event.clone()) }); @@ -3380,7 +3380,7 @@ mod tests { buffer1.update(&mut cx, |buffer, cx| { cx.subscribe(&buffer1, { let events = events.clone(); - move |_, event, _| events.borrow_mut().push(event.clone()) + move |_, _, event, _| events.borrow_mut().push(event.clone()) }); assert!(!buffer.is_dirty()); @@ -3436,7 +3436,7 @@ mod tests { buffer2.update(&mut cx, |_, cx| { cx.subscribe(&buffer2, { let events = events.clone(); - move |_, event, _| events.borrow_mut().push(event.clone()) + move |_, _, event, _| events.borrow_mut().push(event.clone()) }); }); @@ -3456,7 +3456,7 @@ mod tests { buffer3.update(&mut cx, |_, cx| { cx.subscribe(&buffer3, { let events = events.clone(); - move |_, event, _| events.borrow_mut().push(event.clone()) + move |_, _, event, _| events.borrow_mut().push(event.clone()) }); }); From 5ecedd894d4538adbc025e9616bab8c8e345da74 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Aug 2021 15:02:42 -0700 Subject: [PATCH 063/204] Add ChannelList to AppState --- gpui/src/elements/list.rs | 2 ++ server/src/rpc.rs | 18 ++++++------ zed/src/channel.rs | 43 +++++++++++++++++------------ zed/src/chat_panel.rs | 58 +++++++++++++++++++++++---------------- zed/src/editor.rs | 11 ++++---- zed/src/editor/buffer.rs | 13 +++++---- zed/src/file_finder.rs | 8 +++--- zed/src/lib.rs | 4 ++- zed/src/main.rs | 24 ++++++++-------- zed/src/test.rs | 16 ++++++++--- zed/src/workspace.rs | 24 ++++++++++------ zed/src/worktree.rs | 6 ++-- 12 files changed, 134 insertions(+), 93 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 467f53fd4fdd64dbe74060a3682250eb4e696e2c..d6101a59512b1f256f2e6f6438a77a286929e33c 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -106,6 +106,7 @@ impl Element for List { } fn paint(&mut self, bounds: RectF, _: &mut (), cx: &mut PaintContext) { + cx.scene.push_layer(Some(bounds)); let state = &mut *self.state.0.lock(); let visible_range = state.visible_range(bounds.height()); @@ -119,6 +120,7 @@ impl Element for List { element.paint(origin, cx); item_top += element.size().y(); } + cx.scene.pop_layer(); } fn dispatch_event( diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 5b907df01ec9337684723bb82e83b9d76e3c32d0..f489bfb57debddc255eee2e9da06cde6f011c264 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1425,12 +1425,13 @@ mod tests { .await .unwrap(); - let channels_a = ChannelList::new(client_a, &mut cx_a.to_async()) - .await - .unwrap(); + let channels_a = cx_a.add_model(|cx| ChannelList::new(client_a, cx)); + channels_a + .condition(&mut cx_a, |list, _| list.available_channels().is_some()) + .await; channels_a.read_with(&cx_a, |list, _| { assert_eq!( - list.available_channels(), + list.available_channels().unwrap(), &[ChannelDetails { id: channel_id.to_proto(), name: "test-channel".to_string() @@ -1448,12 +1449,13 @@ mod tests { }) .await; - let channels_b = ChannelList::new(client_b, &mut cx_b.to_async()) - .await - .unwrap(); + let channels_b = cx_b.add_model(|cx| ChannelList::new(client_b, cx)); + channels_b + .condition(&mut cx_b, |list, _| list.available_channels().is_some()) + .await; channels_b.read_with(&cx_b, |list, _| { assert_eq!( - list.available_channels(), + list.available_channels().unwrap(), &[ChannelDetails { id: channel_id.to_proto(), name: "test-channel".to_string() diff --git a/zed/src/channel.rs b/zed/src/channel.rs index fe683b728c035fa18d5cb071351f51cc17ef4baf..efb51eb239adededbbd65013cf9598530f26a0d0 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -3,9 +3,7 @@ use crate::{ util::log_async_errors, }; use anyhow::{anyhow, Context, Result}; -use gpui::{ - AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle, -}; +use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle}; use std::{ cmp::Ordering, collections::{hash_map, BTreeSet, HashMap}, @@ -17,7 +15,7 @@ use zrpc::{ }; pub struct ChannelList { - available_channels: Vec, + available_channels: Option>, channels: HashMap>, rpc: Arc, } @@ -55,21 +53,32 @@ impl Entity for ChannelList { } impl ChannelList { - pub async fn new(rpc: Arc, cx: &mut AsyncAppContext) -> Result> { - let response = rpc - .request(proto::GetChannels {}) - .await - .context("failed to fetch available channels")?; - - Ok(cx.add_model(|_| Self { - available_channels: response.channels.into_iter().map(Into::into).collect(), + pub fn new(rpc: Arc, cx: &mut ModelContext) -> Self { + cx.spawn(|this, mut cx| { + let rpc = rpc.clone(); + log_async_errors(async move { + let response = rpc + .request(proto::GetChannels {}) + .await + .context("failed to fetch available channels")?; + this.update(&mut cx, |this, cx| { + this.available_channels = + Some(response.channels.into_iter().map(Into::into).collect()); + cx.notify(); + }); + Ok(()) + }) + }) + .detach(); + Self { + available_channels: None, channels: Default::default(), rpc, - })) + } } - pub fn available_channels(&self) -> &[ChannelDetails] { - &self.available_channels + pub fn available_channels(&self) -> Option<&[ChannelDetails]> { + self.available_channels.as_ref().map(Vec::as_slice) } pub fn get_channel( @@ -82,8 +91,8 @@ impl ChannelList { hash_map::Entry::Vacant(entry) => { if let Some(details) = self .available_channels - .iter() - .find(|details| details.id == id) + .as_ref() + .and_then(|channels| channels.iter().find(|details| details.id == id)) { let rpc = self.rpc.clone(); let channel = cx.add_model(|cx| Channel::new(details.clone(), rpc, cx)); diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 3ef6ed904ae0cf94c8f7deb2032d0bbf1740d1d9..86a7762fac3b33146f078bf912fb25ee30a8d5d6 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -1,38 +1,48 @@ -use crate::Settings; - -use super::channel::{Channel, ChannelList}; +use super::{ + channel::{Channel, ChannelList}, + Settings, +}; use gpui::{elements::*, Entity, ModelHandle, RenderContext, View, ViewContext}; use postage::watch; pub struct ChatPanel { - // channel_list: ModelHandle, - // active_channel: Option>, + channel_list: ModelHandle, + active_channel: Option>, + // active_channel_subscription: Subscription, messages: ListState, } pub enum Event {} impl ChatPanel { - pub fn new(settings: watch::Receiver) -> Self { - let settings = settings.borrow(); - let mut messages = Vec::new(); - for i in 0..1000 { - messages.push( - Container::new( - Label::new( - format!("This is message {}", i), - settings.ui_font_family, - settings.ui_font_size, - ) - .with_style(&settings.theme.selector.label) - .boxed(), - ) - .boxed(), - ); - } - Self { - messages: ListState::new(messages), + pub fn new( + channel_list: ModelHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + let mut this = Self { + channel_list, + messages: ListState::new(Vec::new()), + active_channel: None, + }; + let channel = this.channel_list.update(cx, |list, cx| { + if let Some(channel_id) = list + .available_channels() + .and_then(|channels| channels.first()) + .map(|details| details.id) + { + return list.get_channel(channel_id, cx); + } + None + }); + if let Some(channel) = channel { + this.set_active_channel(channel); } + this + } + + pub fn set_active_channel(&mut self, channel: ModelHandle) { + // } } diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 829233b3cf74a19d4579a2ba9dfadf283ba1c7de..49259f554cb6a140dce5d71a0103a3f2f891de37 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -2596,8 +2596,9 @@ mod tests { use super::*; use crate::{ editor::Point, + language::LanguageRegistry, settings, - test::{build_app_state, sample_text}, + test::{build_settings, sample_text}, }; use buffer::History; use unindent::Unindent; @@ -4120,8 +4121,9 @@ mod tests { #[gpui::test] async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) { - let app_state = cx.read(build_app_state); - let lang = app_state.languages.select_language("z.rs"); + let settings = cx.read(build_settings); + let languages = LanguageRegistry::new(); + let lang = languages.select_language("z.rs"); let text = r#" use mod1::mod2::{mod3, mod4}; @@ -4134,8 +4136,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, app_state.settings.clone(), cx)); + let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings.clone(), cx)); view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing()) .await; diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 624c84697aebab163a5457f87a8d247f64a4997d..07ea5bacdc243c78ac4e9fe29f95814a40efab7c 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -2641,7 +2641,7 @@ impl<'a> Into for &'a Operation { }, ), #[cfg(test)] - Operation::Test(_) => unimplemented!() + Operation::Test(_) => unimplemented!(), }), } } @@ -2895,7 +2895,8 @@ mod tests { use super::*; use crate::{ fs::RealFs, - test::{build_app_state, temp_tree}, + language::LanguageRegistry, + test::temp_tree, util::RandomCharIter, worktree::{Worktree, WorktreeHandle as _}, }; @@ -3825,8 +3826,8 @@ mod tests { #[gpui::test] async fn test_reparse(mut cx: gpui::TestAppContext) { - let app_state = cx.read(build_app_state); - let rust_lang = app_state.languages.select_language("test.rs"); + let languages = LanguageRegistry::new(); + let rust_lang = languages.select_language("test.rs"); assert!(rust_lang.is_some()); let buffer = cx.add_model(|cx| { @@ -3966,8 +3967,8 @@ mod tests { async fn test_enclosing_bracket_ranges(mut cx: gpui::TestAppContext) { use unindent::Unindent as _; - let app_state = cx.read(build_app_state); - let rust_lang = app_state.languages.select_language("test.rs"); + let languages = LanguageRegistry::new(); + let rust_lang = languages.select_language("test.rs"); assert!(rust_lang.is_some()); let buffer = cx.add_model(|cx| { diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 4d4cf13013d3fcc89fc5bc4a16cd658bee186ea4..50bddb37171e7c793f6cc26aa63443c96a0cdb2b 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -455,7 +455,7 @@ mod tests { editor::init(cx); }); - let app_state = cx.read(build_app_state); + let app_state = cx.update(build_app_state); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { @@ -515,7 +515,7 @@ mod tests { ) .await; - let mut app_state = cx.read(build_app_state); + let mut app_state = cx.update(build_app_state); Arc::get_mut(&mut app_state).unwrap().fs = fs; let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); @@ -577,7 +577,7 @@ mod tests { fs::create_dir(&dir_path).unwrap(); fs::write(&file_path, "").unwrap(); - let app_state = cx.read(build_app_state); + let app_state = cx.update(build_app_state); let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { @@ -624,7 +624,7 @@ mod tests { "dir2": { "a.txt": "" } })); - let app_state = cx.read(build_app_state); + let app_state = cx.update(build_app_state); let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace diff --git a/zed/src/lib.rs b/zed/src/lib.rs index d76a308e386d9dc8f133d805d91777e69ad0e94d..2290d5cf28aa476a65658ea35665a6d67fcd4483 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -19,7 +19,8 @@ mod util; pub mod workspace; pub mod worktree; -use gpui::action; +use channel::ChannelList; +use gpui::{action, ModelHandle}; pub use settings::Settings; use parking_lot::Mutex; @@ -36,6 +37,7 @@ pub struct AppState { pub themes: Arc, pub rpc: Arc, pub fs: Arc, + pub channel_list: ModelHandle, } pub fn init(cx: &mut gpui::MutableAppContext) { diff --git a/zed/src/main.rs b/zed/src/main.rs index c35b2bf91507f7723e6f8ea40d3973e3c3d5183a..e3e365d15e888d70983c19cb0eae4917bfd2e5eb 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -7,7 +7,9 @@ use parking_lot::Mutex; use simplelog::SimpleLogger; use std::{fs, path::PathBuf, sync::Arc}; use zed::{ - self, assets, editor, file_finder, + self, assets, + channel::ChannelList, + editor, file_finder, fs::RealFs, language, menus, rpc, settings, theme_selector, workspace::{self, OpenParams, OpenPaths}, @@ -25,17 +27,17 @@ fn main() { let languages = Arc::new(language::LanguageRegistry::new()); languages.set_theme(&settings.borrow().theme); - let app_state = AppState { - languages: languages.clone(), - settings_tx: Arc::new(Mutex::new(settings_tx)), - settings, - themes, - rpc: rpc::Client::new(), - fs: Arc::new(RealFs), - }; - app.run(move |cx| { - let app_state = Arc::new(app_state); + let rpc = rpc::Client::new(); + let app_state = Arc::new(AppState { + languages: languages.clone(), + settings_tx: Arc::new(Mutex::new(settings_tx)), + settings, + themes, + channel_list: cx.add_model(|cx| ChannelList::new(rpc.clone(), cx)), + rpc, + fs: Arc::new(RealFs), + }); zed::init(cx); workspace::init(cx); diff --git a/zed/src/test.rs b/zed/src/test.rs index 6c861a31e5c846be04f2a79ac5e6315d4f420563..9263e0750f3e34cdef236f9d3f43c6aecba76d0e 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -1,13 +1,15 @@ use crate::{ + channel::ChannelList, fs::RealFs, language::LanguageRegistry, rpc, settings::{self, ThemeRegistry}, time::ReplicaId, - AppState, + AppState, Settings, }; -use gpui::{AppContext, Entity, ModelHandle}; +use gpui::{AppContext, Entity, ModelHandle, MutableAppContext}; use parking_lot::Mutex; +use postage::watch; use smol::channel; use std::{ marker::PhantomData, @@ -153,16 +155,22 @@ fn write_tree(path: &Path, tree: serde_json::Value) { } } -pub fn build_app_state(cx: &AppContext) -> Arc { +pub fn build_settings(cx: &AppContext) -> watch::Receiver { + settings::channel(&cx.font_cache()).unwrap().1 +} + +pub fn build_app_state(cx: &mut MutableAppContext) -> Arc { let (settings_tx, settings) = settings::channel(&cx.font_cache()).unwrap(); let languages = Arc::new(LanguageRegistry::new()); let themes = ThemeRegistry::new(()); + let rpc = rpc::Client::new(); Arc::new(AppState { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, themes, languages: languages.clone(), - rpc: rpc::Client::new(), + channel_list: cx.add_model(|cx| ChannelList::new(rpc.clone(), cx)), + rpc, fs: Arc::new(RealFs), }) } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 69baa2283d1d31249a44e68c531f7c48ef1c84ad..0dffa81be921798113a5c7f49a2ec1faedda2ae0 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -372,8 +372,14 @@ impl Workspace { let mut right_sidebar = Sidebar::new(Side::Right); right_sidebar.add_item( "icons/comment-16.svg", - cx.add_view(|_| ChatPanel::new(app_state.settings.clone())) - .into(), + cx.add_view(|cx| { + ChatPanel::new( + app_state.channel_list.clone(), + app_state.settings.clone(), + cx, + ) + }) + .into(), ); right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); @@ -1018,7 +1024,7 @@ mod tests { #[gpui::test] async fn test_open_paths_action(mut cx: gpui::TestAppContext) { - let app_state = cx.read(build_app_state); + let app_state = cx.update(build_app_state); let dir = temp_tree(json!({ "a": { "aa": null, @@ -1091,7 +1097,7 @@ mod tests { }, })); - let app_state = cx.read(build_app_state); + let app_state = cx.update(build_app_state); let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace @@ -1195,7 +1201,7 @@ mod tests { fs.insert_file("/dir1/a.txt", "".into()).await.unwrap(); fs.insert_file("/dir2/b.txt", "".into()).await.unwrap(); - let mut app_state = cx.read(build_app_state); + let mut app_state = cx.update(build_app_state); Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(fs); let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); @@ -1264,7 +1270,7 @@ mod tests { "a.txt": "", })); - let app_state = cx.read(build_app_state); + let app_state = cx.update(build_app_state); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { @@ -1309,7 +1315,7 @@ mod tests { #[gpui::test] async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) { let dir = TempDir::new("test-new-file").unwrap(); - let app_state = cx.read(build_app_state); + let app_state = cx.update(build_app_state); let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { @@ -1408,7 +1414,7 @@ mod tests { async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) { cx.update(init); - let app_state = cx.read(build_app_state); + let app_state = cx.update(build_app_state); cx.dispatch_global_action(OpenNew(app_state)); let window_id = *cx.window_ids().first().unwrap(); let workspace = cx.root_view::(window_id).unwrap(); @@ -1454,7 +1460,7 @@ mod tests { }, })); - let app_state = cx.read(build_app_state); + let app_state = cx.update(build_app_state); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 6c7ba34a15914e3bc75c3b44fc7220e73e928b46..e121bc5d14e3b2b9469619af0af34f7e38c9ae4c 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -2640,13 +2640,12 @@ mod tests { #[gpui::test] async fn test_save_file(mut cx: gpui::TestAppContext) { - let app_state = cx.read(build_app_state); let dir = temp_tree(json!({ "file1": "the old contents", })); let tree = Worktree::open_local( dir.path(), - app_state.languages.clone(), + Arc::new(LanguageRegistry::new()), Arc::new(RealFs), &mut cx.to_async(), ) @@ -2668,7 +2667,6 @@ mod tests { #[gpui::test] async fn test_save_in_single_file_worktree(mut cx: gpui::TestAppContext) { - let app_state = cx.read(build_app_state); let dir = temp_tree(json!({ "file1": "the old contents", })); @@ -2676,7 +2674,7 @@ mod tests { let tree = Worktree::open_local( file_path.clone(), - app_state.languages.clone(), + Arc::new(LanguageRegistry::new()), Arc::new(RealFs), &mut cx.to_async(), ) From fa0739ee2ef98fba1f0ea25aecc6d000f8e64676 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Aug 2021 15:07:50 -0700 Subject: [PATCH 064/204] Remove model and view specific observe/subscribe methods Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 56 ++++++++------------------------------- zed/src/editor.rs | 6 ++--- zed/src/file_finder.rs | 6 ++--- zed/src/theme_selector.rs | 4 +-- zed/src/workspace.rs | 10 +++---- 5 files changed, 24 insertions(+), 58 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 70116be8a8341fc84aaec8b666c2964f85264af4..85b04f8a6a90ddda79d10ce97caa141fab0b874b 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2097,40 +2097,6 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.add_option_view(self.window_id, build_view) } - pub fn subscribe_to_model(&mut self, handle: &ModelHandle, callback: F) - where - E: Entity, - E::Event: 'static, - F: 'static + FnMut(&mut T, ModelHandle, &E::Event, &mut ViewContext), - { - self.subscribe(handle, callback) - } - - pub fn subscribe_to_view(&mut self, handle: &ViewHandle, callback: F) - where - V: View, - V::Event: 'static, - F: 'static + FnMut(&mut T, ViewHandle, &V::Event, &mut ViewContext), - { - self.subscribe(handle, callback) - } - - pub fn observe_model(&mut self, handle: &ModelHandle, callback: F) - where - S: Entity, - F: 'static + FnMut(&mut T, ModelHandle, &mut ViewContext), - { - self.observe(handle, callback) - } - - pub fn observe_view(&mut self, handle: &ViewHandle, callback: F) - where - S: View, - F: 'static + FnMut(&mut T, ViewHandle, &mut ViewContext), - { - self.observe(handle, callback) - } - pub fn subscribe(&mut self, handle: &H, mut callback: F) where E: Entity, @@ -2152,7 +2118,7 @@ impl<'a, T: View> ViewContext<'a, T> { }); } - fn observe(&mut self, handle: &H, mut callback: F) + pub fn observe(&mut self, handle: &H, mut callback: F) where E: Entity, H: Handle, @@ -2594,14 +2560,14 @@ impl ViewHandle { let mut cx = cx.cx.borrow_mut(); self.update(&mut *cx, |_, cx| { - cx.observe_view(self, { + cx.observe(self, { let mut tx = tx.clone(); move |_, _, _| { tx.blocking_send(()).ok(); } }); - cx.subscribe_to_view(self, { + cx.subscribe(self, { let mut tx = tx.clone(); move |_, _, _, _| { tx.blocking_send(()).ok(); @@ -3153,7 +3119,7 @@ mod tests { impl View { fn new(other: Option>, cx: &mut ViewContext) -> Self { if let Some(other) = other.as_ref() { - cx.subscribe_to_view(other, |me, _, event, _| { + cx.subscribe(other, |me, _, event, _| { me.events.push(format!("observed event {}", event)); }); } @@ -3327,15 +3293,15 @@ mod tests { let handle_3 = cx.add_model(|_| Model); handle_1.update(cx, |_, c| { - c.subscribe_to_view(&handle_2, move |me, _, event, c| { + c.subscribe(&handle_2, move |me, _, event, c| { me.events.push(*event); - c.subscribe_to_view(&handle_2b, |me, _, event, _| { + c.subscribe(&handle_2b, |me, _, event, _| { me.events.push(*event * 2); }); }); - c.subscribe_to_model(&handle_3, |me, _, event, _| { + c.subscribe(&handle_3, |me, _, event, _| { me.events.push(*event); }) }); @@ -3381,8 +3347,8 @@ mod tests { let observed_model = cx.add_model(|_| Model); observing_view.update(cx, |_, cx| { - cx.subscribe_to_view(&emitting_view, |_, _, _, _| {}); - cx.subscribe_to_model(&observed_model, |_, _, _, _| {}); + cx.subscribe(&emitting_view, |_, _, _, _| {}); + cx.subscribe(&observed_model, |_, _, _, _| {}); }); observing_model.update(cx, |_, cx| { cx.subscribe(&observed_model, |_, _, _, _| {}); @@ -3431,7 +3397,7 @@ mod tests { let model = cx.add_model(|_| Model::default()); view.update(cx, |_, c| { - c.observe_model(&model, |me, observed, c| { + c.observe(&model, |me, observed, c| { me.events.push(observed.read(c).count) }); }); @@ -3473,7 +3439,7 @@ mod tests { let observed_model = cx.add_model(|_| Model); observing_view.update(cx, |_, cx| { - cx.observe_model(&observed_model, |_, _, _| {}); + cx.observe(&observed_model, |_, _, _| {}); }); observing_model.update(cx, |_, cx| { cx.observe(&observed_model, |_, _, _| {}); diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 49259f554cb6a140dce5d71a0103a3f2f891de37..67b352adf91902d58eb9342ac1ebbf096b41e3b5 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -324,9 +324,9 @@ impl Editor { ) -> Self { let display_map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.borrow().clone(), None, cx)); - cx.observe_model(&buffer, Self::on_buffer_changed); - cx.subscribe_to_model(&buffer, Self::on_buffer_event); - cx.observe_model(&display_map, Self::on_display_map_changed); + cx.observe(&buffer, Self::on_buffer_changed); + cx.subscribe(&buffer, Self::on_buffer_event); + cx.observe(&display_map, Self::on_display_map_changed); let mut next_selection_id = 0; let selection_set_id = buffer.update(cx, |buffer, cx| { diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 50bddb37171e7c793f6cc26aa63443c96a0cdb2b..793d823c3c5f87c1ab1da1d779f6f80f5f4b96e1 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -252,7 +252,7 @@ impl FileFinder { workspace.toggle_modal(cx, |cx, workspace| { let handle = cx.handle(); let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx)); - cx.subscribe_to_view(&finder, Self::on_event); + cx.subscribe(&finder, Self::on_event); finder }); } @@ -281,10 +281,10 @@ impl FileFinder { workspace: ViewHandle, cx: &mut ViewContext, ) -> Self { - cx.observe_view(&workspace, Self::workspace_updated); + cx.observe(&workspace, Self::workspace_updated); let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx)); - cx.subscribe_to_view(&query_buffer, Self::on_query_editor_event); + cx.subscribe(&query_buffer, Self::on_query_editor_event); Self { handle: cx.handle().downgrade(), diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 506d11a278cfd13ce71d10e586cd8b778cf55135..2318f2b78c6e96023238acdc8bc41df19e432bff 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -61,7 +61,7 @@ impl ThemeSelector { cx: &mut ViewContext, ) -> Self { let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx)); - cx.subscribe_to_view(&query_buffer, Self::on_query_editor_event); + cx.subscribe(&query_buffer, Self::on_query_editor_event); let mut this = Self { settings, @@ -86,7 +86,7 @@ impl ThemeSelector { cx, ) }); - cx.subscribe_to_view(&selector, Self::on_event); + cx.subscribe(&selector, Self::on_event); selector }); } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 0dffa81be921798113a5c7f49a2ec1faedda2ae0..f04e6dec1afc6f109c4057f4764cf14e6eb8e13b 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -279,7 +279,7 @@ impl ItemViewHandle for ViewHandle { fn set_parent_pane(&self, pane: &ViewHandle, cx: &mut MutableAppContext) { pane.update(cx, |_, cx| { - cx.subscribe_to_view(self, |pane, item, event, cx| { + cx.subscribe(self, |pane, item, event, cx| { if T::should_activate_item_on_event(event) { if let Some(ix) = pane.item_index(&item) { pane.activate_item(ix, cx); @@ -358,7 +358,7 @@ impl Workspace { pub fn new(app_state: &AppState, cx: &mut ViewContext) -> Self { let pane = cx.add_view(|_| Pane::new(app_state.settings.clone())); let pane_id = pane.id(); - cx.subscribe_to_view(&pane, move |me, _, event, cx| { + cx.subscribe(&pane, move |me, _, event, cx| { me.handle_pane_event(pane_id, event, cx) }); cx.focus(&pane); @@ -526,7 +526,7 @@ impl Workspace { cx.spawn(|this, mut cx| async move { let worktree = Worktree::open_local(path, languages, fs, &mut cx).await?; this.update(&mut cx, |this, cx| { - cx.observe_model(&worktree, |_, _, cx| cx.notify()); + cx.observe(&worktree, |_, _, cx| cx.notify()); this.worktrees.insert(worktree.clone()); cx.notify(); }); @@ -835,7 +835,7 @@ impl Workspace { Worktree::open_remote(rpc.clone(), worktree_id, access_token, languages, &mut cx) .await?; this.update(&mut cx, |workspace, cx| { - cx.observe_model(&worktree, |_, _, cx| cx.notify()); + cx.observe(&worktree, |_, _, cx| cx.notify()); workspace.worktrees.insert(worktree); cx.notify(); }); @@ -854,7 +854,7 @@ impl Workspace { fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { let pane = cx.add_view(|_| Pane::new(self.settings.clone())); let pane_id = pane.id(); - cx.subscribe_to_view(&pane, move |me, _, event, cx| { + cx.subscribe(&pane, move |me, _, event, cx| { me.handle_pane_event(pane_id, event, cx) }); self.panes.push(pane.clone()); From f4847bd38fe8abd41fc209672b7fdff3e34469f0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Aug 2021 15:09:18 -0700 Subject: [PATCH 065/204] Get light theme loading successfully --- zed/assets/themes/light.toml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml index c384e27f27ef32668bff0696f2879412368084f9..380a5cee013debc8802e5f87956e6fa87700f311 100644 --- a/zed/assets/themes/light.toml +++ b/zed/assets/themes/light.toml @@ -6,10 +6,16 @@ extends = "_base" 2 = "#ececec" 3 = "#3a3b3c" -[text_color] -dull = "#acacac" -bright = "#111111" -normal = "#333333" +[text] +0 = "#acacac" +1 = "#111111" +2 = "#333333" + +[status] +good = "#4fac63" +info = "#3c5dd4" +warn = "#faca50" +bad = "#b7372e" [syntax] keyword = "#0000fa" From 6df80d94add5d2bc3522a27995c61407c22b2124 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Aug 2021 15:54:24 -0700 Subject: [PATCH 066/204] Allow subscription/notification to be cancelled by dropping the returned Subscription Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 286 +++++++++++++++++++++++----------- zed/src/editor.rs | 7 +- zed/src/editor/buffer.rs | 15 +- zed/src/editor/display_map.rs | 2 +- zed/src/file_finder.rs | 7 +- zed/src/test.rs | 3 +- zed/src/theme_selector.rs | 5 +- zed/src/workspace.rs | 13 +- 8 files changed, 228 insertions(+), 110 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 85b04f8a6a90ddda79d10ce97caa141fab0b874b..89dd5582c062eaec2b4d5a521bce7580a9109bb4 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -636,9 +636,11 @@ impl ReadViewWith for TestAppContext { type ActionCallback = dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize) -> bool; - type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); +type SubscriptionCallback = Box bool>; +type ObservationCallback = Box bool>; + pub struct MutableAppContext { weak_self: Option>>, foreground_platform: Rc, @@ -649,8 +651,9 @@ pub struct MutableAppContext { keystroke_matcher: keymap::Matcher, next_entity_id: usize, next_window_id: usize, - subscriptions: HashMap bool>>>, - observations: HashMap bool>>>, + next_subscription_id: usize, + subscriptions: Arc>>>, + observations: Arc>>>, presenters_and_platform_windows: HashMap>, Box)>, debug_elements_callbacks: HashMap crate::json::Value>>, @@ -688,8 +691,9 @@ impl MutableAppContext { keystroke_matcher: keymap::Matcher::default(), next_entity_id: 0, next_window_id: 0, - subscriptions: HashMap::new(), - observations: HashMap::new(), + next_subscription_id: 0, + subscriptions: Default::default(), + observations: Default::default(), presenters_and_platform_windows: HashMap::new(), debug_elements_callbacks: HashMap::new(), foreground, @@ -877,7 +881,7 @@ impl MutableAppContext { ); } - pub fn subscribe(&mut self, handle: &H, mut callback: F) + pub fn subscribe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, E::Event: 'static, @@ -890,7 +894,7 @@ impl MutableAppContext { }) } - fn observe(&mut self, handle: &H, mut callback: F) + fn observe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, E::Event: 'static, @@ -903,45 +907,65 @@ impl MutableAppContext { }) } - pub fn subscribe_internal(&mut self, handle: &H, mut callback: F) + pub fn subscribe_internal(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, E::Event: 'static, H: Handle, F: 'static + FnMut(H, &E::Event, &mut Self) -> bool, { + let id = post_inc(&mut self.next_subscription_id); let emitter = handle.downgrade(); self.subscriptions + .lock() .entry(handle.id()) .or_default() - .push(Box::new(move |payload, cx| { - if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { - let payload = payload.downcast_ref().expect("downcast is type safe"); - callback(emitter, payload, cx) - } else { - false - } - })) + .insert( + id, + Box::new(move |payload, cx| { + if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { + let payload = payload.downcast_ref().expect("downcast is type safe"); + callback(emitter, payload, cx) + } else { + false + } + }), + ); + Subscription::Subscription { + id, + entity_id: handle.id(), + subscriptions: Some(Arc::downgrade(&self.subscriptions)), + } } - fn observe_internal(&mut self, handle: &H, mut callback: F) + fn observe_internal(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, E::Event: 'static, H: Handle, F: 'static + FnMut(H, &mut Self) -> bool, { + let id = post_inc(&mut self.next_subscription_id); let observed = handle.downgrade(); self.observations + .lock() .entry(handle.id()) .or_default() - .push(Box::new(move |cx| { - if let Some(observed) = H::upgrade_from(&observed, cx) { - callback(observed, cx) - } else { - false - } - })) + .insert( + id, + Box::new(move |cx| { + if let Some(observed) = H::upgrade_from(&observed, cx) { + callback(observed, cx) + } else { + false + } + }), + ); + Subscription::Observation { + id, + entity_id: handle.id(), + observations: Some(Arc::downgrade(&self.observations)), + } } pub(crate) fn notify_view(&mut self, window_id: usize, view_id: usize) { @@ -1248,15 +1272,15 @@ impl MutableAppContext { } for model_id in dropped_models { - self.subscriptions.remove(&model_id); - self.observations.remove(&model_id); + self.subscriptions.lock().remove(&model_id); + self.observations.lock().remove(&model_id); let mut model = self.cx.models.remove(&model_id).unwrap(); model.release(self); } for (window_id, view_id) in dropped_views { - self.subscriptions.remove(&view_id); - self.observations.remove(&view_id); + self.subscriptions.lock().remove(&view_id); + self.observations.lock().remove(&view_id); let mut view = self.cx.views.remove(&(window_id, view_id)).unwrap(); view.release(self); let change_focus_to = self.cx.windows.get_mut(&window_id).and_then(|window| { @@ -1343,29 +1367,33 @@ impl MutableAppContext { } fn emit_event(&mut self, entity_id: usize, payload: Box) { - if let Some(callbacks) = self.subscriptions.remove(&entity_id) { - for mut callback in callbacks { + let callbacks = self.subscriptions.lock().remove(&entity_id); + if let Some(callbacks) = callbacks { + for (id, mut callback) in callbacks { let alive = callback(payload.as_ref(), self); if alive { self.subscriptions + .lock() .entry(entity_id) .or_default() - .push(callback); + .insert(id, callback); } } } } fn notify_model_observers(&mut self, observed_id: usize) { - if let Some(callbacks) = self.observations.remove(&observed_id) { + let callbacks = self.observations.lock().remove(&observed_id); + if let Some(callbacks) = callbacks { if self.cx.models.contains_key(&observed_id) { - for mut callback in callbacks { + for (id, mut callback) in callbacks { let alive = callback(self); if alive { self.observations + .lock() .entry(observed_id) .or_default() - .push(callback); + .insert(id, callback); } } } @@ -1381,19 +1409,21 @@ impl MutableAppContext { .insert(observed_view_id); } - if let Some(callbacks) = self.observations.remove(&observed_view_id) { + let callbacks = self.observations.lock().remove(&observed_view_id); + if let Some(callbacks) = callbacks { if self .cx .views .contains_key(&(observed_window_id, observed_view_id)) { - for mut callback in callbacks { + for (id, mut callback) in callbacks { let alive = callback(self); if alive { self.observations + .lock() .entry(observed_view_id) .or_default() - .push(callback); + .insert(id, callback); } } } @@ -1873,7 +1903,11 @@ impl<'a, T: Entity> ModelContext<'a, T> { }); } - pub fn subscribe(&mut self, handle: &ModelHandle, mut callback: F) + pub fn subscribe( + &mut self, + handle: &ModelHandle, + mut callback: F, + ) -> Subscription where S::Event: 'static, F: 'static + FnMut(&mut T, ModelHandle, &S::Event, &mut ModelContext), @@ -1889,10 +1923,10 @@ impl<'a, T: Entity> ModelContext<'a, T> { } else { false } - }); + }) } - pub fn observe(&mut self, handle: &ModelHandle, mut callback: F) + pub fn observe(&mut self, handle: &ModelHandle, mut callback: F) -> Subscription where S: Entity, F: 'static + FnMut(&mut T, ModelHandle, &mut ModelContext), @@ -1907,7 +1941,7 @@ impl<'a, T: Entity> ModelContext<'a, T> { } else { false } - }); + }) } pub fn handle(&self) -> ModelHandle { @@ -2097,7 +2131,7 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.add_option_view(self.window_id, build_view) } - pub fn subscribe(&mut self, handle: &H, mut callback: F) + pub fn subscribe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, E::Event: 'static, @@ -2115,10 +2149,10 @@ impl<'a, T: View> ViewContext<'a, T> { } else { false } - }); + }) } - pub fn observe(&mut self, handle: &H, mut callback: F) + pub fn observe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, H: Handle, @@ -2134,7 +2168,7 @@ impl<'a, T: View> ViewContext<'a, T> { } else { false } - }); + }) } pub fn emit(&mut self, payload: T::Event) { @@ -2330,18 +2364,20 @@ impl ModelHandle { let (tx, mut rx) = mpsc::channel(1024); let mut cx = cx.cx.borrow_mut(); - cx.observe(self, { - let mut tx = tx.clone(); - move |_, _| { - tx.blocking_send(()).ok(); - } - }); - cx.subscribe(self, { - let mut tx = tx.clone(); - move |_, _, _| { - tx.blocking_send(()).ok(); - } - }); + let subscriptions = ( + cx.observe(self, { + let mut tx = tx.clone(); + move |_, _| { + tx.blocking_send(()).ok(); + } + }), + cx.subscribe(self, { + let mut tx = tx.clone(); + move |_, _, _| { + tx.blocking_send(()).ok(); + } + }), + ); let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); let handle = self.downgrade(); @@ -2375,6 +2411,7 @@ impl ModelHandle { }) .await .expect("condition timed out"); + drop(subscriptions); } } } @@ -2559,20 +2596,21 @@ impl ViewHandle { let (tx, mut rx) = mpsc::channel(1024); let mut cx = cx.cx.borrow_mut(); - self.update(&mut *cx, |_, cx| { - cx.observe(self, { - let mut tx = tx.clone(); - move |_, _, _| { - tx.blocking_send(()).ok(); - } - }); - - cx.subscribe(self, { - let mut tx = tx.clone(); - move |_, _, _, _| { - tx.blocking_send(()).ok(); - } - }) + let subscriptions = self.update(&mut *cx, |_, cx| { + ( + cx.observe(self, { + let mut tx = tx.clone(); + move |_, _, _| { + tx.blocking_send(()).ok(); + } + }), + cx.subscribe(self, { + let mut tx = tx.clone(); + move |_, _, _, _| { + tx.blocking_send(()).ok(); + } + }), + ) }); let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap(); @@ -2607,6 +2645,7 @@ impl ViewHandle { }) .await .expect("condition timed out"); + drop(subscriptions); } } } @@ -2876,6 +2915,62 @@ impl Drop for ValueHandle { } } +#[must_use] +pub enum Subscription { + Subscription { + id: usize, + entity_id: usize, + subscriptions: Option>>>>, + }, + Observation { + id: usize, + entity_id: usize, + observations: Option>>>>, + }, +} + +impl Subscription { + pub fn detach(&mut self) { + match self { + Subscription::Subscription { subscriptions, .. } => { + subscriptions.take(); + } + Subscription::Observation { observations, .. } => { + observations.take(); + } + } + } +} + +impl Drop for Subscription { + fn drop(&mut self) { + match self { + Subscription::Observation { + id, + entity_id, + observations, + } => { + if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) { + if let Some(observations) = observations.lock().get_mut(entity_id) { + observations.remove(id); + } + } + } + Subscription::Subscription { + id, + entity_id, + subscriptions, + } => { + if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) { + if let Some(subscriptions) = subscriptions.lock().get_mut(entity_id) { + subscriptions.remove(id); + } + } + } + } + } +} + #[derive(Default)] struct RefCounts { entity_counts: HashMap, @@ -2982,10 +3077,12 @@ mod tests { if let Some(other) = other.as_ref() { cx.observe(other, |me, _, _| { me.events.push("notified".into()); - }); + }) + .detach(); cx.subscribe(other, |me, _, event, _| { me.events.push(format!("observed event {}", event)); - }); + }) + .detach(); } Self { @@ -3021,8 +3118,8 @@ mod tests { }); assert_eq!(cx.cx.models.len(), 1); - assert!(cx.subscriptions.is_empty()); - assert!(cx.observations.is_empty()); + assert!(cx.subscriptions.lock().is_empty()); + assert!(cx.observations.lock().is_empty()); } #[crate::test(self)] @@ -3046,8 +3143,10 @@ mod tests { c.subscribe(&handle_2b, |model, _, event, _| { model.events.push(*event * 2); - }); - }); + }) + .detach(); + }) + .detach(); }); handle_2.update(cx, |_, c| c.emit(7)); @@ -3078,8 +3177,10 @@ mod tests { model.events.push(observed.read(c).count); c.observe(&handle_2b, |model, observed, c| { model.events.push(observed.read(c).count * 2); - }); - }); + }) + .detach(); + }) + .detach(); }); handle_2.update(cx, |model, c| { @@ -3121,7 +3222,8 @@ mod tests { if let Some(other) = other.as_ref() { cx.subscribe(other, |me, _, event, _| { me.events.push(format!("observed event {}", event)); - }); + }) + .detach(); } Self { other, @@ -3155,8 +3257,8 @@ mod tests { }); assert_eq!(cx.cx.views.len(), 2); - assert!(cx.subscriptions.is_empty()); - assert!(cx.observations.is_empty()); + assert!(cx.subscriptions.lock().is_empty()); + assert!(cx.observations.lock().is_empty()); } #[crate::test(self)] @@ -3298,12 +3400,15 @@ mod tests { c.subscribe(&handle_2b, |me, _, event, _| { me.events.push(*event * 2); - }); - }); + }) + .detach(); + }) + .detach(); c.subscribe(&handle_3, |me, _, event, _| { me.events.push(*event); }) + .detach(); }); handle_2.update(cx, |_, c| c.emit(7)); @@ -3347,11 +3452,11 @@ mod tests { let observed_model = cx.add_model(|_| Model); observing_view.update(cx, |_, cx| { - cx.subscribe(&emitting_view, |_, _, _, _| {}); - cx.subscribe(&observed_model, |_, _, _, _| {}); + cx.subscribe(&emitting_view, |_, _, _, _| {}).detach(); + cx.subscribe(&observed_model, |_, _, _, _| {}).detach(); }); observing_model.update(cx, |_, cx| { - cx.subscribe(&observed_model, |_, _, _, _| {}); + cx.subscribe(&observed_model, |_, _, _, _| {}).detach(); }); cx.update(|| { @@ -3399,7 +3504,8 @@ mod tests { view.update(cx, |_, c| { c.observe(&model, |me, observed, c| { me.events.push(observed.read(c).count) - }); + }) + .detach(); }); model.update(cx, |model, c| { @@ -3439,10 +3545,10 @@ mod tests { let observed_model = cx.add_model(|_| Model); observing_view.update(cx, |_, cx| { - cx.observe(&observed_model, |_, _, _| {}); + cx.observe(&observed_model, |_, _, _| {}).detach(); }); observing_model.update(cx, |_, cx| { - cx.observe(&observed_model, |_, _, _| {}); + cx.observe(&observed_model, |_, _, _| {}).detach(); }); cx.update(|| { diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 67b352adf91902d58eb9342ac1ebbf096b41e3b5..ba71c0ce60359d07354ed20d44a8d13e5164d740 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -324,9 +324,10 @@ impl Editor { ) -> Self { let display_map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.borrow().clone(), None, cx)); - cx.observe(&buffer, Self::on_buffer_changed); - cx.subscribe(&buffer, Self::on_buffer_event); - cx.observe(&display_map, Self::on_display_map_changed); + 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) + .detach(); let mut next_selection_id = 0; let selection_set_id = buffer.update(cx, |buffer, cx| { diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 07ea5bacdc243c78ac4e9fe29f95814a40efab7c..da3fd222802c2bf63ea1d1d1e1c8579ea8fa7a44 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -2943,11 +2943,13 @@ mod tests { let buffer_1_events = buffer_1_events.clone(); cx.subscribe(&buffer1, move |_, _, event, _| { buffer_1_events.borrow_mut().push(event.clone()) - }); + }) + .detach(); let buffer_2_events = buffer_2_events.clone(); cx.subscribe(&buffer2, move |_, _, event, _| { buffer_2_events.borrow_mut().push(event.clone()) - }); + }) + .detach(); // An edit emits an edited event, followed by a dirtied event, // since the buffer was previously in a clean state. @@ -3382,7 +3384,8 @@ mod tests { cx.subscribe(&buffer1, { let events = events.clone(); move |_, _, event, _| events.borrow_mut().push(event.clone()) - }); + }) + .detach(); assert!(!buffer.is_dirty()); assert!(events.borrow().is_empty()); @@ -3438,7 +3441,8 @@ mod tests { cx.subscribe(&buffer2, { let events = events.clone(); move |_, _, event, _| events.borrow_mut().push(event.clone()) - }); + }) + .detach(); }); fs::remove_file(dir.path().join("file2")).unwrap(); @@ -3458,7 +3462,8 @@ mod tests { cx.subscribe(&buffer3, { let events = events.clone(); move |_, _, event, _| events.borrow_mut().push(event.clone()) - }); + }) + .detach(); }); tree.flush_fs_events(&cx).await; diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 0f0945a009e1c599053982d805625819c0c36750..83b1575ab71534cf5ae52eff9eeca4c20a05f0ed 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -32,7 +32,7 @@ impl DisplayMap { let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx); let (tab_map, snapshot) = TabMap::new(snapshot, settings.tab_size); let wrap_map = cx.add_model(|cx| WrapMap::new(snapshot, settings, wrap_width, cx)); - cx.observe(&wrap_map, |_, _, cx| cx.notify()); + cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach(); DisplayMap { buffer, fold_map, diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 793d823c3c5f87c1ab1da1d779f6f80f5f4b96e1..50ddc48038246e7b9485f6d646b0fec631a429d0 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -252,7 +252,7 @@ impl FileFinder { workspace.toggle_modal(cx, |cx, workspace| { let handle = cx.handle(); let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx)); - cx.subscribe(&finder, Self::on_event); + cx.subscribe(&finder, Self::on_event).detach(); finder }); } @@ -281,10 +281,11 @@ impl FileFinder { workspace: ViewHandle, cx: &mut ViewContext, ) -> Self { - cx.observe(&workspace, Self::workspace_updated); + cx.observe(&workspace, Self::workspace_updated).detach(); let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx)); - cx.subscribe(&query_buffer, Self::on_query_editor_event); + cx.subscribe(&query_buffer, Self::on_query_editor_event) + .detach(); Self { handle: cx.handle().downgrade(), diff --git a/zed/src/test.rs b/zed/src/test.rs index 9263e0750f3e34cdef236f9d3f43c6aecba76d0e..6fac3c8bc9884cf45837d07b1fc9189480b3ccce 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -190,7 +190,8 @@ impl Observer { let observer = cx.add_model(|cx| { cx.observe(handle, move |_, _, _| { let _ = notify_tx.try_send(()); - }); + }) + .detach(); Observer(PhantomData) }); (observer, notify_rx) diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 2318f2b78c6e96023238acdc8bc41df19e432bff..bfe5977e2e175a227ff5810ebdbb27894245c669 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -61,7 +61,8 @@ impl ThemeSelector { cx: &mut ViewContext, ) -> Self { let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx)); - cx.subscribe(&query_buffer, Self::on_query_editor_event); + cx.subscribe(&query_buffer, Self::on_query_editor_event) + .detach(); let mut this = Self { settings, @@ -86,7 +87,7 @@ impl ThemeSelector { cx, ) }); - cx.subscribe(&selector, Self::on_event); + cx.subscribe(&selector, Self::on_event).detach(); selector }); } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index f04e6dec1afc6f109c4057f4764cf14e6eb8e13b..28ed2701d123035c81493a5ce0d3e5b373b7a32e 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -290,7 +290,8 @@ impl ItemViewHandle for ViewHandle { cx.notify() } }) - }) + .detach(); + }); } fn save(&self, cx: &mut MutableAppContext) -> Result>> { @@ -360,7 +361,8 @@ impl Workspace { let pane_id = pane.id(); cx.subscribe(&pane, move |me, _, event, cx| { me.handle_pane_event(pane_id, event, cx) - }); + }) + .detach(); cx.focus(&pane); let mut left_sidebar = Sidebar::new(Side::Left); @@ -526,7 +528,7 @@ impl Workspace { cx.spawn(|this, mut cx| async move { let worktree = Worktree::open_local(path, languages, fs, &mut cx).await?; this.update(&mut cx, |this, cx| { - cx.observe(&worktree, |_, _, cx| cx.notify()); + cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); this.worktrees.insert(worktree.clone()); cx.notify(); }); @@ -835,7 +837,7 @@ impl Workspace { Worktree::open_remote(rpc.clone(), worktree_id, access_token, languages, &mut cx) .await?; this.update(&mut cx, |workspace, cx| { - cx.observe(&worktree, |_, _, cx| cx.notify()); + cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); workspace.worktrees.insert(worktree); cx.notify(); }); @@ -856,7 +858,8 @@ impl Workspace { let pane_id = pane.id(); cx.subscribe(&pane, move |me, _, event, cx| { me.handle_pane_event(pane_id, event, cx) - }); + }) + .detach(); self.panes.push(pane.clone()); self.activate_pane(pane.clone(), cx); pane From baded7d416027aeed66d3763159e853ecfc238b8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Aug 2021 17:21:06 -0700 Subject: [PATCH 067/204] Start work on subscribing to model from ChatPanel --- gpui/src/elements/list.rs | 2 +- zed/src/channel.rs | 1 + zed/src/chat_panel.rs | 44 ++++++++++++++++++++++----------------- zed/src/workspace.rs | 10 ++------- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index d6101a59512b1f256f2e6f6438a77a286929e33c..2e692f4a9914a31284d3fdbb850c798995ade25c 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -219,7 +219,7 @@ impl StateInner { let start_ix = cursor.sum_start().0; cursor.seek(&Height(self.scroll_top + height), Bias::Left, &()); let end_ix = cursor.sum_start().0; - start_ix..end_ix + 1 + start_ix..self.elements.len().min(end_ix + 1) } fn scroll( diff --git a/zed/src/channel.rs b/zed/src/channel.rs index efb51eb239adededbbd65013cf9598530f26a0d0..148d541f03cea6c76fd336ce05785176b9755843 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -183,6 +183,7 @@ impl Channel { }) }) .detach(); + cx.notify(); Ok(()) } diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 86a7762fac3b33146f078bf912fb25ee30a8d5d6..dfb4e545becb6f14f34d8f15652f352bb5767a2d 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -1,31 +1,33 @@ -use super::{ - channel::{Channel, ChannelList}, - Settings, -}; -use gpui::{elements::*, Entity, ModelHandle, RenderContext, View, ViewContext}; -use postage::watch; +use super::channel::{Channel, ChannelList}; +use gpui::{elements::*, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext}; pub struct ChatPanel { channel_list: ModelHandle, - active_channel: Option>, - // active_channel_subscription: Subscription, + active_channel: Option<(ModelHandle, Subscription)>, messages: ListState, } pub enum Event {} impl ChatPanel { - pub fn new( - channel_list: ModelHandle, - settings: watch::Receiver, - cx: &mut ViewContext, - ) -> Self { + pub fn new(channel_list: ModelHandle, cx: &mut ViewContext) -> Self { let mut this = Self { channel_list, messages: ListState::new(Vec::new()), active_channel: None, }; - let channel = this.channel_list.update(cx, |list, cx| { + + this.assign_active_channel(cx); + cx.observe(&this.channel_list, |this, _, cx| { + this.assign_active_channel(cx); + }) + .detach(); + + this + } + + pub fn assign_active_channel(&mut self, cx: &mut ViewContext) { + let channel = self.channel_list.update(cx, |list, cx| { if let Some(channel_id) = list .available_channels() .and_then(|channels| channels.first()) @@ -36,13 +38,17 @@ impl ChatPanel { None }); if let Some(channel) = channel { - this.set_active_channel(channel); + if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) { + let subscription = cx.observe(&channel, Self::channel_did_change); + self.active_channel = Some((channel, subscription)); + } + } else { + self.active_channel = None; } - this } - pub fn set_active_channel(&mut self, channel: ModelHandle) { - // + fn channel_did_change(&mut self, _: ModelHandle, cx: &mut ViewContext) { + cx.notify(); } } @@ -55,7 +61,7 @@ impl View for ChatPanel { "ChatPanel" } - fn render(&self, cx: &RenderContext) -> gpui::ElementBox { + fn render(&self, _: &RenderContext) -> gpui::ElementBox { List::new(self.messages.clone()).boxed() } } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 28ed2701d123035c81493a5ce0d3e5b373b7a32e..b6843fdc0bea5962a2b90e5a6869f0ef23cc7aba 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -374,14 +374,8 @@ impl Workspace { let mut right_sidebar = Sidebar::new(Side::Right); right_sidebar.add_item( "icons/comment-16.svg", - cx.add_view(|cx| { - ChatPanel::new( - app_state.channel_list.clone(), - app_state.settings.clone(), - cx, - ) - }) - .into(), + cx.add_view(|cx| ChatPanel::new(app_state.channel_list.clone(), cx)) + .into(), ); right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); From 405ff1d9dbacf1e54f6e4bd389563659cb02ce31 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 24 Aug 2021 12:23:50 +0200 Subject: [PATCH 068/204] Render chat messages in `ChatPanel` --- server/src/rpc.rs | 2 +- zed/src/channel.rs | 113 ++++++++++++++++++++++++++++++++---------- zed/src/chat_panel.rs | 66 +++++++++++++++++++++--- zed/src/workspace.rs | 10 +++- 4 files changed, 155 insertions(+), 36 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index f489bfb57debddc255eee2e9da06cde6f011c264..a44738e8ffb1ae77715b3f7a5ec13056675973a3 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1520,7 +1520,7 @@ mod tests { fn channel_messages(channel: &Channel) -> Vec<(u64, String)> { channel .messages() - .iter() + .cursor::<(), ()>() .map(|m| (m.sender_id, m.body.clone())) .collect() } diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 148d541f03cea6c76fd336ce05785176b9755843..a82b7d4aee15cfdaad093afdb6823e8543d6a8bd 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -3,10 +3,13 @@ use crate::{ util::log_async_errors, }; use anyhow::{anyhow, Context, Result}; -use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle}; +use gpui::{ + sum_tree::{self, Bias, SumTree}, + Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle, +}; use std::{ - cmp::Ordering, - collections::{hash_map, BTreeSet, HashMap}, + collections::{hash_map, HashMap}, + ops::Range, sync::Arc, }; use zrpc::{ @@ -28,13 +31,14 @@ pub struct ChannelDetails { pub struct Channel { details: ChannelDetails, - messages: BTreeSet, + messages: SumTree, pending_messages: Vec, next_local_message_id: u64, rpc: Arc, _subscription: rpc::Subscription, } +#[derive(Clone, Debug)] pub struct ChannelMessage { pub id: u64, pub sender_id: u64, @@ -46,10 +50,26 @@ pub struct PendingChannelMessage { local_id: u64, } -pub enum Event {} +#[derive(Clone, Debug, Default)] +pub struct ChannelMessageSummary { + max_id: u64, + count: Count, +} + +#[derive(Copy, Clone, Debug, Default)] +struct Count(usize); + +pub enum ChannelListEvent {} + +pub enum ChannelEvent { + Message { + old_range: Range, + message: ChannelMessage, + }, +} impl Entity for ChannelList { - type Event = Event; + type Event = ChannelListEvent; } impl ChannelList { @@ -107,7 +127,7 @@ impl ChannelList { } impl Entity for Channel { - type Event = (); + type Event = ChannelEvent; fn release(&mut self, cx: &mut MutableAppContext) { let rpc = self.rpc.clone(); @@ -132,7 +152,10 @@ impl Channel { cx.spawn(|channel, mut cx| async move { match rpc.request(proto::JoinChannel { channel_id }).await { Ok(response) => channel.update(&mut cx, |channel, cx| { - channel.messages = response.messages.into_iter().map(Into::into).collect(); + channel.messages = SumTree::new(); + channel + .messages + .extend(response.messages.into_iter().map(Into::into), &()); cx.notify(); }), Err(error) => log::error!("error joining channel: {}", error), @@ -171,12 +194,14 @@ impl Channel { .binary_search_by_key(&local_id, |msg| msg.local_id) { let body = this.pending_messages.remove(i).body; - this.messages.insert(ChannelMessage { - id: response.message_id, - sender_id: current_user_id, - body, - }); - cx.notify(); + this.insert_message( + ChannelMessage { + id: response.message_id, + sender_id: current_user_id, + body, + }, + cx, + ); } }); Ok(()) @@ -187,7 +212,7 @@ impl Channel { Ok(()) } - pub fn messages(&self) -> &BTreeSet { + pub fn messages(&self) -> &SumTree { &self.messages } @@ -209,10 +234,31 @@ impl Channel { .payload .message .ok_or_else(|| anyhow!("empty message"))?; - self.messages.insert(message.into()); - cx.notify(); + self.insert_message(message.into(), cx); Ok(()) } + + fn insert_message(&mut self, message: ChannelMessage, 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 mut end_ix = start_ix; + if old_cursor.item().map_or(false, |m| m.id == message.id) { + old_cursor.next(&()); + end_ix += 1; + } + + new_messages.push(message.clone(), &()); + new_messages.push_tree(old_cursor.suffix(&()), &()); + drop(old_cursor); + self.messages = new_messages; + + cx.emit(ChannelEvent::Message { + old_range: start_ix..end_ix, + message, + }); + cx.notify(); + } } impl From for ChannelDetails { @@ -234,22 +280,35 @@ impl From for ChannelMessage { } } -impl PartialOrd for ChannelMessage { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) +impl sum_tree::Item for ChannelMessage { + type Summary = ChannelMessageSummary; + + fn summary(&self) -> Self::Summary { + ChannelMessageSummary { + max_id: self.id, + count: Count(1), + } } } -impl Ord for ChannelMessage { - fn cmp(&self, other: &Self) -> Ordering { - self.id.cmp(&other.id) +impl sum_tree::Summary for ChannelMessageSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.max_id = summary.max_id; + self.count.0 += summary.count.0; } } -impl PartialEq for ChannelMessage { - fn eq(&self, other: &Self) -> bool { - self.id == other.id +impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for u64 { + fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { + debug_assert!(summary.max_id > *self); + *self = summary.max_id; } } -impl Eq for ChannelMessage {} +impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count { + fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { + self.0 += summary.count.0; + } +} diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index dfb4e545becb6f14f34d8f15652f352bb5767a2d..5e9b950b507d66ce6aa1607a31177f6b59665b6b 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -1,20 +1,30 @@ -use super::channel::{Channel, ChannelList}; +use crate::{ + channel::{Channel, ChannelEvent, ChannelList, ChannelMessage}, + Settings, +}; use gpui::{elements::*, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext}; +use postage::watch; pub struct ChatPanel { channel_list: ModelHandle, active_channel: Option<(ModelHandle, Subscription)>, messages: ListState, + settings: watch::Receiver, } pub enum Event {} impl ChatPanel { - pub fn new(channel_list: ModelHandle, cx: &mut ViewContext) -> Self { + pub fn new( + channel_list: ModelHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { let mut this = Self { channel_list, messages: ListState::new(Vec::new()), active_channel: None, + settings, }; this.assign_active_channel(cx); @@ -39,7 +49,15 @@ impl ChatPanel { }); if let Some(channel) = channel { if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) { - let subscription = cx.observe(&channel, Self::channel_did_change); + let subscription = cx.subscribe(&channel, Self::channel_did_change); + self.messages = ListState::new( + channel + .read(cx) + .messages() + .cursor::<(), ()>() + .map(|m| self.render_message(m)) + .collect(), + ); self.active_channel = Some((channel, subscription)); } } else { @@ -47,9 +65,42 @@ impl ChatPanel { } } - fn channel_did_change(&mut self, _: ModelHandle, cx: &mut ViewContext) { + fn channel_did_change( + &mut self, + _: ModelHandle, + event: &ChannelEvent, + cx: &mut ViewContext, + ) { + match event { + ChannelEvent::Message { old_range, message } => { + self.messages + .splice(old_range.clone(), Some(self.render_message(message))); + } + } cx.notify(); } + + fn render_active_channel_messages(&self) -> ElementBox { + Expanded::new(0.8, List::new(self.messages.clone()).boxed()).boxed() + } + + fn render_message(&self, message: &ChannelMessage) -> ElementBox { + let settings = self.settings.borrow(); + Flex::column() + .with_child( + Label::new( + message.body.clone(), + settings.ui_font_family, + settings.ui_font_size, + ) + .boxed(), + ) + .boxed() + } + + fn render_input_box(&self) -> ElementBox { + Empty::new().boxed() + } } impl Entity for ChatPanel { @@ -61,7 +112,10 @@ impl View for ChatPanel { "ChatPanel" } - fn render(&self, _: &RenderContext) -> gpui::ElementBox { - List::new(self.messages.clone()).boxed() + fn render(&self, _: &RenderContext) -> ElementBox { + Flex::column() + .with_child(self.render_active_channel_messages()) + .with_child(self.render_input_box()) + .boxed() } } diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index b6843fdc0bea5962a2b90e5a6869f0ef23cc7aba..28ed2701d123035c81493a5ce0d3e5b373b7a32e 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -374,8 +374,14 @@ impl Workspace { let mut right_sidebar = Sidebar::new(Side::Right); right_sidebar.add_item( "icons/comment-16.svg", - cx.add_view(|cx| ChatPanel::new(app_state.channel_list.clone(), cx)) - .into(), + cx.add_view(|cx| { + ChatPanel::new( + app_state.channel_list.clone(), + app_state.settings.clone(), + cx, + ) + }) + .into(), ); right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); From 54b4a4bf6a8759d9224b37243d7feae0e6bb0dac Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 24 Aug 2021 14:17:15 +0200 Subject: [PATCH 069/204] Allow editor to be created in auto-height mode --- gpui/src/presenter.rs | 7 +++++ zed/src/chat_panel.rs | 16 +++++++--- zed/src/editor.rs | 66 ++++++++++++++++++++++++++++----------- zed/src/editor/element.rs | 20 ++++++++---- 4 files changed, 81 insertions(+), 28 deletions(-) diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index f282e4405ea2e5ca2606a96dea9ca0cef3f9d8fc..ac4652482cc72abc76640af671259a376eebdd97 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -325,6 +325,13 @@ impl SizeConstraint { Axis::Vertical => self.max.y(), } } + + pub fn min_along(&self, axis: Axis) -> f32 { + match axis { + Axis::Horizontal => self.min.x(), + Axis::Vertical => self.min.y(), + } + } } impl ToJson for SizeConstraint { diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 5e9b950b507d66ce6aa1607a31177f6b59665b6b..458215e72c5791be3e9230523b4d0f617413f141 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -1,14 +1,18 @@ use crate::{ channel::{Channel, ChannelEvent, ChannelList, ChannelMessage}, + editor::Editor, Settings, }; -use gpui::{elements::*, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext}; +use gpui::{ + elements::*, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext, ViewHandle, +}; use postage::watch; pub struct ChatPanel { channel_list: ModelHandle, active_channel: Option<(ModelHandle, Subscription)>, messages: ListState, + input_editor: ViewHandle, settings: watch::Receiver, } @@ -20,10 +24,12 @@ impl ChatPanel { settings: watch::Receiver, cx: &mut ViewContext, ) -> Self { + let input_editor = cx.add_view(|cx| Editor::auto_height(settings.clone(), cx)); let mut this = Self { channel_list, - messages: ListState::new(Vec::new()), active_channel: None, + messages: ListState::new(Vec::new()), + input_editor, settings, }; @@ -81,7 +87,7 @@ impl ChatPanel { } fn render_active_channel_messages(&self) -> ElementBox { - Expanded::new(0.8, List::new(self.messages.clone()).boxed()).boxed() + Expanded::new(1., List::new(self.messages.clone()).boxed()).boxed() } fn render_message(&self, message: &ChannelMessage) -> ElementBox { @@ -99,7 +105,9 @@ impl ChatPanel { } fn render_input_box(&self) -> ElementBox { - Empty::new().boxed() + ConstrainedBox::new(ChildView::new(self.input_editor.id()).boxed()) + .with_max_height(100.) + .boxed() } } diff --git a/zed/src/editor.rs b/zed/src/editor.rs index ba71c0ce60359d07354ed20d44a8d13e5164d740..6ac06cd37084605668b8f365473c2dcd3c525fb2 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -43,7 +43,7 @@ const MAX_LINE_LEN: usize = 1024; action!(Cancel); action!(Backspace); action!(Delete); -action!(Newline); +action!(Newline, bool); action!(Insert, String); action!(DeleteLine); action!(DeleteToPreviousWordBoundary); @@ -102,7 +102,8 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-h", Backspace, Some("BufferView")), Binding::new("delete", Delete, Some("BufferView")), Binding::new("ctrl-d", Delete, Some("BufferView")), - Binding::new("enter", Newline, Some("BufferView")), + Binding::new("enter", Newline(false), Some("BufferView")), + Binding::new("alt-enter", Newline(true), Some("BufferView")), Binding::new("tab", Insert("\t".into()), Some("BufferView")), Binding::new("ctrl-shift-K", DeleteLine, Some("BufferView")), Binding::new( @@ -268,6 +269,12 @@ pub enum SelectPhase { End, } +enum EditorMode { + SingleLine, + AutoHeight, + Full, +} + pub struct Editor { handle: WeakViewHandle, buffer: ModelHandle, @@ -285,12 +292,13 @@ pub struct Editor { cursors_visible: bool, blink_epoch: usize, blinking_paused: bool, - single_line: bool, + mode: EditorMode, } pub struct Snapshot { pub display_snapshot: DisplayMapSnapshot, pub gutter_visible: bool, + pub auto_height: bool, pub theme: Arc, pub font_family: FamilyId, pub font_size: f32, @@ -313,7 +321,14 @@ impl Editor { pub fn single_line(settings: watch::Receiver, 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); - view.single_line = true; + view.mode = EditorMode::SingleLine; + view + } + + pub fn auto_height(settings: watch::Receiver, 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); + view.mode = EditorMode::AutoHeight; view } @@ -359,7 +374,7 @@ impl Editor { cursors_visible: false, blink_epoch: 0, blinking_paused: false, - single_line: false, + mode: EditorMode::Full, } } @@ -376,7 +391,8 @@ impl Editor { Snapshot { display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), - gutter_visible: !self.single_line, + gutter_visible: matches!(self.mode, EditorMode::Full), + auto_height: matches!(self.mode, EditorMode::AutoHeight), scroll_position: self.scroll_position, scroll_top_anchor: self.scroll_top_anchor.clone(), theme: settings.theme.clone(), @@ -413,10 +429,15 @@ impl Editor { line_height: f32, cx: &mut ViewContext, ) -> bool { + let visible_lines = viewport_height / line_height; let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut scroll_position = compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor); - let max_scroll_top = display_map.max_point().row().saturating_sub(1) as f32; + let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight) { + (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.) + } else { + display_map.max_point().row().saturating_sub(1) as f32 + }; if scroll_position.y() > max_scroll_top { scroll_position.set_y(max_scroll_top); self.set_scroll_position(scroll_position, cx); @@ -428,7 +449,6 @@ impl Editor { return false; } - let visible_lines = viewport_height / line_height; let first_cursor_top = self .selections(cx) .first() @@ -445,9 +465,13 @@ impl Editor { .row() as f32 + 1.0; - let margin = ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0) - .floor() - .min(3.0); + let margin = if matches!(self.mode, EditorMode::AutoHeight) { + 0. + } else { + ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0) + .floor() + .min(3.0) + }; if margin < 0.0 { return false; } @@ -695,11 +719,17 @@ impl Editor { self.end_transaction(cx); } - fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { - if self.single_line { - cx.propagate_action(); - } else { - self.insert(&Insert("\n".into()), cx); + fn newline(&mut self, Newline(insert_newline): &Newline, cx: &mut ViewContext) { + match self.mode { + EditorMode::SingleLine => cx.propagate_action(), + EditorMode::AutoHeight => { + if *insert_newline { + self.insert(&Insert("\n".into()), cx); + } else { + cx.propagate_action(); + } + } + EditorMode::Full => self.insert(&Insert("\n".into()), cx), } } @@ -1276,7 +1306,7 @@ impl Editor { pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - if self.single_line { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate_action(); } else { let mut selections = self.selections(cx.as_ref()).to_vec(); @@ -1317,7 +1347,7 @@ impl Editor { } pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { - if self.single_line { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate_action(); } else { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index 812bd795a7d0f6c96de59cc0b69ddc374a91e89a..85e54c71b95a27d3e97b841a1588ee82b3e5a9e4 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -1,4 +1,4 @@ -use super::{DisplayPoint, Editor, Select, SelectPhase, Snapshot, Insert, Scroll}; +use super::{DisplayPoint, Editor, EditorMode, Insert, Scroll, Select, SelectPhase, Snapshot}; use crate::time::ReplicaId; use gpui::{ color::Color, @@ -9,8 +9,8 @@ use gpui::{ }, json::{self, ToJson}, text_layout::{self, TextLayoutCache}, - AppContext, Border, Element, Event, EventContext, FontCache, LayoutContext, MutableAppContext, - PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, + AppContext, Axis, Border, Element, Event, EventContext, FontCache, LayoutContext, + MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, }; use json::json; use smallvec::SmallVec; @@ -204,7 +204,7 @@ impl EditorElement { corner_radius: 0., }); - if !editor.single_line { + if let EditorMode::Full = editor.mode { let mut active_rows = layout.active_rows.iter().peekable(); while let Some((start_row, contains_non_empty_selection)) = active_rows.next() { let mut end_row = *start_row; @@ -409,8 +409,16 @@ impl Element for EditorElement { snapshot } }); - if size.y().is_infinite() { - size.set_y((snapshot.max_point().row() + 1) as f32 * line_height); + + let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height; + if snapshot.auto_height { + size.set_y( + scroll_height + .min(constraint.max_along(Axis::Vertical)) + .max(constraint.min_along(Axis::Vertical)), + ) + } else if size.y().is_infinite() { + size.set_y(scroll_height); } let gutter_size = vec2f(gutter_width, size.y()); let text_size = vec2f(text_width, size.y()); From d34f374fe7f0e30aa971656a1f451b34b884440e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 08:15:24 -0600 Subject: [PATCH 070/204] Beautify error logging Co-Authored-By: Antonio Scandurra --- zed/src/channel.rs | 11 ++++---- zed/src/util.rs | 62 ++++++++++++++++++++++++++++++++++++++++----- zed/src/worktree.rs | 46 ++++++++++++++++++--------------- 3 files changed, 88 insertions(+), 31 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index a82b7d4aee15cfdaad093afdb6823e8543d6a8bd..357546298c586e008aa4d8f412c92f7de0207e6b 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,6 +1,6 @@ use crate::{ rpc::{self, Client}, - util::log_async_errors, + util::TryFutureExt, }; use anyhow::{anyhow, Context, Result}; use gpui::{ @@ -76,7 +76,7 @@ impl ChannelList { pub fn new(rpc: Arc, cx: &mut ModelContext) -> Self { cx.spawn(|this, mut cx| { let rpc = rpc.clone(); - log_async_errors(async move { + async move { let response = rpc .request(proto::GetChannels {}) .await @@ -87,7 +87,8 @@ impl ChannelList { cx.notify(); }); Ok(()) - }) + } + .log_err() }) .detach(); Self { @@ -185,7 +186,7 @@ impl Channel { }); let rpc = self.rpc.clone(); cx.spawn(|this, mut cx| { - log_async_errors(async move { + async move { let request = rpc.request(proto::SendChannelMessage { channel_id, body }); let response = request.await?; this.update(&mut cx, |this, cx| { @@ -205,7 +206,7 @@ impl Channel { } }); Ok(()) - }) + }.log_err() }) .detach(); cx.notify(); diff --git a/zed/src/util.rs b/zed/src/util.rs index 8ca2cfd9b6a897d3eb0e2271b2446194f0e4ee03..9e7a912ce8dfa076e9e8d9ccfbe2d66e96b8d7e2 100644 --- a/zed/src/util.rs +++ b/zed/src/util.rs @@ -1,7 +1,11 @@ -use futures::Future; +use futures::{Future}; pub use gpui::sum_tree::Bias; use rand::prelude::*; -use std::cmp::Ordering; +use std::{ + cmp::Ordering, + pin::Pin, + task::{Context, Poll}, +}; pub fn post_inc(value: &mut usize) -> usize { let prev = *value; @@ -60,12 +64,58 @@ impl Iterator for RandomCharIter { } } -pub async fn log_async_errors(f: F) +pub trait ResultExt { + type Ok; + + fn log_err(self) -> Option; +} + +impl ResultExt for anyhow::Result { + type Ok = T; + + fn log_err(self) -> Option { + match self { + Ok(value) => Some(value), + Err(error) => { + log::error!("{:?}", error); + None + } + } + } +} + +pub trait TryFutureExt { + fn log_err(self) -> LogErrorFuture + where + Self: Sized; +} + +impl TryFutureExt for F where - F: Future>, + F: Future>, { - if let Err(error) = f.await { - log::error!("{}", error) + fn log_err(self) -> LogErrorFuture + where + Self: Sized, + { + LogErrorFuture(self) + } +} + +pub struct LogErrorFuture(F); + +impl Future for LogErrorFuture +where + F: Future>, +{ + type Output = Option; + + fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; + match inner.poll(cx) { + Poll::Ready(output) => Poll::Ready(output.log_err()), + Poll::Pending => Poll::Pending, + } } } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index e121bc5d14e3b2b9469619af0af34f7e38c9ae4c..7b0e212d8d6a42a777e528a5fccf3c9c0d4e5a47 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -9,7 +9,7 @@ use crate::{ language::LanguageRegistry, rpc::{self, proto}, time::{self, ReplicaId}, - util::{log_async_errors, Bias}, + util::{Bias, TryFutureExt}, }; use ::ignore::gitignore::Gitignore; use anyhow::{anyhow, Result}; @@ -330,10 +330,13 @@ impl Worktree { .open_remote_buffer(envelope, cx); cx.background() - .spawn(log_async_errors(async move { - rpc.respond(receipt, response.await?).await?; - Ok(()) - })) + .spawn( + async move { + rpc.respond(receipt, response.await?).await?; + Ok(()) + } + .log_err(), + ) .detach(); Ok(()) @@ -465,22 +468,25 @@ impl Worktree { }); cx.background() - .spawn(log_async_errors(async move { - let (version, mtime) = save.await?; - - rpc.respond( - receipt, - proto::BufferSaved { - worktree_id, - buffer_id, - version: (&version).into(), - mtime: Some(mtime.into()), - }, - ) - .await?; + .spawn( + async move { + let (version, mtime) = save.await?; - Ok(()) - })) + rpc.respond( + receipt, + proto::BufferSaved { + worktree_id, + buffer_id, + version: (&version).into(), + mtime: Some(mtime.into()), + }, + ) + .await?; + + Ok(()) + } + .log_err(), + ) .detach(); Ok(()) From e4a232acc9588c6f703c0add0b10903417b7c3de Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 08:15:46 -0600 Subject: [PATCH 071/204] Send messages on enter Co-Authored-By: Antonio Scandurra --- zed/src/chat_panel.rs | 24 +++++++++++++++++++++++- zed/src/editor.rs | 7 +++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 458215e72c5791be3e9230523b4d0f617413f141..de4d183d5a822f4779ad688dc547a3b1f3091c20 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -1,10 +1,12 @@ use crate::{ channel::{Channel, ChannelEvent, ChannelList, ChannelMessage}, editor::Editor, + util::ResultExt, Settings, }; use gpui::{ - elements::*, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext, ViewHandle, + action, elements::*, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, View, + ViewContext, ViewHandle, }; use postage::watch; @@ -18,6 +20,12 @@ pub struct ChatPanel { pub enum Event {} +action!(Send); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ChatPanel::send); +} + impl ChatPanel { pub fn new( channel_list: ModelHandle, @@ -109,6 +117,20 @@ impl ChatPanel { .with_max_height(100.) .boxed() } + + fn send(&mut self, _: &Send, cx: &mut ViewContext) { + if let Some((channel, _)) = self.active_channel.as_ref() { + let body = self.input_editor.update(cx, |editor, cx| { + let body = editor.text(cx); + editor.clear(cx); + body + }); + + channel + .update(cx, |channel, cx| channel.send_message(body, cx)) + .log_err(); + } + } } impl Entity for ChatPanel { diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 6ac06cd37084605668b8f365473c2dcd3c525fb2..4aa5b6ce3e874bb5a6cf01f04b16880e91a1a742 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -719,6 +719,13 @@ impl Editor { self.end_transaction(cx); } + pub fn clear(&mut self, cx: &mut ViewContext) { + self.start_transaction(cx); + self.select_all(&SelectAll, cx); + self.insert(&Insert(String::new()), cx); + self.end_transaction(cx); + } + fn newline(&mut self, Newline(insert_newline): &Newline, cx: &mut ViewContext) { match self.mode { EditorMode::SingleLine => cx.propagate_action(), From 9b636fb81ed31089ca8ab46cc0db405731917ee9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 08:29:36 -0600 Subject: [PATCH 072/204] Add "Zed > Sign" In menu item Co-Authored-By: Antonio Scandurra --- zed/src/lib.rs | 17 ++++++++++++++--- zed/src/main.rs | 4 ++-- zed/src/menus.rs | 5 +++++ zed/src/rpc.rs | 7 +++++-- zed/src/theme_selector.rs | 2 +- zed/src/workspace.rs | 4 ++-- 6 files changed, 29 insertions(+), 10 deletions(-) diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 2290d5cf28aa476a65658ea35665a6d67fcd4483..d674fd5ea662cd49eb5582bb00fb724a7c6987b5 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -19,16 +19,18 @@ mod util; pub mod workspace; pub mod worktree; +use crate::util::TryFutureExt; use channel::ChannelList; use gpui::{action, ModelHandle}; -pub use settings::Settings; - use parking_lot::Mutex; use postage::watch; use std::sync::Arc; +pub use settings::Settings; + action!(About); action!(Quit); +action!(Authenticate); pub struct AppState { pub settings_tx: Arc>>, @@ -40,8 +42,17 @@ pub struct AppState { pub channel_list: ModelHandle, } -pub fn init(cx: &mut gpui::MutableAppContext) { +pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_global_action(quit); + + cx.add_global_action({ + let rpc = app_state.rpc.clone(); + move |_: &Authenticate, cx| { + let rpc = rpc.clone(); + cx.spawn(|cx| async move { rpc.authenticate_and_connect(cx).log_err().await }) + .detach(); + } + }); } fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) { diff --git a/zed/src/main.rs b/zed/src/main.rs index e3e365d15e888d70983c19cb0eae4917bfd2e5eb..1e77edd151ff3ef4032ac66cb6cde1cc5771d11e 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -39,11 +39,11 @@ fn main() { fs: Arc::new(RealFs), }); - zed::init(cx); + zed::init(&app_state, cx); workspace::init(cx); editor::init(cx); file_finder::init(cx); - theme_selector::init(cx, &app_state); + theme_selector::init(&app_state, cx); cx.set_menus(menus::menus(&app_state.clone())); diff --git a/zed/src/menus.rs b/zed/src/menus.rs index c43d72a87dfb83fd8d6dc2332fa183001bb16f8f..9ba28f9cef875b699bdc9ab5cc401c2f0c87332e 100644 --- a/zed/src/menus.rs +++ b/zed/src/menus.rs @@ -16,6 +16,11 @@ pub fn menus(state: &Arc) -> Vec> { action: Box::new(super::About), }, MenuItem::Separator, + MenuItem::Action { + name: "Sign In", + keystroke: None, + action: Box::new(super::Authenticate), + }, MenuItem::Action { name: "Share", keystroke: None, diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index b8909a988a5d1bbf4adfa08a1114d52092d0888e..b9992de6633604b2c066f625d57a1d393cd1ca43 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -124,7 +124,10 @@ impl Client { } } - pub async fn log_in_and_connect(self: &Arc, cx: AsyncAppContext) -> surf::Result<()> { + pub async fn authenticate_and_connect( + self: &Arc, + cx: AsyncAppContext, + ) -> anyhow::Result<()> { if self.state.read().connection_id.is_some() { return Ok(()); } @@ -161,7 +164,7 @@ impl Client { user_id: u64, conn: Conn, cx: AsyncAppContext, - ) -> surf::Result<()> + ) -> anyhow::Result<()> where Conn: 'static + futures::Sink diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index bfe5977e2e175a227ff5810ebdbb27894245c669..eb062899c171505120996b39089f1c30569619e9 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -34,7 +34,7 @@ action!(Confirm); action!(Toggle, Arc); action!(Reload, Arc); -pub fn init(cx: &mut MutableAppContext, app_state: &Arc) { +pub fn init(app_state: &Arc, cx: &mut MutableAppContext) { cx.add_action(ThemeSelector::confirm); cx.add_action(ThemeSelector::select_prev); cx.add_action(ThemeSelector::select_next); diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 28ed2701d123035c81493a5ce0d3e5b373b7a32e..4013b068a3bc62d7d70d0663b4ce616312878d2b 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -791,7 +791,7 @@ impl Workspace { let platform = cx.platform(); let task = cx.spawn(|this, mut cx| async move { - rpc.log_in_and_connect(cx.clone()).await?; + rpc.authenticate_and_connect(cx.clone()).await?; let share_task = this.update(&mut cx, |this, cx| { let worktree = this.worktrees.iter().next()?; @@ -823,7 +823,7 @@ impl Workspace { let languages = self.languages.clone(); let task = cx.spawn(|this, mut cx| async move { - rpc.log_in_and_connect(cx.clone()).await?; + rpc.authenticate_and_connect(cx.clone()).await?; let worktree_url = cx .platform() From 39ad7f6a60fecddaead0da5bc9d976843fe5346f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 08:37:32 -0600 Subject: [PATCH 073/204] Make RPC client's current user_id observable Co-Authored-By: Antonio Scandurra --- zed/src/channel.rs | 9 +++++++-- zed/src/rpc.rs | 21 ++++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 357546298c586e008aa4d8f412c92f7de0207e6b..f65320ae7fae3b501ccf801f177451fa425e4995 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -206,7 +206,8 @@ impl Channel { } }); Ok(()) - }.log_err() + } + .log_err() }) .detach(); cx.notify(); @@ -222,7 +223,11 @@ impl Channel { } fn current_user_id(&self) -> Result { - self.rpc.user_id().ok_or_else(|| anyhow!("not logged in")) + self + .rpc + .user_id() + .borrow() + .ok_or_else(|| anyhow!("not logged in")) } fn handle_message_sent( diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index b9992de6633604b2c066f625d57a1d393cd1ca43..4b1ab0e66e917a2db3910ffaa69f2a4d8eb396e2 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -5,6 +5,7 @@ use gpui::{AsyncAppContext, Entity, ModelContext, Task}; use lazy_static::lazy_static; use parking_lot::RwLock; use postage::prelude::Stream; +use postage::watch; use std::any::TypeId; use std::collections::HashMap; use std::sync::Weak; @@ -28,10 +29,9 @@ pub struct Client { state: RwLock, } -#[derive(Default)] struct ClientState { connection_id: Option, - user_id: Option, + user_id: (watch::Sender>, watch::Receiver>), entity_id_extractors: HashMap u64>>, model_handlers: HashMap< (TypeId, u64), @@ -39,6 +39,17 @@ struct ClientState { >, } +impl Default for ClientState { + fn default() -> Self { + Self { + connection_id: Default::default(), + user_id: watch::channel(), + entity_id_extractors: Default::default(), + model_handlers: Default::default(), + } + } +} + pub struct Subscription { client: Weak, id: (TypeId, u64), @@ -67,8 +78,8 @@ impl Client { }) } - pub fn user_id(&self) -> Option { - self.state.read().user_id + pub fn user_id(&self) -> watch::Receiver> { + self.state.read().user_id.1.clone() } pub fn subscribe_from_model( @@ -214,7 +225,7 @@ impl Client { .detach(); let mut state = self.state.write(); state.connection_id = Some(connection_id); - state.user_id = Some(user_id); + state.user_id = watch::channel_with(Some(user_id)); Ok(()) } From 2701abde11e5d4af3f568c14edb82d5d37bff093 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 09:29:14 -0600 Subject: [PATCH 074/204] WIP Co-Authored-By: Antonio Scandurra --- zed/src/channel.rs | 48 +++++++++++++++++++++----------- zed/src/chat_panel.rs | 64 ++++++++++++++++++++++++------------------- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index f65320ae7fae3b501ccf801f177451fa425e4995..37459a1215d6b241d2c9a6fc4f8ed3f6fc03b001 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -5,8 +5,9 @@ use crate::{ use anyhow::{anyhow, Context, Result}; use gpui::{ sum_tree::{self, Bias, SumTree}, - Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle, + Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle, }; +use postage::prelude::Stream; use std::{ collections::{hash_map, HashMap}, ops::Range, @@ -21,6 +22,7 @@ pub struct ChannelList { available_channels: Option>, channels: HashMap>, rpc: Arc, + _task: Task>, } #[derive(Clone, Debug, PartialEq)] @@ -74,27 +76,42 @@ impl Entity for ChannelList { impl ChannelList { pub fn new(rpc: Arc, cx: &mut ModelContext) -> Self { - cx.spawn(|this, mut cx| { + let _task = cx.spawn(|this, mut cx| { let rpc = rpc.clone(); async move { - let response = rpc - .request(proto::GetChannels {}) - .await - .context("failed to fetch available channels")?; - this.update(&mut cx, |this, cx| { - this.available_channels = - Some(response.channels.into_iter().map(Into::into).collect()); - cx.notify(); - }); - Ok(()) + let mut user_id = rpc.user_id(); + loop { + let available_channels = if user_id.recv().await.unwrap().is_some() { + Some( + rpc.request(proto::GetChannels {}) + .await + .context("failed to fetch available channels")? + .channels + .into_iter() + .map(Into::into) + .collect(), + ) + } else { + None + }; + + this.update(&mut cx, |this, cx| { + if available_channels.is_none() { + this.channels.clear(); + } + this.available_channels = available_channels; + cx.notify(); + }); + } } .log_err() - }) - .detach(); + }); + Self { available_channels: None, channels: Default::default(), rpc, + _task, } } @@ -223,8 +240,7 @@ impl Channel { } fn current_user_id(&self) -> Result { - self - .rpc + self.rpc .user_id() .borrow() .ok_or_else(|| anyhow!("not logged in")) diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index de4d183d5a822f4779ad688dc547a3b1f3091c20..d722d5add9d5d08de24bccbd343fa801de99b57a 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -5,8 +5,8 @@ use crate::{ Settings, }; use gpui::{ - action, elements::*, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, View, - ViewContext, ViewHandle, + action, elements::*, keymap::Binding, Entity, ModelHandle, MutableAppContext, RenderContext, + Subscription, View, ViewContext, ViewHandle, }; use postage::watch; @@ -24,6 +24,8 @@ action!(Send); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ChatPanel::send); + + cx.add_bindings(vec![Binding::new("enter", Send, Some("ChatPanel"))]); } impl ChatPanel { @@ -41,44 +43,50 @@ impl ChatPanel { settings, }; - this.assign_active_channel(cx); + this.init_active_channel(cx); cx.observe(&this.channel_list, |this, _, cx| { - this.assign_active_channel(cx); + this.init_active_channel(cx); }) .detach(); this } - pub fn assign_active_channel(&mut self, cx: &mut ViewContext) { - let channel = self.channel_list.update(cx, |list, cx| { - if let Some(channel_id) = list - .available_channels() - .and_then(|channels| channels.first()) - .map(|details| details.id) - { - return list.get_channel(channel_id, cx); - } - None - }); - if let Some(channel) = channel { - if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) { - let subscription = cx.subscribe(&channel, Self::channel_did_change); - self.messages = ListState::new( - channel - .read(cx) - .messages() - .cursor::<(), ()>() - .map(|m| self.render_message(m)) - .collect(), - ); - self.active_channel = Some((channel, subscription)); + fn init_active_channel(&mut self, cx: &mut ViewContext) { + if self.active_channel.is_none() { + let channel = self.channel_list.update(cx, |list, cx| { + if let Some(channel_id) = list + .available_channels() + .and_then(|channels| channels.first()) + .map(|details| details.id) + { + return list.get_channel(channel_id, cx); + } + None + }); + if let Some(channel) = channel { + self.set_active_channel(channel, cx); } - } else { + } else if self.channel_list.read(cx).available_channels().is_none() { self.active_channel = None; } } + fn set_active_channel(&mut self, channel: ModelHandle, cx: &mut ViewContext) { + if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) { + let subscription = cx.subscribe(&channel, Self::channel_did_change); + self.messages = ListState::new( + channel + .read(cx) + .messages() + .cursor::<(), ()>() + .map(|m| self.render_message(m)) + .collect(), + ); + self.active_channel = Some((channel, subscription)); + } + } + fn channel_did_change( &mut self, _: ModelHandle, From bb570d37028d61620663aba57e81cc3350574769 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 24 Aug 2021 17:54:25 +0200 Subject: [PATCH 075/204] Init chat_panel --- zed/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zed/src/main.rs b/zed/src/main.rs index 1e77edd151ff3ef4032ac66cb6cde1cc5771d11e..7eaa5ea83815fa4a9e077b0df6882d5c96054963 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -9,7 +9,7 @@ use std::{fs, path::PathBuf, sync::Arc}; use zed::{ self, assets, channel::ChannelList, - editor, file_finder, + chat_panel, editor, file_finder, fs::RealFs, language, menus, rpc, settings, theme_selector, workspace::{self, OpenParams, OpenPaths}, @@ -43,6 +43,7 @@ fn main() { workspace::init(cx); editor::init(cx); file_finder::init(cx); + chat_panel::init(cx); theme_selector::init(&app_state, cx); cx.set_menus(menus::menus(&app_state.clone())); From d9d52b8aa53b32e03897cda428e82c93ee66ccf6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 24 Aug 2021 13:11:37 -0700 Subject: [PATCH 076/204] When loggin in, send user id instead of creating a new watch channel --- zed/src/rpc.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 4b1ab0e66e917a2db3910ffaa69f2a4d8eb396e2..ee1ff2fbb6ae8306f6ef65c4730b1cf6afcac17d 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -5,6 +5,7 @@ use gpui::{AsyncAppContext, Entity, ModelContext, Task}; use lazy_static::lazy_static; use parking_lot::RwLock; use postage::prelude::Stream; +use postage::sink::Sink; use postage::watch; use std::any::TypeId; use std::collections::HashMap; @@ -225,7 +226,7 @@ impl Client { .detach(); let mut state = self.state.write(); state.connection_id = Some(connection_id); - state.user_id = watch::channel_with(Some(user_id)); + state.user_id.0.send(Some(user_id)).await?; Ok(()) } From 8179f90589eb9803321b36cbca57d37e0f2b08c8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 24 Aug 2021 13:12:02 -0700 Subject: [PATCH 077/204] Add initial unit test for channel list Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 47 +++++++++++ zed/Cargo.toml | 1 + zed/src/channel.rs | 184 +++++++++++++++++++++++++++++++++++++++++- zed/src/chat_panel.rs | 16 +++- zrpc/src/proto.rs | 5 -- 5 files changed, 240 insertions(+), 13 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 89dd5582c062eaec2b4d5a521bce7580a9109bb4..8c8fd2448548a3c8c616ef8095d5f9d76977e383 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2356,6 +2356,53 @@ impl ModelHandle { cx.update_model(self, update) } + pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { + let (mut tx, mut rx) = mpsc::channel(1); + let mut cx = cx.cx.borrow_mut(); + let subscription = cx.observe(self, move |_, _| { + tx.blocking_send(()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + async move { + let notification = timeout(duration, rx.recv()) + .await + .expect("next notification timed out"); + drop(subscription); + notification.expect("model dropped while test was waiting for its next notification") + } + } + + pub fn next_event(&self, cx: &TestAppContext) -> impl Future + where + T::Event: Clone, + { + let (mut tx, mut rx) = mpsc::channel(1); + let mut cx = cx.cx.borrow_mut(); + let subscription = cx.subscribe(self, move |_, event, _| { + tx.blocking_send(event.clone()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + async move { + let event = timeout(duration, rx.recv()) + .await + .expect("next event timed out"); + drop(subscription); + event.expect("model dropped while test was waiting for its next event") + } + } + pub fn condition( &self, cx: &TestAppContext, diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 03bcdd3a56fa2c9549f1b4fd61dbaba2568b6300..13bc411e3bdda0df9743e3087c261da6983626d6 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -61,6 +61,7 @@ env_logger = "0.8" serde_json = { version = "1.0.64", features = ["preserve_order"] } tempdir = { version = "0.3.7" } unindent = "0.1.7" +zrpc = { path = "../zrpc", features = ["test-support"] } [package.metadata.bundle] icon = ["app-icon@2x.png", "app-icon.png"] diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 37459a1215d6b241d2c9a6fc4f8ed3f6fc03b001..42600781c91fd0726b6092c352ce5801b9b7d0e9 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -40,7 +40,7 @@ pub struct Channel { _subscription: rpc::Subscription, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct ChannelMessage { pub id: u64, pub sender_id: u64, @@ -63,10 +63,11 @@ struct Count(usize); pub enum ChannelListEvent {} +#[derive(Clone, Debug, PartialEq)] pub enum ChannelEvent { Message { old_range: Range, - message: ChannelMessage, + new_count: usize, }, } @@ -170,11 +171,16 @@ impl Channel { cx.spawn(|channel, mut cx| async move { match rpc.request(proto::JoinChannel { channel_id }).await { Ok(response) => channel.update(&mut cx, |channel, cx| { + let old_count = channel.messages.summary().count.0; + let new_count = response.messages.len(); channel.messages = SumTree::new(); channel .messages .extend(response.messages.into_iter().map(Into::into), &()); - cx.notify(); + cx.emit(ChannelEvent::Message { + old_range: 0..old_count, + new_count, + }); }), Err(error) => log::error!("error joining channel: {}", error), } @@ -235,6 +241,12 @@ impl Channel { &self.messages } + pub fn messages_in_range(&self, range: Range) -> impl Iterator { + let mut cursor = self.messages.cursor::(); + cursor.seek(&Count(range.start), Bias::Left, &()); + cursor.take(range.len()) + } + pub fn pending_messages(&self) -> &[PendingChannelMessage] { &self.pending_messages } @@ -277,7 +289,7 @@ impl Channel { cx.emit(ChannelEvent::Message { old_range: start_ix..end_ix, - message, + new_count: 1, }); cx.notify(); } @@ -334,3 +346,167 @@ impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count { self.0 += summary.count.0; } } + +impl<'a> sum_tree::SeekDimension<'a, ChannelMessageSummary> for Count { + fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering { + Ord::cmp(&self.0, &other.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + use postage::mpsc::Receiver; + use zrpc::{test::Channel, ConnectionId, Peer, Receipt}; + + #[gpui::test] + async fn test_channel_messages(mut cx: TestAppContext) { + let user_id = 5; + let client = Client::new(); + let mut server = FakeServer::for_client(user_id, &client, &cx).await; + + let channel_list = cx.add_model(|cx| ChannelList::new(client.clone(), cx)); + channel_list.read_with(&cx, |list, _| assert_eq!(list.available_channels(), None)); + + // Get the available channels. + let message = server.receive::().await; + server + .respond( + message.receipt(), + proto::GetChannelsResponse { + channels: vec![proto::Channel { + id: 5, + name: "the-channel".to_string(), + }], + }, + ) + .await; + channel_list.next_notification(&cx).await; + channel_list.read_with(&cx, |list, _| { + assert_eq!( + list.available_channels().unwrap(), + &[ChannelDetails { + id: 5, + name: "the-channel".into(), + }] + ) + }); + + // Join a channel and populate its existing messages. + let channel = channel_list + .update(&mut cx, |list, cx| { + let channel_id = list.available_channels().unwrap()[0].id; + list.get_channel(channel_id, cx) + }) + .unwrap(); + channel.read_with(&cx, |channel, _| assert!(channel.messages().is_empty())); + let message = server.receive::().await; + server + .respond( + message.receipt(), + proto::JoinChannelResponse { + messages: vec![ + proto::ChannelMessage { + id: 10, + body: "a".into(), + timestamp: 1000, + sender_id: 5, + }, + proto::ChannelMessage { + id: 11, + body: "b".into(), + timestamp: 1001, + sender_id: 5, + }, + ], + }, + ) + .await; + assert_eq!( + channel.next_event(&cx).await, + ChannelEvent::Message { + old_range: 0..0, + new_count: 2, + } + ); + channel.read_with(&cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(0..2) + .map(|message| &message.body) + .collect::>(), + &["a", "b"] + ); + }); + + // Receive a new message. + server + .send(proto::ChannelMessageSent { + channel_id: channel.read_with(&cx, |channel, _| channel.details.id), + message: Some(proto::ChannelMessage { + id: 12, + body: "c".into(), + timestamp: 1002, + sender_id: 5, + }), + }) + .await; + assert_eq!( + channel.next_event(&cx).await, + ChannelEvent::Message { + old_range: 2..2, + new_count: 1, + } + ); + } + + struct FakeServer { + peer: Arc, + incoming: Receiver>, + connection_id: ConnectionId, + } + + impl FakeServer { + async fn for_client(user_id: u64, client: &Arc, cx: &TestAppContext) -> Self { + let (client_conn, server_conn) = Channel::bidirectional(); + let peer = Peer::new(); + let (connection_id, io, incoming) = peer.add_connection(server_conn).await; + cx.background().spawn(io).detach(); + + client + .add_connection(user_id, client_conn, cx.to_async()) + .await + .unwrap(); + + Self { + peer, + incoming, + connection_id, + } + } + + async fn send(&self, message: T) { + self.peer.send(self.connection_id, message).await.unwrap(); + } + + async fn receive(&mut self) -> TypedEnvelope { + *self + .incoming + .recv() + .await + .unwrap() + .into_any() + .downcast::>() + .unwrap() + } + + async fn respond( + &self, + receipt: Receipt, + response: T::Response, + ) { + self.peer.respond(receipt, response).await.unwrap() + } + } +} diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index d722d5add9d5d08de24bccbd343fa801de99b57a..5758879cd1faebd224fa50554498877a147298fc 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -89,14 +89,22 @@ impl ChatPanel { fn channel_did_change( &mut self, - _: ModelHandle, + channel: ModelHandle, event: &ChannelEvent, cx: &mut ViewContext, ) { match event { - ChannelEvent::Message { old_range, message } => { - self.messages - .splice(old_range.clone(), Some(self.render_message(message))); + ChannelEvent::Message { + old_range, + new_count, + } => { + self.messages.splice( + old_range.clone(), + channel + .read(cx) + .messages_in_range(old_range.start..(old_range.start + new_count)) + .map(|message| self.render_message(message)), + ); } } cx.notify(); diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index 0bcbfd4d65ba1152fb089691948f0e09bbbb747c..330e3afa48380629dc0d78dfbd76943ee813bfdb 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -19,7 +19,6 @@ pub trait EnvelopedMessage: Clone + Sized + Send + Sync + 'static { responding_to: Option, original_sender_id: Option, ) -> Envelope; - fn matches_envelope(envelope: &Envelope) -> bool; fn from_envelope(envelope: Envelope) -> Option; } @@ -90,10 +89,6 @@ macro_rules! messages { } } - fn matches_envelope(envelope: &Envelope) -> bool { - matches!(&envelope.payload, Some(envelope::Payload::$name(_))) - } - fn from_envelope(envelope: Envelope) -> Option { if let Some(envelope::Payload::$name(msg)) = envelope.payload { Some(msg) From 8559be9f921d283b9dbfb829c6c5c69137fa76ca Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 17:11:27 -0600 Subject: [PATCH 078/204] Default unstyled text to be red instead of transparent --- gpui/src/fonts.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index e9f84676e791f19fdd7badf3a7e52de277f22a99..ab387b65bb1ff59c5db5e1743f8f5540950c47e6 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -14,12 +14,21 @@ pub struct FontId(pub usize); pub type GlyphId = u32; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TextStyle { pub color: Color, pub font_properties: Properties, } +impl Default for TextStyle { + fn default() -> Self { + Self { + color: Color::from_u32(0xff0000ff), + font_properties: Default::default(), + } + } +} + #[allow(non_camel_case_types)] #[derive(Deserialize)] enum WeightJson { From 3ad894b7e8f7208f0457c782ddf9b8c0953bcfaf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 17:11:40 -0600 Subject: [PATCH 079/204] Hit the local server when debugging --- .vscode/launch.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index aea73eeaa235a52aaf2a41ad1d688d3c0276281c..668019e696d7a0b4ac2e05382d46df143bddd53e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,6 +8,9 @@ "type": "lldb", "request": "launch", "name": "Debug executable 'Zed'", + "env": { + "ZED_SERVER_URL": "http://localhost:8080" + }, "cargo": { "args": [ "build", From ff966c8d04213fd507c37fc1800558e8acf284c8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 17:17:55 -0600 Subject: [PATCH 080/204] Fix off-by-1 in Channel::messages_in_range Co-Authored-By: Max Brunsfeld --- zed/src/channel.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 42600781c91fd0726b6092c352ce5801b9b7d0e9..2eea0d2a99030dfb071b61482e1c86c82a4a5eb3 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -243,7 +243,7 @@ impl Channel { pub fn messages_in_range(&self, range: Range) -> impl Iterator { let mut cursor = self.messages.cursor::(); - cursor.seek(&Count(range.start), Bias::Left, &()); + cursor.seek(&Count(range.start), Bias::Right, &()); cursor.take(range.len()) } @@ -459,6 +459,15 @@ mod tests { new_count: 1, } ); + channel.read_with(&cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(2..3) + .map(|message| &message.body) + .collect::>(), + &["c"] + ) + }) } struct FakeServer { From f8fb45912d97419c892e07e39faf8dc3b68acb49 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 17:18:40 -0600 Subject: [PATCH 081/204] Avoid redundant notification after creating the ChannelList when not authenticated Co-Authored-By: Max Brunsfeld --- zed/src/channel.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 2eea0d2a99030dfb071b61482e1c86c82a4a5eb3..a70b7dd0685fc21055d640d54d2c190ee5a24ff5 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -98,6 +98,9 @@ impl ChannelList { this.update(&mut cx, |this, cx| { if available_channels.is_none() { + if this.available_channels.is_none() { + return; + } this.channels.clear(); } this.available_channels = available_channels; From e69d1f9a9b0e25b47666be9e20de5276e4a15f0f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 17:33:56 -0600 Subject: [PATCH 082/204] Break up theme module Co-Authored-By: Max Brunsfeld --- zed/src/theme.rs | 852 +------------------------------- zed/src/theme/highlight_map.rs | 98 ++++ zed/src/theme/theme_registry.rs | 756 ++++++++++++++++++++++++++++ 3 files changed, 862 insertions(+), 844 deletions(-) create mode 100644 zed/src/theme/highlight_map.rs create mode 100644 zed/src/theme/theme_registry.rs diff --git a/zed/src/theme.rs b/zed/src/theme.rs index a8111d8b0a12d767a4ec5b3c3386458d0562ab56..f2425b5e2f33389f7896c1829bcde0776f9d47fd 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -1,30 +1,19 @@ -use anyhow::{anyhow, Context, Result}; +mod highlight_map; +mod theme_registry; + +use anyhow::Result; use gpui::{ color::Color, elements::{ContainerStyle, LabelStyle}, fonts::TextStyle, - AssetSource, }; -use json::{Map, Value}; -use parking_lot::Mutex; use serde::{Deserialize, Deserializer}; -use serde_json as json; -use std::{collections::HashMap, fmt, mem, sync::Arc}; - -const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); -pub const DEFAULT_THEME_NAME: &'static str = "dark"; +use std::collections::HashMap; -pub struct ThemeRegistry { - assets: Box, - themes: Mutex>>, - theme_data: Mutex>>, -} - -#[derive(Clone, Debug)] -pub struct HighlightMap(Arc<[HighlightId]>); +pub use highlight_map::*; +pub use theme_registry::*; -#[derive(Clone, Copy, Debug)] -pub struct HighlightId(u32); +pub const DEFAULT_THEME_NAME: &'static str = "dark"; #[derive(Debug, Default, Deserialize)] pub struct Theme { @@ -99,30 +88,6 @@ pub struct SelectorItem { pub label: LabelStyle, } -#[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 Default for Editor { fn default() -> Self { Self { @@ -137,119 +102,6 @@ impl Default for Editor { } } -impl ThemeRegistry { - pub fn new(source: impl AssetSource) -> Arc { - Arc::new(Self { - assets: Box::new(source), - themes: Default::default(), - theme_data: Default::default(), - }) - } - - pub fn list(&self) -> impl Iterator { - self.assets.list("themes/").into_iter().filter_map(|path| { - let filename = path.strip_prefix("themes/")?; - let theme_name = filename.strip_suffix(".toml")?; - if theme_name.starts_with('_') { - None - } else { - Some(theme_name.to_string()) - } - }) - } - - pub fn clear(&self) { - self.theme_data.lock().clear(); - self.themes.lock().clear(); - } - - pub fn get(&self, name: &str) -> Result> { - if let Some(theme) = self.themes.lock().get(name) { - return Ok(theme.clone()); - } - - let theme_data = self.load(name, true)?; - let mut theme = serde_json::from_value::(theme_data.as_ref().clone())?; - theme.name = name.into(); - let theme = Arc::new(theme); - self.themes.lock().insert(name.to_string(), theme.clone()); - Ok(theme) - } - - fn load(&self, name: &str, evaluate_references: bool) -> Result> { - if let Some(data) = self.theme_data.lock().get(name) { - return Ok(data.clone()); - } - - let asset_path = format!("themes/{}.toml", name); - let source_code = self - .assets - .load(&asset_path) - .with_context(|| format!("failed to load theme file {}", asset_path))?; - - let mut theme_data: Map = toml::from_slice(source_code.as_ref()) - .with_context(|| format!("failed to parse {}.toml", name))?; - - // If this theme extends another base theme, deeply merge it into the base theme's data - if let Some(base_name) = theme_data - .get("extends") - .and_then(|name| name.as_str()) - .map(str::to_string) - { - let base_theme_data = self - .load(&base_name, false) - .with_context(|| format!("failed to load base theme {}", base_name))? - .as_ref() - .clone(); - if let Value::Object(mut base_theme_object) = base_theme_data { - deep_merge_json(&mut base_theme_object, theme_data); - theme_data = base_theme_object; - } - } - - // 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))?; - } - } - } - - let result = Arc::new(Value::Object(theme_data)); - self.theme_data - .lock() - .insert(name.to_string(), result.clone()); - - Ok(result) - } -} - impl Theme { pub fn highlight_style(&self, id: HighlightId) -> TextStyle { self.syntax @@ -267,416 +119,6 @@ impl Theme { } } -impl HighlightMap { - pub fn new(capture_names: &[String], theme: &Theme) -> Self { - // For each capture name in the highlight query, find the longest - // key in the theme's syntax styles that matches all of the - // dot-separated components of the capture name. - HighlightMap( - capture_names - .iter() - .map(|capture_name| { - theme - .syntax - .iter() - .enumerate() - .filter_map(|(i, (key, _))| { - let mut len = 0; - let capture_parts = capture_name.split('.'); - for key_part in key.split('.') { - if capture_parts.clone().any(|part| part == key_part) { - len += 1; - } else { - return None; - } - } - Some((i, len)) - }) - .max_by_key(|(_, len)| *len) - .map_or(DEFAULT_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32)) - }) - .collect(), - ) - } - - pub fn get(&self, capture_id: u32) -> HighlightId { - self.0 - .get(capture_id as usize) - .copied() - .unwrap_or(DEFAULT_HIGHLIGHT_ID) - } -} - -impl KeyPathReferenceSet { - fn insert(&mut self, reference: KeyPathReference) { - let id = self.references.len(); - let source_ix = self - .reference_ids_by_source - .binary_search_by_key(&&reference.source, |id| &self.references[*id].source) - .unwrap_or_else(|i| i); - let target_ix = self - .reference_ids_by_target - .binary_search_by_key(&&reference.target, |id| &self.references[*id].target) - .unwrap_or_else(|i| i); - - self.populate_dependencies(id, &reference); - self.reference_ids_by_source.insert(source_ix, id); - self.reference_ids_by_target.insert(target_ix, id); - self.references.push(reference); - } - - fn top_sort(mut self) -> Result, Vec> { - let mut results = Vec::with_capacity(self.references.len()); - let mut root_ids = Vec::with_capacity(self.references.len()); - - // Find the initial set of references that have no dependencies. - for (id, dep_count) in self.dependency_counts.iter().enumerate() { - if *dep_count == 0 { - root_ids.push(id); - } - } - - while results.len() < root_ids.len() { - // Just to guarantee a stable result when the inputs are randomized, - // sort references lexicographically in absence of any dependency relationship. - root_ids[results.len()..].sort_by_key(|id| &self.references[*id]); - - let root_id = root_ids[results.len()]; - let root = mem::take(&mut self.references[root_id]); - results.push(root); - - // Remove this reference as a dependency from any of its dependent references. - if let Ok(dep_ix) = self - .dependencies - .binary_search_by_key(&root_id, |edge| edge.0) - { - let mut first_dep_ix = dep_ix; - let mut last_dep_ix = dep_ix + 1; - while first_dep_ix > 0 && self.dependencies[first_dep_ix - 1].0 == root_id { - first_dep_ix -= 1; - } - while last_dep_ix < self.dependencies.len() - && self.dependencies[last_dep_ix].0 == root_id - { - last_dep_ix += 1; - } - - // If any reference no longer has any dependencies, then then mark it as a root. - // Preserve the references' original order where possible. - for (_, successor_id) in self.dependencies.drain(first_dep_ix..last_dep_ix) { - self.dependency_counts[successor_id] -= 1; - if self.dependency_counts[successor_id] == 0 { - root_ids.push(successor_id); - } - } - } - } - - // If any references never became roots, then there are reference cycles - // in the set. Return an error containing all of the key paths that are - // directly involved in cycles. - if results.len() < self.references.len() { - let mut cycle_ref_ids = (0..self.references.len()) - .filter(|id| !root_ids.contains(id)) - .collect::>(); - - // Iteratively remove any references that have no dependencies, - // so that the error will only indicate which key paths are directly - // involved in the cycles. - let mut done = false; - while !done { - done = true; - cycle_ref_ids.retain(|id| { - if self.dependencies.iter().any(|dep| dep.0 == *id) { - true - } else { - done = false; - self.dependencies.retain(|dep| dep.1 != *id); - false - } - }); - } - - let mut cycle_key_paths = Vec::new(); - for id in cycle_ref_ids { - let reference = &self.references[id]; - cycle_key_paths.push(reference.target.clone()); - cycle_key_paths.push(reference.source.clone()); - } - cycle_key_paths.sort_unstable(); - return Err(cycle_key_paths); - } - - Ok(results) - } - - fn populate_dependencies(&mut self, new_id: usize, new_reference: &KeyPathReference) { - self.dependency_counts.push(0); - - // If an existing reference's source path starts with the new reference's - // target path, then insert this new reference before that existing reference. - for id in Self::reference_ids_for_key_path( - &new_reference.target.0, - &self.references, - &self.reference_ids_by_source, - KeyPathReference::source, - KeyPath::starts_with, - ) { - Self::add_dependency( - (new_id, id), - &mut self.dependencies, - &mut self.dependency_counts, - ); - } - - // If an existing reference's target path starts with the new reference's - // source path, then insert this new reference after that existing reference. - for id in Self::reference_ids_for_key_path( - &new_reference.source.0, - &self.references, - &self.reference_ids_by_target, - KeyPathReference::target, - KeyPath::starts_with, - ) { - Self::add_dependency( - (id, new_id), - &mut self.dependencies, - &mut self.dependency_counts, - ); - } - - // If an existing reference's source path is a prefix of the new reference's - // target path, then insert this new reference before that existing reference. - for prefix in new_reference.target.prefixes() { - for id in Self::reference_ids_for_key_path( - prefix, - &self.references, - &self.reference_ids_by_source, - KeyPathReference::source, - PartialEq::eq, - ) { - Self::add_dependency( - (new_id, id), - &mut self.dependencies, - &mut self.dependency_counts, - ); - } - } - - // If an existing reference's target path is a prefix of the new reference's - // source path, then insert this new reference after that existing reference. - for prefix in new_reference.source.prefixes() { - for id in Self::reference_ids_for_key_path( - prefix, - &self.references, - &self.reference_ids_by_target, - KeyPathReference::target, - PartialEq::eq, - ) { - Self::add_dependency( - (id, new_id), - &mut self.dependencies, - &mut self.dependency_counts, - ); - } - } - } - - // Find all existing references that satisfy a given predicate with respect - // to a given key path. Use a sorted array of reference ids in order to avoid - // performing unnecessary comparisons. - fn reference_ids_for_key_path<'a>( - key_path: &[Key], - references: &[KeyPathReference], - sorted_reference_ids: &'a [usize], - reference_attribute: impl Fn(&KeyPathReference) -> &KeyPath, - predicate: impl Fn(&KeyPath, &[Key]) -> bool, - ) -> impl Iterator + 'a { - let ix = sorted_reference_ids - .binary_search_by_key(&key_path, |id| &reference_attribute(&references[*id]).0) - .unwrap_or_else(|i| i); - - let mut start_ix = ix; - while start_ix > 0 { - let reference_id = sorted_reference_ids[start_ix - 1]; - let reference = &references[reference_id]; - if !predicate(&reference_attribute(reference), key_path) { - break; - } - start_ix -= 1; - } - - let mut end_ix = ix; - while end_ix < sorted_reference_ids.len() { - let reference_id = sorted_reference_ids[end_ix]; - let reference = &references[reference_id]; - if !predicate(&reference_attribute(reference), key_path) { - break; - } - end_ix += 1; - } - - sorted_reference_ids[start_ix..end_ix].iter().copied() - } - - fn add_dependency( - (predecessor, successor): (usize, usize), - dependencies: &mut Vec<(usize, usize)>, - dependency_counts: &mut Vec, - ) { - let dependency = (predecessor, successor); - if let Err(i) = dependencies.binary_search(&dependency) { - dependencies.insert(i, dependency); - } - dependency_counts[successor] += 1; - } -} - -impl KeyPathReference { - fn source(&self) -> &KeyPath { - &self.source - } - - fn target(&self) -> &KeyPath { - &self.target - } -} - -impl KeyPath { - fn new(string: &str) -> Self { - Self( - string - .split(".") - .map(|key| Key::Object(key.to_string())) - .collect(), - ) - } - - fn starts_with(&self, other: &[Key]) -> bool { - self.0.starts_with(&other) - } - - fn prefixes(&self) -> impl Iterator { - (1..self.0.len()).map(move |end_ix| &self.0[0..end_ix]) - } -} - -impl PartialEq<[Key]> for KeyPath { - fn eq(&self, other: &[Key]) -> bool { - self.0.eq(other) - } -} - -impl fmt::Debug for KeyPathReference { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "KeyPathReference {{ {} <- {} }}", - self.target, self.source - ) - } -} - -impl fmt::Display for KeyPath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for (i, key) in self.0.iter().enumerate() { - match key { - Key::Array(index) => write!(f, "[{}]", index)?, - Key::Object(key) => { - if i > 0 { - ".".fmt(f)?; - } - key.fmt(f)?; - } - } - } - Ok(()) - } -} - -impl Default for HighlightMap { - fn default() -> Self { - Self(Arc::new([])) - } -} - -impl Default for HighlightId { - fn default() -> Self { - DEFAULT_HIGHLIGHT_ID - } -} - -fn deep_merge_json(base: &mut Map, extension: Map) { - for (key, extension_value) in extension { - if let Value::Object(extension_object) = extension_value { - if let Some(base_object) = base.get_mut(&key).and_then(|value| value.as_object_mut()) { - deep_merge_json(base_object, extension_object); - } else { - base.insert(key, Value::Object(extension_object)); - } - } else { - base.insert(key, extension_value); - } - } -} - -fn find_references(value: &Value, key_path: &mut KeyPath, references: &mut KeyPathReferenceSet) { - match value { - Value::Array(vec) => { - for (ix, value) in vec.iter().enumerate() { - key_path.0.push(Key::Array(ix)); - find_references(value, key_path, references); - key_path.0.pop(); - } - } - Value::Object(map) => { - for (key, value) in map.iter() { - if key == "extends" { - if let Some(source_path) = value.as_str().and_then(|s| s.strip_prefix("$")) { - references.insert(KeyPathReference { - source: KeyPath::new(source_path), - target: key_path.clone(), - }); - } - } else { - key_path.0.push(Key::Object(key.to_string())); - find_references(value, key_path, references); - key_path.0.pop(); - } - } - } - Value::String(string) => { - if let Some(source_path) = string.strip_prefix("$") { - references.insert(KeyPathReference { - source: KeyPath::new(source_path), - target: key_path.clone(), - }); - } - } - _ => {} - } -} - -fn value_at<'a>(object: &'a mut Map, 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 - } -} - pub fn deserialize_syntax_theme<'de, D>( deserializer: D, ) -> Result, D::Error> @@ -696,281 +138,3 @@ where Ok(result) } - -#[cfg(test)] -mod tests { - use rand::{prelude::StdRng, Rng}; - - use super::*; - use crate::assets::Assets; - - #[test] - fn test_bundled_themes() { - let registry = ThemeRegistry::new(Assets); - let mut has_default_theme = false; - for theme_name in registry.list() { - let theme = registry.get(&theme_name).unwrap(); - if theme.name == DEFAULT_THEME_NAME { - has_default_theme = true; - } - assert_eq!(theme.name, theme_name); - } - assert!(has_default_theme); - } - - #[test] - fn test_theme_extension() { - let assets = TestAssets(&[ - ( - "themes/_base.toml", - r##" - [ui.active_tab] - extends = "$ui.tab" - border.color = "#666666" - text = "$text_colors.bright" - - [ui.tab] - extends = "$ui.element" - text = "$text_colors.dull" - - [ui.element] - background = "#111111" - border = {width = 2.0, color = "#00000000"} - - [editor] - background = "#222222" - default_text = "$text_colors.regular" - "##, - ), - ( - "themes/light.toml", - r##" - extends = "_base" - - [text_colors] - bright = "#ffffff" - regular = "#eeeeee" - dull = "#dddddd" - - [editor] - background = "#232323" - "##, - ), - ]); - - let registry = ThemeRegistry::new(assets); - let theme_data = registry.load("light", true).unwrap(); - assert_eq!( - theme_data.as_ref(), - &serde_json::json!({ - "ui": { - "active_tab": { - "background": "#111111", - "border": { - "width": 2.0, - "color": "#666666" - }, - "extends": "$ui.tab", - "text": "#ffffff" - }, - "tab": { - "background": "#111111", - "border": { - "width": 2.0, - "color": "#00000000" - }, - "extends": "$ui.element", - "text": "#dddddd" - }, - "element": { - "background": "#111111", - "border": { - "width": 2.0, - "color": "#00000000" - } - } - }, - "editor": { - "background": "#232323", - "default_text": "#eeeeee" - }, - "extends": "_base", - "text_colors": { - "bright": "#ffffff", - "regular": "#eeeeee", - "dull": "#dddddd" - } - }) - ); - } - - #[test] - fn test_highlight_map() { - let theme = Theme { - name: "test".into(), - syntax: [ - ("function", Color::from_u32(0x100000ff)), - ("function.method", Color::from_u32(0x200000ff)), - ("function.async", Color::from_u32(0x300000ff)), - ("variable.builtin.self.rust", Color::from_u32(0x400000ff)), - ("variable.builtin", Color::from_u32(0x500000ff)), - ("variable", Color::from_u32(0x600000ff)), - ] - .iter() - .map(|(name, color)| (name.to_string(), (*color).into())) - .collect(), - ..Default::default() - }; - - let capture_names = &[ - "function.special".to_string(), - "function.async.rust".to_string(), - "variable.builtin.self".to_string(), - ]; - - let map = HighlightMap::new(capture_names, &theme); - assert_eq!(theme.highlight_name(map.get(0)), Some("function")); - assert_eq!(theme.highlight_name(map.get(1)), Some("function.async")); - assert_eq!(theme.highlight_name(map.get(2)), Some("variable.builtin")); - } - - #[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", "m.n.n.o.q"), - ("x.y.z", "m.n.n.o.p"), - ("u.v.w", "x.y.z"), - ("a.b.c.d", "u.v"), - ("a.b.c.d.e", "u.v"), - ("a.b.c.d.f", "u.v"), - ("a.b.c.d.g", "u.v"), - ], - ]; - - for example in examples { - let expected_references = build_refs(example).collect::>(); - let mut input_references = expected_references.clone(); - input_references.sort_by_key(|_| rng.gen_range(0..1000)); - let mut reference_set = KeyPathReferenceSet::default(); - for reference in input_references { - reference_set.insert(reference); - } - assert_eq!(reference_set.top_sort().unwrap(), expected_references); - } - } - - fn build_refs<'a>(rows: &'a [(&str, &str)]) -> impl Iterator + 'a { - rows.iter().map(|(target, source)| KeyPathReference { - target: KeyPath::new(target), - source: KeyPath::new(source), - }) - } - - struct TestAssets(&'static [(&'static str, &'static str)]); - - impl AssetSource for TestAssets { - fn load(&self, path: &str) -> Result> { - if let Some(row) = self.0.iter().find(|e| e.0 == path) { - Ok(row.1.as_bytes().into()) - } else { - Err(anyhow!("no such path {}", path)) - } - } - - fn list(&self, prefix: &str) -> Vec> { - self.0 - .iter() - .copied() - .filter_map(|(path, _)| { - if path.starts_with(prefix) { - Some(path.into()) - } else { - None - } - }) - .collect() - } - } -} diff --git a/zed/src/theme/highlight_map.rs b/zed/src/theme/highlight_map.rs new file mode 100644 index 0000000000000000000000000000000000000000..55a053113c70420d1ec6bcdad186c65cef201481 --- /dev/null +++ b/zed/src/theme/highlight_map.rs @@ -0,0 +1,98 @@ +use super::Theme; +use std::sync::Arc; + +#[derive(Clone, Debug)] +pub struct HighlightMap(Arc<[HighlightId]>); + +#[derive(Clone, Copy, Debug)] +pub struct HighlightId(pub u32); + +const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); + +impl HighlightMap { + pub fn new(capture_names: &[String], theme: &Theme) -> Self { + // For each capture name in the highlight query, find the longest + // key in the theme's syntax styles that matches all of the + // dot-separated components of the capture name. + HighlightMap( + capture_names + .iter() + .map(|capture_name| { + theme + .syntax + .iter() + .enumerate() + .filter_map(|(i, (key, _))| { + let mut len = 0; + let capture_parts = capture_name.split('.'); + for key_part in key.split('.') { + if capture_parts.clone().any(|part| part == key_part) { + len += 1; + } else { + return None; + } + } + Some((i, len)) + }) + .max_by_key(|(_, len)| *len) + .map_or(DEFAULT_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32)) + }) + .collect(), + ) + } + + pub fn get(&self, capture_id: u32) -> HighlightId { + self.0 + .get(capture_id as usize) + .copied() + .unwrap_or(DEFAULT_HIGHLIGHT_ID) + } +} + +impl Default for HighlightMap { + fn default() -> Self { + Self(Arc::new([])) + } +} + +impl Default for HighlightId { + fn default() -> Self { + DEFAULT_HIGHLIGHT_ID + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::color::Color; + + #[test] + fn test_highlight_map() { + let theme = Theme { + name: "test".into(), + syntax: [ + ("function", Color::from_u32(0x100000ff)), + ("function.method", Color::from_u32(0x200000ff)), + ("function.async", Color::from_u32(0x300000ff)), + ("variable.builtin.self.rust", Color::from_u32(0x400000ff)), + ("variable.builtin", Color::from_u32(0x500000ff)), + ("variable", Color::from_u32(0x600000ff)), + ] + .iter() + .map(|(name, color)| (name.to_string(), (*color).into())) + .collect(), + ..Default::default() + }; + + let capture_names = &[ + "function.special".to_string(), + "function.async.rust".to_string(), + "variable.builtin.self".to_string(), + ]; + + let map = HighlightMap::new(capture_names, &theme); + assert_eq!(theme.highlight_name(map.get(0)), Some("function")); + assert_eq!(theme.highlight_name(map.get(1)), Some("function.async")); + assert_eq!(theme.highlight_name(map.get(2)), Some("variable.builtin")); + } +} diff --git a/zed/src/theme/theme_registry.rs b/zed/src/theme/theme_registry.rs new file mode 100644 index 0000000000000000000000000000000000000000..0b68d8a0c96714ad9c5df48c9914ee937ae146ab --- /dev/null +++ b/zed/src/theme/theme_registry.rs @@ -0,0 +1,756 @@ +use anyhow::{anyhow, Context, Result}; +use gpui::AssetSource; +use json::{Map, Value}; +use parking_lot::Mutex; +use serde_json as json; +use std::{collections::HashMap, fmt, mem, sync::Arc}; + +use super::Theme; + +pub struct ThemeRegistry { + assets: Box, + themes: Mutex>>, + theme_data: Mutex>>, +} + +#[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) -> Arc { + Arc::new(Self { + assets: Box::new(source), + themes: Default::default(), + theme_data: Default::default(), + }) + } + + pub fn list(&self) -> impl Iterator { + self.assets.list("themes/").into_iter().filter_map(|path| { + let filename = path.strip_prefix("themes/")?; + let theme_name = filename.strip_suffix(".toml")?; + if theme_name.starts_with('_') { + None + } else { + Some(theme_name.to_string()) + } + }) + } + + pub fn clear(&self) { + self.theme_data.lock().clear(); + self.themes.lock().clear(); + } + + pub fn get(&self, name: &str) -> Result> { + if let Some(theme) = self.themes.lock().get(name) { + return Ok(theme.clone()); + } + + let theme_data = self.load(name, true)?; + let mut theme = serde_json::from_value::(theme_data.as_ref().clone())?; + theme.name = name.into(); + let theme = Arc::new(theme); + self.themes.lock().insert(name.to_string(), theme.clone()); + Ok(theme) + } + + fn load(&self, name: &str, evaluate_references: bool) -> Result> { + if let Some(data) = self.theme_data.lock().get(name) { + return Ok(data.clone()); + } + + let asset_path = format!("themes/{}.toml", name); + let source_code = self + .assets + .load(&asset_path) + .with_context(|| format!("failed to load theme file {}", asset_path))?; + + let mut theme_data: Map = toml::from_slice(source_code.as_ref()) + .with_context(|| format!("failed to parse {}.toml", name))?; + + // If this theme extends another base theme, deeply merge it into the base theme's data + if let Some(base_name) = theme_data + .get("extends") + .and_then(|name| name.as_str()) + .map(str::to_string) + { + let base_theme_data = self + .load(&base_name, false) + .with_context(|| format!("failed to load base theme {}", base_name))? + .as_ref() + .clone(); + if let Value::Object(mut base_theme_object) = base_theme_data { + deep_merge_json(&mut base_theme_object, theme_data); + theme_data = base_theme_object; + } + } + + // 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))?; + } + } + } + + let result = Arc::new(Value::Object(theme_data)); + self.theme_data + .lock() + .insert(name.to_string(), result.clone()); + + Ok(result) + } +} + +impl KeyPathReferenceSet { + fn insert(&mut self, reference: KeyPathReference) { + let id = self.references.len(); + let source_ix = self + .reference_ids_by_source + .binary_search_by_key(&&reference.source, |id| &self.references[*id].source) + .unwrap_or_else(|i| i); + let target_ix = self + .reference_ids_by_target + .binary_search_by_key(&&reference.target, |id| &self.references[*id].target) + .unwrap_or_else(|i| i); + + self.populate_dependencies(id, &reference); + self.reference_ids_by_source.insert(source_ix, id); + self.reference_ids_by_target.insert(target_ix, id); + self.references.push(reference); + } + + fn top_sort(mut self) -> Result, Vec> { + let mut results = Vec::with_capacity(self.references.len()); + let mut root_ids = Vec::with_capacity(self.references.len()); + + // Find the initial set of references that have no dependencies. + for (id, dep_count) in self.dependency_counts.iter().enumerate() { + if *dep_count == 0 { + root_ids.push(id); + } + } + + while results.len() < root_ids.len() { + // Just to guarantee a stable result when the inputs are randomized, + // sort references lexicographically in absence of any dependency relationship. + root_ids[results.len()..].sort_by_key(|id| &self.references[*id]); + + let root_id = root_ids[results.len()]; + let root = mem::take(&mut self.references[root_id]); + results.push(root); + + // Remove this reference as a dependency from any of its dependent references. + if let Ok(dep_ix) = self + .dependencies + .binary_search_by_key(&root_id, |edge| edge.0) + { + let mut first_dep_ix = dep_ix; + let mut last_dep_ix = dep_ix + 1; + while first_dep_ix > 0 && self.dependencies[first_dep_ix - 1].0 == root_id { + first_dep_ix -= 1; + } + while last_dep_ix < self.dependencies.len() + && self.dependencies[last_dep_ix].0 == root_id + { + last_dep_ix += 1; + } + + // If any reference no longer has any dependencies, then then mark it as a root. + // Preserve the references' original order where possible. + for (_, successor_id) in self.dependencies.drain(first_dep_ix..last_dep_ix) { + self.dependency_counts[successor_id] -= 1; + if self.dependency_counts[successor_id] == 0 { + root_ids.push(successor_id); + } + } + } + } + + // If any references never became roots, then there are reference cycles + // in the set. Return an error containing all of the key paths that are + // directly involved in cycles. + if results.len() < self.references.len() { + let mut cycle_ref_ids = (0..self.references.len()) + .filter(|id| !root_ids.contains(id)) + .collect::>(); + + // Iteratively remove any references that have no dependencies, + // so that the error will only indicate which key paths are directly + // involved in the cycles. + let mut done = false; + while !done { + done = true; + cycle_ref_ids.retain(|id| { + if self.dependencies.iter().any(|dep| dep.0 == *id) { + true + } else { + done = false; + self.dependencies.retain(|dep| dep.1 != *id); + false + } + }); + } + + let mut cycle_key_paths = Vec::new(); + for id in cycle_ref_ids { + let reference = &self.references[id]; + cycle_key_paths.push(reference.target.clone()); + cycle_key_paths.push(reference.source.clone()); + } + cycle_key_paths.sort_unstable(); + return Err(cycle_key_paths); + } + + Ok(results) + } + + fn populate_dependencies(&mut self, new_id: usize, new_reference: &KeyPathReference) { + self.dependency_counts.push(0); + + // If an existing reference's source path starts with the new reference's + // target path, then insert this new reference before that existing reference. + for id in Self::reference_ids_for_key_path( + &new_reference.target.0, + &self.references, + &self.reference_ids_by_source, + KeyPathReference::source, + KeyPath::starts_with, + ) { + Self::add_dependency( + (new_id, id), + &mut self.dependencies, + &mut self.dependency_counts, + ); + } + + // If an existing reference's target path starts with the new reference's + // source path, then insert this new reference after that existing reference. + for id in Self::reference_ids_for_key_path( + &new_reference.source.0, + &self.references, + &self.reference_ids_by_target, + KeyPathReference::target, + KeyPath::starts_with, + ) { + Self::add_dependency( + (id, new_id), + &mut self.dependencies, + &mut self.dependency_counts, + ); + } + + // If an existing reference's source path is a prefix of the new reference's + // target path, then insert this new reference before that existing reference. + for prefix in new_reference.target.prefixes() { + for id in Self::reference_ids_for_key_path( + prefix, + &self.references, + &self.reference_ids_by_source, + KeyPathReference::source, + PartialEq::eq, + ) { + Self::add_dependency( + (new_id, id), + &mut self.dependencies, + &mut self.dependency_counts, + ); + } + } + + // If an existing reference's target path is a prefix of the new reference's + // source path, then insert this new reference after that existing reference. + for prefix in new_reference.source.prefixes() { + for id in Self::reference_ids_for_key_path( + prefix, + &self.references, + &self.reference_ids_by_target, + KeyPathReference::target, + PartialEq::eq, + ) { + Self::add_dependency( + (id, new_id), + &mut self.dependencies, + &mut self.dependency_counts, + ); + } + } + } + + // Find all existing references that satisfy a given predicate with respect + // to a given key path. Use a sorted array of reference ids in order to avoid + // performing unnecessary comparisons. + fn reference_ids_for_key_path<'a>( + key_path: &[Key], + references: &[KeyPathReference], + sorted_reference_ids: &'a [usize], + reference_attribute: impl Fn(&KeyPathReference) -> &KeyPath, + predicate: impl Fn(&KeyPath, &[Key]) -> bool, + ) -> impl Iterator + 'a { + let ix = sorted_reference_ids + .binary_search_by_key(&key_path, |id| &reference_attribute(&references[*id]).0) + .unwrap_or_else(|i| i); + + let mut start_ix = ix; + while start_ix > 0 { + let reference_id = sorted_reference_ids[start_ix - 1]; + let reference = &references[reference_id]; + if !predicate(&reference_attribute(reference), key_path) { + break; + } + start_ix -= 1; + } + + let mut end_ix = ix; + while end_ix < sorted_reference_ids.len() { + let reference_id = sorted_reference_ids[end_ix]; + let reference = &references[reference_id]; + if !predicate(&reference_attribute(reference), key_path) { + break; + } + end_ix += 1; + } + + sorted_reference_ids[start_ix..end_ix].iter().copied() + } + + fn add_dependency( + (predecessor, successor): (usize, usize), + dependencies: &mut Vec<(usize, usize)>, + dependency_counts: &mut Vec, + ) { + let dependency = (predecessor, successor); + if let Err(i) = dependencies.binary_search(&dependency) { + dependencies.insert(i, dependency); + } + dependency_counts[successor] += 1; + } +} + +impl KeyPathReference { + fn source(&self) -> &KeyPath { + &self.source + } + + fn target(&self) -> &KeyPath { + &self.target + } +} + +impl KeyPath { + fn new(string: &str) -> Self { + Self( + string + .split(".") + .map(|key| Key::Object(key.to_string())) + .collect(), + ) + } + + fn starts_with(&self, other: &[Key]) -> bool { + self.0.starts_with(&other) + } + + fn prefixes(&self) -> impl Iterator { + (1..self.0.len()).map(move |end_ix| &self.0[0..end_ix]) + } +} + +impl PartialEq<[Key]> for KeyPath { + fn eq(&self, other: &[Key]) -> bool { + self.0.eq(other) + } +} + +impl fmt::Debug for KeyPathReference { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "KeyPathReference {{ {} <- {} }}", + self.target, self.source + ) + } +} + +impl fmt::Display for KeyPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, key) in self.0.iter().enumerate() { + match key { + Key::Array(index) => write!(f, "[{}]", index)?, + Key::Object(key) => { + if i > 0 { + ".".fmt(f)?; + } + key.fmt(f)?; + } + } + } + Ok(()) + } +} + +fn deep_merge_json(base: &mut Map, extension: Map) { + for (key, extension_value) in extension { + if let Value::Object(extension_object) = extension_value { + if let Some(base_object) = base.get_mut(&key).and_then(|value| value.as_object_mut()) { + deep_merge_json(base_object, extension_object); + } else { + base.insert(key, Value::Object(extension_object)); + } + } else { + base.insert(key, extension_value); + } + } +} + +fn find_references(value: &Value, key_path: &mut KeyPath, references: &mut KeyPathReferenceSet) { + match value { + Value::Array(vec) => { + for (ix, value) in vec.iter().enumerate() { + key_path.0.push(Key::Array(ix)); + find_references(value, key_path, references); + key_path.0.pop(); + } + } + Value::Object(map) => { + for (key, value) in map.iter() { + if key == "extends" { + if let Some(source_path) = value.as_str().and_then(|s| s.strip_prefix("$")) { + references.insert(KeyPathReference { + source: KeyPath::new(source_path), + target: key_path.clone(), + }); + } + } else { + key_path.0.push(Key::Object(key.to_string())); + find_references(value, key_path, references); + key_path.0.pop(); + } + } + } + Value::String(string) => { + if let Some(source_path) = string.strip_prefix("$") { + references.insert(KeyPathReference { + source: KeyPath::new(source_path), + target: key_path.clone(), + }); + } + } + _ => {} + } +} + +fn value_at<'a>(object: &'a mut Map, 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::{assets::Assets, theme::DEFAULT_THEME_NAME}; + use rand::{prelude::StdRng, Rng}; + + #[test] + fn test_bundled_themes() { + let registry = ThemeRegistry::new(Assets); + let mut has_default_theme = false; + for theme_name in registry.list() { + let theme = registry.get(&theme_name).unwrap(); + if theme.name == DEFAULT_THEME_NAME { + has_default_theme = true; + } + assert_eq!(theme.name, theme_name); + } + assert!(has_default_theme); + } + + #[test] + fn test_theme_extension() { + let assets = TestAssets(&[ + ( + "themes/_base.toml", + r##" + [ui.active_tab] + extends = "$ui.tab" + border.color = "#666666" + text = "$text_colors.bright" + + [ui.tab] + extends = "$ui.element" + text = "$text_colors.dull" + + [ui.element] + background = "#111111" + border = {width = 2.0, color = "#00000000"} + + [editor] + background = "#222222" + default_text = "$text_colors.regular" + "##, + ), + ( + "themes/light.toml", + r##" + extends = "_base" + + [text_colors] + bright = "#ffffff" + regular = "#eeeeee" + dull = "#dddddd" + + [editor] + background = "#232323" + "##, + ), + ]); + + let registry = ThemeRegistry::new(assets); + let theme_data = registry.load("light", true).unwrap(); + assert_eq!( + theme_data.as_ref(), + &serde_json::json!({ + "ui": { + "active_tab": { + "background": "#111111", + "border": { + "width": 2.0, + "color": "#666666" + }, + "extends": "$ui.tab", + "text": "#ffffff" + }, + "tab": { + "background": "#111111", + "border": { + "width": 2.0, + "color": "#00000000" + }, + "extends": "$ui.element", + "text": "#dddddd" + }, + "element": { + "background": "#111111", + "border": { + "width": 2.0, + "color": "#00000000" + } + } + }, + "editor": { + "background": "#232323", + "default_text": "#eeeeee" + }, + "extends": "_base", + "text_colors": { + "bright": "#ffffff", + "regular": "#eeeeee", + "dull": "#dddddd" + } + }) + ); + } + + #[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", "m.n.n.o.q"), + ("x.y.z", "m.n.n.o.p"), + ("u.v.w", "x.y.z"), + ("a.b.c.d", "u.v"), + ("a.b.c.d.e", "u.v"), + ("a.b.c.d.f", "u.v"), + ("a.b.c.d.g", "u.v"), + ], + ]; + + for example in examples { + let expected_references = build_refs(example).collect::>(); + let mut input_references = expected_references.clone(); + input_references.sort_by_key(|_| rng.gen_range(0..1000)); + let mut reference_set = KeyPathReferenceSet::default(); + for reference in input_references { + reference_set.insert(reference); + } + assert_eq!(reference_set.top_sort().unwrap(), expected_references); + } + } + + fn build_refs<'a>(rows: &'a [(&str, &str)]) -> impl Iterator + 'a { + rows.iter().map(|(target, source)| KeyPathReference { + target: KeyPath::new(target), + source: KeyPath::new(source), + }) + } + + struct TestAssets(&'static [(&'static str, &'static str)]); + + impl AssetSource for TestAssets { + fn load(&self, path: &str) -> Result> { + if let Some(row) = self.0.iter().find(|e| e.0 == path) { + Ok(row.1.as_bytes().into()) + } else { + Err(anyhow!("no such path {}", path)) + } + } + + fn list(&self, prefix: &str) -> Vec> { + self.0 + .iter() + .copied() + .filter_map(|(path, _)| { + if path.starts_with(prefix) { + Some(path.into()) + } else { + None + } + }) + .collect() + } + } +} From 621203eb60cca61678ec24284b3cc03890d261a3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 17:38:26 -0600 Subject: [PATCH 083/204] Group more elements under workspace theme struct Co-Authored-By: Max Brunsfeld --- zed/assets/themes/_base.toml | 10 +++++----- zed/src/theme.rs | 10 +++++----- zed/src/workspace/pane.rs | 22 +++++++++++----------- zed/src/workspace/sidebar.rs | 6 +++--- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index cf340282d5287f3833bee7809f45b8e3e9717c1c..021ba25789f2da4e0434a8d34bb2c39138c62c95 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -1,25 +1,25 @@ [workspace] background = "$surface.0" -[tab] +[workspace.tab] text = "$text.2" padding = { left = 10, right = 10 } icon_close = "$text.0" icon_dirty = "$status.info" icon_conflict = "$status.warn" -[active_tab] +[workspace.active_tab] extends = "$tab" background = "$surface.1" text = "$text.0" -[sidebar] +[workspace.sidebar] padding = { left = 10, right = 10 } -[sidebar_icon] +[workspace.sidebar_icon] color = "$text.2" -[active_sidebar_icon] +[workspace.active_sidebar_icon] color = "$text.0" [selector] diff --git a/zed/src/theme.rs b/zed/src/theme.rs index f2425b5e2f33389f7896c1829bcde0776f9d47fd..c99cd0e39cc30cdebaf0d2feef556215d1cfbb86 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -20,11 +20,6 @@ pub struct Theme { #[serde(default)] pub name: String, pub workspace: Workspace, - pub tab: Tab, - pub active_tab: Tab, - pub sidebar: ContainerStyle, - pub sidebar_icon: SidebarIcon, - pub active_sidebar_icon: SidebarIcon, pub selector: Selector, pub editor: Editor, #[serde(deserialize_with = "deserialize_syntax_theme")] @@ -34,6 +29,11 @@ pub struct Theme { #[derive(Debug, Default, Deserialize)] pub struct Workspace { pub background: Color, + pub tab: Tab, + pub active_tab: Tab, + pub sidebar: ContainerStyle, + pub sidebar_icon: SidebarIcon, + pub active_sidebar_icon: SidebarIcon, } #[derive(Debug, Deserialize)] diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 845f6e2521fdf367c6e7dc9496bca2ecdbd3aab9..ab74cac19148e40745032ab283ea0168e016a220 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -189,7 +189,7 @@ impl Pane { let is_active = ix == self.active_item; enum Tab {} - let border = &theme.tab.container.border; + let border = &theme.workspace.tab.container.border; row.add_child( Expanded::new( @@ -212,9 +212,9 @@ impl Pane { settings.ui_font_size, ) .with_style(if is_active { - &theme.active_tab.label + &theme.workspace.active_tab.label } else { - &theme.tab.label + &theme.workspace.tab.label }) .boxed(), ) @@ -236,9 +236,9 @@ impl Pane { .boxed(), ) .with_style(if is_active { - &theme.active_tab.container + &theme.workspace.active_tab.container } else { - &theme.tab.container + &theme.workspace.tab.container }) .with_border(border); @@ -267,7 +267,7 @@ impl Pane { // 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.tab.container.border.color; + border.color = theme.workspace.tab.container.border.color; row.add_child( ConstrainedBox::new( @@ -305,19 +305,19 @@ impl Pane { ) -> ElementBox { enum TabCloseButton {} - let mut clicked_color = theme.tab.icon_dirty; + let mut clicked_color = theme.workspace.tab.icon_dirty; clicked_color.a = 180; let current_color = if has_conflict { - Some(theme.tab.icon_conflict) + Some(theme.workspace.tab.icon_conflict) } else if is_dirty { - Some(theme.tab.icon_dirty) + Some(theme.workspace.tab.icon_dirty) } else { None }; let icon = if tab_hovered { - let close_color = current_color.unwrap_or(theme.tab.icon_close); + let close_color = current_color.unwrap_or(theme.workspace.tab.icon_close); let icon = Svg::new("icons/x.svg").with_color(close_color); MouseEventHandler::new::(item_id, cx, |mouse_state| { @@ -326,7 +326,7 @@ impl Pane { .with_background_color(if mouse_state.clicked { clicked_color } else { - theme.tab.icon_dirty + theme.workspace.tab.icon_dirty }) .with_corner_radius(close_icon_size / 2.) .boxed() diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 81807fd01a5ca621da7575065152b6cc14730cb0..8bb3f06fc1a013c8a59688e54aba0daee52446a6 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -70,9 +70,9 @@ impl Sidebar { Flex::column() .with_children(self.items.iter().enumerate().map(|(item_index, item)| { let theme = if Some(item_index) == self.active_item_ix { - &settings.theme.active_sidebar_icon + &settings.theme.workspace.active_sidebar_icon } else { - &settings.theme.sidebar_icon + &settings.theme.workspace.sidebar_icon }; enum SidebarButton {} MouseEventHandler::new::(item.view.id(), cx, |_| { @@ -96,7 +96,7 @@ impl Sidebar { })) .boxed(), ) - .with_style(&settings.theme.sidebar) + .with_style(&settings.theme.workspace.sidebar) .boxed() } } From 87a103bc52553253b1753d96d612b0deea9a5192 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 17:39:56 -0600 Subject: [PATCH 084/204] =?UTF-8?q?=F0=9F=92=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Max Brunsfeld --- zed/assets/themes/_base.toml | 2 +- zed/src/theme.rs | 58 ++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 021ba25789f2da4e0434a8d34bb2c39138c62c95..17145584943a5f9caae04fcec602e2a327470751 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -9,7 +9,7 @@ icon_dirty = "$status.info" icon_conflict = "$status.warn" [workspace.active_tab] -extends = "$tab" +extends = "$workspace.tab" background = "$surface.1" text = "$text.0" diff --git a/zed/src/theme.rs b/zed/src/theme.rs index c99cd0e39cc30cdebaf0d2feef556215d1cfbb86..a502f2ae673e73e39321409f2f472e07b3b7c2c9 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -36,23 +36,6 @@ pub struct Workspace { pub active_sidebar_icon: SidebarIcon, } -#[derive(Debug, Deserialize)] -pub struct Editor { - pub background: Color, - pub gutter_background: Color, - pub active_line_background: Color, - pub line_number: Color, - pub line_number_active: Color, - pub text: Color, - pub replicas: Vec, -} - -#[derive(Clone, Copy, Debug, Default, Deserialize)] -pub struct Replica { - pub cursor: Color, - pub selection: Color, -} - #[derive(Debug, Default, Deserialize)] pub struct Tab { #[serde(flatten)] @@ -88,18 +71,21 @@ pub struct SelectorItem { pub label: LabelStyle, } -impl Default for Editor { - fn default() -> Self { - Self { - background: Default::default(), - gutter_background: Default::default(), - active_line_background: Default::default(), - line_number: Default::default(), - line_number_active: Default::default(), - text: Default::default(), - replicas: vec![Replica::default()], - } - } +#[derive(Debug, Deserialize)] +pub struct Editor { + pub background: Color, + pub gutter_background: Color, + pub active_line_background: Color, + pub line_number: Color, + pub line_number_active: Color, + pub text: Color, + pub replicas: Vec, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +pub struct Replica { + pub cursor: Color, + pub selection: Color, } impl Theme { @@ -119,6 +105,20 @@ impl Theme { } } +impl Default for Editor { + fn default() -> Self { + Self { + background: Default::default(), + gutter_background: Default::default(), + active_line_background: Default::default(), + line_number: Default::default(), + line_number_active: Default::default(), + text: Default::default(), + replicas: vec![Replica::default()], + } + } +} + pub fn deserialize_syntax_theme<'de, D>( deserializer: D, ) -> Result, D::Error> From a79b32cfc26cb04c00040aade1428d73a66b40d4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 17:46:23 -0600 Subject: [PATCH 085/204] Apply theme to chat messages Co-Authored-By: Max Brunsfeld --- zed/assets/themes/_base.toml | 3 +++ zed/src/chat_panel.rs | 17 +++++++---------- zed/src/theme.rs | 12 ++++++++++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 17145584943a5f9caae04fcec602e2a327470751..56dbbdaaf10bdbe8bbf079867957827f8cc84f08 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -22,6 +22,9 @@ color = "$text.2" [workspace.active_sidebar_icon] color = "$text.0" +[chat_panel.message] +text = "$text.0" + [selector] background = "$surface.2" text = "$text.0" diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 5758879cd1faebd224fa50554498877a147298fc..7788ab2eba782dc74b8b14ffcfa95f50ae9bcc1f 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -116,16 +116,13 @@ impl ChatPanel { fn render_message(&self, message: &ChannelMessage) -> ElementBox { let settings = self.settings.borrow(); - Flex::column() - .with_child( - Label::new( - message.body.clone(), - settings.ui_font_family, - settings.ui_font_size, - ) - .boxed(), - ) - .boxed() + Label::new( + message.body.clone(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_style(&settings.theme.chat_panel.message.label) + .boxed() } fn render_input_box(&self) -> ElementBox { diff --git a/zed/src/theme.rs b/zed/src/theme.rs index a502f2ae673e73e39321409f2f472e07b3b7c2c9..0bd5b3593d48cc589d8e1b5e575e7d24a8d657ca 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -20,6 +20,7 @@ pub struct Theme { #[serde(default)] pub name: String, pub workspace: Workspace, + pub chat_panel: ChatPanel, pub selector: Selector, pub editor: Editor, #[serde(deserialize_with = "deserialize_syntax_theme")] @@ -52,6 +53,17 @@ pub struct SidebarIcon { pub color: Color, } +#[derive(Debug, Default, Deserialize)] +pub struct ChatPanel { + pub message: ChatMessage, +} + +#[derive(Debug, Default, Deserialize)] +pub struct ChatMessage { + #[serde(flatten)] + pub label: LabelStyle, +} + #[derive(Debug, Default, Deserialize)] pub struct Selector { #[serde(flatten)] From 3ac489a8b6722eb488675b758a3ee4ac0cd03466 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 17:51:37 -0600 Subject: [PATCH 086/204] Fix panic when scrolling non-overflowing lists Co-Authored-By: Max Brunsfeld --- gpui/src/elements/list.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 2e692f4a9914a31284d3fdbb850c798995ade25c..78f8c2316e32ffc3ab4679d1461ae78f7967f1c6 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -234,8 +234,9 @@ impl StateInner { todo!("still need to handle non-precise scroll events from a mouse wheel"); } - let scroll_max = self.heights.summary().height - height; - self.scroll_top = (self.scroll_top - delta.y()).max(0.0).min(scroll_max); + let scroll_max = (self.heights.summary().height - height).max(0.); + self.scroll_top = (self.scroll_top - delta.y()).max(0.).min(scroll_max); + cx.notify(); true From 0187ac8fde245d6e6d4d8af7c9b706531e5346c8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 18:16:13 -0600 Subject: [PATCH 087/204] Share a single pool of LineWrappers across all threads Co-Authored-By: Max Brunsfeld --- zed/src/editor/display_map/line_wrapper.rs | 39 +++++++++++----------- zed/src/editor/display_map/wrap_map.rs | 6 ++-- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/zed/src/editor/display_map/line_wrapper.rs b/zed/src/editor/display_map/line_wrapper.rs index 97ac2769e72b231e85d9f55c3fc5e763e674b36c..106e55a55facba66f786ef87f5ba2e070c956d33 100644 --- a/zed/src/editor/display_map/line_wrapper.rs +++ b/zed/src/editor/display_map/line_wrapper.rs @@ -1,15 +1,16 @@ use crate::Settings; use gpui::{fonts::FontId, FontCache, FontSystem}; +use lazy_static::lazy_static; +use parking_lot::Mutex; use std::{ - cell::RefCell, collections::HashMap, iter, ops::{Deref, DerefMut}, sync::Arc, }; -thread_local! { - static WRAPPERS: RefCell> = Default::default(); +lazy_static! { + static ref POOL: Mutex> = Default::default(); } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -35,25 +36,24 @@ pub struct LineWrapper { impl LineWrapper { pub const MAX_INDENT: u32 = 256; - pub fn thread_local( + pub fn acquire( font_system: Arc, font_cache: &FontCache, settings: Settings, ) -> LineWrapperHandle { - let wrapper = - if let Some(mut wrapper) = WRAPPERS.with(|wrappers| wrappers.borrow_mut().pop()) { - let font_id = font_cache - .select_font(settings.buffer_font_family, &Default::default()) - .unwrap(); - let font_size = settings.buffer_font_size; - if wrapper.font_id != font_id || wrapper.font_size != font_size { - wrapper.cached_ascii_char_widths = [f32::NAN; 128]; - wrapper.cached_other_char_widths.clear(); - } - wrapper - } else { - LineWrapper::new(font_system, font_cache, settings) - }; + let wrapper = if let Some(mut wrapper) = POOL.lock().pop() { + let font_id = font_cache + .select_font(settings.buffer_font_family, &Default::default()) + .unwrap(); + let font_size = settings.buffer_font_size; + if wrapper.font_id != font_id || wrapper.font_size != font_size { + wrapper.cached_ascii_char_widths = [f32::NAN; 128]; + wrapper.cached_other_char_widths.clear(); + } + wrapper + } else { + LineWrapper::new(font_system, font_cache, settings) + }; LineWrapperHandle(Some(wrapper)) } @@ -163,6 +163,7 @@ impl LineWrapper { } fn compute_width_for_char(&self, c: char) -> f32 { + log::info!("cache miss {}", c); self.font_system .layout_line( &c.to_string(), @@ -178,7 +179,7 @@ pub struct LineWrapperHandle(Option); impl Drop for LineWrapperHandle { fn drop(&mut self) { let wrapper = self.0.take().unwrap(); - WRAPPERS.with(|wrappers| wrappers.borrow_mut().push(wrapper)) + POOL.lock().push(wrapper) } } diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index e831d800429ceab9d584626fd5827eddefae6070..1d5359eabff854d7d2e9a106b7e890f56d8b1f41 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/zed/src/editor/display_map/wrap_map.rs @@ -119,8 +119,7 @@ impl WrapMap { let font_cache = cx.font_cache().clone(); let settings = self.settings.clone(); let task = cx.background().spawn(async move { - let mut line_wrapper = - LineWrapper::thread_local(font_system, &font_cache, settings); + let mut line_wrapper = LineWrapper::acquire(font_system, &font_cache, settings); let tab_snapshot = new_snapshot.tab_snapshot.clone(); let range = TabPoint::zero()..tab_snapshot.max_point(); new_snapshot @@ -194,8 +193,7 @@ impl WrapMap { let font_cache = cx.font_cache().clone(); let settings = self.settings.clone(); let update_task = cx.background().spawn(async move { - let mut line_wrapper = - LineWrapper::thread_local(font_system, &font_cache, settings); + let mut line_wrapper = LineWrapper::acquire(font_system, &font_cache, settings); for (tab_snapshot, edits) in pending_edits { snapshot .update(tab_snapshot, &edits, wrap_width, &mut line_wrapper) From 91c2b5825e5a9341e2a69c42b6e31c9a2413a363 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 24 Aug 2021 18:09:16 -0700 Subject: [PATCH 088/204] Add LineWrapper::wrap_shaped_line This allows us to perform wrapping based on glyph positions in an already-shaped line. We plan to use this in the new Text element, because there we'll already need to do text shaping as part of layout. This text isn't editable so it won't need to be rewrapped with the same frequency as the text editor's content. Co-Authored-By: Nathan Sobo --- gpui/src/text_layout.rs | 10 ++ zed/src/editor/display_map/line_wrapper.rs | 143 ++++++++++++++++++++- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index 57556d61b6b25b25ee28bbda20893dd2c0c196b0..d9103498c474d1231881d2e09da3aa3e6d27c494 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -170,6 +170,10 @@ impl Line { Self { layout, color_runs } } + pub fn runs(&self) -> &[Run] { + &self.layout.runs + } + pub fn width(&self) -> f32 { self.layout.width } @@ -245,3 +249,9 @@ impl Line { } } } + +impl Run { + pub fn glyphs(&self) -> &[Glyph] { + &self.glyphs + } +} diff --git a/zed/src/editor/display_map/line_wrapper.rs b/zed/src/editor/display_map/line_wrapper.rs index 106e55a55facba66f786ef87f5ba2e070c956d33..696a2ac2dd80de0ea0be83074e9121a0a865359a 100644 --- a/zed/src/editor/display_map/line_wrapper.rs +++ b/zed/src/editor/display_map/line_wrapper.rs @@ -1,5 +1,5 @@ use crate::Settings; -use gpui::{fonts::FontId, FontCache, FontSystem}; +use gpui::{fonts::FontId, text_layout::Line, FontCache, FontSystem}; use lazy_static::lazy_static; use parking_lot::Mutex; use std::{ @@ -19,6 +19,12 @@ pub struct Boundary { pub next_indent: u32, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ShapedBoundary { + pub run_ix: usize, + pub glyph_ix: usize, +} + impl Boundary { fn new(ix: usize, next_indent: u32) -> Self { Self { ix, next_indent } @@ -135,6 +141,74 @@ impl LineWrapper { }) } + pub fn wrap_shaped_line<'a>( + &'a mut self, + str: &'a str, + line: &'a Line, + wrap_width: f32, + ) -> impl Iterator + 'a { + let mut width = 0.0; + let mut first_non_whitespace_ix = None; + let mut last_candidate_ix = None; + let mut last_candidate_x = 0.0; + let mut last_wrap_ix = ShapedBoundary { + run_ix: 0, + glyph_ix: 0, + }; + let mut last_wrap_x = 0.; + let mut prev_c = '\0'; + let mut glyphs = line + .runs() + .iter() + .enumerate() + .flat_map(move |(run_ix, run)| { + run.glyphs() + .iter() + .enumerate() + .map(move |(glyph_ix, glyph)| { + let character = str[glyph.index..].chars().next().unwrap(); + ( + ShapedBoundary { run_ix, glyph_ix }, + character, + glyph.position.x(), + ) + }) + }); + + iter::from_fn(move || { + while let Some((ix, c, x)) = glyphs.next() { + if c == '\n' { + continue; + } + + if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() { + last_candidate_ix = Some(ix); + last_candidate_x = x; + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + let width = x - last_wrap_x; + if width > wrap_width && ix > last_wrap_ix { + if let Some(last_candidate_ix) = last_candidate_ix.take() { + last_wrap_ix = last_candidate_ix; + last_wrap_x = last_candidate_x; + } else { + last_wrap_ix = ix; + last_wrap_x = x; + } + + return Some(last_wrap_ix); + } + prev_c = c; + } + + None + }) + } + fn is_boundary(&self, prev: char, next: char) -> bool { (prev == ' ') && (next != ' ') } @@ -163,7 +237,6 @@ impl LineWrapper { } fn compute_width_for_char(&self, c: char) -> f32 { - log::info!("cache miss {}", c); self.font_system .layout_line( &c.to_string(), @@ -200,9 +273,14 @@ impl DerefMut for LineWrapperHandle { #[cfg(test)] mod tests { use super::*; + use gpui::{ + color::Color, + fonts::{Properties, Weight}, + TextLayoutCache, + }; #[gpui::test] - fn test_line_wrapper(cx: &mut gpui::MutableAppContext) { + fn test_wrap_line(cx: &mut gpui::MutableAppContext) { let font_cache = cx.font_cache().clone(); let font_system = cx.platform().fonts(); let settings = Settings { @@ -263,4 +341,63 @@ mod tests { ] ); } + + #[gpui::test] + fn test_wrap_layout_line(cx: &mut gpui::MutableAppContext) { + let font_cache = cx.font_cache().clone(); + let font_system = cx.platform().fonts(); + let text_layout_cache = TextLayoutCache::new(font_system.clone()); + + let family = font_cache.load_family(&["Helvetica"]).unwrap(); + let settings = Settings { + tab_size: 4, + buffer_font_family: family, + buffer_font_size: 16.0, + ..Settings::new(&font_cache).unwrap() + }; + let normal = font_cache.select_font(family, &Default::default()).unwrap(); + let bold = font_cache + .select_font( + family, + &Properties { + weight: Weight::BOLD, + ..Default::default() + }, + ) + .unwrap(); + + let text = "aa bbb cccc ddddd eeee"; + let line = text_layout_cache.layout_str( + text, + 16.0, + &[ + (4, normal, Color::default()), + (5, bold, Color::default()), + (6, normal, Color::default()), + (1, bold, Color::default()), + (7, normal, Color::default()), + ], + ); + + let mut wrapper = LineWrapper::new(font_system, &font_cache, settings); + assert_eq!( + wrapper + .wrap_shaped_line(&text, &line, 72.0) + .collect::>(), + &[ + ShapedBoundary { + run_ix: 1, + glyph_ix: 3 + }, + ShapedBoundary { + run_ix: 2, + glyph_ix: 3 + }, + ShapedBoundary { + run_ix: 4, + glyph_ix: 2 + } + ], + ); + } } From 33dbbf271e0d7ddb3b7defda3d047e51091145cf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 24 Aug 2021 18:16:31 -0600 Subject: [PATCH 089/204] WIP Co-Authored-By: Max Brunsfeld --- gpui/src/elements.rs | 2 + gpui/src/elements/text.rs | 166 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 gpui/src/elements/text.rs diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index c3053c9fa1655882dfbc75e3fb1c0a02132ec82c..d7d042617fe7f8a74b09bc1d28a98aef8fb7a19c 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -11,6 +11,7 @@ mod list; mod mouse_event_handler; mod stack; mod svg; +mod text; mod uniform_list; pub use crate::presenter::ChildView; @@ -27,6 +28,7 @@ pub use list::*; pub use mouse_event_handler::*; pub use stack::*; pub use svg::*; +pub use text::*; pub use uniform_list::*; use crate::{ diff --git a/gpui/src/elements/text.rs b/gpui/src/elements/text.rs new file mode 100644 index 0000000000000000000000000000000000000000..78e9d0d3a48a916756641403ab35d35cfc90d2f9 --- /dev/null +++ b/gpui/src/elements/text.rs @@ -0,0 +1,166 @@ +use crate::{ + color::Color, + font_cache::FamilyId, + fonts::{FontId, TextStyle}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::{ToJson, Value}, + text_layout::Line, + DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext, + SizeConstraint, +}; +use serde::Deserialize; +use serde_json::json; +use smallvec::{smallvec, SmallVec}; + +pub struct Text { + text: String, + family_id: FamilyId, + font_size: f32, + style: TextStyle, +} + +impl Text { + pub fn new(text: String, family_id: FamilyId, font_size: f32) -> Self { + Self { + text, + family_id, + font_size, + style: Default::default(), + } + } + + pub fn with_style(mut self, style: &TextStyle) -> Self { + self.style = style.clone(); + self + } + + pub fn with_default_color(mut self, color: Color) -> Self { + self.style.color = color; + self + } +} + +impl Element for Text { + type LayoutState = Line; + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let font_id = cx + .font_cache + .select_font(self.family_id, &self.style.font_properties) + .unwrap(); + let line = + cx.text_layout_cache + .layout_str(self.text.as_str(), self.font_size, runs.as_slice()); + + let size = vec2f( + line.width().max(constraint.min.x()).min(constraint.max.x()), + cx.font_cache.line_height(font_id, self.font_size).ceil(), + ); + + (size, line) + } + + fn paint( + &mut self, + bounds: RectF, + line: &mut Self::LayoutState, + cx: &mut PaintContext, + ) -> Self::PaintState { + line.paint( + bounds.origin(), + RectF::new(vec2f(0., 0.), bounds.size()), + cx, + ) + } + + 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, + cx: &DebugContext, + ) -> Value { + json!({ + "type": "Label", + "bounds": bounds.to_json(), + "text": &self.text, + "highlight_indices": self.highlight_indices, + "font_family": cx.font_cache.family_name(self.family_id).unwrap(), + "font_size": self.font_size, + "style": self.style.to_json(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fonts::{Properties as FontProperties, Weight}; + + #[crate::test(self)] + fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) { + let menlo = cx.font_cache().load_family(&["Menlo"]).unwrap(); + let menlo_regular = cx + .font_cache() + .select_font(menlo, &FontProperties::new()) + .unwrap(); + let menlo_bold = cx + .font_cache() + .select_font(menlo, FontProperties::new().weight(Weight::BOLD)) + .unwrap(); + let black = Color::black(); + let red = Color::new(255, 0, 0, 255); + + let label = Text::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0) + .with_style(&TextStyle { + text: TextStyle { + color: black, + font_properties: Default::default(), + }, + highlight_text: Some(TextStyle { + color: red, + font_properties: *FontProperties::new().weight(Weight::BOLD), + }), + }) + .with_highlights(vec![ + ".α".len(), + ".αβ".len(), + ".αβγδ".len(), + ".αβγδε.ⓐ".len(), + ".αβγδε.ⓐⓑ".len(), + ]); + + let runs = label.compute_runs(cx.font_cache().as_ref(), menlo_regular); + assert_eq!( + runs.as_slice(), + &[ + (".α".len(), menlo_regular, black), + ("βγ".len(), menlo_bold, red), + ("δ".len(), menlo_regular, black), + ("ε".len(), menlo_bold, red), + (".ⓐ".len(), menlo_regular, black), + ("ⓑⓒ".len(), menlo_bold, red), + ("ⓓⓔ.abcde.".len(), menlo_regular, black), + ] + ); + } +} From f404f5f32fb1339ab000ee78d04312614c8fbfff Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 24 Aug 2021 18:09:27 -0700 Subject: [PATCH 090/204] wip2 --- gpui/src/elements/text.rs | 73 ++++++--------------------------------- 1 file changed, 10 insertions(+), 63 deletions(-) diff --git a/gpui/src/elements/text.rs b/gpui/src/elements/text.rs index 78e9d0d3a48a916756641403ab35d35cfc90d2f9..55f634470863cf10ea702e6e039cf20f004255f2 100644 --- a/gpui/src/elements/text.rs +++ b/gpui/src/elements/text.rs @@ -56,16 +56,18 @@ impl Element for Text { .font_cache .select_font(self.family_id, &self.style.font_properties) .unwrap(); - let line = - cx.text_layout_cache - .layout_str(self.text.as_str(), self.font_size, runs.as_slice()); - let size = vec2f( - line.width().max(constraint.min.x()).min(constraint.max.x()), - cx.font_cache.line_height(font_id, self.font_size).ceil(), - ); + todo!() + // let line = + // cx.text_layout_cache + // .layout_str(self.text.as_str(), self.font_size, runs.as_slice()); - (size, line) + // let size = vec2f( + // line.width().max(constraint.min.x()).min(constraint.max.x()), + // cx.font_cache.line_height(font_id, self.font_size).ceil(), + // ); + + // (size, line) } fn paint( @@ -103,64 +105,9 @@ impl Element for Text { "type": "Label", "bounds": bounds.to_json(), "text": &self.text, - "highlight_indices": self.highlight_indices, "font_family": cx.font_cache.family_name(self.family_id).unwrap(), "font_size": self.font_size, "style": self.style.to_json(), }) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::fonts::{Properties as FontProperties, Weight}; - - #[crate::test(self)] - fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) { - let menlo = cx.font_cache().load_family(&["Menlo"]).unwrap(); - let menlo_regular = cx - .font_cache() - .select_font(menlo, &FontProperties::new()) - .unwrap(); - let menlo_bold = cx - .font_cache() - .select_font(menlo, FontProperties::new().weight(Weight::BOLD)) - .unwrap(); - let black = Color::black(); - let red = Color::new(255, 0, 0, 255); - - let label = Text::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0) - .with_style(&TextStyle { - text: TextStyle { - color: black, - font_properties: Default::default(), - }, - highlight_text: Some(TextStyle { - color: red, - font_properties: *FontProperties::new().weight(Weight::BOLD), - }), - }) - .with_highlights(vec![ - ".α".len(), - ".αβ".len(), - ".αβγδ".len(), - ".αβγδε.ⓐ".len(), - ".αβγδε.ⓐⓑ".len(), - ]); - - let runs = label.compute_runs(cx.font_cache().as_ref(), menlo_regular); - assert_eq!( - runs.as_slice(), - &[ - (".α".len(), menlo_regular, black), - ("βγ".len(), menlo_bold, red), - ("δ".len(), menlo_regular, black), - ("ε".len(), menlo_bold, red), - (".ⓐ".len(), menlo_regular, black), - ("ⓑⓒ".len(), menlo_bold, red), - ("ⓓⓔ.abcde.".len(), menlo_regular, black), - ] - ); - } -} From 463284f0afd4a7845fa3018666a35a2b523cad1f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 Aug 2021 10:43:54 +0200 Subject: [PATCH 091/204] Move `LineWrapper` into gpui --- Cargo.lock | 1 + gpui/Cargo.toml | 1 + gpui/src/elements/text.rs | 1 - gpui/src/text_layout.rs | 375 ++++++++++++++++++- zed/src/editor/display_map.rs | 1 - zed/src/editor/display_map/line_wrapper.rs | 403 --------------------- zed/src/editor/display_map/wrap_map.rs | 19 +- 7 files changed, 391 insertions(+), 410 deletions(-) delete mode 100644 zed/src/editor/display_map/line_wrapper.rs diff --git a/Cargo.lock b/Cargo.lock index 426b99d93224a109bb77470b7cc4eed0851db14c..ec4cfc74fea0517d83c95d0cfece1753873976c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2167,6 +2167,7 @@ dependencies = [ "font-kit", "foreign-types", "gpui_macros", + "lazy_static", "log", "metal", "num_cpus", diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index be3d47e42aed08c25653146aa91b3a951cedbf79..b6b29c39e2541cad4837f68f217ba58910786d95 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" } +lazy_static = "1.4.0" log = "0.4" num_cpus = "1.13" ordered-float = "2.1.1" diff --git a/gpui/src/elements/text.rs b/gpui/src/elements/text.rs index 55f634470863cf10ea702e6e039cf20f004255f2..4c6d0559e680e97e93e23fa414f72344573f3ce2 100644 --- a/gpui/src/elements/text.rs +++ b/gpui/src/elements/text.rs @@ -56,7 +56,6 @@ impl Element for Text { .font_cache .select_font(self.family_id, &self.style.font_properties) .unwrap(); - todo!() // let line = // cx.text_layout_cache diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index d9103498c474d1231881d2e09da3aa3e6d27c494..e19f82ed55c76ad470756bb1db9019f5d7022e8b 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -5,8 +5,9 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - platform, scene, PaintContext, + platform, scene, FontSystem, PaintContext, }; +use lazy_static::lazy_static; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use smallvec::SmallVec; @@ -14,6 +15,8 @@ use std::{ borrow::Borrow, collections::HashMap, hash::{Hash, Hasher}, + iter, + ops::{Deref, DerefMut}, sync::Arc, }; @@ -255,3 +258,373 @@ impl Run { &self.glyphs } } + +lazy_static! { + static ref WRAPPER_POOL: Mutex> = Default::default(); +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Boundary { + pub ix: usize, + pub next_indent: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ShapedBoundary { + pub run_ix: usize, + pub glyph_ix: usize, +} + +impl Boundary { + fn new(ix: usize, next_indent: u32) -> Self { + Self { ix, next_indent } + } +} + +pub struct LineWrapper { + font_system: Arc, + font_id: FontId, + font_size: f32, + cached_ascii_char_widths: [f32; 128], + cached_other_char_widths: HashMap, +} + +impl LineWrapper { + pub const MAX_INDENT: u32 = 256; + + pub fn acquire( + font_id: FontId, + font_size: f32, + font_system: Arc, + ) -> LineWrapperHandle { + let wrapper = if let Some(mut wrapper) = WRAPPER_POOL.lock().pop() { + if wrapper.font_id != font_id || wrapper.font_size != font_size { + wrapper.cached_ascii_char_widths = [f32::NAN; 128]; + wrapper.cached_other_char_widths.clear(); + } + wrapper + } else { + LineWrapper::new(font_id, font_size, font_system) + }; + LineWrapperHandle(Some(wrapper)) + } + + pub fn new(font_id: FontId, font_size: f32, font_system: Arc) -> Self { + Self { + font_system, + font_id, + font_size, + cached_ascii_char_widths: [f32::NAN; 128], + cached_other_char_widths: HashMap::new(), + } + } + + pub fn wrap_line<'a>( + &'a mut self, + line: &'a str, + wrap_width: f32, + ) -> impl Iterator + 'a { + let mut width = 0.0; + let mut first_non_whitespace_ix = None; + let mut indent = None; + let mut last_candidate_ix = 0; + let mut last_candidate_width = 0.0; + let mut last_wrap_ix = 0; + let mut prev_c = '\0'; + let mut char_indices = line.char_indices(); + iter::from_fn(move || { + while let Some((ix, c)) = char_indices.next() { + if c == '\n' { + continue; + } + + if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + let char_width = self.width_for_char(c); + width += char_width; + if width > wrap_width && ix > last_wrap_ix { + if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix) + { + indent = Some( + Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32), + ); + } + + if last_candidate_ix > 0 { + last_wrap_ix = last_candidate_ix; + width -= last_candidate_width; + last_candidate_ix = 0; + } else { + last_wrap_ix = ix; + width = char_width; + } + + let indent_width = + indent.map(|indent| indent as f32 * self.width_for_char(' ')); + width += indent_width.unwrap_or(0.); + + return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0))); + } + prev_c = c; + } + + None + }) + } + + pub fn wrap_shaped_line<'a>( + &'a mut self, + str: &'a str, + line: &'a Line, + wrap_width: f32, + ) -> impl Iterator + 'a { + let mut first_non_whitespace_ix = None; + let mut last_candidate_ix = None; + let mut last_candidate_x = 0.0; + let mut last_wrap_ix = ShapedBoundary { + run_ix: 0, + glyph_ix: 0, + }; + let mut last_wrap_x = 0.; + let mut prev_c = '\0'; + let mut glyphs = line + .runs() + .iter() + .enumerate() + .flat_map(move |(run_ix, run)| { + run.glyphs() + .iter() + .enumerate() + .map(move |(glyph_ix, glyph)| { + let character = str[glyph.index..].chars().next().unwrap(); + ( + ShapedBoundary { run_ix, glyph_ix }, + character, + glyph.position.x(), + ) + }) + }); + + iter::from_fn(move || { + while let Some((ix, c, x)) = glyphs.next() { + if c == '\n' { + continue; + } + + if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() { + last_candidate_ix = Some(ix); + last_candidate_x = x; + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + let width = x - last_wrap_x; + if width > wrap_width && ix > last_wrap_ix { + if let Some(last_candidate_ix) = last_candidate_ix.take() { + last_wrap_ix = last_candidate_ix; + last_wrap_x = last_candidate_x; + } else { + last_wrap_ix = ix; + last_wrap_x = x; + } + + return Some(last_wrap_ix); + } + prev_c = c; + } + + None + }) + } + + fn is_boundary(&self, prev: char, next: char) -> bool { + (prev == ' ') && (next != ' ') + } + + #[inline(always)] + fn width_for_char(&mut self, c: char) -> f32 { + if (c as u32) < 128 { + let mut width = self.cached_ascii_char_widths[c as usize]; + if width.is_nan() { + width = self.compute_width_for_char(c); + self.cached_ascii_char_widths[c as usize] = width; + } + width + } else { + let mut width = self + .cached_other_char_widths + .get(&c) + .copied() + .unwrap_or(f32::NAN); + if width.is_nan() { + width = self.compute_width_for_char(c); + self.cached_other_char_widths.insert(c, width); + } + width + } + } + + fn compute_width_for_char(&self, c: char) -> f32 { + self.font_system + .layout_line( + &c.to_string(), + self.font_size, + &[(1, self.font_id, Default::default())], + ) + .width + } +} + +pub struct LineWrapperHandle(Option); + +impl Drop for LineWrapperHandle { + fn drop(&mut self) { + let wrapper = self.0.take().unwrap(); + WRAPPER_POOL.lock().push(wrapper) + } +} + +impl Deref for LineWrapperHandle { + type Target = LineWrapper; + + fn deref(&self) -> &Self::Target { + self.0.as_ref().unwrap() + } +} + +impl DerefMut for LineWrapperHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.as_mut().unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + color::Color, + fonts::{Properties, Weight}, + }; + + #[crate::test(self)] + fn test_wrap_line(cx: &mut crate::MutableAppContext) { + let font_cache = cx.font_cache().clone(); + let font_system = cx.platform().fonts(); + let family = font_cache.load_family(&["Courier"]).unwrap(); + let font_id = font_cache.select_font(family, &Default::default()).unwrap(); + + let mut wrapper = LineWrapper::new(font_id, 16., font_system); + assert_eq!( + wrapper + .wrap_line("aa bbb cccc ddddd eeee", 72.0) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper + .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0) + .collect::>(), + &[ + Boundary::new(4, 0), + Boundary::new(11, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper.wrap_line(" aaaaaaa", 72.).collect::>(), + &[ + Boundary::new(7, 5), + Boundary::new(9, 5), + Boundary::new(11, 5), + ] + ); + assert_eq!( + wrapper + .wrap_line(" ", 72.) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 0), + Boundary::new(21, 0) + ] + ); + assert_eq!( + wrapper + .wrap_line(" aaaaaaaaaaaaaa", 72.) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 3), + Boundary::new(18, 3), + Boundary::new(22, 3), + ] + ); + } + + #[crate::test(self)] + fn test_wrap_layout_line(cx: &mut crate::MutableAppContext) { + let font_cache = cx.font_cache().clone(); + let font_system = cx.platform().fonts(); + let text_layout_cache = TextLayoutCache::new(font_system.clone()); + + let family = font_cache.load_family(&["Helvetica"]).unwrap(); + let font_id = font_cache.select_font(family, &Default::default()).unwrap(); + let normal = font_cache.select_font(family, &Default::default()).unwrap(); + let bold = font_cache + .select_font( + family, + &Properties { + weight: Weight::BOLD, + ..Default::default() + }, + ) + .unwrap(); + + let text = "aa bbb cccc ddddd eeee"; + let line = text_layout_cache.layout_str( + text, + 16.0, + &[ + (4, normal, Color::default()), + (5, bold, Color::default()), + (6, normal, Color::default()), + (1, bold, Color::default()), + (7, normal, Color::default()), + ], + ); + + let mut wrapper = LineWrapper::new(font_id, 16., font_system); + assert_eq!( + wrapper + .wrap_shaped_line(&text, &line, 72.0) + .collect::>(), + &[ + ShapedBoundary { + run_ix: 1, + glyph_ix: 3 + }, + ShapedBoundary { + run_ix: 2, + glyph_ix: 3 + }, + ShapedBoundary { + run_ix: 4, + glyph_ix: 2 + } + ], + ); + } +} diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 83b1575ab71534cf5ae52eff9eeca4c20a05f0ed..e118f3202d211a33e2e48d413203fcbf012db221 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -1,5 +1,4 @@ mod fold_map; -mod line_wrapper; mod tab_map; mod wrap_map; diff --git a/zed/src/editor/display_map/line_wrapper.rs b/zed/src/editor/display_map/line_wrapper.rs deleted file mode 100644 index 696a2ac2dd80de0ea0be83074e9121a0a865359a..0000000000000000000000000000000000000000 --- a/zed/src/editor/display_map/line_wrapper.rs +++ /dev/null @@ -1,403 +0,0 @@ -use crate::Settings; -use gpui::{fonts::FontId, text_layout::Line, FontCache, FontSystem}; -use lazy_static::lazy_static; -use parking_lot::Mutex; -use std::{ - collections::HashMap, - iter, - ops::{Deref, DerefMut}, - sync::Arc, -}; - -lazy_static! { - static ref POOL: Mutex> = Default::default(); -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Boundary { - pub ix: usize, - pub next_indent: u32, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct ShapedBoundary { - pub run_ix: usize, - pub glyph_ix: usize, -} - -impl Boundary { - fn new(ix: usize, next_indent: u32) -> Self { - Self { ix, next_indent } - } -} - -pub struct LineWrapper { - font_system: Arc, - font_id: FontId, - font_size: f32, - cached_ascii_char_widths: [f32; 128], - cached_other_char_widths: HashMap, -} - -impl LineWrapper { - pub const MAX_INDENT: u32 = 256; - - pub fn acquire( - font_system: Arc, - font_cache: &FontCache, - settings: Settings, - ) -> LineWrapperHandle { - let wrapper = if let Some(mut wrapper) = POOL.lock().pop() { - let font_id = font_cache - .select_font(settings.buffer_font_family, &Default::default()) - .unwrap(); - let font_size = settings.buffer_font_size; - if wrapper.font_id != font_id || wrapper.font_size != font_size { - wrapper.cached_ascii_char_widths = [f32::NAN; 128]; - wrapper.cached_other_char_widths.clear(); - } - wrapper - } else { - LineWrapper::new(font_system, font_cache, settings) - }; - LineWrapperHandle(Some(wrapper)) - } - - pub fn new( - font_system: Arc, - font_cache: &FontCache, - settings: Settings, - ) -> Self { - let font_id = font_cache - .select_font(settings.buffer_font_family, &Default::default()) - .unwrap(); - let font_size = settings.buffer_font_size; - Self { - font_system, - font_id, - font_size, - cached_ascii_char_widths: [f32::NAN; 128], - cached_other_char_widths: HashMap::new(), - } - } - - pub fn wrap_line<'a>( - &'a mut self, - line: &'a str, - wrap_width: f32, - ) -> impl Iterator + 'a { - let mut width = 0.0; - let mut first_non_whitespace_ix = None; - let mut indent = None; - let mut last_candidate_ix = 0; - let mut last_candidate_width = 0.0; - let mut last_wrap_ix = 0; - let mut prev_c = '\0'; - let mut char_indices = line.char_indices(); - iter::from_fn(move || { - while let Some((ix, c)) = char_indices.next() { - if c == '\n' { - continue; - } - - if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() { - last_candidate_ix = ix; - last_candidate_width = width; - } - - if c != ' ' && first_non_whitespace_ix.is_none() { - first_non_whitespace_ix = Some(ix); - } - - let char_width = self.width_for_char(c); - width += char_width; - if width > wrap_width && ix > last_wrap_ix { - if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix) - { - indent = Some( - Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32), - ); - } - - if last_candidate_ix > 0 { - last_wrap_ix = last_candidate_ix; - width -= last_candidate_width; - last_candidate_ix = 0; - } else { - last_wrap_ix = ix; - width = char_width; - } - - let indent_width = - indent.map(|indent| indent as f32 * self.width_for_char(' ')); - width += indent_width.unwrap_or(0.); - - return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0))); - } - prev_c = c; - } - - None - }) - } - - pub fn wrap_shaped_line<'a>( - &'a mut self, - str: &'a str, - line: &'a Line, - wrap_width: f32, - ) -> impl Iterator + 'a { - let mut width = 0.0; - let mut first_non_whitespace_ix = None; - let mut last_candidate_ix = None; - let mut last_candidate_x = 0.0; - let mut last_wrap_ix = ShapedBoundary { - run_ix: 0, - glyph_ix: 0, - }; - let mut last_wrap_x = 0.; - let mut prev_c = '\0'; - let mut glyphs = line - .runs() - .iter() - .enumerate() - .flat_map(move |(run_ix, run)| { - run.glyphs() - .iter() - .enumerate() - .map(move |(glyph_ix, glyph)| { - let character = str[glyph.index..].chars().next().unwrap(); - ( - ShapedBoundary { run_ix, glyph_ix }, - character, - glyph.position.x(), - ) - }) - }); - - iter::from_fn(move || { - while let Some((ix, c, x)) = glyphs.next() { - if c == '\n' { - continue; - } - - if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() { - last_candidate_ix = Some(ix); - last_candidate_x = x; - } - - if c != ' ' && first_non_whitespace_ix.is_none() { - first_non_whitespace_ix = Some(ix); - } - - let width = x - last_wrap_x; - if width > wrap_width && ix > last_wrap_ix { - if let Some(last_candidate_ix) = last_candidate_ix.take() { - last_wrap_ix = last_candidate_ix; - last_wrap_x = last_candidate_x; - } else { - last_wrap_ix = ix; - last_wrap_x = x; - } - - return Some(last_wrap_ix); - } - prev_c = c; - } - - None - }) - } - - fn is_boundary(&self, prev: char, next: char) -> bool { - (prev == ' ') && (next != ' ') - } - - #[inline(always)] - fn width_for_char(&mut self, c: char) -> f32 { - if (c as u32) < 128 { - let mut width = self.cached_ascii_char_widths[c as usize]; - if width.is_nan() { - width = self.compute_width_for_char(c); - self.cached_ascii_char_widths[c as usize] = width; - } - width - } else { - let mut width = self - .cached_other_char_widths - .get(&c) - .copied() - .unwrap_or(f32::NAN); - if width.is_nan() { - width = self.compute_width_for_char(c); - self.cached_other_char_widths.insert(c, width); - } - width - } - } - - fn compute_width_for_char(&self, c: char) -> f32 { - self.font_system - .layout_line( - &c.to_string(), - self.font_size, - &[(1, self.font_id, Default::default())], - ) - .width - } -} - -pub struct LineWrapperHandle(Option); - -impl Drop for LineWrapperHandle { - fn drop(&mut self) { - let wrapper = self.0.take().unwrap(); - POOL.lock().push(wrapper) - } -} - -impl Deref for LineWrapperHandle { - type Target = LineWrapper; - - fn deref(&self) -> &Self::Target { - self.0.as_ref().unwrap() - } -} - -impl DerefMut for LineWrapperHandle { - fn deref_mut(&mut self) -> &mut Self::Target { - self.0.as_mut().unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{ - color::Color, - fonts::{Properties, Weight}, - TextLayoutCache, - }; - - #[gpui::test] - fn test_wrap_line(cx: &mut gpui::MutableAppContext) { - let font_cache = cx.font_cache().clone(); - let font_system = cx.platform().fonts(); - let settings = Settings { - tab_size: 4, - buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(), - buffer_font_size: 16.0, - ..Settings::new(&font_cache).unwrap() - }; - - let mut wrapper = LineWrapper::new(font_system, &font_cache, settings); - assert_eq!( - wrapper - .wrap_line("aa bbb cccc ddddd eeee", 72.0) - .collect::>(), - &[ - Boundary::new(7, 0), - Boundary::new(12, 0), - Boundary::new(18, 0) - ], - ); - assert_eq!( - wrapper - .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0) - .collect::>(), - &[ - Boundary::new(4, 0), - Boundary::new(11, 0), - Boundary::new(18, 0) - ], - ); - assert_eq!( - wrapper.wrap_line(" aaaaaaa", 72.).collect::>(), - &[ - Boundary::new(7, 5), - Boundary::new(9, 5), - Boundary::new(11, 5), - ] - ); - assert_eq!( - wrapper - .wrap_line(" ", 72.) - .collect::>(), - &[ - Boundary::new(7, 0), - Boundary::new(14, 0), - Boundary::new(21, 0) - ] - ); - assert_eq!( - wrapper - .wrap_line(" aaaaaaaaaaaaaa", 72.) - .collect::>(), - &[ - Boundary::new(7, 0), - Boundary::new(14, 3), - Boundary::new(18, 3), - Boundary::new(22, 3), - ] - ); - } - - #[gpui::test] - fn test_wrap_layout_line(cx: &mut gpui::MutableAppContext) { - let font_cache = cx.font_cache().clone(); - let font_system = cx.platform().fonts(); - let text_layout_cache = TextLayoutCache::new(font_system.clone()); - - let family = font_cache.load_family(&["Helvetica"]).unwrap(); - let settings = Settings { - tab_size: 4, - buffer_font_family: family, - buffer_font_size: 16.0, - ..Settings::new(&font_cache).unwrap() - }; - let normal = font_cache.select_font(family, &Default::default()).unwrap(); - let bold = font_cache - .select_font( - family, - &Properties { - weight: Weight::BOLD, - ..Default::default() - }, - ) - .unwrap(); - - let text = "aa bbb cccc ddddd eeee"; - let line = text_layout_cache.layout_str( - text, - 16.0, - &[ - (4, normal, Color::default()), - (5, bold, Color::default()), - (6, normal, Color::default()), - (1, bold, Color::default()), - (7, normal, Color::default()), - ], - ); - - let mut wrapper = LineWrapper::new(font_system, &font_cache, settings); - assert_eq!( - wrapper - .wrap_shaped_line(&text, &line, 72.0) - .collect::>(), - &[ - ShapedBoundary { - run_ix: 1, - glyph_ix: 3 - }, - ShapedBoundary { - run_ix: 2, - glyph_ix: 3 - }, - ShapedBoundary { - run_ix: 4, - glyph_ix: 2 - } - ], - ); - } -} diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index 1d5359eabff854d7d2e9a106b7e890f56d8b1f41..c8c8f067ca5066a085663f62568b0db4295ea54c 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/zed/src/editor/display_map/wrap_map.rs @@ -1,11 +1,11 @@ use super::{ fold_map, - line_wrapper::LineWrapper, tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary}, }; use crate::{editor::Point, settings::HighlightId, util::Bias, Settings}; use gpui::{ sum_tree::{self, Cursor, SumTree}, + text_layout::LineWrapper, Entity, ModelContext, Task, }; use lazy_static::lazy_static; @@ -119,7 +119,11 @@ impl WrapMap { let font_cache = cx.font_cache().clone(); let settings = self.settings.clone(); let task = cx.background().spawn(async move { - let mut line_wrapper = LineWrapper::acquire(font_system, &font_cache, settings); + let font_id = font_cache + .select_font(settings.buffer_font_family, &Default::default()) + .unwrap(); + let mut line_wrapper = + LineWrapper::acquire(font_id, settings.buffer_font_size, font_system); let tab_snapshot = new_snapshot.tab_snapshot.clone(); let range = TabPoint::zero()..tab_snapshot.max_point(); new_snapshot @@ -193,7 +197,11 @@ impl WrapMap { let font_cache = cx.font_cache().clone(); let settings = self.settings.clone(); let update_task = cx.background().spawn(async move { - let mut line_wrapper = LineWrapper::acquire(font_system, &font_cache, settings); + let font_id = font_cache + .select_font(settings.buffer_font_family, &Default::default()) + .unwrap(); + let mut line_wrapper = + LineWrapper::acquire(font_id, settings.buffer_font_size, font_system); for (tab_snapshot, edits) in pending_edits { snapshot .update(tab_snapshot, &edits, wrap_width, &mut line_wrapper) @@ -941,7 +949,10 @@ mod tests { .add_model(|cx| WrapMap::new(tabs_snapshot.clone(), settings.clone(), wrap_width, cx)); let (_observer, notifications) = Observer::new(&wrap_map, &mut cx); - let mut line_wrapper = LineWrapper::new(font_system, &font_cache, settings); + 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 unwrapped_text = tabs_snapshot.text(); let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); From 399ecaabc6b09bd1a43b9e1069e35b33ba586a9f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 Aug 2021 14:43:22 +0200 Subject: [PATCH 092/204] Wrap lines in `Text` element --- gpui/src/elements/text.rs | 59 +++++++++++++++++++++++++-------------- gpui/src/presenter.rs | 4 ++- gpui/src/text_layout.rs | 48 +++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 22 deletions(-) diff --git a/gpui/src/elements/text.rs b/gpui/src/elements/text.rs index 4c6d0559e680e97e93e23fa414f72344573f3ce2..5d30fe03fceaa7035fc5235eb1fa18ec7f50d636 100644 --- a/gpui/src/elements/text.rs +++ b/gpui/src/elements/text.rs @@ -1,19 +1,16 @@ use crate::{ color::Color, font_cache::FamilyId, - fonts::{FontId, TextStyle}, + fonts::TextStyle, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, - text_layout::Line, - DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext, - SizeConstraint, + text_layout::{Line, LineWrapper, ShapedBoundary}, + DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; -use serde::Deserialize; use serde_json::json; -use smallvec::{smallvec, SmallVec}; pub struct Text { text: String, @@ -22,6 +19,12 @@ pub struct Text { style: TextStyle, } +pub struct LayoutState { + line: Line, + wrap_boundaries: Vec, + line_height: f32, +} + impl Text { pub fn new(text: String, family_id: FamilyId, font_size: f32) -> Self { Self { @@ -44,7 +47,7 @@ impl Text { } impl Element for Text { - type LayoutState = Line; + type LayoutState = LayoutState; type PaintState = (); fn layout( @@ -56,30 +59,44 @@ impl Element for Text { .font_cache .select_font(self.family_id, &self.style.font_properties) .unwrap(); - todo!() - // let line = - // cx.text_layout_cache - // .layout_str(self.text.as_str(), self.font_size, runs.as_slice()); - - // let size = vec2f( - // line.width().max(constraint.min.x()).min(constraint.max.x()), - // cx.font_cache.line_height(font_id, self.font_size).ceil(), - // ); + let line_height = cx.font_cache.line_height(font_id, self.font_size); + let line = cx.text_layout_cache.layout_str( + self.text.as_str(), + self.font_size, + &[(self.text.len(), font_id, self.style.color)], + ); + let mut wrapper = LineWrapper::acquire(font_id, self.font_size, cx.font_system.clone()); + let wrap_boundaries = wrapper + .wrap_shaped_line(&self.text, &line, constraint.max.x()) + .collect::>(); + let size = vec2f( + line.width() + .ceil() + .max(constraint.min.x()) + .min(constraint.max.x()), + (line_height * (wrap_boundaries.len() + 1) as f32).ceil(), + ); + let layout = LayoutState { + line, + wrap_boundaries, + line_height, + }; - // (size, line) + (size, layout) } fn paint( &mut self, bounds: RectF, - line: &mut Self::LayoutState, + layout: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - line.paint( + layout.line.paint_wrapped( bounds.origin(), - RectF::new(vec2f(0., 0.), bounds.size()), + layout.line_height, + layout.wrap_boundaries.iter().copied(), cx, - ) + ); } fn dispatch_event( diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index ac4652482cc72abc76640af671259a376eebdd97..030b8262ccb5d2d4cea271d46a9eb96ab90a387c 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -5,7 +5,7 @@ use crate::{ json::{self, ToJson}, platform::Event, text_layout::TextLayoutCache, - Action, AnyAction, AssetCache, ElementBox, Scene, + Action, AnyAction, AssetCache, ElementBox, FontSystem, Scene, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -114,6 +114,7 @@ impl Presenter { rendered_views: &mut self.rendered_views, parents: &mut self.parents, font_cache: &self.font_cache, + font_system: cx.platform().fonts(), text_layout_cache: &self.text_layout_cache, asset_cache: &self.asset_cache, view_stack: Vec::new(), @@ -173,6 +174,7 @@ pub struct LayoutContext<'a> { parents: &'a mut HashMap, view_stack: Vec, pub font_cache: &'a FontCache, + pub font_system: Arc, pub text_layout_cache: &'a TextLayoutCache, pub asset_cache: &'a AssetCache, pub app: &'a mut MutableAppContext, diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index e19f82ed55c76ad470756bb1db9019f5d7022e8b..9d171e180cdfb12ca4294e4df991977b416ba259 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -251,6 +251,54 @@ impl Line { } } } + + pub fn paint_wrapped( + &self, + origin: Vector2F, + line_height: f32, + boundaries: impl IntoIterator, + cx: &mut PaintContext, + ) { + let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.; + let baseline_origin = vec2f(0., padding_top + self.layout.ascent); + + let mut boundaries = boundaries.into_iter().peekable(); + let mut color_runs = self.color_runs.iter(); + let mut color_end = 0; + let mut color = Color::black(); + + let mut glyph_origin = baseline_origin; + let mut prev_position = 0.; + for run in &self.layout.runs { + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + if boundaries.peek().map_or(false, |b| b.glyph_ix == glyph_ix) { + boundaries.next(); + glyph_origin = vec2f(0., glyph_origin.y() + line_height); + } else { + glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); + } + prev_position = glyph.position.x(); + + if glyph.index >= color_end { + if let Some(next_run) = color_runs.next() { + color_end += next_run.0 as usize; + color = next_run.1; + } else { + color_end = self.layout.len; + color = Color::black(); + } + } + + cx.scene.push_glyph(scene::Glyph { + font_id: run.font_id, + font_size: self.layout.font_size, + id: glyph.id, + origin: origin + glyph_origin, + color, + }); + } + } + } } impl Run { From 72f282eb3ade6077c5f697c131915787b2c0ff45 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 Aug 2021 15:21:04 +0200 Subject: [PATCH 093/204] Calculate current line width correctly when wrapping shaped lines --- gpui/src/text_layout.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index 9d171e180cdfb12ca4294e4df991977b416ba259..8591dbe7f2bf3359f561a1222a06d1014f3ba7c7 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -458,7 +458,8 @@ impl LineWrapper { glyph.position.x(), ) }) - }); + }) + .peekable(); iter::from_fn(move || { while let Some((ix, c, x)) = glyphs.next() { @@ -475,7 +476,8 @@ impl LineWrapper { first_non_whitespace_ix = Some(ix); } - let width = x - last_wrap_x; + let next_x = glyphs.peek().map_or(line.width(), |(_, _, x)| *x); + let width = next_x - last_wrap_x; if width > wrap_width && ix > last_wrap_ix { if let Some(last_candidate_ix) = last_candidate_ix.take() { last_wrap_ix = last_candidate_ix; @@ -623,7 +625,7 @@ mod tests { } #[crate::test(self)] - fn test_wrap_layout_line(cx: &mut crate::MutableAppContext) { + fn test_wrap_shaped_line(cx: &mut crate::MutableAppContext) { let font_cache = cx.font_cache().clone(); let font_system = cx.platform().fonts(); let text_layout_cache = TextLayoutCache::new(font_system.clone()); From 3d5cfb78eaf320f69a845cbe86c27c7310e25a7b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 Aug 2021 16:42:35 +0200 Subject: [PATCH 094/204] Use `Text` in chat panel Co-Authored-By: Nathan Sobo --- zed/assets/themes/_base.toml | 2 +- zed/src/chat_panel.rs | 4 ++-- zed/src/theme.rs | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 56dbbdaaf10bdbe8bbf079867957827f8cc84f08..6fed76f81a90b8952fb3e111cce2489d033abe95 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -23,7 +23,7 @@ color = "$text.2" color = "$text.0" [chat_panel.message] -text = "$text.0" +body = "$text.0" [selector] background = "$surface.2" diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 7788ab2eba782dc74b8b14ffcfa95f50ae9bcc1f..30ce44b82909fb61343936dbbdd719925053ec12 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -116,12 +116,12 @@ impl ChatPanel { fn render_message(&self, message: &ChannelMessage) -> ElementBox { let settings = self.settings.borrow(); - Label::new( + Text::new( message.body.clone(), settings.ui_font_family, settings.ui_font_size, ) - .with_style(&settings.theme.chat_panel.message.label) + .with_style(&settings.theme.chat_panel.message.body) .boxed() } diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 0bd5b3593d48cc589d8e1b5e575e7d24a8d657ca..13fa53421144c30045fa5877be4daf78615278bb 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -60,8 +60,7 @@ pub struct ChatPanel { #[derive(Debug, Default, Deserialize)] pub struct ChatMessage { - #[serde(flatten)] - pub label: LabelStyle, + pub body: TextStyle, } #[derive(Debug, Default, Deserialize)] From 0c9e72cce7683b3d9d30d4219c51eb00db67934e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 Aug 2021 16:53:26 +0200 Subject: [PATCH 095/204] Support hard wraps in `Text` element Co-Authored-By: Nathan Sobo --- gpui/src/elements/text.rs | 58 ++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/gpui/src/elements/text.rs b/gpui/src/elements/text.rs index 5d30fe03fceaa7035fc5235eb1fa18ec7f50d636..45aaab465771194c8574b29abff03be7239d74e4 100644 --- a/gpui/src/elements/text.rs +++ b/gpui/src/elements/text.rs @@ -20,8 +20,7 @@ pub struct Text { } pub struct LayoutState { - line: Line, - wrap_boundaries: Vec, + lines: Vec<(Line, Vec)>, line_height: f32, } @@ -60,29 +59,34 @@ impl Element for Text { .select_font(self.family_id, &self.style.font_properties) .unwrap(); let line_height = cx.font_cache.line_height(font_id, self.font_size); - let line = cx.text_layout_cache.layout_str( - self.text.as_str(), - self.font_size, - &[(self.text.len(), font_id, self.style.color)], - ); + let mut wrapper = LineWrapper::acquire(font_id, self.font_size, cx.font_system.clone()); - let wrap_boundaries = wrapper - .wrap_shaped_line(&self.text, &line, constraint.max.x()) - .collect::>(); + let mut lines = Vec::new(); + let mut line_count = 0; + let mut max_line_width = 0_f32; + for line in self.text.lines() { + let shaped_line = cx.text_layout_cache.layout_str( + line, + self.font_size, + &[(line.len(), font_id, self.style.color)], + ); + let wrap_boundaries = wrapper + .wrap_shaped_line(line, &shaped_line, constraint.max.x()) + .collect::>(); + + max_line_width = max_line_width.max(shaped_line.width()); + line_count += wrap_boundaries.len() + 1; + lines.push((shaped_line, wrap_boundaries)); + } + let size = vec2f( - line.width() + max_line_width .ceil() .max(constraint.min.x()) .min(constraint.max.x()), - (line_height * (wrap_boundaries.len() + 1) as f32).ceil(), + (line_height * line_count as f32).ceil(), ); - let layout = LayoutState { - line, - wrap_boundaries, - line_height, - }; - - (size, layout) + (size, LayoutState { lines, line_height }) } fn paint( @@ -91,12 +95,16 @@ impl Element for Text { layout: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - layout.line.paint_wrapped( - bounds.origin(), - layout.line_height, - layout.wrap_boundaries.iter().copied(), - cx, - ); + let mut origin = bounds.origin(); + for (line, wrap_boundaries) in &layout.lines { + line.paint_wrapped( + origin, + layout.line_height, + wrap_boundaries.iter().copied(), + cx, + ); + origin.set_y(origin.y() + (wrap_boundaries.len() + 1) as f32 * layout.line_height); + } } fn dispatch_event( From 1aa1e6c6ab2b21ef860443e4d89e2f4f45f01962 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 Aug 2021 17:28:25 +0200 Subject: [PATCH 096/204] Move pooling of line wrappers into `FontCache` Co-Authored-By: Nathan Sobo --- gpui/src/elements/text.rs | 4 +- gpui/src/font_cache.rs | 62 ++++++++++++++++++++++++-- gpui/src/presenter.rs | 2 +- gpui/src/text_layout.rs | 50 +-------------------- zed/src/editor/display_map/wrap_map.rs | 7 +-- 5 files changed, 65 insertions(+), 60 deletions(-) diff --git a/gpui/src/elements/text.rs b/gpui/src/elements/text.rs index 45aaab465771194c8574b29abff03be7239d74e4..0459a6882944ecfe8682b9debec847647c5dd52c 100644 --- a/gpui/src/elements/text.rs +++ b/gpui/src/elements/text.rs @@ -7,7 +7,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, - text_layout::{Line, LineWrapper, ShapedBoundary}, + text_layout::{Line, ShapedBoundary}, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use serde_json::json; @@ -60,7 +60,7 @@ impl Element for Text { .unwrap(); let line_height = cx.font_cache.line_height(font_id, self.font_size); - let mut wrapper = LineWrapper::acquire(font_id, self.font_size, cx.font_system.clone()); + let mut wrapper = cx.font_cache.line_wrapper(font_id, self.font_size); let mut lines = Vec::new(); let mut line_count = 0; let mut max_line_width = 0_f32; diff --git a/gpui/src/font_cache.rs b/gpui/src/font_cache.rs index 75ee206b35e7e3c35400ed584216da3bc2e512ff..b1deb802fd5bb18b83023a1be71910a53a8626de 100644 --- a/gpui/src/font_cache.rs +++ b/gpui/src/font_cache.rs @@ -2,10 +2,16 @@ use crate::{ fonts::{FontId, Metrics, Properties}, geometry::vector::{vec2f, Vector2F}, platform, + text_layout::LineWrapper, }; use anyhow::{anyhow, Result}; +use ordered_float::OrderedFloat; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, + sync::Arc, +}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct FamilyId(usize); @@ -22,6 +28,12 @@ pub struct FontCacheState { families: Vec, font_selections: HashMap>, metrics: HashMap, + wrapper_pool: HashMap<(FontId, OrderedFloat), Vec>, +} + +pub struct LineWrapperHandle { + wrapper: Option, + font_cache: Arc, } unsafe impl Send for FontCache {} @@ -30,9 +42,10 @@ impl FontCache { pub fn new(fonts: Arc) -> Self { Self(RwLock::new(FontCacheState { fonts, - families: Vec::new(), - font_selections: HashMap::new(), - metrics: HashMap::new(), + families: Default::default(), + font_selections: Default::default(), + metrics: Default::default(), + wrapper_pool: Default::default(), })) } @@ -160,6 +173,47 @@ impl FontCache { 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 line_wrapper(self: &Arc, font_id: FontId, font_size: f32) -> LineWrapperHandle { + let mut state = self.0.write(); + let wrappers = state + .wrapper_pool + .entry((font_id, OrderedFloat(font_size))) + .or_default(); + let wrapper = wrappers + .pop() + .unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.fonts.clone())); + LineWrapperHandle { + wrapper: Some(wrapper), + font_cache: self.clone(), + } + } +} + +impl Drop for LineWrapperHandle { + fn drop(&mut self) { + let mut state = self.font_cache.0.write(); + let wrapper = self.wrapper.take().unwrap(); + state + .wrapper_pool + .get_mut(&(wrapper.font_id, OrderedFloat(wrapper.font_size))) + .unwrap() + .push(wrapper); + } +} + +impl Deref for LineWrapperHandle { + type Target = LineWrapper; + + fn deref(&self) -> &Self::Target { + self.wrapper.as_ref().unwrap() + } +} + +impl DerefMut for LineWrapperHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + self.wrapper.as_mut().unwrap() + } } #[cfg(test)] diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 030b8262ccb5d2d4cea271d46a9eb96ab90a387c..80739a8be66ea0f2b502853e0be48c40d1c60de1 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -173,7 +173,7 @@ pub struct LayoutContext<'a> { rendered_views: &'a mut HashMap, parents: &'a mut HashMap, view_stack: Vec, - pub font_cache: &'a FontCache, + pub font_cache: &'a Arc, pub font_system: Arc, pub text_layout_cache: &'a TextLayoutCache, pub asset_cache: &'a AssetCache, diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index 8591dbe7f2bf3359f561a1222a06d1014f3ba7c7..71db39947664c825864ac7e7f9bdbbf051061d3c 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -7,7 +7,6 @@ use crate::{ }, platform, scene, FontSystem, PaintContext, }; -use lazy_static::lazy_static; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use smallvec::SmallVec; @@ -16,7 +15,6 @@ use std::{ collections::HashMap, hash::{Hash, Hasher}, iter, - ops::{Deref, DerefMut}, sync::Arc, }; @@ -307,10 +305,6 @@ impl Run { } } -lazy_static! { - static ref WRAPPER_POOL: Mutex> = Default::default(); -} - #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Boundary { pub ix: usize, @@ -331,8 +325,8 @@ impl Boundary { pub struct LineWrapper { font_system: Arc, - font_id: FontId, - font_size: f32, + pub(crate) font_id: FontId, + pub(crate) font_size: f32, cached_ascii_char_widths: [f32; 128], cached_other_char_widths: HashMap, } @@ -340,23 +334,6 @@ pub struct LineWrapper { impl LineWrapper { pub const MAX_INDENT: u32 = 256; - pub fn acquire( - font_id: FontId, - font_size: f32, - font_system: Arc, - ) -> LineWrapperHandle { - let wrapper = if let Some(mut wrapper) = WRAPPER_POOL.lock().pop() { - if wrapper.font_id != font_id || wrapper.font_size != font_size { - wrapper.cached_ascii_char_widths = [f32::NAN; 128]; - wrapper.cached_other_char_widths.clear(); - } - wrapper - } else { - LineWrapper::new(font_id, font_size, font_system) - }; - LineWrapperHandle(Some(wrapper)) - } - pub fn new(font_id: FontId, font_size: f32, font_system: Arc) -> Self { Self { font_system, @@ -534,29 +511,6 @@ impl LineWrapper { } } -pub struct LineWrapperHandle(Option); - -impl Drop for LineWrapperHandle { - fn drop(&mut self) { - let wrapper = self.0.take().unwrap(); - WRAPPER_POOL.lock().push(wrapper) - } -} - -impl Deref for LineWrapperHandle { - type Target = LineWrapper; - - fn deref(&self) -> &Self::Target { - self.0.as_ref().unwrap() - } -} - -impl DerefMut for LineWrapperHandle { - fn deref_mut(&mut self) -> &mut Self::Target { - self.0.as_mut().unwrap() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index c8c8f067ca5066a085663f62568b0db4295ea54c..3ba9690c640fcafdd109931c3cebe5dbaefe1a65 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/zed/src/editor/display_map/wrap_map.rs @@ -115,15 +115,13 @@ impl WrapMap { if let Some(wrap_width) = wrap_width { let mut new_snapshot = self.snapshot.clone(); - let font_system = cx.platform().fonts(); let font_cache = cx.font_cache().clone(); let settings = self.settings.clone(); let task = cx.background().spawn(async move { let font_id = font_cache .select_font(settings.buffer_font_family, &Default::default()) .unwrap(); - let mut line_wrapper = - LineWrapper::acquire(font_id, settings.buffer_font_size, font_system); + let mut line_wrapper = font_cache.line_wrapper(font_id, settings.buffer_font_size); let tab_snapshot = new_snapshot.tab_snapshot.clone(); let range = TabPoint::zero()..tab_snapshot.max_point(); new_snapshot @@ -193,7 +191,6 @@ impl WrapMap { if self.background_task.is_none() { let pending_edits = self.pending_edits.clone(); let mut snapshot = self.snapshot.clone(); - let font_system = cx.platform().fonts(); let font_cache = cx.font_cache().clone(); let settings = self.settings.clone(); let update_task = cx.background().spawn(async move { @@ -201,7 +198,7 @@ impl WrapMap { .select_font(settings.buffer_font_family, &Default::default()) .unwrap(); let mut line_wrapper = - LineWrapper::acquire(font_id, settings.buffer_font_size, font_system); + font_cache.line_wrapper(font_id, settings.buffer_font_size); for (tab_snapshot, edits) in pending_edits { snapshot .update(tab_snapshot, &edits, wrap_width, &mut line_wrapper) From 897826f71056d385da9722b5bf4009e6600cfcb9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 Aug 2021 17:35:27 +0200 Subject: [PATCH 097/204] Run subscription/observation callbacks in the order they were added Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 8c8fd2448548a3c8c616ef8095d5f9d76977e383..5d1fdc3a49644613cb10011584f574a7f9f156dd 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -18,7 +18,7 @@ use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, cell::RefCell, - collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, + collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque}, fmt::{self, Debug}, hash::{Hash, Hasher}, marker::PhantomData, @@ -652,8 +652,8 @@ pub struct MutableAppContext { next_entity_id: usize, next_window_id: usize, next_subscription_id: usize, - subscriptions: Arc>>>, - observations: Arc>>>, + subscriptions: Arc>>>, + observations: Arc>>>, presenters_and_platform_windows: HashMap>, Box)>, debug_elements_callbacks: HashMap crate::json::Value>>, @@ -2967,12 +2967,12 @@ pub enum Subscription { Subscription { id: usize, entity_id: usize, - subscriptions: Option>>>>, + subscriptions: Option>>>>, }, Observation { id: usize, entity_id: usize, - observations: Option>>>>, + observations: Option>>>>, }, } @@ -3200,7 +3200,7 @@ mod tests { assert_eq!(handle_1.read(cx).events, vec![7]); handle_2.update(cx, |_, c| c.emit(5)); - assert_eq!(handle_1.read(cx).events, vec![7, 10, 5]); + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]); } #[crate::test(self)] @@ -3240,7 +3240,7 @@ mod tests { model.count = 5; c.notify() }); - assert_eq!(handle_1.read(cx).events, vec![7, 10, 5]) + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]) } #[crate::test(self)] @@ -3462,10 +3462,10 @@ mod tests { assert_eq!(handle_1.read(cx).events, vec![7]); handle_2.update(cx, |_, c| c.emit(5)); - assert_eq!(handle_1.read(cx).events, vec![7, 10, 5]); + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]); handle_3.update(cx, |_, c| c.emit(9)); - assert_eq!(handle_1.read(cx).events, vec![7, 10, 5, 9]); + assert_eq!(handle_1.read(cx).events, vec![7, 5, 10, 9]); } #[crate::test(self)] From 77c7fa53da677e209462eb7a6696b4bb58b01b92 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Aug 2021 12:45:17 -0600 Subject: [PATCH 098/204] Introduce Orientation concept to List When the Orientation is Bottom, we paint elements from the bottom of the list when underflowing and express scroll position relative to the bottom. In either orientation, when inserting elements outside the visible area, we adjust the scroll position as needed to keep the visible elements stable. Co-Authored-By: Max Brunsfeld Co-Authored-By: Antonio Scandurra --- gpui/src/elements/list.rs | 69 ++++++++++++++++++++++++++++++++------- zed/src/chat_panel.rs | 3 +- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 78f8c2316e32ffc3ab4679d1461ae78f7967f1c6..b4213fc5a50c825c9ae353dbb6fdef460f4fb170 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -19,11 +19,18 @@ pub struct List { #[derive(Clone)] pub struct ListState(Arc>); +#[derive(Eq, PartialEq)] +pub enum Orientation { + Top, + Bottom, +} + struct StateInner { last_layout_width: f32, elements: Vec, heights: SumTree, - scroll_top: f32, + scroll_position: f32, + orientation: Orientation, } #[derive(Clone, Debug)] @@ -69,6 +76,11 @@ impl Element for List { item_constraint.min.set_y(0.); item_constraint.max.set_y(f32::INFINITY); + let size = constraint.max; + + let visible_top = state.scroll_top(size.y()); + let visible_bottom = visible_top + size.y(); + if state.last_layout_width == constraint.max.x() { let mut old_heights = state.heights.cursor::(); let mut new_heights = old_heights.slice(&PendingCount(1), sum_tree::Bias::Left, &()); @@ -78,6 +90,21 @@ impl Element for List { let size = state.elements[old_heights.sum_start().count].layout(item_constraint, cx); new_heights.push(ElementHeight::Ready(size.y()), &()); + + // Adjust scroll position to keep visible elements stable + match state.orientation { + Orientation::Top => { + if new_heights.summary().height < visible_top { + state.scroll_position += size.y(); + } + } + Orientation::Bottom => { + if new_heights.summary().height - size.y() > visible_bottom { + state.scroll_position += size.y(); + } + } + } + old_heights.next(&()); } else { new_heights.push_tree( @@ -102,7 +129,7 @@ impl Element for List { state.last_layout_width = constraint.max.x(); } - (constraint.max, ()) + (size, ()) } fn paint(&mut self, bounds: RectF, _: &mut (), cx: &mut PaintContext) { @@ -115,8 +142,15 @@ impl Element for List { cursor.seek(&Count(visible_range.start), Bias::Right, &()); cursor.sum_start().0 }; + if state.orientation == Orientation::Bottom + && bounds.height() > state.heights.summary().height + { + item_top += bounds.height() - state.heights.summary().height; + } + let scroll_top = state.scroll_top(bounds.height()); + for element in &mut state.elements[visible_range] { - let origin = bounds.origin() + vec2f(0., item_top - state.scroll_top); + let origin = bounds.origin() + vec2f(0., item_top - scroll_top); element.paint(origin, cx); item_top += element.size().y(); } @@ -167,20 +201,21 @@ impl Element for List { json!({ "visible_range": visible_range, "visible_elements": visible_elements, - "scroll_top": state.scroll_top, + "scroll_position": state.scroll_position, }) } } impl ListState { - pub fn new(elements: Vec) -> Self { + pub fn new(elements: Vec, orientation: Orientation) -> Self { let mut heights = SumTree::new(); heights.extend(elements.iter().map(|_| ElementHeight::Pending), &()); Self(Arc::new(Mutex::new(StateInner { last_layout_width: 0., elements, heights, - scroll_top: 0., + scroll_position: 0., + orientation, }))) } @@ -215,9 +250,9 @@ impl ListState { impl StateInner { fn visible_range(&self, height: f32) -> Range { let mut cursor = self.heights.cursor::(); - cursor.seek(&Height(self.scroll_top), Bias::Right, &()); + cursor.seek(&Height(self.scroll_top(height)), Bias::Right, &()); let start_ix = cursor.sum_start().0; - cursor.seek(&Height(self.scroll_top + height), Bias::Left, &()); + cursor.seek(&Height(self.scroll_top(height) + height), Bias::Left, &()); let end_ix = cursor.sum_start().0; start_ix..self.elements.len().min(end_ix + 1) } @@ -235,12 +270,24 @@ impl StateInner { } let scroll_max = (self.heights.summary().height - height).max(0.); - self.scroll_top = (self.scroll_top - delta.y()).max(0.).min(scroll_max); - + let delta_y = match self.orientation { + Orientation::Top => -delta.y(), + Orientation::Bottom => delta.y(), + }; + self.scroll_position = (self.scroll_position + delta_y).max(0.).min(scroll_max); cx.notify(); true } + + fn scroll_top(&self, height: f32) -> f32 { + match self.orientation { + Orientation::Top => self.scroll_position, + Orientation::Bottom => { + (self.heights.summary().height - height - self.scroll_position).max(0.) + } + } + } } impl ElementHeight { @@ -329,7 +376,7 @@ mod tests { fn test_layout(cx: &mut crate::MutableAppContext) { let mut presenter = cx.build_presenter(0, 20.0); let mut layout_cx = presenter.layout_cx(cx); - let state = ListState::new(vec![item(20.), item(30.), item(10.)]); + let state = ListState::new(vec![item(20.), item(30.), item(10.)], Orientation::Top); let mut list = List::new(state.clone()).boxed(); let size = list.layout( diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 30ce44b82909fb61343936dbbdd719925053ec12..0c4fbd584f44d530b6e29a1335b06bf820a0a418 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -38,7 +38,7 @@ impl ChatPanel { let mut this = Self { channel_list, active_channel: None, - messages: ListState::new(Vec::new()), + messages: ListState::new(Vec::new(), Orientation::Bottom), input_editor, settings, }; @@ -82,6 +82,7 @@ impl ChatPanel { .cursor::<(), ()>() .map(|m| self.render_message(m)) .collect(), + Orientation::Bottom, ); self.active_channel = Some((channel, subscription)); } From 8f86fa1ccd0d095e1ab3ba0c139d6fec3de72e04 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Aug 2021 14:20:03 -0600 Subject: [PATCH 099/204] Associate messages with their sender, fetching senders if necessary Co-Authored-By: Max Brunsfeld --- server/src/db.rs | 2 +- server/src/rpc.rs | 21 +++--- zed/src/channel.rs | 160 ++++++++++++++++++++++++++++++++++++--------- zed/src/lib.rs | 1 + zed/src/main.rs | 4 +- zed/src/test.rs | 4 +- zed/src/user.rs | 59 +++++++++++++++++ 7 files changed, 207 insertions(+), 44 deletions(-) create mode 100644 zed/src/user.rs diff --git a/server/src/db.rs b/server/src/db.rs index 1e489aae3642cfd896b4449f97af1eb8c10449cf..2ae8fc8f1d310c9d61c30f87d394ab9d8c5a495d 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -162,7 +162,7 @@ impl Db { FROM users, channel_memberships WHERE - users.id IN $1 AND + users.id = ANY ($1) AND channel_memberships.user_id = users.id AND channel_memberships.channel_id IN ( SELECT channel_id diff --git a/server/src/rpc.rs b/server/src/rpc.rs index a44738e8ffb1ae77715b3f7a5ec13056675973a3..c869dd1aea385ccbcf608291a4534bf0dbd13c2c 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -939,6 +939,7 @@ mod tests { language::LanguageRegistry, rpc::Client, settings, test, + user::UserStore, worktree::Worktree, }; use zrpc::Peer; @@ -1425,7 +1426,8 @@ mod tests { .await .unwrap(); - let channels_a = cx_a.add_model(|cx| ChannelList::new(client_a, cx)); + let user_store_a = Arc::new(UserStore::new(client_a.clone())); + let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx)); channels_a .condition(&mut cx_a, |list, _| list.available_channels().is_some()) .await; @@ -1445,11 +1447,12 @@ mod tests { channel_a .condition(&cx_a, |channel, _| { channel_messages(channel) - == [(user_id_b.to_proto(), "hello A, it's B.".to_string())] + == [("user_b".to_string(), "hello A, it's B.".to_string())] }) .await; - let channels_b = cx_b.add_model(|cx| ChannelList::new(client_b, cx)); + let user_store_b = Arc::new(UserStore::new(client_b.clone())); + let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b, client_b, cx)); channels_b .condition(&mut cx_b, |list, _| list.available_channels().is_some()) .await; @@ -1470,7 +1473,7 @@ mod tests { channel_b .condition(&cx_b, |channel, _| { channel_messages(channel) - == [(user_id_b.to_proto(), "hello A, it's B.".to_string())] + == [("user_b".to_string(), "hello A, it's B.".to_string())] }) .await; @@ -1494,9 +1497,9 @@ mod tests { .condition(&cx_b, |channel, _| { channel_messages(channel) == [ - (user_id_b.to_proto(), "hello A, it's B.".to_string()), - (user_id_a.to_proto(), "oh, hi B.".to_string()), - (user_id_a.to_proto(), "sup".to_string()), + ("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()), ] }) .await; @@ -1517,11 +1520,11 @@ mod tests { .condition(|state| !state.channels.contains_key(&channel_id)) .await; - fn channel_messages(channel: &Channel) -> Vec<(u64, String)> { + fn channel_messages(channel: &Channel) -> Vec<(String, String)> { channel .messages() .cursor::<(), ()>() - .map(|m| (m.sender_id, m.body.clone())) + .map(|m| (m.sender.github_login.clone(), m.body.clone())) .collect() } } diff --git a/zed/src/channel.rs b/zed/src/channel.rs index a70b7dd0685fc21055d640d54d2c190ee5a24ff5..73eb015884b110b5e55d740b6c12d50fd5a11302 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -1,5 +1,6 @@ use crate::{ rpc::{self, Client}, + user::{User, UserStore}, util::TryFutureExt, }; use anyhow::{anyhow, Context, Result}; @@ -9,7 +10,7 @@ use gpui::{ }; use postage::prelude::Stream; use std::{ - collections::{hash_map, HashMap}, + collections::{hash_map, HashMap, HashSet}, ops::Range, sync::Arc, }; @@ -22,6 +23,7 @@ pub struct ChannelList { available_channels: Option>, channels: HashMap>, rpc: Arc, + user_store: Arc, _task: Task>, } @@ -36,6 +38,7 @@ pub struct Channel { messages: SumTree, pending_messages: Vec, next_local_message_id: u64, + user_store: Arc, rpc: Arc, _subscription: rpc::Subscription, } @@ -43,8 +46,8 @@ pub struct Channel { #[derive(Clone, Debug, PartialEq)] pub struct ChannelMessage { pub id: u64, - pub sender_id: u64, pub body: String, + pub sender: Arc, } pub struct PendingChannelMessage { @@ -76,7 +79,11 @@ impl Entity for ChannelList { } impl ChannelList { - pub fn new(rpc: Arc, cx: &mut ModelContext) -> Self { + pub fn new( + user_store: Arc, + rpc: Arc, + cx: &mut ModelContext, + ) -> Self { let _task = cx.spawn(|this, mut cx| { let rpc = rpc.clone(); async move { @@ -114,6 +121,7 @@ impl ChannelList { Self { available_channels: None, channels: Default::default(), + user_store, rpc, _task, } @@ -136,8 +144,10 @@ impl ChannelList { .as_ref() .and_then(|channels| channels.iter().find(|details| details.id == id)) { + let user_store = self.user_store.clone(); let rpc = self.rpc.clone(); - let channel = cx.add_model(|cx| Channel::new(details.clone(), rpc, cx)); + let channel = + cx.add_model(|cx| Channel::new(details.clone(), user_store, rpc, cx)); entry.insert(channel.downgrade()); Some(channel) } else { @@ -165,34 +175,58 @@ impl Entity for Channel { } impl Channel { - pub fn new(details: ChannelDetails, rpc: Arc, cx: &mut ModelContext) -> Self { + pub fn new( + details: ChannelDetails, + user_store: Arc, + rpc: Arc, + cx: &mut ModelContext, + ) -> Self { let _subscription = rpc.subscribe_from_model(details.id, cx, Self::handle_message_sent); { + let user_store = user_store.clone(); let rpc = rpc.clone(); let channel_id = details.id; - cx.spawn(|channel, mut cx| async move { - match rpc.request(proto::JoinChannel { channel_id }).await { - Ok(response) => channel.update(&mut cx, |channel, cx| { + cx.spawn(|channel, mut cx| { + async move { + let response = rpc.request(proto::JoinChannel { channel_id }).await?; + + let unique_user_ids = response + .messages + .iter() + .map(|m| m.sender_id) + .collect::>() + .into_iter() + .collect(); + user_store.load_users(unique_user_ids).await?; + + let mut messages = Vec::with_capacity(response.messages.len()); + for message in response.messages { + messages.push(ChannelMessage::from_proto(message, &user_store).await?); + } + + channel.update(&mut cx, |channel, cx| { let old_count = channel.messages.summary().count.0; - let new_count = response.messages.len(); + let new_count = messages.len(); + channel.messages = SumTree::new(); - channel - .messages - .extend(response.messages.into_iter().map(Into::into), &()); + channel.messages.extend(messages, &()); cx.emit(ChannelEvent::Message { old_range: 0..old_count, new_count, }); - }), - Err(error) => log::error!("error joining channel: {}", error), + }); + + Ok(()) } + .log_err() }) .detach(); } Self { details, + user_store, rpc, messages: Default::default(), pending_messages: Default::default(), @@ -210,11 +244,14 @@ impl Channel { local_id, body: body.clone(), }); + let user_store = self.user_store.clone(); let rpc = self.rpc.clone(); cx.spawn(|this, mut cx| { async move { let request = rpc.request(proto::SendChannelMessage { channel_id, body }); let response = request.await?; + let sender = user_store.get_user(current_user_id).await?; + this.update(&mut cx, |this, cx| { if let Ok(i) = this .pending_messages @@ -224,8 +261,8 @@ impl Channel { this.insert_message( ChannelMessage { id: response.message_id, - sender_id: current_user_id, body, + sender, }, cx, ); @@ -267,11 +304,21 @@ impl Channel { _: Arc, cx: &mut ModelContext, ) -> Result<()> { + let user_store = self.user_store.clone(); let message = message .payload .message .ok_or_else(|| anyhow!("empty message"))?; - self.insert_message(message.into(), cx); + + cx.spawn(|this, mut cx| { + async move { + let message = ChannelMessage::from_proto(message, &user_store).await?; + this.update(&mut cx, |this, cx| this.insert_message(message, cx)); + Ok(()) + } + .log_err() + }) + .detach(); Ok(()) } @@ -307,13 +354,17 @@ impl From for ChannelDetails { } } -impl From for ChannelMessage { - fn from(message: proto::ChannelMessage) -> Self { - ChannelMessage { +impl ChannelMessage { + pub async fn from_proto( + message: proto::ChannelMessage, + user_store: &UserStore, + ) -> Result { + let sender = user_store.get_user(message.sender_id).await?; + Ok(ChannelMessage { id: message.id, - sender_id: message.sender_id, body: message.body, - } + sender, + }) } } @@ -368,15 +419,16 @@ mod tests { let user_id = 5; let client = Client::new(); let mut server = FakeServer::for_client(user_id, &client, &cx).await; + let user_store = Arc::new(UserStore::new(client.clone())); - let channel_list = cx.add_model(|cx| ChannelList::new(client.clone(), cx)); + 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)); // Get the available channels. - let message = server.receive::().await; + let get_channels = server.receive::().await; server .respond( - message.receipt(), + get_channels.receipt(), proto::GetChannelsResponse { channels: vec![proto::Channel { id: 5, @@ -404,10 +456,10 @@ mod tests { }) .unwrap(); channel.read_with(&cx, |channel, _| assert!(channel.messages().is_empty())); - let message = server.receive::().await; + let join_channel = server.receive::().await; server .respond( - message.receipt(), + join_channel.receipt(), proto::JoinChannelResponse { messages: vec![ proto::ChannelMessage { @@ -420,12 +472,36 @@ mod tests { id: 11, body: "b".into(), timestamp: 1001, - sender_id: 5, + sender_id: 6, + }, + ], + }, + ) + .await; + // Client requests all users for the received messages + let mut get_users = server.receive::().await; + get_users.payload.user_ids.sort(); + assert_eq!(get_users.payload.user_ids, vec![5, 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(), }, ], }, ) .await; + assert_eq!( channel.next_event(&cx).await, ChannelEvent::Message { @@ -437,9 +513,12 @@ mod tests { assert_eq!( channel .messages_in_range(0..2) - .map(|message| &message.body) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) .collect::>(), - &["a", "b"] + &[ + ("nathansobo".into(), "a".into()), + ("maxbrunsfeld".into(), "b".into()) + ] ); }); @@ -451,10 +530,27 @@ mod tests { id: 12, body: "c".into(), timestamp: 1002, - sender_id: 5, + sender_id: 7, }), }) .await; + + // Client requests user for message since they haven't seen them yet + let get_users = server.receive::().await; + assert_eq!(get_users.payload.user_ids, vec![7]); + server + .respond( + get_users.receipt(), + proto::GetUsersResponse { + users: vec![proto::User { + id: 7, + github_login: "as-cii".into(), + avatar_url: "http://avatar.com/as-cii".into(), + }], + }, + ) + .await; + assert_eq!( channel.next_event(&cx).await, ChannelEvent::Message { @@ -466,9 +562,9 @@ mod tests { assert_eq!( channel .messages_in_range(2..3) - .map(|message| &message.body) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) .collect::>(), - &["c"] + &[("as-cii".into(), "c".into())] ) }) } diff --git a/zed/src/lib.rs b/zed/src/lib.rs index d674fd5ea662cd49eb5582bb00fb724a7c6987b5..d451c187d5cc982354d25ebf9575b4c7df7bbc1b 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -15,6 +15,7 @@ pub mod test; pub mod theme; pub mod theme_selector; mod time; +pub mod user; mod util; pub mod workspace; pub mod worktree; diff --git a/zed/src/main.rs b/zed/src/main.rs index 7eaa5ea83815fa4a9e077b0df6882d5c96054963..2258f746862e930a32b2a0871c6faa5a3b2b3d34 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -12,6 +12,7 @@ use zed::{ chat_panel, editor, file_finder, fs::RealFs, language, menus, rpc, settings, theme_selector, + user::UserStore, workspace::{self, OpenParams, OpenPaths}, AppState, }; @@ -29,12 +30,13 @@ fn main() { app.run(move |cx| { let rpc = rpc::Client::new(); + let user_store = Arc::new(UserStore::new(rpc.clone())); let app_state = Arc::new(AppState { languages: languages.clone(), settings_tx: Arc::new(Mutex::new(settings_tx)), settings, themes, - channel_list: cx.add_model(|cx| ChannelList::new(rpc.clone(), cx)), + channel_list: cx.add_model(|cx| ChannelList::new(user_store, rpc.clone(), cx)), rpc, fs: Arc::new(RealFs), }); diff --git a/zed/src/test.rs b/zed/src/test.rs index 6fac3c8bc9884cf45837d07b1fc9189480b3ccce..f406df1946e431cd3f8f2774db6cbcf550b3c4bb 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -5,6 +5,7 @@ use crate::{ rpc, settings::{self, ThemeRegistry}, time::ReplicaId, + user::UserStore, AppState, Settings, }; use gpui::{AppContext, Entity, ModelHandle, MutableAppContext}; @@ -164,12 +165,13 @@ pub fn build_app_state(cx: &mut MutableAppContext) -> Arc { let languages = Arc::new(LanguageRegistry::new()); let themes = ThemeRegistry::new(()); let rpc = rpc::Client::new(); + let user_store = Arc::new(UserStore::new(rpc.clone())); Arc::new(AppState { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, themes, languages: languages.clone(), - channel_list: cx.add_model(|cx| ChannelList::new(rpc.clone(), cx)), + channel_list: cx.add_model(|cx| ChannelList::new(user_store, rpc.clone(), cx)), rpc, fs: Arc::new(RealFs), }) diff --git a/zed/src/user.rs b/zed/src/user.rs new file mode 100644 index 0000000000000000000000000000000000000000..df98707a8ec907373613d2d6db266934a406149e --- /dev/null +++ b/zed/src/user.rs @@ -0,0 +1,59 @@ +use crate::rpc::Client; +use anyhow::{anyhow, Result}; +use parking_lot::Mutex; +use std::{collections::HashMap, sync::Arc}; +use zrpc::proto; + +pub use proto::User; + +pub struct UserStore { + users: Mutex>>, + rpc: Arc, +} + +impl UserStore { + pub fn new(rpc: Arc) -> Self { + Self { + users: Default::default(), + rpc, + } + } + + pub async fn load_users(&self, mut user_ids: Vec) -> Result<()> { + { + let users = self.users.lock(); + user_ids.retain(|id| !users.contains_key(id)); + } + + if !user_ids.is_empty() { + let response = self.rpc.request(proto::GetUsers { user_ids }).await?; + let mut users = self.users.lock(); + for user in response.users { + users.insert(user.id, Arc::new(user)); + } + } + + Ok(()) + } + + pub async fn get_user(&self, user_id: u64) -> Result> { + if let Some(user) = self.users.lock().get(&user_id).cloned() { + return Ok(user); + } + + let response = self + .rpc + .request(proto::GetUsers { + user_ids: vec![user_id], + }) + .await?; + + if let Some(user) = response.users.into_iter().next() { + let user = Arc::new(user); + self.users.lock().insert(user_id, user.clone()); + Ok(user) + } else { + Err(anyhow!("server responded with no users")) + } + } +} From b923f65a63c2536e66e82c90dd1aa65956fdfbc0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Aug 2021 15:22:14 -0700 Subject: [PATCH 100/204] WIP --- Cargo.lock | 10 ++++ zed/Cargo.toml | 1 + zed/assets/themes/_base.toml | 6 +++ zed/src/channel.rs | 10 +++- zed/src/chat_panel.rs | 93 ++++++++++++++++++++++++++++++------ zed/src/theme.rs | 10 ++-- zed/src/theme_selector.rs | 1 + zed/src/workspace.rs | 4 +- 8 files changed, 114 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec4cfc74fea0517d83c95d0cfece1753873976c5..9da817877e5cc07fb82a7edb56537fdfc14ff2d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5142,6 +5142,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "time" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0a10c9a9fb3a5dce8c2239ed670f1a2569fcf42da035f5face1b19860d52b0" +dependencies = [ + "libc", +] + [[package]] name = "time-macros" version = "0.1.1" @@ -5832,6 +5841,7 @@ dependencies = [ "smol", "surf", "tempdir", + "time 0.3.2", "tiny_http", "toml 0.5.8", "tree-sitter", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 13bc411e3bdda0df9743e3087c261da6983626d6..a9cdb7767578f17d6191d25ab58cb58666e3a6ed 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -48,6 +48,7 @@ smallvec = { version = "1.6", features = ["union"] } smol = "1.2.5" surf = "2.2" tempdir = { version = "0.3.7", optional = true } +time = { version = "0.3", features = ["local-offset"] } tiny_http = "0.8" toml = "0.5" tree-sitter = "0.19.5" diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 6fed76f81a90b8952fb3e111cce2489d033abe95..fbd31f4d977ba79c3c10905d46c7cca119edbd50 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -22,8 +22,14 @@ color = "$text.2" [workspace.active_sidebar_icon] color = "$text.0" +[chat_panel] +padding = { top = 10.0, bottom = 10.0, left = 10.0, right = 10.0 } + [chat_panel.message] body = "$text.0" +sender.margin.right = 10.0 +sender.text = { color = "#ff0000", weight = "bold", italic = true } +timestamp.text = "$text.2" [selector] background = "$surface.2" diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 73eb015884b110b5e55d740b6c12d50fd5a11302..37d75a4c912ff7c7dee85ec0ab09c08fd228227a 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -14,6 +14,7 @@ use std::{ ops::Range, sync::Arc, }; +use time::OffsetDateTime; use zrpc::{ proto::{self, ChannelMessageSent}, TypedEnvelope, @@ -47,6 +48,7 @@ pub struct Channel { pub struct ChannelMessage { pub id: u64, pub body: String, + pub timestamp: OffsetDateTime, pub sender: Arc, } @@ -261,14 +263,17 @@ impl Channel { this.insert_message( ChannelMessage { id: response.message_id, + timestamp: OffsetDateTime::from_unix_timestamp( + response.timestamp as i64, + )?, body, sender, }, cx, ); } - }); - Ok(()) + Ok(()) + }) } .log_err() }) @@ -363,6 +368,7 @@ impl ChannelMessage { Ok(ChannelMessage { id: message.id, body: message.body, + timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?, sender, }) } diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 0c4fbd584f44d530b6e29a1335b06bf820a0a418..582c5b436a0ecc37bfa32cfb989f750978438071 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -9,6 +9,7 @@ use gpui::{ Subscription, View, ViewContext, ViewHandle, }; use postage::watch; +use time::{OffsetDateTime, UtcOffset}; pub struct ChatPanel { channel_list: ModelHandle, @@ -75,12 +76,13 @@ impl ChatPanel { fn set_active_channel(&mut self, channel: ModelHandle, cx: &mut ViewContext) { if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) { let subscription = cx.subscribe(&channel, Self::channel_did_change); + let now = OffsetDateTime::now_utc(); self.messages = ListState::new( channel .read(cx) .messages() .cursor::<(), ()>() - .map(|m| self.render_message(m)) + .map(|m| self.render_message(m, now)) .collect(), Orientation::Bottom, ); @@ -99,12 +101,13 @@ impl ChatPanel { old_range, new_count, } => { + let now = OffsetDateTime::now_utc(); self.messages.splice( old_range.clone(), channel .read(cx) .messages_in_range(old_range.start..(old_range.start + new_count)) - .map(|message| self.render_message(message)), + .map(|message| self.render_message(message, now)), ); } } @@ -115,15 +118,50 @@ impl ChatPanel { Expanded::new(1., List::new(self.messages.clone()).boxed()).boxed() } - fn render_message(&self, message: &ChannelMessage) -> ElementBox { + fn render_message(&self, message: &ChannelMessage, now: OffsetDateTime) -> ElementBox { let settings = self.settings.borrow(); - Text::new( - message.body.clone(), - settings.ui_font_family, - settings.ui_font_size, - ) - .with_style(&settings.theme.chat_panel.message.body) - .boxed() + let theme = &settings.theme.chat_panel.message; + Flex::column() + .with_child( + Flex::row() + .with_child( + Container::new( + Label::new( + message.sender.github_login.clone(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_style(&theme.sender.label) + .boxed(), + ) + .with_style(&theme.sender.container) + .boxed(), + ) + .with_child( + Container::new( + Label::new( + format_timestamp(message.timestamp, now), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_style(&theme.timestamp.label) + .boxed(), + ) + .with_style(&theme.timestamp.container) + .boxed(), + ) + .boxed(), + ) + .with_child( + Text::new( + message.body.clone(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_style(&theme.body) + .boxed(), + ) + .boxed() } fn render_input_box(&self) -> ElementBox { @@ -157,9 +195,36 @@ impl View for ChatPanel { } fn render(&self, _: &RenderContext) -> ElementBox { - Flex::column() - .with_child(self.render_active_channel_messages()) - .with_child(self.render_input_box()) - .boxed() + let theme = &self.settings.borrow().theme; + Container::new( + Flex::column() + .with_child(self.render_active_channel_messages()) + .with_child(self.render_input_box()) + .boxed(), + ) + .with_style(&theme.chat_panel.container) + .boxed() + } +} + +fn format_timestamp(mut timestamp: OffsetDateTime, mut now: OffsetDateTime) -> String { + let local_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC); + timestamp = timestamp.to_offset(local_offset); + now = now.to_offset(local_offset); + + let today = now.date(); + let date = timestamp.date(); + let mut hour = timestamp.hour(); + let mut part = "am"; + if hour > 12 { + hour -= 12; + part = "pm"; + } + if date == today { + format!("{}:{}{}", hour, timestamp.minute(), part) + } else if date.next_day() == Some(today) { + format!("yesterday at {}:{}{}", hour, timestamp.minute(), part) + } else { + format!("{}/{}/{}", date.month(), date.day(), date.year()) } } diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 13fa53421144c30045fa5877be4daf78615278bb..3a8430c4be8aaad2ad1580aa1dec439afac97e4e 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -55,12 +55,16 @@ pub struct SidebarIcon { #[derive(Debug, Default, Deserialize)] pub struct ChatPanel { + #[serde(flatten)] + pub container: ContainerStyle, pub message: ChatMessage, } #[derive(Debug, Default, Deserialize)] pub struct ChatMessage { pub body: TextStyle, + pub sender: ContainedLabel, + pub timestamp: ContainedLabel, } #[derive(Debug, Default, Deserialize)] @@ -70,12 +74,12 @@ pub struct Selector { #[serde(flatten)] pub label: LabelStyle, - pub item: SelectorItem, - pub active_item: SelectorItem, + pub item: ContainedLabel, + pub active_item: ContainedLabel, } #[derive(Debug, Default, Deserialize)] -pub struct SelectorItem { +pub struct ContainedLabel { #[serde(flatten)] pub container: ContainerStyle, #[serde(flatten)] diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index eb062899c171505120996b39089f1c30569619e9..5dc3ae448a98ecdc9842539b869ec0675989ab25 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -99,6 +99,7 @@ impl ThemeSelector { Ok(theme) => { cx.notify_all(); action.0.settings_tx.lock().borrow_mut().theme = theme; + log::info!("reloaded theme {}", current_theme_name); } Err(error) => { log::error!("failed to load theme {}: {:?}", current_theme_name, error) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 4013b068a3bc62d7d70d0663b4ce616312878d2b..e037460891189121db906b518ae2476029cedfad 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -958,7 +958,7 @@ impl View for Workspace { if let Some(panel) = self.left_sidebar.active_item() { content.add_child( ConstrainedBox::new(ChildView::new(panel.id()).boxed()) - .with_width(200.0) + .with_width(300.0) .named("left panel"), ); } @@ -966,7 +966,7 @@ impl View for Workspace { if let Some(panel) = self.right_sidebar.active_item() { content.add_child( ConstrainedBox::new(ChildView::new(panel.id()).boxed()) - .with_width(200.0) + .with_width(300.0) .named("right panel"), ); } From d77211c6b13c53e8659137aa4a2db88f84a20a81 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Aug 2021 12:51:48 +0200 Subject: [PATCH 101/204] Extract a `TestDb` to setup and tear down a database instance --- server/src/db.rs | 66 ++++++++++++++++++++++++++++++++++++----------- server/src/rpc.rs | 46 +++++++-------------------------- 2 files changed, 61 insertions(+), 51 deletions(-) diff --git a/server/src/db.rs b/server/src/db.rs index 2ae8fc8f1d310c9d61c30f87d394ab9d8c5a495d..7c063e140f3f7b5d5553a73249639562ebaab209 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -68,21 +68,6 @@ impl Db { }) } - #[cfg(test)] - pub fn test(url: &str, max_connections: u32) -> Self { - let mut db = block_on(Self::new(url, max_connections)).unwrap(); - db.test_mode = true; - db - } - - #[cfg(test)] - pub fn migrate(&self, path: &std::path::Path) { - block_on(async { - let migrator = sqlx::migrate::Migrator::new(path).await.unwrap(); - migrator.run(&self.db).await.unwrap(); - }); - } - // signups pub async fn create_signup( @@ -457,3 +442,54 @@ id_type!(OrgId); id_type!(ChannelId); id_type!(SignupId); id_type!(MessageId); + +#[cfg(test)] +pub mod tests { + use super::*; + use rand::prelude::*; + use sqlx::{ + migrate::{MigrateDatabase, Migrator}, + Postgres, + }; + use std::path::Path; + + pub struct TestDb { + pub name: String, + pub url: String, + } + + impl TestDb { + pub fn new() -> (Self, Db) { + // Enable tests to run in parallel by serializing the creation of each test database. + lazy_static::lazy_static! { + static ref DB_CREATION: std::sync::Mutex<()> = std::sync::Mutex::new(()); + } + + let mut rng = StdRng::from_entropy(); + let name = format!("zed-test-{}", rng.gen::()); + let url = format!("postgres://postgres@localhost/{}", name); + let migrations_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations")); + let db = block_on(async { + { + let _lock = DB_CREATION.lock(); + Postgres::create_database(&url) + .await + .expect("failed to create test db"); + } + let mut db = Db::new(&url, 5).await.unwrap(); + db.test_mode = true; + let migrator = Migrator::new(migrations_path).await.unwrap(); + migrator.run(&db.db).await.unwrap(); + db + }); + + (Self { name, url }, db) + } + } + + impl Drop for TestDb { + fn drop(&mut self) { + block_on(Postgres::drop_database(&self.url)).unwrap(); + } + } +} diff --git a/server/src/rpc.rs b/server/src/rpc.rs index c869dd1aea385ccbcf608291a4534bf0dbd13c2c..0822ddbef1c42430b7d8f84937c566512e33d95c 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -919,18 +919,14 @@ mod tests { use super::*; use crate::{ auth, - db::{self, UserId}, + db::{tests::TestDb, Db, UserId}, github, AppState, Config, }; - use async_std::{ - sync::RwLockReadGuard, - task::{self, block_on}, - }; + use async_std::{sync::RwLockReadGuard, task}; use gpui::TestAppContext; use postage::mpsc; - use rand::prelude::*; use serde_json::json; - use sqlx::{migrate::MigrateDatabase, types::time::OffsetDateTime, Postgres}; + use sqlx::types::time::OffsetDateTime; use std::{path::Path, sync::Arc, time::Duration}; use zed::{ channel::{Channel, ChannelDetails, ChannelList}, @@ -1533,15 +1529,14 @@ mod tests { peer: Arc, app_state: Arc, server: Arc, - db_name: String, + test_db: TestDb, notifications: mpsc::Receiver<()>, } impl TestServer { async fn start() -> Self { - let mut rng = StdRng::from_entropy(); - let db_name = format!("zed-test-{}", rng.gen::()); - let app_state = Self::build_app_state(&db_name).await; + let (test_db, db) = TestDb::new(); + let app_state = Self::build_app_state(&test_db, db).await; let peer = Peer::new(); let notifications = mpsc::channel(128); let server = Server::new(app_state.clone(), peer.clone(), Some(notifications.0)); @@ -1549,7 +1544,7 @@ mod tests { peer, app_state, server, - db_name, + test_db, notifications: notifications.1, } } @@ -1575,18 +1570,10 @@ mod tests { (user_id, client) } - async fn build_app_state(db_name: &str) -> Arc { + async fn build_app_state(test_db: &TestDb, db: Db) -> Arc { let mut config = Config::default(); config.session_secret = "a".repeat(32); - config.database_url = format!("postgres://postgres@localhost/{}", db_name); - - Self::create_db(&config.database_url); - let db = db::Db::test(&config.database_url, 5); - db.migrate(Path::new(concat!( - env!("CARGO_MANIFEST_DIR"), - "/migrations" - ))); - + config.database_url = test_db.url.clone(); let github_client = github::AppClient::test(); Arc::new(AppState { db, @@ -1598,16 +1585,6 @@ mod tests { }) } - fn create_db(url: &str) { - // Enable tests to run in parallel by serializing the creation of each test database. - lazy_static::lazy_static! { - static ref DB_CREATION: std::sync::Mutex<()> = std::sync::Mutex::new(()); - } - - let _lock = DB_CREATION.lock(); - block_on(Postgres::create_database(url)).expect("failed to create test database"); - } - async fn state<'a>(&'a self) -> RwLockReadGuard<'a, ServerState> { self.server.state.read().await } @@ -1630,10 +1607,7 @@ mod tests { fn drop(&mut self) { task::block_on(async { self.peer.reset().await; - self.app_state.db.close(&self.db_name).await; - Postgres::drop_database(&self.app_state.config.database_url) - .await - .unwrap(); + self.app_state.db.close(&self.test_db.name).await; }); } } From c865f8ad1a26e1e8c25ddb723e49a37f6486f537 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Aug 2021 14:14:22 +0200 Subject: [PATCH 102/204] Fix retrieving recent channel messages --- server/src/db.rs | 126 ++++++++++++++++++++++++++++------------------ server/src/rpc.rs | 19 +++---- 2 files changed, 84 insertions(+), 61 deletions(-) diff --git a/server/src/db.rs b/server/src/db.rs index 7c063e140f3f7b5d5553a73249639562ebaab209..2f1cbc5fba181457d823aa1a5a98c7bc0cbafc67 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -21,8 +21,9 @@ macro_rules! test_support { }}; } +#[derive(Clone)] pub struct Db { - db: sqlx::PgPool, + pool: sqlx::PgPool, test_mode: bool, } @@ -57,13 +58,13 @@ pub struct ChannelMessage { impl Db { pub async fn new(url: &str, max_connections: u32) -> tide::Result { - let db = DbOptions::new() + let pool = DbOptions::new() .max_connections(max_connections) .connect(url) .await .context("failed to connect to postgres database")?; Ok(Self { - db, + pool, test_mode: false, }) } @@ -86,7 +87,7 @@ impl Db { .bind(github_login) .bind(email_address) .bind(about) - .fetch_one(&self.db) + .fetch_one(&self.pool) .await .map(SignupId) }) @@ -95,7 +96,7 @@ impl Db { pub async fn get_all_signups(&self) -> Result> { test_support!(self, { let query = "SELECT * FROM users ORDER BY github_login ASC"; - sqlx::query_as(query).fetch_all(&self.db).await + sqlx::query_as(query).fetch_all(&self.pool).await }) } @@ -104,7 +105,7 @@ impl Db { let query = "DELETE FROM signups WHERE id = $1"; sqlx::query(query) .bind(id.0) - .execute(&self.db) + .execute(&self.pool) .await .map(drop) }) @@ -122,7 +123,7 @@ impl Db { sqlx::query_scalar(query) .bind(github_login) .bind(admin) - .fetch_one(&self.db) + .fetch_one(&self.pool) .await .map(UserId) }) @@ -131,7 +132,7 @@ impl Db { pub async fn get_all_users(&self) -> Result> { test_support!(self, { let query = "SELECT * FROM users ORDER BY github_login ASC"; - sqlx::query_as(query).fetch_all(&self.db).await + sqlx::query_as(query).fetch_all(&self.pool).await }) } @@ -159,7 +160,7 @@ impl Db { sqlx::query_as(query) .bind(&ids.map(|id| id.0).collect::>()) .bind(requester_id) - .fetch_all(&self.db) + .fetch_all(&self.pool) .await }) } @@ -169,7 +170,7 @@ impl Db { let query = "SELECT * FROM users WHERE github_login = $1 LIMIT 1"; sqlx::query_as(query) .bind(github_login) - .fetch_optional(&self.db) + .fetch_optional(&self.pool) .await }) } @@ -180,7 +181,7 @@ impl Db { sqlx::query(query) .bind(is_admin) .bind(id.0) - .execute(&self.db) + .execute(&self.pool) .await .map(drop) }) @@ -191,7 +192,7 @@ impl Db { let query = "DELETE FROM users WHERE id = $1;"; sqlx::query(query) .bind(id.0) - .execute(&self.db) + .execute(&self.pool) .await .map(drop) }) @@ -212,7 +213,7 @@ impl Db { sqlx::query(query) .bind(user_id.0) .bind(access_token_hash) - .execute(&self.db) + .execute(&self.pool) .await .map(drop) }) @@ -223,7 +224,7 @@ impl Db { let query = "SELECT hash FROM access_tokens WHERE user_id = $1"; sqlx::query_scalar(query) .bind(user_id.0) - .fetch_all(&self.db) + .fetch_all(&self.pool) .await }) } @@ -241,7 +242,7 @@ impl Db { sqlx::query_scalar(query) .bind(name) .bind(slug) - .fetch_one(&self.db) + .fetch_one(&self.pool) .await .map(OrgId) }) @@ -263,7 +264,7 @@ impl Db { .bind(org_id.0) .bind(user_id.0) .bind(is_admin) - .execute(&self.db) + .execute(&self.pool) .await .map(drop) }) @@ -282,7 +283,7 @@ impl Db { sqlx::query_scalar(query) .bind(org_id.0) .bind(name) - .fetch_one(&self.db) + .fetch_one(&self.pool) .await .map(ChannelId) }) @@ -301,7 +302,7 @@ impl Db { "; sqlx::query_as(query) .bind(user_id.0) - .fetch_all(&self.db) + .fetch_all(&self.pool) .await }) } @@ -321,7 +322,7 @@ impl Db { sqlx::query_scalar::<_, i32>(query) .bind(user_id.0) .bind(channel_id.0) - .fetch_optional(&self.db) + .fetch_optional(&self.pool) .await .map(|e| e.is_some()) }) @@ -343,7 +344,7 @@ impl Db { .bind(channel_id.0) .bind(user_id.0) .bind(is_admin) - .execute(&self.db) + .execute(&self.pool) .await .map(drop) }) @@ -369,7 +370,7 @@ impl Db { .bind(sender_id.0) .bind(body) .bind(timestamp) - .fetch_one(&self.db) + .fetch_one(&self.pool) .await .map(MessageId) }) @@ -382,36 +383,23 @@ impl Db { ) -> Result> { test_support!(self, { let query = r#" - SELECT - id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at - FROM - channel_messages - WHERE - channel_id = $1 - LIMIT $2 + SELECT * FROM ( + SELECT + id, sender_id, body, sent_at AT TIME ZONE 'UTC' as sent_at + FROM + channel_messages + WHERE + channel_id = $1 + ORDER BY id DESC + LIMIT $2 + ) as recent_messages + ORDER BY id ASC "#; sqlx::query_as(query) .bind(channel_id.0) .bind(count as i64) - .fetch_all(&self.db) - .await - }) - } - - #[cfg(test)] - pub async fn close(&self, db_name: &str) { - test_support!(self, { - let query = " - SELECT pg_terminate_backend(pg_stat_activity.pid) - FROM pg_stat_activity - WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid(); - "; - sqlx::query(query) - .bind(db_name) - .execute(&self.db) + .fetch_all(&self.pool) .await - .unwrap(); - self.db.close().await; }) } } @@ -454,12 +442,13 @@ pub mod tests { use std::path::Path; pub struct TestDb { + pub db: Db, pub name: String, pub url: String, } impl TestDb { - pub fn new() -> (Self, Db) { + pub fn new() -> Self { // Enable tests to run in parallel by serializing the creation of each test database. lazy_static::lazy_static! { static ref DB_CREATION: std::sync::Mutex<()> = std::sync::Mutex::new(()); @@ -479,17 +468,54 @@ pub mod tests { let mut db = Db::new(&url, 5).await.unwrap(); db.test_mode = true; let migrator = Migrator::new(migrations_path).await.unwrap(); - migrator.run(&db.db).await.unwrap(); + migrator.run(&db.pool).await.unwrap(); db }); - (Self { name, url }, db) + Self { db, name, url } + } + + pub fn db(&self) -> &Db { + &self.db } } impl Drop for TestDb { fn drop(&mut self) { - block_on(Postgres::drop_database(&self.url)).unwrap(); + block_on(async { + let query = " + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid(); + "; + sqlx::query(query) + .bind(&self.name) + .execute(&self.db.pool) + .await + .unwrap(); + self.db.pool.close().await; + Postgres::drop_database(&self.url).await.unwrap(); + }); } } + + #[gpui::test] + async fn test_recent_channel_messages() { + 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(); + for i in 0..10 { + db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc()) + .await + .unwrap(); + } + + let messages = db.get_recent_channel_messages(channel, 5).await.unwrap(); + assert_eq!( + messages.iter().map(|m| &m.body).collect::>(), + ["5", "6", "7", "8", "9"] + ); + } } diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 0822ddbef1c42430b7d8f84937c566512e33d95c..1f329f5219071fd8daab38ee67b313f93d557557 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -919,7 +919,7 @@ mod tests { use super::*; use crate::{ auth, - db::{tests::TestDb, Db, UserId}, + db::{tests::TestDb, UserId}, github, AppState, Config, }; use async_std::{sync::RwLockReadGuard, task}; @@ -1529,14 +1529,14 @@ mod tests { peer: Arc, app_state: Arc, server: Arc, - test_db: TestDb, notifications: mpsc::Receiver<()>, + _test_db: TestDb, } impl TestServer { async fn start() -> Self { - let (test_db, db) = TestDb::new(); - let app_state = Self::build_app_state(&test_db, db).await; + let test_db = TestDb::new(); + let app_state = Self::build_app_state(&test_db).await; let peer = Peer::new(); let notifications = mpsc::channel(128); let server = Server::new(app_state.clone(), peer.clone(), Some(notifications.0)); @@ -1544,8 +1544,8 @@ mod tests { peer, app_state, server, - test_db, notifications: notifications.1, + _test_db: test_db, } } @@ -1570,13 +1570,13 @@ mod tests { (user_id, client) } - async fn build_app_state(test_db: &TestDb, db: Db) -> Arc { + async fn build_app_state(test_db: &TestDb) -> Arc { let mut config = Config::default(); config.session_secret = "a".repeat(32); config.database_url = test_db.url.clone(); let github_client = github::AppClient::test(); Arc::new(AppState { - db, + db: test_db.db().clone(), handlebars: Default::default(), auth_client: auth::build_client("", ""), repo_client: github::RepoClient::test(&github_client), @@ -1605,10 +1605,7 @@ mod tests { impl Drop for TestServer { fn drop(&mut self) { - task::block_on(async { - self.peer.reset().await; - self.app_state.db.close(&self.test_db.name).await; - }); + task::block_on(self.peer.reset()); } } From 5fe5685641d5e08911c6967ff0f60c6d5d3b94d5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Aug 2021 15:00:00 +0200 Subject: [PATCH 103/204] Replace `notify_all` with an explicit `refresh_windows` effect Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 60 ++++++++++++++++++++++++++++----------- gpui/src/presenter.rs | 17 ++++++++++- zed/src/theme_selector.rs | 4 +-- 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 5d1fdc3a49644613cb10011584f574a7f9f156dd..af5746043e63e114076bf14f5cb404ff88a1f2aa 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -22,6 +22,7 @@ use std::{ fmt::{self, Debug}, hash::{Hash, Hasher}, marker::PhantomData, + mem, ops::{Deref, DerefMut}, path::{Path, PathBuf}, rc::{self, Rc}, @@ -973,16 +974,6 @@ impl MutableAppContext { .push_back(Effect::ViewNotification { window_id, view_id }); } - pub(crate) fn notify_all_views(&mut self) { - let notifications = self - .views - .keys() - .copied() - .map(|(window_id, view_id)| Effect::ViewNotification { window_id, view_id }) - .collect::>(); - self.pending_effects.extend(notifications); - } - pub fn dispatch_action( &mut self, window_id: usize, @@ -1314,6 +1305,7 @@ impl MutableAppContext { if !self.flushing_effects && self.pending_flushes == 0 { self.flushing_effects = true; + let mut full_refresh = false; loop { if let Some(effect) = self.pending_effects.pop_front() { match effect { @@ -1327,15 +1319,24 @@ impl MutableAppContext { Effect::Focus { window_id, view_id } => { self.focus(window_id, view_id); } + Effect::RefreshWindows => { + full_refresh = true; + } } self.remove_dropped_entities(); } else { self.remove_dropped_entities(); - self.update_windows(); + if full_refresh { + self.perform_window_refresh(); + } else { + self.update_windows(); + } if self.pending_effects.is_empty() { self.flushing_effects = false; break; + } else { + full_refresh = false; } } } @@ -1366,6 +1367,28 @@ impl MutableAppContext { } } + pub fn refresh_windows(&mut self) { + self.pending_effects.push_back(Effect::RefreshWindows); + } + + fn perform_window_refresh(&mut self) { + let mut presenters = mem::take(&mut self.presenters_and_platform_windows); + for (window_id, (presenter, window)) in &mut presenters { + let invalidation = self + .cx + .windows + .get_mut(&window_id) + .unwrap() + .invalidation + .take(); + let mut presenter = presenter.borrow_mut(); + presenter.refresh(invalidation, self.as_ref()); + let scene = presenter.build_scene(window.size(), window.scale_factor(), self); + window.present_scene(scene); + } + self.presenters_and_platform_windows = presenters; + } + fn emit_event(&mut self, entity_id: usize, payload: Box) { let callbacks = self.subscriptions.lock().remove(&entity_id); if let Some(callbacks) = callbacks { @@ -1615,10 +1638,11 @@ impl AppContext { window_id: usize, view_id: usize, titlebar_height: f32, + refresh: bool, ) -> Result { self.views .get(&(window_id, view_id)) - .map(|v| v.render(window_id, view_id, titlebar_height, self)) + .map(|v| v.render(window_id, view_id, titlebar_height, refresh, self)) .ok_or(anyhow!("view not found")) } @@ -1633,7 +1657,7 @@ impl AppContext { if *win_id == window_id { Some(( *view_id, - view.render(*win_id, *view_id, titlebar_height, self), + view.render(*win_id, *view_id, titlebar_height, false, self), )) } else { None @@ -1730,6 +1754,7 @@ pub enum Effect { window_id: usize, view_id: usize, }, + RefreshWindows, } impl Debug for Effect { @@ -1753,6 +1778,7 @@ impl Debug for Effect { .field("window_id", window_id) .field("view_id", view_id) .finish(), + Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(), } } } @@ -1790,6 +1816,7 @@ pub trait AnyView { window_id: usize, view_id: usize, titlebar_height: f32, + refresh: bool, cx: &AppContext, ) -> ElementBox; fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); @@ -1822,6 +1849,7 @@ where window_id: usize, view_id: usize, titlebar_height: f32, + refresh: bool, cx: &AppContext, ) -> ElementBox { View::render( @@ -1832,6 +1860,7 @@ where app: cx, view_type: PhantomData::, titlebar_height, + refresh, }, ) } @@ -2182,10 +2211,6 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.notify_view(self.window_id, self.view_id); } - pub fn notify_all(&mut self) { - self.app.notify_all_views(); - } - pub fn propagate_action(&mut self) { self.halt_action_dispatch = false; } @@ -2204,6 +2229,7 @@ impl<'a, T: View> ViewContext<'a, T> { pub struct RenderContext<'a, T: View> { pub app: &'a AppContext, pub titlebar_height: f32, + pub refresh: bool, window_id: usize, view_id: usize, view_type: PhantomData, diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 80739a8be66ea0f2b502853e0be48c40d1c60de1..6e3487d76d3501e11ed31095b1f8b0b0ebacf8a6 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -66,12 +66,27 @@ impl Presenter { for view_id in invalidation.updated { self.rendered_views.insert( view_id, - cx.render_view(self.window_id, view_id, self.titlebar_height) + cx.render_view(self.window_id, view_id, self.titlebar_height, false) .unwrap(), ); } } + pub fn refresh(&mut self, invalidation: Option, cx: &AppContext) { + if let Some(invalidation) = invalidation { + for view_id in invalidation.removed { + self.rendered_views.remove(&view_id); + self.parents.remove(&view_id); + } + } + + for (view_id, view) in &mut self.rendered_views { + *view = cx + .render_view(self.window_id, *view_id, self.titlebar_height, true) + .unwrap(); + } + } + pub fn build_scene( &mut self, window_size: Vector2F, diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 5dc3ae448a98ecdc9842539b869ec0675989ab25..867c5b7693cffc0e2ee873c4ea215cc70aaec6e5 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -97,7 +97,7 @@ impl ThemeSelector { action.0.themes.clear(); match action.0.themes.get(¤t_theme_name) { Ok(theme) => { - cx.notify_all(); + cx.refresh_windows(); action.0.settings_tx.lock().borrow_mut().theme = theme; log::info!("reloaded theme {}", current_theme_name); } @@ -112,7 +112,7 @@ impl ThemeSelector { match self.registry.get(&mat.string) { Ok(theme) => { self.settings_tx.lock().borrow_mut().theme = theme; - cx.notify_all(); + cx.refresh_windows(); cx.emit(Event::Dismissed); } Err(error) => log::error!("error loading theme {}: {}", mat.string, error), From 4388c450299e97046e1a92d5bd835209419a8709 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Aug 2021 16:36:56 +0200 Subject: [PATCH 104/204] Re-render all list elements when refreshing windows Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 43 ++++++--- gpui/src/elements.rs | 69 ++++++++++---- gpui/src/elements/list.rs | 185 ++++++++++++++++++++++++++------------ gpui/src/lib.rs | 2 +- zed/src/channel.rs | 14 +-- zed/src/chat_panel.rs | 50 +++++------ 6 files changed, 248 insertions(+), 115 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index af5746043e63e114076bf14f5cb404ff88a1f2aa..1e531d3692ea41af3444e55e91d41c873cde568b 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -1305,7 +1305,7 @@ impl MutableAppContext { if !self.flushing_effects && self.pending_flushes == 0 { self.flushing_effects = true; - let mut full_refresh = false; + let mut refreshing = false; loop { if let Some(effect) = self.pending_effects.pop_front() { match effect { @@ -1320,13 +1320,13 @@ impl MutableAppContext { self.focus(window_id, view_id); } Effect::RefreshWindows => { - full_refresh = true; + refreshing = true; } } self.remove_dropped_entities(); } else { self.remove_dropped_entities(); - if full_refresh { + if refreshing { self.perform_window_refresh(); } else { self.update_windows(); @@ -1336,7 +1336,7 @@ impl MutableAppContext { self.flushing_effects = false; break; } else { - full_refresh = false; + refreshing = false; } } } @@ -1638,11 +1638,11 @@ impl AppContext { window_id: usize, view_id: usize, titlebar_height: f32, - refresh: bool, + refreshing: bool, ) -> Result { self.views .get(&(window_id, view_id)) - .map(|v| v.render(window_id, view_id, titlebar_height, refresh, self)) + .map(|v| v.render(window_id, view_id, titlebar_height, refreshing, self)) .ok_or(anyhow!("view not found")) } @@ -1666,6 +1666,23 @@ impl AppContext { .collect::>() } + pub fn render_cx( + &self, + window_id: usize, + view_id: usize, + titlebar_height: f32, + refreshing: bool, + ) -> RenderContext { + RenderContext { + app: self, + titlebar_height, + refreshing, + window_id, + view_id, + view_type: PhantomData, + } + } + pub fn background(&self) -> &Arc { &self.background } @@ -1816,7 +1833,7 @@ pub trait AnyView { window_id: usize, view_id: usize, titlebar_height: f32, - refresh: bool, + refreshing: bool, cx: &AppContext, ) -> ElementBox; fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); @@ -1849,7 +1866,7 @@ where window_id: usize, view_id: usize, titlebar_height: f32, - refresh: bool, + refreshing: bool, cx: &AppContext, ) -> ElementBox { View::render( @@ -1860,7 +1877,7 @@ where app: cx, view_type: PhantomData::, titlebar_height, - refresh, + refreshing, }, ) } @@ -2229,7 +2246,7 @@ impl<'a, T: View> ViewContext<'a, T> { pub struct RenderContext<'a, T: View> { pub app: &'a AppContext, pub titlebar_height: f32, - pub refresh: bool, + pub refreshing: bool, window_id: usize, view_id: usize, view_type: PhantomData, @@ -2255,6 +2272,12 @@ impl Deref for RenderContext<'_, V> { } } +impl ReadModel for RenderContext<'_, V> { + fn read_model(&self, handle: &ModelHandle) -> &T { + self.app.read_model(handle) + } +} + impl AsRef for ViewContext<'_, M> { fn as_ref(&self) -> &AppContext { &self.app.cx diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index d7d042617fe7f8a74b09bc1d28a98aef8fb7a19c..c533b938a3cc170ae02ba536b964c0e1bcff0306 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -37,7 +37,14 @@ use crate::{ }; use core::panic; use json::ToJson; -use std::{any::Any, borrow::Cow, mem}; +use std::{ + any::Any, + borrow::Cow, + cell::RefCell, + mem, + ops::{Deref, DerefMut}, + rc::Rc, +}; trait AnyElement { fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F; @@ -91,20 +98,20 @@ pub trait Element { where Self: 'static + Sized, { - ElementBox { + ElementBox(ElementRc { name: None, - element: Box::new(Lifecycle::Init { element: self }), - } + element: Rc::new(RefCell::new(Lifecycle::Init { element: self })), + }) } fn named(self, name: impl Into>) -> ElementBox where Self: 'static + Sized, { - ElementBox { + ElementBox(ElementRc { name: Some(name.into()), - element: Box::new(Lifecycle::Init { element: self }), - } + element: Rc::new(RefCell::new(Lifecycle::Init { element: self })), + }) } } @@ -127,9 +134,12 @@ pub enum Lifecycle { paint: T::PaintState, }, } -pub struct ElementBox { +pub struct ElementBox(ElementRc); + +#[derive(Clone)] +pub struct ElementRc { name: Option>, - element: Box, + element: Rc>, } impl AnyElement for Lifecycle { @@ -262,28 +272,51 @@ impl Default for Lifecycle { } impl ElementBox { + pub fn metadata(&self) -> Option<&dyn Any> { + let element = unsafe { &*self.0.element.as_ptr() }; + element.metadata() + } +} + +impl Into for ElementBox { + fn into(self) -> ElementRc { + self.0 + } +} + +impl Deref for ElementBox { + type Target = ElementRc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ElementBox { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl ElementRc { pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F { - self.element.layout(constraint, cx) + self.element.borrow_mut().layout(constraint, cx) } pub fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) { - self.element.paint(origin, cx); + self.element.borrow_mut().paint(origin, cx); } pub fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool { - self.element.dispatch_event(event, cx) + self.element.borrow_mut().dispatch_event(event, cx) } pub fn size(&self) -> Vector2F { - self.element.size() - } - - pub fn metadata(&self) -> Option<&dyn Any> { - self.element.metadata() + self.element.borrow().size() } pub fn debug(&self, cx: &DebugContext) -> json::Value { - let mut value = self.element.debug(cx); + let mut value = self.element.borrow().debug(cx); if let Some(name) = &self.name { if let json::Value::Object(map) = &mut value { diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index b4213fc5a50c825c9ae353dbb6fdef460f4fb170..98ab96c427b9c6741c833dec81e6499886e652d8 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -5,19 +5,17 @@ use crate::{ }, json::json, sum_tree::{self, Bias, SumTree}, - DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, + DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext, + RenderContext, SizeConstraint, View, }; -use parking_lot::Mutex; -use std::{ops::Range, sync::Arc}; - -use crate::ElementBox; +use std::{cell::RefCell, ops::Range, rc::Rc}; pub struct List { state: ListState, } #[derive(Clone)] -pub struct ListState(Arc>); +pub struct ListState(Rc>); #[derive(Eq, PartialEq)] pub enum Orientation { @@ -27,7 +25,7 @@ pub enum Orientation { struct StateInner { last_layout_width: f32, - elements: Vec, + elements: Vec>, heights: SumTree, scroll_position: f32, orientation: Orientation, @@ -56,13 +54,55 @@ struct PendingCount(usize); struct Height(f32); impl List { - pub fn new(state: ListState) -> Self { + pub fn new(state: ListState, cx: &RenderContext, build_items: F) -> Self + where + F: Fn(Range) -> I, + I: IntoIterator, + V: View, + { + { + let state = &mut *state.0.borrow_mut(); + if cx.refreshing { + let elements = (build_items)(0..state.elements.len()); + state.elements.clear(); + state + .elements + .extend(elements.into_iter().map(|e| Some(e.into()))); + state.heights = SumTree::new(); + state.heights.extend( + (0..state.elements.len()).map(|_| ElementHeight::Pending), + &(), + ); + } else { + let mut cursor = state.heights.cursor::(); + cursor.seek(&PendingCount(1), sum_tree::Bias::Left, &()); + + while cursor.item().is_some() { + let start_ix = cursor.sum_start().0; + while cursor.item().map_or(false, |h| h.is_pending()) { + cursor.next(&()); + } + let end_ix = cursor.sum_start().0; + if end_ix > start_ix { + state.elements.splice( + start_ix..end_ix, + (build_items)(start_ix..end_ix) + .into_iter() + .map(|e| Some(e.into())), + ); + } + + cursor.seek(&PendingCount(cursor.seek_start().0 + 1), Bias::Left, &()); + } + } + } + Self { state } } } impl Element for List { - type LayoutState = (); + type LayoutState = Vec; type PaintState = (); @@ -71,7 +111,7 @@ impl Element for List { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let state = &mut *self.state.0.lock(); + let state = &mut *self.state.0.borrow_mut(); let mut item_constraint = constraint; item_constraint.min.set_y(0.); item_constraint.max.set_y(f32::INFINITY); @@ -87,8 +127,8 @@ impl Element for List { while let Some(height) = old_heights.item() { if height.is_pending() { - let size = - state.elements[old_heights.sum_start().count].layout(item_constraint, cx); + let element = &mut state.elements[old_heights.sum_start().count]; + let size = element.as_mut().unwrap().layout(item_constraint, cx); new_heights.push(ElementHeight::Ready(size.y()), &()); // Adjust scroll position to keep visible elements stable @@ -123,18 +163,28 @@ impl Element for List { } else { state.heights = SumTree::new(); for element in &mut state.elements { + let element = element.as_mut().unwrap(); let size = element.layout(item_constraint, cx); state.heights.push(ElementHeight::Ready(size.y()), &()); } state.last_layout_width = constraint.max.x(); } - (size, ()) + let visible_elements = state.elements[state.visible_range(size.y())] + .iter() + .map(|e| e.clone().unwrap()) + .collect(); + (size, visible_elements) } - fn paint(&mut self, bounds: RectF, _: &mut (), cx: &mut PaintContext) { + fn paint( + &mut self, + bounds: RectF, + visible_elements: &mut Self::LayoutState, + cx: &mut PaintContext, + ) { cx.scene.push_layer(Some(bounds)); - let state = &mut *self.state.0.lock(); + let state = &mut *self.state.0.borrow_mut(); let visible_range = state.visible_range(bounds.height()); let mut item_top = { @@ -149,7 +199,7 @@ impl Element for List { } let scroll_top = state.scroll_top(bounds.height()); - for element in &mut state.elements[visible_range] { + for element in visible_elements { let origin = bounds.origin() + vec2f(0., item_top - scroll_top); element.paint(origin, cx); item_top += element.size().y(); @@ -161,16 +211,15 @@ impl Element for List { &mut self, event: &Event, bounds: RectF, - _: &mut (), + visible_elements: &mut Self::LayoutState, _: &mut (), cx: &mut EventContext, ) -> bool { let mut handled = false; - let mut state = self.state.0.lock(); - let visible_range = state.visible_range(bounds.height()); - for item in &mut state.elements[visible_range] { - handled = item.dispatch_event(event, cx) || handled; + let mut state = self.state.0.borrow_mut(); + for element in visible_elements { + handled = element.dispatch_event(event, cx) || handled; } match event { @@ -191,10 +240,16 @@ impl Element for List { handled } - fn debug(&self, bounds: RectF, _: &(), _: &(), cx: &DebugContext) -> serde_json::Value { - let state = self.state.0.lock(); + fn debug( + &self, + bounds: RectF, + visible_elements: &Self::LayoutState, + _: &(), + cx: &DebugContext, + ) -> serde_json::Value { + let state = self.state.0.borrow_mut(); let visible_range = state.visible_range(bounds.height()); - let visible_elements = state.elements[visible_range.clone()] + let visible_elements = visible_elements .iter() .map(|e| e.debug(cx)) .collect::>(); @@ -207,40 +262,29 @@ impl Element for List { } impl ListState { - pub fn new(elements: Vec, orientation: Orientation) -> Self { + pub fn new(element_count: usize, orientation: Orientation) -> Self { let mut heights = SumTree::new(); - heights.extend(elements.iter().map(|_| ElementHeight::Pending), &()); - Self(Arc::new(Mutex::new(StateInner { + heights.extend((0..element_count).map(|_| ElementHeight::Pending), &()); + Self(Rc::new(RefCell::new(StateInner { last_layout_width: 0., - elements, + elements: (0..element_count).map(|_| None).collect(), heights, scroll_position: 0., orientation, }))) } - pub fn splice( - &self, - old_range: Range, - new_elements: impl IntoIterator, - ) { - let state = &mut *self.0.lock(); + pub fn splice(&self, old_range: Range, count: usize) { + let state = &mut *self.0.borrow_mut(); let mut old_heights = state.heights.cursor::(); let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &()); old_heights.seek_forward(&Count(old_range.end), Bias::Right, &()); - let mut len = 0; - let old_elements = state.elements.splice( - old_range, - new_elements.into_iter().map(|e| { - len += 1; - e - }), - ); + let old_elements = state.elements.splice(old_range, (0..count).map(|_| None)); drop(old_elements); - new_heights.extend((0..len).map(|_| ElementHeight::Pending), &()); + new_heights.extend((0..count).map(|_| ElementHeight::Pending), &()); new_heights.push_tree(old_heights.suffix(&()), &()); drop(old_heights); state.heights = new_heights; @@ -370,22 +414,28 @@ impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Height { #[cfg(test)] mod tests { use super::*; - use crate::{elements::*, geometry::vector::vec2f}; + use crate::{elements::*, geometry::vector::vec2f, Entity}; #[crate::test(self)] fn test_layout(cx: &mut crate::MutableAppContext) { - let mut presenter = cx.build_presenter(0, 20.0); - let mut layout_cx = presenter.layout_cx(cx); - let state = ListState::new(vec![item(20.), item(30.), item(10.)], Orientation::Top); - let mut list = List::new(state.clone()).boxed(); + let mut presenter = cx.build_presenter(0, 0.); + + let mut elements = vec![20., 30., 10.]; + let state = ListState::new(elements.len(), Orientation::Top); + let mut list = List::new( + state.clone(), + &cx.render_cx::(0, 0, 0., false), + |range| elements[range].iter().copied().map(item), + ) + .boxed(); let size = list.layout( SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)), - &mut layout_cx, + &mut presenter.layout_cx(cx), ); assert_eq!(size, vec2f(100., 40.)); assert_eq!( - state.0.lock().heights.summary(), + state.0.borrow().heights.summary(), ElementHeightSummary { count: 3, pending_count: 0, @@ -393,23 +443,32 @@ mod tests { } ); - state.splice(1..2, vec![item(40.), item(50.)]); - state.splice(3..3, vec![item(60.)]); + elements.splice(1..2, vec![40., 50.]); + elements.push(60.); + state.splice(1..2, 2); + state.splice(4..4, 1); assert_eq!( - state.0.lock().heights.summary(), + state.0.borrow().heights.summary(), ElementHeightSummary { count: 5, pending_count: 3, height: 30. } ); + + let mut list = List::new( + state.clone(), + &cx.render_cx::(0, 0, 0., false), + |range| elements[range].iter().copied().map(item), + ) + .boxed(); let size = list.layout( SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)), - &mut layout_cx, + &mut presenter.layout_cx(cx), ); assert_eq!(size, vec2f(100., 40.)); assert_eq!( - state.0.lock().heights.summary(), + state.0.borrow().heights.summary(), ElementHeightSummary { count: 5, pending_count: 0, @@ -424,4 +483,20 @@ mod tests { .with_width(100.) .boxed() } + + struct TestView; + + impl Entity for TestView { + type Event = (); + } + + impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&self, _: &RenderContext<'_, Self>) -> ElementBox { + unimplemented!() + } + } } diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index e7cfa3b1776ab416be5df8fed36f98aac36080bf..10877e33a00542b248a8ef84ae4b50f016005bf4 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -18,7 +18,7 @@ pub use scene::{Border, Quad, Scene}; pub mod text_layout; pub use text_layout::TextLayoutCache; mod util; -pub use elements::{Element, ElementBox}; +pub use elements::{Element, ElementBox, ElementRc}; pub mod executor; pub use executor::Task; pub mod color; diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 37d75a4c912ff7c7dee85ec0ab09c08fd228227a..02352ec00d0d0b03fcbd874ee8c538fcc847f4b5 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -60,7 +60,7 @@ pub struct PendingChannelMessage { #[derive(Clone, Debug, Default)] pub struct ChannelMessageSummary { max_id: u64, - count: Count, + count: usize, } #[derive(Copy, Clone, Debug, Default)] @@ -208,7 +208,7 @@ impl Channel { } channel.update(&mut cx, |channel, cx| { - let old_count = channel.messages.summary().count.0; + let old_count = channel.messages.summary().count; let new_count = messages.len(); channel.messages = SumTree::new(); @@ -282,6 +282,10 @@ impl Channel { Ok(()) } + pub fn message_count(&self) -> usize { + self.messages.summary().count + } + pub fn messages(&self) -> &SumTree { &self.messages } @@ -380,7 +384,7 @@ impl sum_tree::Item for ChannelMessage { fn summary(&self) -> Self::Summary { ChannelMessageSummary { max_id: self.id, - count: Count(1), + count: 1, } } } @@ -390,7 +394,7 @@ impl sum_tree::Summary for ChannelMessageSummary { fn add_summary(&mut self, summary: &Self, _: &()) { self.max_id = summary.max_id; - self.count.0 += summary.count.0; + self.count += summary.count; } } @@ -403,7 +407,7 @@ impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for u64 { impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count { fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { - self.0 += summary.count.0; + self.0 += summary.count; } } diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 582c5b436a0ecc37bfa32cfb989f750978438071..0573666f9dc9b90579c34682575b989b97b5afbc 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -14,7 +14,7 @@ use time::{OffsetDateTime, UtcOffset}; pub struct ChatPanel { channel_list: ModelHandle, active_channel: Option<(ModelHandle, Subscription)>, - messages: ListState, + message_list: ListState, input_editor: ViewHandle, settings: watch::Receiver, } @@ -38,8 +38,8 @@ impl ChatPanel { let input_editor = cx.add_view(|cx| Editor::auto_height(settings.clone(), cx)); let mut this = Self { channel_list, - active_channel: None, - messages: ListState::new(Vec::new(), Orientation::Bottom), + active_channel: Default::default(), + message_list: ListState::new(0, Orientation::Bottom), input_editor, settings, }; @@ -76,23 +76,15 @@ impl ChatPanel { fn set_active_channel(&mut self, channel: ModelHandle, cx: &mut ViewContext) { if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) { let subscription = cx.subscribe(&channel, Self::channel_did_change); - let now = OffsetDateTime::now_utc(); - self.messages = ListState::new( - channel - .read(cx) - .messages() - .cursor::<(), ()>() - .map(|m| self.render_message(m, now)) - .collect(), - Orientation::Bottom, - ); + self.message_list = + ListState::new(channel.read(cx).message_count(), Orientation::Bottom); self.active_channel = Some((channel, subscription)); } } fn channel_did_change( &mut self, - channel: ModelHandle, + _: ModelHandle, event: &ChannelEvent, cx: &mut ViewContext, ) { @@ -101,21 +93,27 @@ impl ChatPanel { old_range, new_count, } => { - let now = OffsetDateTime::now_utc(); - self.messages.splice( - old_range.clone(), - channel - .read(cx) - .messages_in_range(old_range.start..(old_range.start + new_count)) - .map(|message| self.render_message(message, now)), - ); + self.message_list.splice(old_range.clone(), *new_count); } } cx.notify(); } - fn render_active_channel_messages(&self) -> ElementBox { - Expanded::new(1., List::new(self.messages.clone()).boxed()).boxed() + fn render_active_channel_messages(&self, cx: &RenderContext) -> ElementBox { + let messages = if let Some((channel, _)) = self.active_channel.as_ref() { + let channel = channel.read(cx); + let now = OffsetDateTime::now_utc(); + List::new(self.message_list.clone(), cx, |range| { + channel + .messages_in_range(range) + .map(|message| self.render_message(message, now)) + }) + .boxed() + } else { + Empty::new().boxed() + }; + + Expanded::new(1., messages).boxed() } fn render_message(&self, message: &ChannelMessage, now: OffsetDateTime) -> ElementBox { @@ -194,11 +192,11 @@ impl View for ChatPanel { "ChatPanel" } - fn render(&self, _: &RenderContext) -> ElementBox { + fn render(&self, cx: &RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; Container::new( Flex::column() - .with_child(self.render_active_channel_messages()) + .with_child(self.render_active_channel_messages(cx)) .with_child(self.render_input_box()) .boxed(), ) From 381f2499b27d709797b7751e62ebccbb8607456d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 26 Aug 2021 16:46:28 +0200 Subject: [PATCH 105/204] Preserve scroll position of `List` when refreshing windows Co-Authored-By: Nathan Sobo --- gpui/src/elements/list.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 98ab96c427b9c6741c833dec81e6499886e652d8..6e015b721c1bf2e53adb68f70342f674c3123c0f 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -24,7 +24,7 @@ pub enum Orientation { } struct StateInner { - last_layout_width: f32, + last_layout_width: Option, elements: Vec>, heights: SumTree, scroll_position: f32, @@ -64,6 +64,7 @@ impl List { let state = &mut *state.0.borrow_mut(); if cx.refreshing { let elements = (build_items)(0..state.elements.len()); + state.last_layout_width = None; state.elements.clear(); state .elements @@ -121,26 +122,26 @@ impl Element for List { let visible_top = state.scroll_top(size.y()); let visible_bottom = visible_top + size.y(); - if state.last_layout_width == constraint.max.x() { + if state.last_layout_width == Some(constraint.max.x()) { let mut old_heights = state.heights.cursor::(); let mut new_heights = old_heights.slice(&PendingCount(1), sum_tree::Bias::Left, &()); while let Some(height) = old_heights.item() { if height.is_pending() { let element = &mut state.elements[old_heights.sum_start().count]; - let size = element.as_mut().unwrap().layout(item_constraint, cx); - new_heights.push(ElementHeight::Ready(size.y()), &()); + let element_size = element.as_mut().unwrap().layout(item_constraint, cx); + new_heights.push(ElementHeight::Ready(element_size.y()), &()); // Adjust scroll position to keep visible elements stable match state.orientation { Orientation::Top => { if new_heights.summary().height < visible_top { - state.scroll_position += size.y(); + state.scroll_position += element_size.y(); } } Orientation::Bottom => { - if new_heights.summary().height - size.y() > visible_bottom { - state.scroll_position += size.y(); + if new_heights.summary().height - element_size.y() > visible_bottom { + state.scroll_position += element_size.y(); } } } @@ -167,7 +168,7 @@ impl Element for List { let size = element.layout(item_constraint, cx); state.heights.push(ElementHeight::Ready(size.y()), &()); } - state.last_layout_width = constraint.max.x(); + state.last_layout_width = Some(constraint.max.x()); } let visible_elements = state.elements[state.visible_range(size.y())] @@ -266,7 +267,7 @@ impl ListState { let mut heights = SumTree::new(); heights.extend((0..element_count).map(|_| ElementHeight::Pending), &()); Self(Rc::new(RefCell::new(StateInner { - last_layout_width: 0., + last_layout_width: None, elements: (0..element_count).map(|_| None).collect(), heights, scroll_position: 0., From ee9ee294adf4ccce20b8212f3e810ec7169f1b5c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 Aug 2021 09:17:43 -0600 Subject: [PATCH 106/204] Improve sender styling Co-Authored-By: Antonio Scandurra --- 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 fbd31f4d977ba79c3c10905d46c7cca119edbd50..92d3793bb088fbc534373b495f5d04376c6a3b48 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -28,7 +28,7 @@ padding = { top = 10.0, bottom = 10.0, left = 10.0, right = 10.0 } [chat_panel.message] body = "$text.0" sender.margin.right = 10.0 -sender.text = { color = "#ff0000", weight = "bold", italic = true } +sender.text = { color = "$text.0", weight = "bold" } timestamp.text = "$text.2" [selector] From 3bb5610ad1f50455ef5bb69e68f1723f6617e30e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Aug 2021 15:06:00 -0700 Subject: [PATCH 107/204] Overhaul handling of font families * Specify font families in the theme. * Load fonts eagerly when loading themes, instead of loading them lazily when rendering. Co-Authored-By: Antonio Scandurra Co-Authored-By: Nathan Sobo --- gpui/src/elements/label.rs | 151 ++++++++++------------ gpui/src/elements/text.rs | 34 ++--- gpui/src/fonts.rs | 169 ++++++++++++++++++------- server/src/rpc.rs | 2 +- zed/assets/themes/_base.toml | 22 ++-- zed/assets/themes/dark.toml | 7 +- zed/assets/themes/light.toml | 7 +- zed/src/chat_panel.rs | 18 +-- zed/src/editor.rs | 57 ++++----- zed/src/editor/display_map.rs | 51 +++++--- zed/src/editor/display_map/wrap_map.rs | 2 +- zed/src/editor/movement.rs | 4 +- zed/src/file_finder.rs | 40 ++---- zed/src/language.rs | 6 +- zed/src/main.rs | 7 +- zed/src/settings.rs | 35 +++-- zed/src/test.rs | 15 +-- zed/src/theme.rs | 107 ++++++++-------- zed/src/theme/highlight_map.rs | 15 +-- zed/src/theme/theme_registry.rs | 24 ++-- zed/src/theme_selector.rs | 20 +-- zed/src/workspace.rs | 16 +-- zed/src/workspace/pane.rs | 12 +- 23 files changed, 431 insertions(+), 390 deletions(-) diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index 5c0e7e247f8f6384efbdd3e51e7a080ffbdcf9b1..1ad5d83671cdf04e91af9dc0f3efef95301701d1 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -1,6 +1,5 @@ use crate::{ color::Color, - font_cache::FamilyId, fonts::{FontId, TextStyle}, geometry::{ rect::RectF, @@ -8,8 +7,7 @@ use crate::{ }, json::{ToJson, Value}, text_layout::Line, - DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext, - SizeConstraint, + DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use serde::Deserialize; use serde_json::json; @@ -17,49 +15,41 @@ use smallvec::{smallvec, SmallVec}; pub struct Label { text: String, - family_id: FamilyId, - font_size: f32, style: LabelStyle, highlight_indices: Vec, } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct LabelStyle { pub text: TextStyle, pub highlight_text: Option, } +impl From for LabelStyle { + fn from(text: TextStyle) -> Self { + LabelStyle { + text, + highlight_text: None, + } + } +} + impl Label { - pub fn new(text: String, family_id: FamilyId, font_size: f32) -> Self { + pub fn new(text: String, style: impl Into) -> Self { Self { text, - family_id, - font_size, highlight_indices: Default::default(), - style: Default::default(), + style: style.into(), } } - pub fn with_style(mut self, style: &LabelStyle) -> Self { - self.style = style.clone(); - self - } - - pub fn with_default_color(mut self, color: Color) -> Self { - self.style.text.color = color; - self - } - pub fn with_highlights(mut self, indices: Vec) -> Self { self.highlight_indices = indices; self } - fn compute_runs( - &self, - font_cache: &FontCache, - font_id: FontId, - ) -> SmallVec<[(usize, FontId, Color); 8]> { + fn compute_runs(&self) -> SmallVec<[(usize, FontId, Color); 8]> { + let font_id = self.style.text.font_id; if self.highlight_indices.is_empty() { return smallvec![(self.text.len(), font_id, self.style.text.color)]; } @@ -68,12 +58,7 @@ impl Label { .style .highlight_text .as_ref() - .and_then(|style| { - font_cache - .select_font(self.family_id, &style.font_properties) - .ok() - }) - .unwrap_or(font_id); + .map_or(font_id, |style| style.font_id); let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); let mut runs = SmallVec::new(); @@ -123,18 +108,18 @@ impl Element for Label { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let font_id = cx - .font_cache - .select_font(self.family_id, &self.style.text.font_properties) - .unwrap(); - let runs = self.compute_runs(&cx.font_cache, font_id); - let line = - cx.text_layout_cache - .layout_str(self.text.as_str(), self.font_size, runs.as_slice()); + let runs = self.compute_runs(); + let line = cx.text_layout_cache.layout_str( + self.text.as_str(), + self.style.text.font_size, + runs.as_slice(), + ); let size = vec2f( line.width().max(constraint.min.x()).min(constraint.max.x()), - cx.font_cache.line_height(font_id, self.font_size).ceil(), + cx.font_cache + .line_height(self.style.text.font_id, self.style.text.font_size) + .ceil(), ); (size, line) @@ -169,15 +154,13 @@ impl Element for Label { bounds: RectF, _: &Self::LayoutState, _: &Self::PaintState, - cx: &DebugContext, + _: &DebugContext, ) -> Value { json!({ "type": "Label", "bounds": bounds.to_json(), "text": &self.text, "highlight_indices": self.highlight_indices, - "font_family": cx.font_cache.family_name(self.family_id).unwrap(), - "font_size": self.font_size, "style": self.style.to_json(), }) } @@ -201,48 +184,52 @@ mod tests { #[crate::test(self)] fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) { - let menlo = cx.font_cache().load_family(&["Menlo"]).unwrap(); - let menlo_regular = cx - .font_cache() - .select_font(menlo, &FontProperties::new()) - .unwrap(); - let menlo_bold = cx - .font_cache() - .select_font(menlo, FontProperties::new().weight(Weight::BOLD)) - .unwrap(); - let black = Color::black(); - let red = Color::new(255, 0, 0, 255); - - let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0) - .with_style(&LabelStyle { - text: TextStyle { - color: black, - font_properties: Default::default(), - }, - highlight_text: Some(TextStyle { - color: red, - font_properties: *FontProperties::new().weight(Weight::BOLD), - }), - }) - .with_highlights(vec![ - ".α".len(), - ".αβ".len(), - ".αβγδ".len(), - ".αβγδε.ⓐ".len(), - ".αβγδε.ⓐⓑ".len(), - ]); - - let runs = label.compute_runs(cx.font_cache().as_ref(), menlo_regular); + let default_style = TextStyle::new( + "Menlo", + 12., + Default::default(), + Color::black(), + cx.font_cache(), + ) + .unwrap(); + let highlight_style = TextStyle::new( + "Menlo", + 12., + *FontProperties::new().weight(Weight::BOLD), + Color::new(255, 0, 0, 255), + cx.font_cache(), + ) + .unwrap(); + let label = Label::new( + ".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), + LabelStyle { + text: default_style.clone(), + highlight_text: Some(highlight_style.clone()), + }, + ) + .with_highlights(vec![ + ".α".len(), + ".αβ".len(), + ".αβγδ".len(), + ".αβγδε.ⓐ".len(), + ".αβγδε.ⓐⓑ".len(), + ]); + + let runs = label.compute_runs(); assert_eq!( runs.as_slice(), &[ - (".α".len(), menlo_regular, black), - ("βγ".len(), menlo_bold, red), - ("δ".len(), menlo_regular, black), - ("ε".len(), menlo_bold, red), - (".ⓐ".len(), menlo_regular, black), - ("ⓑⓒ".len(), menlo_bold, red), - ("ⓓⓔ.abcde.".len(), menlo_regular, black), + (".α".len(), default_style.font_id, default_style.color), + ("βγ".len(), highlight_style.font_id, highlight_style.color), + ("δ".len(), default_style.font_id, default_style.color), + ("ε".len(), highlight_style.font_id, highlight_style.color), + (".ⓐ".len(), default_style.font_id, default_style.color), + ("ⓑⓒ".len(), highlight_style.font_id, highlight_style.color), + ( + "ⓓⓔ.abcde.".len(), + default_style.font_id, + default_style.color + ), ] ); } diff --git a/gpui/src/elements/text.rs b/gpui/src/elements/text.rs index 0459a6882944ecfe8682b9debec847647c5dd52c..6978542071aaf89501fb7899c50db42953581c50 100644 --- a/gpui/src/elements/text.rs +++ b/gpui/src/elements/text.rs @@ -1,6 +1,5 @@ use crate::{ color::Color, - font_cache::FamilyId, fonts::TextStyle, geometry::{ rect::RectF, @@ -14,8 +13,6 @@ use serde_json::json; pub struct Text { text: String, - family_id: FamilyId, - font_size: f32, style: TextStyle, } @@ -25,18 +22,8 @@ pub struct LayoutState { } impl Text { - pub fn new(text: String, family_id: FamilyId, font_size: f32) -> Self { - Self { - text, - family_id, - font_size, - style: Default::default(), - } - } - - pub fn with_style(mut self, style: &TextStyle) -> Self { - self.style = style.clone(); - self + pub fn new(text: String, style: TextStyle) -> Self { + Self { text, style } } pub fn with_default_color(mut self, color: Color) -> Self { @@ -54,20 +41,17 @@ impl Element for Text { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let font_id = cx - .font_cache - .select_font(self.family_id, &self.style.font_properties) - .unwrap(); - let line_height = cx.font_cache.line_height(font_id, self.font_size); + let font_id = self.style.font_id; + let line_height = cx.font_cache.line_height(font_id, self.style.font_size); - let mut wrapper = cx.font_cache.line_wrapper(font_id, self.font_size); + let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size); let mut lines = Vec::new(); let mut line_count = 0; let mut max_line_width = 0_f32; for line in self.text.lines() { let shaped_line = cx.text_layout_cache.layout_str( line, - self.font_size, + self.style.font_size, &[(line.len(), font_id, self.style.color)], ); let wrap_boundaries = wrapper @@ -123,14 +107,12 @@ impl Element for Text { bounds: RectF, _: &Self::LayoutState, _: &Self::PaintState, - cx: &DebugContext, + _: &DebugContext, ) -> Value { json!({ - "type": "Label", + "type": "Text", "bounds": bounds.to_json(), "text": &self.text, - "font_family": cx.font_cache.family_name(self.family_id).unwrap(), - "font_size": self.font_size, "style": self.style.to_json(), }) } diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index ab387b65bb1ff59c5db5e1743f8f5540950c47e6..3010e1ada054a0500734f78c402a81f64201a38a 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -1,32 +1,35 @@ use crate::{ color::Color, json::{json, ToJson}, + FontCache, }; +use anyhow::anyhow; pub use font_kit::{ metrics::Metrics, properties::{Properties, Stretch, Style, Weight}, }; use serde::{de, Deserialize}; use serde_json::Value; +use std::{cell::RefCell, sync::Arc}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct FontId(pub usize); pub type GlyphId = u32; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub struct TextStyle { pub color: Color, + pub font_family_name: Arc, + pub font_id: FontId, + pub font_size: f32, pub font_properties: Properties, } -impl Default for TextStyle { - fn default() -> Self { - Self { - color: Color::from_u32(0xff0000ff), - font_properties: Default::default(), - } - } +#[derive(Clone, Debug, Default)] +pub struct HighlightStyle { + pub color: Color, + pub font_properties: Properties, } #[allow(non_camel_case_types)] @@ -43,34 +46,79 @@ enum WeightJson { black, } +thread_local! { + static FONT_CACHE: RefCell>> = Default::default(); +} + #[derive(Deserialize)] struct TextStyleJson { color: Color, + family: String, weight: Option, #[serde(default)] italic: bool, + size: f32, } -impl<'de> Deserialize<'de> for TextStyle { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let json = Value::deserialize(deserializer)?; - if json.is_object() { - let style_json: TextStyleJson = - serde_json::from_value(json).map_err(de::Error::custom)?; - Ok(style_json.into()) - } else { - Ok(Self { - color: serde_json::from_value(json).map_err(de::Error::custom)?, - font_properties: Properties::new(), - }) +#[derive(Deserialize)] +struct HighlightStyleJson { + color: Color, + weight: Option, + #[serde(default)] + italic: bool, +} + +impl TextStyle { + pub fn new( + font_family_name: impl Into>, + font_size: f32, + font_properties: Properties, + color: Color, + 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)?; + Ok(Self { + color, + font_family_name, + font_id, + font_size, + font_properties, + }) + } + + fn from_json(json: TextStyleJson) -> anyhow::Result { + FONT_CACHE.with(|font_cache| { + if let Some(font_cache) = font_cache.borrow().as_ref() { + let font_properties = properties_from_json(json.weight, json.italic); + Self::new( + json.family, + json.size, + font_properties, + json.color, + font_cache, + ) + } else { + Err(anyhow!( + "TextStyle can only be deserialized within a call to with_font_cache" + )) + } + }) + } +} + +impl HighlightStyle { + fn from_json(json: HighlightStyleJson) -> Self { + let font_properties = properties_from_json(json.weight, json.italic); + Self { + color: json.color, + font_properties, } } } -impl From for TextStyle { +impl From for HighlightStyle { fn from(color: Color) -> Self { Self { color, @@ -79,40 +127,61 @@ impl From for TextStyle { } } +impl<'de> Deserialize<'de> for TextStyle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self::from_json(TextStyleJson::deserialize(deserializer)?) + .map_err(|e| de::Error::custom(e))?) + } +} + impl ToJson for TextStyle { fn to_json(&self) -> Value { json!({ "color": self.color.to_json(), + "font_family": self.font_family_name.as_ref(), "font_properties": self.font_properties.to_json(), }) } } -impl Into for TextStyleJson { - fn into(self) -> TextStyle { - let weight = match self.weight.unwrap_or(WeightJson::normal) { - WeightJson::thin => Weight::THIN, - WeightJson::extra_light => Weight::EXTRA_LIGHT, - WeightJson::light => Weight::LIGHT, - WeightJson::normal => Weight::NORMAL, - WeightJson::medium => Weight::MEDIUM, - WeightJson::semibold => Weight::SEMIBOLD, - WeightJson::bold => Weight::BOLD, - WeightJson::extra_bold => Weight::EXTRA_BOLD, - WeightJson::black => Weight::BLACK, - }; - let style = if self.italic { - Style::Italic +impl<'de> Deserialize<'de> for HighlightStyle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let json = serde_json::Value::deserialize(deserializer)?; + if json.is_object() { + Ok(Self::from_json( + serde_json::from_value(json).map_err(de::Error::custom)?, + )) } else { - Style::Normal - }; - TextStyle { - color: self.color, - font_properties: *Properties::new().weight(weight).style(style), + Ok(Self { + color: serde_json::from_value(json).map_err(de::Error::custom)?, + font_properties: Properties::new(), + }) } } } +fn properties_from_json(weight: Option, italic: bool) -> Properties { + let weight = match weight.unwrap_or(WeightJson::normal) { + WeightJson::thin => Weight::THIN, + WeightJson::extra_light => Weight::EXTRA_LIGHT, + WeightJson::light => Weight::LIGHT, + WeightJson::normal => Weight::NORMAL, + WeightJson::medium => Weight::MEDIUM, + WeightJson::semibold => Weight::SEMIBOLD, + WeightJson::bold => Weight::BOLD, + WeightJson::extra_bold => Weight::EXTRA_BOLD, + WeightJson::black => Weight::BLACK, + }; + let style = if italic { Style::Italic } else { Style::Normal }; + *Properties::new().weight(weight).style(style) +} + impl ToJson for Properties { fn to_json(&self) -> crate::json::Value { json!({ @@ -164,3 +233,15 @@ impl ToJson for Stretch { json!(self.0) } } + +pub fn with_font_cache(font_cache: Arc, callback: F) -> T +where + F: FnOnce() -> T, +{ + FONT_CACHE.with(|cache| { + *cache.borrow_mut() = Some(font_cache); + let result = callback(); + cache.borrow_mut().take(); + result + }) +} diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 1f329f5219071fd8daab38ee67b313f93d557557..41f5efb51b043d908849244cc0a28d8614ab1318 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -943,7 +943,7 @@ mod tests { #[gpui::test] async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { let (window_b, _) = cx_b.add_window(|_| EmptyView); - let settings = settings::channel(&cx_b.font_cache()).unwrap().1; + let settings = cx_b.read(settings::test).1; let lang_registry = Arc::new(LanguageRegistry::new()); // Connect to a server as 2 clients. diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 92d3793bb088fbc534373b495f5d04376c6a3b48..06b6346531af0a34b9222e4d49193d1decca4892 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -4,7 +4,7 @@ background = "$surface.0" [workspace.tab] text = "$text.2" padding = { left = 10, right = 10 } -icon_close = "$text.0" +icon_close = "$text.0.color" icon_dirty = "$status.info" icon_conflict = "$status.warn" @@ -17,10 +17,10 @@ text = "$text.0" padding = { left = 10, right = 10 } [workspace.sidebar_icon] -color = "$text.2" +color = "$text.2.color" [workspace.active_sidebar_icon] -color = "$text.0" +color = "$text.0.color" [chat_panel] padding = { top = 10.0, bottom = 10.0, left = 10.0, right = 10.0 } @@ -28,7 +28,7 @@ padding = { top = 10.0, bottom = 10.0, left = 10.0, right = 10.0 } [chat_panel.message] body = "$text.0" sender.margin.right = 10.0 -sender.text = { color = "$text.0", weight = "bold" } +sender.text = { extends = "$text.0", weight = "bold" } timestamp.text = "$text.2" [selector] @@ -41,8 +41,8 @@ shadow = { offset = [0.0, 0.0], blur = 12.0, color = "#00000088" } [selector.item] background = "#424344" -text = "#cccccc" -highlight_text = { color = "#18a3ff", weight = "bold" } +text = "$text.1" +highlight_text = { extends = "$text.base", color = "#18a3ff", weight = "bold" } border = { color = "#000000", width = 1.0 } padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } @@ -54,10 +54,12 @@ background = "#094771" background = "$surface.1" gutter_background = "$surface.1" active_line_background = "$surface.2" -line_number = "$text.2" -line_number_active = "$text.0" -text = "$text.1" +line_number = "$text.2.color" +line_number_active = "$text.0.color" replicas = [ - { selection = "#264f78", cursor = "$text.0" }, + { selection = "#264f78", cursor = "$text.0.color" }, { selection = "#504f31", cursor = "#fcf154" }, ] + +[syntax] +default = "$text.1.color" diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index b019057f651f7b63e3620a6fdf652beb29d9efbd..ce44b87e9646a9978af2296cd317658b42052bcd 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -6,9 +6,10 @@ extends = "_base" 2 = "#131415" [text] -0 = "#ffffff" -1 = "#b3b3b3" -2 = "#7b7d80" +base = { family = "Helvetica", size = 12.0 } +0 = { extends = "$text.base", color = "#ffffff" } +1 = { extends = "$text.base", color = "#b3b3b3" } +2 = { extends = "$text.base", color = "#7b7d80" } [status] good = "#4fac63" diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml index 380a5cee013debc8802e5f87956e6fa87700f311..6702c18f1cb7e8cd854df2cd71930c8f892f8377 100644 --- a/zed/assets/themes/light.toml +++ b/zed/assets/themes/light.toml @@ -7,9 +7,10 @@ extends = "_base" 3 = "#3a3b3c" [text] -0 = "#acacac" -1 = "#111111" -2 = "#333333" +base = { family = "Inconsolata" } +0 = { extends = "$text.base", color = "#acacac" } +1 = { extends = "$text.base", color = "#111111" } +2 = { extends = "$text.base", color = "#333333" } [status] good = "#4fac63" diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 0573666f9dc9b90579c34682575b989b97b5afbc..2572acc12aa59ed4ebd7258c3a7cad5d26a6ae3d 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -126,10 +126,8 @@ impl ChatPanel { Container::new( Label::new( message.sender.github_login.clone(), - settings.ui_font_family, - settings.ui_font_size, + theme.sender.label.clone(), ) - .with_style(&theme.sender.label) .boxed(), ) .with_style(&theme.sender.container) @@ -139,10 +137,8 @@ impl ChatPanel { Container::new( Label::new( format_timestamp(message.timestamp, now), - settings.ui_font_family, - settings.ui_font_size, + theme.timestamp.label.clone(), ) - .with_style(&theme.timestamp.label) .boxed(), ) .with_style(&theme.timestamp.container) @@ -150,15 +146,7 @@ impl ChatPanel { ) .boxed(), ) - .with_child( - Text::new( - message.body.clone(), - settings.ui_font_family, - settings.ui_font_size, - ) - .with_style(&theme.body) - .boxed(), - ) + .with_child(Text::new(message.body.clone(), theme.body.clone()).boxed()) .boxed() } diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 4aa5b6ce3e874bb5a6cf01f04b16880e91a1a742..751d68740372a03d9d64ed20791df3541aac16bd 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -2418,7 +2418,7 @@ impl Snapshot { } if !line_chunk.is_empty() && !line_exceeded_max_len { - let style = self.theme.highlight_style(style_ix); + let style = self.theme.syntax.highlight_style(style_ix); // Avoid a lookup if the font properties match the previous ones. let font_id = if style.font_properties == prev_font_properties { prev_font_id @@ -2632,19 +2632,14 @@ impl workspace::ItemView for Editor { #[cfg(test)] mod tests { use super::*; - use crate::{ - editor::Point, - language::LanguageRegistry, - settings, - test::{build_settings, sample_text}, - }; + use crate::{editor::Point, language::LanguageRegistry, settings, test::sample_text}; use buffer::History; use unindent::Unindent; #[gpui::test] 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::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, editor) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer, settings, cx) }); @@ -2712,7 +2707,7 @@ mod tests { #[gpui::test] 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::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer, settings, cx) }); @@ -2746,7 +2741,7 @@ mod tests { #[gpui::test] 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::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer, settings, cx) }); @@ -2792,7 +2787,7 @@ mod tests { let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx)); - let settings = settings::channel(&font_cache).unwrap().1; + let settings = settings::test(&cx).1; let (_, editor) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer.clone(), settings.clone(), cx) }); @@ -2838,7 +2833,7 @@ mod tests { cx, ) }); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer.clone(), settings, cx) }); @@ -2906,7 +2901,7 @@ mod tests { #[gpui::test] fn test_move_cursor(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx)); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer.clone(), settings, cx) }); @@ -2983,7 +2978,7 @@ mod tests { #[gpui::test] fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx)); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer.clone(), settings, cx) }); @@ -3041,7 +3036,7 @@ mod tests { #[gpui::test] fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, "ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx)); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer.clone(), settings, cx) }); @@ -3072,7 +3067,7 @@ mod tests { #[gpui::test] 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::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer, settings, cx) }); @@ -3217,7 +3212,7 @@ mod tests { fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", cx)); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer, settings, cx) }); @@ -3405,7 +3400,7 @@ mod tests { fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, "use one::{\n two::three::four::five\n};", cx)); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer, settings, cx) }); @@ -3467,7 +3462,7 @@ mod tests { cx, ) }); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer.clone(), settings, cx) }); @@ -3503,7 +3498,7 @@ mod tests { cx, ) }); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer.clone(), settings, cx) }); @@ -3532,7 +3527,7 @@ mod tests { #[gpui::test] fn test_delete_line(cx: &mut gpui::MutableAppContext) { - let settings = settings::channel(&cx.font_cache()).unwrap().1; + 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) @@ -3558,7 +3553,7 @@ mod tests { ); }); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + 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) @@ -3577,7 +3572,7 @@ mod tests { #[gpui::test] fn test_duplicate_line(cx: &mut gpui::MutableAppContext) { - let settings = settings::channel(&cx.font_cache()).unwrap().1; + 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) @@ -3606,7 +3601,7 @@ mod tests { ); }); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + 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) @@ -3634,7 +3629,7 @@ mod tests { #[gpui::test] fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { - let settings = settings::channel(&cx.font_cache()).unwrap().1; + 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) @@ -3719,7 +3714,7 @@ mod tests { #[gpui::test] fn test_clipboard(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, "one two three four five six ", cx)); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let view = cx .add_window(Default::default(), |cx| { Editor::for_buffer(buffer.clone(), settings, cx) @@ -3854,7 +3849,7 @@ mod tests { #[gpui::test] fn test_select_all(cx: &mut gpui::MutableAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, "abc\nde\nfgh", cx)); - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let settings = settings::test(&cx).1; let (_, view) = cx.add_window(Default::default(), |cx| { Editor::for_buffer(buffer, settings, cx) }); @@ -3869,7 +3864,7 @@ mod tests { #[gpui::test] fn test_select_line(cx: &mut gpui::MutableAppContext) { - let settings = settings::channel(&cx.font_cache()).unwrap().1; + 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) @@ -3917,7 +3912,7 @@ mod tests { #[gpui::test] fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { - let settings = settings::channel(&cx.font_cache()).unwrap().1; + 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) @@ -3984,7 +3979,7 @@ mod tests { #[gpui::test] fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) { - let settings = settings::channel(&cx.font_cache()).unwrap().1; + 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) @@ -4159,7 +4154,7 @@ mod tests { #[gpui::test] async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) { - let settings = cx.read(build_settings); + let settings = cx.read(settings::test).1; let languages = LanguageRegistry::new(); let lang = languages.select_language("z.rs"); let text = r#" diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index e118f3202d211a33e2e48d413203fcbf012db221..e5f18b56dbdbd7f4c5dcdf79da4b0c3605f321d6 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -343,8 +343,8 @@ mod tests { use crate::{ editor::movement, language::{Language, LanguageConfig}, - settings::Theme, test::*, + theme::SyntaxTheme, util::RandomCharIter, }; use buffer::{History, SelectionGoal}; @@ -366,7 +366,7 @@ mod tests { tab_size: rng.gen_range(1..=4), buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(), buffer_font_size: 14.0, - ..Settings::new(&font_cache).unwrap() + ..cx.read(Settings::test) }; let max_wrap_width = 300.0; let mut wrap_width = if rng.gen_bool(0.1) { @@ -535,7 +535,7 @@ mod tests { buffer_font_size: 12.0, ui_font_size: 12.0, tab_size: 4, - theme: Arc::new(Theme::default()), + ..cx.read(Settings::test) }; let wrap_width = Some(64.); @@ -606,7 +606,10 @@ mod tests { let map = cx.add_model(|cx| { DisplayMap::new( buffer.clone(), - Settings::new(cx.font_cache()).unwrap().with_tab_size(4), + Settings { + tab_size: 4, + ..Settings::test(cx) + }, None, cx, ) @@ -660,13 +663,13 @@ mod tests { (function_item name: (identifier) @fn.name)"#, ) .unwrap(); - let theme = Theme { - syntax: vec![ + let theme = SyntaxTheme::new( + Default::default(), + vec![ ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), ], - ..Default::default() - }; + ); let lang = Arc::new(Language { config: LanguageConfig { name: "Test".to_string(), @@ -688,7 +691,10 @@ mod tests { let map = cx.add_model(|cx| { DisplayMap::new( buffer, - Settings::new(cx.font_cache()).unwrap().with_tab_size(2), + Settings { + tab_size: 2, + ..Settings::test(cx) + }, None, cx, ) @@ -750,13 +756,13 @@ mod tests { (function_item name: (identifier) @fn.name)"#, ) .unwrap(); - let theme = Theme { - syntax: vec![ + let theme = SyntaxTheme::new( + Default::default(), + vec![ ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), ], - ..Default::default() - }; + ); let lang = Arc::new(Language { config: LanguageConfig { name: "Test".to_string(), @@ -780,7 +786,7 @@ mod tests { tab_size: 4, buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(), buffer_font_size: 16.0, - ..Settings::new(&font_cache).unwrap() + ..cx.read(Settings::test) }; let map = cx.add_model(|cx| DisplayMap::new(buffer, settings, Some(40.0), cx)); assert_eq!( @@ -820,7 +826,10 @@ mod tests { let map = cx.add_model(|cx| { DisplayMap::new( buffer.clone(), - Settings::new(cx.font_cache()).unwrap().with_tab_size(4), + Settings { + tab_size: 4, + ..Settings::test(cx) + }, None, cx, ) @@ -861,7 +870,10 @@ mod tests { let map = cx.add_model(|cx| { DisplayMap::new( buffer.clone(), - Settings::new(cx.font_cache()).unwrap().with_tab_size(4), + Settings { + tab_size: 4, + ..Settings::test(cx) + }, None, cx, ) @@ -925,7 +937,10 @@ mod tests { let map = cx.add_model(|cx| { DisplayMap::new( buffer.clone(), - Settings::new(cx.font_cache()).unwrap().with_tab_size(4), + Settings { + tab_size: 4, + ..Settings::test(cx) + }, None, cx, ) @@ -939,7 +954,7 @@ mod tests { fn highlighted_chunks<'a>( rows: Range, map: &ModelHandle, - theme: &'a Theme, + theme: &'a SyntaxTheme, cx: &mut MutableAppContext, ) -> Vec<(String, Option<&'a str>)> { let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx)); diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index 3ba9690c640fcafdd109931c3cebe5dbaefe1a65..8720e39979a70f6abaa1925ea3f5cc146e905086 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/zed/src/editor/display_map/wrap_map.rs @@ -921,7 +921,7 @@ mod tests { tab_size: rng.gen_range(1..=4), buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(), buffer_font_size: 14.0, - ..Settings::new(&font_cache).unwrap() + ..cx.read(Settings::test) }; log::info!("Tab size: {}", settings.tab_size); log::info!("Wrap width: {:?}", wrap_width); diff --git a/zed/src/editor/movement.rs b/zed/src/editor/movement.rs index e42a63f763450b4cbf5bd6d9d6aff41710f07673..0e244995e7c78c8b148f9210e5ff22bbb0fd552e 100644 --- a/zed/src/editor/movement.rs +++ b/zed/src/editor/movement.rs @@ -182,12 +182,12 @@ mod tests { use super::*; use crate::{ editor::{display_map::DisplayMap, Buffer}, - test::build_app_state, + test::test_app_state, }; #[gpui::test] fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) { - let settings = build_app_state(cx).settings.borrow().clone(); + let settings = test_app_state(cx).settings.borrow().clone(); 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 snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 50ddc48038246e7b9485f6d646b0fec631a429d0..76c2f71dc8c7a1a5c611c575d9c09f6a0cdd9992 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -117,13 +117,7 @@ impl FileFinder { if self.matches.is_empty() { let settings = self.settings.borrow(); return Container::new( - Label::new( - "No matches".into(), - settings.ui_font_family, - settings.ui_font_size, - ) - .with_style(&settings.theme.selector.label) - .boxed(), + Label::new("No matches".into(), settings.theme.selector.label.clone()).boxed(), ) .with_margin_top(6.0) .named("empty matches"); @@ -184,24 +178,14 @@ impl FileFinder { 1.0, Flex::column() .with_child( - Label::new( - file_name.to_string(), - settings.ui_font_family, - settings.ui_font_size, - ) - .with_style(&style.label) - .with_highlights(file_name_positions) - .boxed(), + Label::new(file_name.to_string(), style.label.clone()) + .with_highlights(file_name_positions) + .boxed(), ) .with_child( - Label::new( - full_path, - settings.ui_font_family, - settings.ui_font_size, - ) - .with_style(&style.label) - .with_highlights(full_path_positions) - .boxed(), + Label::new(full_path, style.label.clone()) + .with_highlights(full_path_positions) + .boxed(), ) .boxed(), ) @@ -438,7 +422,7 @@ mod tests { use crate::{ editor::{self, Insert}, fs::FakeFs, - test::{build_app_state, temp_tree}, + test::{temp_tree, test_app_state}, workspace::Workspace, }; use serde_json::json; @@ -456,7 +440,7 @@ mod tests { editor::init(cx); }); - let app_state = cx.update(build_app_state); + let app_state = cx.update(test_app_state); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { @@ -516,7 +500,7 @@ mod tests { ) .await; - let mut app_state = cx.update(build_app_state); + let mut app_state = cx.update(test_app_state); Arc::get_mut(&mut app_state).unwrap().fs = fs; let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); @@ -578,7 +562,7 @@ mod tests { fs::create_dir(&dir_path).unwrap(); fs::write(&file_path, "").unwrap(); - let app_state = cx.update(build_app_state); + let app_state = cx.update(test_app_state); let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { @@ -625,7 +609,7 @@ mod tests { "dir2": { "a.txt": "" } })); - let app_state = cx.update(build_app_state); + let app_state = cx.update(test_app_state); let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace diff --git a/zed/src/language.rs b/zed/src/language.rs index 886befe595e551befa25c78348ef75f6c5db1cf2..f6342bf254af83bcd020a73e9d6e9178bbb2d3ec 100644 --- a/zed/src/language.rs +++ b/zed/src/language.rs @@ -1,4 +1,4 @@ -use crate::settings::{HighlightMap, Theme}; +use crate::{settings::HighlightMap, theme::SyntaxTheme}; use parking_lot::Mutex; use rust_embed::RustEmbed; use serde::Deserialize; @@ -39,7 +39,7 @@ impl Language { self.highlight_map.lock().clone() } - pub fn set_theme(&self, theme: &Theme) { + pub fn set_theme(&self, theme: &SyntaxTheme) { *self.highlight_map.lock() = HighlightMap::new(self.highlight_query.capture_names(), theme); } } @@ -61,7 +61,7 @@ impl LanguageRegistry { } } - pub fn set_theme(&self, theme: &Theme) { + pub fn set_theme(&self, theme: &SyntaxTheme) { for language in &self.languages { language.set_theme(theme); } diff --git a/zed/src/main.rs b/zed/src/main.rs index 2258f746862e930a32b2a0871c6faa5a3b2b3d34..d1361870b808eb3fe32944a01d53629f605f796a 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -22,11 +22,10 @@ fn main() { let app = gpui::App::new(assets::Assets).unwrap(); - let themes = settings::ThemeRegistry::new(assets::Assets); - let (settings_tx, settings) = - settings::channel_with_themes(&app.font_cache(), &themes).unwrap(); + let themes = settings::ThemeRegistry::new(assets::Assets, app.font_cache()); + let (settings_tx, settings) = settings::channel(&app.font_cache(), &themes).unwrap(); let languages = Arc::new(language::LanguageRegistry::new()); - languages.set_theme(&settings.borrow().theme); + languages.set_theme(&settings.borrow().theme.syntax); app.run(move |cx| { let rpc = rpc::Client::new(); diff --git a/zed/src/settings.rs b/zed/src/settings.rs index a6b9e667c1d848e431a9805b3f7d36f385800520..dba30749b4c14422b829c13743b7187bbbc42bf1 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -17,11 +17,27 @@ pub struct Settings { } impl Settings { - pub fn new(font_cache: &FontCache) -> Result { - Self::new_with_theme(font_cache, Arc::new(Theme::default())) + #[cfg(any(test, feature = "test-support"))] + pub fn test(cx: &gpui::AppContext) -> Self { + lazy_static::lazy_static! { + static ref DEFAULT_THEME: parking_lot::Mutex>> = Default::default(); + } + + let mut theme_guard = DEFAULT_THEME.lock(); + let theme = if let Some(theme) = theme_guard.as_ref() { + theme.clone() + } else { + let theme = ThemeRegistry::new(crate::assets::Assets, cx.font_cache().clone()) + .get(DEFAULT_THEME_NAME) + .expect("failed to load default theme in tests"); + *theme_guard = Some(theme.clone()); + theme + }; + + Self::new(cx.font_cache(), theme).unwrap() } - pub fn new_with_theme(font_cache: &FontCache, theme: Arc) -> Result { + pub fn new(font_cache: &FontCache, theme: Arc) -> Result { Ok(Self { buffer_font_family: font_cache.load_family(&["Fira Code", "Monaco"])?, buffer_font_size: 14.0, @@ -38,13 +54,12 @@ impl Settings { } } -pub fn channel( - font_cache: &FontCache, -) -> Result<(watch::Sender, watch::Receiver)> { - Ok(watch::channel_with(Settings::new(font_cache)?)) +#[cfg(any(test, feature = "test-support"))] +pub fn test(cx: &gpui::AppContext) -> (watch::Sender, watch::Receiver) { + watch::channel_with(Settings::test(cx)) } -pub fn channel_with_themes( +pub fn channel( font_cache: &FontCache, themes: &ThemeRegistry, ) -> Result<(watch::Sender, watch::Receiver)> { @@ -54,7 +69,5 @@ pub fn channel_with_themes( panic!("failed to deserialize default theme: {:?}", err) } }; - Ok(watch::channel_with(Settings::new_with_theme( - font_cache, theme, - )?)) + Ok(watch::channel_with(Settings::new(font_cache, theme)?)) } diff --git a/zed/src/test.rs b/zed/src/test.rs index f406df1946e431cd3f8f2774db6cbcf550b3c4bb..d8b30afa6923837d935c1fece436a4b01593015e 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -6,11 +6,10 @@ use crate::{ settings::{self, ThemeRegistry}, time::ReplicaId, user::UserStore, - AppState, Settings, + AppState, }; -use gpui::{AppContext, Entity, ModelHandle, MutableAppContext}; +use gpui::{Entity, ModelHandle, MutableAppContext}; use parking_lot::Mutex; -use postage::watch; use smol::channel; use std::{ marker::PhantomData, @@ -156,14 +155,10 @@ fn write_tree(path: &Path, tree: serde_json::Value) { } } -pub fn build_settings(cx: &AppContext) -> watch::Receiver { - settings::channel(&cx.font_cache()).unwrap().1 -} - -pub fn build_app_state(cx: &mut MutableAppContext) -> Arc { - let (settings_tx, settings) = settings::channel(&cx.font_cache()).unwrap(); +pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { + let (settings_tx, settings) = settings::test(cx); let languages = Arc::new(LanguageRegistry::new()); - let themes = ThemeRegistry::new(()); + let themes = ThemeRegistry::new((), cx.font_cache().clone()); let rpc = rpc::Client::new(); let user_store = Arc::new(UserStore::new(rpc.clone())); Arc::new(AppState { diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 3a8430c4be8aaad2ad1580aa1dec439afac97e4e..0346eba0e8086460a0f684c7c20f1be7383f5153 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -5,9 +5,9 @@ use anyhow::Result; use gpui::{ color::Color, elements::{ContainerStyle, LabelStyle}, - fonts::TextStyle, + fonts::{HighlightStyle, TextStyle}, }; -use serde::{Deserialize, Deserializer}; +use serde::{de, Deserialize}; use std::collections::HashMap; pub use highlight_map::*; @@ -15,7 +15,7 @@ pub use theme_registry::*; pub const DEFAULT_THEME_NAME: &'static str = "dark"; -#[derive(Debug, Default, Deserialize)] +#[derive(Deserialize)] pub struct Theme { #[serde(default)] pub name: String, @@ -23,11 +23,15 @@ pub struct Theme { pub chat_panel: ChatPanel, pub selector: Selector, pub editor: Editor, - #[serde(deserialize_with = "deserialize_syntax_theme")] - pub syntax: Vec<(String, TextStyle)>, + pub syntax: SyntaxTheme, } -#[derive(Debug, Default, Deserialize)] +pub struct SyntaxTheme { + highlights: Vec<(String, HighlightStyle)>, + default_style: HighlightStyle, +} + +#[derive(Deserialize)] pub struct Workspace { pub background: Color, pub tab: Tab, @@ -37,7 +41,7 @@ pub struct Workspace { pub active_sidebar_icon: SidebarIcon, } -#[derive(Debug, Default, Deserialize)] +#[derive(Deserialize)] pub struct Tab { #[serde(flatten)] pub container: ContainerStyle, @@ -48,26 +52,26 @@ pub struct Tab { pub icon_conflict: Color, } -#[derive(Debug, Default, Deserialize)] +#[derive(Deserialize)] pub struct SidebarIcon { pub color: Color, } -#[derive(Debug, Default, Deserialize)] +#[derive(Deserialize)] pub struct ChatPanel { #[serde(flatten)] pub container: ContainerStyle, pub message: ChatMessage, } -#[derive(Debug, Default, Deserialize)] +#[derive(Deserialize)] pub struct ChatMessage { pub body: TextStyle, pub sender: ContainedLabel, pub timestamp: ContainedLabel, } -#[derive(Debug, Default, Deserialize)] +#[derive(Deserialize)] pub struct Selector { #[serde(flatten)] pub container: ContainerStyle, @@ -78,7 +82,7 @@ pub struct Selector { pub active_item: ContainedLabel, } -#[derive(Debug, Default, Deserialize)] +#[derive(Deserialize)] pub struct ContainedLabel { #[serde(flatten)] pub container: ContainerStyle, @@ -86,70 +90,69 @@ pub struct ContainedLabel { pub label: LabelStyle, } -#[derive(Debug, Deserialize)] +#[derive(Deserialize)] pub struct Editor { pub background: Color, pub gutter_background: Color, pub active_line_background: Color, pub line_number: Color, pub line_number_active: Color, - pub text: Color, pub replicas: Vec, } -#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[derive(Clone, Copy, Deserialize)] pub struct Replica { pub cursor: Color, pub selection: Color, } -impl Theme { - pub fn highlight_style(&self, id: HighlightId) -> TextStyle { - self.syntax +impl SyntaxTheme { + pub fn new(default_style: HighlightStyle, highlights: Vec<(String, HighlightStyle)>) -> Self { + Self { + default_style, + highlights, + } + } + + pub fn highlight_style(&self, id: HighlightId) -> HighlightStyle { + self.highlights .get(id.0 as usize) .map(|entry| entry.1.clone()) - .unwrap_or_else(|| TextStyle { - color: self.editor.text, - font_properties: Default::default(), - }) + .unwrap_or_else(|| self.default_style.clone()) } #[cfg(test)] pub fn highlight_name(&self, id: HighlightId) -> Option<&str> { - self.syntax.get(id.0 as usize).map(|e| e.0.as_str()) + self.highlights.get(id.0 as usize).map(|e| e.0.as_str()) } } -impl Default for Editor { - fn default() -> Self { - Self { - background: Default::default(), - gutter_background: Default::default(), - active_line_background: Default::default(), - line_number: Default::default(), - line_number_active: Default::default(), - text: Default::default(), - replicas: vec![Replica::default()], - } - } -} - -pub fn deserialize_syntax_theme<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let mut result = Vec::<(String, TextStyle)>::new(); - - let syntax_data: HashMap = Deserialize::deserialize(deserializer)?; - for (key, style) in syntax_data { - match result.binary_search_by(|(needle, _)| needle.cmp(&key)) { - Ok(i) | Err(i) => { - result.insert(i, (key, style)); +impl<'de> Deserialize<'de> for SyntaxTheme { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut syntax_data: HashMap = + Deserialize::deserialize(deserializer)?; + + let mut result = Self { + highlights: Vec::<(String, HighlightStyle)>::new(), + default_style: syntax_data + .remove("default") + .ok_or_else(|| de::Error::custom("must specify a default color in syntax theme"))?, + }; + + for (key, style) in syntax_data { + match result + .highlights + .binary_search_by(|(needle, _)| needle.cmp(&key)) + { + Ok(i) | Err(i) => { + result.highlights.insert(i, (key, style)); + } } } - } - Ok(result) + Ok(result) + } } diff --git a/zed/src/theme/highlight_map.rs b/zed/src/theme/highlight_map.rs index 55a053113c70420d1ec6bcdad186c65cef201481..c030e2ab1a4f21b36dc040922a7349654e07a171 100644 --- a/zed/src/theme/highlight_map.rs +++ b/zed/src/theme/highlight_map.rs @@ -1,4 +1,4 @@ -use super::Theme; +use super::SyntaxTheme; use std::sync::Arc; #[derive(Clone, Debug)] @@ -10,7 +10,7 @@ pub struct HighlightId(pub u32); const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); impl HighlightMap { - pub fn new(capture_names: &[String], theme: &Theme) -> Self { + pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { // For each capture name in the highlight query, find the longest // key in the theme's syntax styles that matches all of the // dot-separated components of the capture name. @@ -19,7 +19,7 @@ impl HighlightMap { .iter() .map(|capture_name| { theme - .syntax + .highlights .iter() .enumerate() .filter_map(|(i, (key, _))| { @@ -68,9 +68,9 @@ mod tests { #[test] fn test_highlight_map() { - let theme = Theme { - name: "test".into(), - syntax: [ + let theme = SyntaxTheme::new( + Default::default(), + [ ("function", Color::from_u32(0x100000ff)), ("function.method", Color::from_u32(0x200000ff)), ("function.async", Color::from_u32(0x300000ff)), @@ -81,8 +81,7 @@ mod tests { .iter() .map(|(name, color)| (name.to_string(), (*color).into())) .collect(), - ..Default::default() - }; + ); let capture_names = &[ "function.special".to_string(), diff --git a/zed/src/theme/theme_registry.rs b/zed/src/theme/theme_registry.rs index 0b68d8a0c96714ad9c5df48c9914ee937ae146ab..da6d097c315e53fdb5210b024613d02d99d3ef28 100644 --- a/zed/src/theme/theme_registry.rs +++ b/zed/src/theme/theme_registry.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Context, Result}; -use gpui::AssetSource; +use gpui::{fonts, AssetSource, FontCache}; use json::{Map, Value}; use parking_lot::Mutex; use serde_json as json; @@ -11,6 +11,7 @@ pub struct ThemeRegistry { assets: Box, themes: Mutex>>, theme_data: Mutex>>, + font_cache: Arc, } #[derive(Default)] @@ -38,11 +39,12 @@ enum Key { } impl ThemeRegistry { - pub fn new(source: impl AssetSource) -> Arc { + pub fn new(source: impl AssetSource, font_cache: Arc) -> Arc { Arc::new(Self { assets: Box::new(source), themes: Default::default(), theme_data: Default::default(), + font_cache, }) } @@ -69,7 +71,10 @@ impl ThemeRegistry { } let theme_data = self.load(name, true)?; - let mut theme = serde_json::from_value::(theme_data.as_ref().clone())?; + let mut theme = fonts::with_font_cache(self.font_cache.clone(), || { + serde_json::from_value::(theme_data.as_ref().clone()) + })?; + theme.name = name.into(); let theme = Arc::new(theme); self.themes.lock().insert(name.to_string(), theme.clone()); @@ -512,11 +517,12 @@ fn value_at<'a>(object: &'a mut Map, key_path: &KeyPath) -> Optio mod tests { use super::*; use crate::{assets::Assets, theme::DEFAULT_THEME_NAME}; + use gpui::MutableAppContext; use rand::{prelude::StdRng, Rng}; - #[test] - fn test_bundled_themes() { - let registry = ThemeRegistry::new(Assets); + #[gpui::test] + fn test_bundled_themes(cx: &mut MutableAppContext) { + let registry = ThemeRegistry::new(Assets, cx.font_cache().clone()); let mut has_default_theme = false; for theme_name in registry.list() { let theme = registry.get(&theme_name).unwrap(); @@ -528,8 +534,8 @@ mod tests { assert!(has_default_theme); } - #[test] - fn test_theme_extension() { + #[gpui::test] + fn test_theme_extension(cx: &mut MutableAppContext) { let assets = TestAssets(&[ ( "themes/_base.toml", @@ -568,7 +574,7 @@ mod tests { ), ]); - let registry = ThemeRegistry::new(assets); + let registry = ThemeRegistry::new(assets, cx.font_cache().clone()); let theme_data = registry.load("light", true).unwrap(); assert_eq!( theme_data.as_ref(), diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 867c5b7693cffc0e2ee873c4ea215cc70aaec6e5..2b0eea969e98c725bf3b49138fe894ee1ec30fd1 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -204,13 +204,7 @@ impl ThemeSelector { if self.matches.is_empty() { let settings = self.settings.borrow(); return Container::new( - Label::new( - "No matches".into(), - settings.ui_font_family, - settings.ui_font_size, - ) - .with_style(&settings.theme.selector.label) - .boxed(), + Label::new("No matches".into(), settings.theme.selector.label.clone()).boxed(), ) .with_margin_top(6.0) .named("empty matches"); @@ -247,14 +241,12 @@ impl ThemeSelector { let container = Container::new( Label::new( theme_match.string.clone(), - settings.ui_font_family, - settings.ui_font_size, + if index == self.selected_index { + theme.selector.active_item.label.clone() + } else { + theme.selector.item.label.clone() + }, ) - .with_style(if index == self.selected_index { - &theme.selector.active_item.label - } else { - &theme.selector.item.label - }) .with_highlights(theme_match.positions.clone()) .boxed(), ) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index e037460891189121db906b518ae2476029cedfad..e5785028a36e431cea19c91953e99a10d76c5437 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -1018,7 +1018,7 @@ mod tests { use crate::{ editor::{Editor, Insert}, fs::FakeFs, - test::{build_app_state, temp_tree}, + test::{temp_tree, test_app_state}, worktree::WorktreeHandle, }; use serde_json::json; @@ -1027,7 +1027,7 @@ mod tests { #[gpui::test] async fn test_open_paths_action(mut cx: gpui::TestAppContext) { - let app_state = cx.update(build_app_state); + let app_state = cx.update(test_app_state); let dir = temp_tree(json!({ "a": { "aa": null, @@ -1100,7 +1100,7 @@ mod tests { }, })); - let app_state = cx.update(build_app_state); + let app_state = cx.update(test_app_state); let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace @@ -1204,7 +1204,7 @@ mod tests { fs.insert_file("/dir1/a.txt", "".into()).await.unwrap(); fs.insert_file("/dir2/b.txt", "".into()).await.unwrap(); - let mut app_state = cx.update(build_app_state); + let mut app_state = cx.update(test_app_state); Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(fs); let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); @@ -1273,7 +1273,7 @@ mod tests { "a.txt": "", })); - let app_state = cx.update(build_app_state); + let app_state = cx.update(test_app_state); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { @@ -1318,7 +1318,7 @@ mod tests { #[gpui::test] async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) { let dir = TempDir::new("test-new-file").unwrap(); - let app_state = cx.update(build_app_state); + let app_state = cx.update(test_app_state); let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { @@ -1417,7 +1417,7 @@ mod tests { async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) { cx.update(init); - let app_state = cx.update(build_app_state); + let app_state = cx.update(test_app_state); cx.dispatch_global_action(OpenNew(app_state)); let window_id = *cx.window_ids().first().unwrap(); let workspace = cx.root_view::(window_id).unwrap(); @@ -1463,7 +1463,7 @@ mod tests { }, })); - let app_state = cx.update(build_app_state); + let app_state = cx.update(test_app_state); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index ab74cac19148e40745032ab283ea0168e016a220..1be0fccd583df1c1d11e9515e605378de78a5f1e 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -208,14 +208,12 @@ impl Pane { Align::new( Label::new( title, - settings.ui_font_family, - settings.ui_font_size, + if is_active { + theme.workspace.active_tab.label.clone() + } else { + theme.workspace.tab.label.clone() + }, ) - .with_style(if is_active { - &theme.workspace.active_tab.label - } else { - &theme.workspace.tab.label - }) .boxed(), ) .boxed(), From 8c85bc20c7e37ad61cf37d4018b7803e751bd1f1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Aug 2021 15:02:58 -0700 Subject: [PATCH 108/204] Indicate the key-path to the error when failing to load a theme --- Cargo.lock | 1 + zed/Cargo.toml | 1 + zed/src/theme/theme_registry.rs | 7 +++---- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9da817877e5cc07fb82a7edb56537fdfc14ff2d4..3f041b7acdbd347ffc44b040fd5c9eedb93b6d81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5835,6 +5835,7 @@ dependencies = [ "seahash", "serde 1.0.125", "serde_json 1.0.64", + "serde_path_to_error", "similar", "simplelog", "smallvec", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index a9cdb7767578f17d6191d25ab58cb58666e3a6ed..b77ed8caff0bd7c27310870b88aad5112206fefd 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -42,6 +42,7 @@ rust-embed = "5.9.0" seahash = "4.1" serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } +serde_path_to_error = "0.1.4" similar = "1.3" simplelog = "0.9" smallvec = { version = "1.6", features = ["union"] } diff --git a/zed/src/theme/theme_registry.rs b/zed/src/theme/theme_registry.rs index da6d097c315e53fdb5210b024613d02d99d3ef28..0808281f09e4b34f99692130c4f75edf39f85d79 100644 --- a/zed/src/theme/theme_registry.rs +++ b/zed/src/theme/theme_registry.rs @@ -1,8 +1,7 @@ use anyhow::{anyhow, Context, Result}; use gpui::{fonts, AssetSource, FontCache}; -use json::{Map, Value}; use parking_lot::Mutex; -use serde_json as json; +use serde_json::{Map, Value}; use std::{collections::HashMap, fmt, mem, sync::Arc}; use super::Theme; @@ -71,8 +70,8 @@ impl ThemeRegistry { } let theme_data = self.load(name, true)?; - let mut theme = fonts::with_font_cache(self.font_cache.clone(), || { - serde_json::from_value::(theme_data.as_ref().clone()) + let mut theme: Theme = fonts::with_font_cache(self.font_cache.clone(), || { + serde_path_to_error::deserialize(theme_data.as_ref()) })?; theme.name = name.into(); From 8861dea69d3a531a47a01b45981006f059fdc674 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Aug 2021 15:09:26 -0700 Subject: [PATCH 109/204] Add zero padding when displaying timestamps --- zed/src/chat_panel.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 2572acc12aa59ed4ebd7258c3a7cad5d26a6ae3d..d3b42604829bede5c973ec2a3298a1df6d6bff8d 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -207,10 +207,10 @@ fn format_timestamp(mut timestamp: OffsetDateTime, mut now: OffsetDateTime) -> S part = "pm"; } if date == today { - format!("{}:{}{}", hour, timestamp.minute(), part) + format!("{:02}:{:02}{}", hour, timestamp.minute(), part) } else if date.next_day() == Some(today) { - format!("yesterday at {}:{}{}", hour, timestamp.minute(), part) + format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part) } else { - format!("{}/{}/{}", date.month(), date.day(), date.year()) + format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year()) } } From f810464983402bb2b76bca45c1ce35440bceddd2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Aug 2021 16:22:37 -0700 Subject: [PATCH 110/204] Send chat messages based on input buffer's unwrapped text --- zed/src/editor.rs | 88 +++++++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 751d68740372a03d9d64ed20791df3541aac16bd..2e220592bd7b012334f8edf5af8e67a94ee13070 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -2182,7 +2182,11 @@ impl Editor { .max_point() } - pub fn text(&self, cx: &mut MutableAppContext) -> String { + pub fn text(&self, cx: &AppContext) -> String { + self.buffer.read(cx).text() + } + + pub fn display_text(&self, cx: &mut MutableAppContext) -> String { self.display_map .update(cx, |map, cx| map.snapshot(cx)) .text() @@ -2843,7 +2847,7 @@ mod tests { .unwrap(); view.fold(&Fold, cx); assert_eq!( - view.text(cx), + view.display_text(cx), " impl Foo { // Hello! @@ -2864,7 +2868,7 @@ mod tests { view.fold(&Fold, cx); assert_eq!( - view.text(cx), + view.display_text(cx), " impl Foo {… } @@ -2874,7 +2878,7 @@ mod tests { view.unfold(&Unfold, cx); assert_eq!( - view.text(cx), + view.display_text(cx), " impl Foo { // Hello! @@ -2894,7 +2898,7 @@ mod tests { ); view.unfold(&Unfold, cx); - assert_eq!(view.text(cx), buffer.read(cx).text()); + assert_eq!(view.display_text(cx), buffer.read(cx).text()); }); } @@ -2995,7 +2999,7 @@ mod tests { ], cx, ); - assert_eq!(view.text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\n"); + assert_eq!(view.display_text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\n"); view.move_right(&MoveRight, cx); assert_eq!(view.selection_ranges(cx), &[empty_range(0, "ⓐ".len())]); @@ -3185,7 +3189,7 @@ mod tests { view.update(cx, |view, cx| { view.delete_to_end_of_line(&DeleteToEndOfLine, cx); - assert_eq!(view.text(cx), "ab\n de"); + assert_eq!(view.display_text(cx), "ab\n de"); assert_eq!( view.selection_ranges(cx), &[ @@ -3197,7 +3201,7 @@ mod tests { view.update(cx, |view, cx| { view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); - assert_eq!(view.text(cx), "\n"); + assert_eq!(view.display_text(cx), "\n"); assert_eq!( view.selection_ranges(cx), &[ @@ -3373,7 +3377,10 @@ mod tests { view.update(cx, |view, cx| { view.delete_to_next_word_boundary(&DeleteToNextWordBoundary, cx); - assert_eq!(view.text(cx), "use std::s::{foo, bar}\n\n {az.qux()}"); + assert_eq!( + view.display_text(cx), + "use std::s::{foo, bar}\n\n {az.qux()}" + ); assert_eq!( view.selection_ranges(cx), &[ @@ -3385,7 +3392,10 @@ mod tests { view.update(cx, |view, cx| { view.delete_to_previous_word_boundary(&DeleteToPreviousWordBoundary, cx); - assert_eq!(view.text(cx), "use std::::{foo, bar}\n\n az.qux()}"); + assert_eq!( + view.display_text(cx), + "use std::::{foo, bar}\n\n az.qux()}" + ); assert_eq!( view.selection_ranges(cx), &[ @@ -3408,7 +3418,7 @@ mod tests { view.update(cx, |view, cx| { view.set_wrap_width(140., cx); assert_eq!( - view.text(cx), + view.display_text(cx), "use one::{\n two::three::\n four::five\n};" ); @@ -3543,7 +3553,7 @@ mod tests { ) .unwrap(); view.delete_line(&DeleteLine, cx); - assert_eq!(view.text(cx), "ghi"); + assert_eq!(view.display_text(cx), "ghi"); assert_eq!( view.selection_ranges(cx), vec![ @@ -3562,7 +3572,7 @@ mod tests { view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx) .unwrap(); view.delete_line(&DeleteLine, cx); - assert_eq!(view.text(cx), "ghi\n"); + assert_eq!(view.display_text(cx), "ghi\n"); assert_eq!( view.selection_ranges(cx), vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] @@ -3589,7 +3599,7 @@ mod tests { ) .unwrap(); view.duplicate_line(&DuplicateLine, cx); - assert_eq!(view.text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); + assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); assert_eq!( view.selection_ranges(cx), vec![ @@ -3616,7 +3626,7 @@ mod tests { ) .unwrap(); view.duplicate_line(&DuplicateLine, cx); - assert_eq!(view.text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); + assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); assert_eq!( view.selection_ranges(cx), vec![ @@ -3653,10 +3663,16 @@ mod tests { cx, ) .unwrap(); - assert_eq!(view.text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj"); + assert_eq!( + view.display_text(cx), + "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj" + ); view.move_line_up(&MoveLineUp, cx); - assert_eq!(view.text(cx), "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff"); + assert_eq!( + view.display_text(cx), + "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff" + ); assert_eq!( view.selection_ranges(cx), vec![ @@ -3670,7 +3686,10 @@ mod tests { view.update(cx, |view, cx| { view.move_line_down(&MoveLineDown, cx); - assert_eq!(view.text(cx), "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj"); + assert_eq!( + view.display_text(cx), + "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj" + ); assert_eq!( view.selection_ranges(cx), vec![ @@ -3684,7 +3703,10 @@ mod tests { view.update(cx, |view, cx| { view.move_line_down(&MoveLineDown, cx); - assert_eq!(view.text(cx), "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj"); + assert_eq!( + view.display_text(cx), + "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj" + ); assert_eq!( view.selection_ranges(cx), vec![ @@ -3698,7 +3720,10 @@ mod tests { view.update(cx, |view, cx| { view.move_line_up(&MoveLineUp, cx); - assert_eq!(view.text(cx), "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff"); + assert_eq!( + view.display_text(cx), + "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff" + ); assert_eq!( view.selection_ranges(cx), vec![ @@ -3725,14 +3750,14 @@ mod tests { view.update(cx, |view, cx| { view.select_ranges(vec![0..4, 8..14, 19..24], false, cx); view.cut(&Cut, cx); - assert_eq!(view.text(cx), "two four six "); + assert_eq!(view.display_text(cx), "two four six "); }); // Paste with three cursors. Each cursor pastes one slice of the clipboard text. view.update(cx, |view, cx| { view.select_ranges(vec![4..4, 9..9, 13..13], false, cx); view.paste(&Paste, cx); - assert_eq!(view.text(cx), "two one four three six five "); + assert_eq!(view.display_text(cx), "two one four three six five "); assert_eq!( view.selection_ranges(cx), &[ @@ -3752,7 +3777,7 @@ mod tests { view.paste(&Paste, cx); view.insert(&Insert(") ".into()), cx); assert_eq!( - view.text(cx), + view.display_text(cx), "( one three five ) two one four three six five ( one three five ) " ); }); @@ -3761,7 +3786,7 @@ mod tests { view.select_ranges(vec![0..0], false, cx); view.insert(&Insert("123\n4567\n89\n".into()), cx); assert_eq!( - view.text(cx), + view.display_text(cx), "123\n4567\n89\n( one three five ) two one four three six five ( one three five ) " ); }); @@ -3779,7 +3804,7 @@ mod tests { .unwrap(); view.cut(&Cut, cx); assert_eq!( - view.text(cx), + view.display_text(cx), "13\n9\n( one three five ) two one four three six five ( one three five ) " ); }); @@ -3798,7 +3823,7 @@ mod tests { .unwrap(); view.paste(&Paste, cx); assert_eq!( - view.text(cx), + view.display_text(cx), "123\n4567\n9\n( 8ne three five ) two one four three six five ( one three five ) " ); assert_eq!( @@ -3832,7 +3857,7 @@ mod tests { .unwrap(); view.paste(&Paste, cx); assert_eq!( - view.text(cx), + view.display_text(cx), "123\n123\n123\n67\n123\n9\n( 8ne three five ) two one four three six five ( one three five ) " ); assert_eq!( @@ -3936,12 +3961,15 @@ mod tests { cx, ) .unwrap(); - assert_eq!(view.text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i"); + assert_eq!(view.display_text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i"); }); view.update(cx, |view, cx| { view.split_selection_into_lines(&SplitSelectionIntoLines, cx); - assert_eq!(view.text(cx), "aaaaa\nbbbbb\nccc…eeee\nfffff\nggggg\n…i"); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccc…eeee\nfffff\nggggg\n…i" + ); assert_eq!( view.selection_ranges(cx), [ @@ -3958,7 +3986,7 @@ mod tests { .unwrap(); view.split_selection_into_lines(&SplitSelectionIntoLines, cx); assert_eq!( - view.text(cx), + view.display_text(cx), "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" ); assert_eq!( From 38d0258049a91d7ad4e78837121caa9803a48cae Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Aug 2021 16:48:07 -0700 Subject: [PATCH 111/204] Add channel name header to chat panel --- zed/assets/themes/_base.toml | 4 +++- zed/assets/themes/dark.toml | 2 +- zed/src/channel.rs | 4 ++++ zed/src/chat_panel.rs | 23 +++++++++++++++++++++++ zed/src/theme.rs | 2 ++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 06b6346531af0a34b9222e4d49193d1decca4892..08d618aaed03999d9e799ed3e8e1ce9dec6fd953 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -24,9 +24,11 @@ color = "$text.0.color" [chat_panel] padding = { top = 10.0, bottom = 10.0, left = 10.0, right = 10.0 } +channel_name = { extends = "$text.0", weight = "bold" } +channel_name_hash = { text = "$text.2", padding.right = 5.0 } [chat_panel.message] -body = "$text.0" +body = "$text.1" sender.margin.right = 10.0 sender.text = { extends = "$text.0", weight = "bold" } timestamp.text = "$text.2" diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index ce44b87e9646a9978af2296cd317658b42052bcd..9a8ac975bf2d0e1f8f80f23ba091d17b970649cb 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -6,7 +6,7 @@ extends = "_base" 2 = "#131415" [text] -base = { family = "Helvetica", size = 12.0 } +base = { family = "Helvetica", size = 14.0 } 0 = { extends = "$text.base", color = "#ffffff" } 1 = { extends = "$text.base", color = "#b3b3b3" } 2 = { extends = "$text.base", color = "#7b7d80" } diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 02352ec00d0d0b03fcbd874ee8c538fcc847f4b5..f1f2e076077e5b38a7a036fab679d6b880d972d4 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -237,6 +237,10 @@ impl Channel { } } + pub fn name(&self) -> &str { + &self.details.name + } + pub fn send_message(&mut self, body: String, cx: &mut ModelContext) -> Result<()> { let channel_id = self.details.id; let current_user_id = self.current_user_id()?; diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index d3b42604829bede5c973ec2a3298a1df6d6bff8d..03630ac6c8808857a82bbb54391f6e70f1f21d7d 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -99,6 +99,28 @@ impl ChatPanel { cx.notify(); } + fn render_channel_name(&self, cx: &RenderContext) -> ElementBox { + let settings = self.settings.borrow(); + let theme = &settings.theme.chat_panel; + if let Some((channel, _)) = self.active_channel.as_ref() { + let channel = channel.read(cx); + Flex::row() + .with_child( + Container::new( + Label::new("#".to_string(), theme.channel_name_hash.label.clone()).boxed(), + ) + .with_style(&theme.channel_name_hash.container) + .boxed(), + ) + .with_child( + Label::new(channel.name().to_string(), theme.channel_name.clone()).boxed(), + ) + .boxed() + } else { + Empty::new().boxed() + } + } + fn render_active_channel_messages(&self, cx: &RenderContext) -> ElementBox { let messages = if let Some((channel, _)) = self.active_channel.as_ref() { let channel = channel.read(cx); @@ -184,6 +206,7 @@ impl View for ChatPanel { let theme = &self.settings.borrow().theme; Container::new( Flex::column() + .with_child(self.render_channel_name(cx)) .with_child(self.render_active_channel_messages(cx)) .with_child(self.render_input_box()) .boxed(), diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 0346eba0e8086460a0f684c7c20f1be7383f5153..bb9b4b97abae2f5bf7d0910f7a219c8fd11c9a5a 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -62,6 +62,8 @@ pub struct ChatPanel { #[serde(flatten)] pub container: ContainerStyle, pub message: ChatMessage, + pub channel_name: TextStyle, + pub channel_name_hash: ContainedLabel, } #[derive(Deserialize)] From 1489c865e776e596958f49b8c6c5ba589406d159 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Aug 2021 17:11:33 -0700 Subject: [PATCH 112/204] Remove ui font fields from settings --- gpui/src/elements/line_box.rs | 54 ++++++++++++----------------------- zed/src/editor/display_map.rs | 2 -- zed/src/file_finder.rs | 3 +- zed/src/settings.rs | 4 --- zed/src/workspace/pane.rs | 4 +-- zed/src/workspace/sidebar.rs | 5 ++-- 6 files changed, 25 insertions(+), 47 deletions(-) diff --git a/gpui/src/elements/line_box.rs b/gpui/src/elements/line_box.rs index b6ce6d9e9544e274a60aa67783cad25577da2f9d..cf8f6ca605cf45272ab21b12a9f3f6810722a2bc 100644 --- a/gpui/src/elements/line_box.rs +++ b/gpui/src/elements/line_box.rs @@ -1,6 +1,5 @@ use crate::{ - font_cache::FamilyId, - fonts::Properties, + fonts::TextStyle, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -12,19 +11,12 @@ use crate::{ pub struct LineBox { child: ElementBox, - family_id: FamilyId, - font_size: f32, - font_properties: Properties, + style: TextStyle, } impl LineBox { - pub fn new(family_id: FamilyId, font_size: f32, child: ElementBox) -> Self { - Self { - child, - family_id, - font_size, - font_properties: Properties::default(), - } + pub fn new(child: ElementBox, style: TextStyle) -> Self { + Self { child, style } } } @@ -37,27 +29,21 @@ impl Element for LineBox { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - match cx + let line_height = cx .font_cache - .select_font(self.family_id, &self.font_properties) - { - Ok(font_id) => { - let line_height = cx.font_cache.line_height(font_id, self.font_size); - let character_height = cx.font_cache.ascent(font_id, self.font_size) - + cx.font_cache.descent(font_id, self.font_size); - let child_max = vec2f(constraint.max.x(), character_height); - let child_size = self.child.layout( - SizeConstraint::new(constraint.min.min(child_max), child_max), - cx, - ); - let size = vec2f(child_size.x(), line_height); - (size, (line_height - character_height) / 2.) - } - Err(error) => { - log::error!("can't find font for LineBox: {}", error); - (constraint.min, 0.0) - } - } + .line_height(self.style.font_id, self.style.font_size); + let character_height = cx + .font_cache + .ascent(self.style.font_id, self.style.font_size) + + cx.font_cache + .descent(self.style.font_id, self.style.font_size); + let child_max = vec2f(constraint.max.x(), character_height); + let child_size = self.child.layout( + SizeConstraint::new(constraint.min.min(child_max), child_max), + cx, + ); + let size = vec2f(child_size.x(), line_height); + (size, (line_height - character_height) / 2.) } fn paint( @@ -90,9 +76,7 @@ impl Element for LineBox { ) -> serde_json::Value { json!({ "bounds": bounds.to_json(), - "font_family": cx.font_cache.family_name(self.family_id).unwrap(), - "font_size": self.font_size, - "font_properties": self.font_properties.to_json(), + "style": self.style.to_json(), "child": self.child.debug(cx), }) } diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index e5f18b56dbdbd7f4c5dcdf79da4b0c3605f321d6..2758c030b3efc055ee450d058e8103460fc93563 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -531,9 +531,7 @@ mod tests { let settings = Settings { buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(), - ui_font_family: font_cache.load_family(&["Helvetica"]).unwrap(), buffer_font_size: 12.0, - ui_font_size: 12.0, tab_size: 4, ..cx.read(Settings::test) }; diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 76c2f71dc8c7a1a5c611c575d9c09f6a0cdd9992..3cf539909e2d5a6735ec72c943ef307694c5082f 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -162,11 +162,10 @@ impl FileFinder { .with_child( Container::new( LineBox::new( - settings.ui_font_family, - settings.ui_font_size, Svg::new("icons/file-16.svg") .with_color(style.label.text.color) .boxed(), + style.label.text.clone(), ) .boxed(), ) diff --git a/zed/src/settings.rs b/zed/src/settings.rs index dba30749b4c14422b829c13743b7187bbbc42bf1..1cadfdaa9c1f8fa7bc93449f590551ce0ebfe728 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -11,8 +11,6 @@ pub struct Settings { pub buffer_font_family: FamilyId, pub buffer_font_size: f32, pub tab_size: usize, - pub ui_font_family: FamilyId, - pub ui_font_size: f32, pub theme: Arc, } @@ -42,8 +40,6 @@ impl Settings { buffer_font_family: font_cache.load_family(&["Fira Code", "Monaco"])?, buffer_font_size: 14.0, tab_size: 4, - ui_font_family: font_cache.load_family(&["SF Pro", "Helvetica"])?, - ui_font_size: 12.0, theme, }) } diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 1be0fccd583df1c1d11e9515e605378de78a5f1e..7e6e2592bc15729142a09950e4841178084d353d 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -179,8 +179,8 @@ impl Pane { let settings = self.settings.borrow(); let theme = &settings.theme; let line_height = cx.font_cache().line_height( - cx.font_cache().default_font(settings.ui_font_family), - settings.ui_font_size, + theme.workspace.tab.label.text.font_id, + theme.workspace.tab.label.text.font_size, ); let mut row = Flex::row(); diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 8bb3f06fc1a013c8a59688e54aba0daee52446a6..242b557c39841687930413a3ab013b819e771403 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -61,9 +61,10 @@ impl Sidebar { pub fn render(&self, settings: &Settings, cx: &AppContext) -> ElementBox { let side = self.side; + let theme = &settings.theme; let line_height = cx.font_cache().line_height( - cx.font_cache().default_font(settings.ui_font_family), - settings.ui_font_size, + theme.workspace.tab.label.text.font_id, + theme.workspace.tab.label.text.font_size, ); Container::new( From 386631debf2319e3dd56f9783298923d0cb724c6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Aug 2021 10:01:44 +0200 Subject: [PATCH 113/204] Focus toggled elements when interacting with the sidebars Also, restore focus on the workspace when there is no active item on the sidebar that was just toggled. --- gpui/src/app.rs | 6 ++++++ zed/src/chat_panel.rs | 4 ++++ zed/src/workspace.rs | 5 +++++ zed/src/workspace/sidebar.rs | 2 +- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 1e531d3692ea41af3444e55e91d41c873cde568b..a37e6e415723e83f1872c1755b74e8a64638650d 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2857,6 +2857,12 @@ impl Clone for AnyViewHandle { } } +impl From<&AnyViewHandle> for AnyViewHandle { + fn from(handle: &AnyViewHandle) -> Self { + handle.clone() + } +} + impl From<&ViewHandle> for AnyViewHandle { fn from(handle: &ViewHandle) -> Self { handle diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 03630ac6c8808857a82bbb54391f6e70f1f21d7d..140ddcf28d09e4220d4e48e7233e4530d2871f5d 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -214,6 +214,10 @@ impl View for ChatPanel { .with_style(&theme.chat_panel.container) .boxed() } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.input_editor); + } } fn format_timestamp(mut timestamp: OffsetDateTime, mut now: OffsetDateTime) -> String { diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index e5785028a36e431cea19c91953e99a10d76c5437..2d7f44fd011c56721af7d50cd418bc0a9ebc6165 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -767,6 +767,11 @@ impl Workspace { Side::Right => &mut self.right_sidebar, }; sidebar.toggle_item(action.0.item_index); + if let Some(active_item) = sidebar.active_item() { + cx.focus(active_item); + } else { + cx.focus_self(); + } cx.notify(); } diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 242b557c39841687930413a3ab013b819e771403..f0bafd5472a71b796da24867f64b53b5bd04339b 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -49,7 +49,7 @@ impl Sidebar { if self.active_item_ix == Some(item_ix) { self.active_item_ix = None; } else { - self.active_item_ix = Some(item_ix) + self.active_item_ix = Some(item_ix); } } From 8a10234b14d41c951cfb941e77fefcb23021dae7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Aug 2021 14:30:08 +0200 Subject: [PATCH 114/204] Introduce `MouseEventHandler::on_drag` --- gpui/src/elements/mouse_event_handler.rs | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/gpui/src/elements/mouse_event_handler.rs b/gpui/src/elements/mouse_event_handler.rs index 629a9824ff59041295b685f77918ccd92271e3f1..48deef18a236242da86832829e98fc385b309534 100644 --- a/gpui/src/elements/mouse_event_handler.rs +++ b/gpui/src/elements/mouse_event_handler.rs @@ -9,12 +9,14 @@ pub struct MouseEventHandler { state: ValueHandle, child: ElementBox, click_handler: Option>, + drag_handler: Option>, } #[derive(Clone, Copy, Debug, Default)] pub struct MouseState { pub hovered: bool, pub clicked: bool, + prev_drag_position: Option, } impl MouseEventHandler { @@ -30,6 +32,7 @@ impl MouseEventHandler { state: state_handle, child, click_handler: None, + drag_handler: None, } } @@ -37,6 +40,11 @@ impl MouseEventHandler { self.click_handler = Some(Box::new(handler)); self } + + pub fn on_drag(mut self, handler: impl FnMut(Vector2F, &mut EventContext) + 'static) -> Self { + self.drag_handler = Some(Box::new(handler)); + self + } } impl Element for MouseEventHandler { @@ -69,6 +77,7 @@ impl Element for MouseEventHandler { cx: &mut EventContext, ) -> bool { let click_handler = self.click_handler.as_mut(); + let drag_handler = self.drag_handler.as_mut(); let handled_in_child = self.child.dispatch_event(event, cx); @@ -86,6 +95,7 @@ impl Element for MouseEventHandler { Event::LeftMouseDown { position, .. } => { if !handled_in_child && bounds.contains_point(*position) { state.clicked = true; + state.prev_drag_position = Some(*position); cx.notify(); true } else { @@ -93,6 +103,7 @@ impl Element for MouseEventHandler { } } Event::LeftMouseUp { position, .. } => { + state.prev_drag_position = None; if !handled_in_child && state.clicked { state.clicked = false; cx.notify(); @@ -106,6 +117,20 @@ impl Element for MouseEventHandler { handled_in_child } } + Event::LeftMouseDragged { position, .. } => { + if !handled_in_child && state.clicked { + let prev_drag_position = state.prev_drag_position.replace(*position); + if let Some((handler, prev_position)) = drag_handler.zip(prev_drag_position) { + let delta = *position - prev_position; + if !delta.is_zero() { + (handler)(delta, cx); + } + } + true + } else { + handled_in_child + } + } _ => handled_in_child, }) } From bed9d9c9d8f05da148ecb036feba53a451bc189b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Aug 2021 14:30:35 +0200 Subject: [PATCH 115/204] Add the ability to resize sidebar items --- zed/src/workspace.rs | 16 +++------- zed/src/workspace/sidebar.rs | 59 +++++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 2d7f44fd011c56721af7d50cd418bc0a9ebc6165..db35a977f1a174a246a8a410edca77b43ca752ff 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -960,20 +960,12 @@ impl View for Workspace { .with_child({ let mut content = Flex::row(); content.add_child(self.left_sidebar.render(&settings, cx)); - if let Some(panel) = self.left_sidebar.active_item() { - content.add_child( - ConstrainedBox::new(ChildView::new(panel.id()).boxed()) - .with_width(300.0) - .named("left panel"), - ); + if let Some(element) = self.left_sidebar.render_active_item(cx) { + content.add_child(element); } content.add_child(Expanded::new(1.0, self.center.render()).boxed()); - if let Some(panel) = self.right_sidebar.active_item() { - content.add_child( - ConstrainedBox::new(ChildView::new(panel.id()).boxed()) - .with_width(300.0) - .named("right panel"), - ); + if let Some(element) = self.right_sidebar.render_active_item(cx) { + content.add_child(element); } content.add_child(self.right_sidebar.render(&settings, cx)); content.boxed() diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index f0bafd5472a71b796da24867f64b53b5bd04339b..e6af4afeed8b8936d1fc84ad0a1a18cf3d7c9cac 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -1,16 +1,12 @@ use crate::Settings; -use gpui::{ - action, - elements::{ - Align, ConstrainedBox, Container, Flex, MouseEventHandler, ParentElement as _, Svg, - }, - AnyViewHandle, AppContext, Element as _, ElementBox, -}; +use gpui::{action, color::Color, elements::*, AnyViewHandle, AppContext, Border}; +use std::{cell::RefCell, rc::Rc}; pub struct Sidebar { side: Side, items: Vec, active_item_ix: Option, + width: Rc>, } #[derive(Clone, Copy)] @@ -38,6 +34,7 @@ impl Sidebar { side, items: Default::default(), active_item_ix: None, + width: Rc::new(RefCell::new(100.)), } } @@ -100,4 +97,52 @@ impl Sidebar { .with_style(&settings.theme.workspace.sidebar) .boxed() } + + pub fn render_active_item(&self, cx: &AppContext) -> Option { + if let Some(active_item) = self.active_item() { + let mut container = Flex::row(); + if matches!(self.side, Side::Right) { + container.add_child(self.render_resize_handle(cx)); + } + container.add_child( + ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) + .with_width(*self.width.borrow()) + .boxed(), + ); + if matches!(self.side, Side::Left) { + container.add_child(self.render_resize_handle(cx)); + } + Some(container.boxed()) + } else { + None + } + } + + fn render_resize_handle(&self, cx: &AppContext) -> ElementBox { + let width = self.width.clone(); + let side = self.side; + MouseEventHandler::new::(self.side.id(), cx, |_| { + Container::new(Empty::new().boxed()) + .with_border(Border::left(3., Color::white())) + .boxed() + }) + .on_drag(move |delta, cx| { + match side { + Side::Left => *width.borrow_mut() += delta.x(), + Side::Right => *width.borrow_mut() -= delta.x(), + } + + cx.notify(); + }) + .boxed() + } +} + +impl Side { + fn id(self) -> usize { + match self { + Side::Left => 0, + Side::Right => 1, + } + } } From 7f5cd017cbb2bbeff0609c04dead27695553a903 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Aug 2021 14:53:55 +0200 Subject: [PATCH 116/204] Fix potential deadlock when using `FontCache::em_width` --- gpui/src/font_cache.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gpui/src/font_cache.rs b/gpui/src/font_cache.rs index b1deb802fd5bb18b83023a1be71910a53a8626de..3c11b9659cb26441ab7746bac6a6f306fdbcef42 100644 --- a/gpui/src/font_cache.rs +++ b/gpui/src/font_cache.rs @@ -147,9 +147,13 @@ impl FontCache { } pub fn em_width(&self, font_id: FontId, font_size: f32) -> f32 { - let state = self.0.read(); - let glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap(); - let bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap(); + let glyph_id; + let bounds; + { + let state = self.0.read(); + 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) } From 30ce7f6122e1d5c7cbef06e668189bad5023d058 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Aug 2021 14:57:47 +0200 Subject: [PATCH 117/204] Allow styling sidebar's resize handle --- zed/assets/themes/_base.toml | 5 ++++- zed/src/theme.rs | 8 +++++++- zed/src/workspace.rs | 8 ++++++-- zed/src/workspace/sidebar.rs | 14 +++++++------- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 08d618aaed03999d9e799ed3e8e1ce9dec6fd953..8900c34fb0390982015bb59ddb4729fc1c620798 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -13,9 +13,12 @@ extends = "$workspace.tab" background = "$surface.1" text = "$text.0" -[workspace.sidebar] +[workspace.sidebar.icons] padding = { left = 10, right = 10 } +[workspace.sidebar.resize_handle] +margin = { left = 6 } + [workspace.sidebar_icon] color = "$text.2.color" diff --git a/zed/src/theme.rs b/zed/src/theme.rs index bb9b4b97abae2f5bf7d0910f7a219c8fd11c9a5a..3ecdb94baaa0369845caa5cf917f446f7e152b55 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -36,7 +36,7 @@ pub struct Workspace { pub background: Color, pub tab: Tab, pub active_tab: Tab, - pub sidebar: ContainerStyle, + pub sidebar: Sidebar, pub sidebar_icon: SidebarIcon, pub active_sidebar_icon: SidebarIcon, } @@ -52,6 +52,12 @@ pub struct Tab { pub icon_conflict: Color, } +#[derive(Deserialize)] +pub struct Sidebar { + pub icons: ContainerStyle, + pub resize_handle: ContainerStyle, +} + #[derive(Deserialize)] pub struct SidebarIcon { pub color: Color, diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index db35a977f1a174a246a8a410edca77b43ca752ff..5e10b551f66f665f5dba2a96110bb9c4b07b6d30 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -960,11 +960,15 @@ impl View for Workspace { .with_child({ let mut content = Flex::row(); content.add_child(self.left_sidebar.render(&settings, cx)); - if let Some(element) = self.left_sidebar.render_active_item(cx) { + if let Some(element) = + self.left_sidebar.render_active_item(&settings, cx) + { content.add_child(element); } content.add_child(Expanded::new(1.0, self.center.render()).boxed()); - if let Some(element) = self.right_sidebar.render_active_item(cx) { + if let Some(element) = + self.right_sidebar.render_active_item(&settings, cx) + { content.add_child(element); } content.add_child(self.right_sidebar.render(&settings, cx)); diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index e6af4afeed8b8936d1fc84ad0a1a18cf3d7c9cac..fd660bfdbbc8bed5f6e740e9f28025f0232a890a 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -1,5 +1,5 @@ use crate::Settings; -use gpui::{action, color::Color, elements::*, AnyViewHandle, AppContext, Border}; +use gpui::{action, elements::*, AnyViewHandle, AppContext}; use std::{cell::RefCell, rc::Rc}; pub struct Sidebar { @@ -94,15 +94,15 @@ impl Sidebar { })) .boxed(), ) - .with_style(&settings.theme.workspace.sidebar) + .with_style(&settings.theme.workspace.sidebar.icons) .boxed() } - pub fn render_active_item(&self, cx: &AppContext) -> Option { + pub fn render_active_item(&self, settings: &Settings, cx: &AppContext) -> Option { if let Some(active_item) = self.active_item() { let mut container = Flex::row(); if matches!(self.side, Side::Right) { - container.add_child(self.render_resize_handle(cx)); + container.add_child(self.render_resize_handle(settings, cx)); } container.add_child( ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) @@ -110,7 +110,7 @@ impl Sidebar { .boxed(), ); if matches!(self.side, Side::Left) { - container.add_child(self.render_resize_handle(cx)); + container.add_child(self.render_resize_handle(settings, cx)); } Some(container.boxed()) } else { @@ -118,12 +118,12 @@ impl Sidebar { } } - fn render_resize_handle(&self, cx: &AppContext) -> ElementBox { + fn render_resize_handle(&self, settings: &Settings, cx: &AppContext) -> ElementBox { let width = self.width.clone(); let side = self.side; MouseEventHandler::new::(self.side.id(), cx, |_| { Container::new(Empty::new().boxed()) - .with_border(Border::left(3., Color::white())) + .with_style(&settings.theme.workspace.sidebar.resize_handle) .boxed() }) .on_drag(move |delta, cx| { From 2cd21d1da85babd94d481378f18cd403190dd829 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Aug 2021 16:30:43 +0200 Subject: [PATCH 118/204] Ensure sidebar width never goes below 0 --- zed/src/workspace/sidebar.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index fd660bfdbbc8bed5f6e740e9f28025f0232a890a..d726386801fb00e847436637b167220bf45698e8 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -127,9 +127,10 @@ impl Sidebar { .boxed() }) .on_drag(move |delta, cx| { + let prev_width = *width.borrow(); match side { - Side::Left => *width.borrow_mut() += delta.x(), - Side::Right => *width.borrow_mut() -= delta.x(), + Side::Left => *width.borrow_mut() = 0f32.max(prev_width + delta.x()), + Side::Right => *width.borrow_mut() = 0f32.max(prev_width - delta.x()), } cx.notify(); From a9963f1b32bd99240160a306785f759dd75e338a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Aug 2021 16:31:12 +0200 Subject: [PATCH 119/204] Synthesize a mouse moved event also when the mouse is dragged --- gpui/src/presenter.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 6e3487d76d3501e11ed31095b1f8b0b0ebacf8a6..d03e6462790b72ec2a55422e8cb44c087aa48767 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -139,8 +139,11 @@ impl Presenter { pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { - if matches!(event, Event::MouseMoved { .. }) { - self.last_mouse_moved_event = Some(event.clone()); + match event { + Event::MouseMoved { position, .. } | Event::LeftMouseDragged { position } => { + self.last_mouse_moved_event = Some(Event::MouseMoved { position }); + } + _ => {} } let mut event_cx = EventContext { From d5b7e2d4e35d22a33c9899f4f1b2ec7159001ec7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Aug 2021 17:25:13 +0200 Subject: [PATCH 120/204] Pass a MutableAppContext in `render` Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 120 ++++++++++++++++++++++-------------------- gpui/src/presenter.rs | 10 ++-- 2 files changed, 69 insertions(+), 61 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index a37e6e415723e83f1872c1755b74e8a64638650d..0aa85a33b05260e8e0daa0cb35b6ae721c15f305 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -806,12 +806,49 @@ impl MutableAppContext { self.cx.focused_view_id(window_id) } + pub fn render_view( + &mut self, + window_id: usize, + view_id: usize, + titlebar_height: f32, + refreshing: bool, + ) -> Result { + let view = self + .cx + .views + .remove(&(window_id, view_id)) + .ok_or(anyhow!("view not found"))?; + let element = view.render(window_id, view_id, titlebar_height, refreshing, self); + self.cx.views.insert((window_id, view_id), view); + Ok(element) + } + pub fn render_views( - &self, + &mut self, window_id: usize, titlebar_height: f32, ) -> HashMap { - self.cx.render_views(window_id, titlebar_height) + let view_ids = self + .views + .keys() + .filter_map(|(win_id, view_id)| { + if *win_id == window_id { + Some(*view_id) + } else { + None + } + }) + .collect::>(); + view_ids + .into_iter() + .map(|view_id| { + ( + view_id, + self.render_view(window_id, view_id, titlebar_height, false) + .unwrap(), + ) + }) + .collect() } pub fn update T>(&mut self, callback: F) -> T { @@ -1204,7 +1241,7 @@ impl MutableAppContext { }); } - pub fn build_presenter(&self, window_id: usize, titlebar_height: f32) -> Presenter { + pub fn build_presenter(&mut self, window_id: usize, titlebar_height: f32) -> Presenter { Presenter::new( window_id, titlebar_height, @@ -1215,6 +1252,23 @@ impl MutableAppContext { ) } + pub fn render_cx( + &mut self, + window_id: usize, + view_id: usize, + titlebar_height: f32, + refreshing: bool, + ) -> RenderContext { + RenderContext { + app: self, + titlebar_height, + refreshing, + window_id, + view_id, + view_type: PhantomData, + } + } + pub fn add_view(&mut self, window_id: usize, build_view: F) -> ViewHandle where T: View, @@ -1357,7 +1411,7 @@ impl MutableAppContext { { { let mut presenter = presenter.borrow_mut(); - presenter.invalidate(invalidation, self.as_ref()); + presenter.invalidate(invalidation, self); let scene = presenter.build_scene(window.size(), window.scale_factor(), self); window.present_scene(scene); } @@ -1382,7 +1436,7 @@ impl MutableAppContext { .invalidation .take(); let mut presenter = presenter.borrow_mut(); - presenter.refresh(invalidation, self.as_ref()); + presenter.refresh(invalidation, self); let scene = presenter.build_scene(window.size(), window.scale_factor(), self); window.present_scene(scene); } @@ -1633,56 +1687,6 @@ impl AppContext { .map(|window| window.focused_view_id) } - pub fn render_view( - &self, - window_id: usize, - view_id: usize, - titlebar_height: f32, - refreshing: bool, - ) -> Result { - self.views - .get(&(window_id, view_id)) - .map(|v| v.render(window_id, view_id, titlebar_height, refreshing, self)) - .ok_or(anyhow!("view not found")) - } - - pub fn render_views( - &self, - window_id: usize, - titlebar_height: f32, - ) -> HashMap { - self.views - .iter() - .filter_map(|((win_id, view_id), view)| { - if *win_id == window_id { - Some(( - *view_id, - view.render(*win_id, *view_id, titlebar_height, false, self), - )) - } else { - None - } - }) - .collect::>() - } - - pub fn render_cx( - &self, - window_id: usize, - view_id: usize, - titlebar_height: f32, - refreshing: bool, - ) -> RenderContext { - RenderContext { - app: self, - titlebar_height, - refreshing, - window_id, - view_id, - view_type: PhantomData, - } - } - pub fn background(&self) -> &Arc { &self.background } @@ -1834,7 +1838,7 @@ pub trait AnyView { view_id: usize, titlebar_height: f32, refreshing: bool, - cx: &AppContext, + cx: &mut MutableAppContext, ) -> ElementBox; fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); @@ -1867,7 +1871,7 @@ where view_id: usize, titlebar_height: f32, refreshing: bool, - cx: &AppContext, + cx: &mut MutableAppContext, ) -> ElementBox { View::render( self, @@ -2244,7 +2248,7 @@ impl<'a, T: View> ViewContext<'a, T> { } pub struct RenderContext<'a, T: View> { - pub app: &'a AppContext, + pub app: &'a mut MutableAppContext, pub titlebar_height: f32, pub refreshing: bool, window_id: usize, diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index d03e6462790b72ec2a55422e8cb44c087aa48767..06d16269199762ebd5b37324234f1a03c90d335a 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -32,7 +32,7 @@ impl Presenter { font_cache: Arc, text_layout_cache: TextLayoutCache, asset_cache: Arc, - cx: &MutableAppContext, + cx: &mut MutableAppContext, ) -> Self { Self { window_id, @@ -57,7 +57,7 @@ impl Presenter { path } - pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &AppContext) { + pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &mut MutableAppContext) { for view_id in invalidation.removed { invalidation.updated.remove(&view_id); self.rendered_views.remove(&view_id); @@ -72,7 +72,11 @@ impl Presenter { } } - pub fn refresh(&mut self, invalidation: Option, cx: &AppContext) { + pub fn refresh( + &mut self, + invalidation: Option, + cx: &mut MutableAppContext, + ) { if let Some(invalidation) = invalidation { for view_id in invalidation.removed { self.rendered_views.remove(&view_id); From 53dc08dfc55e5b4d55ef71196d63afc7791ec26f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Aug 2021 18:04:21 +0200 Subject: [PATCH 121/204] Remove lock from element states Co-Authored-By: Nathan Sobo --- gpui/examples/text.rs | 2 +- gpui/src/app.rs | 156 ++++++++++++----------- gpui/src/elements/list.rs | 2 +- gpui/src/elements/mouse_event_handler.rs | 18 +-- gpui/src/presenter.rs | 15 +++ server/src/rpc.rs | 2 +- zed/src/chat_panel.rs | 6 +- zed/src/editor.rs | 2 +- zed/src/file_finder.rs | 2 +- zed/src/project_browser.rs | 2 +- zed/src/theme_selector.rs | 4 +- zed/src/workspace.rs | 2 +- zed/src/workspace/pane.rs | 13 +- zed/src/workspace/sidebar.rs | 21 ++- 14 files changed, 143 insertions(+), 104 deletions(-) diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index 1d092a0da7eb7baea312ff8b09ad78a3acbcf457..b76d31622d659ed30ac649d9413151de0b44afcb 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -28,7 +28,7 @@ impl gpui::View for TextView { "View" } - fn render(&self, _: &gpui::RenderContext) -> gpui::ElementBox { + fn render(&self, _: &mut gpui::RenderContext) -> gpui::ElementBox { TextElement.boxed() } } diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 0aa85a33b05260e8e0daa0cb35b6ae721c15f305..4582dc0727e12acdd87e9c67052d5b7394e24881 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -5,13 +5,12 @@ use crate::{ platform::{self, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::{post_inc, timeout}, - AssetCache, AssetSource, ClipboardItem, EventContext, FontCache, PathPromptOptions, - TextLayoutCache, + AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; use async_task::Task; use keymap::MatchResult; -use parking_lot::{Mutex, RwLock}; +use parking_lot::Mutex; use platform::Event; use postage::{mpsc, sink::Sink as _, stream::Stream as _}; use smol::prelude::*; @@ -38,7 +37,7 @@ pub trait Entity: 'static { pub trait View: Entity + Sized { fn ui_name() -> &'static str; - fn render(&self, cx: &RenderContext<'_, Self>) -> ElementBox; + fn render(&self, cx: &mut RenderContext<'_, Self>) -> ElementBox; fn on_focus(&mut self, _: &mut ViewContext) {} fn on_blur(&mut self, _: &mut ViewContext) {} fn keymap_context(&self, _: &AppContext) -> keymap::Context { @@ -681,7 +680,7 @@ impl MutableAppContext { models: Default::default(), views: Default::default(), windows: Default::default(), - values: Default::default(), + element_states: Default::default(), ref_counts: Arc::new(Mutex::new(RefCounts::default())), background, font_cache, @@ -1308,11 +1307,26 @@ impl MutableAppContext { handle } + pub fn element_state( + &mut self, + id: usize, + ) -> ElementStateHandle { + let key = (TypeId::of::(), id); + self.cx + .element_states + .entry(key) + .or_insert_with(|| Box::new(T::default())); + ElementStateHandle::new(TypeId::of::(), id, &self.cx.ref_counts) + } + fn remove_dropped_entities(&mut self) { loop { - let (dropped_models, dropped_views, dropped_values) = + let (dropped_models, dropped_views, dropped_element_states) = self.cx.ref_counts.lock().take_dropped(); - if dropped_models.is_empty() && dropped_views.is_empty() && dropped_values.is_empty() { + if dropped_models.is_empty() + && dropped_views.is_empty() + && dropped_element_states.is_empty() + { break; } @@ -1346,9 +1360,8 @@ impl MutableAppContext { } } - let mut values = self.cx.values.write(); - for key in dropped_values { - values.remove(&key); + for key in dropped_element_states { + self.cx.element_states.remove(&key); } } } @@ -1667,7 +1680,7 @@ pub struct AppContext { models: HashMap>, views: HashMap<(usize, usize), Box>, windows: HashMap, - values: RwLock>>, + element_states: HashMap<(TypeId, usize), Box>, background: Arc, ref_counts: Arc>, font_cache: Arc, @@ -1698,15 +1711,6 @@ impl AppContext { pub fn platform(&self) -> &Arc { &self.platform } - - pub fn value(&self, id: usize) -> ValueHandle { - let key = (TypeId::of::(), id); - self.values - .write() - .entry(key) - .or_insert_with(|| Box::new(T::default())); - ValueHandle::new(TypeId::of::(), id, &self.ref_counts) - } } impl ReadModel for AppContext { @@ -1875,7 +1879,7 @@ where ) -> ElementBox { View::render( self, - &RenderContext { + &mut RenderContext { window_id, view_id, app: cx, @@ -2269,10 +2273,16 @@ impl AsRef for &AppContext { } impl Deref for RenderContext<'_, V> { - type Target = AppContext; + type Target = MutableAppContext; fn deref(&self) -> &Self::Target { - &self.app + self.app + } +} + +impl DerefMut for RenderContext<'_, V> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.app } } @@ -2964,16 +2974,16 @@ impl Clone for WeakViewHandle { } } -pub struct ValueHandle { +pub struct ElementStateHandle { value_type: PhantomData, tag_type_id: TypeId, id: usize, ref_counts: Weak>, } -impl ValueHandle { +impl ElementStateHandle { fn new(tag_type_id: TypeId, id: usize, ref_counts: &Arc>) -> Self { - ref_counts.lock().inc_value(tag_type_id, id); + ref_counts.lock().inc_element_state(tag_type_id, id); Self { value_type: PhantomData, tag_type_id, @@ -2982,41 +2992,39 @@ impl ValueHandle { } } - pub fn read(&self, cx: &AppContext, f: impl FnOnce(&T) -> R) -> R { - f(cx.values - .read() + pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T { + cx.element_states .get(&(self.tag_type_id, self.id)) .unwrap() .downcast_ref() - .unwrap()) + .unwrap() } - pub fn update( - &self, - cx: &mut EventContext, - f: impl FnOnce(&mut T, &mut EventContext) -> R, - ) -> R { - let mut value = cx - .app + pub fn update(&self, cx: &mut C, f: impl FnOnce(&mut T, &mut C) -> R) -> R + where + C: DerefMut, + { + let mut element_state = cx + .deref_mut() .cx - .values - .write() + .element_states .remove(&(self.tag_type_id, self.id)) .unwrap(); - let result = f(value.downcast_mut().unwrap(), cx); - cx.app + let result = f(element_state.downcast_mut().unwrap(), cx); + cx.deref_mut() .cx - .values - .write() - .insert((self.tag_type_id, self.id), value); + .element_states + .insert((self.tag_type_id, self.id), element_state); result } } -impl Drop for ValueHandle { +impl Drop for ElementStateHandle { fn drop(&mut self) { if let Some(ref_counts) = self.ref_counts.upgrade() { - ref_counts.lock().dec_value(self.tag_type_id, self.id); + ref_counts + .lock() + .dec_element_state(self.tag_type_id, self.id); } } } @@ -3080,10 +3088,10 @@ impl Drop for Subscription { #[derive(Default)] struct RefCounts { entity_counts: HashMap, - value_counts: HashMap<(TypeId, usize), usize>, + element_state_counts: HashMap<(TypeId, usize), usize>, dropped_models: HashSet, dropped_views: HashSet<(usize, usize)>, - dropped_values: HashSet<(TypeId, usize)>, + dropped_element_states: HashSet<(TypeId, usize)>, } impl RefCounts { @@ -3107,8 +3115,11 @@ impl RefCounts { } } - fn inc_value(&mut self, tag_type_id: TypeId, id: usize) { - *self.value_counts.entry((tag_type_id, id)).or_insert(0) += 1; + fn inc_element_state(&mut self, tag_type_id: TypeId, id: usize) { + *self + .element_state_counts + .entry((tag_type_id, id)) + .or_insert(0) += 1; } fn dec_model(&mut self, model_id: usize) { @@ -3129,13 +3140,13 @@ impl RefCounts { } } - fn dec_value(&mut self, tag_type_id: TypeId, id: usize) { + fn dec_element_state(&mut self, tag_type_id: TypeId, id: usize) { let key = (tag_type_id, id); - let count = self.value_counts.get_mut(&key).unwrap(); + let count = self.element_state_counts.get_mut(&key).unwrap(); *count -= 1; if *count == 0 { - self.value_counts.remove(&key); - self.dropped_values.insert(key); + self.element_state_counts.remove(&key); + self.dropped_element_states.insert(key); } } @@ -3152,11 +3163,14 @@ impl RefCounts { ) { let mut dropped_models = HashSet::new(); let mut dropped_views = HashSet::new(); - let mut dropped_values = HashSet::new(); + let mut dropped_element_states = HashSet::new(); std::mem::swap(&mut self.dropped_models, &mut dropped_models); std::mem::swap(&mut self.dropped_views, &mut dropped_views); - std::mem::swap(&mut self.dropped_values, &mut dropped_values); - (dropped_models, dropped_views, dropped_values) + std::mem::swap( + &mut self.dropped_element_states, + &mut dropped_element_states, + ); + (dropped_models, dropped_views, dropped_element_states) } } @@ -3314,7 +3328,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3378,7 +3392,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { let mouse_down_count = self.mouse_down_count.clone(); EventHandler::new(Empty::new().boxed()) .on_mouse_down(move |_| { @@ -3440,7 +3454,7 @@ mod tests { "View" } - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3480,7 +3494,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3536,7 +3550,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3586,7 +3600,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3630,7 +3644,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3677,7 +3691,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3735,7 +3749,7 @@ mod tests { } impl View for ViewA { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3753,7 +3767,7 @@ mod tests { } impl View for ViewB { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3849,7 +3863,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3984,7 +3998,7 @@ mod tests { "test view" } - fn render(&self, _: &RenderContext) -> ElementBox { + fn render(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -4029,7 +4043,7 @@ mod tests { "test view" } - fn render(&self, _: &RenderContext) -> ElementBox { + fn render(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -4052,7 +4066,7 @@ mod tests { "test view" } - fn render(&self, _: &RenderContext) -> ElementBox { + fn render(&self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 6e015b721c1bf2e53adb68f70342f674c3123c0f..b6773091c0b1c319280e08b584d6a823c3ddeef6 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -496,7 +496,7 @@ mod tests { "TestView" } - fn render(&self, _: &RenderContext<'_, Self>) -> ElementBox { + fn render(&self, _: &mut RenderContext<'_, Self>) -> ElementBox { unimplemented!() } } diff --git a/gpui/src/elements/mouse_event_handler.rs b/gpui/src/elements/mouse_event_handler.rs index 48deef18a236242da86832829e98fc385b309534..7512fe43b4e5b8afa16ce36db5565d54ed7f4f84 100644 --- a/gpui/src/elements/mouse_event_handler.rs +++ b/gpui/src/elements/mouse_event_handler.rs @@ -1,12 +1,14 @@ +use std::ops::DerefMut; + use crate::{ geometry::{rect::RectF, vector::Vector2F}, - AppContext, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, - PaintContext, SizeConstraint, ValueHandle, + DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, LayoutContext, + MutableAppContext, PaintContext, SizeConstraint, }; use serde_json::json; pub struct MouseEventHandler { - state: ValueHandle, + state: ElementStateHandle, child: ElementBox, click_handler: Option>, drag_handler: Option>, @@ -20,14 +22,14 @@ pub struct MouseState { } impl MouseEventHandler { - pub fn new(id: usize, cx: &AppContext, render_child: F) -> Self + pub fn new(id: usize, cx: &mut C, render_child: F) -> Self where Tag: 'static, - F: FnOnce(MouseState) -> ElementBox, + F: FnOnce(&MouseState, &mut C) -> ElementBox, + C: DerefMut, { - let state_handle = cx.value::(id); - let state = state_handle.read(cx.as_ref(), |state| *state); - let child = render_child(state); + let state_handle = cx.element_state::(id); + let child = state_handle.update(cx, |state, cx| render_child(state, cx)); Self { state: state_handle, child, diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 06d16269199762ebd5b37324234f1a03c90d335a..92f47b0399dbac2329c7440e8531add09fb9a4b8 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -11,6 +11,7 @@ use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; use std::{ collections::{HashMap, HashSet}, + ops::{Deref, DerefMut}, sync::Arc, }; @@ -269,6 +270,20 @@ impl<'a> EventContext<'a> { } } +impl<'a> Deref for EventContext<'a> { + type Target = MutableAppContext; + + fn deref(&self) -> &Self::Target { + self.app + } +} + +impl<'a> DerefMut for EventContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.app + } +} + pub struct DebugContext<'a> { rendered_views: &'a HashMap, pub font_cache: &'a FontCache, diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 41f5efb51b043d908849244cc0a28d8614ab1318..00464450fab56b2dcde76afdccdfce083e250504 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1620,7 +1620,7 @@ mod tests { "empty view" } - fn render<'a>(&self, _: &gpui::RenderContext) -> gpui::ElementBox { + fn render(&self, _: &mut gpui::RenderContext) -> gpui::ElementBox { gpui::Element::boxed(gpui::elements::Empty) } } diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 140ddcf28d09e4220d4e48e7233e4530d2871f5d..37409800feaad2beb810cc87dff5e5df5e062a7b 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -99,7 +99,7 @@ impl ChatPanel { cx.notify(); } - fn render_channel_name(&self, cx: &RenderContext) -> ElementBox { + fn render_channel_name(&self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); let theme = &settings.theme.chat_panel; if let Some((channel, _)) = self.active_channel.as_ref() { @@ -121,7 +121,7 @@ impl ChatPanel { } } - fn render_active_channel_messages(&self, cx: &RenderContext) -> ElementBox { + fn render_active_channel_messages(&self, cx: &mut RenderContext) -> ElementBox { let messages = if let Some((channel, _)) = self.active_channel.as_ref() { let channel = channel.read(cx); let now = OffsetDateTime::now_utc(); @@ -202,7 +202,7 @@ impl View for ChatPanel { "ChatPanel" } - fn render(&self, cx: &RenderContext) -> ElementBox { + fn render(&self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; Container::new( Flex::column() diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 2e220592bd7b012334f8edf5af8e67a94ee13070..e076c22ae4dbf4808c54f388d14a15c7e901d2d4 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -2522,7 +2522,7 @@ impl Entity for Editor { } impl View for Editor { - fn render<'a>(&self, _: &RenderContext) -> ElementBox { + fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { EditorElement::new(self.handle.clone()).boxed() } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 3cf539909e2d5a6735ec72c943ef307694c5082f..464b7e3589bad1b36e9c61bbe592aa5041915e9c 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -79,7 +79,7 @@ impl View for FileFinder { "FileFinder" } - fn render(&self, _: &RenderContext) -> ElementBox { + fn render(&self, _: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); Align::new( diff --git a/zed/src/project_browser.rs b/zed/src/project_browser.rs index 552eab851cb143b17070ba6e058bbe141ef4118c..5793b756188ad9dbe0cbfdcfbb2ca40242da88ff 100644 --- a/zed/src/project_browser.rs +++ b/zed/src/project_browser.rs @@ -13,7 +13,7 @@ impl View for ProjectBrowser { "ProjectBrowser" } - fn render(&self, _: &gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + fn render(&self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { Empty::new().boxed() } } diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 2b0eea969e98c725bf3b49138fe894ee1ec30fd1..a20ea8f75877b7ff9ac0ed47f84672728e34f690 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -200,7 +200,7 @@ impl ThemeSelector { } } - fn render_matches(&self, cx: &RenderContext) -> ElementBox { + fn render_matches(&self, cx: &mut RenderContext) -> ElementBox { if self.matches.is_empty() { let settings = self.settings.borrow(); return Container::new( @@ -269,7 +269,7 @@ impl View for ThemeSelector { "ThemeSelector" } - fn render(&self, cx: &RenderContext) -> ElementBox { + fn render(&self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); Align::new( diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 5e10b551f66f665f5dba2a96110bb9c4b07b6d30..4d6e0b6b754fd050f44d6fe3bd236827ba9ab8d0 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -944,7 +944,7 @@ impl View for Workspace { "Workspace" } - fn render(&self, cx: &RenderContext) -> ElementBox { + fn render(&self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); Container::new( Flex::column() diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 7e6e2592bc15729142a09950e4841178084d353d..34f44253f32b1b3bce3d1c1955417b1c39f57ef1 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -6,8 +6,7 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, - AppContext, Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, - ViewHandle, + Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle, }; use postage::watch; use std::{cmp, path::Path, sync::Arc}; @@ -175,7 +174,7 @@ impl Pane { cx.emit(Event::Split(direction)); } - fn render_tabs(&self, cx: &AppContext) -> ElementBox { + fn render_tabs(&self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); let theme = &settings.theme; let line_height = cx.font_cache().line_height( @@ -194,7 +193,7 @@ impl Pane { row.add_child( Expanded::new( 1.0, - MouseEventHandler::new::(item.id(), cx, |mouse_state| { + MouseEventHandler::new::(item.id(), cx, |mouse_state, cx| { let title = item.title(cx); let mut border = border.clone(); @@ -299,7 +298,7 @@ impl Pane { is_dirty: bool, has_conflict: bool, theme: &theme::Theme, - cx: &AppContext, + cx: &mut RenderContext, ) -> ElementBox { enum TabCloseButton {} @@ -318,7 +317,7 @@ impl Pane { let close_color = current_color.unwrap_or(theme.workspace.tab.icon_close); let icon = Svg::new("icons/x.svg").with_color(close_color); - MouseEventHandler::new::(item_id, cx, |mouse_state| { + MouseEventHandler::new::(item_id, cx, |mouse_state, _| { if mouse_state.hovered { Container::new(icon.with_color(Color::white()).boxed()) .with_background_color(if mouse_state.clicked { @@ -370,7 +369,7 @@ impl View for Pane { "Pane" } - fn render<'a>(&self, cx: &RenderContext) -> ElementBox { + fn render(&self, cx: &mut RenderContext) -> ElementBox { if let Some(active_item) = self.active_item() { Flex::column() .with_child(self.render_tabs(cx)) diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index d726386801fb00e847436637b167220bf45698e8..3c02907067f6e2cb4aa4b04496d9e2c08ca8ab05 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -1,5 +1,6 @@ +use super::Workspace; use crate::Settings; -use gpui::{action, elements::*, AnyViewHandle, AppContext}; +use gpui::{action, elements::*, AnyViewHandle, MutableAppContext, RenderContext}; use std::{cell::RefCell, rc::Rc}; pub struct Sidebar { @@ -56,7 +57,7 @@ impl Sidebar { .map(|item| &item.view) } - pub fn render(&self, settings: &Settings, cx: &AppContext) -> ElementBox { + pub fn render(&self, settings: &Settings, cx: &mut RenderContext) -> ElementBox { let side = self.side; let theme = &settings.theme; let line_height = cx.font_cache().line_height( @@ -73,7 +74,7 @@ impl Sidebar { &settings.theme.workspace.sidebar_icon }; enum SidebarButton {} - MouseEventHandler::new::(item.view.id(), cx, |_| { + MouseEventHandler::new::(item.view.id(), cx, |_, _| { ConstrainedBox::new( Align::new( ConstrainedBox::new( @@ -98,7 +99,11 @@ impl Sidebar { .boxed() } - pub fn render_active_item(&self, settings: &Settings, cx: &AppContext) -> Option { + pub fn render_active_item( + &self, + settings: &Settings, + cx: &mut MutableAppContext, + ) -> Option { if let Some(active_item) = self.active_item() { let mut container = Flex::row(); if matches!(self.side, Side::Right) { @@ -118,10 +123,14 @@ impl Sidebar { } } - fn render_resize_handle(&self, settings: &Settings, cx: &AppContext) -> ElementBox { + fn render_resize_handle( + &self, + settings: &Settings, + mut cx: &mut MutableAppContext, + ) -> ElementBox { let width = self.width.clone(); let side = self.side; - MouseEventHandler::new::(self.side.id(), cx, |_| { + MouseEventHandler::new::(self.side.id(), &mut cx, |_, _| { Container::new(Empty::new().boxed()) .with_style(&settings.theme.workspace.sidebar.resize_handle) .boxed() From 5e6e0c68cd097bee55751b60b857f33312bcf2f7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 27 Aug 2021 19:01:49 +0200 Subject: [PATCH 122/204] Allow styling the cursor in `MouseEventHandler` Co-Authored-By: Max Brunsfeld Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 33 ++++++++++++++++++++++-- gpui/src/elements/mouse_event_handler.rs | 28 +++++++++++++++++--- gpui/src/platform.rs | 9 +++++++ gpui/src/platform/mac/platform.rs | 16 +++++++++++- gpui/src/platform/test.rs | 7 +++++ zed/src/workspace/sidebar.rs | 6 ++++- 6 files changed, 92 insertions(+), 7 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 4582dc0727e12acdd87e9c67052d5b7394e24881..d9caf3e6d2b9b516c09a427dffacaf48be66daaf 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2,7 +2,7 @@ use crate::{ elements::ElementBox, executor, keymap::{self, Keystroke}, - platform::{self, Platform, PromptLevel, WindowOptions}, + platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::{post_inc, timeout}, AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, @@ -25,7 +25,10 @@ use std::{ ops::{Deref, DerefMut}, path::{Path, PathBuf}, rc::{self, Rc}, - sync::{Arc, Weak}, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, Weak, + }, time::Duration, }; @@ -661,6 +664,7 @@ pub struct MutableAppContext { pending_effects: VecDeque, pending_flushes: usize, flushing_effects: bool, + next_cursor_style_handle_id: Arc, } impl MutableAppContext { @@ -700,6 +704,7 @@ impl MutableAppContext { pending_effects: VecDeque::new(), pending_flushes: 0, flushing_effects: false, + next_cursor_style_handle_id: Default::default(), } } @@ -1456,6 +1461,16 @@ impl MutableAppContext { self.presenters_and_platform_windows = presenters; } + pub fn set_cursor_style(&mut self, style: CursorStyle) -> CursorStyleHandle { + self.platform.set_cursor_style(style); + let id = self.next_cursor_style_handle_id.fetch_add(1, SeqCst); + CursorStyleHandle { + id, + next_cursor_style_handle_id: self.next_cursor_style_handle_id.clone(), + platform: self.platform(), + } + } + fn emit_event(&mut self, entity_id: usize, payload: Box) { let callbacks = self.subscriptions.lock().remove(&entity_id); if let Some(callbacks) = callbacks { @@ -3029,6 +3044,20 @@ impl Drop for ElementStateHandle { } } +pub struct CursorStyleHandle { + id: usize, + next_cursor_style_handle_id: Arc, + platform: Arc, +} + +impl Drop for CursorStyleHandle { + fn drop(&mut self) { + if self.id + 1 == self.next_cursor_style_handle_id.load(SeqCst) { + self.platform.set_cursor_style(CursorStyle::Arrow); + } + } +} + #[must_use] pub enum Subscription { Subscription { diff --git a/gpui/src/elements/mouse_event_handler.rs b/gpui/src/elements/mouse_event_handler.rs index 7512fe43b4e5b8afa16ce36db5565d54ed7f4f84..997331545436b53f6b33a32211f5033f2f57e62f 100644 --- a/gpui/src/elements/mouse_event_handler.rs +++ b/gpui/src/elements/mouse_event_handler.rs @@ -2,23 +2,26 @@ use std::ops::DerefMut; use crate::{ geometry::{rect::RectF, vector::Vector2F}, - DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, LayoutContext, - MutableAppContext, PaintContext, SizeConstraint, + platform::CursorStyle, + CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, + LayoutContext, MutableAppContext, PaintContext, SizeConstraint, }; use serde_json::json; pub struct MouseEventHandler { state: ElementStateHandle, child: ElementBox, + cursor_style: Option, click_handler: Option>, drag_handler: Option>, } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Default)] pub struct MouseState { pub hovered: bool, pub clicked: bool, prev_drag_position: Option, + cursor_style_handle: Option, } impl MouseEventHandler { @@ -33,11 +36,17 @@ impl MouseEventHandler { Self { state: state_handle, child, + cursor_style: None, click_handler: None, drag_handler: None, } } + pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self { + self.cursor_style = Some(cursor); + self + } + pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self { self.click_handler = Some(Box::new(handler)); self @@ -78,6 +87,7 @@ impl Element for MouseEventHandler { _: &mut Self::PaintState, cx: &mut EventContext, ) -> bool { + let cursor_style = self.cursor_style; let click_handler = self.click_handler.as_mut(); let drag_handler = self.drag_handler.as_mut(); @@ -88,6 +98,15 @@ impl Element for MouseEventHandler { let mouse_in = bounds.contains_point(*position); if state.hovered != mouse_in { state.hovered = mouse_in; + if let Some(cursor_style) = cursor_style { + if !state.clicked { + if state.hovered { + state.cursor_style_handle = Some(cx.set_cursor_style(cursor_style)); + } else { + state.cursor_style_handle = None; + } + } + } cx.notify(); true } else { @@ -108,6 +127,9 @@ impl Element for MouseEventHandler { state.prev_drag_position = None; if !handled_in_child && state.clicked { state.clicked = false; + if !state.hovered { + state.cursor_style_handle = None; + } cx.notify(); if let Some(handler) = click_handler { if bounds.contains_point(*position) { diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 449f6bb962ea696e6a0b0b455febb4426b0ec38c..9262d99ec7004a63d119ed2b9b431ecf1daef7f1 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -47,6 +47,8 @@ pub trait Platform: Send + Sync { fn write_credentials(&self, url: &str, username: &str, password: &[u8]); fn read_credentials(&self, url: &str) -> Option<(String, Vec)>; + + fn set_cursor_style(&self, style: CursorStyle); } pub(crate) trait ForegroundPlatform { @@ -114,6 +116,13 @@ pub enum PromptLevel { Critical, } +#[derive(Copy, Clone, Debug)] +pub enum CursorStyle { + Arrow, + ResizeLeftRight, + PointingHand, +} + pub trait FontSystem: Send + Sync { fn load_family(&self, name: &str) -> anyhow::Result>; fn select_font( diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index 6593f9e3efa27db8dcd63fb59ea19601040f6ab6..861984a24761ccc1dbe2fe92601383d3c44d8e88 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -1,6 +1,9 @@ use super::{BoolExt as _, Dispatcher, FontSystem, Window}; use crate::{ - executor, keymap::Keystroke, platform, AnyAction, ClipboardItem, Event, Menu, MenuItem, + executor, + keymap::Keystroke, + platform::{self, CursorStyle}, + AnyAction, ClipboardItem, Event, Menu, MenuItem, }; use block::ConcreteBlock; use cocoa::{ @@ -544,6 +547,17 @@ impl platform::Platform for MacPlatform { Some((username.to_string(), password.bytes().to_vec())) } } + + fn set_cursor_style(&self, style: CursorStyle) { + unsafe { + let cursor: id = match style { + CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], + CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], + }; + let _: () = msg_send![cursor, set]; + } + } } unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform { diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 40c9304a7aa1ebe4ed2cc6e2521ee849d6fd3b80..b66a8d5043b3e4c04756064bcf3bc4aef9c2fc3d 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -1,3 +1,4 @@ +use super::CursorStyle; use crate::{AnyAction, ClipboardItem}; use parking_lot::Mutex; use pathfinder_geometry::vector::Vector2F; @@ -13,6 +14,7 @@ pub struct Platform { dispatcher: Arc, fonts: Arc, current_clipboard_item: Mutex>, + cursor: Mutex, } #[derive(Default)] @@ -84,6 +86,7 @@ impl Platform { dispatcher: Arc::new(Dispatcher), fonts: Arc::new(super::current::FontSystem::new()), current_clipboard_item: Default::default(), + cursor: Mutex::new(CursorStyle::Arrow), } } } @@ -129,6 +132,10 @@ impl super::Platform for Platform { fn read_credentials(&self, _: &str) -> Option<(String, Vec)> { None } + + fn set_cursor_style(&self, style: CursorStyle) { + *self.cursor.lock() = style; + } } impl Window { diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 3c02907067f6e2cb4aa4b04496d9e2c08ca8ab05..739aecf46c249f559b6d4f51b11fbaf9bca452e6 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -1,6 +1,8 @@ use super::Workspace; use crate::Settings; -use gpui::{action, elements::*, AnyViewHandle, MutableAppContext, RenderContext}; +use gpui::{ + action, elements::*, platform::CursorStyle, AnyViewHandle, MutableAppContext, RenderContext, +}; use std::{cell::RefCell, rc::Rc}; pub struct Sidebar { @@ -88,6 +90,7 @@ impl Sidebar { .with_height(line_height + 16.0) .boxed() }) + .with_cursor_style(CursorStyle::PointingHand) .on_click(move |cx| { cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index })) }) @@ -135,6 +138,7 @@ impl Sidebar { .with_style(&settings.theme.workspace.sidebar.resize_handle) .boxed() }) + .with_cursor_style(CursorStyle::ResizeLeftRight) .on_drag(move |delta, cx| { let prev_width = *width.borrow(); match side { From 5262dcd3cbf5e596970de4985e951ffa7112c607 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 27 Aug 2021 10:34:11 -0700 Subject: [PATCH 123/204] Don't change cursor on mouse move while mouse button is held down --- gpui/src/elements/mouse_event_handler.rs | 33 ++++++++++++++---------- gpui/src/platform/event.rs | 1 + gpui/src/platform/mac/event.rs | 5 ++-- gpui/src/presenter.rs | 10 +++++-- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/gpui/src/elements/mouse_event_handler.rs b/gpui/src/elements/mouse_event_handler.rs index 997331545436b53f6b33a32211f5033f2f57e62f..9336c8b84a20178c10f57cb5632359b108d98d5c 100644 --- a/gpui/src/elements/mouse_event_handler.rs +++ b/gpui/src/elements/mouse_event_handler.rs @@ -94,24 +94,29 @@ impl Element for MouseEventHandler { let handled_in_child = self.child.dispatch_event(event, cx); self.state.update(cx, |state, cx| match event { - Event::MouseMoved { position } => { - let mouse_in = bounds.contains_point(*position); - if state.hovered != mouse_in { - state.hovered = mouse_in; - if let Some(cursor_style) = cursor_style { - if !state.clicked { - if state.hovered { - state.cursor_style_handle = Some(cx.set_cursor_style(cursor_style)); - } else { - state.cursor_style_handle = None; + Event::MouseMoved { + position, + left_mouse_down, + } => { + if !left_mouse_down { + let mouse_in = bounds.contains_point(*position); + if state.hovered != mouse_in { + state.hovered = mouse_in; + if let Some(cursor_style) = cursor_style { + if !state.clicked { + if state.hovered { + state.cursor_style_handle = + Some(cx.set_cursor_style(cursor_style)); + } else { + state.cursor_style_handle = None; + } } } + cx.notify(); + return true; } - cx.notify(); - true - } else { - handled_in_child } + handled_in_child } Event::LeftMouseDown { position, .. } => { if !handled_in_child && bounds.contains_point(*position) { diff --git a/gpui/src/platform/event.rs b/gpui/src/platform/event.rs index 15ee7172f67084595793e2a607a2930d49d3eb78..fba7b812e6c8ac87479932f9556a267581551dfe 100644 --- a/gpui/src/platform/event.rs +++ b/gpui/src/platform/event.rs @@ -24,5 +24,6 @@ pub enum Event { }, MouseMoved { position: Vector2F, + left_mouse_down: bool, }, } diff --git a/gpui/src/platform/mac/event.rs b/gpui/src/platform/mac/event.rs index d3f9d9accd8ef69dbbb92d51d7fdfd6a8dea8c6a..04712d599ffc85de2a98e9af761f6764d2a3b0c4 100644 --- a/gpui/src/platform/mac/event.rs +++ b/gpui/src/platform/mac/event.rs @@ -6,8 +6,8 @@ use cocoa::appkit::{ NSUpArrowFunctionKey as ARROW_UP_KEY, }; use cocoa::{ - appkit::{NSEvent as _, NSEventModifierFlags, NSEventType}, - base::{id, YES}, + appkit::{NSEvent, NSEventModifierFlags, NSEventType}, + base::{id, nil, YES}, foundation::NSString as _, }; use std::{ffi::CStr, os::raw::c_char}; @@ -116,6 +116,7 @@ impl Event { native_event.locationInWindow().x as f32, window_height - native_event.locationInWindow().y as f32, ), + left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0, }), _ => None, } diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 92f47b0399dbac2329c7440e8531add09fb9a4b8..c2c670f1e706a010f452b2e7fec6277e5497030a 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -145,8 +145,14 @@ impl Presenter { pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { match event { - Event::MouseMoved { position, .. } | Event::LeftMouseDragged { position } => { - self.last_mouse_moved_event = Some(Event::MouseMoved { position }); + Event::MouseMoved { .. } => { + self.last_mouse_moved_event = Some(event.clone()); + } + Event::LeftMouseDragged { position } => { + self.last_mouse_moved_event = Some(Event::MouseMoved { + position, + left_mouse_down: true, + }); } _ => {} } From bc63fca8d716794c107f6181d48379452720da3c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 27 Aug 2021 14:58:28 -0700 Subject: [PATCH 124/204] Fetch older messages when scrolling up in the chat message list Co-Authored-By: Nathan Sobo --- gpui/src/elements/list.rs | 14 +++ server/src/admin.rs | 6 +- server/src/db.rs | 24 ++++- server/src/rpc.rs | 67 ++++++++++++- zed/src/channel.rs | 203 +++++++++++++++++++++++++++----------- zed/src/chat_panel.rs | 17 +++- zrpc/proto/zed.proto | 13 +++ zrpc/src/proto.rs | 3 + 8 files changed, 280 insertions(+), 67 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index b6773091c0b1c319280e08b584d6a823c3ddeef6..c7950482c379e0fdfc90e9fa93cd5980ab976197 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -29,6 +29,7 @@ struct StateInner { heights: SumTree, scroll_position: f32, orientation: Orientation, + scroll_handler: Option, &mut EventContext)>>, } #[derive(Clone, Debug)] @@ -272,6 +273,7 @@ impl ListState { heights, scroll_position: 0., orientation, + scroll_handler: None, }))) } @@ -290,6 +292,13 @@ impl ListState { drop(old_heights); state.heights = new_heights; } + + pub fn set_scroll_handler( + &mut self, + handler: impl FnMut(Range, &mut EventContext) + 'static, + ) { + self.0.borrow_mut().scroll_handler = Some(Box::new(handler)) + } } impl StateInner { @@ -320,6 +329,11 @@ impl StateInner { Orientation::Bottom => delta.y(), }; self.scroll_position = (self.scroll_position + delta_y).max(0.).min(scroll_max); + + if self.scroll_handler.is_some() { + let range = self.visible_range(height); + self.scroll_handler.as_mut().unwrap()(range, cx); + } cx.notify(); true diff --git a/server/src/admin.rs b/server/src/admin.rs index d6e3f8161589e6b2420cc9a7d54bd212ec8d76b1..47b29b5d0294168be720749e94e4f8ed838b802e 100644 --- a/server/src/admin.rs +++ b/server/src/admin.rs @@ -85,7 +85,7 @@ async fn post_user(mut request: Request) -> tide::Result { async fn put_user(mut request: Request) -> tide::Result { request.require_admin().await?; - let user_id = request.param("id")?.parse::()?; + let user_id = request.param("id")?.parse()?; #[derive(Deserialize)] struct Body { @@ -104,14 +104,14 @@ async fn put_user(mut request: Request) -> tide::Result { async fn delete_user(request: Request) -> tide::Result { request.require_admin().await?; - let user_id = db::UserId(request.param("id")?.parse::()?); + let user_id = db::UserId(request.param("id")?.parse()?); request.db().delete_user(user_id).await?; Ok(tide::Redirect::new("/admin").into()) } async fn delete_signup(request: Request) -> tide::Result { request.require_admin().await?; - let signup_id = db::SignupId(request.param("id")?.parse::()?); + let signup_id = db::SignupId(request.param("id")?.parse()?); request.db().delete_signup(signup_id).await?; Ok(tide::Redirect::new("/admin").into()) } diff --git a/server/src/db.rs b/server/src/db.rs index 2f1cbc5fba181457d823aa1a5a98c7bc0cbafc67..f61f5d82b44332ff452c1016239dbe366f840620 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -380,6 +380,7 @@ impl Db { &self, channel_id: ChannelId, count: usize, + before_id: Option, ) -> Result> { test_support!(self, { let query = r#" @@ -389,14 +390,16 @@ impl Db { FROM channel_messages WHERE - channel_id = $1 + channel_id = $1 AND + id < $2 ORDER BY id DESC - LIMIT $2 + LIMIT $3 ) as recent_messages ORDER BY id ASC "#; sqlx::query_as(query) .bind(channel_id.0) + .bind(before_id.unwrap_or(MessageId::MAX)) .bind(count as i64) .fetch_all(&self.pool) .await @@ -412,6 +415,9 @@ macro_rules! id_type { pub struct $name(pub i32); impl $name { + #[allow(unused)] + pub const MAX: Self = Self(i32::MAX); + #[allow(unused)] pub fn from_proto(value: u64) -> Self { Self(value as i32) @@ -512,10 +518,22 @@ pub mod tests { .unwrap(); } - let messages = db.get_recent_channel_messages(channel, 5).await.unwrap(); + let messages = db + .get_recent_channel_messages(channel, 5, None) + .await + .unwrap(); assert_eq!( messages.iter().map(|m| &m.body).collect::>(), ["5", "6", "7", "8", "9"] ); + + let prev_messages = db + .get_recent_channel_messages(channel, 4, Some(messages[0].id)) + .await + .unwrap(); + assert_eq!( + prev_messages.iter().map(|m| &m.body).collect::>(), + ["1", "2", "3", "4"] + ); } } diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 00464450fab56b2dcde76afdccdfce083e250504..b74b923b0f2699fcf27384d8f83a99db659adc72 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1,6 +1,6 @@ use super::{ auth, - db::{ChannelId, UserId}, + db::{ChannelId, MessageId, UserId}, AppState, }; use anyhow::anyhow; @@ -77,6 +77,8 @@ struct Channel { connection_ids: HashSet, } +const MESSAGE_COUNT_PER_PAGE: usize = 50; + impl Server { pub fn new( app_state: Arc, @@ -105,7 +107,8 @@ impl Server { .add_handler(Server::get_users) .add_handler(Server::join_channel) .add_handler(Server::leave_channel) - .add_handler(Server::send_channel_message); + .add_handler(Server::send_channel_message) + .add_handler(Server::get_channel_messages); Arc::new(server) } @@ -592,7 +595,7 @@ impl Server { let messages = self .app_state .db - .get_recent_channel_messages(channel_id, 50) + .get_recent_channel_messages(channel_id, MESSAGE_COUNT_PER_PAGE, None) .await? .into_iter() .map(|msg| proto::ChannelMessage { @@ -601,9 +604,15 @@ impl Server { timestamp: msg.sent_at.unix_timestamp() as u64, sender_id: msg.sender_id.to_proto(), }) - .collect(); + .collect::>(); self.peer - .respond(request.receipt(), proto::JoinChannelResponse { messages }) + .respond( + request.receipt(), + proto::JoinChannelResponse { + done: messages.len() < MESSAGE_COUNT_PER_PAGE, + messages, + }, + ) .await?; Ok(()) } @@ -685,6 +694,54 @@ impl Server { Ok(()) } + async fn get_channel_messages( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let user_id = self + .state + .read() + .await + .user_id_for_connection(request.sender_id)?; + let channel_id = ChannelId::from_proto(request.payload.channel_id); + if !self + .app_state + .db + .can_user_access_channel(user_id, channel_id) + .await? + { + Err(anyhow!("access denied"))?; + } + + let messages = self + .app_state + .db + .get_recent_channel_messages( + channel_id, + MESSAGE_COUNT_PER_PAGE, + Some(MessageId::from_proto(request.payload.before_message_id)), + ) + .await? + .into_iter() + .map(|msg| proto::ChannelMessage { + id: msg.id.to_proto(), + body: msg.body, + timestamp: msg.sent_at.unix_timestamp() as u64, + sender_id: msg.sender_id.to_proto(), + }) + .collect::>(); + self.peer + .respond( + request.receipt(), + proto::GetChannelMessagesResponse { + done: messages.len() < MESSAGE_COUNT_PER_PAGE, + messages, + }, + ) + .await?; + Ok(()) + } + async fn broadcast_in_worktree( &self, worktree_id: u64, diff --git a/zed/src/channel.rs b/zed/src/channel.rs index f1f2e076077e5b38a7a036fab679d6b880d972d4..f0f9d4f43c94fd1e818e0b2a3021e1c4eb173e94 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -37,6 +37,7 @@ pub struct ChannelDetails { pub struct Channel { details: ChannelDetails, messages: SumTree, + loaded_all_messages: bool, pending_messages: Vec, next_local_message_id: u64, user_store: Arc, @@ -70,7 +71,7 @@ pub enum ChannelListEvent {} #[derive(Clone, Debug, PartialEq)] pub enum ChannelEvent { - Message { + MessagesAdded { old_range: Range, new_count: usize, }, @@ -192,31 +193,12 @@ impl Channel { cx.spawn(|channel, mut cx| { async move { let response = rpc.request(proto::JoinChannel { channel_id }).await?; - - let unique_user_ids = response - .messages - .iter() - .map(|m| m.sender_id) - .collect::>() - .into_iter() - .collect(); - user_store.load_users(unique_user_ids).await?; - - let mut messages = Vec::with_capacity(response.messages.len()); - for message in response.messages { - messages.push(ChannelMessage::from_proto(message, &user_store).await?); - } + let messages = messages_from_proto(response.messages, &user_store).await?; + let loaded_all_messages = response.done; channel.update(&mut cx, |channel, cx| { - let old_count = channel.messages.summary().count; - let new_count = messages.len(); - - channel.messages = SumTree::new(); - channel.messages.extend(messages, &()); - cx.emit(ChannelEvent::Message { - old_range: 0..old_count, - new_count, - }); + channel.insert_messages(messages, cx); + channel.loaded_all_messages = loaded_all_messages; }); Ok(()) @@ -232,6 +214,7 @@ impl Channel { rpc, messages: Default::default(), pending_messages: Default::default(), + loaded_all_messages: false, next_local_message_id: 0, _subscription, } @@ -264,15 +247,18 @@ impl Channel { .binary_search_by_key(&local_id, |msg| msg.local_id) { let body = this.pending_messages.remove(i).body; - this.insert_message( - ChannelMessage { - id: response.message_id, - timestamp: OffsetDateTime::from_unix_timestamp( - response.timestamp as i64, - )?, - body, - sender, - }, + this.insert_messages( + SumTree::from_item( + ChannelMessage { + id: response.message_id, + timestamp: OffsetDateTime::from_unix_timestamp( + response.timestamp as i64, + )?, + body, + sender, + }, + &(), + ), cx, ); } @@ -286,6 +272,37 @@ impl Channel { Ok(()) } + pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> bool { + if !self.loaded_all_messages { + 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) { + cx.spawn(|this, mut cx| { + async move { + let response = rpc + .request(proto::GetChannelMessages { + channel_id, + before_message_id, + }) + .await?; + let loaded_all_messages = response.done; + let messages = messages_from_proto(response.messages, &user_store).await?; + this.update(&mut cx, |this, cx| { + this.loaded_all_messages = loaded_all_messages; + this.insert_messages(messages, cx); + }); + Ok(()) + } + .log_err() + }) + .detach(); + return true; + } + } + false + } + pub fn message_count(&self) -> usize { self.messages.summary().count } @@ -326,7 +343,9 @@ impl Channel { cx.spawn(|this, mut cx| { async move { let message = ChannelMessage::from_proto(message, &user_store).await?; - this.update(&mut cx, |this, cx| this.insert_message(message, cx)); + this.update(&mut cx, |this, cx| { + this.insert_messages(SumTree::from_item(message, &()), cx) + }); Ok(()) } .log_err() @@ -335,27 +354,49 @@ impl Channel { Ok(()) } - fn insert_message(&mut self, message: ChannelMessage, 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 mut end_ix = start_ix; - if old_cursor.item().map_or(false, |m| m.id == message.id) { - old_cursor.next(&()); - end_ix += 1; + 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 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, &()); + let removed_count = removed_messages.summary().count; + let new_count = messages.summary().count; + let end_ix = start_ix + removed_count; + + new_messages.push_tree(messages, &()); + new_messages.push_tree(old_cursor.suffix(&()), &()); + drop(old_cursor); + self.messages = new_messages; + + cx.emit(ChannelEvent::MessagesAdded { + old_range: start_ix..end_ix, + new_count, + }); + cx.notify(); } + } +} - new_messages.push(message.clone(), &()); - new_messages.push_tree(old_cursor.suffix(&()), &()); - drop(old_cursor); - self.messages = new_messages; - - cx.emit(ChannelEvent::Message { - old_range: start_ix..end_ix, - new_count: 1, - }); - cx.notify(); +async fn messages_from_proto( + proto_messages: Vec, + user_store: &UserStore, +) -> Result> { + let unique_user_ids = proto_messages + .iter() + .map(|m| m.sender_id) + .collect::>() + .into_iter() + .collect(); + user_store.load_users(unique_user_ids).await?; + + let mut messages = Vec::with_capacity(proto_messages.len()); + for message in proto_messages { + messages.push(ChannelMessage::from_proto(message, &user_store).await?); } + let mut result = SumTree::new(); + result.extend(messages, &()); + Ok(result) } impl From for ChannelDetails { @@ -489,9 +530,11 @@ mod tests { sender_id: 6, }, ], + done: false, }, ) .await; + // Client requests all users for the received messages let mut get_users = server.receive::().await; get_users.payload.user_ids.sort(); @@ -518,7 +561,7 @@ mod tests { assert_eq!( channel.next_event(&cx).await, - ChannelEvent::Message { + ChannelEvent::MessagesAdded { old_range: 0..0, new_count: 2, } @@ -567,7 +610,7 @@ mod tests { assert_eq!( channel.next_event(&cx).await, - ChannelEvent::Message { + ChannelEvent::MessagesAdded { old_range: 2..2, new_count: 1, } @@ -580,7 +623,57 @@ mod tests { .collect::>(), &[("as-cii".into(), "c".into())] ) - }) + }); + + // Scroll up to view older messages. + channel.update(&mut cx, |channel, cx| { + assert!(channel.load_more_messages(cx)); + }); + let get_messages = server.receive::().await; + assert_eq!(get_messages.payload.channel_id, 5); + assert_eq!(get_messages.payload.before_message_id, 10); + server + .respond( + get_messages.receipt(), + proto::GetChannelMessagesResponse { + done: true, + messages: vec![ + proto::ChannelMessage { + id: 8, + body: "y".into(), + timestamp: 998, + sender_id: 5, + }, + proto::ChannelMessage { + id: 9, + body: "z".into(), + timestamp: 999, + sender_id: 6, + }, + ], + }, + ) + .await; + + assert_eq!( + channel.next_event(&cx).await, + ChannelEvent::MessagesAdded { + old_range: 0..0, + new_count: 2, + } + ); + channel.read_with(&cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(0..2) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) + .collect::>(), + &[ + ("nathansobo".into(), "y".into()), + ("maxbrunsfeld".into(), "z".into()) + ] + ); + }); } struct FakeServer { diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 37409800feaad2beb810cc87dff5e5df5e062a7b..a8b4ed94bf790154f5117a74437c820d0a33de0d 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -22,9 +22,11 @@ pub struct ChatPanel { pub enum Event {} action!(Send); +action!(LoadMoreMessages); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ChatPanel::send); + cx.add_action(ChatPanel::load_more_messages); cx.add_bindings(vec![Binding::new("enter", Send, Some("ChatPanel"))]); } @@ -78,6 +80,11 @@ impl ChatPanel { let subscription = cx.subscribe(&channel, Self::channel_did_change); self.message_list = ListState::new(channel.read(cx).message_count(), Orientation::Bottom); + self.message_list.set_scroll_handler(|visible_range, cx| { + if visible_range.start < 5 { + cx.dispatch_action(LoadMoreMessages); + } + }); self.active_channel = Some((channel, subscription)); } } @@ -89,7 +96,7 @@ impl ChatPanel { cx: &mut ViewContext, ) { match event { - ChannelEvent::Message { + ChannelEvent::MessagesAdded { old_range, new_count, } => { @@ -191,6 +198,14 @@ impl ChatPanel { .log_err(); } } + + fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext) { + if let Some((channel, _)) = self.active_channel.as_ref() { + channel.update(cx, |channel, cx| { + channel.load_more_messages(cx); + }) + } + } } impl Entity for ChatPanel { diff --git a/zrpc/proto/zed.proto b/zrpc/proto/zed.proto index f368cb2d475da9dab1e651a8d29bc961dbec1bb8..a94c0f62049d18dcc1bf04463c3cba1b0b33e219 100644 --- a/zrpc/proto/zed.proto +++ b/zrpc/proto/zed.proto @@ -32,6 +32,8 @@ message Envelope { SendChannelMessage send_channel_message = 27; SendChannelMessageResponse send_channel_message_response = 28; ChannelMessageSent channel_message_sent = 29; + GetChannelMessages get_channel_messages = 30; + GetChannelMessagesResponse get_channel_messages_response = 31; } } @@ -130,6 +132,7 @@ message JoinChannel { message JoinChannelResponse { repeated ChannelMessage messages = 1; + bool done = 2; } message LeaveChannel { @@ -159,6 +162,16 @@ message ChannelMessageSent { ChannelMessage message = 2; } +message GetChannelMessages { + uint64 channel_id = 1; + uint64 before_message_id = 2; +} + +message GetChannelMessagesResponse { + repeated ChannelMessage messages = 1; + bool done = 2; +} + // Entities message Peer { diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index 330e3afa48380629dc0d78dfbd76943ee813bfdb..3743a06a07d56a496a97084fb9cc839e1e7afce9 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -125,6 +125,8 @@ messages!( ChannelMessageSent, CloseBuffer, CloseWorktree, + GetChannelMessages, + GetChannelMessagesResponse, GetChannels, GetChannelsResponse, GetUsers, @@ -158,6 +160,7 @@ request_messages!( (SaveBuffer, BufferSaved), (ShareWorktree, ShareWorktreeResponse), (SendChannelMessage, SendChannelMessageResponse), + (GetChannelMessages, GetChannelMessagesResponse), ); entity_messages!( From 936af9bc5e3d5999aa07151211223b8528cc30b0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 27 Aug 2021 15:03:37 -0700 Subject: [PATCH 125/204] Avoid double borrow panic when resizing windows Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 31 +++++++++++++++++++++---------- gpui/src/platform.rs | 2 +- gpui/src/platform/mac/window.rs | 31 ++++++++++++++++--------------- gpui/src/platform/test.rs | 4 ++-- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index d9caf3e6d2b9b516c09a427dffacaf48be66daaf..69591d8052fbad135ce5e226f2a98ace744ab963 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -1217,16 +1217,8 @@ impl MutableAppContext { { let mut app = self.upgrade(); - let presenter = presenter.clone(); - window.on_resize(Box::new(move |window| { - app.update(|cx| { - let scene = presenter.borrow_mut().build_scene( - window.size(), - window.scale_factor(), - cx, - ); - window.present_scene(scene); - }) + window.on_resize(Box::new(move || { + app.update(|cx| cx.resize_window(window_id)) })); } @@ -1391,6 +1383,13 @@ impl MutableAppContext { Effect::Focus { window_id, view_id } => { self.focus(window_id, view_id); } + Effect::ResizeWindow { window_id } => { + if let Some(window) = self.cx.windows.get_mut(&window_id) { + window + .invalidation + .get_or_insert(WindowInvalidation::default()); + } + } Effect::RefreshWindows => { refreshing = true; } @@ -1439,6 +1438,11 @@ impl MutableAppContext { } } + fn resize_window(&mut self, window_id: usize) { + self.pending_effects + .push_back(Effect::ResizeWindow { window_id }); + } + pub fn refresh_windows(&mut self) { self.pending_effects.push_back(Effect::RefreshWindows); } @@ -1794,6 +1798,9 @@ pub enum Effect { window_id: usize, view_id: usize, }, + ResizeWindow { + window_id: usize, + }, RefreshWindows, } @@ -1818,6 +1825,10 @@ impl Debug for Effect { .field("window_id", window_id) .field("view_id", view_id) .finish(), + Effect::ResizeWindow { window_id } => f + .debug_struct("Effect::RefreshWindow") + .field("window_id", window_id) + .finish(), Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(), } } diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 9262d99ec7004a63d119ed2b9b431ecf1daef7f1..21679ae877bb2dab58006bfccf15fe7f54bcfefd 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -80,7 +80,7 @@ pub trait Dispatcher: Send + Sync { pub trait Window: WindowContext { fn as_any_mut(&mut self) -> &mut dyn Any; fn on_event(&mut self, callback: Box); - fn on_resize(&mut self, callback: Box); + fn on_resize(&mut self, callback: Box); fn on_close(&mut self, callback: Box); fn prompt( &self, diff --git a/gpui/src/platform/mac/window.rs b/gpui/src/platform/mac/window.rs index f12fe38ce6bd0184f46eb6f147e55f540c8231dd..bcdb61241c655258c8bd887613c5d9799cce0208 100644 --- a/gpui/src/platform/mac/window.rs +++ b/gpui/src/platform/mac/window.rs @@ -130,7 +130,7 @@ struct WindowState { id: usize, native_window: id, event_callback: Option>, - resize_callback: Option>, + resize_callback: Option>, close_callback: Option>, synthetic_drag_counter: usize, executor: Rc, @@ -280,7 +280,7 @@ impl platform::Window for Window { self.0.as_ref().borrow_mut().event_callback = Some(callback); } - fn on_resize(&mut self, callback: Box) { + fn on_resize(&mut self, callback: Box) { self.0.as_ref().borrow_mut().resize_callback = Some(callback); } @@ -489,24 +489,24 @@ extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id { extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) { let window_state = unsafe { get_window_state(this) }; - let mut window_state = window_state.as_ref().borrow_mut(); + let mut window_state_borrow = window_state.as_ref().borrow_mut(); unsafe { - let _: () = - msg_send![window_state.layer, setContentsScale: window_state.scale_factor() as f64]; + let _: () = msg_send![window_state_borrow.layer, setContentsScale: window_state_borrow.scale_factor() as f64]; } - if let Some(mut callback) = window_state.resize_callback.take() { - callback(&mut *window_state); - window_state.resize_callback = Some(callback); + if let Some(mut callback) = window_state_borrow.resize_callback.take() { + drop(window_state_borrow); + callback(); + window_state.as_ref().borrow_mut().resize_callback = Some(callback); }; } extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) { let window_state = unsafe { get_window_state(this) }; - let mut window_state = window_state.as_ref().borrow_mut(); + let mut window_state_borrow = window_state.as_ref().borrow_mut(); - if window_state.size() == vec2f(size.width as f32, size.height as f32) { + if window_state_borrow.size() == vec2f(size.width as f32, size.height as f32) { return; } @@ -514,19 +514,20 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) { let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size]; } - let scale_factor = window_state.scale_factor() as f64; + let scale_factor = window_state_borrow.scale_factor() as f64; let drawable_size: NSSize = NSSize { width: size.width * scale_factor, height: size.height * scale_factor, }; unsafe { - let _: () = msg_send![window_state.layer, setDrawableSize: drawable_size]; + let _: () = msg_send![window_state_borrow.layer, setDrawableSize: drawable_size]; } - if let Some(mut callback) = window_state.resize_callback.take() { - callback(&mut *window_state); - window_state.resize_callback = Some(callback); + if let Some(mut callback) = window_state_borrow.resize_callback.take() { + drop(window_state_borrow); + callback(); + window_state.borrow_mut().resize_callback = Some(callback); }; } diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index b66a8d5043b3e4c04756064bcf3bc4aef9c2fc3d..3295fb63822eba6fc36324826e58e27929439cc6 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -29,7 +29,7 @@ pub struct Window { scale_factor: f32, current_scene: Option, event_handlers: Vec>, - resize_handlers: Vec>, + resize_handlers: Vec>, close_handlers: Vec>, pub(crate) last_prompt: RefCell>>, } @@ -189,7 +189,7 @@ impl super::Window for Window { self.event_handlers.push(callback); } - fn on_resize(&mut self, callback: Box) { + fn on_resize(&mut self, callback: Box) { self.resize_handlers.push(callback); } From fbc88d5f8894c3d69396c297fa23f5a636a06d5a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 27 Aug 2021 15:11:39 -0700 Subject: [PATCH 126/204] Get the light theme loading again --- zed/assets/themes/light.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml index 6702c18f1cb7e8cd854df2cd71930c8f892f8377..564542817bf56e620365bb460150bdc66515738d 100644 --- a/zed/assets/themes/light.toml +++ b/zed/assets/themes/light.toml @@ -7,7 +7,7 @@ extends = "_base" 3 = "#3a3b3c" [text] -base = { family = "Inconsolata" } +base = { family = "Helvetica", size = 14.0 } 0 = { extends = "$text.base", color = "#acacac" } 1 = { extends = "$text.base", color = "#111111" } 2 = { extends = "$text.base", color = "#333333" } From b3d5f01ba81edfdbe31388b93339772b786cb7c2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 27 Aug 2021 15:50:45 -0700 Subject: [PATCH 127/204] Drop the word 'recent' from get_channel_messages db method name --- server/src/db.rs | 9 +++------ server/src/rpc.rs | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/server/src/db.rs b/server/src/db.rs index f61f5d82b44332ff452c1016239dbe366f840620..374761a73ce181a380bab3492f98fca0c293a73e 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -376,7 +376,7 @@ impl Db { }) } - pub async fn get_recent_channel_messages( + pub async fn get_channel_messages( &self, channel_id: ChannelId, count: usize, @@ -518,17 +518,14 @@ pub mod tests { .unwrap(); } - let messages = db - .get_recent_channel_messages(channel, 5, None) - .await - .unwrap(); + let messages = db.get_channel_messages(channel, 5, None).await.unwrap(); assert_eq!( messages.iter().map(|m| &m.body).collect::>(), ["5", "6", "7", "8", "9"] ); let prev_messages = db - .get_recent_channel_messages(channel, 4, Some(messages[0].id)) + .get_channel_messages(channel, 4, Some(messages[0].id)) .await .unwrap(); assert_eq!( diff --git a/server/src/rpc.rs b/server/src/rpc.rs index b74b923b0f2699fcf27384d8f83a99db659adc72..c712ced835a0388227f03c547b7b64b2943b7367 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -595,7 +595,7 @@ impl Server { let messages = self .app_state .db - .get_recent_channel_messages(channel_id, MESSAGE_COUNT_PER_PAGE, None) + .get_channel_messages(channel_id, MESSAGE_COUNT_PER_PAGE, None) .await? .into_iter() .map(|msg| proto::ChannelMessage { @@ -716,7 +716,7 @@ impl Server { let messages = self .app_state .db - .get_recent_channel_messages( + .get_channel_messages( channel_id, MESSAGE_COUNT_PER_PAGE, Some(MessageId::from_proto(request.payload.before_message_id)), From a98d293f54eba4c18de0a412db060f12b7c7a009 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 27 Aug 2021 16:53:23 -0700 Subject: [PATCH 128/204] Trim whitespace from chat messages and limit their length Add a way for the server to respond to any request with an error --- server/src/rpc.rs | 107 ++++++++++++++++++++++++++++++++++++------ zed/src/channel.rs | 70 ++++++++++++++------------- zed/src/chat_panel.rs | 7 ++- zrpc/proto/zed.proto | 61 +++++++++++++----------- zrpc/src/peer.rs | 27 ++++++++++- zrpc/src/proto.rs | 1 + 6 files changed, 191 insertions(+), 82 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index c712ced835a0388227f03c547b7b64b2943b7367..696487fdca2bf9b03b82b74c2cd911d83ef7338e 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -77,8 +77,14 @@ struct Channel { connection_ids: HashSet, } +#[cfg(debug_assertions)] +const MESSAGE_COUNT_PER_PAGE: usize = 10; + +#[cfg(not(debug_assertions))] const MESSAGE_COUNT_PER_PAGE: usize = 50; +const MAX_MESSAGE_LEN: usize = 1024; + impl Server { pub fn new( app_state: Arc, @@ -661,20 +667,33 @@ impl Server { } } + let receipt = request.receipt(); + let body = request.payload.body.trim().to_string(); + if body.len() > MAX_MESSAGE_LEN { + self.peer + .respond_with_error( + receipt, + proto::Error { + message: "message is too long".to_string(), + }, + ) + .await?; + return Ok(()); + } + let timestamp = OffsetDateTime::now_utc(); let message_id = self .app_state .db - .create_channel_message(channel_id, user_id, &request.payload.body, timestamp) + .create_channel_message(channel_id, user_id, &body, timestamp) .await? .to_proto(); - let receipt = request.receipt(); let message = proto::ChannelMessageSent { channel_id: channel_id.to_proto(), message: Some(proto::ChannelMessage { sender_id: user_id.to_proto(), id: message_id, - body: request.payload.body, + body, timestamp: timestamp.unix_timestamp() as u64, }), }; @@ -1530,18 +1549,25 @@ mod tests { }) .await; - channel_a.update(&mut cx_a, |channel, cx| { - channel.send_message("oh, hi B.".to_string(), cx).unwrap(); - channel.send_message("sup".to_string(), cx).unwrap(); - assert_eq!( + channel_a + .update(&mut cx_a, |channel, cx| { channel - .pending_messages() - .iter() - .map(|m| &m.body) - .collect::>(), - &["oh, hi B.", "sup"] - ) - }); + .send_message("oh, hi B.".to_string(), cx) + .unwrap() + .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"] + ); + task + }) + .await + .unwrap(); channel_a .condition(&cx_a, |channel, _| channel.pending_messages().is_empty()) @@ -1582,6 +1608,59 @@ mod tests { } } + #[gpui::test] + async fn test_chat_message_validation(mut cx_a: TestAppContext) { + cx_a.foreground().forbid_parking(); + + let mut server = TestServer::start().await; + let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await; + + let 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) + .await + .unwrap(); + + let user_store_a = Arc::new(UserStore::new(client_a.clone())); + let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx)); + channels_a + .condition(&mut cx_a, |list, _| list.available_channels().is_some()) + .await; + let channel_a = channels_a.update(&mut cx_a, |this, cx| { + this.get_channel(channel_id.to_proto(), cx).unwrap() + }); + + // Leading and trailing whitespace are trimmed. + channel_a + .update(&mut cx_a, |channel, cx| { + channel + .send_message("\n surrounded by whitespace \n".to_string(), cx) + .unwrap() + }) + .await + .unwrap(); + assert_eq!( + db.get_channel_messages(channel_id, 10, None) + .await + .unwrap() + .iter() + .map(|m| &m.body) + .collect::>(), + &["surrounded by whitespace"] + ); + + // Messages aren't allowed to be too long. + channel_a + .update(&mut cx_a, |channel, cx| { + let long_body = "this is long.\n".repeat(1024); + channel.send_message(long_body, cx).unwrap() + }) + .await + .unwrap_err(); + } + struct TestServer { peer: Arc, app_state: Arc, diff --git a/zed/src/channel.rs b/zed/src/channel.rs index f0f9d4f43c94fd1e818e0b2a3021e1c4eb173e94..6bac81fa199c93564a82dc611d817a75eb030ba8 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -224,7 +224,11 @@ impl Channel { &self.details.name } - pub fn send_message(&mut self, body: String, cx: &mut ModelContext) -> Result<()> { + pub fn send_message( + &mut self, + body: String, + cx: &mut ModelContext, + ) -> Result>> { let channel_id = self.details.id; let current_user_id = self.current_user_id()?; let local_id = self.next_local_message_id; @@ -235,41 +239,35 @@ impl Channel { }); let user_store = self.user_store.clone(); let rpc = self.rpc.clone(); - cx.spawn(|this, mut cx| { - async move { - let request = rpc.request(proto::SendChannelMessage { channel_id, body }); - let response = request.await?; - let sender = user_store.get_user(current_user_id).await?; - - this.update(&mut cx, |this, cx| { - if let Ok(i) = this - .pending_messages - .binary_search_by_key(&local_id, |msg| msg.local_id) - { - let body = this.pending_messages.remove(i).body; - this.insert_messages( - SumTree::from_item( - ChannelMessage { - id: response.message_id, - timestamp: OffsetDateTime::from_unix_timestamp( - response.timestamp as i64, - )?, - body, - sender, - }, - &(), - ), - cx, - ); - } - Ok(()) - }) - } - .log_err() - }) - .detach(); - cx.notify(); - Ok(()) + Ok(cx.spawn(|this, mut cx| async move { + let request = rpc.request(proto::SendChannelMessage { channel_id, body }); + let response = request.await?; + let sender = user_store.get_user(current_user_id).await?; + + this.update(&mut cx, |this, cx| { + if let Ok(i) = this + .pending_messages + .binary_search_by_key(&local_id, |msg| msg.local_id) + { + let body = this.pending_messages.remove(i).body; + this.insert_messages( + SumTree::from_item( + ChannelMessage { + id: response.message_id, + timestamp: OffsetDateTime::from_unix_timestamp( + response.timestamp as i64, + )?, + body, + sender, + }, + &(), + ), + cx, + ); + } + Ok(()) + }) + })) } pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> bool { diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index a8b4ed94bf790154f5117a74437c820d0a33de0d..e94f5712e574954b141399cf492f36ccb186e8f9 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -193,9 +193,12 @@ impl ChatPanel { body }); - channel + if let Some(task) = channel .update(cx, |channel, cx| channel.send_message(body, cx)) - .log_err(); + .log_err() + { + task.detach(); + } } } diff --git a/zrpc/proto/zed.proto b/zrpc/proto/zed.proto index a94c0f62049d18dcc1bf04463c3cba1b0b33e219..123fc0f1dad23f1b5a2446476df50fac133ab175 100644 --- a/zrpc/proto/zed.proto +++ b/zrpc/proto/zed.proto @@ -6,34 +6,35 @@ message Envelope { optional uint32 responding_to = 2; optional uint32 original_sender_id = 3; oneof payload { - Ping ping = 4; - Pong pong = 5; - ShareWorktree share_worktree = 6; - ShareWorktreeResponse share_worktree_response = 7; - OpenWorktree open_worktree = 8; - OpenWorktreeResponse open_worktree_response = 9; - UpdateWorktree update_worktree = 10; - CloseWorktree close_worktree = 11; - OpenBuffer open_buffer = 12; - OpenBufferResponse open_buffer_response = 13; - CloseBuffer close_buffer = 14; - UpdateBuffer update_buffer = 15; - SaveBuffer save_buffer = 16; - BufferSaved buffer_saved = 17; - AddPeer add_peer = 18; - RemovePeer remove_peer = 19; - GetChannels get_channels = 20; - GetChannelsResponse get_channels_response = 21; - GetUsers get_users = 22; - GetUsersResponse get_users_response = 23; - JoinChannel join_channel = 24; - JoinChannelResponse join_channel_response = 25; - LeaveChannel leave_channel = 26; - SendChannelMessage send_channel_message = 27; - SendChannelMessageResponse send_channel_message_response = 28; - ChannelMessageSent channel_message_sent = 29; - GetChannelMessages get_channel_messages = 30; - GetChannelMessagesResponse get_channel_messages_response = 31; + Error error = 4; + Ping ping = 5; + Pong pong = 6; + ShareWorktree share_worktree = 7; + ShareWorktreeResponse share_worktree_response = 8; + OpenWorktree open_worktree = 9; + OpenWorktreeResponse open_worktree_response = 10; + UpdateWorktree update_worktree = 11; + CloseWorktree close_worktree = 12; + OpenBuffer open_buffer = 13; + OpenBufferResponse open_buffer_response = 14; + CloseBuffer close_buffer = 15; + UpdateBuffer update_buffer = 16; + SaveBuffer save_buffer = 17; + BufferSaved buffer_saved = 18; + AddPeer add_peer = 19; + RemovePeer remove_peer = 20; + GetChannels get_channels = 21; + GetChannelsResponse get_channels_response = 22; + GetUsers get_users = 23; + GetUsersResponse get_users_response = 24; + JoinChannel join_channel = 25; + JoinChannelResponse join_channel_response = 26; + LeaveChannel leave_channel = 27; + SendChannelMessage send_channel_message = 28; + SendChannelMessageResponse send_channel_message_response = 29; + ChannelMessageSent channel_message_sent = 30; + GetChannelMessages get_channel_messages = 31; + GetChannelMessagesResponse get_channel_messages_response = 32; } } @@ -47,6 +48,10 @@ message Pong { int32 id = 2; } +message Error { + string message = 1; +} + message ShareWorktree { Worktree worktree = 1; } diff --git a/zrpc/src/peer.rs b/zrpc/src/peer.rs index 06d4b01ae06140c417270a992e3d23142bc65498..9a7954341b0ce1793e25ff8a37a204238e1564de 100644 --- a/zrpc/src/peer.rs +++ b/zrpc/src/peer.rs @@ -238,8 +238,12 @@ impl Peer { .recv() .await .ok_or_else(|| anyhow!("connection was closed"))?; - T::Response::from_envelope(response) - .ok_or_else(|| anyhow!("received response of the wrong type")) + if let Some(proto::envelope::Payload::Error(error)) = &response.payload { + Err(anyhow!("request failed").context(error.message.clone())) + } else { + T::Response::from_envelope(response) + .ok_or_else(|| anyhow!("received response of the wrong type")) + } } } @@ -301,6 +305,25 @@ impl Peer { } } + pub fn respond_with_error( + self: &Arc, + receipt: Receipt, + response: proto::Error, + ) -> impl Future> { + let this = self.clone(); + async move { + let mut connection = this.connection(receipt.sender_id).await?; + let message_id = connection + .next_message_id + .fetch_add(1, atomic::Ordering::SeqCst); + connection + .outgoing_tx + .send(response.into_envelope(message_id, Some(receipt.message_id), None)) + .await?; + Ok(()) + } + } + fn connection( self: &Arc, connection_id: ConnectionId, diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index 3743a06a07d56a496a97084fb9cc839e1e7afce9..002c5bc840a2d3d43a7a7a4cf496b331588db728 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -125,6 +125,7 @@ messages!( ChannelMessageSent, CloseBuffer, CloseWorktree, + Error, GetChannelMessages, GetChannelMessagesResponse, GetChannels, From 18d175a2401b3cc7199468213f0edb08098f424b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 27 Aug 2021 17:21:53 -0700 Subject: [PATCH 129/204] Don't allow empty chat messages --- server/src/rpc.rs | 40 ++++++++++++++++++++++++++++++---------- zed/src/channel.rs | 4 ++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 696487fdca2bf9b03b82b74c2cd911d83ef7338e..5a0199b725c5e59a6376b633d40e92d3df11c256 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -654,6 +654,7 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> tide::Result<()> { + let receipt = request.receipt(); let channel_id = ChannelId::from_proto(request.payload.channel_id); let user_id; let connection_ids; @@ -667,7 +668,7 @@ impl Server { } } - let receipt = request.receipt(); + // Validate the message body. let body = request.payload.body.trim().to_string(); if body.len() > MAX_MESSAGE_LEN { self.peer @@ -680,6 +681,17 @@ impl Server { .await?; return Ok(()); } + if body.is_empty() { + self.peer + .respond_with_error( + receipt, + proto::Error { + message: "message can't be blank".to_string(), + }, + ) + .await?; + return Ok(()); + } let timestamp = OffsetDateTime::now_utc(); let message_id = self @@ -1632,6 +1644,23 @@ mod tests { this.get_channel(channel_id.to_proto(), cx).unwrap() }); + // Messages aren't allowed to be too long. + channel_a + .update(&mut cx_a, |channel, cx| { + let long_body = "this is long.\n".repeat(1024); + channel.send_message(long_body, cx).unwrap() + }) + .await + .unwrap_err(); + + // Messages aren't allowed to be blank. + channel_a + .update(&mut cx_a, |channel, cx| { + channel.send_message(String::new(), cx).unwrap() + }) + .await + .unwrap_err(); + // Leading and trailing whitespace are trimmed. channel_a .update(&mut cx_a, |channel, cx| { @@ -1650,15 +1679,6 @@ mod tests { .collect::>(), &["surrounded by whitespace"] ); - - // Messages aren't allowed to be too long. - channel_a - .update(&mut cx_a, |channel, cx| { - let long_body = "this is long.\n".repeat(1024); - channel.send_message(long_body, cx).unwrap() - }) - .await - .unwrap_err(); } struct TestServer { diff --git a/zed/src/channel.rs b/zed/src/channel.rs index 6bac81fa199c93564a82dc611d817a75eb030ba8..858bbb5ce163a0869d0c9ec7e1d7e9eac84cb737 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -229,6 +229,10 @@ impl Channel { body: String, cx: &mut ModelContext, ) -> Result>> { + if body.is_empty() { + Err(anyhow!("message body can't be empty"))?; + } + let channel_id = self.details.id; let current_user_id = self.current_user_id()?; let local_id = self.next_local_message_id; From 2b39107b490ab98634c642895cfca4a699b03b5a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Aug 2021 15:41:16 +0200 Subject: [PATCH 130/204] Introduce `Scene::push_foreground_layer` --- gpui/src/platform/mac/renderer.rs | 4 +-- gpui/src/scene.rs | 42 +++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index 82e80790cb83d0dfe542cb2aaf4a50ef4d41aff6..d125fe136675df9dd776f3f79ad2b21fe71668b7 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -142,7 +142,7 @@ impl Renderer { let mut sprites = Vec::new(); let mut vertices = Vec::::new(); let mut current_atlas_id = None; - for (layer_id, layer) in scene.layers().iter().enumerate() { + for (layer_id, layer) in scene.layers().enumerate() { for path in layer.paths() { let origin = path.bounds.origin() * scene.scale_factor(); let size = (path.bounds.size() * scene.scale_factor()).ceil(); @@ -285,7 +285,7 @@ impl Renderer { let mut path_sprites = path_sprites.into_iter().peekable(); - for (layer_id, layer) in scene.layers().iter().enumerate() { + for (layer_id, layer) in scene.layers().enumerate() { self.clip(scene, layer, drawable_size, command_encoder); self.render_shadows(scene, layer, offset, drawable_size, command_encoder); self.render_quads(scene, layer, offset, drawable_size, command_encoder); diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 3818a0870120ba7a0183db69ad3d9dfc1ecb8b60..d974db73e193d80fb223bd5c4ba51c06ffda9012 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -12,7 +12,9 @@ use crate::{ pub struct Scene { scale_factor: f32, layers: Vec, - active_layer_stack: Vec, + foreground_layers: Vec, + active_layer_stack: Vec<(usize, bool)>, + pending_foreground_layers: usize, } #[derive(Default)] @@ -123,7 +125,9 @@ impl Scene { Scene { scale_factor, layers: vec![Layer::new(None)], - active_layer_stack: vec![0], + foreground_layers: Default::default(), + active_layer_stack: vec![(0, false)], + pending_foreground_layers: 0, } } @@ -131,19 +135,32 @@ impl Scene { self.scale_factor } - pub fn layers(&self) -> &[Layer] { - self.layers.as_slice() + pub fn layers(&self) -> impl Iterator { + self.layers.iter().chain(self.foreground_layers.iter()) } pub fn push_layer(&mut self, clip_bounds: Option) { - let ix = self.layers.len(); - self.layers.push(Layer::new(clip_bounds)); - self.active_layer_stack.push(ix); + if self.pending_foreground_layers == 0 { + let ix = self.layers.len(); + self.layers.push(Layer::new(clip_bounds)); + self.active_layer_stack.push((ix, false)); + } else { + let ix = self.foreground_layers.len(); + self.foreground_layers.push(Layer::new(clip_bounds)); + self.active_layer_stack.push((ix, true)); + } + } + + pub fn push_foreground_layer(&mut self, clip_bounds: Option) { + self.pending_foreground_layers += 1; + self.push_layer(clip_bounds); } pub fn pop_layer(&mut self) { - assert!(self.active_layer_stack.len() > 1); - self.active_layer_stack.pop(); + let (_, foreground) = self.active_layer_stack.pop().unwrap(); + if foreground { + self.pending_foreground_layers -= 1; + } } pub fn push_quad(&mut self, quad: Quad) { @@ -167,7 +184,12 @@ impl Scene { } fn active_layer(&mut self) -> &mut Layer { - &mut self.layers[*self.active_layer_stack.last().unwrap()] + let (ix, foreground) = *self.active_layer_stack.last().unwrap(); + if foreground { + &mut self.foreground_layers[ix] + } else { + &mut self.layers[ix] + } } } From 171627b63ab9a1715766c00f15e06d82c50b199c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Aug 2021 15:42:03 +0200 Subject: [PATCH 131/204] Add a new zero-sized `Overlay` element and paint it in the foreground --- gpui/src/elements.rs | 2 ++ gpui/src/elements/overlay.rs | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 gpui/src/elements/overlay.rs diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index c533b938a3cc170ae02ba536b964c0e1bcff0306..8b0c338a422ce0d7fb70479942d5ef013032d930 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -9,6 +9,7 @@ mod label; mod line_box; mod list; mod mouse_event_handler; +mod overlay; mod stack; mod svg; mod text; @@ -26,6 +27,7 @@ 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::*; diff --git a/gpui/src/elements/overlay.rs b/gpui/src/elements/overlay.rs new file mode 100644 index 0000000000000000000000000000000000000000..25d3a8d3daf5bf242970da033bf3a2bf7ec766b1 --- /dev/null +++ b/gpui/src/elements/overlay.rs @@ -0,0 +1,57 @@ +use crate::{ + geometry::{rect::RectF, vector::Vector2F}, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, +}; + +pub struct Overlay { + child: ElementBox, +} + +impl Overlay { + pub fn new(child: ElementBox) -> Self { + Self { child } + } +} + +impl Element for Overlay { + type LayoutState = Vector2F; + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let size = self.child.layout(constraint, cx); + (Vector2F::zero(), size) + } + + fn paint(&mut self, bounds: RectF, size: &mut Self::LayoutState, cx: &mut PaintContext) { + let bounds = RectF::new(bounds.origin(), *size); + cx.scene.push_foreground_layer(Some(bounds)); + self.child.paint(bounds.origin(), cx); + cx.scene.pop_layer(); + } + + fn dispatch_event( + &mut self, + event: &Event, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + cx: &mut EventContext, + ) -> bool { + self.child.dispatch_event(event, cx) + } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &DebugContext, + ) -> serde_json::Value { + self.child.debug(cx) + } +} From 87cd668a338f80cf98279734fd9c928636b70972 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Aug 2021 17:15:09 +0200 Subject: [PATCH 132/204] Replace foreground layers with stacking contexts Co-Authored-By: Nathan Sobo --- gpui/src/elements/overlay.rs | 4 +- gpui/src/scene.rs | 91 ++++++++++++++++++++++++------------ 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/gpui/src/elements/overlay.rs b/gpui/src/elements/overlay.rs index 25d3a8d3daf5bf242970da033bf3a2bf7ec766b1..a4c44a00130ef94f1041fc255bfdce24e54958fb 100644 --- a/gpui/src/elements/overlay.rs +++ b/gpui/src/elements/overlay.rs @@ -29,9 +29,9 @@ impl Element for Overlay { fn paint(&mut self, bounds: RectF, size: &mut Self::LayoutState, cx: &mut PaintContext) { let bounds = RectF::new(bounds.origin(), *size); - cx.scene.push_foreground_layer(Some(bounds)); + cx.scene.push_stacking_context(None); self.child.paint(bounds.origin(), cx); - cx.scene.pop_layer(); + cx.scene.pop_stacking_context(); } fn dispatch_event( diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index d974db73e193d80fb223bd5c4ba51c06ffda9012..4edf58c40f95ab9f0896770e07bb32056280d074 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -11,10 +11,13 @@ use crate::{ pub struct Scene { scale_factor: f32, + stacking_contexts: Vec, + active_stacking_context_stack: Vec, +} + +struct StackingContext { layers: Vec, - foreground_layers: Vec, - active_layer_stack: Vec<(usize, bool)>, - pending_foreground_layers: usize, + active_layer_stack: Vec, } #[derive(Default)] @@ -122,12 +125,11 @@ pub struct PathVertex { impl Scene { pub fn new(scale_factor: f32) -> Self { + let stacking_context = StackingContext::new(None); Scene { scale_factor, - layers: vec![Layer::new(None)], - foreground_layers: Default::default(), - active_layer_stack: vec![(0, false)], - pending_foreground_layers: 0, + stacking_contexts: vec![stacking_context], + active_stacking_context_stack: vec![0], } } @@ -136,31 +138,27 @@ impl Scene { } pub fn layers(&self) -> impl Iterator { - self.layers.iter().chain(self.foreground_layers.iter()) + self.stacking_contexts.iter().flat_map(|s| &s.layers) } - pub fn push_layer(&mut self, clip_bounds: Option) { - if self.pending_foreground_layers == 0 { - let ix = self.layers.len(); - self.layers.push(Layer::new(clip_bounds)); - self.active_layer_stack.push((ix, false)); - } else { - let ix = self.foreground_layers.len(); - self.foreground_layers.push(Layer::new(clip_bounds)); - self.active_layer_stack.push((ix, true)); - } + pub fn push_stacking_context(&mut self, clip_bounds: Option) { + self.active_stacking_context_stack + .push(self.stacking_contexts.len()); + self.stacking_contexts + .push(StackingContext::new(clip_bounds)) + } + + pub fn pop_stacking_context(&mut self) { + self.active_stacking_context_stack.pop(); + assert!(!self.active_stacking_context_stack.is_empty()); } - pub fn push_foreground_layer(&mut self, clip_bounds: Option) { - self.pending_foreground_layers += 1; - self.push_layer(clip_bounds); + pub fn push_layer(&mut self, clip_bounds: Option) { + self.active_stacking_context().push_layer(clip_bounds); } pub fn pop_layer(&mut self) { - let (_, foreground) = self.active_layer_stack.pop().unwrap(); - if foreground { - self.pending_foreground_layers -= 1; - } + self.active_stacking_context().pop_layer(); } pub fn push_quad(&mut self, quad: Quad) { @@ -183,14 +181,47 @@ impl Scene { self.active_layer().push_path(path); } + fn active_stacking_context(&mut self) -> &mut StackingContext { + let ix = *self.active_stacking_context_stack.last().unwrap(); + &mut self.stacking_contexts[ix] + } + fn active_layer(&mut self) -> &mut Layer { - let (ix, foreground) = *self.active_layer_stack.last().unwrap(); - if foreground { - &mut self.foreground_layers[ix] - } else { - &mut self.layers[ix] + self.active_stacking_context().active_layer() + } +} + +impl StackingContext { + fn new(clip_bounds: Option) -> Self { + Self { + layers: vec![Layer::new(clip_bounds)], + active_layer_stack: vec![0], } } + + fn active_layer(&mut self) -> &mut Layer { + &mut self.layers[*self.active_layer_stack.last().unwrap()] + } + + fn push_layer(&mut self, clip_bounds: Option) { + let clip_bounds = clip_bounds.map(|clip_bounds| { + clip_bounds + .intersection(self.active_layer().clip_bounds.unwrap_or(clip_bounds)) + .unwrap_or_else(|| { + log::warn!("specified clip bounds are disjoint from parent layer"); + RectF::default() + }) + }); + + let ix = self.layers.len(); + self.layers.push(Layer::new(clip_bounds)); + self.active_layer_stack.push(ix); + } + + fn pop_layer(&mut self) { + self.active_layer_stack.pop().unwrap(); + assert!(!self.active_layer_stack.is_empty()); + } } impl Layer { From 917a80ec360f796e145e3fbf66f0646bfa686708 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Aug 2021 17:24:51 +0200 Subject: [PATCH 133/204] :art: Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 2 +- gpui/src/elements/list.rs | 8 ++++---- gpui/src/presenter.rs | 7 +++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 69591d8052fbad135ce5e226f2a98ace744ab963..c15268c102287396325323855a69b97f8e7e68fa 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -1248,7 +1248,7 @@ impl MutableAppContext { ) } - pub fn render_cx( + pub fn build_render_context( &mut self, window_id: usize, view_id: usize, diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index c7950482c379e0fdfc90e9fa93cd5980ab976197..bfbb6a7b737e9a70364d9d3507e74091590d6d11 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -440,13 +440,13 @@ mod tests { let mut list = List::new( state.clone(), - &cx.render_cx::(0, 0, 0., false), + &cx.build_render_context::(0, 0, 0., false), |range| elements[range].iter().copied().map(item), ) .boxed(); let size = list.layout( SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)), - &mut presenter.layout_cx(cx), + &mut presenter.build_layout_context(cx), ); assert_eq!(size, vec2f(100., 40.)); assert_eq!( @@ -473,13 +473,13 @@ mod tests { let mut list = List::new( state.clone(), - &cx.render_cx::(0, 0, 0., false), + &cx.build_render_context::(0, 0, 0., false), |range| elements[range].iter().copied().map(item), ) .boxed(); let size = list.layout( SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)), - &mut presenter.layout_cx(cx), + &mut presenter.build_layout_context(cx), ); assert_eq!(size, vec2f(100., 40.)); assert_eq!( diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index c2c670f1e706a010f452b2e7fec6277e5497030a..31c91688dbc4957299a5fcc5dbe41b3673d22d2d 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -124,12 +124,15 @@ impl Presenter { fn layout(&mut self, size: Vector2F, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { - self.layout_cx(cx) + self.build_layout_context(cx) .layout(root_view_id, SizeConstraint::strict(size)); } } - pub fn layout_cx<'a>(&'a mut self, cx: &'a mut MutableAppContext) -> LayoutContext<'a> { + pub fn build_layout_context<'a>( + &'a mut self, + cx: &'a mut MutableAppContext, + ) -> LayoutContext<'a> { LayoutContext { rendered_views: &mut self.rendered_views, parents: &mut self.parents, From ef89ceae4d833515d39680801e1be2ea1087d7c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Aug 2021 17:51:26 +0200 Subject: [PATCH 134/204] Take &mut self in `View::render` Co-Authored-By: Nathan Sobo --- gpui/examples/text.rs | 2 +- gpui/src/app.rs | 36 ++++++++++++++++++------------------ gpui/src/elements/list.rs | 2 +- server/src/rpc.rs | 2 +- zed/src/chat_panel.rs | 2 +- zed/src/editor.rs | 2 +- zed/src/file_finder.rs | 2 +- zed/src/project_browser.rs | 2 +- zed/src/theme_selector.rs | 2 +- zed/src/workspace.rs | 2 +- zed/src/workspace/pane.rs | 2 +- 11 files changed, 28 insertions(+), 28 deletions(-) diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index b76d31622d659ed30ac649d9413151de0b44afcb..123481b9910fa113219c6a744b85d3f964b321c2 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -28,7 +28,7 @@ impl gpui::View for TextView { "View" } - fn render(&self, _: &mut gpui::RenderContext) -> gpui::ElementBox { + fn render(&mut self, _: &mut gpui::RenderContext) -> gpui::ElementBox { TextElement.boxed() } } diff --git a/gpui/src/app.rs b/gpui/src/app.rs index c15268c102287396325323855a69b97f8e7e68fa..edfed5af1de39c272ca39ac13942a7b881bb2656 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -40,7 +40,7 @@ pub trait Entity: 'static { pub trait View: Entity + Sized { fn ui_name() -> &'static str; - fn render(&self, cx: &mut RenderContext<'_, Self>) -> ElementBox; + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox; fn on_focus(&mut self, _: &mut ViewContext) {} fn on_blur(&mut self, _: &mut ViewContext) {} fn keymap_context(&self, _: &AppContext) -> keymap::Context { @@ -817,7 +817,7 @@ impl MutableAppContext { titlebar_height: f32, refreshing: bool, ) -> Result { - let view = self + let mut view = self .cx .views .remove(&(window_id, view_id)) @@ -1863,7 +1863,7 @@ pub trait AnyView { fn release(&mut self, cx: &mut MutableAppContext); fn ui_name(&self) -> &'static str; fn render<'a>( - &self, + &mut self, window_id: usize, view_id: usize, titlebar_height: f32, @@ -1896,7 +1896,7 @@ where } fn render<'a>( - &self, + &mut self, window_id: usize, view_id: usize, titlebar_height: f32, @@ -3368,7 +3368,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3432,7 +3432,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { let mouse_down_count = self.mouse_down_count.clone(); EventHandler::new(Empty::new().boxed()) .on_mouse_down(move |_| { @@ -3494,7 +3494,7 @@ mod tests { "View" } - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3534,7 +3534,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3590,7 +3590,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3640,7 +3640,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3684,7 +3684,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3731,7 +3731,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3789,7 +3789,7 @@ mod tests { } impl View for ViewA { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3807,7 +3807,7 @@ mod tests { } impl View for ViewB { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3903,7 +3903,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } @@ -4038,7 +4038,7 @@ mod tests { "test view" } - fn render(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -4083,7 +4083,7 @@ mod tests { "test view" } - fn render(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -4106,7 +4106,7 @@ mod tests { "test view" } - fn render(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index bfbb6a7b737e9a70364d9d3507e74091590d6d11..5c1a3495e3619a0174c3eee47e34e2f9a33ad332 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -510,7 +510,7 @@ mod tests { "TestView" } - fn render(&self, _: &mut RenderContext<'_, Self>) -> ElementBox { + fn render(&mut self, _: &mut RenderContext<'_, Self>) -> ElementBox { unimplemented!() } } diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 5a0199b725c5e59a6376b633d40e92d3df11c256..8a7f2d43af07be09b32aaa7aa072c4888e7e41f9 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1776,7 +1776,7 @@ mod tests { "empty view" } - fn render(&self, _: &mut gpui::RenderContext) -> gpui::ElementBox { + fn render(&mut self, _: &mut gpui::RenderContext) -> gpui::ElementBox { gpui::Element::boxed(gpui::elements::Empty) } } diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index e94f5712e574954b141399cf492f36ccb186e8f9..20b6b52525a2b7e7e8bcb53ef0c6690be31e8f9a 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -220,7 +220,7 @@ impl View for ChatPanel { "ChatPanel" } - fn render(&self, cx: &mut RenderContext) -> ElementBox { + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; Container::new( Flex::column() diff --git a/zed/src/editor.rs b/zed/src/editor.rs index e076c22ae4dbf4808c54f388d14a15c7e901d2d4..64aa3ed4302e1744a7cc2d5f0daef038fa241221 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -2522,7 +2522,7 @@ impl Entity for Editor { } impl View for Editor { - fn render<'a>(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { EditorElement::new(self.handle.clone()).boxed() } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 464b7e3589bad1b36e9c61bbe592aa5041915e9c..c31d617201730ef840aef4334e7c144708b591c6 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -79,7 +79,7 @@ impl View for FileFinder { "FileFinder" } - fn render(&self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); Align::new( diff --git a/zed/src/project_browser.rs b/zed/src/project_browser.rs index 5793b756188ad9dbe0cbfdcfbb2ca40242da88ff..796441c7041e737ec9a78dff95f49dc3cc09595f 100644 --- a/zed/src/project_browser.rs +++ b/zed/src/project_browser.rs @@ -13,7 +13,7 @@ impl View for ProjectBrowser { "ProjectBrowser" } - fn render(&self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { Empty::new().boxed() } } diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index a20ea8f75877b7ff9ac0ed47f84672728e34f690..e1e4a84c8a3b0e04bb0a4705b66eaa2fb6230487 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -269,7 +269,7 @@ impl View for ThemeSelector { "ThemeSelector" } - fn render(&self, cx: &mut RenderContext) -> ElementBox { + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); Align::new( diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 4d6e0b6b754fd050f44d6fe3bd236827ba9ab8d0..8fd1d600109e9655f19b253ab8e989c7cc04617a 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -944,7 +944,7 @@ impl View for Workspace { "Workspace" } - fn render(&self, cx: &mut RenderContext) -> ElementBox { + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); Container::new( Flex::column() diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 34f44253f32b1b3bce3d1c1955417b1c39f57ef1..5d78b9a20ee3742b95ecb6aad72e67dde545d455 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -369,7 +369,7 @@ impl View for Pane { "Pane" } - fn render(&self, cx: &mut RenderContext) -> ElementBox { + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { if let Some(active_item) = self.active_item() { Flex::column() .with_child(self.render_tabs(cx)) From ff01b52819170ed15fe18ede4ed195c9a67b9c71 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 30 Aug 2021 20:15:16 +0200 Subject: [PATCH 135/204] WIP: start on Select --- gpui/src/lib.rs | 1 + gpui/src/views.rs | 3 +++ gpui/src/views/select.rs | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 gpui/src/views.rs create mode 100644 gpui/src/views/select.rs diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index 10877e33a00542b248a8ef84ae4b50f016005bf4..6cb1c6f39d7d5caee1b94516461a371335924231 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -7,6 +7,7 @@ mod test; pub use assets::*; pub mod elements; pub mod font_cache; +pub mod views; pub use font_cache::FontCache; mod clipboard; pub use clipboard::ClipboardItem; diff --git a/gpui/src/views.rs b/gpui/src/views.rs new file mode 100644 index 0000000000000000000000000000000000000000..470d1d019052dc6db768192f6d2800c734676d75 --- /dev/null +++ b/gpui/src/views.rs @@ -0,0 +1,3 @@ +mod select; + +pub use select::*; diff --git a/gpui/src/views/select.rs b/gpui/src/views/select.rs new file mode 100644 index 0000000000000000000000000000000000000000..a97aa201c8038b4698c3154fda44729a674b7fd7 --- /dev/null +++ b/gpui/src/views/select.rs @@ -0,0 +1,24 @@ +use crate::{elements::*, Entity, RenderContext, View}; +use std::ops::Range; + +pub struct Select { + selected_ix: Option, + render_selected_element: Box, + render_elements: Box, &mut RenderContext)>, +} + +pub enum Event {} + +impl Entity for Select { + type Event = Event; +} + +impl View for Select { + fn ui_name() -> &'static str { + "Select" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + todo!() + } +} From 73a8fda9c7b73ec759c92c375e90cbe1ec8db9c4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 30 Aug 2021 17:59:13 -0700 Subject: [PATCH 136/204] Get the channel select looking good Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 52 +++++--- gpui/src/elements/mouse_event_handler.rs | 9 +- gpui/src/elements/uniform_list.rs | 8 +- gpui/src/views.rs | 6 +- gpui/src/views/select.rs | 145 ++++++++++++++++++++++- zed/assets/themes/_base.toml | 37 +++++- zed/src/chat_panel.rs | 130 +++++++++++++------- zed/src/theme.rs | 35 +++++- zed/src/workspace/pane.rs | 4 +- zed/src/workspace/sidebar.rs | 4 +- 10 files changed, 348 insertions(+), 82 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index edfed5af1de39c272ca39ac13942a7b881bb2656..5b6d3d87a35aef0c0d38aeb346fcd532c41f1c26 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -294,7 +294,9 @@ impl App { let platform = self.0.borrow().foreground_platform.clone(); platform.run(Box::new(move || { let mut cx = self.0.borrow_mut(); - on_finish_launching(&mut *cx); + let cx = &mut *cx; + crate::views::init(cx); + on_finish_launching(cx); })) } @@ -1306,7 +1308,7 @@ impl MutableAppContext { pub fn element_state( &mut self, - id: usize, + id: ElementStateId, ) -> ElementStateHandle { let key = (TypeId::of::(), id); self.cx @@ -1699,7 +1701,7 @@ pub struct AppContext { models: HashMap>, views: HashMap<(usize, usize), Box>, windows: HashMap, - element_states: HashMap<(TypeId, usize), Box>, + element_states: HashMap<(TypeId, ElementStateId), Box>, background: Arc, ref_counts: Arc>, font_cache: Arc, @@ -2977,6 +2979,10 @@ impl WeakViewHandle { } } + pub fn id(&self) -> usize { + self.view_id + } + pub fn upgrade(&self, cx: &AppContext) -> Option> { if cx.ref_counts.lock().is_entity_alive(self.view_id) { Some(ViewHandle::new( @@ -3000,15 +3006,30 @@ impl Clone for WeakViewHandle { } } +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct ElementStateId(usize, usize); + +impl From for ElementStateId { + fn from(id: usize) -> Self { + Self(id, 0) + } +} + +impl From<(usize, usize)> for ElementStateId { + fn from(id: (usize, usize)) -> Self { + Self(id.0, id.1) + } +} + pub struct ElementStateHandle { value_type: PhantomData, tag_type_id: TypeId, - id: usize, + id: ElementStateId, ref_counts: Weak>, } impl ElementStateHandle { - fn new(tag_type_id: TypeId, id: usize, ref_counts: &Arc>) -> Self { + fn new(tag_type_id: TypeId, id: ElementStateId, ref_counts: &Arc>) -> Self { ref_counts.lock().inc_element_state(tag_type_id, id); Self { value_type: PhantomData, @@ -3128,10 +3149,10 @@ impl Drop for Subscription { #[derive(Default)] struct RefCounts { entity_counts: HashMap, - element_state_counts: HashMap<(TypeId, usize), usize>, + element_state_counts: HashMap<(TypeId, ElementStateId), usize>, dropped_models: HashSet, dropped_views: HashSet<(usize, usize)>, - dropped_element_states: HashSet<(TypeId, usize)>, + dropped_element_states: HashSet<(TypeId, ElementStateId)>, } impl RefCounts { @@ -3155,11 +3176,14 @@ impl RefCounts { } } - fn inc_element_state(&mut self, tag_type_id: TypeId, id: usize) { - *self - .element_state_counts - .entry((tag_type_id, id)) - .or_insert(0) += 1; + fn inc_element_state(&mut self, tag_type_id: TypeId, id: ElementStateId) { + match self.element_state_counts.entry((tag_type_id, id)) { + Entry::Occupied(mut entry) => *entry.get_mut() += 1, + Entry::Vacant(entry) => { + entry.insert(1); + self.dropped_element_states.remove(&(tag_type_id, id)); + } + } } fn dec_model(&mut self, model_id: usize) { @@ -3180,7 +3204,7 @@ impl RefCounts { } } - fn dec_element_state(&mut self, tag_type_id: TypeId, id: usize) { + fn dec_element_state(&mut self, tag_type_id: TypeId, id: ElementStateId) { let key = (tag_type_id, id); let count = self.element_state_counts.get_mut(&key).unwrap(); *count -= 1; @@ -3199,7 +3223,7 @@ impl RefCounts { ) -> ( HashSet, HashSet<(usize, usize)>, - HashSet<(TypeId, usize)>, + HashSet<(TypeId, ElementStateId)>, ) { let mut dropped_models = HashSet::new(); let mut dropped_views = HashSet::new(); diff --git a/gpui/src/elements/mouse_event_handler.rs b/gpui/src/elements/mouse_event_handler.rs index 9336c8b84a20178c10f57cb5632359b108d98d5c..c9d9bb7a0100b9206674f7e1b847bee88994b8ea 100644 --- a/gpui/src/elements/mouse_event_handler.rs +++ b/gpui/src/elements/mouse_event_handler.rs @@ -3,8 +3,8 @@ use std::ops::DerefMut; use crate::{ geometry::{rect::RectF, vector::Vector2F}, platform::CursorStyle, - CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext, - LayoutContext, MutableAppContext, PaintContext, SizeConstraint, + CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateHandle, ElementStateId, + Event, EventContext, LayoutContext, MutableAppContext, PaintContext, SizeConstraint, }; use serde_json::json; @@ -25,13 +25,14 @@ pub struct MouseState { } impl MouseEventHandler { - pub fn new(id: usize, cx: &mut C, render_child: F) -> Self + pub fn new(id: Id, cx: &mut C, render_child: F) -> Self where Tag: 'static, F: FnOnce(&MouseState, &mut C) -> ElementBox, C: DerefMut, + Id: Into, { - let state_handle = cx.element_state::(id); + let state_handle = cx.element_state::(id.into()); let child = state_handle.update(cx, |state, cx| render_child(state, cx)); Self { state: state_handle, diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index 04567d4223a7d12e28ab846bf97619ad36eaa4fa..62490e6f9b61bb0d6952eb16e00d403ff4dd58eb 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -5,7 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{self, json}, - AppContext, ElementBox, + ElementBox, MutableAppContext, }; use json::ToJson; use parking_lot::Mutex; @@ -38,7 +38,7 @@ pub struct LayoutState { pub struct UniformList where - F: Fn(Range, &mut Vec, &AppContext), + F: Fn(Range, &mut Vec, &mut MutableAppContext), { state: UniformListState, item_count: usize, @@ -47,7 +47,7 @@ where impl UniformList where - F: Fn(Range, &mut Vec, &AppContext), + F: Fn(Range, &mut Vec, &mut MutableAppContext), { pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self { Self { @@ -102,7 +102,7 @@ where impl Element for UniformList where - F: Fn(Range, &mut Vec, &AppContext), + F: Fn(Range, &mut Vec, &mut MutableAppContext), { type LayoutState = LayoutState; type PaintState = (); diff --git a/gpui/src/views.rs b/gpui/src/views.rs index 470d1d019052dc6db768192f6d2800c734676d75..73f8a5751830f6d4f09044883c31d50ffb346b78 100644 --- a/gpui/src/views.rs +++ b/gpui/src/views.rs @@ -1,3 +1,7 @@ mod select; -pub use select::*; +pub use select::{ItemType, Select, SelectStyle}; + +pub fn init(cx: &mut super::MutableAppContext) { + select::init(cx); +} diff --git a/gpui/src/views/select.rs b/gpui/src/views/select.rs index a97aa201c8038b4698c3154fda44729a674b7fd7..3a40133665f95afbae302564921d4e12b1c5935e 100644 --- a/gpui/src/views/select.rs +++ b/gpui/src/views/select.rs @@ -1,14 +1,79 @@ -use crate::{elements::*, Entity, RenderContext, View}; -use std::ops::Range; +use crate::{ + action, elements::*, AppContext, Entity, MutableAppContext, RenderContext, View, ViewContext, + WeakViewHandle, +}; pub struct Select { - selected_ix: Option, - render_selected_element: Box, - render_elements: Box, &mut RenderContext)>, + handle: WeakViewHandle, + render_item: Box ElementBox>, + selected_item_ix: usize, + item_count: usize, + is_open: bool, + list_state: UniformListState, + style: SelectStyle, } +#[derive(Clone, Default)] +pub struct SelectStyle { + pub header: ContainerStyle, + pub menu: ContainerStyle, +} + +pub enum ItemType { + Header, + Selected, + Unselected, +} + +action!(ToggleSelect); +action!(SelectItem, usize); + pub enum Event {} +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(Select::toggle); + cx.add_action(Select::select_item); +} + +impl Select { + pub fn new ElementBox>( + item_count: usize, + cx: &mut ViewContext, + render_item: F, + ) -> Self { + Self { + handle: cx.handle().downgrade(), + render_item: Box::new(render_item), + selected_item_ix: 0, + item_count, + is_open: false, + list_state: UniformListState::default(), + style: Default::default(), + } + } + + pub fn with_style(mut self, style: &SelectStyle) -> Self { + self.style = style.clone(); + self + } + + pub fn set_item_count(&mut self, count: usize, cx: &mut ViewContext) { + self.item_count = count; + cx.notify(); + } + + fn toggle(&mut self, _: &ToggleSelect, cx: &mut ViewContext) { + self.is_open = !self.is_open; + cx.notify(); + } + + fn select_item(&mut self, action: &SelectItem, cx: &mut ViewContext) { + self.selected_item_ix = action.0; + self.is_open = false; + cx.notify(); + } +} + impl Entity for Select { type Event = Event; } @@ -19,6 +84,74 @@ impl View for Select { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - todo!() + if self.item_count == 0 { + return Empty::new().boxed(); + } + + enum Header {} + enum Item {} + + let mut result = Flex::column().with_child( + MouseEventHandler::new::(self.handle.id(), cx, |mouse_state, cx| { + Container::new((self.render_item)( + self.selected_item_ix, + ItemType::Header, + mouse_state.hovered, + cx, + )) + .with_style(&self.style.header) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleSelect)) + .boxed(), + ); + if self.is_open { + let handle = self.handle.clone(); + result.add_child( + Overlay::new( + Container::new( + ConstrainedBox::new( + UniformList::new( + self.list_state.clone(), + self.item_count, + move |mut range, items, mut cx| { + let handle = handle.upgrade(cx).unwrap(); + let this = handle.read(cx); + let selected_item_ix = this.selected_item_ix; + range.end = range.end.min(this.item_count); + items.extend(range.map(|ix| { + MouseEventHandler::new::( + (handle.id(), ix), + &mut cx, + |mouse_state, cx| { + (handle.read(*cx).render_item)( + ix, + if ix == selected_item_ix { + ItemType::Selected + } else { + ItemType::Unselected + }, + mouse_state.hovered, + cx, + ) + }, + ) + .on_click(move |cx| cx.dispatch_action(SelectItem(ix))) + .boxed() + })) + }, + ) + .boxed(), + ) + .with_max_height(200.) + .boxed(), + ) + .with_style(&self.style.menu) + .boxed(), + ) + .boxed(), + ) + } + result.boxed() } } diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 8900c34fb0390982015bb59ddb4729fc1c620798..abc886a5257586e19446cdb9e8bac4c74cf718a7 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -26,15 +26,42 @@ color = "$text.2.color" color = "$text.0.color" [chat_panel] -padding = { top = 10.0, bottom = 10.0, left = 10.0, right = 10.0 } channel_name = { extends = "$text.0", weight = "bold" } -channel_name_hash = { text = "$text.2", padding.right = 5.0 } +channel_name_hash = { text = "$text.2", padding.right = 5 } [chat_panel.message] body = "$text.1" -sender.margin.right = 10.0 -sender.text = { extends = "$text.0", weight = "bold" } -timestamp.text = "$text.2" +sender = { extends = "$text.0", weight = "bold", margin.right = 10.0 } +timestamp = "$text.2" +padding = { top = 10, bottom = 10, left = 10, right = 10 } + +[chat_panel.channel_select.item] +padding = { top = 4, bottom = 4, left = 4, right = 4 } +name = "$text.1" +hash = { extends = "$text.2", margin.right = 5.0 } + +[chat_panel.channel_select.hovered_item] +extends = "$chat_panel.channel_select.item" +background = "$surface.2" +corner_radius = 6.0 + +[chat_panel.channel_select.active_item] +extends = "$chat_panel.channel_select.item" +name = "$text.0" + +[chat_panel.channel_select.hovered_active_item] +extends = "$chat_panel.channel_select.hovered_item" +name = "$text.0" + +[chat_panel.channel_select.header] +extends = "$chat_panel.channel_select.active_item" +padding.bottom = 0 + +[chat_panel.channel_select.menu] +padding = { top = 4, bottom = 4, left = 4, right = 4 } +corner_radius = 6.0 +border = { color = "#000000", width = 1.0 } +background = "$surface.0" [selector] background = "$surface.2" diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 20b6b52525a2b7e7e8bcb53ef0c6690be31e8f9a..0eef94529cac28f58a87cdfce8397e5855634fb9 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -1,12 +1,17 @@ use crate::{ channel::{Channel, ChannelEvent, ChannelList, ChannelMessage}, editor::Editor, + theme, util::ResultExt, Settings, }; use gpui::{ - action, elements::*, keymap::Binding, Entity, ModelHandle, MutableAppContext, RenderContext, - Subscription, View, ViewContext, ViewHandle, + action, + elements::*, + keymap::Binding, + views::{ItemType, Select, SelectStyle}, + AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, View, + ViewContext, ViewHandle, }; use postage::watch; use time::{OffsetDateTime, UtcOffset}; @@ -16,6 +21,7 @@ pub struct ChatPanel { active_channel: Option<(ModelHandle, Subscription)>, message_list: ListState, input_editor: ViewHandle, + channel_select: ViewHandle, settings: watch::Receiver, + local_timezone: UtcOffset, } pub enum Event {} @@ -94,6 +95,7 @@ impl ChatPanel { input_editor, channel_select, settings, + local_timezone: cx.platform().local_timezone(), }; this.init_active_channel(cx); @@ -204,7 +206,7 @@ impl ChatPanel { .with_child( Container::new( Label::new( - format_timestamp(message.timestamp, now), + format_timestamp(message.timestamp, now, self.local_timezone), theme.timestamp.text.clone(), ) .boxed(), @@ -314,10 +316,13 @@ impl View for ChatPanel { } } -fn format_timestamp(mut timestamp: OffsetDateTime, mut now: OffsetDateTime) -> String { - let local_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC); - timestamp = timestamp.to_offset(local_offset); - now = now.to_offset(local_offset); +fn format_timestamp( + mut timestamp: OffsetDateTime, + mut now: OffsetDateTime, + local_timezone: UtcOffset, +) -> String { + timestamp = timestamp.to_offset(local_timezone); + now = now.to_offset(local_timezone); let today = now.date(); let date = timestamp.date(); From 1c43121ae0ce8b1e049c204f504ae358f4601e0e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Sep 2021 11:20:30 -0700 Subject: [PATCH 167/204] In theme, add an InputEditorSyle for styling small editors Co-Authored-By: Antonio Scandurra --- gpui/src/fonts.rs | 2 +- zed/assets/themes/_base.toml | 16 +++++----- zed/assets/themes/dark.toml | 4 +++ zed/assets/themes/light.toml | 4 +++ zed/src/chat_panel.rs | 7 ++++- zed/src/editor.rs | 7 ++++- zed/src/editor/display_map.rs | 22 +++++-------- zed/src/editor/element.rs | 19 +++++++++--- zed/src/file_finder.rs | 23 ++++++++------ zed/src/theme.rs | 57 +++++++++++++++++++++------------- zed/src/theme/highlight_map.rs | 1 - zed/src/theme_selector.rs | 20 +++++++----- 12 files changed, 115 insertions(+), 67 deletions(-) diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index 3010e1ada054a0500734f78c402a81f64201a38a..450d0ac398b1c7f07d22ebb4a5b71f44a50a6bf3 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -26,7 +26,7 @@ pub struct TextStyle { pub font_properties: Properties, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct HighlightStyle { pub color: Color, pub font_properties: Properties, diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index b92a1a55349d87f24dac32e47525b6fbf42acf51..f4b7be73d500db77b6cf45dd17fc02f8711ba738 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -65,6 +65,11 @@ corner_radius = 6 border = { color = "#000000", width = 1 } background = "$surface.0" +[chat_panel.input_editor] +text = "$text.1.color" +background = "$surface.1" +selection = "$selection.host" + [selector] background = "$surface.2" text = "$text.0" @@ -72,6 +77,7 @@ padding = 6 margin.top = 12 corner_radius = 6 shadow = { offset = [0, 0], blur = 12, color = "#00000088" } +input_editor = "$chat_panel.input_editor" [selector.item] background = "#424344" @@ -85,15 +91,11 @@ extends = "$selector.item" background = "#094771" [editor] +text = "$text.1.color" background = "$surface.1" gutter_background = "$surface.1" active_line_background = "$surface.2" line_number = "$text.2.color" line_number_active = "$text.0.color" -replicas = [ - { selection = "#264f78", cursor = "$text.0.color" }, - { selection = "#504f31", cursor = "#fcf154" }, -] - -[syntax] -default = "$text.1.color" +selection = "$selection.host" +guest_selections = "$selection.guests" diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index 9a8ac975bf2d0e1f8f80f23ba091d17b970649cb..035684761374fef7499e402fd28b70ac1e2d168a 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -11,6 +11,10 @@ base = { family = "Helvetica", size = 14.0 } 1 = { extends = "$text.base", color = "#b3b3b3" } 2 = { extends = "$text.base", color = "#7b7d80" } +[selection] +host = { selection = "#264f78", cursor = "$text.0.color" } +guests = [{ selection = "#504f31", cursor = "#fcf154" }] + [status] good = "#4fac63" info = "#3c5dd4" diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml index 564542817bf56e620365bb460150bdc66515738d..e70a088cc930d468f02234f23155fafae75bcc3e 100644 --- a/zed/assets/themes/light.toml +++ b/zed/assets/themes/light.toml @@ -12,6 +12,10 @@ base = { family = "Helvetica", size = 14.0 } 1 = { extends = "$text.base", color = "#111111" } 2 = { extends = "$text.base", color = "#333333" } +[selection] +host = { selection = "#264f78", cursor = "$text.0.color" } +guests = [{ selection = "#504f31", cursor = "#fcf154" }] + [status] good = "#4fac63" info = "#3c5dd4" diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 6b52f7edfd970d781e798959d2af91925fe090f0..dd2a3765108ae43a289bc468f264d7a53ab39353 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -46,7 +46,12 @@ impl ChatPanel { settings: watch::Receiver, cx: &mut ViewContext, ) -> Self { - let input_editor = cx.add_view(|cx| Editor::auto_height(settings.clone(), cx)); + let input_editor = cx.add_view(|cx| { + Editor::auto_height(settings.clone(), cx).with_style({ + let settings = settings.clone(); + move |_| settings.borrow().theme.chat_panel.input_editor.as_editor() + }) + }); let channel_select = cx.add_view(|cx| { let channel_list = channel_list.clone(); Select::new(0, cx, { diff --git a/zed/src/editor.rs b/zed/src/editor.rs index c240da268db10d9f9d710329a8b55d53ecc448dc..148b38eb0a15ca12308bf91a61a43743727deef9 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -2397,6 +2397,7 @@ impl Snapshot { pub fn layout_lines( &mut self, mut rows: Range, + style: &EditorStyle, font_cache: &FontCache, layout_cache: &TextLayoutCache, ) -> Result> { @@ -2433,7 +2434,11 @@ impl Snapshot { } if !line_chunk.is_empty() && !line_exceeded_max_len { - let style = self.theme.syntax.highlight_style(style_ix); + let style = self + .theme + .syntax + .highlight_style(style_ix) + .unwrap_or(style.text.clone()); // Avoid a lookup if the font properties match the previous ones. let font_id = if style.font_properties == prev_font_properties { prev_font_id diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 2758c030b3efc055ee450d058e8103460fc93563..16eeba2e12ee375d852c3bccac8559c700862c07 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -661,13 +661,10 @@ mod tests { (function_item name: (identifier) @fn.name)"#, ) .unwrap(); - let theme = SyntaxTheme::new( - Default::default(), - vec![ - ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), - ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), - ], - ); + let theme = SyntaxTheme::new(vec![ + ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), + ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), + ]); let lang = Arc::new(Language { config: LanguageConfig { name: "Test".to_string(), @@ -754,13 +751,10 @@ mod tests { (function_item name: (identifier) @fn.name)"#, ) .unwrap(); - let theme = SyntaxTheme::new( - Default::default(), - vec![ - ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), - ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), - ], - ); + let theme = SyntaxTheme::new(vec![ + ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), + ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), + ]); let lang = Arc::new(Language { config: LanguageConfig { name: "Test".to_string(), diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index b114400055e5d62f8b08d626239d66560bfe98ed..b0a8d97e5d7c104a085b107cc9778ed08473e1e7 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -280,7 +280,12 @@ impl EditorElement { let content_origin = bounds.origin() + layout.text_offset; for (replica_id, selections) in &layout.selections { - let replica_theme = theme.replicas[*replica_id as usize % theme.replicas.len()]; + let style_ix = *replica_id as usize % (theme.guest_selections.len() + 1); + let style = if style_ix == 0 { + &theme.selection + } else { + &theme.guest_selections[style_ix - 1] + }; for selection in selections { if selection.start != selection.end { @@ -294,7 +299,7 @@ impl EditorElement { }; let selection = Selection { - color: replica_theme.selection, + color: style.selection, line_height: layout.line_height, start_y: content_origin.y() + row_range.start as f32 * layout.line_height - scroll_top, @@ -337,7 +342,7 @@ impl EditorElement { - scroll_left; let y = selection.end.row() as f32 * layout.line_height - scroll_top; cursors.push(Cursor { - color: replica_theme.cursor, + color: style.cursor, origin: content_origin + vec2f(x, y), line_height: layout.line_height, }); @@ -507,8 +512,12 @@ impl Element for EditorElement { }; let mut max_visible_line_width = 0.0; - let line_layouts = match snapshot.layout_lines(start_row..end_row, font_cache, layout_cache) - { + 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); diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index c31d617201730ef840aef4334e7c144708b591c6..cd74709421832c6d47ae5cfd7579ffad0f500bdb 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -30,7 +30,7 @@ pub struct FileFinder { handle: WeakViewHandle, settings: watch::Receiver, workspace: WeakViewHandle, - query_buffer: ViewHandle, + query_editor: ViewHandle, search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, @@ -86,7 +86,7 @@ impl View for FileFinder { ConstrainedBox::new( Container::new( Flex::new(Axis::Vertical) - .with_child(ChildView::new(self.query_buffer.id()).boxed()) + .with_child(ChildView::new(self.query_editor.id()).boxed()) .with_child(Expanded::new(1.0, self.render_matches()).boxed()) .boxed(), ) @@ -102,7 +102,7 @@ impl View for FileFinder { } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_buffer); + cx.focus(&self.query_editor); } fn keymap_context(&self, _: &AppContext) -> keymap::Context { @@ -266,15 +266,20 @@ impl FileFinder { ) -> Self { cx.observe(&workspace, Self::workspace_updated).detach(); - let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx)); - cx.subscribe(&query_buffer, Self::on_query_editor_event) + 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() + }) + }); + cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); Self { handle: cx.handle().downgrade(), settings, workspace: workspace.downgrade(), - query_buffer, + query_editor, search_count: 0, latest_search_id: 0, latest_search_did_cancel: false, @@ -287,7 +292,7 @@ impl FileFinder { } fn workspace_updated(&mut self, _: ViewHandle, cx: &mut ViewContext) { - let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx)); + let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); if let Some(task) = self.spawn_search(query, cx) { task.detach(); } @@ -301,7 +306,7 @@ impl FileFinder { ) { match event { editor::Event::Edited => { - let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx)); + let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); if query.is_empty() { self.latest_search_id = util::post_inc(&mut self.search_count); self.matches.clear(); @@ -460,7 +465,7 @@ mod tests { .downcast::() .unwrap() }); - let query_buffer = cx.read(|cx| finder.read(cx).query_buffer.clone()); + let query_buffer = cx.read(|cx| finder.read(cx).query_editor.clone()); let chain = vec![finder.id(), query_buffer.id()]; cx.dispatch_action(window_id, chain.clone(), Insert("b".into())); diff --git a/zed/src/theme.rs b/zed/src/theme.rs index e1233314ca26e784090118b7572b8e1610f70870..ee39681459713d1cf7a5547a2ddc25201d6624a9 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -7,7 +7,7 @@ use gpui::{ elements::{ContainerStyle, LabelStyle}, fonts::{HighlightStyle, TextStyle}, }; -use serde::{de, Deserialize}; +use serde::Deserialize; use std::collections::HashMap; pub use highlight_map::*; @@ -28,7 +28,6 @@ pub struct Theme { pub struct SyntaxTheme { highlights: Vec<(String, HighlightStyle)>, - default_style: HighlightStyle, } #[derive(Deserialize)] @@ -69,6 +68,7 @@ pub struct ChatPanel { pub container: ContainerStyle, pub message: ChatMessage, pub channel_select: ChannelSelect, + pub input_editor: InputEditorStyle, } #[derive(Deserialize)] @@ -107,6 +107,7 @@ pub struct Selector { #[serde(flatten)] pub label: LabelStyle, + pub input_editor: InputEditorStyle, pub item: ContainedLabel, pub active_item: ContainedLabel, } @@ -129,33 +130,38 @@ pub struct ContainedLabel { #[derive(Clone, Deserialize)] pub struct EditorStyle { + pub 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 replicas: Vec, + pub guest_selections: Vec, +} + +#[derive(Clone, Deserialize)] +pub struct InputEditorStyle { + pub text: HighlightStyle, + pub background: Color, + pub selection: SelectionStyle, } #[derive(Clone, Copy, Default, Deserialize)] -pub struct Replica { +pub struct SelectionStyle { pub cursor: Color, pub selection: Color, } impl SyntaxTheme { - pub fn new(default_style: HighlightStyle, highlights: Vec<(String, HighlightStyle)>) -> Self { - Self { - default_style, - highlights, - } + pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self { + Self { highlights } } - pub fn highlight_style(&self, id: HighlightId) -> HighlightStyle { + pub fn highlight_style(&self, id: HighlightId) -> Option { self.highlights .get(id.0 as usize) .map(|entry| entry.1.clone()) - .unwrap_or_else(|| self.default_style.clone()) } #[cfg(test)] @@ -167,12 +173,28 @@ impl SyntaxTheme { impl Default for EditorStyle { fn default() -> Self { Self { + text: HighlightStyle { + color: Color::from_u32(0xff0000ff), + font_properties: Default::default(), + }, background: Default::default(), gutter_background: Default::default(), active_line_background: Default::default(), line_number: Default::default(), line_number_active: Default::default(), - replicas: vec![Default::default()], + selection: Default::default(), + guest_selections: Default::default(), + } + } +} + +impl InputEditorStyle { + pub fn as_editor(&self) -> EditorStyle { + EditorStyle { + text: self.text.clone(), + background: self.background, + selection: self.selection, + ..Default::default() } } } @@ -182,16 +204,9 @@ impl<'de> Deserialize<'de> for SyntaxTheme { where D: serde::Deserializer<'de>, { - let mut syntax_data: HashMap = - Deserialize::deserialize(deserializer)?; - - let mut result = Self { - highlights: Vec::<(String, HighlightStyle)>::new(), - default_style: syntax_data - .remove("default") - .ok_or_else(|| de::Error::custom("must specify a default color in syntax theme"))?, - }; + let syntax_data: HashMap = Deserialize::deserialize(deserializer)?; + let mut result = Self::new(Vec::new()); for (key, style) in syntax_data { match result .highlights diff --git a/zed/src/theme/highlight_map.rs b/zed/src/theme/highlight_map.rs index c030e2ab1a4f21b36dc040922a7349654e07a171..202e0d38be782b17b917e9b188f2768911c05b6b 100644 --- a/zed/src/theme/highlight_map.rs +++ b/zed/src/theme/highlight_map.rs @@ -69,7 +69,6 @@ mod tests { #[test] fn test_highlight_map() { let theme = SyntaxTheme::new( - Default::default(), [ ("function", Color::from_u32(0x100000ff)), ("function.method", Color::from_u32(0x200000ff)), diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index e1e4a84c8a3b0e04bb0a4705b66eaa2fb6230487..714e84b03fbb1d7b7113f25dc10300ca584eec8d 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -25,7 +25,7 @@ pub struct ThemeSelector { settings: watch::Receiver, registry: Arc, matches: Vec, - query_buffer: ViewHandle, + query_editor: ViewHandle, list_state: UniformListState, selected_index: usize, } @@ -60,15 +60,21 @@ impl ThemeSelector { registry: Arc, cx: &mut ViewContext, ) -> Self { - let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx)); - cx.subscribe(&query_buffer, Self::on_query_editor_event) + 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() + }) + }); + + cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); let mut this = Self { settings, settings_tx, registry, - query_buffer, + query_editor, matches: Vec::new(), list_state: Default::default(), selected_index: 0, @@ -151,7 +157,7 @@ impl ThemeSelector { string: name, }) .collect::>(); - let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx)); + let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); self.matches = if query.is_empty() { candidates @@ -276,7 +282,7 @@ impl View for ThemeSelector { ConstrainedBox::new( Container::new( Flex::new(Axis::Vertical) - .with_child(ChildView::new(self.query_buffer.id()).boxed()) + .with_child(ChildView::new(self.query_editor.id()).boxed()) .with_child(Expanded::new(1.0, self.render_matches(cx)).boxed()) .boxed(), ) @@ -292,7 +298,7 @@ impl View for ThemeSelector { } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_buffer); + cx.focus(&self.query_editor); } fn keymap_context(&self, _: &AppContext) -> keymap::Context { From 522bef2e3ae7ca9a3e1f70dbbc6af79c44e2ba7e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Sep 2021 17:36:56 -0700 Subject: [PATCH 168/204] Add placeholder text as a feature of Editor, use it in chat panel --- gpui/src/fonts.rs | 2 +- zed/assets/themes/_base.toml | 1 + zed/src/chat_panel.rs | 9 +++++++- zed/src/editor.rs | 41 +++++++++++++++++++++++++++++++++++ zed/src/editor/display_map.rs | 4 ++++ zed/src/theme.rs | 8 +++++++ 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index 450d0ac398b1c7f07d22ebb4a5b71f44a50a6bf3..3010e1ada054a0500734f78c402a81f64201a38a 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -26,7 +26,7 @@ pub struct TextStyle { pub font_properties: Properties, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct HighlightStyle { pub color: Color, pub font_properties: Properties, diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index f4b7be73d500db77b6cf45dd17fc02f8711ba738..1beab95ce749eb89a30808a12f384654da20c571 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -67,6 +67,7 @@ background = "$surface.0" [chat_panel.input_editor] text = "$text.1.color" +placeholder_text = "$text.2.color" background = "$surface.1" selection = "$selection.host" diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index dd2a3765108ae43a289bc468f264d7a53ab39353..8c6b85bbf1695f3de21ae3b772656eb2bffcec3a 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -156,7 +156,14 @@ impl ChatPanel { fn set_active_channel(&mut self, channel: ModelHandle, cx: &mut ViewContext) { if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) { - self.message_list.reset(channel.read(cx).message_count()); + { + let channel = channel.read(cx); + self.message_list.reset(channel.message_count()); + let placeholder = format!("Message #{}", channel.name()); + self.input_editor.update(cx, move |editor, cx| { + editor.set_placeholder_text(placeholder, cx); + }); + } let subscription = cx.subscribe(&channel, Self::channel_did_change); self.active_channel = Some((channel, subscription)); } diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 148b38eb0a15ca12308bf91a61a43743727deef9..b8c9601fe26a371e63f4d2247fd9a02382d3ca86 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -295,15 +295,18 @@ pub struct Editor { blink_epoch: usize, blinking_paused: bool, mode: EditorMode, + placeholder_text: Option>, } pub struct Snapshot { pub display_snapshot: DisplayMapSnapshot, + pub placeholder_text: Option>, pub gutter_visible: bool, pub auto_height: bool, pub theme: Arc, pub font_family: FamilyId, pub font_size: f32, + is_focused: bool, scroll_position: Vector2F, scroll_top_anchor: Anchor, } @@ -378,6 +381,7 @@ impl Editor { blink_epoch: 0, blinking_paused: false, mode: EditorMode::Full, + placeholder_text: None, } } @@ -407,11 +411,25 @@ impl Editor { scroll_position: self.scroll_position, 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) + .map_or(false, |handle| handle.is_focused(cx)), } } + pub fn set_placeholder_text( + &mut self, + placeholder_text: impl Into>, + cx: &mut ViewContext, + ) { + self.placeholder_text = Some(placeholder_text.into()); + cx.notify(); + } + fn set_scroll_position(&mut self, mut scroll_position: Vector2F, cx: &mut ViewContext) { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let scroll_top_buffer_offset = @@ -2406,6 +2424,29 @@ impl Snapshot { 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(), font_id, style.placeholder_text.color)], + ) + }) + .collect()); + } + let mut prev_font_properties = FontProperties::new(); let mut prev_font_id = font_cache .select_font(self.font_family, &prev_font_properties) diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 16eeba2e12ee375d852c3bccac8559c700862c07..425c4b97a5ec908f1a109b2fa6b63b27e5076b9b 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -108,6 +108,10 @@ impl DisplayMapSnapshot { self.folds_snapshot.fold_count() } + pub fn is_empty(&self) -> bool { + self.buffer_snapshot.len() == 0 + } + pub fn buffer_rows(&self, start_row: u32) -> BufferRows { self.wraps_snapshot.buffer_rows(start_row) } diff --git a/zed/src/theme.rs b/zed/src/theme.rs index ee39681459713d1cf7a5547a2ddc25201d6624a9..dcbec3ff2da3bdba4bbb904d4136e9e676a8544c 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -131,6 +131,8 @@ pub struct ContainedLabel { #[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, @@ -143,6 +145,7 @@ pub struct EditorStyle { #[derive(Clone, Deserialize)] pub struct InputEditorStyle { pub text: HighlightStyle, + pub placeholder_text: HighlightStyle, pub background: Color, pub selection: SelectionStyle, } @@ -177,6 +180,10 @@ impl Default for EditorStyle { color: Color::from_u32(0xff0000ff), font_properties: Default::default(), }, + placeholder_text: HighlightStyle { + color: Color::from_u32(0x00ff00ff), + font_properties: Default::default(), + }, background: Default::default(), gutter_background: Default::default(), active_line_background: Default::default(), @@ -192,6 +199,7 @@ impl InputEditorStyle { pub fn as_editor(&self) -> EditorStyle { EditorStyle { text: self.text.clone(), + placeholder_text: self.placeholder_text.clone(), background: self.background, selection: self.selection, ..Default::default() From ec36d818c0f65c26e52bec71c3bc6a970e4e338a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 09:49:47 +0200 Subject: [PATCH 169/204] Round corners in the chat panel's input editor --- zed/assets/themes/_base.toml | 5 +++++ zed/src/chat_panel.rs | 11 ++++++++--- zed/src/theme.rs | 1 + zed/src/workspace/sidebar.rs | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 1beab95ce749eb89a30808a12f384654da20c571..2d0cfd12369d270956908f8337f9dbce3bc50d23 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -65,6 +65,11 @@ corner_radius = 6 border = { color = "#000000", width = 1 } background = "$surface.0" +[chat_panel.input_editor_container] +background = "$surface.1" +corner_radius = 6 +padding = 6 + [chat_panel.input_editor] text = "$text.1.color" placeholder_text = "$text.2.color" diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 8c6b85bbf1695f3de21ae3b772656eb2bffcec3a..77c5a7777d1ae1a1b4f0570a54d247f90ed680a8 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -236,9 +236,14 @@ impl ChatPanel { } fn render_input_box(&self) -> ElementBox { - ConstrainedBox::new(ChildView::new(self.input_editor.id()).boxed()) - .with_max_height(100.) - .boxed() + let theme = &self.settings.borrow().theme; + Container::new( + ConstrainedBox::new(ChildView::new(self.input_editor.id()).boxed()) + .with_max_height(100.) + .boxed(), + ) + .with_style(&theme.chat_panel.input_editor_container) + .boxed() } fn render_channel_name( diff --git a/zed/src/theme.rs b/zed/src/theme.rs index dcbec3ff2da3bdba4bbb904d4136e9e676a8544c..e15070d81837fa4b9cbef50da934273c0aaf592b 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -68,6 +68,7 @@ pub struct ChatPanel { pub container: ContainerStyle, pub message: ChatMessage, pub channel_select: ChannelSelect, + pub input_editor_container: ContainerStyle, pub input_editor: InputEditorStyle, } diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 3d57c96c4f2422135e54bf31e1ed4be87f8d2eff..e415551a8f12a9787d3dfc8a38038708cf7759a2 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -37,7 +37,7 @@ impl Sidebar { side, items: Default::default(), active_item_ix: None, - width: Rc::new(RefCell::new(100.)), + width: Rc::new(RefCell::new(200.)), } } From a0dd41cdf65859bc61b4b5ead1b1546de50cc414 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 11:40:18 +0200 Subject: [PATCH 170/204] Add a `Flexible` element that works like in Flutter --- gpui/src/elements/flex.rs | 162 ++++++++++++++++++++++++++++------- zed/src/workspace.rs | 4 +- zed/src/workspace/pane.rs | 2 +- zed/src/workspace/sidebar.rs | 10 ++- 4 files changed, 143 insertions(+), 35 deletions(-) diff --git a/gpui/src/elements/flex.rs b/gpui/src/elements/flex.rs index 58e603fc6fd19a2db73123a6fdab4e9fb661a8cb..a71407cc2562eff46cbe12cca48a54e3ab001a5d 100644 --- a/gpui/src/elements/flex.rs +++ b/gpui/src/elements/flex.rs @@ -32,8 +32,46 @@ impl Flex { Self::new(Axis::Vertical) } - fn child_flex<'b>(child: &ElementBox) -> Option { - child.metadata::().map(|data| data.flex) + fn layout_flex_children( + &mut self, + expanded: bool, + constraint: SizeConstraint, + remaining_space: &mut f32, + remaining_flex: &mut f32, + cross_axis_max: &mut f32, + cx: &mut LayoutContext, + ) { + let cross_axis = self.axis.invert(); + for child in &mut self.children { + if let Some(metadata) = child.metadata::() { + if metadata.expanded != expanded { + continue; + } + + let flex = metadata.flex; + let child_max = if *remaining_flex == 0.0 { + *remaining_space + } else { + let space_per_flex = *remaining_space / *remaining_flex; + space_per_flex * flex + }; + let child_min = if expanded { child_max } else { 0. }; + let child_constraint = match self.axis { + Axis::Horizontal => SizeConstraint::new( + vec2f(child_min, constraint.min.y()), + vec2f(child_max, constraint.max.y()), + ), + Axis::Vertical => SizeConstraint::new( + vec2f(constraint.min.x(), child_min), + vec2f(constraint.max.x(), child_max), + ), + }; + let child_size = child.layout(child_constraint, cx); + *remaining_space -= child_size.along(self.axis); + *remaining_flex -= flex; + *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis)); + } + } } } @@ -58,8 +96,8 @@ impl Element for Flex { let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; for child in &mut self.children { - if let Some(flex) = Self::child_flex(&child) { - total_flex += flex; + if let Some(metadata) = child.metadata::() { + total_flex += metadata.flex; } else { let child_constraint = match self.axis { Axis::Horizontal => SizeConstraint::new( @@ -84,30 +122,22 @@ impl Element for Flex { let mut remaining_space = constraint.max_along(self.axis) - fixed_space; let mut remaining_flex = total_flex; - for child in &mut self.children { - if let Some(flex) = Self::child_flex(&child) { - let child_max = if remaining_flex == 0.0 { - remaining_space - } else { - let space_per_flex = remaining_space / remaining_flex; - space_per_flex * flex - }; - let child_constraint = match self.axis { - Axis::Horizontal => SizeConstraint::new( - vec2f(0.0, constraint.min.y()), - vec2f(child_max, constraint.max.y()), - ), - Axis::Vertical => SizeConstraint::new( - vec2f(constraint.min.x(), 0.0), - vec2f(constraint.max.x(), child_max), - ), - }; - let child_size = child.layout(child_constraint, cx); - remaining_space -= child_size.along(self.axis); - remaining_flex -= flex; - cross_axis_max = cross_axis_max.max(child_size.along(cross_axis)); - } - } + self.layout_flex_children( + false, + constraint, + &mut remaining_space, + &mut remaining_flex, + &mut cross_axis_max, + cx, + ); + self.layout_flex_children( + true, + constraint, + &mut remaining_space, + &mut remaining_flex, + &mut cross_axis_max, + cx, + ); match self.axis { Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max), @@ -181,6 +211,7 @@ impl Element for Flex { struct FlexParentData { flex: f32, + expanded: bool, } pub struct Expanded { @@ -191,7 +222,10 @@ pub struct Expanded { impl Expanded { pub fn new(flex: f32, child: ElementBox) -> Self { Expanded { - metadata: FlexParentData { flex }, + metadata: FlexParentData { + flex, + expanded: true, + }, child, } } @@ -249,3 +283,73 @@ impl Element for Expanded { }) } } + +pub struct Flexible { + metadata: FlexParentData, + child: ElementBox, +} + +impl Flexible { + pub fn new(flex: f32, child: ElementBox) -> Self { + Flexible { + metadata: FlexParentData { + flex, + expanded: false, + }, + child, + } + } +} + +impl Element for Flexible { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let size = self.child.layout(constraint, cx); + (size, ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _: &mut Self::LayoutState, + cx: &mut PaintContext, + ) -> Self::PaintState { + self.child.paint(bounds.origin(), visible_bounds, cx) + } + + fn dispatch_event( + &mut self, + event: &Event, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + cx: &mut EventContext, + ) -> bool { + self.child.dispatch_event(event, cx) + } + + fn metadata(&self) -> Option<&dyn Any> { + Some(&self.metadata) + } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &DebugContext, + ) -> Value { + json!({ + "type": "Flexible", + "flex": self.metadata.flex, + "child": self.child.debug(cx) + }) + } +} diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index e45c5d7d1bf2059a8b473a789a72b6367871479a..f5c9e0e58e6656a6121a7f83164376af7d0dcc14 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -964,13 +964,13 @@ impl View for Workspace { if let Some(element) = self.left_sidebar.render_active_item(&settings, cx) { - content.add_child(element); + content.add_child(Flexible::new(0.8, element).boxed()); } content.add_child(Expanded::new(1.0, self.center.render()).boxed()); if let Some(element) = self.right_sidebar.render_active_item(&settings, cx) { - content.add_child(element); + content.add_child(Flexible::new(0.8, element).boxed()); } content.add_child(self.right_sidebar.render(&settings, cx)); content.boxed() diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 25afac91b0518bcf2a7cb1e837fea3931a995d05..3894ad4cbdac6aba7b336ed2437d80bbf76d052c 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -191,7 +191,7 @@ impl Pane { let border = &theme.workspace.tab.container.border; row.add_child( - Expanded::new( + Flexible::new( 1.0, MouseEventHandler::new::(item.id(), cx, |mouse_state, cx| { let title = item.title(cx); diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index e415551a8f12a9787d3dfc8a38038708cf7759a2..7dbc30dfbfaa6c255f0497a810201aeebdc1d717 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -113,9 +113,13 @@ impl Sidebar { container.add_child(self.render_resize_handle(settings, cx)); } container.add_child( - ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) - .with_width(*self.width.borrow()) - .boxed(), + Flexible::new( + 1., + ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) + .with_max_width(*self.width.borrow()) + .boxed(), + ) + .boxed(), ); if matches!(self.side, Side::Left) { container.add_child(self.render_resize_handle(settings, cx)); From 776f7dd5a9c99e18363984fb3e2c4cc26bb06131 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 12:18:31 +0200 Subject: [PATCH 171/204] Add a new `Hooks` element to invoke a callback before layout This is useful to cap the width of sidebars when dragging the resize handles beyond the maximum bounds of the sidebar. --- gpui/src/elements.rs | 2 + gpui/src/elements/hooks.rs | 79 ++++++++++++++++++++++++++++++++++++ zed/src/workspace/sidebar.rs | 15 +++++-- 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 gpui/src/elements/hooks.rs diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index abfcadc0421f239f6282621d87b08d3dbd192314..7acd5cadad127e7014ee93ec5c49a2cbe11b22e0 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -5,6 +5,7 @@ mod container; mod empty; mod event_handler; mod flex; +mod hooks; mod label; mod line_box; mod list; @@ -23,6 +24,7 @@ pub use container::*; pub use empty::*; pub use event_handler::*; pub use flex::*; +pub use hooks::*; pub use label::*; pub use line_box::*; pub use list::*; diff --git a/gpui/src/elements/hooks.rs b/gpui/src/elements/hooks.rs new file mode 100644 index 0000000000000000000000000000000000000000..6d1e86a2173ab201e921b131828d29e4b73b7a44 --- /dev/null +++ b/gpui/src/elements/hooks.rs @@ -0,0 +1,79 @@ +use crate::{ + geometry::{rect::RectF, vector::Vector2F}, + json::json, + DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, + SizeConstraint, +}; + +pub struct Hooks { + child: ElementBox, + before_layout: Option>, +} + +impl Hooks { + pub fn new(child: ElementBox) -> Self { + Self { + child, + before_layout: None, + } + } + + pub fn on_before_layout( + mut self, + f: impl 'static + FnMut(SizeConstraint, &mut LayoutContext), + ) -> Self { + self.before_layout = Some(Box::new(f)); + self + } +} + +impl Element for Hooks { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + if let Some(handler) = self.before_layout.as_mut() { + handler(constraint, cx); + } + let size = self.child.layout(constraint, cx); + (size, ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _: &mut Self::LayoutState, + cx: &mut PaintContext, + ) { + self.child.paint(bounds.origin(), visible_bounds, cx); + } + + fn dispatch_event( + &mut self, + event: &Event, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + cx: &mut EventContext, + ) -> bool { + self.child.dispatch_event(event, cx) + } + + fn debug( + &self, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + cx: &DebugContext, + ) -> serde_json::Value { + json!({ + "type": "Hooks", + "child": self.child.debug(cx), + }) + } +} diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 7dbc30dfbfaa6c255f0497a810201aeebdc1d717..dca3687240ad2b6e78ab3eaa5d4ee0e3cfcfb1ea 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -112,12 +112,21 @@ impl Sidebar { if matches!(self.side, Side::Right) { container.add_child(self.render_resize_handle(settings, cx)); } + + let width = self.width.clone(); container.add_child( Flexible::new( 1., - ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) - .with_max_width(*self.width.borrow()) - .boxed(), + Hooks::new( + ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) + .with_max_width(*self.width.borrow()) + .boxed(), + ) + .on_before_layout(move |constraint, _| { + let mut width = width.borrow_mut(); + *width = width.min(constraint.max.x()); + }) + .boxed(), ) .boxed(), ); From 4c7eaaebb11e3effa2920ccbae2d2ae422608781 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 13:01:12 +0200 Subject: [PATCH 172/204] Use `Flexible` in file finder and theme switcher instead of `Expanded` --- gpui/src/elements/uniform_list.rs | 2 ++ gpui/src/scene.rs | 4 +++- zed/src/file_finder.rs | 4 ++-- zed/src/theme_selector.rs | 7 ++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index 04435bb36eb28e2b06d7d52880e193ade25a3b5a..c82d8aa3d6fc740c3179f50b367348b32816cc76 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -150,6 +150,8 @@ where for item in &mut items { item.layout(item_constraint, cx); } + } else { + size = constraint.min; } ( diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 012c43647ef9de5c2b2630644cf82f8844bfcfab..0bd0fe8a77fe5899421981419eedd088c38ab54a 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -210,7 +210,9 @@ impl StackingContext { clip_bounds .intersection(parent_clip_bounds.unwrap_or(clip_bounds)) .unwrap_or_else(|| { - log::warn!("specified clip bounds are disjoint from parent layer"); + if !clip_bounds.is_empty() { + log::warn!("specified clip bounds are disjoint from parent layer"); + } RectF::default() }) }) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index cd74709421832c6d47ae5cfd7579ffad0f500bdb..b7394cd553437e7ea5c0cb858c30202c3d7044cb 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -87,7 +87,7 @@ impl View for FileFinder { Container::new( Flex::new(Axis::Vertical) .with_child(ChildView::new(self.query_editor.id()).boxed()) - .with_child(Expanded::new(1.0, self.render_matches()).boxed()) + .with_child(Flexible::new(1.0, self.render_matches()).boxed()) .boxed(), ) .with_style(&settings.theme.selector.container) @@ -173,7 +173,7 @@ impl FileFinder { .boxed(), ) .with_child( - Expanded::new( + Flexible::new( 1.0, Flex::column() .with_child( diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 714e84b03fbb1d7b7113f25dc10300ca584eec8d..28fea8f6c00b2f96d70e9b0cc8811864487a5b5c 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -9,10 +9,7 @@ use crate::{ }; use gpui::{ action, - elements::{ - Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement, - UniformList, UniformListState, - }, + elements::*, keymap::{self, menu, Binding}, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, @@ -283,7 +280,7 @@ impl View for ThemeSelector { Container::new( Flex::new(Axis::Vertical) .with_child(ChildView::new(self.query_editor.id()).boxed()) - .with_child(Expanded::new(1.0, self.render_matches(cx)).boxed()) + .with_child(Flexible::new(1.0, self.render_matches(cx)).boxed()) .boxed(), ) .with_style(&settings.theme.selector.container) From 232020591152d08194c6a3481b6c27ec436595a7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 13:23:28 +0200 Subject: [PATCH 173/204] Update theme selector matches when search query changes --- zed/src/theme_selector.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 28fea8f6c00b2f96d70e9b0cc8811864487a5b5c..7dc54951a8bd34a384d866c20f9e0e71b452245d 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -175,6 +175,7 @@ impl ThemeSelector { background, )) }; + cx.notify(); } fn on_event( From bd13584807894f1381ea35a146ebd412db53f100 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 14:40:31 +0200 Subject: [PATCH 174/204] Clone `Editor::build_style` on split --- zed/src/editor.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index b8c9601fe26a371e63f4d2247fd9a02382d3ca86..71a9174f2ee1fa5b43dd1bfeeadd9aca887426ee 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -27,6 +27,7 @@ use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; use std::{ + cell::RefCell, cmp::{self, Ordering}, collections::BTreeMap, fmt::Write, @@ -34,6 +35,7 @@ use std::{ mem, ops::{Range, RangeInclusive}, path::Path, + rc::Rc, sync::Arc, time::Duration, }; @@ -288,7 +290,7 @@ pub struct Editor { scroll_position: Vector2F, scroll_top_anchor: Anchor, autoscroll_requested: bool, - build_style: Option EditorStyle>>, + build_style: Option EditorStyle>>>, settings: watch::Receiver, focused: bool, cursors_visible: bool, @@ -389,7 +391,7 @@ impl Editor { mut self, f: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle, ) -> Self { - self.build_style = Some(Box::new(f)); + self.build_style = Some(Rc::new(RefCell::new(f))); self } @@ -2582,8 +2584,8 @@ impl View for Editor { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let style = self .build_style - .as_mut() - .map_or(Default::default(), |build| build(cx)); + .as_ref() + .map_or(Default::default(), |build| (build.borrow_mut())(cx)); EditorElement::new(self.handle.clone(), style).boxed() } @@ -2665,6 +2667,7 @@ impl workspace::ItemView for Editor { let mut clone = Editor::for_buffer(self.buffer.clone(), self.settings.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) } From 1b8ea083773fdc95b63603b88bb5870d565484a1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 14:54:24 +0200 Subject: [PATCH 175/204] Exclude selections from editor splits in Editor::active_selection_sets --- zed/src/editor.rs | 9 ++++++--- zed/src/editor/element.rs | 3 +-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 71a9174f2ee1fa5b43dd1bfeeadd9aca887426ee..38e9a3ac3e3606d8bf870785c35ea1e153c4cafc 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -1955,10 +1955,13 @@ impl Editor { &'a self, cx: &'a AppContext, ) -> impl 'a + Iterator { - self.buffer - .read(cx) + let buffer = self.buffer.read(cx); + let replica_id = buffer.replica_id(); + buffer .selection_sets() - .filter(|(_, set)| set.active) + .filter(move |(set_id, set)| { + set.active && (set_id.replica_id != replica_id || **set_id == self.selection_set_id) + }) .map(|(set_id, _)| *set_id) } diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index b0a8d97e5d7c104a085b107cc9778ed08473e1e7..a14356819d840d22e64ea47a1676d2e02530b11e 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -450,7 +450,6 @@ impl Element for EditorElement { let mut selections = HashMap::new(); let mut active_rows = BTreeMap::new(); self.update_view(cx.app, |view, cx| { - let replica_id = view.replica_id(cx); for selection_set_id in view.active_selection_sets(cx).collect::>() { let mut set = Vec::new(); for selection in view.selections_in_range( @@ -459,7 +458,7 @@ impl Element for EditorElement { cx, ) { set.push(selection.clone()); - if selection_set_id.replica_id == replica_id { + if selection_set_id == view.selection_set_id { let is_empty = selection.start == selection.end; let mut selection_start; let mut selection_end; From 0ae70b62cbc3e827d4edc3c9b58e4d647e5052e7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 17:19:57 +0200 Subject: [PATCH 176/204] Use `on_after_layout` to cap the rendered sidebar item's width --- gpui/src/elements/hooks.rs | 16 ++++++++-------- zed/src/workspace/sidebar.rs | 9 +++++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/gpui/src/elements/hooks.rs b/gpui/src/elements/hooks.rs index 6d1e86a2173ab201e921b131828d29e4b73b7a44..9337578297ca038321189e39aae1b0d1c5cf72d2 100644 --- a/gpui/src/elements/hooks.rs +++ b/gpui/src/elements/hooks.rs @@ -7,22 +7,22 @@ use crate::{ pub struct Hooks { child: ElementBox, - before_layout: Option>, + after_layout: Option>, } impl Hooks { pub fn new(child: ElementBox) -> Self { Self { child, - before_layout: None, + after_layout: None, } } - pub fn on_before_layout( + pub fn on_after_layout( mut self, - f: impl 'static + FnMut(SizeConstraint, &mut LayoutContext), + f: impl 'static + FnMut(Vector2F, &mut LayoutContext), ) -> Self { - self.before_layout = Some(Box::new(f)); + self.after_layout = Some(Box::new(f)); self } } @@ -36,10 +36,10 @@ impl Element for Hooks { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - if let Some(handler) = self.before_layout.as_mut() { - handler(constraint, cx); - } let size = self.child.layout(constraint, cx); + if let Some(handler) = self.after_layout.as_mut() { + handler(size, cx); + } (size, ()) } diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index dca3687240ad2b6e78ab3eaa5d4ee0e3cfcfb1ea..afe4b328f2cbd6d6b71b641bf0a81da34d6f7801 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -113,7 +113,6 @@ impl Sidebar { container.add_child(self.render_resize_handle(settings, cx)); } - let width = self.width.clone(); container.add_child( Flexible::new( 1., @@ -122,9 +121,11 @@ impl Sidebar { .with_max_width(*self.width.borrow()) .boxed(), ) - .on_before_layout(move |constraint, _| { - let mut width = width.borrow_mut(); - *width = width.min(constraint.max.x()); + .on_after_layout({ + let width = self.width.clone(); + move |size, _| { + *width.borrow_mut() = size.x(); + } }) .boxed(), ) From 2f0f6e210d5980e5d6d7ed95c04f7ffa5d7fcae3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 17:47:45 +0200 Subject: [PATCH 177/204] Honor max constraint in `Flex` even if children overflow Co-Authored-By: Nathan Sobo --- gpui/src/elements/flex.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/gpui/src/elements/flex.rs b/gpui/src/elements/flex.rs index a71407cc2562eff46cbe12cca48a54e3ab001a5d..73f3a863730a8a07a3542cf44bffbad637a2cc73 100644 --- a/gpui/src/elements/flex.rs +++ b/gpui/src/elements/flex.rs @@ -82,7 +82,7 @@ impl Extend for Flex { } impl Element for Flex { - type LayoutState = (); + type LayoutState = bool; type PaintState = (); fn layout( @@ -153,21 +153,33 @@ impl Element for Flex { if constraint.min.x().is_finite() { size.set_x(size.x().max(constraint.min.x())); } - if constraint.min.y().is_finite() { size.set_y(size.y().max(constraint.min.y())); } - (size, ()) + let mut overflowing = false; + if size.x() > constraint.max.x() { + size.set_x(constraint.max.x()); + overflowing = true; + } + if size.y() > constraint.max.y() { + size.set_y(constraint.max.y()); + overflowing = true; + } + + (size, overflowing) } fn paint( &mut self, bounds: RectF, visible_bounds: RectF, - _: &mut Self::LayoutState, + overflowing: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { + if *overflowing { + cx.scene.push_layer(Some(bounds)); + } let mut child_origin = bounds.origin(); for child in &mut self.children { child.paint(child_origin, visible_bounds, cx); @@ -176,6 +188,9 @@ impl Element for Flex { Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), } } + if *overflowing { + cx.scene.pop_layer(); + } } fn dispatch_event( From f0775aeebc3e97ef37d5392b0a660f6f7659af22 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 17:55:39 +0200 Subject: [PATCH 178/204] Rename `Hooks` to `Hook` Co-Authored-By: Nathan Sobo --- gpui/src/elements.rs | 4 ++-- gpui/src/elements/{hooks.rs => hook.rs} | 6 +++--- zed/src/workspace/sidebar.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename gpui/src/elements/{hooks.rs => hook.rs} (96%) diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 7acd5cadad127e7014ee93ec5c49a2cbe11b22e0..6d7429222c7c1bf74d259932cbc5b3b614892ce3 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -5,7 +5,7 @@ mod container; mod empty; mod event_handler; mod flex; -mod hooks; +mod hook; mod label; mod line_box; mod list; @@ -24,7 +24,7 @@ pub use container::*; pub use empty::*; pub use event_handler::*; pub use flex::*; -pub use hooks::*; +pub use hook::*; pub use label::*; pub use line_box::*; pub use list::*; diff --git a/gpui/src/elements/hooks.rs b/gpui/src/elements/hook.rs similarity index 96% rename from gpui/src/elements/hooks.rs rename to gpui/src/elements/hook.rs index 9337578297ca038321189e39aae1b0d1c5cf72d2..994d5fe281ca1dae5984dd8456ffa32ce5be97a1 100644 --- a/gpui/src/elements/hooks.rs +++ b/gpui/src/elements/hook.rs @@ -5,12 +5,12 @@ use crate::{ SizeConstraint, }; -pub struct Hooks { +pub struct Hook { child: ElementBox, after_layout: Option>, } -impl Hooks { +impl Hook { pub fn new(child: ElementBox) -> Self { Self { child, @@ -27,7 +27,7 @@ impl Hooks { } } -impl Element for Hooks { +impl Element for Hook { type LayoutState = (); type PaintState = (); diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index afe4b328f2cbd6d6b71b641bf0a81da34d6f7801..71946b50acf90de63f056083bc90f1d251c7a97e 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -116,7 +116,7 @@ impl Sidebar { container.add_child( Flexible::new( 1., - Hooks::new( + Hook::new( ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) .with_max_width(*self.width.borrow()) .boxed(), From e9d50159ee88cb4106724c2bc45d65945ada6488 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Sep 2021 17:56:07 +0200 Subject: [PATCH 179/204] Add a min width for the `ChatPanel` Co-Authored-By: Nathan Sobo --- zed/src/chat_panel.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 77c5a7777d1ae1a1b4f0570a54d247f90ed680a8..2b5570ca91d44a6c3ede872f43895f28f148110c 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -313,18 +313,22 @@ impl View for ChatPanel { fn render(&mut self, _: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; - Container::new( - Flex::column() - .with_child( - Container::new(ChildView::new(self.channel_select.id()).boxed()) - .with_style(&theme.chat_panel.channel_select.container) - .boxed(), - ) - .with_child(self.render_active_channel_messages()) - .with_child(self.render_input_box()) - .boxed(), + ConstrainedBox::new( + Container::new( + Flex::column() + .with_child( + Container::new(ChildView::new(self.channel_select.id()).boxed()) + .with_style(&theme.chat_panel.channel_select.container) + .boxed(), + ) + .with_child(self.render_active_channel_messages()) + .with_child(self.render_input_box()) + .boxed(), + ) + .with_style(&theme.chat_panel.container) + .boxed(), ) - .with_style(&theme.chat_panel.container) + .with_min_width(150.) .boxed() } From 0e4f77750a174c6a6f381924fd3af28d16683931 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 3 Sep 2021 16:54:58 -0700 Subject: [PATCH 180/204] Fix cargo feature name in seed-db script --- script/seed-db | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/seed-db b/script/seed-db index 8ccac320a5017d578ac2f58ba8e41c87a7c007c3..195dc5fb8d49c40bb6abf8306def6211111ac56f 100755 --- a/script/seed-db +++ b/script/seed-db @@ -2,4 +2,4 @@ set -e cd server -cargo run --features seed-dependencies --bin seed +cargo run --features seed-support --bin seed From 00f6bdcb24e96cd4e8a5ac97d859e47c001c3fe8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 4 Sep 2021 17:02:20 +0200 Subject: [PATCH 181/204] Bundle and use Inconsolata v2.012 There's a newer version of the font available but ligatures seem broken googlefonts/Inconsolata#58 and googlefonts/Inconsolata#52. As part of this commit I also upgraded rust-embed to use the new exclusion feature, which allows us to skip embedding OS files like `.DS_Store`. --- Cargo.lock | 14 ++++---- gpui/src/app.rs | 4 +++ gpui/src/platform.rs | 1 + gpui/src/platform/mac/fonts.rs | 30 +++++++++++++++--- server/Cargo.toml | 2 +- server/src/assets.rs | 2 +- server/src/main.rs | 4 +-- zed/Cargo.toml | 2 +- .../fonts/inconsolata/Inconsolata-Bold.ttf | Bin 0 -> 120140 bytes .../fonts/inconsolata/Inconsolata-Regular.ttf | Bin 0 -> 110488 bytes zed/assets/themes/dark.toml | 2 +- zed/assets/themes/light.toml | 2 +- zed/src/assets.rs | 5 ++- zed/src/language.rs | 5 +-- zed/src/main.rs | 14 ++++++-- zed/src/settings.rs | 4 +-- 16 files changed, 65 insertions(+), 26 deletions(-) create mode 100644 zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf create mode 100644 zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf diff --git a/Cargo.lock b/Cargo.lock index 89169c0efd64af3fea51c014a8e096f5c4c492c5..562da86feec01872e2eb71718d775b41f454dde7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4040,9 +4040,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "5.9.0" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523" +checksum = "1be44a6694859b7cfc955699935944a6844aa9fe416aeda5d40829e3e38dfee6" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -4051,9 +4051,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "5.9.0" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66" +checksum = "f567ca01565c50c67b29e535f5f67b8ea8aeadaeed16a88f10792ab57438b957" dependencies = [ "proc-macro2", "quote", @@ -4064,10 +4064,12 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "5.1.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d" +checksum = "6116e7ab9ea963f60f2f20291d8fcf6c7273192cdd7273b3c80729a9605c97b2" dependencies = [ + "glob 0.3.0", + "sha2", "walkdir", ] diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 3610957634b0bc7cf58ea634580ce1963f023e86..c1ce8fdba0d266726a8db83eaba36fe3fe3f4de6 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -300,6 +300,10 @@ impl App { })) } + pub fn platform(&self) -> Arc { + self.0.borrow().platform() + } + pub fn font_cache(&self) -> Arc { self.0.borrow().cx.font_cache.clone() } diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index b58ad381a29f9804ea999f70278bd6db1521dde5..2aad986a667164c8f3342769c5ca463cba288eb3 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -127,6 +127,7 @@ pub enum CursorStyle { } pub trait FontSystem: Send + Sync { + fn add_fonts(&self, fonts: Vec>>) -> anyhow::Result<()>; fn load_family(&self, name: &str) -> anyhow::Result>; fn select_font( &self, diff --git a/gpui/src/platform/mac/fonts.rs b/gpui/src/platform/mac/fonts.rs index ba9e3ae3cf764d15fcb2bb8178ccd256819335ce..8f61bfab30bdf4c26055221aa5e9c568a987f942 100644 --- a/gpui/src/platform/mac/fonts.rs +++ b/gpui/src/platform/mac/fonts.rs @@ -21,9 +21,12 @@ use core_graphics::{ base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform, }; use core_text::{line::CTLine, string_attributes::kCTFontAttributeName}; -use font_kit::{canvas::RasterizationOptions, hinting::HintingOptions, source::SystemSource}; +use font_kit::{ + canvas::RasterizationOptions, handle::Handle, hinting::HintingOptions, source::SystemSource, + sources::mem::MemSource, +}; use parking_lot::RwLock; -use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void}; +use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void, sync::Arc}; #[allow(non_upper_case_globals)] const kCGImageAlphaOnly: u32 = 7; @@ -31,20 +34,26 @@ const kCGImageAlphaOnly: u32 = 7; pub struct FontSystem(RwLock); struct FontSystemState { - source: SystemSource, + memory_source: MemSource, + system_source: SystemSource, fonts: Vec, } impl FontSystem { pub fn new() -> Self { Self(RwLock::new(FontSystemState { - source: SystemSource::new(), + memory_source: MemSource::empty(), + system_source: SystemSource::new(), fonts: Vec::new(), })) } } impl platform::FontSystem for FontSystem { + fn add_fonts(&self, fonts: Vec>>) -> anyhow::Result<()> { + self.0.write().add_fonts(fonts) + } + fn load_family(&self, name: &str) -> anyhow::Result> { self.0.write().load_family(name) } @@ -93,9 +102,20 @@ impl platform::FontSystem for FontSystem { } impl FontSystemState { + fn add_fonts(&mut self, fonts: Vec>>) -> anyhow::Result<()> { + self.memory_source + .add_fonts(fonts.into_iter().map(|bytes| Handle::from_memory(bytes, 0)))?; + Ok(()) + } + fn load_family(&mut self, name: &str) -> anyhow::Result> { let mut font_ids = Vec::new(); - for font in self.source.select_family_by_name(name)?.fonts() { + + let family = self + .memory_source + .select_family_by_name(name) + .or_else(|_| self.system_source.select_family_by_name(name))?; + for font in family.fonts() { let font = font.load()?; font_ids.push(FontId(self.fonts.len())); self.fonts.push(font); diff --git a/server/Cargo.toml b/server/Cargo.toml index fec5bec74d30c439652dae44bb38375303131ddc..b73c70102a311ecf1813a5bd85efa315e344dd93 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -29,7 +29,7 @@ oauth2-surf = "0.1.1" parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8" -rust-embed = "5.9.0" +rust-embed = { version = "6.2", features = ["include-exclude"] } scrypt = "0.7" serde = { version = "1.0", features = ["derive"] } sha-1 = "0.9" diff --git a/server/src/assets.rs b/server/src/assets.rs index a53be8ed95f23d9f0666cf6a2b24faecb47fc2e6..4e79af8c600d2b2766b5dc6170aaec38d37ba006 100644 --- a/server/src/assets.rs +++ b/server/src/assets.rs @@ -26,6 +26,6 @@ async fn get_static_asset(request: Request) -> tide::Result { Ok(tide::Response::builder(200) .content_type(content_type) - .body(content.as_ref()) + .body(content.data.as_ref()) .build()) } diff --git a/server/src/main.rs b/server/src/main.rs index 41f2638027c591dd354202b46bf2d53c08decce3..ba54f05f1c4d33fe5947f30f4257d962b3348ba8 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -83,7 +83,7 @@ impl AppState { let partial = Templates::get(path.as_ref()).unwrap(); self.handlebars .write() - .register_partial(partial_name, std::str::from_utf8(partial.as_ref()).unwrap()) + .register_partial(partial_name, std::str::from_utf8(&partial.data).unwrap()) .unwrap() } } @@ -98,7 +98,7 @@ impl AppState { self.register_partials(); self.handlebars.read().render_template( - std::str::from_utf8(Templates::get(path).unwrap().as_ref()).unwrap(), + std::str::from_utf8(&Templates::get(path).unwrap().data).unwrap(), data, ) } diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 17314ebd6542473e0ed7d36c098df251ea7af07d..985901c50cf8a14d4331ed73f89a98fd1323d28d 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -38,7 +38,7 @@ parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" rsa = "0.4" -rust-embed = "5.9.0" +rust-embed = { version = "6.2", features = ["include-exclude"] } seahash = "4.1" serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } diff --git a/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf b/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e8aad4c3cd21d9811f33b08fef26502b36441641 GIT binary patch literal 120140 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 ZZ})}Vj=|aacxWfirgd_jq1*iD{{S;H)C~Xt literal 0 HcmV?d00001 diff --git a/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf b/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..00ffc946a5aaccdbc563b2725b2c921aad1979a0 GIT binary patch literal 110488 zcmdSC34D~*)dzgE00G(eO$0$fM8pjh_q8ra ztHoAPaVc&T*IH|>`ntUq*HT4l(Yn_a7bf5Toclb_OaSZK{@(BXeV^@f=H@)-Zs(qR z@44rk`yiwc!VTaM!BNMI9fQB5kiWSCzlmc;jcz|?)07BM1M!rNnb330Ey?}6giw=( z2&axYW=h-O(s6$lqIjMV>EHDnGq`%=m9Ly2MExw}pSN)3{M8vR?cIUD9}yyT!NPTG zgUMIse=Fp+y?B4wlGRIBjtE?~QOKKC2+?ol()nvv1D}rfd+?jMbj7JlM*cAMLLo*5 zg$!J^Z1Ma>>A(NSb3)F)AMfjz0pU71@gn^F5&kY(wsP(IwxwrXCgg&2A^NKos}{}= zO?iE%kSAse;ks<){Pn9{Po%#rU$wfk%k@=Cy;$!h?wliL7)C0_<Ng2wYKlS$YKEEtI9tsIT&NZT zE>>upI!+xYq_M`>A`Igp`X?W(~AsptKT#kqEt3i7wipQ8sAD%osAv|ULD9jIfq+Dk{Sb^jw zJQv`>>R~_ny&>-fj_RSZt>?=HD_|`eeGy zlOb6~cv&fH`M1gZJ0jcp?~v@KUm>I4lZ5mp5{w??bS^^&LzjF8Z&Mg%5R87$X(z)} zhG`7@F-#{I{f5)IoIWW3j1pXG6<{XAY=)H#oebSfpUi2OY(@z$GiD3Jn+Y$os68@` z(*dl8QU>IgknO*V_PZzW}s13X$;xHbDrc z8yt403F*$kFZK*PhWjdhlzSQI={W(zI$8f;t z+T4|Ps@6_5*{KMnxL&(`?sofax1E|~r>0q{SdZLuES!6homx&Q`k#BX#-Tl2hkG4x z8~F{THrc5QICYu*mQt~|SKD-3DV4DgZ@0U*nW;M|<#t#&_q|psW1oAsu8mV%9;Y7D zrQofu_erE;IIfN9p5=NScpJmL)Hg5Fy=vo3ZXm~A8^>*SQz_hY&avNq`@Ws}#7=!_ zr@pgN9$}?orFfhgXK~k)npNg;6OL0^b}C?}ia1r#hi-`dww_YN-HeYc?&_3hSUk?1 zXK~!3^R#(7bY7&!+o{QRYKEPfXQ!6fsTFqWBucp*@#VSiwBN3`Q)k$zbL~{D1s7=? zdU=JNx|UL&Ul6o$w|Rc$xtsdyx!+DP&hrp(vA3Ke3DY=|u-IEuvhmiM6@5~lTea!g zNF~SUJde{`DmjKT%R?P8oSSD<#y-ze#BtAaoci2O9kfz3GWxAML@99csW@Dj{kGCh z)!L~hI~B1~o|o;^>y)DMVl8mDTW{UnRx0*3gWLDhIo!*wyzWUyNlmkGp117OyH+Z$ zwxjSRRGns^(}K0AY?q2a#s@0{iKD{GRZiWL{4$*b>7TQ z;zs6#jD1LbZl?}fDcXDVTjGY9icre!h|f#Ai}|*jQkhfTyEA7YwSZI0ICVlF+*~nZXcy|`zZB@ z{q_k?J#Ep^j>9}4uDy`Chu(7PO*;koFmVScg>ic!^DrFeU=DGL`^G8AftfdI=e2QO zNR)o-O}10%cFJd`AVX%ocKf^``z_v?rBvFfS}PUnkr(o3(lMW@eO^eN{-2k5N~2}v z@I2+^HOC7cdm$Tulv-q`nB(5n_FGED-mbIhHd0EL?A_$OAeN%FJ#!D?XxA`P>%5nF zucjPsN^P}M+w9bKPF;q#G2ET@Ta<)$?8e&|?lJrAlXmJ^JN1&Cdeu(tr4*MEYYXco z+^Y9&I@U`V_oa>d&Pw@&m5TMq=Ommj)#vtQaVmhnijI=1uyI4|RK1-VW~bUHMLhM5 z_jSalNH2-QO{TZRaSP|0;hU%P`es;T-Y11Jsl0fc*ObY4oNtLmr^|CYj$X1CrRnm# zrkwg_cuhHt&*57^biR}DthZBV*r{`MDR_GkrD%QDDdxcy_S;|huJ!$jaktqi#&Hf# zF*kGyQsc98Xl`if*tvA_DrQ0c=oyxOPzR&E`*OcP&>=t-K)?06xl{&nA zKb^zdZsqm;V5Pj3cBPPR$g{ z7@k1TO8FT_a_ZmUU+X{XXerW0^jnhMIJ(W0gXC1>Xcg5d|0VvbNQ(ViIQ6ETqIxtA z?b9im(`G7P^v~QVo?N-X4Mk(TzwQ}`Ip}%@1)T2OjUi3}ltew-!;V15Z0+3P% z?9@kg>T^5Aqw0^w=g0#8b>6IGJC$yy%oWqBjoWOuPp@^ce#F+CtUUjYEaLqxUTJvs zAuAVIgMQR1M%GW*wAecJ{{h>WwslEZn{12Mck1tyHaxJ0Wf_wx-3yDqc+Yqk@M~Dt zrTEG?25+xYZ_-;M!0;x9eumcp9x!qN52{0e?-{^LVT|Ut@FgHu5<{Z-&VZi)Fc~!c zsP;YXoVLZOSEw0`6ry++LzVYiHo?6R_pO=108%hH|A*I|0#7 zK(r7gP}{!ek~67oz@u$|XdB>}{1zS-q^Y!I;*xYzUDD0`adX}s+@9p<7r>`etMHb4 z*EI!d&{8Tk^571~q=n{rZY|^(nPSe9EPLYQRn@JGu7# zocANDQ5?@4c$&+9ZKW1@J3_ zyg}OD`3?6vf$L4+u`mqs03~qW4f4E*Zf z&_V4-X=uOntM5VomO-s`D70UE#@yQ_e~0v5YJ>Q!7aj<)pVRxf4QrTh`;9WfbNTzJ zG_i?#A=atuQCgbe0Gy%6f?JZNzQWt@s07T`HoWa%dOyXR5%%3D~m4jt6iOY0o&hRRcME$~8kYp~E_oK}3)i+!+pbROjc#sp~ z!ye{CKO>W940wYfCqBGo4+eZDBvEx-V~A@^;~LYL+acy^8rPUbJ%zN;cnS3xV1F8M z(aXI0l-ed<;Tio;)rGh9+`G?tHoeC(a{}8tA zSw25vcz|m^iRlk9eLA=NGUjuXTXKkbbrR=&jb+2&yoY!+53xMFz$NVAx{wqyA(VhM z3Oc5^9gjHP;##+(ULoGZvk%V!^Z5wBpW`7971j}w5njoGvwU%kxBPJ-;L&odR|391 z0bgJ2p?CDqPC#pt`N#;;R6adaKh;BYl((IK3&g!*w|GoEDV`NC!AG=L?1#Vp6Y(W> zFd>~X6+ZSX_@#i5@hnkxrm>8}MyFNvm{k zHm&Ntm9*dW-bibEuaogk+ADiqv`YeFo}zrzD8Awq`0p&YdUcBOuI*4!!~i&XhNg-f zc$|fR&kQLc;Nucm@C6^mN7x(ZFH|ddA4?Tlp6*A^hwwaZK2PEIIXo}pc^%JNc;3ZB zxDW8qDoOaS@xX&ek5qWz#iM;mqBYiIz32Rt$9yEUmddAx>L)yvLwSSzn<_5B|E>~S z5V3Ky*dca`UE&e(gm{|xL^JaL)bpBV1gS>aKYQJ@e?l&>3jt#P!MI`V?@goq2Wjvh zV|x=|vN^)nVO$KvcAPH+VmP)4FGp<9evyPIk*VrfM2XBsbO<6t5EY^j(Lr$`f~nwR z6%@mP*~>96*&Oo{=9rfvj(Hi*F)uR_@v=nB;+U7&9P_e(V_p_=%*z!V^KvD}yxhVu zFJE%ZOE1N|U|u5TMdnbHfXqE(yUFpwJE7{z;98e_vD8*J|tH?3#Fzyh! z#!h3W$TRLW?iKmQgT{j-<>2Xh#K(}fM{zR5Wyz&NfWWO}`E(LI(s8jcIzECTQV^kn zJuwrdlwqwNCB}RMVu`z7S}LMI@+0vn2n&QKt01myO%}I>{Iw<(#n$*mAx64R3>Ra>L@`q=7OTXm;v8|QxXz@axSK2y!k$x)9j8M~f($Id z$ZtU0&SjWA)T(s&?fZ*t5f+0)gJ>0F#WB!*mx>d`Y2rL_xwzh>qIjJgL|9c~_ljWe znk?o(o=y@Q#b$AZ*lJQyEKjZ|7K25j7$LgE6fsvEC)SA5#rfh&@pF@k;(YS3Uk$;| z*e1HgR54F17i+~C;sVU-Uzk)B1C)RSTP?l3nz;+#f9Q$;s$Z!{P`=^ z8oL-i#_%bIFED(S;adz3F#Lq!*9@Zz=dW4pa579~=w_J3Fu<^gVFkk>4C@&VTexuL zYDXKxPKJ{i&SJQT;R=Rp8E#~FF2hR}ov>=9<7$Rm8E#{^o#CAf?`61~;bRP+Wccio zljbjUyu|QLhVL@`gyBJg35sDVLm$H+!-^HhEuEiG%dm-Igkd|wZibT>PGdNS;Ub31 z5w*7>VKu{b3^y{|#P9-!modDW;novQUU^c&Hip|7-pTM@hPxR)#_&mo&oX?8;j61o zT699fUWV^7{D|R~41XZ#bTCY1n8`4YVbL1MgL5FmYKB7@wlEyUu#@2ghEo~NVz^+< znjuxrWeiVXxR&7thG#L{%wMuwXhUcm6O4U11&<+_^TR)*UcZfAHW!+RO-X80JxCmBAA zwH5jRufhLYzY^;umH3}P?5C_HQG_MM=TYl4=0E+F6xWE>{3p=B>gmR+hb{8|wUorZ zLGAm`U@GTjJ03-Rq8w;;fG&nkhDi+BUc`H;{Ewi*4(vrq|69KjdmOd&KZB%`O4g(Q zeaLZv*cGXSe;1NwCt1Jy_aUNhk@r7?(AAOmKZ9AE_uq#B=qT;bwn#@IedjD_16M#x z*(Pp-hQAwUkf(8ad>!Z84{&n)0TG<3G7~YIMbJ}g(OR_FxRYnIbfoba+#x=JI-V)@ ziCp1yIgIDHl%x2)lH27&I;VWn{C(E^eaZZV4o@Y^=MH<1vj*dzGJmc7ubY3rYX0sq ze_u9hd`a#%Qy<75bZeAD)2MXw?-uj#ZRYQ7=I@>6FXHE^Znazg8qb-%&cgw}h;jKQ zCRr#)$lY1{m5IRE;d#m zeOp}mGSK})`Bj$6RymN_Yq3`-wMA6RzsS8jVtwP0RT2y`U&#;TKZHYm1X&RB zYotDgOi1|+QlCIJlst&kr;rgtev8y+kQImg4yk`aW)kH0NPQ03ankITUqFUj@(@yA zLY5L`6n)QCe!x7Hg|Yq$+{?ya0q{ntR8dY`!84qB^3_o|QvL7qfJeU@V@$S#Q=nYX z?U4lj`yAsu{C&Q038YyGm;L^9oXzQWNwRS>=2D7rJ66^-<2N{GyN$;%yE7>_<``@~ z@-6w6cn^OWXcjOt)jq9Ty? zb?OxSoq$q5M4n!igue_`pf+MABDWf(Fi&ybci`NMR*4T0>A!^9Mom#l8Oos&lvBA> zB61ZHy&8hnAwRgb7AxO6;}rCD1A4m=Yu`EG+%3j-aBc^z6n7Huz`5TUkAr*Ah%g`D zmiy#8a=-kWd{_Qm9+2w&TPgJtJf^MOxi_|pmtxNyP zIhc=b{4Ekld&7T0T)z?`i&j_uZ{+kzqp<^v-b2Pr_$Y+&k$h^NkUyiB@)>xsC=1ye z%}3GL(|>uiPNKd{jKx9xog}_t8hS^xKmDJ95geq-)pT`%F#%j}Qq35F)v5^NTA_v< zjgKm<2CGwe)CU+n;A8<>R|*TqXlNMYUO|RJdgWkP!y0%@dT5Sc4xXhTOLdHaSlQG#i#+U->x*b;Fd~uyRLv2u}s`YBQ z+N{o3=c;qS?X!4w#U?H{se!6govzMPv(#y7lNzZ;sbVz=x^*6GJ2%J+)NnOSb*Qnb zQ+25|>IwN@;w`MVGu3F-F5i%E8Z#m7-D-@w5IcS~WTz2!sbgT-ITkjjjj-7W*kc}p zY&4dw&`!BDUy zI3!pTYzmGFt`43WJS});@WS9t!JC7(25%4E6}&h2K=7g9qlKPAUty@QsIa84vhe!C z1BD-iRLB`h4D}DWL*9@-loJYs28237^FoV5*MwC#D_j~L5WYD4LQ%N*tm3PSe_8ye z;{Pgsv81_VUdh6eC8gfdtO2e8*HpT{SG|W|eWU;SK{khoZMbpNY4T&6{EgfVIXZy7 z=sS|}KKyu4J*J*guNwlE{{BV=_z^Zrm>+fEM`9o~kQvAe1Or8Zia=$cKF}0s3v>r& z1m*-*1l9-63S1VriutiE=mJ07!K`3jFv$F<4mL7B)-pfN0zZBo+zx)+5xfWdcrf@# zAAVc`e!K^MBrrcRm>=0TKNgw%D7N__iq9^-rueDiXTgs~=EvevPpN-E0`o&MKVHKK7sE6Dv3268Hs`rvI~O9b5w89G@kS=SVnt z52BR*&&hQ=ole^A|9`y?s7Y9bv5Ko{YPm7Zm;}Ax82J~h#;2<`tmt=Pjo(0L594OF z*_fybja#u(@5Nf}Rhi1C24cOY`*nG|o8)6Z8HOFD8f)=Pb1g2z{&FF&$JdMV)JgCG zJjtu_6Y#eFQr?7J=OgijG_dm^QUE(o5%wP1dD^k}(9V;Ny=N_UpX0FetX6B}ChR_U z(EfvU`6BEIS)vzbiH}7#*6~7_B#LD}Q6kes2>#%5nIS5qTeQkjF^>U&3nLIy?$Ha5;LGhA&RJ;Zo zz+dIF@M#{v-OKmoE8;!5M_eq|iKpa!Vw9|eS7fD}COTxbxK^Gma^cww$ON%Po`W;j zx3H*1#ZZac)3QWNl_SJ#*&)uAtHf3Eba4x`u3Mok{a)TJo`=n0r@R4Y)$L-lJW>1t zx5m$xCyBqwmy8>YpBr0^>y2&3FR_pWui)Ei*8vjCdy{fBOApuIZ~{U$BGkhd;LT? zORPhT)+z8%ohlcJt@1qabGcbuBhM07%QMAx++n{>UM=pCH;Q}ZFU1{li?|DSY44NU z#KZD7@rb-#{8>IOUX+iBH{?^|Rrv?;y8NSf8`hG4$XB8Ne~&ZsA?$Ep;uQ8Z>;&J4 z48#efiLZnU{{JNSi<8AaVM+WP=ig67vkZ&jvRI6i14WI@6?HOS)XRWqkOiVv=80Ky zjF=%uiwoo$agl^4NG=y=%N1giTq(|xCy2A;@!}GBinvUkDlV5B#1-;1aj9G{{vdaX zKjO~tpAhBpjNB!jg*WoQKHXaO;#{`t>RNT3+F~qF zXF}&X7w3c((7>Rj8B2`C#zG?%D|)v2!f>mP43GNQ=&$}^q^l2%6!oUsqyBCrtJl@b z>Rltts4)hq&kaAcodN1IBhP3x2C7$#Vd^hNwfY)+e~VF}{%ka>H;f_bD{1 z_1{LJ`dQf^}A==9rZj`B4jV2W}iqtnozS;}zA*>FmFAYQO zGaTw2=oinbx7ADPkLoS;qIwdVR;hZ%s545`(?*-osGc`Q7(+=bQ!l7L7_~;R`jZhc z8q|N`7};w`Lm4h;CkX~(hM=J&s`u0<>O-~PNHe_Zef3YZUEQdDskW(`)UVX9)y?V_ zb+@`p-Kp+SJJjv!HuW3zF!a7h)vf9YXn!|A3;YG^qQ_$&P>>@b-&GqRL&>5jQkdjW zMu!7y8w?;~G0y66T6ZTVCMG8)dot1!T>b&YVOQ8w;|Z6Ry5#Iqi4SYMbQ<@htP+X$7;@*T zoizg}fA2h$Yvm6c=StH04ln27!^&|PyLul-Yy+2cGfIl9^S4LPo(W6DH=6dFzO+{# zLAyo%{YPl6z8l6>u|BFC<7~T(tK=8pu?rECcfnuFZCh^F$NjX*i1jnBEb!r`xU#@U zl(nn(1$foDtc&cjXnc)d#nD1L0PUHuO6}LQ`}@*f7e}kh-6B6gd@q;#U0>P>CT*#4 zNAFIoaW0(jCW!4K{T?NEZU-%m9{tAg+-v56osovm8du1mmKS`5gT~dC7mE+d*!6h4 z94*oG+!q6NCR%mI<*l%3&Xynj2#qOEDCaH|e?&RG`8+?Ywq2ksFFx@#*Y3;izs0xh z4<HS#Ti55EK&O5;kyKng>onc%{@__k>hzVH4R^!(rhDVx!!9rv#PKKJ` zbR;8S;acAiN(oWE69MQg_rEPxpuq4As zNb=Ve^vta68B#pTTU#)BSn}x0j$d4CS)2{E2aJEAPCHDt*<0MjW;m_z0Fd7aMEa6@$+I26vPzjEA+!r{)d zEsQP!^B{k%t^EPIcl_Pok_ltl}@0kmhTi+GO$t-VL-w$mPE(du%yz;;c0 z6lm=|O4HIFMYOa>J!$Syi+GQc<{pJS_8z74&>ls3ut$0DbCj{+IYoHl17tzd$7DyB zL$asK*|jIW9NN2p=RW#*&oSE)R~PMFn&xbTchJ6cB#kK(DCaJm5ROiBkJ*M&O#@oG zrLh%zbX@bUtikbmv@br*E)Sr)wH2IW!2yAoVB#QC9f3wqD*iN8&iEub=`~x^+;>$ zBeeF!rYBpl89t5NX8DWH@AD9=Sg{p%}vnq8AN&n@X7L#AY5yplvEFLK&W- ziV_zpp^d8KXd4xEwhjxW*BSn=l})m9MZ?f#IaP@c=}O6}3U$q%IWf@YJMrG>bCSnj zHfQ6NjZK3~UCzXQ{c`+k&pYd4kLT*)Uyh)0Dx!AsdeP3~bTzzZF+9mI;jw;(@MF_k zcv?S9`sl~pUrlcqn-y$ON6=$G`g?piB*Q45WjL#^3}ZZCLzKUVtawCWgkn)0hIA}Z z*i8Lh3E0%JePm>KGrW3@1t*+dFE$SP&A_N+gsUexjW6+Zelb?9ayip7-jd1mbLdf7 z9(@h%z!>8{_+M9Hw<@6hD#@i1k{wEEgKG8i zo(g44a%EIuT@4KgHN*OKK@F21RHFseR%@zH`|?Zqc{GjMAU|(trxxXC*l;QHth#q1 z_d5h13(cJiY3~6qnIF9FYP@wOsKz=(e6r~E*+?Rq6`m#FQ%Ihv{pO^?b}SNRAHGX9 z)Sv^HP2gbR#M9Qwy3@{%KGrP1ZElV_o49^1kL#aAyfg0Ej~}#4<7q4jPiL}!#NkO+ zH2x|R-%9N=Xl*7utUxr8QjBoGst^5gL%A3A5jj@AMtIwK+WPlPe9FEjfrOYPUFE3i}VgrL@T)zRaj%mm*^LjkpYGz_ikt0 z@S`J+xmac$0S7HNe&Nf=@RT{BdHG5k8vH(=pO(@(QyP3whL<;%xe{bzYPhs!(0Z9a zPYvv?s=>0*y23^V7Pcn(5$9)*pEL;N=k8Ba7lUkYuvI~TSsN~mo8WT{zhQ#6zOuv z=CPmX79P6k5=Td}%OyKbie5%-X#@Bz#wJN*N60?`l@arD4iw`o)s3~x!0E)J*D&ae zZCJhJ!05$hVc(L!Ly5ND<0RdIOoSbK(XNza>?*ReYiGfPX^}!448`mOUL);-yUOUQeILngd!s|J&_dHnoO*z*y|u5Bp#N0)MWUa z;kHRhJH9x#d9c&vOd6P5HDuGDjJdSldYy+p8-CsB#ssf3HFNA_`C0R!S68o~abg~@ zj4Yx4823DkALxepA^#!LyKQ;L+@5`*}pOWED z&&(~!Yn!r%ca3j{QQIM7G=3rM4=iH}Qe*YW#w%4wLq&M3FY)xI-3s_g@;-Gr*5*u{ zM5acL@%x|$;qXc)q$E1%lWvgRC7e#zY~ga{&>%YIV92t%uyQBPO_qs?p~P-4I_@p= zmKKFWg@L?Gm;w5wqR`}`6qk2^u>0!_7sriZK-p8gCIg3Yxjc~X^A;3%qkou`wCx{r zY6m+KGkjV8!9y>3W#J@M7V!E41zvApj2_gxBP|YJMsiA|O>S(Cj$1ItoPTXNGsXAS zIx}VBEcO_9lSwDf^!_QykXMn2*pg0kvF`-P%cKd=;HqoD36Sgr84dLfUKgF8#@5wM zM9|1~uj6+#CBDcQ?v^b|`4Xew*C(pT@E3>E{3-`{4mw<_D2o&)yA(_lsD`3)40Q&{ zjlYXdyqIim=#yHJtMPcS_hq=kr5WM!hOIZ$wVj`P#t5}4`l75pbcKBN`-9OzG?wKU zODa1A|0s>Uwa-?gPP#kYPXr>lDGCw+7c^Ie#h0t{y3=`StC5y|BB7p)lFn|Y(GdN3 zB4e^W-ETFi$={I{rQi5+-`cCbc7kc#L@2JiMF-o;T^ypLlw^ zKfPmkeaRBjBI$m22F%dOSfr9LVg}Zkfmmn!SS<~M=NjbEl-7{IMC5=KDHg^N(3;W@ zRc48-jLhN^T2>`3C@=tpN*e--bXo8t1Kg2Mxh9SoTo!hwrF#a2RrK4DV3Jy8q$eJ_ zbLO1v7L}Gba-7@~^=Xb-{aD&`C-}FaHmy}k z7>sb8S9YgDu}$^k3qI*QpPZOazPR~B>xpSJs=0ja$;+3Yy!QBxo(W^{E$NPw?blzk zW5+euZy&yS<0%(k45I{(57*1%c^VoX8&8cVU4Za0n|5D%?4_nX8F406NFSs;F?+JcTm6AP*h?+E3)Zh%kpvs;ZduF6U79bN_8hZ}kb!w&()vJafYujB z3qC?-bbYb2zAl4wT-}ackE1zu*`y}|&pbNPPR)7McAMrp`C9KAs9DL~y>C#>b~#r0 zr3TYTEjw|PW}VbVvpn!o(^~6P9QD5_m)ajkd1PPga%tSDPZ@T5ZG*pEKFvKnrdC_@ z9E_{MrX_t@^YFK?$Mev{gNH}6<7@t#a|bkcAuH+8@pc_eLv{__7N!9$S?0+WV$xFc zZCl8XSm!Jr)q#`lVqT~(e$1)Y7{Q5qbzdLjY zKtBu|F%Oe7TwmXiVeN)zCVZ_;qnUyPXP3 zhF28IMbp<`dfEl(4z#moQ-?~1wWlId3i}R1l?}O=&nTeKs)p)LO>w8Ecv3wXML{PY zmDrsG*92Tirk|ks>4gjC%{#B-k8LfH)-%+q1>?ufg=RFdv8JkZZ+u-DIA;uv)WJs~ zGNmH{ra445J7mHVr^K~tSoRm|ius6vuL}Fz*`Dl-qLOqM9aC%AkL4vF5{}4WFB}yO zq@?>Q} z=_<}D3I^zuUsmpdSI1FOSE)?rm(RtXxS9r67)D0uGu9BwA>(UjG>lqYH*Co;H5-wu zr4v=@ije~ck1p<4(K70a<1g9>#Q$N+Ajc<8^*C=i!)J6B zIbbR8H}sf>PXDlBvwD_HtC?On$f=yk{`!(3&Dm|O(?<=OGdj7a6#*leX+v5D<(HP^ z6wPXEpIlx%%#o6o;&a!PupRTxabMhv&bK4*Yhxzq9iL56(W&`#NsBu@-O}N?^KX=AW(yKGyf+^OJr>^^=a> ztILHv5Wg+_W(!Z_5Z8|fUbo7b*N6UG+*{G@1Ky@5{Tq1npYvh8X+U`|sT-gRoU3Pj zMo~D^gTob^{iF`y#FZQAi-RgamJnJZaASy~gm{HWn2T8jg8?1J$ifSkNp2)`6aqFO z5EU8%qUn*Ou%`(Bk!!`$CSc|v8zA}S>l*Yq2M085JaNf2vzPP?tMp}*durQ9O$}UB zQ(xOLYSo0vGcJ&;7fzcuw_s>$YG>=vagC=AZf$Fi7RwioU$nSu4e>+kylb#RkS-=+ zbr4r4bHacAJ@bEdzAZU+X|?D_U{lrcDC$GA3IxWJgs*` zf8=@>=Qp%%^EwZm#dO_PY!CS*sP7sdJ2UmII~Loc@k=y4X0bkB^!0rXLVy0Lp2wF& zP*>yi5$e-*=2@2Cfd{d7Vf_x{de6(~4)`<(ZR(rfqM`O7kcVkd$B&e=>lUjVqFHie zUDR&ey*m61qT7~#Bt6=Ht;8Z?k0I?3nvd2wNb|+SqkL=Mi1!I=8af9dFKDcD(2+F1 zG0QL2G@vCNyN^#;#m+(1y``}+1nmW?oX<2USD$I@c2hi*ZnqT=rQ1a30>a161sYH1 z0>U48F2H$A>-tz9vf(#)3-i;d*N2#C4O<^s%-yEN940&?oIQ5ib))|~s8&Y{!Qb@?_565yI0T`$k{hqKKkNmej#q(rl+~0>F-ZE8a?L3v%sUB zF`1>+7j}FH_@S2I|eTsNT%!2Z19l9QUXrsBu>!)I$-J^)rr>_^i2#c=| zT}VfWUmh-MCXmH94XQ>ETXup|B~WmN1I{FeBZx>Y+$`baF8p^m;HHDF=qPk(K^7CU zvI^TOX#M}AGXH1A(UH}ggO5w(gmXd#`RGVixH#mYU@|B)-f*200VDd%<7b5zF@H7g z`Ub80*2t+#$5))xJ+d=&Pa5N^#9pVyf_ZjHap#-D~5RN_D5jX@?pY%m_Ox(<8T zIoc+2j-~TfVGJL|SMhY&VbkAW=sf?TtUJ(>Bg;ZNjaIuXtKKtltNKh`@0qcBi8eEi z7RoZ(xJAAvPSLcd^r2OEnY6g2Z0^O>&jUt@T?XT+pU|l{>M}OQ%AkH)WyJa!U)G~W zb6i!ThF@vdOKX|NPl&5G7L%pxeZZ#w&9@dkjrX-CK4bvT68R6@b>g_?kH7^>-qam- zJ5Gn)4Kgp1G1i};pCyR(su%Zm?aam8&P>4pCY>(-hbh7$T_4G)vva%FGpq2HN zbyY)32bB&QP>!Y+hr@0-O*8vjMlC+((82(N4te^$a66J7j&NYC9;G-MOMMV>*4H5# z0I#}6EI+QKd{9Anz}TYZ^o+*9SuYJ8Qkfeb*j_R+Gh<6wdFpTnW%VjB;3QFyqR3#&4RHJHnD;bBB5G>a(> zM4)k?w7E9_L}`(qE{IlT7#4)^1$_m1IcRbwf+`YWweyugM=@hOh}GKi5c8jud8(GlKN>?gE~ew4sG9DB9ip(evZ2HDXH@c3#$q6b5)b9e;NVf?#)F5)jR!?R$+r&3>A2c54Rs`=84~F7A`;1*qi1FB!sI<89u-2O8 zor>>g=zJtsSD1KZ?7)GU zz9-$Xuc$#j@}O+xmFAU%f~X=V%TuD|!`!WXek~=QOrIZNH<)*@G{^j|4^FKgI<2a5 zv>R@8X}HF?4BzMhW1CuAhebxVCO4cgzGrz&MX~g-WBtvng7ox)tdaq(3nollFn!## zrPRN&-Y?MMHfZnH^Q!=v_3XVsK}9QGMFD zWSP+6$QTxoncjq&iZrL+{d`qrc};EYkmULkddxv|X1WgD=g&;>mzE4tzs^ig>bEg{ zU}hlb@gDlclJ1EMrj4f&C0Qn!{gU=$NjF+tunEl&?yfBcCUf zS*(TIw7!Ecg=A)tIb!SrY#uK|%HlA>N zif{7Nn$mWe7wKvr&3)x|@qG89deT2}mk2!2c<7E&<4-eVNHw0;6OG?MUlU>aX02;7 z9_t3qrKD*>(~2vP)(xHSRP#Jb<$Xqa9^=E%|8UM_J@4!v*Yix;b5%aKIle5`^Q^M4 zb7?(q^HKD?ujR|6=UKEgeyI0MwfQJ|9>$LJyu)a>pm#^=d7!oV2|e#2*7Ih>cuF!u zJb<2Op4~N1Y3A{@PoEH_#Wo2k7#l|C#u+ z`-~;@KeL>>%s3#XH%6KGkO4gFb6>#9!!fbs<+j>!1l=8Yb2uI8lU6@WeG+SDL+?TP zBJ{pYd_T86GO`dSVRA@?;7-MfSi3}*#+FagM%9#jQ^d=5zic}?*jjpI@ zN(wcFhX#kQ7(M#<;lt)k?paV=&>}}l1;@wW*jPZPHF zM#8TaE}S`TU{_WN)_G?#?xzi|zxLYE@`}ciY14uQb&2GqNlP8tQqmY*4OzulmGn-= zw{G6V*ASZU?c)79&@!`}gYI1-h?rSdBoBT~_{bxKPB75*c(kd*SHO#mCDA39I?+2F zXUvOY;#_#V5i6FIWXuH%LI~Q_m6X{z(DlS5mDo3D9DStvM@XUb2D_*}_JN?fSSHdz z2M-=Ix^?*ArooLR;lV}4CBYQ%PlQ9fGefyAWo=$NlG#o`@&{k8X)%C?>i0wG*V;b> z5Bof2(z)vET*<=HvFjUAX(Q;?DLLSy5A0eG&@ zsIKl9kmczf)iy%@+#4z%G`1qmmzm{lZfx!u)>Ks6-&a~T(AQEtaBRh3pI0{jp$S4v z&qula`lW~Ra1sfT{PB2UtqaK^7(Zp)Vy;CRPwPA34Pyho9}uIb^;_dl#aH8OyfHxI zu}W-`AM}0=X-4!@@7JcR>oRG5({=5DncU8+X#LUnyU3>o{dz3GPVB8&qBK(EBsV_U z$Dn|a`h@dG& z7Dhjmo>48~5f#yI!4ImK>I}&Sq_F4Xn+3Gz!%l7Q`D1af!lprE%zG8^6(X-O!p?h@ zTrDbe-U^=3D1R*Uh4{RdzOV>+FJZlTIGO;x`Fm~(_8{Q#4gDnY`=HN!^&*F3-5s+j ziZ|{i5#PL^n=6HjPG0rf8fQ&^ z8%za6j}bsSY0n{iU;Bm5L%h>@ZjZ0SXtDFauEG1r2l#qB?;}@(J9ZmMK6HNO0pY>3 z7;i$%8&lJY>jUoWoB3{_t5bG)v=35wv=3@Kh;AqCgBowyK{TG^P}gtSK^Ecu#F6!1 zpr)YISUoq`sc?B*e;PIx=j*>` z=jA-UZ}#7Z-lZp2^rAfp4@5t8pbFGYvF=nyNcM2u>;nZKxNdy!BVE8Jk(MOwyBx84 zOY~h1T`$f9-neR$4xrZGbFK0jhug9HwS?WePLz$B|L{@KrNj_znu z#Z8B9;(5d6@SL8667fBWkK<-E`bO`c!TubQcn`i_kFP{fpL@_}s^7xvJ}UujTRzV1 z@(SD{5ec|L0ekpTYijpxVsdh_H#xJI?uhXPir5T~56K_BX63G}(H*iR6wdM&hqKM* zOLt*bR$<8R4^ewChvM7A>kjs74`lRSvpwbFm%Fn3%Egu-GNj>?voM}fm{2xiyXdT^ z)7XKNW@B@VUx&Jc%auGgRVF8gl3`k_k9CN0T0sRrm2r9`*XK)4_Lcj}ieXhN$dC2E zAHoWb-hV~G-+r8c__7h-MDUDgK6hjEJ2}<7knmRDs|Y5CRJnB{;ZXUH+(qcYIWC@W zmOtK-uMCQ#p#wg2Ra{$+o5_7gk}nw2b7lQ&>+?3^Wr;wBfa_k3o_f}GlQ}VO7a86BWj~J$x)4=P+bljS}j9MbE|^U z7a_AHkl9!GyfBmJmX=uwQ;NWMu;>hZ#sla-tHk75a%28!H&TwJ8JPTl@eusL>%Mw} zX`(z@KE70Slq_JwZ^47GrEV|_`luQm?YWaGhmVD#QP5i6E}xlLQ9XNsRx*l4lt)cl zIroA0(b?R$b>{h5<4Nz*_zm!@(0bB=^@RFt;WwN2Wi>tRnM990#yq=qfOqlq=i&>i zV4E0)d)7NqPJ!4JN%Kmu4_{lAFh5xSxje|Q-ha@0va}(azEvDbu*53}PU0=TTsD_g z=A+S(l>O_nB0pIS$y^%hFA#xnMrKhl-(=P|JzxbQuNz;{H3iX2_OX`+gj8xy_I#*$ zt!15^InepKXOG(%8Yw+#TMvwC;UVBbnYHk*C$G`G#T<|4)kS14AfAI^_miK(9E;N> z4W1IEIIljB^J;&QAAj#7$hV1Y!(Mv25b62pd44Yl)A}0r%mZl)0w}2$6gOA%=)C&+ zdBcY@EFB*1Zf@=l4=+s|h9l>S;dJm^F>F}n#G3I77mlx)SV`^UcCZZTJ7kv6P~%Cb z(|F5gsPUu&XuRbYBRu=q7*Dnr_y|ZtJBp936ms=E>k(AOkv=xAgM4guS+M+|tfTqZ zG%eX)bXk_)>}WnVO-r_yzO+a4u`#X9m%cixenGGwC50UQ;t-cF0hICYX zbH~(C52u5U%5>r|U6%PoK0B@_W*6++wwTXO<4ISgcExm6jZf5g)O3z~vG+sM6Y-&W zuIgK6siuLomF3|GS{gsIJQ1VSYpcFb@vqC(ZH}Ylxze}&v2t1GKB5hnKRmCfuQ6Kf z2aGG(mT}VU^?2Qxf-E+kba>4#ONZC(ARS)QTRJ@B*+xZp*s`>Q!^_^+Ms;+$J8-M| zaJo`n1GpZH%LVvucrqjfwqYI51l_8v_i^|HTu|Wyu&PHQ!&8%>>=zdWV7bYZu;mz? zxO6Kzyb>SMv%LcpMr6e|6ciMc7L*i~^T&q#mO0CE29U|Y#TPWNg7|&p(V$Bj?E^cT z7d4fX71xz^Hq36U9#FO`JGZE{ICSaoqT(C!gT)iaEN@>O$rznKcGTLgRn5Lm`Aolo zhE?>`PO6H&gP-P@`QE4<~^;8n)f_z79K$>8Xqj@DrbhzdFgbL>wc(n< zgYsZz2?od?750RRLj9oqIZ82TWDvp)a8jNSR6_R_W2SpA3`Bgr*xngxz|9ZzLz>WtKJ6RHl}ft=x3ln##)AGg{ia@=FHP z%g~sX_SScO8UE&xjV%eD>`7DR%mvrT|2v&+u5QTYyj|&*|Cju6dDzn}HHy9_!dAur zX-|1{Lpq53*!)Sgga$zNvcpKQXL~%JfG58gvI!d_K5=61(Ao?O)05ZsV|lUj4IA6P zv%2=!+WP6$gu}X-Q!jbEeZYYFUQQ8GIQ?y9y({4?Z>$V z@vwCF!h;XvLN&%K_FVs1(|kT(p)ZJb(cKGb7ab{Ch5NClO>p_gl~UWLRabD^Mh$Nq zty-IK&dpBwuxMbcdE=)~TR3mVjPYm#+E)SoK7?~2r;cq@7AGGYFW+BBd74&5M z$mHt>fLv)!XjtmN#EA^4GL!k!b9IC9$5IzCjKPija?3O3mE;dBd*tF z&FhngW97ilY}okUm(SuX*Ai(?0WbLqbO`p2BDgsmbo+v}>DVZ1dv}Pxt3e?qnH9x! z+u2dl(7;1Yh9L9E3x8-0+}c<}aCfQ!2IjDwHV*&lcjv$YoV*-3117sN3ksV{#;T&c zl+^wy@aTHNjrT-4&aAA-${CtusI-30{zaYjUqt(bOO3>oL?dZPNsKqN$70Eh>9Y)d7*_HZSl^ql z+DDTBX9j)MCVc*QG63RQP4mF|mg*6A9y_sRO1K;!T5+ZL8}kM<3>`al%=}r&9ix&Q zsc9+x?9xzviMMKYT}!yQIx!(BEyeAD>#V|?GrX#0B8@M%n`Qi3YNEaGGu|kJJPiRE%azktO)HxfPy1>dJ3X{Il%AU+KrP(ecn zhzgio3X8+^0Tudg3Pgf^NDxeYvj(zY-Kn8nOn(y78GgGm92k&S7aTV(P*m0u89cOQ z)ZB@+$5vMqH?|ErrXjgtXjXBgw5g-9xu&Ll@s#MX^49fdR86cL(BW~>AF29-~YSidAC7gNxL1K+N3IMA}BB)zJN?!p30 z^(;J|!eo4Hi$DHWM~yqIfoP-?N=Y@oCBaRUi*!YCJX8&Qlvl zb>yO*kU8SppVbqPxnH5#F4%?d7GcOY{77L-zS|LFMZ9Gi$d`3zs2y?~!yATmXNbqb zfM#o!c{@C8o2X2+b5pF$d42L){-t)nVv9zD#?dku7(`*5O4Bv z*dbfjOqH^|ad7RJcEdFqFFlrTw)|9O=)+$S>#;!Y_)rXcE<*h{2j7dPJo?>^xOZXwj{EwGNi*-T{4tty z@h!1`T@Km@k{He7N3H8j`Dq-@4M(NfV)~A${}1EeYq$7io#i!ONYBvyx6bk!PiJ|} zU+XN-c%#O|TW5JXIno(j>03uE1NnW%;R5=%cx>4~{IXfD6~C_7J`0#Ek~2IGT%nduml4L@mS~;>Fzz3vw|0&nI6p6KWT0}?GU!A@pq_)fqdS zhoGFfUCE%rPP7>I|R+5VIKoi&m20v!2M(%in{f!>F^ zE*s|aCdg;XC$vQ$i!_y1B{>pPlAN`}pfFBGJP_TRc_{$-kyQU4D!g`o7W=sP|voSAux|^nImVIOG=MioN3dN~^2nblg|k9=osf z1oz=()ESdKQly_yq(^G}2KfoMRpW_AgpcWTbXSzxYSCN!AFY>{1K(z^rBbwFUcCYv zZMJBNG^F5r0;x{uZHhzA@Vx|toT2kjC=f!0`$T?ps6jLecST(Yj@T+<~< zhf#df&kp3K@Tt}jtrr@;iB+SkLaC|gf&9Wq*}V7b<@Ba$gX*eAC-uw7sG2xvC&En7 zUsy4O+yy^!4o-;!#W|5=xWip3G7%qo)$v~VCqAusuT*@p%{3SP*6gk%GF8dh_|zyZ zG305bPlQCu&~*GW5604;BAp)b=jIL^P*Gl1np=`v3{SJC41r<&$iGJ?A2w9QPTu$; zDUROZc;~YVWEtU(($dc1qsxLgh$N+^X9mmuANJk^zOAduAAk2fNl%L78@2mz#a*)rif=ks>e+J)PkeAg_*1W>3u*@N2#`F9v%5XsJW{*FE<9=jpudI zek;M-h8-7>wu*@dkW*Q&({C#&C)cb;S0KmR!?Btlg&~Vc3&RvN%=mk@Z_QV!!YRL8b}#bE(pxs0XKdi zjkKQLd5EdRR@iJdyUmVxWmiZw`4z`HIRLiNmSZuX5E_xCmxU)zKUOY|!}AODSQ4p9fgMm`{}q_jI_Dpi00 z1MJ*i9H{wd+fvydORS%LFU^g?o0amdUIJbMK3k25>Ch1z!P-$LT;0qw3zb856R89R zQ|I%VjaNnPXEl-Fx@Js{gkb^fWvf@O&h};}+d;~8##~_c(De3>)f1zya=72xy=L{9 zo7)Dume{I|dVR8Y$=PeVcAhytrZbH5ag71y4`EzsvM)7IX7Ctj110{z=%88TCzyz} zg-RJ?ELrnKj_|b99Em!jO{7Kk#7q1T7rprI>(o>XBA%^6yYShWV1`soW@mGtw|{cf z?3o#AOAfU)4Q!p<*q+T7((C#x(MTQ&rfN%#W3*>*Q0^Y?X==|j71l4!O;tOl2in)R zVK}ms1qA;MYhwoX!Y8VV39kfsIgl0lLR3jb*$1AHm1|Ed7K_z_(-SfKT=@-rC8tMH z*4Y&{SbU8&^i5AM|IA%Ec5iWe-<>bAK=G$|1eXavgFO>#(xwDom*$VbXmPVjI zd>T2O0#ZgAEet`uAtA1aIfY+5tXaSqavb9tj)sLHQH_moo5(b_#G`255KAlw8PQVq zkSd)sF4~a!T!_MQC5Ihir>9!Gw$^%9wTx_BJJhn; z=ikzmUD?8J7(pCUF4oi)DE{+6Yi5PhIhAfL&|GBrZ5vxJ+;K)0q)fnN-<;d%Im|c%!#tH5c=ab)e%=y$Ze?DcTDVK}RCs&=Tw#r%M z5a*NeIOh{G0Py2UMT*3cW#c0w;}gRmss~+(SW0)TE6lDxZ}V9@cb;|T?)=nHQ{989 z;=eB+4Z1#xO^SVSCDy;0T@1-$`P0y?@uNvF{T5tL&}n}UG&TS%_WLYmBc1=q z+J++MB`l`Y7JOzLi2-U%At*JXU_~mb8vSJ!fQ}?0wZLD#CFy{M78$0$!thjIYQ&YH zS`}3ue>fEhwACl7zxyFWeVnne?t!UTFl+#}R@G#nKd-NTzH1{nHptQy7~|EzgF0wy zu-}NTnmJw7$r0>5GRQ5Ytt}O_HKR=Nkj-R$BFtk=BdMs-NU_KZqiBeX(rG}PjCM!} z-S_7wriVjWFBg@pwwiEGAB%R6Ezie7iN;WbHHY=*tlNH((~~ypOi(fOWc_1(y~}!H z(O8pLwPpt}1>g)SxU-7^=i|^)fK`^0f|g=nU8LW3LkslMn)m-?W6SWtFLyP5{dU?9 z*TDDtt$+h^S>l8N4NZn?Kwv~ZIKD0|6*M%%rXmrjzcP^)kz*eAXA4@??GZt1V zTDcMd9iH$t4z{F+g0H1oR64e=hswbcDXKS3b%a{I=9PT{b-=JA!4Zt*ZIPH9dE@t9>Dl(#X2@%?W*9)Nc#r@q9o`hLX2>%Yr*U- z#Qz8iigm23gH|Y6mxzSWg+Cfe@^vgRL7ZZioHev%MD%ItT*!T{sX>QwYE=$rB(4jj zGXq1deM1w2bx|vW-*PzU*C%o&ow>?n>>UU#?d)1UvvK!ZzT$BiEb-<9MTnh|@zbX2^QkrGW@)h`O=AZZB^LzPqqCcQFxc~k9Gw2OI zZ^rdI`29p*Fg{#A$Uoco=PU5}YJNXNEnL49_g~DfgB0TPHPT<$TlsZi-@g_2AK;%I zwDeMGeBun)QZSXz03ibU=0%l(`J-Tva^QJj^1@Xp2oPDO$uPJ8vF~`t1jN2$#F$eZ z6Y{kuJ|^g#&|qnh+A7WolGp)DDNpAFms05yKO*pn9|xvI$AO&V#{toi(t1Rsse&!y zsHsM(P^$8QDoO$%$b%?!4FhC?m&nVwo1ozYk%auiS?1EWGf$;b3A+vwJcAT@BFP6# zC+#iv-r_G;tw{!Ib72#_6pU%#>Rj)VSa*k|*7{KhKWw1TH8h#`);ENzY$k&tnamV& z!JhV{$!XCUXzV52X_77|)YR2NR)EL~L6Zgsg$V&Bh%s{~WsF{%M`03E5FX8g@MtcO z&`*VwcnAh%yjXaH@IY>oQjthoM?y9Eo2fLE#!`tRsZ*RzLRqnZk9@Evt7isNpe>fk zCX61{nv4|kiDbvxZX~7%C6c4bKab`0<{C>~sID<6=UIJSE}I_cNH;dwjM;D~AEfiD zv?oYDT30a7%SU34o`qr(@IWYY3X3lAcrj>sFr!Kl3NvCz&X^;`6-1~`mDPX#8|_P$ z^c`dyJAU`uu9=(fATv1NUD}I5X=lN<@N5?&2^@38bEM=)3Ex5pTE)qV1jq9?YT^su zNZ8`@OaR7J+Ccx8Ou+03nE>zr8XX}X;NpVgSRO#ksBtkXFi^n?43axs1Azt@N7`a8 zkWqNDIAUj6(HFQ(J4aQWhCs-slf{Fp%+)wFa9o(9ut znYo4zMq{|4z@Q@r1nsO9*7L(TCm+*oO-tgnp}LG;*2%HN##E0rnoRp|m94s;$U9yB z-r~25SMp)&x?2McHU0)fk|_R>e9RLF)(7iVIK)6bJohQ~BW*rzcrC2rGaA)q%xJzH~iC=V=FJY3}!(ZtA&sC6M%b0>XG`TEBIJ1Y^krSfqpQG>Hu;z zywD}q$Bb$lGZ^)yqJkyU7tpQ>)ea?O>r;(pb-mRh8{%zH9%*`U=8)RU4XUOY`-eEdhnSt)mG(tF;YCT+Wg*^VuDNc%Z#K z>Itj5gg?=xt#cV%;2STW`!DDQt^^;A)HKDv3c0rU*D@b{%IlyJg6~Cp?B01k(@Qm| zNOE_9$bp4a*!B`VY#@>gA*4(Ladl&v6e?^LDB*F+$CM6_3el2lr#=U*!w|F&u~btu z+LVg@nG_DhQRbJA4oyuBjS^O3i4N0^|Y5wlhL}(&=~2+ci+C(C4Eu z_#(b=puVDzjp-zXY`DEc6Mh$u%IfymY#w*D{+Q9d#Z_%{xokE!Uk{gi>O)#!m7YlQwd?Div%g*VQM^ljTWMza-NIsrxl4F4<58MWw6lFAd%5Y zAakNMIvvOyyKZI3U}Tib^(y!v`(R$GEqHl$DU(qpxxGpvqrjzZhNa2p!KNm1DiTLQ z-K(xU_;BqMty#a+!u}bSx4CG3x|E)aPjkqsOEq1Iectx6Kj`t!Qxh3 zsIsFbExx_9nSk%*hZ6kNOK?y>Rv$B4ZMINtUF!UI*7iG1RaI7p)gNw+uDp=&S;ZW_ zC++5Qh>))(C)tVc1!+$9yf=lnTWWH!~2QYz>97ozcNq zbRb6fgUBs*RK6Jcz(&eCNlpbxl$TSKDy#v{N3C+J%?$5d{T3?is;`~2LdOK+2$l}@ z1q(@hl-TvBYDKQUE#g+mqDYqYHP!OTo&sSFF-7(D^^Nr?7+c3{oFk{4gM1L^r^Zg6 ztPxesNAmDv#VfD!ml{!Xc&YHdRatbOyU)E%x81c@+2QnTOIG{^wp8$SaBvE5h1g!uQ0qxROuf)9X@vRZ| zS}5lLy#QPCoum8$L0BkZmxh9YXNkrYuzJb?^u&#)JgG~$nrJ8#N{53AdJNIo1l5yi@Am6%#*+)IYV4L z;PxGSAIA5k+MH|)C?xZ99k)p`p){eJ3fHM96bK5z!YI6xH6V!Uu63(gv)PvE^?(ki zEu5yoDpZI!FnJtunS4F^FOgla+??zpD;%wcZV};Cj-f#%!nUk}7F7_p;bX`W=5oxr zKs?>PMxD-oxX+dm*D#<$zHHk z3HpsyiV+3pzLQ{PxmEC6)#kL@>y5AnGo#UHSDimRiP$%(bMC|JPWdxZh$7H@q;)3C z0XY!CTurAC_6`X$8%dCX6A2_4m_TuRFcpf1;$%wJ#o_D>l`>THpjU~G9AuWT+v`F~ zDHZlcd}4{MEs8FkaXDKv@>5IJY>_vvS9(Wt(X#?mW9Keu>&|sg{A16ar5DEQuUR+J z*^T*jVO)1%TqAN9va5iS7Q}oH&E3yFBHsgAJ}5X+2Mt$Nw$XBtV$V-9xetHP@d*_G z9IJqfJP*07tUwV|P#m5jUMuf>hy{N9a`6@Up8nYbJ!pn!m!2^KeFBJU`Po;_-O28e zUk2?qJYKCrR>aJ4ICjCh!;J(g>)Agte8sf$oVQ-S!#RBYeVaCYSsk$Yw6F3NqI;V1Pgb|&!8M;Zl8<`~=~%CPfq_Fl;VU83v_iK3EAY*;C z&TiqIY29Gn3+_|sKH^i|>Mhq^k+}1o9f#kRy3P9_#yJ4q>MqV(MI>jTCSsSdWLTw3 z8ylKstje(*mQVy#@>~NAQA?GzI#gSq-1CWssNlcwH|M`>7MOI2|5~#ruz>Kb*+Xzz z#rCm}%Ll;QMI>+=wO)@=53TbA?QV?10c(OI;t1n9c7%&_yd_lThIh_}E&S%P(I*rw)DwoyA2UYnQa9@M*JdDb-)$#;;L;3CV8h51pmUBl; zdJ5M^_;@`9CzYU{;LyUFV}$Z?NBDATPoyp`pfW!O!I;9r#-xv-&vxD?=%dEpHb_&P z!zKF2^A=ZfxB}YXaM`;Fq-78ji%b2L`Q@5|71dx*#V}jU)mXexgfq;>Dx=j^;}5sQ z(Hlc<)`R*-0m4JW$lL`-*22uaS4KGrNngjsNUt4=%`=O*>mPT_STI( zy&FCKCPciMB5j|a>Mu<6#2drec4e?&yXWRR@3gjO^+O%?I#sQz$*Qw|{qSKZplml> zAmG#uJt5Va4?()!U2q^tf*(17Wd?s%`1I?@3%ZJn?nr?IYs~=+T4MVUd4x9|)t+C(^lQI)WdUg*C$#Gwhv`f8ORgckb|4;d8_xzHN$%f^2Kjen?fQ- zcdc6d7`tkNo2^pPE3$O9l-8^TSqgfkYYTP3hB`%6^4aF5STwB2CWBgIwHS5K6l&`i z=IB9l(qRQvm8ZXK&}wItRy2&uitdoO_4qf<6kr)`Z$ngS+GS6;95EBrs$#u0SO|b% z*}HN%f(V6T%rCwM6)Zns10b;_qwbEzb^U>QN6c-r`D2agEL(cPpvfAu`3yBNS68jT z3e|Y6%lWY1bJ>cS4b=xNj%(Ue@b}FbMpo+0TP{gnf954O54PqEO8-=FP@sv~Sd19{ zd20lbYwx)HKg{gkRb3HG1?{U*>0bDfRbwY7q-BMrwkji(tCF*t<}cTJT(Dp&%dIMQ zS)H5jGNEGw2_QudQ38@XpvQr5g|y*`G!MMZ^NgK1kRZZ(Mapf`o^T}L1P@*v=_7vV40^O^7 zo0@xjo13691rGGjeH-~f-UWqv1%&C(I>E> zp+JNmT!gl=OY?Jc|N8vVW)P;fsp+YWV+AD7*wWn$r;zZ#87op#?Y+&dXPlRB&D+#v z{jy!|G#;&6)|H<&_k+M%^l|e@mt3PHEuU{#+8-ZpH!P9I^y$W)Xz^FQ(MYr7tF(5C zG=yP4D_?_?wN=V7CuM^-8Hr5MG+R-g0jV4$MKA5NQamR^D8w!VV0b5yc!MK-a8fdx zEn6gu#b+S_T$DD&z3`4au&}u#<5)O(YjTtCk_>tS2;oV(IPS9x9VcptX&Oz6aTlhb zjbVPsG4D?U;Tj(4Cs~Jnf6>Z*VTRl8!CQI?&&Ii*y*=g{mwun zy=idChGe+m?5?E-U6nObJabiHXeDxntUU`}@>nKRKKK*r3SJ5vU0+yJ4H2dW4~a9g zlwcOK4nAE9A}`I+N~zsbv%==3kIwe?z)vMz7Uc@9pm>GYdT)_A(_4Evuw zzmmV@eav>|NPlZX?;6Wx7xqTey?f8=8r9p~o>eP*t@VlCH5*gyIn2j_+^s*6KMZ)k zrQmdeSk${@*n>!TV=7#Rg!(5)#u}W<2>yZW#*vQiI+2VZtYYsW>;%UWqJ%1LEoCdf z4I;k)ctRO-U+D9S<=ByeUX3afko_kK`u-V&C*tn*r_#JF6@QEbBW6o>YdPjCis zk3ixavIx&}4n7zRN}NOKy5Jz5wg>!nJH;R(Wd-LSI#Cn0#6ZyMChYhK2;)S)F zdN!ZO`exdC@@M$E4Q5{=+@D!;ZSn50-tO`IfNo&Oe&02F4-Xr&8AHCg$5<1N=x2-i z(Cq^oZgR5!vA*qMfvYz7v;7*l>LX6828=rB7)GrjDdx444o~kSamJvZc6);OKc37m zq$-rbaDI0&N^eTy_pi@*sckZwU5CmLI)7d5OP~3ij`epHi)=jiC#nbW2Nu=;38q27 zJ^>rj_rSx*@@~F&kW3NruKT103BHhipBMDj=|HMTXzLC78XQE+={Ta@19nEZKucTE z5@W;f2C1Rm&o!zsxDZC{$QntRBRhG{8cH2WFDMPzr{8lW1re-XWBN3H&ffcri!Wu~ zhJl7|yzW!ohVzF9hP(0l!z6W%HEqYQuR!lnD@6;TS}(W^41jHo_zl8?fNUHYf+u8qXGXI5k&cWywSLWrJaF;u;@#}rW$R4E8Fs&U?b712 z7Kg%iZokC5b}choX?xe+{(OGm&UK@D zWl8bZYk%9@%Pag+m0xF$rHU`H6tC)W@4Y!-kpkVxYrvs>;MN_|87wExBwqsOQJfj2 zl}t6R8tzdnD#8aWdaCnH+TzOVwguNqm#oxu!6mBsP3K)(HG@EDRHR)fLu7=GS+y8d zqB^n`hk*G{5)BnDO9hZSfS?53@IJ&lg>k-Rq-}+*G59%CjcPqx2IfPhpi-F7chp#Ay$vS~SkxVM zRtNGgJ$%y5vx$wD0C!=4%k$9X`LNE0HM zHPk0+3I(Fan+xS|dPcxVb|fNhPadV!*=q>wYR=g=Pu?(*!Oz!LZnd`;Z~2WapE~dc zlVAUK!|$$U-!WKiYAV-g$74dY$M9lR$CVw=kYsX{<1e@I^fc7}D=b z#)}a0rd$u*JNPeYhswagqOb}e4aaW%dg4uxHeIhKfw}_aNl}&yLIIX<7gWShQIyoyPd^1nBF+p^$zv{xjoQ7keRHay5WP%2NIcK z8e8cc=#VB06X2I*J4kC2Qw<%>aG%hD>H{~RrvR&DkW5B{iQ=|EQDLukZskEgk(GdcQiSz);P!cg)}@UO&3;xiW`FsB#U4fz)KtT2N1z(QuTU@ltXY82s(v~RuJ^Y zK#8O;uxQnA6h~eJ{;xBKxO@nqUi?;aNC%)sym_MRD^kAE-m%)$_)s&3-|re|iFP)P zA9cF&!K&{zx+&yKyplxZUw@r`uU?&8w{C;LA8_Bm=ZkR8!T-VMb|4ahEl0Es5C+<# zog8Tt97Q`wfrMq@k!N0yi>r(H`9m1Z2%{HZb*HFx%Az;oXY??Bo?Cg#4a@(LIeM0K zpL0(4k{&l(R(zf5iqCL7PDyWvy(4xKdx!Z{w)`g++dJL^&E2_(4ycwxb5q4Hg!ra_W_ON2YX;p8u^TlV_ml4U?UT8&4Q7oWNzk`NM z%C*I&0ep|jE3r_>?x09ghk*nVZoVLbPkID*-HI0?;KlXwdawf^IM_YJj*@ej ztY5*2V+}cn@n~$#k@d%qJ+|bbhnBF&z(WrWd|LFU^?xEe0l7(I+%4<_#T{%TDey=$ zcMJPR7q3nqIMBNPK=$A@8RoezedvbFp+o5#Z_J=6pgS&I z%pQ@+R!Vvb?q?&EljL5-k*mTy1fM{dhj`N9x*8}js^Qqd?Rr$srqWe`gH}5p=~(@3 zZJpgwSL=L+)cp6?*E{TWwGMV)t;1PY?{L&EAGyEQ;i{!SiH^5GQ~WjL>ww3BRA2DZ zKxAbbct36cs%7Ac%Qhasq67_s$QK4~6)rMMI1?}@Sg`dIP-n7n;0XD-fza;Y zAs7-?3m_!pCjyfI5vSy&?Sq#Ii4z#kun>R=ipF1Fd}f*T|5^OqvgW?&OV)k$S7$e` zzKq>)>%cX>U3m0;{&#boD&b};_HG`;6Sf9!Lx7T0@75;~H2pF5KykxWUwUl(WipoespoQ zbA*i~yq>xmU$mypKA48*nJZxTG`KRg?J;Jqu~k*MYdz64?GV)cYYNerHcNN2d)Q`( zudr=WmQ7@q#M^p@Ux%=VItt(@tHDqp=O`pNgfCDiF1htedU%OZ(gPs9URo99KjDzkT;Cj> zXMy=qO$q=HoFw@nj$^wk-$j2z15HdY`^{MAvew*4=h)JY7^KJESm$W*2MK>eB+^iq zw0|#BUz>C~Q@)1CUzV?0wG3JQVok}!cyA(@K+@nyB#;P4qv1p&#%={2{U!WryiyQ6 zD)OdzA#_Hyl1y%`5!c!&$yNyofkJH|)3>89wIVR=FYaypeQSTq zWST7x{XR3$GS$NVseiI>XJ4SMuf7j|?dWT$8>s)E^p6wisdQKRCrR9bF)f>`K`!>c zfd2&dNf8iMlI_7)A?Y3s8Ryb2jI-nl1c?^DKv+3sm54+mdNTs_5+Y8+PN^l_9O_g* zWlyLflx%r_7#=q1)T*)HTL-QEOXRnHs%pT}GdX*BW`@=uHedXDmFrBf_I{8%VmPtz zU@i5#urUdn@CGu9^GO(xk+t6Lj@dopL?i(Q%rE%!lEn-%8$;h=y~FjfywRPccs;mY z_+??|k)?;!!H4jtB$M5qNV+VopepQ`e++hPXHHc@?7PJkk8EtdjYTpo8F^3N?2j?v zuDN0MIcyiR6f6)Sl3^;MF32)PlBgu(oP}o_DC0dhn|i9VVS&7Z`Hl= zOZl}~3LuEitp^?bAZS~2Aqi!NOdz4GI^3BGW0d)hq;8qKf;e!FSRtnZ8rA7kz;b`1 zc;~;o#P$`>_~IAYee$;AKNR2Bx05YWD8>L*S*H3{j5BmTJBxQu#v&d72wX`E)9c^W9S>n}xu~S@X#AmV$ceJ#16q2?E zgP|^#3ubiokwkpZ#s&`^e6((^_+L{?bh;Iz%v5}6%xkO4<!Q9cIsUo=-Jt?cO&7ryD>R1vpK$i}0HOZN+qg(S#XPH>mu*9o&5AWG$ z)wS(iziQgxEi|=_hGezB)v|4^&$eXW*aoY;c4nK+*5^BLSunNv!#neL?aXC*H@BNz z{dDl?rFz6Z|5Uyb{4ZsqZjpLemqv5WJnTp!sX~$_s$z@n|@LucgnGWc=`CeRH)2VH2@tl84BW= zOk<-H;&-Me)7{bD*wUCz#3;MA-wVt}WeMb%v2hl$M2uROo4 zWgj^DI%edl*&6}dHcDB{ASWA%d(ajRalNQYVujhRcG7OCQA{TNS=Fjd(d$VeR%pc) z+=AFegk~Pw`j{3oh4wdbxwL+NX|kd+LzzZ-hZZDWJbTZi`}ra zBe!%s*E!ljr#rnq#B|-oH-20CUc7gVm9Nu15&VESrljql%X&y7jYuoIp|Dmm$a4Ey7-kH`q(D9bqsf5rroVi<9~<6s&2i3HUtd)E2LT ztlGs7IAU!C1>sSkf-|5hC#1kAv82zR)!ltoPtV!i-Dme?R%GzMg?_gE7cNw*a!Shq$iMnzXv-hBgg*IbWb1#!7ode!XixjYo=-Pl zdMWqA?UTAOPanHUzJ2c7a=0`~4IW`JSDc}^!d?WPR6!C&_77VXw7X>Z!0$tpni=?^ z*OC1a`Z>}RQVg}8t*~0HPOH=Ig4cDyXs(5-(3=p7SB^F+G?{kxB9!WzUf%OU-8Vb$ zy?4vaY_FmeZ)1DYKl_>T?6bvN*@bw%SsDRM{=}buVWHXv3l$D+y^aoY^70@Dmnfts zS%JS4umhGPoaVA_=i(7T79_F#5FsZ#dX9h7Ouh$W_Kg{3D(#O3Q>TP~6OV5s$`(-~o`Mp1U<9+3O<#(R+ zUiqD`|Dt@a(sR;#m7dr0^Y^}b@_S$X>)ZLg$k?_?In}+|+>{>e-V?5eU0V<3)kgq2 z0cUCQMExTT+$1j2NJ{tKaguxQz{SdYUpdLWui#?Ey)1pAd$E2jU3^j8&GCX=cCvdf zE54)h-Y=Z&-Y*mfEAEx8C%IR)&i+BW7sCPGpWt4`$2&h_z)iNE?0&#lww9nQ=9fMR zE>q7ME{k!T;NJOhh`TjhIt@5i>=VpUFL?_j_tReGaf-Us-U?bd1#%I#o|&Yw8Xp3-CO`keGl=_ToB(tk>?GZjhm8j)iw&qmlPwwdi> zm#}NtyAY@IQA7cJntc@&+FxeBro7CRs`g4ozVdI~ybkAt;dXz&hsVzRe+EA5-R|mY zyq;b3UX*P%vl*|?E_yG1dC{GvmVa+?46Ky11#k(#3tyWAa0$Q*Ul%RDv-J7zEslW| zs*N2jPRUKmr~Xb8N%KGou)ova|2_R(84P;7jg8(VJdeuRRzl&;w}8w&Zv!gnHeiQfmqM;50}h!U78`IXtuzZvGF%rh-(MqHIp{n1 zA|NORJ_HMMS#j64d0Ua;IFDLnE2?~QrG03@C>9uo@OaG8FLkmXLY6Q4H^N?%XFq)9 zl~`NHCx6S*B>LqGiXHP4c;k;^^ccI;ufa-{DDw|`g)QwEC)AR1jetY{I8X>MfX0y{ z1as<#>&VC>GgD|Lza4f65CAw}_k_FMLU+0aASW|Vh*z2jk!#=d;UiFJy>R3(6k8qQ zDeMJ;IOc$-ahT(I_1g2erwTR&)PPt6gGY3U2vI1=KYx6bN4deSwb}-j~;40sf2q zCf0)X8+@9@sIcqweAj8~k5oM5PD!4{2FGyI^#p*eSs=)n;^y| zUqHiR`^&@9#`OUDQpvUmI4*Gd;@C=EaoeIc$I#)U@?VY?pXbw~ClajK3+O}7^Up1s z8g3v!aaBK`7qzda=5PG5)E4V%k`l1ja?dlJq`RIbQT77Vx zVBIL67>!eGh)T;yo?9vGk)y>I<-ZhPJaU*7iqFsfyu4=n*$Y@R;$8SW7uh&?Yidh` znHAo+x7_Xp4j<(D5%ZzOC7P)tvZk$*AiY37^l_9Grl}XX{*8Mv0wx{h>r{F^zBshl zF65-qM~=dm=S2`#F>q~t%g@(bH09$yA1lI%f8^*iD_{&q4l8HQ{+xGIxh|nXfX_n^ zF(RrMi(bzsA`THiLL4GA9zGY|8_h=RPw)hS2jM&c1yfWU*~EWfWn?(Cme~(}KGFHg zy`<4KKV!+P!xu~DY=>Yuw0*K)uE#z>-BO%QAhh5O!P($|qMbrM4scOjKt;fB&_6zC z>K6+Mss~=91poQ|fa0{gCtwA{f)am0|FlJ+9*=`Q)~HmWDhI8SlYjw79GzwfLe-H`%AP3wBi(d zh_HzEwjg@EIp0Dh+I8|hM6ES~ho)i=X*B8-xKGa%#Hob);JFU-@g8q(3Z5en2)EPN zq{EE%_X+PMA31`*xL1P%&N{-OL*x+y@84rMAASy$c|rghI6&P@ADISDIxxO5ZT*QW z#5y7p9xOP{X}yB|CH55P12F+~AXmN|Z+XidrT-=Owi2OT{gew6nKak3@+z`Z9sYrtG`Dme{Ge{|M}z07GNJ&WTa zJ?q404R0NywzEH{rnFHwt>m$^e{vc>eS{eMKhn7?9j0|FKeGaMWg9+bi7^94 zM`wQymjN6MmCrl{T)>~d$uk5|+#EujmJpD@DB$7cBSiG4&6g-3=Y_<<3`Tdsxi0zw zT|qxuFC`xs&j0YWIwcjw{uA@ZNCW`pc>@kRE5+uN&PE-1L4c8$3lC!k>)D(L6^>jEQy* z(V522AwaUCUv2G8B%NdFOa2?g$eplP5Kt@-^*hZuE= z1g~Gc{@{rvbIwZf-nFwB9G&3Dc>fw-ugM%Gc<^Tc80c2Oh7H1*PrgNtg+)c52$#@cah$F-W(uP{W+Rlk z*T$^Ldh_@P)dxF-NL3VxD&gPp)F2xE@s$)+EXxerQH}ki<*Yov_d`~&o=|Rag(Bpl zltUp|9$ODTZ1mW-qknpO*CRWR^zZ09vh$H$#fw1Z08GKtmis%7>{0Gdgz-{{7y3gN z)6>)!^@+~4{y;a6vi;c0Vtg7NF9;)wx`H%5g~x~ie<{o)BKo3E*?#nf+CUefV;Y}y zRNL3(bAJAar{pOz`D|LHs;El`SIQ{7y%nqw!k{}O|no~H3qr!->RPki4|@O}IF zSp}*?QX@h+Nmi{;{W!8dqfxL<0$yQ{inzO5#7QI^5>Bw3wS>Xtn()D zEj*{S6{%s!OWg>bmg0`!z=bKwmpWVyCxuA45gSE8Om4-AY(YxoyWgED|G$ELjD76! z;u-9|$BUbbn;)lSfwWn){`KUiO5FFc`bv8z3VTLo*xZc#?Z@9x9>?5HtDPIs?<|65 zMK*JVGK;1HpTNTrT#xm0B_0A0V8qGteXhy9rz6KhCg5DWCTXA>qClL+@#mddjxS3U zFB}nad>Qw(w#UK!dAYkJL16UrX93|RpX&*IPKoanEbjgzV$QTbPNNNH(aLQE53N$Z z9zn+;%^!~x3Y~|Lph6J%;MxaPkCRqVPY1?3rr|vCqR?IlUKg0p{{ZfDrdQL&kW?(V zUN_>2=3fiAUfK@ygZ__#*WLd&;C1mRI)_UyzG>(Y3whjA!k&0$!Rm5260ds{ysp^u z7KwCPq!PpA=^b+?p0_l0jp?1A^gPomW(*8J3VwIL@_)haVjsA*mo~WyPM6a>PD4+! zhd7r@`xLy>Dfvz8JE41m$U};wlIDr;AHreIdy!6o=rv$)tlCHuqR>y7*#N`Ld#b=| z&bv}Q2kHkDasl1f_LAUPh+lyWf(`^NDARed7tsOfRu;sGmi8ha!cpQzI=}(mPLId! zILA&6GtO{U@F;|ZPwUBJ}AIW#cDV2cG$OefwVO$;EdB0&Q*p9s~WNeg8H*bz;sH$5)_ za2(R~VuB|=eI8JwQ#cefO7$XupmBk`-vvDJ>GFZvLP5;sa3wq`v1%PifYy#EDHl_Zf+yxOE=dRFbpyUy zc#apoZrnn3dh&rL913}9fpEyx$Q7{G@Q(}svW|FSBxNe`#85NvV08+%l7$Um9{);w zH=Vwa#~^`{2r^B&bDdX2J^HAgDviGdZ2Ta`FbW z8CQY}+&e0hy~rd*3PI5)2qOZCIgdzoOi9==xjwzzA(;hI+-&71=Qtprk) zAa?{yq48rv*DiAQKz3Uswk@_BiNRH<+-YE3BciPXKi|Z;aI$lS231nW%%3YI-GWJ? z@o-fX9}f4kPvEmFBz3L{;Goh}@KDeQ%Lv8;9gR3IPkm0&&YM@&Xy;VfK5CIp zLx<0YQX;=x!e69-nWqaU(>IADmyF3|mZ?;9i8E`VZA2hD=`OK!m9`Nv8i2WE!-4{X z4gx5?_&E6adlG#aKR>mDzfxbO$@*lQq4g#H1CNGMW#RMrz7R)I$!4V~>@>P7^q!mt zDfzaD1)q5Wx`4>tEn%z}*dq z>Bim__}kCnB5WXZ1_RrGad2PY)C8MmF)>Mqa}h%Yu=p89Cm8HWFoMt?!#w*nn2}ut zUw~JFj);=n9Y@1ZGWdZlw1`mXth(y6|#a&fN5S*ujfoLSOj{9xyFc4 zP8e(|N)S=@JYp393_(&$rrX0r3i&TVYB&uOq>SdF`J`xY$4taikFi_R1o`%?(8Yjx z=I#g7J2@cG12h)|fEUUL4j&X)BFmFNCY_D3xfP=m%e57oEf=Lds-5ETPyvmTx>Q z8ioDDSwuRbOAHq`+aALs@_is8;{>n4g@na=V$q-0moUGU^u%0O#qs47dSbrIF&@F5 ziH**Q#QYjMsb&vxPls^Cn~+r>g#)>165bhr<$Amkw$ovK46^h6SrP|mPia#xPoK6e zH(P3iVBSB2!@xqmN8;IJR|mN}p;5GC%+;)-z)=ol{v_=Q3uOk;H(z_?lOr9$2?;|P z?={L$0vI2?MjSA5y;SMr*DU3w5!8R)K2+%I)hS5ZPFq%lu&Pz4{($rWcc;_AAbA6Jk8}?Y_NVjduJ&xOIoK4BR^*bXKDB>fMJlBQVP*zOaZj1F zkjdsmK7`XwXH^wm2ekJ@x674k@i*IQGlM7gFMM1yqK4wt*jU?`tM`Tw+k65z6_)h& z=!}#tY7v}_fRkhdq!gp+{~RzBwEU^+&LWZiM60}N zyt8xNU{k7ZWj4FAFV!@-uCsHzzpG=AKN~rl*}r2QxxSjx$>^XxL`E0l7q!QOV{-ER zElyOxR63o^_R>p%S6-24imwKWFSAg9`EVy-Zkqc&a>ZYU!^iNriRY9-c4{g-;>fe_ zr5-r<&VkL#&JW2mpDKRw;s-m<-^nCOT7aVozh0BCKwb;Ofw%jw+`MvY z5ARbqH!Ii3w}?JM^uv@oKk2wEafi%~vxL)clh zHaOK)qoVFQUfYUCA;Xt9mBO~lrP?!TOfwOWM8mE)GRz}6+k8qdWD3-FKe88cx)ldxloC-WA=KtlNC`V5lwi%avQ@wsgE1euK#6_eb^$@Fa}fY+Y2x zAlur4oaA*rui31~A;@a-@-U^11PMby9o8_3L@GRIq=XVv$T!}V=}aY>QOCf8f)F$X zno~J(B!z0Kgu5L`n1@n3^F>X(4v=J;V~(BIct$ooksR$Ek1dHM2Ezlb7x|pBZYY*m z+IJ+Cs%uCNG!0yn$mF6^$xLc2HF$n#!&t08jQ?%txbJRgZZPOthH{(J?AFn6s3jH5 zP8B~t_ONd{nF8W6)VO>Oxh3yHPNhg8h)Gif!vUN@sHp=FAn2u-U!lCXu^K9C;S`hv ztQh_K1AzOs>#pvd>SM!l`|8$-&9f2tUX;ZI?heg;gMCCk0*+}ZvTaojv^55hR}jw6 zRO^Ni3|ZX(ElJs4f}WqsPx~<>2wA{3QpAhWw){4f14rr@bk`N5?4Nq1Bt!l_)D#hE z^GoH}%D^obQz1<8@{E3{D@pcN2i0y9;KldePi+o#sEXMh%(iy7jjkxP#73GYTiVw8 zdJvP)6^(V&b#(jAOoa;RmD%%JyPIA9w!EuO*Og8U$kmRThL&)o$>r~D3FWQHW$~dz zYg4;Ut!rut)`e@`ru5Q4r?q*sd1-=W(!rFoS`B+b%{2yCIjJ6m6ZFi16T49wl(zDG zvTOTLB1wnLe`THtOar+{Arl}f94H1A6$@}&DjV^JykuYp6o&kU#0&%ng9F{*CzJ6C zUeW?cQO4N?p)IN*vMAn!FD%8%dDV=JeJHZKA(^8WAp7jTp`nY0hV~6ljGvp|+|{|c zt7~&7en&?V$)RxPQgernnVdDgP;WlrOIp(I;iz-$tz% zz1w>Fo5oYA(L{T*)8MGKnp-pJB|djUFpAa1R4ZZ9B6Uc6IaJPRW;!*EEYU4Y(Ie?8 z(muk0yqvQQDt(j{A&whmZ1k!QiOV^N@CFc>IEY#^1fpD9rlYkZeoXSV(*qJ}3PAJA zV~eRY7AicTz{81vV;;_I9qbJtLEqH)^0pJh;|o`@XeLqHY`j|iSUSB4=)#a!*D=L|;RidlP0jMDq~mU|-;lcA>mntYSIHkm}>gT7mCDSr8-3)uND(cZlR`O|Nbui-pu zH;4ZzW1Hq#PlE=^`J{_t+f^nF{NZ3UHW;NM)Pu2Lb8};3y19{EjEdCwmM#p$@~xp@ zTU#)gB^vepx${^(^0+BdQq;Pq@)M}X!&p5PZMEPQ%GfHQe(lT?qTViPG`>GFA|HO^ zM}${$MIWlvCwg(C1D^RCUBTfOZsJNk)zLdnW!Q@HujrWUm*||jFMeQq@nbZ`HjMEL z7^6iR6TN%UrymCbE|QBsag_kjQBrT(#Md{q}tOp(#x$nLa_U`^K zezAY{D4&ma?k}i``JhxUMJUcB-7cIbbdt!7561 z&0*w66cd$zvf+eEAjvKXs7M5T#P;_SMsV0#qj2SWcsJ%A6YT`ym`A;xT{J zA8Bl$^Uq<|lImAshe{P$d=gYu3AKdCO~>irY=@8!DMvG#$>t)_*7oR=BWiZX;F3-E z)*`#U-|txe;DhYp?sTTFt2NvG1*OI~`w8~R;!1g6aUZ)RTwfmy*4Kx>)I@vFkDSFX zvmYTMs!^+0*-LA&=%P|bl0=ALeChx;(Qp>4cHD$Y}T#XQ~>{J+=&QZ7^f%L0e z?(*E!x$Qz!iIwL3j|llBJNH@kCGb0{G$b&{OH`RjsM(5hhz!l-Ai|WDVAnalhVFXd z1&z+Y-3dM?Fb=sx*;dw>Encv6^JYrMhI*%x$YbiqzQ48LsAsA!!t_cA%zYU0AQf@5 z1G6%cUSX>KA^;V-afAygI(0p`4iE;+3CU&A7eSd)%%b2g4k>D&=ozYUomPt(V&H;} z;D&2Kcm1?DCJehQ6=RWdoJZ$$KaSHULg@r4wxGEvjT>G9q6litF-fXY+Y8tDwu^c{3r$5D*A`K%`JDg*uK3coB(9aLSX=8u!gDuptPC= zJBkYI2(>2}6C@EvOQ)P7t$^ew=~NxE!Qzu*rzm6w4K}@;T2&89Y|ox9-pJMzzt`kn zv!?W4HWx?Pa|FKP?a0VGe}OudF{W=}OtdC$X}GY27qtRm9dZxx@p{(2TH{$=mp$|2C$%1c{$f%0d1Ft(p!Y$M9K?C0eioj`w} z%OBa-0Yz>(7iEdCDUTOfOGh%rkK%NJ|6zpI?Yiqe_`QR7yB>Rt-P2qIclvdd{=(di z$a(ussTEai$E0P_I_$113ztEfvN8ya=3SmDw?(Je$juSin!VKqMRL^0M!RI(<#+lN zyQC*l_Dn+&ato2(r&n#cJeC?_gOP4KOHZeY2=o{-F z%l2k_I&*kz#u*JoBlN2l{9hNR@jx)HI3uQwmPawHwX`q9D5yWR) zG*YwG3!7DZWbOQJPe63jamDJ@xA1nNJ6xnCl%|38Up+?etR3Fx88q0523@bG++au4~Wj z|K`dmR2%(z@snqKp!VIHKAZe(=jsipYKVt^6?%xM{1m1|erR;^hzL^>5O@(9+Vu*~ zF(--|f7LVICd*Gefj#mrwA+JrVdcD~fT?ocmk72B`2Uj5kMfqUDomlWHkGFZ$pxE< zG{z_a%4IN+fL(|t7IG?T7lK3DNF@wOe_&uMl1ZacD5{MKV;J^c0eThRiOPb=^tTV1Vo5haKAa&Qc)kPBtU?3MLiUF+UR|HQkIl>co>(wcv1P8 z7X87$9{9K}^ZIu_PJgj4qEh;G0|aMQu9I#f9W;1ja_wGjLj{Hj6(=-RCBgq~#1oN> zQY%lPavQHsiQMaC9COF0zD1~$>ZBL1VV`B^72jVRX4mmr>30oW_=PWAIB*yC%58I> zQP!f40@d;{K4~^06Lq#ZFX{)m(bsLD{FAag`-c4dY~lN&j?ne&%&FqN7$)$ffO_uF z$$O*`c_l27()0L&fB!!8yB__HDEsEW$B-YMdxEu7eR8SflK@QQ`8GHr;`pKJ+7Azo z&rA^>7Wc~?bGJbkr6>;;q3fo;i!aHkxk358`0nH7@9tx-&$Y=P$9E5j?<7|IIlkLR z-$ADfaSQn@kU_in^L6n|NREl+AO`$)85MPn6qzpS?WShp}5FPr=D^lVh`R zZ94qwWwWbU2J5WiR0IhG?fijTpKG!iybk$&t{Rhde@)sI2s(j6SRydKb8~mg4+Fj@ z;UAe`0E+wG<$D)Wxyzx$hr<^l=QY}2D$8>pksqyWPkQzVT)fm-;y)G4E0W0nXe@ zvP@=~x1%N<2&8M|x23aL)E^3kBD4;V$@;n7@{@qwBW2jVMIM~{wEPhGzfS(VCO_g^ ztyQGI19<=*gTurFWkxmr=;0`(gBnO-Dy&-Zc;eeh_0F|Vf}bDrqyi|S=JeVewN-7^ zxtKfPw%1nIxNY(~nnKmp3#uUTwY^F1oV!8Ziw8Yi9^c1g%iNUwJjR#i;|o!;akkt6 zKW}YURX`=$T7rrUn;Ym(1l2k$I}=FMTt*_UUG{Pl7H1Fg zpRbRZtu|Yzwk~zP;;rp>nyRX-4y!-h8eIwbW$sRP=iGnG4G=?@QlH{nr9gHG>D@_? zAYH9|HWpl-raDuAwD5wi_BMfo)QNazchFD zT=U#li8JHl7iSrD#83hSJ(GFFBAKIfmQi1o`3{Re1MOslqS-DHeQ{_qVx$CB1d=bYtVs)J!Zn9kvIC>pTG+&A)i<-0Hax&VBjx_c(AF zD}4MtzIJ|#Q{Oku?U=h^?rv<;Acq0S8lWwB2wKn;LbpZ`*MkQx-c&4Z??$^DSYYlp z)Gj=Yc1YP1!u_`5Yiwf=!RIRGN82;OO%w!t=sWq*xmy)CVD?1$yYI@6&Gjl?eD~z+ z4*&^j`##h>*ucMgs{Gxv`0fh+-DBmp`{f(vURIjW_UZB(?3QnyyBGHvBuC*GIln}s zci-%Jf8gL%@{{Lmk1ho=;F2(MiVAp7X&?QJ-vf|W;?LYq&cD|GEe1(dm@om2obq0E?;t6?Qb*!nU4{;O4 z19I!!hvd7V>zORryzmI9!WkkjM~$}e$w$-R$Vp=>Q>|EDj9obk(D6!KV@sm0w7q

T@AFX;4`n_+OelHZI5>>5R5Tv+!Q3ahR)}YMXc5-RP*Yo2(AAeA&3O#%gJ3 zw4Lp!u~?j5T4U)6_7xVBKXfws;5>#tJmGDys8yCaw?YPGU&X4G8|Qw@c0aFGGGV)kGM$h&XVO)2C%b~qyHoE~l3A}R7UjLztI`v) znN1Q8RN|8$VgH}zz6Cz&;@bZ=Gs%WL33&nuuwjz`$!4?JWRndfi#mUZI;^O}#lAlJoeRi&j9N$&=5VG>6LqKY#<>2Xb!7Np4r|F{Yv=b< z%fW*X5M-zET{nF03uPnc5jmcHW>3UNwM?A2onCn23_oH=`QPCyu(^Ca`enjh&~G|E87J3#7UL1~X!QO6UP0H^ZQESTd@Nz(&vY7T-mM+f^ zWYF789qlGQX`%c>7v_+iz*4ojLcY>ObU|^fiQ^?)i3>0A&=oJbTq}yCOfZ?ko{#~7 zOXWAw(NJ>b@oUSZ{1RK%#HsOSLqTDc<44m9rsgJ3SXCUAH!-(F)V~dyyV>Ru<)wx?THk;|j^;7rl$5H~)_0Te0%eou(&D3l{gBe14Z4CmfC!%q$ zHD6KiPhmxT@@n)d*64{zS3X!9&>Yr*syv zVej&#*D;ar-~YqZDH(kILa`Fgm@3yX;CYDr1>;b}K8N?;LlrCY>8_b~aFwC+cKDqx z`gjcXd06f8_xo`=e3>t1d&4|m7l6*Zt>A{K0AA+r9#2Txg*x&eIE>etgmvTaDKae) z8(4l7g@20_!zWU(goM}d%&?;!i|ZR#&vN8o5BVogW(UA&O^wKo4^ju$j@Zj~9Tj^uls(rHnvdlAd0|{bXrtHG`@Y64xALAQ+{3_-o zvg8t86=|19kNP9LDGzrrrNEm~CQh0_H*k+C%gZe-oSd3Ad2(8+=+k_jc#b^IivH%c zpK_J(@+V@CO&-7O;*Z6DcJYTbsW@MN4Aa>)FmVj)$8PX?oRtxxUoSRNm!Ns%eA;Db z<|#e~wqwE$$z=z)fXB5s$GDfJjpW~TVL;{&$oxK>vz%l3$lrqP6PM#gZJVT!AHM>R z0nj|b=5U&EoaT}92eexy&8=ZsH-Y9?ESJ+@8jE2&z^6^pw1sIlgXS=N1T?Xn<}S4O zeo3=GOoP3IHiKOU8Z10OGkpHIwqDY#kD$SR;~OJ1e8(~*X@(+cPO9u8FiZpY7S8!ppK0M|LvR{tpZc-wN`JX{OleJuZTU?K8& zKShW;0z{~_Ash>fE|E5tCLHHuRv04t=Q5;6f=9(uBhtgNEufp8ulJ1hSN;O$upxI%>-9}B##tG@ukI*bj!Ou1z69t*`t z%lMLTsXoi);Bx0ESb~tl0syDyHfjLO25^7je#K>V0k|)5n{%HC+j2ATt$+Za2f+Kl zHGnOEgMb|>{C`Eqb&FOIkC4mpdBDd+uJ03oM^yMNgfW1l3Qi)Nr0{S%-hlWi0H@_? z+}}74&TkXo6@VSU*9zAw;C2fF_}Dfdz~@emb6axYY25C-ZEjV-{gJmrJ>V$-ukTyY zXS5=$w9LS@%K(?d0pKBTKi-Zt0PY7VfOG(tISDWW!1c4KIF~sQz{ibJ0MF-ixd6`Z z0Dz}+--;Y_S0T=I2&eIUUPl;sI?w08{f6t#>sbikwlD&C-MpRzz(fEqn+@Rkyl!50 z4uH$U`w9BU-X5syVhkT6Z{`M7x^ZIxnHx!To;C1kEf{z(L2HXVTa$FrA ze=r8!2B-$yf%Na-?ReV&zXssW%JXjlh5)|-1OOqxPQWk#*R0Run2_J-etrXhrys$o z{apxO0B}Cf0-gkX6TtI1{T={L3IzNT;T?dd0i3>5g_{vR0`P<1A%xo@8z0M$0Jsj< zs5sZ@1;7sh#{j1RzXQAu@B#J$9s+Cw@VV}%fI|Sz4?GATllXYdX}<&DH0=OBmj4;> z41lNc5Jx@dcZ&aO&-a}lIKS!qX6%^7zuXC{-vT@p=G0be?Kpins6D71)n3wmt9^i5 zp0Y_fagwE^iTKHOatpbKJV>4;C+RqvMw{uQ_=@-o^i_J6zDqx51+1HGWxLn`c8I;m zPO-D>UG@oHoS7YEj&epdM0H0EM%^BDU(};f$D&?}IurGN^tkBh(U$1h(Sy;C8K@!2 zFxilAuo>nW>I}OKFB@Jryk+*do}J%+&gg}#cS~i@on)v z@!R5$#lH~$YW$h_4-;s@!~{!%JK?H?^$9x@?o7Bp;jx705>6$&lW;yUEzy$bPOM98 zNem?(NPIByc+$kA+$3vKZPL1={_(Zrw~c>3*^=Cyyft}m^1L z)e~AK^iS9|;lPAL6An)}GU54@*(uJHmX!XKT`31r4y7DUIg;{r%7-cEQ{z%|Q?030 zsS8q@Qae+(rtV9>ak4ve zW#;)3GT+NQH!*4=E?iA~W#Vrqbxztg>BUK>CY_!1?xas9 z8z!er-ZlBnEPvMatf8zsv);^lKkJihLv}{?^lV4=g6ylZL)k;wcV$12t!F=*eKPyC z>^HLCo1#rgn_`@@Ys!Hs_fC0is$uHcsqaqxcp9CSHqAKAJgt4&=4rd9-8t?4X^%}i zHSO%Qcc*=9j4~!0bB$JGwQ-5D!PsW(G43?(Gu~x9WjvdcoRgiiD5pDTd(KeKojIp+ z&Q9;1zIFOzCbP+5I%GO*I$}CuddYM;w>j6JyE*sS+>^Pl<-U>oah@SBJI|K4Aa8YE zOJ0B8P~M$+_vby9_gvoTy!Y~H{=|Gs{_OnP{QCU%{H^)>^54rpHzR7s*@S{*wHX`)y6O9Hv-GozT;!Z`AS1&Qe{Tv)XMxyOJ#ZG+{)U@ zrIo8Ik5-e}RXtUoUXgS~{}nIKZJ8UKyKC;y+}Gz7%(Km#J@44O zH|Kp+J-NENdRz7V)$i9#uPLi(u6fUyovE* zo#HNYSG$|so81T9hulZq&$>^$KlV^hhNr-@-gCtByyv~zwA$IV2Wp>-{GUI4{-Fi4 z7u>UO;==bAtzPuvqSqI@7w=xYZ}IC(GM2Pox&6uym(E@~xb)nzre*goJGt!jW$)JI z)|u;8)^*qIt20t*HGQCvf=iI_ZrSO#x+_Ss~VR!Zf!i+ z_+aCS#@8C(ZPJ=Dn#@hkrgcrhropCrn~pZU)bvKv$7_?;=B}N+wtj8r+TCjpuYG>) z8*9(4%UUr;y*3+%$+ZMDnwT0RawVm=&pVhb0x6gOn_fET^y}o^4`wQ*wb)HD(kwcYg^ZST~BqL?)q?j()!%>v)5m> z{=oW^>(6e8+EBJ((T4RK_HKA!!*d%>Z#eHa`knrM|NZ`#{pUB%+*rSH*T$zdp6xbt zTe_RNw{_p!{Yv+{0Ye}=FgLI+uq|+BKo7ju6V)@jXJyZ}o(FqQ^qk#9H%;H<+|;mX z_ofFoy&Tkn=HTk!VDL!rOm9MOb?^G#TY8`BeKTYTSwc%g{h>QUCqifYXy4R6cVBbg zfxcsXZ}w;RFX#{T-`D>_|2qRI17!oN2W}sDe&B=6ySGf-a?6%Cww7-VZ9TO0sja8B z{`Q)vYb@8izpZ)OzH6sm`{4G1?d!K6+5Xy&s2x>1ZrO2W=fRzCUuU?^aow@&-rVKb zwQJX_yUtv1xc<4_wYyt(ckg~{_lJ9??^(U4f6uNx2lgD^qwhJs=Y>6|_q?&!us45i z?cUJd?R#I?`^pVTH)P+i=!Vb@58v?G4Q~x54^AF*47LyM8oY1t;lZPWCk9Uro*Fze zcF;s-cFV^+Um-2Zr>aSBBodDd{HTO?5Z*+@#<1>b|6X zj(v;vwe8!v@8tft{b~F2_t)+B??1f%<^5;(pF2=?p#Q+&fkOu#K5+cN%LmRL_~2%G zbK1@MH#=@#h?$t5Tfa!oX$9IkJVZpbPf3z?6V{~8M1FS!KMkKu=8BL{+fA~B_>D)5 z=YtqmfN(djz;XAq=2v(Wu6qwDJQ~;KzNT;k(jQj%I6N`(J%z_=x!UUrkH zGi0<`t>wm*oj&8j?$$teZ@}*jd5v|!zI?*XjWat#p`PB^#l;<6q0YV* zOKV_baqE_#&+89u>G2gu6rB};*ZVs9{NA9=VlA=FaW&R^=0s!`soaPpqe@!s3-)#e zx{ab>M1HtPy|<^+xWw1rj4z#H@#k>KxY^>z39+KheOZN8u}gce${aEWnQkFQ%2FOdXhW7u3JmJ$nE zacf%g@yqFRyv>+&rcv_Ql455ZzJZV#m{|?l{Df+Y)%_4YiL~5&$zz3dwrtiye7ST%=wN{kB?#w0$40Fl&-u)`OvxhTMwY#Fy~Z(At5Jq)*(L14tro8F z(&fk2QV%(MUMff_c7mf-Ce zZWq}q0~-Igh#JC@gj>gmJ(KY=^pWjmK|Mi8c`5pKAwvk(vVdY#n{XZfyV7vGbC2Xw z1%J3Zdg)e!*kN1NX(V|93EPI_1 zX}sPVZ8<{Ti)SOe*!sQDg^vb3$mRCn9_~jdTeleU*FulKr;NIOePKEJidYTsiFNdk zy@*8Im5y00SNjQJBnofMF_0KC4(BAXB#y+B1d>RSv=%ZRCj-f30!bmMc*-_Sdkiym zx|XNqYk$=);3`lC$;2y+W@vvPlgMO}MY1sx`N$M96?Yk)Bu0`$rV|s%C3)IQcrVWk zoI%Vavq%9c#H;9wh=mm6>D>bD9I=uTVk4!bOe-XIoD@_L2YTqsq*D7CnT^xvIocZfHtkiiO8W&_ji;I$NF&Z7)@mJO9r+5mO8X_bnluwHX(6qojrd4A=^&k?i>xOb zaGLGc9w!?Ko^952JoojVcngP}^pH(>D?~2|;W?>(ocs=u&DvXJi}n|?m0W|j`d^E4 zjvZttuC-l9c9HAJZnB5$)g0Puk^STVxf!P-w~~W+=IC~E z2f34cmE5IO;>yO|pNPriv0uLrcZ@v(=)%ytZL*!xd2ziuzN83OiBaaiE43i_|3C&NAYU|09sV$usb@ z^F+hR&$GC{^907hcggp0`_6OZ2Y5=NhdfVyh^M%Nm@(McW*Y4 zpOII{&&jLg7vvQAC3%hfikwFOTZ9v&U*oCd-;m#uf7TY0Gvs&VEcqAs^RKlKc|+?b z|B7o}Z<62R3DiFFZ{%(A@8liw2klDoF8L4KJ@6j+Ph2;6pZpj3fc%ksNd6mVP`}se z$e(bBz{li&$T{+7@(J$y`&8SEk$MaHD^9e|lM8Ta+*F6#kGA1Syl86Bw$d1!FvZe1 z8c!2&(w#)d*)%tw9~Y7Jja`{NbDUrPf}Wzkq_5Fm(bM$T^mY0h`dj+X z^bGwSPB#CAzCr($zDa*i-=hCU-`1X{|4!ebf53_7f6({nf718qf6))u^O3%{^xWAa+RE{qv2&slFepwSQWd1&1LggHLGDx=3;K?`alb~S5eUe>}|SsU}QcGkftUN%5btq_)dq2=*G<@I?!&!uS8MNRyI8OGJPWZtt%mip0k)ZK!5Q&2Y#Y0lZD%{! zPIetmjIU?A*&eo+-M|LfjckbB#P+fMI7hyj-NJ5V2ia}xc6JB5lYN!l#qP%G@;|YA z*w@&-?Cb14_6>GF`zCvUeTyAt-)0Z8huFjH5%wti4ttC}&U7}+j<6@#QT8NzihY+I zV^6c=xbN+swSU#lVuyPG?_qXn|AN!*H?-erzhTedy@uaoC)oGdbL}Tv1_VcKvtClP=)U*VB{l1vK?yeF~jYo#gaur%tXj5UC3~R!n zOT`s^jYGw2cxc5oRmH=h;DdBqOwC4bYcS9qQxoV2bo(~M)dah`JG`xZAzw_5M}|Tq zt0Ugk+7)c=+t}{+ZH{xb1w!6d?6N`zcdHi+5Cj9>kgUh5fiOO{< zEnP}#my*tD!swV=ftlzeP8@e4)^sH-yifC9E>hibw;uno_PFNfv60gQuQYp)qxGD@wMEe+)2u%`}M36=;f%Hx@Bvl*7k@=Im;r4i+2(K6*;W#!D%-J#MPhGipKmU!H%&D=^`w`x1L zD(aTC+oVTWZ8p_lrQx<#zTomiFT-g`x2(jiWb}l&DJr+JrN^4Mtg{b(8tmKX_x1@* zVNp-avPdVetI~E+mlfSAep#>I+uIr8)@6!wncxihT!wmSm3nEF`Y~3q+Deq@cBM*5 zxEhaPMFi(^Wr6bY_!SWix?&8!5>L6}SFZS!D^pfVkxCr3Q7hqihLu7fZXj!kq=hWX zP~}REa*tuvNb4Y5$>0fx<YgMF`D{5j^N&k&r z#m5fAD&fVlAuAk)HNqzGYesDlyQZzn7xeXZ^%~Z61ik&f_{PypS- zt@5T)WvW_-p-HfhYZ}vAD{X9*rOV(QF$P+{Rx)u`sIWwZr7E<`uqGV3Rb0{6RI0d> zhj2Glz9t+BK1eT(@hSs()u8Gf9aO!(5g)PIWM3}vSS#aOM@vTX=2RLw!x~n^_#&ISM6oQf$M{A^ zRj8pH$02Cct23rcDbuCK+pZB$wc1K6;@6MXm9QZ~$b=0Y zL7%VN@9l2uYBl&p`x*Q~u>^kvU6dcn$jVEFr7-9!@k*62Wu=C0S-xAA?~W)hikwLf85ExM!gRW{)x8m+rP3u-g-LiI@bQlb3sy$1?uAp4SRT~``9S+<|Mo*ZV zqH-&T@{}Y7K06#(YhwbDK4Mp;?V>IWy3Y8(X9iuxxlC||d~QQfS|up05*%X{jATl5 zyHce>g(czY6st0~p?9Q%VQf_vDX)m{jcDE8G5kTO_?Ihw<&{w(KIn#oK)huz=!!hZ zq6}53x5Gf!<|-)F7Odtjusxf#^p z+b0I!z7dx$@l;eA281o*2S#;|9T*vW2jt+pWi(TIphGoyt=+ICa`07NRjN!>TWQ!T z*vD-h(}^ofS)ZjVCTQvP1-sfgQ=7FyFtwG~%3`F%eW zt6$Oec5Rk)y;zTTOVr0N_ejF-F2zvR(cQN(DC!UhuY)714vxw?B*yCybi58hC+iTX ztV0lrIyk0mC+uRi%JCVYunR(EmkMQ<3T2lHWtR$NmkMQYDigx?+2{>PrEMiLlaDSwzi%T9Dw$L`$N4t)gtmaKNGP#MC$L%NnhBD}@s95I z_4xaG$9pjx%IQy-KE@ji1_t_iWYNlsI1v{YIaQJ)RwT6r2E-a2a#YF`zpq`@SXnMp zS^}X?iS_lgDQb_jbBSDY+Dhc`hfqrClv3K9mC}Wss$K9Neo?<&4GDHNB-k+|v~{;& z!r!3s!*r6L-BA&(a(g1EDrHBp%Ms3Im&;6q?ihc21f4^+w%sAU!tSVy35Fu_m6Y~M zRhyg%uFs6Gt?f1yms2?Cl{|8Fg1Dk93+oe>Up1-R#^EV> zCBIxX*z9r%V6)5l2=XZTup$w1$n`nsRHIZV`Bay-t1fNF21e4W`jqS0E0mma?FRm; z5!Fm)m#bl$9qWBjzD(uI)fV&$rz^fTC4Z@kmxkkVmb1%^IrLQWl&SLNN^ZH#K>H~D zDpWhkEiB3@I=o3h$Wx*CRw(h7+^@a6S{B2=66)#i%CJ#=~zA9d-^pmStq^tHR3x|r2+&{x_ z6+gKffqj&{N|iinfV9g!G}>43muE+yS9EeMkM>mUqDEl5P3fVgA-p77+FdSDQNJo* zuKYso*^wUZAF8}9EKgWIm2XpiA~$wucU8Y!s)CP{x4g76ws&I}hGB23FQKcueip)n6)9yHqGWDwJPWsCFw+bR{L_vA*6A_E4Ck z1&1=_;iV2|bZ4Ni*C$f3(H0@TBcwv9KVC1R;!>|t#lO_$F>LJW#(|@u*N3}k1n=6~ zT6eOTqvYaLOi~#U(|W|@o+6VY=k^p8iJ(gM`+9q?mg@j9|Bn|5qU^|ez*;Ju*jlRA z0j1W8Sg{1ghM^@^tadrZ6#-=1g^wh{2fpE5z1-y^Ukm~WrS)v`Y}sma31>toTf^pZ#0h;w-Em^KCQ;Fn0!<8W zkVGtLWN#zsgmDBqA#!^rQeycQOp*yj1R5_5T@GWHMBvs4yfOkWRXC23M(8;nrssH= zp5vv-lE0uA6-Fdj$0k&cO(+!!xbqH=^y6CIm-}0StGKwK7w?YM@J5NB;3-EzcHMT@_!vEzZ2K>Wcii2rYZk%soiYUwN(wCMx$}@ zF)eBNV%@N!sX;Hx(q}d{w;P96HRv?Y`}8C|j&GwRRP=~4MjM`n_Em#5XE8$1m;I?HRUU)zvl%E=mPFzR)6NUCnkGU^VF zIT{;{!;;$Drq4u@iWzk)&$n{Q=js}akY>nh)Z^+Jnvr7Ud2t*o=U91jRx@5fp9N|4 zI9IE#)i>zcVonSy7v8Nqm19#Edyge+t(@XmwARwt*ye52NnvB7N!RKce2tAoIx`zR zMm;Le3tggJbq#v7$*CJmPG}C&<{~}HY=S1nw&Ca&r;+Dy9kZm&{8x`^_O$A(AP1Q) zBW^-P1;f_pJeXy9Lvvl0x4yB#)R@y~)T>uCATNs>L)BlTN1OE+SK$%dIU;OpK-AjJq923gQyA6B}!`n352a~Zsa7*ZXxP8^O2YY&E;_xBi<4YPAw+=edCf-8kjcwiQdn5a;;oy}l-U#7U8i&7{&qFQ93 z1nzVu=aKk02KUG^BQ^Q#}{%hhc`Kg6ZuaPesq$7mSA~x4a3~^VJznm+85O( z737#uER4e&cs?(J&ckyXp~-y6KOdMqA z9%h<{84o9#hog8n$vhm*!^wDWDG#&E!!bO}HV=>E;S}?5EDvXyVFhV--OvmRn~WBn ze1&(ABE29YGh-xklguoP$jl$f49d)@W=&5l{5$lU2K}Z&E+h2gVGi`;;dJQ7LlgAl zVJ`IJVIK73VLtTZ;SA`Jhxk}#mFPZ2W|%dl+2}&sHgjL_BGk>?S1o3}s8BCL zFDym}m=90AWH&T<9VR}8el7*vut>Ltn|3%M&ci)TFD@94Ch49AjDNfpOCrp3IU1YU zXcv-~f}7;y`Rw3;G3S!f@^mfZsF0!*QBZk98ILhAbwdv1$lBD=#K9S|mR`%H#}F8}(*BmQ*iqc!C<4G3yDM&n7oI z`S>~xLpAD$1g2W_Y_#^p-4tUADU=fsbv3t{bmsE5VThwHZx-<8MvMz%%XlFgMtxJQ z*O6sH4Ykk{fv5%iE>Q^t;RA6L1`jkwG#n1|#u!P!(Tl=)LJ0g~UgIO^XkBoY*v8=JQH=H+{AM+ept8_YxN1PhPo`wq{ga7 z%dmx{q2JECD7QYV?xNi4i*mzdBl(RjQDfE}g^>i|Lg5OXX1%g-2=2#yYzRI5aQd-XVzQ>xD0*@T{ELC=#4xiK(dj^W-fJpXTV>G_v(u)oKPxxM)~ zR%LQzPPNS03g8|r47`93+VwHtCyv?{>lr|$Km=6=A%WjV~33_;{MK41yUvQc9 zg84QtE(+ zl;xbpgRFW^!;ux7h9fIE4M$d)pTKBX18g-gBCs{)CvY1lPip`s(;7J);b~2rPGD;} zoxs*{I)QzK*XaV;RlH7)T+Qp`NHedKBVJBZ3#5h9aHN&faHNgXaKtC%asq1?a&fFf z$i=ZvAs5HGgj^h3FXZCb1|b*6{6a2{ZG??yk2GMni0N~Y5s>ISpgr8^oJ_AqY!l{M zpqwTsQBKn)jJJ0;2qu9GMy*ag}vM)+MXVuIgpiE{Ei66NH3QSu7O?*@r- zfEoitsf7936^0@MzN*O+;+@}{urqOI?9S+&{OdPJ8Sng_R;~U; z3e()fxnyuTb_9bB!>r9coX=y&$L+*ZU)6)HSQdi>8>YtUxcORzVTK{o5S37Hj9l2G WNA1Vt?0zD;&7is6TxR$-|M@>8AH|OV literal 0 HcmV?d00001 diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index 035684761374fef7499e402fd28b70ac1e2d168a..74688fdb0d13dd705a56b900ffe4f049458b41e5 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -6,7 +6,7 @@ extends = "_base" 2 = "#131415" [text] -base = { family = "Helvetica", size = 14.0 } +base = { family = "Inconsolata", size = 14 } 0 = { extends = "$text.base", color = "#ffffff" } 1 = { extends = "$text.base", color = "#b3b3b3" } 2 = { extends = "$text.base", color = "#7b7d80" } diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml index e70a088cc930d468f02234f23155fafae75bcc3e..94274997a14e49b7bfd90a41eee2491c8a095e4e 100644 --- a/zed/assets/themes/light.toml +++ b/zed/assets/themes/light.toml @@ -7,7 +7,7 @@ extends = "_base" 3 = "#3a3b3c" [text] -base = { family = "Helvetica", size = 14.0 } +base = { family = "Inconsolata", size = 14 } 0 = { extends = "$text.base", color = "#acacac" } 1 = { extends = "$text.base", color = "#111111" } 2 = { extends = "$text.base", color = "#333333" } diff --git a/zed/src/assets.rs b/zed/src/assets.rs index e7c0103421620b6888ee2dd28b723ed22d4daa11..c0f3a1fbfc4726ef0cdbe0734518b3b3737c691e 100644 --- a/zed/src/assets.rs +++ b/zed/src/assets.rs @@ -4,11 +4,14 @@ use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "assets"] +#[exclude = "*.DS_Store"] pub struct Assets; impl AssetSource for Assets { fn load(&self, path: &str) -> Result> { - Self::get(path).ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) + Self::get(path) + .map(|f| f.data) + .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) } fn list(&self, path: &str) -> Vec> { diff --git a/zed/src/language.rs b/zed/src/language.rs index f6342bf254af83bcd020a73e9d6e9178bbb2d3ec..bcea6c25adf3122f24976293c7efdc4c4d222ace 100644 --- a/zed/src/language.rs +++ b/zed/src/language.rs @@ -47,7 +47,8 @@ impl Language { impl LanguageRegistry { pub fn new() -> Self { let grammar = tree_sitter_rust::language(); - let rust_config = toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap()).unwrap(); + let rust_config = + toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap(); let rust_language = Language { config: rust_config, grammar, @@ -84,7 +85,7 @@ impl LanguageRegistry { fn load_query(grammar: tree_sitter::Language, path: &str) -> Query { Query::new( grammar, - str::from_utf8(LanguageDir::get(path).unwrap().as_ref()).unwrap(), + str::from_utf8(&LanguageDir::get(path).unwrap().data).unwrap(), ) .unwrap() } diff --git a/zed/src/main.rs b/zed/src/main.rs index d1361870b808eb3fe32944a01d53629f605f796a..3603e760eb07f06e899dab538126ea49df44eb08 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -2,12 +2,14 @@ #![allow(non_snake_case)] use fs::OpenOptions; +use gpui::AssetSource; use log::LevelFilter; use parking_lot::Mutex; use simplelog::SimpleLogger; use std::{fs, path::PathBuf, sync::Arc}; use zed::{ - self, assets, + self, + assets::Assets, channel::ChannelList, chat_panel, editor, file_finder, fs::RealFs, @@ -20,9 +22,15 @@ use zed::{ fn main() { init_logger(); - let app = gpui::App::new(assets::Assets).unwrap(); + let app = gpui::App::new(Assets).unwrap(); + let embedded_fonts = Assets + .list("fonts") + .into_iter() + .map(|f| Arc::new(Assets.load(&f).unwrap().to_vec())) + .collect(); + app.platform().fonts().add_fonts(embedded_fonts).unwrap(); - let themes = settings::ThemeRegistry::new(assets::Assets, app.font_cache()); + let themes = settings::ThemeRegistry::new(Assets, app.font_cache()); let (settings_tx, settings) = settings::channel(&app.font_cache(), &themes).unwrap(); let languages = Arc::new(language::LanguageRegistry::new()); languages.set_theme(&settings.borrow().theme.syntax); diff --git a/zed/src/settings.rs b/zed/src/settings.rs index 1cadfdaa9c1f8fa7bc93449f590551ce0ebfe728..6b7defe1361db50a6ca818a55dd88841c2bc4dc0 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -37,8 +37,8 @@ impl Settings { pub fn new(font_cache: &FontCache, theme: Arc) -> Result { Ok(Self { - buffer_font_family: font_cache.load_family(&["Fira Code", "Monaco"])?, - buffer_font_size: 14.0, + buffer_font_family: font_cache.load_family(&["Inconsolata"])?, + buffer_font_size: 16., tab_size: 4, theme, }) From cff6ffb716aa2f17f086dfc209b0799aa7b679ab Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 4 Sep 2021 16:27:38 -0700 Subject: [PATCH 182/204] Avoid invalid lifecycle state for flex element when total child flex is zero --- gpui/src/elements/flex.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gpui/src/elements/flex.rs b/gpui/src/elements/flex.rs index 73f3a863730a8a07a3542cf44bffbad637a2cc73..e2bd7eb1c97f8838d5acaf348fda08a748b15e54 100644 --- a/gpui/src/elements/flex.rs +++ b/gpui/src/elements/flex.rs @@ -90,14 +90,14 @@ impl Element for Flex { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let mut total_flex = 0.0; + let mut total_flex = None; let mut fixed_space = 0.0; let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; for child in &mut self.children { if let Some(metadata) = child.metadata::() { - total_flex += metadata.flex; + *total_flex.get_or_insert(0.) += metadata.flex; } else { let child_constraint = match self.axis { Axis::Horizontal => SizeConstraint::new( @@ -115,13 +115,12 @@ impl Element for Flex { } } - let mut size = if total_flex > 0.0 { + let mut size = if let Some(mut remaining_flex) = total_flex { if constraint.max_along(self.axis).is_infinite() { panic!("flex contains flexible children but has an infinite constraint along the flex axis"); } let mut remaining_space = constraint.max_along(self.axis) - fixed_space; - let mut remaining_flex = total_flex; self.layout_flex_children( false, constraint, From cb62d53b4999713c52109e620d27d7dcb943c83f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 6 Sep 2021 10:40:19 +0200 Subject: [PATCH 183/204] Return errors instead of panicking when interacting with the keychain Closes #134 --- gpui/src/platform.rs | 5 +++-- gpui/src/platform/mac/platform.rs | 24 +++++++++++++----------- gpui/src/platform/test.rs | 9 ++++++--- zed/src/rpc.rs | 11 +++++++++-- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 2aad986a667164c8f3342769c5ca463cba288eb3..078bf7970560c1c4eca8f99517ddf43526f4f5c3 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -18,6 +18,7 @@ use crate::{ text_layout::LineLayout, AnyAction, ClipboardItem, Menu, Scene, }; +use anyhow::Result; use async_task::Runnable; pub use event::Event; use std::{ @@ -46,8 +47,8 @@ pub trait Platform: Send + Sync { fn read_from_clipboard(&self) -> Option; fn open_url(&self, url: &str); - fn write_credentials(&self, url: &str, username: &str, password: &[u8]); - fn read_credentials(&self, url: &str) -> Option<(String, Vec)>; + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>; + fn read_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 4d81fe964a0b80584527a7cce9936a7b133a4fab..7015cbc713cecc528e742fe902a86c840c25cfd1 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -5,6 +5,7 @@ use crate::{ platform::{self, CursorStyle}, AnyAction, ClipboardItem, Event, Menu, MenuItem, }; +use anyhow::{anyhow, Result}; use block::ConcreteBlock; use cocoa::{ appkit::{ @@ -469,7 +470,7 @@ impl platform::Platform for MacPlatform { } } - fn write_credentials(&self, url: &str, username: &str, password: &[u8]) { + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> { let url = CFString::from(url); let username = CFString::from(username); let password = CFData::from_buffer(password); @@ -502,12 +503,13 @@ impl platform::Platform for MacPlatform { } if status != errSecSuccess { - panic!("{} password failed: {}", verb, status); + return Err(anyhow!("{} password failed: {}", verb, status)); } } + Ok(()) } - fn read_credentials(&self, url: &str) -> Option<(String, Vec)> { + fn read_credentials(&self, url: &str) -> Result)>> { let url = CFString::from(url); let cf_true = CFBoolean::true_value().as_CFTypeRef(); @@ -525,27 +527,27 @@ impl platform::Platform for MacPlatform { let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result); match status { security::errSecSuccess => {} - security::errSecItemNotFound | security::errSecUserCanceled => return None, - _ => panic!("reading password failed: {}", status), + security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None), + _ => return Err(anyhow!("reading password failed: {}", status)), } let result = CFType::wrap_under_create_rule(result) .downcast::() - .expect("keychain item was not a dictionary"); + .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?; let username = result .find(kSecAttrAccount as *const _) - .expect("account was missing from keychain item"); + .ok_or_else(|| anyhow!("account was missing from keychain item"))?; let username = CFType::wrap_under_get_rule(*username) .downcast::() - .expect("account was not a string"); + .ok_or_else(|| anyhow!("account was not a string"))?; let password = result .find(kSecValueData as *const _) - .expect("password was missing from keychain item"); + .ok_or_else(|| anyhow!("password was missing from keychain item"))?; let password = CFType::wrap_under_get_rule(*password) .downcast::() - .expect("password was not a string"); + .ok_or_else(|| anyhow!("password was not a string"))?; - Some((username.to_string(), password.bytes().to_vec())) + Ok(Some((username.to_string(), password.bytes().to_vec()))) } } diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 004b1f94a9ba86b4ce4aad710828b42bcae73023..85afff49994607fc2c16fd9705f5c207f6a6969e 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -1,5 +1,6 @@ use super::CursorStyle; use crate::{AnyAction, ClipboardItem}; +use anyhow::Result; use parking_lot::Mutex; use pathfinder_geometry::vector::Vector2F; use std::{ @@ -128,10 +129,12 @@ impl super::Platform for Platform { fn open_url(&self, _: &str) {} - fn write_credentials(&self, _: &str, _: &str, _: &[u8]) {} + fn write_credentials(&self, _: &str, _: &str, _: &[u8]) -> Result<()> { + Ok(()) + } - fn read_credentials(&self, _: &str) -> Option<(String, Vec)> { - None + fn read_credentials(&self, _: &str) -> Result)>> { + Ok(None) } fn set_cursor_style(&self, style: CursorStyle) { diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index ee1ff2fbb6ae8306f6ef65c4730b1cf6afcac17d..6ec07c90f6470e772cff722146a1fcdc1ab95fa1 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -1,3 +1,4 @@ +use crate::util::ResultExt; use anyhow::{anyhow, Context, Result}; use async_tungstenite::tungstenite::http::Request; use async_tungstenite::tungstenite::{Error as WebSocketError, Message as WebSocketMessage}; @@ -236,7 +237,11 @@ impl Client { ) -> Task> { let executor = executor.clone(); executor.clone().spawn(async move { - if let Some((user_id, access_token)) = platform.read_credentials(&ZED_SERVER_URL) { + if let Some((user_id, access_token)) = platform + .read_credentials(&ZED_SERVER_URL) + .log_err() + .flatten() + { log::info!("already signed in. user_id: {}", user_id); return Ok((user_id, String::from_utf8(access_token).unwrap())); } @@ -301,7 +306,9 @@ 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()); + platform + .write_credentials(&ZED_SERVER_URL, &user_id, access_token.as_bytes()) + .log_err(); Ok((user_id.to_string(), access_token)) }) } From 77d157467937fa2ff55946002f0eb5285f3bd194 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 6 Sep 2021 12:50:04 +0200 Subject: [PATCH 184/204] Pass how many lines the editor should expand to in auto height mode --- zed/src/chat_panel.rs | 12 ++++-------- zed/src/editor.rs | 25 ++++++++++++++----------- zed/src/editor/element.rs | 9 +++++---- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 2b5570ca91d44a6c3ede872f43895f28f148110c..5f4f3e13c71be1c696b003ddd17663d9b3240504 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -47,7 +47,7 @@ impl ChatPanel { cx: &mut ViewContext, ) -> Self { let input_editor = cx.add_view(|cx| { - Editor::auto_height(settings.clone(), cx).with_style({ + Editor::auto_height(4, settings.clone(), cx).with_style({ let settings = settings.clone(); move |_| settings.borrow().theme.chat_panel.input_editor.as_editor() }) @@ -237,13 +237,9 @@ impl ChatPanel { fn render_input_box(&self) -> ElementBox { let theme = &self.settings.borrow().theme; - Container::new( - ConstrainedBox::new(ChildView::new(self.input_editor.id()).boxed()) - .with_max_height(100.) - .boxed(), - ) - .with_style(&theme.chat_panel.input_editor_container) - .boxed() + Container::new(ChildView::new(self.input_editor.id()).boxed()) + .with_style(&theme.chat_panel.input_editor_container) + .boxed() } fn render_channel_name( diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 38e9a3ac3e3606d8bf870785c35ea1e153c4cafc..39c588f8d1a7eab48cd81f56b4f4a5c1978a205d 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -272,9 +272,10 @@ pub enum SelectPhase { End, } -enum EditorMode { +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum EditorMode { SingleLine, - AutoHeight, + AutoHeight { max_lines: usize }, Full, } @@ -301,10 +302,9 @@ pub struct Editor { } pub struct Snapshot { + pub mode: EditorMode, pub display_snapshot: DisplayMapSnapshot, pub placeholder_text: Option>, - pub gutter_visible: bool, - pub auto_height: bool, pub theme: Arc, pub font_family: FamilyId, pub font_size: f32, @@ -332,10 +332,14 @@ impl Editor { view } - pub fn auto_height(settings: watch::Receiver, cx: &mut ViewContext) -> Self { + pub fn auto_height( + max_lines: usize, + settings: watch::Receiver, + 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); - view.mode = EditorMode::AutoHeight; + view.mode = EditorMode::AutoHeight { max_lines }; view } @@ -407,9 +411,8 @@ impl Editor { let settings = self.settings.borrow(); Snapshot { + mode: self.mode, display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), - gutter_visible: matches!(self.mode, EditorMode::Full), - auto_height: matches!(self.mode, EditorMode::AutoHeight), scroll_position: self.scroll_position, scroll_top_anchor: self.scroll_top_anchor.clone(), theme: settings.theme.clone(), @@ -464,7 +467,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut scroll_position = compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor); - let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight) { + let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.) } else { display_map.max_point().row().saturating_sub(1) as f32 @@ -496,7 +499,7 @@ impl Editor { .row() as f32 + 1.0; - let margin = if matches!(self.mode, EditorMode::AutoHeight) { + let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { 0. } else { ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0) @@ -760,7 +763,7 @@ impl Editor { fn newline(&mut self, Newline(insert_newline): &Newline, cx: &mut ViewContext) { match self.mode { EditorMode::SingleLine => cx.propagate_action(), - EditorMode::AutoHeight => { + EditorMode::AutoHeight { .. } => { if *insert_newline { self.insert(&Insert("\n".into()), cx); } else { diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index a14356819d840d22e64ea47a1676d2e02530b11e..791c6a5be07446e0d468591a13005d613a31748d 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -396,7 +396,7 @@ impl Element for EditorElement { let gutter_padding; let gutter_width; - if snapshot.gutter_visible { + 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) => { @@ -424,11 +424,12 @@ impl Element for EditorElement { }); let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height; - if snapshot.auto_height { + if let EditorMode::AutoHeight { max_lines } = snapshot.mode { size.set_y( scroll_height .min(constraint.max_along(Axis::Vertical)) - .max(constraint.min_along(Axis::Vertical)), + .max(constraint.min_along(Axis::Vertical)) + .min(line_height * max_lines as f32), ) } else if size.y().is_infinite() { size.set_y(scroll_height); @@ -485,7 +486,7 @@ impl Element for EditorElement { } }); - let line_number_layouts = if snapshot.gutter_visible { + let line_number_layouts = if snapshot.mode == EditorMode::Full { let settings = self .view .upgrade(cx.app) From ffc873252e19b31999a6ce9196d82f199b30318f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 6 Sep 2021 12:51:11 +0200 Subject: [PATCH 185/204] Rename `BufferView` to `Editor` in `ui_name` --- zed/src/editor.rs | 168 ++++++++++++++++++++++------------------------ 1 file changed, 80 insertions(+), 88 deletions(-) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 39c588f8d1a7eab48cd81f56b4f4a5c1978a205d..7d13628f8af0f743a02ce26819c8a1a121b9e93d 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -100,107 +100,99 @@ action!(Select, SelectPhase); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings(vec![ - Binding::new("escape", Cancel, Some("BufferView")), - Binding::new("backspace", Backspace, Some("BufferView")), - Binding::new("ctrl-h", Backspace, Some("BufferView")), - Binding::new("delete", Delete, Some("BufferView")), - Binding::new("ctrl-d", Delete, Some("BufferView")), - Binding::new("enter", Newline(false), Some("BufferView")), - Binding::new("alt-enter", Newline(true), Some("BufferView")), - Binding::new("tab", Insert("\t".into()), Some("BufferView")), - Binding::new("ctrl-shift-K", DeleteLine, Some("BufferView")), + Binding::new("escape", Cancel, Some("Editor")), + Binding::new("backspace", Backspace, Some("Editor")), + Binding::new("ctrl-h", Backspace, Some("Editor")), + Binding::new("delete", Delete, Some("Editor")), + Binding::new("ctrl-d", Delete, Some("Editor")), + Binding::new("enter", Newline(false), Some("Editor")), + Binding::new("alt-enter", Newline(true), Some("Editor")), + Binding::new("tab", Insert("\t".into()), Some("Editor")), + Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), Binding::new( "alt-backspace", DeleteToPreviousWordBoundary, - Some("BufferView"), + Some("Editor"), ), - Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("BufferView")), - Binding::new("alt-delete", DeleteToNextWordBoundary, Some("BufferView")), - Binding::new("alt-d", DeleteToNextWordBoundary, Some("BufferView")), - Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("BufferView")), - Binding::new("cmd-delete", DeleteToEndOfLine, Some("BufferView")), - Binding::new("ctrl-k", CutToEndOfLine, Some("BufferView")), - Binding::new("cmd-shift-D", DuplicateLine, Some("BufferView")), - Binding::new("ctrl-cmd-up", MoveLineUp, Some("BufferView")), - Binding::new("ctrl-cmd-down", MoveLineDown, Some("BufferView")), - Binding::new("cmd-x", Cut, Some("BufferView")), - Binding::new("cmd-c", Copy, Some("BufferView")), - Binding::new("cmd-v", Paste, Some("BufferView")), - Binding::new("cmd-z", Undo, Some("BufferView")), - Binding::new("cmd-shift-Z", Redo, Some("BufferView")), - Binding::new("up", MoveUp, Some("BufferView")), - Binding::new("down", MoveDown, Some("BufferView")), - Binding::new("left", MoveLeft, Some("BufferView")), - Binding::new("right", MoveRight, Some("BufferView")), - Binding::new("ctrl-p", MoveUp, Some("BufferView")), - Binding::new("ctrl-n", MoveDown, Some("BufferView")), - Binding::new("ctrl-b", MoveLeft, Some("BufferView")), - Binding::new("ctrl-f", MoveRight, Some("BufferView")), - Binding::new("alt-left", MoveToPreviousWordBoundary, Some("BufferView")), - Binding::new("alt-b", MoveToPreviousWordBoundary, Some("BufferView")), - Binding::new("alt-right", MoveToNextWordBoundary, Some("BufferView")), - Binding::new("alt-f", MoveToNextWordBoundary, Some("BufferView")), - Binding::new("cmd-left", MoveToBeginningOfLine, Some("BufferView")), - Binding::new("ctrl-a", MoveToBeginningOfLine, Some("BufferView")), - Binding::new("cmd-right", MoveToEndOfLine, Some("BufferView")), - Binding::new("ctrl-e", MoveToEndOfLine, Some("BufferView")), - Binding::new("cmd-up", MoveToBeginning, Some("BufferView")), - Binding::new("cmd-down", MoveToEnd, Some("BufferView")), - Binding::new("shift-up", SelectUp, Some("BufferView")), - Binding::new("ctrl-shift-P", SelectUp, Some("BufferView")), - Binding::new("shift-down", SelectDown, Some("BufferView")), - Binding::new("ctrl-shift-N", SelectDown, Some("BufferView")), - Binding::new("shift-left", SelectLeft, Some("BufferView")), - Binding::new("ctrl-shift-B", SelectLeft, Some("BufferView")), - Binding::new("shift-right", SelectRight, Some("BufferView")), - Binding::new("ctrl-shift-F", SelectRight, Some("BufferView")), + Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-delete", DeleteToNextWordBoundary, Some("Editor")), + Binding::new("alt-d", DeleteToNextWordBoundary, Some("Editor")), + Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")), + Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")), + Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")), + Binding::new("cmd-shift-D", DuplicateLine, Some("Editor")), + Binding::new("ctrl-cmd-up", MoveLineUp, Some("Editor")), + Binding::new("ctrl-cmd-down", MoveLineDown, Some("Editor")), + Binding::new("cmd-x", Cut, Some("Editor")), + Binding::new("cmd-c", Copy, Some("Editor")), + Binding::new("cmd-v", Paste, Some("Editor")), + Binding::new("cmd-z", Undo, Some("Editor")), + Binding::new("cmd-shift-Z", Redo, Some("Editor")), + Binding::new("up", MoveUp, Some("Editor")), + Binding::new("down", MoveDown, Some("Editor")), + Binding::new("left", MoveLeft, Some("Editor")), + Binding::new("right", MoveRight, Some("Editor")), + Binding::new("ctrl-p", MoveUp, Some("Editor")), + Binding::new("ctrl-n", MoveDown, Some("Editor")), + Binding::new("ctrl-b", MoveLeft, Some("Editor")), + Binding::new("ctrl-f", MoveRight, Some("Editor")), + Binding::new("alt-left", MoveToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-b", MoveToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-right", MoveToNextWordBoundary, Some("Editor")), + Binding::new("alt-f", MoveToNextWordBoundary, Some("Editor")), + Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")), + Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")), + Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")), + Binding::new("ctrl-e", MoveToEndOfLine, Some("Editor")), + Binding::new("cmd-up", MoveToBeginning, Some("Editor")), + Binding::new("cmd-down", MoveToEnd, Some("Editor")), + Binding::new("shift-up", SelectUp, Some("Editor")), + Binding::new("ctrl-shift-P", SelectUp, Some("Editor")), + Binding::new("shift-down", SelectDown, Some("Editor")), + Binding::new("ctrl-shift-N", SelectDown, Some("Editor")), + Binding::new("shift-left", SelectLeft, Some("Editor")), + Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")), + Binding::new("shift-right", SelectRight, Some("Editor")), + Binding::new("ctrl-shift-F", SelectRight, Some("Editor")), Binding::new( "alt-shift-left", SelectToPreviousWordBoundary, - Some("BufferView"), + Some("Editor"), ), - Binding::new( - "alt-shift-B", - SelectToPreviousWordBoundary, - Some("BufferView"), - ), - Binding::new( - "alt-shift-right", - SelectToNextWordBoundary, - Some("BufferView"), - ), - Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("BufferView")), + Binding::new("alt-shift-B", SelectToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-shift-right", SelectToNextWordBoundary, Some("Editor")), + Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("Editor")), Binding::new( "cmd-shift-left", SelectToBeginningOfLine(true), - Some("BufferView"), + Some("Editor"), ), Binding::new( "ctrl-shift-A", SelectToBeginningOfLine(true), - Some("BufferView"), + Some("Editor"), ), - Binding::new("cmd-shift-right", SelectToEndOfLine, Some("BufferView")), - Binding::new("ctrl-shift-E", SelectToEndOfLine, Some("BufferView")), - Binding::new("cmd-shift-up", SelectToBeginning, Some("BufferView")), - Binding::new("cmd-shift-down", SelectToEnd, Some("BufferView")), - Binding::new("cmd-a", SelectAll, Some("BufferView")), - Binding::new("cmd-l", SelectLine, Some("BufferView")), - Binding::new("cmd-shift-L", SplitSelectionIntoLines, Some("BufferView")), - Binding::new("cmd-alt-up", AddSelectionAbove, Some("BufferView")), - Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("BufferView")), - Binding::new("cmd-alt-down", AddSelectionBelow, Some("BufferView")), - Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("BufferView")), - Binding::new("alt-up", SelectLargerSyntaxNode, Some("BufferView")), - Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("BufferView")), - Binding::new("alt-down", SelectSmallerSyntaxNode, Some("BufferView")), - Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("BufferView")), - Binding::new("ctrl-m", MoveToEnclosingBracket, Some("BufferView")), - Binding::new("pageup", PageUp, Some("BufferView")), - Binding::new("pagedown", PageDown, Some("BufferView")), - Binding::new("alt-cmd-[", Fold, Some("BufferView")), - Binding::new("alt-cmd-]", Unfold, Some("BufferView")), - Binding::new("alt-cmd-f", FoldSelectedRanges, Some("BufferView")), + Binding::new("cmd-shift-right", SelectToEndOfLine, Some("Editor")), + Binding::new("ctrl-shift-E", SelectToEndOfLine, Some("Editor")), + Binding::new("cmd-shift-up", SelectToBeginning, Some("Editor")), + Binding::new("cmd-shift-down", SelectToEnd, Some("Editor")), + Binding::new("cmd-a", SelectAll, Some("Editor")), + Binding::new("cmd-l", SelectLine, Some("Editor")), + Binding::new("cmd-shift-L", SplitSelectionIntoLines, Some("Editor")), + Binding::new("cmd-alt-up", AddSelectionAbove, Some("Editor")), + Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("Editor")), + Binding::new("cmd-alt-down", AddSelectionBelow, Some("Editor")), + Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("Editor")), + Binding::new("alt-up", SelectLargerSyntaxNode, Some("Editor")), + Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")), + Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")), + Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")), + Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")), + Binding::new("pageup", PageUp, Some("Editor")), + Binding::new("pagedown", PageDown, Some("Editor")), + Binding::new("alt-cmd-[", Fold, Some("Editor")), + Binding::new("alt-cmd-]", Unfold, Some("Editor")), + Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")), ]); cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); @@ -2088,11 +2080,11 @@ impl Editor { } pub fn page_up(&mut self, _: &PageUp, _: &mut ViewContext) { - log::info!("BufferView::page_up"); + log::info!("Editor::page_up"); } pub fn page_down(&mut self, _: &PageDown, _: &mut ViewContext) { - log::info!("BufferView::page_down"); + log::info!("Editor::page_down"); } pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext) { @@ -2596,7 +2588,7 @@ impl View for Editor { } fn ui_name() -> &'static str { - "BufferView" + "Editor" } fn on_focus(&mut self, cx: &mut ViewContext) { From 2aadc97126f644d5dea3f852d7b1ee4855b45db7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 6 Sep 2021 13:08:19 +0200 Subject: [PATCH 186/204] Simplify how we determine if enter should insert a newline or propagate --- zed/src/editor.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 7d13628f8af0f743a02ce26819c8a1a121b9e93d..6176b402c559e0d22712d096a00afe08f75a723f 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -46,7 +46,7 @@ const MAX_LINE_LEN: usize = 1024; action!(Cancel); action!(Backspace); action!(Delete); -action!(Newline, bool); +action!(Newline); action!(Insert, String); action!(DeleteLine); action!(DeleteToPreviousWordBoundary); @@ -105,8 +105,8 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-h", Backspace, Some("Editor")), Binding::new("delete", Delete, Some("Editor")), Binding::new("ctrl-d", Delete, Some("Editor")), - Binding::new("enter", Newline(false), Some("Editor")), - Binding::new("alt-enter", Newline(true), Some("Editor")), + Binding::new("enter", Newline, Some("Editor && mode == full")), + Binding::new("alt-enter", Newline, Some("Editor && mode == auto_height")), Binding::new("tab", Insert("\t".into()), Some("Editor")), Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), Binding::new( @@ -752,18 +752,8 @@ impl Editor { self.end_transaction(cx); } - fn newline(&mut self, Newline(insert_newline): &Newline, cx: &mut ViewContext) { - match self.mode { - EditorMode::SingleLine => cx.propagate_action(), - EditorMode::AutoHeight { .. } => { - if *insert_newline { - self.insert(&Insert("\n".into()), cx); - } else { - cx.propagate_action(); - } - } - EditorMode::Full => self.insert(&Insert("\n".into()), cx), - } + fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { + self.insert(&Insert("\n".into()), cx); } pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { @@ -2610,6 +2600,17 @@ impl View for Editor { cx.emit(Event::Blurred); cx.notify(); } + + fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context { + let mut cx = Self::default_keymap_context(); + let mode = match self.mode { + EditorMode::SingleLine => "single_line", + EditorMode::AutoHeight { .. } => "auto_height", + EditorMode::Full => "full", + }; + cx.map.insert("mode".into(), mode.into()); + cx + } } impl workspace::Item for Buffer { From 6e71c43d2970ad1a8944992677d0597a0ee8feaa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 6 Sep 2021 13:17:44 +0200 Subject: [PATCH 187/204] Delete `Editor::newline` and just use the `Insert` action --- zed/src/editor.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 6176b402c559e0d22712d096a00afe08f75a723f..63ebe9982f9b27da9e91cc06840a12ad865761fb 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -46,7 +46,6 @@ const MAX_LINE_LEN: usize = 1024; action!(Cancel); action!(Backspace); action!(Delete); -action!(Newline); action!(Insert, String); action!(DeleteLine); action!(DeleteToPreviousWordBoundary); @@ -105,8 +104,12 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-h", Backspace, Some("Editor")), Binding::new("delete", Delete, Some("Editor")), Binding::new("ctrl-d", Delete, Some("Editor")), - Binding::new("enter", Newline, Some("Editor && mode == full")), - Binding::new("alt-enter", Newline, Some("Editor && mode == auto_height")), + Binding::new("enter", Insert("\n".into()), Some("Editor && mode == full")), + Binding::new( + "alt-enter", + Insert("\n".into()), + Some("Editor && mode == auto_height"), + ), Binding::new("tab", Insert("\t".into()), Some("Editor")), Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), Binding::new( @@ -199,7 +202,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::select); cx.add_action(Editor::cancel); cx.add_action(Editor::insert); - cx.add_action(Editor::newline); cx.add_action(Editor::backspace); cx.add_action(Editor::delete); cx.add_action(Editor::delete_line); @@ -752,10 +754,6 @@ impl Editor { self.end_transaction(cx); } - fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { - self.insert(&Insert("\n".into()), cx); - } - pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { self.start_transaction(cx); let mut selections = self.selections(cx.as_ref()).to_vec(); From 94959d18c42026fc6f75b3de4de46e1f59af92e0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 6 Sep 2021 14:08:42 +0200 Subject: [PATCH 188/204] Load embedded fonts when initializing settings --- gpui/src/platform.rs | 2 +- gpui/src/platform/mac/fonts.rs | 11 +++++++---- zed/src/editor.rs | 2 +- zed/src/main.rs | 4 ++-- zed/src/settings.rs | 12 +++++++++++- zed/src/test.rs | 3 ++- zed/src/theme/theme_registry.rs | 8 ++++---- 7 files changed, 28 insertions(+), 14 deletions(-) diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 078bf7970560c1c4eca8f99517ddf43526f4f5c3..ba237d47882b2c9fe7d57586b929b2df72729ce1 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -128,7 +128,7 @@ pub enum CursorStyle { } pub trait FontSystem: Send + Sync { - fn add_fonts(&self, fonts: Vec>>) -> anyhow::Result<()>; + fn add_fonts(&self, fonts: &[Arc>]) -> anyhow::Result<()>; fn load_family(&self, name: &str) -> anyhow::Result>; fn select_font( &self, diff --git a/gpui/src/platform/mac/fonts.rs b/gpui/src/platform/mac/fonts.rs index 8f61bfab30bdf4c26055221aa5e9c568a987f942..4b2716c0442f4bc0f4413d2ec6be4aa209b55393 100644 --- a/gpui/src/platform/mac/fonts.rs +++ b/gpui/src/platform/mac/fonts.rs @@ -50,7 +50,7 @@ impl FontSystem { } impl platform::FontSystem for FontSystem { - fn add_fonts(&self, fonts: Vec>>) -> anyhow::Result<()> { + fn add_fonts(&self, fonts: &[Arc>]) -> anyhow::Result<()> { self.0.write().add_fonts(fonts) } @@ -102,9 +102,12 @@ impl platform::FontSystem for FontSystem { } impl FontSystemState { - fn add_fonts(&mut self, fonts: Vec>>) -> anyhow::Result<()> { - self.memory_source - .add_fonts(fonts.into_iter().map(|bytes| Handle::from_memory(bytes, 0)))?; + fn add_fonts(&mut self, fonts: &[Arc>]) -> anyhow::Result<()> { + self.memory_source.add_fonts( + fonts + .iter() + .map(|bytes| Handle::from_memory(bytes.clone(), 0)), + )?; Ok(()) } diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 63ebe9982f9b27da9e91cc06840a12ad865761fb..2d4b763776d3cb3d1749cb294575e1167a178ae5 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -3478,7 +3478,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.set_wrap_width(140., cx); + view.set_wrap_width(130., cx); assert_eq!( view.display_text(cx), "use one::{\n two::three::\n four::five\n};" diff --git a/zed/src/main.rs b/zed/src/main.rs index 3603e760eb07f06e899dab538126ea49df44eb08..5ad05738e6935496ca9b02d45190c93cc18e7725 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -27,8 +27,8 @@ fn main() { .list("fonts") .into_iter() .map(|f| Arc::new(Assets.load(&f).unwrap().to_vec())) - .collect(); - app.platform().fonts().add_fonts(embedded_fonts).unwrap(); + .collect::>(); + app.platform().fonts().add_fonts(&embedded_fonts).unwrap(); let themes = settings::ThemeRegistry::new(Assets, app.font_cache()); let (settings_tx, settings) = settings::channel(&app.font_cache(), &themes).unwrap(); diff --git a/zed/src/settings.rs b/zed/src/settings.rs index 6b7defe1361db50a6ca818a55dd88841c2bc4dc0..8893e8bd9f430e84a66c082470605d2f6b7ed95d 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -17,15 +17,25 @@ pub struct Settings { impl Settings { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &gpui::AppContext) -> Self { + use crate::assets::Assets; + use gpui::AssetSource; + lazy_static::lazy_static! { static ref DEFAULT_THEME: parking_lot::Mutex>> = Default::default(); + static ref FONTS: Vec>> = Assets + .list("fonts") + .into_iter() + .map(|f| Arc::new(Assets.load(&f).unwrap().to_vec())) + .collect(); } + cx.platform().fonts().add_fonts(&FONTS).unwrap(); + let mut theme_guard = DEFAULT_THEME.lock(); let theme = if let Some(theme) = theme_guard.as_ref() { theme.clone() } else { - let theme = ThemeRegistry::new(crate::assets::Assets, cx.font_cache().clone()) + let theme = ThemeRegistry::new(Assets, cx.font_cache().clone()) .get(DEFAULT_THEME_NAME) .expect("failed to load default theme in tests"); *theme_guard = Some(theme.clone()); diff --git a/zed/src/test.rs b/zed/src/test.rs index d8b30afa6923837d935c1fece436a4b01593015e..b917e428f683df23a6b6cd23f51e1c95be849d27 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -1,4 +1,5 @@ use crate::{ + assets::Assets, channel::ChannelList, fs::RealFs, language::LanguageRegistry, @@ -158,7 +159,7 @@ fn write_tree(path: &Path, tree: serde_json::Value) { pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { let (settings_tx, settings) = settings::test(cx); let languages = Arc::new(LanguageRegistry::new()); - let themes = ThemeRegistry::new((), cx.font_cache().clone()); + let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let rpc = rpc::Client::new(); let user_store = Arc::new(UserStore::new(rpc.clone())); Arc::new(AppState { diff --git a/zed/src/theme/theme_registry.rs b/zed/src/theme/theme_registry.rs index 0808281f09e4b34f99692130c4f75edf39f85d79..cd9781afe942f4e5bee4d1c16b6aa5886ae376a0 100644 --- a/zed/src/theme/theme_registry.rs +++ b/zed/src/theme/theme_registry.rs @@ -515,16 +515,16 @@ fn value_at<'a>(object: &'a mut Map, key_path: &KeyPath) -> Optio #[cfg(test)] mod tests { use super::*; - use crate::{assets::Assets, theme::DEFAULT_THEME_NAME}; + use crate::{test::test_app_state, theme::DEFAULT_THEME_NAME}; use gpui::MutableAppContext; use rand::{prelude::StdRng, Rng}; #[gpui::test] fn test_bundled_themes(cx: &mut MutableAppContext) { - let registry = ThemeRegistry::new(Assets, cx.font_cache().clone()); + let app_state = test_app_state(cx); let mut has_default_theme = false; - for theme_name in registry.list() { - let theme = registry.get(&theme_name).unwrap(); + for theme_name in app_state.themes.list() { + let theme = app_state.themes.get(&theme_name).unwrap(); if theme.name == DEFAULT_THEME_NAME { has_default_theme = true; } From 11b8577d1bdef6bf424e6256499a7df72537a79d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Sep 2021 11:57:55 -0700 Subject: [PATCH 189/204] Update FoldMap snapshot versions when only the parse tree changes even if the buffer's text has not changed Co-Authored-By: Nathan Sobo --- zed/src/editor/buffer.rs | 35 +++++++++++++++++++------- zed/src/editor/display_map/fold_map.rs | 8 +++--- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index da3fd222802c2bf63ea1d1d1e1c8579ea8fa7a44..f353a48c42012bfb87ee845154790d76067a4029 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -121,6 +121,7 @@ pub struct Buffer { language: Option>, syntax_tree: Mutex>, is_parsing: bool, + parse_count: usize, selections: HashMap, deferred_ops: OperationQueue, deferred_replicas: HashSet, @@ -141,7 +142,7 @@ pub struct SelectionSet { #[derive(Clone)] struct SyntaxTree { tree: Tree, - parsed: bool, + dirty: bool, version: time::Global, } @@ -581,6 +582,7 @@ impl Buffer { file, syntax_tree: Mutex::new(None), is_parsing: false, + parse_count: 0, language, saved_mtime, selections: HashMap::default(), @@ -790,9 +792,12 @@ impl Buffer { cx.emit(Event::FileHandleChanged); } + pub fn parse_count(&self) -> usize { + self.parse_count + } + pub fn syntax_tree(&self) -> Option { if let Some(syntax_tree) = self.syntax_tree.lock().as_mut() { - let mut edited = false; let mut delta = 0_isize; for edit in self.edits_since(syntax_tree.version.clone()) { let start_offset = (edit.old_bytes.start as isize + delta) as usize; @@ -809,9 +814,8 @@ impl Buffer { .into(), }); delta += edit.inserted_bytes() as isize - edit.deleted_bytes() as isize; - edited = true; + syntax_tree.dirty = true; } - syntax_tree.parsed &= !edited; syntax_tree.version = self.version(); Some(syntax_tree.tree.clone()) } else { @@ -819,13 +823,14 @@ impl Buffer { } } + #[cfg(test)] pub fn is_parsing(&self) -> bool { self.is_parsing } fn should_reparse(&self) -> bool { if let Some(syntax_tree) = self.syntax_tree.lock().as_ref() { - !syntax_tree.parsed || syntax_tree.version != self.version + syntax_tree.dirty || syntax_tree.version != self.version } else { self.language.is_some() } @@ -841,7 +846,7 @@ impl Buffer { if let Some(language) = self.language.clone() { self.is_parsing = true; cx.spawn(|handle, mut cx| async move { - while handle.read_with(&cx, |this, _| this.should_reparse()) { + loop { // The parse tree is out of date, so grab the syntax tree to synchronously // splice all the edits that have happened since the last parse. let new_tree = handle.update(&mut cx, |this, _| this.syntax_tree()); @@ -857,17 +862,28 @@ impl Buffer { }) .await; - handle.update(&mut cx, |this, cx| { + let parse_again = handle.update(&mut cx, |this, cx| { *this.syntax_tree.lock() = Some(SyntaxTree { tree: new_tree, - parsed: true, + dirty: false, version: new_version, }); + this.parse_count += 1; cx.emit(Event::Reparsed); cx.notify(); + + if this.should_reparse() { + true + } else { + this.is_parsing = false; + false + } }); + + if !parse_again { + break; + } } - handle.update(&mut cx, |this, _| this.is_parsing = false); }) .detach(); } @@ -1916,6 +1932,7 @@ impl Clone for Buffer { language: self.language.clone(), syntax_tree: Mutex::new(self.syntax_tree.lock().clone()), is_parsing: false, + parse_count: self.parse_count, deferred_replicas: self.deferred_replicas.clone(), replica_id: self.replica_id, remote_id: self.remote_id.clone(), diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index 1c24bacafc0179646778a60db0789d5e04ffa325..8aa01495e886d440068378e3ef49c06bc0ddaf86 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/zed/src/editor/display_map/fold_map.rs @@ -201,7 +201,7 @@ pub struct FoldMap { #[derive(Clone)] struct SyncState { version: time::Global, - is_parsing: bool, + parse_count: usize, } impl FoldMap { @@ -222,7 +222,7 @@ impl FoldMap { )), last_sync: Mutex::new(SyncState { version: buffer.version(), - is_parsing: buffer.is_parsing(), + parse_count: buffer.parse_count(), }), version: AtomicUsize::new(0), }; @@ -253,7 +253,7 @@ impl FoldMap { &mut *self.last_sync.lock(), SyncState { version: buffer.version(), - is_parsing: buffer.is_parsing(), + parse_count: buffer.parse_count(), }, ); let edits = buffer @@ -261,7 +261,7 @@ impl FoldMap { .map(Into::into) .collect::>(); if edits.is_empty() { - if last_sync.is_parsing != buffer.is_parsing() { + if last_sync.parse_count != buffer.parse_count() { self.version.fetch_add(1, SeqCst); } Vec::new() From 8e5c7090576634f2d91d1d760cecd200ea788af3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Sep 2021 12:50:38 -0700 Subject: [PATCH 190/204] Block for up to 1ms when reparsing This way, we'll avoid rendering two frames on edits that lead to fast reparses. Co-Authored-By: Nathan Sobo --- zed/src/editor/buffer.rs | 111 +++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index f353a48c42012bfb87ee845154790d76067a4029..c2ba280be6ddf745d540e6af4bd8ab8564b81a8f 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -120,7 +120,7 @@ pub struct Buffer { file: Option, language: Option>, syntax_tree: Mutex>, - is_parsing: bool, + parsing_in_background: bool, parse_count: usize, selections: HashMap, deferred_ops: OperationQueue, @@ -581,7 +581,7 @@ impl Buffer { history, file, syntax_tree: Mutex::new(None), - is_parsing: false, + parsing_in_background: false, parse_count: 0, language, saved_mtime, @@ -610,7 +610,7 @@ impl Buffer { fragments: self.fragments.clone(), version: self.version.clone(), tree: self.syntax_tree(), - is_parsing: self.is_parsing, + is_parsing: self.parsing_in_background, language: self.language.clone(), query_cursor: QueryCursorHandle::new(), } @@ -825,68 +825,67 @@ impl Buffer { #[cfg(test)] pub fn is_parsing(&self) -> bool { - self.is_parsing - } - - fn should_reparse(&self) -> bool { - if let Some(syntax_tree) = self.syntax_tree.lock().as_ref() { - syntax_tree.dirty || syntax_tree.version != self.version - } else { - self.language.is_some() - } + self.parsing_in_background } - fn reparse(&mut self, cx: &mut ModelContext) { - // Avoid spawning a new parsing task if the buffer is already being reparsed - // due to an earlier edit. - if self.is_parsing { - return; + fn reparse(&mut self, cx: &mut ModelContext) -> bool { + if self.parsing_in_background { + return false; } if let Some(language) = self.language.clone() { - self.is_parsing = true; - cx.spawn(|handle, mut cx| async move { - loop { - // The parse tree is out of date, so grab the syntax tree to synchronously - // splice all the edits that have happened since the last parse. - let new_tree = handle.update(&mut cx, |this, _| this.syntax_tree()); - let (new_text, new_version) = handle - .read_with(&cx, |this, _| (this.visible_text.clone(), this.version())); - - // Parse the current text in a background thread. - let new_tree = cx - .background() - .spawn({ - let language = language.clone(); - async move { Self::parse_text(&new_text, new_tree, &language) } - }) - .await; - - let parse_again = handle.update(&mut cx, |this, cx| { - *this.syntax_tree.lock() = Some(SyntaxTree { - tree: new_tree, - dirty: false, - version: new_version, - }); - this.parse_count += 1; - cx.emit(Event::Reparsed); - cx.notify(); + // The parse tree is out of date, so grab the syntax tree to synchronously + // splice all the edits that have happened since the last parse. + let old_tree = self.syntax_tree(); + let parsed_text = self.visible_text.clone(); + let parsed_version = self.version(); + let parse_task = cx.background().spawn({ + let language = language.clone(); + async move { Self::parse_text(&parsed_text, old_tree, &language) } + }); - if this.should_reparse() { - true - } else { - this.is_parsing = false; - false - } + match cx + .background() + .block_with_timeout(Duration::from_millis(1), parse_task) + { + Ok(new_tree) => { + *self.syntax_tree.lock() = Some(SyntaxTree { + tree: new_tree, + dirty: false, + version: parsed_version, }); + self.parse_count += 1; + cx.emit(Event::Reparsed); + cx.notify(); + return true; + } + Err(parse_task) => { + self.parsing_in_background = true; + 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; + *this.syntax_tree.lock() = Some(SyntaxTree { + tree: new_tree, + dirty: false, + version: parsed_version, + }); + this.parse_count += 1; + this.parsing_in_background = false; + + if parse_again && this.reparse(cx) { + return; + } - if !parse_again { - break; - } + cx.emit(Event::Reparsed); + cx.notify(); + }); + }) + .detach(); } - }) - .detach(); + } } + false } fn parse_text(text: &Rope, old_tree: Option, language: &Language) -> Tree { @@ -1931,7 +1930,7 @@ impl Clone for Buffer { file: self.file.clone(), language: self.language.clone(), syntax_tree: Mutex::new(self.syntax_tree.lock().clone()), - is_parsing: false, + parsing_in_background: false, parse_count: self.parse_count, deferred_replicas: self.deferred_replicas.clone(), replica_id: self.replica_id, From 9346aa300da92e4c70b72f78afb5a3e135ce0f72 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Sep 2021 15:36:09 -0700 Subject: [PATCH 191/204] Start work on underlined text Co-Authored-By: Nathan Sobo --- gpui/examples/text.rs | 47 +++++----- gpui/src/elements/label.rs | 78 +++++++++++------ gpui/src/elements/text.rs | 2 +- gpui/src/fonts.rs | 23 ++++- gpui/src/platform.rs | 10 +-- gpui/src/platform/mac/fonts.rs | 94 ++++++++++---------- gpui/src/text_layout.rs | 151 ++++++++++++++++++++++++--------- zed/assets/themes/_base.toml | 2 +- zed/assets/themes/dark.toml | 2 +- zed/src/editor.rs | 58 ++++++++++--- zed/src/theme.rs | 2 + 11 files changed, 317 insertions(+), 152 deletions(-) diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index 32086b7fda327b9a9d9f99ad7b52f3df7c114b93..6c82b2d88a3df891ce9ae9b120d0e24496f790fb 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -1,6 +1,7 @@ use gpui::{ color::Color, fonts::{Properties, Weight}, + text_layout::RunStyle, DebugContext, Element as _, Quad, }; use log::LevelFilter; @@ -55,31 +56,39 @@ impl gpui::Element for TextElement { ) -> Self::PaintState { let font_size = 12.; let family = cx.font_cache.load_family(&["SF Pro Display"]).unwrap(); - let normal = cx - .font_cache - .select_font(family, &Default::default()) - .unwrap(); - let bold = cx - .font_cache - .select_font( - family, - &Properties { - weight: Weight::BOLD, - ..Default::default() - }, - ) - .unwrap(); + let normal = RunStyle { + font_id: cx + .font_cache + .select_font(family, &Default::default()) + .unwrap(), + color: Color::default(), + underline: false, + }; + let bold = RunStyle { + font_id: cx + .font_cache + .select_font( + family, + &Properties { + weight: Weight::BOLD, + ..Default::default() + }, + ) + .unwrap(), + color: Color::default(), + underline: false, + }; let text = "Hello world!"; let line = cx.text_layout_cache.layout_str( text, font_size, &[ - (1, normal, Color::default()), - (1, bold, Color::default()), - (1, normal, Color::default()), - (1, bold, Color::default()), - (text.len() - 4, normal, Color::default()), + (1, normal.clone()), + (1, bold.clone()), + (1, normal.clone()), + (1, bold.clone()), + (text.len() - 4, normal.clone()), ], ); diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index 917281d8c64eefc5930a7b59c45aff018768d3a0..8dbc76a65b5c3106ca43c091ef698f351b6a37af 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -1,12 +1,11 @@ use crate::{ - color::Color, - fonts::{FontId, TextStyle}, + fonts::TextStyle, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, - text_layout::Line, + text_layout::{Line, RunStyle}, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; use serde::Deserialize; @@ -48,10 +47,17 @@ impl Label { self } - fn compute_runs(&self) -> SmallVec<[(usize, FontId, Color); 8]> { + fn compute_runs(&self) -> SmallVec<[(usize, RunStyle); 8]> { let font_id = self.style.text.font_id; if self.highlight_indices.is_empty() { - return smallvec![(self.text.len(), font_id, self.style.text.color)]; + return smallvec![( + self.text.len(), + RunStyle { + font_id, + color: self.style.text.color, + underline: false, + } + )]; } let highlight_font_id = self @@ -62,25 +68,31 @@ impl Label { let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); let mut runs = SmallVec::new(); + let highlight_style = self + .style + .highlight_text + .as_ref() + .unwrap_or(&self.style.text); for (char_ix, c) in self.text.char_indices() { let mut font_id = font_id; let mut color = self.style.text.color; + let mut underline = self.style.text.underline; if let Some(highlight_ix) = highlight_indices.peek() { if char_ix == *highlight_ix { font_id = highlight_font_id; - color = self - .style - .highlight_text - .as_ref() - .unwrap_or(&self.style.text) - .color; + color = highlight_style.color; + underline = highlight_style.underline; highlight_indices.next(); } } - let push_new_run = if let Some((last_len, last_font_id, last_color)) = runs.last_mut() { - if font_id == *last_font_id && color == *last_color { + let last_run: Option<&mut (usize, RunStyle)> = runs.last_mut(); + let push_new_run = if let Some((last_len, last_style)) = last_run { + if font_id == last_style.font_id + && color == last_style.color + && underline == last_style.underline + { *last_len += c.len_utf8(); false } else { @@ -91,7 +103,14 @@ impl Label { }; if push_new_run { - runs.push((c.len_utf8(), font_id, color)); + runs.push(( + c.len_utf8(), + RunStyle { + font_id, + color, + underline, + }, + )); } } @@ -177,6 +196,7 @@ impl ToJson for LabelStyle { #[cfg(test)] mod tests { use super::*; + use crate::color::Color; use crate::fonts::{Properties as FontProperties, Weight}; #[crate::test(self)] @@ -185,6 +205,7 @@ mod tests { "Menlo", 12., Default::default(), + false, Color::black(), cx.font_cache(), ) @@ -193,6 +214,7 @@ mod tests { "Menlo", 12., *FontProperties::new().weight(Weight::BOLD), + false, Color::new(255, 0, 0, 255), cx.font_cache(), ) @@ -212,21 +234,27 @@ mod tests { ".αβγδε.ⓐⓑ".len(), ]); + let default_run_style = RunStyle { + font_id: default_style.font_id, + color: default_style.color, + underline: default_style.underline, + }; + let highlight_run_style = RunStyle { + font_id: highlight_style.font_id, + color: highlight_style.color, + underline: highlight_style.underline, + }; let runs = label.compute_runs(); assert_eq!( runs.as_slice(), &[ - (".α".len(), default_style.font_id, default_style.color), - ("βγ".len(), highlight_style.font_id, highlight_style.color), - ("δ".len(), default_style.font_id, default_style.color), - ("ε".len(), highlight_style.font_id, highlight_style.color), - (".ⓐ".len(), default_style.font_id, default_style.color), - ("ⓑⓒ".len(), highlight_style.font_id, highlight_style.color), - ( - "ⓓⓔ.abcde.".len(), - default_style.font_id, - default_style.color - ), + (".α".len(), default_run_style), + ("βγ".len(), highlight_run_style), + ("δ".len(), default_run_style), + ("ε".len(), highlight_run_style), + (".ⓐ".len(), default_run_style), + ("ⓑⓒ".len(), highlight_run_style), + ("ⓓⓔ.abcde.".len(), default_run_style), ] ); } diff --git a/gpui/src/elements/text.rs b/gpui/src/elements/text.rs index 400c9d599783f766ae9a894e00e709166ec503f6..623af72af6444892b30c75448eea17e379b14a02 100644 --- a/gpui/src/elements/text.rs +++ b/gpui/src/elements/text.rs @@ -52,7 +52,7 @@ impl Element for Text { let shaped_line = cx.text_layout_cache.layout_str( line, self.style.font_size, - &[(line.len(), font_id, self.style.color)], + &[(line.len(), self.style.to_run())], ); let wrap_boundaries = wrapper .wrap_shaped_line(line, &shaped_line, constraint.max.x()) diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index 3010e1ada054a0500734f78c402a81f64201a38a..96248c167577326cb74ed3ee6c1d7934f385f362 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -1,6 +1,7 @@ use crate::{ color::Color, json::{json, ToJson}, + text_layout::RunStyle, FontCache, }; use anyhow::anyhow; @@ -24,12 +25,14 @@ pub struct TextStyle { pub font_id: FontId, pub font_size: f32, pub font_properties: Properties, + pub underline: bool, } #[derive(Clone, Debug, Default)] pub struct HighlightStyle { pub color: Color, pub font_properties: Properties, + pub underline: bool, } #[allow(non_camel_case_types)] @@ -55,9 +58,11 @@ struct TextStyleJson { color: Color, family: String, weight: Option, + size: f32, #[serde(default)] italic: bool, - size: f32, + #[serde(default)] + underline: bool, } #[derive(Deserialize)] @@ -66,6 +71,8 @@ struct HighlightStyleJson { weight: Option, #[serde(default)] italic: bool, + #[serde(default)] + underline: bool, } impl TextStyle { @@ -73,6 +80,7 @@ impl TextStyle { font_family_name: impl Into>, font_size: f32, font_properties: Properties, + underline: bool, color: Color, font_cache: &FontCache, ) -> anyhow::Result { @@ -85,9 +93,18 @@ impl TextStyle { font_id, font_size, font_properties, + underline, }) } + pub fn to_run(&self) -> RunStyle { + RunStyle { + font_id: self.font_id, + color: self.color, + underline: self.underline, + } + } + fn from_json(json: TextStyleJson) -> anyhow::Result { FONT_CACHE.with(|font_cache| { if let Some(font_cache) = font_cache.borrow().as_ref() { @@ -96,6 +113,7 @@ impl TextStyle { json.family, json.size, font_properties, + json.underline, json.color, font_cache, ) @@ -114,6 +132,7 @@ impl HighlightStyle { Self { color: json.color, font_properties, + underline: json.underline, } } } @@ -123,6 +142,7 @@ impl From for HighlightStyle { Self { color, font_properties: Default::default(), + underline: false, } } } @@ -161,6 +181,7 @@ impl<'de> Deserialize<'de> for HighlightStyle { Ok(Self { color: serde_json::from_value(json).map_err(de::Error::custom)?, font_properties: Properties::new(), + underline: false, }) } } diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index ba237d47882b2c9fe7d57586b929b2df72729ce1..dec7db910388ef2bd32c4b54f0f1955f74779661 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -8,14 +8,13 @@ pub mod current { } use crate::{ - color::Color, executor, fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties}, geometry::{ rect::{RectF, RectI}, vector::{vec2f, Vector2F}, }, - text_layout::LineLayout, + text_layout::{LineLayout, RunStyle}, AnyAction, ClipboardItem, Menu, Scene, }; use anyhow::Result; @@ -146,12 +145,7 @@ pub trait FontSystem: Send + Sync { subpixel_shift: Vector2F, scale_factor: f32, ) -> Option<(RectI, Vec)>; - fn layout_line( - &self, - text: &str, - font_size: f32, - runs: &[(usize, FontId, Color)], - ) -> LineLayout; + fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout; fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec; } diff --git a/gpui/src/platform/mac/fonts.rs b/gpui/src/platform/mac/fonts.rs index 4b2716c0442f4bc0f4413d2ec6be4aa209b55393..c01700ce22817b341dca6caf634490a3a0b24666 100644 --- a/gpui/src/platform/mac/fonts.rs +++ b/gpui/src/platform/mac/fonts.rs @@ -1,5 +1,4 @@ use crate::{ - color::Color, fonts::{FontId, GlyphId, Metrics, Properties}, geometry::{ rect::{RectF, RectI}, @@ -7,7 +6,7 @@ use crate::{ vector::{vec2f, vec2i, Vector2F}, }, platform, - text_layout::{Glyph, LineLayout, Run}, + text_layout::{Glyph, LineLayout, Run, RunStyle}, }; use cocoa::appkit::{CGFloat, CGPoint}; use core_foundation::{ @@ -87,12 +86,7 @@ impl platform::FontSystem for FontSystem { .rasterize_glyph(font_id, font_size, glyph_id, subpixel_shift, scale_factor) } - fn layout_line( - &self, - text: &str, - font_size: f32, - runs: &[(usize, FontId, Color)], - ) -> LineLayout { + fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout { self.0.read().layout_line(text, font_size, runs) } @@ -210,12 +204,7 @@ impl FontSystemState { } } - fn layout_line( - &self, - text: &str, - font_size: f32, - runs: &[(usize, FontId, Color)], - ) -> LineLayout { + fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout { let font_id_attr_name = CFString::from_static_string("zed_font_id"); // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. @@ -227,20 +216,20 @@ impl FontSystemState { let last_run: RefCell> = Default::default(); let font_runs = runs .iter() - .filter_map(|(len, font_id, _)| { + .filter_map(|(len, style)| { let mut last_run = last_run.borrow_mut(); if let Some((last_len, last_font_id)) = last_run.as_mut() { - if font_id == last_font_id { + if style.font_id == *last_font_id { *last_len += *len; None } else { let result = (*last_len, *last_font_id); *last_len = *len; - *last_font_id = *font_id; + *last_font_id = style.font_id; Some(result) } } else { - *last_run = Some((*len, *font_id)); + *last_run = Some((*len, style.font_id)); None } }) @@ -415,9 +404,8 @@ extern "C" { #[cfg(test)] mod tests { - use crate::MutableAppContext; - use super::*; + use crate::MutableAppContext; use font_kit::properties::{Style, Weight}; use platform::FontSystem as _; @@ -426,13 +414,25 @@ mod tests { // This is failing intermittently on CI and we don't have time to figure it out let fonts = FontSystem::new(); let menlo = fonts.load_family("Menlo").unwrap(); - let menlo_regular = fonts.select_font(&menlo, &Properties::new()).unwrap(); - let menlo_italic = fonts - .select_font(&menlo, &Properties::new().style(Style::Italic)) - .unwrap(); - let menlo_bold = fonts - .select_font(&menlo, &Properties::new().weight(Weight::BOLD)) - .unwrap(); + let menlo_regular = RunStyle { + font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(), + color: Default::default(), + underline: false, + }; + let menlo_italic = RunStyle { + font_id: fonts + .select_font(&menlo, &Properties::new().style(Style::Italic)) + .unwrap(), + color: Default::default(), + underline: false, + }; + let menlo_bold = RunStyle { + font_id: fonts + .select_font(&menlo, &Properties::new().weight(Weight::BOLD)) + .unwrap(), + color: Default::default(), + underline: false, + }; assert_ne!(menlo_regular, menlo_italic); assert_ne!(menlo_regular, menlo_bold); assert_ne!(menlo_italic, menlo_bold); @@ -440,18 +440,14 @@ mod tests { let line = fonts.layout_line( "hello world", 16.0, - &[ - (2, menlo_bold, Default::default()), - (4, menlo_italic, Default::default()), - (5, menlo_regular, Default::default()), - ], + &[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)], ); assert_eq!(line.runs.len(), 3); - assert_eq!(line.runs[0].font_id, menlo_bold); + assert_eq!(line.runs[0].font_id, menlo_bold.font_id); assert_eq!(line.runs[0].glyphs.len(), 2); - assert_eq!(line.runs[1].font_id, menlo_italic); + assert_eq!(line.runs[1].font_id, menlo_italic.font_id); assert_eq!(line.runs[1].glyphs.len(), 4); - assert_eq!(line.runs[2].font_id, menlo_regular); + assert_eq!(line.runs[2].font_id, menlo_regular.font_id); assert_eq!(line.runs[2].glyphs.len(), 5); } @@ -459,18 +455,26 @@ mod tests { fn test_glyph_offsets() -> anyhow::Result<()> { let fonts = FontSystem::new(); let zapfino = fonts.load_family("Zapfino")?; - let zapfino_regular = fonts.select_font(&zapfino, &Properties::new())?; + let zapfino_regular = RunStyle { + font_id: fonts.select_font(&zapfino, &Properties::new())?, + color: Default::default(), + underline: false, + }; let menlo = fonts.load_family("Menlo")?; - let menlo_regular = fonts.select_font(&menlo, &Properties::new())?; + let menlo_regular = RunStyle { + font_id: fonts.select_font(&menlo, &Properties::new())?, + color: Default::default(), + underline: false, + }; let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈"; let line = fonts.layout_line( text, 16.0, &[ - (9, zapfino_regular, Color::default()), - (13, menlo_regular, Color::default()), - (text.len() - 22, zapfino_regular, Color::default()), + (9, zapfino_regular), + (13, menlo_regular), + (text.len() - 22, zapfino_regular), ], ); assert_eq!( @@ -536,15 +540,19 @@ mod tests { fn test_layout_line_bom_char() { let fonts = FontSystem::new(); let font_ids = fonts.load_family("Helvetica").unwrap(); - let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap(); + let style = RunStyle { + font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(), + color: Default::default(), + underline: false, + }; let line = "\u{feff}"; - let layout = fonts.layout_line(line, 16., &[(line.len(), font_id, Default::default())]); + let layout = fonts.layout_line(line, 16., &[(line.len(), style)]); assert_eq!(layout.len, line.len()); assert!(layout.runs.is_empty()); let line = "a\u{feff}b"; - let layout = fonts.layout_line(line, 16., &[(line.len(), font_id, Default::default())]); + let layout = fonts.layout_line(line, 16., &[(line.len(), style)]); assert_eq!(layout.len, line.len()); assert_eq!(layout.runs.len(), 1); assert_eq!(layout.runs[0].glyphs.len(), 2); diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index d668966dcc0fd05a4fee96dcff67486f77bfbb76..1f710ac31173575c6f0dcd27e5abb15558a98138 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -24,6 +24,13 @@ pub struct TextLayoutCache { fonts: Arc, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RunStyle { + pub color: Color, + pub font_id: FontId, + pub underline: bool, +} + impl TextLayoutCache { pub fn new(fonts: Arc) -> Self { Self { @@ -44,7 +51,7 @@ impl TextLayoutCache { &'a self, text: &'a str, font_size: f32, - runs: &'a [(usize, FontId, Color)], + runs: &'a [(usize, RunStyle)], ) -> Line { let key = &CacheKeyRef { text, @@ -95,7 +102,7 @@ impl<'a> Hash for (dyn CacheKey + 'a) { struct CacheKeyValue { text: String, font_size: OrderedFloat, - runs: SmallVec<[(usize, FontId, Color); 1]>, + runs: SmallVec<[(usize, RunStyle); 1]>, } impl CacheKey for CacheKeyValue { @@ -120,11 +127,11 @@ impl<'a> Borrow for CacheKeyValue { } } -#[derive(Copy, Clone, PartialEq, Eq, Hash)] +#[derive(Copy, Clone)] struct CacheKeyRef<'a> { text: &'a str, font_size: OrderedFloat, - runs: &'a [(usize, FontId, Color)], + runs: &'a [(usize, RunStyle)], } impl<'a> CacheKey for CacheKeyRef<'a> { @@ -133,10 +140,34 @@ impl<'a> CacheKey for CacheKeyRef<'a> { } } +impl<'a> PartialEq for CacheKeyRef<'a> { + fn eq(&self, other: &Self) -> bool { + self.text == other.text + && self.font_size == other.font_size + && self.runs.len() == other.runs.len() + && self.runs.iter().zip(other.runs.iter()).all( + |((len_a, style_a), (len_b, style_b))| { + len_a == len_b && style_a.font_id == style_b.font_id + }, + ) + } +} + +impl<'a> Hash for CacheKeyRef<'a> { + fn hash(&self, state: &mut H) { + self.text.hash(state); + self.font_size.hash(state); + for (len, style_id) in self.runs { + len.hash(state); + style_id.font_id.hash(state); + } + } +} + #[derive(Default, Debug)] pub struct Line { layout: Arc, - color_runs: SmallVec<[(u32, Color); 32]>, + style_runs: SmallVec<[(u32, Color, bool); 32]>, } #[derive(Default, Debug)] @@ -163,12 +194,12 @@ pub struct Glyph { } impl Line { - fn new(layout: Arc, runs: &[(usize, FontId, Color)]) -> Self { - let mut color_runs = SmallVec::new(); - for (len, _, color) in runs { - color_runs.push((*len as u32, *color)); + fn new(layout: Arc, runs: &[(usize, RunStyle)]) -> Self { + let mut style_runs = SmallVec::new(); + for (len, style) in runs { + style_runs.push((*len as u32, style.color, style.underline)); } - Self { layout, color_runs } + Self { layout, style_runs } } pub fn runs(&self) -> &[Run] { @@ -213,11 +244,12 @@ impl Line { cx: &mut PaintContext, ) { let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.; - let baseline_origin = vec2f(0., padding_top + self.layout.ascent); + let baseline_offset = vec2f(0., padding_top + self.layout.ascent); - let mut color_runs = self.color_runs.iter(); - let mut color_end = 0; + let mut style_runs = self.style_runs.iter(); + let mut run_end = 0; let mut color = Color::black(); + let mut underline_start = None; for run in &self.layout.runs { let max_glyph_width = cx @@ -226,7 +258,7 @@ impl Line { .x(); for glyph in &run.glyphs { - let glyph_origin = origin + baseline_origin + glyph.position; + let glyph_origin = origin + baseline_offset + glyph.position; if glyph_origin.x() + max_glyph_width < visible_bounds.origin().x() { continue; @@ -235,12 +267,31 @@ impl Line { break; } - if glyph.index >= color_end { - if let Some(next_run) = color_runs.next() { - color_end += next_run.0 as usize; - color = next_run.1; + if glyph.index >= run_end { + if let Some((run_len, run_color, run_underlined)) = style_runs.next() { + if let Some(underline_origin) = underline_start { + if !*run_underlined || *run_color != color { + cx.scene.push_quad(scene::Quad { + bounds: RectF::from_points( + underline_origin, + glyph_origin + vec2f(0., 1.), + ), + background: Some(color), + border: Default::default(), + corner_radius: 0., + }); + underline_start = None; + } + } + + if *run_underlined { + underline_start.get_or_insert(glyph_origin); + } + + run_end += *run_len as usize; + color = *run_color; } else { - color_end = self.layout.len; + run_end = self.layout.len; color = Color::black(); } } @@ -253,6 +304,16 @@ impl Line { color, }); } + + if let Some(underline_start) = underline_start.take() { + let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.); + cx.scene.push_quad(scene::Quad { + bounds: RectF::from_points(underline_start, line_end + vec2f(0., 1.)), + background: Some(color), + border: Default::default(), + corner_radius: 0., + }); + } } } @@ -268,7 +329,7 @@ impl Line { let baseline_origin = vec2f(0., padding_top + self.layout.ascent); let mut boundaries = boundaries.into_iter().peekable(); - let mut color_runs = self.color_runs.iter(); + let mut color_runs = self.style_runs.iter(); let mut color_end = 0; let mut color = Color::black(); @@ -519,7 +580,14 @@ impl LineWrapper { .layout_line( &c.to_string(), self.font_size, - &[(1, self.font_id, Default::default())], + &[( + 1, + RunStyle { + font_id: self.font_id, + color: Default::default(), + underline: false, + }, + )], ) .width } @@ -528,10 +596,7 @@ impl LineWrapper { #[cfg(test)] mod tests { use super::*; - use crate::{ - color::Color, - fonts::{Properties, Weight}, - }; + use crate::fonts::{Properties, Weight}; #[crate::test(self)] fn test_wrap_line(cx: &mut crate::MutableAppContext) { @@ -600,28 +665,30 @@ mod tests { let family = font_cache.load_family(&["Helvetica"]).unwrap(); let font_id = font_cache.select_font(family, &Default::default()).unwrap(); - let normal = font_cache.select_font(family, &Default::default()).unwrap(); - let bold = font_cache - .select_font( - family, - &Properties { - weight: Weight::BOLD, - ..Default::default() - }, - ) - .unwrap(); + let normal = RunStyle { + font_id, + color: Default::default(), + underline: false, + }; + let bold = RunStyle { + font_id: font_cache + .select_font( + family, + &Properties { + weight: Weight::BOLD, + ..Default::default() + }, + ) + .unwrap(), + color: Default::default(), + underline: false, + }; let text = "aa bbb cccc ddddd eeee"; let line = text_layout_cache.layout_str( text, 16.0, - &[ - (4, normal, Color::default()), - (5, bold, Color::default()), - (6, normal, Color::default()), - (1, bold, Color::default()), - (7, normal, Color::default()), - ], + &[(4, normal), (5, bold), (6, normal), (1, bold), (7, normal)], ); let mut wrapper = LineWrapper::new(font_id, 16., font_system); diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 2d0cfd12369d270956908f8337f9dbce3bc50d23..67ed95f9662b1ca2bf8f1728106bf1ea1a2cf03d 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -2,7 +2,7 @@ background = "$surface.0" [workspace.tab] -text = "$text.2" +text = { extends = "$text.2", underline = true } padding = { left = 10, right = 10 } icon_close = "$text.0.color" icon_dirty = "$status.info" diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index 74688fdb0d13dd705a56b900ffe4f049458b41e5..6f7a8d6e80f432a9a766f6afac931855d78616c2 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -23,7 +23,7 @@ bad = "#b7372e" [syntax] keyword = { color = "#0086c0", weight = "bold" } -function = "#dcdcaa" +function = { color = "#dcdcaa", underline = true } string = "#cb8f77" type = "#4ec9b0" number = "#b5cea8" diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 2d4b763776d3cb3d1749cb294575e1167a178ae5..a19b63c5ce93ff7b57ac2190117ed41c562d9c21 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -17,10 +17,15 @@ pub use display_map::DisplayPoint; use display_map::*; pub use element::*; use gpui::{ - action, color::Color, font_cache::FamilyId, fonts::Properties as FontProperties, - geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, - ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, RenderContext, Task, - TextLayoutCache, View, ViewContext, WeakViewHandle, + action, + color::Color, + font_cache::FamilyId, + fonts::Properties as FontProperties, + geometry::vector::Vector2F, + keymap::Binding, + text_layout::{self, RunStyle}, + AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle, + MutableAppContext, RenderContext, Task, TextLayoutCache, View, ViewContext, WeakViewHandle, }; use postage::watch; use serde::{Deserialize, Serialize}; @@ -2330,7 +2335,7 @@ impl Snapshot { 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) + font_cache.line_height(font_id, self.font_size).ceil() } pub fn em_width(&self, font_cache: &FontCache) -> f32 { @@ -2355,7 +2360,14 @@ impl Snapshot { .layout_str( "1".repeat(digit_count).as_str(), font_size, - &[(digit_count, font_id, Color::black())], + &[( + digit_count, + RunStyle { + font_id, + color: Color::black(), + underline: false, + }, + )], ) .width()) } @@ -2392,7 +2404,14 @@ impl Snapshot { layouts.push(Some(layout_cache.layout_str( &line_number, self.font_size, - &[(line_number.len(), font_id, color)], + &[( + line_number.len(), + RunStyle { + font_id, + color, + underline: false, + }, + )], ))); } } @@ -2429,7 +2448,14 @@ impl Snapshot { layout_cache.layout_str( line, self.font_size, - &[(line.len(), font_id, style.placeholder_text.color)], + &[( + line.len(), + RunStyle { + font_id, + color: style.placeholder_text.color, + underline: false, + }, + )], ) }) .collect()); @@ -2485,7 +2511,14 @@ impl Snapshot { } line.push_str(line_chunk); - styles.push((line_chunk.len(), font_id, style.color)); + 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; } @@ -2518,8 +2551,11 @@ impl Snapshot { self.font_size, &[( self.display_snapshot.line_len(row) as usize, - font_id, - Color::black(), + RunStyle { + font_id, + color: Color::black(), + underline: false, + }, )], )) } diff --git a/zed/src/theme.rs b/zed/src/theme.rs index e15070d81837fa4b9cbef50da934273c0aaf592b..c8b2c61388cd187f459a8f13c52358a5c139fabd 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -180,10 +180,12 @@ impl Default for EditorStyle { 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(), From 6323e8cc5990c55d1561e9c1e1ec051c78d5919c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 6 Sep 2021 16:57:58 -0600 Subject: [PATCH 192/204] WIP: Try rendering underlines after all other quads I thought this would allow underlines to show up in tabs, but it doesn't seem to be working. --- gpui/src/platform/mac/renderer.rs | 10 ++++++---- gpui/src/scene.rs | 14 ++++++++++++++ gpui/src/text_layout.rs | 5 +++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index d125fe136675df9dd776f3f79ad2b21fe71668b7..d215eaf77ce87ed33d9a40592853d513e6e6df58 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -396,11 +396,13 @@ impl Renderer { drawable_size: Vector2F, command_encoder: &metal::RenderCommandEncoderRef, ) { - if layer.quads().is_empty() { + if layer.quads().is_empty() && layer.underlines().is_empty() { return; } align_offset(offset); - let next_offset = *offset + layer.quads().len() * mem::size_of::(); + let next_offset = *offset + + (layer.quads().len() + layer.underlines().len()) + * mem::size_of::(); assert!( next_offset <= INSTANCE_BUFFER_SIZE, "instance buffer exhausted" @@ -430,7 +432,7 @@ impl Renderer { (self.instances.contents() as *mut u8).offset(*offset as isize) as *mut shaders::GPUIQuad }; - for (ix, quad) in layer.quads().iter().enumerate() { + for (ix, quad) in layer.quads().iter().chain(layer.underlines()).enumerate() { let bounds = quad.bounds * scene.scale_factor(); let border_width = quad.border.width * scene.scale_factor(); let shader_quad = shaders::GPUIQuad { @@ -456,7 +458,7 @@ impl Renderer { metal::MTLPrimitiveType::Triangle, 0, 6, - layer.quads().len() as u64, + (layer.quads().len() + layer.underlines().len()) as u64, ); *offset = next_offset; } diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 0bd0fe8a77fe5899421981419eedd088c38ab54a..401918c5fe014426a98496d7f4e2d31b903bb274 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -24,6 +24,7 @@ struct StackingContext { pub struct Layer { clip_bounds: Option, quads: Vec, + underlines: Vec, shadows: Vec, glyphs: Vec, icons: Vec, @@ -165,6 +166,10 @@ impl Scene { self.active_layer().push_quad(quad) } + pub fn push_underline(&mut self, underline: Quad) { + self.active_layer().push_underline(underline) + } + pub fn push_shadow(&mut self, shadow: Shadow) { self.active_layer().push_shadow(shadow) } @@ -234,6 +239,7 @@ impl Layer { Self { clip_bounds, quads: Vec::new(), + underlines: Vec::new(), shadows: Vec::new(), glyphs: Vec::new(), icons: Vec::new(), @@ -253,6 +259,14 @@ impl Layer { self.quads.as_slice() } + fn push_underline(&mut self, underline: Quad) { + self.underlines.push(underline); + } + + pub fn underlines(&self) -> &[Quad] { + self.underlines.as_slice() + } + fn push_shadow(&mut self, shadow: Shadow) { self.shadows.push(shadow); } diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index 1f710ac31173575c6f0dcd27e5abb15558a98138..692efaf4244c8d61fa8ce90190dac75d97ee8c9c 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -271,7 +271,7 @@ impl Line { if let Some((run_len, run_color, run_underlined)) = style_runs.next() { if let Some(underline_origin) = underline_start { if !*run_underlined || *run_color != color { - cx.scene.push_quad(scene::Quad { + cx.scene.push_underline(scene::Quad { bounds: RectF::from_points( underline_origin, glyph_origin + vec2f(0., 1.), @@ -307,7 +307,8 @@ impl Line { if let Some(underline_start) = underline_start.take() { let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.); - cx.scene.push_quad(scene::Quad { + + cx.scene.push_underline(scene::Quad { bounds: RectF::from_points(underline_start, line_end + vec2f(0., 1.)), background: Some(color), border: Default::default(), From 8be85fd8de1f1d169f20bba7b3214ca283ebc653 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Sep 2021 19:03:21 -0700 Subject: [PATCH 193/204] Fix typo in highlighting regex for constants --- zed/languages/rust/highlights.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/languages/rust/highlights.scm b/zed/languages/rust/highlights.scm index 3276182cd609760ed0869a2ce8eb9e396794a4d7..116be758424216869f66772a09b11b9ab90f9da1 100644 --- a/zed/languages/rust/highlights.scm +++ b/zed/languages/rust/highlights.scm @@ -32,7 +32,7 @@ ; Assume all-caps names are constants ((identifier) @constant - (#match? @constant "^[A-Z][A-Z\\d_]+$'")) + (#match? @constant "^[A-Z][A-Z\\d_]+$")) [ "as" From 8cf16c6ecc1b3c785fb70f935f33aa160bc3b6a7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 11:03:08 +0200 Subject: [PATCH 194/204] Use Label's base style `underline` when no highlights are provided --- gpui/src/elements/label.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index 8dbc76a65b5c3106ca43c091ef698f351b6a37af..acfbb5abd9e7dfa9c33004bc34522b16a7bb59a7 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -55,7 +55,7 @@ impl Label { RunStyle { font_id, color: self.style.text.color, - underline: false, + underline: self.style.text.underline, } )]; } From cf68ad3a82a19cd0d66c0c37b2e91ae1ca5392de Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 11:03:43 +0200 Subject: [PATCH 195/204] Trim pending underline when text runs don't cover the whole line --- gpui/src/text_layout.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index 692efaf4244c8d61fa8ce90190dac75d97ee8c9c..a553b3b47831aa242b1b65cad721f14ce7cd1886 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -293,6 +293,17 @@ impl Line { } else { run_end = self.layout.len; color = Color::black(); + if let Some(underline_origin) = underline_start.take() { + cx.scene.push_underline(scene::Quad { + bounds: RectF::from_points( + underline_origin, + glyph_origin + vec2f(0., 1.), + ), + background: Some(color), + border: Default::default(), + corner_radius: 0., + }); + } } } From 5b71901912765377166812852f503ae7d2eee28a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 11:04:44 +0200 Subject: [PATCH 196/204] Flush pending underline when we are at the end of the line --- gpui/src/text_layout.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index a553b3b47831aa242b1b65cad721f14ce7cd1886..c74949b9981a90ba0554b883dcdd6a254c21adaf 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -315,17 +315,17 @@ impl Line { color, }); } + } - if let Some(underline_start) = underline_start.take() { - let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.); + if let Some(underline_start) = underline_start.take() { + let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.); - cx.scene.push_underline(scene::Quad { - bounds: RectF::from_points(underline_start, line_end + vec2f(0., 1.)), - background: Some(color), - border: Default::default(), - corner_radius: 0., - }); - } + cx.scene.push_underline(scene::Quad { + bounds: RectF::from_points(underline_start, line_end + vec2f(0., 1.)), + background: Some(color), + border: Default::default(), + corner_radius: 0., + }); } } From a8011fcde482d856b9bac1ce535768674cacc840 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 11:19:44 +0200 Subject: [PATCH 197/204] Render underlines in front of paths and glyphs --- gpui/src/platform/mac/renderer.rs | 91 ++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index d215eaf77ce87ed33d9a40592853d513e6e6df58..e12a52d6134dbd2c5d110a570f52920c449a287d 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -6,7 +6,7 @@ use crate::{ vector::{vec2f, vec2i, Vector2F}, }, platform, - scene::Layer, + scene::{Glyph, Icon, Layer, Quad, Shadow}, Scene, }; use cocoa::foundation::NSUInteger; @@ -283,12 +283,24 @@ impl Renderer { zfar: 1.0, }); + let scale_factor = scene.scale_factor(); let mut path_sprites = path_sprites.into_iter().peekable(); - for (layer_id, layer) in scene.layers().enumerate() { self.clip(scene, layer, drawable_size, command_encoder); - self.render_shadows(scene, layer, offset, drawable_size, command_encoder); - self.render_quads(scene, layer, offset, drawable_size, command_encoder); + self.render_shadows( + layer.shadows(), + scale_factor, + offset, + drawable_size, + command_encoder, + ); + self.render_quads( + layer.quads(), + scale_factor, + offset, + drawable_size, + command_encoder, + ); self.render_path_sprites( layer_id, &mut path_sprites, @@ -296,7 +308,21 @@ impl Renderer { drawable_size, command_encoder, ); - self.render_sprites(scene, layer, offset, drawable_size, command_encoder); + self.render_sprites( + layer.glyphs(), + layer.icons(), + scale_factor, + offset, + drawable_size, + command_encoder, + ); + self.render_quads( + layer.underlines(), + scale_factor, + offset, + drawable_size, + command_encoder, + ); } command_encoder.end_encoding(); @@ -324,18 +350,18 @@ impl Renderer { fn render_shadows( &mut self, - scene: &Scene, - layer: &Layer, + shadows: &[Shadow], + scale_factor: f32, offset: &mut usize, drawable_size: Vector2F, command_encoder: &metal::RenderCommandEncoderRef, ) { - if layer.shadows().is_empty() { + if shadows.is_empty() { return; } align_offset(offset); - let next_offset = *offset + layer.shadows().len() * mem::size_of::(); + let next_offset = *offset + shadows.len() * mem::size_of::(); assert!( next_offset <= INSTANCE_BUFFER_SIZE, "instance buffer exhausted" @@ -365,12 +391,12 @@ impl Renderer { (self.instances.contents() as *mut u8).offset(*offset as isize) as *mut shaders::GPUIShadow }; - for (ix, shadow) in layer.shadows().iter().enumerate() { - let shape_bounds = shadow.bounds * scene.scale_factor(); + for (ix, shadow) in shadows.iter().enumerate() { + let shape_bounds = shadow.bounds * scale_factor; let shader_shadow = shaders::GPUIShadow { origin: shape_bounds.origin().to_float2(), size: shape_bounds.size().to_float2(), - corner_radius: shadow.corner_radius * scene.scale_factor(), + corner_radius: shadow.corner_radius * scale_factor, sigma: shadow.sigma, color: shadow.color.to_uchar4(), }; @@ -383,26 +409,24 @@ impl Renderer { metal::MTLPrimitiveType::Triangle, 0, 6, - layer.shadows().len() as u64, + shadows.len() as u64, ); *offset = next_offset; } fn render_quads( &mut self, - scene: &Scene, - layer: &Layer, + quads: &[Quad], + scale_factor: f32, offset: &mut usize, drawable_size: Vector2F, command_encoder: &metal::RenderCommandEncoderRef, ) { - if layer.quads().is_empty() && layer.underlines().is_empty() { + if quads.is_empty() { return; } align_offset(offset); - let next_offset = *offset - + (layer.quads().len() + layer.underlines().len()) - * mem::size_of::(); + let next_offset = *offset + quads.len() * mem::size_of::(); assert!( next_offset <= INSTANCE_BUFFER_SIZE, "instance buffer exhausted" @@ -432,9 +456,9 @@ impl Renderer { (self.instances.contents() as *mut u8).offset(*offset as isize) as *mut shaders::GPUIQuad }; - for (ix, quad) in layer.quads().iter().chain(layer.underlines()).enumerate() { - let bounds = quad.bounds * scene.scale_factor(); - let border_width = quad.border.width * scene.scale_factor(); + for (ix, quad) in quads.iter().enumerate() { + let bounds = quad.bounds * scale_factor; + let border_width = quad.border.width * scale_factor; let shader_quad = shaders::GPUIQuad { origin: bounds.origin().round().to_float2(), size: bounds.size().round().to_float2(), @@ -447,7 +471,7 @@ impl Renderer { border_bottom: border_width * (quad.border.bottom as usize as f32), border_left: border_width * (quad.border.left as usize as f32), border_color: quad.border.color.to_uchar4(), - corner_radius: quad.corner_radius * scene.scale_factor(), + corner_radius: quad.corner_radius * scale_factor, }; unsafe { *(buffer_contents.offset(ix as isize)) = shader_quad; @@ -458,35 +482,36 @@ impl Renderer { metal::MTLPrimitiveType::Triangle, 0, 6, - (layer.quads().len() + layer.underlines().len()) as u64, + quads.len() as u64, ); *offset = next_offset; } fn render_sprites( &mut self, - scene: &Scene, - layer: &Layer, + glyphs: &[Glyph], + icons: &[Icon], + scale_factor: f32, offset: &mut usize, drawable_size: Vector2F, command_encoder: &metal::RenderCommandEncoderRef, ) { - if layer.glyphs().is_empty() && layer.icons().is_empty() { + if glyphs.is_empty() && icons.is_empty() { return; } let mut sprites_by_atlas = HashMap::new(); - for glyph in layer.glyphs() { + for glyph in glyphs { if let Some(sprite) = self.sprite_cache.render_glyph( glyph.font_id, glyph.font_size, glyph.id, glyph.origin, - scene.scale_factor(), + scale_factor, ) { // Snap sprite to pixel grid. - let origin = (glyph.origin * scene.scale_factor()).floor() + sprite.offset.to_f32(); + let origin = (glyph.origin * scale_factor).floor() + sprite.offset.to_f32(); sprites_by_atlas .entry(sprite.atlas_id) .or_insert_with(Vec::new) @@ -501,9 +526,9 @@ impl Renderer { } } - for icon in layer.icons() { - let origin = icon.bounds.origin() * scene.scale_factor(); - let target_size = icon.bounds.size() * scene.scale_factor(); + for icon in icons { + let origin = icon.bounds.origin() * scale_factor; + let target_size = icon.bounds.size() * scale_factor; let source_size = (target_size * 2.).ceil().to_i32(); let sprite = From 09bb42c168bf46c3801e16d27d15e854c605057f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 12:27:43 +0200 Subject: [PATCH 198/204] Show "sign in" prompt when opening the chat panel while signed out --- zed/assets/themes/_base.toml | 8 ++++++ zed/src/chat_panel.rs | 49 ++++++++++++++++++++++++++++++------ zed/src/theme.rs | 2 ++ zed/src/workspace.rs | 1 + zed/src/workspace/sidebar.rs | 6 ++--- 5 files changed, 55 insertions(+), 11 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 67ed95f9662b1ca2bf8f1728106bf1ea1a2cf03d..cb0ea42fbc80191f56c9504c89dbbdad8d3b9cfc 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -76,6 +76,14 @@ placeholder_text = "$text.2.color" background = "$surface.1" selection = "$selection.host" +[chat_panel.sign_in_prompt] +extends = "$text.0" +underline = true + +[chat_panel.hovered_sign_in_prompt] +extends = "$chat_panel.sign_in_prompt" +color = "$text.1.color" + [selector] background = "$surface.2" text = "$text.0" diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 5f4f3e13c71be1c696b003ddd17663d9b3240504..fe1ac5ba8a9d3f346767606687fb6590a29f8143 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -1,14 +1,18 @@ +use std::sync::Arc; + use crate::{ channel::{Channel, ChannelEvent, ChannelList, ChannelMessage}, editor::Editor, + rpc::Client, theme, - util::ResultExt, + util::{ResultExt, TryFutureExt}, Settings, }; use gpui::{ action, elements::*, keymap::Binding, + platform::CursorStyle, views::{ItemType, Select, SelectStyle}, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, @@ -19,6 +23,7 @@ use time::{OffsetDateTime, UtcOffset}; const MESSAGE_LOADING_THRESHOLD: usize = 50; pub struct ChatPanel { + rpc: Arc, channel_list: ModelHandle, active_channel: Option<(ModelHandle, Subscription)>, message_list: ListState, @@ -42,6 +47,7 @@ pub fn init(cx: &mut MutableAppContext) { impl ChatPanel { pub fn new( + rpc: Arc, channel_list: ModelHandle, settings: watch::Receiver, cx: &mut ViewContext, @@ -94,6 +100,7 @@ impl ChatPanel { }); let mut this = Self { + rpc, channel_list, active_channel: Default::default(), message_list, @@ -307,9 +314,38 @@ impl View for ChatPanel { "ChatPanel" } - fn render(&mut self, _: &mut RenderContext) -> ElementBox { + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; - ConstrainedBox::new( + let element = if self.rpc.user_id().borrow().is_none() { + enum SignInPromptLabel {} + + Align::new( + MouseEventHandler::new::(0, cx, |mouse_state, _| { + Label::new( + "Sign in to use chat".to_string(), + if mouse_state.hovered { + theme.chat_panel.hovered_sign_in_prompt.clone() + } else { + theme.chat_panel.sign_in_prompt.clone() + }, + ) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click({ + let rpc = self.rpc.clone(); + move |cx| { + let rpc = rpc.clone(); + cx.spawn(|cx| async move { + rpc.authenticate_and_connect(cx).log_err().await; + }) + .detach(); + } + }) + .boxed(), + ) + .boxed() + } else { Container::new( Flex::column() .with_child( @@ -322,10 +358,9 @@ impl View for ChatPanel { .boxed(), ) .with_style(&theme.chat_panel.container) - .boxed(), - ) - .with_min_width(150.) - .boxed() + .boxed() + }; + ConstrainedBox::new(element).with_min_width(150.).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { diff --git a/zed/src/theme.rs b/zed/src/theme.rs index c8b2c61388cd187f459a8f13c52358a5c139fabd..84748e4b0c43490a2d1c5a918bc07df65aed95cc 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -70,6 +70,8 @@ pub struct ChatPanel { pub channel_select: ChannelSelect, pub input_editor_container: ContainerStyle, pub input_editor: InputEditorStyle, + pub sign_in_prompt: TextStyle, + pub hovered_sign_in_prompt: TextStyle, } #[derive(Deserialize)] diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index f5c9e0e58e6656a6121a7f83164376af7d0dcc14..ff87c4b42c4e6af76fbafa6766788085e31d9841 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -376,6 +376,7 @@ impl Workspace { "icons/comment-16.svg", cx.add_view(|cx| { ChatPanel::new( + app_state.rpc.clone(), app_state.channel_list.clone(), app_state.settings.clone(), cx, diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 71946b50acf90de63f056083bc90f1d251c7a97e..b6de41b92c406c87b831796a41616052aeb6f437 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -37,7 +37,7 @@ impl Sidebar { side, items: Default::default(), active_item_ix: None, - width: Rc::new(RefCell::new(200.)), + width: Rc::new(RefCell::new(220.)), } } @@ -123,9 +123,7 @@ impl Sidebar { ) .on_after_layout({ let width = self.width.clone(); - move |size, _| { - *width.borrow_mut() = size.x(); - } + move |size, _| *width.borrow_mut() = size.x() }) .boxed(), ) From 058691d2b27ded8979699a21f10036cecebdc212 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 13:57:30 +0200 Subject: [PATCH 199/204] Focus chat input editor after signing in --- server/src/rpc.rs | 2 +- zed/src/channel.rs | 2 +- zed/src/chat_panel.rs | 115 ++++++++++++++++++++++++++---------------- zed/src/lib.rs | 2 +- zed/src/rpc.rs | 4 +- zed/src/workspace.rs | 4 +- 6 files changed, 78 insertions(+), 51 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 611220568b7601b6301889fbe48e8059230f21c2..34f5f378d931e5ddbedca9f4e2a98a0709da329c 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1714,7 +1714,7 @@ mod tests { ) .detach(); client - .add_connection(user_id.to_proto(), client_conn, cx.to_async()) + .add_connection(user_id.to_proto(), client_conn, &cx.to_async()) .await .unwrap(); (user_id, client) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index e3cb5d29d6635cc614075853dafef195b3dfa871..24997d49642648e5fc59c301270346cb75007a3b 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -671,7 +671,7 @@ mod tests { cx.background().spawn(io).detach(); client - .add_connection(user_id, client_conn, cx.to_async()) + .add_connection(user_id, client_conn, &cx.to_async()) .await .unwrap(); diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index fe1ac5ba8a9d3f346767606687fb6590a29f8143..c8aaac4f38845b4a52bf4261b76ad4f50d6d8c60 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -193,6 +193,19 @@ impl ChatPanel { cx.notify(); } + fn render_channel(&self) -> ElementBox { + let theme = &self.settings.borrow().theme; + Flex::column() + .with_child( + Container::new(ChildView::new(self.channel_select.id()).boxed()) + .with_style(&theme.chat_panel.channel_select.container) + .boxed(), + ) + .with_child(self.render_active_channel_messages()) + .with_child(self.render_input_box()) + .boxed() + } + fn render_active_channel_messages(&self) -> ElementBox { let messages = if self.active_channel.is_some() { List::new(self.message_list.clone()).boxed() @@ -279,6 +292,47 @@ impl ChatPanel { .boxed() } + fn render_sign_in_prompt(&self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme; + let rpc = self.rpc.clone(); + let this = cx.handle(); + + enum SignInPromptLabel {} + + Align::new( + MouseEventHandler::new::(0, cx, |mouse_state, _| { + Label::new( + "Sign in to use chat".to_string(), + if mouse_state.hovered { + theme.chat_panel.hovered_sign_in_prompt.clone() + } else { + theme.chat_panel.sign_in_prompt.clone() + }, + ) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |cx| { + let rpc = rpc.clone(); + let this = this.clone(); + cx.spawn(|mut cx| async move { + if rpc.authenticate_and_connect(&cx).log_err().await.is_some() { + cx.update(|cx| { + if let Some(this) = this.upgrade(cx) { + if this.is_focused(cx) { + this.update(cx, |this, cx| cx.focus(&this.input_editor)); + } + } + }) + } + }) + .detach(); + }) + .boxed(), + ) + .boxed() + } + fn send(&mut self, _: &Send, cx: &mut ViewContext) { if let Some((channel, _)) = self.active_channel.as_ref() { let body = self.input_editor.update(cx, |editor, cx| { @@ -303,6 +357,10 @@ impl ChatPanel { }) } } + + fn is_signed_in(&self) -> bool { + self.rpc.user_id().borrow().is_some() + } } impl Entity for ChatPanel { @@ -316,55 +374,24 @@ impl View for ChatPanel { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; - let element = if self.rpc.user_id().borrow().is_none() { - enum SignInPromptLabel {} - - Align::new( - MouseEventHandler::new::(0, cx, |mouse_state, _| { - Label::new( - "Sign in to use chat".to_string(), - if mouse_state.hovered { - theme.chat_panel.hovered_sign_in_prompt.clone() - } else { - theme.chat_panel.sign_in_prompt.clone() - }, - ) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click({ - let rpc = self.rpc.clone(); - move |cx| { - let rpc = rpc.clone(); - cx.spawn(|cx| async move { - rpc.authenticate_and_connect(cx).log_err().await; - }) - .detach(); - } - }) - .boxed(), - ) - .boxed() + let element = if self.is_signed_in() { + self.render_channel() } else { - Container::new( - Flex::column() - .with_child( - Container::new(ChildView::new(self.channel_select.id()).boxed()) - .with_style(&theme.chat_panel.channel_select.container) - .boxed(), - ) - .with_child(self.render_active_channel_messages()) - .with_child(self.render_input_box()) - .boxed(), - ) - .with_style(&theme.chat_panel.container) - .boxed() + self.render_sign_in_prompt(cx) }; - ConstrainedBox::new(element).with_min_width(150.).boxed() + ConstrainedBox::new( + Container::new(element) + .with_style(&theme.chat_panel.container) + .boxed(), + ) + .with_min_width(150.) + .boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.input_editor); + if self.is_signed_in() { + cx.focus(&self.input_editor); + } } } diff --git a/zed/src/lib.rs b/zed/src/lib.rs index d451c187d5cc982354d25ebf9575b4c7df7bbc1b..e7bb31b207b86a20fe80d091611cbc6ec57947d1 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -50,7 +50,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { let rpc = app_state.rpc.clone(); move |_: &Authenticate, cx| { let rpc = rpc.clone(); - cx.spawn(|cx| async move { rpc.authenticate_and_connect(cx).log_err().await }) + cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await }) .detach(); } }); diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index 6ec07c90f6470e772cff722146a1fcdc1ab95fa1..7c0302834db11954613b106fe7b8f4b9e07fc183 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -139,7 +139,7 @@ impl Client { pub async fn authenticate_and_connect( self: &Arc, - cx: AsyncAppContext, + cx: &AsyncAppContext, ) -> anyhow::Result<()> { if self.state.read().connection_id.is_some() { return Ok(()); @@ -176,7 +176,7 @@ impl Client { self: &Arc, user_id: u64, conn: Conn, - cx: AsyncAppContext, + cx: &AsyncAppContext, ) -> anyhow::Result<()> where Conn: 'static diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index ff87c4b42c4e6af76fbafa6766788085e31d9841..95050ca60791adee7019857bf4b0955a4b761eb3 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -798,7 +798,7 @@ impl Workspace { let platform = cx.platform(); let task = cx.spawn(|this, mut cx| async move { - rpc.authenticate_and_connect(cx.clone()).await?; + rpc.authenticate_and_connect(&cx).await?; let share_task = this.update(&mut cx, |this, cx| { let worktree = this.worktrees.iter().next()?; @@ -830,7 +830,7 @@ impl Workspace { let languages = self.languages.clone(); let task = cx.spawn(|this, mut cx| async move { - rpc.authenticate_and_connect(cx.clone()).await?; + rpc.authenticate_and_connect(&cx).await?; let worktree_url = cx .platform() From c4dac3c6b1651158337f46d22ff15ee3010b38a3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 15:00:24 +0200 Subject: [PATCH 200/204] Open a window with an empty buffer when launching zed with no args --- zed/src/main.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/zed/src/main.rs b/zed/src/main.rs index 5ad05738e6935496ca9b02d45190c93cc18e7725..a7dc346e367ec96641e6a005a4f2d0244f3a8e6a 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -15,7 +15,7 @@ use zed::{ fs::RealFs, language, menus, rpc, settings, theme_selector, user::UserStore, - workspace::{self, OpenParams, OpenPaths}, + workspace::{self, OpenNew, OpenParams, OpenPaths}, AppState, }; @@ -62,11 +62,10 @@ fn main() { } let paths = collect_path_args(); - if !paths.is_empty() { - cx.dispatch_global_action(OpenPaths(OpenParams { - paths, - app_state: app_state.clone(), - })); + if paths.is_empty() { + cx.dispatch_global_action(OpenNew(app_state)); + } else { + cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state })); } }); } From d08ec438ed029955b325654a891a13dd10bc8341 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 18:37:45 +0200 Subject: [PATCH 201/204] Make titlebar taller and position traffic lights accordingly Co-Authored-By: Nathan Sobo --- gpui/src/platform.rs | 2 ++ gpui/src/platform/mac/window.rs | 63 +++++++++++++++++++++++++++++++-- zed/src/workspace.rs | 3 +- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index dec7db910388ef2bd32c4b54f0f1955f74779661..a4c86eab2f9f696754c11677dabb8d32849ba33b 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -105,6 +105,7 @@ pub struct WindowOptions<'a> { pub bounds: RectF, pub title: Option<&'a str>, pub titlebar_appears_transparent: bool, + pub traffic_light_position: Option, } pub struct PathPromptOptions { @@ -155,6 +156,7 @@ impl<'a> Default for WindowOptions<'a> { bounds: RectF::new(Default::default(), vec2f(1024.0, 768.0)), title: Default::default(), titlebar_appears_transparent: Default::default(), + traffic_light_position: Default::default(), } } } diff --git a/gpui/src/platform/mac/window.rs b/gpui/src/platform/mac/window.rs index bcdb61241c655258c8bd887613c5d9799cce0208..e960107a99aa61e11b33619aeaf3709452c6c315 100644 --- a/gpui/src/platform/mac/window.rs +++ b/gpui/src/platform/mac/window.rs @@ -8,8 +8,8 @@ use crate::{ use block::ConcreteBlock; use cocoa::{ appkit::{ - NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, - NSViewWidthSizable, NSWindow, NSWindowStyleMask, + CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, + NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask, }, base::{id, nil}, foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString}, @@ -66,6 +66,10 @@ unsafe fn build_classes() { sel!(sendEvent:), send_event as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(windowDidResize:), + window_did_resize as extern "C" fn(&Object, Sel, id), + ); decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel)); decl.register() }; @@ -139,6 +143,7 @@ struct WindowState { command_queue: metal::CommandQueue, last_fresh_keydown: Option<(Keystroke, String)>, layer: id, + traffic_light_position: Option, } impl Window { @@ -204,12 +209,14 @@ impl Window { command_queue: device.new_command_queue(), last_fresh_keydown: None, layer, + traffic_light_position: options.traffic_light_position, }))); (*native_window).set_ivar( WINDOW_STATE_IVAR, Rc::into_raw(window.0.clone()) as *const c_void, ); + native_window.setDelegate_(native_window); (*native_view).set_ivar( WINDOW_STATE_IVAR, Rc::into_raw(window.0.clone()) as *const c_void, @@ -243,6 +250,7 @@ impl Window { native_window.center(); native_window.makeKeyAndOrderFront_(nil); + window.0.borrow().move_traffic_light(); pool.drain(); window @@ -343,6 +351,52 @@ impl platform::WindowContext for Window { } } +impl WindowState { + fn move_traffic_light(&self) { + if let Some(traffic_light_position) = self.traffic_light_position { + let titlebar_height = self.titlebar_height(); + + unsafe { + let close_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowCloseButton + ]; + let min_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton + ]; + let zoom_button: id = msg_send![ + self.native_window, + standardWindowButton: NSWindowButton::NSWindowZoomButton + ]; + + let mut close_button_frame: CGRect = msg_send![close_button, frame]; + let mut min_button_frame: CGRect = msg_send![min_button, frame]; + let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame]; + let mut origin = vec2f( + traffic_light_position.x(), + titlebar_height + - traffic_light_position.y() + - close_button_frame.size.height as f32, + ); + let button_spacing = + (min_button_frame.origin.x - close_button_frame.origin.x) as f32; + + close_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); + let _: () = msg_send![close_button, setFrame: close_button_frame]; + origin.set_x(origin.x() + button_spacing); + + min_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); + let _: () = msg_send![min_button, setFrame: min_button_frame]; + origin.set_x(origin.x() + button_spacing); + + zoom_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64); + let _: () = msg_send![zoom_button, setFrame: zoom_button_frame]; + } + } + } +} + impl platform::WindowContext for WindowState { fn size(&self) -> Vector2F { let NSSize { width, height, .. } = @@ -462,6 +516,11 @@ extern "C" fn send_event(this: &Object, _: Sel, native_event: id) { } } +extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { + let window_state = unsafe { get_window_state(this) }; + window_state.as_ref().borrow().move_traffic_light(); +} + extern "C" fn close_window(this: &Object, _: Sel) { unsafe { let close_callback = { diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 95050ca60791adee7019857bf4b0955a4b761eb3..1b7b9226d0f3aa2dc8d1375566c731aaaf088193 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -142,6 +142,7 @@ fn window_options() -> WindowOptions<'static> { bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)), title: None, titlebar_appears_transparent: true, + traffic_light_position: Some(vec2f(8., 8.)), } } @@ -952,7 +953,7 @@ impl View for Workspace { Flex::column() .with_child( ConstrainedBox::new(Empty::new().boxed()) - .with_height(cx.titlebar_height) + .with_height(32.) .named("titlebar"), ) .with_child( From 1f0bda71f683ab6a716af3e429aa8fec9abb8ed4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 18:40:42 +0200 Subject: [PATCH 202/204] Remove underlines from tab titles Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- 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 cb0ea42fbc80191f56c9504c89dbbdad8d3b9cfc..08fb222f6de66262c8c4f5672e6b39f366a8be07 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -2,7 +2,7 @@ background = "$surface.0" [workspace.tab] -text = { extends = "$text.2", underline = true } +text = "$text.2" padding = { left = 10, right = 10 } icon_close = "$text.0.color" icon_dirty = "$status.info" From 2a763d09874f5d036a6803b927c75a93e6bc2a9a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 18:51:10 +0200 Subject: [PATCH 203/204] Upgrade sqlx-cli to v0.5.7 to fix compilation errors Co-Authored-By: Max Brunsfeld --- Dockerfile.migrator | 2 +- script/sqlx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.migrator b/Dockerfile.migrator index 76b7e2b729478e8bae1b5ef2e997118f7d431a29..99c21b2230387b2ab1d31016a5c9494d573d21c0 100644 --- a/Dockerfile.migrator +++ b/Dockerfile.migrator @@ -4,7 +4,7 @@ FROM rust 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.5 + cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.7 FROM debian:buster-slim as runtime RUN apt-get update; \ diff --git a/script/sqlx b/script/sqlx index 3765c806c84d81a8e42301740a860b403c0863b0..590aad67ebeb79734884d70096a42688b8d0555d 100755 --- a/script/sqlx +++ b/script/sqlx @@ -3,7 +3,7 @@ set -e # Install sqlx-cli if needed -[[ "$(sqlx --version)" == "sqlx-cli 0.5.5" ]] || cargo install sqlx-cli --version 0.5.5 +[[ "$(sqlx --version)" == "sqlx-cli 0.5.7" ]] || cargo install sqlx-cli --version 0.5.7 cd server From e94099df08aa2ce982199e5346f7a1bf5e90cd34 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Sep 2021 19:08:13 +0200 Subject: [PATCH 204/204] Fix Dockerfile build of zed-server Co-Authored-By: Max Brunsfeld Co-Authored-By: Nathan Sobo --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6f168a9a91e5081b68dd56a8ab805abf67efd9e5..18623704e287191995a432d11da9c14651e2c829 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN --mount=type=cache,target=./script/node_modules \ RUN --mount=type=cache,target=./script/node_modules \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=./target \ - cargo build --release --bin zed-server + cargo build --release --package zed-server --bin zed-server # Copy server binary out of cached directory RUN --mount=type=cache,target=./target \