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