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_released();
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| {
1233 editor.move_right(&editor::actions::MoveRight, cx)
1234 });
1235 assert_eq!(
1236 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1237 None
1238 );
1239
1240 workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
1241 executor.run_until_parked();
1242 assert_eq!(
1243 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1244 Some(leader_id)
1245 );
1246
1247 // When client B edits, it automatically stops following client A.
1248 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
1249 assert_eq!(
1250 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1251 None
1252 );
1253
1254 workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
1255 executor.run_until_parked();
1256 assert_eq!(
1257 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1258 Some(leader_id)
1259 );
1260
1261 // When client B scrolls, it automatically stops following client A.
1262 editor_b2.update(cx_b, |editor, cx| {
1263 editor.set_scroll_position(point(0., 3.), cx)
1264 });
1265 assert_eq!(
1266 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1267 None
1268 );
1269
1270 workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx));
1271 executor.run_until_parked();
1272 assert_eq!(
1273 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1274 Some(leader_id)
1275 );
1276
1277 // When client B activates a different pane, it continues following client A in the original pane.
1278 workspace_b.update(cx_b, |workspace, cx| {
1279 workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
1280 });
1281 assert_eq!(
1282 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1283 Some(leader_id)
1284 );
1285
1286 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
1287 assert_eq!(
1288 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1289 Some(leader_id)
1290 );
1291
1292 // When client B activates a different item in the original pane, it automatically stops following client A.
1293 workspace_b
1294 .update(cx_b, |workspace, cx| {
1295 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
1296 })
1297 .await
1298 .unwrap();
1299 assert_eq!(
1300 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1301 None
1302 );
1303}
1304
1305#[gpui::test(iterations = 10)]
1306async fn test_peers_simultaneously_following_each_other(
1307 cx_a: &mut TestAppContext,
1308 cx_b: &mut TestAppContext,
1309) {
1310 let executor = cx_a.executor();
1311 let mut server = TestServer::start(executor.clone()).await;
1312 let client_a = server.create_client(cx_a, "user_a").await;
1313 let client_b = server.create_client(cx_b, "user_b").await;
1314 server
1315 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1316 .await;
1317 let active_call_a = cx_a.read(ActiveCall::global);
1318
1319 cx_a.update(editor::init);
1320 cx_b.update(editor::init);
1321
1322 client_a.fs().insert_tree("/a", json!({})).await;
1323 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1324 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1325 let project_id = active_call_a
1326 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1327 .await
1328 .unwrap();
1329
1330 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1331 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1332
1333 executor.run_until_parked();
1334 let client_a_id = project_b.update(cx_b, |project, _| {
1335 project.collaborators().values().next().unwrap().peer_id
1336 });
1337 let client_b_id = project_a.update(cx_a, |project, _| {
1338 project.collaborators().values().next().unwrap().peer_id
1339 });
1340
1341 workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx));
1342 workspace_b.update(cx_b, |workspace, cx| workspace.follow(client_a_id, cx));
1343 executor.run_until_parked();
1344
1345 workspace_a.update(cx_a, |workspace, _| {
1346 assert_eq!(
1347 workspace.leader_for_pane(workspace.active_pane()),
1348 Some(client_b_id)
1349 );
1350 });
1351 workspace_b.update(cx_b, |workspace, _| {
1352 assert_eq!(
1353 workspace.leader_for_pane(workspace.active_pane()),
1354 Some(client_a_id)
1355 );
1356 });
1357}
1358
1359#[gpui::test(iterations = 10)]
1360async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1361 // a and b join a channel/call
1362 // a shares project 1
1363 // b shares project 2
1364 //
1365 // b follows a: causes project 2 to be joined, and b to follow a.
1366 // b opens a different file in project 2, a follows b
1367 // b opens a different file in project 1, a cannot follow b
1368 // b shares the project, a joins the project and follows b
1369 let executor = cx_a.executor();
1370 let mut server = TestServer::start(executor.clone()).await;
1371 let client_a = server.create_client(cx_a, "user_a").await;
1372 let client_b = server.create_client(cx_b, "user_b").await;
1373
1374 client_a
1375 .fs()
1376 .insert_tree(
1377 "/a",
1378 json!({
1379 "w.rs": "",
1380 "x.rs": "",
1381 }),
1382 )
1383 .await;
1384
1385 client_b
1386 .fs()
1387 .insert_tree(
1388 "/b",
1389 json!({
1390 "y.rs": "",
1391 "z.rs": "",
1392 }),
1393 )
1394 .await;
1395
1396 server
1397 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1398 .await;
1399 let active_call_a = cx_a.read(ActiveCall::global);
1400 let active_call_b = cx_b.read(ActiveCall::global);
1401
1402 let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
1403 let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
1404
1405 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1406 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1407
1408 active_call_a
1409 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1410 .await
1411 .unwrap();
1412
1413 active_call_a
1414 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1415 .await
1416 .unwrap();
1417 active_call_b
1418 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1419 .await
1420 .unwrap();
1421
1422 workspace_a
1423 .update(cx_a, |workspace, cx| {
1424 workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
1425 })
1426 .await
1427 .unwrap();
1428
1429 executor.run_until_parked();
1430 assert_eq!(visible_push_notifications(cx_b).len(), 1);
1431
1432 workspace_b.update(cx_b, |workspace, cx| {
1433 workspace.follow(client_a.peer_id().unwrap(), cx)
1434 });
1435
1436 executor.run_until_parked();
1437 let window_b_project_a = cx_b
1438 .windows()
1439 .iter()
1440 .max_by_key(|window| window.window_id())
1441 .unwrap()
1442 .clone();
1443
1444 let mut cx_b2 = VisualTestContext::from_window(window_b_project_a.clone(), cx_b);
1445
1446 let workspace_b_project_a = window_b_project_a
1447 .downcast::<Workspace>()
1448 .unwrap()
1449 .root(cx_b)
1450 .unwrap();
1451
1452 // assert that b is following a in project a in w.rs
1453 workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1454 assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
1455 assert_eq!(
1456 client_a.peer_id(),
1457 workspace.leader_for_pane(workspace.active_pane())
1458 );
1459 let item = workspace.active_item(cx).unwrap();
1460 assert_eq!(
1461 item.tab_description(0, cx).unwrap(),
1462 SharedString::from("w.rs")
1463 );
1464 });
1465
1466 // TODO: in app code, this would be done by the collab_ui.
1467 active_call_b
1468 .update(&mut cx_b2, |call, cx| {
1469 let project = workspace_b_project_a.read(cx).project().clone();
1470 call.set_location(Some(&project), cx)
1471 })
1472 .await
1473 .unwrap();
1474
1475 // assert that there are no share notifications open
1476 assert_eq!(visible_push_notifications(cx_b).len(), 0);
1477
1478 // b moves to x.rs in a's project, and a follows
1479 workspace_b_project_a
1480 .update(&mut cx_b2, |workspace, cx| {
1481 workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
1482 })
1483 .await
1484 .unwrap();
1485
1486 executor.run_until_parked();
1487 workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1488 let item = workspace.active_item(cx).unwrap();
1489 assert_eq!(
1490 item.tab_description(0, cx).unwrap(),
1491 SharedString::from("x.rs")
1492 );
1493 });
1494
1495 workspace_a.update(cx_a, |workspace, cx| {
1496 workspace.follow(client_b.peer_id().unwrap(), cx)
1497 });
1498
1499 executor.run_until_parked();
1500 workspace_a.update(cx_a, |workspace, cx| {
1501 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1502 assert_eq!(
1503 client_b.peer_id(),
1504 workspace.leader_for_pane(workspace.active_pane())
1505 );
1506 let item = workspace.active_pane().read(cx).active_item().unwrap();
1507 assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs");
1508 });
1509
1510 // b moves to y.rs in b's project, a is still following but can't yet see
1511 workspace_b
1512 .update(cx_b, |workspace, cx| {
1513 workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
1514 })
1515 .await
1516 .unwrap();
1517
1518 // TODO: in app code, this would be done by the collab_ui.
1519 active_call_b
1520 .update(cx_b, |call, cx| {
1521 let project = workspace_b.read(cx).project().clone();
1522 call.set_location(Some(&project), cx)
1523 })
1524 .await
1525 .unwrap();
1526
1527 let project_b_id = active_call_b
1528 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1529 .await
1530 .unwrap();
1531
1532 executor.run_until_parked();
1533 assert_eq!(visible_push_notifications(cx_a).len(), 1);
1534 cx_a.update(|cx| {
1535 workspace::join_remote_project(
1536 project_b_id,
1537 client_b.user_id().unwrap(),
1538 client_a.app_state.clone(),
1539 cx,
1540 )
1541 })
1542 .await
1543 .unwrap();
1544
1545 executor.run_until_parked();
1546
1547 assert_eq!(visible_push_notifications(cx_a).len(), 0);
1548 let window_a_project_b = cx_a
1549 .windows()
1550 .iter()
1551 .max_by_key(|window| window.window_id())
1552 .unwrap()
1553 .clone();
1554 let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b.clone(), cx_a);
1555 let workspace_a_project_b = window_a_project_b
1556 .downcast::<Workspace>()
1557 .unwrap()
1558 .root(cx_a)
1559 .unwrap();
1560
1561 workspace_a_project_b.update(cx_a2, |workspace, cx| {
1562 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1563 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1564 assert_eq!(
1565 client_b.peer_id(),
1566 workspace.leader_for_pane(workspace.active_pane())
1567 );
1568 let item = workspace.active_item(cx).unwrap();
1569 assert_eq!(
1570 item.tab_description(0, cx).unwrap(),
1571 SharedString::from("y.rs")
1572 );
1573 });
1574}
1575
1576#[gpui::test]
1577async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1578 let (_, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
1579
1580 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
1581 client_a
1582 .host_workspace(&workspace_a, channel_id, cx_a)
1583 .await;
1584 let (workspace_b, cx_b) = client_b.join_workspace(channel_id, cx_b).await;
1585
1586 cx_a.simulate_keystrokes("cmd-p 2 enter");
1587 cx_a.run_until_parked();
1588
1589 let editor_a = workspace_a.update(cx_a, |workspace, cx| {
1590 workspace.active_item_as::<Editor>(cx).unwrap()
1591 });
1592 let editor_b = workspace_b.update(cx_b, |workspace, cx| {
1593 workspace.active_item_as::<Editor>(cx).unwrap()
1594 });
1595
1596 // b should follow a to position 1
1597 editor_a.update(cx_a, |editor, cx| {
1598 editor.change_selections(None, cx, |s| s.select_ranges([1..1]))
1599 });
1600 cx_a.run_until_parked();
1601 editor_b.update(cx_b, |editor, cx| {
1602 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1603 });
1604
1605 // a unshares the project
1606 cx_a.update(|cx| {
1607 let project = workspace_a.read(cx).project().clone();
1608 ActiveCall::global(cx).update(cx, |call, cx| {
1609 call.unshare_project(project, cx).unwrap();
1610 })
1611 });
1612 cx_a.run_until_parked();
1613
1614 // b should not follow a to position 2
1615 editor_a.update(cx_a, |editor, cx| {
1616 editor.change_selections(None, cx, |s| s.select_ranges([2..2]))
1617 });
1618 cx_a.run_until_parked();
1619 editor_b.update(cx_b, |editor, cx| {
1620 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1621 });
1622 cx_b.update(|cx| {
1623 let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
1624 let participant = room.remote_participants().get(&client_a.id()).unwrap();
1625 assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
1626 })
1627}
1628
1629#[gpui::test]
1630async fn test_following_into_excluded_file(
1631 mut cx_a: &mut TestAppContext,
1632 mut cx_b: &mut TestAppContext,
1633) {
1634 let executor = cx_a.executor();
1635 let mut server = TestServer::start(executor.clone()).await;
1636 let client_a = server.create_client(cx_a, "user_a").await;
1637 let client_b = server.create_client(cx_b, "user_b").await;
1638 for cx in [&mut cx_a, &mut cx_b] {
1639 cx.update(|cx| {
1640 cx.update_global::<SettingsStore, _>(|store, cx| {
1641 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
1642 project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
1643 });
1644 });
1645 });
1646 }
1647 server
1648 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1649 .await;
1650 let active_call_a = cx_a.read(ActiveCall::global);
1651 let active_call_b = cx_b.read(ActiveCall::global);
1652 let peer_id_a = client_a.peer_id().unwrap();
1653
1654 client_a
1655 .fs()
1656 .insert_tree(
1657 "/a",
1658 json!({
1659 ".git": {
1660 "COMMIT_EDITMSG": "write your commit message here",
1661 },
1662 "1.txt": "one\none\none",
1663 "2.txt": "two\ntwo\ntwo",
1664 "3.txt": "three\nthree\nthree",
1665 }),
1666 )
1667 .await;
1668 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1669 active_call_a
1670 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1671 .await
1672 .unwrap();
1673
1674 let project_id = active_call_a
1675 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1676 .await
1677 .unwrap();
1678 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1679 active_call_b
1680 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1681 .await
1682 .unwrap();
1683
1684 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1685 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1686
1687 // Client A opens editors for a regular file and an excluded file.
1688 let editor_for_regular = workspace_a
1689 .update(cx_a, |workspace, cx| {
1690 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
1691 })
1692 .await
1693 .unwrap()
1694 .downcast::<Editor>()
1695 .unwrap();
1696 let editor_for_excluded_a = workspace_a
1697 .update(cx_a, |workspace, cx| {
1698 workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
1699 })
1700 .await
1701 .unwrap()
1702 .downcast::<Editor>()
1703 .unwrap();
1704
1705 // Client A updates their selections in those editors
1706 editor_for_regular.update(cx_a, |editor, cx| {
1707 editor.handle_input("a", cx);
1708 editor.handle_input("b", cx);
1709 editor.handle_input("c", cx);
1710 editor.select_left(&Default::default(), cx);
1711 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1712 });
1713 editor_for_excluded_a.update(cx_a, |editor, cx| {
1714 editor.select_all(&Default::default(), cx);
1715 editor.handle_input("new commit message", cx);
1716 editor.select_left(&Default::default(), cx);
1717 assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1718 });
1719
1720 // When client B starts following client A, currently visible file is replicated
1721 workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
1722 executor.run_until_parked();
1723
1724 let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
1725 workspace
1726 .active_item(cx)
1727 .unwrap()
1728 .downcast::<Editor>()
1729 .unwrap()
1730 });
1731 assert_eq!(
1732 cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1733 Some((worktree_id, ".git/COMMIT_EDITMSG").into())
1734 );
1735 assert_eq!(
1736 editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
1737 vec![18..17]
1738 );
1739
1740 editor_for_excluded_a.update(cx_a, |editor, cx| {
1741 editor.select_right(&Default::default(), cx);
1742 });
1743 executor.run_until_parked();
1744
1745 // Changes from B to the excluded file are replicated in A's editor
1746 editor_for_excluded_b.update(cx_b, |editor, cx| {
1747 editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
1748 });
1749 executor.run_until_parked();
1750 editor_for_excluded_a.update(cx_a, |editor, cx| {
1751 assert_eq!(
1752 editor.text(cx),
1753 "new commit message\nCo-Authored-By: B <b@b.b>"
1754 );
1755 });
1756}
1757
1758fn visible_push_notifications(
1759 cx: &mut TestAppContext,
1760) -> Vec<gpui::View<ProjectSharedNotification>> {
1761 let mut ret = Vec::new();
1762 for window in cx.windows() {
1763 window
1764 .update(cx, |window, _| {
1765 if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
1766 ret.push(handle)
1767 }
1768 })
1769 .unwrap();
1770 }
1771 ret
1772}
1773
1774#[derive(Debug, PartialEq, Eq)]
1775struct PaneSummary {
1776 active: bool,
1777 leader: Option<PeerId>,
1778 items: Vec<(bool, String)>,
1779}
1780
1781fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1782 cx.read(|cx| {
1783 let active_call = ActiveCall::global(cx).read(cx);
1784 let peer_id = active_call.client().peer_id();
1785 let room = active_call.room().unwrap().read(cx);
1786 let mut result = room
1787 .remote_participants()
1788 .values()
1789 .map(|participant| participant.peer_id)
1790 .chain(peer_id)
1791 .filter_map(|peer_id| {
1792 let followers = room.followers_for(peer_id, project_id);
1793 if followers.is_empty() {
1794 None
1795 } else {
1796 Some((peer_id, followers.to_vec()))
1797 }
1798 })
1799 .collect::<Vec<_>>();
1800 result.sort_by_key(|e| e.0);
1801 result
1802 })
1803}
1804
1805fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
1806 workspace.update(cx, |workspace, cx| {
1807 let active_pane = workspace.active_pane();
1808 workspace
1809 .panes()
1810 .iter()
1811 .map(|pane| {
1812 let leader = workspace.leader_for_pane(pane);
1813 let active = pane == active_pane;
1814 let pane = pane.read(cx);
1815 let active_ix = pane.active_item_index();
1816 PaneSummary {
1817 active,
1818 leader,
1819 items: pane
1820 .items()
1821 .enumerate()
1822 .map(|(ix, item)| {
1823 (
1824 ix == active_ix,
1825 item.tab_description(0, cx)
1826 .map_or(String::new(), |s| s.to_string()),
1827 )
1828 })
1829 .collect(),
1830 }
1831 })
1832 .collect()
1833 })
1834}
1835
1836#[gpui::test(iterations = 10)]
1837async fn test_following_to_channel_notes_without_a_shared_project(
1838 deterministic: BackgroundExecutor,
1839 mut cx_a: &mut TestAppContext,
1840 mut cx_b: &mut TestAppContext,
1841 mut cx_c: &mut TestAppContext,
1842) {
1843 let mut server = TestServer::start(deterministic.clone()).await;
1844 let client_a = server.create_client(cx_a, "user_a").await;
1845 let client_b = server.create_client(cx_b, "user_b").await;
1846 let client_c = server.create_client(cx_c, "user_c").await;
1847
1848 cx_a.update(editor::init);
1849 cx_b.update(editor::init);
1850 cx_c.update(editor::init);
1851 cx_a.update(collab_ui::channel_view::init);
1852 cx_b.update(collab_ui::channel_view::init);
1853 cx_c.update(collab_ui::channel_view::init);
1854
1855 let channel_1_id = server
1856 .make_channel(
1857 "channel-1",
1858 None,
1859 (&client_a, cx_a),
1860 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1861 )
1862 .await;
1863 let channel_2_id = server
1864 .make_channel(
1865 "channel-2",
1866 None,
1867 (&client_a, cx_a),
1868 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1869 )
1870 .await;
1871
1872 // Clients A, B, and C join a channel.
1873 let active_call_a = cx_a.read(ActiveCall::global);
1874 let active_call_b = cx_b.read(ActiveCall::global);
1875 let active_call_c = cx_c.read(ActiveCall::global);
1876 for (call, cx) in [
1877 (&active_call_a, &mut cx_a),
1878 (&active_call_b, &mut cx_b),
1879 (&active_call_c, &mut cx_c),
1880 ] {
1881 call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
1882 .await
1883 .unwrap();
1884 }
1885 deterministic.run_until_parked();
1886
1887 // Clients A, B, and C all open their own unshared projects.
1888 client_a
1889 .fs()
1890 .insert_tree("/a", json!({ "1.txt": "" }))
1891 .await;
1892 client_b.fs().insert_tree("/b", json!({})).await;
1893 client_c.fs().insert_tree("/c", json!({})).await;
1894 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1895 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1896 let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
1897 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1898 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1899 let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
1900
1901 active_call_a
1902 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1903 .await
1904 .unwrap();
1905
1906 // Client A opens the notes for channel 1.
1907 let channel_notes_1_a = cx_a
1908 .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
1909 .await
1910 .unwrap();
1911 channel_notes_1_a.update(cx_a, |notes, cx| {
1912 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1913 notes.editor.update(cx, |editor, cx| {
1914 editor.insert("Hello from A.", cx);
1915 editor.change_selections(None, cx, |selections| {
1916 selections.select_ranges(vec![3..4]);
1917 });
1918 });
1919 });
1920
1921 // Client B follows client A.
1922 workspace_b
1923 .update(cx_b, |workspace, cx| {
1924 workspace
1925 .start_following(client_a.peer_id().unwrap(), cx)
1926 .unwrap()
1927 })
1928 .await
1929 .unwrap();
1930
1931 // Client B is taken to the notes for channel 1, with the same
1932 // text selected as client A.
1933 deterministic.run_until_parked();
1934 let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
1935 assert_eq!(
1936 workspace.leader_for_pane(workspace.active_pane()),
1937 Some(client_a.peer_id().unwrap())
1938 );
1939 workspace
1940 .active_item(cx)
1941 .expect("no active item")
1942 .downcast::<ChannelView>()
1943 .expect("active item is not a channel view")
1944 });
1945 channel_notes_1_b.update(cx_b, |notes, cx| {
1946 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1947 let editor = notes.editor.read(cx);
1948 assert_eq!(editor.text(cx), "Hello from A.");
1949 assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
1950 });
1951
1952 // Client A opens the notes for channel 2.
1953 let channel_notes_2_a = cx_a
1954 .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
1955 .await
1956 .unwrap();
1957 channel_notes_2_a.update(cx_a, |notes, cx| {
1958 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
1959 });
1960
1961 // Client B is taken to the notes for channel 2.
1962 deterministic.run_until_parked();
1963 let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
1964 assert_eq!(
1965 workspace.leader_for_pane(workspace.active_pane()),
1966 Some(client_a.peer_id().unwrap())
1967 );
1968 workspace
1969 .active_item(cx)
1970 .expect("no active item")
1971 .downcast::<ChannelView>()
1972 .expect("active item is not a channel view")
1973 });
1974 channel_notes_2_b.update(cx_b, |notes, cx| {
1975 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
1976 });
1977
1978 // Client A opens a local buffer in their unshared project.
1979 let _unshared_editor_a1 = workspace_a
1980 .update(cx_a, |workspace, cx| {
1981 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
1982 })
1983 .await
1984 .unwrap()
1985 .downcast::<Editor>()
1986 .unwrap();
1987
1988 // This does not send any leader update message to client B.
1989 // If it did, an error would occur on client B, since this buffer
1990 // is not shared with them.
1991 deterministic.run_until_parked();
1992 workspace_b.update(cx_b, |workspace, cx| {
1993 assert_eq!(
1994 workspace.active_item(cx).expect("no active item").item_id(),
1995 channel_notes_2_b.entity_id()
1996 );
1997 });
1998}