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