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