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