From 31175545685efe094e4b253b3280eae32c02f69d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 09:16:25 +0100 Subject: [PATCH] Automatically unfollow when editing, scrolling or changing selections --- crates/editor/src/editor.rs | 44 ++++-- crates/editor/src/items.rs | 18 ++- crates/file_finder/src/file_finder.rs | 2 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/language/src/buffer.rs | 19 +-- crates/language/src/tests.rs | 12 +- crates/outline/src/outline.rs | 2 +- crates/project/src/project.rs | 13 +- crates/project_symbols/src/project_symbols.rs | 2 +- crates/search/src/buffer_search.rs | 6 +- crates/search/src/project_search.rs | 2 +- crates/server/src/rpc.rs | 148 ++++++++++++++++-- crates/theme_selector/src/theme_selector.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- 14 files changed, 214 insertions(+), 60 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8380eea12d..bffc1a2186 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, cx: &mut ViewContext) { + fn set_scroll_top_anchor( + &mut self, + anchor: Option, + local: bool, + cx: &mut ViewContext, + ) { 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::(&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) { 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) { 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]>, pending_selection: Option, + local: bool, cx: &mut ViewContext, ) { 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) { @@ -5508,10 +5520,10 @@ impl Editor { cx: &mut ViewContext, ) { 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, } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d0457eba5a..eceaa88159 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -58,7 +58,7 @@ impl FollowableItem for Editor { .collect::>() }; 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 { 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::>(); 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, + } } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index ca41eb74a1..4656daa4b3 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -291,7 +291,7 @@ impl FileFinder { cx: &mut ViewContext, ) { 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); diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index f2dd4e76b1..ce8ba787a8 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -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::().ok()); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index fa4208ee5b..9da9e59e4c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -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 { 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, ) { if self.edits_since::(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); } } } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 6c9980b334..d36771c44f 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -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] diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index fd4c8ff60b..968fceb59c 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -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), _ => {} } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4a6f0dd6cf..5f9a63c034 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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())); diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 27e125a592..5eb04718d7 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -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), _ => {} } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 8eae666c45..13c73036f4 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -360,7 +360,7 @@ impl SearchBar { cx: &mut ViewContext, ) { 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, ) { 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), _ => {} } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index f027c965c6..1302040d19 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -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); } }) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index e0f4147faf..761b5737ce 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -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::() - .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::() + .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::() + .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(); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index f879940f21..ebdcc492a9 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -204,7 +204,7 @@ impl ThemeSelector { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { + editor::Event::Edited { .. } => { self.update_matches(cx); self.select_if_matching(&cx.global::().theme.name); self.show_selected_theme(cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5ce61824e3..cb9f7e7fed 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1750,7 +1750,7 @@ impl Workspace { None } - fn leader_for_pane(&self, pane: &ViewHandle) -> Option { + pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { self.follower_states_by_leader .iter() .find_map(|(leader_id, state)| {