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