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_uri: "avatar_a".into(),
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_uri: "avatar_b".into(),
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.create_entry((worktree_id, "c.txt"), false, cx)
2785 })
2786 .await
2787 .unwrap()
2788 .unwrap();
2789
2790 worktree_a.read_with(cx_a, |worktree, _| {
2791 assert_eq!(
2792 worktree
2793 .paths()
2794 .map(|p| p.to_string_lossy())
2795 .collect::<Vec<_>>(),
2796 ["a.txt", "b.txt", "c.txt"]
2797 );
2798 });
2799
2800 worktree_b.read_with(cx_b, |worktree, _| {
2801 assert_eq!(
2802 worktree
2803 .paths()
2804 .map(|p| p.to_string_lossy())
2805 .collect::<Vec<_>>(),
2806 ["a.txt", "b.txt", "c.txt"]
2807 );
2808 });
2809
2810 project_b
2811 .update(cx_b, |project, cx| {
2812 project.rename_entry(entry.id, Path::new("d.txt"), cx)
2813 })
2814 .await
2815 .unwrap()
2816 .unwrap();
2817
2818 worktree_a.read_with(cx_a, |worktree, _| {
2819 assert_eq!(
2820 worktree
2821 .paths()
2822 .map(|p| p.to_string_lossy())
2823 .collect::<Vec<_>>(),
2824 ["a.txt", "b.txt", "d.txt"]
2825 );
2826 });
2827
2828 worktree_b.read_with(cx_b, |worktree, _| {
2829 assert_eq!(
2830 worktree
2831 .paths()
2832 .map(|p| p.to_string_lossy())
2833 .collect::<Vec<_>>(),
2834 ["a.txt", "b.txt", "d.txt"]
2835 );
2836 });
2837
2838 let dir_entry = project_b
2839 .update(cx_b, |project, cx| {
2840 project.create_entry((worktree_id, "DIR"), true, cx)
2841 })
2842 .await
2843 .unwrap()
2844 .unwrap();
2845
2846 worktree_a.read_with(cx_a, |worktree, _| {
2847 assert_eq!(
2848 worktree
2849 .paths()
2850 .map(|p| p.to_string_lossy())
2851 .collect::<Vec<_>>(),
2852 ["DIR", "a.txt", "b.txt", "d.txt"]
2853 );
2854 });
2855
2856 worktree_b.read_with(cx_b, |worktree, _| {
2857 assert_eq!(
2858 worktree
2859 .paths()
2860 .map(|p| p.to_string_lossy())
2861 .collect::<Vec<_>>(),
2862 ["DIR", "a.txt", "b.txt", "d.txt"]
2863 );
2864 });
2865
2866 project_b
2867 .update(cx_b, |project, cx| {
2868 project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
2869 })
2870 .await
2871 .unwrap()
2872 .unwrap();
2873 project_b
2874 .update(cx_b, |project, cx| {
2875 project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
2876 })
2877 .await
2878 .unwrap()
2879 .unwrap();
2880 project_b
2881 .update(cx_b, |project, cx| {
2882 project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
2883 })
2884 .await
2885 .unwrap()
2886 .unwrap();
2887
2888 worktree_a.read_with(cx_a, |worktree, _| {
2889 assert_eq!(
2890 worktree
2891 .paths()
2892 .map(|p| p.to_string_lossy())
2893 .collect::<Vec<_>>(),
2894 [
2895 "DIR",
2896 "DIR/SUBDIR",
2897 "DIR/SUBDIR/f.txt",
2898 "DIR/e.txt",
2899 "a.txt",
2900 "b.txt",
2901 "d.txt"
2902 ]
2903 );
2904 });
2905
2906 worktree_b.read_with(cx_b, |worktree, _| {
2907 assert_eq!(
2908 worktree
2909 .paths()
2910 .map(|p| p.to_string_lossy())
2911 .collect::<Vec<_>>(),
2912 [
2913 "DIR",
2914 "DIR/SUBDIR",
2915 "DIR/SUBDIR/f.txt",
2916 "DIR/e.txt",
2917 "a.txt",
2918 "b.txt",
2919 "d.txt"
2920 ]
2921 );
2922 });
2923
2924 project_b
2925 .update(cx_b, |project, cx| {
2926 project.copy_entry(entry.id, Path::new("f.txt"), cx)
2927 })
2928 .await
2929 .unwrap()
2930 .unwrap();
2931
2932 worktree_a.read_with(cx_a, |worktree, _| {
2933 assert_eq!(
2934 worktree
2935 .paths()
2936 .map(|p| p.to_string_lossy())
2937 .collect::<Vec<_>>(),
2938 [
2939 "DIR",
2940 "DIR/SUBDIR",
2941 "DIR/SUBDIR/f.txt",
2942 "DIR/e.txt",
2943 "a.txt",
2944 "b.txt",
2945 "d.txt",
2946 "f.txt"
2947 ]
2948 );
2949 });
2950
2951 worktree_b.read_with(cx_b, |worktree, _| {
2952 assert_eq!(
2953 worktree
2954 .paths()
2955 .map(|p| p.to_string_lossy())
2956 .collect::<Vec<_>>(),
2957 [
2958 "DIR",
2959 "DIR/SUBDIR",
2960 "DIR/SUBDIR/f.txt",
2961 "DIR/e.txt",
2962 "a.txt",
2963 "b.txt",
2964 "d.txt",
2965 "f.txt"
2966 ]
2967 );
2968 });
2969
2970 project_b
2971 .update(cx_b, |project, cx| {
2972 project.delete_entry(dir_entry.id, cx).unwrap()
2973 })
2974 .await
2975 .unwrap();
2976 executor.run_until_parked();
2977
2978 worktree_a.read_with(cx_a, |worktree, _| {
2979 assert_eq!(
2980 worktree
2981 .paths()
2982 .map(|p| p.to_string_lossy())
2983 .collect::<Vec<_>>(),
2984 ["a.txt", "b.txt", "d.txt", "f.txt"]
2985 );
2986 });
2987
2988 worktree_b.read_with(cx_b, |worktree, _| {
2989 assert_eq!(
2990 worktree
2991 .paths()
2992 .map(|p| p.to_string_lossy())
2993 .collect::<Vec<_>>(),
2994 ["a.txt", "b.txt", "d.txt", "f.txt"]
2995 );
2996 });
2997
2998 project_b
2999 .update(cx_b, |project, cx| {
3000 project.delete_entry(entry.id, cx).unwrap()
3001 })
3002 .await
3003 .unwrap();
3004
3005 worktree_a.read_with(cx_a, |worktree, _| {
3006 assert_eq!(
3007 worktree
3008 .paths()
3009 .map(|p| p.to_string_lossy())
3010 .collect::<Vec<_>>(),
3011 ["a.txt", "b.txt", "f.txt"]
3012 );
3013 });
3014
3015 worktree_b.read_with(cx_b, |worktree, _| {
3016 assert_eq!(
3017 worktree
3018 .paths()
3019 .map(|p| p.to_string_lossy())
3020 .collect::<Vec<_>>(),
3021 ["a.txt", "b.txt", "f.txt"]
3022 );
3023 });
3024}
3025
3026#[gpui::test(iterations = 10)]
3027async fn test_local_settings(
3028 executor: BackgroundExecutor,
3029 cx_a: &mut TestAppContext,
3030 cx_b: &mut TestAppContext,
3031) {
3032 let mut server = TestServer::start(executor.clone()).await;
3033 let client_a = server.create_client(cx_a, "user_a").await;
3034 let client_b = server.create_client(cx_b, "user_b").await;
3035 server
3036 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3037 .await;
3038 let active_call_a = cx_a.read(ActiveCall::global);
3039
3040 // As client A, open a project that contains some local settings files
3041 client_a
3042 .fs()
3043 .insert_tree(
3044 "/dir",
3045 json!({
3046 ".zed": {
3047 "settings.json": r#"{ "tab_size": 2 }"#
3048 },
3049 "a": {
3050 ".zed": {
3051 "settings.json": r#"{ "tab_size": 8 }"#
3052 },
3053 "a.txt": "a-contents",
3054 },
3055 "b": {
3056 "b.txt": "b-contents",
3057 }
3058 }),
3059 )
3060 .await;
3061 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
3062 executor.run_until_parked();
3063 let project_id = active_call_a
3064 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3065 .await
3066 .unwrap();
3067
3068 // As client B, join that project and observe the local settings.
3069 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3070
3071 let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
3072 executor.run_until_parked();
3073 cx_b.read(|cx| {
3074 let store = cx.global::<SettingsStore>();
3075 assert_eq!(
3076 store
3077 .local_settings(worktree_b.read(cx).id().to_usize())
3078 .collect::<Vec<_>>(),
3079 &[
3080 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3081 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3082 ]
3083 )
3084 });
3085
3086 // As client A, update a settings file. As Client B, see the changed settings.
3087 client_a
3088 .fs()
3089 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3090 .await;
3091 executor.run_until_parked();
3092 cx_b.read(|cx| {
3093 let store = cx.global::<SettingsStore>();
3094 assert_eq!(
3095 store
3096 .local_settings(worktree_b.read(cx).id().to_usize())
3097 .collect::<Vec<_>>(),
3098 &[
3099 (Path::new("").into(), r#"{}"#.to_string()),
3100 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3101 ]
3102 )
3103 });
3104
3105 // As client A, create and remove some settings files. As client B, see the changed settings.
3106 client_a
3107 .fs()
3108 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3109 .await
3110 .unwrap();
3111 client_a
3112 .fs()
3113 .create_dir("/dir/b/.zed".as_ref())
3114 .await
3115 .unwrap();
3116 client_a
3117 .fs()
3118 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3119 .await;
3120 executor.run_until_parked();
3121 cx_b.read(|cx| {
3122 let store = cx.global::<SettingsStore>();
3123 assert_eq!(
3124 store
3125 .local_settings(worktree_b.read(cx).id().to_usize())
3126 .collect::<Vec<_>>(),
3127 &[
3128 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3129 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3130 ]
3131 )
3132 });
3133
3134 // As client B, disconnect.
3135 server.forbid_connections();
3136 server.disconnect_client(client_b.peer_id().unwrap());
3137
3138 // As client A, change and remove settings files while client B is disconnected.
3139 client_a
3140 .fs()
3141 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3142 .await;
3143 client_a
3144 .fs()
3145 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3146 .await
3147 .unwrap();
3148 executor.run_until_parked();
3149
3150 // As client B, reconnect and see the changed settings.
3151 server.allow_connections();
3152 executor.advance_clock(RECEIVE_TIMEOUT);
3153 cx_b.read(|cx| {
3154 let store = cx.global::<SettingsStore>();
3155 assert_eq!(
3156 store
3157 .local_settings(worktree_b.read(cx).id().to_usize())
3158 .collect::<Vec<_>>(),
3159 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3160 )
3161 });
3162}
3163
3164#[gpui::test(iterations = 10)]
3165async fn test_buffer_conflict_after_save(
3166 executor: BackgroundExecutor,
3167 cx_a: &mut TestAppContext,
3168 cx_b: &mut TestAppContext,
3169) {
3170 let mut server = TestServer::start(executor.clone()).await;
3171 let client_a = server.create_client(cx_a, "user_a").await;
3172 let client_b = server.create_client(cx_b, "user_b").await;
3173 server
3174 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3175 .await;
3176 let active_call_a = cx_a.read(ActiveCall::global);
3177
3178 client_a
3179 .fs()
3180 .insert_tree(
3181 "/dir",
3182 json!({
3183 "a.txt": "a-contents",
3184 }),
3185 )
3186 .await;
3187 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3188 let project_id = active_call_a
3189 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3190 .await
3191 .unwrap();
3192 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3193
3194 // Open a buffer as client B
3195 let buffer_b = project_b
3196 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3197 .await
3198 .unwrap();
3199
3200 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3201
3202 buffer_b.read_with(cx_b, |buf, _| {
3203 assert!(buf.is_dirty());
3204 assert!(!buf.has_conflict());
3205 });
3206
3207 project_b
3208 .update(cx_b, |project, cx| {
3209 project.save_buffer(buffer_b.clone(), cx)
3210 })
3211 .await
3212 .unwrap();
3213
3214 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3215
3216 buffer_b.read_with(cx_b, |buf, _| {
3217 assert!(!buf.has_conflict());
3218 });
3219
3220 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3221
3222 buffer_b.read_with(cx_b, |buf, _| {
3223 assert!(buf.is_dirty());
3224 assert!(!buf.has_conflict());
3225 });
3226}
3227
3228#[gpui::test(iterations = 10)]
3229async fn test_buffer_reloading(
3230 executor: BackgroundExecutor,
3231 cx_a: &mut TestAppContext,
3232 cx_b: &mut TestAppContext,
3233) {
3234 let mut server = TestServer::start(executor.clone()).await;
3235 let client_a = server.create_client(cx_a, "user_a").await;
3236 let client_b = server.create_client(cx_b, "user_b").await;
3237 server
3238 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3239 .await;
3240 let active_call_a = cx_a.read(ActiveCall::global);
3241
3242 client_a
3243 .fs()
3244 .insert_tree(
3245 "/dir",
3246 json!({
3247 "a.txt": "a\nb\nc",
3248 }),
3249 )
3250 .await;
3251 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3252 let project_id = active_call_a
3253 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3254 .await
3255 .unwrap();
3256 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3257
3258 // Open a buffer as client B
3259 let buffer_b = project_b
3260 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3261 .await
3262 .unwrap();
3263
3264 buffer_b.read_with(cx_b, |buf, _| {
3265 assert!(!buf.is_dirty());
3266 assert!(!buf.has_conflict());
3267 assert_eq!(buf.line_ending(), LineEnding::Unix);
3268 });
3269
3270 let new_contents = Rope::from("d\ne\nf");
3271 client_a
3272 .fs()
3273 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3274 .await
3275 .unwrap();
3276
3277 executor.run_until_parked();
3278
3279 buffer_b.read_with(cx_b, |buf, _| {
3280 assert_eq!(buf.text(), new_contents.to_string());
3281 assert!(!buf.is_dirty());
3282 assert!(!buf.has_conflict());
3283 assert_eq!(buf.line_ending(), LineEnding::Windows);
3284 });
3285}
3286
3287#[gpui::test(iterations = 10)]
3288async fn test_editing_while_guest_opens_buffer(
3289 executor: BackgroundExecutor,
3290 cx_a: &mut TestAppContext,
3291 cx_b: &mut TestAppContext,
3292) {
3293 let mut server = TestServer::start(executor.clone()).await;
3294 let client_a = server.create_client(cx_a, "user_a").await;
3295 let client_b = server.create_client(cx_b, "user_b").await;
3296 server
3297 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3298 .await;
3299 let active_call_a = cx_a.read(ActiveCall::global);
3300
3301 client_a
3302 .fs()
3303 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3304 .await;
3305 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3306 let project_id = active_call_a
3307 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3308 .await
3309 .unwrap();
3310 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3311
3312 // Open a buffer as client A
3313 let buffer_a = project_a
3314 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3315 .await
3316 .unwrap();
3317
3318 // Start opening the same buffer as client B
3319 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3320 let buffer_b = cx_b.executor().spawn(open_buffer);
3321
3322 // Edit the buffer as client A while client B is still opening it.
3323 cx_b.executor().simulate_random_delay().await;
3324 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3325 cx_b.executor().simulate_random_delay().await;
3326 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3327
3328 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3329 let buffer_b = buffer_b.await.unwrap();
3330 executor.run_until_parked();
3331
3332 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3333}
3334
3335#[gpui::test(iterations = 10)]
3336async fn test_leaving_worktree_while_opening_buffer(
3337 executor: BackgroundExecutor,
3338 cx_a: &mut TestAppContext,
3339 cx_b: &mut TestAppContext,
3340) {
3341 let mut server = TestServer::start(executor.clone()).await;
3342 let client_a = server.create_client(cx_a, "user_a").await;
3343 let client_b = server.create_client(cx_b, "user_b").await;
3344 server
3345 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3346 .await;
3347 let active_call_a = cx_a.read(ActiveCall::global);
3348
3349 client_a
3350 .fs()
3351 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3352 .await;
3353 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3354 let project_id = active_call_a
3355 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3356 .await
3357 .unwrap();
3358 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3359
3360 // See that a guest has joined as client A.
3361 executor.run_until_parked();
3362
3363 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3364
3365 // Begin opening a buffer as client B, but leave the project before the open completes.
3366 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3367 let buffer_b = cx_b.executor().spawn(open_buffer);
3368 cx_b.update(|_| drop(project_b));
3369 drop(buffer_b);
3370
3371 // See that the guest has left.
3372 executor.run_until_parked();
3373
3374 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3375}
3376
3377#[gpui::test(iterations = 10)]
3378async fn test_canceling_buffer_opening(
3379 executor: BackgroundExecutor,
3380 cx_a: &mut TestAppContext,
3381 cx_b: &mut TestAppContext,
3382) {
3383 let mut server = TestServer::start(executor.clone()).await;
3384 let client_a = server.create_client(cx_a, "user_a").await;
3385 let client_b = server.create_client(cx_b, "user_b").await;
3386 server
3387 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3388 .await;
3389 let active_call_a = cx_a.read(ActiveCall::global);
3390
3391 client_a
3392 .fs()
3393 .insert_tree(
3394 "/dir",
3395 json!({
3396 "a.txt": "abc",
3397 }),
3398 )
3399 .await;
3400 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3401 let project_id = active_call_a
3402 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3403 .await
3404 .unwrap();
3405 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3406
3407 let buffer_a = project_a
3408 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3409 .await
3410 .unwrap();
3411
3412 // Open a buffer as client B but cancel after a random amount of time.
3413 let buffer_b = project_b.update(cx_b, |p, cx| {
3414 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3415 });
3416 executor.simulate_random_delay().await;
3417 drop(buffer_b);
3418
3419 // Try opening the same buffer again as client B, and ensure we can
3420 // still do it despite the cancellation above.
3421 let buffer_b = project_b
3422 .update(cx_b, |p, cx| {
3423 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3424 })
3425 .await
3426 .unwrap();
3427
3428 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3429}
3430
3431#[gpui::test(iterations = 10)]
3432async fn test_leaving_project(
3433 executor: BackgroundExecutor,
3434 cx_a: &mut TestAppContext,
3435 cx_b: &mut TestAppContext,
3436 cx_c: &mut TestAppContext,
3437) {
3438 let mut server = TestServer::start(executor.clone()).await;
3439 let client_a = server.create_client(cx_a, "user_a").await;
3440 let client_b = server.create_client(cx_b, "user_b").await;
3441 let client_c = server.create_client(cx_c, "user_c").await;
3442 server
3443 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3444 .await;
3445 let active_call_a = cx_a.read(ActiveCall::global);
3446
3447 client_a
3448 .fs()
3449 .insert_tree(
3450 "/a",
3451 json!({
3452 "a.txt": "a-contents",
3453 "b.txt": "b-contents",
3454 }),
3455 )
3456 .await;
3457 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3458 let project_id = active_call_a
3459 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3460 .await
3461 .unwrap();
3462 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3463 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3464
3465 // Client A sees that a guest has joined.
3466 executor.run_until_parked();
3467
3468 project_a.read_with(cx_a, |project, _| {
3469 assert_eq!(project.collaborators().len(), 2);
3470 });
3471
3472 project_b1.read_with(cx_b, |project, _| {
3473 assert_eq!(project.collaborators().len(), 2);
3474 });
3475
3476 project_c.read_with(cx_c, |project, _| {
3477 assert_eq!(project.collaborators().len(), 2);
3478 });
3479
3480 // Client B opens a buffer.
3481 let buffer_b1 = project_b1
3482 .update(cx_b, |project, cx| {
3483 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
3484 project.open_buffer((worktree_id, "a.txt"), cx)
3485 })
3486 .await
3487 .unwrap();
3488
3489 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3490
3491 // Drop client B's project and ensure client A and client C observe client B leaving.
3492 cx_b.update(|_| drop(project_b1));
3493 executor.run_until_parked();
3494
3495 project_a.read_with(cx_a, |project, _| {
3496 assert_eq!(project.collaborators().len(), 1);
3497 });
3498
3499 project_c.read_with(cx_c, |project, _| {
3500 assert_eq!(project.collaborators().len(), 1);
3501 });
3502
3503 // Client B re-joins the project and can open buffers as before.
3504 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3505 executor.run_until_parked();
3506
3507 project_a.read_with(cx_a, |project, _| {
3508 assert_eq!(project.collaborators().len(), 2);
3509 });
3510
3511 project_b2.read_with(cx_b, |project, _| {
3512 assert_eq!(project.collaborators().len(), 2);
3513 });
3514
3515 project_c.read_with(cx_c, |project, _| {
3516 assert_eq!(project.collaborators().len(), 2);
3517 });
3518
3519 let buffer_b2 = project_b2
3520 .update(cx_b, |project, cx| {
3521 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
3522 project.open_buffer((worktree_id, "a.txt"), cx)
3523 })
3524 .await
3525 .unwrap();
3526
3527 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3528
3529 // Drop client B's connection and ensure client A and client C observe client B leaving.
3530 client_b.disconnect(&cx_b.to_async());
3531 executor.advance_clock(RECONNECT_TIMEOUT);
3532
3533 project_a.read_with(cx_a, |project, _| {
3534 assert_eq!(project.collaborators().len(), 1);
3535 });
3536
3537 project_b2.read_with(cx_b, |project, _| {
3538 assert!(project.is_read_only());
3539 });
3540
3541 project_c.read_with(cx_c, |project, _| {
3542 assert_eq!(project.collaborators().len(), 1);
3543 });
3544
3545 // Client B can't join the project, unless they re-join the room.
3546 cx_b.spawn(|cx| {
3547 Project::remote(
3548 project_id,
3549 client_b.app_state.client.clone(),
3550 client_b.user_store().clone(),
3551 client_b.language_registry().clone(),
3552 FakeFs::new(cx.background_executor().clone()),
3553 cx,
3554 )
3555 })
3556 .await
3557 .unwrap_err();
3558
3559 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3560 client_c.wait_for_current_user(cx_c).await;
3561 server.forbid_connections();
3562 server.disconnect_client(client_c.peer_id().unwrap());
3563 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3564 executor.run_until_parked();
3565
3566 project_a.read_with(cx_a, |project, _| {
3567 assert_eq!(project.collaborators().len(), 0);
3568 });
3569
3570 project_b2.read_with(cx_b, |project, _| {
3571 assert!(project.is_read_only());
3572 });
3573
3574 project_c.read_with(cx_c, |project, _| {
3575 assert!(project.is_read_only());
3576 });
3577}
3578
3579#[gpui::test(iterations = 10)]
3580async fn test_collaborating_with_diagnostics(
3581 executor: BackgroundExecutor,
3582 cx_a: &mut TestAppContext,
3583 cx_b: &mut TestAppContext,
3584 cx_c: &mut TestAppContext,
3585) {
3586 let mut server = TestServer::start(executor.clone()).await;
3587 let client_a = server.create_client(cx_a, "user_a").await;
3588 let client_b = server.create_client(cx_b, "user_b").await;
3589 let client_c = server.create_client(cx_c, "user_c").await;
3590 server
3591 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3592 .await;
3593 let active_call_a = cx_a.read(ActiveCall::global);
3594
3595 // Set up a fake language server.
3596 let mut language = Language::new(
3597 LanguageConfig {
3598 name: "Rust".into(),
3599 path_suffixes: vec!["rs".to_string()],
3600 ..Default::default()
3601 },
3602 Some(tree_sitter_rust::language()),
3603 );
3604 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3605 client_a.language_registry().add(Arc::new(language));
3606
3607 // Share a project as client A
3608 client_a
3609 .fs()
3610 .insert_tree(
3611 "/a",
3612 json!({
3613 "a.rs": "let one = two",
3614 "other.rs": "",
3615 }),
3616 )
3617 .await;
3618 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3619
3620 // Cause the language server to start.
3621 let _buffer = project_a
3622 .update(cx_a, |project, cx| {
3623 project.open_buffer(
3624 ProjectPath {
3625 worktree_id,
3626 path: Path::new("other.rs").into(),
3627 },
3628 cx,
3629 )
3630 })
3631 .await
3632 .unwrap();
3633
3634 // Simulate a language server reporting errors for a file.
3635 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3636 fake_language_server
3637 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3638 .await;
3639 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3640 lsp::PublishDiagnosticsParams {
3641 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3642 version: None,
3643 diagnostics: vec![lsp::Diagnostic {
3644 severity: Some(lsp::DiagnosticSeverity::WARNING),
3645 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3646 message: "message 0".to_string(),
3647 ..Default::default()
3648 }],
3649 },
3650 );
3651
3652 // Client A shares the project and, simultaneously, the language server
3653 // publishes a diagnostic. This is done to ensure that the server always
3654 // observes the latest diagnostics for a worktree.
3655 let project_id = active_call_a
3656 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3657 .await
3658 .unwrap();
3659 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3660 lsp::PublishDiagnosticsParams {
3661 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3662 version: None,
3663 diagnostics: vec![lsp::Diagnostic {
3664 severity: Some(lsp::DiagnosticSeverity::ERROR),
3665 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3666 message: "message 1".to_string(),
3667 ..Default::default()
3668 }],
3669 },
3670 );
3671
3672 // Join the worktree as client B.
3673 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3674
3675 // Wait for server to see the diagnostics update.
3676 executor.run_until_parked();
3677
3678 // Ensure client B observes the new diagnostics.
3679
3680 project_b.read_with(cx_b, |project, cx| {
3681 assert_eq!(
3682 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3683 &[(
3684 ProjectPath {
3685 worktree_id,
3686 path: Arc::from(Path::new("a.rs")),
3687 },
3688 LanguageServerId(0),
3689 DiagnosticSummary {
3690 error_count: 1,
3691 warning_count: 0,
3692 ..Default::default()
3693 },
3694 )]
3695 )
3696 });
3697
3698 // Join project as client C and observe the diagnostics.
3699 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3700 let project_c_diagnostic_summaries =
3701 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3702 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
3703 })));
3704 project_c.update(cx_c, |_, cx| {
3705 let summaries = project_c_diagnostic_summaries.clone();
3706 cx.subscribe(&project_c, {
3707 move |p, _, event, cx| {
3708 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3709 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
3710 }
3711 }
3712 })
3713 .detach();
3714 });
3715
3716 executor.run_until_parked();
3717 assert_eq!(
3718 project_c_diagnostic_summaries.borrow().as_slice(),
3719 &[(
3720 ProjectPath {
3721 worktree_id,
3722 path: Arc::from(Path::new("a.rs")),
3723 },
3724 LanguageServerId(0),
3725 DiagnosticSummary {
3726 error_count: 1,
3727 warning_count: 0,
3728 ..Default::default()
3729 },
3730 )]
3731 );
3732
3733 // Simulate a language server reporting more errors for a file.
3734 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3735 lsp::PublishDiagnosticsParams {
3736 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3737 version: None,
3738 diagnostics: vec![
3739 lsp::Diagnostic {
3740 severity: Some(lsp::DiagnosticSeverity::ERROR),
3741 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3742 message: "message 1".to_string(),
3743 ..Default::default()
3744 },
3745 lsp::Diagnostic {
3746 severity: Some(lsp::DiagnosticSeverity::WARNING),
3747 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3748 message: "message 2".to_string(),
3749 ..Default::default()
3750 },
3751 ],
3752 },
3753 );
3754
3755 // Clients B and C get the updated summaries
3756 executor.run_until_parked();
3757
3758 project_b.read_with(cx_b, |project, cx| {
3759 assert_eq!(
3760 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3761 [(
3762 ProjectPath {
3763 worktree_id,
3764 path: Arc::from(Path::new("a.rs")),
3765 },
3766 LanguageServerId(0),
3767 DiagnosticSummary {
3768 error_count: 1,
3769 warning_count: 1,
3770 },
3771 )]
3772 );
3773 });
3774
3775 project_c.read_with(cx_c, |project, cx| {
3776 assert_eq!(
3777 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3778 [(
3779 ProjectPath {
3780 worktree_id,
3781 path: Arc::from(Path::new("a.rs")),
3782 },
3783 LanguageServerId(0),
3784 DiagnosticSummary {
3785 error_count: 1,
3786 warning_count: 1,
3787 },
3788 )]
3789 );
3790 });
3791
3792 // Open the file with the errors on client B. They should be present.
3793 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
3794 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
3795
3796 buffer_b.read_with(cx_b, |buffer, _| {
3797 assert_eq!(
3798 buffer
3799 .snapshot()
3800 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3801 .collect::<Vec<_>>(),
3802 &[
3803 DiagnosticEntry {
3804 range: Point::new(0, 4)..Point::new(0, 7),
3805 diagnostic: Diagnostic {
3806 group_id: 2,
3807 message: "message 1".to_string(),
3808 severity: lsp::DiagnosticSeverity::ERROR,
3809 is_primary: true,
3810 ..Default::default()
3811 }
3812 },
3813 DiagnosticEntry {
3814 range: Point::new(0, 10)..Point::new(0, 13),
3815 diagnostic: Diagnostic {
3816 group_id: 3,
3817 severity: lsp::DiagnosticSeverity::WARNING,
3818 message: "message 2".to_string(),
3819 is_primary: true,
3820 ..Default::default()
3821 }
3822 }
3823 ]
3824 );
3825 });
3826
3827 // Simulate a language server reporting no errors for a file.
3828 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3829 lsp::PublishDiagnosticsParams {
3830 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3831 version: None,
3832 diagnostics: vec![],
3833 },
3834 );
3835 executor.run_until_parked();
3836
3837 project_a.read_with(cx_a, |project, cx| {
3838 assert_eq!(
3839 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3840 []
3841 )
3842 });
3843
3844 project_b.read_with(cx_b, |project, cx| {
3845 assert_eq!(
3846 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3847 []
3848 )
3849 });
3850
3851 project_c.read_with(cx_c, |project, cx| {
3852 assert_eq!(
3853 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3854 []
3855 )
3856 });
3857}
3858
3859#[gpui::test(iterations = 10)]
3860async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
3861 executor: BackgroundExecutor,
3862 cx_a: &mut TestAppContext,
3863 cx_b: &mut TestAppContext,
3864) {
3865 let mut server = TestServer::start(executor.clone()).await;
3866 let client_a = server.create_client(cx_a, "user_a").await;
3867 let client_b = server.create_client(cx_b, "user_b").await;
3868 server
3869 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3870 .await;
3871
3872 // Set up a fake language server.
3873 let mut language = Language::new(
3874 LanguageConfig {
3875 name: "Rust".into(),
3876 path_suffixes: vec!["rs".to_string()],
3877 ..Default::default()
3878 },
3879 Some(tree_sitter_rust::language()),
3880 );
3881 let mut fake_language_servers = language
3882 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3883 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
3884 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
3885 ..Default::default()
3886 }))
3887 .await;
3888 client_a.language_registry().add(Arc::new(language));
3889
3890 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
3891 client_a
3892 .fs()
3893 .insert_tree(
3894 "/test",
3895 json!({
3896 "one.rs": "const ONE: usize = 1;",
3897 "two.rs": "const TWO: usize = 2;",
3898 "three.rs": "const THREE: usize = 3;",
3899 "four.rs": "const FOUR: usize = 3;",
3900 "five.rs": "const FIVE: usize = 3;",
3901 }),
3902 )
3903 .await;
3904
3905 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
3906
3907 // Share a project as client A
3908 let active_call_a = cx_a.read(ActiveCall::global);
3909 let project_id = active_call_a
3910 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3911 .await
3912 .unwrap();
3913
3914 // Join the project as client B and open all three files.
3915 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3916 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
3917 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
3918 }))
3919 .await
3920 .unwrap();
3921
3922 // Simulate a language server reporting errors for a file.
3923 let fake_language_server = fake_language_servers.next().await.unwrap();
3924 fake_language_server
3925 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
3926 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3927 })
3928 .await
3929 .unwrap();
3930 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3931 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3932 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
3933 lsp::WorkDoneProgressBegin {
3934 title: "Progress Began".into(),
3935 ..Default::default()
3936 },
3937 )),
3938 });
3939 for file_name in file_names {
3940 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3941 lsp::PublishDiagnosticsParams {
3942 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
3943 version: None,
3944 diagnostics: vec![lsp::Diagnostic {
3945 severity: Some(lsp::DiagnosticSeverity::WARNING),
3946 source: Some("the-disk-based-diagnostics-source".into()),
3947 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3948 message: "message one".to_string(),
3949 ..Default::default()
3950 }],
3951 },
3952 );
3953 }
3954 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3955 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3956 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
3957 lsp::WorkDoneProgressEnd { message: None },
3958 )),
3959 });
3960
3961 // When the "disk base diagnostics finished" message is received, the buffers'
3962 // diagnostics are expected to be present.
3963 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
3964 project_b.update(cx_b, {
3965 let project_b = project_b.clone();
3966 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
3967 move |_, cx| {
3968 cx.subscribe(&project_b, move |_, _, event, cx| {
3969 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3970 disk_based_diagnostics_finished.store(true, SeqCst);
3971 for buffer in &guest_buffers {
3972 assert_eq!(
3973 buffer
3974 .read(cx)
3975 .snapshot()
3976 .diagnostics_in_range::<_, usize>(0..5, false)
3977 .count(),
3978 1,
3979 "expected a diagnostic for buffer {:?}",
3980 buffer.read(cx).file().unwrap().path(),
3981 );
3982 }
3983 }
3984 })
3985 .detach();
3986 }
3987 });
3988
3989 executor.run_until_parked();
3990 assert!(disk_based_diagnostics_finished.load(SeqCst));
3991}
3992
3993#[gpui::test(iterations = 10)]
3994async fn test_reloading_buffer_manually(
3995 executor: BackgroundExecutor,
3996 cx_a: &mut TestAppContext,
3997 cx_b: &mut TestAppContext,
3998) {
3999 let mut server = TestServer::start(executor.clone()).await;
4000 let client_a = server.create_client(cx_a, "user_a").await;
4001 let client_b = server.create_client(cx_b, "user_b").await;
4002 server
4003 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4004 .await;
4005 let active_call_a = cx_a.read(ActiveCall::global);
4006
4007 client_a
4008 .fs()
4009 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4010 .await;
4011 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4012 let buffer_a = project_a
4013 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4014 .await
4015 .unwrap();
4016 let project_id = active_call_a
4017 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4018 .await
4019 .unwrap();
4020
4021 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4022
4023 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4024 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4025 buffer_b.update(cx_b, |buffer, cx| {
4026 buffer.edit([(4..7, "six")], None, cx);
4027 buffer.edit([(10..11, "6")], None, cx);
4028 assert_eq!(buffer.text(), "let six = 6;");
4029 assert!(buffer.is_dirty());
4030 assert!(!buffer.has_conflict());
4031 });
4032 executor.run_until_parked();
4033
4034 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4035
4036 client_a
4037 .fs()
4038 .save(
4039 "/a/a.rs".as_ref(),
4040 &Rope::from("let seven = 7;"),
4041 LineEnding::Unix,
4042 )
4043 .await
4044 .unwrap();
4045 executor.run_until_parked();
4046
4047 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4048
4049 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4050
4051 project_b
4052 .update(cx_b, |project, cx| {
4053 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4054 })
4055 .await
4056 .unwrap();
4057
4058 buffer_a.read_with(cx_a, |buffer, _| {
4059 assert_eq!(buffer.text(), "let seven = 7;");
4060 assert!(!buffer.is_dirty());
4061 assert!(!buffer.has_conflict());
4062 });
4063
4064 buffer_b.read_with(cx_b, |buffer, _| {
4065 assert_eq!(buffer.text(), "let seven = 7;");
4066 assert!(!buffer.is_dirty());
4067 assert!(!buffer.has_conflict());
4068 });
4069
4070 buffer_a.update(cx_a, |buffer, cx| {
4071 // Undoing on the host is a no-op when the reload was initiated by the guest.
4072 buffer.undo(cx);
4073 assert_eq!(buffer.text(), "let seven = 7;");
4074 assert!(!buffer.is_dirty());
4075 assert!(!buffer.has_conflict());
4076 });
4077 buffer_b.update(cx_b, |buffer, cx| {
4078 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4079 buffer.undo(cx);
4080 assert_eq!(buffer.text(), "let six = 6;");
4081 assert!(buffer.is_dirty());
4082 assert!(!buffer.has_conflict());
4083 });
4084}
4085
4086#[gpui::test(iterations = 10)]
4087async fn test_formatting_buffer(
4088 executor: BackgroundExecutor,
4089 cx_a: &mut TestAppContext,
4090 cx_b: &mut TestAppContext,
4091) {
4092 executor.allow_parking();
4093 let mut server = TestServer::start(executor.clone()).await;
4094 let client_a = server.create_client(cx_a, "user_a").await;
4095 let client_b = server.create_client(cx_b, "user_b").await;
4096 server
4097 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4098 .await;
4099 let active_call_a = cx_a.read(ActiveCall::global);
4100
4101 // Set up a fake language server.
4102 let mut language = Language::new(
4103 LanguageConfig {
4104 name: "Rust".into(),
4105 path_suffixes: vec!["rs".to_string()],
4106 ..Default::default()
4107 },
4108 Some(tree_sitter_rust::language()),
4109 );
4110 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4111 client_a.language_registry().add(Arc::new(language));
4112
4113 // Here we insert a fake tree with a directory that exists on disk. This is needed
4114 // because later we'll invoke a command, which requires passing a working directory
4115 // that points to a valid location on disk.
4116 let directory = env::current_dir().unwrap();
4117 client_a
4118 .fs()
4119 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4120 .await;
4121 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4122 let project_id = active_call_a
4123 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4124 .await
4125 .unwrap();
4126 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4127
4128 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4129 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4130
4131 let fake_language_server = fake_language_servers.next().await.unwrap();
4132 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4133 Ok(Some(vec![
4134 lsp::TextEdit {
4135 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4136 new_text: "h".to_string(),
4137 },
4138 lsp::TextEdit {
4139 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4140 new_text: "y".to_string(),
4141 },
4142 ]))
4143 });
4144
4145 project_b
4146 .update(cx_b, |project, cx| {
4147 project.format(
4148 HashSet::from_iter([buffer_b.clone()]),
4149 true,
4150 FormatTrigger::Save,
4151 cx,
4152 )
4153 })
4154 .await
4155 .unwrap();
4156
4157 // The edits from the LSP are applied, and a final newline is added.
4158 assert_eq!(
4159 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4160 "let honey = \"two\"\n"
4161 );
4162
4163 // Ensure buffer can be formatted using an external command. Notice how the
4164 // host's configuration is honored as opposed to using the guest's settings.
4165 cx_a.update(|cx| {
4166 cx.update_global(|store: &mut SettingsStore, cx| {
4167 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4168 file.defaults.formatter = Some(Formatter::External {
4169 command: "awk".into(),
4170 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4171 });
4172 });
4173 });
4174 });
4175 project_b
4176 .update(cx_b, |project, cx| {
4177 project.format(
4178 HashSet::from_iter([buffer_b.clone()]),
4179 true,
4180 FormatTrigger::Save,
4181 cx,
4182 )
4183 })
4184 .await
4185 .unwrap();
4186 assert_eq!(
4187 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4188 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4189 );
4190}
4191
4192#[gpui::test(iterations = 10)]
4193async fn test_prettier_formatting_buffer(
4194 executor: BackgroundExecutor,
4195 cx_a: &mut TestAppContext,
4196 cx_b: &mut TestAppContext,
4197) {
4198 let mut server = TestServer::start(executor.clone()).await;
4199 let client_a = server.create_client(cx_a, "user_a").await;
4200 let client_b = server.create_client(cx_b, "user_b").await;
4201 server
4202 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4203 .await;
4204 let active_call_a = cx_a.read(ActiveCall::global);
4205
4206 // Set up a fake language server.
4207 let mut language = Language::new(
4208 LanguageConfig {
4209 name: "Rust".into(),
4210 path_suffixes: vec!["rs".to_string()],
4211 prettier_parser_name: Some("test_parser".to_string()),
4212 ..Default::default()
4213 },
4214 Some(tree_sitter_rust::language()),
4215 );
4216 let test_plugin = "test_plugin";
4217 let mut fake_language_servers = language
4218 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4219 prettier_plugins: vec![test_plugin],
4220 ..Default::default()
4221 }))
4222 .await;
4223 let language = Arc::new(language);
4224 client_a.language_registry().add(Arc::clone(&language));
4225
4226 // Here we insert a fake tree with a directory that exists on disk. This is needed
4227 // because later we'll invoke a command, which requires passing a working directory
4228 // that points to a valid location on disk.
4229 let directory = env::current_dir().unwrap();
4230 let buffer_text = "let one = \"two\"";
4231 client_a
4232 .fs()
4233 .insert_tree(&directory, json!({ "a.rs": buffer_text }))
4234 .await;
4235 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4236 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4237 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4238 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4239
4240 let project_id = active_call_a
4241 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4242 .await
4243 .unwrap();
4244 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4245 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4246 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4247
4248 cx_a.update(|cx| {
4249 cx.update_global(|store: &mut SettingsStore, cx| {
4250 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4251 file.defaults.formatter = Some(Formatter::Auto);
4252 });
4253 });
4254 });
4255 cx_b.update(|cx| {
4256 cx.update_global(|store: &mut SettingsStore, cx| {
4257 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4258 file.defaults.formatter = Some(Formatter::LanguageServer);
4259 });
4260 });
4261 });
4262 let fake_language_server = fake_language_servers.next().await.unwrap();
4263 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4264 panic!(
4265 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4266 )
4267 });
4268
4269 project_b
4270 .update(cx_b, |project, cx| {
4271 project.format(
4272 HashSet::from_iter([buffer_b.clone()]),
4273 true,
4274 FormatTrigger::Save,
4275 cx,
4276 )
4277 })
4278 .await
4279 .unwrap();
4280
4281 executor.run_until_parked();
4282 assert_eq!(
4283 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4284 buffer_text.to_string() + "\n" + prettier_format_suffix,
4285 "Prettier formatting was not applied to client buffer after client's request"
4286 );
4287
4288 project_a
4289 .update(cx_a, |project, cx| {
4290 project.format(
4291 HashSet::from_iter([buffer_a.clone()]),
4292 true,
4293 FormatTrigger::Manual,
4294 cx,
4295 )
4296 })
4297 .await
4298 .unwrap();
4299
4300 executor.run_until_parked();
4301 assert_eq!(
4302 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4303 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4304 "Prettier formatting was not applied to client buffer after host's request"
4305 );
4306}
4307
4308#[gpui::test(iterations = 10)]
4309async fn test_definition(
4310 executor: BackgroundExecutor,
4311 cx_a: &mut TestAppContext,
4312 cx_b: &mut TestAppContext,
4313) {
4314 let mut server = TestServer::start(executor.clone()).await;
4315 let client_a = server.create_client(cx_a, "user_a").await;
4316 let client_b = server.create_client(cx_b, "user_b").await;
4317 server
4318 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4319 .await;
4320 let active_call_a = cx_a.read(ActiveCall::global);
4321
4322 // Set up a fake language server.
4323 let mut language = Language::new(
4324 LanguageConfig {
4325 name: "Rust".into(),
4326 path_suffixes: vec!["rs".to_string()],
4327 ..Default::default()
4328 },
4329 Some(tree_sitter_rust::language()),
4330 );
4331 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4332 client_a.language_registry().add(Arc::new(language));
4333
4334 client_a
4335 .fs()
4336 .insert_tree(
4337 "/root",
4338 json!({
4339 "dir-1": {
4340 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4341 },
4342 "dir-2": {
4343 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4344 "c.rs": "type T2 = usize;",
4345 }
4346 }),
4347 )
4348 .await;
4349 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4350 let project_id = active_call_a
4351 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4352 .await
4353 .unwrap();
4354 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4355
4356 // Open the file on client B.
4357 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4358 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4359
4360 // Request the definition of a symbol as the guest.
4361 let fake_language_server = fake_language_servers.next().await.unwrap();
4362 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4363 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4364 lsp::Location::new(
4365 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4366 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4367 ),
4368 )))
4369 });
4370
4371 let definitions_1 = project_b
4372 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4373 .await
4374 .unwrap();
4375 cx_b.read(|cx| {
4376 assert_eq!(definitions_1.len(), 1);
4377 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4378 let target_buffer = definitions_1[0].target.buffer.read(cx);
4379 assert_eq!(
4380 target_buffer.text(),
4381 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4382 );
4383 assert_eq!(
4384 definitions_1[0].target.range.to_point(target_buffer),
4385 Point::new(0, 6)..Point::new(0, 9)
4386 );
4387 });
4388
4389 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4390 // the previous call to `definition`.
4391 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4392 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4393 lsp::Location::new(
4394 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4395 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4396 ),
4397 )))
4398 });
4399
4400 let definitions_2 = project_b
4401 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4402 .await
4403 .unwrap();
4404 cx_b.read(|cx| {
4405 assert_eq!(definitions_2.len(), 1);
4406 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4407 let target_buffer = definitions_2[0].target.buffer.read(cx);
4408 assert_eq!(
4409 target_buffer.text(),
4410 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4411 );
4412 assert_eq!(
4413 definitions_2[0].target.range.to_point(target_buffer),
4414 Point::new(1, 6)..Point::new(1, 11)
4415 );
4416 });
4417 assert_eq!(
4418 definitions_1[0].target.buffer,
4419 definitions_2[0].target.buffer
4420 );
4421
4422 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4423 |req, _| async move {
4424 assert_eq!(
4425 req.text_document_position_params.position,
4426 lsp::Position::new(0, 7)
4427 );
4428 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4429 lsp::Location::new(
4430 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4431 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4432 ),
4433 )))
4434 },
4435 );
4436
4437 let type_definitions = project_b
4438 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4439 .await
4440 .unwrap();
4441 cx_b.read(|cx| {
4442 assert_eq!(type_definitions.len(), 1);
4443 let target_buffer = type_definitions[0].target.buffer.read(cx);
4444 assert_eq!(target_buffer.text(), "type T2 = usize;");
4445 assert_eq!(
4446 type_definitions[0].target.range.to_point(target_buffer),
4447 Point::new(0, 5)..Point::new(0, 7)
4448 );
4449 });
4450}
4451
4452#[gpui::test(iterations = 10)]
4453async fn test_references(
4454 executor: BackgroundExecutor,
4455 cx_a: &mut TestAppContext,
4456 cx_b: &mut TestAppContext,
4457) {
4458 let mut server = TestServer::start(executor.clone()).await;
4459 let client_a = server.create_client(cx_a, "user_a").await;
4460 let client_b = server.create_client(cx_b, "user_b").await;
4461 server
4462 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4463 .await;
4464 let active_call_a = cx_a.read(ActiveCall::global);
4465
4466 // Set up a fake language server.
4467 let mut language = Language::new(
4468 LanguageConfig {
4469 name: "Rust".into(),
4470 path_suffixes: vec!["rs".to_string()],
4471 ..Default::default()
4472 },
4473 Some(tree_sitter_rust::language()),
4474 );
4475 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4476 client_a.language_registry().add(Arc::new(language));
4477
4478 client_a
4479 .fs()
4480 .insert_tree(
4481 "/root",
4482 json!({
4483 "dir-1": {
4484 "one.rs": "const ONE: usize = 1;",
4485 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4486 },
4487 "dir-2": {
4488 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4489 }
4490 }),
4491 )
4492 .await;
4493 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4494 let project_id = active_call_a
4495 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4496 .await
4497 .unwrap();
4498 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4499
4500 // Open the file on client B.
4501 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4502 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4503
4504 // Request references to a symbol as the guest.
4505 let fake_language_server = fake_language_servers.next().await.unwrap();
4506 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4507 assert_eq!(
4508 params.text_document_position.text_document.uri.as_str(),
4509 "file:///root/dir-1/one.rs"
4510 );
4511 Ok(Some(vec![
4512 lsp::Location {
4513 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4514 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4515 },
4516 lsp::Location {
4517 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4518 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4519 },
4520 lsp::Location {
4521 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4522 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4523 },
4524 ]))
4525 });
4526
4527 let references = project_b
4528 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4529 .await
4530 .unwrap();
4531 cx_b.read(|cx| {
4532 assert_eq!(references.len(), 3);
4533 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4534
4535 let two_buffer = references[0].buffer.read(cx);
4536 let three_buffer = references[2].buffer.read(cx);
4537 assert_eq!(
4538 two_buffer.file().unwrap().path().as_ref(),
4539 Path::new("two.rs")
4540 );
4541 assert_eq!(references[1].buffer, references[0].buffer);
4542 assert_eq!(
4543 three_buffer.file().unwrap().full_path(cx),
4544 Path::new("/root/dir-2/three.rs")
4545 );
4546
4547 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4548 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4549 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4550 });
4551}
4552
4553#[gpui::test(iterations = 10)]
4554async fn test_project_search(
4555 executor: BackgroundExecutor,
4556 cx_a: &mut TestAppContext,
4557 cx_b: &mut TestAppContext,
4558) {
4559 let mut server = TestServer::start(executor.clone()).await;
4560 let client_a = server.create_client(cx_a, "user_a").await;
4561 let client_b = server.create_client(cx_b, "user_b").await;
4562 server
4563 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4564 .await;
4565 let active_call_a = cx_a.read(ActiveCall::global);
4566
4567 client_a
4568 .fs()
4569 .insert_tree(
4570 "/root",
4571 json!({
4572 "dir-1": {
4573 "a": "hello world",
4574 "b": "goodnight moon",
4575 "c": "a world of goo",
4576 "d": "world champion of clown world",
4577 },
4578 "dir-2": {
4579 "e": "disney world is fun",
4580 }
4581 }),
4582 )
4583 .await;
4584 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4585 let (worktree_2, _) = project_a
4586 .update(cx_a, |p, cx| {
4587 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4588 })
4589 .await
4590 .unwrap();
4591 worktree_2
4592 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4593 .await;
4594 let project_id = active_call_a
4595 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4596 .await
4597 .unwrap();
4598
4599 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4600
4601 // Perform a search as the guest.
4602 let mut results = HashMap::default();
4603 let mut search_rx = project_b.update(cx_b, |project, cx| {
4604 project.search(
4605 SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
4606 cx,
4607 )
4608 });
4609 while let Some((buffer, ranges)) = search_rx.next().await {
4610 results.entry(buffer).or_insert(ranges);
4611 }
4612
4613 let mut ranges_by_path = results
4614 .into_iter()
4615 .map(|(buffer, ranges)| {
4616 buffer.read_with(cx_b, |buffer, cx| {
4617 let path = buffer.file().unwrap().full_path(cx);
4618 let offset_ranges = ranges
4619 .into_iter()
4620 .map(|range| range.to_offset(buffer))
4621 .collect::<Vec<_>>();
4622 (path, offset_ranges)
4623 })
4624 })
4625 .collect::<Vec<_>>();
4626 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4627
4628 assert_eq!(
4629 ranges_by_path,
4630 &[
4631 (PathBuf::from("dir-1/a"), vec![6..11]),
4632 (PathBuf::from("dir-1/c"), vec![2..7]),
4633 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4634 (PathBuf::from("dir-2/e"), vec![7..12]),
4635 ]
4636 );
4637}
4638
4639#[gpui::test(iterations = 10)]
4640async fn test_document_highlights(
4641 executor: BackgroundExecutor,
4642 cx_a: &mut TestAppContext,
4643 cx_b: &mut TestAppContext,
4644) {
4645 let mut server = TestServer::start(executor.clone()).await;
4646 let client_a = server.create_client(cx_a, "user_a").await;
4647 let client_b = server.create_client(cx_b, "user_b").await;
4648 server
4649 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4650 .await;
4651 let active_call_a = cx_a.read(ActiveCall::global);
4652
4653 client_a
4654 .fs()
4655 .insert_tree(
4656 "/root-1",
4657 json!({
4658 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4659 }),
4660 )
4661 .await;
4662
4663 // Set up a fake language server.
4664 let mut language = Language::new(
4665 LanguageConfig {
4666 name: "Rust".into(),
4667 path_suffixes: vec!["rs".to_string()],
4668 ..Default::default()
4669 },
4670 Some(tree_sitter_rust::language()),
4671 );
4672 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4673 client_a.language_registry().add(Arc::new(language));
4674
4675 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4676 let project_id = active_call_a
4677 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4678 .await
4679 .unwrap();
4680 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4681
4682 // Open the file on client B.
4683 let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4684 let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
4685
4686 // Request document highlights as the guest.
4687 let fake_language_server = fake_language_servers.next().await.unwrap();
4688 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4689 |params, _| async move {
4690 assert_eq!(
4691 params
4692 .text_document_position_params
4693 .text_document
4694 .uri
4695 .as_str(),
4696 "file:///root-1/main.rs"
4697 );
4698 assert_eq!(
4699 params.text_document_position_params.position,
4700 lsp::Position::new(0, 34)
4701 );
4702 Ok(Some(vec![
4703 lsp::DocumentHighlight {
4704 kind: Some(lsp::DocumentHighlightKind::WRITE),
4705 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4706 },
4707 lsp::DocumentHighlight {
4708 kind: Some(lsp::DocumentHighlightKind::READ),
4709 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4710 },
4711 lsp::DocumentHighlight {
4712 kind: Some(lsp::DocumentHighlightKind::READ),
4713 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4714 },
4715 ]))
4716 },
4717 );
4718
4719 let highlights = project_b
4720 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4721 .await
4722 .unwrap();
4723
4724 buffer_b.read_with(cx_b, |buffer, _| {
4725 let snapshot = buffer.snapshot();
4726
4727 let highlights = highlights
4728 .into_iter()
4729 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4730 .collect::<Vec<_>>();
4731 assert_eq!(
4732 highlights,
4733 &[
4734 (lsp::DocumentHighlightKind::WRITE, 10..16),
4735 (lsp::DocumentHighlightKind::READ, 32..38),
4736 (lsp::DocumentHighlightKind::READ, 41..47)
4737 ]
4738 )
4739 });
4740}
4741
4742#[gpui::test(iterations = 10)]
4743async fn test_lsp_hover(
4744 executor: BackgroundExecutor,
4745 cx_a: &mut TestAppContext,
4746 cx_b: &mut TestAppContext,
4747) {
4748 let mut server = TestServer::start(executor.clone()).await;
4749 let client_a = server.create_client(cx_a, "user_a").await;
4750 let client_b = server.create_client(cx_b, "user_b").await;
4751 server
4752 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4753 .await;
4754 let active_call_a = cx_a.read(ActiveCall::global);
4755
4756 client_a
4757 .fs()
4758 .insert_tree(
4759 "/root-1",
4760 json!({
4761 "main.rs": "use std::collections::HashMap;",
4762 }),
4763 )
4764 .await;
4765
4766 // Set up a fake language server.
4767 let mut language = Language::new(
4768 LanguageConfig {
4769 name: "Rust".into(),
4770 path_suffixes: vec!["rs".to_string()],
4771 ..Default::default()
4772 },
4773 Some(tree_sitter_rust::language()),
4774 );
4775 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4776 client_a.language_registry().add(Arc::new(language));
4777
4778 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4779 let project_id = active_call_a
4780 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4781 .await
4782 .unwrap();
4783 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4784
4785 // Open the file as the guest
4786 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4787 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4788
4789 // Request hover information as the guest.
4790 let fake_language_server = fake_language_servers.next().await.unwrap();
4791 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4792 |params, _| async move {
4793 assert_eq!(
4794 params
4795 .text_document_position_params
4796 .text_document
4797 .uri
4798 .as_str(),
4799 "file:///root-1/main.rs"
4800 );
4801 assert_eq!(
4802 params.text_document_position_params.position,
4803 lsp::Position::new(0, 22)
4804 );
4805 Ok(Some(lsp::Hover {
4806 contents: lsp::HoverContents::Array(vec![
4807 lsp::MarkedString::String("Test hover content.".to_string()),
4808 lsp::MarkedString::LanguageString(lsp::LanguageString {
4809 language: "Rust".to_string(),
4810 value: "let foo = 42;".to_string(),
4811 }),
4812 ]),
4813 range: Some(lsp::Range::new(
4814 lsp::Position::new(0, 22),
4815 lsp::Position::new(0, 29),
4816 )),
4817 }))
4818 },
4819 );
4820
4821 let hover_info = project_b
4822 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
4823 .await
4824 .unwrap()
4825 .unwrap();
4826
4827 buffer_b.read_with(cx_b, |buffer, _| {
4828 let snapshot = buffer.snapshot();
4829 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
4830 assert_eq!(
4831 hover_info.contents,
4832 vec![
4833 project::HoverBlock {
4834 text: "Test hover content.".to_string(),
4835 kind: HoverBlockKind::Markdown,
4836 },
4837 project::HoverBlock {
4838 text: "let foo = 42;".to_string(),
4839 kind: HoverBlockKind::Code {
4840 language: "Rust".to_string()
4841 },
4842 }
4843 ]
4844 );
4845 });
4846}
4847
4848#[gpui::test(iterations = 10)]
4849async fn test_project_symbols(
4850 executor: BackgroundExecutor,
4851 cx_a: &mut TestAppContext,
4852 cx_b: &mut TestAppContext,
4853) {
4854 let mut server = TestServer::start(executor.clone()).await;
4855 let client_a = server.create_client(cx_a, "user_a").await;
4856 let client_b = server.create_client(cx_b, "user_b").await;
4857 server
4858 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4859 .await;
4860 let active_call_a = cx_a.read(ActiveCall::global);
4861
4862 // Set up a fake language server.
4863 let mut language = Language::new(
4864 LanguageConfig {
4865 name: "Rust".into(),
4866 path_suffixes: vec!["rs".to_string()],
4867 ..Default::default()
4868 },
4869 Some(tree_sitter_rust::language()),
4870 );
4871 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4872 client_a.language_registry().add(Arc::new(language));
4873
4874 client_a
4875 .fs()
4876 .insert_tree(
4877 "/code",
4878 json!({
4879 "crate-1": {
4880 "one.rs": "const ONE: usize = 1;",
4881 },
4882 "crate-2": {
4883 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
4884 },
4885 "private": {
4886 "passwords.txt": "the-password",
4887 }
4888 }),
4889 )
4890 .await;
4891 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
4892 let project_id = active_call_a
4893 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4894 .await
4895 .unwrap();
4896 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4897
4898 // Cause the language server to start.
4899 let open_buffer_task =
4900 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4901 let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
4902
4903 let fake_language_server = fake_language_servers.next().await.unwrap();
4904 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
4905 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
4906 #[allow(deprecated)]
4907 lsp::SymbolInformation {
4908 name: "TWO".into(),
4909 location: lsp::Location {
4910 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
4911 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4912 },
4913 kind: lsp::SymbolKind::CONSTANT,
4914 tags: None,
4915 container_name: None,
4916 deprecated: None,
4917 },
4918 ])))
4919 });
4920
4921 // Request the definition of a symbol as the guest.
4922 let symbols = project_b
4923 .update(cx_b, |p, cx| p.symbols("two", cx))
4924 .await
4925 .unwrap();
4926 assert_eq!(symbols.len(), 1);
4927 assert_eq!(symbols[0].name, "TWO");
4928
4929 // Open one of the returned symbols.
4930 let buffer_b_2 = project_b
4931 .update(cx_b, |project, cx| {
4932 project.open_buffer_for_symbol(&symbols[0], cx)
4933 })
4934 .await
4935 .unwrap();
4936
4937 buffer_b_2.read_with(cx_b, |buffer, _| {
4938 assert_eq!(
4939 buffer.file().unwrap().path().as_ref(),
4940 Path::new("../crate-2/two.rs")
4941 );
4942 });
4943
4944 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
4945 let mut fake_symbol = symbols[0].clone();
4946 fake_symbol.path.path = Path::new("/code/secrets").into();
4947 let error = project_b
4948 .update(cx_b, |project, cx| {
4949 project.open_buffer_for_symbol(&fake_symbol, cx)
4950 })
4951 .await
4952 .unwrap_err();
4953 assert!(error.to_string().contains("invalid symbol signature"));
4954}
4955
4956#[gpui::test(iterations = 10)]
4957async fn test_open_buffer_while_getting_definition_pointing_to_it(
4958 executor: BackgroundExecutor,
4959 cx_a: &mut TestAppContext,
4960 cx_b: &mut TestAppContext,
4961 mut rng: StdRng,
4962) {
4963 let mut server = TestServer::start(executor.clone()).await;
4964 let client_a = server.create_client(cx_a, "user_a").await;
4965 let client_b = server.create_client(cx_b, "user_b").await;
4966 server
4967 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4968 .await;
4969 let active_call_a = cx_a.read(ActiveCall::global);
4970
4971 // Set up a fake language server.
4972 let mut language = Language::new(
4973 LanguageConfig {
4974 name: "Rust".into(),
4975 path_suffixes: vec!["rs".to_string()],
4976 ..Default::default()
4977 },
4978 Some(tree_sitter_rust::language()),
4979 );
4980 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4981 client_a.language_registry().add(Arc::new(language));
4982
4983 client_a
4984 .fs()
4985 .insert_tree(
4986 "/root",
4987 json!({
4988 "a.rs": "const ONE: usize = b::TWO;",
4989 "b.rs": "const TWO: usize = 2",
4990 }),
4991 )
4992 .await;
4993 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
4994 let project_id = active_call_a
4995 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4996 .await
4997 .unwrap();
4998 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4999
5000 let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
5001 let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5002
5003 let fake_language_server = fake_language_servers.next().await.unwrap();
5004 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5005 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5006 lsp::Location::new(
5007 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5008 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5009 ),
5010 )))
5011 });
5012
5013 let definitions;
5014 let buffer_b2;
5015 if rng.gen() {
5016 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5017 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5018 } else {
5019 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5020 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5021 }
5022
5023 let buffer_b2 = buffer_b2.await.unwrap();
5024 let definitions = definitions.await.unwrap();
5025 assert_eq!(definitions.len(), 1);
5026 assert_eq!(definitions[0].target.buffer, buffer_b2);
5027}
5028
5029#[gpui::test(iterations = 10)]
5030async fn test_contacts(
5031 executor: BackgroundExecutor,
5032 cx_a: &mut TestAppContext,
5033 cx_b: &mut TestAppContext,
5034 cx_c: &mut TestAppContext,
5035 cx_d: &mut TestAppContext,
5036) {
5037 let mut server = TestServer::start(executor.clone()).await;
5038 let client_a = server.create_client(cx_a, "user_a").await;
5039 let client_b = server.create_client(cx_b, "user_b").await;
5040 let client_c = server.create_client(cx_c, "user_c").await;
5041 let client_d = server.create_client(cx_d, "user_d").await;
5042 server
5043 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5044 .await;
5045 let active_call_a = cx_a.read(ActiveCall::global);
5046 let active_call_b = cx_b.read(ActiveCall::global);
5047 let active_call_c = cx_c.read(ActiveCall::global);
5048 let _active_call_d = cx_d.read(ActiveCall::global);
5049
5050 executor.run_until_parked();
5051 assert_eq!(
5052 contacts(&client_a, cx_a),
5053 [
5054 ("user_b".to_string(), "online", "free"),
5055 ("user_c".to_string(), "online", "free")
5056 ]
5057 );
5058 assert_eq!(
5059 contacts(&client_b, cx_b),
5060 [
5061 ("user_a".to_string(), "online", "free"),
5062 ("user_c".to_string(), "online", "free")
5063 ]
5064 );
5065 assert_eq!(
5066 contacts(&client_c, cx_c),
5067 [
5068 ("user_a".to_string(), "online", "free"),
5069 ("user_b".to_string(), "online", "free")
5070 ]
5071 );
5072 assert_eq!(contacts(&client_d, cx_d), []);
5073
5074 server.disconnect_client(client_c.peer_id().unwrap());
5075 server.forbid_connections();
5076 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5077 assert_eq!(
5078 contacts(&client_a, cx_a),
5079 [
5080 ("user_b".to_string(), "online", "free"),
5081 ("user_c".to_string(), "offline", "free")
5082 ]
5083 );
5084 assert_eq!(
5085 contacts(&client_b, cx_b),
5086 [
5087 ("user_a".to_string(), "online", "free"),
5088 ("user_c".to_string(), "offline", "free")
5089 ]
5090 );
5091 assert_eq!(contacts(&client_c, cx_c), []);
5092 assert_eq!(contacts(&client_d, cx_d), []);
5093
5094 server.allow_connections();
5095 client_c
5096 .authenticate_and_connect(false, &cx_c.to_async())
5097 .await
5098 .unwrap();
5099
5100 executor.run_until_parked();
5101 assert_eq!(
5102 contacts(&client_a, cx_a),
5103 [
5104 ("user_b".to_string(), "online", "free"),
5105 ("user_c".to_string(), "online", "free")
5106 ]
5107 );
5108 assert_eq!(
5109 contacts(&client_b, cx_b),
5110 [
5111 ("user_a".to_string(), "online", "free"),
5112 ("user_c".to_string(), "online", "free")
5113 ]
5114 );
5115 assert_eq!(
5116 contacts(&client_c, cx_c),
5117 [
5118 ("user_a".to_string(), "online", "free"),
5119 ("user_b".to_string(), "online", "free")
5120 ]
5121 );
5122 assert_eq!(contacts(&client_d, cx_d), []);
5123
5124 active_call_a
5125 .update(cx_a, |call, cx| {
5126 call.invite(client_b.user_id().unwrap(), None, cx)
5127 })
5128 .await
5129 .unwrap();
5130 executor.run_until_parked();
5131 assert_eq!(
5132 contacts(&client_a, cx_a),
5133 [
5134 ("user_b".to_string(), "online", "busy"),
5135 ("user_c".to_string(), "online", "free")
5136 ]
5137 );
5138 assert_eq!(
5139 contacts(&client_b, cx_b),
5140 [
5141 ("user_a".to_string(), "online", "busy"),
5142 ("user_c".to_string(), "online", "free")
5143 ]
5144 );
5145 assert_eq!(
5146 contacts(&client_c, cx_c),
5147 [
5148 ("user_a".to_string(), "online", "busy"),
5149 ("user_b".to_string(), "online", "busy")
5150 ]
5151 );
5152 assert_eq!(contacts(&client_d, cx_d), []);
5153
5154 // Client B and client D become contacts while client B is being called.
5155 server
5156 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5157 .await;
5158 executor.run_until_parked();
5159 assert_eq!(
5160 contacts(&client_a, cx_a),
5161 [
5162 ("user_b".to_string(), "online", "busy"),
5163 ("user_c".to_string(), "online", "free")
5164 ]
5165 );
5166 assert_eq!(
5167 contacts(&client_b, cx_b),
5168 [
5169 ("user_a".to_string(), "online", "busy"),
5170 ("user_c".to_string(), "online", "free"),
5171 ("user_d".to_string(), "online", "free"),
5172 ]
5173 );
5174 assert_eq!(
5175 contacts(&client_c, cx_c),
5176 [
5177 ("user_a".to_string(), "online", "busy"),
5178 ("user_b".to_string(), "online", "busy")
5179 ]
5180 );
5181 assert_eq!(
5182 contacts(&client_d, cx_d),
5183 [("user_b".to_string(), "online", "busy")]
5184 );
5185
5186 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5187 executor.run_until_parked();
5188 assert_eq!(
5189 contacts(&client_a, cx_a),
5190 [
5191 ("user_b".to_string(), "online", "free"),
5192 ("user_c".to_string(), "online", "free")
5193 ]
5194 );
5195 assert_eq!(
5196 contacts(&client_b, cx_b),
5197 [
5198 ("user_a".to_string(), "online", "free"),
5199 ("user_c".to_string(), "online", "free"),
5200 ("user_d".to_string(), "online", "free")
5201 ]
5202 );
5203 assert_eq!(
5204 contacts(&client_c, cx_c),
5205 [
5206 ("user_a".to_string(), "online", "free"),
5207 ("user_b".to_string(), "online", "free")
5208 ]
5209 );
5210 assert_eq!(
5211 contacts(&client_d, cx_d),
5212 [("user_b".to_string(), "online", "free")]
5213 );
5214
5215 active_call_c
5216 .update(cx_c, |call, cx| {
5217 call.invite(client_a.user_id().unwrap(), None, cx)
5218 })
5219 .await
5220 .unwrap();
5221 executor.run_until_parked();
5222 assert_eq!(
5223 contacts(&client_a, cx_a),
5224 [
5225 ("user_b".to_string(), "online", "free"),
5226 ("user_c".to_string(), "online", "busy")
5227 ]
5228 );
5229 assert_eq!(
5230 contacts(&client_b, cx_b),
5231 [
5232 ("user_a".to_string(), "online", "busy"),
5233 ("user_c".to_string(), "online", "busy"),
5234 ("user_d".to_string(), "online", "free")
5235 ]
5236 );
5237 assert_eq!(
5238 contacts(&client_c, cx_c),
5239 [
5240 ("user_a".to_string(), "online", "busy"),
5241 ("user_b".to_string(), "online", "free")
5242 ]
5243 );
5244 assert_eq!(
5245 contacts(&client_d, cx_d),
5246 [("user_b".to_string(), "online", "free")]
5247 );
5248
5249 active_call_a
5250 .update(cx_a, |call, cx| call.accept_incoming(cx))
5251 .await
5252 .unwrap();
5253 executor.run_until_parked();
5254 assert_eq!(
5255 contacts(&client_a, cx_a),
5256 [
5257 ("user_b".to_string(), "online", "free"),
5258 ("user_c".to_string(), "online", "busy")
5259 ]
5260 );
5261 assert_eq!(
5262 contacts(&client_b, cx_b),
5263 [
5264 ("user_a".to_string(), "online", "busy"),
5265 ("user_c".to_string(), "online", "busy"),
5266 ("user_d".to_string(), "online", "free")
5267 ]
5268 );
5269 assert_eq!(
5270 contacts(&client_c, cx_c),
5271 [
5272 ("user_a".to_string(), "online", "busy"),
5273 ("user_b".to_string(), "online", "free")
5274 ]
5275 );
5276 assert_eq!(
5277 contacts(&client_d, cx_d),
5278 [("user_b".to_string(), "online", "free")]
5279 );
5280
5281 active_call_a
5282 .update(cx_a, |call, cx| {
5283 call.invite(client_b.user_id().unwrap(), None, cx)
5284 })
5285 .await
5286 .unwrap();
5287 executor.run_until_parked();
5288 assert_eq!(
5289 contacts(&client_a, cx_a),
5290 [
5291 ("user_b".to_string(), "online", "busy"),
5292 ("user_c".to_string(), "online", "busy")
5293 ]
5294 );
5295 assert_eq!(
5296 contacts(&client_b, cx_b),
5297 [
5298 ("user_a".to_string(), "online", "busy"),
5299 ("user_c".to_string(), "online", "busy"),
5300 ("user_d".to_string(), "online", "free")
5301 ]
5302 );
5303 assert_eq!(
5304 contacts(&client_c, cx_c),
5305 [
5306 ("user_a".to_string(), "online", "busy"),
5307 ("user_b".to_string(), "online", "busy")
5308 ]
5309 );
5310 assert_eq!(
5311 contacts(&client_d, cx_d),
5312 [("user_b".to_string(), "online", "busy")]
5313 );
5314
5315 active_call_a
5316 .update(cx_a, |call, cx| call.hang_up(cx))
5317 .await
5318 .unwrap();
5319 executor.run_until_parked();
5320 assert_eq!(
5321 contacts(&client_a, cx_a),
5322 [
5323 ("user_b".to_string(), "online", "free"),
5324 ("user_c".to_string(), "online", "free")
5325 ]
5326 );
5327 assert_eq!(
5328 contacts(&client_b, cx_b),
5329 [
5330 ("user_a".to_string(), "online", "free"),
5331 ("user_c".to_string(), "online", "free"),
5332 ("user_d".to_string(), "online", "free")
5333 ]
5334 );
5335 assert_eq!(
5336 contacts(&client_c, cx_c),
5337 [
5338 ("user_a".to_string(), "online", "free"),
5339 ("user_b".to_string(), "online", "free")
5340 ]
5341 );
5342 assert_eq!(
5343 contacts(&client_d, cx_d),
5344 [("user_b".to_string(), "online", "free")]
5345 );
5346
5347 active_call_a
5348 .update(cx_a, |call, cx| {
5349 call.invite(client_b.user_id().unwrap(), None, cx)
5350 })
5351 .await
5352 .unwrap();
5353 executor.run_until_parked();
5354 assert_eq!(
5355 contacts(&client_a, cx_a),
5356 [
5357 ("user_b".to_string(), "online", "busy"),
5358 ("user_c".to_string(), "online", "free")
5359 ]
5360 );
5361 assert_eq!(
5362 contacts(&client_b, cx_b),
5363 [
5364 ("user_a".to_string(), "online", "busy"),
5365 ("user_c".to_string(), "online", "free"),
5366 ("user_d".to_string(), "online", "free")
5367 ]
5368 );
5369 assert_eq!(
5370 contacts(&client_c, cx_c),
5371 [
5372 ("user_a".to_string(), "online", "busy"),
5373 ("user_b".to_string(), "online", "busy")
5374 ]
5375 );
5376 assert_eq!(
5377 contacts(&client_d, cx_d),
5378 [("user_b".to_string(), "online", "busy")]
5379 );
5380
5381 server.forbid_connections();
5382 server.disconnect_client(client_a.peer_id().unwrap());
5383 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5384 assert_eq!(contacts(&client_a, cx_a), []);
5385 assert_eq!(
5386 contacts(&client_b, cx_b),
5387 [
5388 ("user_a".to_string(), "offline", "free"),
5389 ("user_c".to_string(), "online", "free"),
5390 ("user_d".to_string(), "online", "free")
5391 ]
5392 );
5393 assert_eq!(
5394 contacts(&client_c, cx_c),
5395 [
5396 ("user_a".to_string(), "offline", "free"),
5397 ("user_b".to_string(), "online", "free")
5398 ]
5399 );
5400 assert_eq!(
5401 contacts(&client_d, cx_d),
5402 [("user_b".to_string(), "online", "free")]
5403 );
5404
5405 // Test removing a contact
5406 client_b
5407 .user_store()
5408 .update(cx_b, |store, cx| {
5409 store.remove_contact(client_c.user_id().unwrap(), cx)
5410 })
5411 .await
5412 .unwrap();
5413 executor.run_until_parked();
5414 assert_eq!(
5415 contacts(&client_b, cx_b),
5416 [
5417 ("user_a".to_string(), "offline", "free"),
5418 ("user_d".to_string(), "online", "free")
5419 ]
5420 );
5421 assert_eq!(
5422 contacts(&client_c, cx_c),
5423 [("user_a".to_string(), "offline", "free"),]
5424 );
5425
5426 fn contacts(
5427 client: &TestClient,
5428 cx: &TestAppContext,
5429 ) -> Vec<(String, &'static str, &'static str)> {
5430 client.user_store().read_with(cx, |store, _| {
5431 store
5432 .contacts()
5433 .iter()
5434 .map(|contact| {
5435 (
5436 contact.user.github_login.clone(),
5437 if contact.online { "online" } else { "offline" },
5438 if contact.busy { "busy" } else { "free" },
5439 )
5440 })
5441 .collect()
5442 })
5443 }
5444}
5445
5446#[gpui::test(iterations = 10)]
5447async fn test_contact_requests(
5448 executor: BackgroundExecutor,
5449 cx_a: &mut TestAppContext,
5450 cx_a2: &mut TestAppContext,
5451 cx_b: &mut TestAppContext,
5452 cx_b2: &mut TestAppContext,
5453 cx_c: &mut TestAppContext,
5454 cx_c2: &mut TestAppContext,
5455) {
5456 // Connect to a server as 3 clients.
5457 let mut server = TestServer::start(executor.clone()).await;
5458 let client_a = server.create_client(cx_a, "user_a").await;
5459 let client_a2 = server.create_client(cx_a2, "user_a").await;
5460 let client_b = server.create_client(cx_b, "user_b").await;
5461 let client_b2 = server.create_client(cx_b2, "user_b").await;
5462 let client_c = server.create_client(cx_c, "user_c").await;
5463 let client_c2 = server.create_client(cx_c2, "user_c").await;
5464
5465 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5466 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5467 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5468
5469 // User A and User C request that user B become their contact.
5470 client_a
5471 .user_store()
5472 .update(cx_a, |store, cx| {
5473 store.request_contact(client_b.user_id().unwrap(), cx)
5474 })
5475 .await
5476 .unwrap();
5477 client_c
5478 .user_store()
5479 .update(cx_c, |store, cx| {
5480 store.request_contact(client_b.user_id().unwrap(), cx)
5481 })
5482 .await
5483 .unwrap();
5484 executor.run_until_parked();
5485
5486 // All users see the pending request appear in all their clients.
5487 assert_eq!(
5488 client_a.summarize_contacts(cx_a).outgoing_requests,
5489 &["user_b"]
5490 );
5491 assert_eq!(
5492 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5493 &["user_b"]
5494 );
5495 assert_eq!(
5496 client_b.summarize_contacts(cx_b).incoming_requests,
5497 &["user_a", "user_c"]
5498 );
5499 assert_eq!(
5500 client_b2.summarize_contacts(cx_b2).incoming_requests,
5501 &["user_a", "user_c"]
5502 );
5503 assert_eq!(
5504 client_c.summarize_contacts(cx_c).outgoing_requests,
5505 &["user_b"]
5506 );
5507 assert_eq!(
5508 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5509 &["user_b"]
5510 );
5511
5512 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5513 disconnect_and_reconnect(&client_a, cx_a).await;
5514 disconnect_and_reconnect(&client_b, cx_b).await;
5515 disconnect_and_reconnect(&client_c, cx_c).await;
5516 executor.run_until_parked();
5517 assert_eq!(
5518 client_a.summarize_contacts(cx_a).outgoing_requests,
5519 &["user_b"]
5520 );
5521 assert_eq!(
5522 client_b.summarize_contacts(cx_b).incoming_requests,
5523 &["user_a", "user_c"]
5524 );
5525 assert_eq!(
5526 client_c.summarize_contacts(cx_c).outgoing_requests,
5527 &["user_b"]
5528 );
5529
5530 // User B accepts the request from user A.
5531 client_b
5532 .user_store()
5533 .update(cx_b, |store, cx| {
5534 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5535 })
5536 .await
5537 .unwrap();
5538
5539 executor.run_until_parked();
5540
5541 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5542 let contacts_b = client_b.summarize_contacts(cx_b);
5543 assert_eq!(contacts_b.current, &["user_a"]);
5544 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5545 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5546 assert_eq!(contacts_b2.current, &["user_a"]);
5547 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5548
5549 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5550 let contacts_a = client_a.summarize_contacts(cx_a);
5551 assert_eq!(contacts_a.current, &["user_b"]);
5552 assert!(contacts_a.outgoing_requests.is_empty());
5553 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5554 assert_eq!(contacts_a2.current, &["user_b"]);
5555 assert!(contacts_a2.outgoing_requests.is_empty());
5556
5557 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5558 disconnect_and_reconnect(&client_a, cx_a).await;
5559 disconnect_and_reconnect(&client_b, cx_b).await;
5560 disconnect_and_reconnect(&client_c, cx_c).await;
5561 executor.run_until_parked();
5562 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5563 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5564 assert_eq!(
5565 client_b.summarize_contacts(cx_b).incoming_requests,
5566 &["user_c"]
5567 );
5568 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5569 assert_eq!(
5570 client_c.summarize_contacts(cx_c).outgoing_requests,
5571 &["user_b"]
5572 );
5573
5574 // User B rejects the request from user C.
5575 client_b
5576 .user_store()
5577 .update(cx_b, |store, cx| {
5578 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5579 })
5580 .await
5581 .unwrap();
5582
5583 executor.run_until_parked();
5584
5585 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5586 let contacts_b = client_b.summarize_contacts(cx_b);
5587 assert_eq!(contacts_b.current, &["user_a"]);
5588 assert!(contacts_b.incoming_requests.is_empty());
5589 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5590 assert_eq!(contacts_b2.current, &["user_a"]);
5591 assert!(contacts_b2.incoming_requests.is_empty());
5592
5593 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5594 let contacts_c = client_c.summarize_contacts(cx_c);
5595 assert!(contacts_c.current.is_empty());
5596 assert!(contacts_c.outgoing_requests.is_empty());
5597 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5598 assert!(contacts_c2.current.is_empty());
5599 assert!(contacts_c2.outgoing_requests.is_empty());
5600
5601 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5602 disconnect_and_reconnect(&client_a, cx_a).await;
5603 disconnect_and_reconnect(&client_b, cx_b).await;
5604 disconnect_and_reconnect(&client_c, cx_c).await;
5605 executor.run_until_parked();
5606 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5607 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5608 assert!(client_b
5609 .summarize_contacts(cx_b)
5610 .incoming_requests
5611 .is_empty());
5612 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5613 assert!(client_c
5614 .summarize_contacts(cx_c)
5615 .outgoing_requests
5616 .is_empty());
5617
5618 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
5619 client.disconnect(&cx.to_async());
5620 client.clear_contacts(cx).await;
5621 client
5622 .authenticate_and_connect(false, &cx.to_async())
5623 .await
5624 .unwrap();
5625 }
5626}
5627
5628#[gpui::test(iterations = 10)]
5629async fn test_join_call_after_screen_was_shared(
5630 executor: BackgroundExecutor,
5631 cx_a: &mut TestAppContext,
5632 cx_b: &mut TestAppContext,
5633) {
5634 let mut server = TestServer::start(executor.clone()).await;
5635
5636 let client_a = server.create_client(cx_a, "user_a").await;
5637 let client_b = server.create_client(cx_b, "user_b").await;
5638 server
5639 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5640 .await;
5641
5642 let active_call_a = cx_a.read(ActiveCall::global);
5643 let active_call_b = cx_b.read(ActiveCall::global);
5644
5645 // Call users B and C from client A.
5646 active_call_a
5647 .update(cx_a, |call, cx| {
5648 call.invite(client_b.user_id().unwrap(), None, cx)
5649 })
5650 .await
5651 .unwrap();
5652
5653 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
5654 executor.run_until_parked();
5655 assert_eq!(
5656 room_participants(&room_a, cx_a),
5657 RoomParticipants {
5658 remote: Default::default(),
5659 pending: vec!["user_b".to_string()]
5660 }
5661 );
5662
5663 // User B receives the call.
5664
5665 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
5666 let call_b = incoming_call_b.next().await.unwrap().unwrap();
5667 assert_eq!(call_b.calling_user.github_login, "user_a");
5668
5669 // User A shares their screen
5670 let display = MacOSDisplay::new();
5671 active_call_a
5672 .update(cx_a, |call, cx| {
5673 call.room().unwrap().update(cx, |room, cx| {
5674 room.set_display_sources(vec![display.clone()]);
5675 room.share_screen(cx)
5676 })
5677 })
5678 .await
5679 .unwrap();
5680
5681 client_b.user_store().update(cx_b, |user_store, _| {
5682 user_store.clear_cache();
5683 });
5684
5685 // User B joins the room
5686 active_call_b
5687 .update(cx_b, |call, cx| call.accept_incoming(cx))
5688 .await
5689 .unwrap();
5690
5691 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
5692 assert!(incoming_call_b.next().await.unwrap().is_none());
5693
5694 executor.run_until_parked();
5695 assert_eq!(
5696 room_participants(&room_a, cx_a),
5697 RoomParticipants {
5698 remote: vec!["user_b".to_string()],
5699 pending: vec![],
5700 }
5701 );
5702 assert_eq!(
5703 room_participants(&room_b, cx_b),
5704 RoomParticipants {
5705 remote: vec!["user_a".to_string()],
5706 pending: vec![],
5707 }
5708 );
5709
5710 // Ensure User B sees User A's screenshare.
5711
5712 room_b.read_with(cx_b, |room, _| {
5713 assert_eq!(
5714 room.remote_participants()
5715 .get(&client_a.user_id().unwrap())
5716 .unwrap()
5717 .video_tracks
5718 .len(),
5719 1
5720 );
5721 });
5722}