Detailed changes
@@ -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,
}
@@ -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,
+ }
}
}
@@ -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);
@@ -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());
@@ -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);
}
}
}
@@ -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]
@@ -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),
_ => {}
}
}
@@ -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()));
@@ -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),
_ => {}
}
}
@@ -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),
_ => {}
}
}
@@ -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);
}
})
@@ -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();
@@ -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);
@@ -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)| {