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