Always mirror the leader's selections when following
This commit is contained in:
parent
c8f36af823
commit
1728551282
5 changed files with 103 additions and 52 deletions
|
@ -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()
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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>() {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue