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