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