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 45aa593112..08959b77a2 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 579bf5c468..8df38af0d4 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 7107d7763d..62225d66e8 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 b1d86acb12..56969e8089 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 d5507f38c9..b3dc4f8a2b 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 cb543522d8..9badef458e 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 080e0d843a..3765c806c8 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 0000000000..c79c8e39b4 --- /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 c79c8e39b4..9e9d47b731 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 0000000000..61af3bcd88 --- /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 9e9d47b731..5bba4689d9 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 d4d4406e0d..65360ae885 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 0000000000..0bdfc0a5e3 --- /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 3f379ff56f..d6e3f81615 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 4a7107e550..9dde8212ff 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 0000000000..9b61018701 --- /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 b4b8c24bf6..25adde3a0f 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 ebd52b0a8b..ec153bea8f 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 3c189833b2..cd229a3c67 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 66d9047467..d0257e9f41 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 9b61018701..300c8de6d5 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 cd229a3c67..f1ebf605a2 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 d0257e9f41..653e2ae59a 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 bb082c1783..77390cbb17 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 62225d66e8..05c03811fe 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 56969e8089..f12fe38ce6 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 77e23d5aa6..86b1153424 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 8df38af0d4..8d626d2d62 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 844dc05024..05aa2acc46 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 a24b7b81ed..817a0e7c91 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 6d26f66054..aad43e5b6e 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 9dde8212ff..d61428fa37 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 300c8de6d5..de196766e1 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 f1ebf605a2..8696f03691 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 653e2ae59a..86980e8673 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 65360ae885..7d8e3ff742 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 86980e8673..257bdfb8d8 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 0000000000..c846999e8e --- /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 e59fb3fd67..f283584c0e 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 f455376f20..7c1640ce82 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 c846999e8e..2ec8b8c2ac 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 579bf5c468..a2fa818824 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 2ec8b8c2ac..0554f2b183 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 7c1640ce82..c987e34b16 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 2093d6a732..37a8bc3efb 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 77390cbb17..9d44b4da38 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 0554f2b183..9ce1f7a97c 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 f8566cd8c4..1bfe2cd1a2 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 37a8bc3efb..ef3dd1d956 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 9d44b4da38..7d4458f1d7 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,32 +115,36 @@ 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_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); +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")) +} /// A stream of protobuf messages. pub struct MessageStream { 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 | 295 +++++++++++++++++++++++++++++++++----------- zrpc/src/peer.rs | 9 +- zrpc/src/proto.rs | 168 +++++++++++++------------ 7 files changed, 365 insertions(+), 175 deletions(-) diff --git a/server/src/tests.rs b/server/src/tests.rs index 257bdfb8d8..cce311c5f3 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 9ce1f7a97c..2aa2a966ee 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 c987e34b16..07c6cb6fb1 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 dbbb1c4598..ea9b544f9a 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 1bfe2cd1a2..a483a06647 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() } - pub fn add_peer( + fn update_from_remote( &mut self, - envelope: TypedEnvelope, + envelope: &TypedEnvelope, cx: &mut ModelContext, ) -> Result<()> { - let peer = envelope.payload.peer.ok_or_else(|| anyhow!("empty peer"))?; + 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, + cx: &mut ModelContext, + ) -> Result<()> { + 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 ef3dd1d956..c377ad1309 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 7d4458f1d7..1c799ebe21 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)), - } +macro_rules! entity_messages { + ($id_field:ident, $($name:ident),* $(,)?) => { + $(impl EntityMessage for $name { + fn remote_entity_id(&self) -> u64 { + self.$id_field } - - 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 - } - } - } + })* }; } 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 b488dbe95b..a831893614 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 07c6cb6fb1..00f5e2dd74 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 a483a06647..cf60eb87cd 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 7d8e3ff742..3a0b7aabb6 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 8cafad9f1f..be3625e51f 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 c377ad1309..d0dcf836a5 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 0000000000..7ead744bdd --- /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 1c799ebe21..d8c794fd63 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 d61428fa37..ac326b15de 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 8696f03691..b7be90d348 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 be3625e51f..67132cf299 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 ac326b15de..d61428fa37 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 b7be90d348..e628ef2c81 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 cce311c5f3..18a7375f86 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 f283584c0e..90e68c698d 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 00f5e2dd74..2bd9fd7f81 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 e2ddff054e..ce22369126 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 cb543522d8..e074d57a64 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 67132cf299..8cafad9f1f 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 d0dcf836a5..315f56b316 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 7ead744bdd..0000000000 --- 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 e628ef2c81..465cd96a98 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 | 107 ++++++++++++++----------------- 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, 241 insertions(+), 171 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 465cd96a98..77a50aceac 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); + 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"); - 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 { + 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 18a7375f86..5df19aa530 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 2aa2a966ee..60ff50489c 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 a831893614..f087109c99 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 86839e4d3f..227f0b9efc 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 2bd9fd7f81..1c0d6d0894 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 ce22369126..3576681cd3 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 ea9b544f9a..5bae8b7a99 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 e074d57a64..8dfa7fc4da 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 cf60eb87cd..18f057dbab 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 315f56b316..7048fcd0a1 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 d8c794fd63..271fa8e29e 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 18f057dbab..d24c1b412e 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 d24c1b412e..723487ad4a 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 d61428fa37..5a3e301d27 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 ec153bea8f..b98c8b0d04 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 77a50aceac..d107e3606b 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() + 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(()) } - 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() - } -} - -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 5df19aa530..1cc7bf18ba 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 1cc7bf18ba..3846f68794 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 60ff50489c..5fc21206b2 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 90e68c698d..492eaaf104 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 1c0d6d0894..902f17eb9e 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 8dfa7fc4da..663ca5f75e 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 723487ad4a..d9a8aa889b 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 | 29 +++++++++++++++++---------- zed/src/channel.rs | 48 ++++++++++++++++++++++++++++++++++++--------- zed/src/rpc.rs | 14 +++++++------ 3 files changed, 66 insertions(+), 25 deletions(-) diff --git a/server/src/tests.rs b/server/src/tests.rs index 3846f68794..607929327f 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"); + 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() + }] + ) + }); - // assert_eq!( - // db.get_recent_channel_messages(channel_id, 50) - // .await - // .unwrap()[0] - // .body, - // "first message!" - // ); + 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 5fc21206b2..1c7d576c65 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 902f17eb9e..a65551a7ff 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 a2fa818824..221419341e 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 607929327f..dd899e9fed 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 1c7d576c65..3161592f4d 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 3a0b7aabb6..cbced82e7b 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 271fa8e29e..173e802f00 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 cbced82e7b..48b481c5f4 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 7048fcd0a1..06d4b01ae0 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 173e802f00..0bcbfd4d65 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 3161592f4d..d3fc64a505 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 d107e3606b..e5086d870c 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 dd899e9fed..b301d7b2d6 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 d3fc64a505..cbbdec6015 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) -> Option<&VecDeque> { - self.messages.as_ref() + pub fn messages(&self) -> &VecDeque { + &self.messages + } + + 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 a65551a7ff..b8909a988a 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 48b481c5f4..f368cb2d47 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 b301d7b2d6..b6b9045ecb 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 cbbdec6015..9390d8bac4 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 b98c8b0d04..a49705e249 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 e5086d870c..377cf5af4c 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 b6b9045ecb..0000000000 --- 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 9390d8bac4..0ce147113c 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 0ce147113c..fe683b728c 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 de196766e1..8351b9d224 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 377cf5af4c..91dda0964f 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 91dda0964f..e5a8d848e4 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 | 466 +++++++++++++++++++++++++------------------ server/src/main.rs | 11 +- server/src/rpc.rs | 112 +++++------ 5 files changed, 338 insertions(+), 285 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 221419341e..a2fa818824 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 78cb77c6b9..c848cff9c5 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 8351b9d224..1e489aae36 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 - ) - "; + 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.0) - .await + 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 a49705e249..41f2638027 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 e5a8d848e4..1dc372af01 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 1dc372af01..9742ec015f 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 50a1b15e0c..a6663ec9c3 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 58872cc759..2bbd37e934 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 1cdb2c51dd..c384e27f27 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 a6663ec9c3..2ad222c174 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 7c9900c3f4..d370f85a08 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 39412804d8..52b87860c7 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 25816a41d2..2853f938fa 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 663ca5f75e..55f9efd75a 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 511e48dfd4..f99e2a3fd6 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 05c03811fe..f6930c1ae8 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 3f05ec969d..0bcc0a4c29 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 05aa2acc46..a2047a2540 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 0000000000..6316d3a4a9 --- /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 0000000000..f22773b159 --- /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 0000000000..4ec153f538 --- /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 2ad222c174..cf340282d5 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 2bbd37e934..b019057f65 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 492eaaf104..4cec473dfc 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 0000000000..552eab851c --- /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 52b87860c7..a8111d8b0a 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 3169e9e58d..7c133944f1 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 0000000000..7d7e1f7c6e --- /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 0000000000..ec9f3f486f --- /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 4cec473dfc..445647010c 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 | 50 ++++++----- .../{ => 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, 190 insertions(+), 138 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 817a0e7c91..426b99d932 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 535fe16155..be3d47e42a 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 3d03574094..14d33b8e90 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 0000000000..fd3e06f295 --- /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 f2f935d142..308674d45d 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 eb13c2a86c..eeb53cc188 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 8a62cf4b48..990fe30900 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 25aafaac5f..03bcdd3a56 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 9b1f169e48..eb57328a1e 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; +use crate::{ + language::{Language, Tree}, + settings::{HighlightId, HighlightMap}, + time::{self, ReplicaId}, + util::Bias, + worktree::{File, Worktree}, +}; pub use anchor::*; +use anyhow::{anyhow, Result}; +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 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}, -}; -use anyhow::{anyhow, Result}; -use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; -use lazy_static::lazy_static; 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 681d135870..b6efc3fc9c 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 OperationQueue { +impl OperationKey { + pub fn new(timestamp: time::Lamport) -> Self { + Self(timestamp) + } +} + +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 58cd58fd3c..e9732e92c2 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 3bfa1f3d24..1c24bacafc 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 358730748b..e831d80042 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 445647010c..45af7e355c 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 5bae8b7a99..8ca2cfd9b6 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 d9a8aa889b..6c7ba34a15 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 fd3e06f295..16531144b8 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 08959b77a2..1d092a0da7 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 14d33b8e90..e390e84653 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 5b3fd5d0b5..8963e063b6 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 e90c377be1..2e9d5d9e88 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 3d50b70a57..f794927402 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 ae13b5d821..fc7fc941cf 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 fe9ff3c9b9..3c706cc379 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 a66778f8b7..686bfd7acc 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 1ed9204de4..8bec507d9f 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 72f755905c..5c0e7e247f 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 16baf6e00b..b6ce6d9e95 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 16531144b8..d96637ea86 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 2fc310e825..629a9824ff 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 cfc4d9cc6c..5c4f91e0ab 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 93d26f9656..2cf8f07b60 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 74ebccdf37..04567d4223 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 a2047a2540..57e40fff20 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 44b283dee1..297f04cb56 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 d96637ea86..82b3819149 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 0bcc0a4c29..f4ecdf48ff 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 a3a2cbf58c..38a32e5e8c 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 f6930c1ae8..449f6bb962 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 794debe4b1..1b4331bedc 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 86b1153424..40c9304a7a 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 57e40fff20..4e548ad03b 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 b3dc4f8a2b..f7ba4c1cb0 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", + DeleteToPreviousWordBoundary, 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", - 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", + SelectToBeginningOfLine(true), 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", - 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", + 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 297f04cb56..a3da936369 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 f4ecdf48ff..281ad99145 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 38a32e5e8c..da4bfa1ced 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 f7ba4c1cb0..829233b3cf 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 a3da936369..812bd795a7 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 281ad99145..093f5c7d4d 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 da4bfa1ced..4093664193 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 1b4331bedc..6593f9e3ef 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 9742ec015f..5b907df01e 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 d370f85a08..4d4cf13013 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 45af7e355c..d76a308e38 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 f087109c99..c35b2bf915 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 227f0b9efc..c43d72a87d 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 2853f938fa..506d11a278 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 7c133944f1..e5f4c95495 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 f99e2a3fd6..845f6e2521 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: &mut Pane, action: &CloseItem, cx| { + pane.close_item(action.0, 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: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 d5d0040009..d6bf992776 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 7d7e1f7c6e..1cd63bb2fb 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 093f5c7d4d..6148531e6e 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 6148531e6e..d21a1059ef 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 4093664193..b1082123bc 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 d21a1059ef..9c96b41097 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 82b3819149..d64b10cf4c 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 4e548ad03b..ebffa9653a 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 e390e84653..1156a434e1 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 308674d45d..e7cfa3b177 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 ebffa9653a..f282e4405e 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 d64b10cf4c..ecf407946b 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 ecf407946b..c506ef484a 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 c506ef484a..1adf10abcf 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 9c96b41097..30783b35f5 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 ec9f3f486f..57c2e79bd6 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 3576681cd3..6c861a31e5 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 e5f4c95495..35ed7fc467 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 1156a434e1..c3053c9fa1 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 57c2e79bd6..3ef6ed904a 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 35ed7fc467..69baa2283d 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 1cd63bb2fb..81807fd01a 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 1adf10abcf..467f53fd4f 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 30783b35f5..a40698a4dc 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 | 504 ++++++++++++++------------------------- zed/src/editor/buffer.rs | 10 +- 2 files changed, 190 insertions(+), 324 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index a40698a4dc..70116be8a8 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); - } - }); + self.subscribe_internal(handle, move |handle, event, cx| { + callback(handle, event, cx); + true + }) } - 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) + 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,28 +1865,6 @@ 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(); - self.app - .observations - .entry(handle.model_id) - .or_default() - .push(Observation::FromModel { - 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) { self.app .pending_effects @@ -2024,6 +1873,43 @@ impl<'a, T: Entity> ModelContext<'a, T> { }); } + 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 + .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,56 +2097,80 @@ 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 + 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, E::Event: 'static, - F: 'static + FnMut(&mut T, &E::Event, &mut ViewContext), + H: Handle, + F: 'static + FnMut(&mut T, H, &E::Event, &mut ViewContext), { + let subscriber = self.handle().downgrade(); 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); - }), + .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 + } }); } + fn observe(&mut self, handle: &H, mut callback: F) + where + E: Entity, + H: Handle, + F: 'static + FnMut(&mut T, H, &mut ViewContext), + { + 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, @@ -2268,52 +2178,6 @@ impl<'a, T: View> ViewContext<'a, T> { }); } - pub fn observe_model(&mut self, handle: &ModelHandle, mut callback: F) - where - S: Entity, - F: 'static + FnMut(&mut T, ModelHandle, &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, 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); - } - }), - }); - } - - pub fn observe_view(&mut self, handle: &ViewHandle, mut callback: F) - where - S: View, - F: 'static + FnMut(&mut T, ViewHandle, &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); - } - }), - }); - } - pub fn notify(&mut self) { self.app.notify_view(self.window_id, self.view_id); } @@ -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 eb57328a1e..624c84697a 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 467f53fd4f..d6101a5951 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 5b907df01e..f489bfb57d 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 fe683b728c..efb51eb239 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 3ef6ed904a..86a7762fac 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 829233b3cf..49259f554c 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 624c84697a..07ea5bacdc 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 4d4cf13013..50bddb3717 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 d76a308e38..2290d5cf28 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 c35b2bf915..e3e365d15e 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 6c861a31e5..9263e0750f 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 69baa2283d..0dffa81be9 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 6c7ba34a15..e121bc5d14 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 70116be8a8..85b04f8a6a 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 49259f554c..67b352adf9 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 50bddb3717..793d823c3c 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 506d11a278..2318f2b78c 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 0dffa81be9..f04e6dec1a 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 c384e27f27..380a5cee01 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 85b04f8a6a..89dd5582c0 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 67b352adf9..ba71c0ce60 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 07ea5bacdc..da3fd22280 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 0f0945a009..83b1575ab7 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 793d823c3c..50ddc48038 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 9263e0750f..6fac3c8bc9 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 2318f2b78c..bfe5977e2e 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 f04e6dec1a..28ed2701d1 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 d6101a5951..2e692f4a99 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 efb51eb239..148d541f03 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 86a7762fac..dfb4e545be 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 28ed2701d1..b6843fdc0b 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 f489bfb57d..a44738e8ff 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 148d541f03..a82b7d4aee 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 dfb4e545be..5e9b950b50 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 b6843fdc0b..28ed2701d1 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 f282e4405e..ac4652482c 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 5e9b950b50..458215e72c 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 ba71c0ce60..6ac06cd370 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 812bd795a7..85e54c71b9 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 | 44 ++++++++++++++++++-------------- 3 files changed, 87 insertions(+), 30 deletions(-) diff --git a/zed/src/channel.rs b/zed/src/channel.rs index a82b7d4aee..357546298c 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 8ca2cfd9b6..9e7a912ce8 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 e121bc5d14..7b0e212d8d 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?; + .spawn( + async move { + let (version, mtime) = save.await?; - rpc.respond( - receipt, - proto::BufferSaved { - worktree_id, - buffer_id, - version: (&version).into(), - mtime: Some(mtime.into()), - }, - ) - .await?; + rpc.respond( + receipt, + proto::BufferSaved { + worktree_id, + buffer_id, + version: (&version).into(), + mtime: Some(mtime.into()), + }, + ) + .await?; - Ok(()) - })) + 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 458215e72c..de4d183d5a 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 6ac06cd370..4aa5b6ce3e 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 2290d5cf28..d674fd5ea6 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 e3e365d15e..1e77edd151 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 c43d72a87d..9ba28f9cef 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 b8909a988a..b9992de663 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 bfe5977e2e..eb062899c1 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 28ed2701d1..4013b068a3 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 357546298c..f65320ae7f 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 b9992de663..4b1ab0e66e 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 f65320ae7f..37459a1215 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 de4d183d5a..d722d5add9 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); + 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); } - 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)); - } - } 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 1e77edd151..7eaa5ea838 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 4b1ab0e66e..ee1ff2fbb6 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 89dd5582c0..8c8fd24485 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 03bcdd3a56..13bc411e3b 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 37459a1215..42600781c9 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 d722d5add9..5758879cd1 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 0bcbfd4d65..330e3afa48 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 e9f84676e7..ab387b65bb 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 aea73eeaa2..668019e696 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 42600781c9..2eea0d2a99 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 2eea0d2a99..a70b7dd068 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 a8111d8b0a..f2425b5e2f 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -1,31 +1,20 @@ -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}; +use std::collections::HashMap; + +pub use highlight_map::*; +pub use theme_registry::*; -const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); pub const DEFAULT_THEME_NAME: &'static str = "dark"; -pub struct ThemeRegistry { - assets: Box, - themes: Mutex>>, - theme_data: Mutex>>, -} - -#[derive(Clone, Debug)] -pub struct HighlightMap(Arc<[HighlightId]>); - -#[derive(Clone, Copy, Debug)] -pub struct HighlightId(u32); - #[derive(Debug, Default, Deserialize)] pub struct Theme { #[serde(default)] @@ -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 0000000000..55a053113c --- /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 0000000000..0b68d8a0c9 --- /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 cf340282d5..021ba25789 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 f2425b5e2f..c99cd0e39c 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 845f6e2521..ab74cac191 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 81807fd01a..8bb3f06fc1 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 021ba25789..1714558494 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 c99cd0e39c..a502f2ae67 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 1714558494..56dbbdaaf1 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 5758879cd1..7788ab2eba 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 a502f2ae67..0bd5b3593d 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 2e692f4a99..78f8c2316e 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 97ac2769e7..106e55a55f 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 e831d80042..1d5359eabf 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 57556d61b6..d9103498c4 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 106e55a55f..696a2ac2dd 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 c3053c9fa1..d7d042617f 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 0000000000..78e9d0d3a4 --- /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 78e9d0d3a4..55f6344708 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 426b99d932..ec4cfc74fe 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 be3d47e42a..b6b29c39e2 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 55f6344708..4c6d0559e6 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 d9103498c4..e19f82ed55 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 83b1575ab7..e118f3202d 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 696a2ac2dd..0000000000 --- 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 1d5359eabf..c8c8f067ca 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 4c6d0559e6..5d30fe03fc 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 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, + }; - // 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) + (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 ac4652482c..030b8262cc 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 e19f82ed55..9d171e180c 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 9d171e180c..8591dbe7f2 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 56dbbdaaf1..6fed76f81a 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 7788ab2eba..30ce44b829 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 0bd5b3593d..13fa534211 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 5d30fe03fc..45aaab4657 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 45aaab4657..0459a68829 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 75ee206b35..b1deb802fd 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 030b8262cc..80739a8be6 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 8591dbe7f2..71db399476 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 c8c8f067ca..3ba9690c64 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 8c8fd24485..5d1fdc3a49 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 78f8c2316e..b4213fc5a5 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 30ce44b829..0c4fbd584f 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 1e489aae36..2ae8fc8f1d 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 a44738e8ff..c869dd1aea 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 a70b7dd068..73eb015884 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 d674fd5ea6..d451c187d5 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 7eaa5ea838..2258f74686 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 6fac3c8bc9..f406df1946 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 0000000000..df98707a8e --- /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 ec4cfc74fe..9da817877e 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 13bc411e3b..a9cdb77675 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 6fed76f81a..fbd31f4d97 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 73eb015884..37d75a4c91 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 0c4fbd584f..582c5b436a 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 13fa534211..3a8430c4be 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 eb062899c1..5dc3ae448a 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 4013b068a3..e037460891 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 2ae8fc8f1d..7c063e140f 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 c869dd1aea..0822ddbef1 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 7c063e140f..2f1cbc5fba 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,38 +383,25 @@ 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) + .fetch_all(&self.pool) .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) - .await - .unwrap(); - self.db.close().await; - }) - } } macro_rules! id_type { @@ -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 0822ddbef1..1f329f5219 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 5d1fdc3a49..af5746043e 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 80739a8be6..6e3487d76d 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 5dc3ae448a..867c5b7693 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 af5746043e..1e531d3692 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 d7d042617f..c533b938a3 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 b4213fc5a5..98ab96c427 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 e7cfa3b177..10877e33a0 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 37d75a4c91..02352ec00d 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 582c5b436a..0573666f9d 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 98ab96c427..6e015b721c 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 fbd31f4d97..92d3793bb0 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 | 187 ++++++++++++++++++------- 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 | 103 +++++++------- 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, 438 insertions(+), 397 deletions(-) diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index 5c0e7e247f..1ad5d83671 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,37 +15,32 @@ 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 Label { - pub fn new(text: String, family_id: FamilyId, font_size: f32) -> Self { - Self { +impl From for LabelStyle { + fn from(text: TextStyle) -> Self { + LabelStyle { text, - family_id, - font_size, - highlight_indices: Default::default(), - style: Default::default(), + highlight_text: None, } } +} - 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 +impl Label { + pub fn new(text: String, style: impl Into) -> Self { + Self { + text, + highlight_indices: Default::default(), + style: style.into(), + } } pub fn with_highlights(mut self, indices: Vec) -> Self { @@ -55,11 +48,8 @@ impl Label { 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 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 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 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 0459a68829..6978542071 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 ab387b65bb..3010e1ada0 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,24 +46,117 @@ 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, +} + +#[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 HighlightStyle { + fn from(color: Color) -> Self { + Self { + color, + font_properties: Default::default(), + } + } +} + impl<'de> Deserialize<'de> for TextStyle { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - let json = Value::deserialize(deserializer)?; + 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<'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() { - let style_json: TextStyleJson = - serde_json::from_value(json).map_err(de::Error::custom)?; - Ok(style_json.into()) + Ok(Self::from_json( + serde_json::from_value(json).map_err(de::Error::custom)?, + )) } else { Ok(Self { color: serde_json::from_value(json).map_err(de::Error::custom)?, @@ -70,47 +166,20 @@ impl<'de> Deserialize<'de> for TextStyle { } } -impl From for TextStyle { - fn from(color: Color) -> Self { - Self { - color, - font_properties: Default::default(), - } - } -} - -impl ToJson for TextStyle { - fn to_json(&self) -> Value { - json!({ - "color": self.color.to_json(), - "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 - } else { - Style::Normal - }; - TextStyle { - color: self.color, - font_properties: *Properties::new().weight(weight).style(style), - } - } +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 { @@ -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 1f329f5219..41f5efb51b 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 92d3793bb0..06b6346531 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 b019057f65..ce44b87e96 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 380a5cee01..6702c18f1c 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 0573666f9d..2572acc12a 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 4aa5b6ce3e..751d687403 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 e118f3202d..e5f18b56db 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 3ba9690c64..8720e39979 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 e42a63f763..0e244995e7 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 50ddc48038..76c2f71dc8 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 886befe595..f6342bf254 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 2258f74686..d1361870b8 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 a6b9e667c1..dba30749b4 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 f406df1946..d8b30afa69 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 3a8430c4be..0346eba0e8 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()], - } - } -} +impl<'de> Deserialize<'de> for SyntaxTheme { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut syntax_data: HashMap = + Deserialize::deserialize(deserializer)?; -pub fn deserialize_syntax_theme<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let mut result = Vec::<(String, TextStyle)>::new(); + 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)?; - for (key, style) in syntax_data { - match result.binary_search_by(|(needle, _)| needle.cmp(&key)) { - Ok(i) | Err(i) => { - result.insert(i, (key, style)); + 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 55a053113c..c030e2ab1a 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 0b68d8a0c9..da6d097c31 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 867c5b7693..2b0eea969e 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 e037460891..e5785028a3 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 ab74cac191..1be0fccd58 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 9da817877e..3f041b7acd 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 a9cdb77675..b77ed8caff 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 da6d097c31..0808281f09 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 2572acc12a..d3b4260482 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 751d687403..2e220592bd 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 06b6346531..08d618aaed 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 ce44b87e96..9a8ac975bf 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 02352ec00d..f1f2e07607 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 d3b4260482..03630ac6c8 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 0346eba0e8..bb9b4b97ab 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 b6ce6d9e95..cf8f6ca605 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 e5f18b56db..2758c030b3 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 76c2f71dc8..3cf539909e 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 dba30749b4..1cadfdaa9c 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 1be0fccd58..7e6e2592bc 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 8bb3f06fc1..242b557c39 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 1e531d3692..a37e6e4157 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 03630ac6c8..140ddcf28d 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 e5785028a3..2d7f44fd01 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 242b557c39..f0bafd5472 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 629a9824ff..48deef18a2 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 2d7f44fd01..db35a977f1 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 f0bafd5472..e6af4afeed 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 b1deb802fd..3c11b9659c 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 08d618aaed..8900c34fb0 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 bb9b4b97ab..3ecdb94baa 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 db35a977f1..5e10b551f6 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 e6af4afeed..fd660bfdbb 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 fd660bfdbb..d726386801 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 6e3487d76d..d03e646279 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 a37e6e4157..0aa85a33b0 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 d03e646279..06d1626919 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 1d092a0da7..b76d31622d 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 0aa85a33b0..4582dc0727 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 6e015b721c..b6773091c0 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 48deef18a2..7512fe43b4 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 06d1626919..92f47b0399 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 41f5efb51b..00464450fa 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 140ddcf28d..37409800fe 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 2e220592bd..e076c22ae4 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 3cf539909e..464b7e3589 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 552eab851c..5793b75618 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 2b0eea969e..a20ea8f758 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 5e10b551f6..4d6e0b6b75 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 7e6e2592bc..34f44253f3 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 d726386801..3c02907067 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 4582dc0727..d9caf3e6d2 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 7512fe43b4..9973315454 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 449f6bb962..9262d99ec7 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 6593f9e3ef..861984a247 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 40c9304a7a..b66a8d5043 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 3c02907067..739aecf46c 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 9973315454..9336c8b84a 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 15ee7172f6..fba7b812e6 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 d3f9d9accd..04712d599f 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 92f47b0399..c2c670f1e7 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 | 205 +++++++++++++++++++++++++++----------- zed/src/chat_panel.rs | 17 +++- zrpc/proto/zed.proto | 13 +++ zrpc/src/proto.rs | 3 + 8 files changed, 281 insertions(+), 68 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index b6773091c0..c7950482c3 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 d6e3f81615..47b29b5d02 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 2f1cbc5fba..f61f5d82b4 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 00464450fa..b74b923b0f 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 f1f2e07607..f0f9d4f43c 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,29 +354,51 @@ 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 { fn from(message: proto::Channel) -> Self { Self { @@ -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 37409800fe..a8b4ed94bf 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 f368cb2d47..a94c0f6204 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 330e3afa48..3743a06a07 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 d9caf3e6d2..69591d8052 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 9262d99ec7..21679ae877 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 f12fe38ce6..bcdb61241c 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 b66a8d5043..3295fb6382 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 6702c18f1c..564542817b 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 f61f5d82b4..374761a73c 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 b74b923b0f..c712ced835 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 | 68 +++++++++++++-------------- zed/src/chat_panel.rs | 7 ++- zrpc/proto/zed.proto | 61 +++++++++++++----------- zrpc/src/peer.rs | 27 ++++++++++- zrpc/src/proto.rs | 1 + 6 files changed, 190 insertions(+), 81 deletions(-) diff --git a/server/src/rpc.rs b/server/src/rpc.rs index c712ced835..696487fdca 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 f0f9d4f43c..6bac81fa19 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?; + 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(()) - }) - } - .log_err() - }) - .detach(); - cx.notify(); - Ok(()) + 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 a8b4ed94bf..e94f5712e5 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 a94c0f6204..123fc0f1da 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 06d4b01ae0..9a7954341b 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 3743a06a07..002c5bc840 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 696487fdca..5a0199b725 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 6bac81fa19..858bbb5ce1 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 82e80790cb..d125fe1366 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 3818a08701..d974db73e1 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 c533b938a3..8b0c338a42 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 0000000000..25d3a8d3da --- /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 | 93 ++++++++++++++++++++++++------------ 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/gpui/src/elements/overlay.rs b/gpui/src/elements/overlay.rs index 25d3a8d3da..a4c44a0013 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 d974db73e1..4edf58c40f 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_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_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_foreground_layer(&mut self, clip_bounds: Option) { - self.pending_foreground_layers += 1; - self.push_layer(clip_bounds); + 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 69591d8052..c15268c102 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 c7950482c3..bfbb6a7b73 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 c2c670f1e7..31c91688db 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 b76d31622d..123481b991 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 c15268c102..edfed5af1d 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 bfbb6a7b73..5c1a3495e3 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 5a0199b725..8a7f2d43af 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 e94f5712e5..20b6b52525 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 e076c22ae4..64aa3ed430 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 464b7e3589..c31d617201 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 5793b75618..796441c704 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 a20ea8f758..e1e4a84c8a 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 4d6e0b6b75..8fd1d60010 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 34f44253f3..5d78b9a20e 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 10877e33a0..6cb1c6f39d 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 0000000000..470d1d0190 --- /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 0000000000..a97aa201c8 --- /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 edfed5af1d..5b6d3d87a3 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 9336c8b84a..c9d9bb7a01 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 04567d4223..62490e6f9b 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 470d1d0190..73f8a57518 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 a97aa201c8..3a40133665 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 8900c34fb0..abc886a525 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 20b6b52525..0eef94529c 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 3010e1ada0..450d0ac398 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 b92a1a5534..f4b7be73d5 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 9a8ac975bf..0356847613 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 564542817b..e70a088cc9 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 6b52f7edfd..dd2a376510 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 c240da268d..148b38eb0a 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 2758c030b3..16eeba2e12 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 b114400055..b0a8d97e5d 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 c31d617201..cd74709421 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 e1233314ca..ee39681459 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 c030e2ab1a..202e0d38be 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 e1e4a84c8a..714e84b03f 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 450d0ac398..3010e1ada0 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 f4b7be73d5..1beab95ce7 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 dd2a376510..8c6b85bbf1 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 148b38eb0a..b8c9601fe2 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 16eeba2e12..425c4b97a5 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 ee39681459..dcbec3ff2d 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 1beab95ce7..2d0cfd1236 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 8c6b85bbf1..77c5a7777d 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 dcbec3ff2d..e15070d818 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 3d57c96c4f..e415551a8f 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 58e603fc6f..a71407cc25 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 e45c5d7d1b..f5c9e0e58e 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 25afac91b0..3894ad4cbd 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 e415551a8f..7dbc30dfbf 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 abfcadc042..7acd5cadad 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 0000000000..6d1e86a217 --- /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 7dbc30dfbf..dca3687240 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 04435bb36e..c82d8aa3d6 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 012c43647e..0bd0fe8a77 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 cd74709421..b7394cd553 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 714e84b03f..28fea8f6c0 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 28fea8f6c0..7dc54951a8 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 b8c9601fe2..71a9174f2e 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 71a9174f2e..38e9a3ac3e 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 b0a8d97e5d..a14356819d 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 6d1e86a217..9337578297 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 dca3687240..afe4b328f2 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 a71407cc25..73f3a86373 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 7acd5cadad..6d7429222c 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 9337578297..994d5fe281 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 afe4b328f2..71946b50ac 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 77c5a7777d..2b5570ca91 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 8ccac320a5..195dc5fb8d 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 89169c0efd..562da86fee 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 3610957634..c1ce8fdba0 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 b58ad381a2..2aad986a66 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 ba9e3ae3cf..8f61bfab30 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 fec5bec74d..b73c70102a 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 a53be8ed95..4e79af8c60 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 41f2638027..ba54f05f1c 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 17314ebd65..985901c50c 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 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 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 0356847613..74688fdb0d 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 e70a088cc9..94274997a1 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 e7c0103421..c0f3a1fbfc 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 f6342bf254..bcea6c25ad 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 d1361870b8..3603e760eb 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 1cadfdaa9c..6b7defe136 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 73f3a86373..e2bd7eb1c9 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 2aad986a66..078bf79705 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 4d81fe964a..7015cbc713 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 004b1f94a9..85afff4999 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 ee1ff2fbb6..6ec07c90f6 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 2b5570ca91..5f4f3e13c7 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 38e9a3ac3e..39c588f8d1 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 a14356819d..791c6a5be0 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 39c588f8d1..7d13628f8a 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 7d13628f8a..6176b402c5 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 6176b402c5..63ebe9982f 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 078bf79705..ba237d4788 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 8f61bfab30..4b2716c044 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 63ebe9982f..2d4b763776 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 3603e760eb..5ad05738e6 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 6b7defe136..8893e8bd9f 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 d8b30afa69..b917e428f6 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 0808281f09..cd9781afe9 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 da3fd22280..f353a48c42 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 1c24bacafc..8aa01495e8 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 | 113 +++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index f353a48c42..c2ba280be6 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 + self.parsing_in_background } - 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() - } - } - - 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())); + // 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) } + }); - // 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(); - - 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, }); - - if !parse_again { - break; - } + self.parse_count += 1; + cx.emit(Event::Reparsed); + cx.notify(); + return true; } - }) - .detach(); + 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; + } + + cx.emit(Event::Reparsed); + cx.notify(); + }); + }) + .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 32086b7fda..6c82b2d88a 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 917281d8c6..8dbc76a65b 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 400c9d5997..623af72af6 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 3010e1ada0..96248c1675 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 ba237d4788..dec7db9103 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 4b2716c044..c01700ce22 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 d668966dcc..1f710ac311 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 2d0cfd1236..67ed95f966 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 74688fdb0d..6f7a8d6e80 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 2d4b763776..a19b63c5ce 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 e15070d818..c8b2c61388 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 d125fe1366..d215eaf77c 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 0bd0fe8a77..401918c5fe 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 1f710ac311..692efaf424 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 3276182cd6..116be75842 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 8dbc76a65b..acfbb5abd9 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 692efaf424..a553b3b478 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 a553b3b478..c74949b998 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 d215eaf77c..e12a52d613 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 67ed95f966..cb0ea42fbc 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 5f4f3e13c7..fe1ac5ba8a 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 c8b2c61388..84748e4b0c 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 f5c9e0e58e..ff87c4b42c 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 71946b50ac..b6de41b92c 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 611220568b..34f5f378d9 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 e3cb5d29d6..24997d4964 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 fe1ac5ba8a..c8aaac4f38 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 d451c187d5..e7bb31b207 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 6ec07c90f6..7c0302834d 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 ff87c4b42c..95050ca607 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 5ad05738e6..a7dc346e36 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 dec7db9103..a4c86eab2f 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 bcdb61241c..e960107a99 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 95050ca607..1b7b9226d0 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 cb0ea42fbc..08fb222f6d 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 76b7e2b729..99c21b2230 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 3765c806c8..590aad67eb 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 6f168a9a91..18623704e2 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 \