Always mirror the leader's selections when following

This commit is contained in:
Max Brunsfeld 2022-03-21 21:47:24 -07:00
parent c8f36af823
commit 1728551282
5 changed files with 103 additions and 52 deletions

View file

@ -1568,7 +1568,7 @@ impl Editor {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>( pub fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
&self, &self,
cx: &mut MutableAppContext, cx: &AppContext,
) -> Vec<Range<D>> { ) -> Vec<Range<D>> {
self.local_selections::<D>(cx) self.local_selections::<D>(cx)
.iter() .iter()

View file

@ -939,8 +939,12 @@ impl Element for EditorElement {
*contains_non_empty_selection |= !is_empty; *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( selections.insert(
view.replica_id(cx), local_replica_id,
local_selections local_selections
.into_iter() .into_iter()
.map(|selection| crate::Selection { .map(|selection| crate::Selection {
@ -958,6 +962,11 @@ impl Element for EditorElement {
.buffer_snapshot .buffer_snapshot
.remote_selections_in_range(&(start_anchor..end_anchor)) .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 selections
.entry(replica_id) .entry(replica_id)
.or_insert(Vec::new()) .or_insert(Vec::new())

View file

@ -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 anyhow::{anyhow, Result};
use gpui::{ use gpui::{
elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription,
Task, View, ViewContext, ViewHandle, 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 project::{File, Project, ProjectEntryId, ProjectPath};
use rpc::proto::{self, update_view}; use rpc::proto::{self, update_view};
use std::{fmt::Write, path::PathBuf}; use std::{fmt::Write, path::PathBuf};
@ -44,7 +44,23 @@ impl FollowableItem for Editor {
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| {
cx.add_view(pane.window_id(), |cx| { 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::<Vec<_>>()
};
if !selections.is_empty() {
editor.set_selections(selections.into(), None, cx);
}
editor
}) })
})) }))
})) }))
@ -55,33 +71,12 @@ impl FollowableItem for Editor {
leader_replica_id: Option<u16>, leader_replica_id: Option<u16>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
let prev_leader_replica_id = self.leader_replica_id;
self.leader_replica_id = leader_replica_id; self.leader_replica_id = leader_replica_id;
if self.leader_replica_id.is_some() { if self.leader_replica_id.is_some() {
self.show_local_selections = false;
self.buffer.update(cx, |buffer, cx| { self.buffer.update(cx, |buffer, cx| {
buffer.remove_active_selections(cx); buffer.remove_active_selections(cx);
}); });
} else { } 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::<Vec<_>>();
if !selections.is_empty() {
self.set_selections(selections.into(), None, cx);
}
}
self.buffer.update(cx, |buffer, cx| { self.buffer.update(cx, |buffer, cx| {
if self.focused { if self.focused {
buffer.set_active_selections(&self.selections, cx); buffer.set_active_selections(&self.selections, cx);
@ -99,6 +94,7 @@ impl FollowableItem for Editor {
.scroll_top_anchor .scroll_top_anchor
.as_ref() .as_ref()
.map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), .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, _: &AppContext,
) -> Option<update_view::Variant> { ) -> Option<update_view::Variant> {
match event { match event {
Event::ScrollPositionChanged => { Event::ScrollPositionChanged | Event::SelectionsChanged => {
Some(update_view::Variant::Editor(update_view::Editor { Some(update_view::Variant::Editor(update_view::Editor {
scroll_top: self scroll_top: self
.scroll_top_anchor .scroll_top_anchor
.as_ref() .as_ref()
.map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)),
selections: self.selections.iter().map(serialize_selection).collect(),
})) }))
} }
_ => None, _ => None,
@ -127,25 +124,77 @@ impl FollowableItem for Editor {
) -> Result<()> { ) -> Result<()> {
match message { match message {
update_view::Variant::Editor(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 { if let Some(anchor) = message.scroll_top {
let anchor = language::proto::deserialize_anchor(anchor) self.set_scroll_top_anchor(
.ok_or_else(|| anyhow!("invalid scroll top"))?; Some(Anchor {
let anchor = { buffer_id: Some(buffer_id),
let buffer = self.buffer.read(cx); excerpt_id: excerpt_id.clone(),
let buffer = buffer.read(cx); text_anchor: language::proto::deserialize_anchor(anchor)
let (excerpt_id, _, _) = buffer.as_singleton().unwrap(); .ok_or_else(|| anyhow!("invalid scroll top"))?,
buffer.anchor_in_excerpt(excerpt_id.clone(), anchor) }),
}; cx,
self.set_scroll_top_anchor(Some(anchor), cx); );
} else { } else {
self.set_scroll_top_anchor(None, cx); self.set_scroll_top_anchor(None, cx);
} }
let selections = message
.selections
.into_iter()
.filter_map(|selection| {
deserialize_selection(&excerpt_id, buffer_id, selection)
})
.collect::<Vec<_>>();
if !selections.is_empty() {
self.set_selections(selections.into(), None, cx);
}
} }
} }
Ok(()) Ok(())
} }
} }
fn serialize_selection(selection: &Selection<Anchor>) -> 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<Selection<Anchor>> {
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 { impl Item for Editor {
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) { fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
if let Some(data) = data.downcast_ref::<NavigationData>() { if let Some(data) = data.downcast_ref::<NavigationData>() {

View file

@ -579,7 +579,8 @@ message UpdateView {
} }
message Editor { message Editor {
Anchor scroll_top = 1; repeated Selection selections = 1;
Anchor scroll_top = 2;
} }
} }
@ -593,7 +594,8 @@ message View {
message Editor { message Editor {
uint64 buffer_id = 1; uint64 buffer_id = 1;
Anchor scroll_top = 2; repeated Selection selections = 2;
Anchor scroll_top = 3;
} }
} }

View file

@ -1083,8 +1083,8 @@ mod tests {
}; };
use collections::BTreeMap; use collections::BTreeMap;
use editor::{ use editor::{
self, Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
Rename, ToOffset, ToggleCodeActions, Undo, ToOffset, ToggleCodeActions, Undo,
}; };
use gpui::{executor, ModelHandle, TestAppContext, ViewHandle}; use gpui::{executor, ModelHandle, TestAppContext, ViewHandle};
use language::{ use language::{
@ -4324,18 +4324,13 @@ mod tests {
}) })
.await; .await;
// When client A selects something, client B does as well.
editor_a1.update(cx_a, |editor, cx| { 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 editor_b1
.condition(cx_b, |editor, cx| { .condition(cx_b, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx); editor.selected_ranges(cx) == vec![1..1, 2..2]
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; .await;
@ -4343,10 +4338,6 @@ mod tests {
workspace_b.update(cx_b, |workspace, cx| { workspace_b.update(cx_b, |workspace, cx| {
workspace.unfollow(&workspace.active_pane().clone(), cx) workspace.unfollow(&workspace.active_pane().clone(), cx)
}); });
editor_b1.update(cx_b, |editor, cx| {
assert_eq!(editor.selected_ranges::<usize>(cx), &[2..2]);
});
workspace_a.update(cx_a, |workspace, cx| { workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a2, cx); workspace.activate_item(&editor_a2, cx);
editor_a2.update(cx, |editor, cx| editor.set_text("TWO", cx)); editor_a2.update(cx, |editor, cx| editor.set_text("TWO", cx));