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