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