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, 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, "1.txt"), None, true, window, cx)
90 })
91 .await
92 .unwrap()
93 .downcast::<Editor>()
94 .unwrap();
95 let editor_a2 = workspace_a
96 .update_in(cx_a, |workspace, window, cx| {
97 workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
98 })
99 .await
100 .unwrap()
101 .downcast::<Editor>()
102 .unwrap();
103
104 // Client B opens an editor.
105 let editor_b1 = workspace_b
106 .update_in(cx_b, |workspace, window, cx| {
107 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
108 })
109 .await
110 .unwrap()
111 .downcast::<Editor>()
112 .unwrap();
113
114 let peer_id_a = client_a.peer_id().unwrap();
115 let peer_id_b = client_b.peer_id().unwrap();
116 let peer_id_c = client_c.peer_id().unwrap();
117 let peer_id_d = client_d.peer_id().unwrap();
118
119 // Client A updates their selections in those editors
120 editor_a1.update_in(cx_a, |editor, window, cx| {
121 editor.handle_input("a", window, cx);
122 editor.handle_input("b", window, cx);
123 editor.handle_input("c", window, cx);
124 editor.select_left(&Default::default(), window, cx);
125 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
126 });
127 editor_a2.update_in(cx_a, |editor, window, cx| {
128 editor.handle_input("d", window, cx);
129 editor.handle_input("e", window, cx);
130 editor.select_left(&Default::default(), window, cx);
131 assert_eq!(editor.selections.ranges(cx), vec![2..1]);
132 });
133
134 // When client B starts following client A, only the active view state is replicated to client B.
135 workspace_b.update_in(cx_b, |workspace, window, cx| {
136 workspace.follow(peer_id_a, window, cx)
137 });
138
139 cx_c.executor().run_until_parked();
140 let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
141 workspace
142 .active_item(cx)
143 .unwrap()
144 .downcast::<Editor>()
145 .unwrap()
146 });
147 assert_eq!(
148 cx_b.read(|cx| editor_b2.project_path(cx)),
149 Some((worktree_id, "2.txt").into())
150 );
151 assert_eq!(
152 editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
153 vec![2..1]
154 );
155 assert_eq!(
156 editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
157 vec![3..3]
158 );
159
160 executor.run_until_parked();
161 let active_call_c = cx_c.read(ActiveCall::global);
162 let project_c = client_c.join_remote_project(project_id, cx_c).await;
163 let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c);
164 active_call_c
165 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
166 .await
167 .unwrap();
168 drop(project_c);
169
170 // Client C also follows client A.
171 workspace_c.update_in(cx_c, |workspace, window, cx| {
172 workspace.follow(peer_id_a, window, cx)
173 });
174
175 cx_d.executor().run_until_parked();
176 let active_call_d = cx_d.read(ActiveCall::global);
177 let project_d = client_d.join_remote_project(project_id, cx_d).await;
178 let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d);
179 active_call_d
180 .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
181 .await
182 .unwrap();
183 drop(project_d);
184
185 // All clients see that clients B and C are following client A.
186 cx_c.executor().run_until_parked();
187 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
188 assert_eq!(
189 followers_by_leader(project_id, cx),
190 &[(peer_id_a, vec![peer_id_b, peer_id_c])],
191 "followers seen by {name}"
192 );
193 }
194
195 // Client C unfollows client A.
196 workspace_c.update_in(cx_c, |workspace, window, cx| {
197 workspace.unfollow(peer_id_a, window, cx).unwrap();
198 });
199
200 // All clients see that clients B is following client A.
201 cx_c.executor().run_until_parked();
202 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
203 assert_eq!(
204 followers_by_leader(project_id, cx),
205 &[(peer_id_a, vec![peer_id_b])],
206 "followers seen by {name}"
207 );
208 }
209
210 // Client C re-follows client A.
211 workspace_c.update_in(cx_c, |workspace, window, cx| {
212 workspace.follow(peer_id_a, window, cx)
213 });
214
215 // All clients see that clients B and C are following client A.
216 cx_c.executor().run_until_parked();
217 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
218 assert_eq!(
219 followers_by_leader(project_id, cx),
220 &[(peer_id_a, vec![peer_id_b, peer_id_c])],
221 "followers seen by {name}"
222 );
223 }
224
225 // Client D follows client B, then switches to following client C.
226 workspace_d.update_in(cx_d, |workspace, window, cx| {
227 workspace.follow(peer_id_b, window, cx)
228 });
229 cx_a.executor().run_until_parked();
230 workspace_d.update_in(cx_d, |workspace, window, cx| {
231 workspace.follow(peer_id_c, window, cx)
232 });
233
234 // All clients see that D is following C
235 cx_a.executor().run_until_parked();
236 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
237 assert_eq!(
238 followers_by_leader(project_id, cx),
239 &[
240 (peer_id_a, vec![peer_id_b, peer_id_c]),
241 (peer_id_c, vec![peer_id_d])
242 ],
243 "followers seen by {name}"
244 );
245 }
246
247 // Client C closes the project.
248 let weak_workspace_c = workspace_c.downgrade();
249 workspace_c.update_in(cx_c, |workspace, window, cx| {
250 workspace.close_window(&Default::default(), window, cx);
251 });
252 executor.run_until_parked();
253 // are you sure you want to leave the call?
254 cx_c.simulate_prompt_answer("Close window and hang up");
255 cx_c.cx.update(|_| {
256 drop(workspace_c);
257 });
258 executor.run_until_parked();
259 cx_c.cx.update(|_| {});
260
261 weak_workspace_c.assert_released();
262
263 // Clients A and B see that client B is following A, and client C is not present in the followers.
264 executor.run_until_parked();
265 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("D", &cx_d)] {
266 assert_eq!(
267 followers_by_leader(project_id, cx),
268 &[(peer_id_a, vec![peer_id_b]),],
269 "followers seen by {name}"
270 );
271 }
272
273 // When client A activates a different editor, client B does so as well.
274 workspace_a.update_in(cx_a, |workspace, window, cx| {
275 workspace.activate_item(&editor_a1, true, true, window, cx)
276 });
277 executor.run_until_parked();
278 workspace_b.update(cx_b, |workspace, cx| {
279 assert_eq!(
280 workspace.active_item(cx).unwrap().item_id(),
281 editor_b1.item_id()
282 );
283 });
284
285 // When client A opens a multibuffer, client B does so as well.
286 let multibuffer_a = cx_a.new(|cx| {
287 let buffer_a1 = project_a.update(cx, |project, cx| {
288 project
289 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
290 .unwrap()
291 });
292 let buffer_a2 = project_a.update(cx, |project, cx| {
293 project
294 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
295 .unwrap()
296 });
297 let mut result = MultiBuffer::new(Capability::ReadWrite);
298 result.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, "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, "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| {
636 item.project_path(cx)
637 .unwrap()
638 .path
639 .to_str()
640 .unwrap()
641 .to_owned()
642 })
643 .collect::<Vec<_>>()
644 })
645 };
646
647 //Verify that the tabs opened in the order we expect
648 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
649
650 //Follow client B as client A
651 workspace_a.update_in(cx_a, |workspace, window, cx| {
652 workspace.follow(client_b_id, window, cx)
653 });
654 executor.run_until_parked();
655
656 //Open just 2 on client B
657 workspace_b
658 .update_in(cx_b, |workspace, window, cx| {
659 workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
660 })
661 .await
662 .unwrap();
663 executor.run_until_parked();
664
665 // Verify that newly opened followed file is at the end
666 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
667
668 //Open just 1 on client B
669 workspace_b
670 .update_in(cx_b, |workspace, window, cx| {
671 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
672 })
673 .await
674 .unwrap();
675 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
676 executor.run_until_parked();
677
678 // Verify that following into 1 did not reorder
679 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
680}
681
682#[gpui::test(iterations = 10)]
683async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
684 let executor = cx_a.executor();
685 let mut server = TestServer::start(executor.clone()).await;
686 let client_a = server.create_client(cx_a, "user_a").await;
687 let client_b = server.create_client(cx_b, "user_b").await;
688 server
689 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
690 .await;
691 let active_call_a = cx_a.read(ActiveCall::global);
692 let active_call_b = cx_b.read(ActiveCall::global);
693
694 cx_a.update(editor::init);
695 cx_b.update(editor::init);
696
697 // Client A shares a project.
698 client_a
699 .fs()
700 .insert_tree(
701 path!("/a"),
702 json!({
703 "1.txt": "one",
704 "2.txt": "two",
705 "3.txt": "three",
706 "4.txt": "four",
707 }),
708 )
709 .await;
710 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
711 active_call_a
712 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
713 .await
714 .unwrap();
715 let project_id = active_call_a
716 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
717 .await
718 .unwrap();
719
720 // Client B joins the project.
721 let project_b = client_b.join_remote_project(project_id, cx_b).await;
722 active_call_b
723 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
724 .await
725 .unwrap();
726
727 // Client A opens a file.
728 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
729 workspace_a
730 .update_in(cx_a, |workspace, window, cx| {
731 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
732 })
733 .await
734 .unwrap()
735 .downcast::<Editor>()
736 .unwrap();
737
738 // Client B opens a different file.
739 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
740 workspace_b
741 .update_in(cx_b, |workspace, window, cx| {
742 workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
743 })
744 .await
745 .unwrap()
746 .downcast::<Editor>()
747 .unwrap();
748
749 // Clients A and B follow each other in split panes
750 workspace_a.update_in(cx_a, |workspace, window, cx| {
751 workspace.split_and_clone(
752 workspace.active_pane().clone(),
753 SplitDirection::Right,
754 window,
755 cx,
756 );
757 });
758 workspace_a.update_in(cx_a, |workspace, window, cx| {
759 workspace.follow(client_b.peer_id().unwrap(), window, cx)
760 });
761 executor.run_until_parked();
762 workspace_b.update_in(cx_b, |workspace, window, cx| {
763 workspace.split_and_clone(
764 workspace.active_pane().clone(),
765 SplitDirection::Right,
766 window,
767 cx,
768 );
769 });
770 workspace_b.update_in(cx_b, |workspace, window, cx| {
771 workspace.follow(client_a.peer_id().unwrap(), window, cx)
772 });
773 executor.run_until_parked();
774
775 // Clients A and B return focus to the original files they had open
776 workspace_a.update_in(cx_a, |workspace, window, cx| {
777 workspace.activate_next_pane(window, cx)
778 });
779 workspace_b.update_in(cx_b, |workspace, window, cx| {
780 workspace.activate_next_pane(window, cx)
781 });
782 executor.run_until_parked();
783
784 // Both clients see the other client's focused file in their right pane.
785 assert_eq!(
786 pane_summaries(&workspace_a, cx_a),
787 &[
788 PaneSummary {
789 active: true,
790 leader: None,
791 items: vec![(true, "1.txt".into())]
792 },
793 PaneSummary {
794 active: false,
795 leader: client_b.peer_id(),
796 items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
797 },
798 ]
799 );
800 assert_eq!(
801 pane_summaries(&workspace_b, cx_b),
802 &[
803 PaneSummary {
804 active: true,
805 leader: None,
806 items: vec![(true, "2.txt".into())]
807 },
808 PaneSummary {
809 active: false,
810 leader: client_a.peer_id(),
811 items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
812 },
813 ]
814 );
815
816 // Clients A and B each open a new file.
817 workspace_a
818 .update_in(cx_a, |workspace, window, cx| {
819 workspace.open_path((worktree_id, "3.txt"), None, true, window, cx)
820 })
821 .await
822 .unwrap();
823
824 workspace_b
825 .update_in(cx_b, |workspace, window, cx| {
826 workspace.open_path((worktree_id, "4.txt"), None, true, window, cx)
827 })
828 .await
829 .unwrap();
830 executor.run_until_parked();
831
832 // Both client's see the other client open the new file, but keep their
833 // focus on their own active pane.
834 assert_eq!(
835 pane_summaries(&workspace_a, cx_a),
836 &[
837 PaneSummary {
838 active: true,
839 leader: None,
840 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
841 },
842 PaneSummary {
843 active: false,
844 leader: client_b.peer_id(),
845 items: vec![
846 (false, "1.txt".into()),
847 (false, "2.txt".into()),
848 (true, "4.txt".into())
849 ]
850 },
851 ]
852 );
853 assert_eq!(
854 pane_summaries(&workspace_b, cx_b),
855 &[
856 PaneSummary {
857 active: true,
858 leader: None,
859 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
860 },
861 PaneSummary {
862 active: false,
863 leader: client_a.peer_id(),
864 items: vec![
865 (false, "2.txt".into()),
866 (false, "1.txt".into()),
867 (true, "3.txt".into())
868 ]
869 },
870 ]
871 );
872
873 // Client A focuses their right pane, in which they're following client B.
874 workspace_a.update_in(cx_a, |workspace, window, cx| {
875 workspace.activate_next_pane(window, cx)
876 });
877 executor.run_until_parked();
878
879 // Client B sees that client A is now looking at the same file as them.
880 assert_eq!(
881 pane_summaries(&workspace_a, cx_a),
882 &[
883 PaneSummary {
884 active: false,
885 leader: None,
886 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
887 },
888 PaneSummary {
889 active: true,
890 leader: client_b.peer_id(),
891 items: vec![
892 (false, "1.txt".into()),
893 (false, "2.txt".into()),
894 (true, "4.txt".into())
895 ]
896 },
897 ]
898 );
899 assert_eq!(
900 pane_summaries(&workspace_b, cx_b),
901 &[
902 PaneSummary {
903 active: true,
904 leader: None,
905 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
906 },
907 PaneSummary {
908 active: false,
909 leader: client_a.peer_id(),
910 items: vec![
911 (false, "2.txt".into()),
912 (false, "1.txt".into()),
913 (false, "3.txt".into()),
914 (true, "4.txt".into())
915 ]
916 },
917 ]
918 );
919
920 // Client B focuses their right pane, in which they're following client A,
921 // who is following them.
922 workspace_b.update_in(cx_b, |workspace, window, cx| {
923 workspace.activate_next_pane(window, cx)
924 });
925 executor.run_until_parked();
926
927 // Client A sees that client B is now looking at the same file as them.
928 assert_eq!(
929 pane_summaries(&workspace_b, cx_b),
930 &[
931 PaneSummary {
932 active: false,
933 leader: None,
934 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
935 },
936 PaneSummary {
937 active: true,
938 leader: client_a.peer_id(),
939 items: vec![
940 (false, "2.txt".into()),
941 (false, "1.txt".into()),
942 (false, "3.txt".into()),
943 (true, "4.txt".into())
944 ]
945 },
946 ]
947 );
948 assert_eq!(
949 pane_summaries(&workspace_a, cx_a),
950 &[
951 PaneSummary {
952 active: false,
953 leader: None,
954 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
955 },
956 PaneSummary {
957 active: true,
958 leader: client_b.peer_id(),
959 items: vec![
960 (false, "1.txt".into()),
961 (false, "2.txt".into()),
962 (true, "4.txt".into())
963 ]
964 },
965 ]
966 );
967
968 // Client B focuses a file that they previously followed A to, breaking
969 // the follow.
970 workspace_b.update_in(cx_b, |workspace, window, cx| {
971 workspace.active_pane().update(cx, |pane, cx| {
972 pane.activate_previous_item(&Default::default(), window, cx);
973 });
974 });
975 executor.run_until_parked();
976
977 // Both clients see that client B is looking at that previous file.
978 assert_eq!(
979 pane_summaries(&workspace_b, cx_b),
980 &[
981 PaneSummary {
982 active: false,
983 leader: None,
984 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
985 },
986 PaneSummary {
987 active: true,
988 leader: None,
989 items: vec![
990 (false, "2.txt".into()),
991 (false, "1.txt".into()),
992 (true, "3.txt".into()),
993 (false, "4.txt".into())
994 ]
995 },
996 ]
997 );
998 assert_eq!(
999 pane_summaries(&workspace_a, cx_a),
1000 &[
1001 PaneSummary {
1002 active: false,
1003 leader: None,
1004 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1005 },
1006 PaneSummary {
1007 active: true,
1008 leader: client_b.peer_id(),
1009 items: vec![
1010 (false, "1.txt".into()),
1011 (false, "2.txt".into()),
1012 (false, "4.txt".into()),
1013 (true, "3.txt".into()),
1014 ]
1015 },
1016 ]
1017 );
1018
1019 // Client B closes tabs, some of which were originally opened by client A,
1020 // and some of which were originally opened by client B.
1021 workspace_b.update_in(cx_b, |workspace, window, cx| {
1022 workspace.active_pane().update(cx, |pane, cx| {
1023 pane.close_other_items(&Default::default(), None, window, cx)
1024 .detach();
1025 });
1026 });
1027
1028 executor.run_until_parked();
1029
1030 // Both clients see that Client B is looking at the previous tab.
1031 assert_eq!(
1032 pane_summaries(&workspace_b, cx_b),
1033 &[
1034 PaneSummary {
1035 active: false,
1036 leader: None,
1037 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1038 },
1039 PaneSummary {
1040 active: true,
1041 leader: None,
1042 items: vec![(true, "3.txt".into()),]
1043 },
1044 ]
1045 );
1046 assert_eq!(
1047 pane_summaries(&workspace_a, cx_a),
1048 &[
1049 PaneSummary {
1050 active: false,
1051 leader: None,
1052 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1053 },
1054 PaneSummary {
1055 active: true,
1056 leader: client_b.peer_id(),
1057 items: vec![
1058 (false, "1.txt".into()),
1059 (false, "2.txt".into()),
1060 (false, "4.txt".into()),
1061 (true, "3.txt".into()),
1062 ]
1063 },
1064 ]
1065 );
1066
1067 // Client B follows client A again.
1068 workspace_b.update_in(cx_b, |workspace, window, cx| {
1069 workspace.follow(client_a.peer_id().unwrap(), window, cx)
1070 });
1071 executor.run_until_parked();
1072 // Client A cycles through some tabs.
1073 workspace_a.update_in(cx_a, |workspace, window, cx| {
1074 workspace.active_pane().update(cx, |pane, cx| {
1075 pane.activate_previous_item(&Default::default(), window, cx);
1076 });
1077 });
1078 executor.run_until_parked();
1079
1080 // Client B follows client A into those tabs.
1081 assert_eq!(
1082 pane_summaries(&workspace_a, cx_a),
1083 &[
1084 PaneSummary {
1085 active: false,
1086 leader: None,
1087 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1088 },
1089 PaneSummary {
1090 active: true,
1091 leader: None,
1092 items: vec![
1093 (false, "1.txt".into()),
1094 (false, "2.txt".into()),
1095 (true, "4.txt".into()),
1096 (false, "3.txt".into()),
1097 ]
1098 },
1099 ]
1100 );
1101 assert_eq!(
1102 pane_summaries(&workspace_b, cx_b),
1103 &[
1104 PaneSummary {
1105 active: false,
1106 leader: None,
1107 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1108 },
1109 PaneSummary {
1110 active: true,
1111 leader: client_a.peer_id(),
1112 items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
1113 },
1114 ]
1115 );
1116
1117 workspace_a.update_in(cx_a, |workspace, window, cx| {
1118 workspace.active_pane().update(cx, |pane, cx| {
1119 pane.activate_previous_item(&Default::default(), window, cx);
1120 });
1121 });
1122 executor.run_until_parked();
1123
1124 assert_eq!(
1125 pane_summaries(&workspace_a, cx_a),
1126 &[
1127 PaneSummary {
1128 active: false,
1129 leader: None,
1130 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1131 },
1132 PaneSummary {
1133 active: true,
1134 leader: None,
1135 items: vec![
1136 (false, "1.txt".into()),
1137 (true, "2.txt".into()),
1138 (false, "4.txt".into()),
1139 (false, "3.txt".into()),
1140 ]
1141 },
1142 ]
1143 );
1144 assert_eq!(
1145 pane_summaries(&workspace_b, cx_b),
1146 &[
1147 PaneSummary {
1148 active: false,
1149 leader: None,
1150 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1151 },
1152 PaneSummary {
1153 active: true,
1154 leader: client_a.peer_id(),
1155 items: vec![
1156 (false, "3.txt".into()),
1157 (false, "4.txt".into()),
1158 (true, "2.txt".into())
1159 ]
1160 },
1161 ]
1162 );
1163
1164 workspace_a.update_in(cx_a, |workspace, window, cx| {
1165 workspace.active_pane().update(cx, |pane, cx| {
1166 pane.activate_previous_item(&Default::default(), window, cx);
1167 });
1168 });
1169 executor.run_until_parked();
1170
1171 assert_eq!(
1172 pane_summaries(&workspace_a, cx_a),
1173 &[
1174 PaneSummary {
1175 active: false,
1176 leader: None,
1177 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1178 },
1179 PaneSummary {
1180 active: true,
1181 leader: None,
1182 items: vec![
1183 (true, "1.txt".into()),
1184 (false, "2.txt".into()),
1185 (false, "4.txt".into()),
1186 (false, "3.txt".into()),
1187 ]
1188 },
1189 ]
1190 );
1191 assert_eq!(
1192 pane_summaries(&workspace_b, cx_b),
1193 &[
1194 PaneSummary {
1195 active: false,
1196 leader: None,
1197 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1198 },
1199 PaneSummary {
1200 active: true,
1201 leader: client_a.peer_id(),
1202 items: vec![
1203 (false, "3.txt".into()),
1204 (false, "4.txt".into()),
1205 (false, "2.txt".into()),
1206 (true, "1.txt".into()),
1207 ]
1208 },
1209 ]
1210 );
1211}
1212
1213#[gpui::test(iterations = 10)]
1214async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1215 // 2 clients connect to a server.
1216 let executor = cx_a.executor();
1217 let mut server = TestServer::start(executor.clone()).await;
1218 let client_a = server.create_client(cx_a, "user_a").await;
1219 let client_b = server.create_client(cx_b, "user_b").await;
1220 server
1221 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1222 .await;
1223 let active_call_a = cx_a.read(ActiveCall::global);
1224 let active_call_b = cx_b.read(ActiveCall::global);
1225
1226 cx_a.update(editor::init);
1227 cx_b.update(editor::init);
1228
1229 // Client A shares a project.
1230 client_a
1231 .fs()
1232 .insert_tree(
1233 path!("/a"),
1234 json!({
1235 "1.txt": "one",
1236 "2.txt": "two",
1237 "3.txt": "three",
1238 }),
1239 )
1240 .await;
1241 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1242 active_call_a
1243 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1244 .await
1245 .unwrap();
1246
1247 let project_id = active_call_a
1248 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1249 .await
1250 .unwrap();
1251 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1252 active_call_b
1253 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1254 .await
1255 .unwrap();
1256
1257 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1258 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1259
1260 let _editor_a1 = workspace_a
1261 .update_in(cx_a, |workspace, window, cx| {
1262 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
1263 })
1264 .await
1265 .unwrap()
1266 .downcast::<Editor>()
1267 .unwrap();
1268
1269 // Client B starts following client A.
1270 let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
1271 let leader_id = project_b.update(cx_b, |project, _| {
1272 project.collaborators().values().next().unwrap().peer_id
1273 });
1274 workspace_b.update_in(cx_b, |workspace, window, cx| {
1275 workspace.follow(leader_id, window, cx)
1276 });
1277 executor.run_until_parked();
1278 assert_eq!(
1279 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1280 Some(leader_id.into())
1281 );
1282 let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
1283 workspace
1284 .active_item(cx)
1285 .unwrap()
1286 .downcast::<Editor>()
1287 .unwrap()
1288 });
1289
1290 // When client B moves, it automatically stops following client A.
1291 editor_b2.update_in(cx_b, |editor, window, cx| {
1292 editor.move_right(&editor::actions::MoveRight, window, cx)
1293 });
1294 assert_eq!(
1295 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1296 None
1297 );
1298
1299 workspace_b.update_in(cx_b, |workspace, window, cx| {
1300 workspace.follow(leader_id, window, cx)
1301 });
1302 executor.run_until_parked();
1303 assert_eq!(
1304 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1305 Some(leader_id.into())
1306 );
1307
1308 // When client B edits, it automatically stops following client A.
1309 editor_b2.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
1310 assert_eq!(
1311 workspace_b.update_in(cx_b, |workspace, _, _| workspace.leader_for_pane(&pane_b)),
1312 None
1313 );
1314
1315 workspace_b.update_in(cx_b, |workspace, window, cx| {
1316 workspace.follow(leader_id, window, cx)
1317 });
1318 executor.run_until_parked();
1319 assert_eq!(
1320 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1321 Some(leader_id.into())
1322 );
1323
1324 // When client B scrolls, it automatically stops following client A.
1325 editor_b2.update_in(cx_b, |editor, window, cx| {
1326 editor.set_scroll_position(point(0., 3.), window, cx)
1327 });
1328 assert_eq!(
1329 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1330 None
1331 );
1332
1333 workspace_b.update_in(cx_b, |workspace, window, cx| {
1334 workspace.follow(leader_id, window, cx)
1335 });
1336 executor.run_until_parked();
1337 assert_eq!(
1338 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1339 Some(leader_id.into())
1340 );
1341
1342 // When client B activates a different pane, it continues following client A in the original pane.
1343 workspace_b.update_in(cx_b, |workspace, window, cx| {
1344 workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
1345 });
1346 assert_eq!(
1347 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1348 Some(leader_id.into())
1349 );
1350
1351 workspace_b.update_in(cx_b, |workspace, window, cx| {
1352 workspace.activate_next_pane(window, cx)
1353 });
1354 assert_eq!(
1355 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1356 Some(leader_id.into())
1357 );
1358
1359 // When client B activates a different item in the original pane, it automatically stops following client A.
1360 workspace_b
1361 .update_in(cx_b, |workspace, window, cx| {
1362 workspace.open_path((worktree_id, "2.txt"), None, true, window, cx)
1363 })
1364 .await
1365 .unwrap();
1366 assert_eq!(
1367 workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1368 None
1369 );
1370}
1371
1372#[gpui::test(iterations = 10)]
1373async fn test_peers_simultaneously_following_each_other(
1374 cx_a: &mut TestAppContext,
1375 cx_b: &mut TestAppContext,
1376) {
1377 let executor = cx_a.executor();
1378 let mut server = TestServer::start(executor.clone()).await;
1379 let client_a = server.create_client(cx_a, "user_a").await;
1380 let client_b = server.create_client(cx_b, "user_b").await;
1381 server
1382 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1383 .await;
1384 let active_call_a = cx_a.read(ActiveCall::global);
1385
1386 cx_a.update(editor::init);
1387 cx_b.update(editor::init);
1388
1389 client_a.fs().insert_tree("/a", json!({})).await;
1390 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1391 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1392 let project_id = active_call_a
1393 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1394 .await
1395 .unwrap();
1396
1397 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1398 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1399
1400 executor.run_until_parked();
1401 let client_a_id = project_b.update(cx_b, |project, _| {
1402 project.collaborators().values().next().unwrap().peer_id
1403 });
1404 let client_b_id = project_a.update(cx_a, |project, _| {
1405 project.collaborators().values().next().unwrap().peer_id
1406 });
1407
1408 workspace_a.update_in(cx_a, |workspace, window, cx| {
1409 workspace.follow(client_b_id, window, cx)
1410 });
1411 workspace_b.update_in(cx_b, |workspace, window, cx| {
1412 workspace.follow(client_a_id, window, cx)
1413 });
1414 executor.run_until_parked();
1415
1416 workspace_a.update(cx_a, |workspace, _| {
1417 assert_eq!(
1418 workspace.leader_for_pane(workspace.active_pane()),
1419 Some(client_b_id.into())
1420 );
1421 });
1422 workspace_b.update(cx_b, |workspace, _| {
1423 assert_eq!(
1424 workspace.leader_for_pane(workspace.active_pane()),
1425 Some(client_a_id.into())
1426 );
1427 });
1428}
1429
1430#[gpui::test(iterations = 10)]
1431async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1432 // a and b join a channel/call
1433 // a shares project 1
1434 // b shares project 2
1435 //
1436 // b follows a: causes project 2 to be joined, and b to follow a.
1437 // b opens a different file in project 2, a follows b
1438 // b opens a different file in project 1, a cannot follow b
1439 // b shares the project, a joins the project and follows b
1440 let executor = cx_a.executor();
1441 let mut server = TestServer::start(executor.clone()).await;
1442 let client_a = server.create_client(cx_a, "user_a").await;
1443 let client_b = server.create_client(cx_b, "user_b").await;
1444
1445 client_a
1446 .fs()
1447 .insert_tree(
1448 path!("/a"),
1449 json!({
1450 "w.rs": "",
1451 "x.rs": "",
1452 }),
1453 )
1454 .await;
1455
1456 client_b
1457 .fs()
1458 .insert_tree(
1459 path!("/b"),
1460 json!({
1461 "y.rs": "",
1462 "z.rs": "",
1463 }),
1464 )
1465 .await;
1466
1467 server
1468 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1469 .await;
1470 let active_call_a = cx_a.read(ActiveCall::global);
1471 let active_call_b = cx_b.read(ActiveCall::global);
1472
1473 let (project_a, worktree_id_a) = client_a.build_local_project(path!("/a"), cx_a).await;
1474 let (project_b, worktree_id_b) = client_b.build_local_project(path!("/b"), cx_b).await;
1475
1476 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1477 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1478
1479 active_call_a
1480 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1481 .await
1482 .unwrap();
1483
1484 active_call_a
1485 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1486 .await
1487 .unwrap();
1488 active_call_b
1489 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1490 .await
1491 .unwrap();
1492
1493 workspace_a
1494 .update_in(cx_a, |workspace, window, cx| {
1495 workspace.open_path((worktree_id_a, "w.rs"), None, true, window, cx)
1496 })
1497 .await
1498 .unwrap();
1499
1500 executor.run_until_parked();
1501 assert_eq!(visible_push_notifications(cx_b).len(), 1);
1502
1503 workspace_b.update_in(cx_b, |workspace, window, cx| {
1504 workspace.follow(client_a.peer_id().unwrap(), window, cx)
1505 });
1506
1507 executor.run_until_parked();
1508 let window_b_project_a = *cx_b
1509 .windows()
1510 .iter()
1511 .max_by_key(|window| window.window_id())
1512 .unwrap();
1513
1514 let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b);
1515
1516 let workspace_b_project_a = window_b_project_a
1517 .downcast::<Workspace>()
1518 .unwrap()
1519 .root(cx_b)
1520 .unwrap();
1521
1522 // assert that b is following a in project a in w.rs
1523 workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1524 assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
1525 assert_eq!(
1526 client_a.peer_id().map(Into::into),
1527 workspace.leader_for_pane(workspace.active_pane())
1528 );
1529 let item = workspace.active_item(cx).unwrap();
1530 assert_eq!(item.tab_content_text(0, cx), SharedString::from("w.rs"));
1531 });
1532
1533 // TODO: in app code, this would be done by the collab_ui.
1534 active_call_b
1535 .update(&mut cx_b2, |call, cx| {
1536 let project = workspace_b_project_a.read(cx).project().clone();
1537 call.set_location(Some(&project), cx)
1538 })
1539 .await
1540 .unwrap();
1541
1542 // assert that there are no share notifications open
1543 assert_eq!(visible_push_notifications(cx_b).len(), 0);
1544
1545 // b moves to x.rs in a's project, and a follows
1546 workspace_b_project_a
1547 .update_in(&mut cx_b2, |workspace, window, cx| {
1548 workspace.open_path((worktree_id_a, "x.rs"), None, true, window, cx)
1549 })
1550 .await
1551 .unwrap();
1552
1553 executor.run_until_parked();
1554 workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
1555 let item = workspace.active_item(cx).unwrap();
1556 assert_eq!(item.tab_content_text(0, cx), SharedString::from("x.rs"));
1557 });
1558
1559 workspace_a.update_in(cx_a, |workspace, window, cx| {
1560 workspace.follow(client_b.peer_id().unwrap(), window, cx)
1561 });
1562
1563 executor.run_until_parked();
1564 workspace_a.update(cx_a, |workspace, cx| {
1565 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1566 assert_eq!(
1567 client_b.peer_id().map(Into::into),
1568 workspace.leader_for_pane(workspace.active_pane())
1569 );
1570 let item = workspace.active_pane().read(cx).active_item().unwrap();
1571 assert_eq!(item.tab_content_text(0, cx), "x.rs");
1572 });
1573
1574 // b moves to y.rs in b's project, a is still following but can't yet see
1575 workspace_b
1576 .update_in(cx_b, |workspace, window, cx| {
1577 workspace.open_path((worktree_id_b, "y.rs"), None, true, window, cx)
1578 })
1579 .await
1580 .unwrap();
1581
1582 // TODO: in app code, this would be done by the collab_ui.
1583 active_call_b
1584 .update(cx_b, |call, cx| {
1585 let project = workspace_b.read(cx).project().clone();
1586 call.set_location(Some(&project), cx)
1587 })
1588 .await
1589 .unwrap();
1590
1591 let project_b_id = active_call_b
1592 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1593 .await
1594 .unwrap();
1595
1596 executor.run_until_parked();
1597 assert_eq!(visible_push_notifications(cx_a).len(), 1);
1598 cx_a.update(|_, cx| {
1599 workspace::join_in_room_project(
1600 project_b_id,
1601 client_b.user_id().unwrap(),
1602 client_a.app_state.clone(),
1603 cx,
1604 )
1605 })
1606 .await
1607 .unwrap();
1608
1609 executor.run_until_parked();
1610
1611 assert_eq!(visible_push_notifications(cx_a).len(), 0);
1612 let window_a_project_b = *cx_a
1613 .windows()
1614 .iter()
1615 .max_by_key(|window| window.window_id())
1616 .unwrap();
1617 let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a);
1618 let workspace_a_project_b = window_a_project_b
1619 .downcast::<Workspace>()
1620 .unwrap()
1621 .root(cx_a)
1622 .unwrap();
1623
1624 executor.run_until_parked();
1625
1626 workspace_a_project_b.update(cx_a2, |workspace, cx| {
1627 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1628 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1629 assert_eq!(
1630 client_b.peer_id().map(Into::into),
1631 workspace.leader_for_pane(workspace.active_pane())
1632 );
1633 let item = workspace.active_item(cx).unwrap();
1634 assert_eq!(item.tab_content_text(0, cx), SharedString::from("y.rs"));
1635 });
1636}
1637
1638#[gpui::test]
1639async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1640 let (_server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
1641
1642 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
1643 client_a
1644 .host_workspace(&workspace_a, channel_id, cx_a)
1645 .await;
1646 let (workspace_b, cx_b) = client_b.join_workspace(channel_id, cx_b).await;
1647
1648 cx_a.simulate_keystrokes("cmd-p");
1649 cx_a.run_until_parked();
1650 cx_a.simulate_keystrokes("2 enter");
1651
1652 let editor_a = workspace_a.update(cx_a, |workspace, cx| {
1653 workspace.active_item_as::<Editor>(cx).unwrap()
1654 });
1655 let editor_b = workspace_b.update(cx_b, |workspace, cx| {
1656 workspace.active_item_as::<Editor>(cx).unwrap()
1657 });
1658
1659 // b should follow a to position 1
1660 editor_a.update_in(cx_a, |editor, window, cx| {
1661 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1662 s.select_ranges([1..1])
1663 })
1664 });
1665 cx_a.executor()
1666 .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1667 cx_a.run_until_parked();
1668 editor_b.update(cx_b, |editor, cx| {
1669 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1670 });
1671
1672 // a unshares the project
1673 cx_a.update(|_, cx| {
1674 let project = workspace_a.read(cx).project().clone();
1675 ActiveCall::global(cx).update(cx, |call, cx| {
1676 call.unshare_project(project, cx).unwrap();
1677 })
1678 });
1679 cx_a.run_until_parked();
1680
1681 // b should not follow a to position 2
1682 editor_a.update_in(cx_a, |editor, window, cx| {
1683 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1684 s.select_ranges([2..2])
1685 })
1686 });
1687 cx_a.executor()
1688 .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1689 cx_a.run_until_parked();
1690 editor_b.update(cx_b, |editor, cx| {
1691 assert_eq!(editor.selections.ranges(cx), vec![1..1])
1692 });
1693 cx_b.update(|_, cx| {
1694 let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
1695 let participant = room.remote_participants().get(&client_a.id()).unwrap();
1696 assert_eq!(participant.location, ParticipantLocation::UnsharedProject)
1697 })
1698}
1699
1700#[gpui::test]
1701async fn test_following_into_excluded_file(
1702 mut cx_a: &mut TestAppContext,
1703 mut cx_b: &mut TestAppContext,
1704) {
1705 let executor = cx_a.executor();
1706 let mut server = TestServer::start(executor.clone()).await;
1707 let client_a = server.create_client(cx_a, "user_a").await;
1708 let client_b = server.create_client(cx_b, "user_b").await;
1709 for cx in [&mut cx_a, &mut cx_b] {
1710 cx.update(|cx| {
1711 cx.update_global::<SettingsStore, _>(|store, cx| {
1712 store.update_user_settings(cx, |settings| {
1713 settings.project.worktree.file_scan_exclusions =
1714 Some(vec!["**/.git".to_string()]);
1715 });
1716 });
1717 });
1718 }
1719 server
1720 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1721 .await;
1722 let active_call_a = cx_a.read(ActiveCall::global);
1723 let active_call_b = cx_b.read(ActiveCall::global);
1724 let peer_id_a = client_a.peer_id().unwrap();
1725
1726 client_a
1727 .fs()
1728 .insert_tree(
1729 path!("/a"),
1730 json!({
1731 ".git": {
1732 "COMMIT_EDITMSG": "write your commit message here",
1733 },
1734 "1.txt": "one\none\none",
1735 "2.txt": "two\ntwo\ntwo",
1736 "3.txt": "three\nthree\nthree",
1737 }),
1738 )
1739 .await;
1740 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1741 active_call_a
1742 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1743 .await
1744 .unwrap();
1745
1746 let project_id = active_call_a
1747 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1748 .await
1749 .unwrap();
1750 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1751 active_call_b
1752 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1753 .await
1754 .unwrap();
1755
1756 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1757 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1758
1759 // Client A opens editors for a regular file and an excluded file.
1760 let editor_for_regular = workspace_a
1761 .update_in(cx_a, |workspace, window, cx| {
1762 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
1763 })
1764 .await
1765 .unwrap()
1766 .downcast::<Editor>()
1767 .unwrap();
1768 let editor_for_excluded_a = workspace_a
1769 .update_in(cx_a, |workspace, window, cx| {
1770 workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, window, cx)
1771 })
1772 .await
1773 .unwrap()
1774 .downcast::<Editor>()
1775 .unwrap();
1776
1777 // Client A updates their selections in those editors
1778 editor_for_regular.update_in(cx_a, |editor, window, cx| {
1779 editor.handle_input("a", window, cx);
1780 editor.handle_input("b", window, cx);
1781 editor.handle_input("c", window, cx);
1782 editor.select_left(&Default::default(), window, cx);
1783 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1784 });
1785 editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1786 editor.select_all(&Default::default(), window, cx);
1787 editor.handle_input("new commit message", window, cx);
1788 editor.select_left(&Default::default(), window, cx);
1789 assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1790 });
1791
1792 // When client B starts following client A, currently visible file is replicated
1793 workspace_b.update_in(cx_b, |workspace, window, cx| {
1794 workspace.follow(peer_id_a, window, cx)
1795 });
1796 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1797 executor.run_until_parked();
1798
1799 let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
1800 workspace
1801 .active_item(cx)
1802 .unwrap()
1803 .downcast::<Editor>()
1804 .unwrap()
1805 });
1806 assert_eq!(
1807 cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1808 Some((worktree_id, ".git/COMMIT_EDITMSG").into())
1809 );
1810 assert_eq!(
1811 editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
1812 vec![18..17]
1813 );
1814
1815 editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
1816 editor.select_right(&Default::default(), window, cx);
1817 });
1818 executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
1819 executor.run_until_parked();
1820
1821 // Changes from B to the excluded file are replicated in A's editor
1822 editor_for_excluded_b.update_in(cx_b, |editor, window, cx| {
1823 editor.handle_input("\nCo-Authored-By: B <b@b.b>", window, cx);
1824 });
1825 executor.run_until_parked();
1826 editor_for_excluded_a.update(cx_a, |editor, cx| {
1827 assert_eq!(
1828 editor.text(cx),
1829 "new commit message\nCo-Authored-By: B <b@b.b>"
1830 );
1831 });
1832}
1833
1834fn visible_push_notifications(cx: &mut TestAppContext) -> Vec<Entity<ProjectSharedNotification>> {
1835 let mut ret = Vec::new();
1836 for window in cx.windows() {
1837 window
1838 .update(cx, |window, _, _| {
1839 if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
1840 ret.push(handle)
1841 }
1842 })
1843 .unwrap();
1844 }
1845 ret
1846}
1847
1848#[derive(Debug, PartialEq, Eq)]
1849struct PaneSummary {
1850 active: bool,
1851 leader: Option<PeerId>,
1852 items: Vec<(bool, String)>,
1853}
1854
1855fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1856 cx.read(|cx| {
1857 let active_call = ActiveCall::global(cx).read(cx);
1858 let peer_id = active_call.client().peer_id();
1859 let room = active_call.room().unwrap().read(cx);
1860 let mut result = room
1861 .remote_participants()
1862 .values()
1863 .map(|participant| participant.peer_id)
1864 .chain(peer_id)
1865 .filter_map(|peer_id| {
1866 let followers = room.followers_for(peer_id, project_id);
1867 if followers.is_empty() {
1868 None
1869 } else {
1870 Some((peer_id, followers.to_vec()))
1871 }
1872 })
1873 .collect::<Vec<_>>();
1874 result.sort_by_key(|e| e.0);
1875 result
1876 })
1877}
1878
1879fn pane_summaries(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
1880 workspace.update(cx, |workspace, cx| {
1881 let active_pane = workspace.active_pane();
1882 workspace
1883 .panes()
1884 .iter()
1885 .map(|pane| {
1886 let leader = match workspace.leader_for_pane(pane) {
1887 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
1888 Some(CollaboratorId::Agent) => unimplemented!(),
1889 None => None,
1890 };
1891 let active = pane == active_pane;
1892 let pane = pane.read(cx);
1893 let active_ix = pane.active_item_index();
1894 PaneSummary {
1895 active,
1896 leader,
1897 items: pane
1898 .items()
1899 .enumerate()
1900 .map(|(ix, item)| (ix == active_ix, item.tab_content_text(0, cx).into()))
1901 .collect(),
1902 }
1903 })
1904 .collect()
1905 })
1906}
1907
1908#[gpui::test(iterations = 10)]
1909async fn test_following_to_channel_notes_without_a_shared_project(
1910 deterministic: BackgroundExecutor,
1911 mut cx_a: &mut TestAppContext,
1912 mut cx_b: &mut TestAppContext,
1913 mut cx_c: &mut TestAppContext,
1914) {
1915 let mut server = TestServer::start(deterministic.clone()).await;
1916 let client_a = server.create_client(cx_a, "user_a").await;
1917 let client_b = server.create_client(cx_b, "user_b").await;
1918 let client_c = server.create_client(cx_c, "user_c").await;
1919
1920 cx_a.update(editor::init);
1921 cx_b.update(editor::init);
1922 cx_c.update(editor::init);
1923 cx_a.update(collab_ui::channel_view::init);
1924 cx_b.update(collab_ui::channel_view::init);
1925 cx_c.update(collab_ui::channel_view::init);
1926
1927 let channel_1_id = server
1928 .make_channel(
1929 "channel-1",
1930 None,
1931 (&client_a, cx_a),
1932 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1933 )
1934 .await;
1935 let channel_2_id = server
1936 .make_channel(
1937 "channel-2",
1938 None,
1939 (&client_a, cx_a),
1940 &mut [(&client_b, cx_b), (&client_c, cx_c)],
1941 )
1942 .await;
1943
1944 // Clients A, B, and C join a channel.
1945 let active_call_a = cx_a.read(ActiveCall::global);
1946 let active_call_b = cx_b.read(ActiveCall::global);
1947 let active_call_c = cx_c.read(ActiveCall::global);
1948 for (call, cx) in [
1949 (&active_call_a, &mut cx_a),
1950 (&active_call_b, &mut cx_b),
1951 (&active_call_c, &mut cx_c),
1952 ] {
1953 call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
1954 .await
1955 .unwrap();
1956 }
1957 deterministic.run_until_parked();
1958
1959 // Clients A, B, and C all open their own unshared projects.
1960 client_a
1961 .fs()
1962 .insert_tree("/a", json!({ "1.txt": "" }))
1963 .await;
1964 client_b.fs().insert_tree("/b", json!({})).await;
1965 client_c.fs().insert_tree("/c", json!({})).await;
1966 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1967 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1968 let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
1969 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1970 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1971 let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c);
1972
1973 active_call_a
1974 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1975 .await
1976 .unwrap();
1977
1978 // Client A opens the notes for channel 1.
1979 let channel_notes_1_a = cx_a
1980 .update(|window, cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), window, cx))
1981 .await
1982 .unwrap();
1983 channel_notes_1_a.update_in(cx_a, |notes, window, cx| {
1984 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
1985 notes.editor.update(cx, |editor, cx| {
1986 editor.insert("Hello from A.", window, cx);
1987 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1988 selections.select_ranges(vec![3..4]);
1989 });
1990 });
1991 });
1992
1993 // Client B follows client A.
1994 workspace_b
1995 .update_in(cx_b, |workspace, window, cx| {
1996 workspace
1997 .start_following(client_a.peer_id().unwrap(), window, cx)
1998 .unwrap()
1999 })
2000 .await
2001 .unwrap();
2002
2003 // Client B is taken to the notes for channel 1, with the same
2004 // text selected as client A.
2005 deterministic.run_until_parked();
2006 let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
2007 assert_eq!(
2008 workspace.leader_for_pane(workspace.active_pane()),
2009 Some(client_a.peer_id().unwrap().into())
2010 );
2011 workspace
2012 .active_item(cx)
2013 .expect("no active item")
2014 .downcast::<ChannelView>()
2015 .expect("active item is not a channel view")
2016 });
2017 channel_notes_1_b.update(cx_b, |notes, cx| {
2018 assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
2019 notes.editor.update(cx, |editor, cx| {
2020 assert_eq!(editor.text(cx), "Hello from A.");
2021 assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
2022 })
2023 });
2024
2025 // Client A opens the notes for channel 2.
2026 let channel_notes_2_a = cx_a
2027 .update(|window, cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), window, cx))
2028 .await
2029 .unwrap();
2030 channel_notes_2_a.update(cx_a, |notes, cx| {
2031 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2032 });
2033
2034 // Client B is taken to the notes for channel 2.
2035 deterministic.run_until_parked();
2036 let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
2037 assert_eq!(
2038 workspace.leader_for_pane(workspace.active_pane()),
2039 Some(client_a.peer_id().unwrap().into())
2040 );
2041 workspace
2042 .active_item(cx)
2043 .expect("no active item")
2044 .downcast::<ChannelView>()
2045 .expect("active item is not a channel view")
2046 });
2047 channel_notes_2_b.update(cx_b, |notes, cx| {
2048 assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
2049 });
2050
2051 // Client A opens a local buffer in their unshared project.
2052 let _unshared_editor_a1 = workspace_a
2053 .update_in(cx_a, |workspace, window, cx| {
2054 workspace.open_path((worktree_id, "1.txt"), None, true, window, cx)
2055 })
2056 .await
2057 .unwrap()
2058 .downcast::<Editor>()
2059 .unwrap();
2060
2061 // This does not send any leader update message to client B.
2062 // If it did, an error would occur on client B, since this buffer
2063 // is not shared with them.
2064 deterministic.run_until_parked();
2065 workspace_b.update(cx_b, |workspace, cx| {
2066 assert_eq!(
2067 workspace.active_item(cx).expect("no active item").item_id(),
2068 channel_notes_2_b.entity_id()
2069 );
2070 });
2071}
2072
2073pub(crate) async fn join_channel(
2074 channel_id: ChannelId,
2075 client: &TestClient,
2076 cx: &mut TestAppContext,
2077) -> anyhow::Result<()> {
2078 cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
2079 .await
2080}
2081
2082async fn share_workspace(
2083 workspace: &Entity<Workspace>,
2084 cx: &mut VisualTestContext,
2085) -> anyhow::Result<u64> {
2086 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
2087 cx.read(ActiveCall::global)
2088 .update(cx, |call, cx| call.share_project(project, cx))
2089 .await
2090}
2091
2092#[gpui::test]
2093async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2094 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2095
2096 let (workspace, cx_a) = client_a.build_test_workspace(cx_a).await;
2097 join_channel(channel, &client_a, cx_a).await.unwrap();
2098 share_workspace(&workspace, cx_a).await.unwrap();
2099 let buffer = workspace.update(cx_a, |workspace, cx| {
2100 workspace.project().update(cx, |project, cx| {
2101 project.create_local_buffer(&sample_text(26, 5, 'a'), None, false, cx)
2102 })
2103 });
2104 let multibuffer = cx_a.new(|cx| {
2105 let mut mb = MultiBuffer::new(Capability::ReadWrite);
2106 mb.set_excerpts_for_path(
2107 PathKey::for_buffer(&buffer, cx),
2108 buffer.clone(),
2109 [Point::row_range(1..1), Point::row_range(5..5)],
2110 1,
2111 cx,
2112 );
2113 mb
2114 });
2115 let snapshot = buffer.update(cx_a, |buffer, _| buffer.snapshot());
2116 let editor: Entity<Editor> = cx_a.new_window_entity(|window, cx| {
2117 Editor::for_multibuffer(
2118 multibuffer.clone(),
2119 Some(workspace.read(cx).project().clone()),
2120 window,
2121 cx,
2122 )
2123 });
2124 workspace.update_in(cx_a, |workspace, window, cx| {
2125 workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
2126 });
2127 editor.update_in(cx_a, |editor, window, cx| {
2128 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2129 s.select_ranges([Point::row_range(4..4)]);
2130 })
2131 });
2132 let positions = editor.update(cx_a, |editor, _| {
2133 editor
2134 .selections
2135 .disjoint_anchor_ranges()
2136 .map(|range| range.start.text_anchor.to_point(&snapshot))
2137 .collect::<Vec<_>>()
2138 });
2139 multibuffer.update(cx_a, |multibuffer, cx| {
2140 multibuffer.set_excerpts_for_path(
2141 PathKey::for_buffer(&buffer, cx),
2142 buffer,
2143 [Point::row_range(1..5)],
2144 1,
2145 cx,
2146 );
2147 });
2148
2149 let (workspace_b, cx_b) = client_b.join_workspace(channel, cx_b).await;
2150 cx_b.run_until_parked();
2151 let editor_b = workspace_b
2152 .update(cx_b, |workspace, cx| {
2153 workspace
2154 .active_item(cx)
2155 .and_then(|item| item.downcast::<Editor>())
2156 })
2157 .unwrap();
2158
2159 let new_positions = editor_b.update(cx_b, |editor, _| {
2160 editor
2161 .selections
2162 .disjoint_anchor_ranges()
2163 .map(|range| range.start.text_anchor.to_point(&snapshot))
2164 .collect::<Vec<_>>()
2165 });
2166 assert_eq!(positions, new_positions);
2167}
2168
2169#[gpui::test]
2170async fn test_following_to_channel_notes_other_workspace(
2171 cx_a: &mut TestAppContext,
2172 cx_b: &mut TestAppContext,
2173) {
2174 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2175
2176 let mut cx_a2 = cx_a.clone();
2177 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2178 join_channel(channel, &client_a, cx_a).await.unwrap();
2179 share_workspace(&workspace_a, cx_a).await.unwrap();
2180
2181 // a opens 1.txt
2182 cx_a.simulate_keystrokes("cmd-p");
2183 cx_a.run_until_parked();
2184 cx_a.simulate_keystrokes("1 enter");
2185 cx_a.run_until_parked();
2186 workspace_a.update(cx_a, |workspace, cx| {
2187 let editor = workspace.active_item(cx).unwrap();
2188 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2189 });
2190
2191 // b joins channel and is following a
2192 join_channel(channel, &client_b, cx_b).await.unwrap();
2193 cx_b.run_until_parked();
2194 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2195 workspace_b.update(cx_b, |workspace, cx| {
2196 let editor = workspace.active_item(cx).unwrap();
2197 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2198 });
2199
2200 // a opens a second workspace and the channel notes
2201 let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2202 cx_a2.update(|window, _| window.activate_window());
2203 cx_a2
2204 .update(|window, cx| ChannelView::open(channel, None, workspace_a2, window, cx))
2205 .await
2206 .unwrap();
2207 cx_a2.run_until_parked();
2208
2209 // b should follow a to the channel notes
2210 workspace_b.update(cx_b, |workspace, cx| {
2211 let editor = workspace.active_item_as::<ChannelView>(cx).unwrap();
2212 assert_eq!(editor.read(cx).channel(cx).unwrap().id, channel);
2213 });
2214
2215 // a returns to the shared project
2216 cx_a.update(|window, _| window.activate_window());
2217 cx_a.run_until_parked();
2218
2219 workspace_a.update(cx_a, |workspace, cx| {
2220 let editor = workspace.active_item(cx).unwrap();
2221 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2222 });
2223
2224 // b should follow a back
2225 workspace_b.update(cx_b, |workspace, cx| {
2226 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2227 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2228 });
2229}
2230
2231#[gpui::test]
2232async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2233 let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
2234
2235 let mut cx_a2 = cx_a.clone();
2236 let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
2237 join_channel(channel, &client_a, cx_a).await.unwrap();
2238 share_workspace(&workspace_a, cx_a).await.unwrap();
2239
2240 // a opens 1.txt
2241 cx_a.simulate_keystrokes("cmd-p");
2242 cx_a.run_until_parked();
2243 cx_a.simulate_keystrokes("1 enter");
2244 cx_a.run_until_parked();
2245 workspace_a.update(cx_a, |workspace, cx| {
2246 let editor = workspace.active_item(cx).unwrap();
2247 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2248 });
2249
2250 // b joins channel and is following a
2251 join_channel(channel, &client_b, cx_b).await.unwrap();
2252 cx_b.run_until_parked();
2253 let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
2254 workspace_b.update(cx_b, |workspace, cx| {
2255 let editor = workspace.active_item(cx).unwrap();
2256 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2257 });
2258
2259 // stop following
2260 cx_b.simulate_keystrokes("down");
2261
2262 // a opens a different file while not followed
2263 cx_a.simulate_keystrokes("cmd-p");
2264 cx_a.run_until_parked();
2265 cx_a.simulate_keystrokes("2 enter");
2266
2267 workspace_b.update(cx_b, |workspace, cx| {
2268 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2269 assert_eq!(editor.tab_content_text(0, cx), "1.txt");
2270 });
2271
2272 // a opens a file in a new window
2273 let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
2274 cx_a2.update(|window, _| window.activate_window());
2275 cx_a2.simulate_keystrokes("cmd-p");
2276 cx_a2.run_until_parked();
2277 cx_a2.simulate_keystrokes("3 enter");
2278 cx_a2.run_until_parked();
2279
2280 // b starts following a again
2281 cx_b.simulate_keystrokes("cmd-ctrl-alt-f");
2282 cx_a.run_until_parked();
2283
2284 // a returns to the shared project
2285 cx_a.update(|window, _| window.activate_window());
2286 cx_a.run_until_parked();
2287
2288 workspace_a.update(cx_a, |workspace, cx| {
2289 let editor = workspace.active_item(cx).unwrap();
2290 assert_eq!(editor.tab_content_text(0, cx), "2.js");
2291 });
2292
2293 // b should follow a back
2294 workspace_b.update(cx_b, |workspace, cx| {
2295 let editor = workspace.active_item_as::<Editor>(cx).unwrap();
2296 assert_eq!(editor.tab_content_text(0, cx), "2.js");
2297 });
2298}