1use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
2use call::ActiveCall;
3use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
4use editor::{Editor, ExcerptRange, MultiBuffer};
5use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
6use live_kit_client::MacOSDisplay;
7use project::project_settings::ProjectSettings;
8use rpc::proto::PeerId;
9use serde_json::json;
10use settings::SettingsStore;
11use std::{borrow::Cow, sync::Arc};
12use workspace::{
13 dock::{test::TestPanel, DockPosition},
14 item::{test::TestItem, ItemHandle as _},
15 shared_screen::SharedScreen,
16 SplitDirection, Workspace,
17};
18
19#[gpui::test(iterations = 10)]
20async fn test_basic_following(
21 deterministic: Arc<Deterministic>,
22 cx_a: &mut TestAppContext,
23 cx_b: &mut TestAppContext,
24 cx_c: &mut TestAppContext,
25 cx_d: &mut TestAppContext,
26) {
27 deterministic.forbid_parking();
28
29 let mut server = TestServer::start(&deterministic).await;
30 let client_a = server.create_client(cx_a, "user_a").await;
31 let client_b = server.create_client(cx_b, "user_b").await;
32 let client_c = server.create_client(cx_c, "user_c").await;
33 let client_d = server.create_client(cx_d, "user_d").await;
34 server
35 .create_room(&mut [
36 (&client_a, cx_a),
37 (&client_b, cx_b),
38 (&client_c, cx_c),
39 (&client_d, cx_d),
40 ])
41 .await;
42 let active_call_a = cx_a.read(ActiveCall::global);
43 let active_call_b = cx_b.read(ActiveCall::global);
44
45 cx_a.update(editor::init);
46 cx_b.update(editor::init);
47
48 client_a
49 .fs()
50 .insert_tree(
51 "/a",
52 json!({
53 "1.txt": "one\none\none",
54 "2.txt": "two\ntwo\ntwo",
55 "3.txt": "three\nthree\nthree",
56 }),
57 )
58 .await;
59 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
60 active_call_a
61 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
62 .await
63 .unwrap();
64
65 let project_id = active_call_a
66 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
67 .await
68 .unwrap();
69 let project_b = client_b.build_remote_project(project_id, cx_b).await;
70 active_call_b
71 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
72 .await
73 .unwrap();
74
75 let window_a = client_a.build_workspace(&project_a, cx_a);
76 let workspace_a = window_a.root(cx_a);
77 let window_b = client_b.build_workspace(&project_b, cx_b);
78 let workspace_b = window_b.root(cx_b);
79
80 // Client A opens some editors.
81 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
82 let editor_a1 = workspace_a
83 .update(cx_a, |workspace, cx| {
84 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
85 })
86 .await
87 .unwrap()
88 .downcast::<Editor>()
89 .unwrap();
90 let editor_a2 = workspace_a
91 .update(cx_a, |workspace, cx| {
92 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
93 })
94 .await
95 .unwrap()
96 .downcast::<Editor>()
97 .unwrap();
98
99 // Client B opens an editor.
100 let editor_b1 = workspace_b
101 .update(cx_b, |workspace, cx| {
102 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
103 })
104 .await
105 .unwrap()
106 .downcast::<Editor>()
107 .unwrap();
108
109 let peer_id_a = client_a.peer_id().unwrap();
110 let peer_id_b = client_b.peer_id().unwrap();
111 let peer_id_c = client_c.peer_id().unwrap();
112 let peer_id_d = client_d.peer_id().unwrap();
113
114 // Client A updates their selections in those editors
115 editor_a1.update(cx_a, |editor, cx| {
116 editor.handle_input("a", cx);
117 editor.handle_input("b", cx);
118 editor.handle_input("c", cx);
119 editor.select_left(&Default::default(), cx);
120 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
121 });
122 editor_a2.update(cx_a, |editor, cx| {
123 editor.handle_input("d", cx);
124 editor.handle_input("e", cx);
125 editor.select_left(&Default::default(), cx);
126 assert_eq!(editor.selections.ranges(cx), vec![2..1]);
127 });
128
129 // When client B starts following client A, all visible view states are replicated to client B.
130 workspace_b
131 .update(cx_b, |workspace, cx| {
132 workspace.follow(peer_id_a, cx).unwrap()
133 })
134 .await
135 .unwrap();
136
137 cx_c.foreground().run_until_parked();
138 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
139 workspace
140 .active_item(cx)
141 .unwrap()
142 .downcast::<Editor>()
143 .unwrap()
144 });
145 assert_eq!(
146 cx_b.read(|cx| editor_b2.project_path(cx)),
147 Some((worktree_id, "2.txt").into())
148 );
149 assert_eq!(
150 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
151 vec![2..1]
152 );
153 assert_eq!(
154 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
155 vec![3..2]
156 );
157
158 cx_c.foreground().run_until_parked();
159 let active_call_c = cx_c.read(ActiveCall::global);
160 let project_c = client_c.build_remote_project(project_id, cx_c).await;
161 let window_c = client_c.build_workspace(&project_c, cx_c);
162 let workspace_c = window_c.root(cx_c);
163 active_call_c
164 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
165 .await
166 .unwrap();
167 drop(project_c);
168
169 // Client C also follows client A.
170 workspace_c
171 .update(cx_c, |workspace, cx| {
172 workspace.follow(peer_id_a, cx).unwrap()
173 })
174 .await
175 .unwrap();
176
177 cx_d.foreground().run_until_parked();
178 let active_call_d = cx_d.read(ActiveCall::global);
179 let project_d = client_d.build_remote_project(project_id, cx_d).await;
180 let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
181 active_call_d
182 .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
183 .await
184 .unwrap();
185 drop(project_d);
186
187 // All clients see that clients B and C are following client A.
188 cx_c.foreground().run_until_parked();
189 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
190 assert_eq!(
191 followers_by_leader(project_id, cx),
192 &[(peer_id_a, vec![peer_id_b, peer_id_c])],
193 "followers seen by {name}"
194 );
195 }
196
197 // Client C unfollows client A.
198 workspace_c.update(cx_c, |workspace, cx| {
199 workspace.unfollow(&workspace.active_pane().clone(), cx);
200 });
201
202 // All clients see that clients B is following client A.
203 cx_c.foreground().run_until_parked();
204 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
205 assert_eq!(
206 followers_by_leader(project_id, cx),
207 &[(peer_id_a, vec![peer_id_b])],
208 "followers seen by {name}"
209 );
210 }
211
212 // Client C re-follows client A.
213 workspace_c
214 .update(cx_c, |workspace, cx| {
215 workspace.follow(peer_id_a, cx).unwrap()
216 })
217 .await
218 .unwrap();
219
220 // All clients see that clients B and C are following client A.
221 cx_c.foreground().run_until_parked();
222 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
223 assert_eq!(
224 followers_by_leader(project_id, cx),
225 &[(peer_id_a, vec![peer_id_b, peer_id_c])],
226 "followers seen by {name}"
227 );
228 }
229
230 // Client D follows client B, then switches to following client C.
231 workspace_d
232 .update(cx_d, |workspace, cx| {
233 workspace.follow(peer_id_b, cx).unwrap()
234 })
235 .await
236 .unwrap();
237 workspace_d
238 .update(cx_d, |workspace, cx| {
239 workspace.follow(peer_id_c, cx).unwrap()
240 })
241 .await
242 .unwrap();
243
244 // All clients see that D is following C
245 cx_d.foreground().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 window_c.remove(cx_c);
259 cx_c.drop_last(workspace_c);
260
261 // Clients A and B see that client B is following A, and client C is not present in the followers.
262 cx_c.foreground().run_until_parked();
263 for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
264 assert_eq!(
265 followers_by_leader(project_id, cx),
266 &[(peer_id_a, vec![peer_id_b]),],
267 "followers seen by {name}"
268 );
269 }
270
271 // When client A activates a different editor, client B does so as well.
272 workspace_a.update(cx_a, |workspace, cx| {
273 workspace.activate_item(&editor_a1, cx)
274 });
275 deterministic.run_until_parked();
276 workspace_b.read_with(cx_b, |workspace, cx| {
277 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
278 });
279
280 // When client A opens a multibuffer, client B does so as well.
281 let multibuffer_a = cx_a.add_model(|cx| {
282 let buffer_a1 = project_a.update(cx, |project, cx| {
283 project
284 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
285 .unwrap()
286 });
287 let buffer_a2 = project_a.update(cx, |project, cx| {
288 project
289 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
290 .unwrap()
291 });
292 let mut result = MultiBuffer::new(0);
293 result.push_excerpts(
294 buffer_a1,
295 [ExcerptRange {
296 context: 0..3,
297 primary: None,
298 }],
299 cx,
300 );
301 result.push_excerpts(
302 buffer_a2,
303 [ExcerptRange {
304 context: 4..7,
305 primary: None,
306 }],
307 cx,
308 );
309 result
310 });
311 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
312 let editor =
313 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
314 workspace.add_item(Box::new(editor.clone()), cx);
315 editor
316 });
317 deterministic.run_until_parked();
318 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
319 workspace
320 .active_item(cx)
321 .unwrap()
322 .downcast::<Editor>()
323 .unwrap()
324 });
325 assert_eq!(
326 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
327 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
328 );
329
330 // When client A navigates back and forth, client B does so as well.
331 workspace_a
332 .update(cx_a, |workspace, cx| {
333 workspace.go_back(workspace.active_pane().downgrade(), cx)
334 })
335 .await
336 .unwrap();
337 deterministic.run_until_parked();
338 workspace_b.read_with(cx_b, |workspace, cx| {
339 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
340 });
341
342 workspace_a
343 .update(cx_a, |workspace, cx| {
344 workspace.go_back(workspace.active_pane().downgrade(), cx)
345 })
346 .await
347 .unwrap();
348 deterministic.run_until_parked();
349 workspace_b.read_with(cx_b, |workspace, cx| {
350 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
351 });
352
353 workspace_a
354 .update(cx_a, |workspace, cx| {
355 workspace.go_forward(workspace.active_pane().downgrade(), cx)
356 })
357 .await
358 .unwrap();
359 deterministic.run_until_parked();
360 workspace_b.read_with(cx_b, |workspace, cx| {
361 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
362 });
363
364 // Changes to client A's editor are reflected on client B.
365 editor_a1.update(cx_a, |editor, cx| {
366 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
367 });
368 deterministic.run_until_parked();
369 editor_b1.read_with(cx_b, |editor, cx| {
370 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
371 });
372
373 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
374 deterministic.run_until_parked();
375 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
376
377 editor_a1.update(cx_a, |editor, cx| {
378 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
379 editor.set_scroll_position(vec2f(0., 100.), cx);
380 });
381 deterministic.run_until_parked();
382 editor_b1.read_with(cx_b, |editor, cx| {
383 assert_eq!(editor.selections.ranges(cx), &[3..3]);
384 });
385
386 // After unfollowing, client B stops receiving updates from client A.
387 workspace_b.update(cx_b, |workspace, cx| {
388 workspace.unfollow(&workspace.active_pane().clone(), cx)
389 });
390 workspace_a.update(cx_a, |workspace, cx| {
391 workspace.activate_item(&editor_a2, cx)
392 });
393 deterministic.run_until_parked();
394 assert_eq!(
395 workspace_b.read_with(cx_b, |workspace, cx| workspace
396 .active_item(cx)
397 .unwrap()
398 .id()),
399 editor_b1.id()
400 );
401
402 // Client A starts following client B.
403 workspace_a
404 .update(cx_a, |workspace, cx| {
405 workspace.follow(peer_id_b, cx).unwrap()
406 })
407 .await
408 .unwrap();
409 assert_eq!(
410 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
411 Some(peer_id_b)
412 );
413 assert_eq!(
414 workspace_a.read_with(cx_a, |workspace, cx| workspace
415 .active_item(cx)
416 .unwrap()
417 .id()),
418 editor_a1.id()
419 );
420
421 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
422 let display = MacOSDisplay::new();
423 active_call_b
424 .update(cx_b, |call, cx| call.set_location(None, cx))
425 .await
426 .unwrap();
427 active_call_b
428 .update(cx_b, |call, cx| {
429 call.room().unwrap().update(cx, |room, cx| {
430 room.set_display_sources(vec![display.clone()]);
431 room.share_screen(cx)
432 })
433 })
434 .await
435 .unwrap();
436 deterministic.run_until_parked();
437 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
438 workspace
439 .active_item(cx)
440 .expect("no active item")
441 .downcast::<SharedScreen>()
442 .expect("active item isn't a shared screen")
443 });
444
445 // Client B activates Zed again, which causes the previous editor to become focused again.
446 active_call_b
447 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
448 .await
449 .unwrap();
450 deterministic.run_until_parked();
451 workspace_a.read_with(cx_a, |workspace, cx| {
452 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
453 });
454
455 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
456 workspace_b.update(cx_b, |workspace, cx| {
457 workspace.activate_item(&multibuffer_editor_b, cx)
458 });
459 deterministic.run_until_parked();
460 workspace_a.read_with(cx_a, |workspace, cx| {
461 assert_eq!(
462 workspace.active_item(cx).unwrap().id(),
463 multibuffer_editor_a.id()
464 )
465 });
466
467 // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
468 let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
469 workspace_b.update(cx_b, |workspace, cx| {
470 workspace.add_panel(panel, cx);
471 workspace.toggle_panel_focus::<TestPanel>(cx);
472 });
473 deterministic.run_until_parked();
474 assert_eq!(
475 workspace_a.read_with(cx_a, |workspace, cx| workspace
476 .active_item(cx)
477 .unwrap()
478 .id()),
479 shared_screen.id()
480 );
481
482 // Toggling the focus back to the pane causes client A to return to the multibuffer.
483 workspace_b.update(cx_b, |workspace, cx| {
484 workspace.toggle_panel_focus::<TestPanel>(cx);
485 });
486 deterministic.run_until_parked();
487 workspace_a.read_with(cx_a, |workspace, cx| {
488 assert_eq!(
489 workspace.active_item(cx).unwrap().id(),
490 multibuffer_editor_a.id()
491 )
492 });
493
494 // Client B activates an item that doesn't implement following,
495 // so the previously-opened screen-sharing item gets activated.
496 let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
497 workspace_b.update(cx_b, |workspace, cx| {
498 workspace.active_pane().update(cx, |pane, cx| {
499 pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
500 })
501 });
502 deterministic.run_until_parked();
503 assert_eq!(
504 workspace_a.read_with(cx_a, |workspace, cx| workspace
505 .active_item(cx)
506 .unwrap()
507 .id()),
508 shared_screen.id()
509 );
510
511 // Following interrupts when client B disconnects.
512 client_b.disconnect(&cx_b.to_async());
513 deterministic.advance_clock(RECONNECT_TIMEOUT);
514 assert_eq!(
515 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
516 None
517 );
518}
519
520#[gpui::test]
521async fn test_following_tab_order(
522 deterministic: Arc<Deterministic>,
523 cx_a: &mut TestAppContext,
524 cx_b: &mut TestAppContext,
525) {
526 let mut server = TestServer::start(&deterministic).await;
527 let client_a = server.create_client(cx_a, "user_a").await;
528 let client_b = server.create_client(cx_b, "user_b").await;
529 server
530 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
531 .await;
532 let active_call_a = cx_a.read(ActiveCall::global);
533 let active_call_b = cx_b.read(ActiveCall::global);
534
535 cx_a.update(editor::init);
536 cx_b.update(editor::init);
537
538 client_a
539 .fs()
540 .insert_tree(
541 "/a",
542 json!({
543 "1.txt": "one",
544 "2.txt": "two",
545 "3.txt": "three",
546 }),
547 )
548 .await;
549 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
550 active_call_a
551 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
552 .await
553 .unwrap();
554
555 let project_id = active_call_a
556 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
557 .await
558 .unwrap();
559 let project_b = client_b.build_remote_project(project_id, cx_b).await;
560 active_call_b
561 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
562 .await
563 .unwrap();
564
565 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
566 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
567
568 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
569 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
570
571 let client_b_id = project_a.read_with(cx_a, |project, _| {
572 project.collaborators().values().next().unwrap().peer_id
573 });
574
575 //Open 1, 3 in that order on client A
576 workspace_a
577 .update(cx_a, |workspace, cx| {
578 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
579 })
580 .await
581 .unwrap();
582 workspace_a
583 .update(cx_a, |workspace, cx| {
584 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
585 })
586 .await
587 .unwrap();
588
589 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
590 pane.update(cx, |pane, cx| {
591 pane.items()
592 .map(|item| {
593 item.project_path(cx)
594 .unwrap()
595 .path
596 .to_str()
597 .unwrap()
598 .to_owned()
599 })
600 .collect::<Vec<_>>()
601 })
602 };
603
604 //Verify that the tabs opened in the order we expect
605 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
606
607 //Follow client B as client A
608 workspace_a
609 .update(cx_a, |workspace, cx| {
610 workspace.follow(client_b_id, cx).unwrap()
611 })
612 .await
613 .unwrap();
614
615 //Open just 2 on client B
616 workspace_b
617 .update(cx_b, |workspace, cx| {
618 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
619 })
620 .await
621 .unwrap();
622 deterministic.run_until_parked();
623
624 // Verify that newly opened followed file is at the end
625 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
626
627 //Open just 1 on client B
628 workspace_b
629 .update(cx_b, |workspace, cx| {
630 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
631 })
632 .await
633 .unwrap();
634 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
635 deterministic.run_until_parked();
636
637 // Verify that following into 1 did not reorder
638 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
639}
640
641#[gpui::test(iterations = 10)]
642async fn test_peers_following_each_other(
643 deterministic: Arc<Deterministic>,
644 cx_a: &mut TestAppContext,
645 cx_b: &mut TestAppContext,
646) {
647 deterministic.forbid_parking();
648 let mut server = TestServer::start(&deterministic).await;
649 let client_a = server.create_client(cx_a, "user_a").await;
650 let client_b = server.create_client(cx_b, "user_b").await;
651 server
652 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
653 .await;
654 let active_call_a = cx_a.read(ActiveCall::global);
655 let active_call_b = cx_b.read(ActiveCall::global);
656
657 cx_a.update(editor::init);
658 cx_b.update(editor::init);
659
660 // Client A shares a project.
661 client_a
662 .fs()
663 .insert_tree(
664 "/a",
665 json!({
666 "1.txt": "one",
667 "2.txt": "two",
668 "3.txt": "three",
669 "4.txt": "four",
670 }),
671 )
672 .await;
673 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
674 active_call_a
675 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
676 .await
677 .unwrap();
678 let project_id = active_call_a
679 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
680 .await
681 .unwrap();
682
683 // Client B joins the project.
684 let project_b = client_b.build_remote_project(project_id, cx_b).await;
685 active_call_b
686 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
687 .await
688 .unwrap();
689
690 // Client A opens a file.
691 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
692 workspace_a
693 .update(cx_a, |workspace, cx| {
694 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
695 })
696 .await
697 .unwrap()
698 .downcast::<Editor>()
699 .unwrap();
700
701 // Client B opens a different file.
702 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
703 workspace_b
704 .update(cx_b, |workspace, cx| {
705 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
706 })
707 .await
708 .unwrap()
709 .downcast::<Editor>()
710 .unwrap();
711
712 // Clients A and B follow each other in split panes
713 workspace_a.update(cx_a, |workspace, cx| {
714 workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
715 });
716 workspace_a
717 .update(cx_a, |workspace, cx| {
718 workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
719 })
720 .await
721 .unwrap();
722 workspace_b.update(cx_b, |workspace, cx| {
723 workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
724 });
725 workspace_b
726 .update(cx_b, |workspace, cx| {
727 workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
728 })
729 .await
730 .unwrap();
731
732 // Clients A and B return focus to the original files they had open
733 workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
734 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
735 deterministic.run_until_parked();
736
737 // Both clients see the other client's focused file in their right pane.
738 assert_eq!(
739 pane_summaries(&workspace_a, cx_a),
740 &[
741 PaneSummary {
742 active: true,
743 leader: None,
744 items: vec![(true, "1.txt".into())]
745 },
746 PaneSummary {
747 active: false,
748 leader: client_b.peer_id(),
749 items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
750 },
751 ]
752 );
753 assert_eq!(
754 pane_summaries(&workspace_b, cx_b),
755 &[
756 PaneSummary {
757 active: true,
758 leader: None,
759 items: vec![(true, "2.txt".into())]
760 },
761 PaneSummary {
762 active: false,
763 leader: client_a.peer_id(),
764 items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
765 },
766 ]
767 );
768
769 // Clients A and B each open a new file.
770 workspace_a
771 .update(cx_a, |workspace, cx| {
772 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
773 })
774 .await
775 .unwrap();
776
777 workspace_b
778 .update(cx_b, |workspace, cx| {
779 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
780 })
781 .await
782 .unwrap();
783 deterministic.run_until_parked();
784
785 // Both client's see the other client open the new file, but keep their
786 // focus on their own active pane.
787 assert_eq!(
788 pane_summaries(&workspace_a, cx_a),
789 &[
790 PaneSummary {
791 active: true,
792 leader: None,
793 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
794 },
795 PaneSummary {
796 active: false,
797 leader: client_b.peer_id(),
798 items: vec![
799 (false, "1.txt".into()),
800 (false, "2.txt".into()),
801 (true, "4.txt".into())
802 ]
803 },
804 ]
805 );
806 assert_eq!(
807 pane_summaries(&workspace_b, cx_b),
808 &[
809 PaneSummary {
810 active: true,
811 leader: None,
812 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
813 },
814 PaneSummary {
815 active: false,
816 leader: client_a.peer_id(),
817 items: vec![
818 (false, "2.txt".into()),
819 (false, "1.txt".into()),
820 (true, "3.txt".into())
821 ]
822 },
823 ]
824 );
825
826 // Client A focuses their right pane, in which they're following client B.
827 workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
828 deterministic.run_until_parked();
829
830 // Client B sees that client A is now looking at the same file as them.
831 assert_eq!(
832 pane_summaries(&workspace_a, cx_a),
833 &[
834 PaneSummary {
835 active: false,
836 leader: None,
837 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
838 },
839 PaneSummary {
840 active: true,
841 leader: client_b.peer_id(),
842 items: vec![
843 (false, "1.txt".into()),
844 (false, "2.txt".into()),
845 (true, "4.txt".into())
846 ]
847 },
848 ]
849 );
850 assert_eq!(
851 pane_summaries(&workspace_b, cx_b),
852 &[
853 PaneSummary {
854 active: true,
855 leader: None,
856 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
857 },
858 PaneSummary {
859 active: false,
860 leader: client_a.peer_id(),
861 items: vec![
862 (false, "2.txt".into()),
863 (false, "1.txt".into()),
864 (false, "3.txt".into()),
865 (true, "4.txt".into())
866 ]
867 },
868 ]
869 );
870
871 // Client B focuses their right pane, in which they're following client A,
872 // who is following them.
873 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
874 deterministic.run_until_parked();
875
876 // Client A sees that client B is now looking at the same file as them.
877 assert_eq!(
878 pane_summaries(&workspace_b, cx_b),
879 &[
880 PaneSummary {
881 active: false,
882 leader: None,
883 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
884 },
885 PaneSummary {
886 active: true,
887 leader: client_a.peer_id(),
888 items: vec![
889 (false, "2.txt".into()),
890 (false, "1.txt".into()),
891 (false, "3.txt".into()),
892 (true, "4.txt".into())
893 ]
894 },
895 ]
896 );
897 assert_eq!(
898 pane_summaries(&workspace_a, cx_a),
899 &[
900 PaneSummary {
901 active: false,
902 leader: None,
903 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
904 },
905 PaneSummary {
906 active: true,
907 leader: client_b.peer_id(),
908 items: vec![
909 (false, "1.txt".into()),
910 (false, "2.txt".into()),
911 (true, "4.txt".into())
912 ]
913 },
914 ]
915 );
916
917 // Client B focuses a file that they previously followed A to, breaking
918 // the follow.
919 workspace_b.update(cx_b, |workspace, cx| {
920 workspace.active_pane().update(cx, |pane, cx| {
921 pane.activate_prev_item(true, cx);
922 });
923 });
924 deterministic.run_until_parked();
925
926 // Both clients see that client B is looking at that previous file.
927 assert_eq!(
928 pane_summaries(&workspace_b, cx_b),
929 &[
930 PaneSummary {
931 active: false,
932 leader: None,
933 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
934 },
935 PaneSummary {
936 active: true,
937 leader: None,
938 items: vec![
939 (false, "2.txt".into()),
940 (false, "1.txt".into()),
941 (true, "3.txt".into()),
942 (false, "4.txt".into())
943 ]
944 },
945 ]
946 );
947 assert_eq!(
948 pane_summaries(&workspace_a, cx_a),
949 &[
950 PaneSummary {
951 active: false,
952 leader: None,
953 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
954 },
955 PaneSummary {
956 active: true,
957 leader: client_b.peer_id(),
958 items: vec![
959 (false, "1.txt".into()),
960 (false, "2.txt".into()),
961 (false, "4.txt".into()),
962 (true, "3.txt".into()),
963 ]
964 },
965 ]
966 );
967
968 // Client B closes tabs, some of which were originally opened by client A,
969 // and some of which were originally opened by client B.
970 workspace_b.update(cx_b, |workspace, cx| {
971 workspace.active_pane().update(cx, |pane, cx| {
972 pane.close_inactive_items(&Default::default(), cx)
973 .unwrap()
974 .detach();
975 });
976 });
977
978 deterministic.run_until_parked();
979
980 // Both clients see that Client B is looking at the previous tab.
981 assert_eq!(
982 pane_summaries(&workspace_b, cx_b),
983 &[
984 PaneSummary {
985 active: false,
986 leader: None,
987 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
988 },
989 PaneSummary {
990 active: true,
991 leader: None,
992 items: vec![(true, "3.txt".into()),]
993 },
994 ]
995 );
996 assert_eq!(
997 pane_summaries(&workspace_a, cx_a),
998 &[
999 PaneSummary {
1000 active: false,
1001 leader: None,
1002 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1003 },
1004 PaneSummary {
1005 active: true,
1006 leader: client_b.peer_id(),
1007 items: vec![
1008 (false, "1.txt".into()),
1009 (false, "2.txt".into()),
1010 (false, "4.txt".into()),
1011 (true, "3.txt".into()),
1012 ]
1013 },
1014 ]
1015 );
1016
1017 // Client B follows client A again.
1018 workspace_b
1019 .update(cx_b, |workspace, cx| {
1020 workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
1021 })
1022 .await
1023 .unwrap();
1024
1025 // Client A cycles through some tabs.
1026 workspace_a.update(cx_a, |workspace, cx| {
1027 workspace.active_pane().update(cx, |pane, cx| {
1028 pane.activate_prev_item(true, cx);
1029 });
1030 });
1031 deterministic.run_until_parked();
1032
1033 // Client B follows client A into those tabs.
1034 assert_eq!(
1035 pane_summaries(&workspace_a, cx_a),
1036 &[
1037 PaneSummary {
1038 active: false,
1039 leader: None,
1040 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1041 },
1042 PaneSummary {
1043 active: true,
1044 leader: None,
1045 items: vec![
1046 (false, "1.txt".into()),
1047 (false, "2.txt".into()),
1048 (true, "4.txt".into()),
1049 (false, "3.txt".into()),
1050 ]
1051 },
1052 ]
1053 );
1054 assert_eq!(
1055 pane_summaries(&workspace_b, cx_b),
1056 &[
1057 PaneSummary {
1058 active: false,
1059 leader: None,
1060 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1061 },
1062 PaneSummary {
1063 active: true,
1064 leader: client_a.peer_id(),
1065 items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
1066 },
1067 ]
1068 );
1069
1070 workspace_a.update(cx_a, |workspace, cx| {
1071 workspace.active_pane().update(cx, |pane, cx| {
1072 pane.activate_prev_item(true, cx);
1073 });
1074 });
1075 deterministic.run_until_parked();
1076
1077 assert_eq!(
1078 pane_summaries(&workspace_a, cx_a),
1079 &[
1080 PaneSummary {
1081 active: false,
1082 leader: None,
1083 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1084 },
1085 PaneSummary {
1086 active: true,
1087 leader: None,
1088 items: vec![
1089 (false, "1.txt".into()),
1090 (true, "2.txt".into()),
1091 (false, "4.txt".into()),
1092 (false, "3.txt".into()),
1093 ]
1094 },
1095 ]
1096 );
1097 assert_eq!(
1098 pane_summaries(&workspace_b, cx_b),
1099 &[
1100 PaneSummary {
1101 active: false,
1102 leader: None,
1103 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1104 },
1105 PaneSummary {
1106 active: true,
1107 leader: client_a.peer_id(),
1108 items: vec![
1109 (false, "3.txt".into()),
1110 (false, "4.txt".into()),
1111 (true, "2.txt".into())
1112 ]
1113 },
1114 ]
1115 );
1116
1117 workspace_a.update(cx_a, |workspace, cx| {
1118 workspace.active_pane().update(cx, |pane, cx| {
1119 pane.activate_prev_item(true, cx);
1120 });
1121 });
1122 deterministic.run_until_parked();
1123
1124 assert_eq!(
1125 pane_summaries(&workspace_a, cx_a),
1126 &[
1127 PaneSummary {
1128 active: false,
1129 leader: None,
1130 items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
1131 },
1132 PaneSummary {
1133 active: true,
1134 leader: None,
1135 items: vec![
1136 (true, "1.txt".into()),
1137 (false, "2.txt".into()),
1138 (false, "4.txt".into()),
1139 (false, "3.txt".into()),
1140 ]
1141 },
1142 ]
1143 );
1144 assert_eq!(
1145 pane_summaries(&workspace_b, cx_b),
1146 &[
1147 PaneSummary {
1148 active: false,
1149 leader: None,
1150 items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
1151 },
1152 PaneSummary {
1153 active: true,
1154 leader: client_a.peer_id(),
1155 items: vec![
1156 (false, "3.txt".into()),
1157 (false, "4.txt".into()),
1158 (false, "2.txt".into()),
1159 (true, "1.txt".into()),
1160 ]
1161 },
1162 ]
1163 );
1164}
1165
1166#[gpui::test(iterations = 10)]
1167async fn test_auto_unfollowing(
1168 deterministic: Arc<Deterministic>,
1169 cx_a: &mut TestAppContext,
1170 cx_b: &mut TestAppContext,
1171) {
1172 deterministic.forbid_parking();
1173
1174 // 2 clients connect to a server.
1175 let mut server = TestServer::start(&deterministic).await;
1176 let client_a = server.create_client(cx_a, "user_a").await;
1177 let client_b = server.create_client(cx_b, "user_b").await;
1178 server
1179 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1180 .await;
1181 let active_call_a = cx_a.read(ActiveCall::global);
1182 let active_call_b = cx_b.read(ActiveCall::global);
1183
1184 cx_a.update(editor::init);
1185 cx_b.update(editor::init);
1186
1187 // Client A shares a project.
1188 client_a
1189 .fs()
1190 .insert_tree(
1191 "/a",
1192 json!({
1193 "1.txt": "one",
1194 "2.txt": "two",
1195 "3.txt": "three",
1196 }),
1197 )
1198 .await;
1199 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1200 active_call_a
1201 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1202 .await
1203 .unwrap();
1204
1205 let project_id = active_call_a
1206 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1207 .await
1208 .unwrap();
1209 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1210 active_call_b
1211 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1212 .await
1213 .unwrap();
1214
1215 // Client A opens some editors.
1216 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1217 let _editor_a1 = workspace_a
1218 .update(cx_a, |workspace, cx| {
1219 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
1220 })
1221 .await
1222 .unwrap()
1223 .downcast::<Editor>()
1224 .unwrap();
1225
1226 // Client B starts following client A.
1227 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1228 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
1229 let leader_id = project_b.read_with(cx_b, |project, _| {
1230 project.collaborators().values().next().unwrap().peer_id
1231 });
1232 workspace_b
1233 .update(cx_b, |workspace, cx| {
1234 workspace.follow(leader_id, cx).unwrap()
1235 })
1236 .await
1237 .unwrap();
1238 assert_eq!(
1239 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1240 Some(leader_id)
1241 );
1242 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
1243 workspace
1244 .active_item(cx)
1245 .unwrap()
1246 .downcast::<Editor>()
1247 .unwrap()
1248 });
1249
1250 // When client B moves, it automatically stops following client A.
1251 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
1252 assert_eq!(
1253 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1254 None
1255 );
1256
1257 workspace_b
1258 .update(cx_b, |workspace, cx| {
1259 workspace.follow(leader_id, cx).unwrap()
1260 })
1261 .await
1262 .unwrap();
1263 assert_eq!(
1264 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1265 Some(leader_id)
1266 );
1267
1268 // When client B edits, it automatically stops following client A.
1269 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
1270 assert_eq!(
1271 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1272 None
1273 );
1274
1275 workspace_b
1276 .update(cx_b, |workspace, cx| {
1277 workspace.follow(leader_id, cx).unwrap()
1278 })
1279 .await
1280 .unwrap();
1281 assert_eq!(
1282 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1283 Some(leader_id)
1284 );
1285
1286 // When client B scrolls, it automatically stops following client A.
1287 editor_b2.update(cx_b, |editor, cx| {
1288 editor.set_scroll_position(vec2f(0., 3.), cx)
1289 });
1290 assert_eq!(
1291 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1292 None
1293 );
1294
1295 workspace_b
1296 .update(cx_b, |workspace, cx| {
1297 workspace.follow(leader_id, cx).unwrap()
1298 })
1299 .await
1300 .unwrap();
1301 assert_eq!(
1302 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1303 Some(leader_id)
1304 );
1305
1306 // When client B activates a different pane, it continues following client A in the original pane.
1307 workspace_b.update(cx_b, |workspace, cx| {
1308 workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
1309 });
1310 assert_eq!(
1311 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1312 Some(leader_id)
1313 );
1314
1315 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
1316 assert_eq!(
1317 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1318 Some(leader_id)
1319 );
1320
1321 // When client B activates a different item in the original pane, it automatically stops following client A.
1322 workspace_b
1323 .update(cx_b, |workspace, cx| {
1324 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
1325 })
1326 .await
1327 .unwrap();
1328 assert_eq!(
1329 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
1330 None
1331 );
1332}
1333
1334#[gpui::test(iterations = 10)]
1335async fn test_peers_simultaneously_following_each_other(
1336 deterministic: Arc<Deterministic>,
1337 cx_a: &mut TestAppContext,
1338 cx_b: &mut TestAppContext,
1339) {
1340 deterministic.forbid_parking();
1341
1342 let mut server = TestServer::start(&deterministic).await;
1343 let client_a = server.create_client(cx_a, "user_a").await;
1344 let client_b = server.create_client(cx_b, "user_b").await;
1345 server
1346 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1347 .await;
1348 let active_call_a = cx_a.read(ActiveCall::global);
1349
1350 cx_a.update(editor::init);
1351 cx_b.update(editor::init);
1352
1353 client_a.fs().insert_tree("/a", json!({})).await;
1354 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1355 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1356 let project_id = active_call_a
1357 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1358 .await
1359 .unwrap();
1360
1361 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1362 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1363
1364 deterministic.run_until_parked();
1365 let client_a_id = project_b.read_with(cx_b, |project, _| {
1366 project.collaborators().values().next().unwrap().peer_id
1367 });
1368 let client_b_id = project_a.read_with(cx_a, |project, _| {
1369 project.collaborators().values().next().unwrap().peer_id
1370 });
1371
1372 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
1373 workspace.follow(client_b_id, cx).unwrap()
1374 });
1375 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
1376 workspace.follow(client_a_id, cx).unwrap()
1377 });
1378
1379 futures::try_join!(a_follow_b, b_follow_a).unwrap();
1380 workspace_a.read_with(cx_a, |workspace, _| {
1381 assert_eq!(
1382 workspace.leader_for_pane(workspace.active_pane()),
1383 Some(client_b_id)
1384 );
1385 });
1386 workspace_b.read_with(cx_b, |workspace, _| {
1387 assert_eq!(
1388 workspace.leader_for_pane(workspace.active_pane()),
1389 Some(client_a_id)
1390 );
1391 });
1392}
1393
1394#[gpui::test(iterations = 10)]
1395async fn test_following_across_workspaces(
1396 deterministic: Arc<Deterministic>,
1397 cx_a: &mut TestAppContext,
1398 cx_b: &mut TestAppContext,
1399) {
1400 // a and b join a channel/call
1401 // a shares project 1
1402 // b shares project 2
1403 //
1404 // b follows a: causes project 2 to be joined, and b to follow a.
1405 // b opens a different file in project 2, a follows b
1406 // b opens a different file in project 1, a cannot follow b
1407 // b shares the project, a joins the project and follows b
1408 deterministic.forbid_parking();
1409 let mut server = TestServer::start(&deterministic).await;
1410 let client_a = server.create_client(cx_a, "user_a").await;
1411 let client_b = server.create_client(cx_b, "user_b").await;
1412 cx_a.update(editor::init);
1413 cx_b.update(editor::init);
1414
1415 client_a
1416 .fs()
1417 .insert_tree(
1418 "/a",
1419 json!({
1420 "w.rs": "",
1421 "x.rs": "",
1422 }),
1423 )
1424 .await;
1425
1426 client_b
1427 .fs()
1428 .insert_tree(
1429 "/b",
1430 json!({
1431 "y.rs": "",
1432 "z.rs": "",
1433 }),
1434 )
1435 .await;
1436
1437 server
1438 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1439 .await;
1440 let active_call_a = cx_a.read(ActiveCall::global);
1441 let active_call_b = cx_b.read(ActiveCall::global);
1442
1443 let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
1444 let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
1445
1446 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1447 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1448
1449 cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
1450 cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
1451
1452 active_call_a
1453 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1454 .await
1455 .unwrap();
1456
1457 active_call_a
1458 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1459 .await
1460 .unwrap();
1461 active_call_b
1462 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1463 .await
1464 .unwrap();
1465
1466 workspace_a
1467 .update(cx_a, |workspace, cx| {
1468 workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
1469 })
1470 .await
1471 .unwrap();
1472
1473 deterministic.run_until_parked();
1474 assert_eq!(visible_push_notifications(cx_b).len(), 1);
1475
1476 workspace_b.update(cx_b, |workspace, cx| {
1477 workspace
1478 .follow(client_a.peer_id().unwrap(), cx)
1479 .unwrap()
1480 .detach()
1481 });
1482
1483 deterministic.run_until_parked();
1484 let workspace_b_project_a = cx_b
1485 .windows()
1486 .iter()
1487 .max_by_key(|window| window.id())
1488 .unwrap()
1489 .downcast::<Workspace>()
1490 .unwrap()
1491 .root(cx_b);
1492
1493 // assert that b is following a in project a in w.rs
1494 workspace_b_project_a.update(cx_b, |workspace, cx| {
1495 assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
1496 assert_eq!(
1497 client_a.peer_id(),
1498 workspace.leader_for_pane(workspace.active_pane())
1499 );
1500 let item = workspace.active_item(cx).unwrap();
1501 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs"));
1502 });
1503
1504 // TODO: in app code, this would be done by the collab_ui.
1505 active_call_b
1506 .update(cx_b, |call, cx| {
1507 let project = workspace_b_project_a.read(cx).project().clone();
1508 call.set_location(Some(&project), cx)
1509 })
1510 .await
1511 .unwrap();
1512
1513 // assert that there are no share notifications open
1514 assert_eq!(visible_push_notifications(cx_b).len(), 0);
1515
1516 // b moves to x.rs in a's project, and a follows
1517 workspace_b_project_a
1518 .update(cx_b, |workspace, cx| {
1519 workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
1520 })
1521 .await
1522 .unwrap();
1523
1524 deterministic.run_until_parked();
1525 workspace_b_project_a.update(cx_b, |workspace, cx| {
1526 let item = workspace.active_item(cx).unwrap();
1527 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
1528 });
1529
1530 workspace_a.update(cx_a, |workspace, cx| {
1531 workspace
1532 .follow(client_b.peer_id().unwrap(), cx)
1533 .unwrap()
1534 .detach()
1535 });
1536
1537 deterministic.run_until_parked();
1538 workspace_a.update(cx_a, |workspace, cx| {
1539 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1540 assert_eq!(
1541 client_b.peer_id(),
1542 workspace.leader_for_pane(workspace.active_pane())
1543 );
1544 let item = workspace.active_pane().read(cx).active_item().unwrap();
1545 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
1546 });
1547
1548 // b moves to y.rs in b's project, a is still following but can't yet see
1549 workspace_b
1550 .update(cx_b, |workspace, cx| {
1551 workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
1552 })
1553 .await
1554 .unwrap();
1555
1556 // TODO: in app code, this would be done by the collab_ui.
1557 active_call_b
1558 .update(cx_b, |call, cx| {
1559 let project = workspace_b.read(cx).project().clone();
1560 call.set_location(Some(&project), cx)
1561 })
1562 .await
1563 .unwrap();
1564
1565 let project_b_id = active_call_b
1566 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1567 .await
1568 .unwrap();
1569
1570 deterministic.run_until_parked();
1571 assert_eq!(visible_push_notifications(cx_a).len(), 1);
1572 cx_a.update(|cx| {
1573 workspace::join_remote_project(
1574 project_b_id,
1575 client_b.user_id().unwrap(),
1576 client_a.app_state.clone(),
1577 cx,
1578 )
1579 })
1580 .await
1581 .unwrap();
1582
1583 deterministic.run_until_parked();
1584
1585 assert_eq!(visible_push_notifications(cx_a).len(), 0);
1586 let workspace_a_project_b = cx_a
1587 .windows()
1588 .iter()
1589 .max_by_key(|window| window.id())
1590 .unwrap()
1591 .downcast::<Workspace>()
1592 .unwrap()
1593 .root(cx_a);
1594
1595 workspace_a_project_b.update(cx_a, |workspace, cx| {
1596 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1597 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
1598 assert_eq!(
1599 client_b.peer_id(),
1600 workspace.leader_for_pane(workspace.active_pane())
1601 );
1602 let item = workspace.active_item(cx).unwrap();
1603 assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
1604 });
1605}
1606
1607#[gpui::test]
1608async fn test_following_into_excluded_file(
1609 deterministic: Arc<Deterministic>,
1610 mut cx_a: &mut TestAppContext,
1611 mut cx_b: &mut TestAppContext,
1612) {
1613 deterministic.forbid_parking();
1614
1615 let mut server = TestServer::start(&deterministic).await;
1616 let client_a = server.create_client(cx_a, "user_a").await;
1617 let client_b = server.create_client(cx_b, "user_b").await;
1618 for cx in [&mut cx_a, &mut cx_b] {
1619 cx.update(|cx| {
1620 cx.update_global::<SettingsStore, _, _>(|store, cx| {
1621 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
1622 project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
1623 });
1624 });
1625 });
1626 }
1627 server
1628 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1629 .await;
1630 let active_call_a = cx_a.read(ActiveCall::global);
1631 let active_call_b = cx_b.read(ActiveCall::global);
1632
1633 cx_a.update(editor::init);
1634 cx_b.update(editor::init);
1635
1636 client_a
1637 .fs()
1638 .insert_tree(
1639 "/a",
1640 json!({
1641 ".git": {
1642 "COMMIT_EDITMSG": "write your commit message here",
1643 },
1644 "1.txt": "one\none\none",
1645 "2.txt": "two\ntwo\ntwo",
1646 "3.txt": "three\nthree\nthree",
1647 }),
1648 )
1649 .await;
1650 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1651 active_call_a
1652 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1653 .await
1654 .unwrap();
1655
1656 let project_id = active_call_a
1657 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1658 .await
1659 .unwrap();
1660 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1661 active_call_b
1662 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1663 .await
1664 .unwrap();
1665
1666 let window_a = client_a.build_workspace(&project_a, cx_a);
1667 let workspace_a = window_a.root(cx_a);
1668 let peer_id_a = client_a.peer_id().unwrap();
1669 let window_b = client_b.build_workspace(&project_b, cx_b);
1670 let workspace_b = window_b.root(cx_b);
1671
1672 // Client A opens editors for a regular file and an excluded file.
1673 let editor_for_regular = workspace_a
1674 .update(cx_a, |workspace, cx| {
1675 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
1676 })
1677 .await
1678 .unwrap()
1679 .downcast::<Editor>()
1680 .unwrap();
1681 let editor_for_excluded_a = workspace_a
1682 .update(cx_a, |workspace, cx| {
1683 workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
1684 })
1685 .await
1686 .unwrap()
1687 .downcast::<Editor>()
1688 .unwrap();
1689
1690 // Client A updates their selections in those editors
1691 editor_for_regular.update(cx_a, |editor, cx| {
1692 editor.handle_input("a", cx);
1693 editor.handle_input("b", cx);
1694 editor.handle_input("c", cx);
1695 editor.select_left(&Default::default(), cx);
1696 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
1697 });
1698 editor_for_excluded_a.update(cx_a, |editor, cx| {
1699 editor.select_all(&Default::default(), cx);
1700 editor.handle_input("new commit message", cx);
1701 editor.select_left(&Default::default(), cx);
1702 assert_eq!(editor.selections.ranges(cx), vec![18..17]);
1703 });
1704
1705 // When client B starts following client A, currently visible file is replicated
1706 workspace_b
1707 .update(cx_b, |workspace, cx| {
1708 workspace.follow(peer_id_a, cx).unwrap()
1709 })
1710 .await
1711 .unwrap();
1712
1713 let editor_for_excluded_b = workspace_b.read_with(cx_b, |workspace, cx| {
1714 workspace
1715 .active_item(cx)
1716 .unwrap()
1717 .downcast::<Editor>()
1718 .unwrap()
1719 });
1720 assert_eq!(
1721 cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
1722 Some((worktree_id, ".git/COMMIT_EDITMSG").into())
1723 );
1724 assert_eq!(
1725 editor_for_excluded_b.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
1726 vec![18..17]
1727 );
1728
1729 // Changes from B to the excluded file are replicated in A's editor
1730 editor_for_excluded_b.update(cx_b, |editor, cx| {
1731 editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
1732 });
1733 deterministic.run_until_parked();
1734 editor_for_excluded_a.update(cx_a, |editor, cx| {
1735 assert_eq!(
1736 editor.text(cx),
1737 "new commit messag\nCo-Authored-By: B <b@b.b>"
1738 );
1739 });
1740}
1741
1742fn visible_push_notifications(
1743 cx: &mut TestAppContext,
1744) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
1745 let mut ret = Vec::new();
1746 for window in cx.windows() {
1747 window.read_with(cx, |window| {
1748 if let Some(handle) = window
1749 .root_view()
1750 .clone()
1751 .downcast::<ProjectSharedNotification>()
1752 {
1753 ret.push(handle)
1754 }
1755 });
1756 }
1757 ret
1758}
1759
1760#[derive(Debug, PartialEq, Eq)]
1761struct PaneSummary {
1762 active: bool,
1763 leader: Option<PeerId>,
1764 items: Vec<(bool, String)>,
1765}
1766
1767fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
1768 cx.read(|cx| {
1769 let active_call = ActiveCall::global(cx).read(cx);
1770 let peer_id = active_call.client().peer_id();
1771 let room = active_call.room().unwrap().read(cx);
1772 let mut result = room
1773 .remote_participants()
1774 .values()
1775 .map(|participant| participant.peer_id)
1776 .chain(peer_id)
1777 .filter_map(|peer_id| {
1778 let followers = room.followers_for(peer_id, project_id);
1779 if followers.is_empty() {
1780 None
1781 } else {
1782 Some((peer_id, followers.to_vec()))
1783 }
1784 })
1785 .collect::<Vec<_>>();
1786 result.sort_by_key(|e| e.0);
1787 result
1788 })
1789}
1790
1791fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
1792 workspace.read_with(cx, |workspace, cx| {
1793 let active_pane = workspace.active_pane();
1794 workspace
1795 .panes()
1796 .iter()
1797 .map(|pane| {
1798 let leader = workspace.leader_for_pane(pane);
1799 let active = pane == active_pane;
1800 let pane = pane.read(cx);
1801 let active_ix = pane.active_item_index();
1802 PaneSummary {
1803 active,
1804 leader,
1805 items: pane
1806 .items()
1807 .enumerate()
1808 .map(|(ix, item)| {
1809 (
1810 ix == active_ix,
1811 item.tab_description(0, cx)
1812 .map_or(String::new(), |s| s.to_string()),
1813 )
1814 })
1815 .collect(),
1816 }
1817 })
1818 .collect()
1819 })
1820}