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