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