Automatically unfollow when editing, scrolling or changing selections

This commit is contained in:
Antonio Scandurra 2022-03-22 09:16:25 +01:00
parent c550fc3f01
commit 3117554568
14 changed files with 214 additions and 60 deletions

View file

@ -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<Anchor>, cx: &mut ViewContext<Self>) {
fn set_scroll_top_anchor(
&mut self,
anchor: Option<Anchor>,
local: bool,
cx: &mut ViewContext<Self>,
) {
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::<usize>(&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<Self>) {
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<Self>) {
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<Anchor>]>,
pending_selection: Option<PendingSelection>,
local: bool,
cx: &mut ViewContext<Self>,
) {
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<Self>) {
@ -5508,10 +5520,10 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
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,
}

View file

@ -58,7 +58,7 @@ impl FollowableItem for Editor {
.collect::<Vec<_>>()
};
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<update_view::Variant> {
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::<Vec<_>>();
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,
}
}
}

View file

@ -291,7 +291,7 @@ impl FileFinder {
cx: &mut ViewContext<Self>,
) {
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);

View file

@ -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::<u32>().ok());

View file

@ -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<TransactionId> {
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<Self>,
) {
if self.edits_since::<usize>(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);
}
}
}

View file

@ -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]

View file

@ -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),
_ => {}
}
}

View file

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

View file

@ -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),
_ => {}
}
}

View file

@ -360,7 +360,7 @@ impl SearchBar {
cx: &mut ViewContext<Self>,
) {
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<Self>,
) {
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),
_ => {}
}
}

View file

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

View file

@ -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::<Editor>()
.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::<Editor>()
.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::<Editor>()
.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();

View file

@ -204,7 +204,7 @@ impl ThemeSelector {
cx: &mut ViewContext<Self>,
) {
match event {
editor::Event::Edited => {
editor::Event::Edited { .. } => {
self.update_matches(cx);
self.select_if_matching(&cx.global::<Settings>().theme.name);
self.show_selected_theme(cx);

View file

@ -1750,7 +1750,7 @@ impl Workspace {
None
}
fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
self.follower_states_by_leader
.iter()
.find_map(|(leader_id, state)| {