From 4bbfd0918eb78e0310b4aad1893d71e1437cb1d5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 16:50:29 +0100 Subject: [PATCH 01/56] Start defining follow protocol Co-Authored-By: Nathan Sobo --- crates/project/src/project.rs | 8 ++-- crates/rpc/proto/zed.proto | 74 +++++++++++++++++++++++++---------- crates/rpc/src/proto.rs | 9 +++-- crates/server/src/rpc.rs | 2 +- 4 files changed, 65 insertions(+), 28 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 317cf1ba028d19b920d04c4a70094adddb932b97..c44364adac96dc0eaa0243b8014d240ac68b5a5f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -279,7 +279,7 @@ impl Project { client.add_entity_request_handler(Self::handle_search_project); client.add_entity_request_handler(Self::handle_get_project_symbols); client.add_entity_request_handler(Self::handle_open_buffer_for_symbol); - client.add_entity_request_handler(Self::handle_open_buffer); + client.add_entity_request_handler(Self::handle_open_buffer_by_path); client.add_entity_request_handler(Self::handle_save_buffer); } @@ -930,7 +930,7 @@ impl Project { let path_string = path.to_string_lossy().to_string(); cx.spawn(|this, mut cx| async move { let response = rpc - .request(proto::OpenBuffer { + .request(proto::OpenBufferByPath { project_id, worktree_id: remote_worktree_id.to_proto(), path: path_string, @@ -3887,9 +3887,9 @@ impl Project { hasher.finalize().as_slice().try_into().unwrap() } - async fn handle_open_buffer( + async fn handle_open_buffer_by_path( this: ModelHandle, - envelope: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d0cb621ab92457adf9942a5cc1287f17d2572f99..3cc5a91cbeff89d906272b4e445a2eabb84e2749 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -40,8 +40,9 @@ message Envelope { StartLanguageServer start_language_server = 33; UpdateLanguageServer update_language_server = 34; - OpenBuffer open_buffer = 35; - OpenBufferResponse open_buffer_response = 36; + OpenBufferById open_buffer_by_id = 35; + OpenBufferByPath open_buffer_by_path = 36; + OpenBufferResponse open_buffer_response = 37; UpdateBuffer update_buffer = 38; UpdateBufferFile update_buffer_file = 39; SaveBuffer save_buffer = 40; @@ -79,6 +80,10 @@ message Envelope { GetUsers get_users = 70; GetUsersResponse get_users_response = 71; + + Follow follow = 72; + FollowResponse follow_response = 73; + UpdateFollower update_follower = 74; } } @@ -241,12 +246,17 @@ message OpenBufferForSymbolResponse { Buffer buffer = 1; } -message OpenBuffer { +message OpenBufferByPath { uint64 project_id = 1; uint64 worktree_id = 2; string path = 3; } +message OpenBufferById { + uint64 project_id = 1; + uint64 id = 2; +} + message OpenBufferResponse { Buffer buffer = 1; } @@ -521,8 +531,49 @@ message UpdateContacts { repeated Contact contacts = 1; } +message UpdateDiagnostics { + uint32 replica_id = 1; + uint32 lamport_timestamp = 2; + repeated Diagnostic diagnostics = 3; +} + +message Follow {} + +message FollowResponse { + uint64 current_view_id = 1; + repeated View views = 2; +} + +message UpdateFollower { + uint64 current_view_id = 1; + repeated ViewUpdate view_updates = 2; +} + // Entities +message View { + uint64 id = 1; + oneof variant { + Editor editor = 2; + } + + message Editor { + uint64 buffer_id = 1; + Selection newest_selection = 2; + } +} + +message ViewUpdate { + uint64 id = 1; + oneof variant { + Editor editor = 2; + } + + message Editor { + Selection newest_selection = 1; + } +} + message Collaborator { uint32 peer_id = 1; uint32 replica_id = 2; @@ -578,17 +629,6 @@ message BufferState { repeated string completion_triggers = 8; } -message BufferFragment { - uint32 replica_id = 1; - uint32 local_timestamp = 2; - uint32 lamport_timestamp = 3; - uint32 insertion_offset = 4; - uint32 len = 5; - bool visible = 6; - repeated VectorClockEntry deletions = 7; - repeated VectorClockEntry max_undos = 8; -} - message SelectionSet { uint32 replica_id = 1; repeated Selection selections = 2; @@ -614,12 +654,6 @@ enum Bias { Right = 1; } -message UpdateDiagnostics { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated Diagnostic diagnostics = 3; -} - message Diagnostic { Anchor start = 1; Anchor end = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 54b26b830ca584f74110c74e5b18d3371d092c18..230db3119c3ff76b6a7f2897d8c0a32712156003 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -175,7 +175,8 @@ messages!( (UpdateLanguageServer, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), - (OpenBuffer, Background), + (OpenBufferById, Background), + (OpenBufferByPath, Background), (OpenBufferForSymbol, Background), (OpenBufferForSymbolResponse, Background), (OpenBufferResponse, Background), @@ -223,7 +224,8 @@ request_messages!( (GetUsers, GetUsersResponse), (JoinChannel, JoinChannelResponse), (JoinProject, JoinProjectResponse), - (OpenBuffer, OpenBufferResponse), + (OpenBufferById, OpenBufferResponse), + (OpenBufferByPath, OpenBufferResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse), (Ping, Ack), (PerformRename, PerformRenameResponse), @@ -255,7 +257,8 @@ entity_messages!( GetProjectSymbols, JoinProject, LeaveProject, - OpenBuffer, + OpenBufferById, + OpenBufferByPath, OpenBufferForSymbol, PerformRename, PrepareRename, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 7e9bb38021809685565033e5c007e07f4c2cf97a..948901c2a10c3141be12c3885492bdea0520c2fc 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -92,7 +92,7 @@ impl Server { .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) - .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler( Server::forward_project_request::, From 2b4738d82dc4c00827b7fffd3894aa25c592b6e6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 17:34:35 +0100 Subject: [PATCH 02/56] Avoid passing a closure to `workspace::register_project_item` Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 4 +--- crates/workspace/src/workspace.rs | 11 +++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 30888d8a408ada78170e6dd6a39979392e085fe4..1ae9fdce27a9613c92ad6e2e1184e2de1304ef87 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -340,9 +340,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::confirm_rename); cx.add_async_action(Editor::find_all_references); - workspace::register_project_item(cx, |project, buffer, cx| { - Editor::for_buffer(buffer, Some(project), cx) - }); + workspace::register_project_item::(cx); } trait SelectionExt { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 33155b5d4f4710338d6c4d4570af129f21e92624..df771a700a84e0c63065a35032cf92d4634103c7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -108,17 +108,16 @@ pub fn init(cx: &mut MutableAppContext) { ]); } -pub fn register_project_item(cx: &mut MutableAppContext, build_item: F) +pub fn register_project_item(cx: &mut MutableAppContext) where V: ProjectItem, - F: 'static + Fn(ModelHandle, ModelHandle, &mut ViewContext) -> V, { cx.update_default_global(|builders: &mut ItemBuilders, _| { builders.insert( TypeId::of::(), Arc::new(move |window_id, project, model, cx| { - let model = model.downcast::().unwrap(); - Box::new(cx.add_view(window_id, |cx| build_item(project, model, cx))) + let item = model.downcast::().unwrap(); + Box::new(cx.add_view(window_id, |cx| V::for_project_item(project, item, cx))) }), ); }); @@ -813,13 +812,13 @@ impl Workspace { let pane = self.active_pane().downgrade(); let task = self.load_path(path, cx); cx.spawn(|this, mut cx| async move { - let (project_entry_id, build_editor) = task.await?; + let (project_entry_id, build_item) = task.await?; let pane = pane .upgrade(&cx) .ok_or_else(|| anyhow!("pane was closed"))?; this.update(&mut cx, |_, cx| { pane.update(cx, |pane, cx| { - Ok(pane.open_item(project_entry_id, cx, build_editor)) + Ok(pane.open_item(project_entry_id, cx, build_item)) }) }) }) From 9716ff79648353a13f8109478947e4443c3e112c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Mar 2022 10:46:54 -0700 Subject: [PATCH 03/56] Set up logic for starting following Co-Authored-By: Antonio Scandurra --- crates/rpc/proto/zed.proto | 22 ++++-- crates/rpc/src/proto.rs | 8 +++ crates/workspace/src/pane.rs | 24 +++++++ crates/workspace/src/workspace.rs | 116 ++++++++++++++++++++++++------ 4 files changed, 142 insertions(+), 28 deletions(-) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3cc5a91cbeff89d906272b4e445a2eabb84e2749..d7a928e424e469b7abb63c288d18e7bd8ee2e19b 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -83,7 +83,8 @@ message Envelope { Follow follow = 72; FollowResponse follow_response = 73; - UpdateFollower update_follower = 74; + UpdateFollowers update_followers = 74; + Unfollow unfollow = 75; } } @@ -537,16 +538,27 @@ message UpdateDiagnostics { repeated Diagnostic diagnostics = 3; } -message Follow {} +message Follow { + uint64 project_id = 1; + uint32 leader_id = 2; +} message FollowResponse { uint64 current_view_id = 1; repeated View views = 2; } -message UpdateFollower { - uint64 current_view_id = 1; - repeated ViewUpdate view_updates = 2; +message UpdateFollowers { + uint64 project_id = 1; + uint64 current_view_id = 2; + repeated View created_views = 3; + repeated ViewUpdate updated_views = 4; + repeated uint32 follower_ids = 5; +} + +message Unfollow { + uint64 project_id = 1; + uint32 leader_id = 2; } // Entities diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 230db3119c3ff76b6a7f2897d8c0a32712156003..39a0d669d5747673e5640f1939d57351ddadf4fe 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -147,6 +147,8 @@ messages!( (BufferSaved, Foreground), (ChannelMessageSent, Foreground), (Error, Foreground), + (Follow, Foreground), + (FollowResponse, Foreground), (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), (GetChannelMessages, Foreground), @@ -196,6 +198,7 @@ messages!( (SendChannelMessageResponse, Foreground), (ShareProject, Foreground), (Test, Foreground), + (Unfollow, Foreground), (UnregisterProject, Foreground), (UnregisterWorktree, Foreground), (UnshareProject, Foreground), @@ -203,6 +206,7 @@ messages!( (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), (UpdateDiagnosticSummary, Foreground), + (UpdateFollowers, Foreground), (UpdateWorktree, Foreground), ); @@ -212,6 +216,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), + (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), (GetChannels, GetChannelsResponse), @@ -248,6 +253,7 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, + Follow, FormatBuffers, GetCodeActions, GetCompletions, @@ -266,11 +272,13 @@ entity_messages!( SaveBuffer, SearchProject, StartLanguageServer, + Unfollow, UnregisterWorktree, UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, + UpdateFollowers, UpdateLanguageServer, RegisterWorktree, UpdateWorktree, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 57a74b52ef405bcbe0457536caa852187cfe8c2c..ec27d2400ed0c3b7a86f68cbc6e22c45e36de892 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,5 +1,7 @@ use super::{ItemHandle, SplitDirection}; use crate::{Item, Settings, WeakItemHandle, Workspace}; +use anyhow::{anyhow, Result}; +use client::PeerId; use collections::{HashMap, VecDeque}; use gpui::{ action, @@ -105,6 +107,13 @@ pub struct Pane { active_toolbar_visible: bool, } +pub(crate) struct FollowerState { + pub(crate) leader_id: PeerId, + pub(crate) current_view_id: usize, + pub(crate) items_by_leader_view_id: + HashMap, Box)>, +} + pub trait Toolbar: View { fn active_item_changed( &mut self, @@ -313,6 +322,21 @@ impl Pane { cx.notify(); } + pub(crate) fn set_follow_state( + &mut self, + follower_state: FollowerState, + cx: &mut ViewContext, + ) -> Result<()> { + let current_view_id = follower_state.current_view_id as usize; + let (project_entry_id, item) = follower_state + .items_by_leader_view_id + .get(¤t_view_id) + .ok_or_else(|| anyhow!("invalid current view id"))? + .clone(); + self.add_item(project_entry_id, item, cx); + Ok(()) + } + pub fn items(&self) -> impl Iterator> { self.items.iter().map(|(_, view)| view) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index df771a700a84e0c63065a35032cf92d4634103c7..80f5b4e7a1e579fa713f706e09833b6986be3e3f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7,7 +7,7 @@ pub mod sidebar; mod status_bar; use anyhow::{anyhow, Result}; -use client::{Authenticate, ChannelList, Client, User, UserStore}; +use client::{proto, Authenticate, ChannelList, Client, PeerId, User, UserStore}; use clock::ReplicaId; use collections::HashMap; use gpui::{ @@ -42,16 +42,18 @@ use std::{ }; use theme::{Theme, ThemeRegistry}; -type ItemBuilders = HashMap< +type ProjectItemBuilders = HashMap< TypeId, - Arc< - dyn Fn( - usize, - ModelHandle, - AnyModelHandle, - &mut MutableAppContext, - ) -> Box, - >, + fn(usize, ModelHandle, AnyModelHandle, &mut MutableAppContext) -> Box, +>; + +type FollowedItemBuilders = Vec< + fn( + ViewHandle, + ModelHandle, + &mut Option, + &mut MutableAppContext, + ) -> Option, Box)>>>, >; action!(Open, Arc); @@ -108,18 +110,18 @@ pub fn init(cx: &mut MutableAppContext) { ]); } -pub fn register_project_item(cx: &mut MutableAppContext) -where - V: ProjectItem, -{ - cx.update_default_global(|builders: &mut ItemBuilders, _| { - builders.insert( - TypeId::of::(), - Arc::new(move |window_id, project, model, cx| { - let item = model.downcast::().unwrap(); - Box::new(cx.add_view(window_id, |cx| V::for_project_item(project, item, cx))) - }), - ); +pub fn register_project_item(cx: &mut MutableAppContext) { + cx.update_default_global(|builders: &mut ProjectItemBuilders, _| { + builders.insert(TypeId::of::(), |window_id, project, model, cx| { + let item = model.downcast::().unwrap(); + Box::new(cx.add_view(window_id, |cx| I::for_project_item(project, item, cx))) + }); + }); +} + +pub fn register_followed_item(cx: &mut MutableAppContext) { + cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { + builders.push(I::for_state_message) }); } @@ -214,6 +216,17 @@ pub trait ProjectItem: Item { ) -> Self; } +pub trait FollowedItem: Item { + type UpdateMessage; + + fn for_state_message( + pane: ViewHandle, + project: ModelHandle, + state: &mut Option, + cx: &mut MutableAppContext, + ) -> Option, Box)>>>; +} + pub trait ItemHandle: 'static { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; @@ -840,7 +853,7 @@ impl Workspace { cx.as_mut().spawn(|mut cx| async move { let (project_entry_id, project_item) = project_item.await?; let build_item = cx.update(|cx| { - cx.default_global::() + cx.default_global::() .get(&project_item.model_type()) .ok_or_else(|| anyhow!("no item builder for project item")) .cloned() @@ -990,6 +1003,63 @@ impl Workspace { }); } + pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Task> { + if let Some(project_id) = self.project.read(cx).remote_id() { + let request = self.client.request(proto::Follow { + project_id, + leader_id: leader_id.0, + }); + cx.spawn_weak(|this, mut cx| async move { + let mut response = request.await?; + if let Some(this) = this.upgrade(&cx) { + let mut item_tasks = Vec::new(); + let (project, pane) = this.read_with(&cx, |this, _| { + (this.project.clone(), this.active_pane().clone()) + }); + for view in &mut response.views { + let variant = view + .variant + .take() + .ok_or_else(|| anyhow!("missing variant"))?; + cx.update(|cx| { + let mut variant = Some(variant); + for build_item in cx.default_global::().clone() { + if let Some(task) = + build_item(pane.clone(), project.clone(), &mut variant, cx) + { + item_tasks.push(task); + break; + } else { + assert!(variant.is_some()); + } + } + }); + } + + let items = futures::future::try_join_all(item_tasks).await?; + let mut items_by_leader_view_id = HashMap::default(); + for (view, item) in response.views.into_iter().zip(items) { + items_by_leader_view_id.insert(view.id as usize, item); + } + + pane.update(&mut cx, |pane, cx| { + pane.set_follow_state( + FollowerState { + leader_id, + current_view_id: response.current_view_id as usize, + items_by_leader_view_id, + }, + cx, + ) + })?; + } + Ok(()) + }) + } else { + Task::ready(Err(anyhow!("project is not remote"))) + } + } + fn render_connection_status(&self, cx: &mut RenderContext) -> Option { let theme = &cx.global::().theme; match &*self.client.status().borrow() { From 845457e2c46c6d921433930478b873986c008d1d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Mar 2022 10:58:20 -0700 Subject: [PATCH 04/56] Always read project entry id from workspace::Item We cannot store a workspace item's project entry id separately, since buffers' entry ids can change (for example when doing a *save as*). Co-Authored-By: Antonio Scandurra --- crates/diagnostics/src/diagnostics.rs | 4 ++ crates/editor/src/items.rs | 6 ++- crates/search/src/project_search.rs | 4 ++ crates/workspace/src/pane.rs | 70 +++++++++++++-------------- crates/workspace/src/workspace.rs | 17 ++++--- 5 files changed, 59 insertions(+), 42 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index a1dd7be744ae4c2f79ee78075f620c60595ea640..7bee815ad92523ef84f768c1686fe110fd0e8564 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -450,6 +450,10 @@ impl workspace::Item for ProjectDiagnosticsEditor { None } + fn project_entry_id(&self, _: &AppContext) -> Option { + None + } + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { self.editor .update(cx, |editor, cx| editor.navigate(data, cx)); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6f083f00a91eb1c18504be4e3c3efcd17f8ac866..d865511a62eb6b06105a3624e333db4d5f8f43c6 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -5,7 +5,7 @@ use gpui::{ ViewContext, ViewHandle, }; use language::{Bias, Buffer, Diagnostic, File as _}; -use project::{File, Project, ProjectPath}; +use project::{File, Project, ProjectEntryId, ProjectPath}; use std::fmt::Write; use std::path::PathBuf; use text::{Point, Selection}; @@ -41,6 +41,10 @@ impl Item for Editor { }) } + fn project_entry_id(&self, cx: &AppContext) -> Option { + File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx)) + } + fn clone_on_split(&self, cx: &mut ViewContext) -> Option where Self: Sized, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ef8ff3611ae675df8f7f86969b3c8adeaa9161cc..f027c965c631f0e9f83d5ba49d82eff28366004d 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -250,6 +250,10 @@ impl Item for ProjectSearchView { None } + fn project_entry_id(&self, _: &AppContext) -> Option { + None + } + fn can_save(&self, _: &gpui::AppContext) -> bool { true } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ec27d2400ed0c3b7a86f68cbc6e22c45e36de892..c3b6a5fd29b4742097cee6412dffb44c946d866a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -9,8 +9,8 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::{CursorStyle, NavigationDirection}, - AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use project::{ProjectEntryId, ProjectPath}; use std::{ @@ -99,7 +99,7 @@ pub enum Event { } pub struct Pane { - items: Vec<(Option, Box)>, + items: Vec>, active_item_index: usize, nav_history: Rc>, toolbars: HashMap>, @@ -110,8 +110,7 @@ pub struct Pane { pub(crate) struct FollowerState { pub(crate) leader_id: PeerId, pub(crate) current_view_id: usize, - pub(crate) items_by_leader_view_id: - HashMap, Box)>, + pub(crate) items_by_leader_view_id: HashMap>, } pub trait Toolbar: View { @@ -295,8 +294,8 @@ impl Pane { cx: &mut ViewContext, build_item: impl FnOnce(&mut MutableAppContext) -> Box, ) -> Box { - for (ix, (existing_entry_id, item)) in self.items.iter().enumerate() { - if *existing_entry_id == Some(project_entry_id) { + for (ix, item) in self.items.iter().enumerate() { + if item.project_entry_id(cx) == Some(project_entry_id) { let item = item.boxed_clone(); self.activate_item(ix, cx); return item; @@ -304,20 +303,15 @@ impl Pane { } let item = build_item(cx); - self.add_item(Some(project_entry_id), item.boxed_clone(), cx); + self.add_item(item.boxed_clone(), cx); item } - pub(crate) fn add_item( - &mut self, - project_entry_id: Option, - mut item: Box, - cx: &mut ViewContext, - ) { + pub(crate) fn add_item(&mut self, mut item: Box, cx: &mut ViewContext) { item.set_nav_history(self.nav_history.clone(), cx); item.added_to_pane(cx); let item_idx = cmp::min(self.active_item_index + 1, self.items.len()); - self.items.insert(item_idx, (project_entry_id, item)); + self.items.insert(item_idx, item); self.activate_item(item_idx, cx); cx.notify(); } @@ -328,39 +322,45 @@ impl Pane { cx: &mut ViewContext, ) -> Result<()> { let current_view_id = follower_state.current_view_id as usize; - let (project_entry_id, item) = follower_state + let item = follower_state .items_by_leader_view_id .get(¤t_view_id) .ok_or_else(|| anyhow!("invalid current view id"))? .clone(); - self.add_item(project_entry_id, item, cx); + self.add_item(item, cx); Ok(()) } pub fn items(&self) -> impl Iterator> { - self.items.iter().map(|(_, view)| view) + self.items.iter() } pub fn active_item(&self) -> Option> { - self.items - .get(self.active_item_index) - .map(|(_, view)| view.clone()) + self.items.get(self.active_item_index).cloned() } - pub fn project_entry_id_for_item(&self, item: &dyn ItemHandle) -> Option { - self.items.iter().find_map(|(entry_id, existing)| { + pub fn project_entry_id_for_item( + &self, + item: &dyn ItemHandle, + cx: &AppContext, + ) -> Option { + self.items.iter().find_map(|existing| { if existing.id() == item.id() { - *entry_id + existing.project_entry_id(cx) } else { None } }) } - pub fn item_for_entry(&self, entry_id: ProjectEntryId) -> Option> { - self.items.iter().find_map(|(id, view)| { - if *id == Some(entry_id) { - Some(view.boxed_clone()) + pub fn item_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option> { + self.items.iter().find_map(|item| { + if item.project_entry_id(cx) == Some(entry_id) { + Some(item.boxed_clone()) } else { None } @@ -368,7 +368,7 @@ impl Pane { } pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { - self.items.iter().position(|(_, i)| i.id() == item.id()) + self.items.iter().position(|i| i.id() == item.id()) } pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { @@ -377,7 +377,7 @@ impl Pane { if prev_active_item_ix != self.active_item_index && prev_active_item_ix < self.items.len() { - self.items[prev_active_item_ix].1.deactivated(cx); + self.items[prev_active_item_ix].deactivated(cx); } self.update_active_toolbar(cx); self.focus_active_item(cx); @@ -408,13 +408,13 @@ impl Pane { pub fn close_active_item(&mut self, cx: &mut ViewContext) { if !self.items.is_empty() { - self.close_item(self.items[self.active_item_index].1.id(), cx) + self.close_item(self.items[self.active_item_index].id(), cx) } } pub fn close_inactive_items(&mut self, cx: &mut ViewContext) { if !self.items.is_empty() { - let active_item_id = self.items[self.active_item_index].1.id(); + let active_item_id = self.items[self.active_item_index].id(); self.close_items(cx, |id| id != active_item_id); } } @@ -430,7 +430,7 @@ impl Pane { ) { let mut item_ix = 0; let mut new_active_item_index = self.active_item_index; - self.items.retain(|(_, item)| { + self.items.retain(|item| { if should_close(item.id()) { if item_ix == self.active_item_index { item.deactivated(cx); @@ -529,7 +529,7 @@ impl Pane { fn update_active_toolbar(&mut self, cx: &mut ViewContext) { let active_item = self.items.get(self.active_item_index); for (toolbar_type_id, toolbar) in &self.toolbars { - let visible = toolbar.active_item_changed(active_item.map(|i| i.1.clone()), cx); + let visible = toolbar.active_item_changed(active_item.cloned(), cx); if Some(*toolbar_type_id) == self.active_toolbar_type { self.active_toolbar_visible = visible; } @@ -542,7 +542,7 @@ impl Pane { enum Tabs {} let tabs = MouseEventHandler::new::(0, cx, |mouse_state, cx| { let mut row = Flex::row(); - for (ix, (_, item)) in self.items.iter().enumerate() { + for (ix, item) in self.items.iter().enumerate() { let is_active = ix == self.active_item_index; row.add_child({ diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 80f5b4e7a1e579fa713f706e09833b6986be3e3f..85672db11beb3a37d953932c81a1bbaffefb05c6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -53,7 +53,7 @@ type FollowedItemBuilders = Vec< ModelHandle, &mut Option, &mut MutableAppContext, - ) -> Option, Box)>>>, + ) -> Option>>>, >; action!(Open, Arc); @@ -157,6 +157,7 @@ pub trait Item: View { fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; + fn project_entry_id(&self, cx: &AppContext) -> Option; fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext); fn clone_on_split(&self, _: &mut ViewContext) -> Option where @@ -224,12 +225,13 @@ pub trait FollowedItem: Item { project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, - ) -> Option, Box)>>>; + ) -> Option>>>; } pub trait ItemHandle: 'static { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; + fn project_entry_id(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext); fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; @@ -277,6 +279,10 @@ impl ItemHandle for ViewHandle { self.read(cx).project_path(cx) } + fn project_entry_id(&self, cx: &AppContext) -> Option { + self.read(cx).project_entry_id(cx) + } + fn boxed_clone(&self) -> Box { Box::new(self.clone()) } @@ -814,7 +820,7 @@ impl Workspace { pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { self.active_pane() - .update(cx, |pane, cx| pane.add_item(None, item, cx)) + .update(cx, |pane, cx| pane.add_item(item, cx)) } pub fn open_path( @@ -877,7 +883,7 @@ impl Workspace { if let Some(item) = project_item .read(cx) .entry_id(cx) - .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id)) + .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) .and_then(|item| item.downcast()) { self.activate_item(&item, cx); @@ -959,10 +965,9 @@ impl Workspace { let new_pane = self.add_pane(cx); self.activate_pane(new_pane.clone(), cx); if let Some(item) = pane.read(cx).active_item() { - let project_entry_id = pane.read(cx).project_entry_id_for_item(item.as_ref()); if let Some(clone) = item.clone_on_split(cx.as_mut()) { new_pane.update(cx, |new_pane, cx| { - new_pane.add_item(project_entry_id, clone, cx); + new_pane.add_item(clone, cx); }); } } From 5702737de28013c758fbbad38279c2c7d30f1028 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Mar 2022 13:53:06 -0700 Subject: [PATCH 05/56] Start work on an integration test for following --- crates/server/src/rpc.rs | 180 +++++++++++++++++++++++++++++- crates/workspace/src/workspace.rs | 4 +- 2 files changed, 177 insertions(+), 7 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 948901c2a10c3141be12c3885492bdea0520c2fc..ce3cfff64661b409d621e22358539cda13c63627 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -112,6 +112,7 @@ impl Server { .add_request_handler(Server::join_channel) .add_message_handler(Server::leave_channel) .add_request_handler(Server::send_channel_message) + .add_request_handler(Server::follow) .add_request_handler(Server::get_channel_messages); Arc::new(server) @@ -669,6 +670,25 @@ impl Server { Ok(()) } + async fn follow( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result { + let leader_id = ConnectionId(request.payload.leader_id); + if !self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id)? + .contains(&leader_id) + { + Err(anyhow!("no such peer"))?; + } + let response = self + .peer + .forward_request(request.sender_id, leader_id, request.payload) + .await?; + Ok(response) + } + async fn get_channels( self: Arc, request: TypedEnvelope, @@ -1016,7 +1036,7 @@ mod tests { self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }; - use gpui::{executor, ModelHandle, TestAppContext}; + use gpui::{executor, ModelHandle, TestAppContext, ViewHandle}; use language::{ tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition, @@ -1028,7 +1048,7 @@ mod tests { fs::{FakeFs, Fs as _}, search::SearchQuery, worktree::WorktreeHandle, - DiagnosticSummary, Project, ProjectPath, + DiagnosticSummary, Project, ProjectPath, WorktreeId, }; use rand::prelude::*; use rpc::PeerId; @@ -1046,7 +1066,7 @@ mod tests { }, time::Duration, }; - use workspace::{Settings, Workspace, WorkspaceParams}; + use workspace::{Settings, SplitDirection, Workspace, WorkspaceParams}; #[cfg(test)] #[ctor::ctor] @@ -3225,7 +3245,7 @@ mod tests { let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs").into(), cx) + workspace.open_path((worktree_id, "main.rs"), cx) }) .await .unwrap() @@ -3459,7 +3479,7 @@ mod tests { let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "one.rs").into(), cx) + workspace.open_path((worktree_id, "one.rs"), cx) }) .await .unwrap() @@ -4148,6 +4168,80 @@ mod tests { } } + #[gpui::test(iterations = 10)] + async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + cx_a.foreground().forbid_parking(); + let fs = FakeFs::new(cx_a.background()); + + // 2 clients connect to a server. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let mut client_a = server.create_client(cx_a, "user_a").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + + // Client B joins the project. + let project_b = client_b + .build_remote_project( + project_a + .read_with(cx_a, |project, _| project.remote_id()) + .unwrap(), + cx_b, + ) + .await; + + // Client A opens some editors. + let workspace_a = client_a.build_workspace(&project_a, cx_a); + let editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), cx) + }) + .await + .unwrap(); + let editor_a2 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), cx) + }) + .await + .unwrap(); + + // Client B opens an editor. + let workspace_b = client_b.build_workspace(&project_b, cx_b); + let editor_b1 = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), cx) + }) + .await + .unwrap(); + + // Client B starts following client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + let leader_id = project_b.read(cx).collaborators().keys().next().unwrap(); + workspace.follow(*leader_id, cx) + }) + .await + .unwrap(); + } + #[gpui::test(iterations = 100)] async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) { cx.foreground().forbid_parking(); @@ -4477,6 +4571,7 @@ mod tests { client, peer_id, user_store, + language_registry: Arc::new(LanguageRegistry::test()), project: Default::default(), buffers: Default::default(), }; @@ -4541,6 +4636,7 @@ mod tests { client: Arc, pub peer_id: PeerId, pub user_store: ModelHandle, + language_registry: Arc, project: Option>, buffers: HashSet>, } @@ -4568,6 +4664,80 @@ mod tests { while authed_user.next().await.unwrap().is_none() {} } + async fn build_local_project( + &mut self, + fs: Arc, + root_path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, WorktreeId) { + let project = cx.update(|cx| { + Project::local( + self.client.clone(), + self.user_store.clone(), + self.language_registry.clone(), + fs, + cx, + ) + }); + self.project = Some(project.clone()); + let (worktree, _) = project + .update(cx, |p, cx| { + p.find_or_create_local_worktree(root_path, true, cx) + }) + .await + .unwrap(); + worktree + .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + project + .update(cx, |project, _| project.next_remote_id()) + .await; + (project, worktree.read_with(cx, |tree, _| tree.id())) + } + + async fn build_remote_project( + &mut self, + project_id: u64, + cx: &mut TestAppContext, + ) -> ModelHandle { + let project = Project::remote( + project_id, + self.client.clone(), + self.user_store.clone(), + self.language_registry.clone(), + FakeFs::new(cx.background()), + &mut cx.to_async(), + ) + .await + .unwrap(); + self.project = Some(project.clone()); + project + } + + fn build_workspace( + &self, + project: &ModelHandle, + cx: &mut TestAppContext, + ) -> ViewHandle { + let (window_id, _) = cx.add_window(|cx| EmptyView); + cx.add_view(window_id, |cx| { + let fs = project.read(cx).fs().clone(); + Workspace::new( + &WorkspaceParams { + fs, + project: project.clone(), + user_store: self.user_store.clone(), + languages: self.language_registry.clone(), + channel_list: cx.add_model(|cx| { + ChannelList::new(self.user_store.clone(), self.client.clone(), cx) + }), + client: self.client.clone(), + }, + cx, + ) + }) + } + fn simulate_host( mut self, project: ModelHandle, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 85672db11beb3a37d953932c81a1bbaffefb05c6..b80372a98106ffd0a69bcf1236e400664571cad1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -825,11 +825,11 @@ impl Workspace { pub fn open_path( &mut self, - path: ProjectPath, + path: impl Into, cx: &mut ViewContext, ) -> Task, Arc>> { let pane = self.active_pane().downgrade(); - let task = self.load_path(path, cx); + let task = self.load_path(path.into(), cx); cx.spawn(|this, mut cx| async move { let (project_entry_id, build_item) = task.await?; let pane = pane From eda06ee4086c93d3cc7c3d9bfa71a5e3fae4099e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Mar 2022 17:53:38 -0700 Subject: [PATCH 06/56] Add AnyWeakViewHandle --- crates/gpui/src/app.rs | 57 ++++++++++++++++++++++++++++++++++++ crates/gpui/src/presenter.rs | 4 +++ 2 files changed, 61 insertions(+) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5488d31416368a19f91f88c1d3bc51e2e7a6c264..a4dcd52f5ad01fea3a4f843b586cb42800d30025 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -93,6 +93,8 @@ pub trait UpgradeModelHandle { pub trait UpgradeViewHandle { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option>; + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option; } pub trait ReadView { @@ -647,6 +649,10 @@ impl UpgradeViewHandle for AsyncAppContext { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.0.borrow_mut().upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + self.0.borrow_mut().upgrade_any_view_handle(handle) + } } impl ReadModelWith for AsyncAppContext { @@ -2017,6 +2023,10 @@ impl UpgradeViewHandle for MutableAppContext { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.cx.upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + self.cx.upgrade_any_view_handle(handle) + } } impl ReadView for MutableAppContext { @@ -2174,6 +2184,19 @@ impl UpgradeViewHandle for AppContext { None } } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + if self.ref_counts.lock().is_entity_alive(handle.view_id) { + Some(AnyViewHandle::new( + handle.window_id, + handle.view_id, + handle.view_type, + self.ref_counts.clone(), + )) + } else { + None + } + } } impl ReadView for AppContext { @@ -2931,6 +2954,10 @@ impl UpgradeViewHandle for ViewContext<'_, V> { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.cx.upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + self.cx.upgrade_any_view_handle(handle) + } } impl UpdateModel for ViewContext<'_, V> { @@ -3619,6 +3646,14 @@ impl AnyViewHandle { None } } + + pub fn downgrade(&self) -> AnyWeakViewHandle { + AnyWeakViewHandle { + window_id: self.window_id, + view_id: self.view_id, + view_type: self.view_type, + } + } } impl Clone for AnyViewHandle { @@ -3845,6 +3880,28 @@ impl Hash for WeakViewHandle { } } +pub struct AnyWeakViewHandle { + window_id: usize, + view_id: usize, + view_type: TypeId, +} + +impl AnyWeakViewHandle { + pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option { + cx.upgrade_any_view_handle(self) + } +} + +impl From> for AnyWeakViewHandle { + fn from(handle: WeakViewHandle) -> Self { + AnyWeakViewHandle { + window_id: handle.window_id, + view_id: handle.view_id, + view_type: TypeId::of::(), + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ElementStateId { view_id: usize, diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 88ccb694d2c31896467837010e944bc8296454b8..b4e419107a32dc14ddd80c9bbf5ba2365ec29aa8 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -299,6 +299,10 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.app.upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &crate::AnyWeakViewHandle) -> Option { + self.app.upgrade_any_view_handle(handle) + } } impl<'a> ElementStateContext for LayoutContext<'a> { From 0fdaa1d7158d2a5754eee48951d50d2e1a9c2c04 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Mar 2022 17:53:49 -0700 Subject: [PATCH 07/56] WIP --- Cargo.lock | 1 + crates/client/src/channel.rs | 2 +- crates/client/src/client.rs | 156 ++++++++++++++++++++++++------ crates/editor/Cargo.toml | 1 + crates/editor/src/items.rs | 27 +++++- crates/project/src/project.rs | 28 +++--- crates/server/src/rpc.rs | 3 + crates/workspace/src/workspace.rs | 81 +++++++++++++--- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- 10 files changed, 245 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4cfb831a58cd49ea148afd07382d7fd983795e58..bd93ae3e274cd9b02267e2e2da4e7840e5110281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1629,6 +1629,7 @@ dependencies = [ "postage", "project", "rand 0.8.3", + "rpc", "serde", "smallvec", "smol", diff --git a/crates/client/src/channel.rs b/crates/client/src/channel.rs index 18a0e156db6b881cf0c6a0a8e779c573d1c34f40..ac235dc19e0eb7d4f30d2804b277dcb672fe9a1a 100644 --- a/crates/client/src/channel.rs +++ b/crates/client/src/channel.rs @@ -181,7 +181,7 @@ impl Entity for Channel { impl Channel { pub fn init(rpc: &Arc) { - rpc.add_entity_message_handler(Self::handle_message_sent); + rpc.add_model_message_handler(Self::handle_message_sent); } pub fn new( diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 59110f73c643cfac93730a0ef5e1900d896397eb..b56017c78924f8828b3a379bceeb616d679cee62 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -13,8 +13,8 @@ use async_tungstenite::tungstenite::{ }; use futures::{future::LocalBoxFuture, FutureExt, StreamExt}; use gpui::{ - action, AnyModelHandle, AnyWeakModelHandle, AsyncAppContext, Entity, ModelContext, ModelHandle, - MutableAppContext, Task, + action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext, + Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; use http::HttpClient; use lazy_static::lazy_static; @@ -139,16 +139,16 @@ struct ClientState { entity_id_extractors: HashMap u64>>, _reconnect_task: Option>, reconnect_interval: Duration, - models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>, + entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>, models_by_message_type: HashMap, - model_types_by_message_type: HashMap, + entity_types_by_message_type: HashMap, message_handlers: HashMap< TypeId, Arc< dyn Send + Sync + Fn( - AnyModelHandle, + AnyEntityHandle, Box, AsyncAppContext, ) -> LocalBoxFuture<'static, Result<()>>, @@ -156,6 +156,16 @@ struct ClientState { >, } +enum AnyWeakEntityHandle { + Model(AnyWeakModelHandle), + View(AnyWeakViewHandle), +} + +enum AnyEntityHandle { + Model(AnyModelHandle), + View(AnyViewHandle), +} + #[derive(Clone, Debug)] pub struct Credentials { pub user_id: u64, @@ -171,8 +181,8 @@ impl Default for ClientState { _reconnect_task: None, reconnect_interval: Duration::from_secs(5), models_by_message_type: Default::default(), - models_by_entity_type_and_remote_id: Default::default(), - model_types_by_message_type: Default::default(), + entities_by_type_and_remote_id: Default::default(), + entity_types_by_message_type: Default::default(), message_handlers: Default::default(), } } @@ -195,13 +205,13 @@ impl Drop for Subscription { Subscription::Entity { client, id } => { if let Some(client) = client.upgrade() { let mut state = client.state.write(); - let _ = state.models_by_entity_type_and_remote_id.remove(id); + let _ = state.entities_by_type_and_remote_id.remove(id); } } Subscription::Message { client, id } => { if let Some(client) = client.upgrade() { let mut state = client.state.write(); - let _ = state.model_types_by_message_type.remove(id); + let _ = state.entity_types_by_message_type.remove(id); let _ = state.message_handlers.remove(id); } } @@ -239,7 +249,7 @@ impl Client { state._reconnect_task.take(); state.message_handlers.clear(); state.models_by_message_type.clear(); - state.models_by_entity_type_and_remote_id.clear(); + state.entities_by_type_and_remote_id.clear(); state.entity_id_extractors.clear(); self.peer.reset(); } @@ -313,6 +323,23 @@ impl Client { } } + pub fn add_view_for_remote_entity( + self: &Arc, + remote_id: u64, + cx: &mut ViewContext, + ) -> Subscription { + let handle = AnyViewHandle::from(cx.handle()); + let mut state = self.state.write(); + let id = (TypeId::of::(), remote_id); + state + .entities_by_type_and_remote_id + .insert(id, AnyWeakEntityHandle::View(handle.downgrade())); + Subscription::Entity { + client: Arc::downgrade(self), + id, + } + } + pub fn add_model_for_remote_entity( self: &Arc, remote_id: u64, @@ -322,8 +349,8 @@ impl Client { let mut state = self.state.write(); let id = (TypeId::of::(), remote_id); state - .models_by_entity_type_and_remote_id - .insert(id, handle.downgrade()); + .entities_by_type_and_remote_id + .insert(id, AnyWeakEntityHandle::Model(handle.downgrade())); Subscription::Entity { client: Arc::downgrade(self), id, @@ -355,6 +382,11 @@ impl Client { let prev_handler = state.message_handlers.insert( message_type_id, Arc::new(move |handle, envelope, cx| { + let handle = if let AnyEntityHandle::Model(handle) = handle { + handle + } else { + unreachable!(); + }; let model = handle.downcast::().unwrap(); let envelope = envelope.into_any().downcast::>().unwrap(); if let Some(client) = client.upgrade() { @@ -374,7 +406,60 @@ impl Client { } } - pub fn add_entity_message_handler(self: &Arc, handler: H) + pub fn add_view_message_handler(self: &Arc, handler: H) + where + M: EntityMessage, + E: View, + H: 'static + + Send + + Sync + + Fn(ViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + F: 'static + Future>, + { + let entity_type_id = TypeId::of::(); + let message_type_id = TypeId::of::(); + + let client = Arc::downgrade(self); + let mut state = self.state.write(); + state + .entity_types_by_message_type + .insert(message_type_id, entity_type_id); + state + .entity_id_extractors + .entry(message_type_id) + .or_insert_with(|| { + Box::new(|envelope| { + let envelope = envelope + .as_any() + .downcast_ref::>() + .unwrap(); + envelope.payload.remote_entity_id() + }) + }); + + let prev_handler = state.message_handlers.insert( + message_type_id, + Arc::new(move |handle, envelope, cx| { + let handle = if let AnyEntityHandle::View(handle) = handle { + handle + } else { + unreachable!(); + }; + let model = handle.downcast::().unwrap(); + let envelope = envelope.into_any().downcast::>().unwrap(); + if let Some(client) = client.upgrade() { + handler(model, *envelope, client.clone(), cx).boxed_local() + } else { + async move { Ok(()) }.boxed_local() + } + }), + ); + if prev_handler.is_some() { + panic!("registered handler for the same message twice"); + } + } + + pub fn add_model_message_handler(self: &Arc, handler: H) where M: EntityMessage, E: Entity, @@ -390,7 +475,7 @@ impl Client { let client = Arc::downgrade(self); let mut state = self.state.write(); state - .model_types_by_message_type + .entity_types_by_message_type .insert(message_type_id, model_type_id); state .entity_id_extractors @@ -408,9 +493,15 @@ impl Client { let prev_handler = state.message_handlers.insert( message_type_id, Arc::new(move |handle, envelope, cx| { - let model = handle.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); if let Some(client) = client.upgrade() { + let model = handle.downcast::().unwrap(); + let envelope = envelope.into_any().downcast::>().unwrap(); + let handle = if let AnyEntityHandle::Model(handle) = handle { + handle + } else { + unreachable!(); + }; + handler(model, *envelope, client.clone(), cx).boxed_local() } else { async move { Ok(()) }.boxed_local() @@ -432,7 +523,7 @@ impl Client { + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, { - self.add_entity_message_handler(move |model, envelope, client, cx| { + self.add_model_message_handler(move |model, envelope, client, cx| { let receipt = envelope.receipt(); let response = handler(model, envelope, client.clone(), cx); async move { @@ -561,24 +652,26 @@ impl Client { .models_by_message_type .get(&payload_type_id) .and_then(|model| model.upgrade(&cx)) + .map(AnyEntityHandle::Model) .or_else(|| { - let model_type_id = - *state.model_types_by_message_type.get(&payload_type_id)?; + let entity_type_id = + *state.entity_types_by_message_type.get(&payload_type_id)?; let entity_id = state .entity_id_extractors .get(&message.payload_type_id()) .map(|extract_entity_id| { (extract_entity_id)(message.as_ref()) })?; - let model = state - .models_by_entity_type_and_remote_id - .get(&(model_type_id, entity_id))?; - if let Some(model) = model.upgrade(&cx) { - Some(model) + + let entity = state + .entities_by_type_and_remote_id + .get(&(entity_type_id, entity_id))?; + if let Some(entity) = entity.upgrade(&cx) { + Some(entity) } else { state - .models_by_entity_type_and_remote_id - .remove(&(model_type_id, entity_id)); + .entities_by_type_and_remote_id + .remove(&(entity_type_id, entity_id)); None } }); @@ -891,6 +984,15 @@ impl Client { } } +impl AnyWeakEntityHandle { + fn upgrade(&self, cx: &AsyncAppContext) -> Option { + match self { + AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model), + AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View), + } + } +} + fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { if IMPERSONATE_LOGIN.is_some() { return None; @@ -994,7 +1096,7 @@ mod tests { let (done_tx1, mut done_rx1) = smol::channel::unbounded(); let (done_tx2, mut done_rx2) = smol::channel::unbounded(); - client.add_entity_message_handler( + client.add_model_message_handler( move |model: ModelHandle, _: TypedEnvelope, _, cx| { match model.read_with(&cx, |model, _| model.id) { 1 => done_tx1.try_send(()).unwrap(), diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 609e92af0fdd8531c00a9068521c617c0888a3e6..02069fb6108dc4f0a54254594b2d53e1c2032311 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -27,6 +27,7 @@ gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } project = { path = "../project" } +rpc = { path = "../rpc" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } theme = { path = "../theme" } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d865511a62eb6b06105a3624e333db4d5f8f43c6..522c490cfa74bde9c98c1ab5fabe122ae566083f 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -6,13 +6,34 @@ use gpui::{ }; use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectEntryId, ProjectPath}; -use std::fmt::Write; -use std::path::PathBuf; +use rpc::proto; +use std::{fmt::Write, path::PathBuf}; use text::{Point, Selection}; use util::ResultExt; -use workspace::{Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView}; +use workspace::{ + FollowedItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView, +}; + +impl FollowedItem for Editor { + fn for_state_message( + pane: ViewHandle, + project: ModelHandle, + state: &mut Option, + cx: &mut gpui::MutableAppContext, + ) -> Option>>> { + todo!() + } + + fn to_state_message(&self, cx: &mut gpui::MutableAppContext) -> proto::view::Variant { + todo!() + } +} impl Item for Editor { + fn as_followed(&self) -> Option<&dyn FollowedItem> { + Some(self) + } + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { if let Some(data) = data.downcast_ref::() { let buffer = self.buffer.read(cx).read(cx); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c44364adac96dc0eaa0243b8014d240ac68b5a5f..4e2abbc2fe0b1b080cbf12243333bcca38898779 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -124,6 +124,7 @@ pub enum Event { DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsFinished, DiagnosticsUpdated(ProjectPath), + RemoteIdChanged(Option), } enum LanguageServerEvent { @@ -253,19 +254,19 @@ impl ProjectEntryId { impl Project { pub fn init(client: &Arc) { - client.add_entity_message_handler(Self::handle_add_collaborator); - client.add_entity_message_handler(Self::handle_buffer_reloaded); - client.add_entity_message_handler(Self::handle_buffer_saved); - client.add_entity_message_handler(Self::handle_start_language_server); - client.add_entity_message_handler(Self::handle_update_language_server); - client.add_entity_message_handler(Self::handle_remove_collaborator); - client.add_entity_message_handler(Self::handle_register_worktree); - client.add_entity_message_handler(Self::handle_unregister_worktree); - client.add_entity_message_handler(Self::handle_unshare_project); - client.add_entity_message_handler(Self::handle_update_buffer_file); - client.add_entity_message_handler(Self::handle_update_buffer); - client.add_entity_message_handler(Self::handle_update_diagnostic_summary); - client.add_entity_message_handler(Self::handle_update_worktree); + client.add_model_message_handler(Self::handle_add_collaborator); + client.add_model_message_handler(Self::handle_buffer_reloaded); + client.add_model_message_handler(Self::handle_buffer_saved); + client.add_model_message_handler(Self::handle_start_language_server); + client.add_model_message_handler(Self::handle_update_language_server); + client.add_model_message_handler(Self::handle_remove_collaborator); + client.add_model_message_handler(Self::handle_register_worktree); + client.add_model_message_handler(Self::handle_unregister_worktree); + client.add_model_message_handler(Self::handle_unshare_project); + client.add_model_message_handler(Self::handle_update_buffer_file); + client.add_model_message_handler(Self::handle_update_buffer); + client.add_model_message_handler(Self::handle_update_diagnostic_summary); + client.add_model_message_handler(Self::handle_update_worktree); client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_entity_request_handler(Self::handle_apply_code_action); client.add_entity_request_handler(Self::handle_format_buffers); @@ -566,6 +567,7 @@ impl Project { self.subscriptions .push(self.client.add_model_for_remote_entity(remote_id, cx)); } + cx.emit(Event::RemoteIdChanged(remote_id)) } pub fn remote_id(&self) -> Option { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index ce3cfff64661b409d621e22358539cda13c63627..23f7e1c182cd836a85df4bfa0c7f60f7e815dec9 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4563,6 +4563,9 @@ mod tests { Channel::init(&client); Project::init(&client); + cx.update(|cx| { + workspace::init(&client, cx); + }); let peer_id = PeerId(connection_id_rx.next().await.unwrap().0); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b80372a98106ffd0a69bcf1236e400664571cad1..25159fa6896934a8a263a17e79dd91e9a31f3d69 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7,7 +7,9 @@ pub mod sidebar; mod status_bar; use anyhow::{anyhow, Result}; -use client::{proto, Authenticate, ChannelList, Client, PeerId, User, UserStore}; +use client::{ + proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, +}; use clock::ReplicaId; use collections::HashMap; use gpui::{ @@ -18,9 +20,9 @@ use gpui::{ json::{self, to_string_pretty, ToJson}, keymap::Binding, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, - MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Entity, ImageData, + ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -64,7 +66,7 @@ action!(JoinProject, JoinProjectParams); action!(Save); action!(DebugElements); -pub fn init(cx: &mut MutableAppContext) { +pub fn init(client: &Arc, cx: &mut MutableAppContext) { pane::init(cx); menu::init(cx); @@ -108,6 +110,9 @@ pub fn init(cx: &mut MutableAppContext) { None, ), ]); + + client.add_entity_request_handler(Workspace::handle_follow); + client.add_model_message_handler(Workspace::handle_unfollow); } pub fn register_project_item(cx: &mut MutableAppContext) { @@ -119,7 +124,7 @@ pub fn register_project_item(cx: &mut MutableAppContext) { }); } -pub fn register_followed_item(cx: &mut MutableAppContext) { +pub fn register_followed_item(cx: &mut MutableAppContext) { cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { builders.push(I::for_state_message) }); @@ -153,6 +158,9 @@ pub struct JoinProjectParams { } pub trait Item: View { + fn as_followed(&self) -> Option<&dyn FollowedItem> { + None + } fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; @@ -217,15 +225,17 @@ pub trait ProjectItem: Item { ) -> Self; } -pub trait FollowedItem: Item { - type UpdateMessage; - +pub trait FollowedItem { fn for_state_message( pane: ViewHandle, project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, - ) -> Option>>>; + ) -> Option>>> + where + Self: Sized; + + fn to_state_message(&self, cx: &mut MutableAppContext) -> proto::view::Variant; } pub trait ItemHandle: 'static { @@ -459,6 +469,7 @@ pub struct Workspace { weak_self: WeakViewHandle, client: Arc, user_store: ModelHandle, + remote_entity_subscription: Option, fs: Arc, modal: Option, center: PaneGroup, @@ -481,6 +492,17 @@ impl Workspace { }) .detach(); + cx.subscribe(¶ms.project, move |this, project, event, cx| { + if let project::Event::RemoteIdChanged(remote_id) = event { + this.project_remote_id_changed(*remote_id, cx); + } + if project.read(cx).is_read_only() { + cx.blur(); + } + cx.notify() + }) + .detach(); + let pane = cx.add_view(|_| Pane::new()); let pane_id = pane.id(); cx.observe(&pane, move |me, _, cx| { @@ -517,7 +539,7 @@ impl Workspace { cx.emit_global(WorkspaceCreated(weak_self.clone())); - Workspace { + let mut this = Workspace { modal: None, weak_self, center: PaneGroup::new(pane.clone()), @@ -525,13 +547,16 @@ impl Workspace { active_pane: pane.clone(), status_bar, client: params.client.clone(), + remote_entity_subscription: None, user_store: params.user_store.clone(), fs: params.fs.clone(), left_sidebar: Sidebar::new(Side::Left), right_sidebar: Sidebar::new(Side::Right), project: params.project.clone(), _observe_current_user, - } + }; + this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); + this } pub fn weak_handle(&self) -> WeakViewHandle { @@ -1008,6 +1033,15 @@ impl Workspace { }); } + fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { + if let Some(remote_id) = remote_id { + self.remote_entity_subscription = + Some(self.client.add_view_for_remote_entity(remote_id, cx)); + } else { + self.remote_entity_subscription.take(); + } + } + pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Task> { if let Some(project_id) = self.project.read(cx).remote_id() { let request = self.client.request(proto::Follow { @@ -1271,6 +1305,29 @@ impl Workspace { None } } + + // RPC handlers + + async fn handle_follow( + this: ViewHandle, + envelope: TypedEnvelope, + _: Arc, + cx: AsyncAppContext, + ) -> Result { + Ok(proto::FollowResponse { + current_view_id: 0, + views: Default::default(), + }) + } + + async fn handle_unfollow( + this: ViewHandle, + envelope: TypedEnvelope, + _: Arc, + cx: AsyncAppContext, + ) -> Result<()> { + Ok(()) + } } impl Entity for Workspace { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index cf149b2469d5c66ebecfaca239decda7cca01e2f..05437834942508a63ad067b3bfcbb9e57f4a8f35 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -69,7 +69,7 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); - workspace::init(cx); + workspace::init(&client, cx); editor::init(cx); go_to_line::init(cx); file_finder::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 2b61279a2cf4fa29e85eac14d51817e27da55b24..c33a96a94b8fb0efa030e6d82b38cb32deaa9c54 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -252,7 +252,7 @@ mod tests { async fn test_new_empty_workspace(cx: &mut TestAppContext) { let app_state = cx.update(test_app_state); cx.update(|cx| { - workspace::init(cx); + workspace::init(&app_state.client, cx); }); cx.dispatch_global_action(workspace::OpenNew(app_state.clone())); let window_id = *cx.window_ids().first().unwrap(); From f0b7bd6e17899a423e859efc5cd1cca5cde13ec5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2022 10:22:13 +0100 Subject: [PATCH 08/56] Serialize initial follow state in leader and reflect it in follower --- crates/client/src/client.rs | 40 ++++++++++++++-- crates/editor/src/editor.rs | 1 + crates/editor/src/items.rs | 59 ++++++++++++++++++++--- crates/language/src/proto.rs | 19 ++++---- crates/project/src/project.rs | 77 ++++++++++++++++++++++++------- crates/rpc/proto/zed.proto | 2 +- crates/server/src/rpc.rs | 8 ++++ crates/text/src/selection.rs | 6 +++ crates/workspace/src/pane.rs | 28 +++++++---- crates/workspace/src/workspace.rs | 69 +++++++++++++++++++-------- 10 files changed, 246 insertions(+), 63 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index b56017c78924f8828b3a379bceeb616d679cee62..c2527ed94a6a5d5d44a15318fd92a78f9ca1433f 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -494,14 +494,13 @@ impl Client { message_type_id, Arc::new(move |handle, envelope, cx| { if let Some(client) = client.upgrade() { - let model = handle.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); let handle = if let AnyEntityHandle::Model(handle) = handle { handle } else { unreachable!(); }; - + let model = handle.downcast::().unwrap(); + let envelope = envelope.into_any().downcast::>().unwrap(); handler(model, *envelope, client.clone(), cx).boxed_local() } else { async move { Ok(()) }.boxed_local() @@ -513,7 +512,7 @@ impl Client { } } - pub fn add_entity_request_handler(self: &Arc, handler: H) + pub fn add_model_request_handler(self: &Arc, handler: H) where M: EntityMessage + RequestMessage, E: Entity, @@ -546,6 +545,39 @@ impl Client { }) } + pub fn add_view_request_handler(self: &Arc, handler: H) + where + M: EntityMessage + RequestMessage, + E: View, + H: 'static + + Send + + Sync + + Fn(ViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + F: 'static + Future>, + { + self.add_view_message_handler(move |view, envelope, client, cx| { + let receipt = envelope.receipt(); + let response = handler(view, envelope, client.clone(), cx); + async move { + match response.await { + Ok(response) => { + client.respond(receipt, response)?; + Ok(()) + } + Err(error) => { + client.respond_with_error( + receipt, + proto::Error { + message: error.to_string(), + }, + )?; + Err(error) + } + } + } + }) + } + pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { read_credentials_from_keychain(cx).is_some() } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1ae9fdce27a9613c92ad6e2e1184e2de1304ef87..00aa8283254a8465b11b99385dbf570fc4ec5715 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -341,6 +341,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::find_all_references); workspace::register_project_item::(cx); + workspace::register_followed_item::(cx); } trait SelectionExt { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 522c490cfa74bde9c98c1ab5fabe122ae566083f..2c454dedb5ccc70cea74d0f80e4d08f3c7a2dd97 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,8 +1,8 @@ use crate::{Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use gpui::{ - elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, Task, View, - ViewContext, ViewHandle, + elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, + Task, View, ViewContext, ViewHandle, }; use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectEntryId, ProjectPath}; @@ -19,13 +19,58 @@ impl FollowedItem for Editor { pane: ViewHandle, project: ModelHandle, state: &mut Option, - cx: &mut gpui::MutableAppContext, + cx: &mut MutableAppContext, ) -> Option>>> { - todo!() + let state = if matches!(state, Some(proto::view::Variant::Editor(_))) { + if let Some(proto::view::Variant::Editor(state)) = state.take() { + state + } else { + unreachable!() + } + } else { + return None; + }; + + let buffer = project.update(cx, |project, cx| { + project.open_buffer_by_id(state.buffer_id, cx) + }); + Some(cx.spawn(|mut cx| async move { + let buffer = buffer.await?; + let editor = pane + .read_with(&cx, |pane, cx| { + pane.items_of_type::().find(|editor| { + editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer) + }) + }) + .unwrap_or_else(|| { + cx.add_view(pane.window_id(), |cx| { + Editor::for_buffer(buffer, Some(project), cx) + }) + }); + Ok(Box::new(editor) as Box<_>) + })) } - fn to_state_message(&self, cx: &mut gpui::MutableAppContext) -> proto::view::Variant { - todo!() + fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant { + let buffer_id = self + .buffer + .read(cx) + .as_singleton() + .unwrap() + .read(cx) + .remote_id(); + let selection = self.newest_anchor_selection(); + let selection = Selection { + id: selection.id, + start: selection.start.text_anchor.clone(), + end: selection.end.text_anchor.clone(), + reversed: selection.reversed, + goal: Default::default(), + }; + proto::view::Variant::Editor(proto::view::Editor { + buffer_id, + newest_selection: Some(language::proto::serialize_selection(&selection)), + }) } } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 4a22d6ce5af061ae248e00788d0d4aa5af62a67e..09d4236afe281f9c8983896993689fc777c74465 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry( } pub fn serialize_selections(selections: &Arc<[Selection]>) -> Vec { - selections - .iter() - .map(|selection| proto::Selection { - id: selection.id as u64, - start: Some(serialize_anchor(&selection.start)), - end: Some(serialize_anchor(&selection.end)), - reversed: selection.reversed, - }) - .collect() + selections.iter().map(serialize_selection).collect() +} + +pub fn serialize_selection(selection: &Selection) -> proto::Selection { + proto::Selection { + id: selection.id as u64, + start: Some(serialize_anchor(&selection.start)), + end: Some(serialize_anchor(&selection.end)), + reversed: selection.reversed, + } } pub fn serialize_diagnostics<'a>( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4e2abbc2fe0b1b080cbf12243333bcca38898779..4a6f0dd6cff8f9830d0da0b86c6f0794c8a4d3ba 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -267,21 +267,22 @@ impl Project { client.add_model_message_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_diagnostic_summary); client.add_model_message_handler(Self::handle_update_worktree); - client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); - client.add_entity_request_handler(Self::handle_apply_code_action); - client.add_entity_request_handler(Self::handle_format_buffers); - client.add_entity_request_handler(Self::handle_get_code_actions); - client.add_entity_request_handler(Self::handle_get_completions); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_search_project); - client.add_entity_request_handler(Self::handle_get_project_symbols); - client.add_entity_request_handler(Self::handle_open_buffer_for_symbol); - client.add_entity_request_handler(Self::handle_open_buffer_by_path); - client.add_entity_request_handler(Self::handle_save_buffer); + client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); + client.add_model_request_handler(Self::handle_apply_code_action); + client.add_model_request_handler(Self::handle_format_buffers); + client.add_model_request_handler(Self::handle_get_code_actions); + client.add_model_request_handler(Self::handle_get_completions); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_search_project); + client.add_model_request_handler(Self::handle_get_project_symbols); + client.add_model_request_handler(Self::handle_open_buffer_for_symbol); + client.add_model_request_handler(Self::handle_open_buffer_by_id); + client.add_model_request_handler(Self::handle_open_buffer_by_path); + client.add_model_request_handler(Self::handle_save_buffer); } pub fn local( @@ -488,7 +489,6 @@ impl Project { cx.update(|cx| Project::local(client, user_store, languages, fs, cx)) } - #[cfg(any(test, feature = "test-support"))] pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option> { self.opened_buffers .get(&remote_id) @@ -981,6 +981,32 @@ impl Project { }) } + pub fn open_buffer_by_id( + &mut self, + id: u64, + cx: &mut ModelContext, + ) -> Task>> { + if let Some(buffer) = self.buffer_for_id(id, cx) { + Task::ready(Ok(buffer)) + } else if self.is_local() { + Task::ready(Err(anyhow!("buffer {} does not exist", id))) + } else if let Some(project_id) = self.remote_id() { + let request = self + .client + .request(proto::OpenBufferById { project_id, id }); + cx.spawn(|this, mut cx| async move { + let buffer = request + .await? + .buffer + .ok_or_else(|| anyhow!("invalid buffer"))?; + this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await + }) + } else { + Task::ready(Err(anyhow!("cannot open buffer while disconnected"))) + } + } + pub fn save_buffer_as( &mut self, buffer: ModelHandle, @@ -3889,6 +3915,25 @@ impl Project { hasher.finalize().as_slice().try_into().unwrap() } + async fn handle_open_buffer_by_id( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let peer_id = envelope.original_sender_id()?; + let buffer = this + .update(&mut cx, |this, cx| { + this.open_buffer_by_id(envelope.payload.id, cx) + }) + .await?; + this.update(&mut cx, |this, cx| { + Ok(proto::OpenBufferResponse { + buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)), + }) + }) + } + async fn handle_open_buffer_by_path( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d7a928e424e469b7abb63c288d18e7bd8ee2e19b..f2197055f3229c5faf2a069e7e0ecc3167b376ca 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -544,7 +544,7 @@ message Follow { } message FollowResponse { - uint64 current_view_id = 1; + optional uint64 current_view_id = 1; repeated View views = 2; } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 23f7e1c182cd836a85df4bfa0c7f60f7e815dec9..785de7ea089d9ae427026057bce1ff0ccd12d952 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -92,6 +92,7 @@ impl Server { .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler( @@ -4240,6 +4241,13 @@ mod tests { }) .await .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .project_path(cx)), + Some((worktree_id, "2.txt").into()) + ); } #[gpui::test(iterations = 100)] diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 4e7d6f52367094fa5bbf07cd8f55250259bd7601..0c73c7388d0494e75cf2793a975606d3a90d0706 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -18,6 +18,12 @@ pub struct Selection { pub goal: SelectionGoal, } +impl Default for SelectionGoal { + fn default() -> Self { + Self::None + } +} + impl Selection { pub fn head(&self) -> T { if self.reversed { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c3b6a5fd29b4742097cee6412dffb44c946d866a..997aae3d96379c42f2e1d6412af6305c4d41e372 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -109,7 +109,7 @@ pub struct Pane { pub(crate) struct FollowerState { pub(crate) leader_id: PeerId, - pub(crate) current_view_id: usize, + pub(crate) current_view_id: Option, pub(crate) items_by_leader_view_id: HashMap>, } @@ -308,6 +308,11 @@ impl Pane { } pub(crate) fn add_item(&mut self, mut item: Box, cx: &mut ViewContext) { + // Prevent adding the same item to the pane more than once. + if self.items.iter().any(|i| i.id() == item.id()) { + return; + } + item.set_nav_history(self.nav_history.clone(), cx); item.added_to_pane(cx); let item_idx = cmp::min(self.active_item_index + 1, self.items.len()); @@ -321,13 +326,14 @@ impl Pane { follower_state: FollowerState, cx: &mut ViewContext, ) -> Result<()> { - let current_view_id = follower_state.current_view_id as usize; - let item = follower_state - .items_by_leader_view_id - .get(¤t_view_id) - .ok_or_else(|| anyhow!("invalid current view id"))? - .clone(); - self.add_item(item, cx); + if let Some(current_view_id) = follower_state.current_view_id { + let item = follower_state + .items_by_leader_view_id + .get(¤t_view_id) + .ok_or_else(|| anyhow!("invalid current view id"))? + .clone(); + self.add_item(item, cx); + } Ok(()) } @@ -335,6 +341,12 @@ impl Pane { self.items.iter() } + pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator> { + self.items + .iter() + .filter_map(|item| item.to_any().downcast()) + } + pub fn active_item(&self) -> Option> { self.items.get(self.active_item_index).cloned() } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 25159fa6896934a8a263a17e79dd91e9a31f3d69..eba7f12981d59bc69aac67130b492b8fa42fe4fa 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -111,8 +111,8 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { ), ]); - client.add_entity_request_handler(Workspace::handle_follow); - client.add_model_message_handler(Workspace::handle_unfollow); + client.add_view_request_handler(Workspace::handle_follow); + client.add_view_message_handler(Workspace::handle_unfollow); } pub fn register_project_item(cx: &mut MutableAppContext) { @@ -235,7 +235,7 @@ pub trait FollowedItem { where Self: Sized; - fn to_state_message(&self, cx: &mut MutableAppContext) -> proto::view::Variant; + fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; } pub trait ItemHandle: 'static { @@ -262,6 +262,8 @@ pub trait ItemHandle: 'static { cx: &mut MutableAppContext, ) -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; + fn can_be_followed(&self, cx: &AppContext) -> bool; + fn to_state_message(&self, cx: &AppContext) -> Option; } pub trait WeakItemHandle { @@ -297,11 +299,7 @@ impl ItemHandle for ViewHandle { Box::new(self.clone()) } - fn clone_on_split( - &self, - // nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Option> { + fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option> { self.update(cx, |item, cx| { cx.add_option_view(|cx| item.clone_on_split(cx)) }) @@ -381,6 +379,16 @@ impl ItemHandle for ViewHandle { fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option { self.read(cx).act_as_type(type_id, self, cx) } + + fn can_be_followed(&self, cx: &AppContext) -> bool { + self.read(cx).as_followed().is_some() + } + + fn to_state_message(&self, cx: &AppContext) -> Option { + self.read(cx) + .as_followed() + .map(|item| item.to_state_message(cx)) + } } impl Into for Box { @@ -709,6 +717,13 @@ impl Workspace { } } + pub fn items<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.panes.iter().flat_map(|pane| pane.read(cx).items()) + } + pub fn item_of_type(&self, cx: &AppContext) -> Option> { self.items_of_type(cx).max_by_key(|item| item.id()) } @@ -717,11 +732,9 @@ impl Workspace { &'a self, cx: &'a AppContext, ) -> impl 'a + Iterator> { - self.panes.iter().flat_map(|pane| { - pane.read(cx) - .items() - .filter_map(|item| item.to_any().downcast()) - }) + self.panes + .iter() + .flat_map(|pane| pane.read(cx).items_of_type()) } pub fn active_item(&self, cx: &AppContext) -> Option> { @@ -1085,7 +1098,7 @@ impl Workspace { pane.set_follow_state( FollowerState { leader_id, - current_view_id: response.current_view_id as usize, + current_view_id: response.current_view_id.map(|id| id as usize), items_by_leader_view_id, }, cx, @@ -1310,13 +1323,33 @@ impl Workspace { async fn handle_follow( this: ViewHandle, - envelope: TypedEnvelope, + _: TypedEnvelope, _: Arc, cx: AsyncAppContext, ) -> Result { - Ok(proto::FollowResponse { - current_view_id: 0, - views: Default::default(), + this.read_with(&cx, |this, cx| { + let current_view_id = if let Some(active_item) = this.active_item(cx) { + if active_item.can_be_followed(cx) { + Some(active_item.id() as u64) + } else { + None + } + } else { + None + }; + Ok(proto::FollowResponse { + current_view_id, + views: this + .items(cx) + .filter_map(|item| { + let variant = item.to_state_message(cx)?; + Some(proto::View { + id: item.id() as u64, + variant: Some(variant), + }) + }) + .collect(), + }) }) } From 10e6d82c3ec95cf9a86979ee47794726057df9c9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2022 14:20:09 +0100 Subject: [PATCH 09/56] WIP: Start on sending view updates to followers --- crates/editor/src/items.rs | 29 ++++++- crates/gpui/src/app.rs | 8 ++ crates/server/src/rpc.rs | 11 +++ crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 139 +++++++++++++++++++++--------- 5 files changed, 145 insertions(+), 44 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 2c454dedb5ccc70cea74d0f80e4d08f3c7a2dd97..98610c4a07fa035d40d8813a8c67606b364c2371 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -72,13 +72,34 @@ impl FollowedItem for Editor { newest_selection: Some(language::proto::serialize_selection(&selection)), }) } -} -impl Item for Editor { - fn as_followed(&self) -> Option<&dyn FollowedItem> { - Some(self) + fn to_update_message( + &self, + event: &Self::Event, + cx: &AppContext, + ) -> Option { + match event { + Event::SelectionsChanged => { + let selection = self.newest_anchor_selection(); + let selection = Selection { + id: selection.id, + start: selection.start.text_anchor.clone(), + end: selection.end.text_anchor.clone(), + reversed: selection.reversed, + goal: Default::default(), + }; + Some(proto::view_update::Variant::Editor( + proto::view_update::Editor { + newest_selection: Some(language::proto::serialize_selection(&selection)), + }, + )) + } + _ => None, + } } +} +impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) { if let Some(data) = data.downcast_ref::() { let buffer = self.buffer.read(cx).read(cx); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a4dcd52f5ad01fea3a4f843b586cb42800d30025..f8f505ee783fe274e9f4b182bbbe68c4d34cc760 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2121,6 +2121,10 @@ impl AppContext { &self.platform } + pub fn has_global(&self) -> bool { + self.globals.contains_key(&TypeId::of::()) + } + pub fn global(&self) -> &T { self.globals .get(&TypeId::of::()) @@ -3654,6 +3658,10 @@ impl AnyViewHandle { view_type: self.view_type, } } + + pub fn view_type(&self) -> TypeId { + self.view_type + } } impl Clone for AnyViewHandle { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 785de7ea089d9ae427026057bce1ff0ccd12d952..7668533a35e75fe60845662e0d73d87795dbfe5e 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4248,6 +4248,17 @@ mod tests { .project_path(cx)), Some((worktree_id, "2.txt").into()) ); + + // When client A activates a different editor, client B does so as well. + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(editor_a1.as_ref(), cx) + }); + workspace_b + .condition(cx_b, |workspace, cx| { + let active_item = workspace.active_item(cx).unwrap(); + active_item.project_path(cx) == Some((worktree_id, "1.txt").into()) + }) + .await; } #[gpui::test(iterations = 100)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 997aae3d96379c42f2e1d6412af6305c4d41e372..43aa1f54a11e4b16b7831c87e51d2f8c2325ef71 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -288,7 +288,7 @@ impl Pane { } } - pub fn open_item( + pub(crate) fn open_item( &mut self, project_entry_id: ProjectEntryId, cx: &mut ViewContext, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index eba7f12981d59bc69aac67130b492b8fa42fe4fa..6f013d910f3525986b49de5c3b36a7cb8ef7868e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -11,7 +11,7 @@ use client::{ proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, }; use clock::ReplicaId; -use collections::HashMap; +use collections::{HashMap, HashSet}; use gpui::{ action, color::Color, @@ -49,13 +49,18 @@ type ProjectItemBuilders = HashMap< fn(usize, ModelHandle, AnyModelHandle, &mut MutableAppContext) -> Box, >; -type FollowedItemBuilders = Vec< - fn( - ViewHandle, - ModelHandle, - &mut Option, - &mut MutableAppContext, - ) -> Option>>>, +type FollowedItemBuilder = fn( + ViewHandle, + ModelHandle, + &mut Option, + &mut MutableAppContext, +) -> Option>>>; +type FollowedItemBuilders = HashMap< + TypeId, + ( + FollowedItemBuilder, + fn(AnyViewHandle) -> Box, + ), >; action!(Open, Arc); @@ -124,9 +129,14 @@ pub fn register_project_item(cx: &mut MutableAppContext) { }); } -pub fn register_followed_item(cx: &mut MutableAppContext) { +pub fn register_followed_item(cx: &mut MutableAppContext) { cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { - builders.push(I::for_state_message) + builders.insert( + TypeId::of::(), + (I::for_state_message, |this| { + Box::new(this.downcast::().unwrap()) + }), + ); }); } @@ -158,9 +168,6 @@ pub struct JoinProjectParams { } pub trait Item: View { - fn as_followed(&self) -> Option<&dyn FollowedItem> { - None - } fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; @@ -225,7 +232,7 @@ pub trait ProjectItem: Item { ) -> Self; } -pub trait FollowedItem { +pub trait FollowedItem: Item { fn for_state_message( pane: ViewHandle, project: ModelHandle, @@ -234,8 +241,40 @@ pub trait FollowedItem { ) -> Option>>> where Self: Sized; + fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; + fn to_update_message( + &self, + event: &Self::Event, + cx: &AppContext, + ) -> Option; +} +pub trait FollowedItemHandle { + fn id(&self) -> usize; fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; + fn to_update_message( + &self, + event: &dyn Any, + cx: &AppContext, + ) -> Option; +} + +impl FollowedItemHandle for ViewHandle { + fn id(&self) -> usize { + self.id() + } + + fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant { + self.read(cx).to_state_message(cx) + } + + fn to_update_message( + &self, + event: &dyn Any, + cx: &AppContext, + ) -> Option { + self.read(cx).to_update_message(event.downcast_ref()?, cx) + } } pub trait ItemHandle: 'static { @@ -262,8 +301,7 @@ pub trait ItemHandle: 'static { cx: &mut MutableAppContext, ) -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; - fn can_be_followed(&self, cx: &AppContext) -> bool; - fn to_state_message(&self, cx: &AppContext) -> Option; + fn to_followed_item_handle(&self, cx: &AppContext) -> Option>; } pub trait WeakItemHandle { @@ -318,15 +356,22 @@ impl ItemHandle for ViewHandle { pane.close_item(item.id(), cx); return; } + if T::should_activate_item_on_event(event) { if let Some(ix) = pane.index_for_item(&item) { pane.activate_item(ix, cx); pane.activate(cx); } } + if T::should_update_tab_on_event(event) { cx.notify() } + + if let Some(message) = item + .to_followed_item_handle(cx) + .and_then(|i| i.to_update_message(event, cx)) + {} }) .detach(); } @@ -380,14 +425,14 @@ impl ItemHandle for ViewHandle { self.read(cx).act_as_type(type_id, self, cx) } - fn can_be_followed(&self, cx: &AppContext) -> bool { - self.read(cx).as_followed().is_some() - } - - fn to_state_message(&self, cx: &AppContext) -> Option { - self.read(cx) - .as_followed() - .map(|item| item.to_state_message(cx)) + fn to_followed_item_handle(&self, cx: &AppContext) -> Option> { + if cx.has_global::() { + let builders = cx.global::(); + let item = self.to_any(); + Some(builders.get(&item.view_type())?.1(item)) + } else { + None + } } } @@ -487,9 +532,16 @@ pub struct Workspace { active_pane: ViewHandle, status_bar: ViewHandle, project: ModelHandle, + leader_state: LeaderState, _observe_current_user: Task<()>, } +#[derive(Default)] +struct LeaderState { + followers: HashSet, + subscriptions: Vec, +} + impl Workspace { pub fn new(params: &WorkspaceParams, cx: &mut ViewContext) -> Self { cx.observe(¶ms.project, |_, project, cx| { @@ -561,6 +613,7 @@ impl Workspace { left_sidebar: Sidebar::new(Side::Left), right_sidebar: Sidebar::new(Side::Right), project: params.project.clone(), + leader_state: Default::default(), _observe_current_user, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); @@ -1068,6 +1121,13 @@ impl Workspace { let (project, pane) = this.read_with(&cx, |this, _| { (this.project.clone(), this.active_pane().clone()) }); + let item_builders = cx.update(|cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + .clone() + }); for view in &mut response.views { let variant = view .variant @@ -1075,7 +1135,7 @@ impl Workspace { .ok_or_else(|| anyhow!("missing variant"))?; cx.update(|cx| { let mut variant = Some(variant); - for build_item in cx.default_global::().clone() { + for build_item in &item_builders { if let Some(task) = build_item(pane.clone(), project.clone(), &mut variant, cx) { @@ -1323,28 +1383,29 @@ impl Workspace { async fn handle_follow( this: ViewHandle, - _: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, - cx: AsyncAppContext, + mut cx: AsyncAppContext, ) -> Result { - this.read_with(&cx, |this, cx| { - let current_view_id = if let Some(active_item) = this.active_item(cx) { - if active_item.can_be_followed(cx) { - Some(active_item.id() as u64) - } else { - None - } - } else { - None - }; + this.update(&mut cx, |this, cx| { + this.leader_state + .followers + .insert(envelope.original_sender_id()?); + + let current_view_id = this + .active_item(cx) + .and_then(|i| i.to_followed_item_handle(cx)) + .map(|i| i.id() as u64); Ok(proto::FollowResponse { current_view_id, views: this .items(cx) .filter_map(|item| { - let variant = item.to_state_message(cx)?; + let id = item.id() as u64; + let item = item.to_followed_item_handle(cx)?; + let variant = item.to_state_message(cx); Some(proto::View { - id: item.id() as u64, + id, variant: Some(variant), }) }) From 3d81eb9ddf53b3baf019e2fd20080d321b2b7554 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2022 14:59:53 +0100 Subject: [PATCH 10/56] Allow accessing workspace after adding item to pane --- crates/workspace/src/pane.rs | 86 ++++++++++++------------ crates/workspace/src/workspace.rs | 104 +++++++++++++++++++++--------- 2 files changed, 116 insertions(+), 74 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 43aa1f54a11e4b16b7831c87e51d2f8c2325ef71..8112fa7af41cc67360e2594d2f7048fd5cfa9aff 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -107,12 +107,6 @@ pub struct Pane { active_toolbar_visible: bool, } -pub(crate) struct FollowerState { - pub(crate) leader_id: PeerId, - pub(crate) current_view_id: Option, - pub(crate) items_by_leader_view_id: HashMap>, -} - pub trait Toolbar: View { fn active_item_changed( &mut self, @@ -266,7 +260,17 @@ impl Pane { if let Some((project_entry_id, build_item)) = task.log_err() { pane.update(&mut cx, |pane, cx| { pane.nav_history.borrow_mut().set_mode(mode); - let item = pane.open_item(project_entry_id, cx, build_item); + }); + let item = workspace.update(&mut cx, |workspace, cx| { + Self::open_item( + workspace, + pane.clone(), + project_entry_id, + cx, + build_item, + ) + }); + pane.update(&mut cx, |pane, cx| { pane.nav_history .borrow_mut() .set_mode(NavigationMode::Normal); @@ -289,52 +293,50 @@ impl Pane { } pub(crate) fn open_item( - &mut self, + workspace: &mut Workspace, + pane: ViewHandle, project_entry_id: ProjectEntryId, - cx: &mut ViewContext, + cx: &mut ViewContext, build_item: impl FnOnce(&mut MutableAppContext) -> Box, ) -> Box { - for (ix, item) in self.items.iter().enumerate() { - if item.project_entry_id(cx) == Some(project_entry_id) { - let item = item.boxed_clone(); - self.activate_item(ix, cx); - return item; + let existing_item = pane.update(cx, |pane, cx| { + for (ix, item) in pane.items.iter().enumerate() { + if item.project_entry_id(cx) == Some(project_entry_id) { + let item = item.boxed_clone(); + pane.activate_item(ix, cx); + return Some(item); + } } + None + }); + if let Some(existing_item) = existing_item { + existing_item + } else { + let item = build_item(cx); + Self::add_item(workspace, pane, item.boxed_clone(), cx); + item } - - let item = build_item(cx); - self.add_item(item.boxed_clone(), cx); - item } - pub(crate) fn add_item(&mut self, mut item: Box, cx: &mut ViewContext) { + pub(crate) fn add_item( + workspace: &mut Workspace, + pane: ViewHandle, + mut item: Box, + cx: &mut ViewContext, + ) { // Prevent adding the same item to the pane more than once. - if self.items.iter().any(|i| i.id() == item.id()) { + if pane.read(cx).items.iter().any(|i| i.id() == item.id()) { return; } - item.set_nav_history(self.nav_history.clone(), cx); - item.added_to_pane(cx); - let item_idx = cmp::min(self.active_item_index + 1, self.items.len()); - self.items.insert(item_idx, item); - self.activate_item(item_idx, cx); - cx.notify(); - } - - pub(crate) fn set_follow_state( - &mut self, - follower_state: FollowerState, - cx: &mut ViewContext, - ) -> Result<()> { - if let Some(current_view_id) = follower_state.current_view_id { - let item = follower_state - .items_by_leader_view_id - .get(¤t_view_id) - .ok_or_else(|| anyhow!("invalid current view id"))? - .clone(); - self.add_item(item, cx); - } - Ok(()) + item.set_nav_history(pane.read(cx).nav_history.clone(), cx); + item.added_to_pane(workspace, pane.clone(), cx); + pane.update(cx, |pane, cx| { + let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len()); + pane.items.insert(item_idx, item); + pane.activate_item(item_idx, cx); + cx.notify(); + }); } pub fn items(&self) -> impl Iterator> { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6f013d910f3525986b49de5c3b36a7cb8ef7868e..5955f81e62d41eb7cfd93e554d4649e5f56f2ab8 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -284,7 +284,12 @@ pub trait ItemHandle: 'static { fn boxed_clone(&self) -> Box; fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext); fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; - fn added_to_pane(&mut self, cx: &mut ViewContext); + fn added_to_pane( + &self, + workspace: &mut Workspace, + pane: ViewHandle, + cx: &mut ViewContext, + ); fn deactivated(&self, cx: &mut MutableAppContext); fn navigate(&self, data: Box, cx: &mut MutableAppContext); fn id(&self) -> usize; @@ -350,22 +355,37 @@ impl ItemHandle for ViewHandle { }) } - fn added_to_pane(&mut self, cx: &mut ViewContext) { - cx.subscribe(self, |pane, item, event, cx| { + fn added_to_pane( + &self, + workspace: &mut Workspace, + pane: ViewHandle, + cx: &mut ViewContext, + ) { + let pane = pane.downgrade(); + cx.subscribe(self, move |workspace, item, event, cx| { + let pane = if let Some(pane) = pane.upgrade(cx) { + pane + } else { + log::error!("unexpected item event after pane was dropped"); + return; + }; + if T::should_close_item_on_event(event) { - pane.close_item(item.id(), cx); + pane.update(cx, |pane, cx| pane.close_item(item.id(), cx)); return; } if T::should_activate_item_on_event(event) { - if let Some(ix) = pane.index_for_item(&item) { - pane.activate_item(ix, cx); - pane.activate(cx); - } + pane.update(cx, |pane, cx| { + if let Some(ix) = pane.index_for_item(&item) { + pane.activate_item(ix, cx); + pane.activate(cx); + } + }); } if T::should_update_tab_on_event(event) { - cx.notify() + pane.update(cx, |_, cx| cx.notify()); } if let Some(message) = item @@ -533,6 +553,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, + follower_states_by_leader: HashMap, _observe_current_user: Task<()>, } @@ -542,6 +563,11 @@ struct LeaderState { subscriptions: Vec, } +struct FollowerState { + current_view_id: Option, + items_by_leader_view_id: HashMap>, +} + impl Workspace { pub fn new(params: &WorkspaceParams, cx: &mut ViewContext) -> Self { cx.observe(¶ms.project, |_, project, cx| { @@ -614,6 +640,7 @@ impl Workspace { right_sidebar: Sidebar::new(Side::Right), project: params.project.clone(), leader_state: Default::default(), + follower_states_by_leader: Default::default(), _observe_current_user, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); @@ -910,8 +937,8 @@ impl Workspace { } pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { - self.active_pane() - .update(cx, |pane, cx| pane.add_item(item, cx)) + let pane = self.active_pane().clone(); + Pane::add_item(self, pane, item, cx); } pub fn open_path( @@ -926,10 +953,14 @@ impl Workspace { let pane = pane .upgrade(&cx) .ok_or_else(|| anyhow!("pane was closed"))?; - this.update(&mut cx, |_, cx| { - pane.update(cx, |pane, cx| { - Ok(pane.open_item(project_entry_id, cx, build_item)) - }) + this.update(&mut cx, |this, cx| { + Ok(Pane::open_item( + this, + pane, + project_entry_id, + cx, + build_item, + )) }) }) } @@ -1057,9 +1088,7 @@ impl Workspace { self.activate_pane(new_pane.clone(), cx); if let Some(item) = pane.read(cx).active_item() { if let Some(clone) = item.clone_on_split(cx.as_mut()) { - new_pane.update(cx, |new_pane, cx| { - new_pane.add_item(clone, cx); - }); + Pane::add_item(self, new_pane.clone(), clone, cx); } } self.center.split(&pane, &new_pane, direction).unwrap(); @@ -1149,21 +1178,32 @@ impl Workspace { } let items = futures::future::try_join_all(item_tasks).await?; - let mut items_by_leader_view_id = HashMap::default(); - for (view, item) in response.views.into_iter().zip(items) { - items_by_leader_view_id.insert(view.id as usize, item); - } - - pane.update(&mut cx, |pane, cx| { - pane.set_follow_state( - FollowerState { - leader_id, - current_view_id: response.current_view_id.map(|id| id as usize), - items_by_leader_view_id, - }, - cx, + let follower_state = FollowerState { + current_view_id: response.current_view_id.map(|id| id as usize), + items_by_leader_view_id: response + .views + .iter() + .map(|v| v.id as usize) + .zip(items) + .collect(), + }; + let current_item = if let Some(current_view_id) = follower_state.current_view_id + { + Some( + follower_state + .items_by_leader_view_id + .get(¤t_view_id) + .ok_or_else(|| anyhow!("invalid current view id"))? + .clone(), ) - })?; + } else { + None + }; + this.update(&mut cx, |this, cx| { + if let Some(item) = current_item { + Pane::add_item(this, pane, item, cx); + } + }); } Ok(()) }) From 7d7e10598ae9e1109a6e1a5d97b4ced8fc65f98f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2022 16:00:03 +0100 Subject: [PATCH 11/56] Broadcast active view to followers --- crates/editor/src/items.rs | 6 +- crates/rpc/proto/zed.proto | 38 ++++---- crates/server/src/rpc.rs | 36 ++++++++ crates/workspace/src/pane.rs | 5 +- crates/workspace/src/workspace.rs | 145 ++++++++++++++++++++++++------ 5 files changed, 182 insertions(+), 48 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 98610c4a07fa035d40d8813a8c67606b364c2371..7d89341c0d339ee35875ed772955330fdfa1747d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -77,7 +77,7 @@ impl FollowedItem for Editor { &self, event: &Self::Event, cx: &AppContext, - ) -> Option { + ) -> Option { match event { Event::SelectionsChanged => { let selection = self.newest_anchor_selection(); @@ -88,8 +88,8 @@ impl FollowedItem for Editor { reversed: selection.reversed, goal: Default::default(), }; - Some(proto::view_update::Variant::Editor( - proto::view_update::Editor { + Some(proto::update_followers::update_view::Variant::Editor( + proto::update_followers::update_view::Editor { newest_selection: Some(language::proto::serialize_selection(&selection)), }, )) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f2197055f3229c5faf2a069e7e0ecc3167b376ca..1ea340278ac962d7631df5690e70c1f4819d9fee 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -544,16 +544,33 @@ message Follow { } message FollowResponse { - optional uint64 current_view_id = 1; + optional uint64 active_view_id = 1; repeated View views = 2; } message UpdateFollowers { uint64 project_id = 1; - uint64 current_view_id = 2; - repeated View created_views = 3; - repeated ViewUpdate updated_views = 4; - repeated uint32 follower_ids = 5; + repeated uint32 follower_ids = 2; + oneof variant { + UpdateActiveView update_active_view = 3; + View create_view = 4; + UpdateView update_view = 5; + } + + message UpdateActiveView { + optional uint64 id = 1; + } + + message UpdateView { + uint64 id = 1; + oneof variant { + Editor editor = 2; + } + + message Editor { + Selection newest_selection = 1; + } + } } message Unfollow { @@ -575,17 +592,6 @@ message View { } } -message ViewUpdate { - uint64 id = 1; - oneof variant { - Editor editor = 2; - } - - message Editor { - Selection newest_selection = 1; - } -} - message Collaborator { uint32 peer_id = 1; uint32 replica_id = 2; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 7668533a35e75fe60845662e0d73d87795dbfe5e..25857c53e48cd79250c93e3f80f89c1e3c0b81e5 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -114,6 +114,8 @@ impl Server { .add_message_handler(Server::leave_channel) .add_request_handler(Server::send_channel_message) .add_request_handler(Server::follow) + .add_message_handler(Server::unfollow) + .add_message_handler(Server::update_followers) .add_request_handler(Server::get_channel_messages); Arc::new(server) @@ -690,6 +692,40 @@ impl Server { Ok(response) } + async fn unfollow( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let leader_id = ConnectionId(request.payload.leader_id); + if !self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id)? + .contains(&leader_id) + { + Err(anyhow!("no such peer"))?; + } + self.peer + .forward_send(request.sender_id, leader_id, request.payload)?; + Ok(()) + } + + async fn update_followers( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let connection_ids = self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id)?; + for follower_id in &request.payload.follower_ids { + let follower_id = ConnectionId(*follower_id); + if connection_ids.contains(&follower_id) { + self.peer + .forward_send(request.sender_id, follower_id, request.payload.clone())?; + } + } + Ok(()) + } + async fn get_channels( self: Arc, request: TypedEnvelope, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 8112fa7af41cc67360e2594d2f7048fd5cfa9aff..a2e49f7f481d65825f93d3ebfad5f2b110416e87 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -321,11 +321,12 @@ impl Pane { pub(crate) fn add_item( workspace: &mut Workspace, pane: ViewHandle, - mut item: Box, + item: Box, cx: &mut ViewContext, ) { // Prevent adding the same item to the pane more than once. - if pane.read(cx).items.iter().any(|i| i.id() == item.id()) { + if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) { + pane.update(cx, |pane, cx| pane.activate_item(item_ix, cx)); return; } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5955f81e62d41eb7cfd93e554d4649e5f56f2ab8..3cbd167319d06e364c62b0fe6f31b1d1171adf05 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -43,6 +43,7 @@ use std::{ sync::Arc, }; use theme::{Theme, ThemeRegistry}; +use util::ResultExt; type ProjectItemBuilders = HashMap< TypeId, @@ -118,6 +119,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { client.add_view_request_handler(Workspace::handle_follow); client.add_view_message_handler(Workspace::handle_unfollow); + client.add_view_message_handler(Workspace::handle_update_followers); } pub fn register_project_item(cx: &mut MutableAppContext) { @@ -246,7 +248,7 @@ pub trait FollowedItem: Item { &self, event: &Self::Event, cx: &AppContext, - ) -> Option; + ) -> Option; } pub trait FollowedItemHandle { @@ -256,7 +258,7 @@ pub trait FollowedItemHandle { &self, event: &dyn Any, cx: &AppContext, - ) -> Option; + ) -> Option; } impl FollowedItemHandle for ViewHandle { @@ -272,7 +274,7 @@ impl FollowedItemHandle for ViewHandle { &self, event: &dyn Any, cx: &AppContext, - ) -> Option { + ) -> Option { self.read(cx).to_update_message(event.downcast_ref()?, cx) } } @@ -361,6 +363,16 @@ impl ItemHandle for ViewHandle { pane: ViewHandle, cx: &mut ViewContext, ) { + if let Some(followed_item) = self.to_followed_item_handle(cx) { + workspace.update_followers( + proto::update_followers::Variant::CreateView(proto::View { + id: followed_item.id() as u64, + variant: Some(followed_item.to_state_message(cx)), + }), + cx, + ); + } + let pane = pane.downgrade(); cx.subscribe(self, move |workspace, item, event, cx| { let pane = if let Some(pane) = pane.upgrade(cx) { @@ -391,7 +403,17 @@ impl ItemHandle for ViewHandle { if let Some(message) = item .to_followed_item_handle(cx) .and_then(|i| i.to_update_message(event, cx)) - {} + { + workspace.update_followers( + proto::update_followers::Variant::UpdateView( + proto::update_followers::UpdateView { + id: item.id() as u64, + variant: Some(message), + }, + ), + cx, + ); + } }) .detach(); } @@ -553,18 +575,17 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, - follower_states_by_leader: HashMap, + follower_states_by_leader: HashMap, FollowerState>>, _observe_current_user: Task<()>, } #[derive(Default)] struct LeaderState { followers: HashSet, - subscriptions: Vec, } struct FollowerState { - current_view_id: Option, + active_view_id: Option, items_by_leader_view_id: HashMap>, } @@ -1053,6 +1074,15 @@ impl Workspace { cx.focus(&self.active_pane); cx.notify(); } + + self.update_followers( + proto::update_followers::Variant::UpdateActiveView( + proto::update_followers::UpdateActiveView { + id: self.active_item(cx).map(|item| item.id() as u64), + }, + ), + cx, + ); } fn handle_pane_event( @@ -1179,7 +1209,7 @@ impl Workspace { let items = futures::future::try_join_all(item_tasks).await?; let follower_state = FollowerState { - current_view_id: response.current_view_id.map(|id| id as usize), + active_view_id: response.active_view_id.map(|id| id as usize), items_by_leader_view_id: response .views .iter() @@ -1187,22 +1217,12 @@ impl Workspace { .zip(items) .collect(), }; - let current_item = if let Some(current_view_id) = follower_state.current_view_id - { - Some( - follower_state - .items_by_leader_view_id - .get(¤t_view_id) - .ok_or_else(|| anyhow!("invalid current view id"))? - .clone(), - ) - } else { - None - }; this.update(&mut cx, |this, cx| { - if let Some(item) = current_item { - Pane::add_item(this, pane, item, cx); - } + this.follower_states_by_leader + .entry(leader_id) + .or_default() + .insert(pane.downgrade(), follower_state); + this.leader_updated(leader_id, cx); }); } Ok(()) @@ -1212,6 +1232,24 @@ impl Workspace { } } + fn update_followers( + &self, + update: proto::update_followers::Variant, + cx: &AppContext, + ) -> Option<()> { + let project_id = self.project.read(cx).remote_id()?; + if !self.leader_state.followers.is_empty() { + self.client + .send(proto::UpdateFollowers { + project_id, + follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(), + variant: Some(update), + }) + .log_err(); + } + None + } + fn render_connection_status(&self, cx: &mut RenderContext) -> Option { let theme = &cx.global::().theme; match &*self.client.status().borrow() { @@ -1432,12 +1470,12 @@ impl Workspace { .followers .insert(envelope.original_sender_id()?); - let current_view_id = this + let active_view_id = this .active_item(cx) .and_then(|i| i.to_followed_item_handle(cx)) .map(|i| i.id() as u64); Ok(proto::FollowResponse { - current_view_id, + active_view_id, views: this .items(cx) .filter_map(|item| { @@ -1458,9 +1496,62 @@ impl Workspace { this: ViewHandle, envelope: TypedEnvelope, _: Arc, - cx: AsyncAppContext, + mut cx: AsyncAppContext, ) -> Result<()> { - Ok(()) + this.update(&mut cx, |this, cx| { + this.leader_state + .followers + .remove(&envelope.original_sender_id()?); + Ok(()) + }) + } + + async fn handle_update_followers( + this: ViewHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let leader_id = envelope.original_sender_id()?; + let follower_states = this + .follower_states_by_leader + .get_mut(&leader_id) + .ok_or_else(|| anyhow!("received follow update for an unfollowed peer"))?; + match envelope + .payload + .variant + .ok_or_else(|| anyhow!("invalid update"))? + { + proto::update_followers::Variant::UpdateActiveView(update_active_view) => { + for (pane, state) in follower_states { + state.active_view_id = update_active_view.id.map(|id| id as usize); + } + } + proto::update_followers::Variant::CreateView(_) => todo!(), + proto::update_followers::Variant::UpdateView(_) => todo!(), + } + + this.leader_updated(leader_id, cx); + Ok(()) + }) + } + + fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { + let mut items_to_add = Vec::new(); + for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { + if let Some((pane, active_view_id)) = pane.upgrade(cx).zip(state.active_view_id) { + if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { + items_to_add.push((pane, item.clone())); + } + } + } + + for (pane, item) in items_to_add { + Pane::add_item(self, pane, item.clone(), cx); + } + + None } } From f4520d4184251b787db70ccee4403154b308a437 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2022 18:07:03 +0100 Subject: [PATCH 12/56] WIP --- crates/editor/src/editor.rs | 2 +- crates/editor/src/items.rs | 4 ++-- crates/workspace/src/workspace.rs | 21 +++++++++++++++------ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 00aa8283254a8465b11b99385dbf570fc4ec5715..3f1c9c5fd2aacbf53f6687c280b902cc03120d64 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -341,7 +341,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::find_all_references); workspace::register_project_item::(cx); - workspace::register_followed_item::(cx); + workspace::register_followable_item::(cx); } trait SelectionExt { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7d89341c0d339ee35875ed772955330fdfa1747d..410ffcddca38f4127605023689eec593d5a066d9 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -11,10 +11,10 @@ use std::{fmt::Write, path::PathBuf}; use text::{Point, Selection}; use util::ResultExt; use workspace::{ - FollowedItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView, + FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView, }; -impl FollowedItem for Editor { +impl FollowableItem for Editor { fn for_state_message( pane: ViewHandle, project: ModelHandle, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3cbd167319d06e364c62b0fe6f31b1d1171adf05..b011566c0fbe93a7065b896c0a8c402068967e27 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -131,7 +131,7 @@ pub fn register_project_item(cx: &mut MutableAppContext) { }); } -pub fn register_followed_item(cx: &mut MutableAppContext) { +pub fn register_followable_item(cx: &mut MutableAppContext) { cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { builders.insert( TypeId::of::(), @@ -234,7 +234,7 @@ pub trait ProjectItem: Item { ) -> Self; } -pub trait FollowedItem: Item { +pub trait FollowableItem: Item { fn for_state_message( pane: ViewHandle, project: ModelHandle, @@ -261,7 +261,7 @@ pub trait FollowedItemHandle { ) -> Option; } -impl FollowedItemHandle for ViewHandle { +impl FollowedItemHandle for ViewHandle { fn id(&self) -> usize { self.id() } @@ -586,7 +586,12 @@ struct LeaderState { struct FollowerState { active_view_id: Option, - items_by_leader_view_id: HashMap>, + items_by_leader_view_id: HashMap, +} + +enum FollowerItem { + Loading(Vec), + Loaded(Box), } impl Workspace { @@ -1524,12 +1529,16 @@ impl Workspace { .ok_or_else(|| anyhow!("invalid update"))? { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - for (pane, state) in follower_states { + for state in follower_states.values_mut() { state.active_view_id = update_active_view.id.map(|id| id as usize); } } + proto::update_followers::Variant::UpdateView(update_view) => { + for state in follower_states.values_mut() { + state.items_by_leader_view_id.get(k) + } + } proto::update_followers::Variant::CreateView(_) => todo!(), - proto::update_followers::Variant::UpdateView(_) => todo!(), } this.leader_updated(leader_id, cx); From 2c5317556635f9d16a80ff1829f0c51c52a1c6f7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 10:12:02 -0700 Subject: [PATCH 13/56] Rename FollowedItem -> FollowableItem Co-Authored-By: Antonio Scandurra --- crates/workspace/src/workspace.rs | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b011566c0fbe93a7065b896c0a8c402068967e27..ca1e733fe3ea9a7bb9cbc1353d58fd43a4115a78 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -50,17 +50,17 @@ type ProjectItemBuilders = HashMap< fn(usize, ModelHandle, AnyModelHandle, &mut MutableAppContext) -> Box, >; -type FollowedItemBuilder = fn( +type FollowableItemBuilder = fn( ViewHandle, ModelHandle, &mut Option, &mut MutableAppContext, ) -> Option>>>; -type FollowedItemBuilders = HashMap< +type FollowableItemBuilders = HashMap< TypeId, ( - FollowedItemBuilder, - fn(AnyViewHandle) -> Box, + FollowableItemBuilder, + fn(AnyViewHandle) -> Box, ), >; @@ -132,7 +132,7 @@ pub fn register_project_item(cx: &mut MutableAppContext) { } pub fn register_followable_item(cx: &mut MutableAppContext) { - cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { + cx.update_default_global(|builders: &mut FollowableItemBuilders, _| { builders.insert( TypeId::of::(), (I::for_state_message, |this| { @@ -251,7 +251,7 @@ pub trait FollowableItem: Item { ) -> Option; } -pub trait FollowedItemHandle { +pub trait FollowableItemHandle { fn id(&self) -> usize; fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; fn to_update_message( @@ -261,7 +261,7 @@ pub trait FollowedItemHandle { ) -> Option; } -impl FollowedItemHandle for ViewHandle { +impl FollowableItemHandle for ViewHandle { fn id(&self) -> usize { self.id() } @@ -308,7 +308,7 @@ pub trait ItemHandle: 'static { cx: &mut MutableAppContext, ) -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; - fn to_followed_item_handle(&self, cx: &AppContext) -> Option>; + fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; } pub trait WeakItemHandle { @@ -363,7 +363,7 @@ impl ItemHandle for ViewHandle { pane: ViewHandle, cx: &mut ViewContext, ) { - if let Some(followed_item) = self.to_followed_item_handle(cx) { + if let Some(followed_item) = self.to_followable_item_handle(cx) { workspace.update_followers( proto::update_followers::Variant::CreateView(proto::View { id: followed_item.id() as u64, @@ -401,7 +401,7 @@ impl ItemHandle for ViewHandle { } if let Some(message) = item - .to_followed_item_handle(cx) + .to_followable_item_handle(cx) .and_then(|i| i.to_update_message(event, cx)) { workspace.update_followers( @@ -467,9 +467,9 @@ impl ItemHandle for ViewHandle { self.read(cx).act_as_type(type_id, self, cx) } - fn to_followed_item_handle(&self, cx: &AppContext) -> Option> { - if cx.has_global::() { - let builders = cx.global::(); + fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { + if cx.has_global::() { + let builders = cx.global::(); let item = self.to_any(); Some(builders.get(&item.view_type())?.1(item)) } else { @@ -1186,7 +1186,7 @@ impl Workspace { (this.project.clone(), this.active_pane().clone()) }); let item_builders = cx.update(|cx| { - cx.default_global::() + cx.default_global::() .values() .map(|b| b.0) .collect::>() @@ -1477,7 +1477,7 @@ impl Workspace { let active_view_id = this .active_item(cx) - .and_then(|i| i.to_followed_item_handle(cx)) + .and_then(|i| i.to_followable_item_handle(cx)) .map(|i| i.id() as u64); Ok(proto::FollowResponse { active_view_id, @@ -1485,7 +1485,7 @@ impl Workspace { .items(cx) .filter_map(|item| { let id = item.id() as u64; - let item = item.to_followed_item_handle(cx)?; + let item = item.to_followable_item_handle(cx)?; let variant = item.to_state_message(cx); Some(proto::View { id, From d02ab9bd061e1a11fb065531cdc25aff15f782bc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 12:25:30 -0700 Subject: [PATCH 14/56] Start work on updating editors's scroll positions when following Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 9 +++ crates/editor/src/items.rs | 68 +++++++++++--------- crates/editor/src/multi_buffer.rs | 8 +++ crates/language/src/proto.rs | 20 +++--- crates/rpc/proto/zed.proto | 4 +- crates/workspace/src/workspace.rs | 101 +++++++++++++++++++++--------- 6 files changed, 139 insertions(+), 71 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3f1c9c5fd2aacbf53f6687c280b902cc03120d64..aa4162402943f6ad08c8cba0d4425d279e4a9f31 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1033,6 +1033,14 @@ impl Editor { self.scroll_top_anchor = Some(anchor); } + cx.emit(Event::ScrollPositionChanged); + cx.notify(); + } + + fn set_scroll_top_anchor(&mut self, anchor: Anchor, cx: &mut ViewContext) { + self.scroll_position = Vector2F::zero(); + self.scroll_top_anchor = Some(anchor); + cx.emit(Event::ScrollPositionChanged); cx.notify(); } @@ -5634,6 +5642,7 @@ pub enum Event { Saved, TitleChanged, SelectionsChanged, + ScrollPositionChanged, Closed, } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 410ffcddca38f4127605023689eec593d5a066d9..5ef739510b6047d212ffe04127a8a945648eeb5b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -6,7 +6,7 @@ use gpui::{ }; use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectEntryId, ProjectPath}; -use rpc::proto; +use rpc::proto::{self, update_followers::update_view}; use std::{fmt::Write, path::PathBuf}; use text::{Point, Selection}; use util::ResultExt; @@ -20,7 +20,7 @@ impl FollowableItem for Editor { project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, - ) -> Option>>> { + ) -> Option>>> { let state = if matches!(state, Some(proto::view::Variant::Editor(_))) { if let Some(proto::view::Variant::Editor(state)) = state.take() { state @@ -36,7 +36,7 @@ impl FollowableItem for Editor { }); Some(cx.spawn(|mut cx| async move { let buffer = buffer.await?; - let editor = pane + Ok(pane .read_with(&cx, |pane, cx| { pane.items_of_type::().find(|editor| { editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer) @@ -46,8 +46,7 @@ impl FollowableItem for Editor { cx.add_view(pane.window_id(), |cx| { Editor::for_buffer(buffer, Some(project), cx) }) - }); - Ok(Box::new(editor) as Box<_>) + })) })) } @@ -59,17 +58,12 @@ impl FollowableItem for Editor { .unwrap() .read(cx) .remote_id(); - let selection = self.newest_anchor_selection(); - let selection = Selection { - id: selection.id, - start: selection.start.text_anchor.clone(), - end: selection.end.text_anchor.clone(), - reversed: selection.reversed, - goal: Default::default(), - }; proto::view::Variant::Editor(proto::view::Editor { buffer_id, - newest_selection: Some(language::proto::serialize_selection(&selection)), + scroll_top: self + .scroll_top_anchor + .as_ref() + .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), }) } @@ -77,26 +71,42 @@ impl FollowableItem for Editor { &self, event: &Self::Event, cx: &AppContext, - ) -> Option { + ) -> Option { match event { - Event::SelectionsChanged => { - let selection = self.newest_anchor_selection(); - let selection = Selection { - id: selection.id, - start: selection.start.text_anchor.clone(), - end: selection.end.text_anchor.clone(), - reversed: selection.reversed, - goal: Default::default(), - }; - Some(proto::update_followers::update_view::Variant::Editor( - proto::update_followers::update_view::Editor { - newest_selection: Some(language::proto::serialize_selection(&selection)), - }, - )) + Event::ScrollPositionChanged => { + Some(update_view::Variant::Editor(update_view::Editor { + scroll_top: self + .scroll_top_anchor + .as_ref() + .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), + })) } _ => None, } } + + fn apply_update_message( + &mut self, + message: update_view::Variant, + cx: &mut ViewContext, + ) -> Result<()> { + match message { + update_view::Variant::Editor(message) => { + if let Some(anchor) = message.scroll_top { + let anchor = language::proto::deserialize_anchor(anchor) + .ok_or_else(|| anyhow!("invalid scroll top"))?; + let anchor = { + let buffer = self.buffer.read(cx); + let buffer = buffer.read(cx); + let (excerpt_id, _, _) = buffer.as_singleton().unwrap(); + buffer.anchor_in_excerpt(excerpt_id.clone(), anchor) + }; + self.set_scroll_top_anchor(anchor, cx); + } + } + } + Ok(()) + } } impl Item for Editor { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index e145488d650cb49d44d6b3de3af9e6304f02609e..12493d6728ac213cd0d10e4364e85b282899ef36 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -821,6 +821,14 @@ impl MultiBuffer { .map_or(Vec::new(), |state| state.excerpts.clone()) } + pub fn excerpt_ids(&self) -> Vec { + self.buffers + .borrow() + .values() + .flat_map(|state| state.excerpts.iter().cloned()) + .collect() + } + pub fn excerpt_containing( &self, position: impl ToOffset, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 09d4236afe281f9c8983896993689fc777c74465..deedf3e88b596209457b5b50087e4655cb968a10 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -275,19 +275,21 @@ pub fn deserialize_selections(selections: Vec) -> Arc<[Selecti Arc::from( selections .into_iter() - .filter_map(|selection| { - Some(Selection { - id: selection.id as usize, - start: deserialize_anchor(selection.start?)?, - end: deserialize_anchor(selection.end?)?, - reversed: selection.reversed, - goal: SelectionGoal::None, - }) - }) + .filter_map(deserialize_selection) .collect::>(), ) } +pub fn deserialize_selection(selection: proto::Selection) -> Option> { + Some(Selection { + id: selection.id as usize, + start: deserialize_anchor(selection.start?)?, + end: deserialize_anchor(selection.end?)?, + reversed: selection.reversed, + goal: SelectionGoal::None, + }) +} + pub fn deserialize_diagnostics( diagnostics: Vec, ) -> Arc<[DiagnosticEntry]> { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 1ea340278ac962d7631df5690e70c1f4819d9fee..f0743c5b4fa02ba4952051eb7a76cd8c004dd058 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -568,7 +568,7 @@ message UpdateFollowers { } message Editor { - Selection newest_selection = 1; + Anchor scroll_top = 1; } } } @@ -588,7 +588,7 @@ message View { message Editor { uint64 buffer_id = 1; - Selection newest_selection = 2; + Anchor scroll_top = 2; } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ca1e733fe3ea9a7bb9cbc1353d58fd43a4115a78..386eba66eb76290b70a53299eb32834faaff7248 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6,7 +6,7 @@ pub mod settings; pub mod sidebar; mod status_bar; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use client::{ proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, }; @@ -43,7 +43,7 @@ use std::{ sync::Arc, }; use theme::{Theme, ThemeRegistry}; -use util::ResultExt; +use util::{ResultExt, TryFutureExt}; type ProjectItemBuilders = HashMap< TypeId, @@ -55,7 +55,7 @@ type FollowableItemBuilder = fn( ModelHandle, &mut Option, &mut MutableAppContext, -) -> Option>>>; +) -> Option>>>; type FollowableItemBuilders = HashMap< TypeId, ( @@ -135,9 +135,15 @@ pub fn register_followable_item(cx: &mut MutableAppContext) { cx.update_default_global(|builders: &mut FollowableItemBuilders, _| { builders.insert( TypeId::of::(), - (I::for_state_message, |this| { - Box::new(this.downcast::().unwrap()) - }), + ( + |pane, project, state, cx| { + I::for_state_message(pane, project, state, cx).map(|task| { + cx.foreground() + .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) + }) + }, + |this| Box::new(this.downcast::().unwrap()), + ), ); }); } @@ -240,32 +246,35 @@ pub trait FollowableItem: Item { project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, - ) -> Option>>> - where - Self: Sized; + ) -> Option>>>; fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; fn to_update_message( &self, event: &Self::Event, cx: &AppContext, ) -> Option; + fn apply_update_message( + &mut self, + message: proto::update_followers::update_view::Variant, + cx: &mut ViewContext, + ) -> Result<()>; } -pub trait FollowableItemHandle { - fn id(&self) -> usize; +pub trait FollowableItemHandle: ItemHandle { fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; fn to_update_message( &self, event: &dyn Any, cx: &AppContext, ) -> Option; + fn apply_update_message( + &self, + message: proto::update_followers::update_view::Variant, + cx: &mut MutableAppContext, + ) -> Result<()>; } impl FollowableItemHandle for ViewHandle { - fn id(&self) -> usize { - self.id() - } - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant { self.read(cx).to_state_message(cx) } @@ -277,6 +286,14 @@ impl FollowableItemHandle for ViewHandle { ) -> Option { self.read(cx).to_update_message(event.downcast_ref()?, cx) } + + fn apply_update_message( + &self, + message: proto::update_followers::update_view::Variant, + cx: &mut MutableAppContext, + ) -> Result<()> { + self.update(cx, |this, cx| this.apply_update_message(message, cx)) + } } pub trait ItemHandle: 'static { @@ -584,14 +601,15 @@ struct LeaderState { followers: HashSet, } +#[derive(Default)] struct FollowerState { active_view_id: Option, items_by_leader_view_id: HashMap, } enum FollowerItem { - Loading(Vec), - Loaded(Box), + Loading(Vec), + Loaded(Box), } impl Workspace { @@ -1212,21 +1230,40 @@ impl Workspace { }); } - let items = futures::future::try_join_all(item_tasks).await?; - let follower_state = FollowerState { - active_view_id: response.active_view_id.map(|id| id as usize), - items_by_leader_view_id: response - .views - .iter() - .map(|v| v.id as usize) - .zip(items) - .collect(), - }; this.update(&mut cx, |this, cx| { this.follower_states_by_leader .entry(leader_id) .or_default() - .insert(pane.downgrade(), follower_state); + .insert( + pane.downgrade(), + FollowerState { + active_view_id: response.active_view_id.map(|id| id as usize), + items_by_leader_view_id: Default::default(), + }, + ); + }); + + let items = futures::future::try_join_all(item_tasks).await?; + this.update(&mut cx, |this, cx| { + let follower_state = this + .follower_states_by_leader + .entry(leader_id) + .or_default() + .entry(pane.downgrade()) + .or_default(); + for (id, item) in response.views.iter().map(|v| v.id as usize).zip(items) { + let prev_state = follower_state.items_by_leader_view_id.remove(&id); + if let Some(FollowerItem::Loading(updates)) = prev_state { + for update in updates { + item.apply_update_message(update, cx) + .context("failed to apply view update") + .log_err(); + } + } + follower_state + .items_by_leader_view_id + .insert(id, FollowerItem::Loaded(item)); + } this.leader_updated(leader_id, cx); }); } @@ -1535,7 +1572,7 @@ impl Workspace { } proto::update_followers::Variant::UpdateView(update_view) => { for state in follower_states.values_mut() { - state.items_by_leader_view_id.get(k) + // state.items_by_leader_view_id.get(k) } } proto::update_followers::Variant::CreateView(_) => todo!(), @@ -1550,8 +1587,10 @@ impl Workspace { let mut items_to_add = Vec::new(); for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { if let Some((pane, active_view_id)) = pane.upgrade(cx).zip(state.active_view_id) { - if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { - items_to_add.push((pane, item.clone())); + if let Some(FollowerItem::Loaded(item)) = + state.items_by_leader_view_id.get(&active_view_id) + { + items_to_add.push((pane, item.boxed_clone())); } } } From df0632011c802054167b493ac3d04a24291efad9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 13:03:43 -0700 Subject: [PATCH 15/56] :art: client Forgot to push this yesterday night. --- crates/client/src/client.rs | 198 +++++++++++++++--------------------- 1 file changed, 80 insertions(+), 118 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index c2527ed94a6a5d5d44a15318fd92a78f9ca1433f..d4a5a47b4b509fab8af4eabc9e632a2d952c58e1 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -136,7 +136,7 @@ impl Status { struct ClientState { credentials: Option, status: (watch::Sender, watch::Receiver), - entity_id_extractors: HashMap u64>>, + entity_id_extractors: HashMap u64>, _reconnect_task: Option>, reconnect_interval: Duration, entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>, @@ -150,6 +150,7 @@ struct ClientState { + Fn( AnyEntityHandle, Box, + &Arc, AsyncAppContext, ) -> LocalBoxFuture<'static, Result<()>>, >, @@ -328,12 +329,11 @@ impl Client { remote_id: u64, cx: &mut ViewContext, ) -> Subscription { - let handle = AnyViewHandle::from(cx.handle()); - let mut state = self.state.write(); let id = (TypeId::of::(), remote_id); - state + self.state + .write() .entities_by_type_and_remote_id - .insert(id, AnyWeakEntityHandle::View(handle.downgrade())); + .insert(id, AnyWeakEntityHandle::View(cx.weak_handle().into())); Subscription::Entity { client: Arc::downgrade(self), id, @@ -345,12 +345,11 @@ impl Client { remote_id: u64, cx: &mut ModelContext, ) -> Subscription { - let handle = AnyModelHandle::from(cx.handle()); - let mut state = self.state.write(); let id = (TypeId::of::(), remote_id); - state + self.state + .write() .entities_by_type_and_remote_id - .insert(id, AnyWeakEntityHandle::Model(handle.downgrade())); + .insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into())); Subscription::Entity { client: Arc::downgrade(self), id, @@ -373,7 +372,6 @@ impl Client { { let message_type_id = TypeId::of::(); - let client = Arc::downgrade(self); let mut state = self.state.write(); state .models_by_message_type @@ -381,7 +379,7 @@ impl Client { let prev_handler = state.message_handlers.insert( message_type_id, - Arc::new(move |handle, envelope, cx| { + Arc::new(move |handle, envelope, client, cx| { let handle = if let AnyEntityHandle::Model(handle) = handle { handle } else { @@ -389,11 +387,7 @@ impl Client { }; let model = handle.downcast::().unwrap(); let envelope = envelope.into_any().downcast::>().unwrap(); - if let Some(client) = client.upgrade() { - handler(model, *envelope, client.clone(), cx).boxed_local() - } else { - async move { Ok(()) }.boxed_local() - } + handler(model, *envelope, client.clone(), cx).boxed_local() }), ); if prev_handler.is_some() { @@ -416,47 +410,13 @@ impl Client { + Fn(ViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, { - let entity_type_id = TypeId::of::(); - let message_type_id = TypeId::of::(); - - let client = Arc::downgrade(self); - let mut state = self.state.write(); - state - .entity_types_by_message_type - .insert(message_type_id, entity_type_id); - state - .entity_id_extractors - .entry(message_type_id) - .or_insert_with(|| { - Box::new(|envelope| { - let envelope = envelope - .as_any() - .downcast_ref::>() - .unwrap(); - envelope.payload.remote_entity_id() - }) - }); - - let prev_handler = state.message_handlers.insert( - message_type_id, - Arc::new(move |handle, envelope, cx| { - let handle = if let AnyEntityHandle::View(handle) = handle { - handle - } else { - unreachable!(); - }; - let model = handle.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - if let Some(client) = client.upgrade() { - handler(model, *envelope, client.clone(), cx).boxed_local() - } else { - async move { Ok(()) }.boxed_local() - } - }), - ); - if prev_handler.is_some() { - panic!("registered handler for the same message twice"); - } + self.add_entity_message_handler::(move |handle, message, client, cx| { + if let AnyEntityHandle::View(handle) = handle { + handler(handle.downcast::().unwrap(), message, client, cx) + } else { + unreachable!(); + } + }) } pub fn add_model_message_handler(self: &Arc, handler: H) @@ -468,11 +428,29 @@ impl Client { + Sync + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, + { + self.add_entity_message_handler::(move |handle, message, client, cx| { + if let AnyEntityHandle::Model(handle) = handle { + handler(handle.downcast::().unwrap(), message, client, cx) + } else { + unreachable!(); + } + }) + } + + fn add_entity_message_handler(self: &Arc, handler: H) + where + M: EntityMessage, + E: Entity, + H: 'static + + Send + + Sync + + Fn(AnyEntityHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + F: 'static + Future>, { let model_type_id = TypeId::of::(); let message_type_id = TypeId::of::(); - let client = Arc::downgrade(self); let mut state = self.state.write(); state .entity_types_by_message_type @@ -481,30 +459,20 @@ impl Client { .entity_id_extractors .entry(message_type_id) .or_insert_with(|| { - Box::new(|envelope| { - let envelope = envelope + |envelope| { + envelope .as_any() .downcast_ref::>() - .unwrap(); - envelope.payload.remote_entity_id() - }) + .unwrap() + .payload + .remote_entity_id() + } }); - let prev_handler = state.message_handlers.insert( message_type_id, - Arc::new(move |handle, envelope, cx| { - if let Some(client) = client.upgrade() { - let handle = if let AnyEntityHandle::Model(handle) = handle { - handle - } else { - unreachable!(); - }; - let model = handle.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - handler(model, *envelope, client.clone(), cx).boxed_local() - } else { - async move { Ok(()) }.boxed_local() - } + Arc::new(move |handle, envelope, client, cx| { + let envelope = envelope.into_any().downcast::>().unwrap(); + handler(handle, *envelope, client.clone(), cx).boxed_local() }), ); if prev_handler.is_some() { @@ -522,26 +490,12 @@ impl Client { + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, { - self.add_model_message_handler(move |model, envelope, client, cx| { - let receipt = envelope.receipt(); - let response = handler(model, envelope, client.clone(), cx); - async move { - match response.await { - Ok(response) => { - client.respond(receipt, response)?; - Ok(()) - } - Err(error) => { - client.respond_with_error( - receipt, - proto::Error { - message: error.to_string(), - }, - )?; - Err(error) - } - } - } + self.add_model_message_handler(move |entity, envelope, client, cx| { + Self::respond_to_request::( + envelope.receipt(), + handler(entity, envelope, client.clone(), cx), + client, + ) }) } @@ -555,29 +509,37 @@ impl Client { + Fn(ViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, { - self.add_view_message_handler(move |view, envelope, client, cx| { - let receipt = envelope.receipt(); - let response = handler(view, envelope, client.clone(), cx); - async move { - match response.await { - Ok(response) => { - client.respond(receipt, response)?; - Ok(()) - } - Err(error) => { - client.respond_with_error( - receipt, - proto::Error { - message: error.to_string(), - }, - )?; - Err(error) - } - } - } + self.add_view_message_handler(move |entity, envelope, client, cx| { + Self::respond_to_request::( + envelope.receipt(), + handler(entity, envelope, client.clone(), cx), + client, + ) }) } + async fn respond_to_request>>( + receipt: Receipt, + response: F, + client: Arc, + ) -> Result<()> { + match response.await { + Ok(response) => { + client.respond(receipt, response)?; + Ok(()) + } + Err(error) => { + client.respond_with_error( + receipt, + proto::Error { + message: error.to_string(), + }, + )?; + Err(error) + } + } + } + pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { read_credentials_from_keychain(cx).is_some() } @@ -718,7 +680,7 @@ impl Client { if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned() { drop(state); // Avoid deadlocks if the handler interacts with rpc::Client - let future = handler(model, message, cx.clone()); + let future = handler(model, message, &this, cx.clone()); let client_id = this.id; log::debug!( From d860ed25c18e4691813a7e96223f6cb9e2e455a1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 13:36:05 -0700 Subject: [PATCH 16/56] Allow FollowableItem::to_state_message to return None This way, we can avoid a panic if we don't handle certain cases, like a non-singleton editor. --- crates/editor/src/items.rs | 14 ++++---------- crates/workspace/src/workspace.rs | 24 +++++++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 5ef739510b6047d212ffe04127a8a945648eeb5b..d4163ce353eda5d54afac069d86dc833de9f4d61 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -50,21 +50,15 @@ impl FollowableItem for Editor { })) } - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant { - let buffer_id = self - .buffer - .read(cx) - .as_singleton() - .unwrap() - .read(cx) - .remote_id(); - proto::view::Variant::Editor(proto::view::Editor { + fn to_state_message(&self, cx: &AppContext) -> Option { + let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); + Some(proto::view::Variant::Editor(proto::view::Editor { buffer_id, scroll_top: self .scroll_top_anchor .as_ref() .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), - }) + })) } fn to_update_message( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 386eba66eb76290b70a53299eb32834faaff7248..2e84f6c55cb2b5b42f2439257545c55231f4f203 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -247,7 +247,7 @@ pub trait FollowableItem: Item { state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; + fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, event: &Self::Event, @@ -261,7 +261,7 @@ pub trait FollowableItem: Item { } pub trait FollowableItemHandle: ItemHandle { - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; + fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, event: &dyn Any, @@ -275,7 +275,7 @@ pub trait FollowableItemHandle: ItemHandle { } impl FollowableItemHandle for ViewHandle { - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant { + fn to_state_message(&self, cx: &AppContext) -> Option { self.read(cx).to_state_message(cx) } @@ -381,13 +381,15 @@ impl ItemHandle for ViewHandle { cx: &mut ViewContext, ) { if let Some(followed_item) = self.to_followable_item_handle(cx) { - workspace.update_followers( - proto::update_followers::Variant::CreateView(proto::View { - id: followed_item.id() as u64, - variant: Some(followed_item.to_state_message(cx)), - }), - cx, - ); + if let Some(message) = followed_item.to_state_message(cx) { + workspace.update_followers( + proto::update_followers::Variant::CreateView(proto::View { + id: followed_item.id() as u64, + variant: Some(message), + }), + cx, + ); + } } let pane = pane.downgrade(); @@ -1523,7 +1525,7 @@ impl Workspace { .filter_map(|item| { let id = item.id() as u64; let item = item.to_followable_item_handle(cx)?; - let variant = item.to_state_message(cx); + let variant = item.to_state_message(cx)?; Some(proto::View { id, variant: Some(variant), From e338da0271390333bd1de2877e4abd86158d6d41 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 13:37:07 -0700 Subject: [PATCH 17/56] Allow clicking a titlebar avatar to initiate following --- crates/gpui/src/app.rs | 6 + crates/server/src/rpc.rs | 7 +- crates/workspace/src/workspace.rs | 233 ++++++++++++++++-------------- 3 files changed, 133 insertions(+), 113 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f8f505ee783fe274e9f4b182bbbe68c4d34cc760..80fc36cba3d2d758fb3898218985e4dc3efd6c65 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -184,6 +184,12 @@ macro_rules! action { Box::new(self.clone()) } } + + impl From<$arg> for $name { + fn from(arg: $arg) -> Self { + Self(arg) + } + } }; ($name:ident) => { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 25857c53e48cd79250c93e3f80f89c1e3c0b81e5..daa817b670adbd636320b875b3ea2ed484bc7cde 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4272,8 +4272,8 @@ mod tests { workspace_b .update(cx_b, |workspace, cx| { workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); - let leader_id = project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.follow(*leader_id, cx) + let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); + workspace.follow(&leader_id.into(), cx).unwrap() }) .await .unwrap(); @@ -4291,8 +4291,7 @@ mod tests { }); workspace_b .condition(cx_b, |workspace, cx| { - let active_item = workspace.active_item(cx).unwrap(); - active_item.project_path(cx) == Some((worktree_id, "1.txt").into()) + workspace.active_item(cx).unwrap().id() == editor_b1.id() }) .await; } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2e84f6c55cb2b5b42f2439257545c55231f4f203..7b893c566678db14b51a10120be2509ee4d18f2b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -43,7 +43,7 @@ use std::{ sync::Arc, }; use theme::{Theme, ThemeRegistry}; -use util::{ResultExt, TryFutureExt}; +use util::ResultExt; type ProjectItemBuilders = HashMap< TypeId, @@ -68,6 +68,7 @@ action!(Open, Arc); action!(OpenNew, Arc); action!(OpenPaths, OpenParams); action!(ToggleShare); +action!(FollowCollaborator, PeerId); action!(JoinProject, JoinProjectParams); action!(Save); action!(DebugElements); @@ -88,6 +89,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { }); cx.add_action(Workspace::toggle_share); + cx.add_async_action(Workspace::follow); cx.add_action( |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext| { workspace.save_active_item(cx).detach_and_log_err(cx); @@ -1192,88 +1194,90 @@ impl Workspace { } } - pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Task> { - if let Some(project_id) = self.project.read(cx).remote_id() { - let request = self.client.request(proto::Follow { - project_id, - leader_id: leader_id.0, - }); - cx.spawn_weak(|this, mut cx| async move { - let mut response = request.await?; - if let Some(this) = this.upgrade(&cx) { - let mut item_tasks = Vec::new(); - let (project, pane) = this.read_with(&cx, |this, _| { - (this.project.clone(), this.active_pane().clone()) - }); - let item_builders = cx.update(|cx| { - cx.default_global::() - .values() - .map(|b| b.0) - .collect::>() - .clone() - }); - for view in &mut response.views { - let variant = view - .variant - .take() - .ok_or_else(|| anyhow!("missing variant"))?; - cx.update(|cx| { - let mut variant = Some(variant); - for build_item in &item_builders { - if let Some(task) = - build_item(pane.clone(), project.clone(), &mut variant, cx) - { - item_tasks.push(task); - break; - } else { - assert!(variant.is_some()); - } + pub fn follow( + &mut self, + FollowCollaborator(leader_id): &FollowCollaborator, + cx: &mut ViewContext, + ) -> Option>> { + let leader_id = *leader_id; + let project_id = self.project.read(cx).remote_id()?; + let request = self.client.request(proto::Follow { + project_id, + leader_id: leader_id.0, + }); + Some(cx.spawn_weak(|this, mut cx| async move { + let mut response = request.await?; + if let Some(this) = this.upgrade(&cx) { + let mut item_tasks = Vec::new(); + let (project, pane) = this.read_with(&cx, |this, _| { + (this.project.clone(), this.active_pane().clone()) + }); + let item_builders = cx.update(|cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + .clone() + }); + for view in &mut response.views { + let variant = view + .variant + .take() + .ok_or_else(|| anyhow!("missing variant"))?; + cx.update(|cx| { + let mut variant = Some(variant); + for build_item in &item_builders { + if let Some(task) = + build_item(pane.clone(), project.clone(), &mut variant, cx) + { + item_tasks.push(task); + break; + } else { + assert!(variant.is_some()); } - }); - } - - this.update(&mut cx, |this, cx| { - this.follower_states_by_leader - .entry(leader_id) - .or_default() - .insert( - pane.downgrade(), - FollowerState { - active_view_id: response.active_view_id.map(|id| id as usize), - items_by_leader_view_id: Default::default(), - }, - ); + } }); + } - let items = futures::future::try_join_all(item_tasks).await?; - this.update(&mut cx, |this, cx| { - let follower_state = this - .follower_states_by_leader - .entry(leader_id) - .or_default() - .entry(pane.downgrade()) - .or_default(); - for (id, item) in response.views.iter().map(|v| v.id as usize).zip(items) { - let prev_state = follower_state.items_by_leader_view_id.remove(&id); - if let Some(FollowerItem::Loading(updates)) = prev_state { - for update in updates { - item.apply_update_message(update, cx) - .context("failed to apply view update") - .log_err(); - } + this.update(&mut cx, |this, cx| { + this.follower_states_by_leader + .entry(leader_id) + .or_default() + .insert( + pane.downgrade(), + FollowerState { + active_view_id: response.active_view_id.map(|id| id as usize), + items_by_leader_view_id: Default::default(), + }, + ); + }); + + let items = futures::future::try_join_all(item_tasks).await?; + this.update(&mut cx, |this, cx| { + let follower_state = this + .follower_states_by_leader + .entry(leader_id) + .or_default() + .entry(pane.downgrade()) + .or_default(); + for (id, item) in response.views.iter().map(|v| v.id as usize).zip(items) { + let prev_state = follower_state.items_by_leader_view_id.remove(&id); + if let Some(FollowerItem::Loading(updates)) = prev_state { + for update in updates { + item.apply_update_message(update, cx) + .context("failed to apply view update") + .log_err(); } - follower_state - .items_by_leader_view_id - .insert(id, FollowerItem::Loaded(item)); } - this.leader_updated(leader_id, cx); - }); - } - Ok(()) - }) - } else { - Task::ready(Err(anyhow!("project is not remote"))) - } + follower_state + .items_by_leader_view_id + .insert(id, FollowerItem::Loaded(item)); + } + this.leader_updated(leader_id, cx); + }); + } + Ok(()) + })) } fn update_followers( @@ -1383,7 +1387,9 @@ impl Workspace { Some(self.render_avatar( collaborator.user.avatar.clone()?, collaborator.replica_id, + Some(collaborator.peer_id), theme, + cx, )) }) .collect() @@ -1397,7 +1403,7 @@ impl Workspace { cx: &mut RenderContext, ) -> ElementBox { if let Some(avatar) = user.and_then(|user| user.avatar.clone()) { - self.render_avatar(avatar, replica_id, theme) + self.render_avatar(avatar, replica_id, None, theme, cx) } else { MouseEventHandler::new::(0, cx, |state, _| { let style = if state.hovered { @@ -1421,52 +1427,61 @@ impl Workspace { &self, avatar: Arc, replica_id: ReplicaId, + peer_id: Option, theme: &Theme, + cx: &mut RenderContext, ) -> ElementBox { - ConstrainedBox::new( - Stack::new() - .with_child( - ConstrainedBox::new( - Image::new(avatar) - .with_style(theme.workspace.titlebar.avatar) - .boxed(), - ) + let content = Stack::new() + .with_child( + Image::new(avatar) + .with_style(theme.workspace.titlebar.avatar) + .constrained() .with_width(theme.workspace.titlebar.avatar_width) .aligned() .boxed(), - ) - .with_child( - AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor) - .constrained() - .with_width(theme.workspace.titlebar.avatar_ribbon.width) - .with_height(theme.workspace.titlebar.avatar_ribbon.height) - .aligned() - .bottom() - .boxed(), - ) - .boxed(), - ) - .with_width(theme.workspace.right_sidebar.width) - .boxed() + ) + .with_child( + AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor) + .constrained() + .with_width(theme.workspace.titlebar.avatar_ribbon.width) + .with_height(theme.workspace.titlebar.avatar_ribbon.height) + .aligned() + .bottom() + .boxed(), + ) + .constrained() + .with_width(theme.workspace.right_sidebar.width) + .boxed(); + + if let Some(peer_id) = peer_id { + MouseEventHandler::new::( + replica_id.into(), + cx, + move |_, _| content, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |cx| cx.dispatch_action(FollowCollaborator(peer_id))) + .boxed() + } else { + content + } } fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext) -> Option { if self.project().read(cx).is_local() && self.client.user_id().is_some() { - enum Share {} - let color = if self.project().read(cx).is_shared() { theme.workspace.titlebar.share_icon_active_color } else { theme.workspace.titlebar.share_icon_color }; Some( - MouseEventHandler::new::(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Align::new( - ConstrainedBox::new( - Svg::new("icons/broadcast-24.svg").with_color(color).boxed(), - ) - .with_width(24.) - .boxed(), + Svg::new("icons/broadcast-24.svg") + .with_color(color) + .constrained() + .with_width(24.) + .boxed(), ) .boxed() }) From 570c98745574de51bacd3792b46263eadd0f18a4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 15:56:57 -0700 Subject: [PATCH 18/56] Handle view updates when following Basic following now works. Editors' scroll positions are their only replicated view state. --- crates/editor/src/items.rs | 2 +- crates/server/src/rpc.rs | 8 +- crates/workspace/src/pane.rs | 4 +- crates/workspace/src/workspace.rs | 247 +++++++++++++++++------------- 4 files changed, 151 insertions(+), 110 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d4163ce353eda5d54afac069d86dc833de9f4d61..f3f00ee01605a6d26acd2e792d687bd6046cabda 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -64,7 +64,7 @@ impl FollowableItem for Editor { fn to_update_message( &self, event: &Self::Event, - cx: &AppContext, + _: &AppContext, ) -> Option { match event { Event::ScrollPositionChanged => { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index daa817b670adbd636320b875b3ea2ed484bc7cde..5585b04ad21c07219e8c34cdd63eb8b2c63100a8 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4252,7 +4252,7 @@ mod tests { }) .await .unwrap(); - let editor_a2 = workspace_a + let _editor_a2 = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), cx) }) @@ -4261,6 +4261,9 @@ mod tests { // Client B opens an editor. let workspace_b = client_b.build_workspace(&project_b, cx_b); + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); let editor_b1 = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), cx) @@ -4271,7 +4274,6 @@ mod tests { // Client B starts following client A. workspace_b .update(cx_b, |workspace, cx| { - workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); workspace.follow(&leader_id.into(), cx).unwrap() }) @@ -4776,7 +4778,7 @@ mod tests { project: &ModelHandle, cx: &mut TestAppContext, ) -> ViewHandle { - let (window_id, _) = cx.add_window(|cx| EmptyView); + let (window_id, _) = cx.add_window(|_| EmptyView); cx.add_view(window_id, |cx| { let fs = project.read(cx).fs().clone(); Workspace::new( diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a2e49f7f481d65825f93d3ebfad5f2b110416e87..367cc967fca85baf5a111341c9e4a914f3139055 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,7 +1,5 @@ use super::{ItemHandle, SplitDirection}; use crate::{Item, Settings, WeakItemHandle, Workspace}; -use anyhow::{anyhow, Result}; -use client::PeerId; use collections::{HashMap, VecDeque}; use gpui::{ action, @@ -258,7 +256,7 @@ impl Pane { let task = task.await; if let Some(pane) = pane.upgrade(&cx) { if let Some((project_entry_id, build_item)) = task.log_err() { - pane.update(&mut cx, |pane, cx| { + pane.update(&mut cx, |pane, _| { pane.nav_history.borrow_mut().set_mode(mode); }); let item = workspace.update(&mut cx, |workspace, cx| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7b893c566678db14b51a10120be2509ee4d18f2b..c1da70193186874b10d7d7c5f2718248bbc741a7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -11,7 +11,7 @@ use client::{ proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, }; use clock::ReplicaId; -use collections::{HashMap, HashSet}; +use collections::{hash_map, HashMap, HashSet}; use gpui::{ action, color::Color, @@ -37,6 +37,7 @@ pub use status_bar::StatusItemView; use std::{ any::{Any, TypeId}, cell::RefCell, + fmt, future::Future, path::{Path, PathBuf}, rc::Rc, @@ -298,7 +299,7 @@ impl FollowableItemHandle for ViewHandle { } } -pub trait ItemHandle: 'static { +pub trait ItemHandle: 'static + fmt::Debug { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn project_entry_id(&self, cx: &AppContext) -> Option; @@ -596,7 +597,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, - follower_states_by_leader: HashMap, FollowerState>>, + follower_states_by_leader: HashMap, _observe_current_user: Task<()>, } @@ -607,10 +608,12 @@ struct LeaderState { #[derive(Default)] struct FollowerState { - active_view_id: Option, - items_by_leader_view_id: HashMap, + active_view_id: Option, + items_by_leader_view_id: HashMap, + panes: HashSet>, } +#[derive(Debug)] enum FollowerItem { Loading(Vec), Loaded(Box), @@ -1206,78 +1209,84 @@ impl Workspace { leader_id: leader_id.0, }); Some(cx.spawn_weak(|this, mut cx| async move { - let mut response = request.await?; + let response = request.await?; if let Some(this) = this.upgrade(&cx) { - let mut item_tasks = Vec::new(); - let (project, pane) = this.read_with(&cx, |this, _| { - (this.project.clone(), this.active_pane().clone()) - }); - let item_builders = cx.update(|cx| { - cx.default_global::() - .values() - .map(|b| b.0) - .collect::>() - .clone() - }); - for view in &mut response.views { - let variant = view - .variant - .take() - .ok_or_else(|| anyhow!("missing variant"))?; - cx.update(|cx| { - let mut variant = Some(variant); - for build_item in &item_builders { - if let Some(task) = - build_item(pane.clone(), project.clone(), &mut variant, cx) - { - item_tasks.push(task); - break; - } else { - assert!(variant.is_some()); - } - } - }); - } - + Self::add_views_from_leader(this.clone(), leader_id, response.views, &mut cx) + .await?; this.update(&mut cx, |this, cx| { - this.follower_states_by_leader - .entry(leader_id) - .or_default() - .insert( - pane.downgrade(), - FollowerState { - active_view_id: response.active_view_id.map(|id| id as usize), - items_by_leader_view_id: Default::default(), - }, - ); - }); + this.follower_state(leader_id)?.active_view_id = response.active_view_id; + this.leader_updated(leader_id, cx); + Ok::<_, anyhow::Error>(()) + })?; + } + Ok(()) + })) + } - let items = futures::future::try_join_all(item_tasks).await?; - this.update(&mut cx, |this, cx| { - let follower_state = this - .follower_states_by_leader - .entry(leader_id) - .or_default() - .entry(pane.downgrade()) - .or_default(); - for (id, item) in response.views.iter().map(|v| v.id as usize).zip(items) { - let prev_state = follower_state.items_by_leader_view_id.remove(&id); - if let Some(FollowerItem::Loading(updates)) = prev_state { - for update in updates { + async fn add_views_from_leader( + this: ViewHandle, + leader_id: PeerId, + views: Vec, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let (project, pane) = this.read_with(cx, |this, _| { + (this.project.clone(), this.active_pane().clone()) + }); + + let item_builders = cx.update(|cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + .clone() + }); + + let mut item_tasks = Vec::new(); + let mut leader_view_ids = Vec::new(); + for view in views { + let mut variant = view.variant; + if variant.is_none() { + Err(anyhow!("missing variant"))?; + } + for build_item in &item_builders { + let task = + cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx)); + if let Some(task) = task { + item_tasks.push(task); + leader_view_ids.push(view.id); + break; + } else { + assert!(variant.is_some()); + } + } + } + + let pane = pane.downgrade(); + let items = futures::future::try_join_all(item_tasks).await?; + this.update(cx, |this, cx| { + let state = this.follower_states_by_leader.entry(leader_id).or_default(); + state.panes.insert(pane); + for (id, item) in leader_view_ids.into_iter().zip(items) { + match state.items_by_leader_view_id.entry(id) { + hash_map::Entry::Occupied(e) => { + let e = e.into_mut(); + if let FollowerItem::Loading(updates) = e { + for update in updates.drain(..) { item.apply_update_message(update, cx) .context("failed to apply view update") .log_err(); } } - follower_state - .items_by_leader_view_id - .insert(id, FollowerItem::Loaded(item)); + *e = FollowerItem::Loaded(item); } - this.leader_updated(leader_id, cx); - }); + hash_map::Entry::Vacant(e) => { + e.insert(FollowerItem::Loaded(item)); + } + } } - Ok(()) - })) + }); + + Ok(()) } fn update_followers( @@ -1557,7 +1566,7 @@ impl Workspace { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, _| { this.leader_state .followers .remove(&envelope.original_sender_id()?); @@ -1571,49 +1580,81 @@ impl Workspace { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let leader_id = envelope.original_sender_id()?; - let follower_states = this - .follower_states_by_leader - .get_mut(&leader_id) - .ok_or_else(|| anyhow!("received follow update for an unfollowed peer"))?; - match envelope - .payload - .variant - .ok_or_else(|| anyhow!("invalid update"))? - { - proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - for state in follower_states.values_mut() { - state.active_view_id = update_active_view.id.map(|id| id as usize); - } - } - proto::update_followers::Variant::UpdateView(update_view) => { - for state in follower_states.values_mut() { - // state.items_by_leader_view_id.get(k) + let leader_id = envelope.original_sender_id()?; + match envelope + .payload + .variant + .ok_or_else(|| anyhow!("invalid update"))? + { + proto::update_followers::Variant::UpdateActiveView(update_active_view) => { + this.update(&mut cx, |this, cx| { + this.follower_state(leader_id)?.active_view_id = update_active_view.id; + this.leader_updated(leader_id, cx); + Ok::<_, anyhow::Error>(()) + }) + } + proto::update_followers::Variant::UpdateView(update_view) => { + this.update(&mut cx, |this, cx| { + let variant = update_view + .variant + .ok_or_else(|| anyhow!("missing update view variant"))?; + match this + .follower_state(leader_id)? + .items_by_leader_view_id + .entry(update_view.id) + .or_insert(FollowerItem::Loading(Vec::new())) + { + FollowerItem::Loaded(item) => { + item.apply_update_message(variant, cx).log_err(); + } + FollowerItem::Loading(updates) => updates.push(variant), } - } - proto::update_followers::Variant::CreateView(_) => todo!(), + this.leader_updated(leader_id, cx); + Ok(()) + }) } + proto::update_followers::Variant::CreateView(view) => { + Self::add_views_from_leader(this.clone(), leader_id, vec![view], &mut cx).await?; + this.update(&mut cx, |this, cx| { + this.leader_updated(leader_id, cx); + }); + Ok(()) + } + } + .log_err(); - this.leader_updated(leader_id, cx); - Ok(()) - }) + Ok(()) + } + + fn follower_state(&mut self, leader_id: PeerId) -> Result<&mut FollowerState> { + self.follower_states_by_leader + .get_mut(&leader_id) + .ok_or_else(|| anyhow!("received follow update for an unfollowed peer")) } fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { - let mut items_to_add = Vec::new(); - for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { - if let Some((pane, active_view_id)) = pane.upgrade(cx).zip(state.active_view_id) { - if let Some(FollowerItem::Loaded(item)) = - state.items_by_leader_view_id.get(&active_view_id) - { - items_to_add.push((pane, item.boxed_clone())); + let state = self.follower_states_by_leader.get_mut(&leader_id)?; + let active_item = state.items_by_leader_view_id.get(&state.active_view_id?)?; + if let FollowerItem::Loaded(item) = active_item { + let mut panes = Vec::new(); + state.panes.retain(|pane| { + if let Some(pane) = pane.upgrade(cx) { + panes.push(pane); + true + } else { + false } - } - } + }); - for (pane, item) in items_to_add { - Pane::add_item(self, pane, item.clone(), cx); + if panes.is_empty() { + self.follower_states_by_leader.remove(&leader_id); + } else { + let item = item.boxed_clone(); + for pane in panes { + Pane::add_item(self, pane, item.clone(), cx); + } + cx.notify(); + } } None From 0e920ad5e97aa1c4638a9027a3ea2f8e7f67051d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Mar 2022 10:50:23 +0100 Subject: [PATCH 19/56] Unset follower's scroll anchor when editor is scrolled all the way up --- crates/editor/src/editor.rs | 4 ++-- crates/editor/src/items.rs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index aa4162402943f6ad08c8cba0d4425d279e4a9f31..b3d74d03a7fb66821208fffdbf4d7e140590489e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1037,9 +1037,9 @@ impl Editor { cx.notify(); } - fn set_scroll_top_anchor(&mut self, anchor: Anchor, cx: &mut ViewContext) { + fn set_scroll_top_anchor(&mut self, anchor: Option, cx: &mut ViewContext) { self.scroll_position = Vector2F::zero(); - self.scroll_top_anchor = Some(anchor); + self.scroll_top_anchor = anchor; cx.emit(Event::ScrollPositionChanged); cx.notify(); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index f3f00ee01605a6d26acd2e792d687bd6046cabda..1ce59dd93fe8df9ad977883655b08538aef31598 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -95,7 +95,9 @@ impl FollowableItem for Editor { let (excerpt_id, _, _) = buffer.as_singleton().unwrap(); buffer.anchor_in_excerpt(excerpt_id.clone(), anchor) }; - self.set_scroll_top_anchor(anchor, cx); + self.set_scroll_top_anchor(Some(anchor), cx); + } else { + self.set_scroll_top_anchor(None, cx); } } } From a2dbebd9ba8742266469366857bdbfa0af9ba394 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Mar 2022 15:16:56 +0100 Subject: [PATCH 20/56] Hide cursor both locally and remotely when following --- crates/editor/src/editor.rs | 8 ++++++-- crates/editor/src/items.rs | 18 ++++++++++++++++++ crates/workspace/src/workspace.rs | 7 +++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b3d74d03a7fb66821208fffdbf4d7e140590489e..0db67e73e79394f950924b81f247231e91883a45 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -464,6 +464,7 @@ pub struct Editor { pending_rename: Option, searchable: bool, cursor_shape: CursorShape, + following: bool, } pub struct EditorSnapshot { @@ -937,6 +938,7 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), + following: false, }; this.end_selection(cx); this @@ -5036,7 +5038,7 @@ impl Editor { self.selections = selections; self.pending_selection = pending_selection; - if self.focused { + if self.focused && !self.following { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections(&self.selections, cx) }); @@ -5671,7 +5673,9 @@ impl View for Editor { self.blink_cursors(self.blink_epoch, cx); self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); - buffer.set_active_selections(&self.selections, cx) + if !self.following { + buffer.set_active_selections(&self.selections, cx); + } }); } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1ce59dd93fe8df9ad977883655b08538aef31598..7f28af0c40c7715d9c5599c780a8576b83bbb221 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -50,6 +50,24 @@ impl FollowableItem for Editor { })) } + fn set_following(&mut self, following: bool, cx: &mut ViewContext) { + self.following = following; + if self.following { + self.show_local_selections = false; + self.buffer.update(cx, |buffer, cx| { + buffer.remove_active_selections(cx); + }); + } else { + self.show_local_selections = true; + if self.focused { + self.buffer.update(cx, |buffer, cx| { + buffer.set_active_selections(&self.selections, cx); + }); + } + } + cx.notify(); + } + fn to_state_message(&self, cx: &AppContext) -> Option { let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); Some(proto::view::Variant::Editor(proto::view::Editor { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c1da70193186874b10d7d7c5f2718248bbc741a7..748b0ced6140b346281ff1ee507a19a0451b42b6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -250,6 +250,7 @@ pub trait FollowableItem: Item { state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; + fn set_following(&mut self, following: bool, cx: &mut ViewContext); fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, @@ -264,6 +265,7 @@ pub trait FollowableItem: Item { } pub trait FollowableItemHandle: ItemHandle { + fn set_following(&self, following: bool, cx: &mut MutableAppContext); fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, @@ -278,6 +280,10 @@ pub trait FollowableItemHandle: ItemHandle { } impl FollowableItemHandle for ViewHandle { + fn set_following(&self, following: bool, cx: &mut MutableAppContext) { + self.update(cx, |this, cx| this.set_following(following, cx)) + } + fn to_state_message(&self, cx: &AppContext) -> Option { self.read(cx).to_state_message(cx) } @@ -1267,6 +1273,7 @@ impl Workspace { let state = this.follower_states_by_leader.entry(leader_id).or_default(); state.panes.insert(pane); for (id, item) in leader_view_ids.into_iter().zip(items) { + item.set_following(true, cx); match state.items_by_leader_view_id.entry(id) { hash_map::Entry::Occupied(e) => { let e = e.into_mut(); From a154e4500bd7f276e9371c0bae474b135bb10977 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Mar 2022 16:46:10 +0100 Subject: [PATCH 21/56] Implement `Workspace::unfollow` This also changes the structure of the follow state back to be per-pane. This is because we can't share the same view state across different panes for a couple of reasons: - Rendering the same view in N different panes is almost always not something that we want due to global state such as focus. - If we allowed it and a user followed the same person in two different panes, there would be no way of unfollowing in one pane without also unfollowing in the other. --- crates/gpui/src/app.rs | 7 + crates/server/src/rpc.rs | 34 +++- crates/workspace/src/workspace.rs | 318 ++++++++++++++++++------------ 3 files changed, 235 insertions(+), 124 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 80fc36cba3d2d758fb3898218985e4dc3efd6c65..41d3bf2cdd18e1fcfe422879ba86bbfee1b1ece5 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3542,6 +3542,13 @@ impl PartialEq> for WeakViewHandle { impl Eq for ViewHandle {} +impl Hash for ViewHandle { + fn hash(&self, state: &mut H) { + self.window_id.hash(state); + self.view_id.hash(state); + } +} + impl Debug for ViewHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(&format!("ViewHandle<{}>", type_name::())) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 5585b04ad21c07219e8c34cdd63eb8b2c63100a8..044f42368207870bdeaf89d56e87eda49969be35 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4251,12 +4251,16 @@ mod tests { workspace.open_path((worktree_id, "1.txt"), cx) }) .await + .unwrap() + .downcast::() .unwrap(); - let _editor_a2 = workspace_a + let editor_a2 = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), cx) }) .await + .unwrap() + .downcast::() .unwrap(); // Client B opens an editor. @@ -4269,6 +4273,8 @@ mod tests { workspace.open_path((worktree_id, "1.txt"), cx) }) .await + .unwrap() + .downcast::() .unwrap(); // Client B starts following client A. @@ -4286,16 +4292,40 @@ mod tests { .project_path(cx)), Some((worktree_id, "2.txt").into()) ); + let editor_b2 = workspace_b + .read_with(cx_b, |workspace, cx| workspace.active_item(cx)) + .unwrap() + .downcast::() + .unwrap(); // When client A activates a different editor, client B does so as well. workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(editor_a1.as_ref(), cx) + workspace.activate_item(&editor_a1, cx) }); workspace_b .condition(cx_b, |workspace, cx| { workspace.active_item(cx).unwrap().id() == editor_b1.id() }) .await; + + // After unfollowing, client B stops receiving updates from client A. + workspace_b.update(cx_b, |workspace, cx| { + workspace.unfollow(&workspace.active_pane().clone(), cx) + }); + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a2, cx); + editor_a2.update(cx, |editor, cx| editor.set_text("ONE", cx)); + }); + editor_b2 + .condition(cx_b, |editor, cx| editor.text(cx) == "ONE") + .await; + assert_eq!( + workspace_b.read_with(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + editor_b1.id() + ); } #[gpui::test(iterations = 100)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 748b0ced6140b346281ff1ee507a19a0451b42b6..5b3a21f7e893ad42a7bf64c9db75ce7835e79c65 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -70,6 +70,7 @@ action!(OpenNew, Arc); action!(OpenPaths, OpenParams); action!(ToggleShare); action!(FollowCollaborator, PeerId); +action!(Unfollow); action!(JoinProject, JoinProjectParams); action!(Save); action!(DebugElements); @@ -91,6 +92,12 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::toggle_share); cx.add_async_action(Workspace::follow); + cx.add_action( + |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { + let pane = workspace.active_pane().clone(); + workspace.unfollow(&pane, cx); + }, + ); cx.add_action( |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext| { workspace.save_active_item(cx).detach_and_log_err(cx); @@ -100,6 +107,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::toggle_sidebar_item_focus); cx.add_bindings(vec![ + Binding::new("cmd-alt-shift-U", Unfollow, None), Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), Binding::new( @@ -603,7 +611,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, - follower_states_by_leader: HashMap, + follower_states_by_leader: HashMap, FollowerState>>, _observe_current_user: Task<()>, } @@ -616,7 +624,6 @@ struct LeaderState { struct FollowerState { active_view_id: Option, items_by_leader_view_id: HashMap, - panes: HashSet>, } #[derive(Debug)] @@ -1166,6 +1173,7 @@ impl Workspace { if self.center.remove(&pane).unwrap() { self.panes.retain(|p| p != &pane); self.activate_pane(self.panes.last().unwrap().clone(), cx); + self.unfollow(&pane, cx); cx.notify(); } } @@ -1209,6 +1217,14 @@ impl Workspace { cx: &mut ViewContext, ) -> Option>> { let leader_id = *leader_id; + let pane = self.active_pane().clone(); + + self.unfollow(&pane, cx); + self.follower_states_by_leader + .entry(leader_id) + .or_default() + .insert(pane.clone(), Default::default()); + let project_id = self.project.read(cx).remote_id()?; let request = self.client.request(proto::Follow { project_id, @@ -1217,99 +1233,50 @@ impl Workspace { Some(cx.spawn_weak(|this, mut cx| async move { let response = request.await?; if let Some(this) = this.upgrade(&cx) { - Self::add_views_from_leader(this.clone(), leader_id, response.views, &mut cx) - .await?; - this.update(&mut cx, |this, cx| { - this.follower_state(leader_id)?.active_view_id = response.active_view_id; - this.leader_updated(leader_id, cx); + this.update(&mut cx, |this, _| { + let state = this + .follower_states_by_leader + .get_mut(&leader_id) + .and_then(|states_by_pane| states_by_pane.get_mut(&pane)) + .ok_or_else(|| anyhow!("following interrupted"))?; + state.active_view_id = response.active_view_id; Ok::<_, anyhow::Error>(()) })?; + Self::add_views_from_leader( + this, + leader_id, + vec![pane.clone()], + response.views, + &mut cx, + ) + .await?; } Ok(()) })) } - async fn add_views_from_leader( - this: ViewHandle, - leader_id: PeerId, - views: Vec, - cx: &mut AsyncAppContext, - ) -> Result<()> { - let (project, pane) = this.read_with(cx, |this, _| { - (this.project.clone(), this.active_pane().clone()) - }); - - let item_builders = cx.update(|cx| { - cx.default_global::() - .values() - .map(|b| b.0) - .collect::>() - .clone() - }); - - let mut item_tasks = Vec::new(); - let mut leader_view_ids = Vec::new(); - for view in views { - let mut variant = view.variant; - if variant.is_none() { - Err(anyhow!("missing variant"))?; - } - for build_item in &item_builders { - let task = - cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx)); - if let Some(task) = task { - item_tasks.push(task); - leader_view_ids.push(view.id); - break; - } else { - assert!(variant.is_some()); + pub fn unfollow(&mut self, pane: &ViewHandle, cx: &mut ViewContext) -> Option<()> { + for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { + if let Some(state) = states_by_pane.remove(&pane) { + for (_, item) in state.items_by_leader_view_id { + if let FollowerItem::Loaded(item) = item { + item.set_following(false, cx); + } } - } - } - let pane = pane.downgrade(); - let items = futures::future::try_join_all(item_tasks).await?; - this.update(cx, |this, cx| { - let state = this.follower_states_by_leader.entry(leader_id).or_default(); - state.panes.insert(pane); - for (id, item) in leader_view_ids.into_iter().zip(items) { - item.set_following(true, cx); - match state.items_by_leader_view_id.entry(id) { - hash_map::Entry::Occupied(e) => { - let e = e.into_mut(); - if let FollowerItem::Loading(updates) = e { - for update in updates.drain(..) { - item.apply_update_message(update, cx) - .context("failed to apply view update") - .log_err(); - } - } - *e = FollowerItem::Loaded(item); - } - hash_map::Entry::Vacant(e) => { - e.insert(FollowerItem::Loaded(item)); + if states_by_pane.is_empty() { + if let Some(project_id) = self.project.read(cx).remote_id() { + self.client + .send(proto::Unfollow { + project_id, + leader_id: leader_id.0, + }) + .log_err(); } } - } - }); - Ok(()) - } - - fn update_followers( - &self, - update: proto::update_followers::Variant, - cx: &AppContext, - ) -> Option<()> { - let project_id = self.project.read(cx).remote_id()?; - if !self.leader_state.followers.is_empty() { - self.client - .send(proto::UpdateFollowers { - project_id, - follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(), - variant: Some(update), - }) - .log_err(); + cx.notify(); + } } None } @@ -1595,8 +1562,9 @@ impl Workspace { { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { this.update(&mut cx, |this, cx| { - this.follower_state(leader_id)?.active_view_id = update_active_view.id; - this.leader_updated(leader_id, cx); + this.update_leader_state(leader_id, cx, |state, _| { + state.active_view_id = update_active_view.id; + }); Ok::<_, anyhow::Error>(()) }) } @@ -1605,26 +1573,33 @@ impl Workspace { let variant = update_view .variant .ok_or_else(|| anyhow!("missing update view variant"))?; - match this - .follower_state(leader_id)? - .items_by_leader_view_id - .entry(update_view.id) - .or_insert(FollowerItem::Loading(Vec::new())) - { - FollowerItem::Loaded(item) => { - item.apply_update_message(variant, cx).log_err(); + this.update_leader_state(leader_id, cx, |state, cx| { + let variant = variant.clone(); + match state + .items_by_leader_view_id + .entry(update_view.id) + .or_insert(FollowerItem::Loading(Vec::new())) + { + FollowerItem::Loaded(item) => { + item.apply_update_message(variant, cx).log_err(); + } + FollowerItem::Loading(updates) => updates.push(variant), } - FollowerItem::Loading(updates) => updates.push(variant), - } - this.leader_updated(leader_id, cx); + }); Ok(()) }) } proto::update_followers::Variant::CreateView(view) => { - Self::add_views_from_leader(this.clone(), leader_id, vec![view], &mut cx).await?; - this.update(&mut cx, |this, cx| { - this.leader_updated(leader_id, cx); + let panes = this.read_with(&cx, |this, _| { + this.follower_states_by_leader + .get(&leader_id) + .into_iter() + .flat_map(|states_by_pane| states_by_pane.keys()) + .cloned() + .collect() }); + Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx) + .await?; Ok(()) } } @@ -1633,37 +1608,136 @@ impl Workspace { Ok(()) } - fn follower_state(&mut self, leader_id: PeerId) -> Result<&mut FollowerState> { - self.follower_states_by_leader - .get_mut(&leader_id) - .ok_or_else(|| anyhow!("received follow update for an unfollowed peer")) - } + async fn add_views_from_leader( + this: ViewHandle, + leader_id: PeerId, + panes: Vec>, + views: Vec, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let project = this.read_with(cx, |this, _| this.project.clone()); - fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { - let state = self.follower_states_by_leader.get_mut(&leader_id)?; - let active_item = state.items_by_leader_view_id.get(&state.active_view_id?)?; - if let FollowerItem::Loaded(item) = active_item { - let mut panes = Vec::new(); - state.panes.retain(|pane| { - if let Some(pane) = pane.upgrade(cx) { - panes.push(pane); - true - } else { - false + let item_builders = cx.update(|cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + .clone() + }); + + let mut item_tasks_by_pane = HashMap::default(); + for pane in panes { + let mut item_tasks = Vec::new(); + let mut leader_view_ids = Vec::new(); + for view in &views { + let mut variant = view.variant.clone(); + if variant.is_none() { + Err(anyhow!("missing variant"))?; + } + for build_item in &item_builders { + let task = + cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx)); + if let Some(task) = task { + item_tasks.push(task); + leader_view_ids.push(view.id); + break; + } else { + assert!(variant.is_some()); + } } + } + + item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids)); + } + + for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane { + let items = futures::future::try_join_all(item_tasks).await?; + this.update(cx, |this, cx| { + let state = this + .follower_states_by_leader + .get_mut(&leader_id)? + .get_mut(&pane)?; + + for (id, item) in leader_view_ids.into_iter().zip(items) { + item.set_following(true, cx); + match state.items_by_leader_view_id.entry(id) { + hash_map::Entry::Occupied(e) => { + let e = e.into_mut(); + if let FollowerItem::Loading(updates) = e { + for update in updates.drain(..) { + item.apply_update_message(update, cx) + .context("failed to apply view update") + .log_err(); + } + } + *e = FollowerItem::Loaded(item); + } + hash_map::Entry::Vacant(e) => { + e.insert(FollowerItem::Loaded(item)); + } + } + } + + Some(()) }); + } + this.update(cx, |this, cx| this.leader_updated(leader_id, cx)); - if panes.is_empty() { - self.follower_states_by_leader.remove(&leader_id); - } else { - let item = item.boxed_clone(); - for pane in panes { - Pane::add_item(self, pane, item.clone(), cx); + Ok(()) + } + + fn update_followers( + &self, + update: proto::update_followers::Variant, + cx: &AppContext, + ) -> Option<()> { + let project_id = self.project.read(cx).remote_id()?; + if !self.leader_state.followers.is_empty() { + self.client + .send(proto::UpdateFollowers { + project_id, + follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(), + variant: Some(update), + }) + .log_err(); + } + None + } + + fn update_leader_state( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext), + ) { + for (_, state) in self + .follower_states_by_leader + .get_mut(&leader_id) + .into_iter() + .flatten() + { + update_fn(state, cx); + } + self.leader_updated(leader_id, cx); + } + + fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { + let mut items_to_add = Vec::new(); + for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { + if let Some(active_item) = state + .active_view_id + .and_then(|id| state.items_by_leader_view_id.get(&id)) + { + if let FollowerItem::Loaded(item) = active_item { + items_to_add.push((pane.clone(), item.boxed_clone())); } - cx.notify(); } } + for (pane, item) in items_to_add { + Pane::add_item(self, pane.clone(), item.boxed_clone(), cx); + cx.notify(); + } None } } From 9575796f9ec164aeed22e7a4beb8ab88837746b2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Mar 2022 17:10:23 +0100 Subject: [PATCH 22/56] Allow unfollowing of leaders by clicking on their avatar --- crates/server/src/rpc.rs | 2 +- crates/workspace/src/workspace.rs | 35 ++++++++++++++++++------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 044f42368207870bdeaf89d56e87eda49969be35..fe56cea2e299c763fc29b9605ebd5c1015728a3a 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4281,7 +4281,7 @@ mod tests { workspace_b .update(cx_b, |workspace, cx| { let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.follow(&leader_id.into(), cx).unwrap() + workspace.toggle_follow(&leader_id.into(), cx).unwrap() }) .await .unwrap(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5b3a21f7e893ad42a7bf64c9db75ce7835e79c65..760ffa8c1a8d84a954e3e9ac597e7af68fc12b15 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -69,7 +69,7 @@ action!(Open, Arc); action!(OpenNew, Arc); action!(OpenPaths, OpenParams); action!(ToggleShare); -action!(FollowCollaborator, PeerId); +action!(ToggleFollow, PeerId); action!(Unfollow); action!(JoinProject, JoinProjectParams); action!(Save); @@ -91,7 +91,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { }); cx.add_action(Workspace::toggle_share); - cx.add_async_action(Workspace::follow); + cx.add_async_action(Workspace::toggle_follow); cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); @@ -1211,15 +1211,21 @@ impl Workspace { } } - pub fn follow( + pub fn toggle_follow( &mut self, - FollowCollaborator(leader_id): &FollowCollaborator, + ToggleFollow(leader_id): &ToggleFollow, cx: &mut ViewContext, ) -> Option>> { let leader_id = *leader_id; let pane = self.active_pane().clone(); - self.unfollow(&pane, cx); + if let Some(prev_leader_id) = self.unfollow(&pane, cx) { + if leader_id == prev_leader_id { + cx.notify(); + return None; + } + } + self.follower_states_by_leader .entry(leader_id) .or_default() @@ -1255,7 +1261,11 @@ impl Workspace { })) } - pub fn unfollow(&mut self, pane: &ViewHandle, cx: &mut ViewContext) -> Option<()> { + pub fn unfollow( + &mut self, + pane: &ViewHandle, + cx: &mut ViewContext, + ) -> Option { for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { if let Some(state) = states_by_pane.remove(&pane) { for (_, item) in state.items_by_leader_view_id { @@ -1276,6 +1286,7 @@ impl Workspace { } cx.notify(); + return Some(*leader_id); } } None @@ -1437,14 +1448,10 @@ impl Workspace { .boxed(); if let Some(peer_id) = peer_id { - MouseEventHandler::new::( - replica_id.into(), - cx, - move |_, _| content, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |cx| cx.dispatch_action(FollowCollaborator(peer_id))) - .boxed() + MouseEventHandler::new::(replica_id.into(), cx, move |_, _| content) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |cx| cx.dispatch_action(ToggleFollow(peer_id))) + .boxed() } else { content } From 13a2dacc6078a4dd58d3f3eb5f29db2950800dee Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Mar 2022 18:16:06 +0100 Subject: [PATCH 23/56] :lipstick: --- crates/workspace/src/workspace.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 760ffa8c1a8d84a954e3e9ac597e7af68fc12b15..3c5d1dfbf6412c9ad8703622dcf9d1f89077b5b3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1248,14 +1248,8 @@ impl Workspace { state.active_view_id = response.active_view_id; Ok::<_, anyhow::Error>(()) })?; - Self::add_views_from_leader( - this, - leader_id, - vec![pane.clone()], - response.views, - &mut cx, - ) - .await?; + Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx) + .await?; } Ok(()) })) From 3e0bc979c3bc864b046108bb69155af6815b5787 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 11:47:00 -0700 Subject: [PATCH 24/56] Avoid infinite loop when collaborators follow each other Co-Authored-By: Antonio Scandurra --- crates/editor/src/items.rs | 2 +- crates/rpc/proto/zed.proto | 37 +++++---- crates/server/src/rpc.rs | 124 ++++++++++++++++++++++++++++-- crates/workspace/src/workspace.rs | 73 +++++++++++------- 4 files changed, 186 insertions(+), 50 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7f28af0c40c7715d9c5599c780a8576b83bbb221..fee7cc445e91984c0facac81c81c14289890b9dc 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -6,7 +6,7 @@ use gpui::{ }; use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectEntryId, ProjectPath}; -use rpc::proto::{self, update_followers::update_view}; +use rpc::proto::{self, update_view}; use std::{fmt::Write, path::PathBuf}; use text::{Point, Selection}; use util::ResultExt; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f0743c5b4fa02ba4952051eb7a76cd8c004dd058..dbf1218ccd0f90e5c537160303561e7717287993 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -556,21 +556,6 @@ message UpdateFollowers { View create_view = 4; UpdateView update_view = 5; } - - message UpdateActiveView { - optional uint64 id = 1; - } - - message UpdateView { - uint64 id = 1; - oneof variant { - Editor editor = 2; - } - - message Editor { - Anchor scroll_top = 1; - } - } } message Unfollow { @@ -580,10 +565,30 @@ message Unfollow { // Entities +message UpdateActiveView { + optional uint64 id = 1; + optional uint32 leader_id = 2; +} + +message UpdateView { + uint64 id = 1; + optional uint32 leader_id = 2; + + oneof variant { + Editor editor = 3; + } + + message Editor { + Anchor scroll_top = 1; + } +} + message View { uint64 id = 1; + optional uint32 leader_id = 2; + oneof variant { - Editor editor = 2; + Editor editor = 3; } message Editor { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index fe56cea2e299c763fc29b9605ebd5c1015728a3a..771d82e0173b2f07d27e481380b5a3bdfe5a2e70 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -678,17 +678,21 @@ impl Server { request: TypedEnvelope, ) -> tide::Result { let leader_id = ConnectionId(request.payload.leader_id); + let follower_id = request.sender_id; if !self .state() - .project_connection_ids(request.payload.project_id, request.sender_id)? + .project_connection_ids(request.payload.project_id, follower_id)? .contains(&leader_id) { Err(anyhow!("no such peer"))?; } - let response = self + let mut response = self .peer .forward_request(request.sender_id, leader_id, request.payload) .await?; + response + .views + .retain(|view| view.leader_id != Some(follower_id.0)); Ok(response) } @@ -716,9 +720,18 @@ impl Server { let connection_ids = self .state() .project_connection_ids(request.payload.project_id, request.sender_id)?; + let leader_id = request + .payload + .variant + .as_ref() + .and_then(|variant| match variant { + proto::update_followers::Variant::CreateView(payload) => payload.leader_id, + proto::update_followers::Variant::UpdateView(payload) => payload.leader_id, + proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id, + }); for follower_id in &request.payload.follower_ids { let follower_id = ConnectionId(*follower_id); - if connection_ids.contains(&follower_id) { + if connection_ids.contains(&follower_id) && Some(follower_id.0) != leader_id { self.peer .forward_send(request.sender_id, follower_id, request.payload.clone())?; } @@ -4265,9 +4278,6 @@ mod tests { // Client B opens an editor. let workspace_b = client_b.build_workspace(&project_b, cx_b); - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); - }); let editor_b1 = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), cx) @@ -4328,6 +4338,108 @@ mod tests { ); } + #[gpui::test(iterations = 10)] + async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + cx_a.foreground().forbid_parking(); + let fs = FakeFs::new(cx_a.background()); + + // 2 clients connect to a server. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let mut client_a = server.create_client(cx_a, "user_a").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + + // Client B joins the project. + let project_b = client_b + .build_remote_project( + project_a + .read_with(cx_a, |project, _| project.remote_id()) + .unwrap(), + cx_b, + ) + .await; + + // Client A opens some editors. + let workspace_a = client_a.build_workspace(&project_a, cx_a); + let _editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens an editor. + let workspace_b = client_b.build_workspace(&project_b, cx_b); + let _editor_b1 = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Clients A and B follow each other in split panes + workspace_a + .update(cx_a, |workspace, cx| { + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); + workspace + .toggle_follow(&workspace::ToggleFollow(leader_id), cx) + .unwrap() + }) + .await + .unwrap(); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); + workspace + .toggle_follow(&workspace::ToggleFollow(leader_id), cx) + .unwrap() + }) + .await + .unwrap(); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.activate_next_pane(cx); + workspace.open_path((worktree_id, "3.txt"), cx) + }) + .await + .unwrap(); + + // Ensure peers following each other doesn't cause an infinite loop. + cx_a.foreground().run_until_parked(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .project_path(cx)), + Some((worktree_id, "3.txt").into()) + ); + } + #[gpui::test(iterations = 100)] async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) { cx.foreground().forbid_parking(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3c5d1dfbf6412c9ad8703622dcf9d1f89077b5b3..78a59e9d91daaa8d0c4e216e65fe69a69eb0f0fb 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -264,10 +264,10 @@ pub trait FollowableItem: Item { &self, event: &Self::Event, cx: &AppContext, - ) -> Option; + ) -> Option; fn apply_update_message( &mut self, - message: proto::update_followers::update_view::Variant, + message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Result<()>; } @@ -279,10 +279,10 @@ pub trait FollowableItemHandle: ItemHandle { &self, event: &dyn Any, cx: &AppContext, - ) -> Option; + ) -> Option; fn apply_update_message( &self, - message: proto::update_followers::update_view::Variant, + message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()>; } @@ -300,13 +300,13 @@ impl FollowableItemHandle for ViewHandle { &self, event: &dyn Any, cx: &AppContext, - ) -> Option { + ) -> Option { self.read(cx).to_update_message(event.downcast_ref()?, cx) } fn apply_update_message( &self, - message: proto::update_followers::update_view::Variant, + message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()> { self.update(cx, |this, cx| this.apply_update_message(message, cx)) @@ -403,6 +403,7 @@ impl ItemHandle for ViewHandle { proto::update_followers::Variant::CreateView(proto::View { id: followed_item.id() as u64, variant: Some(message), + leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), }), cx, ); @@ -441,12 +442,11 @@ impl ItemHandle for ViewHandle { .and_then(|i| i.to_update_message(event, cx)) { workspace.update_followers( - proto::update_followers::Variant::UpdateView( - proto::update_followers::UpdateView { - id: item.id() as u64, - variant: Some(message), - }, - ), + proto::update_followers::Variant::UpdateView(proto::UpdateView { + id: item.id() as u64, + variant: Some(message), + leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), + }), cx, ); } @@ -628,7 +628,7 @@ struct FollowerState { #[derive(Debug)] enum FollowerItem { - Loading(Vec), + Loading(Vec), Loaded(Box), } @@ -1110,7 +1110,7 @@ impl Workspace { fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.active_pane != pane { - self.active_pane = pane; + self.active_pane = pane.clone(); self.status_bar.update(cx, |status_bar, cx| { status_bar.set_active_pane(&self.active_pane, cx); }); @@ -1119,11 +1119,10 @@ impl Workspace { } self.update_followers( - proto::update_followers::Variant::UpdateActiveView( - proto::update_followers::UpdateActiveView { - id: self.active_item(cx).map(|item| item.id() as u64), - }, - ), + proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { + id: self.active_item(cx).map(|item| item.id() as u64), + leader_id: self.leader_for_pane(&pane).map(|id| id.0), + }), cx, ); } @@ -1520,14 +1519,22 @@ impl Workspace { Ok(proto::FollowResponse { active_view_id, views: this - .items(cx) - .filter_map(|item| { - let id = item.id() as u64; - let item = item.to_followable_item_handle(cx)?; - let variant = item.to_state_message(cx)?; - Some(proto::View { - id, - variant: Some(variant), + .panes() + .iter() + .flat_map(|pane| { + let leader_id = this.leader_for_pane(pane).map(|id| id.0); + pane.read(cx).items().filter_map({ + let cx = &cx; + move |item| { + let id = item.id() as u64; + let item = item.to_followable_item_handle(cx)?; + let variant = item.to_state_message(cx)?; + Some(proto::View { + id, + leader_id, + variant: Some(variant), + }) + } }) }) .collect(), @@ -1705,6 +1712,18 @@ impl Workspace { None } + fn leader_for_pane(&self, pane: &ViewHandle) -> Option { + self.follower_states_by_leader + .iter() + .find_map(|(leader_id, state)| { + if state.contains_key(pane) { + Some(*leader_id) + } else { + None + } + }) + } + fn update_leader_state( &mut self, leader_id: PeerId, From 06cd9ac664b53c7e836d0b7262f14ac27d947b44 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 14:04:55 -0700 Subject: [PATCH 25/56] Match the leader's last selection when unfollowing Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 8 +++---- crates/editor/src/items.rs | 39 ++++++++++++++++++++++++------- crates/language/src/buffer.rs | 6 ----- crates/server/src/rpc.rs | 35 +++++++++++++++++++++++---- crates/workspace/src/workspace.rs | 22 ++++++++++++----- 5 files changed, 81 insertions(+), 29 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0db67e73e79394f950924b81f247231e91883a45..80ed9bc51219d7657d640a35ddf989693e0f9186 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -464,7 +464,7 @@ pub struct Editor { pending_rename: Option, searchable: bool, cursor_shape: CursorShape, - following: bool, + leader_replica_id: Option, } pub struct EditorSnapshot { @@ -938,7 +938,7 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), - following: false, + leader_replica_id: None, }; this.end_selection(cx); this @@ -5038,7 +5038,7 @@ impl Editor { self.selections = selections; self.pending_selection = pending_selection; - if self.focused && !self.following { + if self.focused && self.leader_replica_id.is_none() { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections(&self.selections, cx) }); @@ -5673,7 +5673,7 @@ impl View for Editor { self.blink_cursors(self.blink_epoch, cx); self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); - if !self.following { + if self.leader_replica_id.is_none() { buffer.set_active_selections(&self.selections, cx); } }); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index fee7cc445e91984c0facac81c81c14289890b9dc..3fdaad8b8f462369a046a15034c7b3240a830c4a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,4 +1,4 @@ -use crate::{Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _}; +use crate::{Anchor, Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _}; use anyhow::{anyhow, Result}; use gpui::{ elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, @@ -50,20 +50,43 @@ impl FollowableItem for Editor { })) } - fn set_following(&mut self, following: bool, cx: &mut ViewContext) { - self.following = following; - if self.following { + fn set_leader_replica_id( + &mut self, + leader_replica_id: Option, + cx: &mut ViewContext, + ) { + let prev_leader_replica_id = self.leader_replica_id; + self.leader_replica_id = leader_replica_id; + if self.leader_replica_id.is_some() { self.show_local_selections = false; self.buffer.update(cx, |buffer, cx| { buffer.remove_active_selections(cx); }); } else { self.show_local_selections = true; - if self.focused { - self.buffer.update(cx, |buffer, cx| { - buffer.set_active_selections(&self.selections, cx); - }); + if let Some(leader_replica_id) = prev_leader_replica_id { + let selections = self + .buffer + .read(cx) + .snapshot(cx) + .remote_selections_in_range(&(Anchor::min()..Anchor::max())) + .filter_map(|(replica_id, selections)| { + if replica_id == leader_replica_id { + Some(selections) + } else { + None + } + }) + .collect::>(); + if !selections.is_empty() { + self.set_selections(selections.into(), None, cx); + } } + self.buffer.update(cx, |buffer, cx| { + if self.focused { + buffer.set_active_selections(&self.selections, cx); + } + }); } cx.notify(); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a4fee82805845fc6aed39de37a441d0a66e55c02..fa4208ee5b42d09ae9fb96461ca604635e6e4e7e 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1801,12 +1801,6 @@ impl BufferSnapshot { .min_by_key(|(open_range, close_range)| close_range.end - open_range.start) } - /* - impl BufferSnapshot - pub fn remote_selections_in_range(&self, Range) -> impl Iterator>)> - pub fn remote_selections_in_range(&self, Range) -> impl Iterator( &'a self, range: Range, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 771d82e0173b2f07d27e481380b5a3bdfe5a2e70..c6627ce7b88b6cc949155d87ddbe6eeaadb597c9 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1083,8 +1083,8 @@ mod tests { }; use collections::BTreeMap; use editor::{ - self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, - ToOffset, ToggleCodeActions, Undo, + self, Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, + Rename, ToOffset, ToggleCodeActions, Undo, }; use gpui::{executor, ModelHandle, TestAppContext, ViewHandle}; use language::{ @@ -4290,7 +4290,13 @@ mod tests { // Client B starts following client A. workspace_b .update(cx_b, |workspace, cx| { - let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); + let leader_id = project_b + .read(cx) + .collaborators() + .values() + .next() + .unwrap() + .peer_id; workspace.toggle_follow(&leader_id.into(), cx).unwrap() }) .await @@ -4318,16 +4324,35 @@ mod tests { }) .await; + editor_a1.update(cx_a, |editor, cx| { + editor.select_ranges([2..2], None, cx); + }); + editor_b1 + .condition(cx_b, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let selection = snapshot + .remote_selections_in_range(&(Anchor::min()..Anchor::max())) + .next(); + selection.map_or(false, |selection| { + selection.1.start.to_offset(&snapshot) == 2 + }) + }) + .await; + // After unfollowing, client B stops receiving updates from client A. workspace_b.update(cx_b, |workspace, cx| { workspace.unfollow(&workspace.active_pane().clone(), cx) }); + editor_b1.update(cx_b, |editor, cx| { + assert_eq!(editor.selected_ranges::(cx), &[2..2]); + }); + workspace_a.update(cx_a, |workspace, cx| { workspace.activate_item(&editor_a2, cx); - editor_a2.update(cx, |editor, cx| editor.set_text("ONE", cx)); + editor_a2.update(cx, |editor, cx| editor.set_text("TWO", cx)); }); editor_b2 - .condition(cx_b, |editor, cx| editor.text(cx) == "ONE") + .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") .await; assert_eq!( workspace_b.read_with(cx_b, |workspace, cx| workspace diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 78a59e9d91daaa8d0c4e216e65fe69a69eb0f0fb..1314b53e636dd6f15995f29d6bee99bc7000cf3c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -258,7 +258,7 @@ pub trait FollowableItem: Item { state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; - fn set_following(&mut self, following: bool, cx: &mut ViewContext); + fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, @@ -273,7 +273,7 @@ pub trait FollowableItem: Item { } pub trait FollowableItemHandle: ItemHandle { - fn set_following(&self, following: bool, cx: &mut MutableAppContext); + fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext); fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, @@ -288,8 +288,10 @@ pub trait FollowableItemHandle: ItemHandle { } impl FollowableItemHandle for ViewHandle { - fn set_following(&self, following: bool, cx: &mut MutableAppContext) { - self.update(cx, |this, cx| this.set_following(following, cx)) + fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext) { + self.update(cx, |this, cx| { + this.set_leader_replica_id(leader_replica_id, cx) + }) } fn to_state_message(&self, cx: &AppContext) -> Option { @@ -1263,7 +1265,7 @@ impl Workspace { if let Some(state) = states_by_pane.remove(&pane) { for (_, item) in state.items_by_leader_view_id { if let FollowerItem::Loaded(item) = item { - item.set_following(false, cx); + item.set_leader_replica_id(None, cx); } } @@ -1624,6 +1626,14 @@ impl Workspace { cx: &mut AsyncAppContext, ) -> Result<()> { let project = this.read_with(cx, |this, _| this.project.clone()); + let replica_id = project + .read_with(cx, |project, _| { + project + .collaborators() + .get(&leader_id) + .map(|c| c.replica_id) + }) + .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?; let item_builders = cx.update(|cx| { cx.default_global::() @@ -1667,7 +1677,7 @@ impl Workspace { .get_mut(&pane)?; for (id, item) in leader_view_ids.into_iter().zip(items) { - item.set_following(true, cx); + item.set_leader_replica_id(Some(replica_id), cx); match state.items_by_leader_view_id.entry(id) { hash_map::Entry::Occupied(e) => { let e = e.into_mut(); From c8f36af82365c5976322d58110a004e87c9858ff Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 15:12:15 -0700 Subject: [PATCH 26/56] Show borders around avatars and panes to indicate following state --- crates/theme/src/theme.rs | 1 + crates/workspace/src/pane_group.rs | 61 ++++++++++++++++++++++++----- crates/workspace/src/workspace.rs | 38 +++++++++++++----- crates/zed/assets/themes/_base.toml | 1 + 4 files changed, 82 insertions(+), 19 deletions(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5e8e799b4c58fd9d073ff078ad19fbb6db0af53d..61d0bf3f67e3799b4a3c8364b9eea9815f210bb2 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -35,6 +35,7 @@ pub struct Workspace { pub tab: Tab, pub active_tab: Tab, pub pane_divider: Border, + pub leader_border_opacity: f32, pub left_sidebar: Sidebar, pub right_sidebar: Sidebar, pub status_bar: StatusBar, diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 2b56a023fc2a7dbc423b177503c7117e6c02b044..c3f4d2d3a6e8a5086a5c78d183a47fbbfe940551 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,9 +1,11 @@ +use crate::{FollowerStatesByLeader, Pane}; use anyhow::{anyhow, Result}; -use gpui::{elements::*, Axis, ViewHandle}; +use client::PeerId; +use collections::HashMap; +use gpui::{elements::*, Axis, Border, ViewHandle}; +use project::Collaborator; use theme::Theme; -use crate::Pane; - #[derive(Clone, Debug, Eq, PartialEq)] pub struct PaneGroup { root: Member, @@ -47,8 +49,13 @@ impl PaneGroup { } } - pub fn render<'a>(&self, theme: &Theme) -> ElementBox { - self.root.render(theme) + pub(crate) fn render<'a>( + &self, + theme: &Theme, + follower_states: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { + self.root.render(theme, follower_states, collaborators) } } @@ -80,10 +87,39 @@ impl Member { Member::Axis(PaneAxis { axis, members }) } - pub fn render(&self, theme: &Theme) -> ElementBox { + pub fn render( + &self, + theme: &Theme, + follower_states: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { match self { - Member::Pane(pane) => ChildView::new(pane).boxed(), - Member::Axis(axis) => axis.render(theme), + Member::Pane(pane) => { + let mut border = Border::default(); + let leader = follower_states + .iter() + .find_map(|(leader_id, follower_states)| { + if follower_states.contains_key(pane) { + Some(leader_id) + } else { + None + } + }) + .and_then(|leader_id| collaborators.get(leader_id)); + if let Some(leader) = leader { + let leader_color = theme + .editor + .replica_selection_style(leader.replica_id) + .cursor; + border = Border::all(1.0, leader_color); + border + .color + .fade_out(1. - theme.workspace.leader_border_opacity); + border.overlay = true; + } + ChildView::new(pane).contained().with_border(border).boxed() + } + Member::Axis(axis) => axis.render(theme, follower_states, collaborators), } } } @@ -172,11 +208,16 @@ impl PaneAxis { } } - fn render<'a>(&self, theme: &Theme) -> ElementBox { + fn render( + &self, + theme: &Theme, + follower_state: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { let last_member_ix = self.members.len() - 1; Flex::new(self.axis) .with_children(self.members.iter().enumerate().map(|(ix, member)| { - let mut member = member.render(theme); + let mut member = member.render(theme, follower_state, collaborators); if ix < last_member_ix { let mut border = theme.workspace.pane_divider; border.left = false; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1314b53e636dd6f15995f29d6bee99bc7000cf3c..387bfa9b6eff2b37403c3cf5df26d2476988711c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -20,9 +20,9 @@ use gpui::{ json::{self, to_string_pretty, ToJson}, keymap::Binding, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Entity, ImageData, - ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity, + ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -613,7 +613,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, - follower_states_by_leader: HashMap, FollowerState>>, + follower_states_by_leader: FollowerStatesByLeader, _observe_current_user: Task<()>, } @@ -622,6 +622,8 @@ struct LeaderState { followers: HashSet, } +type FollowerStatesByLeader = HashMap, FollowerState>>; + #[derive(Default)] struct FollowerState { active_view_id: Option, @@ -1262,6 +1264,7 @@ impl Workspace { cx: &mut ViewContext, ) -> Option { for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { + let leader_id = *leader_id; if let Some(state) = states_by_pane.remove(&pane) { for (_, item) in state.items_by_leader_view_id { if let FollowerItem::Loaded(item) = item { @@ -1270,6 +1273,7 @@ impl Workspace { } if states_by_pane.is_empty() { + self.follower_states_by_leader.remove(&leader_id); if let Some(project_id) = self.project.read(cx).remote_id() { self.client .send(proto::Unfollow { @@ -1281,7 +1285,7 @@ impl Workspace { } cx.notify(); - return Some(*leader_id); + return Some(leader_id); } } None @@ -1420,17 +1424,25 @@ impl Workspace { theme: &Theme, cx: &mut RenderContext, ) -> ElementBox { + let replica_color = theme.editor.replica_selection_style(replica_id).cursor; + let is_followed = peer_id.map_or(false, |peer_id| { + self.follower_states_by_leader.contains_key(&peer_id) + }); + let mut avatar_style = theme.workspace.titlebar.avatar; + if is_followed { + avatar_style.border = Border::all(1.0, replica_color); + } let content = Stack::new() .with_child( Image::new(avatar) - .with_style(theme.workspace.titlebar.avatar) + .with_style(avatar_style) .constrained() .with_width(theme.workspace.titlebar.avatar_width) .aligned() .boxed(), ) .with_child( - AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor) + AvatarRibbon::new(replica_color) .constrained() .with_width(theme.workspace.titlebar.avatar_ribbon.width) .with_height(theme.workspace.titlebar.avatar_ribbon.height) @@ -1800,8 +1812,16 @@ impl View for Workspace { content.add_child( Flex::column() .with_child( - Flexible::new(1., true, self.center.render(&theme)) - .boxed(), + Flexible::new( + 1., + true, + self.center.render( + &theme, + &self.follower_states_by_leader, + self.project.read(cx).collaborators(), + ), + ) + .boxed(), ) .with_child(ChildView::new(&self.status_bar).boxed()) .flexible(1., true) diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 76547967bfb8cc34cec0fae1307250e57c08b6f4..d0368d69338911a66669b9f9f88c0c107dc95ac8 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -4,6 +4,7 @@ base = { family = "Zed Sans", size = 14 } [workspace] background = "$surface.0" pane_divider = { width = 1, color = "$border.0" } +leader_border_opacity = 0.6 [workspace.titlebar] height = 32 From 17285512822c037e09d73a2eb23c0c1cfc046531 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 21:47:24 -0700 Subject: [PATCH 27/56] Always mirror the leader's selections when following --- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 11 +++- crates/editor/src/items.rs | 117 +++++++++++++++++++++++++---------- crates/rpc/proto/zed.proto | 6 +- crates/server/src/rpc.rs | 19 ++---- 5 files changed, 103 insertions(+), 52 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 80ed9bc51219d7657d640a35ddf989693e0f9186..8380eea12d9e3c59f821329304c03cc83205087d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1568,7 +1568,7 @@ impl Editor { #[cfg(any(test, feature = "test-support"))] pub fn selected_ranges>( &self, - cx: &mut MutableAppContext, + cx: &AppContext, ) -> Vec> { self.local_selections::(cx) .iter() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 49d800d619829b4e691095bcc5e588712691c5da..af02a353d4c2baa8038524d115c1fe887c11732a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -939,8 +939,12 @@ impl Element for EditorElement { *contains_non_empty_selection |= !is_empty; } } + + // Render the local selections in the leader's color when following. + let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx)); + selections.insert( - view.replica_id(cx), + local_replica_id, local_selections .into_iter() .map(|selection| crate::Selection { @@ -958,6 +962,11 @@ impl Element for EditorElement { .buffer_snapshot .remote_selections_in_range(&(start_anchor..end_anchor)) { + // The local selections match the leader's selections. + if Some(replica_id) == view.leader_replica_id { + continue; + } + selections .entry(replica_id) .or_insert(Vec::new()) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3fdaad8b8f462369a046a15034c7b3240a830c4a..7afc79f8b384a15c8a9111c24e0e11629730970e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,10 +1,10 @@ -use crate::{Anchor, Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _}; +use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _}; use anyhow::{anyhow, Result}; use gpui::{ elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; -use language::{Bias, Buffer, Diagnostic, File as _}; +use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal}; use project::{File, Project, ProjectEntryId, ProjectPath}; use rpc::proto::{self, update_view}; use std::{fmt::Write, path::PathBuf}; @@ -44,7 +44,23 @@ impl FollowableItem for Editor { }) .unwrap_or_else(|| { cx.add_view(pane.window_id(), |cx| { - Editor::for_buffer(buffer, Some(project), cx) + let mut editor = Editor::for_buffer(buffer, Some(project), cx); + let selections = { + let buffer = editor.buffer.read(cx); + let buffer = buffer.read(cx); + let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); + state + .selections + .into_iter() + .filter_map(|selection| { + deserialize_selection(&excerpt_id, buffer_id, selection) + }) + .collect::>() + }; + if !selections.is_empty() { + editor.set_selections(selections.into(), None, cx); + } + editor }) })) })) @@ -55,33 +71,12 @@ impl FollowableItem for Editor { leader_replica_id: Option, cx: &mut ViewContext, ) { - let prev_leader_replica_id = self.leader_replica_id; self.leader_replica_id = leader_replica_id; if self.leader_replica_id.is_some() { - self.show_local_selections = false; self.buffer.update(cx, |buffer, cx| { buffer.remove_active_selections(cx); }); } else { - self.show_local_selections = true; - if let Some(leader_replica_id) = prev_leader_replica_id { - let selections = self - .buffer - .read(cx) - .snapshot(cx) - .remote_selections_in_range(&(Anchor::min()..Anchor::max())) - .filter_map(|(replica_id, selections)| { - if replica_id == leader_replica_id { - Some(selections) - } else { - None - } - }) - .collect::>(); - if !selections.is_empty() { - self.set_selections(selections.into(), None, cx); - } - } self.buffer.update(cx, |buffer, cx| { if self.focused { buffer.set_active_selections(&self.selections, cx); @@ -99,6 +94,7 @@ impl FollowableItem for Editor { .scroll_top_anchor .as_ref() .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), + selections: self.selections.iter().map(serialize_selection).collect(), })) } @@ -108,12 +104,13 @@ impl FollowableItem for Editor { _: &AppContext, ) -> Option { match event { - Event::ScrollPositionChanged => { + Event::ScrollPositionChanged | Event::SelectionsChanged => { Some(update_view::Variant::Editor(update_view::Editor { scroll_top: self .scroll_top_anchor .as_ref() .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), + selections: self.selections.iter().map(serialize_selection).collect(), })) } _ => None, @@ -127,25 +124,77 @@ impl FollowableItem for Editor { ) -> Result<()> { match message { update_view::Variant::Editor(message) => { + let buffer = self.buffer.read(cx); + let buffer = buffer.read(cx); + let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); + let excerpt_id = excerpt_id.clone(); + drop(buffer); + if let Some(anchor) = message.scroll_top { - let anchor = language::proto::deserialize_anchor(anchor) - .ok_or_else(|| anyhow!("invalid scroll top"))?; - let anchor = { - let buffer = self.buffer.read(cx); - let buffer = buffer.read(cx); - let (excerpt_id, _, _) = buffer.as_singleton().unwrap(); - buffer.anchor_in_excerpt(excerpt_id.clone(), anchor) - }; - self.set_scroll_top_anchor(Some(anchor), cx); + self.set_scroll_top_anchor( + Some(Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: language::proto::deserialize_anchor(anchor) + .ok_or_else(|| anyhow!("invalid scroll top"))?, + }), + cx, + ); } else { self.set_scroll_top_anchor(None, cx); } + + let selections = message + .selections + .into_iter() + .filter_map(|selection| { + deserialize_selection(&excerpt_id, buffer_id, selection) + }) + .collect::>(); + if !selections.is_empty() { + self.set_selections(selections.into(), None, cx); + } } } Ok(()) } } +fn serialize_selection(selection: &Selection) -> proto::Selection { + proto::Selection { + id: selection.id as u64, + start: Some(language::proto::serialize_anchor( + &selection.start.text_anchor, + )), + end: Some(language::proto::serialize_anchor( + &selection.end.text_anchor, + )), + reversed: selection.reversed, + } +} + +fn deserialize_selection( + excerpt_id: &ExcerptId, + buffer_id: usize, + selection: proto::Selection, +) -> Option> { + Some(Selection { + id: selection.id as usize, + start: Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: language::proto::deserialize_anchor(selection.start?)?, + }, + end: Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: language::proto::deserialize_anchor(selection.end?)?, + }, + reversed: selection.reversed, + goal: SelectionGoal::None, + }) +} + impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) { if let Some(data) = data.downcast_ref::() { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index dbf1218ccd0f90e5c537160303561e7717287993..487c7e01a5a389689becbf7819572ba16566031e 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -579,7 +579,8 @@ message UpdateView { } message Editor { - Anchor scroll_top = 1; + repeated Selection selections = 1; + Anchor scroll_top = 2; } } @@ -593,7 +594,8 @@ message View { message Editor { uint64 buffer_id = 1; - Anchor scroll_top = 2; + repeated Selection selections = 2; + Anchor scroll_top = 3; } } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index c6627ce7b88b6cc949155d87ddbe6eeaadb597c9..e0f4147faf46b9f5f3ee63becdaac2124bf024ff 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1083,8 +1083,8 @@ mod tests { }; use collections::BTreeMap; use editor::{ - self, Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, - Rename, ToOffset, ToggleCodeActions, Undo, + self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, + ToOffset, ToggleCodeActions, Undo, }; use gpui::{executor, ModelHandle, TestAppContext, ViewHandle}; use language::{ @@ -4324,18 +4324,13 @@ mod tests { }) .await; + // When client A selects something, client B does as well. editor_a1.update(cx_a, |editor, cx| { - editor.select_ranges([2..2], None, cx); + editor.select_ranges([1..1, 2..2], None, cx); }); editor_b1 .condition(cx_b, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let selection = snapshot - .remote_selections_in_range(&(Anchor::min()..Anchor::max())) - .next(); - selection.map_or(false, |selection| { - selection.1.start.to_offset(&snapshot) == 2 - }) + editor.selected_ranges(cx) == vec![1..1, 2..2] }) .await; @@ -4343,10 +4338,6 @@ mod tests { workspace_b.update(cx_b, |workspace, cx| { workspace.unfollow(&workspace.active_pane().clone(), cx) }); - editor_b1.update(cx_b, |editor, cx| { - assert_eq!(editor.selected_ranges::(cx), &[2..2]); - }); - workspace_a.update(cx_a, |workspace, cx| { workspace.activate_item(&editor_a2, cx); editor_a2.update(cx, |editor, cx| editor.set_text("TWO", cx)); From c550fc3f01c9fad44b0358c3949eb41cefda8851 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 21:52:28 -0700 Subject: [PATCH 28/56] WIP - Start work on unfollowing automatically --- crates/editor/src/items.rs | 4 ++++ crates/workspace/src/workspace.rs | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7afc79f8b384a15c8a9111c24e0e11629730970e..d0457eba5adb3b4a18f240bf8141a87c5719276e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -158,6 +158,10 @@ impl FollowableItem for Editor { } Ok(()) } + + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { + false + } } fn serialize_selection(selection: &Selection) -> proto::Selection { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 387bfa9b6eff2b37403c3cf5df26d2476988711c..5ce61824e35a1f2fa13accf7ec430d6515b08d10 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -270,6 +270,7 @@ pub trait FollowableItem: Item { message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Result<()>; + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; } pub trait FollowableItemHandle: ItemHandle { @@ -285,6 +286,7 @@ pub trait FollowableItemHandle: ItemHandle { message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()>; + fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; } impl FollowableItemHandle for ViewHandle { @@ -313,6 +315,14 @@ impl FollowableItemHandle for ViewHandle { ) -> Result<()> { self.update(cx, |this, cx| this.apply_update_message(message, cx)) } + + fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { + if let Some(event) = event.downcast_ref() { + T::should_unfollow_on_event(event, cx) + } else { + false + } + } } pub trait ItemHandle: 'static + fmt::Debug { @@ -421,6 +431,12 @@ impl ItemHandle for ViewHandle { return; }; + if let Some(item) = item.to_followable_item_handle(cx) { + if item.should_unfollow_on_event(event, cx) { + workspace.unfollow(&pane, cx); + } + } + if T::should_close_item_on_event(event) { pane.update(cx, |pane, cx| pane.close_item(item.id(), cx)); return; From 31175545685efe094e4b253b3280eae32c02f69d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 09:16:25 +0100 Subject: [PATCH 29/56] Automatically unfollow when editing, scrolling or changing selections --- crates/editor/src/editor.rs | 44 ++++-- crates/editor/src/items.rs | 18 ++- crates/file_finder/src/file_finder.rs | 2 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/language/src/buffer.rs | 19 +-- crates/language/src/tests.rs | 12 +- crates/outline/src/outline.rs | 2 +- crates/project/src/project.rs | 13 +- crates/project_symbols/src/project_symbols.rs | 2 +- crates/search/src/buffer_search.rs | 6 +- crates/search/src/project_search.rs | 2 +- crates/server/src/rpc.rs | 148 ++++++++++++++++-- crates/theme_selector/src/theme_selector.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- 14 files changed, 214 insertions(+), 60 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8380eea12d9e3c59f821329304c03cc83205087d..bffc1a2186ad8ad7517a1fdd0e5feeb9dcff9c93 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1035,14 +1035,19 @@ impl Editor { self.scroll_top_anchor = Some(anchor); } - cx.emit(Event::ScrollPositionChanged); + cx.emit(Event::ScrollPositionChanged { local: true }); cx.notify(); } - fn set_scroll_top_anchor(&mut self, anchor: Option, cx: &mut ViewContext) { + fn set_scroll_top_anchor( + &mut self, + anchor: Option, + local: bool, + cx: &mut ViewContext, + ) { self.scroll_position = Vector2F::zero(); self.scroll_top_anchor = anchor; - cx.emit(Event::ScrollPositionChanged); + cx.emit(Event::ScrollPositionChanged { local }); cx.notify(); } @@ -1267,7 +1272,7 @@ impl Editor { _ => {} } - self.set_selections(self.selections.clone(), Some(pending), cx); + self.set_selections(self.selections.clone(), Some(pending), true, cx); } fn begin_selection( @@ -1347,7 +1352,12 @@ impl Editor { } else { selections = Arc::from([]); } - self.set_selections(selections, Some(PendingSelection { selection, mode }), cx); + self.set_selections( + selections, + Some(PendingSelection { selection, mode }), + true, + cx, + ); cx.notify(); } @@ -1461,7 +1471,7 @@ impl Editor { pending.selection.end = buffer.anchor_before(head); pending.selection.reversed = false; } - self.set_selections(self.selections.clone(), Some(pending), cx); + self.set_selections(self.selections.clone(), Some(pending), true, cx); } else { log::error!("update_selection dispatched with no pending selection"); return; @@ -1548,7 +1558,7 @@ impl Editor { if selections.is_empty() { selections = Arc::from([pending.selection]); } - self.set_selections(selections, None, cx); + self.set_selections(selections, None, true, cx); self.request_autoscroll(Autoscroll::Fit, cx); } else { let mut oldest_selection = self.oldest_selection::(&cx); @@ -1895,7 +1905,7 @@ impl Editor { } drop(snapshot); - self.set_selections(selections.into(), None, cx); + self.set_selections(selections.into(), None, true, cx); true } } else { @@ -3294,7 +3304,7 @@ impl Editor { pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() { - self.set_selections(selections, None, cx); + self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); } @@ -3303,7 +3313,7 @@ impl Editor { pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() { - self.set_selections(selections, None, cx); + self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); } @@ -4967,6 +4977,7 @@ impl Editor { } })), None, + true, cx, ); } @@ -5027,6 +5038,7 @@ impl Editor { &mut self, selections: Arc<[Selection]>, pending_selection: Option, + local: bool, cx: &mut ViewContext, ) { assert!( @@ -5095,7 +5107,7 @@ impl Editor { self.refresh_document_highlights(cx); self.pause_cursor_blinking(cx); - cx.emit(Event::SelectionsChanged); + cx.emit(Event::SelectionsChanged { local }); } pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { @@ -5508,10 +5520,10 @@ impl Editor { cx: &mut ViewContext, ) { match event { - language::Event::Edited => { + language::Event::Edited { local } => { self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); - cx.emit(Event::Edited); + cx.emit(Event::Edited { local: *local }); } language::Event::Dirtied => cx.emit(Event::Dirtied), language::Event::Saved => cx.emit(Event::Saved), @@ -5638,13 +5650,13 @@ fn compute_scroll_position( #[derive(Copy, Clone)] pub enum Event { Activate, - Edited, + Edited { local: bool }, Blurred, Dirtied, Saved, TitleChanged, - SelectionsChanged, - ScrollPositionChanged, + SelectionsChanged { local: bool }, + ScrollPositionChanged { local: bool }, Closed, } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d0457eba5adb3b4a18f240bf8141a87c5719276e..eceaa8815967bb13e35006c840abdcd721c64c78 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -58,7 +58,7 @@ impl FollowableItem for Editor { .collect::>() }; if !selections.is_empty() { - editor.set_selections(selections.into(), None, cx); + editor.set_selections(selections.into(), None, false, cx); } editor }) @@ -104,7 +104,7 @@ impl FollowableItem for Editor { _: &AppContext, ) -> Option { match event { - Event::ScrollPositionChanged | Event::SelectionsChanged => { + Event::ScrollPositionChanged { .. } | Event::SelectionsChanged { .. } => { Some(update_view::Variant::Editor(update_view::Editor { scroll_top: self .scroll_top_anchor @@ -138,10 +138,11 @@ impl FollowableItem for Editor { text_anchor: language::proto::deserialize_anchor(anchor) .ok_or_else(|| anyhow!("invalid scroll top"))?, }), + false, cx, ); } else { - self.set_scroll_top_anchor(None, cx); + self.set_scroll_top_anchor(None, false, cx); } let selections = message @@ -152,15 +153,20 @@ impl FollowableItem for Editor { }) .collect::>(); if !selections.is_empty() { - self.set_selections(selections.into(), None, cx); + self.set_selections(selections.into(), None, false, cx); } } } Ok(()) } - fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { - false + fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { + match event { + Event::Edited { local } => *local, + Event::SelectionsChanged { local } => *local, + Event::ScrollPositionChanged { local } => *local, + _ => false, + } } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index ca41eb74a11cb2d08103c22e90eebff3ccef7d53..4656daa4b3c1adbcf9cda5fd92428240c46ae13a 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -291,7 +291,7 @@ impl FileFinder { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { + editor::Event::Edited { .. } => { let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); if query.is_empty() { self.latest_search_id = post_inc(&mut self.search_count); diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index f2dd4e76b1dc414bd6df9113ae4e61da3524e6f1..ce8ba787a827b5daacb400334a91f6ef2967fcce 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -102,7 +102,7 @@ impl GoToLine { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited => { + editor::Event::Edited { .. } => { let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text(); let mut components = line_editor.trim().split(&[',', ':'][..]); let row = components.next().and_then(|row| row.parse::().ok()); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index fa4208ee5b42d09ae9fb96461ca604635e6e4e7e..9da9e59e4cd55bd63241d21a412e9711d6a65d90 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -142,7 +142,7 @@ pub enum Operation { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { Operation(Operation), - Edited, + Edited { local: bool }, Dirtied, Saved, FileHandleChanged, @@ -968,7 +968,7 @@ impl Buffer { ) -> Option { if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) { let was_dirty = start_version != self.saved_version; - self.did_edit(&start_version, was_dirty, cx); + self.did_edit(&start_version, was_dirty, true, cx); Some(transaction_id) } else { None @@ -1161,6 +1161,7 @@ impl Buffer { &mut self, old_version: &clock::Global, was_dirty: bool, + local: bool, cx: &mut ModelContext, ) { if self.edits_since::(old_version).next().is_none() { @@ -1169,7 +1170,7 @@ impl Buffer { self.reparse(cx); - cx.emit(Event::Edited); + cx.emit(Event::Edited { local }); if !was_dirty { cx.emit(Event::Dirtied); } @@ -1206,7 +1207,7 @@ impl Buffer { self.text.apply_ops(buffer_ops)?; self.deferred_ops.insert(deferred_ops); self.flush_deferred_ops(cx); - self.did_edit(&old_version, was_dirty, cx); + self.did_edit(&old_version, was_dirty, false, cx); // Notify independently of whether the buffer was edited as the operations could include a // selection update. cx.notify(); @@ -1321,7 +1322,7 @@ impl Buffer { if let Some((transaction_id, operation)) = self.text.undo() { self.send_operation(Operation::Buffer(operation), cx); - self.did_edit(&old_version, was_dirty, cx); + self.did_edit(&old_version, was_dirty, true, cx); Some(transaction_id) } else { None @@ -1342,7 +1343,7 @@ impl Buffer { self.send_operation(Operation::Buffer(operation), cx); } if undone { - self.did_edit(&old_version, was_dirty, cx) + self.did_edit(&old_version, was_dirty, true, cx) } undone } @@ -1353,7 +1354,7 @@ impl Buffer { if let Some((transaction_id, operation)) = self.text.redo() { self.send_operation(Operation::Buffer(operation), cx); - self.did_edit(&old_version, was_dirty, cx); + self.did_edit(&old_version, was_dirty, true, cx); Some(transaction_id) } else { None @@ -1374,7 +1375,7 @@ impl Buffer { self.send_operation(Operation::Buffer(operation), cx); } if redone { - self.did_edit(&old_version, was_dirty, cx) + self.did_edit(&old_version, was_dirty, true, cx) } redone } @@ -1440,7 +1441,7 @@ impl Buffer { if !ops.is_empty() { for op in ops { self.send_operation(Operation::Buffer(op), cx); - self.did_edit(&old_version, was_dirty, cx); + self.did_edit(&old_version, was_dirty, true, cx); } } } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 6c9980b334ac3c3c9a8861baf902d47f11bc4788..d36771c44fb302aead93c2eb74df3d0d574ff526 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -122,11 +122,19 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { let buffer_1_events = buffer_1_events.borrow(); assert_eq!( *buffer_1_events, - vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] + vec![ + Event::Edited { local: true }, + Event::Dirtied, + Event::Edited { local: true }, + Event::Edited { local: true } + ] ); let buffer_2_events = buffer_2_events.borrow(); - assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); + assert_eq!( + *buffer_2_events, + vec![Event::Edited { local: false }, Event::Dirtied] + ); } #[gpui::test] diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index fd4c8ff60b5d660d199e888596cd70649ae478a8..968fceb59c5c8eccd05961164fefd6529c617eef 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -224,7 +224,7 @@ impl OutlineView { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited => self.update_matches(cx), + editor::Event::Edited { .. } => self.update_matches(cx), _ => {} } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4a6f0dd6cff8f9830d0da0b86c6f0794c8a4d3ba..5f9a63c034d88711577aafad022584078504407a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1178,7 +1178,7 @@ impl Project { }); cx.background().spawn(request).detach_and_log_err(cx); } - BufferEvent::Edited => { + BufferEvent::Edited { .. } => { let language_server = self .language_server_for_buffer(buffer.read(cx), cx)? .clone(); @@ -6227,7 +6227,10 @@ mod tests { assert!(buffer.is_dirty()); assert_eq!( *events.borrow(), - &[language::Event::Edited, language::Event::Dirtied] + &[ + language::Event::Edited { local: true }, + language::Event::Dirtied + ] ); events.borrow_mut().clear(); buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), None, cx); @@ -6250,9 +6253,9 @@ mod tests { assert_eq!( *events.borrow(), &[ - language::Event::Edited, + language::Event::Edited { local: true }, language::Event::Dirtied, - language::Event::Edited, + language::Event::Edited { local: true }, ], ); events.borrow_mut().clear(); @@ -6264,7 +6267,7 @@ mod tests { assert!(buffer.is_dirty()); }); - assert_eq!(*events.borrow(), &[language::Event::Edited]); + assert_eq!(*events.borrow(), &[language::Event::Edited { local: true }]); // When a file is deleted, the buffer is considered dirty. let events = Rc::new(RefCell::new(Vec::new())); diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 27e125a59205d39c9b3a647103ea7ada1fceb8a8..5eb04718d7cf604551fd8668a3fd630f76077b53 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -328,7 +328,7 @@ impl ProjectSymbolsView { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited => self.update_matches(cx), + editor::Event::Edited { .. } => self.update_matches(cx), _ => {} } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 8eae666c454ea2870301f4238cfd3e137da3fd9a..13c73036f4dcc0a5c6c90557221e6d7b9ee6bdfb 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -360,7 +360,7 @@ impl SearchBar { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { + editor::Event::Edited { .. } => { self.query_contains_error = false; self.clear_matches(cx); self.update_matches(true, cx); @@ -377,8 +377,8 @@ impl SearchBar { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => self.update_matches(false, cx), - editor::Event::SelectionsChanged => self.update_match_index(cx), + editor::Event::Edited { .. } => self.update_matches(false, cx), + editor::Event::SelectionsChanged { .. } => self.update_match_index(cx), _ => {} } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index f027c965c631f0e9f83d5ba49d82eff28366004d..1302040d19c3c02606db7995d47eac6b147680a2 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -350,7 +350,7 @@ impl ProjectSearchView { cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) .detach(); cx.subscribe(&results_editor, |this, _, event, cx| { - if matches!(event, editor::Event::SelectionsChanged) { + if matches!(event, editor::Event::SelectionsChanged { .. }) { this.update_match_index(cx); } }) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index e0f4147faf46b9f5f3ee63becdaac2124bf024ff..761b5737ce86b27c8d1da548ef0fff7bfedba94b 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1086,7 +1086,7 @@ mod tests { self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }; - use gpui::{executor, ModelHandle, TestAppContext, ViewHandle}; + use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle}; use language::{ tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition, @@ -4308,11 +4308,6 @@ mod tests { .project_path(cx)), Some((worktree_id, "2.txt").into()) ); - let editor_b2 = workspace_b - .read_with(cx_b, |workspace, cx| workspace.active_item(cx)) - .unwrap() - .downcast::() - .unwrap(); // When client A activates a different editor, client B does so as well. workspace_a.update(cx_a, |workspace, cx| { @@ -4324,7 +4319,7 @@ mod tests { }) .await; - // When client A selects something, client B does as well. + // Changes to client A's editor are reflected on client B. editor_a1.update(cx_a, |editor, cx| { editor.select_ranges([1..1, 2..2], None, cx); }); @@ -4334,17 +4329,26 @@ mod tests { }) .await; + editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); + editor_b1 + .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") + .await; + + editor_a1.update(cx_a, |editor, cx| { + editor.select_ranges([3..3], None, cx); + }); + editor_b1 + .condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3]) + .await; + // After unfollowing, client B stops receiving updates from client A. workspace_b.update(cx_b, |workspace, cx| { workspace.unfollow(&workspace.active_pane().clone(), cx) }); workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(&editor_a2, cx); - editor_a2.update(cx, |editor, cx| editor.set_text("TWO", cx)); + workspace.activate_item(&editor_a2, cx) }); - editor_b2 - .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") - .await; + cx_a.foreground().run_until_parked(); assert_eq!( workspace_b.read_with(cx_b, |workspace, cx| workspace .active_item(cx) @@ -4456,6 +4460,126 @@ mod tests { ); } + #[gpui::test(iterations = 10)] + async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + cx_a.foreground().forbid_parking(); + let fs = FakeFs::new(cx_a.background()); + + // 2 clients connect to a server. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let mut client_a = server.create_client(cx_a, "user_a").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + + // Client B joins the project. + let project_b = client_b + .build_remote_project( + project_a + .read_with(cx_a, |project, _| project.remote_id()) + .unwrap(), + cx_b, + ) + .await; + + // Client A opens some editors. + let workspace_a = client_a.build_workspace(&project_a, cx_a); + let _editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B starts following client A. + let workspace_b = client_b.build_workspace(&project_b, cx_b); + let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); + let leader_id = project_b.read_with(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(&leader_id.into(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + + // When client B moves, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(&leader_id.into(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B edits, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(&leader_id.into(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B scrolls, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| { + editor.set_scroll_position(vec2f(0., 3.), cx) + }); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + } + #[gpui::test(iterations = 100)] async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) { cx.foreground().forbid_parking(); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index f879940f219f266a1f3108e0f570948cb9ef584e..ebdcc492a9f95af0232ef93b8ab934668d634c97 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -204,7 +204,7 @@ impl ThemeSelector { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { + editor::Event::Edited { .. } => { self.update_matches(cx); self.select_if_matching(&cx.global::().theme.name); self.show_selected_theme(cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5ce61824e35a1f2fa13accf7ec430d6515b08d10..cb9f7e7fed92b6c7b62433d05c11f93dcd05b3bf 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1750,7 +1750,7 @@ impl Workspace { None } - fn leader_for_pane(&self, pane: &ViewHandle) -> Option { + pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { self.follower_states_by_leader .iter() .find_map(|(leader_id, state)| { From 67dbc3117d8f0673d981a2f63eda612eaa938eb4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 09:42:37 +0100 Subject: [PATCH 30/56] Stop following when activating a different item on the follower pane --- crates/server/src/rpc.rs | 38 +++++++++++++++++++++++++++++++ crates/workspace/src/pane.rs | 19 +++++++++------- crates/workspace/src/workspace.rs | 15 ++++++++---- 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 761b5737ce86b27c8d1da548ef0fff7bfedba94b..047912c0ffd951890bfba11a7e37ed88525aead8 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4578,6 +4578,44 @@ mod tests { workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), None ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(&leader_id.into(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different pane, it continues following client A in the original pane. + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx) + }); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different item in the original pane, it automatically stops following client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), cx) + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); } #[gpui::test(iterations = 100)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 367cc967fca85baf5a111341c9e4a914f3139055..ab54b0053e461bdd9111d78ab941add7c40b4b3b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -33,7 +33,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { - pane.activate_item(action.0, cx); + pane.activate_item(action.0, true, cx); }); cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { pane.activate_prev_item(cx); @@ -92,6 +92,7 @@ pub fn init(cx: &mut MutableAppContext) { pub enum Event { Activate, + ActivateItem { local: bool }, Remove, Split(SplitDirection), } @@ -301,7 +302,7 @@ impl Pane { for (ix, item) in pane.items.iter().enumerate() { if item.project_entry_id(cx) == Some(project_entry_id) { let item = item.boxed_clone(); - pane.activate_item(ix, cx); + pane.activate_item(ix, true, cx); return Some(item); } } @@ -311,7 +312,7 @@ impl Pane { existing_item } else { let item = build_item(cx); - Self::add_item(workspace, pane, item.boxed_clone(), cx); + Self::add_item(workspace, pane, item.boxed_clone(), true, cx); item } } @@ -320,11 +321,12 @@ impl Pane { workspace: &mut Workspace, pane: ViewHandle, item: Box, + local: bool, cx: &mut ViewContext, ) { // Prevent adding the same item to the pane more than once. if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) { - pane.update(cx, |pane, cx| pane.activate_item(item_ix, cx)); + pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx)); return; } @@ -333,7 +335,7 @@ impl Pane { pane.update(cx, |pane, cx| { let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len()); pane.items.insert(item_idx, item); - pane.activate_item(item_idx, cx); + pane.activate_item(item_idx, local, cx); cx.notify(); }); } @@ -384,13 +386,14 @@ impl Pane { self.items.iter().position(|i| i.id() == item.id()) } - pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { + pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext) { if index < self.items.len() { let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); if prev_active_item_ix != self.active_item_index && prev_active_item_ix < self.items.len() { self.items[prev_active_item_ix].deactivated(cx); + cx.emit(Event::ActivateItem { local }); } self.update_active_toolbar(cx); self.focus_active_item(cx); @@ -406,7 +409,7 @@ impl Pane { } else if self.items.len() > 0 { index = self.items.len() - 1; } - self.activate_item(index, cx); + self.activate_item(index, true, cx); } pub fn activate_next_item(&mut self, cx: &mut ViewContext) { @@ -416,7 +419,7 @@ impl Pane { } else { index = 0; } - self.activate_item(index, cx); + self.activate_item(index, true, cx); } pub fn close_active_item(&mut self, cx: &mut ViewContext) { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index cb9f7e7fed92b6c7b62433d05c11f93dcd05b3bf..9c8da079ae1af0817eaebe0548129d7b160be5ad 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -445,7 +445,7 @@ impl ItemHandle for ViewHandle { if T::should_activate_item_on_event(event) { pane.update(cx, |pane, cx| { if let Some(ix) = pane.index_for_item(&item) { - pane.activate_item(ix, cx); + pane.activate_item(ix, true, cx); pane.activate(cx); } }); @@ -1022,7 +1022,7 @@ impl Workspace { pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { let pane = self.active_pane().clone(); - Pane::add_item(self, pane, item, cx); + Pane::add_item(self, pane, item, true, cx); } pub fn open_path( @@ -1111,7 +1111,7 @@ impl Workspace { }); if let Some((pane, ix)) = result { self.activate_pane(pane.clone(), cx); - pane.update(cx, |pane, cx| pane.activate_item(ix, cx)); + pane.update(cx, |pane, cx| pane.activate_item(ix, true, cx)); true } else { false @@ -1164,6 +1164,11 @@ impl Workspace { pane::Event::Activate => { self.activate_pane(pane, cx); } + pane::Event::ActivateItem { local } => { + if *local { + self.unfollow(&pane, cx); + } + } } } else { error!("pane {} not found", pane_id); @@ -1180,7 +1185,7 @@ impl Workspace { self.activate_pane(new_pane.clone(), cx); if let Some(item) = pane.read(cx).active_item() { if let Some(clone) = item.clone_on_split(cx.as_mut()) { - Pane::add_item(self, new_pane.clone(), clone, cx); + Pane::add_item(self, new_pane.clone(), clone, true, cx); } } self.center.split(&pane, &new_pane, direction).unwrap(); @@ -1793,7 +1798,7 @@ impl Workspace { } for (pane, item) in items_to_add { - Pane::add_item(self, pane.clone(), item.boxed_clone(), cx); + Pane::add_item(self, pane.clone(), item.boxed_clone(), false, cx); cx.notify(); } None From 7d566ce455a919e123331af5b147af4c1f468a6b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 10:16:58 +0100 Subject: [PATCH 31/56] Follow last collaborator or the next one via `cmd-alt-shift-f` --- crates/workspace/src/workspace.rs | 40 ++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9c8da079ae1af0817eaebe0548129d7b160be5ad..35f3ff9a0cf6519157b16b32fb2dd49cd820df40 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -70,6 +70,7 @@ action!(OpenNew, Arc); action!(OpenPaths, OpenParams); action!(ToggleShare); action!(ToggleFollow, PeerId); +action!(FollowNextCollaborator); action!(Unfollow); action!(JoinProject, JoinProjectParams); action!(Save); @@ -92,6 +93,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::toggle_share); cx.add_async_action(Workspace::toggle_follow); + cx.add_async_action(Workspace::follow_next_collaborator); cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); @@ -107,6 +109,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::toggle_sidebar_item_focus); cx.add_bindings(vec![ + Binding::new("cmd-alt-shift-F", FollowNextCollaborator, None), Binding::new("cmd-alt-shift-U", Unfollow, None), Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), @@ -630,6 +633,7 @@ pub struct Workspace { project: ModelHandle, leader_state: LeaderState, follower_states_by_leader: FollowerStatesByLeader, + last_leaders_by_pane: HashMap, PeerId>, _observe_current_user: Task<()>, } @@ -725,6 +729,7 @@ impl Workspace { project: params.project.clone(), leader_state: Default::default(), follower_states_by_leader: Default::default(), + last_leaders_by_pane: Default::default(), _observe_current_user, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); @@ -1245,15 +1250,17 @@ impl Workspace { if let Some(prev_leader_id) = self.unfollow(&pane, cx) { if leader_id == prev_leader_id { - cx.notify(); return None; } } + self.last_leaders_by_pane + .insert(pane.downgrade(), leader_id); self.follower_states_by_leader .entry(leader_id) .or_default() .insert(pane.clone(), Default::default()); + cx.notify(); let project_id = self.project.read(cx).remote_id()?; let request = self.client.request(proto::Follow { @@ -1279,6 +1286,37 @@ impl Workspace { })) } + pub fn follow_next_collaborator( + &mut self, + _: &FollowNextCollaborator, + cx: &mut ViewContext, + ) -> Option>> { + let collaborators = self.project.read(cx).collaborators(); + let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { + let mut collaborators = collaborators.keys().copied(); + while let Some(peer_id) = collaborators.next() { + if peer_id == leader_id { + break; + } + } + collaborators.next() + } else if let Some(last_leader_id) = + self.last_leaders_by_pane.get(&self.active_pane.downgrade()) + { + if collaborators.contains_key(last_leader_id) { + Some(*last_leader_id) + } else { + None + } + } else { + None + }; + + next_leader_id + .or_else(|| collaborators.keys().copied().next()) + .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx)) + } + pub fn unfollow( &mut self, pane: &ViewHandle, From 34e5a1f6bb20fc0217dccecdec34a63af84fd0fc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 10:31:28 +0100 Subject: [PATCH 32/56] Always render local selections on top of remote ones --- crates/editor/src/element.rs | 58 ++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index af02a353d4c2baa8038524d115c1fe887c11732a..0daf8f2fc25d821ed5acb041a9478bc0c15894e2 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -909,7 +909,7 @@ impl Element for EditorElement { .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) }; - let mut selections = HashMap::default(); + let mut selections = Vec::new(); let mut active_rows = BTreeMap::new(); let mut highlighted_rows = None; let mut highlighted_ranges = Vec::new(); @@ -922,11 +922,32 @@ impl Element for EditorElement { &display_map, ); + let mut remote_selections = HashMap::default(); + for (replica_id, selection) in display_map + .buffer_snapshot + .remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone())) + { + // The local selections match the leader's selections. + if Some(replica_id) == view.leader_replica_id { + continue; + } + + remote_selections + .entry(replica_id) + .or_insert(Vec::new()) + .push(crate::Selection { + id: selection.id, + goal: selection.goal, + reversed: selection.reversed, + start: selection.start.to_display_point(&display_map), + end: selection.end.to_display_point(&display_map), + }); + } + selections.extend(remote_selections); + if view.show_local_selections { - let local_selections = view.local_selections_in_range( - start_anchor.clone()..end_anchor.clone(), - &display_map, - ); + let local_selections = + view.local_selections_in_range(start_anchor..end_anchor, &display_map); for selection in &local_selections { let is_empty = selection.start == selection.end; let selection_start = snapshot.prev_line_boundary(selection.start).1; @@ -943,7 +964,7 @@ impl Element for EditorElement { // Render the local selections in the leader's color when following. let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx)); - selections.insert( + selections.push(( local_replica_id, local_selections .into_iter() @@ -955,28 +976,7 @@ impl Element for EditorElement { end: selection.end.to_display_point(&display_map), }) .collect(), - ); - } - - for (replica_id, selection) in display_map - .buffer_snapshot - .remote_selections_in_range(&(start_anchor..end_anchor)) - { - // The local selections match the leader's selections. - if Some(replica_id) == view.leader_replica_id { - continue; - } - - selections - .entry(replica_id) - .or_insert(Vec::new()) - .push(crate::Selection { - id: selection.id, - goal: selection.goal, - reversed: selection.reversed, - start: selection.start.to_display_point(&display_map), - end: selection.end.to_display_point(&display_map), - }); + )); } }); @@ -1222,7 +1222,7 @@ pub struct LayoutState { em_width: f32, em_advance: f32, highlighted_ranges: Vec<(Range, Color)>, - selections: HashMap>>, + selections: Vec<(ReplicaId, Vec>)>, context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, } From 5dc36260e511d87b44193f174fe46ac7edcab170 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 10:51:17 +0100 Subject: [PATCH 33/56] Reflect leader's view state when recycling existing local editors --- crates/editor/src/items.rs | 61 ++++++++++++++++++++++++++------------ crates/server/src/rpc.rs | 24 +++++++++++---- 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index eceaa8815967bb13e35006c840abdcd721c64c78..3f8bb2eab3412719f2efee6da08ee5f946b32d10 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -36,7 +36,7 @@ impl FollowableItem for Editor { }); Some(cx.spawn(|mut cx| async move { let buffer = buffer.await?; - Ok(pane + let editor = pane .read_with(&cx, |pane, cx| { pane.items_of_type::().find(|editor| { editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer) @@ -44,25 +44,48 @@ impl FollowableItem for Editor { }) .unwrap_or_else(|| { cx.add_view(pane.window_id(), |cx| { - let mut editor = Editor::for_buffer(buffer, Some(project), cx); - let selections = { - let buffer = editor.buffer.read(cx); - let buffer = buffer.read(cx); - let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); - state - .selections - .into_iter() - .filter_map(|selection| { - deserialize_selection(&excerpt_id, buffer_id, selection) - }) - .collect::>() - }; - if !selections.is_empty() { - editor.set_selections(selections.into(), None, false, cx); - } - editor + Editor::for_buffer(buffer, Some(project), cx) }) - })) + }); + editor.update(&mut cx, |editor, cx| { + let excerpt_id; + let buffer_id; + { + let buffer = editor.buffer.read(cx).read(cx); + let singleton = buffer.as_singleton().unwrap(); + excerpt_id = singleton.0.clone(); + buffer_id = singleton.1; + } + let selections = state + .selections + .into_iter() + .map(|selection| { + deserialize_selection(&excerpt_id, buffer_id, selection) + .ok_or_else(|| anyhow!("invalid selection")) + }) + .collect::>>()?; + if !selections.is_empty() { + editor.set_selections(selections.into(), None, false, cx); + } + + if let Some(anchor) = state.scroll_top { + editor.set_scroll_top_anchor( + Some(Anchor { + buffer_id: Some(state.buffer_id as usize), + excerpt_id: excerpt_id.clone(), + text_anchor: language::proto::deserialize_anchor(anchor) + .ok_or_else(|| anyhow!("invalid scroll top"))?, + }), + false, + cx, + ); + } else { + editor.set_scroll_top_anchor(None, false, cx); + } + + Ok::<_, anyhow::Error>(()) + })?; + Ok(editor) })) } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 047912c0ffd951890bfba11a7e37ed88525aead8..3f149390d35e91b751cda9eeab5c706e2ed11109 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1116,7 +1116,7 @@ mod tests { }, time::Duration, }; - use workspace::{Settings, SplitDirection, Workspace, WorkspaceParams}; + use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams}; #[cfg(test)] #[ctor::ctor] @@ -4287,7 +4287,9 @@ mod tests { .downcast::() .unwrap(); - // Client B starts following client A. + // When client B starts following client A, all visible view states are replicated to client B. + editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx)); + editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx)); workspace_b .update(cx_b, |workspace, cx| { let leader_id = project_b @@ -4301,13 +4303,25 @@ mod tests { }) .await .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, cx| workspace + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace .active_item(cx) .unwrap() - .project_path(cx)), + .downcast::() + .unwrap() + }); + assert_eq!( + editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)), Some((worktree_id, "2.txt").into()) ); + assert_eq!( + editor_b2.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)), + vec![2..3] + ); + assert_eq!( + editor_b1.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)), + vec![0..1] + ); // When client A activates a different editor, client B does so as well. workspace_a.update(cx_a, |workspace, cx| { From ffaf409a3111df1d6d70d4c4bd791f0a890f28da Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 11:06:52 +0100 Subject: [PATCH 34/56] Forget last pane's leader when such pane is removed This is just a memory optimization and doesn't cause any observable change in behavior. --- crates/workspace/src/workspace.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 35f3ff9a0cf6519157b16b32fb2dd49cd820df40..1f0a89a083a924d8b25791dd55da849c8e52dfaa 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1203,6 +1203,7 @@ impl Workspace { self.panes.retain(|p| p != &pane); self.activate_pane(self.panes.last().unwrap().clone(), cx); self.unfollow(&pane, cx); + self.last_leaders_by_pane.remove(&pane.downgrade()); cx.notify(); } } From e5a99cf8cd01e9e977aa2f9442e081574f515d66 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 11:15:39 +0100 Subject: [PATCH 35/56] Stop following when leader disconnects --- crates/project/src/project.rs | 2 ++ crates/server/src/rpc.rs | 44 +++++++++++++++++++++++++------ crates/workspace/src/workspace.rs | 24 +++++++++++++++-- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5f9a63c034d88711577aafad022584078504407a..c3c2f4e2a036e52c58b76a8a85d985f33180aa94 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -125,6 +125,7 @@ pub enum Event { DiskBasedDiagnosticsFinished, DiagnosticsUpdated(ProjectPath), RemoteIdChanged(Option), + CollaboratorLeft(PeerId), } enum LanguageServerEvent { @@ -3368,6 +3369,7 @@ impl Project { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } + cx.emit(Event::CollaboratorLeft(peer_id)); cx.notify(); Ok(()) }) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 3f149390d35e91b751cda9eeab5c706e2ed11109..3274e70d61fa612e93366611df196e57a91d5939 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4259,6 +4259,7 @@ mod tests { // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); + let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let editor_a1 = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), cx) @@ -4287,19 +4288,19 @@ mod tests { .downcast::() .unwrap(); + let client_a_id = project_b.read_with(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + let client_b_id = project_a.read_with(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + // When client B starts following client A, all visible view states are replicated to client B. editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx)); editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx)); workspace_b .update(cx_b, |workspace, cx| { - let leader_id = project_b - .read(cx) - .collaborators() - .values() - .next() - .unwrap() - .peer_id; - workspace.toggle_follow(&leader_id.into(), cx).unwrap() + workspace.toggle_follow(&client_a_id.into(), cx).unwrap() }) .await .unwrap(); @@ -4370,6 +4371,33 @@ mod tests { .id()), editor_b1.id() ); + + // Client A starts following client B. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.toggle_follow(&client_b_id.into(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + Some(client_b_id) + ); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + editor_a1.id() + ); + + // Following interrupts when client B disconnects. + client_b.disconnect(&cx_b.to_async()).unwrap(); + cx_a.foreground().run_until_parked(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + None + ); } #[gpui::test(iterations = 10)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1f0a89a083a924d8b25791dd55da849c8e52dfaa..353c3f251088bdb0697ec991bbf39f05d607564b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -667,8 +667,14 @@ impl Workspace { .detach(); cx.subscribe(¶ms.project, move |this, project, event, cx| { - if let project::Event::RemoteIdChanged(remote_id) = event { - this.project_remote_id_changed(*remote_id, cx); + match event { + project::Event::RemoteIdChanged(remote_id) => { + this.project_remote_id_changed(*remote_id, cx); + } + project::Event::CollaboratorLeft(peer_id) => { + this.collaborator_left(*peer_id, cx); + } + _ => {} } if project.read(cx).is_read_only() { cx.blur(); @@ -1241,6 +1247,20 @@ impl Workspace { } } + fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { + self.leader_state.followers.remove(&peer_id); + if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { + for state in states_by_pane.into_values() { + for item in state.items_by_leader_view_id.into_values() { + if let FollowerItem::Loaded(item) = item { + item.set_leader_replica_id(None, cx); + } + } + } + } + cx.notify(); + } + pub fn toggle_follow( &mut self, ToggleFollow(leader_id): &ToggleFollow, From 381c82714bdf9aaef67d8d55b0471206933b4c70 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 13:19:07 +0100 Subject: [PATCH 36/56] Bump protocol version --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index e937f6daaf2d9b754ee2b3b6457145a9206ecfd7..cfe780d5118c764a829cf047d288bc1e7a0b590e 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -5,4 +5,4 @@ pub mod proto; pub use conn::Connection; pub use peer::*; -pub const PROTOCOL_VERSION: u32 = 11; +pub const PROTOCOL_VERSION: u32 = 12; From 284a446be7fb0d794913e9517af9315b9e0c5ffa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 13:35:37 +0100 Subject: [PATCH 37/56] WIP --- crates/server/src/rpc.rs | 22 +++++++++++++++++++++- crates/workspace/src/pane.rs | 6 ++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 3274e70d61fa612e93366611df196e57a91d5939..9527df8e1f4634331ae301127d62067872d40c04 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4420,6 +4420,7 @@ mod tests { "1.txt": "one", "2.txt": "two", "3.txt": "three", + "4.txt": "four", }), ) .await; @@ -4441,6 +4442,7 @@ mod tests { // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); + let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let _editor_a1 = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), cx) @@ -4452,6 +4454,7 @@ mod tests { // Client B opens an editor. let workspace_b = client_b.build_workspace(&project_b, cx_b); + let pane_b1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let _editor_b1 = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), cx) @@ -4465,6 +4468,7 @@ mod tests { workspace_a .update(cx_a, |workspace, cx| { workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + assert_ne!(*workspace.active_pane(), pane_a1); let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); workspace .toggle_follow(&workspace::ToggleFollow(leader_id), cx) @@ -4475,6 +4479,7 @@ mod tests { workspace_b .update(cx_b, |workspace, cx| { workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + assert_ne!(*workspace.active_pane(), pane_b1); let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); workspace .toggle_follow(&workspace::ToggleFollow(leader_id), cx) @@ -4490,9 +4495,24 @@ mod tests { }) .await .unwrap(); + workspace_b + .update(cx_a, |workspace, cx| { + workspace.activate_next_pane(cx); + workspace.open_path((worktree_id, "4.txt"), cx) + }) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + + // Ensure leader updates don't change the active pane of followers + workspace_a.read_with(cx_a, |workspace, cx| { + assert_ne!(*workspace.active_pane(), pane_a1); + }); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_ne!(*workspace.active_pane(), pane_b1); + }); // Ensure peers following each other doesn't cause an infinite loop. - cx_a.foreground().run_until_parked(); assert_eq!( workspace_b.read_with(cx_b, |workspace, cx| workspace .active_item(cx) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ab54b0053e461bdd9111d78ab941add7c40b4b3b..9903d414e411b03b3f268e3775266a076b4b06b0 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -396,8 +396,10 @@ impl Pane { cx.emit(Event::ActivateItem { local }); } self.update_active_toolbar(cx); - self.focus_active_item(cx); - self.activate(cx); + if local { + self.focus_active_item(cx); + self.activate(cx); + } cx.notify(); } } From 73eae287a1bdd0de7fb30ef347923df89caef78b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 15:57:30 +0100 Subject: [PATCH 38/56] Don't trigger subscriptions with events emitted prior to subscribing Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 171 +++++++++++++++++++++++++++++++++-------- 1 file changed, 140 insertions(+), 31 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 41d3bf2cdd18e1fcfe422879ba86bbfee1b1ece5..fcf8c05d0c8746909371a66fd1bea62d9f4fc18c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -8,6 +8,7 @@ use crate::{ AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; +use collections::btree_map; use keymap::MatchResult; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -1150,25 +1151,22 @@ impl MutableAppContext { H: Handle, F: 'static + FnMut(H, &E::Event, &mut Self) -> bool, { - let id = post_inc(&mut self.next_subscription_id); + let subscription_id = post_inc(&mut self.next_subscription_id); let emitter = handle.downgrade(); - self.subscriptions - .lock() - .entry(handle.id()) - .or_default() - .insert( - id, - Some(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 - } - })), - ); + self.pending_effects.push_back(Effect::Subscribe { + entity_id: handle.id(), + subscription_id, + callback: 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, + id: subscription_id, entity_id: handle.id(), subscriptions: Some(Arc::downgrade(&self.subscriptions)), } @@ -1655,6 +1653,11 @@ impl MutableAppContext { loop { if let Some(effect) = self.pending_effects.pop_front() { match effect { + Effect::Subscribe { + entity_id, + subscription_id, + callback, + } => self.handle_subscribe_effect(entity_id, subscription_id, callback), Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload), Effect::GlobalEvent { payload } => self.emit_global_event(payload), Effect::ModelNotification { model_id } => { @@ -1771,6 +1774,30 @@ impl MutableAppContext { } } + fn handle_subscribe_effect( + &mut self, + entity_id: usize, + subscription_id: usize, + callback: SubscriptionCallback, + ) { + match self + .subscriptions + .lock() + .entry(entity_id) + .or_default() + .entry(subscription_id) + { + btree_map::Entry::Vacant(entry) => { + entry.insert(Some(callback)); + } + // Subscription was dropped before effect was processed + btree_map::Entry::Occupied(entry) => { + debug_assert!(entry.get().is_none()); + entry.remove(); + } + } + } + fn emit_event(&mut self, entity_id: usize, payload: Box) { let callbacks = self.subscriptions.lock().remove(&entity_id); if let Some(callbacks) = callbacks { @@ -1785,10 +1812,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -1812,10 +1839,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -1839,10 +1866,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -1880,10 +1907,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -2234,6 +2261,11 @@ pub struct WindowInvalidation { } pub enum Effect { + Subscribe { + entity_id: usize, + subscription_id: usize, + callback: SubscriptionCallback, + }, Event { entity_id: usize, payload: Box, @@ -2270,6 +2302,10 @@ pub enum Effect { impl Debug for Effect { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Effect::Subscribe { entity_id, .. } => f + .debug_struct("Effect::Subscribe") + .field("entity_id", entity_id) + .finish(), Effect::Event { entity_id, .. } => f .debug_struct("Effect::Event") .field("entity_id", entity_id) @@ -4053,10 +4089,10 @@ impl Drop for Subscription { .or_default() .entry(*id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(None); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -4069,10 +4105,10 @@ impl Drop for Subscription { } => { if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) { match subscriptions.lock().entry(*type_id).or_default().entry(*id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(None); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -4090,10 +4126,10 @@ impl Drop for Subscription { .or_default() .entry(*id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(None); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -4375,6 +4411,7 @@ mod tests { let handle_1 = cx.add_model(|_| Model::default()); let handle_2 = cx.add_model(|_| Model::default()); + handle_1.update(cx, |_, cx| { cx.subscribe(&handle_2, move |model: &mut Model, emitter, event, cx| { model.events.push(*event); @@ -4394,6 +4431,37 @@ mod tests { assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]); } + #[crate::test(self)] + fn test_model_emit_before_subscribe_in_same_update_cycle(cx: &mut MutableAppContext) { + #[derive(Default)] + struct Model; + + impl Entity for Model { + type Event = (); + } + + let events = Rc::new(RefCell::new(Vec::new())); + cx.add_model(|cx| { + drop(cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("dropped before flush") + })); + cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("before emit") + }) + .detach(); + cx.emit(()); + cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("after emit") + }) + .detach(); + Model + }); + assert_eq!(*events.borrow(), ["before emit"]); + } + #[crate::test(self)] fn test_observe_and_notify_from_model(cx: &mut MutableAppContext) { #[derive(Default)] @@ -4814,6 +4882,47 @@ mod tests { observed_model.update(cx, |_, cx| cx.emit(())); } + #[crate::test(self)] + fn test_view_emit_before_subscribe_in_same_update_cycle(cx: &mut MutableAppContext) { + #[derive(Default)] + struct TestView; + + impl Entity for TestView { + type Event = (); + } + + impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new().boxed() + } + } + + let events = Rc::new(RefCell::new(Vec::new())); + cx.add_window(Default::default(), |cx| { + drop(cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("dropped before flush") + })); + cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("before emit") + }) + .detach(); + cx.emit(()); + cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("after emit") + }) + .detach(); + TestView + }); + assert_eq!(*events.borrow(), ["before emit"]); + } + #[crate::test(self)] fn test_observe_and_notify_from_view(cx: &mut MutableAppContext) { #[derive(Default)] From 5ecf945e282be9d0801d751cfde760448ad4eca9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 16:11:55 +0100 Subject: [PATCH 39/56] Don't trigger global subscriptions with events emitted prior to subscribing Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 108 +++++++++++++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 14 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index fcf8c05d0c8746909371a66fd1bea62d9f4fc18c..0b3dd26f554ef76d2e2e87e3a29d7d60b1b58584 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1111,21 +1111,18 @@ impl MutableAppContext { E: Any, F: 'static + FnMut(&E, &mut Self), { - let id = post_inc(&mut self.next_subscription_id); + let subscription_id = post_inc(&mut self.next_subscription_id); let type_id = TypeId::of::(); - self.global_subscriptions - .lock() - .entry(type_id) - .or_default() - .insert( - id, - Some(Box::new(move |payload, cx| { - let payload = payload.downcast_ref().expect("downcast is type safe"); - callback(payload, cx) - })), - ); + self.pending_effects.push_back(Effect::SubscribeGlobal { + type_id, + subscription_id, + callback: Box::new(move |payload, cx| { + let payload = payload.downcast_ref().expect("downcast is type safe"); + callback(payload, cx) + }), + }); Subscription::GlobalSubscription { - id, + id: subscription_id, type_id, subscriptions: Some(Arc::downgrade(&self.global_subscriptions)), } @@ -1659,6 +1656,13 @@ impl MutableAppContext { callback, } => self.handle_subscribe_effect(entity_id, subscription_id, callback), Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload), + Effect::SubscribeGlobal { + type_id, + subscription_id, + callback, + } => { + self.handle_subscribe_global_effect(type_id, subscription_id, callback) + } Effect::GlobalEvent { payload } => self.emit_global_event(payload), Effect::ModelNotification { model_id } => { self.notify_model_observers(model_id) @@ -1825,6 +1829,30 @@ impl MutableAppContext { } } + fn handle_subscribe_global_effect( + &mut self, + type_id: TypeId, + subscription_id: usize, + callback: GlobalSubscriptionCallback, + ) { + match self + .global_subscriptions + .lock() + .entry(type_id) + .or_default() + .entry(subscription_id) + { + btree_map::Entry::Vacant(entry) => { + entry.insert(Some(callback)); + } + // Subscription was dropped before effect was processed + btree_map::Entry::Occupied(entry) => { + debug_assert!(entry.get().is_none()); + entry.remove(); + } + } + } + fn emit_global_event(&mut self, payload: Box) { let type_id = (&*payload).type_id(); let callbacks = self.global_subscriptions.lock().remove(&type_id); @@ -2270,6 +2298,11 @@ pub enum Effect { entity_id: usize, payload: Box, }, + SubscribeGlobal { + type_id: TypeId, + subscription_id: usize, + callback: GlobalSubscriptionCallback, + }, GlobalEvent { payload: Box, }, @@ -2302,14 +2335,28 @@ pub enum Effect { impl Debug for Effect { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Effect::Subscribe { entity_id, .. } => f + Effect::Subscribe { + entity_id, + subscription_id, + .. + } => f .debug_struct("Effect::Subscribe") .field("entity_id", entity_id) + .field("subscription_id", subscription_id) .finish(), Effect::Event { entity_id, .. } => f .debug_struct("Effect::Event") .field("entity_id", entity_id) .finish(), + Effect::SubscribeGlobal { + type_id, + subscription_id, + .. + } => f + .debug_struct("Effect::Subscribe") + .field("type_id", type_id) + .field("subscription_id", subscription_id) + .finish(), Effect::GlobalEvent { payload, .. } => f .debug_struct("Effect::GlobalEvent") .field("type_id", &(&*payload).type_id()) @@ -4795,6 +4842,39 @@ mod tests { ); } + #[crate::test(self)] + fn test_global_events_emitted_before_subscription(cx: &mut MutableAppContext) { + let events = Rc::new(RefCell::new(Vec::new())); + cx.update(|cx| { + { + let events = events.clone(); + drop(cx.subscribe_global(move |_: &(), _| { + events.borrow_mut().push("dropped before emit"); + })); + } + + { + let events = events.clone(); + cx.subscribe_global(move |_: &(), _| { + events.borrow_mut().push("before emit"); + }) + .detach(); + } + + cx.emit_global(()); + + { + let events = events.clone(); + cx.subscribe_global(move |_: &(), _| { + events.borrow_mut().push("after emit"); + }) + .detach(); + } + }); + + assert_eq!(*events.borrow(), ["before emit"]); + } + #[crate::test(self)] fn test_global_nested_events(cx: &mut MutableAppContext) { #[derive(Clone, Debug, Eq, PartialEq)] From 9885c4f6bafd6a3cac1836b85507b39f0e5cbc53 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 16:24:48 +0100 Subject: [PATCH 40/56] Don't trigger observations with notifications emitted prior to observing Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 181 ++++++++++++++++++++++++++++++++------- crates/server/src/rpc.rs | 4 +- 2 files changed, 151 insertions(+), 34 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 0b3dd26f554ef76d2e2e87e3a29d7d60b1b58584..1e4448de98ef5017845c0d552b20778f456c6a89 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1113,7 +1113,7 @@ impl MutableAppContext { { let subscription_id = post_inc(&mut self.next_subscription_id); let type_id = TypeId::of::(); - self.pending_effects.push_back(Effect::SubscribeGlobal { + self.pending_effects.push_back(Effect::GlobalSubscription { type_id, subscription_id, callback: Box::new(move |payload, cx| { @@ -1150,7 +1150,7 @@ impl MutableAppContext { { let subscription_id = post_inc(&mut self.next_subscription_id); let emitter = handle.downgrade(); - self.pending_effects.push_back(Effect::Subscribe { + self.pending_effects.push_back(Effect::Subscription { entity_id: handle.id(), subscription_id, callback: Box::new(move |payload, cx| { @@ -1176,25 +1176,23 @@ impl MutableAppContext { H: Handle, F: 'static + FnMut(H, &mut Self) -> bool, { - let id = post_inc(&mut self.next_subscription_id); + let subscription_id = post_inc(&mut self.next_subscription_id); let observed = handle.downgrade(); - self.observations - .lock() - .entry(handle.id()) - .or_default() - .insert( - id, - Some(Box::new(move |cx| { - if let Some(observed) = H::upgrade_from(&observed, cx) { - callback(observed, cx) - } else { - false - } - })), - ); + let entity_id = handle.id(); + self.pending_effects.push_back(Effect::Observation { + entity_id, + subscription_id, + callback: 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(), + id: subscription_id, + entity_id, observations: Some(Arc::downgrade(&self.observations)), } } @@ -1650,20 +1648,27 @@ impl MutableAppContext { loop { if let Some(effect) = self.pending_effects.pop_front() { match effect { - Effect::Subscribe { + Effect::Subscription { entity_id, subscription_id, callback, - } => self.handle_subscribe_effect(entity_id, subscription_id, callback), + } => self.handle_subscription_effect(entity_id, subscription_id, callback), Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload), - Effect::SubscribeGlobal { + Effect::GlobalSubscription { type_id, subscription_id, callback, - } => { - self.handle_subscribe_global_effect(type_id, subscription_id, callback) - } + } => self.handle_global_subscription_effect( + type_id, + subscription_id, + callback, + ), Effect::GlobalEvent { payload } => self.emit_global_event(payload), + Effect::Observation { + entity_id, + subscription_id, + callback, + } => self.handle_observation_effect(entity_id, subscription_id, callback), Effect::ModelNotification { model_id } => { self.notify_model_observers(model_id) } @@ -1778,7 +1783,7 @@ impl MutableAppContext { } } - fn handle_subscribe_effect( + fn handle_subscription_effect( &mut self, entity_id: usize, subscription_id: usize, @@ -1829,7 +1834,7 @@ impl MutableAppContext { } } - fn handle_subscribe_global_effect( + fn handle_global_subscription_effect( &mut self, type_id: TypeId, subscription_id: usize, @@ -1879,6 +1884,30 @@ impl MutableAppContext { } } + fn handle_observation_effect( + &mut self, + entity_id: usize, + subscription_id: usize, + callback: ObservationCallback, + ) { + match self + .observations + .lock() + .entry(entity_id) + .or_default() + .entry(subscription_id) + { + btree_map::Entry::Vacant(entry) => { + entry.insert(Some(callback)); + } + // Observation was dropped before effect was processed + btree_map::Entry::Occupied(entry) => { + debug_assert!(entry.get().is_none()); + entry.remove(); + } + } + } + fn notify_model_observers(&mut self, observed_id: usize) { let callbacks = self.observations.lock().remove(&observed_id); if let Some(callbacks) = callbacks { @@ -2289,7 +2318,7 @@ pub struct WindowInvalidation { } pub enum Effect { - Subscribe { + Subscription { entity_id: usize, subscription_id: usize, callback: SubscriptionCallback, @@ -2298,7 +2327,7 @@ pub enum Effect { entity_id: usize, payload: Box, }, - SubscribeGlobal { + GlobalSubscription { type_id: TypeId, subscription_id: usize, callback: GlobalSubscriptionCallback, @@ -2306,6 +2335,11 @@ pub enum Effect { GlobalEvent { payload: Box, }, + Observation { + entity_id: usize, + subscription_id: usize, + callback: ObservationCallback, + }, ModelNotification { model_id: usize, }, @@ -2335,7 +2369,7 @@ pub enum Effect { impl Debug for Effect { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Effect::Subscribe { + Effect::Subscription { entity_id, subscription_id, .. @@ -2348,7 +2382,7 @@ impl Debug for Effect { .debug_struct("Effect::Event") .field("entity_id", entity_id) .finish(), - Effect::SubscribeGlobal { + Effect::GlobalSubscription { type_id, subscription_id, .. @@ -2361,6 +2395,15 @@ impl Debug for Effect { .debug_struct("Effect::GlobalEvent") .field("type_id", &(&*payload).type_id()) .finish(), + Effect::Observation { + entity_id, + subscription_id, + .. + } => f + .debug_struct("Effect::Observation") + .field("entity_id", entity_id) + .field("subscription_id", subscription_id) + .finish(), Effect::ModelNotification { model_id } => f .debug_struct("Effect::ModelNotification") .field("model_id", model_id) @@ -4548,6 +4591,37 @@ mod tests { assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]) } + #[crate::test(self)] + fn test_model_notify_before_observe_in_same_update_cycle(cx: &mut MutableAppContext) { + #[derive(Default)] + struct Model; + + impl Entity for Model { + type Event = (); + } + + let events = Rc::new(RefCell::new(Vec::new())); + cx.add_model(|cx| { + drop(cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("dropped before flush") + })); + cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("before notify") + }) + .detach(); + cx.notify(); + cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("after notify") + }) + .detach(); + Model + }); + assert_eq!(*events.borrow(), ["before notify"]); + } + #[crate::test(self)] fn test_view_handles(cx: &mut MutableAppContext) { struct View { @@ -4843,7 +4917,9 @@ mod tests { } #[crate::test(self)] - fn test_global_events_emitted_before_subscription(cx: &mut MutableAppContext) { + fn test_global_events_emitted_before_subscription_in_same_update_cycle( + cx: &mut MutableAppContext, + ) { let events = Rc::new(RefCell::new(Vec::new())); cx.update(|cx| { { @@ -5050,6 +5126,47 @@ mod tests { assert_eq!(view.read(cx).events, vec![11]); } + #[crate::test(self)] + fn test_view_notify_before_observe_in_same_update_cycle(cx: &mut MutableAppContext) { + #[derive(Default)] + struct TestView; + + impl Entity for TestView { + type Event = (); + } + + impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new().boxed() + } + } + + let events = Rc::new(RefCell::new(Vec::new())); + cx.add_window(Default::default(), |cx| { + drop(cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("dropped before flush") + })); + cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("before notify") + }) + .detach(); + cx.notify(); + cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("after notify") + }) + .detach(); + TestView + }); + assert_eq!(*events.borrow(), ["before notify"]); + } + #[crate::test(self)] fn test_dropping_observers(cx: &mut MutableAppContext) { struct View; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 9527df8e1f4634331ae301127d62067872d40c04..53d1315662ec7cd585be71667d3be9ac06f81cce 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4505,10 +4505,10 @@ mod tests { cx_a.foreground().run_until_parked(); // Ensure leader updates don't change the active pane of followers - workspace_a.read_with(cx_a, |workspace, cx| { + workspace_a.read_with(cx_a, |workspace, _| { assert_ne!(*workspace.active_pane(), pane_a1); }); - workspace_b.read_with(cx_b, |workspace, cx| { + workspace_b.read_with(cx_b, |workspace, _| { assert_ne!(*workspace.active_pane(), pane_b1); }); From c78bcf711624881a409d8df67c6fb4c13b8ff3c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 16:44:59 +0100 Subject: [PATCH 41/56] Ensure leader updates don't change the active pane of followers Co-Authored-By: Nathan Sobo --- crates/server/src/rpc.rs | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 53d1315662ec7cd585be71667d3be9ac06f81cce..79fb49da809f6d0b36fc51a336cdd3c8fb9aa4b4 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4454,7 +4454,7 @@ mod tests { // Client B opens an editor. let workspace_b = client_b.build_workspace(&project_b, cx_b); - let pane_b1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); + let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); let _editor_b1 = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), cx) @@ -4491,13 +4491,15 @@ mod tests { workspace_a .update(cx_a, |workspace, cx| { workspace.activate_next_pane(cx); + assert_eq!(*workspace.active_pane(), pane_a1); workspace.open_path((worktree_id, "3.txt"), cx) }) .await .unwrap(); workspace_b - .update(cx_a, |workspace, cx| { + .update(cx_b, |workspace, cx| { workspace.activate_next_pane(cx); + assert_eq!(*workspace.active_pane(), pane_b1); workspace.open_path((worktree_id, "4.txt"), cx) }) .await @@ -4506,20 +4508,42 @@ mod tests { // Ensure leader updates don't change the active pane of followers workspace_a.read_with(cx_a, |workspace, _| { - assert_ne!(*workspace.active_pane(), pane_a1); + assert_eq!(*workspace.active_pane(), pane_a1); }); workspace_b.read_with(cx_b, |workspace, _| { - assert_ne!(*workspace.active_pane(), pane_b1); + assert_eq!(*workspace.active_pane(), pane_b1); }); // Ensure peers following each other doesn't cause an infinite loop. assert_eq!( - workspace_b.read_with(cx_b, |workspace, cx| workspace + workspace_a.read_with(cx_a, |workspace, cx| workspace .active_item(cx) .unwrap() .project_path(cx)), Some((worktree_id, "3.txt").into()) ); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "3.txt").into()) + ); + workspace.activate_next_pane(cx); + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "4.txt").into()) + ); + }); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "4.txt").into()) + ); + workspace.activate_next_pane(cx); + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "3.txt").into()) + ); + }); } #[gpui::test(iterations = 10)] From 4ed8f6fbb490db2f8158115cb4c9313ab34f14a8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 09:39:39 -0700 Subject: [PATCH 42/56] Make UpdateBuffer a foreground message --- crates/rpc/src/proto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 39a0d669d5747673e5640f1939d57351ddadf4fe..59d6773451fd2feebc28b17120e0b50a58de1127 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -202,7 +202,7 @@ messages!( (UnregisterProject, Foreground), (UnregisterWorktree, Foreground), (UnshareProject, Foreground), - (UpdateBuffer, Background), + (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), (UpdateDiagnosticSummary, Foreground), From c105802b2d1504f757fec27b401fbee8964263df Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 11:43:30 -0700 Subject: [PATCH 43/56] Allow customizing the pane's following border width in the theme --- crates/theme/src/theme.rs | 1 + crates/workspace/src/pane_group.rs | 2 +- crates/zed/assets/themes/_base.toml | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 61d0bf3f67e3799b4a3c8364b9eea9815f210bb2..d10c282e3526056acd397794b184491bd17a43b6 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -36,6 +36,7 @@ pub struct Workspace { pub active_tab: Tab, pub pane_divider: Border, pub leader_border_opacity: f32, + pub leader_border_width: f32, pub left_sidebar: Sidebar, pub right_sidebar: Sidebar, pub status_bar: StatusBar, diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index c3f4d2d3a6e8a5086a5c78d183a47fbbfe940551..d34613df4ac19633a9659d59b33580f38d4825ad 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -111,7 +111,7 @@ impl Member { .editor .replica_selection_style(leader.replica_id) .cursor; - border = Border::all(1.0, leader_color); + border = Border::all(theme.workspace.leader_border_width, leader_color); border .color .fade_out(1. - theme.workspace.leader_border_opacity); diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index d0368d69338911a66669b9f9f88c0c107dc95ac8..7bd0c59045fa6f99dee87429d5eef38b327f0137 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -4,7 +4,8 @@ base = { family = "Zed Sans", size = 14 } [workspace] background = "$surface.0" pane_divider = { width = 1, color = "$border.0" } -leader_border_opacity = 0.6 +leader_border_opacity = 0.7 +leader_border_width = 2.0 [workspace.titlebar] height = 32 From 0a3f013e00693d2475589f67d70bd5cd8d94004d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 11:44:54 -0700 Subject: [PATCH 44/56] Use env_logger when running the app in a terminal --- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index b0e5a63137dcbd6b8a0cdde080b99772f87e9447..cbf9389fd4028213aa8c23ad8183e0419f0f6634 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -64,6 +64,7 @@ crossbeam-channel = "0.5.0" ctor = "0.1.20" dirs = "3.0" easy-parallel = "3.1.0" +env_logger = "0.8" futures = "0.3" http-auth-basic = "0.1.3" ignore = "0.4" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 05437834942508a63ad067b3bfcbb9e57f4a8f35..61967bfcdfb00738c56d52fa228041a30f9e90ce 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -9,7 +9,6 @@ use gpui::{App, AssetSource, Task}; use log::LevelFilter; use parking_lot::Mutex; use project::Fs; -use simplelog::SimpleLogger; use smol::process::Command; use std::{env, fs, path::PathBuf, sync::Arc}; use theme::{ThemeRegistry, DEFAULT_THEME_NAME}; @@ -142,11 +141,10 @@ fn main() { } fn init_logger() { - let level = LevelFilter::Info; - if stdout_is_a_pty() { - SimpleLogger::init(level, Default::default()).expect("could not initialize logger"); + env_logger::init(); } else { + let level = LevelFilter::Info; let log_dir_path = dirs::home_dir() .expect("could not locate home directory for logging") .join("Library/Logs/"); From fc811e08562a913d2efc42501ecbef17c57935c2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 13:31:13 -0700 Subject: [PATCH 45/56] Don't represent editor's scroll top anchor as an option Use Anchor::min as the special value representing a scroll top of zero --- crates/editor/src/editor.rs | 23 +++++++++-------------- crates/editor/src/items.rs | 26 ++++++++++---------------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bffc1a2186ad8ad7517a1fdd0e5feeb9dcff9c93..8d1890e04d488f41c70d51b4238fbbb81bc2eb10 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -438,7 +438,7 @@ pub struct Editor { select_larger_syntax_node_stack: Vec]>>, active_diagnostics: Option, scroll_position: Vector2F, - scroll_top_anchor: Option, + scroll_top_anchor: Anchor, autoscroll_request: Option, soft_wrap_mode_override: Option, get_field_editor_theme: Option, @@ -473,7 +473,7 @@ pub struct EditorSnapshot { pub placeholder_text: Option>, is_focused: bool, scroll_position: Vector2F, - scroll_top_anchor: Option, + scroll_top_anchor: Anchor, } #[derive(Clone)] @@ -915,7 +915,7 @@ impl Editor { get_field_editor_theme, project, scroll_position: Vector2F::zero(), - scroll_top_anchor: None, + scroll_top_anchor: Anchor::min(), autoscroll_request: None, focused: false, show_local_cursors: false, @@ -1020,7 +1020,7 @@ impl Editor { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); if scroll_position.y() == 0. { - self.scroll_top_anchor = None; + self.scroll_top_anchor = Anchor::min(); self.scroll_position = scroll_position; } else { let scroll_top_buffer_offset = @@ -1032,19 +1032,14 @@ impl Editor { scroll_position.x(), scroll_position.y() - anchor.to_display_point(&map).row() as f32, ); - self.scroll_top_anchor = Some(anchor); + self.scroll_top_anchor = anchor; } cx.emit(Event::ScrollPositionChanged { local: true }); cx.notify(); } - fn set_scroll_top_anchor( - &mut self, - anchor: Option, - local: bool, - cx: &mut ViewContext, - ) { + fn set_scroll_top_anchor(&mut self, anchor: Anchor, local: bool, cx: &mut ViewContext) { self.scroll_position = Vector2F::zero(); self.scroll_top_anchor = anchor; cx.emit(Event::ScrollPositionChanged { local }); @@ -5636,10 +5631,10 @@ impl Deref for EditorSnapshot { fn compute_scroll_position( snapshot: &DisplaySnapshot, mut scroll_position: Vector2F, - scroll_top_anchor: &Option, + scroll_top_anchor: &Anchor, ) -> Vector2F { - if let Some(anchor) = scroll_top_anchor { - let scroll_top = anchor.to_display_point(snapshot).row() as f32; + if *scroll_top_anchor != Anchor::min() { + let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32; scroll_position.set_y(scroll_top + scroll_position.y()); } else { scroll_position.set_y(0.); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3f8bb2eab3412719f2efee6da08ee5f946b32d10..295f7f664c1d679c56d1aad0732b20a91365af40 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -70,17 +70,15 @@ impl FollowableItem for Editor { if let Some(anchor) = state.scroll_top { editor.set_scroll_top_anchor( - Some(Anchor { + Anchor { buffer_id: Some(state.buffer_id as usize), excerpt_id: excerpt_id.clone(), text_anchor: language::proto::deserialize_anchor(anchor) .ok_or_else(|| anyhow!("invalid scroll top"))?, - }), + }, false, cx, ); - } else { - editor.set_scroll_top_anchor(None, false, cx); } Ok::<_, anyhow::Error>(()) @@ -113,10 +111,9 @@ impl FollowableItem for Editor { let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); Some(proto::view::Variant::Editor(proto::view::Editor { buffer_id, - scroll_top: self - .scroll_top_anchor - .as_ref() - .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), + scroll_top: Some(language::proto::serialize_anchor( + &self.scroll_top_anchor.text_anchor, + )), selections: self.selections.iter().map(serialize_selection).collect(), })) } @@ -129,10 +126,9 @@ impl FollowableItem for Editor { match event { Event::ScrollPositionChanged { .. } | Event::SelectionsChanged { .. } => { Some(update_view::Variant::Editor(update_view::Editor { - scroll_top: self - .scroll_top_anchor - .as_ref() - .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), + scroll_top: Some(language::proto::serialize_anchor( + &self.scroll_top_anchor.text_anchor, + )), selections: self.selections.iter().map(serialize_selection).collect(), })) } @@ -155,17 +151,15 @@ impl FollowableItem for Editor { if let Some(anchor) = message.scroll_top { self.set_scroll_top_anchor( - Some(Anchor { + Anchor { buffer_id: Some(buffer_id), excerpt_id: excerpt_id.clone(), text_anchor: language::proto::deserialize_anchor(anchor) .ok_or_else(|| anyhow!("invalid scroll top"))?, - }), + }, false, cx, ); - } else { - self.set_scroll_top_anchor(None, false, cx); } let selections = message From 4435d9b1060be71f3a050b86fd2c4a65dafa6d88 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 15:27:05 -0700 Subject: [PATCH 46/56] Combine updates from multiple view events when updating followers Co-Authored-By: Nathan Sobo --- crates/editor/src/items.rs | 41 ++++++++---- crates/server/src/rpc.rs | 3 + crates/workspace/src/workspace.rs | 103 +++++++++++++++++++----------- 3 files changed, 95 insertions(+), 52 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 295f7f664c1d679c56d1aad0732b20a91365af40..3cac47bf5912d8c969b15c9302c46e248aa1c339 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -15,7 +15,7 @@ use workspace::{ }; impl FollowableItem for Editor { - fn for_state_message( + fn from_state_proto( pane: ViewHandle, project: ModelHandle, state: &mut Option, @@ -107,7 +107,7 @@ impl FollowableItem for Editor { cx.notify(); } - fn to_state_message(&self, cx: &AppContext) -> Option { + fn to_state_proto(&self, cx: &AppContext) -> Option { let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); Some(proto::view::Variant::Editor(proto::view::Editor { buffer_id, @@ -118,25 +118,38 @@ impl FollowableItem for Editor { })) } - fn to_update_message( + fn add_event_to_update_proto( &self, event: &Self::Event, + update: &mut Option, _: &AppContext, - ) -> Option { - match event { - Event::ScrollPositionChanged { .. } | Event::SelectionsChanged { .. } => { - Some(update_view::Variant::Editor(update_view::Editor { - scroll_top: Some(language::proto::serialize_anchor( + ) -> bool { + let update = + update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default())); + + match update { + proto::update_view::Variant::Editor(update) => match event { + Event::ScrollPositionChanged { .. } => { + update.scroll_top = Some(language::proto::serialize_anchor( &self.scroll_top_anchor.text_anchor, - )), - selections: self.selections.iter().map(serialize_selection).collect(), - })) - } - _ => None, + )); + true + } + Event::SelectionsChanged { .. } => { + update.selections = self + .selections + .iter() + .chain(self.pending_selection.as_ref().map(|p| &p.selection)) + .map(serialize_selection) + .collect(); + true + } + _ => false, + }, } } - fn apply_update_message( + fn apply_update_proto( &mut self, message: update_view::Variant, cx: &mut ViewContext, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 79fb49da809f6d0b36fc51a336cdd3c8fb9aa4b4..f8ed78a13f58ae9004122aeb8cff15b30215b2f0 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4349,9 +4349,12 @@ mod tests { .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") .await; + eprintln!("=========================>>>>>>>>"); editor_a1.update(cx_a, |editor, cx| { editor.select_ranges([3..3], None, cx); + editor.set_scroll_position(vec2f(0., 100.), cx); }); + eprintln!("=========================<<<<<<<<<"); editor_b1 .condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3]) .await; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8296d10f4a8364656d8c80f6654b73cbf10b9a53..55338130dab81644332e2112a24afbab038c38c4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -41,7 +41,10 @@ use std::{ future::Future, path::{Path, PathBuf}, rc::Rc, - sync::Arc, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, }; use theme::{Theme, ThemeRegistry}; use util::ResultExt; @@ -151,7 +154,7 @@ pub fn register_followable_item(cx: &mut MutableAppContext) { TypeId::of::(), ( |pane, project, state, cx| { - I::for_state_message(pane, project, state, cx).map(|task| { + I::from_state_proto(pane, project, state, cx).map(|task| { cx.foreground() .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) }) @@ -255,36 +258,39 @@ pub trait ProjectItem: Item { } pub trait FollowableItem: Item { - fn for_state_message( + fn to_state_proto(&self, cx: &AppContext) -> Option; + fn from_state_proto( pane: ViewHandle, project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; - fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); - fn to_state_message(&self, cx: &AppContext) -> Option; - fn to_update_message( + fn add_event_to_update_proto( &self, event: &Self::Event, + update: &mut Option, cx: &AppContext, - ) -> Option; - fn apply_update_message( + ) -> bool; + fn apply_update_proto( &mut self, message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Result<()>; + + fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; } pub trait FollowableItemHandle: ItemHandle { fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext); - fn to_state_message(&self, cx: &AppContext) -> Option; - fn to_update_message( + fn to_state_proto(&self, cx: &AppContext) -> Option; + fn add_event_to_update_proto( &self, event: &dyn Any, + update: &mut Option, cx: &AppContext, - ) -> Option; - fn apply_update_message( + ) -> bool; + fn apply_update_proto( &self, message: proto::update_view::Variant, cx: &mut MutableAppContext, @@ -299,24 +305,29 @@ impl FollowableItemHandle for ViewHandle { }) } - fn to_state_message(&self, cx: &AppContext) -> Option { - self.read(cx).to_state_message(cx) + fn to_state_proto(&self, cx: &AppContext) -> Option { + self.read(cx).to_state_proto(cx) } - fn to_update_message( + fn add_event_to_update_proto( &self, event: &dyn Any, + update: &mut Option, cx: &AppContext, - ) -> Option { - self.read(cx).to_update_message(event.downcast_ref()?, cx) + ) -> bool { + if let Some(event) = event.downcast_ref() { + self.read(cx).add_event_to_update_proto(event, update, cx) + } else { + false + } } - fn apply_update_message( + fn apply_update_proto( &self, message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()> { - self.update(cx, |this, cx| this.apply_update_message(message, cx)) + self.update(cx, |this, cx| this.apply_update_proto(message, cx)) } fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { @@ -413,7 +424,7 @@ impl ItemHandle for ViewHandle { cx: &mut ViewContext, ) { if let Some(followed_item) = self.to_followable_item_handle(cx) { - if let Some(message) = followed_item.to_state_message(cx) { + if let Some(message) = followed_item.to_state_proto(cx) { workspace.update_followers( proto::update_followers::Variant::CreateView(proto::View { id: followed_item.id() as u64, @@ -425,6 +436,8 @@ impl ItemHandle for ViewHandle { } } + let pending_update = Rc::new(RefCell::new(None)); + let pending_update_scheduled = Rc::new(AtomicBool::new(false)); let pane = pane.downgrade(); cx.subscribe(self, move |workspace, item, event, cx| { let pane = if let Some(pane) = pane.upgrade(cx) { @@ -435,9 +448,37 @@ impl ItemHandle for ViewHandle { }; if let Some(item) = item.to_followable_item_handle(cx) { - if item.should_unfollow_on_event(event, cx) { + let leader_id = workspace.leader_for_pane(&pane); + + if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { workspace.unfollow(&pane, cx); } + + if item.add_event_to_update_proto(event, &mut *pending_update.borrow_mut(), cx) + && !pending_update_scheduled.load(SeqCst) + { + pending_update_scheduled.store(true, SeqCst); + cx.spawn({ + let pending_update = pending_update.clone(); + let pending_update_scheduled = pending_update_scheduled.clone(); + move |this, mut cx| async move { + this.update(&mut cx, |this, cx| { + pending_update_scheduled.store(false, SeqCst); + this.update_followers( + proto::update_followers::Variant::UpdateView( + proto::UpdateView { + id: item.id() as u64, + variant: pending_update.borrow_mut().take(), + leader_id: leader_id.map(|id| id.0), + }, + ), + cx, + ); + }); + } + }) + .detach(); + } } if T::should_close_item_on_event(event) { @@ -457,20 +498,6 @@ impl ItemHandle for ViewHandle { if T::should_update_tab_on_event(event) { pane.update(cx, |_, cx| cx.notify()); } - - if let Some(message) = item - .to_followable_item_handle(cx) - .and_then(|i| i.to_update_message(event, cx)) - { - workspace.update_followers( - proto::update_followers::Variant::UpdateView(proto::UpdateView { - id: item.id() as u64, - variant: Some(message), - leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), - }), - cx, - ); - } }) .detach(); } @@ -1621,7 +1648,7 @@ impl Workspace { move |item| { let id = item.id() as u64; let item = item.to_followable_item_handle(cx)?; - let variant = item.to_state_message(cx)?; + let variant = item.to_state_proto(cx)?; Some(proto::View { id, leader_id, @@ -1682,7 +1709,7 @@ impl Workspace { .or_insert(FollowerItem::Loading(Vec::new())) { FollowerItem::Loaded(item) => { - item.apply_update_message(variant, cx).log_err(); + item.apply_update_proto(variant, cx).log_err(); } FollowerItem::Loading(updates) => updates.push(variant), } @@ -1774,7 +1801,7 @@ impl Workspace { let e = e.into_mut(); if let FollowerItem::Loading(updates) = e { for update in updates.drain(..) { - item.apply_update_message(update, cx) + item.apply_update_proto(update, cx) .context("failed to apply view update") .log_err(); } From 880eaa268b20cca964c39329b0b83a94890b4449 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 17:03:24 -0700 Subject: [PATCH 47/56] Coalesce followed view updates only within one frame Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 113 +++++++++++++++++++++++++++--- crates/workspace/src/workspace.rs | 29 ++++---- 2 files changed, 115 insertions(+), 27 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 1e4448de98ef5017845c0d552b20778f456c6a89..d5a7fcc6e6fc03830c1ed5da925edb3f2da79a33 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1224,7 +1224,17 @@ impl MutableAppContext { } fn defer(&mut self, callback: Box) { - self.pending_effects.push_back(Effect::Deferred(callback)) + self.pending_effects.push_back(Effect::Deferred { + callback, + after_window_update: false, + }) + } + + pub fn after_window_update(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) { + self.pending_effects.push_back(Effect::Deferred { + callback: Box::new(callback), + after_window_update: true, + }) } pub(crate) fn notify_model(&mut self, model_id: usize) { @@ -1640,6 +1650,7 @@ impl MutableAppContext { fn flush_effects(&mut self) { self.pending_flushes = self.pending_flushes.saturating_sub(1); + let mut after_window_update_callbacks = Vec::new(); if !self.flushing_effects && self.pending_flushes == 0 { self.flushing_effects = true; @@ -1675,7 +1686,16 @@ impl MutableAppContext { Effect::ViewNotification { window_id, view_id } => { self.notify_view_observers(window_id, view_id) } - Effect::Deferred(callback) => callback(self), + Effect::Deferred { + callback, + after_window_update, + } => { + if after_window_update { + after_window_update_callbacks.push(callback); + } else { + callback(self) + } + } Effect::ModelRelease { model_id, model } => { self.notify_release_observers(model_id, model.as_any()) } @@ -1707,12 +1727,18 @@ impl MutableAppContext { } if self.pending_effects.is_empty() { - self.flushing_effects = false; - self.pending_notifications.clear(); - break; - } else { - refreshing = false; + for callback in after_window_update_callbacks.drain(..) { + callback(self); + } + + if self.pending_effects.is_empty() { + self.flushing_effects = false; + self.pending_notifications.clear(); + break; + } } + + refreshing = false; } } } @@ -2347,7 +2373,10 @@ pub enum Effect { window_id: usize, view_id: usize, }, - Deferred(Box), + Deferred { + callback: Box, + after_window_update: bool, + }, ModelRelease { model_id: usize, model: Box, @@ -2413,7 +2442,7 @@ impl Debug for Effect { .field("window_id", window_id) .field("view_id", view_id) .finish(), - Effect::Deferred(_) => f.debug_struct("Effect::Deferred").finish(), + Effect::Deferred { .. } => f.debug_struct("Effect::Deferred").finish(), Effect::ModelRelease { model_id, .. } => f .debug_struct("Effect::ModelRelease") .field("model_id", model_id) @@ -2945,6 +2974,18 @@ impl<'a, T: View> ViewContext<'a, T> { })) } + pub fn after_window_update( + &mut self, + callback: impl 'static + FnOnce(&mut T, &mut ViewContext), + ) { + let handle = self.handle(); + self.app.after_window_update(move |cx| { + handle.update(cx, |view, cx| { + callback(view, cx); + }) + }) + } + pub fn propagate_action(&mut self) { self.app.halt_action_dispatch = false; } @@ -4424,7 +4465,7 @@ mod tests { use smol::future::poll_once; use std::{ cell::Cell, - sync::atomic::{AtomicUsize, Ordering::SeqCst}, + sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, }; #[crate::test(self)] @@ -4622,6 +4663,58 @@ mod tests { assert_eq!(*events.borrow(), ["before notify"]); } + #[crate::test(self)] + fn test_defer_and_after_window_update(cx: &mut MutableAppContext) { + struct View { + render_count: usize, + } + + impl Entity for View { + type Event = usize; + } + + impl super::View for View { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + post_inc(&mut self.render_count); + Empty::new().boxed() + } + + fn ui_name() -> &'static str { + "View" + } + } + + let (_, view) = cx.add_window(Default::default(), |_| View { render_count: 0 }); + let called_defer = Rc::new(AtomicBool::new(false)); + let called_after_window_update = Rc::new(AtomicBool::new(false)); + + view.update(cx, |this, cx| { + assert_eq!(this.render_count, 1); + cx.defer({ + let called_defer = called_defer.clone(); + move |this, _| { + assert_eq!(this.render_count, 1); + called_defer.store(true, SeqCst); + } + }); + cx.after_window_update({ + let called_after_window_update = called_after_window_update.clone(); + move |this, cx| { + assert_eq!(this.render_count, 2); + called_after_window_update.store(true, SeqCst); + cx.notify(); + } + }); + assert!(!called_defer.load(SeqCst)); + assert!(!called_after_window_update.load(SeqCst)); + cx.notify(); + }); + + assert!(called_defer.load(SeqCst)); + assert!(called_after_window_update.load(SeqCst)); + assert_eq!(view.read(cx).render_count, 3); + } + #[crate::test(self)] fn test_view_handles(cx: &mut MutableAppContext) { struct View { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 55338130dab81644332e2112a24afbab038c38c4..2d8891b1e05f44abed00c49cc00c39e0d849d343 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -458,26 +458,21 @@ impl ItemHandle for ViewHandle { && !pending_update_scheduled.load(SeqCst) { pending_update_scheduled.store(true, SeqCst); - cx.spawn({ + cx.after_window_update({ let pending_update = pending_update.clone(); let pending_update_scheduled = pending_update_scheduled.clone(); - move |this, mut cx| async move { - this.update(&mut cx, |this, cx| { - pending_update_scheduled.store(false, SeqCst); - this.update_followers( - proto::update_followers::Variant::UpdateView( - proto::UpdateView { - id: item.id() as u64, - variant: pending_update.borrow_mut().take(), - leader_id: leader_id.map(|id| id.0), - }, - ), - cx, - ); - }); + move |this, cx| { + pending_update_scheduled.store(false, SeqCst); + this.update_followers( + proto::update_followers::Variant::UpdateView(proto::UpdateView { + id: item.id() as u64, + variant: pending_update.borrow_mut().take(), + leader_id: leader_id.map(|id| id.0), + }), + cx, + ); } - }) - .detach(); + }); } } From 8699dd9c56ee3d0090be8cc158340855515655c0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 17:20:13 -0700 Subject: [PATCH 48/56] Replicate fractional component of leader's scroll position Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 11 ++++++++--- crates/editor/src/items.rs | 20 ++++++++++++-------- crates/rpc/proto/zed.proto | 8 ++++++-- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d1890e04d488f41c70d51b4238fbbb81bc2eb10..3d763478e5f21073cfdb18a76a6c96b049427481 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1039,10 +1039,15 @@ impl Editor { cx.notify(); } - fn set_scroll_top_anchor(&mut self, anchor: Anchor, local: bool, cx: &mut ViewContext) { - self.scroll_position = Vector2F::zero(); + fn set_scroll_top_anchor( + &mut self, + anchor: Anchor, + position: Vector2F, + cx: &mut ViewContext, + ) { self.scroll_top_anchor = anchor; - cx.emit(Event::ScrollPositionChanged { local }); + self.scroll_position = position; + cx.emit(Event::ScrollPositionChanged { local: false }); cx.notify(); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3cac47bf5912d8c969b15c9302c46e248aa1c339..ab5359600d980576d8f3ef9fd249a7c2df78478e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,8 +1,8 @@ use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _}; use anyhow::{anyhow, Result}; use gpui::{ - elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, - Task, View, ViewContext, ViewHandle, + elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, + RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal}; use project::{File, Project, ProjectEntryId, ProjectPath}; @@ -68,7 +68,7 @@ impl FollowableItem for Editor { editor.set_selections(selections.into(), None, false, cx); } - if let Some(anchor) = state.scroll_top { + if let Some(anchor) = state.scroll_top_anchor { editor.set_scroll_top_anchor( Anchor { buffer_id: Some(state.buffer_id as usize), @@ -76,7 +76,7 @@ impl FollowableItem for Editor { text_anchor: language::proto::deserialize_anchor(anchor) .ok_or_else(|| anyhow!("invalid scroll top"))?, }, - false, + vec2f(state.scroll_x, state.scroll_y), cx, ); } @@ -111,9 +111,11 @@ impl FollowableItem for Editor { let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); Some(proto::view::Variant::Editor(proto::view::Editor { buffer_id, - scroll_top: Some(language::proto::serialize_anchor( + scroll_top_anchor: Some(language::proto::serialize_anchor( &self.scroll_top_anchor.text_anchor, )), + scroll_x: self.scroll_position.x(), + scroll_y: self.scroll_position.y(), selections: self.selections.iter().map(serialize_selection).collect(), })) } @@ -130,9 +132,11 @@ impl FollowableItem for Editor { match update { proto::update_view::Variant::Editor(update) => match event { Event::ScrollPositionChanged { .. } => { - update.scroll_top = Some(language::proto::serialize_anchor( + update.scroll_top_anchor = Some(language::proto::serialize_anchor( &self.scroll_top_anchor.text_anchor, )); + update.scroll_x = self.scroll_position.x(); + update.scroll_y = self.scroll_position.y(); true } Event::SelectionsChanged { .. } => { @@ -162,7 +166,7 @@ impl FollowableItem for Editor { let excerpt_id = excerpt_id.clone(); drop(buffer); - if let Some(anchor) = message.scroll_top { + if let Some(anchor) = message.scroll_top_anchor { self.set_scroll_top_anchor( Anchor { buffer_id: Some(buffer_id), @@ -170,7 +174,7 @@ impl FollowableItem for Editor { text_anchor: language::proto::deserialize_anchor(anchor) .ok_or_else(|| anyhow!("invalid scroll top"))?, }, - false, + vec2f(message.scroll_x, message.scroll_y), cx, ); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 487c7e01a5a389689becbf7819572ba16566031e..9d25e66190b14bc7d4624885ec7da3dc57acb61b 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -580,7 +580,9 @@ message UpdateView { message Editor { repeated Selection selections = 1; - Anchor scroll_top = 2; + Anchor scroll_top_anchor = 2; + float scroll_x = 3; + float scroll_y = 4; } } @@ -595,7 +597,9 @@ message View { message Editor { uint64 buffer_id = 1; repeated Selection selections = 2; - Anchor scroll_top = 3; + Anchor scroll_top_anchor = 3; + float scroll_x = 4; + float scroll_y = 5; } } From fad299eb3f71dccfb377ba695254e2af202137a9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 17:39:34 -0700 Subject: [PATCH 49/56] Add unit test for editor's following methods Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3d763478e5f21073cfdb18a76a6c96b049427481..7ec32e4714346f90cc079c17f7c95feb598d7865 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6093,6 +6093,10 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { use super::*; + use gpui::{ + geometry::rect::RectF, + platform::{WindowBounds, WindowOptions}, + }; use language::{LanguageConfig, LanguageServerConfig}; use lsp::FakeLanguageServer; use project::FakeFs; @@ -6101,6 +6105,7 @@ mod tests { use text::Point; use unindent::Unindent; use util::test::sample_text; + use workspace::FollowableItem; #[gpui::test] fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { @@ -9035,6 +9040,47 @@ mod tests { }); } + #[gpui::test] + fn test_following(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + populate_settings(cx); + + let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); + let (_, follower) = cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), + ..Default::default() + }, + |cx| build_editor(buffer.clone(), cx), + ); + + follower.update(cx, |_, cx| { + cx.subscribe(&leader, |follower, leader, event, cx| { + let mut update = None; + leader + .read(cx) + .add_event_to_update_proto(event, &mut update, cx); + if let Some(update) = update { + follower.apply_update_proto(update, cx).unwrap(); + } + }) + .detach(); + }); + + leader.update(cx, |leader, cx| { + leader.select_ranges([1..1], None, cx); + }); + assert_eq!(follower.read(cx).selected_ranges(cx), vec![1..1]); + + leader.update(cx, |leader, cx| { + leader.set_scroll_position(vec2f(1.5, 3.5), cx); + }); + assert_eq!( + follower.update(cx, |follower, cx| follower.scroll_position(cx)), + vec2f(1.5, 3.5) + ); + } + #[test] fn test_combine_syntax_and_fuzzy_match_highlights() { let string = "abcdefghijklmnop"; From fa62fd968f2b82c57582fb5edc8cb4505f73b600 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 18:02:54 -0700 Subject: [PATCH 50/56] Autoscroll when leader moves cursors instead of copying their scroll top. Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 78 ++++++++++++++++++++++++++++--------- crates/editor/src/items.rs | 27 ++++++------- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7ec32e4714346f90cc079c17f7c95feb598d7865..e1bba12b420dfe1d64d050988906ad30d5b1d960 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -439,7 +439,7 @@ pub struct Editor { active_diagnostics: Option, scroll_position: Vector2F, scroll_top_anchor: Anchor, - autoscroll_request: Option, + autoscroll_request: Option<(Autoscroll, bool)>, soft_wrap_mode_override: Option, get_field_editor_theme: Option, override_text_style: Option>, @@ -1017,6 +1017,15 @@ impl Editor { } pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { + self.set_scroll_position_internal(scroll_position, true, cx); + } + + fn set_scroll_position_internal( + &mut self, + scroll_position: Vector2F, + local: bool, + cx: &mut ViewContext, + ) { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); if scroll_position.y() == 0. { @@ -1035,7 +1044,7 @@ impl Editor { self.scroll_top_anchor = anchor; } - cx.emit(Event::ScrollPositionChanged { local: true }); + cx.emit(Event::ScrollPositionChanged { local }); cx.notify(); } @@ -1090,7 +1099,7 @@ impl Editor { self.set_scroll_position(scroll_position, cx); } - let autoscroll = if let Some(autoscroll) = self.autoscroll_request.take() { + let (autoscroll, local) = if let Some(autoscroll) = self.autoscroll_request.take() { autoscroll } else { return false; @@ -1142,15 +1151,15 @@ impl Editor { if target_top < start_row { scroll_position.set_y(target_top); - self.set_scroll_position(scroll_position, cx); + self.set_scroll_position_internal(scroll_position, local, cx); } else if target_bottom >= end_row { scroll_position.set_y(target_bottom - visible_lines); - self.set_scroll_position(scroll_position, cx); + self.set_scroll_position_internal(scroll_position, local, cx); } } Autoscroll::Center => { scroll_position.set_y((first_cursor_top - margin).max(0.0)); - self.set_scroll_position(scroll_position, cx); + self.set_scroll_position_internal(scroll_position, local, cx); } } @@ -5111,7 +5120,12 @@ impl Editor { } pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { - self.autoscroll_request = Some(autoscroll); + self.autoscroll_request = Some((autoscroll, true)); + cx.notify(); + } + + fn request_autoscroll_remotely(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { + self.autoscroll_request = Some((autoscroll, false)); cx.notify(); } @@ -9054,31 +9068,59 @@ mod tests { |cx| build_editor(buffer.clone(), cx), ); - follower.update(cx, |_, cx| { - cx.subscribe(&leader, |follower, leader, event, cx| { - let mut update = None; - leader - .read(cx) - .add_event_to_update_proto(event, &mut update, cx); - if let Some(update) = update { - follower.apply_update_proto(update, cx).unwrap(); - } - }) - .detach(); + let pending_update = Rc::new(RefCell::new(None)); + follower.update(cx, { + let update = pending_update.clone(); + |_, cx| { + cx.subscribe(&leader, move |_, leader, event, cx| { + leader + .read(cx) + .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + }) + .detach(); + } }); + // Update the selections only leader.update(cx, |leader, cx| { leader.select_ranges([1..1], None, cx); }); + follower.update(cx, |follower, cx| { + follower + .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .unwrap(); + }); assert_eq!(follower.read(cx).selected_ranges(cx), vec![1..1]); + // Update the scroll position only leader.update(cx, |leader, cx| { leader.set_scroll_position(vec2f(1.5, 3.5), cx); }); + follower.update(cx, |follower, cx| { + follower + .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .unwrap(); + }); assert_eq!( follower.update(cx, |follower, cx| follower.scroll_position(cx)), vec2f(1.5, 3.5) ); + + // Update the selections and scroll position + leader.update(cx, |leader, cx| { + leader.select_ranges([0..0], None, cx); + leader.request_autoscroll(Autoscroll::Newest, cx); + leader.set_scroll_position(vec2f(1.5, 3.5), cx); + }); + follower.update(cx, |follower, cx| { + let initial_scroll_position = follower.scroll_position(cx); + follower + .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .unwrap(); + assert_eq!(follower.scroll_position(cx), initial_scroll_position); + assert!(follower.autoscroll_request.is_some()); + }); + assert_eq!(follower.read(cx).selected_ranges(cx), vec![0..0]); } #[test] diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index ab5359600d980576d8f3ef9fd249a7c2df78478e..5971cbc07bb5dfbb87cf826418e546551d60637c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -166,19 +166,6 @@ impl FollowableItem for Editor { let excerpt_id = excerpt_id.clone(); drop(buffer); - if let Some(anchor) = message.scroll_top_anchor { - self.set_scroll_top_anchor( - Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: language::proto::deserialize_anchor(anchor) - .ok_or_else(|| anyhow!("invalid scroll top"))?, - }, - vec2f(message.scroll_x, message.scroll_y), - cx, - ); - } - let selections = message .selections .into_iter() @@ -188,6 +175,20 @@ impl FollowableItem for Editor { .collect::>(); if !selections.is_empty() { self.set_selections(selections.into(), None, false, cx); + self.request_autoscroll_remotely(Autoscroll::Newest, cx); + } else { + if let Some(anchor) = message.scroll_top_anchor { + self.set_scroll_top_anchor( + Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: language::proto::deserialize_anchor(anchor) + .ok_or_else(|| anyhow!("invalid scroll top"))?, + }, + vec2f(message.scroll_x, message.scroll_y), + cx, + ); + } } } } From 3298529ed1fa2cca864a4b6bc77cf8937257bff4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 09:14:33 +0100 Subject: [PATCH 51/56] Fix global nested event test after turning subscriptions into effects --- crates/gpui/src/app.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index d5a7fcc6e6fc03830c1ed5da925edb3f2da79a33..43a2c49263f7337128430b451def87876d389613 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -5056,11 +5056,13 @@ mod tests { cx.subscribe_global(move |e: &GlobalEvent, cx| { events.borrow_mut().push(("Outer", e.clone())); - let events = events.clone(); - cx.subscribe_global(move |e: &GlobalEvent, _| { - events.borrow_mut().push(("Inner", e.clone())); - }) - .detach(); + if e.0 == 1 { + let events = events.clone(); + cx.subscribe_global(move |e: &GlobalEvent, _| { + events.borrow_mut().push(("Inner", e.clone())); + }) + .detach(); + } }) .detach(); } @@ -5070,16 +5072,18 @@ mod tests { cx.emit_global(GlobalEvent(2)); cx.emit_global(GlobalEvent(3)); }); + cx.update(|cx| { + cx.emit_global(GlobalEvent(4)); + }); assert_eq!( &*events.borrow(), &[ ("Outer", GlobalEvent(1)), ("Outer", GlobalEvent(2)), - ("Inner", GlobalEvent(2)), ("Outer", GlobalEvent(3)), - ("Inner", GlobalEvent(3)), - ("Inner", GlobalEvent(3)), + ("Outer", GlobalEvent(4)), + ("Inner", GlobalEvent(4)), ] ); } From 097bbe3e077275bf3f0bfe441beab0516aee2615 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 23 Mar 2022 06:19:35 -0600 Subject: [PATCH 52/56] Update follow binding, remove unfollow binding The previous binding to follow had ergonomics issues for the frequency that I think we'll want to use it. It would also conflict with the sub-word selection binding. Now that moving the cursor etc unfollows, I don't think we need the follow binding. --- crates/workspace/src/workspace.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2d8891b1e05f44abed00c49cc00c39e0d849d343..b24e316b55fdfca8f6200a40d6d5ab621db6c547 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -112,8 +112,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::toggle_sidebar_item_focus); cx.add_bindings(vec![ - Binding::new("cmd-alt-shift-F", FollowNextCollaborator, None), - Binding::new("cmd-alt-shift-U", Unfollow, None), + Binding::new("ctrl-alt-cmd-f", FollowNextCollaborator, None), Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), Binding::new( From edc038a1cf9c49b9509648d7726fd5a252d2be0f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 14:26:00 +0100 Subject: [PATCH 53/56] Activate previous pane and next pane via `cmd-k cmd-left` and `cmd-k cmd-right` Co-Authored-By: Nathan Sobo --- crates/workspace/src/workspace.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b24e316b55fdfca8f6200a40d6d5ab621db6c547..ed822a348758ec29c37459776e26f1b4b0c1720b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -78,6 +78,8 @@ action!(Unfollow); action!(JoinProject, JoinProjectParams); action!(Save); action!(DebugElements); +action!(ActivatePreviousPane); +action!(ActivateNextPane); pub fn init(client: &Arc, cx: &mut MutableAppContext) { pane::init(cx); @@ -111,10 +113,18 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::debug_elements); cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::toggle_sidebar_item_focus); + cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { + workspace.activate_previous_pane(cx) + }); + cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { + workspace.activate_next_pane(cx) + }); cx.add_bindings(vec![ Binding::new("ctrl-alt-cmd-f", FollowNextCollaborator, None), Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), + Binding::new("cmd-k cmd-left", ActivatePreviousPane, None), + Binding::new("cmd-k cmd-right", ActivateNextPane, None), Binding::new( "cmd-shift-!", ToggleSidebarItem(SidebarItemId { @@ -1159,6 +1169,20 @@ impl Workspace { self.activate_pane(self.panes[next_ix].clone(), cx); } + pub fn activate_previous_pane(&mut self, cx: &mut ViewContext) { + let ix = self + .panes + .iter() + .position(|pane| pane == &self.active_pane) + .unwrap(); + let prev_ix = if ix == 0 { + self.panes.len() - 1 + } else { + ix - 1 + }; + self.activate_pane(self.panes[prev_ix].clone(), cx); + } + fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.active_pane != pane { self.active_pane = pane.clone(); From 4f27049305491f2403aefde1e3fb1e6dd8e1e717 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 14:33:22 +0100 Subject: [PATCH 54/56] Focus followed items when they become active if the pane is active Co-Authored-By: Nathan Sobo --- crates/server/src/rpc.rs | 3 +-- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index f8ed78a13f58ae9004122aeb8cff15b30215b2f0..1020fd686a2c38fb7ca0f77ffabd81d21d6c5ada 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4311,6 +4311,7 @@ mod tests { .downcast::() .unwrap() }); + assert!(cx_b.read(|cx| editor_b2.is_focused(cx))); assert_eq!( editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)), Some((worktree_id, "2.txt").into()) @@ -4349,12 +4350,10 @@ mod tests { .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") .await; - eprintln!("=========================>>>>>>>>"); editor_a1.update(cx_a, |editor, cx| { editor.select_ranges([3..3], None, cx); editor.set_scroll_position(vec2f(0., 100.), cx); }); - eprintln!("=========================<<<<<<<<<"); editor_b1 .condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3]) .await; diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 9903d414e411b03b3f268e3775266a076b4b06b0..e04af153ba1d76e6ca48855ea98c397da7bc6ebf 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -485,7 +485,7 @@ impl Pane { cx.notify(); } - fn focus_active_item(&mut self, cx: &mut ViewContext) { + pub fn focus_active_item(&mut self, cx: &mut ViewContext) { if let Some(active_item) = self.active_item() { cx.focus(active_item); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ed822a348758ec29c37459776e26f1b4b0c1720b..19208294d75630901ec502bbd06ebcf5b3b1ee91 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1902,6 +1902,9 @@ impl Workspace { for (pane, item) in items_to_add { Pane::add_item(self, pane.clone(), item.boxed_clone(), false, cx); + if pane == self.active_pane { + pane.update(cx, |pane, cx| pane.focus_active_item(cx)); + } cx.notify(); } None From 5ac39aa7cd925e131f93585ef3e43ad6067d40b9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 14:46:33 +0100 Subject: [PATCH 55/56] Don't show local cursors when editor is not focused Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e1bba12b420dfe1d64d050988906ad30d5b1d960..6d4785ccade91dbcf4f58135cdfafcdc6d2557c5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5520,7 +5520,7 @@ impl Editor { } pub fn show_local_cursors(&self) -> bool { - self.show_local_cursors + self.show_local_cursors && self.focused } fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { From 60b6b0b317df55d16c6496239321eca999d5aa15 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 15:06:25 +0100 Subject: [PATCH 56/56] Cycle through panes spatially rather than in the order in which they created Co-Authored-By: Nathan Sobo --- crates/workspace/src/pane_group.rs | 17 ++++++++++++++ crates/workspace/src/workspace.rs | 36 ++++++++++++++++-------------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index d34613df4ac19633a9659d59b33580f38d4825ad..afffec507452ee00bcf4d185abb92ef09a92a9d7 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -57,6 +57,12 @@ impl PaneGroup { ) -> ElementBox { self.root.render(theme, follower_states, collaborators) } + + pub(crate) fn panes(&self) -> Vec<&ViewHandle> { + let mut panes = Vec::new(); + self.root.collect_panes(&mut panes); + panes + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -122,6 +128,17 @@ impl Member { Member::Axis(axis) => axis.render(theme, follower_states, collaborators), } } + + fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle>) { + match self { + Member::Axis(axis) => { + for member in &axis.members { + member.collect_panes(panes); + } + } + Member::Pane(pane) => panes.push(pane), + } + } } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 19208294d75630901ec502bbd06ebcf5b3b1ee91..691f17ca559deee7eb66c8f4c1f3b3bd0d4a6423 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1160,27 +1160,29 @@ impl Workspace { } pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { - let ix = self - .panes - .iter() - .position(|pane| pane == &self.active_pane) - .unwrap(); - let next_ix = (ix + 1) % self.panes.len(); - self.activate_pane(self.panes[next_ix].clone(), cx); + let next_pane = { + let panes = self.center.panes(); + let ix = panes + .iter() + .position(|pane| **pane == self.active_pane) + .unwrap(); + let next_ix = (ix + 1) % panes.len(); + panes[next_ix].clone() + }; + self.activate_pane(next_pane, cx); } pub fn activate_previous_pane(&mut self, cx: &mut ViewContext) { - let ix = self - .panes - .iter() - .position(|pane| pane == &self.active_pane) - .unwrap(); - let prev_ix = if ix == 0 { - self.panes.len() - 1 - } else { - ix - 1 + let prev_pane = { + let panes = self.center.panes(); + let ix = panes + .iter() + .position(|pane| **pane == self.active_pane) + .unwrap(); + let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 }; + panes[prev_ix].clone() }; - self.activate_pane(self.panes[prev_ix].clone(), cx); + self.activate_pane(prev_pane, cx); } fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) {