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