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