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