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(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(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(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(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(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!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3848 });
3849
3850 project_b.read_with(cx_b, |project, cx| {
3851 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3852 });
3853
3854 project_c.read_with(cx_c, |project, cx| {
3855 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3856 });
3857}
3858
3859#[gpui::test(iterations = 10)]
3860async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
3861 executor: BackgroundExecutor,
3862 cx_a: &mut TestAppContext,
3863 cx_b: &mut TestAppContext,
3864) {
3865 let mut server = TestServer::start(executor.clone()).await;
3866 let client_a = server.create_client(cx_a, "user_a").await;
3867 let client_b = server.create_client(cx_b, "user_b").await;
3868 server
3869 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3870 .await;
3871
3872 // Set up a fake language server.
3873 let mut language = Language::new(
3874 LanguageConfig {
3875 name: "Rust".into(),
3876 path_suffixes: vec!["rs".to_string()],
3877 ..Default::default()
3878 },
3879 Some(tree_sitter_rust::language()),
3880 );
3881 let mut fake_language_servers = language
3882 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3883 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
3884 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
3885 ..Default::default()
3886 }))
3887 .await;
3888 client_a.language_registry().add(Arc::new(language));
3889
3890 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
3891 client_a
3892 .fs()
3893 .insert_tree(
3894 "/test",
3895 json!({
3896 "one.rs": "const ONE: usize = 1;",
3897 "two.rs": "const TWO: usize = 2;",
3898 "three.rs": "const THREE: usize = 3;",
3899 "four.rs": "const FOUR: usize = 3;",
3900 "five.rs": "const FIVE: usize = 3;",
3901 }),
3902 )
3903 .await;
3904
3905 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
3906
3907 // Share a project as client A
3908 let active_call_a = cx_a.read(ActiveCall::global);
3909 let project_id = active_call_a
3910 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3911 .await
3912 .unwrap();
3913
3914 // Join the project as client B and open all three files.
3915 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3916 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
3917 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
3918 }))
3919 .await
3920 .unwrap();
3921
3922 // Simulate a language server reporting errors for a file.
3923 let fake_language_server = fake_language_servers.next().await.unwrap();
3924 fake_language_server
3925 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
3926 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3927 })
3928 .await
3929 .unwrap();
3930 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3931 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3932 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
3933 lsp::WorkDoneProgressBegin {
3934 title: "Progress Began".into(),
3935 ..Default::default()
3936 },
3937 )),
3938 });
3939 for file_name in file_names {
3940 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3941 lsp::PublishDiagnosticsParams {
3942 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
3943 version: None,
3944 diagnostics: vec![lsp::Diagnostic {
3945 severity: Some(lsp::DiagnosticSeverity::WARNING),
3946 source: Some("the-disk-based-diagnostics-source".into()),
3947 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3948 message: "message one".to_string(),
3949 ..Default::default()
3950 }],
3951 },
3952 );
3953 }
3954 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3955 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3956 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
3957 lsp::WorkDoneProgressEnd { message: None },
3958 )),
3959 });
3960
3961 // When the "disk base diagnostics finished" message is received, the buffers'
3962 // diagnostics are expected to be present.
3963 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
3964 project_b.update(cx_b, {
3965 let project_b = project_b.clone();
3966 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
3967 move |_, cx| {
3968 cx.subscribe(&project_b, move |_, _, event, cx| {
3969 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3970 disk_based_diagnostics_finished.store(true, SeqCst);
3971 for buffer in &guest_buffers {
3972 assert_eq!(
3973 buffer
3974 .read(cx)
3975 .snapshot()
3976 .diagnostics_in_range::<_, usize>(0..5, false)
3977 .count(),
3978 1,
3979 "expected a diagnostic for buffer {:?}",
3980 buffer.read(cx).file().unwrap().path(),
3981 );
3982 }
3983 }
3984 })
3985 .detach();
3986 }
3987 });
3988
3989 executor.run_until_parked();
3990 assert!(disk_based_diagnostics_finished.load(SeqCst));
3991}
3992
3993#[gpui::test(iterations = 10)]
3994async fn test_reloading_buffer_manually(
3995 executor: BackgroundExecutor,
3996 cx_a: &mut TestAppContext,
3997 cx_b: &mut TestAppContext,
3998) {
3999 let mut server = TestServer::start(executor.clone()).await;
4000 let client_a = server.create_client(cx_a, "user_a").await;
4001 let client_b = server.create_client(cx_b, "user_b").await;
4002 server
4003 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4004 .await;
4005 let active_call_a = cx_a.read(ActiveCall::global);
4006
4007 client_a
4008 .fs()
4009 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4010 .await;
4011 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4012 let buffer_a = project_a
4013 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4014 .await
4015 .unwrap();
4016 let project_id = active_call_a
4017 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4018 .await
4019 .unwrap();
4020
4021 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4022
4023 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4024 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4025 buffer_b.update(cx_b, |buffer, cx| {
4026 buffer.edit([(4..7, "six")], None, cx);
4027 buffer.edit([(10..11, "6")], None, cx);
4028 assert_eq!(buffer.text(), "let six = 6;");
4029 assert!(buffer.is_dirty());
4030 assert!(!buffer.has_conflict());
4031 });
4032 executor.run_until_parked();
4033
4034 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4035
4036 client_a
4037 .fs()
4038 .save(
4039 "/a/a.rs".as_ref(),
4040 &Rope::from("let seven = 7;"),
4041 LineEnding::Unix,
4042 )
4043 .await
4044 .unwrap();
4045 executor.run_until_parked();
4046
4047 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4048
4049 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4050
4051 project_b
4052 .update(cx_b, |project, cx| {
4053 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4054 })
4055 .await
4056 .unwrap();
4057
4058 buffer_a.read_with(cx_a, |buffer, _| {
4059 assert_eq!(buffer.text(), "let seven = 7;");
4060 assert!(!buffer.is_dirty());
4061 assert!(!buffer.has_conflict());
4062 });
4063
4064 buffer_b.read_with(cx_b, |buffer, _| {
4065 assert_eq!(buffer.text(), "let seven = 7;");
4066 assert!(!buffer.is_dirty());
4067 assert!(!buffer.has_conflict());
4068 });
4069
4070 buffer_a.update(cx_a, |buffer, cx| {
4071 // Undoing on the host is a no-op when the reload was initiated by the guest.
4072 buffer.undo(cx);
4073 assert_eq!(buffer.text(), "let seven = 7;");
4074 assert!(!buffer.is_dirty());
4075 assert!(!buffer.has_conflict());
4076 });
4077 buffer_b.update(cx_b, |buffer, cx| {
4078 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4079 buffer.undo(cx);
4080 assert_eq!(buffer.text(), "let six = 6;");
4081 assert!(buffer.is_dirty());
4082 assert!(!buffer.has_conflict());
4083 });
4084}
4085
4086#[gpui::test(iterations = 10)]
4087async fn test_formatting_buffer(
4088 executor: BackgroundExecutor,
4089 cx_a: &mut TestAppContext,
4090 cx_b: &mut TestAppContext,
4091) {
4092 executor.allow_parking();
4093 let mut server = TestServer::start(executor.clone()).await;
4094 let client_a = server.create_client(cx_a, "user_a").await;
4095 let client_b = server.create_client(cx_b, "user_b").await;
4096 server
4097 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4098 .await;
4099 let active_call_a = cx_a.read(ActiveCall::global);
4100
4101 // Set up a fake language server.
4102 let mut language = Language::new(
4103 LanguageConfig {
4104 name: "Rust".into(),
4105 path_suffixes: vec!["rs".to_string()],
4106 ..Default::default()
4107 },
4108 Some(tree_sitter_rust::language()),
4109 );
4110 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4111 client_a.language_registry().add(Arc::new(language));
4112
4113 // Here we insert a fake tree with a directory that exists on disk. This is needed
4114 // because later we'll invoke a command, which requires passing a working directory
4115 // that points to a valid location on disk.
4116 let directory = env::current_dir().unwrap();
4117 client_a
4118 .fs()
4119 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4120 .await;
4121 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4122 let project_id = active_call_a
4123 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4124 .await
4125 .unwrap();
4126 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4127
4128 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4129 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4130
4131 let fake_language_server = fake_language_servers.next().await.unwrap();
4132 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4133 Ok(Some(vec![
4134 lsp::TextEdit {
4135 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4136 new_text: "h".to_string(),
4137 },
4138 lsp::TextEdit {
4139 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4140 new_text: "y".to_string(),
4141 },
4142 ]))
4143 });
4144
4145 project_b
4146 .update(cx_b, |project, cx| {
4147 project.format(
4148 HashSet::from_iter([buffer_b.clone()]),
4149 true,
4150 FormatTrigger::Save,
4151 cx,
4152 )
4153 })
4154 .await
4155 .unwrap();
4156
4157 // The edits from the LSP are applied, and a final newline is added.
4158 assert_eq!(
4159 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4160 "let honey = \"two\"\n"
4161 );
4162
4163 // Ensure buffer can be formatted using an external command. Notice how the
4164 // host's configuration is honored as opposed to using the guest's settings.
4165 cx_a.update(|cx| {
4166 cx.update_global(|store: &mut SettingsStore, cx| {
4167 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4168 file.defaults.formatter = Some(Formatter::External {
4169 command: "awk".into(),
4170 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4171 });
4172 });
4173 });
4174 });
4175 project_b
4176 .update(cx_b, |project, cx| {
4177 project.format(
4178 HashSet::from_iter([buffer_b.clone()]),
4179 true,
4180 FormatTrigger::Save,
4181 cx,
4182 )
4183 })
4184 .await
4185 .unwrap();
4186 assert_eq!(
4187 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4188 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4189 );
4190}
4191
4192#[gpui::test(iterations = 10)]
4193async fn test_prettier_formatting_buffer(
4194 executor: BackgroundExecutor,
4195 cx_a: &mut TestAppContext,
4196 cx_b: &mut TestAppContext,
4197) {
4198 let mut server = TestServer::start(executor.clone()).await;
4199 let client_a = server.create_client(cx_a, "user_a").await;
4200 let client_b = server.create_client(cx_b, "user_b").await;
4201 server
4202 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4203 .await;
4204 let active_call_a = cx_a.read(ActiveCall::global);
4205
4206 // Set up a fake language server.
4207 let mut language = Language::new(
4208 LanguageConfig {
4209 name: "Rust".into(),
4210 path_suffixes: vec!["rs".to_string()],
4211 prettier_parser_name: Some("test_parser".to_string()),
4212 ..Default::default()
4213 },
4214 Some(tree_sitter_rust::language()),
4215 );
4216 let test_plugin = "test_plugin";
4217 let mut fake_language_servers = language
4218 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4219 prettier_plugins: vec![test_plugin],
4220 ..Default::default()
4221 }))
4222 .await;
4223 let language = Arc::new(language);
4224 client_a.language_registry().add(Arc::clone(&language));
4225
4226 // Here we insert a fake tree with a directory that exists on disk. This is needed
4227 // because later we'll invoke a command, which requires passing a working directory
4228 // that points to a valid location on disk.
4229 let directory = env::current_dir().unwrap();
4230 let buffer_text = "let one = \"two\"";
4231 client_a
4232 .fs()
4233 .insert_tree(&directory, json!({ "a.rs": buffer_text }))
4234 .await;
4235 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4236 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4237 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4238 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4239
4240 let project_id = active_call_a
4241 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4242 .await
4243 .unwrap();
4244 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4245 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4246 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4247
4248 cx_a.update(|cx| {
4249 cx.update_global(|store: &mut SettingsStore, cx| {
4250 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4251 file.defaults.formatter = Some(Formatter::Auto);
4252 });
4253 });
4254 });
4255 cx_b.update(|cx| {
4256 cx.update_global(|store: &mut SettingsStore, cx| {
4257 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4258 file.defaults.formatter = Some(Formatter::LanguageServer);
4259 });
4260 });
4261 });
4262 let fake_language_server = fake_language_servers.next().await.unwrap();
4263 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4264 panic!(
4265 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4266 )
4267 });
4268
4269 project_b
4270 .update(cx_b, |project, cx| {
4271 project.format(
4272 HashSet::from_iter([buffer_b.clone()]),
4273 true,
4274 FormatTrigger::Save,
4275 cx,
4276 )
4277 })
4278 .await
4279 .unwrap();
4280
4281 executor.run_until_parked();
4282 assert_eq!(
4283 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4284 buffer_text.to_string() + "\n" + prettier_format_suffix,
4285 "Prettier formatting was not applied to client buffer after client's request"
4286 );
4287
4288 project_a
4289 .update(cx_a, |project, cx| {
4290 project.format(
4291 HashSet::from_iter([buffer_a.clone()]),
4292 true,
4293 FormatTrigger::Manual,
4294 cx,
4295 )
4296 })
4297 .await
4298 .unwrap();
4299
4300 executor.run_until_parked();
4301 assert_eq!(
4302 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4303 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4304 "Prettier formatting was not applied to client buffer after host's request"
4305 );
4306}
4307
4308#[gpui::test(iterations = 10)]
4309async fn test_definition(
4310 executor: BackgroundExecutor,
4311 cx_a: &mut TestAppContext,
4312 cx_b: &mut TestAppContext,
4313) {
4314 let mut server = TestServer::start(executor.clone()).await;
4315 let client_a = server.create_client(cx_a, "user_a").await;
4316 let client_b = server.create_client(cx_b, "user_b").await;
4317 server
4318 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4319 .await;
4320 let active_call_a = cx_a.read(ActiveCall::global);
4321
4322 // Set up a fake language server.
4323 let mut language = Language::new(
4324 LanguageConfig {
4325 name: "Rust".into(),
4326 path_suffixes: vec!["rs".to_string()],
4327 ..Default::default()
4328 },
4329 Some(tree_sitter_rust::language()),
4330 );
4331 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4332 client_a.language_registry().add(Arc::new(language));
4333
4334 client_a
4335 .fs()
4336 .insert_tree(
4337 "/root",
4338 json!({
4339 "dir-1": {
4340 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4341 },
4342 "dir-2": {
4343 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4344 "c.rs": "type T2 = usize;",
4345 }
4346 }),
4347 )
4348 .await;
4349 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4350 let project_id = active_call_a
4351 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4352 .await
4353 .unwrap();
4354 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4355
4356 // Open the file on client B.
4357 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4358 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4359
4360 // Request the definition of a symbol as the guest.
4361 let fake_language_server = fake_language_servers.next().await.unwrap();
4362 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4363 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4364 lsp::Location::new(
4365 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4366 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4367 ),
4368 )))
4369 });
4370
4371 let definitions_1 = project_b
4372 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4373 .await
4374 .unwrap();
4375 cx_b.read(|cx| {
4376 assert_eq!(definitions_1.len(), 1);
4377 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4378 let target_buffer = definitions_1[0].target.buffer.read(cx);
4379 assert_eq!(
4380 target_buffer.text(),
4381 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4382 );
4383 assert_eq!(
4384 definitions_1[0].target.range.to_point(target_buffer),
4385 Point::new(0, 6)..Point::new(0, 9)
4386 );
4387 });
4388
4389 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4390 // the previous call to `definition`.
4391 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4392 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4393 lsp::Location::new(
4394 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4395 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4396 ),
4397 )))
4398 });
4399
4400 let definitions_2 = project_b
4401 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4402 .await
4403 .unwrap();
4404 cx_b.read(|cx| {
4405 assert_eq!(definitions_2.len(), 1);
4406 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4407 let target_buffer = definitions_2[0].target.buffer.read(cx);
4408 assert_eq!(
4409 target_buffer.text(),
4410 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4411 );
4412 assert_eq!(
4413 definitions_2[0].target.range.to_point(target_buffer),
4414 Point::new(1, 6)..Point::new(1, 11)
4415 );
4416 });
4417 assert_eq!(
4418 definitions_1[0].target.buffer,
4419 definitions_2[0].target.buffer
4420 );
4421
4422 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4423 |req, _| async move {
4424 assert_eq!(
4425 req.text_document_position_params.position,
4426 lsp::Position::new(0, 7)
4427 );
4428 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4429 lsp::Location::new(
4430 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4431 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4432 ),
4433 )))
4434 },
4435 );
4436
4437 let type_definitions = project_b
4438 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4439 .await
4440 .unwrap();
4441 cx_b.read(|cx| {
4442 assert_eq!(type_definitions.len(), 1);
4443 let target_buffer = type_definitions[0].target.buffer.read(cx);
4444 assert_eq!(target_buffer.text(), "type T2 = usize;");
4445 assert_eq!(
4446 type_definitions[0].target.range.to_point(target_buffer),
4447 Point::new(0, 5)..Point::new(0, 7)
4448 );
4449 });
4450}
4451
4452#[gpui::test(iterations = 10)]
4453async fn test_references(
4454 executor: BackgroundExecutor,
4455 cx_a: &mut TestAppContext,
4456 cx_b: &mut TestAppContext,
4457) {
4458 let mut server = TestServer::start(executor.clone()).await;
4459 let client_a = server.create_client(cx_a, "user_a").await;
4460 let client_b = server.create_client(cx_b, "user_b").await;
4461 server
4462 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4463 .await;
4464 let active_call_a = cx_a.read(ActiveCall::global);
4465
4466 // Set up a fake language server.
4467 let mut language = Language::new(
4468 LanguageConfig {
4469 name: "Rust".into(),
4470 path_suffixes: vec!["rs".to_string()],
4471 ..Default::default()
4472 },
4473 Some(tree_sitter_rust::language()),
4474 );
4475 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4476 client_a.language_registry().add(Arc::new(language));
4477
4478 client_a
4479 .fs()
4480 .insert_tree(
4481 "/root",
4482 json!({
4483 "dir-1": {
4484 "one.rs": "const ONE: usize = 1;",
4485 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4486 },
4487 "dir-2": {
4488 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4489 }
4490 }),
4491 )
4492 .await;
4493 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4494 let project_id = active_call_a
4495 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4496 .await
4497 .unwrap();
4498 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4499
4500 // Open the file on client B.
4501 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4502 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4503
4504 // Request references to a symbol as the guest.
4505 let fake_language_server = fake_language_servers.next().await.unwrap();
4506 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4507 assert_eq!(
4508 params.text_document_position.text_document.uri.as_str(),
4509 "file:///root/dir-1/one.rs"
4510 );
4511 Ok(Some(vec![
4512 lsp::Location {
4513 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4514 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4515 },
4516 lsp::Location {
4517 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4518 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4519 },
4520 lsp::Location {
4521 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4522 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4523 },
4524 ]))
4525 });
4526
4527 let references = project_b
4528 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4529 .await
4530 .unwrap();
4531 cx_b.read(|cx| {
4532 assert_eq!(references.len(), 3);
4533 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4534
4535 let two_buffer = references[0].buffer.read(cx);
4536 let three_buffer = references[2].buffer.read(cx);
4537 assert_eq!(
4538 two_buffer.file().unwrap().path().as_ref(),
4539 Path::new("two.rs")
4540 );
4541 assert_eq!(references[1].buffer, references[0].buffer);
4542 assert_eq!(
4543 three_buffer.file().unwrap().full_path(cx),
4544 Path::new("/root/dir-2/three.rs")
4545 );
4546
4547 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4548 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4549 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4550 });
4551}
4552
4553#[gpui::test(iterations = 10)]
4554async fn test_project_search(
4555 executor: BackgroundExecutor,
4556 cx_a: &mut TestAppContext,
4557 cx_b: &mut TestAppContext,
4558) {
4559 let mut server = TestServer::start(executor.clone()).await;
4560 let client_a = server.create_client(cx_a, "user_a").await;
4561 let client_b = server.create_client(cx_b, "user_b").await;
4562 server
4563 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4564 .await;
4565 let active_call_a = cx_a.read(ActiveCall::global);
4566
4567 client_a
4568 .fs()
4569 .insert_tree(
4570 "/root",
4571 json!({
4572 "dir-1": {
4573 "a": "hello world",
4574 "b": "goodnight moon",
4575 "c": "a world of goo",
4576 "d": "world champion of clown world",
4577 },
4578 "dir-2": {
4579 "e": "disney world is fun",
4580 }
4581 }),
4582 )
4583 .await;
4584 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4585 let (worktree_2, _) = project_a
4586 .update(cx_a, |p, cx| {
4587 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4588 })
4589 .await
4590 .unwrap();
4591 worktree_2
4592 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4593 .await;
4594 let project_id = active_call_a
4595 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4596 .await
4597 .unwrap();
4598
4599 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4600
4601 // Perform a search as the guest.
4602 let mut results = HashMap::default();
4603 let mut search_rx = project_b.update(cx_b, |project, cx| {
4604 project.search(
4605 SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
4606 cx,
4607 )
4608 });
4609 while let Some((buffer, ranges)) = search_rx.next().await {
4610 results.entry(buffer).or_insert(ranges);
4611 }
4612
4613 let mut ranges_by_path = results
4614 .into_iter()
4615 .map(|(buffer, ranges)| {
4616 buffer.read_with(cx_b, |buffer, cx| {
4617 let path = buffer.file().unwrap().full_path(cx);
4618 let offset_ranges = ranges
4619 .into_iter()
4620 .map(|range| range.to_offset(buffer))
4621 .collect::<Vec<_>>();
4622 (path, offset_ranges)
4623 })
4624 })
4625 .collect::<Vec<_>>();
4626 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4627
4628 assert_eq!(
4629 ranges_by_path,
4630 &[
4631 (PathBuf::from("dir-1/a"), vec![6..11]),
4632 (PathBuf::from("dir-1/c"), vec![2..7]),
4633 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4634 (PathBuf::from("dir-2/e"), vec![7..12]),
4635 ]
4636 );
4637}
4638
4639#[gpui::test(iterations = 10)]
4640async fn test_document_highlights(
4641 executor: BackgroundExecutor,
4642 cx_a: &mut TestAppContext,
4643 cx_b: &mut TestAppContext,
4644) {
4645 let mut server = TestServer::start(executor.clone()).await;
4646 let client_a = server.create_client(cx_a, "user_a").await;
4647 let client_b = server.create_client(cx_b, "user_b").await;
4648 server
4649 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4650 .await;
4651 let active_call_a = cx_a.read(ActiveCall::global);
4652
4653 client_a
4654 .fs()
4655 .insert_tree(
4656 "/root-1",
4657 json!({
4658 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4659 }),
4660 )
4661 .await;
4662
4663 // Set up a fake language server.
4664 let mut language = Language::new(
4665 LanguageConfig {
4666 name: "Rust".into(),
4667 path_suffixes: vec!["rs".to_string()],
4668 ..Default::default()
4669 },
4670 Some(tree_sitter_rust::language()),
4671 );
4672 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4673 client_a.language_registry().add(Arc::new(language));
4674
4675 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4676 let project_id = active_call_a
4677 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4678 .await
4679 .unwrap();
4680 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4681
4682 // Open the file on client B.
4683 let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4684 let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
4685
4686 // Request document highlights as the guest.
4687 let fake_language_server = fake_language_servers.next().await.unwrap();
4688 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4689 |params, _| async move {
4690 assert_eq!(
4691 params
4692 .text_document_position_params
4693 .text_document
4694 .uri
4695 .as_str(),
4696 "file:///root-1/main.rs"
4697 );
4698 assert_eq!(
4699 params.text_document_position_params.position,
4700 lsp::Position::new(0, 34)
4701 );
4702 Ok(Some(vec![
4703 lsp::DocumentHighlight {
4704 kind: Some(lsp::DocumentHighlightKind::WRITE),
4705 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4706 },
4707 lsp::DocumentHighlight {
4708 kind: Some(lsp::DocumentHighlightKind::READ),
4709 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4710 },
4711 lsp::DocumentHighlight {
4712 kind: Some(lsp::DocumentHighlightKind::READ),
4713 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4714 },
4715 ]))
4716 },
4717 );
4718
4719 let highlights = project_b
4720 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4721 .await
4722 .unwrap();
4723
4724 buffer_b.read_with(cx_b, |buffer, _| {
4725 let snapshot = buffer.snapshot();
4726
4727 let highlights = highlights
4728 .into_iter()
4729 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4730 .collect::<Vec<_>>();
4731 assert_eq!(
4732 highlights,
4733 &[
4734 (lsp::DocumentHighlightKind::WRITE, 10..16),
4735 (lsp::DocumentHighlightKind::READ, 32..38),
4736 (lsp::DocumentHighlightKind::READ, 41..47)
4737 ]
4738 )
4739 });
4740}
4741
4742#[gpui::test(iterations = 10)]
4743async fn test_lsp_hover(
4744 executor: BackgroundExecutor,
4745 cx_a: &mut TestAppContext,
4746 cx_b: &mut TestAppContext,
4747) {
4748 let mut server = TestServer::start(executor.clone()).await;
4749 let client_a = server.create_client(cx_a, "user_a").await;
4750 let client_b = server.create_client(cx_b, "user_b").await;
4751 server
4752 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4753 .await;
4754 let active_call_a = cx_a.read(ActiveCall::global);
4755
4756 client_a
4757 .fs()
4758 .insert_tree(
4759 "/root-1",
4760 json!({
4761 "main.rs": "use std::collections::HashMap;",
4762 }),
4763 )
4764 .await;
4765
4766 // Set up a fake language server.
4767 let mut language = Language::new(
4768 LanguageConfig {
4769 name: "Rust".into(),
4770 path_suffixes: vec!["rs".to_string()],
4771 ..Default::default()
4772 },
4773 Some(tree_sitter_rust::language()),
4774 );
4775 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4776 client_a.language_registry().add(Arc::new(language));
4777
4778 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4779 let project_id = active_call_a
4780 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4781 .await
4782 .unwrap();
4783 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4784
4785 // Open the file as the guest
4786 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4787 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4788
4789 // Request hover information as the guest.
4790 let fake_language_server = fake_language_servers.next().await.unwrap();
4791 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4792 |params, _| async move {
4793 assert_eq!(
4794 params
4795 .text_document_position_params
4796 .text_document
4797 .uri
4798 .as_str(),
4799 "file:///root-1/main.rs"
4800 );
4801 assert_eq!(
4802 params.text_document_position_params.position,
4803 lsp::Position::new(0, 22)
4804 );
4805 Ok(Some(lsp::Hover {
4806 contents: lsp::HoverContents::Array(vec![
4807 lsp::MarkedString::String("Test hover content.".to_string()),
4808 lsp::MarkedString::LanguageString(lsp::LanguageString {
4809 language: "Rust".to_string(),
4810 value: "let foo = 42;".to_string(),
4811 }),
4812 ]),
4813 range: Some(lsp::Range::new(
4814 lsp::Position::new(0, 22),
4815 lsp::Position::new(0, 29),
4816 )),
4817 }))
4818 },
4819 );
4820
4821 let hover_info = project_b
4822 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
4823 .await
4824 .unwrap()
4825 .unwrap();
4826
4827 buffer_b.read_with(cx_b, |buffer, _| {
4828 let snapshot = buffer.snapshot();
4829 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
4830 assert_eq!(
4831 hover_info.contents,
4832 vec![
4833 project::HoverBlock {
4834 text: "Test hover content.".to_string(),
4835 kind: HoverBlockKind::Markdown,
4836 },
4837 project::HoverBlock {
4838 text: "let foo = 42;".to_string(),
4839 kind: HoverBlockKind::Code {
4840 language: "Rust".to_string()
4841 },
4842 }
4843 ]
4844 );
4845 });
4846}
4847
4848#[gpui::test(iterations = 10)]
4849async fn test_project_symbols(
4850 executor: BackgroundExecutor,
4851 cx_a: &mut TestAppContext,
4852 cx_b: &mut TestAppContext,
4853) {
4854 let mut server = TestServer::start(executor.clone()).await;
4855 let client_a = server.create_client(cx_a, "user_a").await;
4856 let client_b = server.create_client(cx_b, "user_b").await;
4857 server
4858 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4859 .await;
4860 let active_call_a = cx_a.read(ActiveCall::global);
4861
4862 // Set up a fake language server.
4863 let mut language = Language::new(
4864 LanguageConfig {
4865 name: "Rust".into(),
4866 path_suffixes: vec!["rs".to_string()],
4867 ..Default::default()
4868 },
4869 Some(tree_sitter_rust::language()),
4870 );
4871 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4872 client_a.language_registry().add(Arc::new(language));
4873
4874 client_a
4875 .fs()
4876 .insert_tree(
4877 "/code",
4878 json!({
4879 "crate-1": {
4880 "one.rs": "const ONE: usize = 1;",
4881 },
4882 "crate-2": {
4883 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
4884 },
4885 "private": {
4886 "passwords.txt": "the-password",
4887 }
4888 }),
4889 )
4890 .await;
4891 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
4892 let project_id = active_call_a
4893 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4894 .await
4895 .unwrap();
4896 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4897
4898 // Cause the language server to start.
4899 let open_buffer_task =
4900 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4901 let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
4902
4903 let fake_language_server = fake_language_servers.next().await.unwrap();
4904 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
4905 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
4906 #[allow(deprecated)]
4907 lsp::SymbolInformation {
4908 name: "TWO".into(),
4909 location: lsp::Location {
4910 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
4911 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4912 },
4913 kind: lsp::SymbolKind::CONSTANT,
4914 tags: None,
4915 container_name: None,
4916 deprecated: None,
4917 },
4918 ])))
4919 });
4920
4921 // Request the definition of a symbol as the guest.
4922 let symbols = project_b
4923 .update(cx_b, |p, cx| p.symbols("two", cx))
4924 .await
4925 .unwrap();
4926 assert_eq!(symbols.len(), 1);
4927 assert_eq!(symbols[0].name, "TWO");
4928
4929 // Open one of the returned symbols.
4930 let buffer_b_2 = project_b
4931 .update(cx_b, |project, cx| {
4932 project.open_buffer_for_symbol(&symbols[0], cx)
4933 })
4934 .await
4935 .unwrap();
4936
4937 buffer_b_2.read_with(cx_b, |buffer, _| {
4938 assert_eq!(
4939 buffer.file().unwrap().path().as_ref(),
4940 Path::new("../crate-2/two.rs")
4941 );
4942 });
4943
4944 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
4945 let mut fake_symbol = symbols[0].clone();
4946 fake_symbol.path.path = Path::new("/code/secrets").into();
4947 let error = project_b
4948 .update(cx_b, |project, cx| {
4949 project.open_buffer_for_symbol(&fake_symbol, cx)
4950 })
4951 .await
4952 .unwrap_err();
4953 assert!(error.to_string().contains("invalid symbol signature"));
4954}
4955
4956#[gpui::test(iterations = 10)]
4957async fn test_open_buffer_while_getting_definition_pointing_to_it(
4958 executor: BackgroundExecutor,
4959 cx_a: &mut TestAppContext,
4960 cx_b: &mut TestAppContext,
4961 mut rng: StdRng,
4962) {
4963 let mut server = TestServer::start(executor.clone()).await;
4964 let client_a = server.create_client(cx_a, "user_a").await;
4965 let client_b = server.create_client(cx_b, "user_b").await;
4966 server
4967 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4968 .await;
4969 let active_call_a = cx_a.read(ActiveCall::global);
4970
4971 // Set up a fake language server.
4972 let mut language = Language::new(
4973 LanguageConfig {
4974 name: "Rust".into(),
4975 path_suffixes: vec!["rs".to_string()],
4976 ..Default::default()
4977 },
4978 Some(tree_sitter_rust::language()),
4979 );
4980 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4981 client_a.language_registry().add(Arc::new(language));
4982
4983 client_a
4984 .fs()
4985 .insert_tree(
4986 "/root",
4987 json!({
4988 "a.rs": "const ONE: usize = b::TWO;",
4989 "b.rs": "const TWO: usize = 2",
4990 }),
4991 )
4992 .await;
4993 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
4994 let project_id = active_call_a
4995 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4996 .await
4997 .unwrap();
4998 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4999
5000 let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
5001 let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5002
5003 let fake_language_server = fake_language_servers.next().await.unwrap();
5004 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5005 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5006 lsp::Location::new(
5007 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5008 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5009 ),
5010 )))
5011 });
5012
5013 let definitions;
5014 let buffer_b2;
5015 if rng.gen() {
5016 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5017 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5018 } else {
5019 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5020 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5021 }
5022
5023 let buffer_b2 = buffer_b2.await.unwrap();
5024 let definitions = definitions.await.unwrap();
5025 assert_eq!(definitions.len(), 1);
5026 assert_eq!(definitions[0].target.buffer, buffer_b2);
5027}
5028
5029#[gpui::test(iterations = 10)]
5030async fn test_contacts(
5031 executor: BackgroundExecutor,
5032 cx_a: &mut TestAppContext,
5033 cx_b: &mut TestAppContext,
5034 cx_c: &mut TestAppContext,
5035 cx_d: &mut TestAppContext,
5036) {
5037 let mut server = TestServer::start(executor.clone()).await;
5038 let client_a = server.create_client(cx_a, "user_a").await;
5039 let client_b = server.create_client(cx_b, "user_b").await;
5040 let client_c = server.create_client(cx_c, "user_c").await;
5041 let client_d = server.create_client(cx_d, "user_d").await;
5042 server
5043 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5044 .await;
5045 let active_call_a = cx_a.read(ActiveCall::global);
5046 let active_call_b = cx_b.read(ActiveCall::global);
5047 let active_call_c = cx_c.read(ActiveCall::global);
5048 let _active_call_d = cx_d.read(ActiveCall::global);
5049
5050 executor.run_until_parked();
5051 assert_eq!(
5052 contacts(&client_a, cx_a),
5053 [
5054 ("user_b".to_string(), "online", "free"),
5055 ("user_c".to_string(), "online", "free")
5056 ]
5057 );
5058 assert_eq!(
5059 contacts(&client_b, cx_b),
5060 [
5061 ("user_a".to_string(), "online", "free"),
5062 ("user_c".to_string(), "online", "free")
5063 ]
5064 );
5065 assert_eq!(
5066 contacts(&client_c, cx_c),
5067 [
5068 ("user_a".to_string(), "online", "free"),
5069 ("user_b".to_string(), "online", "free")
5070 ]
5071 );
5072 assert_eq!(contacts(&client_d, cx_d), []);
5073
5074 server.disconnect_client(client_c.peer_id().unwrap());
5075 server.forbid_connections();
5076 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5077 assert_eq!(
5078 contacts(&client_a, cx_a),
5079 [
5080 ("user_b".to_string(), "online", "free"),
5081 ("user_c".to_string(), "offline", "free")
5082 ]
5083 );
5084 assert_eq!(
5085 contacts(&client_b, cx_b),
5086 [
5087 ("user_a".to_string(), "online", "free"),
5088 ("user_c".to_string(), "offline", "free")
5089 ]
5090 );
5091 assert_eq!(contacts(&client_c, cx_c), []);
5092 assert_eq!(contacts(&client_d, cx_d), []);
5093
5094 server.allow_connections();
5095 client_c
5096 .authenticate_and_connect(false, &cx_c.to_async())
5097 .await
5098 .unwrap();
5099
5100 executor.run_until_parked();
5101 assert_eq!(
5102 contacts(&client_a, cx_a),
5103 [
5104 ("user_b".to_string(), "online", "free"),
5105 ("user_c".to_string(), "online", "free")
5106 ]
5107 );
5108 assert_eq!(
5109 contacts(&client_b, cx_b),
5110 [
5111 ("user_a".to_string(), "online", "free"),
5112 ("user_c".to_string(), "online", "free")
5113 ]
5114 );
5115 assert_eq!(
5116 contacts(&client_c, cx_c),
5117 [
5118 ("user_a".to_string(), "online", "free"),
5119 ("user_b".to_string(), "online", "free")
5120 ]
5121 );
5122 assert_eq!(contacts(&client_d, cx_d), []);
5123
5124 active_call_a
5125 .update(cx_a, |call, cx| {
5126 call.invite(client_b.user_id().unwrap(), None, cx)
5127 })
5128 .await
5129 .unwrap();
5130 executor.run_until_parked();
5131 assert_eq!(
5132 contacts(&client_a, cx_a),
5133 [
5134 ("user_b".to_string(), "online", "busy"),
5135 ("user_c".to_string(), "online", "free")
5136 ]
5137 );
5138 assert_eq!(
5139 contacts(&client_b, cx_b),
5140 [
5141 ("user_a".to_string(), "online", "busy"),
5142 ("user_c".to_string(), "online", "free")
5143 ]
5144 );
5145 assert_eq!(
5146 contacts(&client_c, cx_c),
5147 [
5148 ("user_a".to_string(), "online", "busy"),
5149 ("user_b".to_string(), "online", "busy")
5150 ]
5151 );
5152 assert_eq!(contacts(&client_d, cx_d), []);
5153
5154 // Client B and client D become contacts while client B is being called.
5155 server
5156 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5157 .await;
5158 executor.run_until_parked();
5159 assert_eq!(
5160 contacts(&client_a, cx_a),
5161 [
5162 ("user_b".to_string(), "online", "busy"),
5163 ("user_c".to_string(), "online", "free")
5164 ]
5165 );
5166 assert_eq!(
5167 contacts(&client_b, cx_b),
5168 [
5169 ("user_a".to_string(), "online", "busy"),
5170 ("user_c".to_string(), "online", "free"),
5171 ("user_d".to_string(), "online", "free"),
5172 ]
5173 );
5174 assert_eq!(
5175 contacts(&client_c, cx_c),
5176 [
5177 ("user_a".to_string(), "online", "busy"),
5178 ("user_b".to_string(), "online", "busy")
5179 ]
5180 );
5181 assert_eq!(
5182 contacts(&client_d, cx_d),
5183 [("user_b".to_string(), "online", "busy")]
5184 );
5185
5186 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5187 executor.run_until_parked();
5188 assert_eq!(
5189 contacts(&client_a, cx_a),
5190 [
5191 ("user_b".to_string(), "online", "free"),
5192 ("user_c".to_string(), "online", "free")
5193 ]
5194 );
5195 assert_eq!(
5196 contacts(&client_b, cx_b),
5197 [
5198 ("user_a".to_string(), "online", "free"),
5199 ("user_c".to_string(), "online", "free"),
5200 ("user_d".to_string(), "online", "free")
5201 ]
5202 );
5203 assert_eq!(
5204 contacts(&client_c, cx_c),
5205 [
5206 ("user_a".to_string(), "online", "free"),
5207 ("user_b".to_string(), "online", "free")
5208 ]
5209 );
5210 assert_eq!(
5211 contacts(&client_d, cx_d),
5212 [("user_b".to_string(), "online", "free")]
5213 );
5214
5215 active_call_c
5216 .update(cx_c, |call, cx| {
5217 call.invite(client_a.user_id().unwrap(), None, cx)
5218 })
5219 .await
5220 .unwrap();
5221 executor.run_until_parked();
5222 assert_eq!(
5223 contacts(&client_a, cx_a),
5224 [
5225 ("user_b".to_string(), "online", "free"),
5226 ("user_c".to_string(), "online", "busy")
5227 ]
5228 );
5229 assert_eq!(
5230 contacts(&client_b, cx_b),
5231 [
5232 ("user_a".to_string(), "online", "busy"),
5233 ("user_c".to_string(), "online", "busy"),
5234 ("user_d".to_string(), "online", "free")
5235 ]
5236 );
5237 assert_eq!(
5238 contacts(&client_c, cx_c),
5239 [
5240 ("user_a".to_string(), "online", "busy"),
5241 ("user_b".to_string(), "online", "free")
5242 ]
5243 );
5244 assert_eq!(
5245 contacts(&client_d, cx_d),
5246 [("user_b".to_string(), "online", "free")]
5247 );
5248
5249 active_call_a
5250 .update(cx_a, |call, cx| call.accept_incoming(cx))
5251 .await
5252 .unwrap();
5253 executor.run_until_parked();
5254 assert_eq!(
5255 contacts(&client_a, cx_a),
5256 [
5257 ("user_b".to_string(), "online", "free"),
5258 ("user_c".to_string(), "online", "busy")
5259 ]
5260 );
5261 assert_eq!(
5262 contacts(&client_b, cx_b),
5263 [
5264 ("user_a".to_string(), "online", "busy"),
5265 ("user_c".to_string(), "online", "busy"),
5266 ("user_d".to_string(), "online", "free")
5267 ]
5268 );
5269 assert_eq!(
5270 contacts(&client_c, cx_c),
5271 [
5272 ("user_a".to_string(), "online", "busy"),
5273 ("user_b".to_string(), "online", "free")
5274 ]
5275 );
5276 assert_eq!(
5277 contacts(&client_d, cx_d),
5278 [("user_b".to_string(), "online", "free")]
5279 );
5280
5281 active_call_a
5282 .update(cx_a, |call, cx| {
5283 call.invite(client_b.user_id().unwrap(), None, cx)
5284 })
5285 .await
5286 .unwrap();
5287 executor.run_until_parked();
5288 assert_eq!(
5289 contacts(&client_a, cx_a),
5290 [
5291 ("user_b".to_string(), "online", "busy"),
5292 ("user_c".to_string(), "online", "busy")
5293 ]
5294 );
5295 assert_eq!(
5296 contacts(&client_b, cx_b),
5297 [
5298 ("user_a".to_string(), "online", "busy"),
5299 ("user_c".to_string(), "online", "busy"),
5300 ("user_d".to_string(), "online", "free")
5301 ]
5302 );
5303 assert_eq!(
5304 contacts(&client_c, cx_c),
5305 [
5306 ("user_a".to_string(), "online", "busy"),
5307 ("user_b".to_string(), "online", "busy")
5308 ]
5309 );
5310 assert_eq!(
5311 contacts(&client_d, cx_d),
5312 [("user_b".to_string(), "online", "busy")]
5313 );
5314
5315 active_call_a
5316 .update(cx_a, |call, cx| call.hang_up(cx))
5317 .await
5318 .unwrap();
5319 executor.run_until_parked();
5320 assert_eq!(
5321 contacts(&client_a, cx_a),
5322 [
5323 ("user_b".to_string(), "online", "free"),
5324 ("user_c".to_string(), "online", "free")
5325 ]
5326 );
5327 assert_eq!(
5328 contacts(&client_b, cx_b),
5329 [
5330 ("user_a".to_string(), "online", "free"),
5331 ("user_c".to_string(), "online", "free"),
5332 ("user_d".to_string(), "online", "free")
5333 ]
5334 );
5335 assert_eq!(
5336 contacts(&client_c, cx_c),
5337 [
5338 ("user_a".to_string(), "online", "free"),
5339 ("user_b".to_string(), "online", "free")
5340 ]
5341 );
5342 assert_eq!(
5343 contacts(&client_d, cx_d),
5344 [("user_b".to_string(), "online", "free")]
5345 );
5346
5347 active_call_a
5348 .update(cx_a, |call, cx| {
5349 call.invite(client_b.user_id().unwrap(), None, cx)
5350 })
5351 .await
5352 .unwrap();
5353 executor.run_until_parked();
5354 assert_eq!(
5355 contacts(&client_a, cx_a),
5356 [
5357 ("user_b".to_string(), "online", "busy"),
5358 ("user_c".to_string(), "online", "free")
5359 ]
5360 );
5361 assert_eq!(
5362 contacts(&client_b, cx_b),
5363 [
5364 ("user_a".to_string(), "online", "busy"),
5365 ("user_c".to_string(), "online", "free"),
5366 ("user_d".to_string(), "online", "free")
5367 ]
5368 );
5369 assert_eq!(
5370 contacts(&client_c, cx_c),
5371 [
5372 ("user_a".to_string(), "online", "busy"),
5373 ("user_b".to_string(), "online", "busy")
5374 ]
5375 );
5376 assert_eq!(
5377 contacts(&client_d, cx_d),
5378 [("user_b".to_string(), "online", "busy")]
5379 );
5380
5381 server.forbid_connections();
5382 server.disconnect_client(client_a.peer_id().unwrap());
5383 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5384 assert_eq!(contacts(&client_a, cx_a), []);
5385 assert_eq!(
5386 contacts(&client_b, cx_b),
5387 [
5388 ("user_a".to_string(), "offline", "free"),
5389 ("user_c".to_string(), "online", "free"),
5390 ("user_d".to_string(), "online", "free")
5391 ]
5392 );
5393 assert_eq!(
5394 contacts(&client_c, cx_c),
5395 [
5396 ("user_a".to_string(), "offline", "free"),
5397 ("user_b".to_string(), "online", "free")
5398 ]
5399 );
5400 assert_eq!(
5401 contacts(&client_d, cx_d),
5402 [("user_b".to_string(), "online", "free")]
5403 );
5404
5405 // Test removing a contact
5406 client_b
5407 .user_store()
5408 .update(cx_b, |store, cx| {
5409 store.remove_contact(client_c.user_id().unwrap(), cx)
5410 })
5411 .await
5412 .unwrap();
5413 executor.run_until_parked();
5414 assert_eq!(
5415 contacts(&client_b, cx_b),
5416 [
5417 ("user_a".to_string(), "offline", "free"),
5418 ("user_d".to_string(), "online", "free")
5419 ]
5420 );
5421 assert_eq!(
5422 contacts(&client_c, cx_c),
5423 [("user_a".to_string(), "offline", "free"),]
5424 );
5425
5426 fn contacts(
5427 client: &TestClient,
5428 cx: &TestAppContext,
5429 ) -> Vec<(String, &'static str, &'static str)> {
5430 client.user_store().read_with(cx, |store, _| {
5431 store
5432 .contacts()
5433 .iter()
5434 .map(|contact| {
5435 (
5436 contact.user.github_login.clone(),
5437 if contact.online { "online" } else { "offline" },
5438 if contact.busy { "busy" } else { "free" },
5439 )
5440 })
5441 .collect()
5442 })
5443 }
5444}
5445
5446#[gpui::test(iterations = 10)]
5447async fn test_contact_requests(
5448 executor: BackgroundExecutor,
5449 cx_a: &mut TestAppContext,
5450 cx_a2: &mut TestAppContext,
5451 cx_b: &mut TestAppContext,
5452 cx_b2: &mut TestAppContext,
5453 cx_c: &mut TestAppContext,
5454 cx_c2: &mut TestAppContext,
5455) {
5456 // Connect to a server as 3 clients.
5457 let mut server = TestServer::start(executor.clone()).await;
5458 let client_a = server.create_client(cx_a, "user_a").await;
5459 let client_a2 = server.create_client(cx_a2, "user_a").await;
5460 let client_b = server.create_client(cx_b, "user_b").await;
5461 let client_b2 = server.create_client(cx_b2, "user_b").await;
5462 let client_c = server.create_client(cx_c, "user_c").await;
5463 let client_c2 = server.create_client(cx_c2, "user_c").await;
5464
5465 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5466 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5467 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5468
5469 // User A and User C request that user B become their contact.
5470 client_a
5471 .user_store()
5472 .update(cx_a, |store, cx| {
5473 store.request_contact(client_b.user_id().unwrap(), cx)
5474 })
5475 .await
5476 .unwrap();
5477 client_c
5478 .user_store()
5479 .update(cx_c, |store, cx| {
5480 store.request_contact(client_b.user_id().unwrap(), cx)
5481 })
5482 .await
5483 .unwrap();
5484 executor.run_until_parked();
5485
5486 // All users see the pending request appear in all their clients.
5487 assert_eq!(
5488 client_a.summarize_contacts(cx_a).outgoing_requests,
5489 &["user_b"]
5490 );
5491 assert_eq!(
5492 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5493 &["user_b"]
5494 );
5495 assert_eq!(
5496 client_b.summarize_contacts(cx_b).incoming_requests,
5497 &["user_a", "user_c"]
5498 );
5499 assert_eq!(
5500 client_b2.summarize_contacts(cx_b2).incoming_requests,
5501 &["user_a", "user_c"]
5502 );
5503 assert_eq!(
5504 client_c.summarize_contacts(cx_c).outgoing_requests,
5505 &["user_b"]
5506 );
5507 assert_eq!(
5508 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5509 &["user_b"]
5510 );
5511
5512 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5513 disconnect_and_reconnect(&client_a, cx_a).await;
5514 disconnect_and_reconnect(&client_b, cx_b).await;
5515 disconnect_and_reconnect(&client_c, cx_c).await;
5516 executor.run_until_parked();
5517 assert_eq!(
5518 client_a.summarize_contacts(cx_a).outgoing_requests,
5519 &["user_b"]
5520 );
5521 assert_eq!(
5522 client_b.summarize_contacts(cx_b).incoming_requests,
5523 &["user_a", "user_c"]
5524 );
5525 assert_eq!(
5526 client_c.summarize_contacts(cx_c).outgoing_requests,
5527 &["user_b"]
5528 );
5529
5530 // User B accepts the request from user A.
5531 client_b
5532 .user_store()
5533 .update(cx_b, |store, cx| {
5534 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5535 })
5536 .await
5537 .unwrap();
5538
5539 executor.run_until_parked();
5540
5541 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5542 let contacts_b = client_b.summarize_contacts(cx_b);
5543 assert_eq!(contacts_b.current, &["user_a"]);
5544 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5545 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5546 assert_eq!(contacts_b2.current, &["user_a"]);
5547 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5548
5549 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5550 let contacts_a = client_a.summarize_contacts(cx_a);
5551 assert_eq!(contacts_a.current, &["user_b"]);
5552 assert!(contacts_a.outgoing_requests.is_empty());
5553 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5554 assert_eq!(contacts_a2.current, &["user_b"]);
5555 assert!(contacts_a2.outgoing_requests.is_empty());
5556
5557 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5558 disconnect_and_reconnect(&client_a, cx_a).await;
5559 disconnect_and_reconnect(&client_b, cx_b).await;
5560 disconnect_and_reconnect(&client_c, cx_c).await;
5561 executor.run_until_parked();
5562 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5563 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5564 assert_eq!(
5565 client_b.summarize_contacts(cx_b).incoming_requests,
5566 &["user_c"]
5567 );
5568 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5569 assert_eq!(
5570 client_c.summarize_contacts(cx_c).outgoing_requests,
5571 &["user_b"]
5572 );
5573
5574 // User B rejects the request from user C.
5575 client_b
5576 .user_store()
5577 .update(cx_b, |store, cx| {
5578 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5579 })
5580 .await
5581 .unwrap();
5582
5583 executor.run_until_parked();
5584
5585 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5586 let contacts_b = client_b.summarize_contacts(cx_b);
5587 assert_eq!(contacts_b.current, &["user_a"]);
5588 assert!(contacts_b.incoming_requests.is_empty());
5589 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5590 assert_eq!(contacts_b2.current, &["user_a"]);
5591 assert!(contacts_b2.incoming_requests.is_empty());
5592
5593 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5594 let contacts_c = client_c.summarize_contacts(cx_c);
5595 assert!(contacts_c.current.is_empty());
5596 assert!(contacts_c.outgoing_requests.is_empty());
5597 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5598 assert!(contacts_c2.current.is_empty());
5599 assert!(contacts_c2.outgoing_requests.is_empty());
5600
5601 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5602 disconnect_and_reconnect(&client_a, cx_a).await;
5603 disconnect_and_reconnect(&client_b, cx_b).await;
5604 disconnect_and_reconnect(&client_c, cx_c).await;
5605 executor.run_until_parked();
5606 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5607 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5608 assert!(client_b
5609 .summarize_contacts(cx_b)
5610 .incoming_requests
5611 .is_empty());
5612 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5613 assert!(client_c
5614 .summarize_contacts(cx_c)
5615 .outgoing_requests
5616 .is_empty());
5617
5618 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
5619 client.disconnect(&cx.to_async());
5620 client.clear_contacts(cx).await;
5621 client
5622 .authenticate_and_connect(false, &cx.to_async())
5623 .await
5624 .unwrap();
5625 }
5626}
5627
5628#[gpui::test(iterations = 10)]
5629async fn test_join_call_after_screen_was_shared(
5630 executor: BackgroundExecutor,
5631 cx_a: &mut TestAppContext,
5632 cx_b: &mut TestAppContext,
5633) {
5634 let mut server = TestServer::start(executor.clone()).await;
5635
5636 let client_a = server.create_client(cx_a, "user_a").await;
5637 let client_b = server.create_client(cx_b, "user_b").await;
5638 server
5639 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5640 .await;
5641
5642 let active_call_a = cx_a.read(ActiveCall::global);
5643 let active_call_b = cx_b.read(ActiveCall::global);
5644
5645 // Call users B and C from client A.
5646 active_call_a
5647 .update(cx_a, |call, cx| {
5648 call.invite(client_b.user_id().unwrap(), None, cx)
5649 })
5650 .await
5651 .unwrap();
5652
5653 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
5654 executor.run_until_parked();
5655 assert_eq!(
5656 room_participants(&room_a, cx_a),
5657 RoomParticipants {
5658 remote: Default::default(),
5659 pending: vec!["user_b".to_string()]
5660 }
5661 );
5662
5663 // User B receives the call.
5664
5665 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
5666 let call_b = incoming_call_b.next().await.unwrap().unwrap();
5667 assert_eq!(call_b.calling_user.github_login, "user_a");
5668
5669 // User A shares their screen
5670 let display = MacOSDisplay::new();
5671 active_call_a
5672 .update(cx_a, |call, cx| {
5673 call.room().unwrap().update(cx, |room, cx| {
5674 room.set_display_sources(vec![display.clone()]);
5675 room.share_screen(cx)
5676 })
5677 })
5678 .await
5679 .unwrap();
5680
5681 client_b.user_store().update(cx_b, |user_store, _| {
5682 user_store.clear_cache();
5683 });
5684
5685 // User B joins the room
5686 active_call_b
5687 .update(cx_b, |call, cx| call.accept_incoming(cx))
5688 .await
5689 .unwrap();
5690
5691 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
5692 assert!(incoming_call_b.next().await.unwrap().is_none());
5693
5694 executor.run_until_parked();
5695 assert_eq!(
5696 room_participants(&room_a, cx_a),
5697 RoomParticipants {
5698 remote: vec!["user_b".to_string()],
5699 pending: vec![],
5700 }
5701 );
5702 assert_eq!(
5703 room_participants(&room_b, cx_b),
5704 RoomParticipants {
5705 remote: vec!["user_a".to_string()],
5706 pending: vec![],
5707 }
5708 );
5709
5710 // Ensure User B sees User A's screenshare.
5711
5712 room_b.read_with(cx_b, |room, _| {
5713 assert_eq!(
5714 room.remote_participants()
5715 .get(&client_a.user_id().unwrap())
5716 .unwrap()
5717 .video_tracks
5718 .len(),
5719 1
5720 );
5721 });
5722}