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