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, MultiBuffer, PathKey, SelectionEffects};
10use gpui::{
11 AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext,
12 VisualContext, VisualTestContext, point,
13};
14use language::Capability;
15use rpc::proto::PeerId;
16use serde_json::json;
17use settings::SettingsStore;
18use text::{Point, ToPoint};
19use util::{path, rel_path::rel_path, test::sample_text};
20use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _};
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, rel_path("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, rel_path("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, rel_path("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, rel_path("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, rel_path("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, rel_path("2.txt")).into(), cx)
295 .unwrap()
296 });
297 let mut result = MultiBuffer::new(Capability::ReadWrite);
298 result.set_excerpts_for_path(
299 PathKey::for_buffer(&buffer_a1, cx),
300 buffer_a1,
301 [Point::row_range(1..2)],
302 1,
303 cx,
304 );
305 result.set_excerpts_for_path(
306 PathKey::for_buffer(&buffer_a2, cx),
307 buffer_a2,
308 [Point::row_range(5..6)],
309 1,
310 cx,
311 );
312 result
313 });
314 let multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
315 let editor = cx
316 .new(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), window, cx));
317 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
318 editor
319 });
320 executor.run_until_parked();
321 let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
322 workspace
323 .active_item(cx)
324 .unwrap()
325 .downcast::<Editor>()
326 .unwrap()
327 });
328 assert_eq!(
329 multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
330 multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
331 );
332
333 // When client A navigates back and forth, client B does so as well.
334 workspace_a
335 .update_in(cx_a, |workspace, window, cx| {
336 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
337 })
338 .await
339 .unwrap();
340 executor.run_until_parked();
341 workspace_b.update(cx_b, |workspace, cx| {
342 assert_eq!(
343 workspace.active_item(cx).unwrap().item_id(),
344 editor_b1.item_id()
345 );
346 });
347
348 workspace_a
349 .update_in(cx_a, |workspace, window, cx| {
350 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
351 })
352 .await
353 .unwrap();
354 executor.run_until_parked();
355 workspace_b.update(cx_b, |workspace, cx| {
356 assert_eq!(
357 workspace.active_item(cx).unwrap().item_id(),
358 editor_b2.item_id()
359 );
360 });
361
362 workspace_a
363 .update_in(cx_a, |workspace, window, cx| {
364 workspace.go_forward(workspace.active_pane().downgrade(), window, cx)
365 })
366 .await
367 .unwrap();
368 executor.run_until_parked();
369 workspace_b.update(cx_b, |workspace, cx| {
370 assert_eq!(
371 workspace.active_item(cx).unwrap().item_id(),
372 editor_b1.item_id()
373 );
374 });
375
376 // Changes to client A's editor are reflected on client B.
377 editor_a1.update_in(cx_a, |editor, window, cx| {
378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
379 s.select_ranges([1..1, 2..2])
380 });
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(SelectionEffects::no_scroll(), window, cx, |s| {
398 s.select_ranges([3..3])
399 });
400 editor.set_scroll_position(point(0., 100.), window, cx);
401 });
402 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
403 executor.run_until_parked();
404 editor_b1.update(cx_b, |editor, cx| {
405 assert_eq!(editor.selections.ranges(cx), &[3..3]);
406 });
407
408 // After unfollowing, client B stops receiving updates from client A.
409 workspace_b.update_in(cx_b, |workspace, window, cx| {
410 workspace.unfollow(peer_id_a, window, cx).unwrap()
411 });
412 workspace_a.update_in(cx_a, |workspace, window, cx| {
413 workspace.activate_item(&editor_a2, true, true, window, cx)
414 });
415 executor.run_until_parked();
416 assert_eq!(
417 workspace_b.update(cx_b, |workspace, cx| workspace
418 .active_item(cx)
419 .unwrap()
420 .item_id()),
421 editor_b1.item_id()
422 );
423
424 // Client A starts following client B.
425 workspace_a.update_in(cx_a, |workspace, window, cx| {
426 workspace.follow(peer_id_b, window, cx)
427 });
428 executor.run_until_parked();
429 assert_eq!(
430 workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
431 Some(peer_id_b.into())
432 );
433 assert_eq!(
434 workspace_a.update_in(cx_a, |workspace, _, cx| workspace
435 .active_item(cx)
436 .unwrap()
437 .item_id()),
438 editor_a1.item_id()
439 );
440
441 // #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
442 {
443 use crate::rpc::RECONNECT_TIMEOUT;
444 use gpui::TestScreenCaptureSource;
445 use workspace::{
446 dock::{DockPosition, test::TestPanel},
447 item::test::TestItem,
448 shared_screen::SharedScreen,
449 };
450
451 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
452 let display = TestScreenCaptureSource::new();
453 active_call_b
454 .update(cx_b, |call, cx| call.set_location(None, cx))
455 .await
456 .unwrap();
457 cx_b.set_screen_capture_sources(vec![display]);
458 let source = cx_b
459 .read(|cx| cx.screen_capture_sources())
460 .await
461 .unwrap()
462 .unwrap()
463 .into_iter()
464 .next()
465 .unwrap();
466 active_call_b
467 .update(cx_b, |call, cx| {
468 call.room()
469 .unwrap()
470 .update(cx, |room, cx| room.share_screen(source, cx))
471 })
472 .await
473 .unwrap();
474 executor.run_until_parked();
475
476 let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
477 workspace
478 .active_item(cx)
479 .expect("no active item")
480 .downcast::<SharedScreen>()
481 .expect("active item isn't a shared screen")
482 });
483
484 // Client B activates Zed again, which causes the previous editor to become focused again.
485 active_call_b
486 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
487 .await
488 .unwrap();
489 executor.run_until_parked();
490 workspace_a.update(cx_a, |workspace, cx| {
491 assert_eq!(
492 workspace.active_item(cx).unwrap().item_id(),
493 editor_a1.item_id()
494 )
495 });
496
497 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
498 workspace_b.update_in(cx_b, |workspace, window, cx| {
499 workspace.activate_item(&multibuffer_editor_b, true, true, window, cx)
500 });
501 executor.run_until_parked();
502 workspace_a.update(cx_a, |workspace, cx| {
503 assert_eq!(
504 workspace.active_item(cx).unwrap().item_id(),
505 multibuffer_editor_a.item_id()
506 )
507 });
508
509 // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
510 let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, cx));
511 workspace_b.update_in(cx_b, |workspace, window, cx| {
512 workspace.add_panel(panel, window, cx);
513 workspace.toggle_panel_focus::<TestPanel>(window, cx);
514 });
515 executor.run_until_parked();
516 assert_eq!(
517 workspace_a.update(cx_a, |workspace, cx| workspace
518 .active_item(cx)
519 .unwrap()
520 .item_id()),
521 shared_screen.item_id()
522 );
523
524 // Toggling the focus back to the pane causes client A to return to the multibuffer.
525 workspace_b.update_in(cx_b, |workspace, window, cx| {
526 workspace.toggle_panel_focus::<TestPanel>(window, cx);
527 });
528 executor.run_until_parked();
529 workspace_a.update(cx_a, |workspace, cx| {
530 assert_eq!(
531 workspace.active_item(cx).unwrap().item_id(),
532 multibuffer_editor_a.item_id()
533 )
534 });
535
536 // Client B activates an item that doesn't implement following,
537 // so the previously-opened screen-sharing item gets activated.
538 let unfollowable_item = cx_b.new(TestItem::new);
539 workspace_b.update_in(cx_b, |workspace, window, cx| {
540 workspace.active_pane().update(cx, |pane, cx| {
541 pane.add_item(Box::new(unfollowable_item), true, true, None, window, cx)
542 })
543 });
544 executor.run_until_parked();
545 assert_eq!(
546 workspace_a.update(cx_a, |workspace, cx| workspace
547 .active_item(cx)
548 .unwrap()
549 .item_id()),
550 shared_screen.item_id()
551 );
552
553 // Following interrupts when client B disconnects.
554 client_b.disconnect(&cx_b.to_async());
555 executor.advance_clock(RECONNECT_TIMEOUT);
556 assert_eq!(
557 workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
558 None
559 );
560 }
561}
562
563#[gpui::test]
564async fn test_following_tab_order(
565 executor: BackgroundExecutor,
566 cx_a: &mut TestAppContext,
567 cx_b: &mut TestAppContext,
568) {
569 let mut server = TestServer::start(executor.clone()).await;
570 let client_a = server.create_client(cx_a, "user_a").await;
571 let client_b = server.create_client(cx_b, "user_b").await;
572 server
573 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
574 .await;
575 let active_call_a = cx_a.read(ActiveCall::global);
576 let active_call_b = cx_b.read(ActiveCall::global);
577
578 cx_a.update(editor::init);
579 cx_b.update(editor::init);
580
581 client_a
582 .fs()
583 .insert_tree(
584 path!("/a"),
585 json!({
586 "1.txt": "one",
587 "2.txt": "two",
588 "3.txt": "three",
589 }),
590 )
591 .await;
592 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
593 active_call_a
594 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
595 .await
596 .unwrap();
597
598 let project_id = active_call_a
599 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
600 .await
601 .unwrap();
602 let project_b = client_b.join_remote_project(project_id, cx_b).await;
603 active_call_b
604 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
605 .await
606 .unwrap();
607
608 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
609 let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
610
611 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
612 let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
613
614 let client_b_id = project_a.update(cx_a, |project, _| {
615 project.collaborators().values().next().unwrap().peer_id
616 });
617
618 //Open 1, 3 in that order on client A
619 workspace_a
620 .update_in(cx_a, |workspace, window, cx| {
621 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
622 })
623 .await
624 .unwrap();
625 workspace_a
626 .update_in(cx_a, |workspace, window, cx| {
627 workspace.open_path((worktree_id, rel_path("3.txt")), None, true, window, cx)
628 })
629 .await
630 .unwrap();
631
632 let pane_paths = |pane: &Entity<workspace::Pane>, cx: &mut VisualTestContext| {
633 pane.update(cx, |pane, cx| {
634 pane.items()
635 .map(|item| item.project_path(cx).unwrap().path.as_str().to_owned())
636 .collect::<Vec<_>>()
637 })
638 };
639
640 //Verify that the tabs opened in the order we expect
641 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
642
643 //Follow client B as client A
644 workspace_a.update_in(cx_a, |workspace, window, cx| {
645 workspace.follow(client_b_id, window, cx)
646 });
647 executor.run_until_parked();
648
649 //Open just 2 on client B
650 workspace_b
651 .update_in(cx_b, |workspace, window, cx| {
652 workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
653 })
654 .await
655 .unwrap();
656 executor.run_until_parked();
657
658 // Verify that newly opened followed file is at the end
659 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
660
661 //Open just 1 on client B
662 workspace_b
663 .update_in(cx_b, |workspace, window, cx| {
664 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
665 })
666 .await
667 .unwrap();
668 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
669 executor.run_until_parked();
670
671 // Verify that following into 1 did not reorder
672 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
673}
674
675#[gpui::test(iterations = 10)]
676async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
677 let executor = cx_a.executor();
678 let mut server = TestServer::start(executor.clone()).await;
679 let client_a = server.create_client(cx_a, "user_a").await;
680 let client_b = server.create_client(cx_b, "user_b").await;
681 server
682 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
683 .await;
684 let active_call_a = cx_a.read(ActiveCall::global);
685 let active_call_b = cx_b.read(ActiveCall::global);
686
687 cx_a.update(editor::init);
688 cx_b.update(editor::init);
689
690 // Client A shares a project.
691 client_a
692 .fs()
693 .insert_tree(
694 path!("/a"),
695 json!({
696 "1.txt": "one",
697 "2.txt": "two",
698 "3.txt": "three",
699 "4.txt": "four",
700 }),
701 )
702 .await;
703 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
704 active_call_a
705 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
706 .await
707 .unwrap();
708 let project_id = active_call_a
709 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
710 .await
711 .unwrap();
712
713 // Client B joins the project.
714 let project_b = client_b.join_remote_project(project_id, cx_b).await;
715 active_call_b
716 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
717 .await
718 .unwrap();
719
720 // Client A opens a file.
721 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
722 workspace_a
723 .update_in(cx_a, |workspace, window, cx| {
724 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
725 })
726 .await
727 .unwrap()
728 .downcast::<Editor>()
729 .unwrap();
730
731 // Client B opens a different file.
732 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
733 workspace_b
734 .update_in(cx_b, |workspace, window, cx| {
735 workspace.open_path((worktree_id, rel_path("2.txt")), None, true, window, cx)
736 })
737 .await
738 .unwrap()
739 .downcast::<Editor>()
740 .unwrap();
741
742 // Clients A and B follow each other in split panes
743 workspace_a.update_in(cx_a, |workspace, window, cx| {
744 workspace.split_and_clone(
745 workspace.active_pane().clone(),
746 SplitDirection::Right,
747 window,
748 cx,
749 );
750 });
751 workspace_a.update_in(cx_a, |workspace, window, cx| {
752 workspace.follow(client_b.peer_id().unwrap(), window, cx)
753 });
754 executor.run_until_parked();
755 workspace_b.update_in(cx_b, |workspace, window, cx| {
756 workspace.split_and_clone(
757 workspace.active_pane().clone(),
758 SplitDirection::Right,
759 window,
760 cx,
761 );
762 });
763 workspace_b.update_in(cx_b, |workspace, window, cx| {
764 workspace.follow(client_a.peer_id().unwrap(), window, cx)
765 });
766 executor.run_until_parked();
767
768 // Clients A and B return focus to the original files they had open
769 workspace_a.update_in(cx_a, |workspace, window, cx| {
770 workspace.activate_next_pane(window, cx)
771 });
772 workspace_b.update_in(cx_b, |workspace, window, cx| {
773 workspace.activate_next_pane(window, cx)
774 });
775 executor.run_until_parked();
776
777 // Both clients see the other client's focused file in their right pane.
778 assert_eq!(
779 pane_summaries(&workspace_a, cx_a),
780 &[
781 PaneSummary {
782 active: true,
783 leader: None,
784 items: vec![(true, "1.txt".into())]
785 },
786 PaneSummary {
787 active: false,
788 leader: client_b.peer_id(),
789 items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
790 },
791 ]
792 );
793 assert_eq!(
794 pane_summaries(&workspace_b, cx_b),
795 &[
796 PaneSummary {
797 active: true,
798 leader: None,
799 items: vec![(true, "2.txt".into())]
800 },
801 PaneSummary {
802 active: false,
803 leader: client_a.peer_id(),
804 items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
805 },
806 ]
807 );
808
809 // Clients A and B each open a new file.
810 workspace_a
811 .update_in(cx_a, |workspace, window, cx| {
812 workspace.open_path((worktree_id, rel_path("3.txt")), None, true, window, cx)
813 })
814 .await
815 .unwrap();
816
817 workspace_b
818 .update_in(cx_b, |workspace, window, cx| {
819 workspace.open_path((worktree_id, rel_path("4.txt")), None, true, window, cx)
820 })
821 .await
822 .unwrap();
823 executor.run_until_parked();
824
825 // Both client's see the other client open the new file, but keep their
826 // focus on their own active pane.
827 assert_eq!(
828 pane_summaries(&workspace_a, cx_a),
829 &[
830 PaneSummary {
831 active: true,
832 leader: None,
833 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
834 },
835 PaneSummary {
836 active: false,
837 leader: client_b.peer_id(),
838 items: vec![
839 (false, "1.txt".into()),
840 (false, "2.txt".into()),
841 (true, "4.txt".into())
842 ]
843 },
844 ]
845 );
846 assert_eq!(
847 pane_summaries(&workspace_b, cx_b),
848 &[
849 PaneSummary {
850 active: true,
851 leader: None,
852 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
853 },
854 PaneSummary {
855 active: false,
856 leader: client_a.peer_id(),
857 items: vec![
858 (false, "2.txt".into()),
859 (false, "1.txt".into()),
860 (true, "3.txt".into())
861 ]
862 },
863 ]
864 );
865
866 // Client A focuses their right pane, in which they're following client B.
867 workspace_a.update_in(cx_a, |workspace, window, cx| {
868 workspace.activate_next_pane(window, cx)
869 });
870 executor.run_until_parked();
871
872 // Client B sees that client A is now looking at the same file as them.
873 assert_eq!(
874 pane_summaries(&workspace_a, cx_a),
875 &[
876 PaneSummary {
877 active: false,
878 leader: None,
879 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
880 },
881 PaneSummary {
882 active: true,
883 leader: client_b.peer_id(),
884 items: vec![
885 (false, "1.txt".into()),
886 (false, "2.txt".into()),
887 (true, "4.txt".into())
888 ]
889 },
890 ]
891 );
892 assert_eq!(
893 pane_summaries(&workspace_b, cx_b),
894 &[
895 PaneSummary {
896 active: true,
897 leader: None,
898 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
899 },
900 PaneSummary {
901 active: false,
902 leader: client_a.peer_id(),
903 items: vec![
904 (false, "2.txt".into()),
905 (false, "1.txt".into()),
906 (false, "3.txt".into()),
907 (true, "4.txt".into())
908 ]
909 },
910 ]
911 );
912
913 // Client B focuses their right pane, in which they're following client A,
914 // who is following them.
915 workspace_b.update_in(cx_b, |workspace, window, cx| {
916 workspace.activate_next_pane(window, cx)
917 });
918 executor.run_until_parked();
919
920 // Client A sees that client B is now looking at the same file as them.
921 assert_eq!(
922 pane_summaries(&workspace_b, cx_b),
923 &[
924 PaneSummary {
925 active: false,
926 leader: None,
927 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
928 },
929 PaneSummary {
930 active: true,
931 leader: client_a.peer_id(),
932 items: vec![
933 (false, "2.txt".into()),
934 (false, "1.txt".into()),
935 (false, "3.txt".into()),
936 (true, "4.txt".into())
937 ]
938 },
939 ]
940 );
941 assert_eq!(
942 pane_summaries(&workspace_a, cx_a),
943 &[
944 PaneSummary {
945 active: false,
946 leader: None,
947 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
948 },
949 PaneSummary {
950 active: true,
951 leader: client_b.peer_id(),
952 items: vec![
953 (false, "1.txt".into()),
954 (false, "2.txt".into()),
955 (true, "4.txt".into())
956 ]
957 },
958 ]
959 );
960
961 // Client B focuses a file that they previously followed A to, breaking
962 // the follow.
963 workspace_b.update_in(cx_b, |workspace, window, cx| {
964 workspace.active_pane().update(cx, |pane, cx| {
965 pane.activate_previous_item(&Default::default(), window, cx);
966 });
967 });
968 executor.run_until_parked();
969
970 // Both clients see that client B is looking at that previous file.
971 assert_eq!(
972 pane_summaries(&workspace_b, cx_b),
973 &[
974 PaneSummary {
975 active: false,
976 leader: None,
977 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
978 },
979 PaneSummary {
980 active: true,
981 leader: None,
982 items: vec![
983 (false, "2.txt".into()),
984 (false, "1.txt".into()),
985 (true, "3.txt".into()),
986 (false, "4.txt".into())
987 ]
988 },
989 ]
990 );
991 assert_eq!(
992 pane_summaries(&workspace_a, cx_a),
993 &[
994 PaneSummary {
995 active: false,
996 leader: None,
997 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
998 },
999 PaneSummary {
1000 active: true,
1001 leader: client_b.peer_id(),
1002 items: vec![
1003 (false, "1.txt".into()),
1004 (false, "2.txt".into()),
1005 (false, "4.txt".into()),
1006 (true, "3.txt".into()),
1007 ]
1008 },
1009 ]
1010 );
1011
1012 // Client B closes tabs, some of which were originally opened by client A,
1013 // and some of which were originally opened by client B.
1014 workspace_b.update_in(cx_b, |workspace, window, cx| {
1015 workspace.active_pane().update(cx, |pane, cx| {
1016 pane.close_other_items(&Default::default(), None, window, cx)
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_previous_item(&Default::default(), 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_previous_item(&Default::default(), 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_previous_item(&Default::default(), 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, rel_path("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.into())
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.into())
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.into())
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.into())
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.into())
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.into())
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, rel_path("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.into())
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.into())
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, rel_path("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().map(Into::into),
1520 workspace.leader_for_pane(workspace.active_pane())
1521 );
1522 let item = workspace.active_item(cx).unwrap();
1523 assert_eq!(item.tab_content_text(0, cx), SharedString::from("w.rs"));
1524 });
1525
1526 // TODO: in app code, this would be done by the collab_ui.
1527 active_call_b
1528 .update(&mut cx_b2, |call, cx| {
1529 let project = workspace_b_project_a.read(cx).project().clone();
1530 call.set_location(Some(&project), cx)
1531 })
1532 .await
1533 .unwrap();
1534
1535 // assert that there are no share notifications open
1536 assert_eq!(visible_push_notifications(cx_b).len(), 0);
1537
1538 // b moves to x.rs in a's project, and a follows
1539 workspace_b_project_a
1540 .update_in(&mut cx_b2, |workspace, window, cx| {
1541 workspace.open_path((worktree_id_a, rel_path("x.rs")), None, true, window, cx)
1542 })
1543 .await
1544 .unwrap();
1545
1546 executor.run_until_parked();
1547 workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1548 let item = workspace.active_item(cx).unwrap();
1549 assert_eq!(item.tab_content_text(0, cx), SharedString::from("x.rs"));
1550 });
1551
1552 workspace_a.update_in(cx_a, |workspace, window, cx| {
1553 workspace.follow(client_b.peer_id().unwrap(), window, cx)
1554 });
1555
1556 executor.run_until_parked();
1557 workspace_a.update(cx_a, |workspace, cx| {
1558 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1559 assert_eq!(
1560 client_b.peer_id().map(Into::into),
1561 workspace.leader_for_pane(workspace.active_pane())
1562 );
1563 let item = workspace.active_pane().read(cx).active_item().unwrap();
1564 assert_eq!(item.tab_content_text(0, cx), "x.rs");
1565 });
1566
1567 // b moves to y.rs in b's project, a is still following but can't yet see
1568 workspace_b
1569 .update_in(cx_b, |workspace, window, cx| {
1570 workspace.open_path((worktree_id_b, rel_path("y.rs")), None, true, window, cx)
1571 })
1572 .await
1573 .unwrap();
1574
1575 // TODO: in app code, this would be done by the collab_ui.
1576 active_call_b
1577 .update(cx_b, |call, cx| {
1578 let project = workspace_b.read(cx).project().clone();
1579 call.set_location(Some(&project), cx)
1580 })
1581 .await
1582 .unwrap();
1583
1584 let project_b_id = active_call_b
1585 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1586 .await
1587 .unwrap();
1588
1589 executor.run_until_parked();
1590 assert_eq!(visible_push_notifications(cx_a).len(), 1);
1591 cx_a.update(|_, cx| {
1592 workspace::join_in_room_project(
1593 project_b_id,
1594 client_b.user_id().unwrap(),
1595 client_a.app_state.clone(),
1596 cx,
1597 )
1598 })
1599 .await
1600 .unwrap();
1601
1602 executor.run_until_parked();
1603
1604 assert_eq!(visible_push_notifications(cx_a).len(), 0);
1605 let window_a_project_b = *cx_a
1606 .windows()
1607 .iter()
1608 .max_by_key(|window| window.window_id())
1609 .unwrap();
1610 let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a);
1611 let workspace_a_project_b = window_a_project_b
1612 .downcast::<Workspace>()
1613 .unwrap()
1614 .root(cx_a)
1615 .unwrap();
1616
1617 executor.run_until_parked();
1618
1619 workspace_a_project_b.update(cx_a2, |workspace, cx| {
1620 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1621 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1622 assert_eq!(
1623 client_b.peer_id().map(Into::into),
1624 workspace.leader_for_pane(workspace.active_pane())
1625 );
1626 let item = workspace.active_item(cx).unwrap();
1627 assert_eq!(item.tab_content_text(0, cx), SharedString::from("y.rs"));
1628 });
1629}
1630
1631#[gpui::test]
1632async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1633 let (_server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
1634
1635 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
1636 client_a
1637 .host_workspace(&workspace_a, channel_id, cx_a)
1638 .await;
1639 let (workspace_b, cx_b) = client_b.join_workspace(channel_id, cx_b).await;
1640
1641 cx_a.simulate_keystrokes("cmd-p");
1642 cx_a.run_until_parked();
1643 cx_a.simulate_keystrokes("2 enter");
1644
1645 let editor_a = workspace_a.update(cx_a, |workspace, cx| {
1646 workspace.active_item_as::<Editor>(cx).unwrap()
1647 });
1648 let editor_b = workspace_b.update(cx_b, |workspace, cx| {
1649 workspace.active_item_as::<Editor>(cx).unwrap()
1650 });
1651
1652 // b should follow a to position 1
1653 editor_a.update_in(cx_a, |editor, window, cx| {
1654 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1655 s.select_ranges([1..1])
1656 })
1657 });
1658 cx_a.executor()
1659 .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1660 cx_a.run_until_parked();
1661 editor_b.update(cx_b, |editor, cx| {
1662 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1663 });
1664
1665 // a unshares the project
1666 cx_a.update(|_, cx| {
1667 let project = workspace_a.read(cx).project().clone();
1668 ActiveCall::global(cx).update(cx, |call, cx| {
1669 call.unshare_project(project, cx).unwrap();
1670 })
1671 });
1672 cx_a.run_until_parked();
1673
1674 // b should not follow a to position 2
1675 editor_a.update_in(cx_a, |editor, window, cx| {
1676 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1677 s.select_ranges([2..2])
1678 })
1679 });
1680 cx_a.executor()
1681 .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1682 cx_a.run_until_parked();
1683 editor_b.update(cx_b, |editor, cx| {
1684 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1685 });
1686 cx_b.update(|_, cx| {
1687 let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
1688 let participant = room.remote_participants().get(&client_a.id()).unwrap();
1689 assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
1690 })
1691}
1692
1693#[gpui::test]
1694async fn test_following_into_excluded_file(
1695 mut cx_a: &mut TestAppContext,
1696 mut cx_b: &mut TestAppContext,
1697) {
1698 let executor = cx_a.executor();
1699 let mut server = TestServer::start(executor.clone()).await;
1700 let client_a = server.create_client(cx_a, "user_a").await;
1701 let client_b = server.create_client(cx_b, "user_b").await;
1702 for cx in [&mut cx_a, &mut cx_b] {
1703 cx.update(|cx| {
1704 cx.update_global::<SettingsStore, _>(|store, cx| {
1705 store.update_user_settings(cx, |settings| {
1706 settings.project.worktree.file_scan_exclusions =
1707 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, rel_path("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(
1764 (worktree_id, rel_path(".git/COMMIT_EDITMSG")),
1765 None,
1766 true,
1767 window,
1768 cx,
1769 )
1770 })
1771 .await
1772 .unwrap()
1773 .downcast::<Editor>()
1774 .unwrap();
1775
1776 // Client A updates their selections in those editors
1777 editor_for_regular.update_in(cx_a, |editor, window, cx| {
1778 editor.handle_input("a", window, cx);
1779 editor.handle_input("b", window, cx);
1780 editor.handle_input("c", window, cx);
1781 editor.select_left(&Default::default(), window, cx);
1782 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1783 });
1784 editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1785 editor.select_all(&Default::default(), window, cx);
1786 editor.handle_input("new commit message", window, cx);
1787 editor.select_left(&Default::default(), window, cx);
1788 assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1789 });
1790
1791 // When client B starts following client A, currently visible file is replicated
1792 workspace_b.update_in(cx_b, |workspace, window, cx| {
1793 workspace.follow(peer_id_a, window, cx)
1794 });
1795 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1796 executor.run_until_parked();
1797
1798 let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
1799 workspace
1800 .active_item(cx)
1801 .unwrap()
1802 .downcast::<Editor>()
1803 .unwrap()
1804 });
1805 assert_eq!(
1806 cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1807 Some((worktree_id, rel_path(".git/COMMIT_EDITMSG")).into())
1808 );
1809 assert_eq!(
1810 editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
1811 vec![18..17]
1812 );
1813
1814 editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1815 editor.select_right(&Default::default(), window, cx);
1816 });
1817 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1818 executor.run_until_parked();
1819
1820 // Changes from B to the excluded file are replicated in A's editor
1821 editor_for_excluded_b.update_in(cx_b, |editor, window, cx| {
1822 editor.handle_input("\nCo-Authored-By: B <b@b.b>", window, cx);
1823 });
1824 executor.run_until_parked();
1825 editor_for_excluded_a.update(cx_a, |editor, cx| {
1826 assert_eq!(
1827 editor.text(cx),
1828 "new commit message\nCo-Authored-By: B <b@b.b>"
1829 );
1830 });
1831}
1832
1833fn visible_push_notifications(cx: &mut TestAppContext) -> Vec<Entity<ProjectSharedNotification>> {
1834 let mut ret = Vec::new();
1835 for window in cx.windows() {
1836 window
1837 .update(cx, |window, _, _| {
1838 if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
1839 ret.push(handle)
1840 }
1841 })
1842 .unwrap();
1843 }
1844 ret
1845}
1846
1847#[derive(Debug, PartialEq, Eq)]
1848struct PaneSummary {
1849 active: bool,
1850 leader: Option<PeerId>,
1851 items: Vec<(bool, String)>,
1852}
1853
1854fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1855 cx.read(|cx| {
1856 let active_call = ActiveCall::global(cx).read(cx);
1857 let peer_id = active_call.client().peer_id();
1858 let room = active_call.room().unwrap().read(cx);
1859 let mut result = room
1860 .remote_participants()
1861 .values()
1862 .map(|participant| participant.peer_id)
1863 .chain(peer_id)
1864 .filter_map(|peer_id| {
1865 let followers = room.followers_for(peer_id, project_id);
1866 if followers.is_empty() {
1867 None
1868 } else {
1869 Some((peer_id, followers.to_vec()))
1870 }
1871 })
1872 .collect::<Vec<_>>();
1873 result.sort_by_key(|e| e.0);
1874 result
1875 })
1876}
1877
1878fn pane_summaries(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
1879 workspace.update(cx, |workspace, cx| {
1880 let active_pane = workspace.active_pane();
1881 workspace
1882 .panes()
1883 .iter()
1884 .map(|pane| {
1885 let leader = match workspace.leader_for_pane(pane) {
1886 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
1887 Some(CollaboratorId::Agent) => unimplemented!(),
1888 None => None,
1889 };
1890 let active = pane == active_pane;
1891 let pane = pane.read(cx);
1892 let active_ix = pane.active_item_index();
1893 PaneSummary {
1894 active,
1895 leader,
1896 items: pane
1897 .items()
1898 .enumerate()
1899 .map(|(ix, item)| (ix == active_ix, item.tab_content_text(0, cx).into()))
1900 .collect(),
1901 }
1902 })
1903 .collect()
1904 })
1905}
1906
1907#[gpui::test(iterations = 10)]
1908async fn test_following_to_channel_notes_without_a_shared_project(
1909 deterministic: BackgroundExecutor,
1910 mut cx_a: &mut TestAppContext,
1911 mut cx_b: &mut TestAppContext,
1912 mut cx_c: &mut TestAppContext,
1913) {
1914 let mut server = TestServer::start(deterministic.clone()).await;
1915 let client_a = server.create_client(cx_a, "user_a").await;
1916 let client_b = server.create_client(cx_b, "user_b").await;
1917 let client_c = server.create_client(cx_c, "user_c").await;
1918
1919 cx_a.update(editor::init);
1920 cx_b.update(editor::init);
1921 cx_c.update(editor::init);
1922 cx_a.update(collab_ui::channel_view::init);
1923 cx_b.update(collab_ui::channel_view::init);
1924 cx_c.update(collab_ui::channel_view::init);
1925
1926 let channel_1_id = server
1927 .make_channel(
1928 "channel-1",
1929 None,
1930 (&client_a, cx_a),
1931 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1932 )
1933 .await;
1934 let channel_2_id = server
1935 .make_channel(
1936 "channel-2",
1937 None,
1938 (&client_a, cx_a),
1939 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1940 )
1941 .await;
1942
1943 // Clients A, B, and C join a channel.
1944 let active_call_a = cx_a.read(ActiveCall::global);
1945 let active_call_b = cx_b.read(ActiveCall::global);
1946 let active_call_c = cx_c.read(ActiveCall::global);
1947 for (call, cx) in [
1948 (&active_call_a, &mut cx_a),
1949 (&active_call_b, &mut cx_b),
1950 (&active_call_c, &mut cx_c),
1951 ] {
1952 call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
1953 .await
1954 .unwrap();
1955 }
1956 deterministic.run_until_parked();
1957
1958 // Clients A, B, and C all open their own unshared projects.
1959 client_a
1960 .fs()
1961 .insert_tree("/a", json!({ "1.txt": "" }))
1962 .await;
1963 client_b.fs().insert_tree("/b", json!({})).await;
1964 client_c.fs().insert_tree("/c", json!({})).await;
1965 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1966 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1967 let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
1968 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1969 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1970 let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
1971
1972 active_call_a
1973 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1974 .await
1975 .unwrap();
1976
1977 // Client A opens the notes for channel 1.
1978 let channel_notes_1_a = cx_a
1979 .update(|window, cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), window, cx))
1980 .await
1981 .unwrap();
1982 channel_notes_1_a.update_in(cx_a, |notes, window, cx| {
1983 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1984 notes.editor.update(cx, |editor, cx| {
1985 editor.insert("Hello from A.", window, cx);
1986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1987 selections.select_ranges(vec![3..4]);
1988 });
1989 });
1990 });
1991
1992 // Client B follows client A.
1993 workspace_b
1994 .update_in(cx_b, |workspace, window, cx| {
1995 workspace
1996 .start_following(client_a.peer_id().unwrap(), window, cx)
1997 .unwrap()
1998 })
1999 .await
2000 .unwrap();
2001
2002 // Client B is taken to the notes for channel 1, with the same
2003 // text selected as client A.
2004 deterministic.run_until_parked();
2005 let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
2006 assert_eq!(
2007 workspace.leader_for_pane(workspace.active_pane()),
2008 Some(client_a.peer_id().unwrap().into())
2009 );
2010 workspace
2011 .active_item(cx)
2012 .expect("no active item")
2013 .downcast::<ChannelView>()
2014 .expect("active item is not a channel view")
2015 });
2016 channel_notes_1_b.update(cx_b, |notes, cx| {
2017 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
2018 notes.editor.update(cx, |editor, cx| {
2019 assert_eq!(editor.text(cx), "Hello from A.");
2020 assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
2021 })
2022 });
2023
2024 // Client A opens the notes for channel 2.
2025 let channel_notes_2_a = cx_a
2026 .update(|window, cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), window, cx))
2027 .await
2028 .unwrap();
2029 channel_notes_2_a.update(cx_a, |notes, cx| {
2030 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2031 });
2032
2033 // Client B is taken to the notes for channel 2.
2034 deterministic.run_until_parked();
2035 let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
2036 assert_eq!(
2037 workspace.leader_for_pane(workspace.active_pane()),
2038 Some(client_a.peer_id().unwrap().into())
2039 );
2040 workspace
2041 .active_item(cx)
2042 .expect("no active item")
2043 .downcast::<ChannelView>()
2044 .expect("active item is not a channel view")
2045 });
2046 channel_notes_2_b.update(cx_b, |notes, cx| {
2047 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2048 });
2049
2050 // Client A opens a local buffer in their unshared project.
2051 let _unshared_editor_a1 = workspace_a
2052 .update_in(cx_a, |workspace, window, cx| {
2053 workspace.open_path((worktree_id, rel_path("1.txt")), None, true, window, cx)
2054 })
2055 .await
2056 .unwrap()
2057 .downcast::<Editor>()
2058 .unwrap();
2059
2060 // This does not send any leader update message to client B.
2061 // If it did, an error would occur on client B, since this buffer
2062 // is not shared with them.
2063 deterministic.run_until_parked();
2064 workspace_b.update(cx_b, |workspace, cx| {
2065 assert_eq!(
2066 workspace.active_item(cx).expect("no active item").item_id(),
2067 channel_notes_2_b.entity_id()
2068 );
2069 });
2070}
2071
2072pub(crate) async fn join_channel(
2073 channel_id: ChannelId,
2074 client: &TestClient,
2075 cx: &mut TestAppContext,
2076) -> anyhow::Result<()> {
2077 cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
2078 .await
2079}
2080
2081async fn share_workspace(
2082 workspace: &Entity<Workspace>,
2083 cx: &mut VisualTestContext,
2084) -> anyhow::Result<u64> {
2085 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
2086 cx.read(ActiveCall::global)
2087 .update(cx, |call, cx| call.share_project(project, cx))
2088 .await
2089}
2090
2091#[gpui::test]
2092async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2093 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2094
2095 let (workspace, cx_a) = client_a.build_test_workspace(cx_a).await;
2096 join_channel(channel, &client_a, cx_a).await.unwrap();
2097 share_workspace(&workspace, cx_a).await.unwrap();
2098 let buffer = workspace.update(cx_a, |workspace, cx| {
2099 workspace.project().update(cx, |project, cx| {
2100 project.create_local_buffer(&sample_text(26, 5, 'a'), None, false, cx)
2101 })
2102 });
2103 let multibuffer = cx_a.new(|cx| {
2104 let mut mb = MultiBuffer::new(Capability::ReadWrite);
2105 mb.set_excerpts_for_path(
2106 PathKey::for_buffer(&buffer, cx),
2107 buffer.clone(),
2108 [Point::row_range(1..1), Point::row_range(5..5)],
2109 1,
2110 cx,
2111 );
2112 mb
2113 });
2114 let snapshot = buffer.update(cx_a, |buffer, _| buffer.snapshot());
2115 let editor: Entity<Editor> = cx_a.new_window_entity(|window, cx| {
2116 Editor::for_multibuffer(
2117 multibuffer.clone(),
2118 Some(workspace.read(cx).project().clone()),
2119 window,
2120 cx,
2121 )
2122 });
2123 workspace.update_in(cx_a, |workspace, window, cx| {
2124 workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
2125 });
2126 editor.update_in(cx_a, |editor, window, cx| {
2127 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2128 s.select_ranges([Point::row_range(4..4)]);
2129 })
2130 });
2131 let positions = editor.update(cx_a, |editor, _| {
2132 editor
2133 .selections
2134 .disjoint_anchor_ranges()
2135 .map(|range| range.start.text_anchor.to_point(&snapshot))
2136 .collect::<Vec<_>>()
2137 });
2138 multibuffer.update(cx_a, |multibuffer, cx| {
2139 multibuffer.set_excerpts_for_path(
2140 PathKey::for_buffer(&buffer, cx),
2141 buffer,
2142 [Point::row_range(1..5)],
2143 1,
2144 cx,
2145 );
2146 });
2147
2148 let (workspace_b, cx_b) = client_b.join_workspace(channel, cx_b).await;
2149 cx_b.run_until_parked();
2150 let editor_b = workspace_b
2151 .update(cx_b, |workspace, cx| {
2152 workspace
2153 .active_item(cx)
2154 .and_then(|item| item.downcast::<Editor>())
2155 })
2156 .unwrap();
2157
2158 let new_positions = editor_b.update(cx_b, |editor, _| {
2159 editor
2160 .selections
2161 .disjoint_anchor_ranges()
2162 .map(|range| range.start.text_anchor.to_point(&snapshot))
2163 .collect::<Vec<_>>()
2164 });
2165 assert_eq!(positions, new_positions);
2166}
2167
2168#[gpui::test]
2169async fn test_following_to_channel_notes_other_workspace(
2170 cx_a: &mut TestAppContext,
2171 cx_b: &mut TestAppContext,
2172) {
2173 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2174
2175 let mut cx_a2 = cx_a.clone();
2176 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2177 join_channel(channel, &client_a, cx_a).await.unwrap();
2178 share_workspace(&workspace_a, cx_a).await.unwrap();
2179
2180 // a opens 1.txt
2181 cx_a.simulate_keystrokes("cmd-p");
2182 cx_a.run_until_parked();
2183 cx_a.simulate_keystrokes("1 enter");
2184 cx_a.run_until_parked();
2185 workspace_a.update(cx_a, |workspace, cx| {
2186 let editor = workspace.active_item(cx).unwrap();
2187 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2188 });
2189
2190 // b joins channel and is following a
2191 join_channel(channel, &client_b, cx_b).await.unwrap();
2192 cx_b.run_until_parked();
2193 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2194 workspace_b.update(cx_b, |workspace, cx| {
2195 let editor = workspace.active_item(cx).unwrap();
2196 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2197 });
2198
2199 // a opens a second workspace and the channel notes
2200 let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2201 cx_a2.update(|window, _| window.activate_window());
2202 cx_a2
2203 .update(|window, cx| ChannelView::open(channel, None, workspace_a2, window, cx))
2204 .await
2205 .unwrap();
2206 cx_a2.run_until_parked();
2207
2208 // b should follow a to the channel notes
2209 workspace_b.update(cx_b, |workspace, cx| {
2210 let editor = workspace.active_item_as::<ChannelView>(cx).unwrap();
2211 assert_eq!(editor.read(cx).channel(cx).unwrap().id, channel);
2212 });
2213
2214 // a returns to the shared project
2215 cx_a.update(|window, _| window.activate_window());
2216 cx_a.run_until_parked();
2217
2218 workspace_a.update(cx_a, |workspace, cx| {
2219 let editor = workspace.active_item(cx).unwrap();
2220 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2221 });
2222
2223 // b should follow a back
2224 workspace_b.update(cx_b, |workspace, cx| {
2225 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2226 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2227 });
2228}
2229
2230#[gpui::test]
2231async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2232 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2233
2234 let mut cx_a2 = cx_a.clone();
2235 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2236 join_channel(channel, &client_a, cx_a).await.unwrap();
2237 share_workspace(&workspace_a, cx_a).await.unwrap();
2238
2239 // a opens 1.txt
2240 cx_a.simulate_keystrokes("cmd-p");
2241 cx_a.run_until_parked();
2242 cx_a.simulate_keystrokes("1 enter");
2243 cx_a.run_until_parked();
2244 workspace_a.update(cx_a, |workspace, cx| {
2245 let editor = workspace.active_item(cx).unwrap();
2246 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2247 });
2248
2249 // b joins channel and is following a
2250 join_channel(channel, &client_b, cx_b).await.unwrap();
2251 cx_b.run_until_parked();
2252 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2253 workspace_b.update(cx_b, |workspace, cx| {
2254 let editor = workspace.active_item(cx).unwrap();
2255 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2256 });
2257
2258 // stop following
2259 cx_b.simulate_keystrokes("down");
2260
2261 // a opens a different file while not followed
2262 cx_a.simulate_keystrokes("cmd-p");
2263 cx_a.run_until_parked();
2264 cx_a.simulate_keystrokes("2 enter");
2265
2266 workspace_b.update(cx_b, |workspace, cx| {
2267 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2268 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2269 });
2270
2271 // a opens a file in a new window
2272 let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2273 cx_a2.update(|window, _| window.activate_window());
2274 cx_a2.simulate_keystrokes("cmd-p");
2275 cx_a2.run_until_parked();
2276 cx_a2.simulate_keystrokes("3 enter");
2277 cx_a2.run_until_parked();
2278
2279 // b starts following a again
2280 cx_b.simulate_keystrokes("cmd-ctrl-alt-f");
2281 cx_a.run_until_parked();
2282
2283 // a returns to the shared project
2284 cx_a.update(|window, _| window.activate_window());
2285 cx_a.run_until_parked();
2286
2287 workspace_a.update(cx_a, |workspace, cx| {
2288 let editor = workspace.active_item(cx).unwrap();
2289 assert_eq!(editor.tab_content_text(0, cx), "2.js");
2290 });
2291
2292 // b should follow a back
2293 workspace_b.update(cx_b, |workspace, cx| {
2294 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2295 assert_eq!(editor.tab_content_text(0, cx), "2.js");
2296 });
2297}