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_to_active_pane(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
1446 let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b);
1447
1448 let workspace_b_project_a = window_b_project_a
1449 .downcast::<Workspace>()
1450 .unwrap()
1451 .root(cx_b)
1452 .unwrap();
1453
1454 // assert that b is following a in project a in w.rs
1455 workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1456 assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
1457 assert_eq!(
1458 client_a.peer_id(),
1459 workspace.leader_for_pane(workspace.active_pane())
1460 );
1461 let item = workspace.active_item(cx).unwrap();
1462 assert_eq!(
1463 item.tab_description(0, cx).unwrap(),
1464 SharedString::from("w.rs")
1465 );
1466 });
1467
1468 // TODO: in app code, this would be done by the collab_ui.
1469 active_call_b
1470 .update(&mut cx_b2, |call, cx| {
1471 let project = workspace_b_project_a.read(cx).project().clone();
1472 call.set_location(Some(&project), cx)
1473 })
1474 .await
1475 .unwrap();
1476
1477 // assert that there are no share notifications open
1478 assert_eq!(visible_push_notifications(cx_b).len(), 0);
1479
1480 // b moves to x.rs in a's project, and a follows
1481 workspace_b_project_a
1482 .update(&mut cx_b2, |workspace, cx| {
1483 workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
1484 })
1485 .await
1486 .unwrap();
1487
1488 executor.run_until_parked();
1489 workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1490 let item = workspace.active_item(cx).unwrap();
1491 assert_eq!(
1492 item.tab_description(0, cx).unwrap(),
1493 SharedString::from("x.rs")
1494 );
1495 });
1496
1497 workspace_a.update(cx_a, |workspace, cx| {
1498 workspace.follow(client_b.peer_id().unwrap(), cx)
1499 });
1500
1501 executor.run_until_parked();
1502 workspace_a.update(cx_a, |workspace, cx| {
1503 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1504 assert_eq!(
1505 client_b.peer_id(),
1506 workspace.leader_for_pane(workspace.active_pane())
1507 );
1508 let item = workspace.active_pane().read(cx).active_item().unwrap();
1509 assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs");
1510 });
1511
1512 // b moves to y.rs in b's project, a is still following but can't yet see
1513 workspace_b
1514 .update(cx_b, |workspace, cx| {
1515 workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
1516 })
1517 .await
1518 .unwrap();
1519
1520 // TODO: in app code, this would be done by the collab_ui.
1521 active_call_b
1522 .update(cx_b, |call, cx| {
1523 let project = workspace_b.read(cx).project().clone();
1524 call.set_location(Some(&project), cx)
1525 })
1526 .await
1527 .unwrap();
1528
1529 let project_b_id = active_call_b
1530 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1531 .await
1532 .unwrap();
1533
1534 executor.run_until_parked();
1535 assert_eq!(visible_push_notifications(cx_a).len(), 1);
1536 cx_a.update(|cx| {
1537 workspace::join_in_room_project(
1538 project_b_id,
1539 client_b.user_id().unwrap(),
1540 client_a.app_state.clone(),
1541 cx,
1542 )
1543 })
1544 .await
1545 .unwrap();
1546
1547 executor.run_until_parked();
1548
1549 assert_eq!(visible_push_notifications(cx_a).len(), 0);
1550 let window_a_project_b = *cx_a
1551 .windows()
1552 .iter()
1553 .max_by_key(|window| window.window_id())
1554 .unwrap();
1555 let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a);
1556 let workspace_a_project_b = window_a_project_b
1557 .downcast::<Workspace>()
1558 .unwrap()
1559 .root(cx_a)
1560 .unwrap();
1561
1562 workspace_a_project_b.update(cx_a2, |workspace, cx| {
1563 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1564 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1565 assert_eq!(
1566 client_b.peer_id(),
1567 workspace.leader_for_pane(workspace.active_pane())
1568 );
1569 let item = workspace.active_item(cx).unwrap();
1570 assert_eq!(
1571 item.tab_description(0, cx).unwrap(),
1572 SharedString::from("y.rs")
1573 );
1574 });
1575}
1576
1577#[gpui::test]
1578async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1579 let (_server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
1580
1581 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
1582 client_a
1583 .host_workspace(&workspace_a, channel_id, cx_a)
1584 .await;
1585 let (workspace_b, cx_b) = client_b.join_workspace(channel_id, cx_b).await;
1586
1587 cx_a.simulate_keystrokes("cmd-p 2 enter");
1588 cx_a.run_until_parked();
1589
1590 let editor_a = workspace_a.update(cx_a, |workspace, cx| {
1591 workspace.active_item_as::<Editor>(cx).unwrap()
1592 });
1593 let editor_b = workspace_b.update(cx_b, |workspace, cx| {
1594 workspace.active_item_as::<Editor>(cx).unwrap()
1595 });
1596
1597 // b should follow a to position 1
1598 editor_a.update(cx_a, |editor, cx| {
1599 editor.change_selections(None, cx, |s| s.select_ranges([1..1]))
1600 });
1601 cx_a.run_until_parked();
1602 editor_b.update(cx_b, |editor, cx| {
1603 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1604 });
1605
1606 // a unshares the project
1607 cx_a.update(|cx| {
1608 let project = workspace_a.read(cx).project().clone();
1609 ActiveCall::global(cx).update(cx, |call, cx| {
1610 call.unshare_project(project, cx).unwrap();
1611 })
1612 });
1613 cx_a.run_until_parked();
1614
1615 // b should not follow a to position 2
1616 editor_a.update(cx_a, |editor, cx| {
1617 editor.change_selections(None, cx, |s| s.select_ranges([2..2]))
1618 });
1619 cx_a.run_until_parked();
1620 editor_b.update(cx_b, |editor, cx| {
1621 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1622 });
1623 cx_b.update(|cx| {
1624 let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
1625 let participant = room.remote_participants().get(&client_a.id()).unwrap();
1626 assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
1627 })
1628}
1629
1630#[gpui::test]
1631async fn test_following_into_excluded_file(
1632 mut cx_a: &mut TestAppContext,
1633 mut cx_b: &mut TestAppContext,
1634) {
1635 let executor = cx_a.executor();
1636 let mut server = TestServer::start(executor.clone()).await;
1637 let client_a = server.create_client(cx_a, "user_a").await;
1638 let client_b = server.create_client(cx_b, "user_b").await;
1639 for cx in [&mut cx_a, &mut cx_b] {
1640 cx.update(|cx| {
1641 cx.update_global::<SettingsStore, _>(|store, cx| {
1642 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
1643 project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
1644 });
1645 });
1646 });
1647 }
1648 server
1649 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1650 .await;
1651 let active_call_a = cx_a.read(ActiveCall::global);
1652 let active_call_b = cx_b.read(ActiveCall::global);
1653 let peer_id_a = client_a.peer_id().unwrap();
1654
1655 client_a
1656 .fs()
1657 .insert_tree(
1658 "/a",
1659 json!({
1660 ".git": {
1661 "COMMIT_EDITMSG": "write your commit message here",
1662 },
1663 "1.txt": "one\none\none",
1664 "2.txt": "two\ntwo\ntwo",
1665 "3.txt": "three\nthree\nthree",
1666 }),
1667 )
1668 .await;
1669 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1670 active_call_a
1671 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1672 .await
1673 .unwrap();
1674
1675 let project_id = active_call_a
1676 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1677 .await
1678 .unwrap();
1679 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1680 active_call_b
1681 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1682 .await
1683 .unwrap();
1684
1685 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1686 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1687
1688 // Client A opens editors for a regular file and an excluded file.
1689 let editor_for_regular = workspace_a
1690 .update(cx_a, |workspace, cx| {
1691 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
1692 })
1693 .await
1694 .unwrap()
1695 .downcast::<Editor>()
1696 .unwrap();
1697 let editor_for_excluded_a = workspace_a
1698 .update(cx_a, |workspace, cx| {
1699 workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
1700 })
1701 .await
1702 .unwrap()
1703 .downcast::<Editor>()
1704 .unwrap();
1705
1706 // Client A updates their selections in those editors
1707 editor_for_regular.update(cx_a, |editor, cx| {
1708 editor.handle_input("a", cx);
1709 editor.handle_input("b", cx);
1710 editor.handle_input("c", cx);
1711 editor.select_left(&Default::default(), cx);
1712 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1713 });
1714 editor_for_excluded_a.update(cx_a, |editor, cx| {
1715 editor.select_all(&Default::default(), cx);
1716 editor.handle_input("new commit message", cx);
1717 editor.select_left(&Default::default(), cx);
1718 assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1719 });
1720
1721 // When client B starts following client A, currently visible file is replicated
1722 workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
1723 executor.run_until_parked();
1724
1725 let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
1726 workspace
1727 .active_item(cx)
1728 .unwrap()
1729 .downcast::<Editor>()
1730 .unwrap()
1731 });
1732 assert_eq!(
1733 cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1734 Some((worktree_id, ".git/COMMIT_EDITMSG").into())
1735 );
1736 assert_eq!(
1737 editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
1738 vec![18..17]
1739 );
1740
1741 editor_for_excluded_a.update(cx_a, |editor, cx| {
1742 editor.select_right(&Default::default(), cx);
1743 });
1744 executor.run_until_parked();
1745
1746 // Changes from B to the excluded file are replicated in A's editor
1747 editor_for_excluded_b.update(cx_b, |editor, cx| {
1748 editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
1749 });
1750 executor.run_until_parked();
1751 editor_for_excluded_a.update(cx_a, |editor, cx| {
1752 assert_eq!(
1753 editor.text(cx),
1754 "new commit message\nCo-Authored-By: B <b@b.b>"
1755 );
1756 });
1757}
1758
1759fn visible_push_notifications(
1760 cx: &mut TestAppContext,
1761) -> Vec<gpui::View<ProjectSharedNotification>> {
1762 let mut ret = Vec::new();
1763 for window in cx.windows() {
1764 window
1765 .update(cx, |window, _| {
1766 if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
1767 ret.push(handle)
1768 }
1769 })
1770 .unwrap();
1771 }
1772 ret
1773}
1774
1775#[derive(Debug, PartialEq, Eq)]
1776struct PaneSummary {
1777 active: bool,
1778 leader: Option<PeerId>,
1779 items: Vec<(bool, String)>,
1780}
1781
1782fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1783 cx.read(|cx| {
1784 let active_call = ActiveCall::global(cx).read(cx);
1785 let peer_id = active_call.client().peer_id();
1786 let room = active_call.room().unwrap().read(cx);
1787 let mut result = room
1788 .remote_participants()
1789 .values()
1790 .map(|participant| participant.peer_id)
1791 .chain(peer_id)
1792 .filter_map(|peer_id| {
1793 let followers = room.followers_for(peer_id, project_id);
1794 if followers.is_empty() {
1795 None
1796 } else {
1797 Some((peer_id, followers.to_vec()))
1798 }
1799 })
1800 .collect::<Vec<_>>();
1801 result.sort_by_key(|e| e.0);
1802 result
1803 })
1804}
1805
1806fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
1807 workspace.update(cx, |workspace, cx| {
1808 let active_pane = workspace.active_pane();
1809 workspace
1810 .panes()
1811 .iter()
1812 .map(|pane| {
1813 let leader = workspace.leader_for_pane(pane);
1814 let active = pane == active_pane;
1815 let pane = pane.read(cx);
1816 let active_ix = pane.active_item_index();
1817 PaneSummary {
1818 active,
1819 leader,
1820 items: pane
1821 .items()
1822 .enumerate()
1823 .map(|(ix, item)| {
1824 (
1825 ix == active_ix,
1826 item.tab_description(0, cx)
1827 .map_or(String::new(), |s| s.to_string()),
1828 )
1829 })
1830 .collect(),
1831 }
1832 })
1833 .collect()
1834 })
1835}
1836
1837#[gpui::test(iterations = 10)]
1838async fn test_following_to_channel_notes_without_a_shared_project(
1839 deterministic: BackgroundExecutor,
1840 mut cx_a: &mut TestAppContext,
1841 mut cx_b: &mut TestAppContext,
1842 mut cx_c: &mut TestAppContext,
1843) {
1844 let mut server = TestServer::start(deterministic.clone()).await;
1845 let client_a = server.create_client(cx_a, "user_a").await;
1846 let client_b = server.create_client(cx_b, "user_b").await;
1847 let client_c = server.create_client(cx_c, "user_c").await;
1848
1849 cx_a.update(editor::init);
1850 cx_b.update(editor::init);
1851 cx_c.update(editor::init);
1852 cx_a.update(collab_ui::channel_view::init);
1853 cx_b.update(collab_ui::channel_view::init);
1854 cx_c.update(collab_ui::channel_view::init);
1855
1856 let channel_1_id = server
1857 .make_channel(
1858 "channel-1",
1859 None,
1860 (&client_a, cx_a),
1861 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1862 )
1863 .await;
1864 let channel_2_id = server
1865 .make_channel(
1866 "channel-2",
1867 None,
1868 (&client_a, cx_a),
1869 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1870 )
1871 .await;
1872
1873 // Clients A, B, and C join a channel.
1874 let active_call_a = cx_a.read(ActiveCall::global);
1875 let active_call_b = cx_b.read(ActiveCall::global);
1876 let active_call_c = cx_c.read(ActiveCall::global);
1877 for (call, cx) in [
1878 (&active_call_a, &mut cx_a),
1879 (&active_call_b, &mut cx_b),
1880 (&active_call_c, &mut cx_c),
1881 ] {
1882 call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
1883 .await
1884 .unwrap();
1885 }
1886 deterministic.run_until_parked();
1887
1888 // Clients A, B, and C all open their own unshared projects.
1889 client_a
1890 .fs()
1891 .insert_tree("/a", json!({ "1.txt": "" }))
1892 .await;
1893 client_b.fs().insert_tree("/b", json!({})).await;
1894 client_c.fs().insert_tree("/c", json!({})).await;
1895 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1896 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1897 let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
1898 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1899 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1900 let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
1901
1902 active_call_a
1903 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1904 .await
1905 .unwrap();
1906
1907 // Client A opens the notes for channel 1.
1908 let channel_notes_1_a = cx_a
1909 .update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), cx))
1910 .await
1911 .unwrap();
1912 channel_notes_1_a.update(cx_a, |notes, cx| {
1913 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1914 notes.editor.update(cx, |editor, cx| {
1915 editor.insert("Hello from A.", cx);
1916 editor.change_selections(None, cx, |selections| {
1917 selections.select_ranges(vec![3..4]);
1918 });
1919 });
1920 });
1921
1922 // Client B follows client A.
1923 workspace_b
1924 .update(cx_b, |workspace, cx| {
1925 workspace
1926 .start_following(client_a.peer_id().unwrap(), cx)
1927 .unwrap()
1928 })
1929 .await
1930 .unwrap();
1931
1932 // Client B is taken to the notes for channel 1, with the same
1933 // text selected as client A.
1934 deterministic.run_until_parked();
1935 let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
1936 assert_eq!(
1937 workspace.leader_for_pane(workspace.active_pane()),
1938 Some(client_a.peer_id().unwrap())
1939 );
1940 workspace
1941 .active_item(cx)
1942 .expect("no active item")
1943 .downcast::<ChannelView>()
1944 .expect("active item is not a channel view")
1945 });
1946 channel_notes_1_b.update(cx_b, |notes, cx| {
1947 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1948 let editor = notes.editor.read(cx);
1949 assert_eq!(editor.text(cx), "Hello from A.");
1950 assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
1951 });
1952
1953 // Client A opens the notes for channel 2.
1954 let channel_notes_2_a = cx_a
1955 .update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), cx))
1956 .await
1957 .unwrap();
1958 channel_notes_2_a.update(cx_a, |notes, cx| {
1959 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
1960 });
1961
1962 // Client B is taken to the notes for channel 2.
1963 deterministic.run_until_parked();
1964 let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
1965 assert_eq!(
1966 workspace.leader_for_pane(workspace.active_pane()),
1967 Some(client_a.peer_id().unwrap())
1968 );
1969 workspace
1970 .active_item(cx)
1971 .expect("no active item")
1972 .downcast::<ChannelView>()
1973 .expect("active item is not a channel view")
1974 });
1975 channel_notes_2_b.update(cx_b, |notes, cx| {
1976 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
1977 });
1978
1979 // Client A opens a local buffer in their unshared project.
1980 let _unshared_editor_a1 = workspace_a
1981 .update(cx_a, |workspace, cx| {
1982 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
1983 })
1984 .await
1985 .unwrap()
1986 .downcast::<Editor>()
1987 .unwrap();
1988
1989 // This does not send any leader update message to client B.
1990 // If it did, an error would occur on client B, since this buffer
1991 // is not shared with them.
1992 deterministic.run_until_parked();
1993 workspace_b.update(cx_b, |workspace, cx| {
1994 assert_eq!(
1995 workspace.active_item(cx).expect("no active item").item_id(),
1996 channel_notes_2_b.entity_id()
1997 );
1998 });
1999}
2000
2001async fn join_channel(
2002 channel_id: ChannelId,
2003 client: &TestClient,
2004 cx: &mut TestAppContext,
2005) -> anyhow::Result<()> {
2006 cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
2007 .await
2008}
2009
2010async fn share_workspace(
2011 workspace: &View<Workspace>,
2012 cx: &mut VisualTestContext,
2013) -> anyhow::Result<u64> {
2014 let project = workspace.update(cx, |workspace, _| workspace.project().clone());
2015 cx.read(ActiveCall::global)
2016 .update(cx, |call, cx| call.share_project(project, cx))
2017 .await
2018}
2019
2020#[gpui::test]
2021async fn test_following_to_channel_notes_other_workspace(
2022 cx_a: &mut TestAppContext,
2023 cx_b: &mut TestAppContext,
2024) {
2025 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2026
2027 let mut cx_a2 = cx_a.clone();
2028 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2029 join_channel(channel, &client_a, cx_a).await.unwrap();
2030 share_workspace(&workspace_a, cx_a).await.unwrap();
2031
2032 // a opens 1.txt
2033 cx_a.simulate_keystrokes("cmd-p 1 enter");
2034 cx_a.run_until_parked();
2035 workspace_a.update(cx_a, |workspace, cx| {
2036 let editor = workspace.active_item(cx).unwrap();
2037 assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
2038 });
2039
2040 // b joins channel and is following a
2041 join_channel(channel, &client_b, cx_b).await.unwrap();
2042 cx_b.run_until_parked();
2043 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2044 workspace_b.update(cx_b, |workspace, cx| {
2045 let editor = workspace.active_item(cx).unwrap();
2046 assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
2047 });
2048
2049 // a opens a second workspace and the channel notes
2050 let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2051 cx_a2.update(|cx| cx.activate_window());
2052 cx_a2
2053 .update(|cx| ChannelView::open(channel, None, workspace_a2, cx))
2054 .await
2055 .unwrap();
2056 cx_a2.run_until_parked();
2057
2058 // b should follow a to the channel notes
2059 workspace_b.update(cx_b, |workspace, cx| {
2060 let editor = workspace.active_item_as::<ChannelView>(cx).unwrap();
2061 assert_eq!(editor.read(cx).channel(cx).unwrap().id, channel);
2062 });
2063
2064 // a returns to the shared project
2065 cx_a.update(|cx| cx.activate_window());
2066 cx_a.run_until_parked();
2067
2068 workspace_a.update(cx_a, |workspace, cx| {
2069 let editor = workspace.active_item(cx).unwrap();
2070 assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
2071 });
2072
2073 // b should follow a back
2074 workspace_b.update(cx_b, |workspace, cx| {
2075 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2076 assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
2077 });
2078}
2079
2080#[gpui::test]
2081async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2082 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2083
2084 let mut cx_a2 = cx_a.clone();
2085 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2086 join_channel(channel, &client_a, cx_a).await.unwrap();
2087 share_workspace(&workspace_a, cx_a).await.unwrap();
2088
2089 // a opens 1.txt
2090 cx_a.simulate_keystrokes("cmd-p 1 enter");
2091 cx_a.run_until_parked();
2092 workspace_a.update(cx_a, |workspace, cx| {
2093 let editor = workspace.active_item(cx).unwrap();
2094 assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
2095 });
2096
2097 // b joins channel and is following a
2098 join_channel(channel, &client_b, cx_b).await.unwrap();
2099 cx_b.run_until_parked();
2100 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2101 workspace_b.update(cx_b, |workspace, cx| {
2102 let editor = workspace.active_item(cx).unwrap();
2103 assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
2104 });
2105
2106 // stop following
2107 cx_b.simulate_keystrokes("down");
2108
2109 // a opens a different file while not followed
2110 cx_a.simulate_keystrokes("cmd-p 2 enter");
2111
2112 workspace_b.update(cx_b, |workspace, cx| {
2113 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2114 assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
2115 });
2116
2117 // a opens a file in a new window
2118 let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2119 cx_a2.update(|cx| cx.activate_window());
2120 cx_a2.simulate_keystrokes("cmd-p 3 enter");
2121 cx_a2.run_until_parked();
2122
2123 // b starts following a again
2124 cx_b.simulate_keystrokes("cmd-ctrl-alt-f");
2125 cx_a.run_until_parked();
2126
2127 // a returns to the shared project
2128 cx_a.update(|cx| cx.activate_window());
2129 cx_a.run_until_parked();
2130
2131 workspace_a.update(cx_a, |workspace, cx| {
2132 let editor = workspace.active_item(cx).unwrap();
2133 assert_eq!(editor.tab_description(0, cx).unwrap(), "2.js");
2134 });
2135
2136 // b should follow a back
2137 workspace_b.update(cx_b, |workspace, cx| {
2138 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2139 assert_eq!(editor.tab_description(0, cx).unwrap(), "2.js");
2140 });
2141}