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