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