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