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