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