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