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 executor.run_until_parked();
3069
3070 // As client B, join that project and observe the local settings.
3071 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3072
3073 let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
3074 executor.run_until_parked();
3075 cx_b.read(|cx| {
3076 let store = cx.global::<SettingsStore>();
3077 assert_eq!(
3078 store
3079 .local_settings(worktree_b.read(cx).id().to_usize())
3080 .collect::<Vec<_>>(),
3081 &[
3082 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3083 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3084 ]
3085 )
3086 });
3087
3088 // As client A, update a settings file. As Client B, see the changed settings.
3089 client_a
3090 .fs()
3091 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3092 .await;
3093 executor.run_until_parked();
3094 cx_b.read(|cx| {
3095 let store = cx.global::<SettingsStore>();
3096 assert_eq!(
3097 store
3098 .local_settings(worktree_b.read(cx).id().to_usize())
3099 .collect::<Vec<_>>(),
3100 &[
3101 (Path::new("").into(), r#"{}"#.to_string()),
3102 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3103 ]
3104 )
3105 });
3106
3107 // As client A, create and remove some settings files. As client B, see the changed settings.
3108 client_a
3109 .fs()
3110 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3111 .await
3112 .unwrap();
3113 client_a
3114 .fs()
3115 .create_dir("/dir/b/.zed".as_ref())
3116 .await
3117 .unwrap();
3118 client_a
3119 .fs()
3120 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3121 .await;
3122 executor.run_until_parked();
3123 cx_b.read(|cx| {
3124 let store = cx.global::<SettingsStore>();
3125 assert_eq!(
3126 store
3127 .local_settings(worktree_b.read(cx).id().to_usize())
3128 .collect::<Vec<_>>(),
3129 &[
3130 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3131 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3132 ]
3133 )
3134 });
3135
3136 // As client B, disconnect.
3137 server.forbid_connections();
3138 server.disconnect_client(client_b.peer_id().unwrap());
3139
3140 // As client A, change and remove settings files while client B is disconnected.
3141 client_a
3142 .fs()
3143 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3144 .await;
3145 client_a
3146 .fs()
3147 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3148 .await
3149 .unwrap();
3150 executor.run_until_parked();
3151
3152 // As client B, reconnect and see the changed settings.
3153 server.allow_connections();
3154 executor.advance_clock(RECEIVE_TIMEOUT);
3155 cx_b.read(|cx| {
3156 let store = cx.global::<SettingsStore>();
3157 assert_eq!(
3158 store
3159 .local_settings(worktree_b.read(cx).id().to_usize())
3160 .collect::<Vec<_>>(),
3161 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3162 )
3163 });
3164}
3165
3166#[gpui::test(iterations = 10)]
3167async fn test_buffer_conflict_after_save(
3168 executor: BackgroundExecutor,
3169 cx_a: &mut TestAppContext,
3170 cx_b: &mut TestAppContext,
3171) {
3172 let mut server = TestServer::start(executor.clone()).await;
3173 let client_a = server.create_client(cx_a, "user_a").await;
3174 let client_b = server.create_client(cx_b, "user_b").await;
3175 server
3176 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3177 .await;
3178 let active_call_a = cx_a.read(ActiveCall::global);
3179
3180 client_a
3181 .fs()
3182 .insert_tree(
3183 "/dir",
3184 json!({
3185 "a.txt": "a-contents",
3186 }),
3187 )
3188 .await;
3189 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3190 let project_id = active_call_a
3191 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3192 .await
3193 .unwrap();
3194 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3195
3196 // Open a buffer as client B
3197 let buffer_b = project_b
3198 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3199 .await
3200 .unwrap();
3201
3202 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3203
3204 buffer_b.read_with(cx_b, |buf, _| {
3205 assert!(buf.is_dirty());
3206 assert!(!buf.has_conflict());
3207 });
3208
3209 project_b
3210 .update(cx_b, |project, cx| {
3211 project.save_buffer(buffer_b.clone(), cx)
3212 })
3213 .await
3214 .unwrap();
3215
3216 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3217
3218 buffer_b.read_with(cx_b, |buf, _| {
3219 assert!(!buf.has_conflict());
3220 });
3221
3222 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3223
3224 buffer_b.read_with(cx_b, |buf, _| {
3225 assert!(buf.is_dirty());
3226 assert!(!buf.has_conflict());
3227 });
3228}
3229
3230#[gpui::test(iterations = 10)]
3231async fn test_buffer_reloading(
3232 executor: BackgroundExecutor,
3233 cx_a: &mut TestAppContext,
3234 cx_b: &mut TestAppContext,
3235) {
3236 let mut server = TestServer::start(executor.clone()).await;
3237 let client_a = server.create_client(cx_a, "user_a").await;
3238 let client_b = server.create_client(cx_b, "user_b").await;
3239 server
3240 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3241 .await;
3242 let active_call_a = cx_a.read(ActiveCall::global);
3243
3244 client_a
3245 .fs()
3246 .insert_tree(
3247 "/dir",
3248 json!({
3249 "a.txt": "a\nb\nc",
3250 }),
3251 )
3252 .await;
3253 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3254 let project_id = active_call_a
3255 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3256 .await
3257 .unwrap();
3258 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3259
3260 // Open a buffer as client B
3261 let buffer_b = project_b
3262 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3263 .await
3264 .unwrap();
3265
3266 buffer_b.read_with(cx_b, |buf, _| {
3267 assert!(!buf.is_dirty());
3268 assert!(!buf.has_conflict());
3269 assert_eq!(buf.line_ending(), LineEnding::Unix);
3270 });
3271
3272 let new_contents = Rope::from("d\ne\nf");
3273 client_a
3274 .fs()
3275 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3276 .await
3277 .unwrap();
3278
3279 executor.run_until_parked();
3280
3281 buffer_b.read_with(cx_b, |buf, _| {
3282 assert_eq!(buf.text(), new_contents.to_string());
3283 assert!(!buf.is_dirty());
3284 assert!(!buf.has_conflict());
3285 assert_eq!(buf.line_ending(), LineEnding::Windows);
3286 });
3287}
3288
3289#[gpui::test(iterations = 10)]
3290async fn test_editing_while_guest_opens_buffer(
3291 executor: BackgroundExecutor,
3292 cx_a: &mut TestAppContext,
3293 cx_b: &mut TestAppContext,
3294) {
3295 let mut server = TestServer::start(executor.clone()).await;
3296 let client_a = server.create_client(cx_a, "user_a").await;
3297 let client_b = server.create_client(cx_b, "user_b").await;
3298 server
3299 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3300 .await;
3301 let active_call_a = cx_a.read(ActiveCall::global);
3302
3303 client_a
3304 .fs()
3305 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3306 .await;
3307 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3308 let project_id = active_call_a
3309 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3310 .await
3311 .unwrap();
3312 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3313
3314 // Open a buffer as client A
3315 let buffer_a = project_a
3316 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3317 .await
3318 .unwrap();
3319
3320 // Start opening the same buffer as client B
3321 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3322 let buffer_b = cx_b.executor().spawn(open_buffer);
3323
3324 // Edit the buffer as client A while client B is still opening it.
3325 cx_b.executor().simulate_random_delay().await;
3326 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3327 cx_b.executor().simulate_random_delay().await;
3328 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3329
3330 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3331 let buffer_b = buffer_b.await.unwrap();
3332 executor.run_until_parked();
3333
3334 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3335}
3336
3337#[gpui::test(iterations = 10)]
3338async fn test_leaving_worktree_while_opening_buffer(
3339 executor: BackgroundExecutor,
3340 cx_a: &mut TestAppContext,
3341 cx_b: &mut TestAppContext,
3342) {
3343 let mut server = TestServer::start(executor.clone()).await;
3344 let client_a = server.create_client(cx_a, "user_a").await;
3345 let client_b = server.create_client(cx_b, "user_b").await;
3346 server
3347 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3348 .await;
3349 let active_call_a = cx_a.read(ActiveCall::global);
3350
3351 client_a
3352 .fs()
3353 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3354 .await;
3355 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3356 let project_id = active_call_a
3357 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3358 .await
3359 .unwrap();
3360 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3361
3362 // See that a guest has joined as client A.
3363 executor.run_until_parked();
3364
3365 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3366
3367 // Begin opening a buffer as client B, but leave the project before the open completes.
3368 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3369 let buffer_b = cx_b.executor().spawn(open_buffer);
3370 cx_b.update(|_| drop(project_b));
3371 drop(buffer_b);
3372
3373 // See that the guest has left.
3374 executor.run_until_parked();
3375
3376 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3377}
3378
3379#[gpui::test(iterations = 10)]
3380async fn test_canceling_buffer_opening(
3381 executor: BackgroundExecutor,
3382 cx_a: &mut TestAppContext,
3383 cx_b: &mut TestAppContext,
3384) {
3385 let mut server = TestServer::start(executor.clone()).await;
3386 let client_a = server.create_client(cx_a, "user_a").await;
3387 let client_b = server.create_client(cx_b, "user_b").await;
3388 server
3389 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3390 .await;
3391 let active_call_a = cx_a.read(ActiveCall::global);
3392
3393 client_a
3394 .fs()
3395 .insert_tree(
3396 "/dir",
3397 json!({
3398 "a.txt": "abc",
3399 }),
3400 )
3401 .await;
3402 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3403 let project_id = active_call_a
3404 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3405 .await
3406 .unwrap();
3407 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3408
3409 let buffer_a = project_a
3410 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3411 .await
3412 .unwrap();
3413
3414 // Open a buffer as client B but cancel after a random amount of time.
3415 let buffer_b = project_b.update(cx_b, |p, cx| {
3416 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3417 });
3418 executor.simulate_random_delay().await;
3419 drop(buffer_b);
3420
3421 // Try opening the same buffer again as client B, and ensure we can
3422 // still do it despite the cancellation above.
3423 let buffer_b = project_b
3424 .update(cx_b, |p, cx| {
3425 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3426 })
3427 .await
3428 .unwrap();
3429
3430 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3431}
3432
3433#[gpui::test(iterations = 10)]
3434async fn test_leaving_project(
3435 executor: BackgroundExecutor,
3436 cx_a: &mut TestAppContext,
3437 cx_b: &mut TestAppContext,
3438 cx_c: &mut TestAppContext,
3439) {
3440 let mut server = TestServer::start(executor.clone()).await;
3441 let client_a = server.create_client(cx_a, "user_a").await;
3442 let client_b = server.create_client(cx_b, "user_b").await;
3443 let client_c = server.create_client(cx_c, "user_c").await;
3444 server
3445 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3446 .await;
3447 let active_call_a = cx_a.read(ActiveCall::global);
3448
3449 client_a
3450 .fs()
3451 .insert_tree(
3452 "/a",
3453 json!({
3454 "a.txt": "a-contents",
3455 "b.txt": "b-contents",
3456 }),
3457 )
3458 .await;
3459 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3460 let project_id = active_call_a
3461 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3462 .await
3463 .unwrap();
3464 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3465 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3466
3467 // Client A sees that a guest has joined.
3468 executor.run_until_parked();
3469
3470 project_a.read_with(cx_a, |project, _| {
3471 assert_eq!(project.collaborators().len(), 2);
3472 });
3473
3474 project_b1.read_with(cx_b, |project, _| {
3475 assert_eq!(project.collaborators().len(), 2);
3476 });
3477
3478 project_c.read_with(cx_c, |project, _| {
3479 assert_eq!(project.collaborators().len(), 2);
3480 });
3481
3482 // Client B opens a buffer.
3483 let buffer_b1 = project_b1
3484 .update(cx_b, |project, cx| {
3485 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
3486 project.open_buffer((worktree_id, "a.txt"), cx)
3487 })
3488 .await
3489 .unwrap();
3490
3491 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3492
3493 // Drop client B's project and ensure client A and client C observe client B leaving.
3494 cx_b.update(|_| drop(project_b1));
3495 executor.run_until_parked();
3496
3497 project_a.read_with(cx_a, |project, _| {
3498 assert_eq!(project.collaborators().len(), 1);
3499 });
3500
3501 project_c.read_with(cx_c, |project, _| {
3502 assert_eq!(project.collaborators().len(), 1);
3503 });
3504
3505 // Client B re-joins the project and can open buffers as before.
3506 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3507 executor.run_until_parked();
3508
3509 project_a.read_with(cx_a, |project, _| {
3510 assert_eq!(project.collaborators().len(), 2);
3511 });
3512
3513 project_b2.read_with(cx_b, |project, _| {
3514 assert_eq!(project.collaborators().len(), 2);
3515 });
3516
3517 project_c.read_with(cx_c, |project, _| {
3518 assert_eq!(project.collaborators().len(), 2);
3519 });
3520
3521 let buffer_b2 = project_b2
3522 .update(cx_b, |project, cx| {
3523 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
3524 project.open_buffer((worktree_id, "a.txt"), cx)
3525 })
3526 .await
3527 .unwrap();
3528
3529 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3530
3531 // Drop client B's connection and ensure client A and client C observe client B leaving.
3532 client_b.disconnect(&cx_b.to_async());
3533 executor.advance_clock(RECONNECT_TIMEOUT);
3534
3535 project_a.read_with(cx_a, |project, _| {
3536 assert_eq!(project.collaborators().len(), 1);
3537 });
3538
3539 project_b2.read_with(cx_b, |project, _| {
3540 assert!(project.is_disconnected());
3541 });
3542
3543 project_c.read_with(cx_c, |project, _| {
3544 assert_eq!(project.collaborators().len(), 1);
3545 });
3546
3547 // Client B can't join the project, unless they re-join the room.
3548 cx_b.spawn(|cx| {
3549 Project::remote(
3550 project_id,
3551 client_b.app_state.client.clone(),
3552 client_b.user_store().clone(),
3553 client_b.language_registry().clone(),
3554 FakeFs::new(cx.background_executor().clone()),
3555 ChannelRole::Member,
3556 cx,
3557 )
3558 })
3559 .await
3560 .unwrap_err();
3561
3562 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3563 client_c.wait_for_current_user(cx_c).await;
3564 server.forbid_connections();
3565 server.disconnect_client(client_c.peer_id().unwrap());
3566 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3567 executor.run_until_parked();
3568
3569 project_a.read_with(cx_a, |project, _| {
3570 assert_eq!(project.collaborators().len(), 0);
3571 });
3572
3573 project_b2.read_with(cx_b, |project, _| {
3574 assert!(project.is_disconnected());
3575 });
3576
3577 project_c.read_with(cx_c, |project, _| {
3578 assert!(project.is_disconnected());
3579 });
3580}
3581
3582#[gpui::test(iterations = 10)]
3583async fn test_collaborating_with_diagnostics(
3584 executor: BackgroundExecutor,
3585 cx_a: &mut TestAppContext,
3586 cx_b: &mut TestAppContext,
3587 cx_c: &mut TestAppContext,
3588) {
3589 let mut server = TestServer::start(executor.clone()).await;
3590 let client_a = server.create_client(cx_a, "user_a").await;
3591 let client_b = server.create_client(cx_b, "user_b").await;
3592 let client_c = server.create_client(cx_c, "user_c").await;
3593 server
3594 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3595 .await;
3596 let active_call_a = cx_a.read(ActiveCall::global);
3597
3598 // Set up a fake language server.
3599 let mut language = Language::new(
3600 LanguageConfig {
3601 name: "Rust".into(),
3602 path_suffixes: vec!["rs".to_string()],
3603 ..Default::default()
3604 },
3605 Some(tree_sitter_rust::language()),
3606 );
3607 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3608 client_a.language_registry().add(Arc::new(language));
3609
3610 // Share a project as client A
3611 client_a
3612 .fs()
3613 .insert_tree(
3614 "/a",
3615 json!({
3616 "a.rs": "let one = two",
3617 "other.rs": "",
3618 }),
3619 )
3620 .await;
3621 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3622
3623 // Cause the language server to start.
3624 let _buffer = project_a
3625 .update(cx_a, |project, cx| {
3626 project.open_buffer(
3627 ProjectPath {
3628 worktree_id,
3629 path: Path::new("other.rs").into(),
3630 },
3631 cx,
3632 )
3633 })
3634 .await
3635 .unwrap();
3636
3637 // Simulate a language server reporting errors for a file.
3638 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3639 fake_language_server
3640 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3641 .await;
3642 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3643 lsp::PublishDiagnosticsParams {
3644 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3645 version: None,
3646 diagnostics: vec![lsp::Diagnostic {
3647 severity: Some(lsp::DiagnosticSeverity::WARNING),
3648 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3649 message: "message 0".to_string(),
3650 ..Default::default()
3651 }],
3652 },
3653 );
3654
3655 // Client A shares the project and, simultaneously, the language server
3656 // publishes a diagnostic. This is done to ensure that the server always
3657 // observes the latest diagnostics for a worktree.
3658 let project_id = active_call_a
3659 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3660 .await
3661 .unwrap();
3662 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3663 lsp::PublishDiagnosticsParams {
3664 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3665 version: None,
3666 diagnostics: vec![lsp::Diagnostic {
3667 severity: Some(lsp::DiagnosticSeverity::ERROR),
3668 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3669 message: "message 1".to_string(),
3670 ..Default::default()
3671 }],
3672 },
3673 );
3674
3675 // Join the worktree as client B.
3676 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3677
3678 // Wait for server to see the diagnostics update.
3679 executor.run_until_parked();
3680
3681 // Ensure client B observes the new diagnostics.
3682
3683 project_b.read_with(cx_b, |project, cx| {
3684 assert_eq!(
3685 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3686 &[(
3687 ProjectPath {
3688 worktree_id,
3689 path: Arc::from(Path::new("a.rs")),
3690 },
3691 LanguageServerId(0),
3692 DiagnosticSummary {
3693 error_count: 1,
3694 warning_count: 0,
3695 ..Default::default()
3696 },
3697 )]
3698 )
3699 });
3700
3701 // Join project as client C and observe the diagnostics.
3702 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3703 let project_c_diagnostic_summaries =
3704 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3705 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
3706 })));
3707 project_c.update(cx_c, |_, cx| {
3708 let summaries = project_c_diagnostic_summaries.clone();
3709 cx.subscribe(&project_c, {
3710 move |p, _, event, cx| {
3711 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3712 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
3713 }
3714 }
3715 })
3716 .detach();
3717 });
3718
3719 executor.run_until_parked();
3720 assert_eq!(
3721 project_c_diagnostic_summaries.borrow().as_slice(),
3722 &[(
3723 ProjectPath {
3724 worktree_id,
3725 path: Arc::from(Path::new("a.rs")),
3726 },
3727 LanguageServerId(0),
3728 DiagnosticSummary {
3729 error_count: 1,
3730 warning_count: 0,
3731 ..Default::default()
3732 },
3733 )]
3734 );
3735
3736 // Simulate a language server reporting more errors for a file.
3737 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3738 lsp::PublishDiagnosticsParams {
3739 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3740 version: None,
3741 diagnostics: vec![
3742 lsp::Diagnostic {
3743 severity: Some(lsp::DiagnosticSeverity::ERROR),
3744 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3745 message: "message 1".to_string(),
3746 ..Default::default()
3747 },
3748 lsp::Diagnostic {
3749 severity: Some(lsp::DiagnosticSeverity::WARNING),
3750 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3751 message: "message 2".to_string(),
3752 ..Default::default()
3753 },
3754 ],
3755 },
3756 );
3757
3758 // Clients B and C get the updated summaries
3759 executor.run_until_parked();
3760
3761 project_b.read_with(cx_b, |project, cx| {
3762 assert_eq!(
3763 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3764 [(
3765 ProjectPath {
3766 worktree_id,
3767 path: Arc::from(Path::new("a.rs")),
3768 },
3769 LanguageServerId(0),
3770 DiagnosticSummary {
3771 error_count: 1,
3772 warning_count: 1,
3773 },
3774 )]
3775 );
3776 });
3777
3778 project_c.read_with(cx_c, |project, cx| {
3779 assert_eq!(
3780 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3781 [(
3782 ProjectPath {
3783 worktree_id,
3784 path: Arc::from(Path::new("a.rs")),
3785 },
3786 LanguageServerId(0),
3787 DiagnosticSummary {
3788 error_count: 1,
3789 warning_count: 1,
3790 },
3791 )]
3792 );
3793 });
3794
3795 // Open the file with the errors on client B. They should be present.
3796 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
3797 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
3798
3799 buffer_b.read_with(cx_b, |buffer, _| {
3800 assert_eq!(
3801 buffer
3802 .snapshot()
3803 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3804 .collect::<Vec<_>>(),
3805 &[
3806 DiagnosticEntry {
3807 range: Point::new(0, 4)..Point::new(0, 7),
3808 diagnostic: Diagnostic {
3809 group_id: 2,
3810 message: "message 1".to_string(),
3811 severity: lsp::DiagnosticSeverity::ERROR,
3812 is_primary: true,
3813 ..Default::default()
3814 }
3815 },
3816 DiagnosticEntry {
3817 range: Point::new(0, 10)..Point::new(0, 13),
3818 diagnostic: Diagnostic {
3819 group_id: 3,
3820 severity: lsp::DiagnosticSeverity::WARNING,
3821 message: "message 2".to_string(),
3822 is_primary: true,
3823 ..Default::default()
3824 }
3825 }
3826 ]
3827 );
3828 });
3829
3830 // Simulate a language server reporting no errors for a file.
3831 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3832 lsp::PublishDiagnosticsParams {
3833 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3834 version: None,
3835 diagnostics: vec![],
3836 },
3837 );
3838 executor.run_until_parked();
3839
3840 project_a.read_with(cx_a, |project, cx| {
3841 assert_eq!(
3842 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3843 []
3844 )
3845 });
3846
3847 project_b.read_with(cx_b, |project, cx| {
3848 assert_eq!(
3849 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3850 []
3851 )
3852 });
3853
3854 project_c.read_with(cx_c, |project, cx| {
3855 assert_eq!(
3856 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3857 []
3858 )
3859 });
3860}
3861
3862#[gpui::test(iterations = 10)]
3863async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
3864 executor: BackgroundExecutor,
3865 cx_a: &mut TestAppContext,
3866 cx_b: &mut TestAppContext,
3867) {
3868 let mut server = TestServer::start(executor.clone()).await;
3869 let client_a = server.create_client(cx_a, "user_a").await;
3870 let client_b = server.create_client(cx_b, "user_b").await;
3871 server
3872 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3873 .await;
3874
3875 // Set up a fake language server.
3876 let mut language = Language::new(
3877 LanguageConfig {
3878 name: "Rust".into(),
3879 path_suffixes: vec!["rs".to_string()],
3880 ..Default::default()
3881 },
3882 Some(tree_sitter_rust::language()),
3883 );
3884 let mut fake_language_servers = language
3885 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3886 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
3887 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
3888 ..Default::default()
3889 }))
3890 .await;
3891 client_a.language_registry().add(Arc::new(language));
3892
3893 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
3894 client_a
3895 .fs()
3896 .insert_tree(
3897 "/test",
3898 json!({
3899 "one.rs": "const ONE: usize = 1;",
3900 "two.rs": "const TWO: usize = 2;",
3901 "three.rs": "const THREE: usize = 3;",
3902 "four.rs": "const FOUR: usize = 3;",
3903 "five.rs": "const FIVE: usize = 3;",
3904 }),
3905 )
3906 .await;
3907
3908 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
3909
3910 // Share a project as client A
3911 let active_call_a = cx_a.read(ActiveCall::global);
3912 let project_id = active_call_a
3913 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3914 .await
3915 .unwrap();
3916
3917 // Join the project as client B and open all three files.
3918 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3919 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
3920 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
3921 }))
3922 .await
3923 .unwrap();
3924
3925 // Simulate a language server reporting errors for a file.
3926 let fake_language_server = fake_language_servers.next().await.unwrap();
3927 fake_language_server
3928 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
3929 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3930 })
3931 .await
3932 .unwrap();
3933 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3934 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3935 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
3936 lsp::WorkDoneProgressBegin {
3937 title: "Progress Began".into(),
3938 ..Default::default()
3939 },
3940 )),
3941 });
3942 for file_name in file_names {
3943 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3944 lsp::PublishDiagnosticsParams {
3945 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
3946 version: None,
3947 diagnostics: vec![lsp::Diagnostic {
3948 severity: Some(lsp::DiagnosticSeverity::WARNING),
3949 source: Some("the-disk-based-diagnostics-source".into()),
3950 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3951 message: "message one".to_string(),
3952 ..Default::default()
3953 }],
3954 },
3955 );
3956 }
3957 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3958 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3959 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
3960 lsp::WorkDoneProgressEnd { message: None },
3961 )),
3962 });
3963
3964 // When the "disk base diagnostics finished" message is received, the buffers'
3965 // diagnostics are expected to be present.
3966 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
3967 project_b.update(cx_b, {
3968 let project_b = project_b.clone();
3969 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
3970 move |_, cx| {
3971 cx.subscribe(&project_b, move |_, _, event, cx| {
3972 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3973 disk_based_diagnostics_finished.store(true, SeqCst);
3974 for buffer in &guest_buffers {
3975 assert_eq!(
3976 buffer
3977 .read(cx)
3978 .snapshot()
3979 .diagnostics_in_range::<_, usize>(0..5, false)
3980 .count(),
3981 1,
3982 "expected a diagnostic for buffer {:?}",
3983 buffer.read(cx).file().unwrap().path(),
3984 );
3985 }
3986 }
3987 })
3988 .detach();
3989 }
3990 });
3991
3992 executor.run_until_parked();
3993 assert!(disk_based_diagnostics_finished.load(SeqCst));
3994}
3995
3996#[gpui::test(iterations = 10)]
3997async fn test_reloading_buffer_manually(
3998 executor: BackgroundExecutor,
3999 cx_a: &mut TestAppContext,
4000 cx_b: &mut TestAppContext,
4001) {
4002 let mut server = TestServer::start(executor.clone()).await;
4003 let client_a = server.create_client(cx_a, "user_a").await;
4004 let client_b = server.create_client(cx_b, "user_b").await;
4005 server
4006 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4007 .await;
4008 let active_call_a = cx_a.read(ActiveCall::global);
4009
4010 client_a
4011 .fs()
4012 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4013 .await;
4014 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4015 let buffer_a = project_a
4016 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4017 .await
4018 .unwrap();
4019 let project_id = active_call_a
4020 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4021 .await
4022 .unwrap();
4023
4024 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4025
4026 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4027 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4028 buffer_b.update(cx_b, |buffer, cx| {
4029 buffer.edit([(4..7, "six")], None, cx);
4030 buffer.edit([(10..11, "6")], None, cx);
4031 assert_eq!(buffer.text(), "let six = 6;");
4032 assert!(buffer.is_dirty());
4033 assert!(!buffer.has_conflict());
4034 });
4035 executor.run_until_parked();
4036
4037 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4038
4039 client_a
4040 .fs()
4041 .save(
4042 "/a/a.rs".as_ref(),
4043 &Rope::from("let seven = 7;"),
4044 LineEnding::Unix,
4045 )
4046 .await
4047 .unwrap();
4048 executor.run_until_parked();
4049
4050 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4051
4052 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4053
4054 project_b
4055 .update(cx_b, |project, cx| {
4056 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4057 })
4058 .await
4059 .unwrap();
4060
4061 buffer_a.read_with(cx_a, |buffer, _| {
4062 assert_eq!(buffer.text(), "let seven = 7;");
4063 assert!(!buffer.is_dirty());
4064 assert!(!buffer.has_conflict());
4065 });
4066
4067 buffer_b.read_with(cx_b, |buffer, _| {
4068 assert_eq!(buffer.text(), "let seven = 7;");
4069 assert!(!buffer.is_dirty());
4070 assert!(!buffer.has_conflict());
4071 });
4072
4073 buffer_a.update(cx_a, |buffer, cx| {
4074 // Undoing on the host is a no-op when the reload was initiated by the guest.
4075 buffer.undo(cx);
4076 assert_eq!(buffer.text(), "let seven = 7;");
4077 assert!(!buffer.is_dirty());
4078 assert!(!buffer.has_conflict());
4079 });
4080 buffer_b.update(cx_b, |buffer, cx| {
4081 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4082 buffer.undo(cx);
4083 assert_eq!(buffer.text(), "let six = 6;");
4084 assert!(buffer.is_dirty());
4085 assert!(!buffer.has_conflict());
4086 });
4087}
4088
4089#[gpui::test(iterations = 10)]
4090async fn test_formatting_buffer(
4091 executor: BackgroundExecutor,
4092 cx_a: &mut TestAppContext,
4093 cx_b: &mut TestAppContext,
4094) {
4095 executor.allow_parking();
4096 let mut server = TestServer::start(executor.clone()).await;
4097 let client_a = server.create_client(cx_a, "user_a").await;
4098 let client_b = server.create_client(cx_b, "user_b").await;
4099 server
4100 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4101 .await;
4102 let active_call_a = cx_a.read(ActiveCall::global);
4103
4104 // Set up a fake language server.
4105 let mut language = Language::new(
4106 LanguageConfig {
4107 name: "Rust".into(),
4108 path_suffixes: vec!["rs".to_string()],
4109 ..Default::default()
4110 },
4111 Some(tree_sitter_rust::language()),
4112 );
4113 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4114 client_a.language_registry().add(Arc::new(language));
4115
4116 // Here we insert a fake tree with a directory that exists on disk. This is needed
4117 // because later we'll invoke a command, which requires passing a working directory
4118 // that points to a valid location on disk.
4119 let directory = env::current_dir().unwrap();
4120 client_a
4121 .fs()
4122 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4123 .await;
4124 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4125 let project_id = active_call_a
4126 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4127 .await
4128 .unwrap();
4129 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4130
4131 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4132 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4133
4134 let fake_language_server = fake_language_servers.next().await.unwrap();
4135 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4136 Ok(Some(vec![
4137 lsp::TextEdit {
4138 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4139 new_text: "h".to_string(),
4140 },
4141 lsp::TextEdit {
4142 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4143 new_text: "y".to_string(),
4144 },
4145 ]))
4146 });
4147
4148 project_b
4149 .update(cx_b, |project, cx| {
4150 project.format(
4151 HashSet::from_iter([buffer_b.clone()]),
4152 true,
4153 FormatTrigger::Save,
4154 cx,
4155 )
4156 })
4157 .await
4158 .unwrap();
4159
4160 // The edits from the LSP are applied, and a final newline is added.
4161 assert_eq!(
4162 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4163 "let honey = \"two\"\n"
4164 );
4165
4166 // Ensure buffer can be formatted using an external command. Notice how the
4167 // host's configuration is honored as opposed to using the guest's settings.
4168 cx_a.update(|cx| {
4169 cx.update_global(|store: &mut SettingsStore, cx| {
4170 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4171 file.defaults.formatter = Some(Formatter::External {
4172 command: "awk".into(),
4173 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4174 });
4175 });
4176 });
4177 });
4178 project_b
4179 .update(cx_b, |project, cx| {
4180 project.format(
4181 HashSet::from_iter([buffer_b.clone()]),
4182 true,
4183 FormatTrigger::Save,
4184 cx,
4185 )
4186 })
4187 .await
4188 .unwrap();
4189 assert_eq!(
4190 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4191 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4192 );
4193}
4194
4195#[gpui::test(iterations = 10)]
4196async fn test_prettier_formatting_buffer(
4197 executor: BackgroundExecutor,
4198 cx_a: &mut TestAppContext,
4199 cx_b: &mut TestAppContext,
4200) {
4201 let mut server = TestServer::start(executor.clone()).await;
4202 let client_a = server.create_client(cx_a, "user_a").await;
4203 let client_b = server.create_client(cx_b, "user_b").await;
4204 server
4205 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4206 .await;
4207 let active_call_a = cx_a.read(ActiveCall::global);
4208
4209 // Set up a fake language server.
4210 let mut language = Language::new(
4211 LanguageConfig {
4212 name: "Rust".into(),
4213 path_suffixes: vec!["rs".to_string()],
4214 prettier_parser_name: Some("test_parser".to_string()),
4215 ..Default::default()
4216 },
4217 Some(tree_sitter_rust::language()),
4218 );
4219 let test_plugin = "test_plugin";
4220 let mut fake_language_servers = language
4221 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4222 prettier_plugins: vec![test_plugin],
4223 ..Default::default()
4224 }))
4225 .await;
4226 let language = Arc::new(language);
4227 client_a.language_registry().add(Arc::clone(&language));
4228
4229 // Here we insert a fake tree with a directory that exists on disk. This is needed
4230 // because later we'll invoke a command, which requires passing a working directory
4231 // that points to a valid location on disk.
4232 let directory = env::current_dir().unwrap();
4233 let buffer_text = "let one = \"two\"";
4234 client_a
4235 .fs()
4236 .insert_tree(&directory, json!({ "a.rs": buffer_text }))
4237 .await;
4238 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4239 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4240 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4241 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4242
4243 let project_id = active_call_a
4244 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4245 .await
4246 .unwrap();
4247 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4248 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4249 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4250
4251 cx_a.update(|cx| {
4252 cx.update_global(|store: &mut SettingsStore, cx| {
4253 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4254 file.defaults.formatter = Some(Formatter::Auto);
4255 });
4256 });
4257 });
4258 cx_b.update(|cx| {
4259 cx.update_global(|store: &mut SettingsStore, cx| {
4260 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4261 file.defaults.formatter = Some(Formatter::LanguageServer);
4262 });
4263 });
4264 });
4265 let fake_language_server = fake_language_servers.next().await.unwrap();
4266 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4267 panic!(
4268 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4269 )
4270 });
4271
4272 project_b
4273 .update(cx_b, |project, cx| {
4274 project.format(
4275 HashSet::from_iter([buffer_b.clone()]),
4276 true,
4277 FormatTrigger::Save,
4278 cx,
4279 )
4280 })
4281 .await
4282 .unwrap();
4283
4284 executor.run_until_parked();
4285 assert_eq!(
4286 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4287 buffer_text.to_string() + "\n" + prettier_format_suffix,
4288 "Prettier formatting was not applied to client buffer after client's request"
4289 );
4290
4291 project_a
4292 .update(cx_a, |project, cx| {
4293 project.format(
4294 HashSet::from_iter([buffer_a.clone()]),
4295 true,
4296 FormatTrigger::Manual,
4297 cx,
4298 )
4299 })
4300 .await
4301 .unwrap();
4302
4303 executor.run_until_parked();
4304 assert_eq!(
4305 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4306 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4307 "Prettier formatting was not applied to client buffer after host's request"
4308 );
4309}
4310
4311#[gpui::test(iterations = 10)]
4312async fn test_definition(
4313 executor: BackgroundExecutor,
4314 cx_a: &mut TestAppContext,
4315 cx_b: &mut TestAppContext,
4316) {
4317 let mut server = TestServer::start(executor.clone()).await;
4318 let client_a = server.create_client(cx_a, "user_a").await;
4319 let client_b = server.create_client(cx_b, "user_b").await;
4320 server
4321 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4322 .await;
4323 let active_call_a = cx_a.read(ActiveCall::global);
4324
4325 // Set up a fake language server.
4326 let mut language = Language::new(
4327 LanguageConfig {
4328 name: "Rust".into(),
4329 path_suffixes: vec!["rs".to_string()],
4330 ..Default::default()
4331 },
4332 Some(tree_sitter_rust::language()),
4333 );
4334 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4335 client_a.language_registry().add(Arc::new(language));
4336
4337 client_a
4338 .fs()
4339 .insert_tree(
4340 "/root",
4341 json!({
4342 "dir-1": {
4343 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4344 },
4345 "dir-2": {
4346 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4347 "c.rs": "type T2 = usize;",
4348 }
4349 }),
4350 )
4351 .await;
4352 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4353 let project_id = active_call_a
4354 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4355 .await
4356 .unwrap();
4357 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4358
4359 // Open the file on client B.
4360 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4361 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4362
4363 // Request the definition of a symbol as the guest.
4364 let fake_language_server = fake_language_servers.next().await.unwrap();
4365 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4366 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4367 lsp::Location::new(
4368 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4369 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4370 ),
4371 )))
4372 });
4373
4374 let definitions_1 = project_b
4375 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4376 .await
4377 .unwrap();
4378 cx_b.read(|cx| {
4379 assert_eq!(definitions_1.len(), 1);
4380 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4381 let target_buffer = definitions_1[0].target.buffer.read(cx);
4382 assert_eq!(
4383 target_buffer.text(),
4384 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4385 );
4386 assert_eq!(
4387 definitions_1[0].target.range.to_point(target_buffer),
4388 Point::new(0, 6)..Point::new(0, 9)
4389 );
4390 });
4391
4392 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4393 // the previous call to `definition`.
4394 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4395 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4396 lsp::Location::new(
4397 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4398 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4399 ),
4400 )))
4401 });
4402
4403 let definitions_2 = project_b
4404 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4405 .await
4406 .unwrap();
4407 cx_b.read(|cx| {
4408 assert_eq!(definitions_2.len(), 1);
4409 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4410 let target_buffer = definitions_2[0].target.buffer.read(cx);
4411 assert_eq!(
4412 target_buffer.text(),
4413 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4414 );
4415 assert_eq!(
4416 definitions_2[0].target.range.to_point(target_buffer),
4417 Point::new(1, 6)..Point::new(1, 11)
4418 );
4419 });
4420 assert_eq!(
4421 definitions_1[0].target.buffer,
4422 definitions_2[0].target.buffer
4423 );
4424
4425 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4426 |req, _| async move {
4427 assert_eq!(
4428 req.text_document_position_params.position,
4429 lsp::Position::new(0, 7)
4430 );
4431 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4432 lsp::Location::new(
4433 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4434 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4435 ),
4436 )))
4437 },
4438 );
4439
4440 let type_definitions = project_b
4441 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4442 .await
4443 .unwrap();
4444 cx_b.read(|cx| {
4445 assert_eq!(type_definitions.len(), 1);
4446 let target_buffer = type_definitions[0].target.buffer.read(cx);
4447 assert_eq!(target_buffer.text(), "type T2 = usize;");
4448 assert_eq!(
4449 type_definitions[0].target.range.to_point(target_buffer),
4450 Point::new(0, 5)..Point::new(0, 7)
4451 );
4452 });
4453}
4454
4455#[gpui::test(iterations = 10)]
4456async fn test_references(
4457 executor: BackgroundExecutor,
4458 cx_a: &mut TestAppContext,
4459 cx_b: &mut TestAppContext,
4460) {
4461 let mut server = TestServer::start(executor.clone()).await;
4462 let client_a = server.create_client(cx_a, "user_a").await;
4463 let client_b = server.create_client(cx_b, "user_b").await;
4464 server
4465 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4466 .await;
4467 let active_call_a = cx_a.read(ActiveCall::global);
4468
4469 // Set up a fake language server.
4470 let mut language = Language::new(
4471 LanguageConfig {
4472 name: "Rust".into(),
4473 path_suffixes: vec!["rs".to_string()],
4474 ..Default::default()
4475 },
4476 Some(tree_sitter_rust::language()),
4477 );
4478 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4479 client_a.language_registry().add(Arc::new(language));
4480
4481 client_a
4482 .fs()
4483 .insert_tree(
4484 "/root",
4485 json!({
4486 "dir-1": {
4487 "one.rs": "const ONE: usize = 1;",
4488 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4489 },
4490 "dir-2": {
4491 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4492 }
4493 }),
4494 )
4495 .await;
4496 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4497 let project_id = active_call_a
4498 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4499 .await
4500 .unwrap();
4501 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4502
4503 // Open the file on client B.
4504 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4505 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4506
4507 // Request references to a symbol as the guest.
4508 let fake_language_server = fake_language_servers.next().await.unwrap();
4509 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4510 assert_eq!(
4511 params.text_document_position.text_document.uri.as_str(),
4512 "file:///root/dir-1/one.rs"
4513 );
4514 Ok(Some(vec![
4515 lsp::Location {
4516 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4517 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4518 },
4519 lsp::Location {
4520 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4521 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4522 },
4523 lsp::Location {
4524 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4525 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4526 },
4527 ]))
4528 });
4529
4530 let references = project_b
4531 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4532 .await
4533 .unwrap();
4534 cx_b.read(|cx| {
4535 assert_eq!(references.len(), 3);
4536 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4537
4538 let two_buffer = references[0].buffer.read(cx);
4539 let three_buffer = references[2].buffer.read(cx);
4540 assert_eq!(
4541 two_buffer.file().unwrap().path().as_ref(),
4542 Path::new("two.rs")
4543 );
4544 assert_eq!(references[1].buffer, references[0].buffer);
4545 assert_eq!(
4546 three_buffer.file().unwrap().full_path(cx),
4547 Path::new("/root/dir-2/three.rs")
4548 );
4549
4550 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4551 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4552 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4553 });
4554}
4555
4556#[gpui::test(iterations = 10)]
4557async fn test_project_search(
4558 executor: BackgroundExecutor,
4559 cx_a: &mut TestAppContext,
4560 cx_b: &mut TestAppContext,
4561) {
4562 let mut server = TestServer::start(executor.clone()).await;
4563 let client_a = server.create_client(cx_a, "user_a").await;
4564 let client_b = server.create_client(cx_b, "user_b").await;
4565 server
4566 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4567 .await;
4568 let active_call_a = cx_a.read(ActiveCall::global);
4569
4570 client_a
4571 .fs()
4572 .insert_tree(
4573 "/root",
4574 json!({
4575 "dir-1": {
4576 "a": "hello world",
4577 "b": "goodnight moon",
4578 "c": "a world of goo",
4579 "d": "world champion of clown world",
4580 },
4581 "dir-2": {
4582 "e": "disney world is fun",
4583 }
4584 }),
4585 )
4586 .await;
4587 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4588 let (worktree_2, _) = project_a
4589 .update(cx_a, |p, cx| {
4590 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4591 })
4592 .await
4593 .unwrap();
4594 worktree_2
4595 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4596 .await;
4597 let project_id = active_call_a
4598 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4599 .await
4600 .unwrap();
4601
4602 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4603
4604 // Perform a search as the guest.
4605 let mut results = HashMap::default();
4606 let mut search_rx = project_b.update(cx_b, |project, cx| {
4607 project.search(
4608 SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
4609 cx,
4610 )
4611 });
4612 while let Some((buffer, ranges)) = search_rx.next().await {
4613 results.entry(buffer).or_insert(ranges);
4614 }
4615
4616 let mut ranges_by_path = results
4617 .into_iter()
4618 .map(|(buffer, ranges)| {
4619 buffer.read_with(cx_b, |buffer, cx| {
4620 let path = buffer.file().unwrap().full_path(cx);
4621 let offset_ranges = ranges
4622 .into_iter()
4623 .map(|range| range.to_offset(buffer))
4624 .collect::<Vec<_>>();
4625 (path, offset_ranges)
4626 })
4627 })
4628 .collect::<Vec<_>>();
4629 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4630
4631 assert_eq!(
4632 ranges_by_path,
4633 &[
4634 (PathBuf::from("dir-1/a"), vec![6..11]),
4635 (PathBuf::from("dir-1/c"), vec![2..7]),
4636 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4637 (PathBuf::from("dir-2/e"), vec![7..12]),
4638 ]
4639 );
4640}
4641
4642#[gpui::test(iterations = 10)]
4643async fn test_document_highlights(
4644 executor: BackgroundExecutor,
4645 cx_a: &mut TestAppContext,
4646 cx_b: &mut TestAppContext,
4647) {
4648 let mut server = TestServer::start(executor.clone()).await;
4649 let client_a = server.create_client(cx_a, "user_a").await;
4650 let client_b = server.create_client(cx_b, "user_b").await;
4651 server
4652 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4653 .await;
4654 let active_call_a = cx_a.read(ActiveCall::global);
4655
4656 client_a
4657 .fs()
4658 .insert_tree(
4659 "/root-1",
4660 json!({
4661 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4662 }),
4663 )
4664 .await;
4665
4666 // Set up a fake language server.
4667 let mut language = Language::new(
4668 LanguageConfig {
4669 name: "Rust".into(),
4670 path_suffixes: vec!["rs".to_string()],
4671 ..Default::default()
4672 },
4673 Some(tree_sitter_rust::language()),
4674 );
4675 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4676 client_a.language_registry().add(Arc::new(language));
4677
4678 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4679 let project_id = active_call_a
4680 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4681 .await
4682 .unwrap();
4683 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4684
4685 // Open the file on client B.
4686 let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4687 let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
4688
4689 // Request document highlights as the guest.
4690 let fake_language_server = fake_language_servers.next().await.unwrap();
4691 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4692 |params, _| async move {
4693 assert_eq!(
4694 params
4695 .text_document_position_params
4696 .text_document
4697 .uri
4698 .as_str(),
4699 "file:///root-1/main.rs"
4700 );
4701 assert_eq!(
4702 params.text_document_position_params.position,
4703 lsp::Position::new(0, 34)
4704 );
4705 Ok(Some(vec![
4706 lsp::DocumentHighlight {
4707 kind: Some(lsp::DocumentHighlightKind::WRITE),
4708 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4709 },
4710 lsp::DocumentHighlight {
4711 kind: Some(lsp::DocumentHighlightKind::READ),
4712 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4713 },
4714 lsp::DocumentHighlight {
4715 kind: Some(lsp::DocumentHighlightKind::READ),
4716 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4717 },
4718 ]))
4719 },
4720 );
4721
4722 let highlights = project_b
4723 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4724 .await
4725 .unwrap();
4726
4727 buffer_b.read_with(cx_b, |buffer, _| {
4728 let snapshot = buffer.snapshot();
4729
4730 let highlights = highlights
4731 .into_iter()
4732 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4733 .collect::<Vec<_>>();
4734 assert_eq!(
4735 highlights,
4736 &[
4737 (lsp::DocumentHighlightKind::WRITE, 10..16),
4738 (lsp::DocumentHighlightKind::READ, 32..38),
4739 (lsp::DocumentHighlightKind::READ, 41..47)
4740 ]
4741 )
4742 });
4743}
4744
4745#[gpui::test(iterations = 10)]
4746async fn test_lsp_hover(
4747 executor: BackgroundExecutor,
4748 cx_a: &mut TestAppContext,
4749 cx_b: &mut TestAppContext,
4750) {
4751 let mut server = TestServer::start(executor.clone()).await;
4752 let client_a = server.create_client(cx_a, "user_a").await;
4753 let client_b = server.create_client(cx_b, "user_b").await;
4754 server
4755 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4756 .await;
4757 let active_call_a = cx_a.read(ActiveCall::global);
4758
4759 client_a
4760 .fs()
4761 .insert_tree(
4762 "/root-1",
4763 json!({
4764 "main.rs": "use std::collections::HashMap;",
4765 }),
4766 )
4767 .await;
4768
4769 // Set up a fake language server.
4770 let mut language = Language::new(
4771 LanguageConfig {
4772 name: "Rust".into(),
4773 path_suffixes: vec!["rs".to_string()],
4774 ..Default::default()
4775 },
4776 Some(tree_sitter_rust::language()),
4777 );
4778 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4779 client_a.language_registry().add(Arc::new(language));
4780
4781 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4782 let project_id = active_call_a
4783 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4784 .await
4785 .unwrap();
4786 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4787
4788 // Open the file as the guest
4789 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4790 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4791
4792 // Request hover information as the guest.
4793 let fake_language_server = fake_language_servers.next().await.unwrap();
4794 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4795 |params, _| async move {
4796 assert_eq!(
4797 params
4798 .text_document_position_params
4799 .text_document
4800 .uri
4801 .as_str(),
4802 "file:///root-1/main.rs"
4803 );
4804 assert_eq!(
4805 params.text_document_position_params.position,
4806 lsp::Position::new(0, 22)
4807 );
4808 Ok(Some(lsp::Hover {
4809 contents: lsp::HoverContents::Array(vec![
4810 lsp::MarkedString::String("Test hover content.".to_string()),
4811 lsp::MarkedString::LanguageString(lsp::LanguageString {
4812 language: "Rust".to_string(),
4813 value: "let foo = 42;".to_string(),
4814 }),
4815 ]),
4816 range: Some(lsp::Range::new(
4817 lsp::Position::new(0, 22),
4818 lsp::Position::new(0, 29),
4819 )),
4820 }))
4821 },
4822 );
4823
4824 let hover_info = project_b
4825 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
4826 .await
4827 .unwrap()
4828 .unwrap();
4829
4830 buffer_b.read_with(cx_b, |buffer, _| {
4831 let snapshot = buffer.snapshot();
4832 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
4833 assert_eq!(
4834 hover_info.contents,
4835 vec![
4836 project::HoverBlock {
4837 text: "Test hover content.".to_string(),
4838 kind: HoverBlockKind::Markdown,
4839 },
4840 project::HoverBlock {
4841 text: "let foo = 42;".to_string(),
4842 kind: HoverBlockKind::Code {
4843 language: "Rust".to_string()
4844 },
4845 }
4846 ]
4847 );
4848 });
4849}
4850
4851#[gpui::test(iterations = 10)]
4852async fn test_project_symbols(
4853 executor: BackgroundExecutor,
4854 cx_a: &mut TestAppContext,
4855 cx_b: &mut TestAppContext,
4856) {
4857 let mut server = TestServer::start(executor.clone()).await;
4858 let client_a = server.create_client(cx_a, "user_a").await;
4859 let client_b = server.create_client(cx_b, "user_b").await;
4860 server
4861 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4862 .await;
4863 let active_call_a = cx_a.read(ActiveCall::global);
4864
4865 // Set up a fake language server.
4866 let mut language = Language::new(
4867 LanguageConfig {
4868 name: "Rust".into(),
4869 path_suffixes: vec!["rs".to_string()],
4870 ..Default::default()
4871 },
4872 Some(tree_sitter_rust::language()),
4873 );
4874 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4875 client_a.language_registry().add(Arc::new(language));
4876
4877 client_a
4878 .fs()
4879 .insert_tree(
4880 "/code",
4881 json!({
4882 "crate-1": {
4883 "one.rs": "const ONE: usize = 1;",
4884 },
4885 "crate-2": {
4886 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
4887 },
4888 "private": {
4889 "passwords.txt": "the-password",
4890 }
4891 }),
4892 )
4893 .await;
4894 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
4895 let project_id = active_call_a
4896 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4897 .await
4898 .unwrap();
4899 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4900
4901 // Cause the language server to start.
4902 let open_buffer_task =
4903 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4904 let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
4905
4906 let fake_language_server = fake_language_servers.next().await.unwrap();
4907 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
4908 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
4909 #[allow(deprecated)]
4910 lsp::SymbolInformation {
4911 name: "TWO".into(),
4912 location: lsp::Location {
4913 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
4914 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4915 },
4916 kind: lsp::SymbolKind::CONSTANT,
4917 tags: None,
4918 container_name: None,
4919 deprecated: None,
4920 },
4921 ])))
4922 });
4923
4924 // Request the definition of a symbol as the guest.
4925 let symbols = project_b
4926 .update(cx_b, |p, cx| p.symbols("two", cx))
4927 .await
4928 .unwrap();
4929 assert_eq!(symbols.len(), 1);
4930 assert_eq!(symbols[0].name, "TWO");
4931
4932 // Open one of the returned symbols.
4933 let buffer_b_2 = project_b
4934 .update(cx_b, |project, cx| {
4935 project.open_buffer_for_symbol(&symbols[0], cx)
4936 })
4937 .await
4938 .unwrap();
4939
4940 buffer_b_2.read_with(cx_b, |buffer, cx| {
4941 assert_eq!(
4942 buffer.file().unwrap().full_path(cx),
4943 Path::new("/code/crate-2/two.rs")
4944 );
4945 });
4946
4947 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
4948 let mut fake_symbol = symbols[0].clone();
4949 fake_symbol.path.path = Path::new("/code/secrets").into();
4950 let error = project_b
4951 .update(cx_b, |project, cx| {
4952 project.open_buffer_for_symbol(&fake_symbol, cx)
4953 })
4954 .await
4955 .unwrap_err();
4956 assert!(error.to_string().contains("invalid symbol signature"));
4957}
4958
4959#[gpui::test(iterations = 10)]
4960async fn test_open_buffer_while_getting_definition_pointing_to_it(
4961 executor: BackgroundExecutor,
4962 cx_a: &mut TestAppContext,
4963 cx_b: &mut TestAppContext,
4964 mut rng: StdRng,
4965) {
4966 let mut server = TestServer::start(executor.clone()).await;
4967 let client_a = server.create_client(cx_a, "user_a").await;
4968 let client_b = server.create_client(cx_b, "user_b").await;
4969 server
4970 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4971 .await;
4972 let active_call_a = cx_a.read(ActiveCall::global);
4973
4974 // Set up a fake language server.
4975 let mut language = Language::new(
4976 LanguageConfig {
4977 name: "Rust".into(),
4978 path_suffixes: vec!["rs".to_string()],
4979 ..Default::default()
4980 },
4981 Some(tree_sitter_rust::language()),
4982 );
4983 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4984 client_a.language_registry().add(Arc::new(language));
4985
4986 client_a
4987 .fs()
4988 .insert_tree(
4989 "/root",
4990 json!({
4991 "a.rs": "const ONE: usize = b::TWO;",
4992 "b.rs": "const TWO: usize = 2",
4993 }),
4994 )
4995 .await;
4996 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
4997 let project_id = active_call_a
4998 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4999 .await
5000 .unwrap();
5001 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5002
5003 let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
5004 let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5005
5006 let fake_language_server = fake_language_servers.next().await.unwrap();
5007 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5008 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5009 lsp::Location::new(
5010 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5011 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5012 ),
5013 )))
5014 });
5015
5016 let definitions;
5017 let buffer_b2;
5018 if rng.gen() {
5019 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5020 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5021 } else {
5022 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5023 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5024 }
5025
5026 let buffer_b2 = buffer_b2.await.unwrap();
5027 let definitions = definitions.await.unwrap();
5028 assert_eq!(definitions.len(), 1);
5029 assert_eq!(definitions[0].target.buffer, buffer_b2);
5030}
5031
5032#[gpui::test(iterations = 10)]
5033async fn test_contacts(
5034 executor: BackgroundExecutor,
5035 cx_a: &mut TestAppContext,
5036 cx_b: &mut TestAppContext,
5037 cx_c: &mut TestAppContext,
5038 cx_d: &mut TestAppContext,
5039) {
5040 let mut server = TestServer::start(executor.clone()).await;
5041 let client_a = server.create_client(cx_a, "user_a").await;
5042 let client_b = server.create_client(cx_b, "user_b").await;
5043 let client_c = server.create_client(cx_c, "user_c").await;
5044 let client_d = server.create_client(cx_d, "user_d").await;
5045 server
5046 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5047 .await;
5048 let active_call_a = cx_a.read(ActiveCall::global);
5049 let active_call_b = cx_b.read(ActiveCall::global);
5050 let active_call_c = cx_c.read(ActiveCall::global);
5051 let _active_call_d = cx_d.read(ActiveCall::global);
5052
5053 executor.run_until_parked();
5054 assert_eq!(
5055 contacts(&client_a, cx_a),
5056 [
5057 ("user_b".to_string(), "online", "free"),
5058 ("user_c".to_string(), "online", "free")
5059 ]
5060 );
5061 assert_eq!(
5062 contacts(&client_b, cx_b),
5063 [
5064 ("user_a".to_string(), "online", "free"),
5065 ("user_c".to_string(), "online", "free")
5066 ]
5067 );
5068 assert_eq!(
5069 contacts(&client_c, cx_c),
5070 [
5071 ("user_a".to_string(), "online", "free"),
5072 ("user_b".to_string(), "online", "free")
5073 ]
5074 );
5075 assert_eq!(contacts(&client_d, cx_d), []);
5076
5077 server.disconnect_client(client_c.peer_id().unwrap());
5078 server.forbid_connections();
5079 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5080 assert_eq!(
5081 contacts(&client_a, cx_a),
5082 [
5083 ("user_b".to_string(), "online", "free"),
5084 ("user_c".to_string(), "offline", "free")
5085 ]
5086 );
5087 assert_eq!(
5088 contacts(&client_b, cx_b),
5089 [
5090 ("user_a".to_string(), "online", "free"),
5091 ("user_c".to_string(), "offline", "free")
5092 ]
5093 );
5094 assert_eq!(contacts(&client_c, cx_c), []);
5095 assert_eq!(contacts(&client_d, cx_d), []);
5096
5097 server.allow_connections();
5098 client_c
5099 .authenticate_and_connect(false, &cx_c.to_async())
5100 .await
5101 .unwrap();
5102
5103 executor.run_until_parked();
5104 assert_eq!(
5105 contacts(&client_a, cx_a),
5106 [
5107 ("user_b".to_string(), "online", "free"),
5108 ("user_c".to_string(), "online", "free")
5109 ]
5110 );
5111 assert_eq!(
5112 contacts(&client_b, cx_b),
5113 [
5114 ("user_a".to_string(), "online", "free"),
5115 ("user_c".to_string(), "online", "free")
5116 ]
5117 );
5118 assert_eq!(
5119 contacts(&client_c, cx_c),
5120 [
5121 ("user_a".to_string(), "online", "free"),
5122 ("user_b".to_string(), "online", "free")
5123 ]
5124 );
5125 assert_eq!(contacts(&client_d, cx_d), []);
5126
5127 active_call_a
5128 .update(cx_a, |call, cx| {
5129 call.invite(client_b.user_id().unwrap(), None, cx)
5130 })
5131 .await
5132 .unwrap();
5133 executor.run_until_parked();
5134 assert_eq!(
5135 contacts(&client_a, cx_a),
5136 [
5137 ("user_b".to_string(), "online", "busy"),
5138 ("user_c".to_string(), "online", "free")
5139 ]
5140 );
5141 assert_eq!(
5142 contacts(&client_b, cx_b),
5143 [
5144 ("user_a".to_string(), "online", "busy"),
5145 ("user_c".to_string(), "online", "free")
5146 ]
5147 );
5148 assert_eq!(
5149 contacts(&client_c, cx_c),
5150 [
5151 ("user_a".to_string(), "online", "busy"),
5152 ("user_b".to_string(), "online", "busy")
5153 ]
5154 );
5155 assert_eq!(contacts(&client_d, cx_d), []);
5156
5157 // Client B and client D become contacts while client B is being called.
5158 server
5159 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5160 .await;
5161 executor.run_until_parked();
5162 assert_eq!(
5163 contacts(&client_a, cx_a),
5164 [
5165 ("user_b".to_string(), "online", "busy"),
5166 ("user_c".to_string(), "online", "free")
5167 ]
5168 );
5169 assert_eq!(
5170 contacts(&client_b, cx_b),
5171 [
5172 ("user_a".to_string(), "online", "busy"),
5173 ("user_c".to_string(), "online", "free"),
5174 ("user_d".to_string(), "online", "free"),
5175 ]
5176 );
5177 assert_eq!(
5178 contacts(&client_c, cx_c),
5179 [
5180 ("user_a".to_string(), "online", "busy"),
5181 ("user_b".to_string(), "online", "busy")
5182 ]
5183 );
5184 assert_eq!(
5185 contacts(&client_d, cx_d),
5186 [("user_b".to_string(), "online", "busy")]
5187 );
5188
5189 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5190 executor.run_until_parked();
5191 assert_eq!(
5192 contacts(&client_a, cx_a),
5193 [
5194 ("user_b".to_string(), "online", "free"),
5195 ("user_c".to_string(), "online", "free")
5196 ]
5197 );
5198 assert_eq!(
5199 contacts(&client_b, cx_b),
5200 [
5201 ("user_a".to_string(), "online", "free"),
5202 ("user_c".to_string(), "online", "free"),
5203 ("user_d".to_string(), "online", "free")
5204 ]
5205 );
5206 assert_eq!(
5207 contacts(&client_c, cx_c),
5208 [
5209 ("user_a".to_string(), "online", "free"),
5210 ("user_b".to_string(), "online", "free")
5211 ]
5212 );
5213 assert_eq!(
5214 contacts(&client_d, cx_d),
5215 [("user_b".to_string(), "online", "free")]
5216 );
5217
5218 active_call_c
5219 .update(cx_c, |call, cx| {
5220 call.invite(client_a.user_id().unwrap(), None, cx)
5221 })
5222 .await
5223 .unwrap();
5224 executor.run_until_parked();
5225 assert_eq!(
5226 contacts(&client_a, cx_a),
5227 [
5228 ("user_b".to_string(), "online", "free"),
5229 ("user_c".to_string(), "online", "busy")
5230 ]
5231 );
5232 assert_eq!(
5233 contacts(&client_b, cx_b),
5234 [
5235 ("user_a".to_string(), "online", "busy"),
5236 ("user_c".to_string(), "online", "busy"),
5237 ("user_d".to_string(), "online", "free")
5238 ]
5239 );
5240 assert_eq!(
5241 contacts(&client_c, cx_c),
5242 [
5243 ("user_a".to_string(), "online", "busy"),
5244 ("user_b".to_string(), "online", "free")
5245 ]
5246 );
5247 assert_eq!(
5248 contacts(&client_d, cx_d),
5249 [("user_b".to_string(), "online", "free")]
5250 );
5251
5252 active_call_a
5253 .update(cx_a, |call, cx| call.accept_incoming(cx))
5254 .await
5255 .unwrap();
5256 executor.run_until_parked();
5257 assert_eq!(
5258 contacts(&client_a, cx_a),
5259 [
5260 ("user_b".to_string(), "online", "free"),
5261 ("user_c".to_string(), "online", "busy")
5262 ]
5263 );
5264 assert_eq!(
5265 contacts(&client_b, cx_b),
5266 [
5267 ("user_a".to_string(), "online", "busy"),
5268 ("user_c".to_string(), "online", "busy"),
5269 ("user_d".to_string(), "online", "free")
5270 ]
5271 );
5272 assert_eq!(
5273 contacts(&client_c, cx_c),
5274 [
5275 ("user_a".to_string(), "online", "busy"),
5276 ("user_b".to_string(), "online", "free")
5277 ]
5278 );
5279 assert_eq!(
5280 contacts(&client_d, cx_d),
5281 [("user_b".to_string(), "online", "free")]
5282 );
5283
5284 active_call_a
5285 .update(cx_a, |call, cx| {
5286 call.invite(client_b.user_id().unwrap(), None, cx)
5287 })
5288 .await
5289 .unwrap();
5290 executor.run_until_parked();
5291 assert_eq!(
5292 contacts(&client_a, cx_a),
5293 [
5294 ("user_b".to_string(), "online", "busy"),
5295 ("user_c".to_string(), "online", "busy")
5296 ]
5297 );
5298 assert_eq!(
5299 contacts(&client_b, cx_b),
5300 [
5301 ("user_a".to_string(), "online", "busy"),
5302 ("user_c".to_string(), "online", "busy"),
5303 ("user_d".to_string(), "online", "free")
5304 ]
5305 );
5306 assert_eq!(
5307 contacts(&client_c, cx_c),
5308 [
5309 ("user_a".to_string(), "online", "busy"),
5310 ("user_b".to_string(), "online", "busy")
5311 ]
5312 );
5313 assert_eq!(
5314 contacts(&client_d, cx_d),
5315 [("user_b".to_string(), "online", "busy")]
5316 );
5317
5318 active_call_a
5319 .update(cx_a, |call, cx| call.hang_up(cx))
5320 .await
5321 .unwrap();
5322 executor.run_until_parked();
5323 assert_eq!(
5324 contacts(&client_a, cx_a),
5325 [
5326 ("user_b".to_string(), "online", "free"),
5327 ("user_c".to_string(), "online", "free")
5328 ]
5329 );
5330 assert_eq!(
5331 contacts(&client_b, cx_b),
5332 [
5333 ("user_a".to_string(), "online", "free"),
5334 ("user_c".to_string(), "online", "free"),
5335 ("user_d".to_string(), "online", "free")
5336 ]
5337 );
5338 assert_eq!(
5339 contacts(&client_c, cx_c),
5340 [
5341 ("user_a".to_string(), "online", "free"),
5342 ("user_b".to_string(), "online", "free")
5343 ]
5344 );
5345 assert_eq!(
5346 contacts(&client_d, cx_d),
5347 [("user_b".to_string(), "online", "free")]
5348 );
5349
5350 active_call_a
5351 .update(cx_a, |call, cx| {
5352 call.invite(client_b.user_id().unwrap(), None, cx)
5353 })
5354 .await
5355 .unwrap();
5356 executor.run_until_parked();
5357 assert_eq!(
5358 contacts(&client_a, cx_a),
5359 [
5360 ("user_b".to_string(), "online", "busy"),
5361 ("user_c".to_string(), "online", "free")
5362 ]
5363 );
5364 assert_eq!(
5365 contacts(&client_b, cx_b),
5366 [
5367 ("user_a".to_string(), "online", "busy"),
5368 ("user_c".to_string(), "online", "free"),
5369 ("user_d".to_string(), "online", "free")
5370 ]
5371 );
5372 assert_eq!(
5373 contacts(&client_c, cx_c),
5374 [
5375 ("user_a".to_string(), "online", "busy"),
5376 ("user_b".to_string(), "online", "busy")
5377 ]
5378 );
5379 assert_eq!(
5380 contacts(&client_d, cx_d),
5381 [("user_b".to_string(), "online", "busy")]
5382 );
5383
5384 server.forbid_connections();
5385 server.disconnect_client(client_a.peer_id().unwrap());
5386 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5387 assert_eq!(contacts(&client_a, cx_a), []);
5388 assert_eq!(
5389 contacts(&client_b, cx_b),
5390 [
5391 ("user_a".to_string(), "offline", "free"),
5392 ("user_c".to_string(), "online", "free"),
5393 ("user_d".to_string(), "online", "free")
5394 ]
5395 );
5396 assert_eq!(
5397 contacts(&client_c, cx_c),
5398 [
5399 ("user_a".to_string(), "offline", "free"),
5400 ("user_b".to_string(), "online", "free")
5401 ]
5402 );
5403 assert_eq!(
5404 contacts(&client_d, cx_d),
5405 [("user_b".to_string(), "online", "free")]
5406 );
5407
5408 // Test removing a contact
5409 client_b
5410 .user_store()
5411 .update(cx_b, |store, cx| {
5412 store.remove_contact(client_c.user_id().unwrap(), cx)
5413 })
5414 .await
5415 .unwrap();
5416 executor.run_until_parked();
5417 assert_eq!(
5418 contacts(&client_b, cx_b),
5419 [
5420 ("user_a".to_string(), "offline", "free"),
5421 ("user_d".to_string(), "online", "free")
5422 ]
5423 );
5424 assert_eq!(
5425 contacts(&client_c, cx_c),
5426 [("user_a".to_string(), "offline", "free"),]
5427 );
5428
5429 fn contacts(
5430 client: &TestClient,
5431 cx: &TestAppContext,
5432 ) -> Vec<(String, &'static str, &'static str)> {
5433 client.user_store().read_with(cx, |store, _| {
5434 store
5435 .contacts()
5436 .iter()
5437 .map(|contact| {
5438 (
5439 contact.user.github_login.clone(),
5440 if contact.online { "online" } else { "offline" },
5441 if contact.busy { "busy" } else { "free" },
5442 )
5443 })
5444 .collect()
5445 })
5446 }
5447}
5448
5449#[gpui::test(iterations = 10)]
5450async fn test_contact_requests(
5451 executor: BackgroundExecutor,
5452 cx_a: &mut TestAppContext,
5453 cx_a2: &mut TestAppContext,
5454 cx_b: &mut TestAppContext,
5455 cx_b2: &mut TestAppContext,
5456 cx_c: &mut TestAppContext,
5457 cx_c2: &mut TestAppContext,
5458) {
5459 // Connect to a server as 3 clients.
5460 let mut server = TestServer::start(executor.clone()).await;
5461 let client_a = server.create_client(cx_a, "user_a").await;
5462 let client_a2 = server.create_client(cx_a2, "user_a").await;
5463 let client_b = server.create_client(cx_b, "user_b").await;
5464 let client_b2 = server.create_client(cx_b2, "user_b").await;
5465 let client_c = server.create_client(cx_c, "user_c").await;
5466 let client_c2 = server.create_client(cx_c2, "user_c").await;
5467
5468 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5469 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5470 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5471
5472 // User A and User C request that user B become their contact.
5473 client_a
5474 .user_store()
5475 .update(cx_a, |store, cx| {
5476 store.request_contact(client_b.user_id().unwrap(), cx)
5477 })
5478 .await
5479 .unwrap();
5480 client_c
5481 .user_store()
5482 .update(cx_c, |store, cx| {
5483 store.request_contact(client_b.user_id().unwrap(), cx)
5484 })
5485 .await
5486 .unwrap();
5487 executor.run_until_parked();
5488
5489 // All users see the pending request appear in all their clients.
5490 assert_eq!(
5491 client_a.summarize_contacts(cx_a).outgoing_requests,
5492 &["user_b"]
5493 );
5494 assert_eq!(
5495 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5496 &["user_b"]
5497 );
5498 assert_eq!(
5499 client_b.summarize_contacts(cx_b).incoming_requests,
5500 &["user_a", "user_c"]
5501 );
5502 assert_eq!(
5503 client_b2.summarize_contacts(cx_b2).incoming_requests,
5504 &["user_a", "user_c"]
5505 );
5506 assert_eq!(
5507 client_c.summarize_contacts(cx_c).outgoing_requests,
5508 &["user_b"]
5509 );
5510 assert_eq!(
5511 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5512 &["user_b"]
5513 );
5514
5515 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5516 disconnect_and_reconnect(&client_a, cx_a).await;
5517 disconnect_and_reconnect(&client_b, cx_b).await;
5518 disconnect_and_reconnect(&client_c, cx_c).await;
5519 executor.run_until_parked();
5520 assert_eq!(
5521 client_a.summarize_contacts(cx_a).outgoing_requests,
5522 &["user_b"]
5523 );
5524 assert_eq!(
5525 client_b.summarize_contacts(cx_b).incoming_requests,
5526 &["user_a", "user_c"]
5527 );
5528 assert_eq!(
5529 client_c.summarize_contacts(cx_c).outgoing_requests,
5530 &["user_b"]
5531 );
5532
5533 // User B accepts the request from user A.
5534 client_b
5535 .user_store()
5536 .update(cx_b, |store, cx| {
5537 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5538 })
5539 .await
5540 .unwrap();
5541
5542 executor.run_until_parked();
5543
5544 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5545 let contacts_b = client_b.summarize_contacts(cx_b);
5546 assert_eq!(contacts_b.current, &["user_a"]);
5547 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5548 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5549 assert_eq!(contacts_b2.current, &["user_a"]);
5550 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5551
5552 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5553 let contacts_a = client_a.summarize_contacts(cx_a);
5554 assert_eq!(contacts_a.current, &["user_b"]);
5555 assert!(contacts_a.outgoing_requests.is_empty());
5556 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5557 assert_eq!(contacts_a2.current, &["user_b"]);
5558 assert!(contacts_a2.outgoing_requests.is_empty());
5559
5560 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5561 disconnect_and_reconnect(&client_a, cx_a).await;
5562 disconnect_and_reconnect(&client_b, cx_b).await;
5563 disconnect_and_reconnect(&client_c, cx_c).await;
5564 executor.run_until_parked();
5565 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5566 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5567 assert_eq!(
5568 client_b.summarize_contacts(cx_b).incoming_requests,
5569 &["user_c"]
5570 );
5571 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5572 assert_eq!(
5573 client_c.summarize_contacts(cx_c).outgoing_requests,
5574 &["user_b"]
5575 );
5576
5577 // User B rejects the request from user C.
5578 client_b
5579 .user_store()
5580 .update(cx_b, |store, cx| {
5581 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5582 })
5583 .await
5584 .unwrap();
5585
5586 executor.run_until_parked();
5587
5588 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5589 let contacts_b = client_b.summarize_contacts(cx_b);
5590 assert_eq!(contacts_b.current, &["user_a"]);
5591 assert!(contacts_b.incoming_requests.is_empty());
5592 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5593 assert_eq!(contacts_b2.current, &["user_a"]);
5594 assert!(contacts_b2.incoming_requests.is_empty());
5595
5596 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5597 let contacts_c = client_c.summarize_contacts(cx_c);
5598 assert!(contacts_c.current.is_empty());
5599 assert!(contacts_c.outgoing_requests.is_empty());
5600 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5601 assert!(contacts_c2.current.is_empty());
5602 assert!(contacts_c2.outgoing_requests.is_empty());
5603
5604 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5605 disconnect_and_reconnect(&client_a, cx_a).await;
5606 disconnect_and_reconnect(&client_b, cx_b).await;
5607 disconnect_and_reconnect(&client_c, cx_c).await;
5608 executor.run_until_parked();
5609 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5610 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5611 assert!(client_b
5612 .summarize_contacts(cx_b)
5613 .incoming_requests
5614 .is_empty());
5615 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5616 assert!(client_c
5617 .summarize_contacts(cx_c)
5618 .outgoing_requests
5619 .is_empty());
5620
5621 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
5622 client.disconnect(&cx.to_async());
5623 client.clear_contacts(cx).await;
5624 client
5625 .authenticate_and_connect(false, &cx.to_async())
5626 .await
5627 .unwrap();
5628 }
5629}
5630
5631#[gpui::test(iterations = 10)]
5632async fn test_join_call_after_screen_was_shared(
5633 executor: BackgroundExecutor,
5634 cx_a: &mut TestAppContext,
5635 cx_b: &mut TestAppContext,
5636) {
5637 let mut server = TestServer::start(executor.clone()).await;
5638
5639 let client_a = server.create_client(cx_a, "user_a").await;
5640 let client_b = server.create_client(cx_b, "user_b").await;
5641 server
5642 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5643 .await;
5644
5645 let active_call_a = cx_a.read(ActiveCall::global);
5646 let active_call_b = cx_b.read(ActiveCall::global);
5647
5648 // Call users B and C from client A.
5649 active_call_a
5650 .update(cx_a, |call, cx| {
5651 call.invite(client_b.user_id().unwrap(), None, cx)
5652 })
5653 .await
5654 .unwrap();
5655
5656 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
5657 executor.run_until_parked();
5658 assert_eq!(
5659 room_participants(&room_a, cx_a),
5660 RoomParticipants {
5661 remote: Default::default(),
5662 pending: vec!["user_b".to_string()]
5663 }
5664 );
5665
5666 // User B receives the call.
5667
5668 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
5669 let call_b = incoming_call_b.next().await.unwrap().unwrap();
5670 assert_eq!(call_b.calling_user.github_login, "user_a");
5671
5672 // User A shares their screen
5673 let display = MacOSDisplay::new();
5674 active_call_a
5675 .update(cx_a, |call, cx| {
5676 call.room().unwrap().update(cx, |room, cx| {
5677 room.set_display_sources(vec![display.clone()]);
5678 room.share_screen(cx)
5679 })
5680 })
5681 .await
5682 .unwrap();
5683
5684 client_b.user_store().update(cx_b, |user_store, _| {
5685 user_store.clear_cache();
5686 });
5687
5688 // User B joins the room
5689 active_call_b
5690 .update(cx_b, |call, cx| call.accept_incoming(cx))
5691 .await
5692 .unwrap();
5693
5694 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
5695 assert!(incoming_call_b.next().await.unwrap().is_none());
5696
5697 executor.run_until_parked();
5698 assert_eq!(
5699 room_participants(&room_a, cx_a),
5700 RoomParticipants {
5701 remote: vec!["user_b".to_string()],
5702 pending: vec![],
5703 }
5704 );
5705 assert_eq!(
5706 room_participants(&room_b, cx_b),
5707 RoomParticipants {
5708 remote: vec!["user_a".to_string()],
5709 pending: vec![],
5710 }
5711 );
5712
5713 // Ensure User B sees User A's screenshare.
5714
5715 room_b.read_with(cx_b, |room, _| {
5716 assert_eq!(
5717 room.remote_participants()
5718 .get(&client_a.user_id().unwrap())
5719 .unwrap()
5720 .video_tracks
5721 .len(),
5722 1
5723 );
5724 });
5725}