diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index b56017c789..c2527ed94a 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 1ae9fdce27..00aa828325 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 522c490cfa..2c454dedb5 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 4a22d6ce5a..09d4236afe 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 4e2abbc2fe..4a6f0dd6cf 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 d7a928e424..f2197055f3 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 23f7e1c182..785de7ea08 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 4e7d6f5236..0c73c7388d 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 c3b6a5fd29..997aae3d96 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 25159fa689..eba7f12981 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(), + }) }) }