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