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