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 rpc::proto::PeerId;
8use serde_json::json;
9use std::{borrow::Cow, sync::Arc};
10use workspace::{
11 dock::{test::TestPanel, DockPosition},
12 item::{test::TestItem, ItemHandle as _},
13 shared_screen::SharedScreen,
14 SplitDirection, Workspace,
15};
16
17#[gpui::test(iterations = 10)]
18async fn test_basic_following(
19 deterministic: Arc<Deterministic>,
20 cx_a: &mut TestAppContext,
21 cx_b: &mut TestAppContext,
22 cx_c: &mut TestAppContext,
23 cx_d: &mut TestAppContext,
24) {
25 deterministic.forbid_parking();
26
27 let mut server = TestServer::start(&deterministic).await;
28 let client_a = server.create_client(cx_a, "user_a").await;
29 let client_b = server.create_client(cx_b, "user_b").await;
30 let client_c = server.create_client(cx_c, "user_c").await;
31 let client_d = server.create_client(cx_d, "user_d").await;
32 server
33 .create_room(&mut [
34 (&client_a, cx_a),
35 (&client_b, cx_b),
36 (&client_c, cx_c),
37 (&client_d, cx_d),
38 ])
39 .await;
40 let active_call_a = cx_a.read(ActiveCall::global);
41 let active_call_b = cx_b.read(ActiveCall::global);
42
43 cx_a.update(editor::init);
44 cx_b.update(editor::init);
45
46 client_a
47 .fs()
48 .insert_tree(
49 "/a",
50 json!({
51 "1.txt": "one\none\none",
52 "2.txt": "two\ntwo\ntwo",
53 "3.txt": "three\nthree\nthree",
54 }),
55 )
56 .await;
57 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
58 active_call_a
59 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
60 .await
61 .unwrap();
62
63 let project_id = active_call_a
64 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
65 .await
66 .unwrap();
67 let project_b = client_b.build_remote_project(project_id, cx_b).await;
68 active_call_b
69 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
70 .await
71 .unwrap();
72
73 let window_a = client_a.build_workspace(&project_a, cx_a);
74 let workspace_a = window_a.root(cx_a);
75 let window_b = client_b.build_workspace(&project_b, cx_b);
76 let workspace_b = window_b.root(cx_b);
77
78 // Client A opens some editors.
79 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
80 let editor_a1 = workspace_a
81 .update(cx_a, |workspace, cx| {
82 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
83 })
84 .await
85 .unwrap()
86 .downcast::<Editor>()
87 .unwrap();
88 let editor_a2 = workspace_a
89 .update(cx_a, |workspace, cx| {
90 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
91 })
92 .await
93 .unwrap()
94 .downcast::<Editor>()
95 .unwrap();
96
97 // Client B opens an editor.
98 let editor_b1 = workspace_b
99 .update(cx_b, |workspace, cx| {
100 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
101 })
102 .await
103 .unwrap()
104 .downcast::<Editor>()
105 .unwrap();
106
107 let peer_id_a = client_a.peer_id().unwrap();
108 let peer_id_b = client_b.peer_id().unwrap();
109 let peer_id_c = client_c.peer_id().unwrap();
110 let peer_id_d = client_d.peer_id().unwrap();
111
112 // Client A updates their selections in those editors
113 editor_a1.update(cx_a, |editor, cx| {
114 editor.handle_input("a", cx);
115 editor.handle_input("b", cx);
116 editor.handle_input("c", cx);
117 editor.select_left(&Default::default(), cx);
118 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
119 });
120 editor_a2.update(cx_a, |editor, cx| {
121 editor.handle_input("d", cx);
122 editor.handle_input("e", cx);
123 editor.select_left(&Default::default(), cx);
124 assert_eq!(editor.selections.ranges(cx), vec![2..1]);
125 });
126
127 // When client B starts following client A, all visible view states are replicated to client B.
128 workspace_b
129 .update(cx_b, |workspace, cx| {
130 workspace.follow(peer_id_a, cx).unwrap()
131 })
132 .await
133 .unwrap();
134
135 cx_c.foreground().run_until_parked();
136 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
137 workspace
138 .active_item(cx)
139 .unwrap()
140 .downcast::<Editor>()
141 .unwrap()
142 });
143 assert_eq!(
144 cx_b.read(|cx| editor_b2.project_path(cx)),
145 Some((worktree_id, "2.txt").into())
146 );
147 assert_eq!(
148 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
149 vec![2..1]
150 );
151 assert_eq!(
152 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
153 vec![3..2]
154 );
155
156 cx_c.foreground().run_until_parked();
157 let active_call_c = cx_c.read(ActiveCall::global);
158 let project_c = client_c.build_remote_project(project_id, cx_c).await;
159 let window_c = client_c.build_workspace(&project_c, cx_c);
160 let workspace_c = window_c.root(cx_c);
161 active_call_c
162 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
163 .await
164 .unwrap();
165 drop(project_c);
166
167 // Client C also follows client A.
168 workspace_c
169 .update(cx_c, |workspace, cx| {
170 workspace.follow(peer_id_a, cx).unwrap()
171 })
172 .await
173 .unwrap();
174
175 cx_d.foreground().run_until_parked();
176 let active_call_d = cx_d.read(ActiveCall::global);
177 let project_d = client_d.build_remote_project(project_id, cx_d).await;
178 let workspace_d = client_d.build_workspace(&project_d, cx_d).root(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.foreground().run_until_parked();
187 for (name, active_call, cx) in [
188 ("A", &active_call_a, &cx_a),
189 ("B", &active_call_b, &cx_b),
190 ("C", &active_call_c, &cx_c),
191 ("D", &active_call_d, &cx_d),
192 ] {
193 active_call.read_with(*cx, |call, cx| {
194 let room = call.room().unwrap().read(cx);
195 assert_eq!(
196 room.followers_for(peer_id_a, project_id),
197 &[peer_id_b, peer_id_c],
198 "checking followers for A as {name}"
199 );
200 });
201 }
202
203 // Client C unfollows client A.
204 workspace_c.update(cx_c, |workspace, cx| {
205 workspace.unfollow(&workspace.active_pane().clone(), cx);
206 });
207
208 // All clients see that clients B is following client A.
209 cx_c.foreground().run_until_parked();
210 for (name, active_call, cx) in [
211 ("A", &active_call_a, &cx_a),
212 ("B", &active_call_b, &cx_b),
213 ("C", &active_call_c, &cx_c),
214 ("D", &active_call_d, &cx_d),
215 ] {
216 active_call.read_with(*cx, |call, cx| {
217 let room = call.room().unwrap().read(cx);
218 assert_eq!(
219 room.followers_for(peer_id_a, project_id),
220 &[peer_id_b],
221 "checking followers for A as {name}"
222 );
223 });
224 }
225
226 // Client C re-follows client A.
227 workspace_c.update(cx_c, |workspace, cx| {
228 workspace.follow(peer_id_a, cx);
229 });
230
231 // All clients see that clients B and C are following client A.
232 cx_c.foreground().run_until_parked();
233 for (name, active_call, cx) in [
234 ("A", &active_call_a, &cx_a),
235 ("B", &active_call_b, &cx_b),
236 ("C", &active_call_c, &cx_c),
237 ("D", &active_call_d, &cx_d),
238 ] {
239 active_call.read_with(*cx, |call, cx| {
240 let room = call.room().unwrap().read(cx);
241 assert_eq!(
242 room.followers_for(peer_id_a, project_id),
243 &[peer_id_b, peer_id_c],
244 "checking followers for A as {name}"
245 );
246 });
247 }
248
249 // Client D follows client C.
250 workspace_d
251 .update(cx_d, |workspace, cx| {
252 workspace.follow(peer_id_c, cx).unwrap()
253 })
254 .await
255 .unwrap();
256
257 // All clients see that D is following C
258 cx_d.foreground().run_until_parked();
259 for (name, active_call, cx) in [
260 ("A", &active_call_a, &cx_a),
261 ("B", &active_call_b, &cx_b),
262 ("C", &active_call_c, &cx_c),
263 ("D", &active_call_d, &cx_d),
264 ] {
265 active_call.read_with(*cx, |call, cx| {
266 let room = call.room().unwrap().read(cx);
267 assert_eq!(
268 room.followers_for(peer_id_c, project_id),
269 &[peer_id_d],
270 "checking followers for C as {name}"
271 );
272 });
273 }
274
275 // Client C closes the project.
276 window_c.remove(cx_c);
277 cx_c.drop_last(workspace_c);
278
279 // Clients A and B see that client B is following A, and client C is not present in the followers.
280 cx_c.foreground().run_until_parked();
281 for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
282 active_call.read_with(*cx, |call, cx| {
283 let room = call.room().unwrap().read(cx);
284 assert_eq!(
285 room.followers_for(peer_id_a, project_id),
286 &[peer_id_b],
287 "checking followers for A as {name}"
288 );
289 });
290 }
291
292 // All clients see that no-one is following C
293 for (name, active_call, cx) in [
294 ("A", &active_call_a, &cx_a),
295 ("B", &active_call_b, &cx_b),
296 ("C", &active_call_c, &cx_c),
297 ("D", &active_call_d, &cx_d),
298 ] {
299 active_call.read_with(*cx, |call, cx| {
300 let room = call.room().unwrap().read(cx);
301 assert_eq!(
302 room.followers_for(peer_id_c, project_id),
303 &[],
304 "checking followers for C as {name}"
305 );
306 });
307 }
308
309 // When client A activates a different editor, client B does so as well.
310 workspace_a.update(cx_a, |workspace, cx| {
311 workspace.activate_item(&editor_a1, cx)
312 });
313 deterministic.run_until_parked();
314 workspace_b.read_with(cx_b, |workspace, cx| {
315 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
316 });
317
318 // When client A opens a multibuffer, client B does so as well.
319 let multibuffer_a = cx_a.add_model(|cx| {
320 let buffer_a1 = project_a.update(cx, |project, cx| {
321 project
322 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
323 .unwrap()
324 });
325 let buffer_a2 = project_a.update(cx, |project, cx| {
326 project
327 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
328 .unwrap()
329 });
330 let mut result = MultiBuffer::new(0);
331 result.push_excerpts(
332 buffer_a1,
333 [ExcerptRange {
334 context: 0..3,
335 primary: None,
336 }],
337 cx,
338 );
339 result.push_excerpts(
340 buffer_a2,
341 [ExcerptRange {
342 context: 4..7,
343 primary: None,
344 }],
345 cx,
346 );
347 result
348 });
349 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
350 let editor =
351 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
352 workspace.add_item(Box::new(editor.clone()), cx);
353 editor
354 });
355 deterministic.run_until_parked();
356 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
357 workspace
358 .active_item(cx)
359 .unwrap()
360 .downcast::<Editor>()
361 .unwrap()
362 });
363 assert_eq!(
364 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
365 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
366 );
367
368 // When client A navigates back and forth, client B does so as well.
369 workspace_a
370 .update(cx_a, |workspace, cx| {
371 workspace.go_back(workspace.active_pane().downgrade(), cx)
372 })
373 .await
374 .unwrap();
375 deterministic.run_until_parked();
376 workspace_b.read_with(cx_b, |workspace, cx| {
377 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
378 });
379
380 workspace_a
381 .update(cx_a, |workspace, cx| {
382 workspace.go_back(workspace.active_pane().downgrade(), cx)
383 })
384 .await
385 .unwrap();
386 deterministic.run_until_parked();
387 workspace_b.read_with(cx_b, |workspace, cx| {
388 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
389 });
390
391 workspace_a
392 .update(cx_a, |workspace, cx| {
393 workspace.go_forward(workspace.active_pane().downgrade(), cx)
394 })
395 .await
396 .unwrap();
397 deterministic.run_until_parked();
398 workspace_b.read_with(cx_b, |workspace, cx| {
399 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
400 });
401
402 // Changes to client A's editor are reflected on client B.
403 editor_a1.update(cx_a, |editor, cx| {
404 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
405 });
406 deterministic.run_until_parked();
407 editor_b1.read_with(cx_b, |editor, cx| {
408 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
409 });
410
411 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
412 deterministic.run_until_parked();
413 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
414
415 editor_a1.update(cx_a, |editor, cx| {
416 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
417 editor.set_scroll_position(vec2f(0., 100.), cx);
418 });
419 deterministic.run_until_parked();
420 editor_b1.read_with(cx_b, |editor, cx| {
421 assert_eq!(editor.selections.ranges(cx), &[3..3]);
422 });
423
424 // After unfollowing, client B stops receiving updates from client A.
425 workspace_b.update(cx_b, |workspace, cx| {
426 workspace.unfollow(&workspace.active_pane().clone(), cx)
427 });
428 workspace_a.update(cx_a, |workspace, cx| {
429 workspace.activate_item(&editor_a2, cx)
430 });
431 deterministic.run_until_parked();
432 assert_eq!(
433 workspace_b.read_with(cx_b, |workspace, cx| workspace
434 .active_item(cx)
435 .unwrap()
436 .id()),
437 editor_b1.id()
438 );
439
440 // Client A starts following client B.
441 workspace_a
442 .update(cx_a, |workspace, cx| {
443 workspace.follow(peer_id_b, cx).unwrap()
444 })
445 .await
446 .unwrap();
447 assert_eq!(
448 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
449 Some(peer_id_b)
450 );
451 assert_eq!(
452 workspace_a.read_with(cx_a, |workspace, cx| workspace
453 .active_item(cx)
454 .unwrap()
455 .id()),
456 editor_a1.id()
457 );
458
459 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
460 let display = MacOSDisplay::new();
461 active_call_b
462 .update(cx_b, |call, cx| call.set_location(None, cx))
463 .await
464 .unwrap();
465 active_call_b
466 .update(cx_b, |call, cx| {
467 call.room().unwrap().update(cx, |room, cx| {
468 room.set_display_sources(vec![display.clone()]);
469 room.share_screen(cx)
470 })
471 })
472 .await
473 .unwrap();
474 deterministic.run_until_parked();
475 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
476 workspace
477 .active_item(cx)
478 .expect("no active item")
479 .downcast::<SharedScreen>()
480 .expect("active item isn't a shared screen")
481 });
482
483 // Client B activates Zed again, which causes the previous editor to become focused again.
484 active_call_b
485 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
486 .await
487 .unwrap();
488 deterministic.run_until_parked();
489 workspace_a.read_with(cx_a, |workspace, cx| {
490 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
491 });
492
493 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
494 workspace_b.update(cx_b, |workspace, cx| {
495 workspace.activate_item(&multibuffer_editor_b, cx)
496 });
497 deterministic.run_until_parked();
498 workspace_a.read_with(cx_a, |workspace, cx| {
499 assert_eq!(
500 workspace.active_item(cx).unwrap().id(),
501 multibuffer_editor_a.id()
502 )
503 });
504
505 // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
506 let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
507 workspace_b.update(cx_b, |workspace, cx| {
508 workspace.add_panel(panel, cx);
509 workspace.toggle_panel_focus::<TestPanel>(cx);
510 });
511 deterministic.run_until_parked();
512 assert_eq!(
513 workspace_a.read_with(cx_a, |workspace, cx| workspace
514 .active_item(cx)
515 .unwrap()
516 .id()),
517 shared_screen.id()
518 );
519
520 // Toggling the focus back to the pane causes client A to return to the multibuffer.
521 workspace_b.update(cx_b, |workspace, cx| {
522 workspace.toggle_panel_focus::<TestPanel>(cx);
523 });
524 deterministic.run_until_parked();
525 workspace_a.read_with(cx_a, |workspace, cx| {
526 assert_eq!(
527 workspace.active_item(cx).unwrap().id(),
528 multibuffer_editor_a.id()
529 )
530 });
531
532 // Client B activates an item that doesn't implement following,
533 // so the previously-opened screen-sharing item gets activated.
534 let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
535 workspace_b.update(cx_b, |workspace, cx| {
536 workspace.active_pane().update(cx, |pane, cx| {
537 pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
538 })
539 });
540 deterministic.run_until_parked();
541 assert_eq!(
542 workspace_a.read_with(cx_a, |workspace, cx| workspace
543 .active_item(cx)
544 .unwrap()
545 .id()),
546 shared_screen.id()
547 );
548
549 // Following interrupts when client B disconnects.
550 client_b.disconnect(&cx_b.to_async());
551 deterministic.advance_clock(RECONNECT_TIMEOUT);
552 assert_eq!(
553 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
554 None
555 );
556}
557
558#[gpui::test]
559async fn test_following_tab_order(
560 deterministic: Arc<Deterministic>,
561 cx_a: &mut TestAppContext,
562 cx_b: &mut TestAppContext,
563) {
564 let mut server = TestServer::start(&deterministic).await;
565 let client_a = server.create_client(cx_a, "user_a").await;
566 let client_b = server.create_client(cx_b, "user_b").await;
567 server
568 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
569 .await;
570 let active_call_a = cx_a.read(ActiveCall::global);
571 let active_call_b = cx_b.read(ActiveCall::global);
572
573 cx_a.update(editor::init);
574 cx_b.update(editor::init);
575
576 client_a
577 .fs()
578 .insert_tree(
579 "/a",
580 json!({
581 "1.txt": "one",
582 "2.txt": "two",
583 "3.txt": "three",
584 }),
585 )
586 .await;
587 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
588 active_call_a
589 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
590 .await
591 .unwrap();
592
593 let project_id = active_call_a
594 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
595 .await
596 .unwrap();
597 let project_b = client_b.build_remote_project(project_id, cx_b).await;
598 active_call_b
599 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
600 .await
601 .unwrap();
602
603 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
604 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
605
606 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
607 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
608
609 let client_b_id = project_a.read_with(cx_a, |project, _| {
610 project.collaborators().values().next().unwrap().peer_id
611 });
612
613 //Open 1, 3 in that order on client A
614 workspace_a
615 .update(cx_a, |workspace, cx| {
616 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
617 })
618 .await
619 .unwrap();
620 workspace_a
621 .update(cx_a, |workspace, cx| {
622 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
623 })
624 .await
625 .unwrap();
626
627 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
628 pane.update(cx, |pane, cx| {
629 pane.items()
630 .map(|item| {
631 item.project_path(cx)
632 .unwrap()
633 .path
634 .to_str()
635 .unwrap()
636 .to_owned()
637 })
638 .collect::<Vec<_>>()
639 })
640 };
641
642 //Verify that the tabs opened in the order we expect
643 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
644
645 //Follow client B as client A
646 workspace_a
647 .update(cx_a, |workspace, cx| {
648 workspace.follow(client_b_id, cx).unwrap()
649 })
650 .await
651 .unwrap();
652
653 //Open just 2 on client B
654 workspace_b
655 .update(cx_b, |workspace, cx| {
656 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
657 })
658 .await
659 .unwrap();
660 deterministic.run_until_parked();
661
662 // Verify that newly opened followed file is at the end
663 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
664
665 //Open just 1 on client B
666 workspace_b
667 .update(cx_b, |workspace, cx| {
668 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
669 })
670 .await
671 .unwrap();
672 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
673 deterministic.run_until_parked();
674
675 // Verify that following into 1 did not reorder
676 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
677}
678
679#[gpui::test(iterations = 10)]
680async fn test_peers_following_each_other(
681 deterministic: Arc<Deterministic>,
682 cx_a: &mut TestAppContext,
683 cx_b: &mut TestAppContext,
684) {
685 deterministic.forbid_parking();
686 let mut server = TestServer::start(&deterministic).await;
687 let client_a = server.create_client(cx_a, "user_a").await;
688 let client_b = server.create_client(cx_b, "user_b").await;
689 server
690 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
691 .await;
692 let active_call_a = cx_a.read(ActiveCall::global);
693 let active_call_b = cx_b.read(ActiveCall::global);
694
695 cx_a.update(editor::init);
696 cx_b.update(editor::init);
697
698 // Client A shares a project.
699 client_a
700 .fs()
701 .insert_tree(
702 "/a",
703 json!({
704 "1.txt": "one",
705 "2.txt": "two",
706 "3.txt": "three",
707 "4.txt": "four",
708 }),
709 )
710 .await;
711 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
712 active_call_a
713 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
714 .await
715 .unwrap();
716 let project_id = active_call_a
717 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
718 .await
719 .unwrap();
720
721 // Client B joins the project.
722 let project_b = client_b.build_remote_project(project_id, cx_b).await;
723 active_call_b
724 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
725 .await
726 .unwrap();
727
728 // Client A opens a file.
729 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
730 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 a different file.
740 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
741 workspace_b
742 .update(cx_b, |workspace, cx| {
743 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
744 })
745 .await
746 .unwrap()
747 .downcast::<Editor>()
748 .unwrap();
749
750 // Clients A and B follow each other in split panes
751 workspace_a.update(cx_a, |workspace, cx| {
752 workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
753 });
754 workspace_a
755 .update(cx_a, |workspace, cx| {
756 workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
757 })
758 .await
759 .unwrap();
760 workspace_b.update(cx_b, |workspace, cx| {
761 workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
762 });
763 workspace_b
764 .update(cx_b, |workspace, cx| {
765 workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
766 })
767 .await
768 .unwrap();
769
770 // Clients A and B return focus to the original files they had open
771 workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
772 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
773 deterministic.run_until_parked();
774
775 // Both clients see the other client's focused file in their right pane.
776 assert_eq!(
777 pane_summaries(&workspace_a, cx_a),
778 &[
779 PaneSummary {
780 active: true,
781 leader: None,
782 items: vec![(true, "1.txt".into())]
783 },
784 PaneSummary {
785 active: false,
786 leader: client_b.peer_id(),
787 items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
788 },
789 ]
790 );
791 assert_eq!(
792 pane_summaries(&workspace_b, cx_b),
793 &[
794 PaneSummary {
795 active: true,
796 leader: None,
797 items: vec![(true, "2.txt".into())]
798 },
799 PaneSummary {
800 active: false,
801 leader: client_a.peer_id(),
802 items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
803 },
804 ]
805 );
806
807 // Clients A and B each open a new file.
808 workspace_a
809 .update(cx_a, |workspace, cx| {
810 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
811 })
812 .await
813 .unwrap();
814
815 workspace_b
816 .update(cx_b, |workspace, cx| {
817 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
818 })
819 .await
820 .unwrap();
821 deterministic.run_until_parked();
822
823 // Both client's see the other client open the new file, but keep their
824 // focus on their own active pane.
825 assert_eq!(
826 pane_summaries(&workspace_a, cx_a),
827 &[
828 PaneSummary {
829 active: true,
830 leader: None,
831 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
832 },
833 PaneSummary {
834 active: false,
835 leader: client_b.peer_id(),
836 items: vec![
837 (false, "1.txt".into()),
838 (false, "2.txt".into()),
839 (true, "4.txt".into())
840 ]
841 },
842 ]
843 );
844 assert_eq!(
845 pane_summaries(&workspace_b, cx_b),
846 &[
847 PaneSummary {
848 active: true,
849 leader: None,
850 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
851 },
852 PaneSummary {
853 active: false,
854 leader: client_a.peer_id(),
855 items: vec![
856 (false, "2.txt".into()),
857 (false, "1.txt".into()),
858 (true, "3.txt".into())
859 ]
860 },
861 ]
862 );
863
864 // Client A focuses their right pane, in which they're following client B.
865 workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
866 deterministic.run_until_parked();
867
868 // Client B sees that client A is now looking at the same file as them.
869 assert_eq!(
870 pane_summaries(&workspace_a, cx_a),
871 &[
872 PaneSummary {
873 active: false,
874 leader: None,
875 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
876 },
877 PaneSummary {
878 active: true,
879 leader: client_b.peer_id(),
880 items: vec![
881 (false, "1.txt".into()),
882 (false, "2.txt".into()),
883 (true, "4.txt".into())
884 ]
885 },
886 ]
887 );
888 assert_eq!(
889 pane_summaries(&workspace_b, cx_b),
890 &[
891 PaneSummary {
892 active: true,
893 leader: None,
894 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
895 },
896 PaneSummary {
897 active: false,
898 leader: client_a.peer_id(),
899 items: vec![
900 (false, "2.txt".into()),
901 (false, "1.txt".into()),
902 (false, "3.txt".into()),
903 (true, "4.txt".into())
904 ]
905 },
906 ]
907 );
908
909 // Client B focuses their right pane, in which they're following client A,
910 // who is following them.
911 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
912 deterministic.run_until_parked();
913
914 // Client A sees that client B is now looking at the same file as them.
915 assert_eq!(
916 pane_summaries(&workspace_b, cx_b),
917 &[
918 PaneSummary {
919 active: false,
920 leader: None,
921 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
922 },
923 PaneSummary {
924 active: true,
925 leader: client_a.peer_id(),
926 items: vec![
927 (false, "2.txt".into()),
928 (false, "1.txt".into()),
929 (false, "3.txt".into()),
930 (true, "4.txt".into())
931 ]
932 },
933 ]
934 );
935 assert_eq!(
936 pane_summaries(&workspace_a, cx_a),
937 &[
938 PaneSummary {
939 active: false,
940 leader: None,
941 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
942 },
943 PaneSummary {
944 active: true,
945 leader: client_b.peer_id(),
946 items: vec![
947 (false, "1.txt".into()),
948 (false, "2.txt".into()),
949 (true, "4.txt".into())
950 ]
951 },
952 ]
953 );
954
955 // Client B focuses a file that they previously followed A to, breaking
956 // the follow.
957 workspace_b.update(cx_b, |workspace, cx| {
958 workspace.active_pane().update(cx, |pane, cx| {
959 pane.activate_prev_item(true, cx);
960 });
961 });
962 deterministic.run_until_parked();
963
964 // Both clients see that client B is looking at that previous file.
965 assert_eq!(
966 pane_summaries(&workspace_b, cx_b),
967 &[
968 PaneSummary {
969 active: false,
970 leader: None,
971 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
972 },
973 PaneSummary {
974 active: true,
975 leader: None,
976 items: vec![
977 (false, "2.txt".into()),
978 (false, "1.txt".into()),
979 (true, "3.txt".into()),
980 (false, "4.txt".into())
981 ]
982 },
983 ]
984 );
985 assert_eq!(
986 pane_summaries(&workspace_a, cx_a),
987 &[
988 PaneSummary {
989 active: false,
990 leader: None,
991 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
992 },
993 PaneSummary {
994 active: true,
995 leader: client_b.peer_id(),
996 items: vec![
997 (false, "1.txt".into()),
998 (false, "2.txt".into()),
999 (false, "4.txt".into()),
1000 (true, "3.txt".into()),
1001 ]
1002 },
1003 ]
1004 );
1005
1006 // Client B closes tabs, some of which were originally opened by client A,
1007 // and some of which were originally opened by client B.
1008 workspace_b.update(cx_b, |workspace, cx| {
1009 workspace.active_pane().update(cx, |pane, cx| {
1010 pane.close_inactive_items(&Default::default(), cx)
1011 .unwrap()
1012 .detach();
1013 });
1014 });
1015
1016 deterministic.run_until_parked();
1017
1018 // Both clients see that Client B is looking at the previous tab.
1019 assert_eq!(
1020 pane_summaries(&workspace_b, cx_b),
1021 &[
1022 PaneSummary {
1023 active: false,
1024 leader: None,
1025 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1026 },
1027 PaneSummary {
1028 active: true,
1029 leader: None,
1030 items: vec![(true, "3.txt".into()),]
1031 },
1032 ]
1033 );
1034 assert_eq!(
1035 pane_summaries(&workspace_a, cx_a),
1036 &[
1037 PaneSummary {
1038 active: false,
1039 leader: None,
1040 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1041 },
1042 PaneSummary {
1043 active: true,
1044 leader: client_b.peer_id(),
1045 items: vec![
1046 (false, "1.txt".into()),
1047 (false, "2.txt".into()),
1048 (false, "4.txt".into()),
1049 (true, "3.txt".into()),
1050 ]
1051 },
1052 ]
1053 );
1054
1055 // Client B follows client A again.
1056 workspace_b
1057 .update(cx_b, |workspace, cx| {
1058 workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
1059 })
1060 .await
1061 .unwrap();
1062
1063 // Client A cycles through some tabs.
1064 workspace_a.update(cx_a, |workspace, cx| {
1065 workspace.active_pane().update(cx, |pane, cx| {
1066 pane.activate_prev_item(true, cx);
1067 });
1068 });
1069 deterministic.run_until_parked();
1070
1071 // Client B follows client A into those tabs.
1072 assert_eq!(
1073 pane_summaries(&workspace_a, cx_a),
1074 &[
1075 PaneSummary {
1076 active: false,
1077 leader: None,
1078 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1079 },
1080 PaneSummary {
1081 active: true,
1082 leader: None,
1083 items: vec![
1084 (false, "1.txt".into()),
1085 (false, "2.txt".into()),
1086 (true, "4.txt".into()),
1087 (false, "3.txt".into()),
1088 ]
1089 },
1090 ]
1091 );
1092 assert_eq!(
1093 pane_summaries(&workspace_b, cx_b),
1094 &[
1095 PaneSummary {
1096 active: false,
1097 leader: None,
1098 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1099 },
1100 PaneSummary {
1101 active: true,
1102 leader: client_a.peer_id(),
1103 items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
1104 },
1105 ]
1106 );
1107
1108 workspace_a.update(cx_a, |workspace, cx| {
1109 workspace.active_pane().update(cx, |pane, cx| {
1110 pane.activate_prev_item(true, cx);
1111 });
1112 });
1113 deterministic.run_until_parked();
1114
1115 assert_eq!(
1116 pane_summaries(&workspace_a, cx_a),
1117 &[
1118 PaneSummary {
1119 active: false,
1120 leader: None,
1121 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1122 },
1123 PaneSummary {
1124 active: true,
1125 leader: None,
1126 items: vec![
1127 (false, "1.txt".into()),
1128 (true, "2.txt".into()),
1129 (false, "4.txt".into()),
1130 (false, "3.txt".into()),
1131 ]
1132 },
1133 ]
1134 );
1135 assert_eq!(
1136 pane_summaries(&workspace_b, cx_b),
1137 &[
1138 PaneSummary {
1139 active: false,
1140 leader: None,
1141 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1142 },
1143 PaneSummary {
1144 active: true,
1145 leader: client_a.peer_id(),
1146 items: vec![
1147 (false, "3.txt".into()),
1148 (false, "4.txt".into()),
1149 (true, "2.txt".into())
1150 ]
1151 },
1152 ]
1153 );
1154
1155 workspace_a.update(cx_a, |workspace, cx| {
1156 workspace.active_pane().update(cx, |pane, cx| {
1157 pane.activate_prev_item(true, cx);
1158 });
1159 });
1160 deterministic.run_until_parked();
1161
1162 assert_eq!(
1163 pane_summaries(&workspace_a, cx_a),
1164 &[
1165 PaneSummary {
1166 active: false,
1167 leader: None,
1168 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1169 },
1170 PaneSummary {
1171 active: true,
1172 leader: None,
1173 items: vec![
1174 (true, "1.txt".into()),
1175 (false, "2.txt".into()),
1176 (false, "4.txt".into()),
1177 (false, "3.txt".into()),
1178 ]
1179 },
1180 ]
1181 );
1182 assert_eq!(
1183 pane_summaries(&workspace_b, cx_b),
1184 &[
1185 PaneSummary {
1186 active: false,
1187 leader: None,
1188 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1189 },
1190 PaneSummary {
1191 active: true,
1192 leader: client_a.peer_id(),
1193 items: vec![
1194 (false, "3.txt".into()),
1195 (false, "4.txt".into()),
1196 (false, "2.txt".into()),
1197 (true, "1.txt".into()),
1198 ]
1199 },
1200 ]
1201 );
1202}
1203
1204#[gpui::test(iterations = 10)]
1205async fn test_auto_unfollowing(
1206 deterministic: Arc<Deterministic>,
1207 cx_a: &mut TestAppContext,
1208 cx_b: &mut TestAppContext,
1209) {
1210 deterministic.forbid_parking();
1211
1212 // 2 clients connect to a server.
1213 let mut server = TestServer::start(&deterministic).await;
1214 let client_a = server.create_client(cx_a, "user_a").await;
1215 let client_b = server.create_client(cx_b, "user_b").await;
1216 server
1217 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1218 .await;
1219 let active_call_a = cx_a.read(ActiveCall::global);
1220 let active_call_b = cx_b.read(ActiveCall::global);
1221
1222 cx_a.update(editor::init);
1223 cx_b.update(editor::init);
1224
1225 // Client A shares a project.
1226 client_a
1227 .fs()
1228 .insert_tree(
1229 "/a",
1230 json!({
1231 "1.txt": "one",
1232 "2.txt": "two",
1233 "3.txt": "three",
1234 }),
1235 )
1236 .await;
1237 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1238 active_call_a
1239 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1240 .await
1241 .unwrap();
1242
1243 let project_id = active_call_a
1244 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1245 .await
1246 .unwrap();
1247 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1248 active_call_b
1249 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1250 .await
1251 .unwrap();
1252
1253 // Client A opens some editors.
1254 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1255 let _editor_a1 = workspace_a
1256 .update(cx_a, |workspace, cx| {
1257 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
1258 })
1259 .await
1260 .unwrap()
1261 .downcast::<Editor>()
1262 .unwrap();
1263
1264 // Client B starts following client A.
1265 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1266 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
1267 let leader_id = project_b.read_with(cx_b, |project, _| {
1268 project.collaborators().values().next().unwrap().peer_id
1269 });
1270 workspace_b
1271 .update(cx_b, |workspace, cx| {
1272 workspace.follow(leader_id, cx).unwrap()
1273 })
1274 .await
1275 .unwrap();
1276 assert_eq!(
1277 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1278 Some(leader_id)
1279 );
1280 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
1281 workspace
1282 .active_item(cx)
1283 .unwrap()
1284 .downcast::<Editor>()
1285 .unwrap()
1286 });
1287
1288 // When client B moves, it automatically stops following client A.
1289 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
1290 assert_eq!(
1291 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1292 None
1293 );
1294
1295 workspace_b
1296 .update(cx_b, |workspace, cx| {
1297 workspace.follow(leader_id, cx).unwrap()
1298 })
1299 .await
1300 .unwrap();
1301 assert_eq!(
1302 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1303 Some(leader_id)
1304 );
1305
1306 // When client B edits, it automatically stops following client A.
1307 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
1308 assert_eq!(
1309 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1310 None
1311 );
1312
1313 workspace_b
1314 .update(cx_b, |workspace, cx| {
1315 workspace.follow(leader_id, cx).unwrap()
1316 })
1317 .await
1318 .unwrap();
1319 assert_eq!(
1320 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1321 Some(leader_id)
1322 );
1323
1324 // When client B scrolls, it automatically stops following client A.
1325 editor_b2.update(cx_b, |editor, cx| {
1326 editor.set_scroll_position(vec2f(0., 3.), cx)
1327 });
1328 assert_eq!(
1329 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1330 None
1331 );
1332
1333 workspace_b
1334 .update(cx_b, |workspace, cx| {
1335 workspace.follow(leader_id, cx).unwrap()
1336 })
1337 .await
1338 .unwrap();
1339 assert_eq!(
1340 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1341 Some(leader_id)
1342 );
1343
1344 // When client B activates a different pane, it continues following client A in the original pane.
1345 workspace_b.update(cx_b, |workspace, cx| {
1346 workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
1347 });
1348 assert_eq!(
1349 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1350 Some(leader_id)
1351 );
1352
1353 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
1354 assert_eq!(
1355 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1356 Some(leader_id)
1357 );
1358
1359 // When client B activates a different item in the original pane, it automatically stops following client A.
1360 workspace_b
1361 .update(cx_b, |workspace, cx| {
1362 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
1363 })
1364 .await
1365 .unwrap();
1366 assert_eq!(
1367 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1368 None
1369 );
1370}
1371
1372#[gpui::test(iterations = 10)]
1373async fn test_peers_simultaneously_following_each_other(
1374 deterministic: Arc<Deterministic>,
1375 cx_a: &mut TestAppContext,
1376 cx_b: &mut TestAppContext,
1377) {
1378 deterministic.forbid_parking();
1379
1380 let mut server = TestServer::start(&deterministic).await;
1381 let client_a = server.create_client(cx_a, "user_a").await;
1382 let client_b = server.create_client(cx_b, "user_b").await;
1383 server
1384 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1385 .await;
1386 let active_call_a = cx_a.read(ActiveCall::global);
1387
1388 cx_a.update(editor::init);
1389 cx_b.update(editor::init);
1390
1391 client_a.fs().insert_tree("/a", json!({})).await;
1392 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1393 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1394 let project_id = active_call_a
1395 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1396 .await
1397 .unwrap();
1398
1399 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1400 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1401
1402 deterministic.run_until_parked();
1403 let client_a_id = project_b.read_with(cx_b, |project, _| {
1404 project.collaborators().values().next().unwrap().peer_id
1405 });
1406 let client_b_id = project_a.read_with(cx_a, |project, _| {
1407 project.collaborators().values().next().unwrap().peer_id
1408 });
1409
1410 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
1411 workspace.follow(client_b_id, cx).unwrap()
1412 });
1413 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
1414 workspace.follow(client_a_id, cx).unwrap()
1415 });
1416
1417 futures::try_join!(a_follow_b, b_follow_a).unwrap();
1418 workspace_a.read_with(cx_a, |workspace, _| {
1419 assert_eq!(
1420 workspace.leader_for_pane(workspace.active_pane()),
1421 Some(client_b_id)
1422 );
1423 });
1424 workspace_b.read_with(cx_b, |workspace, _| {
1425 assert_eq!(
1426 workspace.leader_for_pane(workspace.active_pane()),
1427 Some(client_a_id)
1428 );
1429 });
1430}
1431
1432#[gpui::test(iterations = 10)]
1433async fn test_following_across_workspaces(
1434 deterministic: Arc<Deterministic>,
1435 cx_a: &mut TestAppContext,
1436 cx_b: &mut TestAppContext,
1437) {
1438 // a and b join a channel/call
1439 // a shares project 1
1440 // b shares project 2
1441 //
1442 // b follows a: causes project 2 to be joined, and b to follow a.
1443 // b opens a different file in project 2, a follows b
1444 // b opens a different file in project 1, a cannot follow b
1445 // b shares the project, a joins the project and follows b
1446 deterministic.forbid_parking();
1447 let mut server = TestServer::start(&deterministic).await;
1448 let client_a = server.create_client(cx_a, "user_a").await;
1449 let client_b = server.create_client(cx_b, "user_b").await;
1450 cx_a.update(editor::init);
1451 cx_b.update(editor::init);
1452
1453 client_a
1454 .fs()
1455 .insert_tree(
1456 "/a",
1457 json!({
1458 "w.rs": "",
1459 "x.rs": "",
1460 }),
1461 )
1462 .await;
1463
1464 client_b
1465 .fs()
1466 .insert_tree(
1467 "/b",
1468 json!({
1469 "y.rs": "",
1470 "z.rs": "",
1471 }),
1472 )
1473 .await;
1474
1475 server
1476 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1477 .await;
1478 let active_call_a = cx_a.read(ActiveCall::global);
1479 let active_call_b = cx_b.read(ActiveCall::global);
1480
1481 let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
1482 let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
1483
1484 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1485 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1486
1487 cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
1488 cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
1489
1490 active_call_a
1491 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1492 .await
1493 .unwrap();
1494
1495 active_call_a
1496 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1497 .await
1498 .unwrap();
1499 active_call_b
1500 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1501 .await
1502 .unwrap();
1503
1504 workspace_a
1505 .update(cx_a, |workspace, cx| {
1506 workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
1507 })
1508 .await
1509 .unwrap();
1510
1511 deterministic.run_until_parked();
1512 assert_eq!(visible_push_notifications(cx_b).len(), 1);
1513
1514 workspace_b.update(cx_b, |workspace, cx| {
1515 workspace
1516 .follow(client_a.peer_id().unwrap(), cx)
1517 .unwrap()
1518 .detach()
1519 });
1520
1521 deterministic.run_until_parked();
1522 let workspace_b_project_a = cx_b
1523 .windows()
1524 .iter()
1525 .max_by_key(|window| window.id())
1526 .unwrap()
1527 .downcast::<Workspace>()
1528 .unwrap()
1529 .root(cx_b);
1530
1531 // assert that b is following a in project a in w.rs
1532 workspace_b_project_a.update(cx_b, |workspace, cx| {
1533 assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
1534 assert_eq!(
1535 client_a.peer_id(),
1536 workspace.leader_for_pane(workspace.active_pane())
1537 );
1538 let item = workspace.active_item(cx).unwrap();
1539 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs"));
1540 });
1541
1542 // TODO: in app code, this would be done by the collab_ui.
1543 active_call_b
1544 .update(cx_b, |call, cx| {
1545 let project = workspace_b_project_a.read(cx).project().clone();
1546 call.set_location(Some(&project), cx)
1547 })
1548 .await
1549 .unwrap();
1550
1551 // assert that there are no share notifications open
1552 assert_eq!(visible_push_notifications(cx_b).len(), 0);
1553
1554 // b moves to x.rs in a's project, and a follows
1555 workspace_b_project_a
1556 .update(cx_b, |workspace, cx| {
1557 workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
1558 })
1559 .await
1560 .unwrap();
1561
1562 deterministic.run_until_parked();
1563 workspace_b_project_a.update(cx_b, |workspace, cx| {
1564 let item = workspace.active_item(cx).unwrap();
1565 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
1566 });
1567
1568 workspace_a.update(cx_a, |workspace, cx| {
1569 workspace
1570 .follow(client_b.peer_id().unwrap(), cx)
1571 .unwrap()
1572 .detach()
1573 });
1574
1575 deterministic.run_until_parked();
1576 workspace_a.update(cx_a, |workspace, cx| {
1577 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1578 assert_eq!(
1579 client_b.peer_id(),
1580 workspace.leader_for_pane(workspace.active_pane())
1581 );
1582 let item = workspace.active_pane().read(cx).active_item().unwrap();
1583 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
1584 });
1585
1586 // b moves to y.rs in b's project, a is still following but can't yet see
1587 workspace_b
1588 .update(cx_b, |workspace, cx| {
1589 workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
1590 })
1591 .await
1592 .unwrap();
1593
1594 // TODO: in app code, this would be done by the collab_ui.
1595 active_call_b
1596 .update(cx_b, |call, cx| {
1597 let project = workspace_b.read(cx).project().clone();
1598 call.set_location(Some(&project), cx)
1599 })
1600 .await
1601 .unwrap();
1602
1603 let project_b_id = active_call_b
1604 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1605 .await
1606 .unwrap();
1607
1608 deterministic.run_until_parked();
1609 assert_eq!(visible_push_notifications(cx_a).len(), 1);
1610 cx_a.update(|cx| {
1611 workspace::join_remote_project(
1612 project_b_id,
1613 client_b.user_id().unwrap(),
1614 client_a.app_state.clone(),
1615 cx,
1616 )
1617 })
1618 .await
1619 .unwrap();
1620
1621 deterministic.run_until_parked();
1622
1623 assert_eq!(visible_push_notifications(cx_a).len(), 0);
1624 let workspace_a_project_b = cx_a
1625 .windows()
1626 .iter()
1627 .max_by_key(|window| window.id())
1628 .unwrap()
1629 .downcast::<Workspace>()
1630 .unwrap()
1631 .root(cx_a);
1632
1633 workspace_a_project_b.update(cx_a, |workspace, cx| {
1634 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1635 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1636 assert_eq!(
1637 client_b.peer_id(),
1638 workspace.leader_for_pane(workspace.active_pane())
1639 );
1640 let item = workspace.active_item(cx).unwrap();
1641 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
1642 });
1643}
1644
1645fn visible_push_notifications(
1646 cx: &mut TestAppContext,
1647) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
1648 let mut ret = Vec::new();
1649 for window in cx.windows() {
1650 window.read_with(cx, |window| {
1651 if let Some(handle) = window
1652 .root_view()
1653 .clone()
1654 .downcast::<ProjectSharedNotification>()
1655 {
1656 ret.push(handle)
1657 }
1658 });
1659 }
1660 ret
1661}
1662
1663#[derive(Debug, PartialEq, Eq)]
1664struct PaneSummary {
1665 active: bool,
1666 leader: Option<PeerId>,
1667 items: Vec<(bool, String)>,
1668}
1669
1670fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
1671 workspace.read_with(cx, |workspace, cx| {
1672 let active_pane = workspace.active_pane();
1673 workspace
1674 .panes()
1675 .iter()
1676 .map(|pane| {
1677 let leader = workspace.leader_for_pane(pane);
1678 let active = pane == active_pane;
1679 let pane = pane.read(cx);
1680 let active_ix = pane.active_item_index();
1681 PaneSummary {
1682 active,
1683 leader,
1684 items: pane
1685 .items()
1686 .enumerate()
1687 .map(|(ix, item)| {
1688 (
1689 ix == active_ix,
1690 item.tab_description(0, cx)
1691 .map_or(String::new(), |s| s.to_string()),
1692 )
1693 })
1694 .collect(),
1695 }
1696 })
1697 .collect()
1698 })
1699}