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));