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 project::WorktreeSettings;
16use rpc::proto::PeerId;
17use serde_json::json;
18use settings::SettingsStore;
19use text::{Point, ToPoint};
20use util::{path, test::sample_text};
21use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _};
22
23use super::TestClient;
24
25#[gpui::test(iterations = 10)]
26async fn test_basic_following(
27 cx_a: &mut TestAppContext,
28 cx_b: &mut TestAppContext,
29 cx_c: &mut TestAppContext,
30 cx_d: &mut TestAppContext,
31) {
32 let executor = cx_a.executor();
33 let mut server = TestServer::start(executor.clone()).await;
34 let client_a = server.create_client(cx_a, "user_a").await;
35 let client_b = server.create_client(cx_b, "user_b").await;
36 let client_c = server.create_client(cx_c, "user_c").await;
37 let client_d = server.create_client(cx_d, "user_d").await;
38 server
39 .create_room(&mut [
40 (&client_a, cx_a),
41 (&client_b, cx_b),
42 (&client_c, cx_c),
43 (&client_d, cx_d),
44 ])
45 .await;
46 let active_call_a = cx_a.read(ActiveCall::global);
47 let active_call_b = cx_b.read(ActiveCall::global);
48
49 cx_a.update(editor::init);
50 cx_b.update(editor::init);
51
52 client_a
53 .fs()
54 .insert_tree(
55 path!("/a"),
56 json!({
57 "1.txt": "one\none\none",
58 "2.txt": "two\ntwo\ntwo",
59 "3.txt": "three\nthree\nthree",
60 }),
61 )
62 .await;
63 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
64 active_call_a
65 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
66 .await
67 .unwrap();
68
69 let project_id = active_call_a
70 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
71 .await
72 .unwrap();
73 let project_b = client_b.join_remote_project(project_id, cx_b).await;
74 active_call_b
75 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
76 .await
77 .unwrap();
78
79 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
80 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
81
82 cx_b.update(|window, _| {
83 assert!(window.is_window_active());
84 });
85
86 // Client A opens some editors.
87 let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
88 let editor_a1 = workspace_a
89 .update_in(cx_a, |workspace, window, cx| {
90 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
91 })
92 .await
93 .unwrap()
94 .downcast::<Editor>()
95 .unwrap();
96 let editor_a2 = workspace_a
97 .update_in(cx_a, |workspace, window, cx| {
98 workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
99 })
100 .await
101 .unwrap()
102 .downcast::<Editor>()
103 .unwrap();
104
105 // Client B opens an editor.
106 let editor_b1 = workspace_b
107 .update_in(cx_b, |workspace, window, cx| {
108 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
109 })
110 .await
111 .unwrap()
112 .downcast::<Editor>()
113 .unwrap();
114
115 let peer_id_a = client_a.peer_id().unwrap();
116 let peer_id_b = client_b.peer_id().unwrap();
117 let peer_id_c = client_c.peer_id().unwrap();
118 let peer_id_d = client_d.peer_id().unwrap();
119
120 // Client A updates their selections in those editors
121 editor_a1.update_in(cx_a, |editor, window, cx| {
122 editor.handle_input("a", window, cx);
123 editor.handle_input("b", window, cx);
124 editor.handle_input("c", window, cx);
125 editor.select_left(&Default::default(), window, cx);
126 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
127 });
128 editor_a2.update_in(cx_a, |editor, window, cx| {
129 editor.handle_input("d", window, cx);
130 editor.handle_input("e", window, cx);
131 editor.select_left(&Default::default(), window, cx);
132 assert_eq!(editor.selections.ranges(cx), vec![2..1]);
133 });
134
135 // When client B starts following client A, only the active view state is replicated to client B.
136 workspace_b.update_in(cx_b, |workspace, window, cx| {
137 workspace.follow(peer_id_a, window, cx)
138 });
139
140 cx_c.executor().run_until_parked();
141 let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
142 workspace
143 .active_item(cx)
144 .unwrap()
145 .downcast::<Editor>()
146 .unwrap()
147 });
148 assert_eq!(
149 cx_b.read(|cx| editor_b2.project_path(cx)),
150 Some((worktree_id, "2.txt").into())
151 );
152 assert_eq!(
153 editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
154 vec![2..1]
155 );
156 assert_eq!(
157 editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
158 vec![3..3]
159 );
160
161 executor.run_until_parked();
162 let active_call_c = cx_c.read(ActiveCall::global);
163 let project_c = client_c.join_remote_project(project_id, cx_c).await;
164 let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c);
165 active_call_c
166 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
167 .await
168 .unwrap();
169 drop(project_c);
170
171 // Client C also follows client A.
172 workspace_c.update_in(cx_c, |workspace, window, cx| {
173 workspace.follow(peer_id_a, window, cx)
174 });
175
176 cx_d.executor().run_until_parked();
177 let active_call_d = cx_d.read(ActiveCall::global);
178 let project_d = client_d.join_remote_project(project_id, cx_d).await;
179 let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d);
180 active_call_d
181 .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
182 .await
183 .unwrap();
184 drop(project_d);
185
186 // All clients see that clients B and C are following client A.
187 cx_c.executor().run_until_parked();
188 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
189 assert_eq!(
190 followers_by_leader(project_id, cx),
191 &[(peer_id_a, vec![peer_id_b, peer_id_c])],
192 "followers seen by {name}"
193 );
194 }
195
196 // Client C unfollows client A.
197 workspace_c.update_in(cx_c, |workspace, window, cx| {
198 workspace.unfollow(peer_id_a, window, cx).unwrap();
199 });
200
201 // All clients see that clients B is following client A.
202 cx_c.executor().run_until_parked();
203 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
204 assert_eq!(
205 followers_by_leader(project_id, cx),
206 &[(peer_id_a, vec![peer_id_b])],
207 "followers seen by {name}"
208 );
209 }
210
211 // Client C re-follows client A.
212 workspace_c.update_in(cx_c, |workspace, window, cx| {
213 workspace.follow(peer_id_a, window, cx)
214 });
215
216 // All clients see that clients B and C are following client A.
217 cx_c.executor().run_until_parked();
218 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
219 assert_eq!(
220 followers_by_leader(project_id, cx),
221 &[(peer_id_a, vec![peer_id_b, peer_id_c])],
222 "followers seen by {name}"
223 );
224 }
225
226 // Client D follows client B, then switches to following client C.
227 workspace_d.update_in(cx_d, |workspace, window, cx| {
228 workspace.follow(peer_id_b, window, cx)
229 });
230 cx_a.executor().run_until_parked();
231 workspace_d.update_in(cx_d, |workspace, window, cx| {
232 workspace.follow(peer_id_c, window, cx)
233 });
234
235 // All clients see that D is following C
236 cx_a.executor().run_until_parked();
237 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
238 assert_eq!(
239 followers_by_leader(project_id, cx),
240 &[
241 (peer_id_a, vec![peer_id_b, peer_id_c]),
242 (peer_id_c, vec![peer_id_d])
243 ],
244 "followers seen by {name}"
245 );
246 }
247
248 // Client C closes the project.
249 let weak_workspace_c = workspace_c.downgrade();
250 workspace_c.update_in(cx_c, |workspace, window, cx| {
251 workspace.close_window(&Default::default(), window, cx);
252 });
253 executor.run_until_parked();
254 // are you sure you want to leave the call?
255 cx_c.simulate_prompt_answer("Close window and hang up");
256 cx_c.cx.update(|_| {
257 drop(workspace_c);
258 });
259 executor.run_until_parked();
260 cx_c.cx.update(|_| {});
261
262 weak_workspace_c.assert_released();
263
264 // Clients A and B see that client B is following A, and client C is not present in the followers.
265 executor.run_until_parked();
266 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("D", &cx_d)] {
267 assert_eq!(
268 followers_by_leader(project_id, cx),
269 &[(peer_id_a, vec![peer_id_b]),],
270 "followers seen by {name}"
271 );
272 }
273
274 // When client A activates a different editor, client B does so as well.
275 workspace_a.update_in(cx_a, |workspace, window, cx| {
276 workspace.activate_item(&editor_a1, true, true, window, cx)
277 });
278 executor.run_until_parked();
279 workspace_b.update(cx_b, |workspace, cx| {
280 assert_eq!(
281 workspace.active_item(cx).unwrap().item_id(),
282 editor_b1.item_id()
283 );
284 });
285
286 // When client A opens a multibuffer, client B does so as well.
287 let multibuffer_a = cx_a.new(|cx| {
288 let buffer_a1 = project_a.update(cx, |project, cx| {
289 project
290 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
291 .unwrap()
292 });
293 let buffer_a2 = project_a.update(cx, |project, cx| {
294 project
295 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
296 .unwrap()
297 });
298 let mut result = MultiBuffer::new(Capability::ReadWrite);
299 result.set_excerpts_for_path(
300 PathKey::for_buffer(&buffer_a1, cx),
301 buffer_a1,
302 [Point::row_range(1..2)],
303 1,
304 cx,
305 );
306 result.set_excerpts_for_path(
307 PathKey::for_buffer(&buffer_a2, cx),
308 buffer_a2,
309 [Point::row_range(5..6)],
310 1,
311 cx,
312 );
313 result
314 });
315 let multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
316 let editor = cx
317 .new(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), window, cx));
318 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
319 editor
320 });
321 executor.run_until_parked();
322 let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
323 workspace
324 .active_item(cx)
325 .unwrap()
326 .downcast::<Editor>()
327 .unwrap()
328 });
329 assert_eq!(
330 multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
331 multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
332 );
333
334 // When client A navigates back and forth, client B does so as well.
335 workspace_a
336 .update_in(cx_a, |workspace, window, cx| {
337 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
338 })
339 .await
340 .unwrap();
341 executor.run_until_parked();
342 workspace_b.update(cx_b, |workspace, cx| {
343 assert_eq!(
344 workspace.active_item(cx).unwrap().item_id(),
345 editor_b1.item_id()
346 );
347 });
348
349 workspace_a
350 .update_in(cx_a, |workspace, window, cx| {
351 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
352 })
353 .await
354 .unwrap();
355 executor.run_until_parked();
356 workspace_b.update(cx_b, |workspace, cx| {
357 assert_eq!(
358 workspace.active_item(cx).unwrap().item_id(),
359 editor_b2.item_id()
360 );
361 });
362
363 workspace_a
364 .update_in(cx_a, |workspace, window, cx| {
365 workspace.go_forward(workspace.active_pane().downgrade(), window, cx)
366 })
367 .await
368 .unwrap();
369 executor.run_until_parked();
370 workspace_b.update(cx_b, |workspace, cx| {
371 assert_eq!(
372 workspace.active_item(cx).unwrap().item_id(),
373 editor_b1.item_id()
374 );
375 });
376
377 // Changes to client A's editor are reflected on client B.
378 editor_a1.update_in(cx_a, |editor, window, cx| {
379 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
380 s.select_ranges([1..1, 2..2])
381 });
382 });
383 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
384 executor.run_until_parked();
385 cx_b.background_executor.run_until_parked();
386
387 editor_b1.update(cx_b, |editor, cx| {
388 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
389 });
390
391 editor_a1.update_in(cx_a, |editor, window, cx| {
392 editor.set_text("TWO", window, cx)
393 });
394 executor.run_until_parked();
395 editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
396
397 editor_a1.update_in(cx_a, |editor, window, cx| {
398 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
399 s.select_ranges([3..3])
400 });
401 editor.set_scroll_position(point(0., 100.), window, cx);
402 });
403 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
404 executor.run_until_parked();
405 editor_b1.update(cx_b, |editor, cx| {
406 assert_eq!(editor.selections.ranges(cx), &[3..3]);
407 });
408
409 // After unfollowing, client B stops receiving updates from client A.
410 workspace_b.update_in(cx_b, |workspace, window, cx| {
411 workspace.unfollow(peer_id_a, window, cx).unwrap()
412 });
413 workspace_a.update_in(cx_a, |workspace, window, cx| {
414 workspace.activate_item(&editor_a2, true, true, window, cx)
415 });
416 executor.run_until_parked();
417 assert_eq!(
418 workspace_b.update(cx_b, |workspace, cx| workspace
419 .active_item(cx)
420 .unwrap()
421 .item_id()),
422 editor_b1.item_id()
423 );
424
425 // Client A starts following client B.
426 workspace_a.update_in(cx_a, |workspace, window, cx| {
427 workspace.follow(peer_id_b, window, cx)
428 });
429 executor.run_until_parked();
430 assert_eq!(
431 workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
432 Some(peer_id_b.into())
433 );
434 assert_eq!(
435 workspace_a.update_in(cx_a, |workspace, _, cx| workspace
436 .active_item(cx)
437 .unwrap()
438 .item_id()),
439 editor_a1.item_id()
440 );
441
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::{DockPosition, test::TestPanel},
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();
467 executor.run_until_parked();
468
469 let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
470 workspace
471 .active_item(cx)
472 .expect("no active item")
473 .downcast::<SharedScreen>()
474 .expect("active item isn't a shared screen")
475 });
476
477 // Client B activates Zed again, which causes the previous editor to become focused again.
478 active_call_b
479 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
480 .await
481 .unwrap();
482 executor.run_until_parked();
483 workspace_a.update(cx_a, |workspace, cx| {
484 assert_eq!(
485 workspace.active_item(cx).unwrap().item_id(),
486 editor_a1.item_id()
487 )
488 });
489
490 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
491 workspace_b.update_in(cx_b, |workspace, window, cx| {
492 workspace.activate_item(&multibuffer_editor_b, true, true, window, cx)
493 });
494 executor.run_until_parked();
495 workspace_a.update(cx_a, |workspace, cx| {
496 assert_eq!(
497 workspace.active_item(cx).unwrap().item_id(),
498 multibuffer_editor_a.item_id()
499 )
500 });
501
502 // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
503 let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, cx));
504 workspace_b.update_in(cx_b, |workspace, window, cx| {
505 workspace.add_panel(panel, window, cx);
506 workspace.toggle_panel_focus::<TestPanel>(window, cx);
507 });
508 executor.run_until_parked();
509 assert_eq!(
510 workspace_a.update(cx_a, |workspace, cx| workspace
511 .active_item(cx)
512 .unwrap()
513 .item_id()),
514 shared_screen.item_id()
515 );
516
517 // Toggling the focus back to the pane causes client A to return to the multibuffer.
518 workspace_b.update_in(cx_b, |workspace, window, cx| {
519 workspace.toggle_panel_focus::<TestPanel>(window, cx);
520 });
521 executor.run_until_parked();
522 workspace_a.update(cx_a, |workspace, cx| {
523 assert_eq!(
524 workspace.active_item(cx).unwrap().item_id(),
525 multibuffer_editor_a.item_id()
526 )
527 });
528
529 // Client B activates an item that doesn't implement following,
530 // so the previously-opened screen-sharing item gets activated.
531 let unfollowable_item = cx_b.new(TestItem::new);
532 workspace_b.update_in(cx_b, |workspace, window, cx| {
533 workspace.active_pane().update(cx, |pane, cx| {
534 pane.add_item(Box::new(unfollowable_item), true, true, None, window, cx)
535 })
536 });
537 executor.run_until_parked();
538 assert_eq!(
539 workspace_a.update(cx_a, |workspace, cx| workspace
540 .active_item(cx)
541 .unwrap()
542 .item_id()),
543 shared_screen.item_id()
544 );
545
546 // Following interrupts when client B disconnects.
547 client_b.disconnect(&cx_b.to_async());
548 executor.advance_clock(RECONNECT_TIMEOUT);
549 assert_eq!(
550 workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
551 None
552 );
553 }
554}
555
556#[gpui::test]
557async fn test_following_tab_order(
558 executor: BackgroundExecutor,
559 cx_a: &mut TestAppContext,
560 cx_b: &mut TestAppContext,
561) {
562 let mut server = TestServer::start(executor.clone()).await;
563 let client_a = server.create_client(cx_a, "user_a").await;
564 let client_b = server.create_client(cx_b, "user_b").await;
565 server
566 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
567 .await;
568 let active_call_a = cx_a.read(ActiveCall::global);
569 let active_call_b = cx_b.read(ActiveCall::global);
570
571 cx_a.update(editor::init);
572 cx_b.update(editor::init);
573
574 client_a
575 .fs()
576 .insert_tree(
577 path!("/a"),
578 json!({
579 "1.txt": "one",
580 "2.txt": "two",
581 "3.txt": "three",
582 }),
583 )
584 .await;
585 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
586 active_call_a
587 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
588 .await
589 .unwrap();
590
591 let project_id = active_call_a
592 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
593 .await
594 .unwrap();
595 let project_b = client_b.join_remote_project(project_id, cx_b).await;
596 active_call_b
597 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
598 .await
599 .unwrap();
600
601 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
602 let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
603
604 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
605 let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
606
607 let client_b_id = project_a.update(cx_a, |project, _| {
608 project.collaborators().values().next().unwrap().peer_id
609 });
610
611 //Open 1, 3 in that order on client A
612 workspace_a
613 .update_in(cx_a, |workspace, window, cx| {
614 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
615 })
616 .await
617 .unwrap();
618 workspace_a
619 .update_in(cx_a, |workspace, window, cx| {
620 workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
621 })
622 .await
623 .unwrap();
624
625 let pane_paths = |pane: &Entity<workspace::Pane>, cx: &mut VisualTestContext| {
626 pane.update(cx, |pane, cx| {
627 pane.items()
628 .map(|item| {
629 item.project_path(cx)
630 .unwrap()
631 .path
632 .to_str()
633 .unwrap()
634 .to_owned()
635 })
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, "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, "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, "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, "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, "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, "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_prev_item(true, 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_inactive_items(&Default::default(), 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_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.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, "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, "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, "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, "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::<WorktreeSettings>(cx, |settings| {
1706 settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
1707 });
1708 });
1709 });
1710 }
1711 server
1712 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1713 .await;
1714 let active_call_a = cx_a.read(ActiveCall::global);
1715 let active_call_b = cx_b.read(ActiveCall::global);
1716 let peer_id_a = client_a.peer_id().unwrap();
1717
1718 client_a
1719 .fs()
1720 .insert_tree(
1721 path!("/a"),
1722 json!({
1723 ".git": {
1724 "COMMIT_EDITMSG": "write your commit message here",
1725 },
1726 "1.txt": "one\none\none",
1727 "2.txt": "two\ntwo\ntwo",
1728 "3.txt": "three\nthree\nthree",
1729 }),
1730 )
1731 .await;
1732 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1733 active_call_a
1734 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1735 .await
1736 .unwrap();
1737
1738 let project_id = active_call_a
1739 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1740 .await
1741 .unwrap();
1742 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1743 active_call_b
1744 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1745 .await
1746 .unwrap();
1747
1748 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1749 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1750
1751 // Client A opens editors for a regular file and an excluded file.
1752 let editor_for_regular = workspace_a
1753 .update_in(cx_a, |workspace, window, cx| {
1754 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
1755 })
1756 .await
1757 .unwrap()
1758 .downcast::<Editor>()
1759 .unwrap();
1760 let editor_for_excluded_a = workspace_a
1761 .update_in(cx_a, |workspace, window, cx| {
1762 workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, window, cx)
1763 })
1764 .await
1765 .unwrap()
1766 .downcast::<Editor>()
1767 .unwrap();
1768
1769 // Client A updates their selections in those editors
1770 editor_for_regular.update_in(cx_a, |editor, window, cx| {
1771 editor.handle_input("a", window, cx);
1772 editor.handle_input("b", window, cx);
1773 editor.handle_input("c", window, cx);
1774 editor.select_left(&Default::default(), window, cx);
1775 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1776 });
1777 editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1778 editor.select_all(&Default::default(), window, cx);
1779 editor.handle_input("new commit message", window, cx);
1780 editor.select_left(&Default::default(), window, cx);
1781 assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1782 });
1783
1784 // When client B starts following client A, currently visible file is replicated
1785 workspace_b.update_in(cx_b, |workspace, window, cx| {
1786 workspace.follow(peer_id_a, window, cx)
1787 });
1788 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1789 executor.run_until_parked();
1790
1791 let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
1792 workspace
1793 .active_item(cx)
1794 .unwrap()
1795 .downcast::<Editor>()
1796 .unwrap()
1797 });
1798 assert_eq!(
1799 cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1800 Some((worktree_id, ".git/COMMIT_EDITMSG").into())
1801 );
1802 assert_eq!(
1803 editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
1804 vec![18..17]
1805 );
1806
1807 editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1808 editor.select_right(&Default::default(), window, cx);
1809 });
1810 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1811 executor.run_until_parked();
1812
1813 // Changes from B to the excluded file are replicated in A's editor
1814 editor_for_excluded_b.update_in(cx_b, |editor, window, cx| {
1815 editor.handle_input("\nCo-Authored-By: B <b@b.b>", window, cx);
1816 });
1817 executor.run_until_parked();
1818 editor_for_excluded_a.update(cx_a, |editor, cx| {
1819 assert_eq!(
1820 editor.text(cx),
1821 "new commit message\nCo-Authored-By: B <b@b.b>"
1822 );
1823 });
1824}
1825
1826fn visible_push_notifications(cx: &mut TestAppContext) -> Vec<Entity<ProjectSharedNotification>> {
1827 let mut ret = Vec::new();
1828 for window in cx.windows() {
1829 window
1830 .update(cx, |window, _, _| {
1831 if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
1832 ret.push(handle)
1833 }
1834 })
1835 .unwrap();
1836 }
1837 ret
1838}
1839
1840#[derive(Debug, PartialEq, Eq)]
1841struct PaneSummary {
1842 active: bool,
1843 leader: Option<PeerId>,
1844 items: Vec<(bool, String)>,
1845}
1846
1847fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1848 cx.read(|cx| {
1849 let active_call = ActiveCall::global(cx).read(cx);
1850 let peer_id = active_call.client().peer_id();
1851 let room = active_call.room().unwrap().read(cx);
1852 let mut result = room
1853 .remote_participants()
1854 .values()
1855 .map(|participant| participant.peer_id)
1856 .chain(peer_id)
1857 .filter_map(|peer_id| {
1858 let followers = room.followers_for(peer_id, project_id);
1859 if followers.is_empty() {
1860 None
1861 } else {
1862 Some((peer_id, followers.to_vec()))
1863 }
1864 })
1865 .collect::<Vec<_>>();
1866 result.sort_by_key(|e| e.0);
1867 result
1868 })
1869}
1870
1871fn pane_summaries(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
1872 workspace.update(cx, |workspace, cx| {
1873 let active_pane = workspace.active_pane();
1874 workspace
1875 .panes()
1876 .iter()
1877 .map(|pane| {
1878 let leader = match workspace.leader_for_pane(pane) {
1879 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
1880 Some(CollaboratorId::Agent) => unimplemented!(),
1881 None => None,
1882 };
1883 let active = pane == active_pane;
1884 let pane = pane.read(cx);
1885 let active_ix = pane.active_item_index();
1886 PaneSummary {
1887 active,
1888 leader,
1889 items: pane
1890 .items()
1891 .enumerate()
1892 .map(|(ix, item)| (ix == active_ix, item.tab_content_text(0, cx).into()))
1893 .collect(),
1894 }
1895 })
1896 .collect()
1897 })
1898}
1899
1900#[gpui::test(iterations = 10)]
1901async fn test_following_to_channel_notes_without_a_shared_project(
1902 deterministic: BackgroundExecutor,
1903 mut cx_a: &mut TestAppContext,
1904 mut cx_b: &mut TestAppContext,
1905 mut cx_c: &mut TestAppContext,
1906) {
1907 let mut server = TestServer::start(deterministic.clone()).await;
1908 let client_a = server.create_client(cx_a, "user_a").await;
1909 let client_b = server.create_client(cx_b, "user_b").await;
1910 let client_c = server.create_client(cx_c, "user_c").await;
1911
1912 cx_a.update(editor::init);
1913 cx_b.update(editor::init);
1914 cx_c.update(editor::init);
1915 cx_a.update(collab_ui::channel_view::init);
1916 cx_b.update(collab_ui::channel_view::init);
1917 cx_c.update(collab_ui::channel_view::init);
1918
1919 let channel_1_id = server
1920 .make_channel(
1921 "channel-1",
1922 None,
1923 (&client_a, cx_a),
1924 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1925 )
1926 .await;
1927 let channel_2_id = server
1928 .make_channel(
1929 "channel-2",
1930 None,
1931 (&client_a, cx_a),
1932 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1933 )
1934 .await;
1935
1936 // Clients A, B, and C join a channel.
1937 let active_call_a = cx_a.read(ActiveCall::global);
1938 let active_call_b = cx_b.read(ActiveCall::global);
1939 let active_call_c = cx_c.read(ActiveCall::global);
1940 for (call, cx) in [
1941 (&active_call_a, &mut cx_a),
1942 (&active_call_b, &mut cx_b),
1943 (&active_call_c, &mut cx_c),
1944 ] {
1945 call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
1946 .await
1947 .unwrap();
1948 }
1949 deterministic.run_until_parked();
1950
1951 // Clients A, B, and C all open their own unshared projects.
1952 client_a
1953 .fs()
1954 .insert_tree("/a", json!({ "1.txt": "" }))
1955 .await;
1956 client_b.fs().insert_tree("/b", json!({})).await;
1957 client_c.fs().insert_tree("/c", json!({})).await;
1958 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1959 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1960 let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
1961 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1962 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1963 let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
1964
1965 active_call_a
1966 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1967 .await
1968 .unwrap();
1969
1970 // Client A opens the notes for channel 1.
1971 let channel_notes_1_a = cx_a
1972 .update(|window, cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), window, cx))
1973 .await
1974 .unwrap();
1975 channel_notes_1_a.update_in(cx_a, |notes, window, cx| {
1976 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1977 notes.editor.update(cx, |editor, cx| {
1978 editor.insert("Hello from A.", window, cx);
1979 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1980 selections.select_ranges(vec![3..4]);
1981 });
1982 });
1983 });
1984
1985 // Client B follows client A.
1986 workspace_b
1987 .update_in(cx_b, |workspace, window, cx| {
1988 workspace
1989 .start_following(client_a.peer_id().unwrap(), window, cx)
1990 .unwrap()
1991 })
1992 .await
1993 .unwrap();
1994
1995 // Client B is taken to the notes for channel 1, with the same
1996 // text selected as client A.
1997 deterministic.run_until_parked();
1998 let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
1999 assert_eq!(
2000 workspace.leader_for_pane(workspace.active_pane()),
2001 Some(client_a.peer_id().unwrap().into())
2002 );
2003 workspace
2004 .active_item(cx)
2005 .expect("no active item")
2006 .downcast::<ChannelView>()
2007 .expect("active item is not a channel view")
2008 });
2009 channel_notes_1_b.update(cx_b, |notes, cx| {
2010 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
2011 notes.editor.update(cx, |editor, cx| {
2012 assert_eq!(editor.text(cx), "Hello from A.");
2013 assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
2014 })
2015 });
2016
2017 // Client A opens the notes for channel 2.
2018 let channel_notes_2_a = cx_a
2019 .update(|window, cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), window, cx))
2020 .await
2021 .unwrap();
2022 channel_notes_2_a.update(cx_a, |notes, cx| {
2023 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2024 });
2025
2026 // Client B is taken to the notes for channel 2.
2027 deterministic.run_until_parked();
2028 let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
2029 assert_eq!(
2030 workspace.leader_for_pane(workspace.active_pane()),
2031 Some(client_a.peer_id().unwrap().into())
2032 );
2033 workspace
2034 .active_item(cx)
2035 .expect("no active item")
2036 .downcast::<ChannelView>()
2037 .expect("active item is not a channel view")
2038 });
2039 channel_notes_2_b.update(cx_b, |notes, cx| {
2040 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2041 });
2042
2043 // Client A opens a local buffer in their unshared project.
2044 let _unshared_editor_a1 = workspace_a
2045 .update_in(cx_a, |workspace, window, cx| {
2046 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
2047 })
2048 .await
2049 .unwrap()
2050 .downcast::<Editor>()
2051 .unwrap();
2052
2053 // This does not send any leader update message to client B.
2054 // If it did, an error would occur on client B, since this buffer
2055 // is not shared with them.
2056 deterministic.run_until_parked();
2057 workspace_b.update(cx_b, |workspace, cx| {
2058 assert_eq!(
2059 workspace.active_item(cx).expect("no active item").item_id(),
2060 channel_notes_2_b.entity_id()
2061 );
2062 });
2063}
2064
2065pub(crate) async fn join_channel(
2066 channel_id: ChannelId,
2067 client: &TestClient,
2068 cx: &mut TestAppContext,
2069) -> anyhow::Result<()> {
2070 cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
2071 .await
2072}
2073
2074async fn share_workspace(
2075 workspace: &Entity<Workspace>,
2076 cx: &mut VisualTestContext,
2077) -> anyhow::Result<u64> {
2078 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
2079 cx.read(ActiveCall::global)
2080 .update(cx, |call, cx| call.share_project(project, cx))
2081 .await
2082}
2083
2084#[gpui::test]
2085async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2086 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2087
2088 let (workspace, cx_a) = client_a.build_test_workspace(cx_a).await;
2089 join_channel(channel, &client_a, cx_a).await.unwrap();
2090 share_workspace(&workspace, cx_a).await.unwrap();
2091 let buffer = workspace.update(cx_a, |workspace, cx| {
2092 workspace.project().update(cx, |project, cx| {
2093 project.create_local_buffer(&sample_text(26, 5, 'a'), None, cx)
2094 })
2095 });
2096 let multibuffer = cx_a.new(|cx| {
2097 let mut mb = MultiBuffer::new(Capability::ReadWrite);
2098 mb.set_excerpts_for_path(
2099 PathKey::for_buffer(&buffer, cx),
2100 buffer.clone(),
2101 [Point::row_range(1..1), Point::row_range(5..5)],
2102 1,
2103 cx,
2104 );
2105 mb
2106 });
2107 let snapshot = buffer.update(cx_a, |buffer, _| buffer.snapshot());
2108 let editor: Entity<Editor> = cx_a.new_window_entity(|window, cx| {
2109 Editor::for_multibuffer(
2110 multibuffer.clone(),
2111 Some(workspace.read(cx).project().clone()),
2112 window,
2113 cx,
2114 )
2115 });
2116 workspace.update_in(cx_a, |workspace, window, cx| {
2117 workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
2118 });
2119 editor.update_in(cx_a, |editor, window, cx| {
2120 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2121 s.select_ranges([Point::row_range(4..4)]);
2122 })
2123 });
2124 let positions = editor.update(cx_a, |editor, _| {
2125 editor
2126 .selections
2127 .disjoint_anchor_ranges()
2128 .map(|range| range.start.text_anchor.to_point(&snapshot))
2129 .collect::<Vec<_>>()
2130 });
2131 multibuffer.update(cx_a, |multibuffer, cx| {
2132 multibuffer.set_excerpts_for_path(
2133 PathKey::for_buffer(&buffer, cx),
2134 buffer,
2135 [Point::row_range(1..5)],
2136 1,
2137 cx,
2138 );
2139 });
2140
2141 let (workspace_b, cx_b) = client_b.join_workspace(channel, cx_b).await;
2142 cx_b.run_until_parked();
2143 let editor_b = workspace_b
2144 .update(cx_b, |workspace, cx| {
2145 workspace
2146 .active_item(cx)
2147 .and_then(|item| item.downcast::<Editor>())
2148 })
2149 .unwrap();
2150
2151 let new_positions = editor_b.update(cx_b, |editor, _| {
2152 editor
2153 .selections
2154 .disjoint_anchor_ranges()
2155 .map(|range| range.start.text_anchor.to_point(&snapshot))
2156 .collect::<Vec<_>>()
2157 });
2158 assert_eq!(positions, new_positions);
2159}
2160
2161#[gpui::test]
2162async fn test_following_to_channel_notes_other_workspace(
2163 cx_a: &mut TestAppContext,
2164 cx_b: &mut TestAppContext,
2165) {
2166 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2167
2168 let mut cx_a2 = cx_a.clone();
2169 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2170 join_channel(channel, &client_a, cx_a).await.unwrap();
2171 share_workspace(&workspace_a, cx_a).await.unwrap();
2172
2173 // a opens 1.txt
2174 cx_a.simulate_keystrokes("cmd-p");
2175 cx_a.run_until_parked();
2176 cx_a.simulate_keystrokes("1 enter");
2177 cx_a.run_until_parked();
2178 workspace_a.update(cx_a, |workspace, cx| {
2179 let editor = workspace.active_item(cx).unwrap();
2180 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2181 });
2182
2183 // b joins channel and is following a
2184 join_channel(channel, &client_b, cx_b).await.unwrap();
2185 cx_b.run_until_parked();
2186 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2187 workspace_b.update(cx_b, |workspace, cx| {
2188 let editor = workspace.active_item(cx).unwrap();
2189 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2190 });
2191
2192 // a opens a second workspace and the channel notes
2193 let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2194 cx_a2.update(|window, _| window.activate_window());
2195 cx_a2
2196 .update(|window, cx| ChannelView::open(channel, None, workspace_a2, window, cx))
2197 .await
2198 .unwrap();
2199 cx_a2.run_until_parked();
2200
2201 // b should follow a to the channel notes
2202 workspace_b.update(cx_b, |workspace, cx| {
2203 let editor = workspace.active_item_as::<ChannelView>(cx).unwrap();
2204 assert_eq!(editor.read(cx).channel(cx).unwrap().id, channel);
2205 });
2206
2207 // a returns to the shared project
2208 cx_a.update(|window, _| window.activate_window());
2209 cx_a.run_until_parked();
2210
2211 workspace_a.update(cx_a, |workspace, cx| {
2212 let editor = workspace.active_item(cx).unwrap();
2213 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2214 });
2215
2216 // b should follow a back
2217 workspace_b.update(cx_b, |workspace, cx| {
2218 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2219 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2220 });
2221}
2222
2223#[gpui::test]
2224async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2225 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2226
2227 let mut cx_a2 = cx_a.clone();
2228 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2229 join_channel(channel, &client_a, cx_a).await.unwrap();
2230 share_workspace(&workspace_a, cx_a).await.unwrap();
2231
2232 // a opens 1.txt
2233 cx_a.simulate_keystrokes("cmd-p");
2234 cx_a.run_until_parked();
2235 cx_a.simulate_keystrokes("1 enter");
2236 cx_a.run_until_parked();
2237 workspace_a.update(cx_a, |workspace, cx| {
2238 let editor = workspace.active_item(cx).unwrap();
2239 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2240 });
2241
2242 // b joins channel and is following a
2243 join_channel(channel, &client_b, cx_b).await.unwrap();
2244 cx_b.run_until_parked();
2245 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2246 workspace_b.update(cx_b, |workspace, cx| {
2247 let editor = workspace.active_item(cx).unwrap();
2248 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2249 });
2250
2251 // stop following
2252 cx_b.simulate_keystrokes("down");
2253
2254 // a opens a different file while not followed
2255 cx_a.simulate_keystrokes("cmd-p");
2256 cx_a.run_until_parked();
2257 cx_a.simulate_keystrokes("2 enter");
2258
2259 workspace_b.update(cx_b, |workspace, cx| {
2260 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2261 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2262 });
2263
2264 // a opens a file in a new window
2265 let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2266 cx_a2.update(|window, _| window.activate_window());
2267 cx_a2.simulate_keystrokes("cmd-p");
2268 cx_a2.run_until_parked();
2269 cx_a2.simulate_keystrokes("3 enter");
2270 cx_a2.run_until_parked();
2271
2272 // b starts following a again
2273 cx_b.simulate_keystrokes("cmd-ctrl-alt-f");
2274 cx_a.run_until_parked();
2275
2276 // a returns to the shared project
2277 cx_a.update(|window, _| window.activate_window());
2278 cx_a.run_until_parked();
2279
2280 workspace_a.update(cx_a, |workspace, cx| {
2281 let editor = workspace.active_item(cx).unwrap();
2282 assert_eq!(editor.tab_content_text(0, cx), "2.js");
2283 });
2284
2285 // b should follow a back
2286 workspace_b.update(cx_b, |workspace, cx| {
2287 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2288 assert_eq!(editor.tab_content_text(0, cx), "2.js");
2289 });
2290}