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