1use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
2use call::ActiveCall;
3use collab_ui::project_shared_notification::ProjectSharedNotification;
4use editor::{Editor, ExcerptRange, MultiBuffer};
5use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
6use live_kit_client::MacOSDisplay;
7use serde_json::json;
8use std::{borrow::Cow, sync::Arc};
9use workspace::{
10 dock::{test::TestPanel, DockPosition},
11 item::{test::TestItem, ItemHandle as _},
12 shared_screen::SharedScreen,
13 SplitDirection, Workspace,
14};
15
16#[gpui::test(iterations = 10)]
17async fn test_basic_following(
18 deterministic: Arc<Deterministic>,
19 cx_a: &mut TestAppContext,
20 cx_b: &mut TestAppContext,
21 cx_c: &mut TestAppContext,
22 cx_d: &mut TestAppContext,
23) {
24 deterministic.forbid_parking();
25
26 let mut server = TestServer::start(&deterministic).await;
27 let client_a = server.create_client(cx_a, "user_a").await;
28 let client_b = server.create_client(cx_b, "user_b").await;
29 let client_c = server.create_client(cx_c, "user_c").await;
30 let client_d = server.create_client(cx_d, "user_d").await;
31 server
32 .create_room(&mut [
33 (&client_a, cx_a),
34 (&client_b, cx_b),
35 (&client_c, cx_c),
36 (&client_d, cx_d),
37 ])
38 .await;
39 let active_call_a = cx_a.read(ActiveCall::global);
40 let active_call_b = cx_b.read(ActiveCall::global);
41
42 cx_a.update(editor::init);
43 cx_b.update(editor::init);
44
45 client_a
46 .fs()
47 .insert_tree(
48 "/a",
49 json!({
50 "1.txt": "one\none\none",
51 "2.txt": "two\ntwo\ntwo",
52 "3.txt": "three\nthree\nthree",
53 }),
54 )
55 .await;
56 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
57 active_call_a
58 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
59 .await
60 .unwrap();
61
62 let project_id = active_call_a
63 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
64 .await
65 .unwrap();
66 let project_b = client_b.build_remote_project(project_id, cx_b).await;
67 active_call_b
68 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
69 .await
70 .unwrap();
71
72 let window_a = client_a.build_workspace(&project_a, cx_a);
73 let workspace_a = window_a.root(cx_a);
74 let window_b = client_b.build_workspace(&project_b, cx_b);
75 let workspace_b = window_b.root(cx_b);
76
77 // Client A opens some editors.
78 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
79 let editor_a1 = workspace_a
80 .update(cx_a, |workspace, cx| {
81 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
82 })
83 .await
84 .unwrap()
85 .downcast::<Editor>()
86 .unwrap();
87 let editor_a2 = workspace_a
88 .update(cx_a, |workspace, cx| {
89 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
90 })
91 .await
92 .unwrap()
93 .downcast::<Editor>()
94 .unwrap();
95
96 // Client B opens an editor.
97 let editor_b1 = workspace_b
98 .update(cx_b, |workspace, cx| {
99 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
100 })
101 .await
102 .unwrap()
103 .downcast::<Editor>()
104 .unwrap();
105
106 let peer_id_a = client_a.peer_id().unwrap();
107 let peer_id_b = client_b.peer_id().unwrap();
108 let peer_id_c = client_c.peer_id().unwrap();
109 let peer_id_d = client_d.peer_id().unwrap();
110
111 // Client A updates their selections in those editors
112 editor_a1.update(cx_a, |editor, cx| {
113 editor.handle_input("a", cx);
114 editor.handle_input("b", cx);
115 editor.handle_input("c", cx);
116 editor.select_left(&Default::default(), cx);
117 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
118 });
119 editor_a2.update(cx_a, |editor, cx| {
120 editor.handle_input("d", cx);
121 editor.handle_input("e", cx);
122 editor.select_left(&Default::default(), cx);
123 assert_eq!(editor.selections.ranges(cx), vec![2..1]);
124 });
125
126 // When client B starts following client A, all visible view states are replicated to client B.
127 workspace_b
128 .update(cx_b, |workspace, cx| {
129 workspace.follow(peer_id_a, cx).unwrap()
130 })
131 .await
132 .unwrap();
133
134 cx_c.foreground().run_until_parked();
135 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
136 workspace
137 .active_item(cx)
138 .unwrap()
139 .downcast::<Editor>()
140 .unwrap()
141 });
142 assert_eq!(
143 cx_b.read(|cx| editor_b2.project_path(cx)),
144 Some((worktree_id, "2.txt").into())
145 );
146 assert_eq!(
147 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
148 vec![2..1]
149 );
150 assert_eq!(
151 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
152 vec![3..2]
153 );
154
155 cx_c.foreground().run_until_parked();
156 let active_call_c = cx_c.read(ActiveCall::global);
157 let project_c = client_c.build_remote_project(project_id, cx_c).await;
158 let window_c = client_c.build_workspace(&project_c, cx_c);
159 let workspace_c = window_c.root(cx_c);
160 active_call_c
161 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
162 .await
163 .unwrap();
164 drop(project_c);
165
166 // Client C also follows client A.
167 workspace_c
168 .update(cx_c, |workspace, cx| {
169 workspace.follow(peer_id_a, cx).unwrap()
170 })
171 .await
172 .unwrap();
173
174 cx_d.foreground().run_until_parked();
175 let active_call_d = cx_d.read(ActiveCall::global);
176 let project_d = client_d.build_remote_project(project_id, cx_d).await;
177 let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
178 active_call_d
179 .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
180 .await
181 .unwrap();
182 drop(project_d);
183
184 // All clients see that clients B and C are following client A.
185 cx_c.foreground().run_until_parked();
186 for (name, active_call, cx) in [
187 ("A", &active_call_a, &cx_a),
188 ("B", &active_call_b, &cx_b),
189 ("C", &active_call_c, &cx_c),
190 ("D", &active_call_d, &cx_d),
191 ] {
192 active_call.read_with(*cx, |call, cx| {
193 let room = call.room().unwrap().read(cx);
194 assert_eq!(
195 room.followers_for(peer_id_a, project_id),
196 &[peer_id_b, peer_id_c],
197 "checking followers for A as {name}"
198 );
199 });
200 }
201
202 // Client C unfollows client A.
203 workspace_c.update(cx_c, |workspace, cx| {
204 workspace.unfollow(&workspace.active_pane().clone(), cx);
205 });
206
207 // All clients see that clients B is following client A.
208 cx_c.foreground().run_until_parked();
209 for (name, active_call, cx) in [
210 ("A", &active_call_a, &cx_a),
211 ("B", &active_call_b, &cx_b),
212 ("C", &active_call_c, &cx_c),
213 ("D", &active_call_d, &cx_d),
214 ] {
215 active_call.read_with(*cx, |call, cx| {
216 let room = call.room().unwrap().read(cx);
217 assert_eq!(
218 room.followers_for(peer_id_a, project_id),
219 &[peer_id_b],
220 "checking followers for A as {name}"
221 );
222 });
223 }
224
225 // Client C re-follows client A.
226 workspace_c.update(cx_c, |workspace, cx| {
227 workspace.follow(peer_id_a, cx);
228 });
229
230 // All clients see that clients B and C are following client A.
231 cx_c.foreground().run_until_parked();
232 for (name, active_call, cx) in [
233 ("A", &active_call_a, &cx_a),
234 ("B", &active_call_b, &cx_b),
235 ("C", &active_call_c, &cx_c),
236 ("D", &active_call_d, &cx_d),
237 ] {
238 active_call.read_with(*cx, |call, cx| {
239 let room = call.room().unwrap().read(cx);
240 assert_eq!(
241 room.followers_for(peer_id_a, project_id),
242 &[peer_id_b, peer_id_c],
243 "checking followers for A as {name}"
244 );
245 });
246 }
247
248 // Client D follows client C.
249 workspace_d
250 .update(cx_d, |workspace, cx| {
251 workspace.follow(peer_id_c, cx).unwrap()
252 })
253 .await
254 .unwrap();
255
256 // All clients see that D is following C
257 cx_d.foreground().run_until_parked();
258 for (name, active_call, cx) in [
259 ("A", &active_call_a, &cx_a),
260 ("B", &active_call_b, &cx_b),
261 ("C", &active_call_c, &cx_c),
262 ("D", &active_call_d, &cx_d),
263 ] {
264 active_call.read_with(*cx, |call, cx| {
265 let room = call.room().unwrap().read(cx);
266 assert_eq!(
267 room.followers_for(peer_id_c, project_id),
268 &[peer_id_d],
269 "checking followers for C as {name}"
270 );
271 });
272 }
273
274 // Client C closes the project.
275 window_c.remove(cx_c);
276 cx_c.drop_last(workspace_c);
277
278 // Clients A and B see that client B is following A, and client C is not present in the followers.
279 cx_c.foreground().run_until_parked();
280 for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
281 active_call.read_with(*cx, |call, cx| {
282 let room = call.room().unwrap().read(cx);
283 assert_eq!(
284 room.followers_for(peer_id_a, project_id),
285 &[peer_id_b],
286 "checking followers for A as {name}"
287 );
288 });
289 }
290
291 // All clients see that no-one is following C
292 for (name, active_call, cx) in [
293 ("A", &active_call_a, &cx_a),
294 ("B", &active_call_b, &cx_b),
295 ("C", &active_call_c, &cx_c),
296 ("D", &active_call_d, &cx_d),
297 ] {
298 active_call.read_with(*cx, |call, cx| {
299 let room = call.room().unwrap().read(cx);
300 assert_eq!(
301 room.followers_for(peer_id_c, project_id),
302 &[],
303 "checking followers for C as {name}"
304 );
305 });
306 }
307
308 // When client A activates a different editor, client B does so as well.
309 workspace_a.update(cx_a, |workspace, cx| {
310 workspace.activate_item(&editor_a1, cx)
311 });
312 deterministic.run_until_parked();
313 workspace_b.read_with(cx_b, |workspace, cx| {
314 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
315 });
316
317 // When client A opens a multibuffer, client B does so as well.
318 let multibuffer_a = cx_a.add_model(|cx| {
319 let buffer_a1 = project_a.update(cx, |project, cx| {
320 project
321 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
322 .unwrap()
323 });
324 let buffer_a2 = project_a.update(cx, |project, cx| {
325 project
326 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
327 .unwrap()
328 });
329 let mut result = MultiBuffer::new(0);
330 result.push_excerpts(
331 buffer_a1,
332 [ExcerptRange {
333 context: 0..3,
334 primary: None,
335 }],
336 cx,
337 );
338 result.push_excerpts(
339 buffer_a2,
340 [ExcerptRange {
341 context: 4..7,
342 primary: None,
343 }],
344 cx,
345 );
346 result
347 });
348 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
349 let editor =
350 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
351 workspace.add_item(Box::new(editor.clone()), cx);
352 editor
353 });
354 deterministic.run_until_parked();
355 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
356 workspace
357 .active_item(cx)
358 .unwrap()
359 .downcast::<Editor>()
360 .unwrap()
361 });
362 assert_eq!(
363 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
364 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
365 );
366
367 // When client A navigates back and forth, client B does so as well.
368 workspace_a
369 .update(cx_a, |workspace, cx| {
370 workspace.go_back(workspace.active_pane().downgrade(), cx)
371 })
372 .await
373 .unwrap();
374 deterministic.run_until_parked();
375 workspace_b.read_with(cx_b, |workspace, cx| {
376 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
377 });
378
379 workspace_a
380 .update(cx_a, |workspace, cx| {
381 workspace.go_back(workspace.active_pane().downgrade(), cx)
382 })
383 .await
384 .unwrap();
385 deterministic.run_until_parked();
386 workspace_b.read_with(cx_b, |workspace, cx| {
387 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
388 });
389
390 workspace_a
391 .update(cx_a, |workspace, cx| {
392 workspace.go_forward(workspace.active_pane().downgrade(), cx)
393 })
394 .await
395 .unwrap();
396 deterministic.run_until_parked();
397 workspace_b.read_with(cx_b, |workspace, cx| {
398 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
399 });
400
401 // Changes to client A's editor are reflected on client B.
402 editor_a1.update(cx_a, |editor, cx| {
403 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
404 });
405 deterministic.run_until_parked();
406 editor_b1.read_with(cx_b, |editor, cx| {
407 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
408 });
409
410 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
411 deterministic.run_until_parked();
412 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
413
414 editor_a1.update(cx_a, |editor, cx| {
415 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
416 editor.set_scroll_position(vec2f(0., 100.), cx);
417 });
418 deterministic.run_until_parked();
419 editor_b1.read_with(cx_b, |editor, cx| {
420 assert_eq!(editor.selections.ranges(cx), &[3..3]);
421 });
422
423 // After unfollowing, client B stops receiving updates from client A.
424 workspace_b.update(cx_b, |workspace, cx| {
425 workspace.unfollow(&workspace.active_pane().clone(), cx)
426 });
427 workspace_a.update(cx_a, |workspace, cx| {
428 workspace.activate_item(&editor_a2, cx)
429 });
430 deterministic.run_until_parked();
431 assert_eq!(
432 workspace_b.read_with(cx_b, |workspace, cx| workspace
433 .active_item(cx)
434 .unwrap()
435 .id()),
436 editor_b1.id()
437 );
438
439 // Client A starts following client B.
440 workspace_a
441 .update(cx_a, |workspace, cx| {
442 workspace.follow(peer_id_b, cx).unwrap()
443 })
444 .await
445 .unwrap();
446 assert_eq!(
447 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
448 Some(peer_id_b)
449 );
450 assert_eq!(
451 workspace_a.read_with(cx_a, |workspace, cx| workspace
452 .active_item(cx)
453 .unwrap()
454 .id()),
455 editor_a1.id()
456 );
457
458 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
459 let display = MacOSDisplay::new();
460 active_call_b
461 .update(cx_b, |call, cx| call.set_location(None, cx))
462 .await
463 .unwrap();
464 active_call_b
465 .update(cx_b, |call, cx| {
466 call.room().unwrap().update(cx, |room, cx| {
467 room.set_display_sources(vec![display.clone()]);
468 room.share_screen(cx)
469 })
470 })
471 .await
472 .unwrap();
473 deterministic.run_until_parked();
474 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
475 workspace
476 .active_item(cx)
477 .expect("no active item")
478 .downcast::<SharedScreen>()
479 .expect("active item isn't a shared screen")
480 });
481
482 // Client B activates Zed again, which causes the previous editor to become focused again.
483 active_call_b
484 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
485 .await
486 .unwrap();
487 deterministic.run_until_parked();
488 workspace_a.read_with(cx_a, |workspace, cx| {
489 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
490 });
491
492 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
493 workspace_b.update(cx_b, |workspace, cx| {
494 workspace.activate_item(&multibuffer_editor_b, cx)
495 });
496 deterministic.run_until_parked();
497 workspace_a.read_with(cx_a, |workspace, cx| {
498 assert_eq!(
499 workspace.active_item(cx).unwrap().id(),
500 multibuffer_editor_a.id()
501 )
502 });
503
504 // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
505 let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
506 workspace_b.update(cx_b, |workspace, cx| {
507 workspace.add_panel(panel, cx);
508 workspace.toggle_panel_focus::<TestPanel>(cx);
509 });
510 deterministic.run_until_parked();
511 assert_eq!(
512 workspace_a.read_with(cx_a, |workspace, cx| workspace
513 .active_item(cx)
514 .unwrap()
515 .id()),
516 shared_screen.id()
517 );
518
519 // Toggling the focus back to the pane causes client A to return to the multibuffer.
520 workspace_b.update(cx_b, |workspace, cx| {
521 workspace.toggle_panel_focus::<TestPanel>(cx);
522 });
523 deterministic.run_until_parked();
524 workspace_a.read_with(cx_a, |workspace, cx| {
525 assert_eq!(
526 workspace.active_item(cx).unwrap().id(),
527 multibuffer_editor_a.id()
528 )
529 });
530
531 // Client B activates an item that doesn't implement following,
532 // so the previously-opened screen-sharing item gets activated.
533 let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
534 workspace_b.update(cx_b, |workspace, cx| {
535 workspace.active_pane().update(cx, |pane, cx| {
536 pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
537 })
538 });
539 deterministic.run_until_parked();
540 assert_eq!(
541 workspace_a.read_with(cx_a, |workspace, cx| workspace
542 .active_item(cx)
543 .unwrap()
544 .id()),
545 shared_screen.id()
546 );
547
548 // Following interrupts when client B disconnects.
549 client_b.disconnect(&cx_b.to_async());
550 deterministic.advance_clock(RECONNECT_TIMEOUT);
551 assert_eq!(
552 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
553 None
554 );
555}
556
557#[gpui::test]
558async fn test_following_tab_order(
559 deterministic: Arc<Deterministic>,
560 cx_a: &mut TestAppContext,
561 cx_b: &mut TestAppContext,
562) {
563 let mut server = TestServer::start(&deterministic).await;
564 let client_a = server.create_client(cx_a, "user_a").await;
565 let client_b = server.create_client(cx_b, "user_b").await;
566 server
567 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
568 .await;
569 let active_call_a = cx_a.read(ActiveCall::global);
570 let active_call_b = cx_b.read(ActiveCall::global);
571
572 cx_a.update(editor::init);
573 cx_b.update(editor::init);
574
575 client_a
576 .fs()
577 .insert_tree(
578 "/a",
579 json!({
580 "1.txt": "one",
581 "2.txt": "two",
582 "3.txt": "three",
583 }),
584 )
585 .await;
586 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
587 active_call_a
588 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
589 .await
590 .unwrap();
591
592 let project_id = active_call_a
593 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
594 .await
595 .unwrap();
596 let project_b = client_b.build_remote_project(project_id, cx_b).await;
597 active_call_b
598 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
599 .await
600 .unwrap();
601
602 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
603 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
604
605 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
606 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
607
608 let client_b_id = project_a.read_with(cx_a, |project, _| {
609 project.collaborators().values().next().unwrap().peer_id
610 });
611
612 //Open 1, 3 in that order on client A
613 workspace_a
614 .update(cx_a, |workspace, cx| {
615 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
616 })
617 .await
618 .unwrap();
619 workspace_a
620 .update(cx_a, |workspace, cx| {
621 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
622 })
623 .await
624 .unwrap();
625
626 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
627 pane.update(cx, |pane, cx| {
628 pane.items()
629 .map(|item| {
630 item.project_path(cx)
631 .unwrap()
632 .path
633 .to_str()
634 .unwrap()
635 .to_owned()
636 })
637 .collect::<Vec<_>>()
638 })
639 };
640
641 //Verify that the tabs opened in the order we expect
642 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
643
644 //Follow client B as client A
645 workspace_a
646 .update(cx_a, |workspace, cx| {
647 workspace.follow(client_b_id, cx).unwrap()
648 })
649 .await
650 .unwrap();
651
652 //Open just 2 on client B
653 workspace_b
654 .update(cx_b, |workspace, cx| {
655 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
656 })
657 .await
658 .unwrap();
659 deterministic.run_until_parked();
660
661 // Verify that newly opened followed file is at the end
662 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
663
664 //Open just 1 on client B
665 workspace_b
666 .update(cx_b, |workspace, cx| {
667 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
668 })
669 .await
670 .unwrap();
671 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
672 deterministic.run_until_parked();
673
674 // Verify that following into 1 did not reorder
675 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
676}
677
678#[gpui::test(iterations = 10)]
679async fn test_peers_following_each_other(
680 deterministic: Arc<Deterministic>,
681 cx_a: &mut TestAppContext,
682 cx_b: &mut TestAppContext,
683) {
684 deterministic.forbid_parking();
685 let mut server = TestServer::start(&deterministic).await;
686 let client_a = server.create_client(cx_a, "user_a").await;
687 let client_b = server.create_client(cx_b, "user_b").await;
688 server
689 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
690 .await;
691 let active_call_a = cx_a.read(ActiveCall::global);
692 let active_call_b = cx_b.read(ActiveCall::global);
693
694 cx_a.update(editor::init);
695 cx_b.update(editor::init);
696
697 // Client A shares a project.
698 client_a
699 .fs()
700 .insert_tree(
701 "/a",
702 json!({
703 "1.txt": "one",
704 "2.txt": "two",
705 "3.txt": "three",
706 "4.txt": "four",
707 }),
708 )
709 .await;
710 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
711 active_call_a
712 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
713 .await
714 .unwrap();
715 let project_id = active_call_a
716 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
717 .await
718 .unwrap();
719
720 // Client B joins the project.
721 let project_b = client_b.build_remote_project(project_id, cx_b).await;
722 active_call_b
723 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
724 .await
725 .unwrap();
726
727 // Client A opens some editors.
728 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
729 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
730 let _editor_a1 = workspace_a
731 .update(cx_a, |workspace, cx| {
732 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
733 })
734 .await
735 .unwrap()
736 .downcast::<Editor>()
737 .unwrap();
738
739 // Client B opens an editor.
740 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
741 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
742 let _editor_b1 = workspace_b
743 .update(cx_b, |workspace, cx| {
744 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
745 })
746 .await
747 .unwrap()
748 .downcast::<Editor>()
749 .unwrap();
750
751 // Clients A and B follow each other in split panes
752 workspace_a.update(cx_a, |workspace, cx| {
753 workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
754 });
755 workspace_a
756 .update(cx_a, |workspace, cx| {
757 assert_ne!(*workspace.active_pane(), pane_a1);
758 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
759 workspace.follow(leader_id, cx).unwrap()
760 })
761 .await
762 .unwrap();
763 workspace_b.update(cx_b, |workspace, cx| {
764 workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
765 });
766 workspace_b
767 .update(cx_b, |workspace, cx| {
768 assert_ne!(*workspace.active_pane(), pane_b1);
769 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
770 workspace.follow(leader_id, cx).unwrap()
771 })
772 .await
773 .unwrap();
774
775 workspace_a.update(cx_a, |workspace, cx| {
776 workspace.activate_next_pane(cx);
777 });
778 // Wait for focus effects to be fully flushed
779 workspace_a.update(cx_a, |workspace, _| {
780 assert_eq!(*workspace.active_pane(), pane_a1);
781 });
782
783 workspace_a
784 .update(cx_a, |workspace, cx| {
785 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
786 })
787 .await
788 .unwrap();
789 workspace_b.update(cx_b, |workspace, cx| {
790 workspace.activate_next_pane(cx);
791 });
792
793 workspace_b
794 .update(cx_b, |workspace, cx| {
795 assert_eq!(*workspace.active_pane(), pane_b1);
796 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
797 })
798 .await
799 .unwrap();
800 cx_a.foreground().run_until_parked();
801
802 // Ensure leader updates don't change the active pane of followers
803 workspace_a.read_with(cx_a, |workspace, _| {
804 assert_eq!(*workspace.active_pane(), pane_a1);
805 });
806 workspace_b.read_with(cx_b, |workspace, _| {
807 assert_eq!(*workspace.active_pane(), pane_b1);
808 });
809
810 // Ensure peers following each other doesn't cause an infinite loop.
811 assert_eq!(
812 workspace_a.read_with(cx_a, |workspace, cx| workspace
813 .active_item(cx)
814 .unwrap()
815 .project_path(cx)),
816 Some((worktree_id, "3.txt").into())
817 );
818 workspace_a.update(cx_a, |workspace, cx| {
819 assert_eq!(
820 workspace.active_item(cx).unwrap().project_path(cx),
821 Some((worktree_id, "3.txt").into())
822 );
823 workspace.activate_next_pane(cx);
824 });
825
826 workspace_a.update(cx_a, |workspace, cx| {
827 assert_eq!(
828 workspace.active_item(cx).unwrap().project_path(cx),
829 Some((worktree_id, "4.txt").into())
830 );
831 });
832
833 workspace_b.update(cx_b, |workspace, cx| {
834 assert_eq!(
835 workspace.active_item(cx).unwrap().project_path(cx),
836 Some((worktree_id, "4.txt").into())
837 );
838 workspace.activate_next_pane(cx);
839 });
840
841 workspace_b.update(cx_b, |workspace, cx| {
842 assert_eq!(
843 workspace.active_item(cx).unwrap().project_path(cx),
844 Some((worktree_id, "3.txt").into())
845 );
846 });
847}
848
849#[gpui::test(iterations = 10)]
850async fn test_auto_unfollowing(
851 deterministic: Arc<Deterministic>,
852 cx_a: &mut TestAppContext,
853 cx_b: &mut TestAppContext,
854) {
855 deterministic.forbid_parking();
856
857 // 2 clients connect to a server.
858 let mut server = TestServer::start(&deterministic).await;
859 let client_a = server.create_client(cx_a, "user_a").await;
860 let client_b = server.create_client(cx_b, "user_b").await;
861 server
862 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
863 .await;
864 let active_call_a = cx_a.read(ActiveCall::global);
865 let active_call_b = cx_b.read(ActiveCall::global);
866
867 cx_a.update(editor::init);
868 cx_b.update(editor::init);
869
870 // Client A shares a project.
871 client_a
872 .fs()
873 .insert_tree(
874 "/a",
875 json!({
876 "1.txt": "one",
877 "2.txt": "two",
878 "3.txt": "three",
879 }),
880 )
881 .await;
882 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
883 active_call_a
884 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
885 .await
886 .unwrap();
887
888 let project_id = active_call_a
889 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
890 .await
891 .unwrap();
892 let project_b = client_b.build_remote_project(project_id, cx_b).await;
893 active_call_b
894 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
895 .await
896 .unwrap();
897
898 // Client A opens some editors.
899 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
900 let _editor_a1 = workspace_a
901 .update(cx_a, |workspace, cx| {
902 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
903 })
904 .await
905 .unwrap()
906 .downcast::<Editor>()
907 .unwrap();
908
909 // Client B starts following client A.
910 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
911 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
912 let leader_id = project_b.read_with(cx_b, |project, _| {
913 project.collaborators().values().next().unwrap().peer_id
914 });
915 workspace_b
916 .update(cx_b, |workspace, cx| {
917 workspace.follow(leader_id, cx).unwrap()
918 })
919 .await
920 .unwrap();
921 assert_eq!(
922 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
923 Some(leader_id)
924 );
925 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
926 workspace
927 .active_item(cx)
928 .unwrap()
929 .downcast::<Editor>()
930 .unwrap()
931 });
932
933 // When client B moves, it automatically stops following client A.
934 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
935 assert_eq!(
936 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
937 None
938 );
939
940 workspace_b
941 .update(cx_b, |workspace, cx| {
942 workspace.follow(leader_id, cx).unwrap()
943 })
944 .await
945 .unwrap();
946 assert_eq!(
947 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
948 Some(leader_id)
949 );
950
951 // When client B edits, it automatically stops following client A.
952 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
953 assert_eq!(
954 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
955 None
956 );
957
958 workspace_b
959 .update(cx_b, |workspace, cx| {
960 workspace.follow(leader_id, cx).unwrap()
961 })
962 .await
963 .unwrap();
964 assert_eq!(
965 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
966 Some(leader_id)
967 );
968
969 // When client B scrolls, it automatically stops following client A.
970 editor_b2.update(cx_b, |editor, cx| {
971 editor.set_scroll_position(vec2f(0., 3.), cx)
972 });
973 assert_eq!(
974 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
975 None
976 );
977
978 workspace_b
979 .update(cx_b, |workspace, cx| {
980 workspace.follow(leader_id, cx).unwrap()
981 })
982 .await
983 .unwrap();
984 assert_eq!(
985 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
986 Some(leader_id)
987 );
988
989 // When client B activates a different pane, it continues following client A in the original pane.
990 workspace_b.update(cx_b, |workspace, cx| {
991 workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
992 });
993 assert_eq!(
994 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
995 Some(leader_id)
996 );
997
998 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
999 assert_eq!(
1000 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1001 Some(leader_id)
1002 );
1003
1004 // When client B activates a different item in the original pane, it automatically stops following client A.
1005 workspace_b
1006 .update(cx_b, |workspace, cx| {
1007 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
1008 })
1009 .await
1010 .unwrap();
1011 assert_eq!(
1012 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1013 None
1014 );
1015}
1016
1017#[gpui::test(iterations = 10)]
1018async fn test_peers_simultaneously_following_each_other(
1019 deterministic: Arc<Deterministic>,
1020 cx_a: &mut TestAppContext,
1021 cx_b: &mut TestAppContext,
1022) {
1023 deterministic.forbid_parking();
1024
1025 let mut server = TestServer::start(&deterministic).await;
1026 let client_a = server.create_client(cx_a, "user_a").await;
1027 let client_b = server.create_client(cx_b, "user_b").await;
1028 server
1029 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1030 .await;
1031 let active_call_a = cx_a.read(ActiveCall::global);
1032
1033 cx_a.update(editor::init);
1034 cx_b.update(editor::init);
1035
1036 client_a.fs().insert_tree("/a", json!({})).await;
1037 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1038 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1039 let project_id = active_call_a
1040 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1041 .await
1042 .unwrap();
1043
1044 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1045 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1046
1047 deterministic.run_until_parked();
1048 let client_a_id = project_b.read_with(cx_b, |project, _| {
1049 project.collaborators().values().next().unwrap().peer_id
1050 });
1051 let client_b_id = project_a.read_with(cx_a, |project, _| {
1052 project.collaborators().values().next().unwrap().peer_id
1053 });
1054
1055 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
1056 workspace.follow(client_b_id, cx).unwrap()
1057 });
1058 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
1059 workspace.follow(client_a_id, cx).unwrap()
1060 });
1061
1062 futures::try_join!(a_follow_b, b_follow_a).unwrap();
1063 workspace_a.read_with(cx_a, |workspace, _| {
1064 assert_eq!(
1065 workspace.leader_for_pane(workspace.active_pane()),
1066 Some(client_b_id)
1067 );
1068 });
1069 workspace_b.read_with(cx_b, |workspace, _| {
1070 assert_eq!(
1071 workspace.leader_for_pane(workspace.active_pane()),
1072 Some(client_a_id)
1073 );
1074 });
1075}
1076
1077fn visible_push_notifications(
1078 cx: &mut TestAppContext,
1079) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
1080 let mut ret = Vec::new();
1081 for window in cx.windows() {
1082 window.read_with(cx, |window| {
1083 if let Some(handle) = window
1084 .root_view()
1085 .clone()
1086 .downcast::<ProjectSharedNotification>()
1087 {
1088 ret.push(handle)
1089 }
1090 });
1091 }
1092 ret
1093}
1094
1095#[gpui::test(iterations = 10)]
1096async fn test_following_across_workspaces(
1097 deterministic: Arc<Deterministic>,
1098 cx_a: &mut TestAppContext,
1099 cx_b: &mut TestAppContext,
1100) {
1101 // a and b join a channel/call
1102 // a shares project 1
1103 // b shares project 2
1104 //
1105 // b follows a: causes project 2 to be joined, and b to follow a.
1106 // b opens a different file in project 2, a follows b
1107 // b opens a different file in project 1, a cannot follow b
1108 // b shares the project, a joins the project and follows b
1109 deterministic.forbid_parking();
1110 let mut server = TestServer::start(&deterministic).await;
1111 let client_a = server.create_client(cx_a, "user_a").await;
1112 let client_b = server.create_client(cx_b, "user_b").await;
1113 cx_a.update(editor::init);
1114 cx_b.update(editor::init);
1115
1116 client_a
1117 .fs()
1118 .insert_tree(
1119 "/a",
1120 json!({
1121 "w.rs": "",
1122 "x.rs": "",
1123 }),
1124 )
1125 .await;
1126
1127 client_b
1128 .fs()
1129 .insert_tree(
1130 "/b",
1131 json!({
1132 "y.rs": "",
1133 "z.rs": "",
1134 }),
1135 )
1136 .await;
1137
1138 server
1139 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1140 .await;
1141 let active_call_a = cx_a.read(ActiveCall::global);
1142 let active_call_b = cx_b.read(ActiveCall::global);
1143
1144 let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
1145 let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
1146
1147 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1148 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1149
1150 cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
1151 cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
1152
1153 active_call_a
1154 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1155 .await
1156 .unwrap();
1157
1158 active_call_a
1159 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1160 .await
1161 .unwrap();
1162 active_call_b
1163 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1164 .await
1165 .unwrap();
1166
1167 workspace_a
1168 .update(cx_a, |workspace, cx| {
1169 workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
1170 })
1171 .await
1172 .unwrap();
1173
1174 deterministic.run_until_parked();
1175 assert_eq!(visible_push_notifications(cx_b).len(), 1);
1176
1177 workspace_b.update(cx_b, |workspace, cx| {
1178 workspace
1179 .follow(client_a.peer_id().unwrap(), cx)
1180 .unwrap()
1181 .detach()
1182 });
1183
1184 deterministic.run_until_parked();
1185 let workspace_b_project_a = cx_b
1186 .windows()
1187 .iter()
1188 .max_by_key(|window| window.id())
1189 .unwrap()
1190 .downcast::<Workspace>()
1191 .unwrap()
1192 .root(cx_b);
1193
1194 // assert that b is following a in project a in w.rs
1195 workspace_b_project_a.update(cx_b, |workspace, cx| {
1196 assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
1197 assert_eq!(
1198 client_a.peer_id(),
1199 workspace.leader_for_pane(workspace.active_pane())
1200 );
1201 let item = workspace.active_item(cx).unwrap();
1202 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs"));
1203 });
1204
1205 // TODO: in app code, this would be done by the collab_ui.
1206 active_call_b
1207 .update(cx_b, |call, cx| {
1208 let project = workspace_b_project_a.read(cx).project().clone();
1209 call.set_location(Some(&project), cx)
1210 })
1211 .await
1212 .unwrap();
1213
1214 // assert that there are no share notifications open
1215 assert_eq!(visible_push_notifications(cx_b).len(), 0);
1216
1217 // b moves to x.rs in a's project, and a follows
1218 workspace_b_project_a
1219 .update(cx_b, |workspace, cx| {
1220 workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
1221 })
1222 .await
1223 .unwrap();
1224
1225 deterministic.run_until_parked();
1226 workspace_b_project_a.update(cx_b, |workspace, cx| {
1227 let item = workspace.active_item(cx).unwrap();
1228 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
1229 });
1230
1231 workspace_a.update(cx_a, |workspace, cx| {
1232 workspace
1233 .follow(client_b.peer_id().unwrap(), cx)
1234 .unwrap()
1235 .detach()
1236 });
1237
1238 deterministic.run_until_parked();
1239 workspace_a.update(cx_a, |workspace, cx| {
1240 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1241 assert_eq!(
1242 client_b.peer_id(),
1243 workspace.leader_for_pane(workspace.active_pane())
1244 );
1245 let item = workspace.active_pane().read(cx).active_item().unwrap();
1246 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
1247 });
1248
1249 // b moves to y.rs in b's project, a is still following but can't yet see
1250 workspace_b
1251 .update(cx_b, |workspace, cx| {
1252 workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
1253 })
1254 .await
1255 .unwrap();
1256
1257 // TODO: in app code, this would be done by the collab_ui.
1258 active_call_b
1259 .update(cx_b, |call, cx| {
1260 let project = workspace_b.read(cx).project().clone();
1261 call.set_location(Some(&project), cx)
1262 })
1263 .await
1264 .unwrap();
1265
1266 let project_b_id = active_call_b
1267 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1268 .await
1269 .unwrap();
1270
1271 deterministic.run_until_parked();
1272 assert_eq!(visible_push_notifications(cx_a).len(), 1);
1273 cx_a.update(|cx| {
1274 workspace::join_remote_project(
1275 project_b_id,
1276 client_b.user_id().unwrap(),
1277 client_a.app_state.clone(),
1278 cx,
1279 )
1280 })
1281 .await
1282 .unwrap();
1283
1284 deterministic.run_until_parked();
1285
1286 assert_eq!(visible_push_notifications(cx_a).len(), 0);
1287 let workspace_a_project_b = cx_a
1288 .windows()
1289 .iter()
1290 .max_by_key(|window| window.id())
1291 .unwrap()
1292 .downcast::<Workspace>()
1293 .unwrap()
1294 .root(cx_a);
1295
1296 workspace_a_project_b.update(cx_a, |workspace, cx| {
1297 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1298 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1299 assert_eq!(
1300 client_b.peer_id(),
1301 workspace.leader_for_pane(workspace.active_pane())
1302 );
1303 let item = workspace.active_item(cx).unwrap();
1304 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
1305 });
1306}