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