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