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