1use crate::{
2 rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
3 tests::{TestClient, TestServer},
4};
5use call::{room, ActiveCall, ParticipantLocation, Room};
6use client::{User, RECEIVE_TIMEOUT};
7use collections::HashSet;
8use editor::{
9 ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo,
10 Rename, ToOffset, ToggleCodeActions, Undo,
11};
12use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions};
13use futures::StreamExt as _;
14use gpui::{
15 executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext,
16 ViewHandle,
17};
18use language::{
19 tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
20 LanguageConfig, OffsetRangeExt, Point, Rope,
21};
22use live_kit_client::MacOSDisplay;
23use project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath};
24use rand::prelude::*;
25use serde_json::json;
26use settings::{Formatter, Settings};
27use std::{
28 cell::{Cell, RefCell},
29 env, future, mem,
30 path::{Path, PathBuf},
31 rc::Rc,
32 sync::Arc,
33};
34use unindent::Unindent as _;
35use workspace::{
36 item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, ToggleFollow, Workspace,
37};
38
39#[ctor::ctor]
40fn init_logger() {
41 if std::env::var("RUST_LOG").is_ok() {
42 env_logger::init();
43 }
44}
45
46#[gpui::test(iterations = 10)]
47async fn test_basic_calls(
48 deterministic: Arc<Deterministic>,
49 cx_a: &mut TestAppContext,
50 cx_b: &mut TestAppContext,
51 cx_b2: &mut TestAppContext,
52 cx_c: &mut TestAppContext,
53) {
54 deterministic.forbid_parking();
55 let mut server = TestServer::start(&deterministic).await;
56
57 let client_a = server.create_client(cx_a, "user_a").await;
58 let client_b = server.create_client(cx_b, "user_b").await;
59 let client_c = server.create_client(cx_c, "user_c").await;
60 server
61 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
62 .await;
63
64 let active_call_a = cx_a.read(ActiveCall::global);
65 let active_call_b = cx_b.read(ActiveCall::global);
66 let active_call_c = cx_c.read(ActiveCall::global);
67
68 // Call user B from client A.
69 active_call_a
70 .update(cx_a, |call, cx| {
71 call.invite(client_b.user_id().unwrap(), None, cx)
72 })
73 .await
74 .unwrap();
75 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
76 deterministic.run_until_parked();
77 assert_eq!(
78 room_participants(&room_a, cx_a),
79 RoomParticipants {
80 remote: Default::default(),
81 pending: vec!["user_b".to_string()]
82 }
83 );
84
85 // User B receives the call.
86 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
87 let call_b = incoming_call_b.next().await.unwrap().unwrap();
88 assert_eq!(call_b.calling_user.github_login, "user_a");
89
90 // User B connects via another client and also receives a ring on the newly-connected client.
91 let _client_b2 = server.create_client(cx_b2, "user_b").await;
92 let active_call_b2 = cx_b2.read(ActiveCall::global);
93 let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
94 deterministic.run_until_parked();
95 let call_b2 = incoming_call_b2.next().await.unwrap().unwrap();
96 assert_eq!(call_b2.calling_user.github_login, "user_a");
97
98 // User B joins the room using the first client.
99 active_call_b
100 .update(cx_b, |call, cx| call.accept_incoming(cx))
101 .await
102 .unwrap();
103 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
104 assert!(incoming_call_b.next().await.unwrap().is_none());
105
106 deterministic.run_until_parked();
107 assert_eq!(
108 room_participants(&room_a, cx_a),
109 RoomParticipants {
110 remote: vec!["user_b".to_string()],
111 pending: Default::default()
112 }
113 );
114 assert_eq!(
115 room_participants(&room_b, cx_b),
116 RoomParticipants {
117 remote: vec!["user_a".to_string()],
118 pending: Default::default()
119 }
120 );
121
122 // Call user C from client B.
123 let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
124 active_call_b
125 .update(cx_b, |call, cx| {
126 call.invite(client_c.user_id().unwrap(), None, cx)
127 })
128 .await
129 .unwrap();
130
131 deterministic.run_until_parked();
132 assert_eq!(
133 room_participants(&room_a, cx_a),
134 RoomParticipants {
135 remote: vec!["user_b".to_string()],
136 pending: vec!["user_c".to_string()]
137 }
138 );
139 assert_eq!(
140 room_participants(&room_b, cx_b),
141 RoomParticipants {
142 remote: vec!["user_a".to_string()],
143 pending: vec!["user_c".to_string()]
144 }
145 );
146
147 // User C receives the call, but declines it.
148 let call_c = incoming_call_c.next().await.unwrap().unwrap();
149 assert_eq!(call_c.calling_user.github_login, "user_b");
150 active_call_c.update(cx_c, |call, _| call.decline_incoming().unwrap());
151 assert!(incoming_call_c.next().await.unwrap().is_none());
152
153 deterministic.run_until_parked();
154 assert_eq!(
155 room_participants(&room_a, cx_a),
156 RoomParticipants {
157 remote: vec!["user_b".to_string()],
158 pending: Default::default()
159 }
160 );
161 assert_eq!(
162 room_participants(&room_b, cx_b),
163 RoomParticipants {
164 remote: vec!["user_a".to_string()],
165 pending: Default::default()
166 }
167 );
168
169 // Call user C again from user A.
170 active_call_a
171 .update(cx_a, |call, cx| {
172 call.invite(client_c.user_id().unwrap(), None, cx)
173 })
174 .await
175 .unwrap();
176
177 deterministic.run_until_parked();
178 assert_eq!(
179 room_participants(&room_a, cx_a),
180 RoomParticipants {
181 remote: vec!["user_b".to_string()],
182 pending: vec!["user_c".to_string()]
183 }
184 );
185 assert_eq!(
186 room_participants(&room_b, cx_b),
187 RoomParticipants {
188 remote: vec!["user_a".to_string()],
189 pending: vec!["user_c".to_string()]
190 }
191 );
192
193 // User C accepts the call.
194 let call_c = incoming_call_c.next().await.unwrap().unwrap();
195 assert_eq!(call_c.calling_user.github_login, "user_a");
196 active_call_c
197 .update(cx_c, |call, cx| call.accept_incoming(cx))
198 .await
199 .unwrap();
200 assert!(incoming_call_c.next().await.unwrap().is_none());
201 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
202
203 deterministic.run_until_parked();
204 assert_eq!(
205 room_participants(&room_a, cx_a),
206 RoomParticipants {
207 remote: vec!["user_b".to_string(), "user_c".to_string()],
208 pending: Default::default()
209 }
210 );
211 assert_eq!(
212 room_participants(&room_b, cx_b),
213 RoomParticipants {
214 remote: vec!["user_a".to_string(), "user_c".to_string()],
215 pending: Default::default()
216 }
217 );
218 assert_eq!(
219 room_participants(&room_c, cx_c),
220 RoomParticipants {
221 remote: vec!["user_a".to_string(), "user_b".to_string()],
222 pending: Default::default()
223 }
224 );
225
226 // User A shares their screen
227 let display = MacOSDisplay::new();
228 let events_b = active_call_events(cx_b);
229 let events_c = active_call_events(cx_c);
230 active_call_a
231 .update(cx_a, |call, cx| {
232 call.room().unwrap().update(cx, |room, cx| {
233 room.set_display_sources(vec![display.clone()]);
234 room.share_screen(cx)
235 })
236 })
237 .await
238 .unwrap();
239
240 deterministic.run_until_parked();
241
242 // User B observes the remote screen sharing track.
243 assert_eq!(events_b.borrow().len(), 1);
244 let event_b = events_b.borrow().first().unwrap().clone();
245 if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
246 assert_eq!(participant_id, client_a.peer_id().unwrap());
247 room_b.read_with(cx_b, |room, _| {
248 assert_eq!(
249 room.remote_participants()[&client_a.user_id().unwrap()]
250 .tracks
251 .len(),
252 1
253 );
254 });
255 } else {
256 panic!("unexpected event")
257 }
258
259 // User C observes the remote screen sharing track.
260 assert_eq!(events_c.borrow().len(), 1);
261 let event_c = events_c.borrow().first().unwrap().clone();
262 if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
263 assert_eq!(participant_id, client_a.peer_id().unwrap());
264 room_c.read_with(cx_c, |room, _| {
265 assert_eq!(
266 room.remote_participants()[&client_a.user_id().unwrap()]
267 .tracks
268 .len(),
269 1
270 );
271 });
272 } else {
273 panic!("unexpected event")
274 }
275
276 // User A leaves the room.
277 active_call_a.update(cx_a, |call, cx| {
278 call.hang_up(cx).unwrap();
279 assert!(call.room().is_none());
280 });
281 deterministic.run_until_parked();
282 assert_eq!(
283 room_participants(&room_a, cx_a),
284 RoomParticipants {
285 remote: Default::default(),
286 pending: Default::default()
287 }
288 );
289 assert_eq!(
290 room_participants(&room_b, cx_b),
291 RoomParticipants {
292 remote: vec!["user_c".to_string()],
293 pending: Default::default()
294 }
295 );
296 assert_eq!(
297 room_participants(&room_c, cx_c),
298 RoomParticipants {
299 remote: vec!["user_b".to_string()],
300 pending: Default::default()
301 }
302 );
303
304 // User B gets disconnected from the LiveKit server, which causes them
305 // to automatically leave the room. User C leaves the room as well because
306 // nobody else is in there.
307 server
308 .test_live_kit_server
309 .disconnect_client(client_b.user_id().unwrap().to_string())
310 .await;
311 deterministic.run_until_parked();
312 active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
313 active_call_c.read_with(cx_c, |call, _| assert!(call.room().is_none()));
314 assert_eq!(
315 room_participants(&room_a, cx_a),
316 RoomParticipants {
317 remote: Default::default(),
318 pending: Default::default()
319 }
320 );
321 assert_eq!(
322 room_participants(&room_b, cx_b),
323 RoomParticipants {
324 remote: Default::default(),
325 pending: Default::default()
326 }
327 );
328 assert_eq!(
329 room_participants(&room_c, cx_c),
330 RoomParticipants {
331 remote: Default::default(),
332 pending: Default::default()
333 }
334 );
335}
336
337#[gpui::test(iterations = 10)]
338async fn test_calling_multiple_users_simultaneously(
339 deterministic: Arc<Deterministic>,
340 cx_a: &mut TestAppContext,
341 cx_b: &mut TestAppContext,
342 cx_c: &mut TestAppContext,
343 cx_d: &mut TestAppContext,
344) {
345 deterministic.forbid_parking();
346 let mut server = TestServer::start(&deterministic).await;
347
348 let client_a = server.create_client(cx_a, "user_a").await;
349 let client_b = server.create_client(cx_b, "user_b").await;
350 let client_c = server.create_client(cx_c, "user_c").await;
351 let client_d = server.create_client(cx_d, "user_d").await;
352 server
353 .make_contacts(&mut [
354 (&client_a, cx_a),
355 (&client_b, cx_b),
356 (&client_c, cx_c),
357 (&client_d, cx_d),
358 ])
359 .await;
360
361 let active_call_a = cx_a.read(ActiveCall::global);
362 let active_call_b = cx_b.read(ActiveCall::global);
363 let active_call_c = cx_c.read(ActiveCall::global);
364 let active_call_d = cx_d.read(ActiveCall::global);
365
366 // Simultaneously call user B and user C from client A.
367 let b_invite = active_call_a.update(cx_a, |call, cx| {
368 call.invite(client_b.user_id().unwrap(), None, cx)
369 });
370 let c_invite = active_call_a.update(cx_a, |call, cx| {
371 call.invite(client_c.user_id().unwrap(), None, cx)
372 });
373 b_invite.await.unwrap();
374 c_invite.await.unwrap();
375
376 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
377 deterministic.run_until_parked();
378 assert_eq!(
379 room_participants(&room_a, cx_a),
380 RoomParticipants {
381 remote: Default::default(),
382 pending: vec!["user_b".to_string(), "user_c".to_string()]
383 }
384 );
385
386 // Call client D from client A.
387 active_call_a
388 .update(cx_a, |call, cx| {
389 call.invite(client_d.user_id().unwrap(), None, cx)
390 })
391 .await
392 .unwrap();
393 deterministic.run_until_parked();
394 assert_eq!(
395 room_participants(&room_a, cx_a),
396 RoomParticipants {
397 remote: Default::default(),
398 pending: vec![
399 "user_b".to_string(),
400 "user_c".to_string(),
401 "user_d".to_string()
402 ]
403 }
404 );
405
406 // Accept the call on all clients simultaneously.
407 let accept_b = active_call_b.update(cx_b, |call, cx| call.accept_incoming(cx));
408 let accept_c = active_call_c.update(cx_c, |call, cx| call.accept_incoming(cx));
409 let accept_d = active_call_d.update(cx_d, |call, cx| call.accept_incoming(cx));
410 accept_b.await.unwrap();
411 accept_c.await.unwrap();
412 accept_d.await.unwrap();
413
414 deterministic.run_until_parked();
415
416 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
417 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
418 let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
419 assert_eq!(
420 room_participants(&room_a, cx_a),
421 RoomParticipants {
422 remote: vec![
423 "user_b".to_string(),
424 "user_c".to_string(),
425 "user_d".to_string(),
426 ],
427 pending: Default::default()
428 }
429 );
430 assert_eq!(
431 room_participants(&room_b, cx_b),
432 RoomParticipants {
433 remote: vec![
434 "user_a".to_string(),
435 "user_c".to_string(),
436 "user_d".to_string(),
437 ],
438 pending: Default::default()
439 }
440 );
441 assert_eq!(
442 room_participants(&room_c, cx_c),
443 RoomParticipants {
444 remote: vec![
445 "user_a".to_string(),
446 "user_b".to_string(),
447 "user_d".to_string(),
448 ],
449 pending: Default::default()
450 }
451 );
452 assert_eq!(
453 room_participants(&room_d, cx_d),
454 RoomParticipants {
455 remote: vec![
456 "user_a".to_string(),
457 "user_b".to_string(),
458 "user_c".to_string(),
459 ],
460 pending: Default::default()
461 }
462 );
463}
464
465#[gpui::test(iterations = 10)]
466async fn test_room_uniqueness(
467 deterministic: Arc<Deterministic>,
468 cx_a: &mut TestAppContext,
469 cx_a2: &mut TestAppContext,
470 cx_b: &mut TestAppContext,
471 cx_b2: &mut TestAppContext,
472 cx_c: &mut TestAppContext,
473) {
474 deterministic.forbid_parking();
475 let mut server = TestServer::start(&deterministic).await;
476 let client_a = server.create_client(cx_a, "user_a").await;
477 let _client_a2 = server.create_client(cx_a2, "user_a").await;
478 let client_b = server.create_client(cx_b, "user_b").await;
479 let _client_b2 = server.create_client(cx_b2, "user_b").await;
480 let client_c = server.create_client(cx_c, "user_c").await;
481 server
482 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
483 .await;
484
485 let active_call_a = cx_a.read(ActiveCall::global);
486 let active_call_a2 = cx_a2.read(ActiveCall::global);
487 let active_call_b = cx_b.read(ActiveCall::global);
488 let active_call_b2 = cx_b2.read(ActiveCall::global);
489 let active_call_c = cx_c.read(ActiveCall::global);
490
491 // Call user B from client A.
492 active_call_a
493 .update(cx_a, |call, cx| {
494 call.invite(client_b.user_id().unwrap(), None, cx)
495 })
496 .await
497 .unwrap();
498
499 // Ensure a new room can't be created given user A just created one.
500 active_call_a2
501 .update(cx_a2, |call, cx| {
502 call.invite(client_c.user_id().unwrap(), None, cx)
503 })
504 .await
505 .unwrap_err();
506 active_call_a2.read_with(cx_a2, |call, _| assert!(call.room().is_none()));
507
508 // User B receives the call from user A.
509 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
510 let call_b1 = incoming_call_b.next().await.unwrap().unwrap();
511 assert_eq!(call_b1.calling_user.github_login, "user_a");
512
513 // Ensure calling users A and B from client C fails.
514 active_call_c
515 .update(cx_c, |call, cx| {
516 call.invite(client_a.user_id().unwrap(), None, cx)
517 })
518 .await
519 .unwrap_err();
520 active_call_c
521 .update(cx_c, |call, cx| {
522 call.invite(client_b.user_id().unwrap(), None, cx)
523 })
524 .await
525 .unwrap_err();
526
527 // Ensure User B can't create a room while they still have an incoming call.
528 active_call_b2
529 .update(cx_b2, |call, cx| {
530 call.invite(client_c.user_id().unwrap(), None, cx)
531 })
532 .await
533 .unwrap_err();
534 active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none()));
535
536 // User B joins the room and calling them after they've joined still fails.
537 active_call_b
538 .update(cx_b, |call, cx| call.accept_incoming(cx))
539 .await
540 .unwrap();
541 active_call_c
542 .update(cx_c, |call, cx| {
543 call.invite(client_b.user_id().unwrap(), None, cx)
544 })
545 .await
546 .unwrap_err();
547
548 // Ensure User B can't create a room while they belong to another room.
549 active_call_b2
550 .update(cx_b2, |call, cx| {
551 call.invite(client_c.user_id().unwrap(), None, cx)
552 })
553 .await
554 .unwrap_err();
555 active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none()));
556
557 // Client C can successfully call client B after client B leaves the room.
558 active_call_b
559 .update(cx_b, |call, cx| call.hang_up(cx))
560 .unwrap();
561 deterministic.run_until_parked();
562 active_call_c
563 .update(cx_c, |call, cx| {
564 call.invite(client_b.user_id().unwrap(), None, cx)
565 })
566 .await
567 .unwrap();
568 deterministic.run_until_parked();
569 let call_b2 = incoming_call_b.next().await.unwrap().unwrap();
570 assert_eq!(call_b2.calling_user.github_login, "user_c");
571}
572
573#[gpui::test(iterations = 10)]
574async fn test_client_disconnecting_from_room(
575 deterministic: Arc<Deterministic>,
576 cx_a: &mut TestAppContext,
577 cx_b: &mut TestAppContext,
578) {
579 deterministic.forbid_parking();
580 let mut server = TestServer::start(&deterministic).await;
581 let client_a = server.create_client(cx_a, "user_a").await;
582 let client_b = server.create_client(cx_b, "user_b").await;
583 server
584 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
585 .await;
586
587 let active_call_a = cx_a.read(ActiveCall::global);
588 let active_call_b = cx_b.read(ActiveCall::global);
589
590 // Call user B from client A.
591 active_call_a
592 .update(cx_a, |call, cx| {
593 call.invite(client_b.user_id().unwrap(), None, cx)
594 })
595 .await
596 .unwrap();
597 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
598
599 // User B receives the call and joins the room.
600 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
601 incoming_call_b.next().await.unwrap().unwrap();
602 active_call_b
603 .update(cx_b, |call, cx| call.accept_incoming(cx))
604 .await
605 .unwrap();
606 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
607 deterministic.run_until_parked();
608 assert_eq!(
609 room_participants(&room_a, cx_a),
610 RoomParticipants {
611 remote: vec!["user_b".to_string()],
612 pending: Default::default()
613 }
614 );
615 assert_eq!(
616 room_participants(&room_b, cx_b),
617 RoomParticipants {
618 remote: vec!["user_a".to_string()],
619 pending: Default::default()
620 }
621 );
622
623 // User A automatically reconnects to the room upon disconnection.
624 server.disconnect_client(client_a.peer_id().unwrap());
625 deterministic.advance_clock(RECEIVE_TIMEOUT);
626 deterministic.run_until_parked();
627 assert_eq!(
628 room_participants(&room_a, cx_a),
629 RoomParticipants {
630 remote: vec!["user_b".to_string()],
631 pending: Default::default()
632 }
633 );
634 assert_eq!(
635 room_participants(&room_b, cx_b),
636 RoomParticipants {
637 remote: vec!["user_a".to_string()],
638 pending: Default::default()
639 }
640 );
641
642 // When user A disconnects, both client A and B clear their room on the active call.
643 server.forbid_connections();
644 server.disconnect_client(client_a.peer_id().unwrap());
645 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
646 active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
647 active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
648 assert_eq!(
649 room_participants(&room_a, cx_a),
650 RoomParticipants {
651 remote: Default::default(),
652 pending: Default::default()
653 }
654 );
655 assert_eq!(
656 room_participants(&room_b, cx_b),
657 RoomParticipants {
658 remote: Default::default(),
659 pending: Default::default()
660 }
661 );
662
663 // Allow user A to reconnect to the server.
664 server.allow_connections();
665 deterministic.advance_clock(RECEIVE_TIMEOUT);
666
667 // Call user B again from client A.
668 active_call_a
669 .update(cx_a, |call, cx| {
670 call.invite(client_b.user_id().unwrap(), None, cx)
671 })
672 .await
673 .unwrap();
674 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
675
676 // User B receives the call and joins the room.
677 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
678 incoming_call_b.next().await.unwrap().unwrap();
679 active_call_b
680 .update(cx_b, |call, cx| call.accept_incoming(cx))
681 .await
682 .unwrap();
683 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
684 deterministic.run_until_parked();
685 assert_eq!(
686 room_participants(&room_a, cx_a),
687 RoomParticipants {
688 remote: vec!["user_b".to_string()],
689 pending: Default::default()
690 }
691 );
692 assert_eq!(
693 room_participants(&room_b, cx_b),
694 RoomParticipants {
695 remote: vec!["user_a".to_string()],
696 pending: Default::default()
697 }
698 );
699
700 // User B gets disconnected from the LiveKit server, which causes it
701 // to automatically leave the room.
702 server
703 .test_live_kit_server
704 .disconnect_client(client_b.user_id().unwrap().to_string())
705 .await;
706 deterministic.run_until_parked();
707 active_call_a.update(cx_a, |call, _| assert!(call.room().is_none()));
708 active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
709 assert_eq!(
710 room_participants(&room_a, cx_a),
711 RoomParticipants {
712 remote: Default::default(),
713 pending: Default::default()
714 }
715 );
716 assert_eq!(
717 room_participants(&room_b, cx_b),
718 RoomParticipants {
719 remote: Default::default(),
720 pending: Default::default()
721 }
722 );
723}
724
725#[gpui::test(iterations = 10)]
726async fn test_server_restarts(
727 deterministic: Arc<Deterministic>,
728 cx_a: &mut TestAppContext,
729 cx_b: &mut TestAppContext,
730 cx_c: &mut TestAppContext,
731 cx_d: &mut TestAppContext,
732) {
733 deterministic.forbid_parking();
734 let mut server = TestServer::start(&deterministic).await;
735 let client_a = server.create_client(cx_a, "user_a").await;
736 let client_b = server.create_client(cx_b, "user_b").await;
737 let client_c = server.create_client(cx_c, "user_c").await;
738 let client_d = server.create_client(cx_d, "user_d").await;
739 server
740 .make_contacts(&mut [
741 (&client_a, cx_a),
742 (&client_b, cx_b),
743 (&client_c, cx_c),
744 (&client_d, cx_d),
745 ])
746 .await;
747
748 let active_call_a = cx_a.read(ActiveCall::global);
749 let active_call_b = cx_b.read(ActiveCall::global);
750 let active_call_c = cx_c.read(ActiveCall::global);
751 let active_call_d = cx_d.read(ActiveCall::global);
752
753 // User A calls users B, C, and D.
754 active_call_a
755 .update(cx_a, |call, cx| {
756 call.invite(client_b.user_id().unwrap(), None, cx)
757 })
758 .await
759 .unwrap();
760 active_call_a
761 .update(cx_a, |call, cx| {
762 call.invite(client_c.user_id().unwrap(), None, cx)
763 })
764 .await
765 .unwrap();
766 active_call_a
767 .update(cx_a, |call, cx| {
768 call.invite(client_d.user_id().unwrap(), None, cx)
769 })
770 .await
771 .unwrap();
772 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
773
774 // User B receives the call and joins the room.
775 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
776 assert!(incoming_call_b.next().await.unwrap().is_some());
777 active_call_b
778 .update(cx_b, |call, cx| call.accept_incoming(cx))
779 .await
780 .unwrap();
781 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
782
783 // User C receives the call and joins the room.
784 let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
785 assert!(incoming_call_c.next().await.unwrap().is_some());
786 active_call_c
787 .update(cx_c, |call, cx| call.accept_incoming(cx))
788 .await
789 .unwrap();
790 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
791
792 // User D receives the call but doesn't join the room yet.
793 let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming());
794 assert!(incoming_call_d.next().await.unwrap().is_some());
795
796 deterministic.run_until_parked();
797 assert_eq!(
798 room_participants(&room_a, cx_a),
799 RoomParticipants {
800 remote: vec!["user_b".to_string(), "user_c".to_string()],
801 pending: vec!["user_d".to_string()]
802 }
803 );
804 assert_eq!(
805 room_participants(&room_b, cx_b),
806 RoomParticipants {
807 remote: vec!["user_a".to_string(), "user_c".to_string()],
808 pending: vec!["user_d".to_string()]
809 }
810 );
811 assert_eq!(
812 room_participants(&room_c, cx_c),
813 RoomParticipants {
814 remote: vec!["user_a".to_string(), "user_b".to_string()],
815 pending: vec!["user_d".to_string()]
816 }
817 );
818
819 // The server is torn down.
820 server.reset().await;
821
822 // Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
823 client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
824 deterministic.advance_clock(RECEIVE_TIMEOUT);
825 assert_eq!(
826 room_participants(&room_a, cx_a),
827 RoomParticipants {
828 remote: vec!["user_b".to_string(), "user_c".to_string()],
829 pending: vec!["user_d".to_string()]
830 }
831 );
832 assert_eq!(
833 room_participants(&room_b, cx_b),
834 RoomParticipants {
835 remote: vec!["user_a".to_string(), "user_c".to_string()],
836 pending: vec!["user_d".to_string()]
837 }
838 );
839 assert_eq!(
840 room_participants(&room_c, cx_c),
841 RoomParticipants {
842 remote: vec![],
843 pending: vec![]
844 }
845 );
846
847 // User D is notified again of the incoming call and accepts it.
848 assert!(incoming_call_d.next().await.unwrap().is_some());
849 active_call_d
850 .update(cx_d, |call, cx| call.accept_incoming(cx))
851 .await
852 .unwrap();
853 deterministic.run_until_parked();
854 let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
855 assert_eq!(
856 room_participants(&room_a, cx_a),
857 RoomParticipants {
858 remote: vec![
859 "user_b".to_string(),
860 "user_c".to_string(),
861 "user_d".to_string(),
862 ],
863 pending: vec![]
864 }
865 );
866 assert_eq!(
867 room_participants(&room_b, cx_b),
868 RoomParticipants {
869 remote: vec![
870 "user_a".to_string(),
871 "user_c".to_string(),
872 "user_d".to_string(),
873 ],
874 pending: vec![]
875 }
876 );
877 assert_eq!(
878 room_participants(&room_c, cx_c),
879 RoomParticipants {
880 remote: vec![],
881 pending: vec![]
882 }
883 );
884 assert_eq!(
885 room_participants(&room_d, cx_d),
886 RoomParticipants {
887 remote: vec![
888 "user_a".to_string(),
889 "user_b".to_string(),
890 "user_c".to_string(),
891 ],
892 pending: vec![]
893 }
894 );
895
896 // The server finishes restarting, cleaning up stale connections.
897 server.start().await.unwrap();
898 deterministic.advance_clock(CLEANUP_TIMEOUT);
899 assert_eq!(
900 room_participants(&room_a, cx_a),
901 RoomParticipants {
902 remote: vec!["user_b".to_string(), "user_d".to_string()],
903 pending: vec![]
904 }
905 );
906 assert_eq!(
907 room_participants(&room_b, cx_b),
908 RoomParticipants {
909 remote: vec!["user_a".to_string(), "user_d".to_string()],
910 pending: vec![]
911 }
912 );
913 assert_eq!(
914 room_participants(&room_c, cx_c),
915 RoomParticipants {
916 remote: vec![],
917 pending: vec![]
918 }
919 );
920 assert_eq!(
921 room_participants(&room_d, cx_d),
922 RoomParticipants {
923 remote: vec!["user_a".to_string(), "user_b".to_string()],
924 pending: vec![]
925 }
926 );
927
928 // User D hangs up.
929 active_call_d
930 .update(cx_d, |call, cx| call.hang_up(cx))
931 .unwrap();
932 deterministic.run_until_parked();
933 assert_eq!(
934 room_participants(&room_a, cx_a),
935 RoomParticipants {
936 remote: vec!["user_b".to_string()],
937 pending: vec![]
938 }
939 );
940 assert_eq!(
941 room_participants(&room_b, cx_b),
942 RoomParticipants {
943 remote: vec!["user_a".to_string()],
944 pending: vec![]
945 }
946 );
947 assert_eq!(
948 room_participants(&room_c, cx_c),
949 RoomParticipants {
950 remote: vec![],
951 pending: vec![]
952 }
953 );
954 assert_eq!(
955 room_participants(&room_d, cx_d),
956 RoomParticipants {
957 remote: vec![],
958 pending: vec![]
959 }
960 );
961
962 // User B calls user D again.
963 active_call_b
964 .update(cx_b, |call, cx| {
965 call.invite(client_d.user_id().unwrap(), None, cx)
966 })
967 .await
968 .unwrap();
969
970 // User D receives the call but doesn't join the room yet.
971 let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming());
972 assert!(incoming_call_d.next().await.unwrap().is_some());
973 deterministic.run_until_parked();
974 assert_eq!(
975 room_participants(&room_a, cx_a),
976 RoomParticipants {
977 remote: vec!["user_b".to_string()],
978 pending: vec!["user_d".to_string()]
979 }
980 );
981 assert_eq!(
982 room_participants(&room_b, cx_b),
983 RoomParticipants {
984 remote: vec!["user_a".to_string()],
985 pending: vec!["user_d".to_string()]
986 }
987 );
988
989 // The server is torn down.
990 server.reset().await;
991
992 // Users A and B have troubles reconnecting, so they leave the room.
993 client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
994 client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
995 client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
996 deterministic.advance_clock(RECEIVE_TIMEOUT);
997 assert_eq!(
998 room_participants(&room_a, cx_a),
999 RoomParticipants {
1000 remote: vec![],
1001 pending: vec![]
1002 }
1003 );
1004 assert_eq!(
1005 room_participants(&room_b, cx_b),
1006 RoomParticipants {
1007 remote: vec![],
1008 pending: vec![]
1009 }
1010 );
1011
1012 // User D is notified again of the incoming call but doesn't accept it.
1013 assert!(incoming_call_d.next().await.unwrap().is_some());
1014
1015 // The server finishes restarting, cleaning up stale connections and canceling the
1016 // call to user D because the room has become empty.
1017 server.start().await.unwrap();
1018 deterministic.advance_clock(CLEANUP_TIMEOUT);
1019 assert!(incoming_call_d.next().await.unwrap().is_none());
1020}
1021
1022#[gpui::test(iterations = 10)]
1023async fn test_calls_on_multiple_connections(
1024 deterministic: Arc<Deterministic>,
1025 cx_a: &mut TestAppContext,
1026 cx_b1: &mut TestAppContext,
1027 cx_b2: &mut TestAppContext,
1028) {
1029 deterministic.forbid_parking();
1030 let mut server = TestServer::start(&deterministic).await;
1031 let client_a = server.create_client(cx_a, "user_a").await;
1032 let client_b1 = server.create_client(cx_b1, "user_b").await;
1033 let client_b2 = server.create_client(cx_b2, "user_b").await;
1034 server
1035 .make_contacts(&mut [(&client_a, cx_a), (&client_b1, cx_b1)])
1036 .await;
1037
1038 let active_call_a = cx_a.read(ActiveCall::global);
1039 let active_call_b1 = cx_b1.read(ActiveCall::global);
1040 let active_call_b2 = cx_b2.read(ActiveCall::global);
1041 let mut incoming_call_b1 = active_call_b1.read_with(cx_b1, |call, _| call.incoming());
1042 let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
1043 assert!(incoming_call_b1.next().await.unwrap().is_none());
1044 assert!(incoming_call_b2.next().await.unwrap().is_none());
1045
1046 // Call user B from client A, ensuring both clients for user B ring.
1047 active_call_a
1048 .update(cx_a, |call, cx| {
1049 call.invite(client_b1.user_id().unwrap(), None, cx)
1050 })
1051 .await
1052 .unwrap();
1053 deterministic.run_until_parked();
1054 assert!(incoming_call_b1.next().await.unwrap().is_some());
1055 assert!(incoming_call_b2.next().await.unwrap().is_some());
1056
1057 // User B declines the call on one of the two connections, causing both connections
1058 // to stop ringing.
1059 active_call_b2.update(cx_b2, |call, _| call.decline_incoming().unwrap());
1060 deterministic.run_until_parked();
1061 assert!(incoming_call_b1.next().await.unwrap().is_none());
1062 assert!(incoming_call_b2.next().await.unwrap().is_none());
1063
1064 // Call user B again from client A.
1065 active_call_a
1066 .update(cx_a, |call, cx| {
1067 call.invite(client_b1.user_id().unwrap(), None, cx)
1068 })
1069 .await
1070 .unwrap();
1071 deterministic.run_until_parked();
1072 assert!(incoming_call_b1.next().await.unwrap().is_some());
1073 assert!(incoming_call_b2.next().await.unwrap().is_some());
1074
1075 // User B accepts the call on one of the two connections, causing both connections
1076 // to stop ringing.
1077 active_call_b2
1078 .update(cx_b2, |call, cx| call.accept_incoming(cx))
1079 .await
1080 .unwrap();
1081 deterministic.run_until_parked();
1082 assert!(incoming_call_b1.next().await.unwrap().is_none());
1083 assert!(incoming_call_b2.next().await.unwrap().is_none());
1084
1085 // User B disconnects the client that is not on the call. Everything should be fine.
1086 client_b1.disconnect(&cx_b1.to_async()).unwrap();
1087 deterministic.advance_clock(RECEIVE_TIMEOUT);
1088 client_b1
1089 .authenticate_and_connect(false, &cx_b1.to_async())
1090 .await
1091 .unwrap();
1092
1093 // User B hangs up, and user A calls them again.
1094 active_call_b2.update(cx_b2, |call, cx| call.hang_up(cx).unwrap());
1095 deterministic.run_until_parked();
1096 active_call_a
1097 .update(cx_a, |call, cx| {
1098 call.invite(client_b1.user_id().unwrap(), None, cx)
1099 })
1100 .await
1101 .unwrap();
1102 deterministic.run_until_parked();
1103 assert!(incoming_call_b1.next().await.unwrap().is_some());
1104 assert!(incoming_call_b2.next().await.unwrap().is_some());
1105
1106 // User A cancels the call, causing both connections to stop ringing.
1107 active_call_a
1108 .update(cx_a, |call, cx| {
1109 call.cancel_invite(client_b1.user_id().unwrap(), cx)
1110 })
1111 .await
1112 .unwrap();
1113 deterministic.run_until_parked();
1114 assert!(incoming_call_b1.next().await.unwrap().is_none());
1115 assert!(incoming_call_b2.next().await.unwrap().is_none());
1116
1117 // User A calls user B again.
1118 active_call_a
1119 .update(cx_a, |call, cx| {
1120 call.invite(client_b1.user_id().unwrap(), None, cx)
1121 })
1122 .await
1123 .unwrap();
1124 deterministic.run_until_parked();
1125 assert!(incoming_call_b1.next().await.unwrap().is_some());
1126 assert!(incoming_call_b2.next().await.unwrap().is_some());
1127
1128 // User A hangs up, causing both connections to stop ringing.
1129 active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
1130 deterministic.run_until_parked();
1131 assert!(incoming_call_b1.next().await.unwrap().is_none());
1132 assert!(incoming_call_b2.next().await.unwrap().is_none());
1133
1134 // User A calls user B again.
1135 active_call_a
1136 .update(cx_a, |call, cx| {
1137 call.invite(client_b1.user_id().unwrap(), None, cx)
1138 })
1139 .await
1140 .unwrap();
1141 deterministic.run_until_parked();
1142 assert!(incoming_call_b1.next().await.unwrap().is_some());
1143 assert!(incoming_call_b2.next().await.unwrap().is_some());
1144
1145 // User A disconnects, causing both connections to stop ringing.
1146 server.forbid_connections();
1147 server.disconnect_client(client_a.peer_id().unwrap());
1148 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1149 assert!(incoming_call_b1.next().await.unwrap().is_none());
1150 assert!(incoming_call_b2.next().await.unwrap().is_none());
1151
1152 // User A reconnects automatically, then calls user B again.
1153 server.allow_connections();
1154 deterministic.advance_clock(RECEIVE_TIMEOUT);
1155 active_call_a
1156 .update(cx_a, |call, cx| {
1157 call.invite(client_b1.user_id().unwrap(), None, cx)
1158 })
1159 .await
1160 .unwrap();
1161 deterministic.run_until_parked();
1162 assert!(incoming_call_b1.next().await.unwrap().is_some());
1163 assert!(incoming_call_b2.next().await.unwrap().is_some());
1164
1165 // User B disconnects all clients, causing user A to no longer see a pending call for them.
1166 server.forbid_connections();
1167 server.disconnect_client(client_b1.peer_id().unwrap());
1168 server.disconnect_client(client_b2.peer_id().unwrap());
1169 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1170 active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
1171}
1172
1173#[gpui::test(iterations = 10)]
1174async fn test_share_project(
1175 deterministic: Arc<Deterministic>,
1176 cx_a: &mut TestAppContext,
1177 cx_b: &mut TestAppContext,
1178 cx_c: &mut TestAppContext,
1179) {
1180 deterministic.forbid_parking();
1181 let (_, window_b) = cx_b.add_window(|_| EmptyView);
1182 let mut server = TestServer::start(&deterministic).await;
1183 let client_a = server.create_client(cx_a, "user_a").await;
1184 let client_b = server.create_client(cx_b, "user_b").await;
1185 let client_c = server.create_client(cx_c, "user_c").await;
1186 server
1187 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1188 .await;
1189 let active_call_a = cx_a.read(ActiveCall::global);
1190 let active_call_b = cx_b.read(ActiveCall::global);
1191 let active_call_c = cx_c.read(ActiveCall::global);
1192
1193 client_a
1194 .fs
1195 .insert_tree(
1196 "/a",
1197 json!({
1198 ".gitignore": "ignored-dir",
1199 "a.txt": "a-contents",
1200 "b.txt": "b-contents",
1201 "ignored-dir": {
1202 "c.txt": "",
1203 "d.txt": "",
1204 }
1205 }),
1206 )
1207 .await;
1208
1209 // Invite client B to collaborate on a project
1210 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1211 active_call_a
1212 .update(cx_a, |call, cx| {
1213 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1214 })
1215 .await
1216 .unwrap();
1217
1218 // Join that project as client B
1219 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1220 deterministic.run_until_parked();
1221 let call = incoming_call_b.borrow().clone().unwrap();
1222 assert_eq!(call.calling_user.github_login, "user_a");
1223 let initial_project = call.initial_project.unwrap();
1224 active_call_b
1225 .update(cx_b, |call, cx| call.accept_incoming(cx))
1226 .await
1227 .unwrap();
1228 let client_b_peer_id = client_b.peer_id().unwrap();
1229 let project_b = client_b
1230 .build_remote_project(initial_project.id, cx_b)
1231 .await;
1232 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1233
1234 deterministic.run_until_parked();
1235 project_a.read_with(cx_a, |project, _| {
1236 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1237 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1238 });
1239 project_b.read_with(cx_b, |project, cx| {
1240 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1241 assert_eq!(
1242 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1243 [
1244 Path::new(".gitignore"),
1245 Path::new("a.txt"),
1246 Path::new("b.txt"),
1247 Path::new("ignored-dir"),
1248 Path::new("ignored-dir/c.txt"),
1249 Path::new("ignored-dir/d.txt"),
1250 ]
1251 );
1252 });
1253
1254 // Open the same file as client B and client A.
1255 let buffer_b = project_b
1256 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1257 .await
1258 .unwrap();
1259 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1260 project_a.read_with(cx_a, |project, cx| {
1261 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1262 });
1263 let buffer_a = project_a
1264 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1265 .await
1266 .unwrap();
1267
1268 let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
1269
1270 // Client A sees client B's selection
1271 deterministic.run_until_parked();
1272 buffer_a.read_with(cx_a, |buffer, _| {
1273 buffer
1274 .snapshot()
1275 .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1276 .count()
1277 == 1
1278 });
1279
1280 // Edit the buffer as client B and see that edit as client A.
1281 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1282 deterministic.run_until_parked();
1283 buffer_a.read_with(cx_a, |buffer, _| {
1284 assert_eq!(buffer.text(), "ok, b-contents")
1285 });
1286
1287 // Client B can invite client C on a project shared by client A.
1288 active_call_b
1289 .update(cx_b, |call, cx| {
1290 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1291 })
1292 .await
1293 .unwrap();
1294
1295 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1296 deterministic.run_until_parked();
1297 let call = incoming_call_c.borrow().clone().unwrap();
1298 assert_eq!(call.calling_user.github_login, "user_b");
1299 let initial_project = call.initial_project.unwrap();
1300 active_call_c
1301 .update(cx_c, |call, cx| call.accept_incoming(cx))
1302 .await
1303 .unwrap();
1304 let _project_c = client_c
1305 .build_remote_project(initial_project.id, cx_c)
1306 .await;
1307
1308 // Client B closes the editor, and client A sees client B's selections removed.
1309 cx_b.update(move |_| drop(editor_b));
1310 deterministic.run_until_parked();
1311 buffer_a.read_with(cx_a, |buffer, _| {
1312 buffer
1313 .snapshot()
1314 .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1315 .count()
1316 == 0
1317 });
1318}
1319
1320#[gpui::test(iterations = 10)]
1321async fn test_unshare_project(
1322 deterministic: Arc<Deterministic>,
1323 cx_a: &mut TestAppContext,
1324 cx_b: &mut TestAppContext,
1325 cx_c: &mut TestAppContext,
1326) {
1327 deterministic.forbid_parking();
1328 let mut server = TestServer::start(&deterministic).await;
1329 let client_a = server.create_client(cx_a, "user_a").await;
1330 let client_b = server.create_client(cx_b, "user_b").await;
1331 let client_c = server.create_client(cx_c, "user_c").await;
1332 server
1333 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1334 .await;
1335
1336 let active_call_a = cx_a.read(ActiveCall::global);
1337 let active_call_b = cx_b.read(ActiveCall::global);
1338
1339 client_a
1340 .fs
1341 .insert_tree(
1342 "/a",
1343 json!({
1344 "a.txt": "a-contents",
1345 "b.txt": "b-contents",
1346 }),
1347 )
1348 .await;
1349
1350 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1351 let project_id = active_call_a
1352 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1353 .await
1354 .unwrap();
1355 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
1356 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1357 deterministic.run_until_parked();
1358 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
1359
1360 project_b
1361 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1362 .await
1363 .unwrap();
1364
1365 // When client B leaves the room, the project becomes read-only.
1366 active_call_b.update(cx_b, |call, cx| call.hang_up(cx).unwrap());
1367 deterministic.run_until_parked();
1368 assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
1369
1370 // Client C opens the project.
1371 let project_c = client_c.build_remote_project(project_id, cx_c).await;
1372
1373 // When client A unshares the project, client C's project becomes read-only.
1374 project_a
1375 .update(cx_a, |project, cx| project.unshare(cx))
1376 .unwrap();
1377 deterministic.run_until_parked();
1378 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
1379 assert!(project_c.read_with(cx_c, |project, _| project.is_read_only()));
1380
1381 // Client C can open the project again after client A re-shares.
1382 let project_id = active_call_a
1383 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1384 .await
1385 .unwrap();
1386 let project_c2 = client_c.build_remote_project(project_id, cx_c).await;
1387 deterministic.run_until_parked();
1388 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
1389 project_c2
1390 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1391 .await
1392 .unwrap();
1393
1394 // When client A (the host) leaves the room, the project gets unshared and guests are notified.
1395 active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
1396 deterministic.run_until_parked();
1397 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
1398 project_c2.read_with(cx_c, |project, _| {
1399 assert!(project.is_read_only());
1400 assert!(project.collaborators().is_empty());
1401 });
1402}
1403
1404#[gpui::test(iterations = 10)]
1405async fn test_host_disconnect(
1406 deterministic: Arc<Deterministic>,
1407 cx_a: &mut TestAppContext,
1408 cx_b: &mut TestAppContext,
1409 cx_c: &mut TestAppContext,
1410) {
1411 cx_b.update(editor::init);
1412 deterministic.forbid_parking();
1413 let mut server = TestServer::start(&deterministic).await;
1414 let client_a = server.create_client(cx_a, "user_a").await;
1415 let client_b = server.create_client(cx_b, "user_b").await;
1416 let client_c = server.create_client(cx_c, "user_c").await;
1417 server
1418 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1419 .await;
1420
1421 client_a
1422 .fs
1423 .insert_tree(
1424 "/a",
1425 json!({
1426 "a.txt": "a-contents",
1427 "b.txt": "b-contents",
1428 }),
1429 )
1430 .await;
1431
1432 let active_call_a = cx_a.read(ActiveCall::global);
1433 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1434 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
1435 let project_id = active_call_a
1436 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1437 .await
1438 .unwrap();
1439
1440 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1441 deterministic.run_until_parked();
1442 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
1443
1444 let (_, workspace_b) = cx_b.add_window(|cx| {
1445 Workspace::new(
1446 Default::default(),
1447 0,
1448 project_b.clone(),
1449 |_, _| unimplemented!(),
1450 cx,
1451 )
1452 });
1453 let editor_b = workspace_b
1454 .update(cx_b, |workspace, cx| {
1455 workspace.open_path((worktree_id, "b.txt"), None, true, cx)
1456 })
1457 .await
1458 .unwrap()
1459 .downcast::<Editor>()
1460 .unwrap();
1461 cx_b.read(|cx| {
1462 assert_eq!(
1463 cx.focused_view_id(workspace_b.window_id()),
1464 Some(editor_b.id())
1465 );
1466 });
1467 editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
1468 assert!(cx_b.is_window_edited(workspace_b.window_id()));
1469
1470 // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
1471 server.forbid_connections();
1472 server.disconnect_client(client_a.peer_id().unwrap());
1473 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1474 project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
1475 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
1476 project_b.read_with(cx_b, |project, _| project.is_read_only());
1477 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
1478
1479 // Ensure client B's edited state is reset and that the whole window is blurred.
1480 cx_b.read(|cx| {
1481 assert_eq!(cx.focused_view_id(workspace_b.window_id()), None);
1482 });
1483 assert!(!cx_b.is_window_edited(workspace_b.window_id()));
1484
1485 // Ensure client B is not prompted to save edits when closing window after disconnecting.
1486 let can_close = workspace_b
1487 .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx))
1488 .await
1489 .unwrap();
1490 assert!(can_close);
1491
1492 // Allow client A to reconnect to the server.
1493 server.allow_connections();
1494 deterministic.advance_clock(RECEIVE_TIMEOUT);
1495
1496 // Client B calls client A again after they reconnected.
1497 let active_call_b = cx_b.read(ActiveCall::global);
1498 active_call_b
1499 .update(cx_b, |call, cx| {
1500 call.invite(client_a.user_id().unwrap(), None, cx)
1501 })
1502 .await
1503 .unwrap();
1504 deterministic.run_until_parked();
1505 active_call_a
1506 .update(cx_a, |call, cx| call.accept_incoming(cx))
1507 .await
1508 .unwrap();
1509
1510 active_call_a
1511 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1512 .await
1513 .unwrap();
1514
1515 // Drop client A's connection again. We should still unshare it successfully.
1516 server.forbid_connections();
1517 server.disconnect_client(client_a.peer_id().unwrap());
1518 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1519 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
1520}
1521
1522#[gpui::test(iterations = 10)]
1523async fn test_project_reconnect(
1524 deterministic: Arc<Deterministic>,
1525 cx_a: &mut TestAppContext,
1526 cx_b: &mut TestAppContext,
1527) {
1528 cx_b.update(editor::init);
1529 deterministic.forbid_parking();
1530 let mut server = TestServer::start(&deterministic).await;
1531 let client_a = server.create_client(cx_a, "user_a").await;
1532 let client_b = server.create_client(cx_b, "user_b").await;
1533 server
1534 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1535 .await;
1536
1537 client_a
1538 .fs
1539 .insert_tree(
1540 "/root-1",
1541 json!({
1542 "dir1": {
1543 "a.txt": "a",
1544 "b.txt": "b",
1545 "subdir1": {
1546 "c.txt": "c",
1547 "d.txt": "d",
1548 "e.txt": "e",
1549 }
1550 },
1551 "dir2": {
1552 "v.txt": "v",
1553 },
1554 "dir3": {
1555 "w.txt": "w",
1556 "x.txt": "x",
1557 "y.txt": "y",
1558 },
1559 "dir4": {
1560 "z.txt": "z",
1561 },
1562 }),
1563 )
1564 .await;
1565 client_a
1566 .fs
1567 .insert_tree(
1568 "/root-2",
1569 json!({
1570 "2.txt": "2",
1571 }),
1572 )
1573 .await;
1574 client_a
1575 .fs
1576 .insert_tree(
1577 "/root-3",
1578 json!({
1579 "3.txt": "3",
1580 }),
1581 )
1582 .await;
1583
1584 let active_call_a = cx_a.read(ActiveCall::global);
1585 let (project_a1, _) = client_a.build_local_project("/root-1/dir1", cx_a).await;
1586 let (project_a2, _) = client_a.build_local_project("/root-2", cx_a).await;
1587 let (project_a3, _) = client_a.build_local_project("/root-3", cx_a).await;
1588 let worktree_a1 =
1589 project_a1.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
1590 let project1_id = active_call_a
1591 .update(cx_a, |call, cx| call.share_project(project_a1.clone(), cx))
1592 .await
1593 .unwrap();
1594 let project2_id = active_call_a
1595 .update(cx_a, |call, cx| call.share_project(project_a2.clone(), cx))
1596 .await
1597 .unwrap();
1598 let project3_id = active_call_a
1599 .update(cx_a, |call, cx| call.share_project(project_a3.clone(), cx))
1600 .await
1601 .unwrap();
1602
1603 let project_b1 = client_b.build_remote_project(project1_id, cx_b).await;
1604 let project_b2 = client_b.build_remote_project(project2_id, cx_b).await;
1605 let project_b3 = client_b.build_remote_project(project3_id, cx_b).await;
1606 deterministic.run_until_parked();
1607
1608 let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| {
1609 assert!(worktree.as_local().unwrap().is_shared());
1610 worktree.id()
1611 });
1612 let (worktree_a2, _) = project_a1
1613 .update(cx_a, |p, cx| {
1614 p.find_or_create_local_worktree("/root-1/dir2", true, cx)
1615 })
1616 .await
1617 .unwrap();
1618 worktree_a2
1619 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
1620 .await;
1621 let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| {
1622 assert!(tree.as_local().unwrap().is_shared());
1623 tree.id()
1624 });
1625 deterministic.run_until_parked();
1626 project_b1.read_with(cx_b, |project, cx| {
1627 assert!(project.worktree_for_id(worktree2_id, cx).is_some())
1628 });
1629
1630 let buffer_a1 = project_a1
1631 .update(cx_a, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
1632 .await
1633 .unwrap();
1634 let buffer_b1 = project_b1
1635 .update(cx_b, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
1636 .await
1637 .unwrap();
1638
1639 // Drop client A's connection.
1640 server.forbid_connections();
1641 server.disconnect_client(client_a.peer_id().unwrap());
1642 deterministic.advance_clock(RECEIVE_TIMEOUT);
1643 project_a1.read_with(cx_a, |project, _| {
1644 assert!(project.is_shared());
1645 assert_eq!(project.collaborators().len(), 1);
1646 });
1647 project_b1.read_with(cx_b, |project, _| {
1648 assert!(!project.is_read_only());
1649 assert_eq!(project.collaborators().len(), 1);
1650 });
1651 worktree_a1.read_with(cx_a, |tree, _| {
1652 assert!(tree.as_local().unwrap().is_shared())
1653 });
1654
1655 // While client A is disconnected, add and remove files from client A's project.
1656 client_a
1657 .fs
1658 .insert_tree(
1659 "/root-1/dir1/subdir2",
1660 json!({
1661 "f.txt": "f-contents",
1662 "g.txt": "g-contents",
1663 "h.txt": "h-contents",
1664 "i.txt": "i-contents",
1665 }),
1666 )
1667 .await;
1668 client_a
1669 .fs
1670 .remove_dir(
1671 "/root-1/dir1/subdir1".as_ref(),
1672 RemoveOptions {
1673 recursive: true,
1674 ..Default::default()
1675 },
1676 )
1677 .await
1678 .unwrap();
1679
1680 // While client A is disconnected, add and remove worktrees from client A's project.
1681 project_a1
1682 .update(cx_a, |project, cx| {
1683 project.remove_worktree(worktree2_id, cx)
1684 })
1685 .await;
1686 let (worktree_a3, _) = project_a1
1687 .update(cx_a, |p, cx| {
1688 p.find_or_create_local_worktree("/root-1/dir3", true, cx)
1689 })
1690 .await
1691 .unwrap();
1692 worktree_a3
1693 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
1694 .await;
1695 let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
1696 assert!(!tree.as_local().unwrap().is_shared());
1697 tree.id()
1698 });
1699 deterministic.run_until_parked();
1700
1701 // While client A is disconnected, close project 2
1702 cx_a.update(|_| drop(project_a2));
1703
1704 // While client A is disconnected, mutate a buffer on both the host and the guest.
1705 buffer_a1.update(cx_a, |buf, cx| buf.edit([(0..0, "W")], None, cx));
1706 buffer_b1.update(cx_b, |buf, cx| buf.edit([(1..1, "Z")], None, cx));
1707 deterministic.run_until_parked();
1708
1709 // Client A reconnects. Their project is re-shared, and client B re-joins it.
1710 server.allow_connections();
1711 client_a
1712 .authenticate_and_connect(false, &cx_a.to_async())
1713 .await
1714 .unwrap();
1715 deterministic.run_until_parked();
1716 project_a1.read_with(cx_a, |project, cx| {
1717 assert!(project.is_shared());
1718 assert!(worktree_a1.read(cx).as_local().unwrap().is_shared());
1719 assert_eq!(
1720 worktree_a1
1721 .read(cx)
1722 .snapshot()
1723 .paths()
1724 .map(|p| p.to_str().unwrap())
1725 .collect::<Vec<_>>(),
1726 vec![
1727 "a.txt",
1728 "b.txt",
1729 "subdir1",
1730 "subdir1/c.txt",
1731 "subdir1/d.txt",
1732 "subdir1/e.txt",
1733 "subdir2",
1734 "subdir2/f.txt",
1735 "subdir2/g.txt",
1736 "subdir2/h.txt",
1737 "subdir2/i.txt"
1738 ]
1739 );
1740 assert!(worktree_a3.read(cx).as_local().unwrap().is_shared());
1741 assert_eq!(
1742 worktree_a3
1743 .read(cx)
1744 .snapshot()
1745 .paths()
1746 .map(|p| p.to_str().unwrap())
1747 .collect::<Vec<_>>(),
1748 vec!["w.txt", "x.txt", "y.txt"]
1749 );
1750 });
1751 project_b1.read_with(cx_b, |project, cx| {
1752 assert!(!project.is_read_only());
1753 assert_eq!(
1754 project
1755 .worktree_for_id(worktree1_id, cx)
1756 .unwrap()
1757 .read(cx)
1758 .snapshot()
1759 .paths()
1760 .map(|p| p.to_str().unwrap())
1761 .collect::<Vec<_>>(),
1762 vec![
1763 "a.txt",
1764 "b.txt",
1765 "subdir1",
1766 "subdir1/c.txt",
1767 "subdir1/d.txt",
1768 "subdir1/e.txt",
1769 "subdir2",
1770 "subdir2/f.txt",
1771 "subdir2/g.txt",
1772 "subdir2/h.txt",
1773 "subdir2/i.txt"
1774 ]
1775 );
1776 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
1777 assert_eq!(
1778 project
1779 .worktree_for_id(worktree3_id, cx)
1780 .unwrap()
1781 .read(cx)
1782 .snapshot()
1783 .paths()
1784 .map(|p| p.to_str().unwrap())
1785 .collect::<Vec<_>>(),
1786 vec!["w.txt", "x.txt", "y.txt"]
1787 );
1788 });
1789 project_b2.read_with(cx_b, |project, _| assert!(project.is_read_only()));
1790 project_b3.read_with(cx_b, |project, _| assert!(!project.is_read_only()));
1791 buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
1792 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
1793
1794 // Drop client B's connection.
1795 server.forbid_connections();
1796 server.disconnect_client(client_b.peer_id().unwrap());
1797 deterministic.advance_clock(RECEIVE_TIMEOUT);
1798
1799 // While client B is disconnected, add and remove files from client A's project
1800 client_a
1801 .fs
1802 .insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into())
1803 .await;
1804 client_a
1805 .fs
1806 .remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default())
1807 .await
1808 .unwrap();
1809
1810 // While client B is disconnected, add and remove worktrees from client A's project.
1811 let (worktree_a4, _) = project_a1
1812 .update(cx_a, |p, cx| {
1813 p.find_or_create_local_worktree("/root-1/dir4", true, cx)
1814 })
1815 .await
1816 .unwrap();
1817 worktree_a4
1818 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
1819 .await;
1820 let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| {
1821 assert!(tree.as_local().unwrap().is_shared());
1822 tree.id()
1823 });
1824 project_a1
1825 .update(cx_a, |project, cx| {
1826 project.remove_worktree(worktree3_id, cx)
1827 })
1828 .await;
1829 deterministic.run_until_parked();
1830
1831 // While client B is disconnected, mutate a buffer on both the host and the guest.
1832 buffer_a1.update(cx_a, |buf, cx| buf.edit([(1..1, "X")], None, cx));
1833 buffer_b1.update(cx_b, |buf, cx| buf.edit([(2..2, "Y")], None, cx));
1834 deterministic.run_until_parked();
1835
1836 // While disconnected, close project 3
1837 cx_a.update(|_| drop(project_a3));
1838
1839 // Client B reconnects. They re-join the room and the remaining shared project.
1840 server.allow_connections();
1841 client_b
1842 .authenticate_and_connect(false, &cx_b.to_async())
1843 .await
1844 .unwrap();
1845 deterministic.run_until_parked();
1846 project_b1.read_with(cx_b, |project, cx| {
1847 assert!(!project.is_read_only());
1848 assert_eq!(
1849 project
1850 .worktree_for_id(worktree1_id, cx)
1851 .unwrap()
1852 .read(cx)
1853 .snapshot()
1854 .paths()
1855 .map(|p| p.to_str().unwrap())
1856 .collect::<Vec<_>>(),
1857 vec![
1858 "a.txt",
1859 "b.txt",
1860 "subdir1",
1861 "subdir1/c.txt",
1862 "subdir1/d.txt",
1863 "subdir1/e.txt",
1864 "subdir2",
1865 "subdir2/f.txt",
1866 "subdir2/g.txt",
1867 "subdir2/h.txt",
1868 "subdir2/j.txt"
1869 ]
1870 );
1871 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
1872 assert_eq!(
1873 project
1874 .worktree_for_id(worktree4_id, cx)
1875 .unwrap()
1876 .read(cx)
1877 .snapshot()
1878 .paths()
1879 .map(|p| p.to_str().unwrap())
1880 .collect::<Vec<_>>(),
1881 vec!["z.txt"]
1882 );
1883 });
1884 project_b3.read_with(cx_b, |project, _| assert!(project.is_read_only()));
1885 buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
1886 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
1887}
1888
1889#[gpui::test(iterations = 10)]
1890async fn test_active_call_events(
1891 deterministic: Arc<Deterministic>,
1892 cx_a: &mut TestAppContext,
1893 cx_b: &mut TestAppContext,
1894) {
1895 deterministic.forbid_parking();
1896 let mut server = TestServer::start(&deterministic).await;
1897 let client_a = server.create_client(cx_a, "user_a").await;
1898 let client_b = server.create_client(cx_b, "user_b").await;
1899 client_a.fs.insert_tree("/a", json!({})).await;
1900 client_b.fs.insert_tree("/b", json!({})).await;
1901
1902 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1903 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1904
1905 server
1906 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1907 .await;
1908 let active_call_a = cx_a.read(ActiveCall::global);
1909 let active_call_b = cx_b.read(ActiveCall::global);
1910
1911 let events_a = active_call_events(cx_a);
1912 let events_b = active_call_events(cx_b);
1913
1914 let project_a_id = active_call_a
1915 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1916 .await
1917 .unwrap();
1918 deterministic.run_until_parked();
1919 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1920 assert_eq!(
1921 mem::take(&mut *events_b.borrow_mut()),
1922 vec![room::Event::RemoteProjectShared {
1923 owner: Arc::new(User {
1924 id: client_a.user_id().unwrap(),
1925 github_login: "user_a".to_string(),
1926 avatar: None,
1927 }),
1928 project_id: project_a_id,
1929 worktree_root_names: vec!["a".to_string()],
1930 }]
1931 );
1932
1933 let project_b_id = active_call_b
1934 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1935 .await
1936 .unwrap();
1937 deterministic.run_until_parked();
1938 assert_eq!(
1939 mem::take(&mut *events_a.borrow_mut()),
1940 vec![room::Event::RemoteProjectShared {
1941 owner: Arc::new(User {
1942 id: client_b.user_id().unwrap(),
1943 github_login: "user_b".to_string(),
1944 avatar: None,
1945 }),
1946 project_id: project_b_id,
1947 worktree_root_names: vec!["b".to_string()]
1948 }]
1949 );
1950 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1951
1952 // Sharing a project twice is idempotent.
1953 let project_b_id_2 = active_call_b
1954 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1955 .await
1956 .unwrap();
1957 assert_eq!(project_b_id_2, project_b_id);
1958 deterministic.run_until_parked();
1959 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1960 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1961}
1962
1963fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
1964 let events = Rc::new(RefCell::new(Vec::new()));
1965 let active_call = cx.read(ActiveCall::global);
1966 cx.update({
1967 let events = events.clone();
1968 |cx| {
1969 cx.subscribe(&active_call, move |_, event, _| {
1970 events.borrow_mut().push(event.clone())
1971 })
1972 .detach()
1973 }
1974 });
1975 events
1976}
1977
1978#[gpui::test(iterations = 10)]
1979async fn test_room_location(
1980 deterministic: Arc<Deterministic>,
1981 cx_a: &mut TestAppContext,
1982 cx_b: &mut TestAppContext,
1983) {
1984 deterministic.forbid_parking();
1985 let mut server = TestServer::start(&deterministic).await;
1986 let client_a = server.create_client(cx_a, "user_a").await;
1987 let client_b = server.create_client(cx_b, "user_b").await;
1988 client_a.fs.insert_tree("/a", json!({})).await;
1989 client_b.fs.insert_tree("/b", json!({})).await;
1990
1991 let active_call_a = cx_a.read(ActiveCall::global);
1992 let active_call_b = cx_b.read(ActiveCall::global);
1993
1994 let a_notified = Rc::new(Cell::new(false));
1995 cx_a.update({
1996 let notified = a_notified.clone();
1997 |cx| {
1998 cx.observe(&active_call_a, move |_, _| notified.set(true))
1999 .detach()
2000 }
2001 });
2002
2003 let b_notified = Rc::new(Cell::new(false));
2004 cx_b.update({
2005 let b_notified = b_notified.clone();
2006 |cx| {
2007 cx.observe(&active_call_b, move |_, _| b_notified.set(true))
2008 .detach()
2009 }
2010 });
2011
2012 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
2013 active_call_a
2014 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2015 .await
2016 .unwrap();
2017 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
2018
2019 server
2020 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2021 .await;
2022 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
2023 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
2024 deterministic.run_until_parked();
2025 assert!(a_notified.take());
2026 assert_eq!(
2027 participant_locations(&room_a, cx_a),
2028 vec![("user_b".to_string(), ParticipantLocation::External)]
2029 );
2030 assert!(b_notified.take());
2031 assert_eq!(
2032 participant_locations(&room_b, cx_b),
2033 vec![("user_a".to_string(), ParticipantLocation::UnsharedProject)]
2034 );
2035
2036 let project_a_id = active_call_a
2037 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2038 .await
2039 .unwrap();
2040 deterministic.run_until_parked();
2041 assert!(a_notified.take());
2042 assert_eq!(
2043 participant_locations(&room_a, cx_a),
2044 vec![("user_b".to_string(), ParticipantLocation::External)]
2045 );
2046 assert!(b_notified.take());
2047 assert_eq!(
2048 participant_locations(&room_b, cx_b),
2049 vec![(
2050 "user_a".to_string(),
2051 ParticipantLocation::SharedProject {
2052 project_id: project_a_id
2053 }
2054 )]
2055 );
2056
2057 let project_b_id = active_call_b
2058 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
2059 .await
2060 .unwrap();
2061 deterministic.run_until_parked();
2062 assert!(a_notified.take());
2063 assert_eq!(
2064 participant_locations(&room_a, cx_a),
2065 vec![("user_b".to_string(), ParticipantLocation::External)]
2066 );
2067 assert!(b_notified.take());
2068 assert_eq!(
2069 participant_locations(&room_b, cx_b),
2070 vec![(
2071 "user_a".to_string(),
2072 ParticipantLocation::SharedProject {
2073 project_id: project_a_id
2074 }
2075 )]
2076 );
2077
2078 active_call_b
2079 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2080 .await
2081 .unwrap();
2082 deterministic.run_until_parked();
2083 assert!(a_notified.take());
2084 assert_eq!(
2085 participant_locations(&room_a, cx_a),
2086 vec![(
2087 "user_b".to_string(),
2088 ParticipantLocation::SharedProject {
2089 project_id: project_b_id
2090 }
2091 )]
2092 );
2093 assert!(b_notified.take());
2094 assert_eq!(
2095 participant_locations(&room_b, cx_b),
2096 vec![(
2097 "user_a".to_string(),
2098 ParticipantLocation::SharedProject {
2099 project_id: project_a_id
2100 }
2101 )]
2102 );
2103
2104 active_call_b
2105 .update(cx_b, |call, cx| call.set_location(None, cx))
2106 .await
2107 .unwrap();
2108 deterministic.run_until_parked();
2109 assert!(a_notified.take());
2110 assert_eq!(
2111 participant_locations(&room_a, cx_a),
2112 vec![("user_b".to_string(), ParticipantLocation::External)]
2113 );
2114 assert!(b_notified.take());
2115 assert_eq!(
2116 participant_locations(&room_b, cx_b),
2117 vec![(
2118 "user_a".to_string(),
2119 ParticipantLocation::SharedProject {
2120 project_id: project_a_id
2121 }
2122 )]
2123 );
2124
2125 fn participant_locations(
2126 room: &ModelHandle<Room>,
2127 cx: &TestAppContext,
2128 ) -> Vec<(String, ParticipantLocation)> {
2129 room.read_with(cx, |room, _| {
2130 room.remote_participants()
2131 .values()
2132 .map(|participant| {
2133 (
2134 participant.user.github_login.to_string(),
2135 participant.location,
2136 )
2137 })
2138 .collect()
2139 })
2140 }
2141}
2142
2143#[gpui::test(iterations = 10)]
2144async fn test_propagate_saves_and_fs_changes(
2145 deterministic: Arc<Deterministic>,
2146 cx_a: &mut TestAppContext,
2147 cx_b: &mut TestAppContext,
2148 cx_c: &mut TestAppContext,
2149) {
2150 deterministic.forbid_parking();
2151 let mut server = TestServer::start(&deterministic).await;
2152 let client_a = server.create_client(cx_a, "user_a").await;
2153 let client_b = server.create_client(cx_b, "user_b").await;
2154 let client_c = server.create_client(cx_c, "user_c").await;
2155
2156 server
2157 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2158 .await;
2159 let active_call_a = cx_a.read(ActiveCall::global);
2160
2161 let rust = Arc::new(Language::new(
2162 LanguageConfig {
2163 name: "Rust".into(),
2164 path_suffixes: vec!["rs".to_string()],
2165 ..Default::default()
2166 },
2167 Some(tree_sitter_rust::language()),
2168 ));
2169 let javascript = Arc::new(Language::new(
2170 LanguageConfig {
2171 name: "JavaScript".into(),
2172 path_suffixes: vec!["js".to_string()],
2173 ..Default::default()
2174 },
2175 Some(tree_sitter_rust::language()),
2176 ));
2177 for client in [&client_a, &client_b, &client_c] {
2178 client.language_registry.add(rust.clone());
2179 client.language_registry.add(javascript.clone());
2180 }
2181
2182 client_a
2183 .fs
2184 .insert_tree(
2185 "/a",
2186 json!({
2187 "file1.rs": "",
2188 "file2": ""
2189 }),
2190 )
2191 .await;
2192 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2193 let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap());
2194 let project_id = active_call_a
2195 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2196 .await
2197 .unwrap();
2198
2199 // Join that worktree as clients B and C.
2200 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2201 let project_c = client_c.build_remote_project(project_id, cx_c).await;
2202 let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
2203 let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap());
2204
2205 // Open and edit a buffer as both guests B and C.
2206 let buffer_b = project_b
2207 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2208 .await
2209 .unwrap();
2210 let buffer_c = project_c
2211 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2212 .await
2213 .unwrap();
2214 buffer_b.read_with(cx_b, |buffer, _| {
2215 assert_eq!(&*buffer.language().unwrap().name(), "Rust");
2216 });
2217 buffer_c.read_with(cx_c, |buffer, _| {
2218 assert_eq!(&*buffer.language().unwrap().name(), "Rust");
2219 });
2220 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
2221 buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
2222
2223 // Open and edit that buffer as the host.
2224 let buffer_a = project_a
2225 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2226 .await
2227 .unwrap();
2228
2229 deterministic.run_until_parked();
2230 buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, "));
2231 buffer_a.update(cx_a, |buf, cx| {
2232 buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
2233 });
2234
2235 deterministic.run_until_parked();
2236 buffer_a.read_with(cx_a, |buf, _| {
2237 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2238 });
2239 buffer_b.read_with(cx_b, |buf, _| {
2240 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2241 });
2242 buffer_c.read_with(cx_c, |buf, _| {
2243 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2244 });
2245
2246 // Edit the buffer as the host and concurrently save as guest B.
2247 let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx));
2248 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
2249 save_b.await.unwrap();
2250 assert_eq!(
2251 client_a.fs.load("/a/file1.rs".as_ref()).await.unwrap(),
2252 "hi-a, i-am-c, i-am-b, i-am-a"
2253 );
2254
2255 deterministic.run_until_parked();
2256 buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
2257 buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
2258 buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty()));
2259
2260 // Make changes on host's file system, see those changes on guest worktrees.
2261 client_a
2262 .fs
2263 .rename(
2264 "/a/file1.rs".as_ref(),
2265 "/a/file1.js".as_ref(),
2266 Default::default(),
2267 )
2268 .await
2269 .unwrap();
2270 client_a
2271 .fs
2272 .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
2273 .await
2274 .unwrap();
2275 client_a.fs.insert_file("/a/file4", "4".into()).await;
2276 deterministic.run_until_parked();
2277
2278 worktree_a.read_with(cx_a, |tree, _| {
2279 assert_eq!(
2280 tree.paths()
2281 .map(|p| p.to_string_lossy())
2282 .collect::<Vec<_>>(),
2283 ["file1.js", "file3", "file4"]
2284 )
2285 });
2286 worktree_b.read_with(cx_b, |tree, _| {
2287 assert_eq!(
2288 tree.paths()
2289 .map(|p| p.to_string_lossy())
2290 .collect::<Vec<_>>(),
2291 ["file1.js", "file3", "file4"]
2292 )
2293 });
2294 worktree_c.read_with(cx_c, |tree, _| {
2295 assert_eq!(
2296 tree.paths()
2297 .map(|p| p.to_string_lossy())
2298 .collect::<Vec<_>>(),
2299 ["file1.js", "file3", "file4"]
2300 )
2301 });
2302
2303 // Ensure buffer files are updated as well.
2304 buffer_a.read_with(cx_a, |buffer, _| {
2305 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2306 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2307 });
2308 buffer_b.read_with(cx_b, |buffer, _| {
2309 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2310 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2311 });
2312 buffer_c.read_with(cx_c, |buffer, _| {
2313 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2314 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2315 });
2316}
2317
2318#[gpui::test(iterations = 10)]
2319async fn test_git_diff_base_change(
2320 deterministic: Arc<Deterministic>,
2321 cx_a: &mut TestAppContext,
2322 cx_b: &mut TestAppContext,
2323) {
2324 deterministic.forbid_parking();
2325 let mut server = TestServer::start(&deterministic).await;
2326 let client_a = server.create_client(cx_a, "user_a").await;
2327 let client_b = server.create_client(cx_b, "user_b").await;
2328 server
2329 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2330 .await;
2331 let active_call_a = cx_a.read(ActiveCall::global);
2332
2333 client_a
2334 .fs
2335 .insert_tree(
2336 "/dir",
2337 json!({
2338 ".git": {},
2339 "sub": {
2340 ".git": {},
2341 "b.txt": "
2342 one
2343 two
2344 three
2345 ".unindent(),
2346 },
2347 "a.txt": "
2348 one
2349 two
2350 three
2351 ".unindent(),
2352 }),
2353 )
2354 .await;
2355
2356 let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2357 let project_id = active_call_a
2358 .update(cx_a, |call, cx| {
2359 call.share_project(project_local.clone(), cx)
2360 })
2361 .await
2362 .unwrap();
2363
2364 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2365
2366 let diff_base = "
2367 one
2368 three
2369 "
2370 .unindent();
2371
2372 let new_diff_base = "
2373 one
2374 two
2375 "
2376 .unindent();
2377
2378 client_a
2379 .fs
2380 .as_fake()
2381 .set_index_for_repo(
2382 Path::new("/dir/.git"),
2383 &[(Path::new("a.txt"), diff_base.clone())],
2384 )
2385 .await;
2386
2387 // Create the buffer
2388 let buffer_local_a = project_local
2389 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2390 .await
2391 .unwrap();
2392
2393 // Wait for it to catch up to the new diff
2394 deterministic.run_until_parked();
2395
2396 // Smoke test diffing
2397 buffer_local_a.read_with(cx_a, |buffer, _| {
2398 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2399 git::diff::assert_hunks(
2400 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2401 &buffer,
2402 &diff_base,
2403 &[(1..2, "", "two\n")],
2404 );
2405 });
2406
2407 // Create remote buffer
2408 let buffer_remote_a = project_remote
2409 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2410 .await
2411 .unwrap();
2412
2413 // Wait remote buffer to catch up to the new diff
2414 deterministic.run_until_parked();
2415
2416 // Smoke test diffing
2417 buffer_remote_a.read_with(cx_b, |buffer, _| {
2418 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2419 git::diff::assert_hunks(
2420 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2421 &buffer,
2422 &diff_base,
2423 &[(1..2, "", "two\n")],
2424 );
2425 });
2426
2427 client_a
2428 .fs
2429 .as_fake()
2430 .set_index_for_repo(
2431 Path::new("/dir/.git"),
2432 &[(Path::new("a.txt"), new_diff_base.clone())],
2433 )
2434 .await;
2435
2436 // Wait for buffer_local_a to receive it
2437 deterministic.run_until_parked();
2438
2439 // Smoke test new diffing
2440 buffer_local_a.read_with(cx_a, |buffer, _| {
2441 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2442
2443 git::diff::assert_hunks(
2444 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2445 &buffer,
2446 &diff_base,
2447 &[(2..3, "", "three\n")],
2448 );
2449 });
2450
2451 // Smoke test B
2452 buffer_remote_a.read_with(cx_b, |buffer, _| {
2453 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2454 git::diff::assert_hunks(
2455 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2456 &buffer,
2457 &diff_base,
2458 &[(2..3, "", "three\n")],
2459 );
2460 });
2461
2462 //Nested git dir
2463
2464 let diff_base = "
2465 one
2466 three
2467 "
2468 .unindent();
2469
2470 let new_diff_base = "
2471 one
2472 two
2473 "
2474 .unindent();
2475
2476 client_a
2477 .fs
2478 .as_fake()
2479 .set_index_for_repo(
2480 Path::new("/dir/sub/.git"),
2481 &[(Path::new("b.txt"), diff_base.clone())],
2482 )
2483 .await;
2484
2485 // Create the buffer
2486 let buffer_local_b = project_local
2487 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2488 .await
2489 .unwrap();
2490
2491 // Wait for it to catch up to the new diff
2492 deterministic.run_until_parked();
2493
2494 // Smoke test diffing
2495 buffer_local_b.read_with(cx_a, |buffer, _| {
2496 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2497 git::diff::assert_hunks(
2498 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2499 &buffer,
2500 &diff_base,
2501 &[(1..2, "", "two\n")],
2502 );
2503 });
2504
2505 // Create remote buffer
2506 let buffer_remote_b = project_remote
2507 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2508 .await
2509 .unwrap();
2510
2511 // Wait remote buffer to catch up to the new diff
2512 deterministic.run_until_parked();
2513
2514 // Smoke test diffing
2515 buffer_remote_b.read_with(cx_b, |buffer, _| {
2516 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2517 git::diff::assert_hunks(
2518 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2519 &buffer,
2520 &diff_base,
2521 &[(1..2, "", "two\n")],
2522 );
2523 });
2524
2525 client_a
2526 .fs
2527 .as_fake()
2528 .set_index_for_repo(
2529 Path::new("/dir/sub/.git"),
2530 &[(Path::new("b.txt"), new_diff_base.clone())],
2531 )
2532 .await;
2533
2534 // Wait for buffer_local_b to receive it
2535 deterministic.run_until_parked();
2536
2537 // Smoke test new diffing
2538 buffer_local_b.read_with(cx_a, |buffer, _| {
2539 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2540 println!("{:?}", buffer.as_rope().to_string());
2541 println!("{:?}", buffer.diff_base());
2542 println!(
2543 "{:?}",
2544 buffer
2545 .snapshot()
2546 .git_diff_hunks_in_row_range(0..4, false)
2547 .collect::<Vec<_>>()
2548 );
2549
2550 git::diff::assert_hunks(
2551 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2552 &buffer,
2553 &diff_base,
2554 &[(2..3, "", "three\n")],
2555 );
2556 });
2557
2558 // Smoke test B
2559 buffer_remote_b.read_with(cx_b, |buffer, _| {
2560 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2561 git::diff::assert_hunks(
2562 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2563 &buffer,
2564 &diff_base,
2565 &[(2..3, "", "three\n")],
2566 );
2567 });
2568}
2569
2570#[gpui::test(iterations = 10)]
2571async fn test_fs_operations(
2572 deterministic: Arc<Deterministic>,
2573 cx_a: &mut TestAppContext,
2574 cx_b: &mut TestAppContext,
2575) {
2576 deterministic.forbid_parking();
2577 let mut server = TestServer::start(&deterministic).await;
2578 let client_a = server.create_client(cx_a, "user_a").await;
2579 let client_b = server.create_client(cx_b, "user_b").await;
2580 server
2581 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2582 .await;
2583 let active_call_a = cx_a.read(ActiveCall::global);
2584
2585 client_a
2586 .fs
2587 .insert_tree(
2588 "/dir",
2589 json!({
2590 "a.txt": "a-contents",
2591 "b.txt": "b-contents",
2592 }),
2593 )
2594 .await;
2595 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2596 let project_id = active_call_a
2597 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2598 .await
2599 .unwrap();
2600 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2601
2602 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
2603 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
2604
2605 let entry = project_b
2606 .update(cx_b, |project, cx| {
2607 project
2608 .create_entry((worktree_id, "c.txt"), false, cx)
2609 .unwrap()
2610 })
2611 .await
2612 .unwrap();
2613 worktree_a.read_with(cx_a, |worktree, _| {
2614 assert_eq!(
2615 worktree
2616 .paths()
2617 .map(|p| p.to_string_lossy())
2618 .collect::<Vec<_>>(),
2619 ["a.txt", "b.txt", "c.txt"]
2620 );
2621 });
2622 worktree_b.read_with(cx_b, |worktree, _| {
2623 assert_eq!(
2624 worktree
2625 .paths()
2626 .map(|p| p.to_string_lossy())
2627 .collect::<Vec<_>>(),
2628 ["a.txt", "b.txt", "c.txt"]
2629 );
2630 });
2631
2632 project_b
2633 .update(cx_b, |project, cx| {
2634 project.rename_entry(entry.id, Path::new("d.txt"), cx)
2635 })
2636 .unwrap()
2637 .await
2638 .unwrap();
2639 worktree_a.read_with(cx_a, |worktree, _| {
2640 assert_eq!(
2641 worktree
2642 .paths()
2643 .map(|p| p.to_string_lossy())
2644 .collect::<Vec<_>>(),
2645 ["a.txt", "b.txt", "d.txt"]
2646 );
2647 });
2648 worktree_b.read_with(cx_b, |worktree, _| {
2649 assert_eq!(
2650 worktree
2651 .paths()
2652 .map(|p| p.to_string_lossy())
2653 .collect::<Vec<_>>(),
2654 ["a.txt", "b.txt", "d.txt"]
2655 );
2656 });
2657
2658 let dir_entry = project_b
2659 .update(cx_b, |project, cx| {
2660 project
2661 .create_entry((worktree_id, "DIR"), true, cx)
2662 .unwrap()
2663 })
2664 .await
2665 .unwrap();
2666 worktree_a.read_with(cx_a, |worktree, _| {
2667 assert_eq!(
2668 worktree
2669 .paths()
2670 .map(|p| p.to_string_lossy())
2671 .collect::<Vec<_>>(),
2672 ["DIR", "a.txt", "b.txt", "d.txt"]
2673 );
2674 });
2675 worktree_b.read_with(cx_b, |worktree, _| {
2676 assert_eq!(
2677 worktree
2678 .paths()
2679 .map(|p| p.to_string_lossy())
2680 .collect::<Vec<_>>(),
2681 ["DIR", "a.txt", "b.txt", "d.txt"]
2682 );
2683 });
2684
2685 project_b
2686 .update(cx_b, |project, cx| {
2687 project
2688 .create_entry((worktree_id, "DIR/e.txt"), false, cx)
2689 .unwrap()
2690 })
2691 .await
2692 .unwrap();
2693 project_b
2694 .update(cx_b, |project, cx| {
2695 project
2696 .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
2697 .unwrap()
2698 })
2699 .await
2700 .unwrap();
2701 project_b
2702 .update(cx_b, |project, cx| {
2703 project
2704 .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
2705 .unwrap()
2706 })
2707 .await
2708 .unwrap();
2709 worktree_a.read_with(cx_a, |worktree, _| {
2710 assert_eq!(
2711 worktree
2712 .paths()
2713 .map(|p| p.to_string_lossy())
2714 .collect::<Vec<_>>(),
2715 [
2716 "DIR",
2717 "DIR/SUBDIR",
2718 "DIR/SUBDIR/f.txt",
2719 "DIR/e.txt",
2720 "a.txt",
2721 "b.txt",
2722 "d.txt"
2723 ]
2724 );
2725 });
2726 worktree_b.read_with(cx_b, |worktree, _| {
2727 assert_eq!(
2728 worktree
2729 .paths()
2730 .map(|p| p.to_string_lossy())
2731 .collect::<Vec<_>>(),
2732 [
2733 "DIR",
2734 "DIR/SUBDIR",
2735 "DIR/SUBDIR/f.txt",
2736 "DIR/e.txt",
2737 "a.txt",
2738 "b.txt",
2739 "d.txt"
2740 ]
2741 );
2742 });
2743
2744 project_b
2745 .update(cx_b, |project, cx| {
2746 project
2747 .copy_entry(entry.id, Path::new("f.txt"), cx)
2748 .unwrap()
2749 })
2750 .await
2751 .unwrap();
2752 worktree_a.read_with(cx_a, |worktree, _| {
2753 assert_eq!(
2754 worktree
2755 .paths()
2756 .map(|p| p.to_string_lossy())
2757 .collect::<Vec<_>>(),
2758 [
2759 "DIR",
2760 "DIR/SUBDIR",
2761 "DIR/SUBDIR/f.txt",
2762 "DIR/e.txt",
2763 "a.txt",
2764 "b.txt",
2765 "d.txt",
2766 "f.txt"
2767 ]
2768 );
2769 });
2770 worktree_b.read_with(cx_b, |worktree, _| {
2771 assert_eq!(
2772 worktree
2773 .paths()
2774 .map(|p| p.to_string_lossy())
2775 .collect::<Vec<_>>(),
2776 [
2777 "DIR",
2778 "DIR/SUBDIR",
2779 "DIR/SUBDIR/f.txt",
2780 "DIR/e.txt",
2781 "a.txt",
2782 "b.txt",
2783 "d.txt",
2784 "f.txt"
2785 ]
2786 );
2787 });
2788
2789 project_b
2790 .update(cx_b, |project, cx| {
2791 project.delete_entry(dir_entry.id, cx).unwrap()
2792 })
2793 .await
2794 .unwrap();
2795 deterministic.run_until_parked();
2796
2797 worktree_a.read_with(cx_a, |worktree, _| {
2798 assert_eq!(
2799 worktree
2800 .paths()
2801 .map(|p| p.to_string_lossy())
2802 .collect::<Vec<_>>(),
2803 ["a.txt", "b.txt", "d.txt", "f.txt"]
2804 );
2805 });
2806 worktree_b.read_with(cx_b, |worktree, _| {
2807 assert_eq!(
2808 worktree
2809 .paths()
2810 .map(|p| p.to_string_lossy())
2811 .collect::<Vec<_>>(),
2812 ["a.txt", "b.txt", "d.txt", "f.txt"]
2813 );
2814 });
2815
2816 project_b
2817 .update(cx_b, |project, cx| {
2818 project.delete_entry(entry.id, cx).unwrap()
2819 })
2820 .await
2821 .unwrap();
2822 worktree_a.read_with(cx_a, |worktree, _| {
2823 assert_eq!(
2824 worktree
2825 .paths()
2826 .map(|p| p.to_string_lossy())
2827 .collect::<Vec<_>>(),
2828 ["a.txt", "b.txt", "f.txt"]
2829 );
2830 });
2831 worktree_b.read_with(cx_b, |worktree, _| {
2832 assert_eq!(
2833 worktree
2834 .paths()
2835 .map(|p| p.to_string_lossy())
2836 .collect::<Vec<_>>(),
2837 ["a.txt", "b.txt", "f.txt"]
2838 );
2839 });
2840}
2841
2842#[gpui::test(iterations = 10)]
2843async fn test_buffer_conflict_after_save(
2844 deterministic: Arc<Deterministic>,
2845 cx_a: &mut TestAppContext,
2846 cx_b: &mut TestAppContext,
2847) {
2848 deterministic.forbid_parking();
2849 let mut server = TestServer::start(&deterministic).await;
2850 let client_a = server.create_client(cx_a, "user_a").await;
2851 let client_b = server.create_client(cx_b, "user_b").await;
2852 server
2853 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2854 .await;
2855 let active_call_a = cx_a.read(ActiveCall::global);
2856
2857 client_a
2858 .fs
2859 .insert_tree(
2860 "/dir",
2861 json!({
2862 "a.txt": "a-contents",
2863 }),
2864 )
2865 .await;
2866 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2867 let project_id = active_call_a
2868 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2869 .await
2870 .unwrap();
2871 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2872
2873 // Open a buffer as client B
2874 let buffer_b = project_b
2875 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2876 .await
2877 .unwrap();
2878
2879 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
2880 buffer_b.read_with(cx_b, |buf, _| {
2881 assert!(buf.is_dirty());
2882 assert!(!buf.has_conflict());
2883 });
2884
2885 buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
2886 cx_a.foreground().forbid_parking();
2887 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
2888 buffer_b.read_with(cx_b, |buf, _| {
2889 assert!(!buf.has_conflict());
2890 });
2891
2892 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
2893 buffer_b.read_with(cx_b, |buf, _| {
2894 assert!(buf.is_dirty());
2895 assert!(!buf.has_conflict());
2896 });
2897}
2898
2899#[gpui::test(iterations = 10)]
2900async fn test_buffer_reloading(
2901 deterministic: Arc<Deterministic>,
2902 cx_a: &mut TestAppContext,
2903 cx_b: &mut TestAppContext,
2904) {
2905 deterministic.forbid_parking();
2906 let mut server = TestServer::start(&deterministic).await;
2907 let client_a = server.create_client(cx_a, "user_a").await;
2908 let client_b = server.create_client(cx_b, "user_b").await;
2909 server
2910 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2911 .await;
2912 let active_call_a = cx_a.read(ActiveCall::global);
2913
2914 client_a
2915 .fs
2916 .insert_tree(
2917 "/dir",
2918 json!({
2919 "a.txt": "a\nb\nc",
2920 }),
2921 )
2922 .await;
2923 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2924 let project_id = active_call_a
2925 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2926 .await
2927 .unwrap();
2928 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2929
2930 // Open a buffer as client B
2931 let buffer_b = project_b
2932 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2933 .await
2934 .unwrap();
2935 buffer_b.read_with(cx_b, |buf, _| {
2936 assert!(!buf.is_dirty());
2937 assert!(!buf.has_conflict());
2938 assert_eq!(buf.line_ending(), LineEnding::Unix);
2939 });
2940
2941 let new_contents = Rope::from("d\ne\nf");
2942 client_a
2943 .fs
2944 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
2945 .await
2946 .unwrap();
2947 cx_a.foreground().run_until_parked();
2948 buffer_b.read_with(cx_b, |buf, _| {
2949 assert_eq!(buf.text(), new_contents.to_string());
2950 assert!(!buf.is_dirty());
2951 assert!(!buf.has_conflict());
2952 assert_eq!(buf.line_ending(), LineEnding::Windows);
2953 });
2954}
2955
2956#[gpui::test(iterations = 10)]
2957async fn test_editing_while_guest_opens_buffer(
2958 deterministic: Arc<Deterministic>,
2959 cx_a: &mut TestAppContext,
2960 cx_b: &mut TestAppContext,
2961) {
2962 deterministic.forbid_parking();
2963 let mut server = TestServer::start(&deterministic).await;
2964 let client_a = server.create_client(cx_a, "user_a").await;
2965 let client_b = server.create_client(cx_b, "user_b").await;
2966 server
2967 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2968 .await;
2969 let active_call_a = cx_a.read(ActiveCall::global);
2970
2971 client_a
2972 .fs
2973 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
2974 .await;
2975 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2976 let project_id = active_call_a
2977 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2978 .await
2979 .unwrap();
2980 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2981
2982 // Open a buffer as client A
2983 let buffer_a = project_a
2984 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2985 .await
2986 .unwrap();
2987
2988 // Start opening the same buffer as client B
2989 let buffer_b = cx_b
2990 .background()
2991 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
2992
2993 // Edit the buffer as client A while client B is still opening it.
2994 cx_b.background().simulate_random_delay().await;
2995 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
2996 cx_b.background().simulate_random_delay().await;
2997 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
2998
2999 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3000 let buffer_b = buffer_b.await.unwrap();
3001 cx_a.foreground().run_until_parked();
3002 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3003}
3004
3005#[gpui::test(iterations = 10)]
3006async fn test_leaving_worktree_while_opening_buffer(
3007 deterministic: Arc<Deterministic>,
3008 cx_a: &mut TestAppContext,
3009 cx_b: &mut TestAppContext,
3010) {
3011 deterministic.forbid_parking();
3012 let mut server = TestServer::start(&deterministic).await;
3013 let client_a = server.create_client(cx_a, "user_a").await;
3014 let client_b = server.create_client(cx_b, "user_b").await;
3015 server
3016 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3017 .await;
3018 let active_call_a = cx_a.read(ActiveCall::global);
3019
3020 client_a
3021 .fs
3022 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3023 .await;
3024 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3025 let project_id = active_call_a
3026 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3027 .await
3028 .unwrap();
3029 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3030
3031 // See that a guest has joined as client A.
3032 cx_a.foreground().run_until_parked();
3033 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3034
3035 // Begin opening a buffer as client B, but leave the project before the open completes.
3036 let buffer_b = cx_b
3037 .background()
3038 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3039 cx_b.update(|_| drop(project_b));
3040 drop(buffer_b);
3041
3042 // See that the guest has left.
3043 cx_a.foreground().run_until_parked();
3044 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3045}
3046
3047#[gpui::test(iterations = 10)]
3048async fn test_canceling_buffer_opening(
3049 deterministic: Arc<Deterministic>,
3050 cx_a: &mut TestAppContext,
3051 cx_b: &mut TestAppContext,
3052) {
3053 deterministic.forbid_parking();
3054
3055 let mut server = TestServer::start(&deterministic).await;
3056 let client_a = server.create_client(cx_a, "user_a").await;
3057 let client_b = server.create_client(cx_b, "user_b").await;
3058 server
3059 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3060 .await;
3061 let active_call_a = cx_a.read(ActiveCall::global);
3062
3063 client_a
3064 .fs
3065 .insert_tree(
3066 "/dir",
3067 json!({
3068 "a.txt": "abc",
3069 }),
3070 )
3071 .await;
3072 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3073 let project_id = active_call_a
3074 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3075 .await
3076 .unwrap();
3077 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3078
3079 let buffer_a = project_a
3080 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3081 .await
3082 .unwrap();
3083
3084 // Open a buffer as client B but cancel after a random amount of time.
3085 let buffer_b = project_b.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx));
3086 deterministic.simulate_random_delay().await;
3087 drop(buffer_b);
3088
3089 // Try opening the same buffer again as client B, and ensure we can
3090 // still do it despite the cancellation above.
3091 let buffer_b = project_b
3092 .update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx))
3093 .await
3094 .unwrap();
3095 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3096}
3097
3098#[gpui::test(iterations = 10)]
3099async fn test_leaving_project(
3100 deterministic: Arc<Deterministic>,
3101 cx_a: &mut TestAppContext,
3102 cx_b: &mut TestAppContext,
3103 cx_c: &mut TestAppContext,
3104) {
3105 deterministic.forbid_parking();
3106 let mut server = TestServer::start(&deterministic).await;
3107 let client_a = server.create_client(cx_a, "user_a").await;
3108 let client_b = server.create_client(cx_b, "user_b").await;
3109 let client_c = server.create_client(cx_c, "user_c").await;
3110 server
3111 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3112 .await;
3113 let active_call_a = cx_a.read(ActiveCall::global);
3114
3115 client_a
3116 .fs
3117 .insert_tree(
3118 "/a",
3119 json!({
3120 "a.txt": "a-contents",
3121 "b.txt": "b-contents",
3122 }),
3123 )
3124 .await;
3125 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3126 let project_id = active_call_a
3127 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3128 .await
3129 .unwrap();
3130 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3131 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3132
3133 // Client A sees that a guest has joined.
3134 deterministic.run_until_parked();
3135 project_a.read_with(cx_a, |project, _| {
3136 assert_eq!(project.collaborators().len(), 2);
3137 });
3138 project_b1.read_with(cx_b, |project, _| {
3139 assert_eq!(project.collaborators().len(), 2);
3140 });
3141 project_c.read_with(cx_c, |project, _| {
3142 assert_eq!(project.collaborators().len(), 2);
3143 });
3144
3145 // Client B opens a buffer.
3146 let buffer_b1 = project_b1
3147 .update(cx_b, |project, cx| {
3148 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3149 project.open_buffer((worktree_id, "a.txt"), cx)
3150 })
3151 .await
3152 .unwrap();
3153 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3154
3155 // Drop client B's project and ensure client A and client C observe client B leaving.
3156 cx_b.update(|_| drop(project_b1));
3157 deterministic.run_until_parked();
3158 project_a.read_with(cx_a, |project, _| {
3159 assert_eq!(project.collaborators().len(), 1);
3160 });
3161 project_c.read_with(cx_c, |project, _| {
3162 assert_eq!(project.collaborators().len(), 1);
3163 });
3164
3165 // Client B re-joins the project and can open buffers as before.
3166 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3167 deterministic.run_until_parked();
3168 project_a.read_with(cx_a, |project, _| {
3169 assert_eq!(project.collaborators().len(), 2);
3170 });
3171 project_b2.read_with(cx_b, |project, _| {
3172 assert_eq!(project.collaborators().len(), 2);
3173 });
3174 project_c.read_with(cx_c, |project, _| {
3175 assert_eq!(project.collaborators().len(), 2);
3176 });
3177
3178 let buffer_b2 = project_b2
3179 .update(cx_b, |project, cx| {
3180 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3181 project.open_buffer((worktree_id, "a.txt"), cx)
3182 })
3183 .await
3184 .unwrap();
3185 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3186
3187 // Drop client B's connection and ensure client A and client C observe client B leaving.
3188 client_b.disconnect(&cx_b.to_async()).unwrap();
3189 deterministic.advance_clock(RECONNECT_TIMEOUT);
3190 project_a.read_with(cx_a, |project, _| {
3191 assert_eq!(project.collaborators().len(), 1);
3192 });
3193 project_b2.read_with(cx_b, |project, _| {
3194 assert!(project.is_read_only());
3195 });
3196 project_c.read_with(cx_c, |project, _| {
3197 assert_eq!(project.collaborators().len(), 1);
3198 });
3199
3200 // Client B can't join the project, unless they re-join the room.
3201 cx_b.spawn(|cx| {
3202 Project::remote(
3203 project_id,
3204 client_b.client.clone(),
3205 client_b.user_store.clone(),
3206 client_b.language_registry.clone(),
3207 FakeFs::new(cx.background()),
3208 cx,
3209 )
3210 })
3211 .await
3212 .unwrap_err();
3213
3214 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3215 client_c.wait_for_current_user(cx_c).await;
3216 server.forbid_connections();
3217 server.disconnect_client(client_c.peer_id().unwrap());
3218 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3219 deterministic.run_until_parked();
3220 project_a.read_with(cx_a, |project, _| {
3221 assert_eq!(project.collaborators().len(), 0);
3222 });
3223 project_b2.read_with(cx_b, |project, _| {
3224 assert!(project.is_read_only());
3225 });
3226 project_c.read_with(cx_c, |project, _| {
3227 assert!(project.is_read_only());
3228 });
3229}
3230
3231#[gpui::test(iterations = 10)]
3232async fn test_collaborating_with_diagnostics(
3233 deterministic: Arc<Deterministic>,
3234 cx_a: &mut TestAppContext,
3235 cx_b: &mut TestAppContext,
3236 cx_c: &mut TestAppContext,
3237) {
3238 deterministic.forbid_parking();
3239 let mut server = TestServer::start(&deterministic).await;
3240 let client_a = server.create_client(cx_a, "user_a").await;
3241 let client_b = server.create_client(cx_b, "user_b").await;
3242 let client_c = server.create_client(cx_c, "user_c").await;
3243 server
3244 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3245 .await;
3246 let active_call_a = cx_a.read(ActiveCall::global);
3247
3248 // Set up a fake language server.
3249 let mut language = Language::new(
3250 LanguageConfig {
3251 name: "Rust".into(),
3252 path_suffixes: vec!["rs".to_string()],
3253 ..Default::default()
3254 },
3255 Some(tree_sitter_rust::language()),
3256 );
3257 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3258 client_a.language_registry.add(Arc::new(language));
3259
3260 // Share a project as client A
3261 client_a
3262 .fs
3263 .insert_tree(
3264 "/a",
3265 json!({
3266 "a.rs": "let one = two",
3267 "other.rs": "",
3268 }),
3269 )
3270 .await;
3271 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3272
3273 // Cause the language server to start.
3274 let _buffer = project_a
3275 .update(cx_a, |project, cx| {
3276 project.open_buffer(
3277 ProjectPath {
3278 worktree_id,
3279 path: Path::new("other.rs").into(),
3280 },
3281 cx,
3282 )
3283 })
3284 .await
3285 .unwrap();
3286
3287 // Simulate a language server reporting errors for a file.
3288 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3289 fake_language_server
3290 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3291 .await;
3292 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3293 lsp::PublishDiagnosticsParams {
3294 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3295 version: None,
3296 diagnostics: vec![lsp::Diagnostic {
3297 severity: Some(lsp::DiagnosticSeverity::WARNING),
3298 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3299 message: "message 0".to_string(),
3300 ..Default::default()
3301 }],
3302 },
3303 );
3304
3305 // Client A shares the project and, simultaneously, the language server
3306 // publishes a diagnostic. This is done to ensure that the server always
3307 // observes the latest diagnostics for a worktree.
3308 let project_id = active_call_a
3309 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3310 .await
3311 .unwrap();
3312 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3313 lsp::PublishDiagnosticsParams {
3314 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3315 version: None,
3316 diagnostics: vec![lsp::Diagnostic {
3317 severity: Some(lsp::DiagnosticSeverity::ERROR),
3318 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3319 message: "message 1".to_string(),
3320 ..Default::default()
3321 }],
3322 },
3323 );
3324
3325 // Join the worktree as client B.
3326 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3327
3328 // Wait for server to see the diagnostics update.
3329 deterministic.run_until_parked();
3330
3331 // Ensure client B observes the new diagnostics.
3332 project_b.read_with(cx_b, |project, cx| {
3333 assert_eq!(
3334 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3335 &[(
3336 ProjectPath {
3337 worktree_id,
3338 path: Arc::from(Path::new("a.rs")),
3339 },
3340 DiagnosticSummary {
3341 error_count: 1,
3342 warning_count: 0,
3343 ..Default::default()
3344 },
3345 )]
3346 )
3347 });
3348
3349 // Join project as client C and observe the diagnostics.
3350 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3351 let project_c_diagnostic_summaries =
3352 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3353 project.diagnostic_summaries(cx).collect::<Vec<_>>()
3354 })));
3355 project_c.update(cx_c, |_, cx| {
3356 let summaries = project_c_diagnostic_summaries.clone();
3357 cx.subscribe(&project_c, {
3358 move |p, _, event, cx| {
3359 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3360 *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
3361 }
3362 }
3363 })
3364 .detach();
3365 });
3366
3367 deterministic.run_until_parked();
3368 assert_eq!(
3369 project_c_diagnostic_summaries.borrow().as_slice(),
3370 &[(
3371 ProjectPath {
3372 worktree_id,
3373 path: Arc::from(Path::new("a.rs")),
3374 },
3375 DiagnosticSummary {
3376 error_count: 1,
3377 warning_count: 0,
3378 ..Default::default()
3379 },
3380 )]
3381 );
3382
3383 // Simulate a language server reporting more errors for a file.
3384 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3385 lsp::PublishDiagnosticsParams {
3386 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3387 version: None,
3388 diagnostics: vec![
3389 lsp::Diagnostic {
3390 severity: Some(lsp::DiagnosticSeverity::ERROR),
3391 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3392 message: "message 1".to_string(),
3393 ..Default::default()
3394 },
3395 lsp::Diagnostic {
3396 severity: Some(lsp::DiagnosticSeverity::WARNING),
3397 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3398 message: "message 2".to_string(),
3399 ..Default::default()
3400 },
3401 ],
3402 },
3403 );
3404
3405 // Clients B and C get the updated summaries
3406 deterministic.run_until_parked();
3407 project_b.read_with(cx_b, |project, cx| {
3408 assert_eq!(
3409 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3410 [(
3411 ProjectPath {
3412 worktree_id,
3413 path: Arc::from(Path::new("a.rs")),
3414 },
3415 DiagnosticSummary {
3416 error_count: 1,
3417 warning_count: 1,
3418 ..Default::default()
3419 },
3420 )]
3421 );
3422 });
3423 project_c.read_with(cx_c, |project, cx| {
3424 assert_eq!(
3425 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3426 [(
3427 ProjectPath {
3428 worktree_id,
3429 path: Arc::from(Path::new("a.rs")),
3430 },
3431 DiagnosticSummary {
3432 error_count: 1,
3433 warning_count: 1,
3434 ..Default::default()
3435 },
3436 )]
3437 );
3438 });
3439
3440 // Open the file with the errors on client B. They should be present.
3441 let buffer_b = cx_b
3442 .background()
3443 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3444 .await
3445 .unwrap();
3446
3447 buffer_b.read_with(cx_b, |buffer, _| {
3448 assert_eq!(
3449 buffer
3450 .snapshot()
3451 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3452 .collect::<Vec<_>>(),
3453 &[
3454 DiagnosticEntry {
3455 range: Point::new(0, 4)..Point::new(0, 7),
3456 diagnostic: Diagnostic {
3457 group_id: 2,
3458 message: "message 1".to_string(),
3459 severity: lsp::DiagnosticSeverity::ERROR,
3460 is_primary: true,
3461 ..Default::default()
3462 }
3463 },
3464 DiagnosticEntry {
3465 range: Point::new(0, 10)..Point::new(0, 13),
3466 diagnostic: Diagnostic {
3467 group_id: 3,
3468 severity: lsp::DiagnosticSeverity::WARNING,
3469 message: "message 2".to_string(),
3470 is_primary: true,
3471 ..Default::default()
3472 }
3473 }
3474 ]
3475 );
3476 });
3477
3478 // Simulate a language server reporting no errors for a file.
3479 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3480 lsp::PublishDiagnosticsParams {
3481 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3482 version: None,
3483 diagnostics: vec![],
3484 },
3485 );
3486 deterministic.run_until_parked();
3487 project_a.read_with(cx_a, |project, cx| {
3488 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3489 });
3490 project_b.read_with(cx_b, |project, cx| {
3491 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3492 });
3493 project_c.read_with(cx_c, |project, cx| {
3494 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3495 });
3496}
3497
3498#[gpui::test(iterations = 10)]
3499async fn test_collaborating_with_completion(
3500 deterministic: Arc<Deterministic>,
3501 cx_a: &mut TestAppContext,
3502 cx_b: &mut TestAppContext,
3503) {
3504 deterministic.forbid_parking();
3505 let mut server = TestServer::start(&deterministic).await;
3506 let client_a = server.create_client(cx_a, "user_a").await;
3507 let client_b = server.create_client(cx_b, "user_b").await;
3508 server
3509 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3510 .await;
3511 let active_call_a = cx_a.read(ActiveCall::global);
3512
3513 // Set up a fake language server.
3514 let mut language = Language::new(
3515 LanguageConfig {
3516 name: "Rust".into(),
3517 path_suffixes: vec!["rs".to_string()],
3518 ..Default::default()
3519 },
3520 Some(tree_sitter_rust::language()),
3521 );
3522 let mut fake_language_servers = language
3523 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3524 capabilities: lsp::ServerCapabilities {
3525 completion_provider: Some(lsp::CompletionOptions {
3526 trigger_characters: Some(vec![".".to_string()]),
3527 ..Default::default()
3528 }),
3529 ..Default::default()
3530 },
3531 ..Default::default()
3532 }))
3533 .await;
3534 client_a.language_registry.add(Arc::new(language));
3535
3536 client_a
3537 .fs
3538 .insert_tree(
3539 "/a",
3540 json!({
3541 "main.rs": "fn main() { a }",
3542 "other.rs": "",
3543 }),
3544 )
3545 .await;
3546 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3547 let project_id = active_call_a
3548 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3549 .await
3550 .unwrap();
3551 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3552
3553 // Open a file in an editor as the guest.
3554 let buffer_b = project_b
3555 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3556 .await
3557 .unwrap();
3558 let (_, window_b) = cx_b.add_window(|_| EmptyView);
3559 let editor_b = cx_b.add_view(&window_b, |cx| {
3560 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
3561 });
3562
3563 let fake_language_server = fake_language_servers.next().await.unwrap();
3564 cx_a.foreground().run_until_parked();
3565 buffer_b.read_with(cx_b, |buffer, _| {
3566 assert!(!buffer.completion_triggers().is_empty())
3567 });
3568
3569 // Type a completion trigger character as the guest.
3570 editor_b.update(cx_b, |editor, cx| {
3571 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
3572 editor.handle_input(".", cx);
3573 cx.focus(&editor_b);
3574 });
3575
3576 // Receive a completion request as the host's language server.
3577 // Return some completions from the host's language server.
3578 cx_a.foreground().start_waiting();
3579 fake_language_server
3580 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
3581 assert_eq!(
3582 params.text_document_position.text_document.uri,
3583 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3584 );
3585 assert_eq!(
3586 params.text_document_position.position,
3587 lsp::Position::new(0, 14),
3588 );
3589
3590 Ok(Some(lsp::CompletionResponse::Array(vec![
3591 lsp::CompletionItem {
3592 label: "first_method(…)".into(),
3593 detail: Some("fn(&mut self, B) -> C".into()),
3594 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3595 new_text: "first_method($1)".to_string(),
3596 range: lsp::Range::new(
3597 lsp::Position::new(0, 14),
3598 lsp::Position::new(0, 14),
3599 ),
3600 })),
3601 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3602 ..Default::default()
3603 },
3604 lsp::CompletionItem {
3605 label: "second_method(…)".into(),
3606 detail: Some("fn(&mut self, C) -> D<E>".into()),
3607 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3608 new_text: "second_method()".to_string(),
3609 range: lsp::Range::new(
3610 lsp::Position::new(0, 14),
3611 lsp::Position::new(0, 14),
3612 ),
3613 })),
3614 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3615 ..Default::default()
3616 },
3617 ])))
3618 })
3619 .next()
3620 .await
3621 .unwrap();
3622 cx_a.foreground().finish_waiting();
3623
3624 // Open the buffer on the host.
3625 let buffer_a = project_a
3626 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3627 .await
3628 .unwrap();
3629 cx_a.foreground().run_until_parked();
3630 buffer_a.read_with(cx_a, |buffer, _| {
3631 assert_eq!(buffer.text(), "fn main() { a. }")
3632 });
3633
3634 // Confirm a completion on the guest.
3635 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
3636 editor_b.update(cx_b, |editor, cx| {
3637 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
3638 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
3639 });
3640
3641 // Return a resolved completion from the host's language server.
3642 // The resolved completion has an additional text edit.
3643 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
3644 |params, _| async move {
3645 assert_eq!(params.label, "first_method(…)");
3646 Ok(lsp::CompletionItem {
3647 label: "first_method(…)".into(),
3648 detail: Some("fn(&mut self, B) -> C".into()),
3649 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3650 new_text: "first_method($1)".to_string(),
3651 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
3652 })),
3653 additional_text_edits: Some(vec![lsp::TextEdit {
3654 new_text: "use d::SomeTrait;\n".to_string(),
3655 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3656 }]),
3657 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3658 ..Default::default()
3659 })
3660 },
3661 );
3662
3663 // The additional edit is applied.
3664 cx_a.foreground().run_until_parked();
3665 buffer_a.read_with(cx_a, |buffer, _| {
3666 assert_eq!(
3667 buffer.text(),
3668 "use d::SomeTrait;\nfn main() { a.first_method() }"
3669 );
3670 });
3671 buffer_b.read_with(cx_b, |buffer, _| {
3672 assert_eq!(
3673 buffer.text(),
3674 "use d::SomeTrait;\nfn main() { a.first_method() }"
3675 );
3676 });
3677}
3678
3679#[gpui::test(iterations = 10)]
3680async fn test_reloading_buffer_manually(
3681 deterministic: Arc<Deterministic>,
3682 cx_a: &mut TestAppContext,
3683 cx_b: &mut TestAppContext,
3684) {
3685 deterministic.forbid_parking();
3686 let mut server = TestServer::start(&deterministic).await;
3687 let client_a = server.create_client(cx_a, "user_a").await;
3688 let client_b = server.create_client(cx_b, "user_b").await;
3689 server
3690 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3691 .await;
3692 let active_call_a = cx_a.read(ActiveCall::global);
3693
3694 client_a
3695 .fs
3696 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
3697 .await;
3698 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3699 let buffer_a = project_a
3700 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
3701 .await
3702 .unwrap();
3703 let project_id = active_call_a
3704 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3705 .await
3706 .unwrap();
3707
3708 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3709
3710 let buffer_b = cx_b
3711 .background()
3712 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3713 .await
3714 .unwrap();
3715 buffer_b.update(cx_b, |buffer, cx| {
3716 buffer.edit([(4..7, "six")], None, cx);
3717 buffer.edit([(10..11, "6")], None, cx);
3718 assert_eq!(buffer.text(), "let six = 6;");
3719 assert!(buffer.is_dirty());
3720 assert!(!buffer.has_conflict());
3721 });
3722 cx_a.foreground().run_until_parked();
3723 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
3724
3725 client_a
3726 .fs
3727 .save(
3728 "/a/a.rs".as_ref(),
3729 &Rope::from("let seven = 7;"),
3730 LineEnding::Unix,
3731 )
3732 .await
3733 .unwrap();
3734 cx_a.foreground().run_until_parked();
3735 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
3736 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
3737
3738 project_b
3739 .update(cx_b, |project, cx| {
3740 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
3741 })
3742 .await
3743 .unwrap();
3744 buffer_a.read_with(cx_a, |buffer, _| {
3745 assert_eq!(buffer.text(), "let seven = 7;");
3746 assert!(!buffer.is_dirty());
3747 assert!(!buffer.has_conflict());
3748 });
3749 buffer_b.read_with(cx_b, |buffer, _| {
3750 assert_eq!(buffer.text(), "let seven = 7;");
3751 assert!(!buffer.is_dirty());
3752 assert!(!buffer.has_conflict());
3753 });
3754
3755 buffer_a.update(cx_a, |buffer, cx| {
3756 // Undoing on the host is a no-op when the reload was initiated by the guest.
3757 buffer.undo(cx);
3758 assert_eq!(buffer.text(), "let seven = 7;");
3759 assert!(!buffer.is_dirty());
3760 assert!(!buffer.has_conflict());
3761 });
3762 buffer_b.update(cx_b, |buffer, cx| {
3763 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
3764 buffer.undo(cx);
3765 assert_eq!(buffer.text(), "let six = 6;");
3766 assert!(buffer.is_dirty());
3767 assert!(!buffer.has_conflict());
3768 });
3769}
3770
3771#[gpui::test(iterations = 10)]
3772async fn test_formatting_buffer(
3773 deterministic: Arc<Deterministic>,
3774 cx_a: &mut TestAppContext,
3775 cx_b: &mut TestAppContext,
3776) {
3777 use project::FormatTrigger;
3778
3779 let mut server = TestServer::start(&deterministic).await;
3780 let client_a = server.create_client(cx_a, "user_a").await;
3781 let client_b = server.create_client(cx_b, "user_b").await;
3782 server
3783 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3784 .await;
3785 let active_call_a = cx_a.read(ActiveCall::global);
3786
3787 // Set up a fake language server.
3788 let mut language = Language::new(
3789 LanguageConfig {
3790 name: "Rust".into(),
3791 path_suffixes: vec!["rs".to_string()],
3792 ..Default::default()
3793 },
3794 Some(tree_sitter_rust::language()),
3795 );
3796 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3797 client_a.language_registry.add(Arc::new(language));
3798
3799 // Here we insert a fake tree with a directory that exists on disk. This is needed
3800 // because later we'll invoke a command, which requires passing a working directory
3801 // that points to a valid location on disk.
3802 let directory = env::current_dir().unwrap();
3803 client_a
3804 .fs
3805 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
3806 .await;
3807 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
3808 let project_id = active_call_a
3809 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3810 .await
3811 .unwrap();
3812 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3813
3814 let buffer_b = cx_b
3815 .background()
3816 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3817 .await
3818 .unwrap();
3819
3820 let fake_language_server = fake_language_servers.next().await.unwrap();
3821 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
3822 Ok(Some(vec![
3823 lsp::TextEdit {
3824 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
3825 new_text: "h".to_string(),
3826 },
3827 lsp::TextEdit {
3828 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
3829 new_text: "y".to_string(),
3830 },
3831 ]))
3832 });
3833
3834 project_b
3835 .update(cx_b, |project, cx| {
3836 project.format(
3837 HashSet::from_iter([buffer_b.clone()]),
3838 true,
3839 FormatTrigger::Save,
3840 cx,
3841 )
3842 })
3843 .await
3844 .unwrap();
3845 assert_eq!(
3846 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
3847 "let honey = \"two\""
3848 );
3849
3850 // Ensure buffer can be formatted using an external command. Notice how the
3851 // host's configuration is honored as opposed to using the guest's settings.
3852 cx_a.update(|cx| {
3853 cx.update_global(|settings: &mut Settings, _| {
3854 settings.editor_defaults.formatter = Some(Formatter::External {
3855 command: "awk".to_string(),
3856 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
3857 });
3858 });
3859 });
3860 project_b
3861 .update(cx_b, |project, cx| {
3862 project.format(
3863 HashSet::from_iter([buffer_b.clone()]),
3864 true,
3865 FormatTrigger::Save,
3866 cx,
3867 )
3868 })
3869 .await
3870 .unwrap();
3871 assert_eq!(
3872 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
3873 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
3874 );
3875}
3876
3877#[gpui::test(iterations = 10)]
3878async fn test_definition(
3879 deterministic: Arc<Deterministic>,
3880 cx_a: &mut TestAppContext,
3881 cx_b: &mut TestAppContext,
3882) {
3883 deterministic.forbid_parking();
3884 let mut server = TestServer::start(&deterministic).await;
3885 let client_a = server.create_client(cx_a, "user_a").await;
3886 let client_b = server.create_client(cx_b, "user_b").await;
3887 server
3888 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3889 .await;
3890 let active_call_a = cx_a.read(ActiveCall::global);
3891
3892 // Set up a fake language server.
3893 let mut language = Language::new(
3894 LanguageConfig {
3895 name: "Rust".into(),
3896 path_suffixes: vec!["rs".to_string()],
3897 ..Default::default()
3898 },
3899 Some(tree_sitter_rust::language()),
3900 );
3901 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3902 client_a.language_registry.add(Arc::new(language));
3903
3904 client_a
3905 .fs
3906 .insert_tree(
3907 "/root",
3908 json!({
3909 "dir-1": {
3910 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
3911 },
3912 "dir-2": {
3913 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
3914 "c.rs": "type T2 = usize;",
3915 }
3916 }),
3917 )
3918 .await;
3919 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
3920 let project_id = active_call_a
3921 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3922 .await
3923 .unwrap();
3924 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3925
3926 // Open the file on client B.
3927 let buffer_b = cx_b
3928 .background()
3929 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3930 .await
3931 .unwrap();
3932
3933 // Request the definition of a symbol as the guest.
3934 let fake_language_server = fake_language_servers.next().await.unwrap();
3935 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
3936 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
3937 lsp::Location::new(
3938 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
3939 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3940 ),
3941 )))
3942 });
3943
3944 let definitions_1 = project_b
3945 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
3946 .await
3947 .unwrap();
3948 cx_b.read(|cx| {
3949 assert_eq!(definitions_1.len(), 1);
3950 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
3951 let target_buffer = definitions_1[0].target.buffer.read(cx);
3952 assert_eq!(
3953 target_buffer.text(),
3954 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
3955 );
3956 assert_eq!(
3957 definitions_1[0].target.range.to_point(target_buffer),
3958 Point::new(0, 6)..Point::new(0, 9)
3959 );
3960 });
3961
3962 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
3963 // the previous call to `definition`.
3964 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
3965 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
3966 lsp::Location::new(
3967 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
3968 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
3969 ),
3970 )))
3971 });
3972
3973 let definitions_2 = project_b
3974 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
3975 .await
3976 .unwrap();
3977 cx_b.read(|cx| {
3978 assert_eq!(definitions_2.len(), 1);
3979 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
3980 let target_buffer = definitions_2[0].target.buffer.read(cx);
3981 assert_eq!(
3982 target_buffer.text(),
3983 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
3984 );
3985 assert_eq!(
3986 definitions_2[0].target.range.to_point(target_buffer),
3987 Point::new(1, 6)..Point::new(1, 11)
3988 );
3989 });
3990 assert_eq!(
3991 definitions_1[0].target.buffer,
3992 definitions_2[0].target.buffer
3993 );
3994
3995 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
3996 |req, _| async move {
3997 assert_eq!(
3998 req.text_document_position_params.position,
3999 lsp::Position::new(0, 7)
4000 );
4001 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4002 lsp::Location::new(
4003 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4004 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4005 ),
4006 )))
4007 },
4008 );
4009
4010 let type_definitions = project_b
4011 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4012 .await
4013 .unwrap();
4014 cx_b.read(|cx| {
4015 assert_eq!(type_definitions.len(), 1);
4016 let target_buffer = type_definitions[0].target.buffer.read(cx);
4017 assert_eq!(target_buffer.text(), "type T2 = usize;");
4018 assert_eq!(
4019 type_definitions[0].target.range.to_point(target_buffer),
4020 Point::new(0, 5)..Point::new(0, 7)
4021 );
4022 });
4023}
4024
4025#[gpui::test(iterations = 10)]
4026async fn test_references(
4027 deterministic: Arc<Deterministic>,
4028 cx_a: &mut TestAppContext,
4029 cx_b: &mut TestAppContext,
4030) {
4031 deterministic.forbid_parking();
4032 let mut server = TestServer::start(&deterministic).await;
4033 let client_a = server.create_client(cx_a, "user_a").await;
4034 let client_b = server.create_client(cx_b, "user_b").await;
4035 server
4036 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4037 .await;
4038 let active_call_a = cx_a.read(ActiveCall::global);
4039
4040 // Set up a fake language server.
4041 let mut language = Language::new(
4042 LanguageConfig {
4043 name: "Rust".into(),
4044 path_suffixes: vec!["rs".to_string()],
4045 ..Default::default()
4046 },
4047 Some(tree_sitter_rust::language()),
4048 );
4049 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4050 client_a.language_registry.add(Arc::new(language));
4051
4052 client_a
4053 .fs
4054 .insert_tree(
4055 "/root",
4056 json!({
4057 "dir-1": {
4058 "one.rs": "const ONE: usize = 1;",
4059 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4060 },
4061 "dir-2": {
4062 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4063 }
4064 }),
4065 )
4066 .await;
4067 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4068 let project_id = active_call_a
4069 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4070 .await
4071 .unwrap();
4072 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4073
4074 // Open the file on client B.
4075 let buffer_b = cx_b
4076 .background()
4077 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4078 .await
4079 .unwrap();
4080
4081 // Request references to a symbol as the guest.
4082 let fake_language_server = fake_language_servers.next().await.unwrap();
4083 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4084 assert_eq!(
4085 params.text_document_position.text_document.uri.as_str(),
4086 "file:///root/dir-1/one.rs"
4087 );
4088 Ok(Some(vec![
4089 lsp::Location {
4090 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4091 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4092 },
4093 lsp::Location {
4094 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4095 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4096 },
4097 lsp::Location {
4098 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4099 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4100 },
4101 ]))
4102 });
4103
4104 let references = project_b
4105 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4106 .await
4107 .unwrap();
4108 cx_b.read(|cx| {
4109 assert_eq!(references.len(), 3);
4110 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4111
4112 let two_buffer = references[0].buffer.read(cx);
4113 let three_buffer = references[2].buffer.read(cx);
4114 assert_eq!(
4115 two_buffer.file().unwrap().path().as_ref(),
4116 Path::new("two.rs")
4117 );
4118 assert_eq!(references[1].buffer, references[0].buffer);
4119 assert_eq!(
4120 three_buffer.file().unwrap().full_path(cx),
4121 Path::new("/root/dir-2/three.rs")
4122 );
4123
4124 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4125 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4126 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4127 });
4128}
4129
4130#[gpui::test(iterations = 10)]
4131async fn test_project_search(
4132 deterministic: Arc<Deterministic>,
4133 cx_a: &mut TestAppContext,
4134 cx_b: &mut TestAppContext,
4135) {
4136 deterministic.forbid_parking();
4137 let mut server = TestServer::start(&deterministic).await;
4138 let client_a = server.create_client(cx_a, "user_a").await;
4139 let client_b = server.create_client(cx_b, "user_b").await;
4140 server
4141 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4142 .await;
4143 let active_call_a = cx_a.read(ActiveCall::global);
4144
4145 client_a
4146 .fs
4147 .insert_tree(
4148 "/root",
4149 json!({
4150 "dir-1": {
4151 "a": "hello world",
4152 "b": "goodnight moon",
4153 "c": "a world of goo",
4154 "d": "world champion of clown world",
4155 },
4156 "dir-2": {
4157 "e": "disney world is fun",
4158 }
4159 }),
4160 )
4161 .await;
4162 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4163 let (worktree_2, _) = project_a
4164 .update(cx_a, |p, cx| {
4165 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4166 })
4167 .await
4168 .unwrap();
4169 worktree_2
4170 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4171 .await;
4172 let project_id = active_call_a
4173 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4174 .await
4175 .unwrap();
4176
4177 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4178
4179 // Perform a search as the guest.
4180 let results = project_b
4181 .update(cx_b, |project, cx| {
4182 project.search(SearchQuery::text("world", false, false), cx)
4183 })
4184 .await
4185 .unwrap();
4186
4187 let mut ranges_by_path = results
4188 .into_iter()
4189 .map(|(buffer, ranges)| {
4190 buffer.read_with(cx_b, |buffer, cx| {
4191 let path = buffer.file().unwrap().full_path(cx);
4192 let offset_ranges = ranges
4193 .into_iter()
4194 .map(|range| range.to_offset(buffer))
4195 .collect::<Vec<_>>();
4196 (path, offset_ranges)
4197 })
4198 })
4199 .collect::<Vec<_>>();
4200 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4201
4202 assert_eq!(
4203 ranges_by_path,
4204 &[
4205 (PathBuf::from("dir-1/a"), vec![6..11]),
4206 (PathBuf::from("dir-1/c"), vec![2..7]),
4207 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4208 (PathBuf::from("dir-2/e"), vec![7..12]),
4209 ]
4210 );
4211}
4212
4213#[gpui::test(iterations = 10)]
4214async fn test_document_highlights(
4215 deterministic: Arc<Deterministic>,
4216 cx_a: &mut TestAppContext,
4217 cx_b: &mut TestAppContext,
4218) {
4219 deterministic.forbid_parking();
4220 let mut server = TestServer::start(&deterministic).await;
4221 let client_a = server.create_client(cx_a, "user_a").await;
4222 let client_b = server.create_client(cx_b, "user_b").await;
4223 server
4224 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4225 .await;
4226 let active_call_a = cx_a.read(ActiveCall::global);
4227
4228 client_a
4229 .fs
4230 .insert_tree(
4231 "/root-1",
4232 json!({
4233 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4234 }),
4235 )
4236 .await;
4237
4238 // Set up a fake language server.
4239 let mut language = Language::new(
4240 LanguageConfig {
4241 name: "Rust".into(),
4242 path_suffixes: vec!["rs".to_string()],
4243 ..Default::default()
4244 },
4245 Some(tree_sitter_rust::language()),
4246 );
4247 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4248 client_a.language_registry.add(Arc::new(language));
4249
4250 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4251 let project_id = active_call_a
4252 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4253 .await
4254 .unwrap();
4255 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4256
4257 // Open the file on client B.
4258 let buffer_b = cx_b
4259 .background()
4260 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4261 .await
4262 .unwrap();
4263
4264 // Request document highlights as the guest.
4265 let fake_language_server = fake_language_servers.next().await.unwrap();
4266 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4267 |params, _| async move {
4268 assert_eq!(
4269 params
4270 .text_document_position_params
4271 .text_document
4272 .uri
4273 .as_str(),
4274 "file:///root-1/main.rs"
4275 );
4276 assert_eq!(
4277 params.text_document_position_params.position,
4278 lsp::Position::new(0, 34)
4279 );
4280 Ok(Some(vec![
4281 lsp::DocumentHighlight {
4282 kind: Some(lsp::DocumentHighlightKind::WRITE),
4283 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4284 },
4285 lsp::DocumentHighlight {
4286 kind: Some(lsp::DocumentHighlightKind::READ),
4287 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4288 },
4289 lsp::DocumentHighlight {
4290 kind: Some(lsp::DocumentHighlightKind::READ),
4291 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4292 },
4293 ]))
4294 },
4295 );
4296
4297 let highlights = project_b
4298 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4299 .await
4300 .unwrap();
4301 buffer_b.read_with(cx_b, |buffer, _| {
4302 let snapshot = buffer.snapshot();
4303
4304 let highlights = highlights
4305 .into_iter()
4306 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4307 .collect::<Vec<_>>();
4308 assert_eq!(
4309 highlights,
4310 &[
4311 (lsp::DocumentHighlightKind::WRITE, 10..16),
4312 (lsp::DocumentHighlightKind::READ, 32..38),
4313 (lsp::DocumentHighlightKind::READ, 41..47)
4314 ]
4315 )
4316 });
4317}
4318
4319#[gpui::test(iterations = 10)]
4320async fn test_lsp_hover(
4321 deterministic: Arc<Deterministic>,
4322 cx_a: &mut TestAppContext,
4323 cx_b: &mut TestAppContext,
4324) {
4325 deterministic.forbid_parking();
4326 let mut server = TestServer::start(&deterministic).await;
4327 let client_a = server.create_client(cx_a, "user_a").await;
4328 let client_b = server.create_client(cx_b, "user_b").await;
4329 server
4330 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4331 .await;
4332 let active_call_a = cx_a.read(ActiveCall::global);
4333
4334 client_a
4335 .fs
4336 .insert_tree(
4337 "/root-1",
4338 json!({
4339 "main.rs": "use std::collections::HashMap;",
4340 }),
4341 )
4342 .await;
4343
4344 // Set up a fake language server.
4345 let mut language = Language::new(
4346 LanguageConfig {
4347 name: "Rust".into(),
4348 path_suffixes: vec!["rs".to_string()],
4349 ..Default::default()
4350 },
4351 Some(tree_sitter_rust::language()),
4352 );
4353 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4354 client_a.language_registry.add(Arc::new(language));
4355
4356 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4357 let project_id = active_call_a
4358 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4359 .await
4360 .unwrap();
4361 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4362
4363 // Open the file as the guest
4364 let buffer_b = cx_b
4365 .background()
4366 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4367 .await
4368 .unwrap();
4369
4370 // Request hover information as the guest.
4371 let fake_language_server = fake_language_servers.next().await.unwrap();
4372 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4373 |params, _| async move {
4374 assert_eq!(
4375 params
4376 .text_document_position_params
4377 .text_document
4378 .uri
4379 .as_str(),
4380 "file:///root-1/main.rs"
4381 );
4382 assert_eq!(
4383 params.text_document_position_params.position,
4384 lsp::Position::new(0, 22)
4385 );
4386 Ok(Some(lsp::Hover {
4387 contents: lsp::HoverContents::Array(vec![
4388 lsp::MarkedString::String("Test hover content.".to_string()),
4389 lsp::MarkedString::LanguageString(lsp::LanguageString {
4390 language: "Rust".to_string(),
4391 value: "let foo = 42;".to_string(),
4392 }),
4393 ]),
4394 range: Some(lsp::Range::new(
4395 lsp::Position::new(0, 22),
4396 lsp::Position::new(0, 29),
4397 )),
4398 }))
4399 },
4400 );
4401
4402 let hover_info = project_b
4403 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
4404 .await
4405 .unwrap()
4406 .unwrap();
4407 buffer_b.read_with(cx_b, |buffer, _| {
4408 let snapshot = buffer.snapshot();
4409 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
4410 assert_eq!(
4411 hover_info.contents,
4412 vec![
4413 project::HoverBlock {
4414 text: "Test hover content.".to_string(),
4415 language: None,
4416 },
4417 project::HoverBlock {
4418 text: "let foo = 42;".to_string(),
4419 language: Some("Rust".to_string()),
4420 }
4421 ]
4422 );
4423 });
4424}
4425
4426#[gpui::test(iterations = 10)]
4427async fn test_project_symbols(
4428 deterministic: Arc<Deterministic>,
4429 cx_a: &mut TestAppContext,
4430 cx_b: &mut TestAppContext,
4431) {
4432 deterministic.forbid_parking();
4433 let mut server = TestServer::start(&deterministic).await;
4434 let client_a = server.create_client(cx_a, "user_a").await;
4435 let client_b = server.create_client(cx_b, "user_b").await;
4436 server
4437 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4438 .await;
4439 let active_call_a = cx_a.read(ActiveCall::global);
4440
4441 // Set up a fake language server.
4442 let mut language = Language::new(
4443 LanguageConfig {
4444 name: "Rust".into(),
4445 path_suffixes: vec!["rs".to_string()],
4446 ..Default::default()
4447 },
4448 Some(tree_sitter_rust::language()),
4449 );
4450 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4451 client_a.language_registry.add(Arc::new(language));
4452
4453 client_a
4454 .fs
4455 .insert_tree(
4456 "/code",
4457 json!({
4458 "crate-1": {
4459 "one.rs": "const ONE: usize = 1;",
4460 },
4461 "crate-2": {
4462 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
4463 },
4464 "private": {
4465 "passwords.txt": "the-password",
4466 }
4467 }),
4468 )
4469 .await;
4470 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
4471 let project_id = active_call_a
4472 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4473 .await
4474 .unwrap();
4475 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4476
4477 // Cause the language server to start.
4478 let _buffer = cx_b
4479 .background()
4480 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4481 .await
4482 .unwrap();
4483
4484 let fake_language_server = fake_language_servers.next().await.unwrap();
4485 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
4486 #[allow(deprecated)]
4487 Ok(Some(vec![lsp::SymbolInformation {
4488 name: "TWO".into(),
4489 location: lsp::Location {
4490 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
4491 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4492 },
4493 kind: lsp::SymbolKind::CONSTANT,
4494 tags: None,
4495 container_name: None,
4496 deprecated: None,
4497 }]))
4498 });
4499
4500 // Request the definition of a symbol as the guest.
4501 let symbols = project_b
4502 .update(cx_b, |p, cx| p.symbols("two", cx))
4503 .await
4504 .unwrap();
4505 assert_eq!(symbols.len(), 1);
4506 assert_eq!(symbols[0].name, "TWO");
4507
4508 // Open one of the returned symbols.
4509 let buffer_b_2 = project_b
4510 .update(cx_b, |project, cx| {
4511 project.open_buffer_for_symbol(&symbols[0], cx)
4512 })
4513 .await
4514 .unwrap();
4515 buffer_b_2.read_with(cx_b, |buffer, _| {
4516 assert_eq!(
4517 buffer.file().unwrap().path().as_ref(),
4518 Path::new("../crate-2/two.rs")
4519 );
4520 });
4521
4522 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
4523 let mut fake_symbol = symbols[0].clone();
4524 fake_symbol.path.path = Path::new("/code/secrets").into();
4525 let error = project_b
4526 .update(cx_b, |project, cx| {
4527 project.open_buffer_for_symbol(&fake_symbol, cx)
4528 })
4529 .await
4530 .unwrap_err();
4531 assert!(error.to_string().contains("invalid symbol signature"));
4532}
4533
4534#[gpui::test(iterations = 10)]
4535async fn test_open_buffer_while_getting_definition_pointing_to_it(
4536 deterministic: Arc<Deterministic>,
4537 cx_a: &mut TestAppContext,
4538 cx_b: &mut TestAppContext,
4539 mut rng: StdRng,
4540) {
4541 deterministic.forbid_parking();
4542 let mut server = TestServer::start(&deterministic).await;
4543 let client_a = server.create_client(cx_a, "user_a").await;
4544 let client_b = server.create_client(cx_b, "user_b").await;
4545 server
4546 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4547 .await;
4548 let active_call_a = cx_a.read(ActiveCall::global);
4549
4550 // Set up a fake language server.
4551 let mut language = Language::new(
4552 LanguageConfig {
4553 name: "Rust".into(),
4554 path_suffixes: vec!["rs".to_string()],
4555 ..Default::default()
4556 },
4557 Some(tree_sitter_rust::language()),
4558 );
4559 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4560 client_a.language_registry.add(Arc::new(language));
4561
4562 client_a
4563 .fs
4564 .insert_tree(
4565 "/root",
4566 json!({
4567 "a.rs": "const ONE: usize = b::TWO;",
4568 "b.rs": "const TWO: usize = 2",
4569 }),
4570 )
4571 .await;
4572 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
4573 let project_id = active_call_a
4574 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4575 .await
4576 .unwrap();
4577 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4578
4579 let buffer_b1 = cx_b
4580 .background()
4581 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4582 .await
4583 .unwrap();
4584
4585 let fake_language_server = fake_language_servers.next().await.unwrap();
4586 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4587 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4588 lsp::Location::new(
4589 lsp::Url::from_file_path("/root/b.rs").unwrap(),
4590 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4591 ),
4592 )))
4593 });
4594
4595 let definitions;
4596 let buffer_b2;
4597 if rng.gen() {
4598 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
4599 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
4600 } else {
4601 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
4602 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
4603 }
4604
4605 let buffer_b2 = buffer_b2.await.unwrap();
4606 let definitions = definitions.await.unwrap();
4607 assert_eq!(definitions.len(), 1);
4608 assert_eq!(definitions[0].target.buffer, buffer_b2);
4609}
4610
4611#[gpui::test(iterations = 10)]
4612async fn test_collaborating_with_code_actions(
4613 deterministic: Arc<Deterministic>,
4614 cx_a: &mut TestAppContext,
4615 cx_b: &mut TestAppContext,
4616) {
4617 deterministic.forbid_parking();
4618 cx_b.update(editor::init);
4619 let mut server = TestServer::start(&deterministic).await;
4620 let client_a = server.create_client(cx_a, "user_a").await;
4621 let client_b = server.create_client(cx_b, "user_b").await;
4622 server
4623 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4624 .await;
4625 let active_call_a = cx_a.read(ActiveCall::global);
4626
4627 // Set up a fake language server.
4628 let mut language = Language::new(
4629 LanguageConfig {
4630 name: "Rust".into(),
4631 path_suffixes: vec!["rs".to_string()],
4632 ..Default::default()
4633 },
4634 Some(tree_sitter_rust::language()),
4635 );
4636 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4637 client_a.language_registry.add(Arc::new(language));
4638
4639 client_a
4640 .fs
4641 .insert_tree(
4642 "/a",
4643 json!({
4644 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
4645 "other.rs": "pub fn foo() -> usize { 4 }",
4646 }),
4647 )
4648 .await;
4649 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4650 let project_id = active_call_a
4651 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4652 .await
4653 .unwrap();
4654
4655 // Join the project as client B.
4656 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4657 let (_window_b, workspace_b) = cx_b.add_window(|cx| {
4658 Workspace::new(
4659 Default::default(),
4660 0,
4661 project_b.clone(),
4662 |_, _| unimplemented!(),
4663 cx,
4664 )
4665 });
4666 let editor_b = workspace_b
4667 .update(cx_b, |workspace, cx| {
4668 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
4669 })
4670 .await
4671 .unwrap()
4672 .downcast::<Editor>()
4673 .unwrap();
4674
4675 let mut fake_language_server = fake_language_servers.next().await.unwrap();
4676 fake_language_server
4677 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
4678 assert_eq!(
4679 params.text_document.uri,
4680 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4681 );
4682 assert_eq!(params.range.start, lsp::Position::new(0, 0));
4683 assert_eq!(params.range.end, lsp::Position::new(0, 0));
4684 Ok(None)
4685 })
4686 .next()
4687 .await;
4688
4689 // Move cursor to a location that contains code actions.
4690 editor_b.update(cx_b, |editor, cx| {
4691 editor.change_selections(None, cx, |s| {
4692 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
4693 });
4694 cx.focus(&editor_b);
4695 });
4696
4697 fake_language_server
4698 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
4699 assert_eq!(
4700 params.text_document.uri,
4701 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4702 );
4703 assert_eq!(params.range.start, lsp::Position::new(1, 31));
4704 assert_eq!(params.range.end, lsp::Position::new(1, 31));
4705
4706 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4707 lsp::CodeAction {
4708 title: "Inline into all callers".to_string(),
4709 edit: Some(lsp::WorkspaceEdit {
4710 changes: Some(
4711 [
4712 (
4713 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4714 vec![lsp::TextEdit::new(
4715 lsp::Range::new(
4716 lsp::Position::new(1, 22),
4717 lsp::Position::new(1, 34),
4718 ),
4719 "4".to_string(),
4720 )],
4721 ),
4722 (
4723 lsp::Url::from_file_path("/a/other.rs").unwrap(),
4724 vec![lsp::TextEdit::new(
4725 lsp::Range::new(
4726 lsp::Position::new(0, 0),
4727 lsp::Position::new(0, 27),
4728 ),
4729 "".to_string(),
4730 )],
4731 ),
4732 ]
4733 .into_iter()
4734 .collect(),
4735 ),
4736 ..Default::default()
4737 }),
4738 data: Some(json!({
4739 "codeActionParams": {
4740 "range": {
4741 "start": {"line": 1, "column": 31},
4742 "end": {"line": 1, "column": 31},
4743 }
4744 }
4745 })),
4746 ..Default::default()
4747 },
4748 )]))
4749 })
4750 .next()
4751 .await;
4752
4753 // Toggle code actions and wait for them to display.
4754 editor_b.update(cx_b, |editor, cx| {
4755 editor.toggle_code_actions(
4756 &ToggleCodeActions {
4757 deployed_from_indicator: false,
4758 },
4759 cx,
4760 );
4761 });
4762 cx_a.foreground().run_until_parked();
4763 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
4764
4765 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
4766
4767 // Confirming the code action will trigger a resolve request.
4768 let confirm_action = workspace_b
4769 .update(cx_b, |workspace, cx| {
4770 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
4771 })
4772 .unwrap();
4773 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
4774 |_, _| async move {
4775 Ok(lsp::CodeAction {
4776 title: "Inline into all callers".to_string(),
4777 edit: Some(lsp::WorkspaceEdit {
4778 changes: Some(
4779 [
4780 (
4781 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4782 vec![lsp::TextEdit::new(
4783 lsp::Range::new(
4784 lsp::Position::new(1, 22),
4785 lsp::Position::new(1, 34),
4786 ),
4787 "4".to_string(),
4788 )],
4789 ),
4790 (
4791 lsp::Url::from_file_path("/a/other.rs").unwrap(),
4792 vec![lsp::TextEdit::new(
4793 lsp::Range::new(
4794 lsp::Position::new(0, 0),
4795 lsp::Position::new(0, 27),
4796 ),
4797 "".to_string(),
4798 )],
4799 ),
4800 ]
4801 .into_iter()
4802 .collect(),
4803 ),
4804 ..Default::default()
4805 }),
4806 ..Default::default()
4807 })
4808 },
4809 );
4810
4811 // After the action is confirmed, an editor containing both modified files is opened.
4812 confirm_action.await.unwrap();
4813 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
4814 workspace
4815 .active_item(cx)
4816 .unwrap()
4817 .downcast::<Editor>()
4818 .unwrap()
4819 });
4820 code_action_editor.update(cx_b, |editor, cx| {
4821 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
4822 editor.undo(&Undo, cx);
4823 assert_eq!(
4824 editor.text(cx),
4825 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
4826 );
4827 editor.redo(&Redo, cx);
4828 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
4829 });
4830}
4831
4832#[gpui::test(iterations = 10)]
4833async fn test_collaborating_with_renames(
4834 deterministic: Arc<Deterministic>,
4835 cx_a: &mut TestAppContext,
4836 cx_b: &mut TestAppContext,
4837) {
4838 deterministic.forbid_parking();
4839 cx_b.update(editor::init);
4840 let mut server = TestServer::start(&deterministic).await;
4841 let client_a = server.create_client(cx_a, "user_a").await;
4842 let client_b = server.create_client(cx_b, "user_b").await;
4843 server
4844 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4845 .await;
4846 let active_call_a = cx_a.read(ActiveCall::global);
4847
4848 // Set up a fake language server.
4849 let mut language = Language::new(
4850 LanguageConfig {
4851 name: "Rust".into(),
4852 path_suffixes: vec!["rs".to_string()],
4853 ..Default::default()
4854 },
4855 Some(tree_sitter_rust::language()),
4856 );
4857 let mut fake_language_servers = language
4858 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4859 capabilities: lsp::ServerCapabilities {
4860 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
4861 prepare_provider: Some(true),
4862 work_done_progress_options: Default::default(),
4863 })),
4864 ..Default::default()
4865 },
4866 ..Default::default()
4867 }))
4868 .await;
4869 client_a.language_registry.add(Arc::new(language));
4870
4871 client_a
4872 .fs
4873 .insert_tree(
4874 "/dir",
4875 json!({
4876 "one.rs": "const ONE: usize = 1;",
4877 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
4878 }),
4879 )
4880 .await;
4881 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
4882 let project_id = active_call_a
4883 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4884 .await
4885 .unwrap();
4886 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4887
4888 let (_window_b, workspace_b) = cx_b.add_window(|cx| {
4889 Workspace::new(
4890 Default::default(),
4891 0,
4892 project_b.clone(),
4893 |_, _| unimplemented!(),
4894 cx,
4895 )
4896 });
4897 let editor_b = workspace_b
4898 .update(cx_b, |workspace, cx| {
4899 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
4900 })
4901 .await
4902 .unwrap()
4903 .downcast::<Editor>()
4904 .unwrap();
4905 let fake_language_server = fake_language_servers.next().await.unwrap();
4906
4907 // Move cursor to a location that can be renamed.
4908 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
4909 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
4910 editor.rename(&Rename, cx).unwrap()
4911 });
4912
4913 fake_language_server
4914 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
4915 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
4916 assert_eq!(params.position, lsp::Position::new(0, 7));
4917 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4918 lsp::Position::new(0, 6),
4919 lsp::Position::new(0, 9),
4920 ))))
4921 })
4922 .next()
4923 .await
4924 .unwrap();
4925 prepare_rename.await.unwrap();
4926 editor_b.update(cx_b, |editor, cx| {
4927 let rename = editor.pending_rename().unwrap();
4928 let buffer = editor.buffer().read(cx).snapshot(cx);
4929 assert_eq!(
4930 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
4931 6..9
4932 );
4933 rename.editor.update(cx, |rename_editor, cx| {
4934 rename_editor.buffer().update(cx, |rename_buffer, cx| {
4935 rename_buffer.edit([(0..3, "THREE")], None, cx);
4936 });
4937 });
4938 });
4939
4940 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
4941 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
4942 });
4943 fake_language_server
4944 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
4945 assert_eq!(
4946 params.text_document_position.text_document.uri.as_str(),
4947 "file:///dir/one.rs"
4948 );
4949 assert_eq!(
4950 params.text_document_position.position,
4951 lsp::Position::new(0, 6)
4952 );
4953 assert_eq!(params.new_name, "THREE");
4954 Ok(Some(lsp::WorkspaceEdit {
4955 changes: Some(
4956 [
4957 (
4958 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
4959 vec![lsp::TextEdit::new(
4960 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4961 "THREE".to_string(),
4962 )],
4963 ),
4964 (
4965 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
4966 vec![
4967 lsp::TextEdit::new(
4968 lsp::Range::new(
4969 lsp::Position::new(0, 24),
4970 lsp::Position::new(0, 27),
4971 ),
4972 "THREE".to_string(),
4973 ),
4974 lsp::TextEdit::new(
4975 lsp::Range::new(
4976 lsp::Position::new(0, 35),
4977 lsp::Position::new(0, 38),
4978 ),
4979 "THREE".to_string(),
4980 ),
4981 ],
4982 ),
4983 ]
4984 .into_iter()
4985 .collect(),
4986 ),
4987 ..Default::default()
4988 }))
4989 })
4990 .next()
4991 .await
4992 .unwrap();
4993 confirm_rename.await.unwrap();
4994
4995 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
4996 workspace
4997 .active_item(cx)
4998 .unwrap()
4999 .downcast::<Editor>()
5000 .unwrap()
5001 });
5002 rename_editor.update(cx_b, |editor, cx| {
5003 assert_eq!(
5004 editor.text(cx),
5005 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5006 );
5007 editor.undo(&Undo, cx);
5008 assert_eq!(
5009 editor.text(cx),
5010 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
5011 );
5012 editor.redo(&Redo, cx);
5013 assert_eq!(
5014 editor.text(cx),
5015 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5016 );
5017 });
5018
5019 // Ensure temporary rename edits cannot be undone/redone.
5020 editor_b.update(cx_b, |editor, cx| {
5021 editor.undo(&Undo, cx);
5022 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5023 editor.undo(&Undo, cx);
5024 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5025 editor.redo(&Redo, cx);
5026 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
5027 })
5028}
5029
5030#[gpui::test(iterations = 10)]
5031async fn test_language_server_statuses(
5032 deterministic: Arc<Deterministic>,
5033 cx_a: &mut TestAppContext,
5034 cx_b: &mut TestAppContext,
5035) {
5036 deterministic.forbid_parking();
5037
5038 cx_b.update(editor::init);
5039 let mut server = TestServer::start(&deterministic).await;
5040 let client_a = server.create_client(cx_a, "user_a").await;
5041 let client_b = server.create_client(cx_b, "user_b").await;
5042 server
5043 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5044 .await;
5045 let active_call_a = cx_a.read(ActiveCall::global);
5046
5047 // Set up a fake language server.
5048 let mut language = Language::new(
5049 LanguageConfig {
5050 name: "Rust".into(),
5051 path_suffixes: vec!["rs".to_string()],
5052 ..Default::default()
5053 },
5054 Some(tree_sitter_rust::language()),
5055 );
5056 let mut fake_language_servers = language
5057 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5058 name: "the-language-server",
5059 ..Default::default()
5060 }))
5061 .await;
5062 client_a.language_registry.add(Arc::new(language));
5063
5064 client_a
5065 .fs
5066 .insert_tree(
5067 "/dir",
5068 json!({
5069 "main.rs": "const ONE: usize = 1;",
5070 }),
5071 )
5072 .await;
5073 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5074
5075 let _buffer_a = project_a
5076 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
5077 .await
5078 .unwrap();
5079
5080 let fake_language_server = fake_language_servers.next().await.unwrap();
5081 fake_language_server.start_progress("the-token").await;
5082 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5083 token: lsp::NumberOrString::String("the-token".to_string()),
5084 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5085 lsp::WorkDoneProgressReport {
5086 message: Some("the-message".to_string()),
5087 ..Default::default()
5088 },
5089 )),
5090 });
5091 deterministic.run_until_parked();
5092 project_a.read_with(cx_a, |project, _| {
5093 let status = project.language_server_statuses().next().unwrap();
5094 assert_eq!(status.name, "the-language-server");
5095 assert_eq!(status.pending_work.len(), 1);
5096 assert_eq!(
5097 status.pending_work["the-token"].message.as_ref().unwrap(),
5098 "the-message"
5099 );
5100 });
5101
5102 let project_id = active_call_a
5103 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5104 .await
5105 .unwrap();
5106 deterministic.run_until_parked();
5107 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5108 project_b.read_with(cx_b, |project, _| {
5109 let status = project.language_server_statuses().next().unwrap();
5110 assert_eq!(status.name, "the-language-server");
5111 });
5112
5113 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5114 token: lsp::NumberOrString::String("the-token".to_string()),
5115 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5116 lsp::WorkDoneProgressReport {
5117 message: Some("the-message-2".to_string()),
5118 ..Default::default()
5119 },
5120 )),
5121 });
5122 deterministic.run_until_parked();
5123 project_a.read_with(cx_a, |project, _| {
5124 let status = project.language_server_statuses().next().unwrap();
5125 assert_eq!(status.name, "the-language-server");
5126 assert_eq!(status.pending_work.len(), 1);
5127 assert_eq!(
5128 status.pending_work["the-token"].message.as_ref().unwrap(),
5129 "the-message-2"
5130 );
5131 });
5132 project_b.read_with(cx_b, |project, _| {
5133 let status = project.language_server_statuses().next().unwrap();
5134 assert_eq!(status.name, "the-language-server");
5135 assert_eq!(status.pending_work.len(), 1);
5136 assert_eq!(
5137 status.pending_work["the-token"].message.as_ref().unwrap(),
5138 "the-message-2"
5139 );
5140 });
5141}
5142
5143#[gpui::test(iterations = 10)]
5144async fn test_contacts(
5145 deterministic: Arc<Deterministic>,
5146 cx_a: &mut TestAppContext,
5147 cx_b: &mut TestAppContext,
5148 cx_c: &mut TestAppContext,
5149 cx_d: &mut TestAppContext,
5150) {
5151 deterministic.forbid_parking();
5152 let mut server = TestServer::start(&deterministic).await;
5153 let client_a = server.create_client(cx_a, "user_a").await;
5154 let client_b = server.create_client(cx_b, "user_b").await;
5155 let client_c = server.create_client(cx_c, "user_c").await;
5156 let client_d = server.create_client(cx_d, "user_d").await;
5157 server
5158 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5159 .await;
5160 let active_call_a = cx_a.read(ActiveCall::global);
5161 let active_call_b = cx_b.read(ActiveCall::global);
5162 let active_call_c = cx_c.read(ActiveCall::global);
5163 let _active_call_d = cx_d.read(ActiveCall::global);
5164
5165 deterministic.run_until_parked();
5166 assert_eq!(
5167 contacts(&client_a, cx_a),
5168 [
5169 ("user_b".to_string(), "online", "free"),
5170 ("user_c".to_string(), "online", "free")
5171 ]
5172 );
5173 assert_eq!(
5174 contacts(&client_b, cx_b),
5175 [
5176 ("user_a".to_string(), "online", "free"),
5177 ("user_c".to_string(), "online", "free")
5178 ]
5179 );
5180 assert_eq!(
5181 contacts(&client_c, cx_c),
5182 [
5183 ("user_a".to_string(), "online", "free"),
5184 ("user_b".to_string(), "online", "free")
5185 ]
5186 );
5187 assert_eq!(contacts(&client_d, cx_d), []);
5188
5189 server.disconnect_client(client_c.peer_id().unwrap());
5190 server.forbid_connections();
5191 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5192 assert_eq!(
5193 contacts(&client_a, cx_a),
5194 [
5195 ("user_b".to_string(), "online", "free"),
5196 ("user_c".to_string(), "offline", "free")
5197 ]
5198 );
5199 assert_eq!(
5200 contacts(&client_b, cx_b),
5201 [
5202 ("user_a".to_string(), "online", "free"),
5203 ("user_c".to_string(), "offline", "free")
5204 ]
5205 );
5206 assert_eq!(contacts(&client_c, cx_c), []);
5207 assert_eq!(contacts(&client_d, cx_d), []);
5208
5209 server.allow_connections();
5210 client_c
5211 .authenticate_and_connect(false, &cx_c.to_async())
5212 .await
5213 .unwrap();
5214
5215 deterministic.run_until_parked();
5216 assert_eq!(
5217 contacts(&client_a, cx_a),
5218 [
5219 ("user_b".to_string(), "online", "free"),
5220 ("user_c".to_string(), "online", "free")
5221 ]
5222 );
5223 assert_eq!(
5224 contacts(&client_b, cx_b),
5225 [
5226 ("user_a".to_string(), "online", "free"),
5227 ("user_c".to_string(), "online", "free")
5228 ]
5229 );
5230 assert_eq!(
5231 contacts(&client_c, cx_c),
5232 [
5233 ("user_a".to_string(), "online", "free"),
5234 ("user_b".to_string(), "online", "free")
5235 ]
5236 );
5237 assert_eq!(contacts(&client_d, cx_d), []);
5238
5239 active_call_a
5240 .update(cx_a, |call, cx| {
5241 call.invite(client_b.user_id().unwrap(), None, cx)
5242 })
5243 .await
5244 .unwrap();
5245 deterministic.run_until_parked();
5246 assert_eq!(
5247 contacts(&client_a, cx_a),
5248 [
5249 ("user_b".to_string(), "online", "busy"),
5250 ("user_c".to_string(), "online", "free")
5251 ]
5252 );
5253 assert_eq!(
5254 contacts(&client_b, cx_b),
5255 [
5256 ("user_a".to_string(), "online", "busy"),
5257 ("user_c".to_string(), "online", "free")
5258 ]
5259 );
5260 assert_eq!(
5261 contacts(&client_c, cx_c),
5262 [
5263 ("user_a".to_string(), "online", "busy"),
5264 ("user_b".to_string(), "online", "busy")
5265 ]
5266 );
5267 assert_eq!(contacts(&client_d, cx_d), []);
5268
5269 // Client B and client D become contacts while client B is being called.
5270 server
5271 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5272 .await;
5273 deterministic.run_until_parked();
5274 assert_eq!(
5275 contacts(&client_a, cx_a),
5276 [
5277 ("user_b".to_string(), "online", "busy"),
5278 ("user_c".to_string(), "online", "free")
5279 ]
5280 );
5281 assert_eq!(
5282 contacts(&client_b, cx_b),
5283 [
5284 ("user_a".to_string(), "online", "busy"),
5285 ("user_c".to_string(), "online", "free"),
5286 ("user_d".to_string(), "online", "free"),
5287 ]
5288 );
5289 assert_eq!(
5290 contacts(&client_c, cx_c),
5291 [
5292 ("user_a".to_string(), "online", "busy"),
5293 ("user_b".to_string(), "online", "busy")
5294 ]
5295 );
5296 assert_eq!(
5297 contacts(&client_d, cx_d),
5298 [("user_b".to_string(), "online", "busy")]
5299 );
5300
5301 active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
5302 deterministic.run_until_parked();
5303 assert_eq!(
5304 contacts(&client_a, cx_a),
5305 [
5306 ("user_b".to_string(), "online", "free"),
5307 ("user_c".to_string(), "online", "free")
5308 ]
5309 );
5310 assert_eq!(
5311 contacts(&client_b, cx_b),
5312 [
5313 ("user_a".to_string(), "online", "free"),
5314 ("user_c".to_string(), "online", "free"),
5315 ("user_d".to_string(), "online", "free")
5316 ]
5317 );
5318 assert_eq!(
5319 contacts(&client_c, cx_c),
5320 [
5321 ("user_a".to_string(), "online", "free"),
5322 ("user_b".to_string(), "online", "free")
5323 ]
5324 );
5325 assert_eq!(
5326 contacts(&client_d, cx_d),
5327 [("user_b".to_string(), "online", "free")]
5328 );
5329
5330 active_call_c
5331 .update(cx_c, |call, cx| {
5332 call.invite(client_a.user_id().unwrap(), None, cx)
5333 })
5334 .await
5335 .unwrap();
5336 deterministic.run_until_parked();
5337 assert_eq!(
5338 contacts(&client_a, cx_a),
5339 [
5340 ("user_b".to_string(), "online", "free"),
5341 ("user_c".to_string(), "online", "busy")
5342 ]
5343 );
5344 assert_eq!(
5345 contacts(&client_b, cx_b),
5346 [
5347 ("user_a".to_string(), "online", "busy"),
5348 ("user_c".to_string(), "online", "busy"),
5349 ("user_d".to_string(), "online", "free")
5350 ]
5351 );
5352 assert_eq!(
5353 contacts(&client_c, cx_c),
5354 [
5355 ("user_a".to_string(), "online", "busy"),
5356 ("user_b".to_string(), "online", "free")
5357 ]
5358 );
5359 assert_eq!(
5360 contacts(&client_d, cx_d),
5361 [("user_b".to_string(), "online", "free")]
5362 );
5363
5364 active_call_a
5365 .update(cx_a, |call, cx| call.accept_incoming(cx))
5366 .await
5367 .unwrap();
5368 deterministic.run_until_parked();
5369 assert_eq!(
5370 contacts(&client_a, cx_a),
5371 [
5372 ("user_b".to_string(), "online", "free"),
5373 ("user_c".to_string(), "online", "busy")
5374 ]
5375 );
5376 assert_eq!(
5377 contacts(&client_b, cx_b),
5378 [
5379 ("user_a".to_string(), "online", "busy"),
5380 ("user_c".to_string(), "online", "busy"),
5381 ("user_d".to_string(), "online", "free")
5382 ]
5383 );
5384 assert_eq!(
5385 contacts(&client_c, cx_c),
5386 [
5387 ("user_a".to_string(), "online", "busy"),
5388 ("user_b".to_string(), "online", "free")
5389 ]
5390 );
5391 assert_eq!(
5392 contacts(&client_d, cx_d),
5393 [("user_b".to_string(), "online", "free")]
5394 );
5395
5396 active_call_a
5397 .update(cx_a, |call, cx| {
5398 call.invite(client_b.user_id().unwrap(), None, cx)
5399 })
5400 .await
5401 .unwrap();
5402 deterministic.run_until_parked();
5403 assert_eq!(
5404 contacts(&client_a, cx_a),
5405 [
5406 ("user_b".to_string(), "online", "busy"),
5407 ("user_c".to_string(), "online", "busy")
5408 ]
5409 );
5410 assert_eq!(
5411 contacts(&client_b, cx_b),
5412 [
5413 ("user_a".to_string(), "online", "busy"),
5414 ("user_c".to_string(), "online", "busy"),
5415 ("user_d".to_string(), "online", "free")
5416 ]
5417 );
5418 assert_eq!(
5419 contacts(&client_c, cx_c),
5420 [
5421 ("user_a".to_string(), "online", "busy"),
5422 ("user_b".to_string(), "online", "busy")
5423 ]
5424 );
5425 assert_eq!(
5426 contacts(&client_d, cx_d),
5427 [("user_b".to_string(), "online", "busy")]
5428 );
5429
5430 active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
5431 deterministic.run_until_parked();
5432 assert_eq!(
5433 contacts(&client_a, cx_a),
5434 [
5435 ("user_b".to_string(), "online", "free"),
5436 ("user_c".to_string(), "online", "free")
5437 ]
5438 );
5439 assert_eq!(
5440 contacts(&client_b, cx_b),
5441 [
5442 ("user_a".to_string(), "online", "free"),
5443 ("user_c".to_string(), "online", "free"),
5444 ("user_d".to_string(), "online", "free")
5445 ]
5446 );
5447 assert_eq!(
5448 contacts(&client_c, cx_c),
5449 [
5450 ("user_a".to_string(), "online", "free"),
5451 ("user_b".to_string(), "online", "free")
5452 ]
5453 );
5454 assert_eq!(
5455 contacts(&client_d, cx_d),
5456 [("user_b".to_string(), "online", "free")]
5457 );
5458
5459 active_call_a
5460 .update(cx_a, |call, cx| {
5461 call.invite(client_b.user_id().unwrap(), None, cx)
5462 })
5463 .await
5464 .unwrap();
5465 deterministic.run_until_parked();
5466 assert_eq!(
5467 contacts(&client_a, cx_a),
5468 [
5469 ("user_b".to_string(), "online", "busy"),
5470 ("user_c".to_string(), "online", "free")
5471 ]
5472 );
5473 assert_eq!(
5474 contacts(&client_b, cx_b),
5475 [
5476 ("user_a".to_string(), "online", "busy"),
5477 ("user_c".to_string(), "online", "free"),
5478 ("user_d".to_string(), "online", "free")
5479 ]
5480 );
5481 assert_eq!(
5482 contacts(&client_c, cx_c),
5483 [
5484 ("user_a".to_string(), "online", "busy"),
5485 ("user_b".to_string(), "online", "busy")
5486 ]
5487 );
5488 assert_eq!(
5489 contacts(&client_d, cx_d),
5490 [("user_b".to_string(), "online", "busy")]
5491 );
5492
5493 server.forbid_connections();
5494 server.disconnect_client(client_a.peer_id().unwrap());
5495 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5496 assert_eq!(contacts(&client_a, cx_a), []);
5497 assert_eq!(
5498 contacts(&client_b, cx_b),
5499 [
5500 ("user_a".to_string(), "offline", "free"),
5501 ("user_c".to_string(), "online", "free"),
5502 ("user_d".to_string(), "online", "free")
5503 ]
5504 );
5505 assert_eq!(
5506 contacts(&client_c, cx_c),
5507 [
5508 ("user_a".to_string(), "offline", "free"),
5509 ("user_b".to_string(), "online", "free")
5510 ]
5511 );
5512 assert_eq!(
5513 contacts(&client_d, cx_d),
5514 [("user_b".to_string(), "online", "free")]
5515 );
5516
5517 // Test removing a contact
5518 client_b
5519 .user_store
5520 .update(cx_b, |store, cx| {
5521 store.remove_contact(client_c.user_id().unwrap(), cx)
5522 })
5523 .await
5524 .unwrap();
5525 deterministic.run_until_parked();
5526 assert_eq!(
5527 contacts(&client_b, cx_b),
5528 [
5529 ("user_a".to_string(), "offline", "free"),
5530 ("user_d".to_string(), "online", "free")
5531 ]
5532 );
5533 assert_eq!(
5534 contacts(&client_c, cx_c),
5535 [("user_a".to_string(), "offline", "free"),]
5536 );
5537
5538 fn contacts(
5539 client: &TestClient,
5540 cx: &TestAppContext,
5541 ) -> Vec<(String, &'static str, &'static str)> {
5542 client.user_store.read_with(cx, |store, _| {
5543 store
5544 .contacts()
5545 .iter()
5546 .map(|contact| {
5547 (
5548 contact.user.github_login.clone(),
5549 if contact.online { "online" } else { "offline" },
5550 if contact.busy { "busy" } else { "free" },
5551 )
5552 })
5553 .collect()
5554 })
5555 }
5556}
5557
5558#[gpui::test(iterations = 10)]
5559async fn test_contact_requests(
5560 deterministic: Arc<Deterministic>,
5561 cx_a: &mut TestAppContext,
5562 cx_a2: &mut TestAppContext,
5563 cx_b: &mut TestAppContext,
5564 cx_b2: &mut TestAppContext,
5565 cx_c: &mut TestAppContext,
5566 cx_c2: &mut TestAppContext,
5567) {
5568 deterministic.forbid_parking();
5569
5570 // Connect to a server as 3 clients.
5571 let mut server = TestServer::start(&deterministic).await;
5572 let client_a = server.create_client(cx_a, "user_a").await;
5573 let client_a2 = server.create_client(cx_a2, "user_a").await;
5574 let client_b = server.create_client(cx_b, "user_b").await;
5575 let client_b2 = server.create_client(cx_b2, "user_b").await;
5576 let client_c = server.create_client(cx_c, "user_c").await;
5577 let client_c2 = server.create_client(cx_c2, "user_c").await;
5578
5579 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5580 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5581 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5582
5583 // User A and User C request that user B become their contact.
5584 client_a
5585 .user_store
5586 .update(cx_a, |store, cx| {
5587 store.request_contact(client_b.user_id().unwrap(), cx)
5588 })
5589 .await
5590 .unwrap();
5591 client_c
5592 .user_store
5593 .update(cx_c, |store, cx| {
5594 store.request_contact(client_b.user_id().unwrap(), cx)
5595 })
5596 .await
5597 .unwrap();
5598 deterministic.run_until_parked();
5599
5600 // All users see the pending request appear in all their clients.
5601 assert_eq!(
5602 client_a.summarize_contacts(cx_a).outgoing_requests,
5603 &["user_b"]
5604 );
5605 assert_eq!(
5606 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5607 &["user_b"]
5608 );
5609 assert_eq!(
5610 client_b.summarize_contacts(cx_b).incoming_requests,
5611 &["user_a", "user_c"]
5612 );
5613 assert_eq!(
5614 client_b2.summarize_contacts(cx_b2).incoming_requests,
5615 &["user_a", "user_c"]
5616 );
5617 assert_eq!(
5618 client_c.summarize_contacts(cx_c).outgoing_requests,
5619 &["user_b"]
5620 );
5621 assert_eq!(
5622 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5623 &["user_b"]
5624 );
5625
5626 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5627 disconnect_and_reconnect(&client_a, cx_a).await;
5628 disconnect_and_reconnect(&client_b, cx_b).await;
5629 disconnect_and_reconnect(&client_c, cx_c).await;
5630 deterministic.run_until_parked();
5631 assert_eq!(
5632 client_a.summarize_contacts(cx_a).outgoing_requests,
5633 &["user_b"]
5634 );
5635 assert_eq!(
5636 client_b.summarize_contacts(cx_b).incoming_requests,
5637 &["user_a", "user_c"]
5638 );
5639 assert_eq!(
5640 client_c.summarize_contacts(cx_c).outgoing_requests,
5641 &["user_b"]
5642 );
5643
5644 // User B accepts the request from user A.
5645 client_b
5646 .user_store
5647 .update(cx_b, |store, cx| {
5648 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5649 })
5650 .await
5651 .unwrap();
5652
5653 deterministic.run_until_parked();
5654
5655 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5656 let contacts_b = client_b.summarize_contacts(cx_b);
5657 assert_eq!(contacts_b.current, &["user_a"]);
5658 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5659 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5660 assert_eq!(contacts_b2.current, &["user_a"]);
5661 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5662
5663 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5664 let contacts_a = client_a.summarize_contacts(cx_a);
5665 assert_eq!(contacts_a.current, &["user_b"]);
5666 assert!(contacts_a.outgoing_requests.is_empty());
5667 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5668 assert_eq!(contacts_a2.current, &["user_b"]);
5669 assert!(contacts_a2.outgoing_requests.is_empty());
5670
5671 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5672 disconnect_and_reconnect(&client_a, cx_a).await;
5673 disconnect_and_reconnect(&client_b, cx_b).await;
5674 disconnect_and_reconnect(&client_c, cx_c).await;
5675 deterministic.run_until_parked();
5676 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5677 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5678 assert_eq!(
5679 client_b.summarize_contacts(cx_b).incoming_requests,
5680 &["user_c"]
5681 );
5682 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5683 assert_eq!(
5684 client_c.summarize_contacts(cx_c).outgoing_requests,
5685 &["user_b"]
5686 );
5687
5688 // User B rejects the request from user C.
5689 client_b
5690 .user_store
5691 .update(cx_b, |store, cx| {
5692 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5693 })
5694 .await
5695 .unwrap();
5696
5697 deterministic.run_until_parked();
5698
5699 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5700 let contacts_b = client_b.summarize_contacts(cx_b);
5701 assert_eq!(contacts_b.current, &["user_a"]);
5702 assert!(contacts_b.incoming_requests.is_empty());
5703 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5704 assert_eq!(contacts_b2.current, &["user_a"]);
5705 assert!(contacts_b2.incoming_requests.is_empty());
5706
5707 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5708 let contacts_c = client_c.summarize_contacts(cx_c);
5709 assert!(contacts_c.current.is_empty());
5710 assert!(contacts_c.outgoing_requests.is_empty());
5711 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5712 assert!(contacts_c2.current.is_empty());
5713 assert!(contacts_c2.outgoing_requests.is_empty());
5714
5715 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5716 disconnect_and_reconnect(&client_a, cx_a).await;
5717 disconnect_and_reconnect(&client_b, cx_b).await;
5718 disconnect_and_reconnect(&client_c, cx_c).await;
5719 deterministic.run_until_parked();
5720 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5721 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5722 assert!(client_b
5723 .summarize_contacts(cx_b)
5724 .incoming_requests
5725 .is_empty());
5726 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5727 assert!(client_c
5728 .summarize_contacts(cx_c)
5729 .outgoing_requests
5730 .is_empty());
5731
5732 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
5733 client.disconnect(&cx.to_async()).unwrap();
5734 client.clear_contacts(cx).await;
5735 client
5736 .authenticate_and_connect(false, &cx.to_async())
5737 .await
5738 .unwrap();
5739 }
5740}
5741
5742#[gpui::test(iterations = 10)]
5743async fn test_following(
5744 deterministic: Arc<Deterministic>,
5745 cx_a: &mut TestAppContext,
5746 cx_b: &mut TestAppContext,
5747) {
5748 deterministic.forbid_parking();
5749 cx_a.update(editor::init);
5750 cx_b.update(editor::init);
5751
5752 let mut server = TestServer::start(&deterministic).await;
5753 let client_a = server.create_client(cx_a, "user_a").await;
5754 let client_b = server.create_client(cx_b, "user_b").await;
5755 server
5756 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5757 .await;
5758 let active_call_a = cx_a.read(ActiveCall::global);
5759 let active_call_b = cx_b.read(ActiveCall::global);
5760
5761 client_a
5762 .fs
5763 .insert_tree(
5764 "/a",
5765 json!({
5766 "1.txt": "one\none\none",
5767 "2.txt": "two\ntwo\ntwo",
5768 "3.txt": "three\nthree\nthree",
5769 }),
5770 )
5771 .await;
5772 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5773 active_call_a
5774 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5775 .await
5776 .unwrap();
5777
5778 let project_id = active_call_a
5779 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5780 .await
5781 .unwrap();
5782 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5783 active_call_b
5784 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5785 .await
5786 .unwrap();
5787
5788 // Client A opens some editors.
5789 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5790 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
5791 let editor_a1 = workspace_a
5792 .update(cx_a, |workspace, cx| {
5793 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5794 })
5795 .await
5796 .unwrap()
5797 .downcast::<Editor>()
5798 .unwrap();
5799 let editor_a2 = workspace_a
5800 .update(cx_a, |workspace, cx| {
5801 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
5802 })
5803 .await
5804 .unwrap()
5805 .downcast::<Editor>()
5806 .unwrap();
5807
5808 // Client B opens an editor.
5809 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5810 let editor_b1 = workspace_b
5811 .update(cx_b, |workspace, cx| {
5812 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5813 })
5814 .await
5815 .unwrap()
5816 .downcast::<Editor>()
5817 .unwrap();
5818
5819 let client_a_id = project_b.read_with(cx_b, |project, _| {
5820 project.collaborators().values().next().unwrap().peer_id
5821 });
5822 let client_b_id = project_a.read_with(cx_a, |project, _| {
5823 project.collaborators().values().next().unwrap().peer_id
5824 });
5825
5826 // When client B starts following client A, all visible view states are replicated to client B.
5827 editor_a1.update(cx_a, |editor, cx| {
5828 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
5829 });
5830 editor_a2.update(cx_a, |editor, cx| {
5831 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
5832 });
5833 workspace_b
5834 .update(cx_b, |workspace, cx| {
5835 workspace
5836 .toggle_follow(&ToggleFollow(client_a_id), cx)
5837 .unwrap()
5838 })
5839 .await
5840 .unwrap();
5841
5842 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
5843 workspace
5844 .active_item(cx)
5845 .unwrap()
5846 .downcast::<Editor>()
5847 .unwrap()
5848 });
5849 assert_eq!(
5850 cx_b.read(|cx| editor_b2.project_path(cx)),
5851 Some((worktree_id, "2.txt").into())
5852 );
5853 assert_eq!(
5854 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
5855 vec![2..3]
5856 );
5857 assert_eq!(
5858 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
5859 vec![0..1]
5860 );
5861
5862 // When client A activates a different editor, client B does so as well.
5863 workspace_a.update(cx_a, |workspace, cx| {
5864 workspace.activate_item(&editor_a1, cx)
5865 });
5866 deterministic.run_until_parked();
5867 workspace_b.read_with(cx_b, |workspace, cx| {
5868 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
5869 });
5870
5871 // When client A opens a multibuffer, client B does so as well.
5872 let multibuffer_a = cx_a.add_model(|cx| {
5873 let buffer_a1 = project_a.update(cx, |project, cx| {
5874 project
5875 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
5876 .unwrap()
5877 });
5878 let buffer_a2 = project_a.update(cx, |project, cx| {
5879 project
5880 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
5881 .unwrap()
5882 });
5883 let mut result = MultiBuffer::new(0);
5884 result.push_excerpts(
5885 buffer_a1,
5886 [ExcerptRange {
5887 context: 0..3,
5888 primary: None,
5889 }],
5890 cx,
5891 );
5892 result.push_excerpts(
5893 buffer_a2,
5894 [ExcerptRange {
5895 context: 4..7,
5896 primary: None,
5897 }],
5898 cx,
5899 );
5900 result
5901 });
5902 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
5903 let editor =
5904 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
5905 workspace.add_item(Box::new(editor.clone()), cx);
5906 editor
5907 });
5908 deterministic.run_until_parked();
5909 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
5910 workspace
5911 .active_item(cx)
5912 .unwrap()
5913 .downcast::<Editor>()
5914 .unwrap()
5915 });
5916 assert_eq!(
5917 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
5918 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
5919 );
5920
5921 // When client A navigates back and forth, client B does so as well.
5922 workspace_a
5923 .update(cx_a, |workspace, cx| {
5924 workspace::Pane::go_back(workspace, None, cx)
5925 })
5926 .await;
5927 deterministic.run_until_parked();
5928 workspace_b.read_with(cx_b, |workspace, cx| {
5929 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
5930 });
5931
5932 workspace_a
5933 .update(cx_a, |workspace, cx| {
5934 workspace::Pane::go_back(workspace, None, cx)
5935 })
5936 .await;
5937 deterministic.run_until_parked();
5938 workspace_b.read_with(cx_b, |workspace, cx| {
5939 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
5940 });
5941
5942 workspace_a
5943 .update(cx_a, |workspace, cx| {
5944 workspace::Pane::go_forward(workspace, None, cx)
5945 })
5946 .await;
5947 deterministic.run_until_parked();
5948 workspace_b.read_with(cx_b, |workspace, cx| {
5949 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
5950 });
5951
5952 // Changes to client A's editor are reflected on client B.
5953 editor_a1.update(cx_a, |editor, cx| {
5954 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
5955 });
5956 deterministic.run_until_parked();
5957 editor_b1.read_with(cx_b, |editor, cx| {
5958 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
5959 });
5960
5961 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
5962 deterministic.run_until_parked();
5963 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
5964
5965 editor_a1.update(cx_a, |editor, cx| {
5966 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
5967 editor.set_scroll_position(vec2f(0., 100.), cx);
5968 });
5969 deterministic.run_until_parked();
5970 editor_b1.read_with(cx_b, |editor, cx| {
5971 assert_eq!(editor.selections.ranges(cx), &[3..3]);
5972 });
5973
5974 // After unfollowing, client B stops receiving updates from client A.
5975 workspace_b.update(cx_b, |workspace, cx| {
5976 workspace.unfollow(&workspace.active_pane().clone(), cx)
5977 });
5978 workspace_a.update(cx_a, |workspace, cx| {
5979 workspace.activate_item(&editor_a2, cx)
5980 });
5981 deterministic.run_until_parked();
5982 assert_eq!(
5983 workspace_b.read_with(cx_b, |workspace, cx| workspace
5984 .active_item(cx)
5985 .unwrap()
5986 .id()),
5987 editor_b1.id()
5988 );
5989
5990 // Client A starts following client B.
5991 workspace_a
5992 .update(cx_a, |workspace, cx| {
5993 workspace
5994 .toggle_follow(&ToggleFollow(client_b_id), cx)
5995 .unwrap()
5996 })
5997 .await
5998 .unwrap();
5999 assert_eq!(
6000 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6001 Some(client_b_id)
6002 );
6003 assert_eq!(
6004 workspace_a.read_with(cx_a, |workspace, cx| workspace
6005 .active_item(cx)
6006 .unwrap()
6007 .id()),
6008 editor_a1.id()
6009 );
6010
6011 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
6012 let display = MacOSDisplay::new();
6013 active_call_b
6014 .update(cx_b, |call, cx| call.set_location(None, cx))
6015 .await
6016 .unwrap();
6017 active_call_b
6018 .update(cx_b, |call, cx| {
6019 call.room().unwrap().update(cx, |room, cx| {
6020 room.set_display_sources(vec![display.clone()]);
6021 room.share_screen(cx)
6022 })
6023 })
6024 .await
6025 .unwrap();
6026 deterministic.run_until_parked();
6027 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
6028 workspace
6029 .active_item(cx)
6030 .unwrap()
6031 .downcast::<SharedScreen>()
6032 .unwrap()
6033 });
6034
6035 // Client B activates Zed again, which causes the previous editor to become focused again.
6036 active_call_b
6037 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6038 .await
6039 .unwrap();
6040 deterministic.run_until_parked();
6041 workspace_a.read_with(cx_a, |workspace, cx| {
6042 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
6043 });
6044
6045 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
6046 workspace_b.update(cx_b, |workspace, cx| {
6047 workspace.activate_item(&multibuffer_editor_b, cx)
6048 });
6049 deterministic.run_until_parked();
6050 workspace_a.read_with(cx_a, |workspace, cx| {
6051 assert_eq!(
6052 workspace.active_item(cx).unwrap().id(),
6053 multibuffer_editor_a.id()
6054 )
6055 });
6056
6057 // Client B activates an external window again, and the previously-opened screen-sharing item
6058 // gets activated.
6059 active_call_b
6060 .update(cx_b, |call, cx| call.set_location(None, cx))
6061 .await
6062 .unwrap();
6063 deterministic.run_until_parked();
6064 assert_eq!(
6065 workspace_a.read_with(cx_a, |workspace, cx| workspace
6066 .active_item(cx)
6067 .unwrap()
6068 .id()),
6069 shared_screen.id()
6070 );
6071
6072 // Following interrupts when client B disconnects.
6073 client_b.disconnect(&cx_b.to_async()).unwrap();
6074 deterministic.advance_clock(RECONNECT_TIMEOUT);
6075 assert_eq!(
6076 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6077 None
6078 );
6079}
6080
6081#[gpui::test]
6082async fn test_following_tab_order(
6083 deterministic: Arc<Deterministic>,
6084 cx_a: &mut TestAppContext,
6085 cx_b: &mut TestAppContext,
6086) {
6087 cx_a.update(editor::init);
6088 cx_b.update(editor::init);
6089
6090 let mut server = TestServer::start(&deterministic).await;
6091 let client_a = server.create_client(cx_a, "user_a").await;
6092 let client_b = server.create_client(cx_b, "user_b").await;
6093 server
6094 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6095 .await;
6096 let active_call_a = cx_a.read(ActiveCall::global);
6097 let active_call_b = cx_b.read(ActiveCall::global);
6098
6099 client_a
6100 .fs
6101 .insert_tree(
6102 "/a",
6103 json!({
6104 "1.txt": "one",
6105 "2.txt": "two",
6106 "3.txt": "three",
6107 }),
6108 )
6109 .await;
6110 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6111 active_call_a
6112 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6113 .await
6114 .unwrap();
6115
6116 let project_id = active_call_a
6117 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6118 .await
6119 .unwrap();
6120 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6121 active_call_b
6122 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6123 .await
6124 .unwrap();
6125
6126 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6127 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6128
6129 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6130 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6131
6132 let client_b_id = project_a.read_with(cx_a, |project, _| {
6133 project.collaborators().values().next().unwrap().peer_id
6134 });
6135
6136 //Open 1, 3 in that order on client A
6137 workspace_a
6138 .update(cx_a, |workspace, cx| {
6139 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6140 })
6141 .await
6142 .unwrap();
6143 workspace_a
6144 .update(cx_a, |workspace, cx| {
6145 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6146 })
6147 .await
6148 .unwrap();
6149
6150 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
6151 pane.update(cx, |pane, cx| {
6152 pane.items()
6153 .map(|item| {
6154 item.project_path(cx)
6155 .unwrap()
6156 .path
6157 .to_str()
6158 .unwrap()
6159 .to_owned()
6160 })
6161 .collect::<Vec<_>>()
6162 })
6163 };
6164
6165 //Verify that the tabs opened in the order we expect
6166 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
6167
6168 //Follow client B as client A
6169 workspace_a
6170 .update(cx_a, |workspace, cx| {
6171 workspace
6172 .toggle_follow(&ToggleFollow(client_b_id), cx)
6173 .unwrap()
6174 })
6175 .await
6176 .unwrap();
6177
6178 //Open just 2 on client B
6179 workspace_b
6180 .update(cx_b, |workspace, cx| {
6181 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6182 })
6183 .await
6184 .unwrap();
6185 deterministic.run_until_parked();
6186
6187 // Verify that newly opened followed file is at the end
6188 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6189
6190 //Open just 1 on client B
6191 workspace_b
6192 .update(cx_b, |workspace, cx| {
6193 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6194 })
6195 .await
6196 .unwrap();
6197 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
6198 deterministic.run_until_parked();
6199
6200 // Verify that following into 1 did not reorder
6201 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6202}
6203
6204#[gpui::test(iterations = 10)]
6205async fn test_peers_following_each_other(
6206 deterministic: Arc<Deterministic>,
6207 cx_a: &mut TestAppContext,
6208 cx_b: &mut TestAppContext,
6209) {
6210 deterministic.forbid_parking();
6211 cx_a.update(editor::init);
6212 cx_b.update(editor::init);
6213
6214 let mut server = TestServer::start(&deterministic).await;
6215 let client_a = server.create_client(cx_a, "user_a").await;
6216 let client_b = server.create_client(cx_b, "user_b").await;
6217 server
6218 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6219 .await;
6220 let active_call_a = cx_a.read(ActiveCall::global);
6221 let active_call_b = cx_b.read(ActiveCall::global);
6222
6223 // Client A shares a project.
6224 client_a
6225 .fs
6226 .insert_tree(
6227 "/a",
6228 json!({
6229 "1.txt": "one",
6230 "2.txt": "two",
6231 "3.txt": "three",
6232 "4.txt": "four",
6233 }),
6234 )
6235 .await;
6236 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6237 active_call_a
6238 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6239 .await
6240 .unwrap();
6241 let project_id = active_call_a
6242 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6243 .await
6244 .unwrap();
6245
6246 // Client B joins the project.
6247 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6248 active_call_b
6249 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6250 .await
6251 .unwrap();
6252
6253 // Client A opens some editors.
6254 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6255 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6256 let _editor_a1 = workspace_a
6257 .update(cx_a, |workspace, cx| {
6258 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6259 })
6260 .await
6261 .unwrap()
6262 .downcast::<Editor>()
6263 .unwrap();
6264
6265 // Client B opens an editor.
6266 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6267 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6268 let _editor_b1 = workspace_b
6269 .update(cx_b, |workspace, cx| {
6270 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6271 })
6272 .await
6273 .unwrap()
6274 .downcast::<Editor>()
6275 .unwrap();
6276
6277 // Clients A and B follow each other in split panes
6278 workspace_a.update(cx_a, |workspace, cx| {
6279 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6280 let pane_a1 = pane_a1.clone();
6281 cx.defer(move |workspace, _| {
6282 assert_ne!(*workspace.active_pane(), pane_a1);
6283 });
6284 });
6285 workspace_a
6286 .update(cx_a, |workspace, cx| {
6287 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
6288 workspace
6289 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
6290 .unwrap()
6291 })
6292 .await
6293 .unwrap();
6294 workspace_b.update(cx_b, |workspace, cx| {
6295 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6296 let pane_b1 = pane_b1.clone();
6297 cx.defer(move |workspace, _| {
6298 assert_ne!(*workspace.active_pane(), pane_b1);
6299 });
6300 });
6301 workspace_b
6302 .update(cx_b, |workspace, cx| {
6303 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
6304 workspace
6305 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
6306 .unwrap()
6307 })
6308 .await
6309 .unwrap();
6310
6311 workspace_a.update(cx_a, |workspace, cx| {
6312 workspace.activate_next_pane(cx);
6313 });
6314 // Wait for focus effects to be fully flushed
6315 workspace_a.update(cx_a, |workspace, _| {
6316 assert_eq!(*workspace.active_pane(), pane_a1);
6317 });
6318
6319 workspace_a
6320 .update(cx_a, |workspace, cx| {
6321 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6322 })
6323 .await
6324 .unwrap();
6325 workspace_b.update(cx_b, |workspace, cx| {
6326 workspace.activate_next_pane(cx);
6327 });
6328
6329 workspace_b
6330 .update(cx_b, |workspace, cx| {
6331 assert_eq!(*workspace.active_pane(), pane_b1);
6332 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
6333 })
6334 .await
6335 .unwrap();
6336 cx_a.foreground().run_until_parked();
6337
6338 // Ensure leader updates don't change the active pane of followers
6339 workspace_a.read_with(cx_a, |workspace, _| {
6340 assert_eq!(*workspace.active_pane(), pane_a1);
6341 });
6342 workspace_b.read_with(cx_b, |workspace, _| {
6343 assert_eq!(*workspace.active_pane(), pane_b1);
6344 });
6345
6346 // Ensure peers following each other doesn't cause an infinite loop.
6347 assert_eq!(
6348 workspace_a.read_with(cx_a, |workspace, cx| workspace
6349 .active_item(cx)
6350 .unwrap()
6351 .project_path(cx)),
6352 Some((worktree_id, "3.txt").into())
6353 );
6354 workspace_a.update(cx_a, |workspace, cx| {
6355 assert_eq!(
6356 workspace.active_item(cx).unwrap().project_path(cx),
6357 Some((worktree_id, "3.txt").into())
6358 );
6359 workspace.activate_next_pane(cx);
6360 });
6361
6362 workspace_a.update(cx_a, |workspace, cx| {
6363 assert_eq!(
6364 workspace.active_item(cx).unwrap().project_path(cx),
6365 Some((worktree_id, "4.txt").into())
6366 );
6367 });
6368
6369 workspace_b.update(cx_b, |workspace, cx| {
6370 assert_eq!(
6371 workspace.active_item(cx).unwrap().project_path(cx),
6372 Some((worktree_id, "4.txt").into())
6373 );
6374 workspace.activate_next_pane(cx);
6375 });
6376
6377 workspace_b.update(cx_b, |workspace, cx| {
6378 assert_eq!(
6379 workspace.active_item(cx).unwrap().project_path(cx),
6380 Some((worktree_id, "3.txt").into())
6381 );
6382 });
6383}
6384
6385#[gpui::test(iterations = 10)]
6386async fn test_auto_unfollowing(
6387 deterministic: Arc<Deterministic>,
6388 cx_a: &mut TestAppContext,
6389 cx_b: &mut TestAppContext,
6390) {
6391 deterministic.forbid_parking();
6392 cx_a.update(editor::init);
6393 cx_b.update(editor::init);
6394
6395 // 2 clients connect to a server.
6396 let mut server = TestServer::start(&deterministic).await;
6397 let client_a = server.create_client(cx_a, "user_a").await;
6398 let client_b = server.create_client(cx_b, "user_b").await;
6399 server
6400 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6401 .await;
6402 let active_call_a = cx_a.read(ActiveCall::global);
6403 let active_call_b = cx_b.read(ActiveCall::global);
6404
6405 // Client A shares a project.
6406 client_a
6407 .fs
6408 .insert_tree(
6409 "/a",
6410 json!({
6411 "1.txt": "one",
6412 "2.txt": "two",
6413 "3.txt": "three",
6414 }),
6415 )
6416 .await;
6417 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6418 active_call_a
6419 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6420 .await
6421 .unwrap();
6422
6423 let project_id = active_call_a
6424 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6425 .await
6426 .unwrap();
6427 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6428 active_call_b
6429 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6430 .await
6431 .unwrap();
6432
6433 // Client A opens some editors.
6434 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6435 let _editor_a1 = workspace_a
6436 .update(cx_a, |workspace, cx| {
6437 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6438 })
6439 .await
6440 .unwrap()
6441 .downcast::<Editor>()
6442 .unwrap();
6443
6444 // Client B starts following client A.
6445 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6446 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6447 let leader_id = project_b.read_with(cx_b, |project, _| {
6448 project.collaborators().values().next().unwrap().peer_id
6449 });
6450 workspace_b
6451 .update(cx_b, |workspace, cx| {
6452 workspace
6453 .toggle_follow(&ToggleFollow(leader_id), cx)
6454 .unwrap()
6455 })
6456 .await
6457 .unwrap();
6458 assert_eq!(
6459 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6460 Some(leader_id)
6461 );
6462 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
6463 workspace
6464 .active_item(cx)
6465 .unwrap()
6466 .downcast::<Editor>()
6467 .unwrap()
6468 });
6469
6470 // When client B moves, it automatically stops following client A.
6471 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
6472 assert_eq!(
6473 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6474 None
6475 );
6476
6477 workspace_b
6478 .update(cx_b, |workspace, cx| {
6479 workspace
6480 .toggle_follow(&ToggleFollow(leader_id), cx)
6481 .unwrap()
6482 })
6483 .await
6484 .unwrap();
6485 assert_eq!(
6486 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6487 Some(leader_id)
6488 );
6489
6490 // When client B edits, it automatically stops following client A.
6491 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
6492 assert_eq!(
6493 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6494 None
6495 );
6496
6497 workspace_b
6498 .update(cx_b, |workspace, cx| {
6499 workspace
6500 .toggle_follow(&ToggleFollow(leader_id), cx)
6501 .unwrap()
6502 })
6503 .await
6504 .unwrap();
6505 assert_eq!(
6506 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6507 Some(leader_id)
6508 );
6509
6510 // When client B scrolls, it automatically stops following client A.
6511 editor_b2.update(cx_b, |editor, cx| {
6512 editor.set_scroll_position(vec2f(0., 3.), cx)
6513 });
6514 assert_eq!(
6515 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6516 None
6517 );
6518
6519 workspace_b
6520 .update(cx_b, |workspace, cx| {
6521 workspace
6522 .toggle_follow(&ToggleFollow(leader_id), cx)
6523 .unwrap()
6524 })
6525 .await
6526 .unwrap();
6527 assert_eq!(
6528 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6529 Some(leader_id)
6530 );
6531
6532 // When client B activates a different pane, it continues following client A in the original pane.
6533 workspace_b.update(cx_b, |workspace, cx| {
6534 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
6535 });
6536 assert_eq!(
6537 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6538 Some(leader_id)
6539 );
6540
6541 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
6542 assert_eq!(
6543 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6544 Some(leader_id)
6545 );
6546
6547 // When client B activates a different item in the original pane, it automatically stops following client A.
6548 workspace_b
6549 .update(cx_b, |workspace, cx| {
6550 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6551 })
6552 .await
6553 .unwrap();
6554 assert_eq!(
6555 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6556 None
6557 );
6558}
6559
6560#[gpui::test(iterations = 10)]
6561async fn test_peers_simultaneously_following_each_other(
6562 deterministic: Arc<Deterministic>,
6563 cx_a: &mut TestAppContext,
6564 cx_b: &mut TestAppContext,
6565) {
6566 deterministic.forbid_parking();
6567 cx_a.update(editor::init);
6568 cx_b.update(editor::init);
6569
6570 let mut server = TestServer::start(&deterministic).await;
6571 let client_a = server.create_client(cx_a, "user_a").await;
6572 let client_b = server.create_client(cx_b, "user_b").await;
6573 server
6574 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6575 .await;
6576 let active_call_a = cx_a.read(ActiveCall::global);
6577
6578 client_a.fs.insert_tree("/a", json!({})).await;
6579 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
6580 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6581 let project_id = active_call_a
6582 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6583 .await
6584 .unwrap();
6585
6586 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6587 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6588
6589 deterministic.run_until_parked();
6590 let client_a_id = project_b.read_with(cx_b, |project, _| {
6591 project.collaborators().values().next().unwrap().peer_id
6592 });
6593 let client_b_id = project_a.read_with(cx_a, |project, _| {
6594 project.collaborators().values().next().unwrap().peer_id
6595 });
6596
6597 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
6598 workspace
6599 .toggle_follow(&ToggleFollow(client_b_id), cx)
6600 .unwrap()
6601 });
6602 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
6603 workspace
6604 .toggle_follow(&ToggleFollow(client_a_id), cx)
6605 .unwrap()
6606 });
6607
6608 futures::try_join!(a_follow_b, b_follow_a).unwrap();
6609 workspace_a.read_with(cx_a, |workspace, _| {
6610 assert_eq!(
6611 workspace.leader_for_pane(workspace.active_pane()),
6612 Some(client_b_id)
6613 );
6614 });
6615 workspace_b.read_with(cx_b, |workspace, _| {
6616 assert_eq!(
6617 workspace.leader_for_pane(workspace.active_pane()),
6618 Some(client_a_id)
6619 );
6620 });
6621}
6622
6623#[derive(Debug, Eq, PartialEq)]
6624struct RoomParticipants {
6625 remote: Vec<String>,
6626 pending: Vec<String>,
6627}
6628
6629fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
6630 room.read_with(cx, |room, _| {
6631 let mut remote = room
6632 .remote_participants()
6633 .iter()
6634 .map(|(_, participant)| participant.user.github_login.clone())
6635 .collect::<Vec<_>>();
6636 let mut pending = room
6637 .pending_participants()
6638 .iter()
6639 .map(|user| user.github_login.clone())
6640 .collect::<Vec<_>>();
6641 remote.sort();
6642 pending.sort();
6643 RoomParticipants { remote, pending }
6644 })
6645}