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