1#![allow(clippy::reversed_empty_ranges)]
2use crate::tests::TestServer;
3use call::{ActiveCall, ParticipantLocation};
4use client::ChannelId;
5use collab_ui::{
6 channel_view::ChannelView,
7 notifications::project_shared_notification::ProjectSharedNotification,
8};
9use editor::{Editor, MultiBuffer, PathKey, SelectionEffects};
10use gpui::{
11 AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext,
12 VisualContext, VisualTestContext, point,
13};
14use language::Capability;
15use rpc::proto::PeerId;
16use serde_json::json;
17use settings::SettingsStore;
18use text::{Point, ToPoint};
19use util::{path, rel_path::rel_path, test::sample_text};
20use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _};
21
22use super::TestClient;
23
24#[gpui::test(iterations = 10)]
25async fn test_basic_following(
26 cx_a: &mut TestAppContext,
27 cx_b: &mut TestAppContext,
28 cx_c: &mut TestAppContext,
29 cx_d: &mut TestAppContext,
30) {
31 let executor = cx_a.executor();
32 let mut server = TestServer::start(executor.clone()).await;
33 let client_a = server.create_client(cx_a, "user_a").await;
34 let client_b = server.create_client(cx_b, "user_b").await;
35 let client_c = server.create_client(cx_c, "user_c").await;
36 let client_d = server.create_client(cx_d, "user_d").await;
37 server
38 .create_room(&mut [
39 (&client_a, cx_a),
40 (&client_b, cx_b),
41 (&client_c, cx_c),
42 (&client_d, cx_d),
43 ])
44 .await;
45 let active_call_a = cx_a.read(ActiveCall::global);
46 let active_call_b = cx_b.read(ActiveCall::global);
47
48 cx_a.update(editor::init);
49 cx_b.update(editor::init);
50
51 client_a
52 .fs()
53 .insert_tree(
54 path!("/a"),
55 json!({
56 "1.txt": "one\none\none",
57 "2.txt": "two\ntwo\ntwo",
58 "3.txt": "three\nthree\nthree",
59 }),
60 )
61 .await;
62 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
63 active_call_a
64 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
65 .await
66 .unwrap();
67
68 let project_id = active_call_a
69 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
70 .await
71 .unwrap();
72 let project_b = client_b.join_remote_project(project_id, cx_b).await;
73 active_call_b
74 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
75 .await
76 .unwrap();
77
78 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
79 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
80
81 cx_b.update(|window, _| {
82 assert!(window.is_window_active());
83 });
84
85 // Client A opens some editors.
86 let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
87 let editor_a1 = workspace_a
88 .update_in(cx_a, |workspace, window, cx| {
89 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
90 })
91 .await
92 .unwrap()
93 .downcast::<Editor>()
94 .unwrap();
95 let editor_a2 = workspace_a
96 .update_in(cx_a, |workspace, window, cx| {
97 workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
98 })
99 .await
100 .unwrap()
101 .downcast::<Editor>()
102 .unwrap();
103
104 // Client B opens an editor.
105 let editor_b1 = workspace_b
106 .update_in(cx_b, |workspace, window, cx| {
107 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
108 })
109 .await
110 .unwrap()
111 .downcast::<Editor>()
112 .unwrap();
113
114 let peer_id_a = client_a.peer_id().unwrap();
115 let peer_id_b = client_b.peer_id().unwrap();
116 let peer_id_c = client_c.peer_id().unwrap();
117 let peer_id_d = client_d.peer_id().unwrap();
118
119 // Client A updates their selections in those editors
120 editor_a1.update_in(cx_a, |editor, window, cx| {
121 editor.handle_input("a", window, cx);
122 editor.handle_input("b", window, cx);
123 editor.handle_input("c", window, cx);
124 editor.select_left(&Default::default(), window, cx);
125 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
126 });
127 editor_a2.update_in(cx_a, |editor, window, cx| {
128 editor.handle_input("d", window, cx);
129 editor.handle_input("e", window, cx);
130 editor.select_left(&Default::default(), window, cx);
131 assert_eq!(editor.selections.ranges(cx), vec![2..1]);
132 });
133
134 // When client B starts following client A, only the active view state is replicated to client B.
135 workspace_b.update_in(cx_b, |workspace, window, cx| {
136 workspace.follow(peer_id_a, window, cx)
137 });
138
139 cx_c.executor().run_until_parked();
140 let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
141 workspace
142 .active_item(cx)
143 .unwrap()
144 .downcast::<Editor>()
145 .unwrap()
146 });
147 assert_eq!(
148 cx_b.read(|cx| editor_b2.project_path(cx)),
149 Some((worktree_id, rel_path("2.txt")).into())
150 );
151 assert_eq!(
152 editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
153 vec![2..1]
154 );
155 assert_eq!(
156 editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
157 vec![3..3]
158 );
159
160 executor.run_until_parked();
161 let active_call_c = cx_c.read(ActiveCall::global);
162 let project_c = client_c.join_remote_project(project_id, cx_c).await;
163 let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c);
164 active_call_c
165 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
166 .await
167 .unwrap();
168 drop(project_c);
169
170 // Client C also follows client A.
171 workspace_c.update_in(cx_c, |workspace, window, cx| {
172 workspace.follow(peer_id_a, window, cx)
173 });
174
175 cx_d.executor().run_until_parked();
176 let active_call_d = cx_d.read(ActiveCall::global);
177 let project_d = client_d.join_remote_project(project_id, cx_d).await;
178 let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d);
179 active_call_d
180 .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
181 .await
182 .unwrap();
183 drop(project_d);
184
185 // All clients see that clients B and C are following client A.
186 cx_c.executor().run_until_parked();
187 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
188 assert_eq!(
189 followers_by_leader(project_id, cx),
190 &[(peer_id_a, vec![peer_id_b, peer_id_c])],
191 "followers seen by {name}"
192 );
193 }
194
195 // Client C unfollows client A.
196 workspace_c.update_in(cx_c, |workspace, window, cx| {
197 workspace.unfollow(peer_id_a, window, cx).unwrap();
198 });
199
200 // All clients see that clients B is following client A.
201 cx_c.executor().run_until_parked();
202 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
203 assert_eq!(
204 followers_by_leader(project_id, cx),
205 &[(peer_id_a, vec![peer_id_b])],
206 "followers seen by {name}"
207 );
208 }
209
210 // Client C re-follows client A.
211 workspace_c.update_in(cx_c, |workspace, window, cx| {
212 workspace.follow(peer_id_a, window, cx)
213 });
214
215 // All clients see that clients B and C are following client A.
216 cx_c.executor().run_until_parked();
217 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
218 assert_eq!(
219 followers_by_leader(project_id, cx),
220 &[(peer_id_a, vec![peer_id_b, peer_id_c])],
221 "followers seen by {name}"
222 );
223 }
224
225 // Client D follows client B, then switches to following client C.
226 workspace_d.update_in(cx_d, |workspace, window, cx| {
227 workspace.follow(peer_id_b, window, cx)
228 });
229 cx_a.executor().run_until_parked();
230 workspace_d.update_in(cx_d, |workspace, window, cx| {
231 workspace.follow(peer_id_c, window, cx)
232 });
233
234 // All clients see that D is following C
235 cx_a.executor().run_until_parked();
236 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
237 assert_eq!(
238 followers_by_leader(project_id, cx),
239 &[
240 (peer_id_a, vec![peer_id_b, peer_id_c]),
241 (peer_id_c, vec![peer_id_d])
242 ],
243 "followers seen by {name}"
244 );
245 }
246
247 // Client C closes the project.
248 let weak_workspace_c = workspace_c.downgrade();
249 workspace_c.update_in(cx_c, |workspace, window, cx| {
250 workspace.close_window(&Default::default(), window, cx);
251 });
252 executor.run_until_parked();
253 // are you sure you want to leave the call?
254 cx_c.simulate_prompt_answer("Close window and hang up");
255 cx_c.cx.update(|_| {
256 drop(workspace_c);
257 });
258 executor.run_until_parked();
259 cx_c.cx.update(|_| {});
260
261 weak_workspace_c.assert_released();
262
263 // Clients A and B see that client B is following A, and client C is not present in the followers.
264 executor.run_until_parked();
265 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("D", &cx_d)] {
266 assert_eq!(
267 followers_by_leader(project_id, cx),
268 &[(peer_id_a, vec![peer_id_b]),],
269 "followers seen by {name}"
270 );
271 }
272
273 // When client A activates a different editor, client B does so as well.
274 workspace_a.update_in(cx_a, |workspace, window, cx| {
275 workspace.activate_item(&editor_a1, true, true, window, cx)
276 });
277 executor.run_until_parked();
278 workspace_b.update(cx_b, |workspace, cx| {
279 assert_eq!(
280 workspace.active_item(cx).unwrap().item_id(),
281 editor_b1.item_id()
282 );
283 });
284
285 // When client A opens a multibuffer, client B does so as well.
286 let multibuffer_a = cx_a.new(|cx| {
287 let buffer_a1 = project_a.update(cx, |project, cx| {
288 project
289 .get_open_buffer(&(worktree_id, rel_path("1.txt")).into(), cx)
290 .unwrap()
291 });
292 let buffer_a2 = project_a.update(cx, |project, cx| {
293 project
294 .get_open_buffer(&(worktree_id, rel_path("2.txt")).into(), cx)
295 .unwrap()
296 });
297 let mut result = MultiBuffer::new(Capability::ReadWrite);
298 result.set_excerpts_for_path(
299 PathKey::for_buffer(&buffer_a1, cx),
300 buffer_a1,
301 [Point::row_range(1..2)],
302 1,
303 cx,
304 );
305 result.set_excerpts_for_path(
306 PathKey::for_buffer(&buffer_a2, cx),
307 buffer_a2,
308 [Point::row_range(5..6)],
309 1,
310 cx,
311 );
312 result
313 });
314 let multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
315 let editor = cx
316 .new(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), window, cx));
317 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
318 editor
319 });
320 executor.run_until_parked();
321 let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
322 workspace
323 .active_item(cx)
324 .unwrap()
325 .downcast::<Editor>()
326 .unwrap()
327 });
328 assert_eq!(
329 multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
330 multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
331 );
332
333 // When client A navigates back and forth, client B does so as well.
334 workspace_a
335 .update_in(cx_a, |workspace, window, cx| {
336 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
337 })
338 .await
339 .unwrap();
340 executor.run_until_parked();
341 workspace_b.update(cx_b, |workspace, cx| {
342 assert_eq!(
343 workspace.active_item(cx).unwrap().item_id(),
344 editor_b1.item_id()
345 );
346 });
347
348 workspace_a
349 .update_in(cx_a, |workspace, window, cx| {
350 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
351 })
352 .await
353 .unwrap();
354 executor.run_until_parked();
355 workspace_b.update(cx_b, |workspace, cx| {
356 assert_eq!(
357 workspace.active_item(cx).unwrap().item_id(),
358 editor_b2.item_id()
359 );
360 });
361
362 workspace_a
363 .update_in(cx_a, |workspace, window, cx| {
364 workspace.go_forward(workspace.active_pane().downgrade(), window, cx)
365 })
366 .await
367 .unwrap();
368 executor.run_until_parked();
369 workspace_b.update(cx_b, |workspace, cx| {
370 assert_eq!(
371 workspace.active_item(cx).unwrap().item_id(),
372 editor_b1.item_id()
373 );
374 });
375
376 // Changes to client A's editor are reflected on client B.
377 editor_a1.update_in(cx_a, |editor, window, cx| {
378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
379 s.select_ranges([1..1, 2..2])
380 });
381 });
382 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
383 executor.run_until_parked();
384 cx_b.background_executor.run_until_parked();
385
386 editor_b1.update(cx_b, |editor, cx| {
387 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
388 });
389
390 editor_a1.update_in(cx_a, |editor, window, cx| {
391 editor.set_text("TWO", window, cx)
392 });
393 executor.run_until_parked();
394 editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
395
396 editor_a1.update_in(cx_a, |editor, window, cx| {
397 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
398 s.select_ranges([3..3])
399 });
400 editor.set_scroll_position(point(0., 100.), window, cx);
401 });
402 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
403 executor.run_until_parked();
404 editor_b1.update(cx_b, |editor, cx| {
405 assert_eq!(editor.selections.ranges(cx), &[3..3]);
406 });
407
408 // After unfollowing, client B stops receiving updates from client A.
409 workspace_b.update_in(cx_b, |workspace, window, cx| {
410 workspace.unfollow(peer_id_a, window, cx).unwrap()
411 });
412 workspace_a.update_in(cx_a, |workspace, window, cx| {
413 workspace.activate_item(&editor_a2, true, true, window, cx)
414 });
415 executor.run_until_parked();
416 assert_eq!(
417 workspace_b.update(cx_b, |workspace, cx| workspace
418 .active_item(cx)
419 .unwrap()
420 .item_id()),
421 editor_b1.item_id()
422 );
423
424 // Client A starts following client B.
425 workspace_a.update_in(cx_a, |workspace, window, cx| {
426 workspace.follow(peer_id_b, window, cx)
427 });
428 executor.run_until_parked();
429 assert_eq!(
430 workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
431 Some(peer_id_b.into())
432 );
433 assert_eq!(
434 workspace_a.update_in(cx_a, |workspace, _, cx| workspace
435 .active_item(cx)
436 .unwrap()
437 .item_id()),
438 editor_a1.item_id()
439 );
440
441 // #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
442 {
443 use crate::rpc::RECONNECT_TIMEOUT;
444 use gpui::TestScreenCaptureSource;
445 use workspace::{
446 dock::{DockPosition, test::TestPanel},
447 item::test::TestItem,
448 shared_screen::SharedScreen,
449 };
450
451 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
452 let display = TestScreenCaptureSource::new();
453 active_call_b
454 .update(cx_b, |call, cx| call.set_location(None, cx))
455 .await
456 .unwrap();
457 cx_b.set_screen_capture_sources(vec![display]);
458 let source = cx_b
459 .read(|cx| cx.screen_capture_sources())
460 .await
461 .unwrap()
462 .unwrap()
463 .into_iter()
464 .next()
465 .unwrap();
466 active_call_b
467 .update(cx_b, |call, cx| {
468 call.room()
469 .unwrap()
470 .update(cx, |room, cx| room.share_screen(source, cx))
471 })
472 .await
473 .unwrap();
474 executor.run_until_parked();
475
476 let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
477 workspace
478 .active_item(cx)
479 .expect("no active item")
480 .downcast::<SharedScreen>()
481 .expect("active item isn't a shared screen")
482 });
483
484 // Client B activates Zed again, which causes the previous editor to become focused again.
485 active_call_b
486 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
487 .await
488 .unwrap();
489 executor.run_until_parked();
490 workspace_a.update(cx_a, |workspace, cx| {
491 assert_eq!(
492 workspace.active_item(cx).unwrap().item_id(),
493 editor_a1.item_id()
494 )
495 });
496
497 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
498 workspace_b.update_in(cx_b, |workspace, window, cx| {
499 workspace.activate_item(&multibuffer_editor_b, true, true, window, cx)
500 });
501 executor.run_until_parked();
502 workspace_a.update(cx_a, |workspace, cx| {
503 assert_eq!(
504 workspace.active_item(cx).unwrap().item_id(),
505 multibuffer_editor_a.item_id()
506 )
507 });
508
509 // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
510 let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, cx));
511 workspace_b.update_in(cx_b, |workspace, window, cx| {
512 workspace.add_panel(panel, window, cx);
513 workspace.toggle_panel_focus::<TestPanel>(window, cx);
514 });
515 executor.run_until_parked();
516 assert_eq!(
517 workspace_a.update(cx_a, |workspace, cx| workspace
518 .active_item(cx)
519 .unwrap()
520 .item_id()),
521 shared_screen.item_id()
522 );
523
524 // Toggling the focus back to the pane causes client A to return to the multibuffer.
525 workspace_b.update_in(cx_b, |workspace, window, cx| {
526 workspace.toggle_panel_focus::<TestPanel>(window, cx);
527 });
528 executor.run_until_parked();
529 workspace_a.update(cx_a, |workspace, cx| {
530 assert_eq!(
531 workspace.active_item(cx).unwrap().item_id(),
532 multibuffer_editor_a.item_id()
533 )
534 });
535
536 // Client B activates an item that doesn't implement following,
537 // so the previously-opened screen-sharing item gets activated.
538 let unfollowable_item = cx_b.new(TestItem::new);
539 workspace_b.update_in(cx_b, |workspace, window, cx| {
540 workspace.active_pane().update(cx, |pane, cx| {
541 pane.add_item(Box::new(unfollowable_item), true, true, None, window, cx)
542 })
543 });
544 executor.run_until_parked();
545 assert_eq!(
546 workspace_a.update(cx_a, |workspace, cx| workspace
547 .active_item(cx)
548 .unwrap()
549 .item_id()),
550 shared_screen.item_id()
551 );
552
553 // Following interrupts when client B disconnects.
554 client_b.disconnect(&cx_b.to_async());
555 executor.advance_clock(RECONNECT_TIMEOUT);
556 assert_eq!(
557 workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
558 None
559 );
560 }
561}
562
563#[gpui::test]
564async fn test_following_tab_order(
565 executor: BackgroundExecutor,
566 cx_a: &mut TestAppContext,
567 cx_b: &mut TestAppContext,
568) {
569 let mut server = TestServer::start(executor.clone()).await;
570 let client_a = server.create_client(cx_a, "user_a").await;
571 let client_b = server.create_client(cx_b, "user_b").await;
572 server
573 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
574 .await;
575 let active_call_a = cx_a.read(ActiveCall::global);
576 let active_call_b = cx_b.read(ActiveCall::global);
577
578 cx_a.update(editor::init);
579 cx_b.update(editor::init);
580
581 client_a
582 .fs()
583 .insert_tree(
584 path!("/a"),
585 json!({
586 "1.txt": "one",
587 "2.txt": "two",
588 "3.txt": "three",
589 }),
590 )
591 .await;
592 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
593 active_call_a
594 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
595 .await
596 .unwrap();
597
598 let project_id = active_call_a
599 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
600 .await
601 .unwrap();
602 let project_b = client_b.join_remote_project(project_id, cx_b).await;
603 active_call_b
604 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
605 .await
606 .unwrap();
607
608 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
609 let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
610
611 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
612 let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
613
614 let client_b_id = project_a.update(cx_a, |project, _| {
615 project.collaborators().values().next().unwrap().peer_id
616 });
617
618 //Open 1, 3 in that order on client A
619 workspace_a
620 .update_in(cx_a, |workspace, window, cx| {
621 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
622 })
623 .await
624 .unwrap();
625 workspace_a
626 .update_in(cx_a, |workspace, window, cx| {
627 workspace.open_path((worktree_id, rel_path("3.txt")), None, true, window, cx)
628 })
629 .await
630 .unwrap();
631
632 let pane_paths = |pane: &Entity<workspace::Pane>, cx: &mut VisualTestContext| {
633 pane.update(cx, |pane, cx| {
634 pane.items()
635 .map(|item| item.project_path(cx).unwrap().path)
636 .collect::<Vec<_>>()
637 })
638 };
639
640 //Verify that the tabs opened in the order we expect
641 assert_eq!(
642 &pane_paths(&pane_a, cx_a),
643 &[rel_path("1.txt").into(), rel_path("3.txt").into()]
644 );
645
646 //Follow client B as client A
647 workspace_a.update_in(cx_a, |workspace, window, cx| {
648 workspace.follow(client_b_id, window, cx)
649 });
650 executor.run_until_parked();
651
652 //Open just 2 on client B
653 workspace_b
654 .update_in(cx_b, |workspace, window, cx| {
655 workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
656 })
657 .await
658 .unwrap();
659 executor.run_until_parked();
660
661 // Verify that newly opened followed file is at the end
662 assert_eq!(
663 &pane_paths(&pane_a, cx_a),
664 &[
665 rel_path("1.txt").into(),
666 rel_path("3.txt").into(),
667 rel_path("2.txt").into()
668 ]
669 );
670
671 //Open just 1 on client B
672 workspace_b
673 .update_in(cx_b, |workspace, window, cx| {
674 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
675 })
676 .await
677 .unwrap();
678 assert_eq!(
679 &pane_paths(&pane_b, cx_b),
680 &[rel_path("2.txt").into(), rel_path("1.txt").into()]
681 );
682 executor.run_until_parked();
683
684 // Verify that following into 1 did not reorder
685 assert_eq!(
686 &pane_paths(&pane_a, cx_a),
687 &[
688 rel_path("1.txt").into(),
689 rel_path("3.txt").into(),
690 rel_path("2.txt").into()
691 ]
692 );
693}
694
695#[gpui::test(iterations = 10)]
696async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
697 let executor = cx_a.executor();
698 let mut server = TestServer::start(executor.clone()).await;
699 let client_a = server.create_client(cx_a, "user_a").await;
700 let client_b = server.create_client(cx_b, "user_b").await;
701 server
702 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
703 .await;
704 let active_call_a = cx_a.read(ActiveCall::global);
705 let active_call_b = cx_b.read(ActiveCall::global);
706
707 cx_a.update(editor::init);
708 cx_b.update(editor::init);
709
710 // Client A shares a project.
711 client_a
712 .fs()
713 .insert_tree(
714 path!("/a"),
715 json!({
716 "1.txt": "one",
717 "2.txt": "two",
718 "3.txt": "three",
719 "4.txt": "four",
720 }),
721 )
722 .await;
723 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
724 active_call_a
725 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
726 .await
727 .unwrap();
728 let project_id = active_call_a
729 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
730 .await
731 .unwrap();
732
733 // Client B joins the project.
734 let project_b = client_b.join_remote_project(project_id, cx_b).await;
735 active_call_b
736 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
737 .await
738 .unwrap();
739
740 // Client A opens a file.
741 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
742 workspace_a
743 .update_in(cx_a, |workspace, window, cx| {
744 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
745 })
746 .await
747 .unwrap()
748 .downcast::<Editor>()
749 .unwrap();
750
751 // Client B opens a different file.
752 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
753 workspace_b
754 .update_in(cx_b, |workspace, window, cx| {
755 workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
756 })
757 .await
758 .unwrap()
759 .downcast::<Editor>()
760 .unwrap();
761
762 // Clients A and B follow each other in split panes
763 workspace_a.update_in(cx_a, |workspace, window, cx| {
764 workspace.split_and_clone(
765 workspace.active_pane().clone(),
766 SplitDirection::Right,
767 window,
768 cx,
769 );
770 });
771 workspace_a.update_in(cx_a, |workspace, window, cx| {
772 workspace.follow(client_b.peer_id().unwrap(), window, cx)
773 });
774 executor.run_until_parked();
775 workspace_b.update_in(cx_b, |workspace, window, cx| {
776 workspace.split_and_clone(
777 workspace.active_pane().clone(),
778 SplitDirection::Right,
779 window,
780 cx,
781 );
782 });
783 workspace_b.update_in(cx_b, |workspace, window, cx| {
784 workspace.follow(client_a.peer_id().unwrap(), window, cx)
785 });
786 executor.run_until_parked();
787
788 // Clients A and B return focus to the original files they had open
789 workspace_a.update_in(cx_a, |workspace, window, cx| {
790 workspace.activate_next_pane(window, cx)
791 });
792 workspace_b.update_in(cx_b, |workspace, window, cx| {
793 workspace.activate_next_pane(window, cx)
794 });
795 executor.run_until_parked();
796
797 // Both clients see the other client's focused file in their right pane.
798 assert_eq!(
799 pane_summaries(&workspace_a, cx_a),
800 &[
801 PaneSummary {
802 active: true,
803 leader: None,
804 items: vec![(true, "1.txt".into())]
805 },
806 PaneSummary {
807 active: false,
808 leader: client_b.peer_id(),
809 items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
810 },
811 ]
812 );
813 assert_eq!(
814 pane_summaries(&workspace_b, cx_b),
815 &[
816 PaneSummary {
817 active: true,
818 leader: None,
819 items: vec![(true, "2.txt".into())]
820 },
821 PaneSummary {
822 active: false,
823 leader: client_a.peer_id(),
824 items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
825 },
826 ]
827 );
828
829 // Clients A and B each open a new file.
830 workspace_a
831 .update_in(cx_a, |workspace, window, cx| {
832 workspace.open_path((worktree_id, rel_path("3.txt")), None, true, window, cx)
833 })
834 .await
835 .unwrap();
836
837 workspace_b
838 .update_in(cx_b, |workspace, window, cx| {
839 workspace.open_path((worktree_id, rel_path("4.txt")), None, true, window, cx)
840 })
841 .await
842 .unwrap();
843 executor.run_until_parked();
844
845 // Both client's see the other client open the new file, but keep their
846 // focus on their own active pane.
847 assert_eq!(
848 pane_summaries(&workspace_a, cx_a),
849 &[
850 PaneSummary {
851 active: true,
852 leader: None,
853 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
854 },
855 PaneSummary {
856 active: false,
857 leader: client_b.peer_id(),
858 items: vec![
859 (false, "1.txt".into()),
860 (false, "2.txt".into()),
861 (true, "4.txt".into())
862 ]
863 },
864 ]
865 );
866 assert_eq!(
867 pane_summaries(&workspace_b, cx_b),
868 &[
869 PaneSummary {
870 active: true,
871 leader: None,
872 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
873 },
874 PaneSummary {
875 active: false,
876 leader: client_a.peer_id(),
877 items: vec![
878 (false, "2.txt".into()),
879 (false, "1.txt".into()),
880 (true, "3.txt".into())
881 ]
882 },
883 ]
884 );
885
886 // Client A focuses their right pane, in which they're following client B.
887 workspace_a.update_in(cx_a, |workspace, window, cx| {
888 workspace.activate_next_pane(window, cx)
889 });
890 executor.run_until_parked();
891
892 // Client B sees that client A is now looking at the same file as them.
893 assert_eq!(
894 pane_summaries(&workspace_a, cx_a),
895 &[
896 PaneSummary {
897 active: false,
898 leader: None,
899 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
900 },
901 PaneSummary {
902 active: true,
903 leader: client_b.peer_id(),
904 items: vec![
905 (false, "1.txt".into()),
906 (false, "2.txt".into()),
907 (true, "4.txt".into())
908 ]
909 },
910 ]
911 );
912 assert_eq!(
913 pane_summaries(&workspace_b, cx_b),
914 &[
915 PaneSummary {
916 active: true,
917 leader: None,
918 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
919 },
920 PaneSummary {
921 active: false,
922 leader: client_a.peer_id(),
923 items: vec![
924 (false, "2.txt".into()),
925 (false, "1.txt".into()),
926 (false, "3.txt".into()),
927 (true, "4.txt".into())
928 ]
929 },
930 ]
931 );
932
933 // Client B focuses their right pane, in which they're following client A,
934 // who is following them.
935 workspace_b.update_in(cx_b, |workspace, window, cx| {
936 workspace.activate_next_pane(window, cx)
937 });
938 executor.run_until_parked();
939
940 // Client A sees that client B is now looking at the same file as them.
941 assert_eq!(
942 pane_summaries(&workspace_b, cx_b),
943 &[
944 PaneSummary {
945 active: false,
946 leader: None,
947 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
948 },
949 PaneSummary {
950 active: true,
951 leader: client_a.peer_id(),
952 items: vec![
953 (false, "2.txt".into()),
954 (false, "1.txt".into()),
955 (false, "3.txt".into()),
956 (true, "4.txt".into())
957 ]
958 },
959 ]
960 );
961 assert_eq!(
962 pane_summaries(&workspace_a, cx_a),
963 &[
964 PaneSummary {
965 active: false,
966 leader: None,
967 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
968 },
969 PaneSummary {
970 active: true,
971 leader: client_b.peer_id(),
972 items: vec![
973 (false, "1.txt".into()),
974 (false, "2.txt".into()),
975 (true, "4.txt".into())
976 ]
977 },
978 ]
979 );
980
981 // Client B focuses a file that they previously followed A to, breaking
982 // the follow.
983 workspace_b.update_in(cx_b, |workspace, window, cx| {
984 workspace.active_pane().update(cx, |pane, cx| {
985 pane.activate_previous_item(&Default::default(), window, cx);
986 });
987 });
988 executor.run_until_parked();
989
990 // Both clients see that client B is looking at that previous file.
991 assert_eq!(
992 pane_summaries(&workspace_b, cx_b),
993 &[
994 PaneSummary {
995 active: false,
996 leader: None,
997 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
998 },
999 PaneSummary {
1000 active: true,
1001 leader: None,
1002 items: vec![
1003 (false, "2.txt".into()),
1004 (false, "1.txt".into()),
1005 (true, "3.txt".into()),
1006 (false, "4.txt".into())
1007 ]
1008 },
1009 ]
1010 );
1011 assert_eq!(
1012 pane_summaries(&workspace_a, cx_a),
1013 &[
1014 PaneSummary {
1015 active: false,
1016 leader: None,
1017 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1018 },
1019 PaneSummary {
1020 active: true,
1021 leader: client_b.peer_id(),
1022 items: vec![
1023 (false, "1.txt".into()),
1024 (false, "2.txt".into()),
1025 (false, "4.txt".into()),
1026 (true, "3.txt".into()),
1027 ]
1028 },
1029 ]
1030 );
1031
1032 // Client B closes tabs, some of which were originally opened by client A,
1033 // and some of which were originally opened by client B.
1034 workspace_b.update_in(cx_b, |workspace, window, cx| {
1035 workspace.active_pane().update(cx, |pane, cx| {
1036 pane.close_other_items(&Default::default(), None, window, cx)
1037 .detach();
1038 });
1039 });
1040
1041 executor.run_until_parked();
1042
1043 // Both clients see that Client B is looking at the previous tab.
1044 assert_eq!(
1045 pane_summaries(&workspace_b, cx_b),
1046 &[
1047 PaneSummary {
1048 active: false,
1049 leader: None,
1050 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1051 },
1052 PaneSummary {
1053 active: true,
1054 leader: None,
1055 items: vec![(true, "3.txt".into()),]
1056 },
1057 ]
1058 );
1059 assert_eq!(
1060 pane_summaries(&workspace_a, cx_a),
1061 &[
1062 PaneSummary {
1063 active: false,
1064 leader: None,
1065 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1066 },
1067 PaneSummary {
1068 active: true,
1069 leader: client_b.peer_id(),
1070 items: vec![
1071 (false, "1.txt".into()),
1072 (false, "2.txt".into()),
1073 (false, "4.txt".into()),
1074 (true, "3.txt".into()),
1075 ]
1076 },
1077 ]
1078 );
1079
1080 // Client B follows client A again.
1081 workspace_b.update_in(cx_b, |workspace, window, cx| {
1082 workspace.follow(client_a.peer_id().unwrap(), window, cx)
1083 });
1084 executor.run_until_parked();
1085 // Client A cycles through some tabs.
1086 workspace_a.update_in(cx_a, |workspace, window, cx| {
1087 workspace.active_pane().update(cx, |pane, cx| {
1088 pane.activate_previous_item(&Default::default(), window, cx);
1089 });
1090 });
1091 executor.run_until_parked();
1092
1093 // Client B follows client A into those tabs.
1094 assert_eq!(
1095 pane_summaries(&workspace_a, cx_a),
1096 &[
1097 PaneSummary {
1098 active: false,
1099 leader: None,
1100 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1101 },
1102 PaneSummary {
1103 active: true,
1104 leader: None,
1105 items: vec![
1106 (false, "1.txt".into()),
1107 (false, "2.txt".into()),
1108 (true, "4.txt".into()),
1109 (false, "3.txt".into()),
1110 ]
1111 },
1112 ]
1113 );
1114 assert_eq!(
1115 pane_summaries(&workspace_b, cx_b),
1116 &[
1117 PaneSummary {
1118 active: false,
1119 leader: None,
1120 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1121 },
1122 PaneSummary {
1123 active: true,
1124 leader: client_a.peer_id(),
1125 items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
1126 },
1127 ]
1128 );
1129
1130 workspace_a.update_in(cx_a, |workspace, window, cx| {
1131 workspace.active_pane().update(cx, |pane, cx| {
1132 pane.activate_previous_item(&Default::default(), window, cx);
1133 });
1134 });
1135 executor.run_until_parked();
1136
1137 assert_eq!(
1138 pane_summaries(&workspace_a, cx_a),
1139 &[
1140 PaneSummary {
1141 active: false,
1142 leader: None,
1143 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1144 },
1145 PaneSummary {
1146 active: true,
1147 leader: None,
1148 items: vec![
1149 (false, "1.txt".into()),
1150 (true, "2.txt".into()),
1151 (false, "4.txt".into()),
1152 (false, "3.txt".into()),
1153 ]
1154 },
1155 ]
1156 );
1157 assert_eq!(
1158 pane_summaries(&workspace_b, cx_b),
1159 &[
1160 PaneSummary {
1161 active: false,
1162 leader: None,
1163 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1164 },
1165 PaneSummary {
1166 active: true,
1167 leader: client_a.peer_id(),
1168 items: vec![
1169 (false, "3.txt".into()),
1170 (false, "4.txt".into()),
1171 (true, "2.txt".into())
1172 ]
1173 },
1174 ]
1175 );
1176
1177 workspace_a.update_in(cx_a, |workspace, window, cx| {
1178 workspace.active_pane().update(cx, |pane, cx| {
1179 pane.activate_previous_item(&Default::default(), window, cx);
1180 });
1181 });
1182 executor.run_until_parked();
1183
1184 assert_eq!(
1185 pane_summaries(&workspace_a, cx_a),
1186 &[
1187 PaneSummary {
1188 active: false,
1189 leader: None,
1190 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1191 },
1192 PaneSummary {
1193 active: true,
1194 leader: None,
1195 items: vec![
1196 (true, "1.txt".into()),
1197 (false, "2.txt".into()),
1198 (false, "4.txt".into()),
1199 (false, "3.txt".into()),
1200 ]
1201 },
1202 ]
1203 );
1204 assert_eq!(
1205 pane_summaries(&workspace_b, cx_b),
1206 &[
1207 PaneSummary {
1208 active: false,
1209 leader: None,
1210 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1211 },
1212 PaneSummary {
1213 active: true,
1214 leader: client_a.peer_id(),
1215 items: vec![
1216 (false, "3.txt".into()),
1217 (false, "4.txt".into()),
1218 (false, "2.txt".into()),
1219 (true, "1.txt".into()),
1220 ]
1221 },
1222 ]
1223 );
1224}
1225
1226#[gpui::test(iterations = 10)]
1227async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1228 // 2 clients connect to a server.
1229 let executor = cx_a.executor();
1230 let mut server = TestServer::start(executor.clone()).await;
1231 let client_a = server.create_client(cx_a, "user_a").await;
1232 let client_b = server.create_client(cx_b, "user_b").await;
1233 server
1234 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1235 .await;
1236 let active_call_a = cx_a.read(ActiveCall::global);
1237 let active_call_b = cx_b.read(ActiveCall::global);
1238
1239 cx_a.update(editor::init);
1240 cx_b.update(editor::init);
1241
1242 // Client A shares a project.
1243 client_a
1244 .fs()
1245 .insert_tree(
1246 path!("/a"),
1247 json!({
1248 "1.txt": "one",
1249 "2.txt": "two",
1250 "3.txt": "three",
1251 }),
1252 )
1253 .await;
1254 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1255 active_call_a
1256 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1257 .await
1258 .unwrap();
1259
1260 let project_id = active_call_a
1261 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1262 .await
1263 .unwrap();
1264 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1265 active_call_b
1266 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1267 .await
1268 .unwrap();
1269
1270 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1271 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1272
1273 let _editor_a1 = workspace_a
1274 .update_in(cx_a, |workspace, window, cx| {
1275 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
1276 })
1277 .await
1278 .unwrap()
1279 .downcast::<Editor>()
1280 .unwrap();
1281
1282 // Client B starts following client A.
1283 let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
1284 let leader_id = project_b.update(cx_b, |project, _| {
1285 project.collaborators().values().next().unwrap().peer_id
1286 });
1287 workspace_b.update_in(cx_b, |workspace, window, cx| {
1288 workspace.follow(leader_id, window, cx)
1289 });
1290 executor.run_until_parked();
1291 assert_eq!(
1292 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1293 Some(leader_id.into())
1294 );
1295 let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
1296 workspace
1297 .active_item(cx)
1298 .unwrap()
1299 .downcast::<Editor>()
1300 .unwrap()
1301 });
1302
1303 // When client B moves, it automatically stops following client A.
1304 editor_b2.update_in(cx_b, |editor, window, cx| {
1305 editor.move_right(&editor::actions::MoveRight, window, cx)
1306 });
1307 assert_eq!(
1308 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1309 None
1310 );
1311
1312 workspace_b.update_in(cx_b, |workspace, window, cx| {
1313 workspace.follow(leader_id, window, cx)
1314 });
1315 executor.run_until_parked();
1316 assert_eq!(
1317 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1318 Some(leader_id.into())
1319 );
1320
1321 // When client B edits, it automatically stops following client A.
1322 editor_b2.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
1323 assert_eq!(
1324 workspace_b.update_in(cx_b, |workspace, _, _| workspace.leader_for_pane(&pane_b)),
1325 None
1326 );
1327
1328 workspace_b.update_in(cx_b, |workspace, window, cx| {
1329 workspace.follow(leader_id, window, cx)
1330 });
1331 executor.run_until_parked();
1332 assert_eq!(
1333 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1334 Some(leader_id.into())
1335 );
1336
1337 // When client B scrolls, it automatically stops following client A.
1338 editor_b2.update_in(cx_b, |editor, window, cx| {
1339 editor.set_scroll_position(point(0., 3.), window, cx)
1340 });
1341 assert_eq!(
1342 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1343 None
1344 );
1345
1346 workspace_b.update_in(cx_b, |workspace, window, cx| {
1347 workspace.follow(leader_id, window, cx)
1348 });
1349 executor.run_until_parked();
1350 assert_eq!(
1351 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1352 Some(leader_id.into())
1353 );
1354
1355 // When client B activates a different pane, it continues following client A in the original pane.
1356 workspace_b.update_in(cx_b, |workspace, window, cx| {
1357 workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
1358 });
1359 assert_eq!(
1360 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1361 Some(leader_id.into())
1362 );
1363
1364 workspace_b.update_in(cx_b, |workspace, window, cx| {
1365 workspace.activate_next_pane(window, cx)
1366 });
1367 assert_eq!(
1368 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1369 Some(leader_id.into())
1370 );
1371
1372 // When client B activates a different item in the original pane, it automatically stops following client A.
1373 workspace_b
1374 .update_in(cx_b, |workspace, window, cx| {
1375 workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
1376 })
1377 .await
1378 .unwrap();
1379 assert_eq!(
1380 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1381 None
1382 );
1383}
1384
1385#[gpui::test(iterations = 10)]
1386async fn test_peers_simultaneously_following_each_other(
1387 cx_a: &mut TestAppContext,
1388 cx_b: &mut TestAppContext,
1389) {
1390 let executor = cx_a.executor();
1391 let mut server = TestServer::start(executor.clone()).await;
1392 let client_a = server.create_client(cx_a, "user_a").await;
1393 let client_b = server.create_client(cx_b, "user_b").await;
1394 server
1395 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1396 .await;
1397 let active_call_a = cx_a.read(ActiveCall::global);
1398
1399 cx_a.update(editor::init);
1400 cx_b.update(editor::init);
1401
1402 client_a.fs().insert_tree("/a", json!({})).await;
1403 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1404 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1405 let project_id = active_call_a
1406 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1407 .await
1408 .unwrap();
1409
1410 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1411 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1412
1413 executor.run_until_parked();
1414 let client_a_id = project_b.update(cx_b, |project, _| {
1415 project.collaborators().values().next().unwrap().peer_id
1416 });
1417 let client_b_id = project_a.update(cx_a, |project, _| {
1418 project.collaborators().values().next().unwrap().peer_id
1419 });
1420
1421 workspace_a.update_in(cx_a, |workspace, window, cx| {
1422 workspace.follow(client_b_id, window, cx)
1423 });
1424 workspace_b.update_in(cx_b, |workspace, window, cx| {
1425 workspace.follow(client_a_id, window, cx)
1426 });
1427 executor.run_until_parked();
1428
1429 workspace_a.update(cx_a, |workspace, _| {
1430 assert_eq!(
1431 workspace.leader_for_pane(workspace.active_pane()),
1432 Some(client_b_id.into())
1433 );
1434 });
1435 workspace_b.update(cx_b, |workspace, _| {
1436 assert_eq!(
1437 workspace.leader_for_pane(workspace.active_pane()),
1438 Some(client_a_id.into())
1439 );
1440 });
1441}
1442
1443#[gpui::test(iterations = 10)]
1444async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1445 // a and b join a channel/call
1446 // a shares project 1
1447 // b shares project 2
1448 //
1449 // b follows a: causes project 2 to be joined, and b to follow a.
1450 // b opens a different file in project 2, a follows b
1451 // b opens a different file in project 1, a cannot follow b
1452 // b shares the project, a joins the project and follows b
1453 let executor = cx_a.executor();
1454 let mut server = TestServer::start(executor.clone()).await;
1455 let client_a = server.create_client(cx_a, "user_a").await;
1456 let client_b = server.create_client(cx_b, "user_b").await;
1457
1458 client_a
1459 .fs()
1460 .insert_tree(
1461 path!("/a"),
1462 json!({
1463 "w.rs": "",
1464 "x.rs": "",
1465 }),
1466 )
1467 .await;
1468
1469 client_b
1470 .fs()
1471 .insert_tree(
1472 path!("/b"),
1473 json!({
1474 "y.rs": "",
1475 "z.rs": "",
1476 }),
1477 )
1478 .await;
1479
1480 server
1481 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1482 .await;
1483 let active_call_a = cx_a.read(ActiveCall::global);
1484 let active_call_b = cx_b.read(ActiveCall::global);
1485
1486 let (project_a, worktree_id_a) = client_a.build_local_project(path!("/a"), cx_a).await;
1487 let (project_b, worktree_id_b) = client_b.build_local_project(path!("/b"), cx_b).await;
1488
1489 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1490 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1491
1492 active_call_a
1493 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1494 .await
1495 .unwrap();
1496
1497 active_call_a
1498 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1499 .await
1500 .unwrap();
1501 active_call_b
1502 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1503 .await
1504 .unwrap();
1505
1506 workspace_a
1507 .update_in(cx_a, |workspace, window, cx| {
1508 workspace.open_path((worktree_id_a, rel_path("w.rs")), None, true, window, cx)
1509 })
1510 .await
1511 .unwrap();
1512
1513 executor.run_until_parked();
1514 assert_eq!(visible_push_notifications(cx_b).len(), 1);
1515
1516 workspace_b.update_in(cx_b, |workspace, window, cx| {
1517 workspace.follow(client_a.peer_id().unwrap(), window, cx)
1518 });
1519
1520 executor.run_until_parked();
1521 let window_b_project_a = *cx_b
1522 .windows()
1523 .iter()
1524 .max_by_key(|window| window.window_id())
1525 .unwrap();
1526
1527 let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b);
1528
1529 let workspace_b_project_a = window_b_project_a
1530 .downcast::<Workspace>()
1531 .unwrap()
1532 .root(cx_b)
1533 .unwrap();
1534
1535 // assert that b is following a in project a in w.rs
1536 workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1537 assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
1538 assert_eq!(
1539 client_a.peer_id().map(Into::into),
1540 workspace.leader_for_pane(workspace.active_pane())
1541 );
1542 let item = workspace.active_item(cx).unwrap();
1543 assert_eq!(item.tab_content_text(0, cx), SharedString::from("w.rs"));
1544 });
1545
1546 // TODO: in app code, this would be done by the collab_ui.
1547 active_call_b
1548 .update(&mut cx_b2, |call, cx| {
1549 let project = workspace_b_project_a.read(cx).project().clone();
1550 call.set_location(Some(&project), cx)
1551 })
1552 .await
1553 .unwrap();
1554
1555 // assert that there are no share notifications open
1556 assert_eq!(visible_push_notifications(cx_b).len(), 0);
1557
1558 // b moves to x.rs in a's project, and a follows
1559 workspace_b_project_a
1560 .update_in(&mut cx_b2, |workspace, window, cx| {
1561 workspace.open_path((worktree_id_a, rel_path("x.rs")), None, true, window, cx)
1562 })
1563 .await
1564 .unwrap();
1565
1566 executor.run_until_parked();
1567 workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1568 let item = workspace.active_item(cx).unwrap();
1569 assert_eq!(item.tab_content_text(0, cx), SharedString::from("x.rs"));
1570 });
1571
1572 workspace_a.update_in(cx_a, |workspace, window, cx| {
1573 workspace.follow(client_b.peer_id().unwrap(), window, cx)
1574 });
1575
1576 executor.run_until_parked();
1577 workspace_a.update(cx_a, |workspace, cx| {
1578 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1579 assert_eq!(
1580 client_b.peer_id().map(Into::into),
1581 workspace.leader_for_pane(workspace.active_pane())
1582 );
1583 let item = workspace.active_pane().read(cx).active_item().unwrap();
1584 assert_eq!(item.tab_content_text(0, cx), "x.rs");
1585 });
1586
1587 // b moves to y.rs in b's project, a is still following but can't yet see
1588 workspace_b
1589 .update_in(cx_b, |workspace, window, cx| {
1590 workspace.open_path((worktree_id_b, rel_path("y.rs")), None, true, window, cx)
1591 })
1592 .await
1593 .unwrap();
1594
1595 // TODO: in app code, this would be done by the collab_ui.
1596 active_call_b
1597 .update(cx_b, |call, cx| {
1598 let project = workspace_b.read(cx).project().clone();
1599 call.set_location(Some(&project), cx)
1600 })
1601 .await
1602 .unwrap();
1603
1604 let project_b_id = active_call_b
1605 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1606 .await
1607 .unwrap();
1608
1609 executor.run_until_parked();
1610 assert_eq!(visible_push_notifications(cx_a).len(), 1);
1611 cx_a.update(|_, cx| {
1612 workspace::join_in_room_project(
1613 project_b_id,
1614 client_b.user_id().unwrap(),
1615 client_a.app_state.clone(),
1616 cx,
1617 )
1618 })
1619 .await
1620 .unwrap();
1621
1622 executor.run_until_parked();
1623
1624 assert_eq!(visible_push_notifications(cx_a).len(), 0);
1625 let window_a_project_b = *cx_a
1626 .windows()
1627 .iter()
1628 .max_by_key(|window| window.window_id())
1629 .unwrap();
1630 let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a);
1631 let workspace_a_project_b = window_a_project_b
1632 .downcast::<Workspace>()
1633 .unwrap()
1634 .root(cx_a)
1635 .unwrap();
1636
1637 executor.run_until_parked();
1638
1639 workspace_a_project_b.update(cx_a2, |workspace, cx| {
1640 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1641 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1642 assert_eq!(
1643 client_b.peer_id().map(Into::into),
1644 workspace.leader_for_pane(workspace.active_pane())
1645 );
1646 let item = workspace.active_item(cx).unwrap();
1647 assert_eq!(item.tab_content_text(0, cx), SharedString::from("y.rs"));
1648 });
1649}
1650
1651#[gpui::test]
1652async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1653 let (_server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
1654
1655 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
1656 client_a
1657 .host_workspace(&workspace_a, channel_id, cx_a)
1658 .await;
1659 let (workspace_b, cx_b) = client_b.join_workspace(channel_id, cx_b).await;
1660
1661 cx_a.simulate_keystrokes("cmd-p");
1662 cx_a.run_until_parked();
1663 cx_a.simulate_keystrokes("2 enter");
1664
1665 let editor_a = workspace_a.update(cx_a, |workspace, cx| {
1666 workspace.active_item_as::<Editor>(cx).unwrap()
1667 });
1668 let editor_b = workspace_b.update(cx_b, |workspace, cx| {
1669 workspace.active_item_as::<Editor>(cx).unwrap()
1670 });
1671
1672 // b should follow a to position 1
1673 editor_a.update_in(cx_a, |editor, window, cx| {
1674 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1675 s.select_ranges([1..1])
1676 })
1677 });
1678 cx_a.executor()
1679 .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1680 cx_a.run_until_parked();
1681 editor_b.update(cx_b, |editor, cx| {
1682 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1683 });
1684
1685 // a unshares the project
1686 cx_a.update(|_, cx| {
1687 let project = workspace_a.read(cx).project().clone();
1688 ActiveCall::global(cx).update(cx, |call, cx| {
1689 call.unshare_project(project, cx).unwrap();
1690 })
1691 });
1692 cx_a.run_until_parked();
1693
1694 // b should not follow a to position 2
1695 editor_a.update_in(cx_a, |editor, window, cx| {
1696 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1697 s.select_ranges([2..2])
1698 })
1699 });
1700 cx_a.executor()
1701 .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1702 cx_a.run_until_parked();
1703 editor_b.update(cx_b, |editor, cx| {
1704 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1705 });
1706 cx_b.update(|_, cx| {
1707 let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
1708 let participant = room.remote_participants().get(&client_a.id()).unwrap();
1709 assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
1710 })
1711}
1712
1713#[gpui::test]
1714async fn test_following_into_excluded_file(
1715 mut cx_a: &mut TestAppContext,
1716 mut cx_b: &mut TestAppContext,
1717) {
1718 let executor = cx_a.executor();
1719 let mut server = TestServer::start(executor.clone()).await;
1720 let client_a = server.create_client(cx_a, "user_a").await;
1721 let client_b = server.create_client(cx_b, "user_b").await;
1722 for cx in [&mut cx_a, &mut cx_b] {
1723 cx.update(|cx| {
1724 cx.update_global::<SettingsStore, _>(|store, cx| {
1725 store.update_user_settings(cx, |settings| {
1726 settings.project.worktree.file_scan_exclusions =
1727 Some(vec!["**/.git".to_string()]);
1728 });
1729 });
1730 });
1731 }
1732 server
1733 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1734 .await;
1735 let active_call_a = cx_a.read(ActiveCall::global);
1736 let active_call_b = cx_b.read(ActiveCall::global);
1737 let peer_id_a = client_a.peer_id().unwrap();
1738
1739 client_a
1740 .fs()
1741 .insert_tree(
1742 path!("/a"),
1743 json!({
1744 ".git": {
1745 "COMMIT_EDITMSG": "write your commit message here",
1746 },
1747 "1.txt": "one\none\none",
1748 "2.txt": "two\ntwo\ntwo",
1749 "3.txt": "three\nthree\nthree",
1750 }),
1751 )
1752 .await;
1753 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1754 active_call_a
1755 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1756 .await
1757 .unwrap();
1758
1759 let project_id = active_call_a
1760 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1761 .await
1762 .unwrap();
1763 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1764 active_call_b
1765 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1766 .await
1767 .unwrap();
1768
1769 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1770 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1771
1772 // Client A opens editors for a regular file and an excluded file.
1773 let editor_for_regular = workspace_a
1774 .update_in(cx_a, |workspace, window, cx| {
1775 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
1776 })
1777 .await
1778 .unwrap()
1779 .downcast::<Editor>()
1780 .unwrap();
1781 let editor_for_excluded_a = workspace_a
1782 .update_in(cx_a, |workspace, window, cx| {
1783 workspace.open_path(
1784 (worktree_id, rel_path(".git/COMMIT_EDITMSG")),
1785 None,
1786 true,
1787 window,
1788 cx,
1789 )
1790 })
1791 .await
1792 .unwrap()
1793 .downcast::<Editor>()
1794 .unwrap();
1795
1796 // Client A updates their selections in those editors
1797 editor_for_regular.update_in(cx_a, |editor, window, cx| {
1798 editor.handle_input("a", window, cx);
1799 editor.handle_input("b", window, cx);
1800 editor.handle_input("c", window, cx);
1801 editor.select_left(&Default::default(), window, cx);
1802 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1803 });
1804 editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1805 editor.select_all(&Default::default(), window, cx);
1806 editor.handle_input("new commit message", window, cx);
1807 editor.select_left(&Default::default(), window, cx);
1808 assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1809 });
1810
1811 // When client B starts following client A, currently visible file is replicated
1812 workspace_b.update_in(cx_b, |workspace, window, cx| {
1813 workspace.follow(peer_id_a, window, cx)
1814 });
1815 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1816 executor.run_until_parked();
1817
1818 let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
1819 workspace
1820 .active_item(cx)
1821 .unwrap()
1822 .downcast::<Editor>()
1823 .unwrap()
1824 });
1825 assert_eq!(
1826 cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1827 Some((worktree_id, rel_path(".git/COMMIT_EDITMSG")).into())
1828 );
1829 assert_eq!(
1830 editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
1831 vec![18..17]
1832 );
1833
1834 editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1835 editor.select_right(&Default::default(), window, cx);
1836 });
1837 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1838 executor.run_until_parked();
1839
1840 // Changes from B to the excluded file are replicated in A's editor
1841 editor_for_excluded_b.update_in(cx_b, |editor, window, cx| {
1842 editor.handle_input("\nCo-Authored-By: B <b@b.b>", window, cx);
1843 });
1844 executor.run_until_parked();
1845 editor_for_excluded_a.update(cx_a, |editor, cx| {
1846 assert_eq!(
1847 editor.text(cx),
1848 "new commit message\nCo-Authored-By: B <b@b.b>"
1849 );
1850 });
1851}
1852
1853fn visible_push_notifications(cx: &mut TestAppContext) -> Vec<Entity<ProjectSharedNotification>> {
1854 let mut ret = Vec::new();
1855 for window in cx.windows() {
1856 window
1857 .update(cx, |window, _, _| {
1858 if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
1859 ret.push(handle)
1860 }
1861 })
1862 .unwrap();
1863 }
1864 ret
1865}
1866
1867#[derive(Debug, PartialEq, Eq)]
1868struct PaneSummary {
1869 active: bool,
1870 leader: Option<PeerId>,
1871 items: Vec<(bool, String)>,
1872}
1873
1874fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1875 cx.read(|cx| {
1876 let active_call = ActiveCall::global(cx).read(cx);
1877 let peer_id = active_call.client().peer_id();
1878 let room = active_call.room().unwrap().read(cx);
1879 let mut result = room
1880 .remote_participants()
1881 .values()
1882 .map(|participant| participant.peer_id)
1883 .chain(peer_id)
1884 .filter_map(|peer_id| {
1885 let followers = room.followers_for(peer_id, project_id);
1886 if followers.is_empty() {
1887 None
1888 } else {
1889 Some((peer_id, followers.to_vec()))
1890 }
1891 })
1892 .collect::<Vec<_>>();
1893 result.sort_by_key(|e| e.0);
1894 result
1895 })
1896}
1897
1898fn pane_summaries(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
1899 workspace.update(cx, |workspace, cx| {
1900 let active_pane = workspace.active_pane();
1901 workspace
1902 .panes()
1903 .iter()
1904 .map(|pane| {
1905 let leader = match workspace.leader_for_pane(pane) {
1906 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
1907 Some(CollaboratorId::Agent) => unimplemented!(),
1908 None => None,
1909 };
1910 let active = pane == active_pane;
1911 let pane = pane.read(cx);
1912 let active_ix = pane.active_item_index();
1913 PaneSummary {
1914 active,
1915 leader,
1916 items: pane
1917 .items()
1918 .enumerate()
1919 .map(|(ix, item)| (ix == active_ix, item.tab_content_text(0, cx).into()))
1920 .collect(),
1921 }
1922 })
1923 .collect()
1924 })
1925}
1926
1927#[gpui::test(iterations = 10)]
1928async fn test_following_to_channel_notes_without_a_shared_project(
1929 deterministic: BackgroundExecutor,
1930 mut cx_a: &mut TestAppContext,
1931 mut cx_b: &mut TestAppContext,
1932 mut cx_c: &mut TestAppContext,
1933) {
1934 let mut server = TestServer::start(deterministic.clone()).await;
1935 let client_a = server.create_client(cx_a, "user_a").await;
1936 let client_b = server.create_client(cx_b, "user_b").await;
1937 let client_c = server.create_client(cx_c, "user_c").await;
1938
1939 cx_a.update(editor::init);
1940 cx_b.update(editor::init);
1941 cx_c.update(editor::init);
1942 cx_a.update(collab_ui::channel_view::init);
1943 cx_b.update(collab_ui::channel_view::init);
1944 cx_c.update(collab_ui::channel_view::init);
1945
1946 let channel_1_id = server
1947 .make_channel(
1948 "channel-1",
1949 None,
1950 (&client_a, cx_a),
1951 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1952 )
1953 .await;
1954 let channel_2_id = server
1955 .make_channel(
1956 "channel-2",
1957 None,
1958 (&client_a, cx_a),
1959 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1960 )
1961 .await;
1962
1963 // Clients A, B, and C join a channel.
1964 let active_call_a = cx_a.read(ActiveCall::global);
1965 let active_call_b = cx_b.read(ActiveCall::global);
1966 let active_call_c = cx_c.read(ActiveCall::global);
1967 for (call, cx) in [
1968 (&active_call_a, &mut cx_a),
1969 (&active_call_b, &mut cx_b),
1970 (&active_call_c, &mut cx_c),
1971 ] {
1972 call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
1973 .await
1974 .unwrap();
1975 }
1976 deterministic.run_until_parked();
1977
1978 // Clients A, B, and C all open their own unshared projects.
1979 client_a
1980 .fs()
1981 .insert_tree("/a", json!({ "1.txt": "" }))
1982 .await;
1983 client_b.fs().insert_tree("/b", json!({})).await;
1984 client_c.fs().insert_tree("/c", json!({})).await;
1985 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1986 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1987 let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
1988 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1989 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1990 let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
1991
1992 active_call_a
1993 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1994 .await
1995 .unwrap();
1996
1997 // Client A opens the notes for channel 1.
1998 let channel_notes_1_a = cx_a
1999 .update(|window, cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), window, cx))
2000 .await
2001 .unwrap();
2002 channel_notes_1_a.update_in(cx_a, |notes, window, cx| {
2003 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
2004 notes.editor.update(cx, |editor, cx| {
2005 editor.insert("Hello from A.", window, cx);
2006 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
2007 selections.select_ranges(vec![3..4]);
2008 });
2009 });
2010 });
2011
2012 // Client B follows client A.
2013 workspace_b
2014 .update_in(cx_b, |workspace, window, cx| {
2015 workspace
2016 .start_following(client_a.peer_id().unwrap(), window, cx)
2017 .unwrap()
2018 })
2019 .await
2020 .unwrap();
2021
2022 // Client B is taken to the notes for channel 1, with the same
2023 // text selected as client A.
2024 deterministic.run_until_parked();
2025 let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
2026 assert_eq!(
2027 workspace.leader_for_pane(workspace.active_pane()),
2028 Some(client_a.peer_id().unwrap().into())
2029 );
2030 workspace
2031 .active_item(cx)
2032 .expect("no active item")
2033 .downcast::<ChannelView>()
2034 .expect("active item is not a channel view")
2035 });
2036 channel_notes_1_b.update(cx_b, |notes, cx| {
2037 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
2038 notes.editor.update(cx, |editor, cx| {
2039 assert_eq!(editor.text(cx), "Hello from A.");
2040 assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
2041 })
2042 });
2043
2044 // Client A opens the notes for channel 2.
2045 let channel_notes_2_a = cx_a
2046 .update(|window, cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), window, cx))
2047 .await
2048 .unwrap();
2049 channel_notes_2_a.update(cx_a, |notes, cx| {
2050 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2051 });
2052
2053 // Client B is taken to the notes for channel 2.
2054 deterministic.run_until_parked();
2055 let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
2056 assert_eq!(
2057 workspace.leader_for_pane(workspace.active_pane()),
2058 Some(client_a.peer_id().unwrap().into())
2059 );
2060 workspace
2061 .active_item(cx)
2062 .expect("no active item")
2063 .downcast::<ChannelView>()
2064 .expect("active item is not a channel view")
2065 });
2066 channel_notes_2_b.update(cx_b, |notes, cx| {
2067 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2068 });
2069
2070 // Client A opens a local buffer in their unshared project.
2071 let _unshared_editor_a1 = workspace_a
2072 .update_in(cx_a, |workspace, window, cx| {
2073 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
2074 })
2075 .await
2076 .unwrap()
2077 .downcast::<Editor>()
2078 .unwrap();
2079
2080 // This does not send any leader update message to client B.
2081 // If it did, an error would occur on client B, since this buffer
2082 // is not shared with them.
2083 deterministic.run_until_parked();
2084 workspace_b.update(cx_b, |workspace, cx| {
2085 assert_eq!(
2086 workspace.active_item(cx).expect("no active item").item_id(),
2087 channel_notes_2_b.entity_id()
2088 );
2089 });
2090}
2091
2092pub(crate) async fn join_channel(
2093 channel_id: ChannelId,
2094 client: &TestClient,
2095 cx: &mut TestAppContext,
2096) -> anyhow::Result<()> {
2097 cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
2098 .await
2099}
2100
2101async fn share_workspace(
2102 workspace: &Entity<Workspace>,
2103 cx: &mut VisualTestContext,
2104) -> anyhow::Result<u64> {
2105 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
2106 cx.read(ActiveCall::global)
2107 .update(cx, |call, cx| call.share_project(project, cx))
2108 .await
2109}
2110
2111#[gpui::test]
2112async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2113 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2114
2115 let (workspace, cx_a) = client_a.build_test_workspace(cx_a).await;
2116 join_channel(channel, &client_a, cx_a).await.unwrap();
2117 share_workspace(&workspace, cx_a).await.unwrap();
2118 let buffer = workspace.update(cx_a, |workspace, cx| {
2119 workspace.project().update(cx, |project, cx| {
2120 project.create_local_buffer(&sample_text(26, 5, 'a'), None, false, cx)
2121 })
2122 });
2123 let multibuffer = cx_a.new(|cx| {
2124 let mut mb = MultiBuffer::new(Capability::ReadWrite);
2125 mb.set_excerpts_for_path(
2126 PathKey::for_buffer(&buffer, cx),
2127 buffer.clone(),
2128 [Point::row_range(1..1), Point::row_range(5..5)],
2129 1,
2130 cx,
2131 );
2132 mb
2133 });
2134 let snapshot = buffer.update(cx_a, |buffer, _| buffer.snapshot());
2135 let editor: Entity<Editor> = cx_a.new_window_entity(|window, cx| {
2136 Editor::for_multibuffer(
2137 multibuffer.clone(),
2138 Some(workspace.read(cx).project().clone()),
2139 window,
2140 cx,
2141 )
2142 });
2143 workspace.update_in(cx_a, |workspace, window, cx| {
2144 workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
2145 });
2146 editor.update_in(cx_a, |editor, window, cx| {
2147 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2148 s.select_ranges([Point::row_range(4..4)]);
2149 })
2150 });
2151 let positions = editor.update(cx_a, |editor, _| {
2152 editor
2153 .selections
2154 .disjoint_anchor_ranges()
2155 .map(|range| range.start.text_anchor.to_point(&snapshot))
2156 .collect::<Vec<_>>()
2157 });
2158 multibuffer.update(cx_a, |multibuffer, cx| {
2159 multibuffer.set_excerpts_for_path(
2160 PathKey::for_buffer(&buffer, cx),
2161 buffer,
2162 [Point::row_range(1..5)],
2163 1,
2164 cx,
2165 );
2166 });
2167
2168 let (workspace_b, cx_b) = client_b.join_workspace(channel, cx_b).await;
2169 cx_b.run_until_parked();
2170 let editor_b = workspace_b
2171 .update(cx_b, |workspace, cx| {
2172 workspace
2173 .active_item(cx)
2174 .and_then(|item| item.downcast::<Editor>())
2175 })
2176 .unwrap();
2177
2178 let new_positions = editor_b.update(cx_b, |editor, _| {
2179 editor
2180 .selections
2181 .disjoint_anchor_ranges()
2182 .map(|range| range.start.text_anchor.to_point(&snapshot))
2183 .collect::<Vec<_>>()
2184 });
2185 assert_eq!(positions, new_positions);
2186}
2187
2188#[gpui::test]
2189async fn test_following_to_channel_notes_other_workspace(
2190 cx_a: &mut TestAppContext,
2191 cx_b: &mut TestAppContext,
2192) {
2193 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2194
2195 let mut cx_a2 = cx_a.clone();
2196 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2197 join_channel(channel, &client_a, cx_a).await.unwrap();
2198 share_workspace(&workspace_a, cx_a).await.unwrap();
2199
2200 // a opens 1.txt
2201 cx_a.simulate_keystrokes("cmd-p");
2202 cx_a.run_until_parked();
2203 cx_a.simulate_keystrokes("1 enter");
2204 cx_a.run_until_parked();
2205 workspace_a.update(cx_a, |workspace, cx| {
2206 let editor = workspace.active_item(cx).unwrap();
2207 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2208 });
2209
2210 // b joins channel and is following a
2211 join_channel(channel, &client_b, cx_b).await.unwrap();
2212 cx_b.run_until_parked();
2213 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2214 workspace_b.update(cx_b, |workspace, cx| {
2215 let editor = workspace.active_item(cx).unwrap();
2216 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2217 });
2218
2219 // a opens a second workspace and the channel notes
2220 let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2221 cx_a2.update(|window, _| window.activate_window());
2222 cx_a2
2223 .update(|window, cx| ChannelView::open(channel, None, workspace_a2, window, cx))
2224 .await
2225 .unwrap();
2226 cx_a2.run_until_parked();
2227
2228 // b should follow a to the channel notes
2229 workspace_b.update(cx_b, |workspace, cx| {
2230 let editor = workspace.active_item_as::<ChannelView>(cx).unwrap();
2231 assert_eq!(editor.read(cx).channel(cx).unwrap().id, channel);
2232 });
2233
2234 // a returns to the shared project
2235 cx_a.update(|window, _| window.activate_window());
2236 cx_a.run_until_parked();
2237
2238 workspace_a.update(cx_a, |workspace, cx| {
2239 let editor = workspace.active_item(cx).unwrap();
2240 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2241 });
2242
2243 // b should follow a back
2244 workspace_b.update(cx_b, |workspace, cx| {
2245 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2246 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2247 });
2248}
2249
2250#[gpui::test]
2251async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2252 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2253
2254 let mut cx_a2 = cx_a.clone();
2255 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2256 join_channel(channel, &client_a, cx_a).await.unwrap();
2257 share_workspace(&workspace_a, cx_a).await.unwrap();
2258
2259 // a opens 1.txt
2260 cx_a.simulate_keystrokes("cmd-p");
2261 cx_a.run_until_parked();
2262 cx_a.simulate_keystrokes("1 enter");
2263 cx_a.run_until_parked();
2264 workspace_a.update(cx_a, |workspace, cx| {
2265 let editor = workspace.active_item(cx).unwrap();
2266 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2267 });
2268
2269 // b joins channel and is following a
2270 join_channel(channel, &client_b, cx_b).await.unwrap();
2271 cx_b.run_until_parked();
2272 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2273 workspace_b.update(cx_b, |workspace, cx| {
2274 let editor = workspace.active_item(cx).unwrap();
2275 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2276 });
2277
2278 // stop following
2279 cx_b.simulate_keystrokes("down");
2280
2281 // a opens a different file while not followed
2282 cx_a.simulate_keystrokes("cmd-p");
2283 cx_a.run_until_parked();
2284 cx_a.simulate_keystrokes("2 enter");
2285
2286 workspace_b.update(cx_b, |workspace, cx| {
2287 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2288 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2289 });
2290
2291 // a opens a file in a new window
2292 let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2293 cx_a2.update(|window, _| window.activate_window());
2294 cx_a2.simulate_keystrokes("cmd-p");
2295 cx_a2.run_until_parked();
2296 cx_a2.simulate_keystrokes("3 enter");
2297 cx_a2.run_until_parked();
2298
2299 // b starts following a again
2300 cx_b.simulate_keystrokes("cmd-ctrl-alt-f");
2301 cx_a.run_until_parked();
2302
2303 // a returns to the shared project
2304 cx_a.update(|window, _| window.activate_window());
2305 cx_a.run_until_parked();
2306
2307 workspace_a.update(cx_a, |workspace, cx| {
2308 let editor = workspace.active_item(cx).unwrap();
2309 assert_eq!(editor.tab_content_text(0, cx), "2.js");
2310 });
2311
2312 // b should follow a back
2313 workspace_b.update(cx_b, |workspace, cx| {
2314 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2315 assert_eq!(editor.tab_content_text(0, cx), "2.js");
2316 });
2317}