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