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