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