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