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
4663 .language_registry()
4664 .register_fake_lsp_adapter("Rust", Default::default());
4665
4666 client_a
4667 .fs()
4668 .insert_tree(
4669 "/root",
4670 json!({
4671 "dir-1": {
4672 "one.rs": "const ONE: usize = 1;",
4673 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4674 },
4675 "dir-2": {
4676 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4677 }
4678 }),
4679 )
4680 .await;
4681 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4682 let project_id = active_call_a
4683 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4684 .await
4685 .unwrap();
4686 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4687
4688 // Open the file on client B.
4689 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4690 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4691
4692 // Request references to a symbol as the guest.
4693 let fake_language_server = fake_language_servers.next().await.unwrap();
4694 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4695 assert_eq!(
4696 params.text_document_position.text_document.uri.as_str(),
4697 "file:///root/dir-1/one.rs"
4698 );
4699 Ok(Some(vec![
4700 lsp::Location {
4701 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4702 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4703 },
4704 lsp::Location {
4705 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4706 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4707 },
4708 lsp::Location {
4709 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4710 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4711 },
4712 ]))
4713 });
4714
4715 let references = project_b
4716 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4717 .await
4718 .unwrap();
4719 cx_b.read(|cx| {
4720 assert_eq!(references.len(), 3);
4721 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4722
4723 let two_buffer = references[0].buffer.read(cx);
4724 let three_buffer = references[2].buffer.read(cx);
4725 assert_eq!(
4726 two_buffer.file().unwrap().path().as_ref(),
4727 Path::new("two.rs")
4728 );
4729 assert_eq!(references[1].buffer, references[0].buffer);
4730 assert_eq!(
4731 three_buffer.file().unwrap().full_path(cx),
4732 Path::new("/root/dir-2/three.rs")
4733 );
4734
4735 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4736 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4737 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4738 });
4739}
4740
4741#[gpui::test(iterations = 10)]
4742async fn test_project_search(
4743 executor: BackgroundExecutor,
4744 cx_a: &mut TestAppContext,
4745 cx_b: &mut TestAppContext,
4746) {
4747 let mut server = TestServer::start(executor.clone()).await;
4748 let client_a = server.create_client(cx_a, "user_a").await;
4749 let client_b = server.create_client(cx_b, "user_b").await;
4750 server
4751 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4752 .await;
4753 let active_call_a = cx_a.read(ActiveCall::global);
4754
4755 client_a
4756 .fs()
4757 .insert_tree(
4758 "/root",
4759 json!({
4760 "dir-1": {
4761 "a": "hello world",
4762 "b": "goodnight moon",
4763 "c": "a world of goo",
4764 "d": "world champion of clown world",
4765 },
4766 "dir-2": {
4767 "e": "disney world is fun",
4768 }
4769 }),
4770 )
4771 .await;
4772 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4773 let (worktree_2, _) = project_a
4774 .update(cx_a, |p, cx| {
4775 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4776 })
4777 .await
4778 .unwrap();
4779 worktree_2
4780 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4781 .await;
4782 let project_id = active_call_a
4783 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4784 .await
4785 .unwrap();
4786
4787 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4788
4789 // Perform a search as the guest.
4790 let mut results = HashMap::default();
4791 let mut search_rx = project_b.update(cx_b, |project, cx| {
4792 project.search(
4793 SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
4794 cx,
4795 )
4796 });
4797 while let Some(result) = search_rx.next().await {
4798 match result {
4799 SearchResult::Buffer { buffer, ranges } => {
4800 results.entry(buffer).or_insert(ranges);
4801 }
4802 SearchResult::LimitReached => {
4803 panic!("Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call.")
4804 }
4805 };
4806 }
4807
4808 let mut ranges_by_path = results
4809 .into_iter()
4810 .map(|(buffer, ranges)| {
4811 buffer.read_with(cx_b, |buffer, cx| {
4812 let path = buffer.file().unwrap().full_path(cx);
4813 let offset_ranges = ranges
4814 .into_iter()
4815 .map(|range| range.to_offset(buffer))
4816 .collect::<Vec<_>>();
4817 (path, offset_ranges)
4818 })
4819 })
4820 .collect::<Vec<_>>();
4821 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4822
4823 assert_eq!(
4824 ranges_by_path,
4825 &[
4826 (PathBuf::from("dir-1/a"), vec![6..11]),
4827 (PathBuf::from("dir-1/c"), vec![2..7]),
4828 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4829 (PathBuf::from("dir-2/e"), vec![7..12]),
4830 ]
4831 );
4832}
4833
4834#[gpui::test(iterations = 10)]
4835async fn test_document_highlights(
4836 executor: BackgroundExecutor,
4837 cx_a: &mut TestAppContext,
4838 cx_b: &mut TestAppContext,
4839) {
4840 let mut server = TestServer::start(executor.clone()).await;
4841 let client_a = server.create_client(cx_a, "user_a").await;
4842 let client_b = server.create_client(cx_b, "user_b").await;
4843 server
4844 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4845 .await;
4846 let active_call_a = cx_a.read(ActiveCall::global);
4847
4848 client_a
4849 .fs()
4850 .insert_tree(
4851 "/root-1",
4852 json!({
4853 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4854 }),
4855 )
4856 .await;
4857
4858 let mut fake_language_servers = client_a
4859 .language_registry()
4860 .register_fake_lsp_adapter("Rust", Default::default());
4861 client_a.language_registry().add(rust_lang());
4862
4863 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4864 let project_id = active_call_a
4865 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4866 .await
4867 .unwrap();
4868 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4869
4870 // Open the file on client B.
4871 let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4872 let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
4873
4874 // Request document highlights as the guest.
4875 let fake_language_server = fake_language_servers.next().await.unwrap();
4876 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4877 |params, _| async move {
4878 assert_eq!(
4879 params
4880 .text_document_position_params
4881 .text_document
4882 .uri
4883 .as_str(),
4884 "file:///root-1/main.rs"
4885 );
4886 assert_eq!(
4887 params.text_document_position_params.position,
4888 lsp::Position::new(0, 34)
4889 );
4890 Ok(Some(vec![
4891 lsp::DocumentHighlight {
4892 kind: Some(lsp::DocumentHighlightKind::WRITE),
4893 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4894 },
4895 lsp::DocumentHighlight {
4896 kind: Some(lsp::DocumentHighlightKind::READ),
4897 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4898 },
4899 lsp::DocumentHighlight {
4900 kind: Some(lsp::DocumentHighlightKind::READ),
4901 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4902 },
4903 ]))
4904 },
4905 );
4906
4907 let highlights = project_b
4908 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4909 .await
4910 .unwrap();
4911
4912 buffer_b.read_with(cx_b, |buffer, _| {
4913 let snapshot = buffer.snapshot();
4914
4915 let highlights = highlights
4916 .into_iter()
4917 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4918 .collect::<Vec<_>>();
4919 assert_eq!(
4920 highlights,
4921 &[
4922 (lsp::DocumentHighlightKind::WRITE, 10..16),
4923 (lsp::DocumentHighlightKind::READ, 32..38),
4924 (lsp::DocumentHighlightKind::READ, 41..47)
4925 ]
4926 )
4927 });
4928}
4929
4930#[gpui::test(iterations = 10)]
4931async fn test_lsp_hover(
4932 executor: BackgroundExecutor,
4933 cx_a: &mut TestAppContext,
4934 cx_b: &mut TestAppContext,
4935) {
4936 let mut server = TestServer::start(executor.clone()).await;
4937 let client_a = server.create_client(cx_a, "user_a").await;
4938 let client_b = server.create_client(cx_b, "user_b").await;
4939 server
4940 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4941 .await;
4942 let active_call_a = cx_a.read(ActiveCall::global);
4943
4944 client_a
4945 .fs()
4946 .insert_tree(
4947 "/root-1",
4948 json!({
4949 "main.rs": "use std::collections::HashMap;",
4950 }),
4951 )
4952 .await;
4953
4954 client_a.language_registry().add(rust_lang());
4955 let language_server_names = ["rust-analyzer", "CrabLang-ls"];
4956 let mut fake_language_servers = client_a
4957 .language_registry()
4958 .register_specific_fake_lsp_adapter(
4959 "Rust",
4960 true,
4961 FakeLspAdapter {
4962 name: "rust-analyzer",
4963 capabilities: lsp::ServerCapabilities {
4964 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4965 ..lsp::ServerCapabilities::default()
4966 },
4967 ..FakeLspAdapter::default()
4968 },
4969 );
4970 let _other_server = client_a
4971 .language_registry()
4972 .register_specific_fake_lsp_adapter(
4973 "Rust",
4974 false,
4975 FakeLspAdapter {
4976 name: "CrabLang-ls",
4977 capabilities: lsp::ServerCapabilities {
4978 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4979 ..lsp::ServerCapabilities::default()
4980 },
4981 ..FakeLspAdapter::default()
4982 },
4983 );
4984
4985 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4986 let project_id = active_call_a
4987 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4988 .await
4989 .unwrap();
4990 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4991
4992 // Open the file as the guest
4993 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4994 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4995
4996 let mut servers_with_hover_requests = HashMap::default();
4997 for i in 0..language_server_names.len() {
4998 let new_server = fake_language_servers.next().await.unwrap_or_else(|| {
4999 panic!(
5000 "Failed to get language server #{i} with name {}",
5001 &language_server_names[i]
5002 )
5003 });
5004 let new_server_name = new_server.server.name();
5005 assert!(
5006 !servers_with_hover_requests.contains_key(new_server_name),
5007 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5008 );
5009 let new_server_name = new_server_name.to_string();
5010 match new_server_name.as_str() {
5011 "CrabLang-ls" => {
5012 servers_with_hover_requests.insert(
5013 new_server_name.clone(),
5014 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5015 move |params, _| {
5016 assert_eq!(
5017 params
5018 .text_document_position_params
5019 .text_document
5020 .uri
5021 .as_str(),
5022 "file:///root-1/main.rs"
5023 );
5024 let name = new_server_name.clone();
5025 async move {
5026 Ok(Some(lsp::Hover {
5027 contents: lsp::HoverContents::Scalar(
5028 lsp::MarkedString::String(format!("{name} hover")),
5029 ),
5030 range: None,
5031 }))
5032 }
5033 },
5034 ),
5035 );
5036 }
5037 "rust-analyzer" => {
5038 servers_with_hover_requests.insert(
5039 new_server_name.clone(),
5040 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5041 |params, _| async move {
5042 assert_eq!(
5043 params
5044 .text_document_position_params
5045 .text_document
5046 .uri
5047 .as_str(),
5048 "file:///root-1/main.rs"
5049 );
5050 assert_eq!(
5051 params.text_document_position_params.position,
5052 lsp::Position::new(0, 22)
5053 );
5054 Ok(Some(lsp::Hover {
5055 contents: lsp::HoverContents::Array(vec![
5056 lsp::MarkedString::String("Test hover content.".to_string()),
5057 lsp::MarkedString::LanguageString(lsp::LanguageString {
5058 language: "Rust".to_string(),
5059 value: "let foo = 42;".to_string(),
5060 }),
5061 ]),
5062 range: Some(lsp::Range::new(
5063 lsp::Position::new(0, 22),
5064 lsp::Position::new(0, 29),
5065 )),
5066 }))
5067 },
5068 ),
5069 );
5070 }
5071 unexpected => panic!("Unexpected server name: {unexpected}"),
5072 }
5073 }
5074
5075 // Request hover information as the guest.
5076 let mut hovers = project_b
5077 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5078 .await;
5079 assert_eq!(
5080 hovers.len(),
5081 2,
5082 "Expected two hovers from both language servers, but got: {hovers:?}"
5083 );
5084
5085 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5086 |mut hover_request| async move {
5087 hover_request
5088 .next()
5089 .await
5090 .expect("All hover requests should have been triggered")
5091 },
5092 ))
5093 .await;
5094
5095 hovers.sort_by_key(|hover| hover.contents.len());
5096 let first_hover = hovers.first().cloned().unwrap();
5097 assert_eq!(
5098 first_hover.contents,
5099 vec![project::HoverBlock {
5100 text: "CrabLang-ls hover".to_string(),
5101 kind: HoverBlockKind::Markdown,
5102 },]
5103 );
5104 let second_hover = hovers.last().cloned().unwrap();
5105 assert_eq!(
5106 second_hover.contents,
5107 vec![
5108 project::HoverBlock {
5109 text: "Test hover content.".to_string(),
5110 kind: HoverBlockKind::Markdown,
5111 },
5112 project::HoverBlock {
5113 text: "let foo = 42;".to_string(),
5114 kind: HoverBlockKind::Code {
5115 language: "Rust".to_string()
5116 },
5117 }
5118 ]
5119 );
5120 buffer_b.read_with(cx_b, |buffer, _| {
5121 let snapshot = buffer.snapshot();
5122 assert_eq!(second_hover.range.unwrap().to_offset(&snapshot), 22..29);
5123 });
5124}
5125
5126#[gpui::test(iterations = 10)]
5127async fn test_project_symbols(
5128 executor: BackgroundExecutor,
5129 cx_a: &mut TestAppContext,
5130 cx_b: &mut TestAppContext,
5131) {
5132 let mut server = TestServer::start(executor.clone()).await;
5133 let client_a = server.create_client(cx_a, "user_a").await;
5134 let client_b = server.create_client(cx_b, "user_b").await;
5135 server
5136 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5137 .await;
5138 let active_call_a = cx_a.read(ActiveCall::global);
5139
5140 client_a.language_registry().add(rust_lang());
5141 let mut fake_language_servers = client_a
5142 .language_registry()
5143 .register_fake_lsp_adapter("Rust", Default::default());
5144
5145 client_a
5146 .fs()
5147 .insert_tree(
5148 "/code",
5149 json!({
5150 "crate-1": {
5151 "one.rs": "const ONE: usize = 1;",
5152 },
5153 "crate-2": {
5154 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5155 },
5156 "private": {
5157 "passwords.txt": "the-password",
5158 }
5159 }),
5160 )
5161 .await;
5162 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
5163 let project_id = active_call_a
5164 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5165 .await
5166 .unwrap();
5167 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5168
5169 // Cause the language server to start.
5170 let open_buffer_task =
5171 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
5172 let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5173
5174 let fake_language_server = fake_language_servers.next().await.unwrap();
5175 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
5176 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5177 #[allow(deprecated)]
5178 lsp::SymbolInformation {
5179 name: "TWO".into(),
5180 location: lsp::Location {
5181 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
5182 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5183 },
5184 kind: lsp::SymbolKind::CONSTANT,
5185 tags: None,
5186 container_name: None,
5187 deprecated: None,
5188 },
5189 ])))
5190 });
5191
5192 // Request the definition of a symbol as the guest.
5193 let symbols = project_b
5194 .update(cx_b, |p, cx| p.symbols("two", cx))
5195 .await
5196 .unwrap();
5197 assert_eq!(symbols.len(), 1);
5198 assert_eq!(symbols[0].name, "TWO");
5199
5200 // Open one of the returned symbols.
5201 let buffer_b_2 = project_b
5202 .update(cx_b, |project, cx| {
5203 project.open_buffer_for_symbol(&symbols[0], cx)
5204 })
5205 .await
5206 .unwrap();
5207
5208 buffer_b_2.read_with(cx_b, |buffer, cx| {
5209 assert_eq!(
5210 buffer.file().unwrap().full_path(cx),
5211 Path::new("/code/crate-2/two.rs")
5212 );
5213 });
5214
5215 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5216 let mut fake_symbol = symbols[0].clone();
5217 fake_symbol.path.path = Path::new("/code/secrets").into();
5218 let error = project_b
5219 .update(cx_b, |project, cx| {
5220 project.open_buffer_for_symbol(&fake_symbol, cx)
5221 })
5222 .await
5223 .unwrap_err();
5224 assert!(error.to_string().contains("invalid symbol signature"));
5225}
5226
5227#[gpui::test(iterations = 10)]
5228async fn test_open_buffer_while_getting_definition_pointing_to_it(
5229 executor: BackgroundExecutor,
5230 cx_a: &mut TestAppContext,
5231 cx_b: &mut TestAppContext,
5232 mut rng: StdRng,
5233) {
5234 let mut server = TestServer::start(executor.clone()).await;
5235 let client_a = server.create_client(cx_a, "user_a").await;
5236 let client_b = server.create_client(cx_b, "user_b").await;
5237 server
5238 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5239 .await;
5240 let active_call_a = cx_a.read(ActiveCall::global);
5241
5242 client_a.language_registry().add(rust_lang());
5243 let mut fake_language_servers = client_a
5244 .language_registry()
5245 .register_fake_lsp_adapter("Rust", Default::default());
5246
5247 client_a
5248 .fs()
5249 .insert_tree(
5250 "/root",
5251 json!({
5252 "a.rs": "const ONE: usize = b::TWO;",
5253 "b.rs": "const TWO: usize = 2",
5254 }),
5255 )
5256 .await;
5257 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5258 let project_id = active_call_a
5259 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5260 .await
5261 .unwrap();
5262 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5263
5264 let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
5265 let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5266
5267 let fake_language_server = fake_language_servers.next().await.unwrap();
5268 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5269 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5270 lsp::Location::new(
5271 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5272 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5273 ),
5274 )))
5275 });
5276
5277 let definitions;
5278 let buffer_b2;
5279 if rng.gen() {
5280 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5281 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5282 } else {
5283 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5284 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5285 }
5286
5287 let buffer_b2 = buffer_b2.await.unwrap();
5288 let definitions = definitions.await.unwrap();
5289 assert_eq!(definitions.len(), 1);
5290 assert_eq!(definitions[0].target.buffer, buffer_b2);
5291}
5292
5293#[gpui::test(iterations = 10)]
5294async fn test_contacts(
5295 executor: BackgroundExecutor,
5296 cx_a: &mut TestAppContext,
5297 cx_b: &mut TestAppContext,
5298 cx_c: &mut TestAppContext,
5299 cx_d: &mut TestAppContext,
5300) {
5301 let mut server = TestServer::start(executor.clone()).await;
5302 let client_a = server.create_client(cx_a, "user_a").await;
5303 let client_b = server.create_client(cx_b, "user_b").await;
5304 let client_c = server.create_client(cx_c, "user_c").await;
5305 let client_d = server.create_client(cx_d, "user_d").await;
5306 server
5307 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5308 .await;
5309 let active_call_a = cx_a.read(ActiveCall::global);
5310 let active_call_b = cx_b.read(ActiveCall::global);
5311 let active_call_c = cx_c.read(ActiveCall::global);
5312 let _active_call_d = cx_d.read(ActiveCall::global);
5313
5314 executor.run_until_parked();
5315 assert_eq!(
5316 contacts(&client_a, cx_a),
5317 [
5318 ("user_b".to_string(), "online", "free"),
5319 ("user_c".to_string(), "online", "free")
5320 ]
5321 );
5322 assert_eq!(
5323 contacts(&client_b, cx_b),
5324 [
5325 ("user_a".to_string(), "online", "free"),
5326 ("user_c".to_string(), "online", "free")
5327 ]
5328 );
5329 assert_eq!(
5330 contacts(&client_c, cx_c),
5331 [
5332 ("user_a".to_string(), "online", "free"),
5333 ("user_b".to_string(), "online", "free")
5334 ]
5335 );
5336 assert_eq!(contacts(&client_d, cx_d), []);
5337
5338 server.disconnect_client(client_c.peer_id().unwrap());
5339 server.forbid_connections();
5340 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5341 assert_eq!(
5342 contacts(&client_a, cx_a),
5343 [
5344 ("user_b".to_string(), "online", "free"),
5345 ("user_c".to_string(), "offline", "free")
5346 ]
5347 );
5348 assert_eq!(
5349 contacts(&client_b, cx_b),
5350 [
5351 ("user_a".to_string(), "online", "free"),
5352 ("user_c".to_string(), "offline", "free")
5353 ]
5354 );
5355 assert_eq!(contacts(&client_c, cx_c), []);
5356 assert_eq!(contacts(&client_d, cx_d), []);
5357
5358 server.allow_connections();
5359 client_c
5360 .authenticate_and_connect(false, &cx_c.to_async())
5361 .await
5362 .unwrap();
5363
5364 executor.run_until_parked();
5365 assert_eq!(
5366 contacts(&client_a, cx_a),
5367 [
5368 ("user_b".to_string(), "online", "free"),
5369 ("user_c".to_string(), "online", "free")
5370 ]
5371 );
5372 assert_eq!(
5373 contacts(&client_b, cx_b),
5374 [
5375 ("user_a".to_string(), "online", "free"),
5376 ("user_c".to_string(), "online", "free")
5377 ]
5378 );
5379 assert_eq!(
5380 contacts(&client_c, cx_c),
5381 [
5382 ("user_a".to_string(), "online", "free"),
5383 ("user_b".to_string(), "online", "free")
5384 ]
5385 );
5386 assert_eq!(contacts(&client_d, cx_d), []);
5387
5388 active_call_a
5389 .update(cx_a, |call, cx| {
5390 call.invite(client_b.user_id().unwrap(), None, cx)
5391 })
5392 .await
5393 .unwrap();
5394 executor.run_until_parked();
5395 assert_eq!(
5396 contacts(&client_a, cx_a),
5397 [
5398 ("user_b".to_string(), "online", "busy"),
5399 ("user_c".to_string(), "online", "free")
5400 ]
5401 );
5402 assert_eq!(
5403 contacts(&client_b, cx_b),
5404 [
5405 ("user_a".to_string(), "online", "busy"),
5406 ("user_c".to_string(), "online", "free")
5407 ]
5408 );
5409 assert_eq!(
5410 contacts(&client_c, cx_c),
5411 [
5412 ("user_a".to_string(), "online", "busy"),
5413 ("user_b".to_string(), "online", "busy")
5414 ]
5415 );
5416 assert_eq!(contacts(&client_d, cx_d), []);
5417
5418 // Client B and client D become contacts while client B is being called.
5419 server
5420 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5421 .await;
5422 executor.run_until_parked();
5423 assert_eq!(
5424 contacts(&client_a, cx_a),
5425 [
5426 ("user_b".to_string(), "online", "busy"),
5427 ("user_c".to_string(), "online", "free")
5428 ]
5429 );
5430 assert_eq!(
5431 contacts(&client_b, cx_b),
5432 [
5433 ("user_a".to_string(), "online", "busy"),
5434 ("user_c".to_string(), "online", "free"),
5435 ("user_d".to_string(), "online", "free"),
5436 ]
5437 );
5438 assert_eq!(
5439 contacts(&client_c, cx_c),
5440 [
5441 ("user_a".to_string(), "online", "busy"),
5442 ("user_b".to_string(), "online", "busy")
5443 ]
5444 );
5445 assert_eq!(
5446 contacts(&client_d, cx_d),
5447 [("user_b".to_string(), "online", "busy")]
5448 );
5449
5450 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5451 executor.run_until_parked();
5452 assert_eq!(
5453 contacts(&client_a, cx_a),
5454 [
5455 ("user_b".to_string(), "online", "free"),
5456 ("user_c".to_string(), "online", "free")
5457 ]
5458 );
5459 assert_eq!(
5460 contacts(&client_b, cx_b),
5461 [
5462 ("user_a".to_string(), "online", "free"),
5463 ("user_c".to_string(), "online", "free"),
5464 ("user_d".to_string(), "online", "free")
5465 ]
5466 );
5467 assert_eq!(
5468 contacts(&client_c, cx_c),
5469 [
5470 ("user_a".to_string(), "online", "free"),
5471 ("user_b".to_string(), "online", "free")
5472 ]
5473 );
5474 assert_eq!(
5475 contacts(&client_d, cx_d),
5476 [("user_b".to_string(), "online", "free")]
5477 );
5478
5479 active_call_c
5480 .update(cx_c, |call, cx| {
5481 call.invite(client_a.user_id().unwrap(), None, cx)
5482 })
5483 .await
5484 .unwrap();
5485 executor.run_until_parked();
5486 assert_eq!(
5487 contacts(&client_a, cx_a),
5488 [
5489 ("user_b".to_string(), "online", "free"),
5490 ("user_c".to_string(), "online", "busy")
5491 ]
5492 );
5493 assert_eq!(
5494 contacts(&client_b, cx_b),
5495 [
5496 ("user_a".to_string(), "online", "busy"),
5497 ("user_c".to_string(), "online", "busy"),
5498 ("user_d".to_string(), "online", "free")
5499 ]
5500 );
5501 assert_eq!(
5502 contacts(&client_c, cx_c),
5503 [
5504 ("user_a".to_string(), "online", "busy"),
5505 ("user_b".to_string(), "online", "free")
5506 ]
5507 );
5508 assert_eq!(
5509 contacts(&client_d, cx_d),
5510 [("user_b".to_string(), "online", "free")]
5511 );
5512
5513 active_call_a
5514 .update(cx_a, |call, cx| call.accept_incoming(cx))
5515 .await
5516 .unwrap();
5517 executor.run_until_parked();
5518 assert_eq!(
5519 contacts(&client_a, cx_a),
5520 [
5521 ("user_b".to_string(), "online", "free"),
5522 ("user_c".to_string(), "online", "busy")
5523 ]
5524 );
5525 assert_eq!(
5526 contacts(&client_b, cx_b),
5527 [
5528 ("user_a".to_string(), "online", "busy"),
5529 ("user_c".to_string(), "online", "busy"),
5530 ("user_d".to_string(), "online", "free")
5531 ]
5532 );
5533 assert_eq!(
5534 contacts(&client_c, cx_c),
5535 [
5536 ("user_a".to_string(), "online", "busy"),
5537 ("user_b".to_string(), "online", "free")
5538 ]
5539 );
5540 assert_eq!(
5541 contacts(&client_d, cx_d),
5542 [("user_b".to_string(), "online", "free")]
5543 );
5544
5545 active_call_a
5546 .update(cx_a, |call, cx| {
5547 call.invite(client_b.user_id().unwrap(), None, cx)
5548 })
5549 .await
5550 .unwrap();
5551 executor.run_until_parked();
5552 assert_eq!(
5553 contacts(&client_a, cx_a),
5554 [
5555 ("user_b".to_string(), "online", "busy"),
5556 ("user_c".to_string(), "online", "busy")
5557 ]
5558 );
5559 assert_eq!(
5560 contacts(&client_b, cx_b),
5561 [
5562 ("user_a".to_string(), "online", "busy"),
5563 ("user_c".to_string(), "online", "busy"),
5564 ("user_d".to_string(), "online", "free")
5565 ]
5566 );
5567 assert_eq!(
5568 contacts(&client_c, cx_c),
5569 [
5570 ("user_a".to_string(), "online", "busy"),
5571 ("user_b".to_string(), "online", "busy")
5572 ]
5573 );
5574 assert_eq!(
5575 contacts(&client_d, cx_d),
5576 [("user_b".to_string(), "online", "busy")]
5577 );
5578
5579 active_call_a
5580 .update(cx_a, |call, cx| call.hang_up(cx))
5581 .await
5582 .unwrap();
5583 executor.run_until_parked();
5584 assert_eq!(
5585 contacts(&client_a, cx_a),
5586 [
5587 ("user_b".to_string(), "online", "free"),
5588 ("user_c".to_string(), "online", "free")
5589 ]
5590 );
5591 assert_eq!(
5592 contacts(&client_b, cx_b),
5593 [
5594 ("user_a".to_string(), "online", "free"),
5595 ("user_c".to_string(), "online", "free"),
5596 ("user_d".to_string(), "online", "free")
5597 ]
5598 );
5599 assert_eq!(
5600 contacts(&client_c, cx_c),
5601 [
5602 ("user_a".to_string(), "online", "free"),
5603 ("user_b".to_string(), "online", "free")
5604 ]
5605 );
5606 assert_eq!(
5607 contacts(&client_d, cx_d),
5608 [("user_b".to_string(), "online", "free")]
5609 );
5610
5611 active_call_a
5612 .update(cx_a, |call, cx| {
5613 call.invite(client_b.user_id().unwrap(), None, cx)
5614 })
5615 .await
5616 .unwrap();
5617 executor.run_until_parked();
5618 assert_eq!(
5619 contacts(&client_a, cx_a),
5620 [
5621 ("user_b".to_string(), "online", "busy"),
5622 ("user_c".to_string(), "online", "free")
5623 ]
5624 );
5625 assert_eq!(
5626 contacts(&client_b, cx_b),
5627 [
5628 ("user_a".to_string(), "online", "busy"),
5629 ("user_c".to_string(), "online", "free"),
5630 ("user_d".to_string(), "online", "free")
5631 ]
5632 );
5633 assert_eq!(
5634 contacts(&client_c, cx_c),
5635 [
5636 ("user_a".to_string(), "online", "busy"),
5637 ("user_b".to_string(), "online", "busy")
5638 ]
5639 );
5640 assert_eq!(
5641 contacts(&client_d, cx_d),
5642 [("user_b".to_string(), "online", "busy")]
5643 );
5644
5645 server.forbid_connections();
5646 server.disconnect_client(client_a.peer_id().unwrap());
5647 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5648 assert_eq!(contacts(&client_a, cx_a), []);
5649 assert_eq!(
5650 contacts(&client_b, cx_b),
5651 [
5652 ("user_a".to_string(), "offline", "free"),
5653 ("user_c".to_string(), "online", "free"),
5654 ("user_d".to_string(), "online", "free")
5655 ]
5656 );
5657 assert_eq!(
5658 contacts(&client_c, cx_c),
5659 [
5660 ("user_a".to_string(), "offline", "free"),
5661 ("user_b".to_string(), "online", "free")
5662 ]
5663 );
5664 assert_eq!(
5665 contacts(&client_d, cx_d),
5666 [("user_b".to_string(), "online", "free")]
5667 );
5668
5669 // Test removing a contact
5670 client_b
5671 .user_store()
5672 .update(cx_b, |store, cx| {
5673 store.remove_contact(client_c.user_id().unwrap(), cx)
5674 })
5675 .await
5676 .unwrap();
5677 executor.run_until_parked();
5678 assert_eq!(
5679 contacts(&client_b, cx_b),
5680 [
5681 ("user_a".to_string(), "offline", "free"),
5682 ("user_d".to_string(), "online", "free")
5683 ]
5684 );
5685 assert_eq!(
5686 contacts(&client_c, cx_c),
5687 [("user_a".to_string(), "offline", "free"),]
5688 );
5689
5690 fn contacts(
5691 client: &TestClient,
5692 cx: &TestAppContext,
5693 ) -> Vec<(String, &'static str, &'static str)> {
5694 client.user_store().read_with(cx, |store, _| {
5695 store
5696 .contacts()
5697 .iter()
5698 .map(|contact| {
5699 (
5700 contact.user.github_login.clone(),
5701 if contact.online { "online" } else { "offline" },
5702 if contact.busy { "busy" } else { "free" },
5703 )
5704 })
5705 .collect()
5706 })
5707 }
5708}
5709
5710#[gpui::test(iterations = 10)]
5711async fn test_contact_requests(
5712 executor: BackgroundExecutor,
5713 cx_a: &mut TestAppContext,
5714 cx_a2: &mut TestAppContext,
5715 cx_b: &mut TestAppContext,
5716 cx_b2: &mut TestAppContext,
5717 cx_c: &mut TestAppContext,
5718 cx_c2: &mut TestAppContext,
5719) {
5720 // Connect to a server as 3 clients.
5721 let mut server = TestServer::start(executor.clone()).await;
5722 let client_a = server.create_client(cx_a, "user_a").await;
5723 let client_a2 = server.create_client(cx_a2, "user_a").await;
5724 let client_b = server.create_client(cx_b, "user_b").await;
5725 let client_b2 = server.create_client(cx_b2, "user_b").await;
5726 let client_c = server.create_client(cx_c, "user_c").await;
5727 let client_c2 = server.create_client(cx_c2, "user_c").await;
5728
5729 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5730 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5731 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5732
5733 // User A and User C request that user B become their contact.
5734 client_a
5735 .user_store()
5736 .update(cx_a, |store, cx| {
5737 store.request_contact(client_b.user_id().unwrap(), cx)
5738 })
5739 .await
5740 .unwrap();
5741 client_c
5742 .user_store()
5743 .update(cx_c, |store, cx| {
5744 store.request_contact(client_b.user_id().unwrap(), cx)
5745 })
5746 .await
5747 .unwrap();
5748 executor.run_until_parked();
5749
5750 // All users see the pending request appear in all their clients.
5751 assert_eq!(
5752 client_a.summarize_contacts(cx_a).outgoing_requests,
5753 &["user_b"]
5754 );
5755 assert_eq!(
5756 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5757 &["user_b"]
5758 );
5759 assert_eq!(
5760 client_b.summarize_contacts(cx_b).incoming_requests,
5761 &["user_a", "user_c"]
5762 );
5763 assert_eq!(
5764 client_b2.summarize_contacts(cx_b2).incoming_requests,
5765 &["user_a", "user_c"]
5766 );
5767 assert_eq!(
5768 client_c.summarize_contacts(cx_c).outgoing_requests,
5769 &["user_b"]
5770 );
5771 assert_eq!(
5772 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5773 &["user_b"]
5774 );
5775
5776 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5777 disconnect_and_reconnect(&client_a, cx_a).await;
5778 disconnect_and_reconnect(&client_b, cx_b).await;
5779 disconnect_and_reconnect(&client_c, cx_c).await;
5780 executor.run_until_parked();
5781 assert_eq!(
5782 client_a.summarize_contacts(cx_a).outgoing_requests,
5783 &["user_b"]
5784 );
5785 assert_eq!(
5786 client_b.summarize_contacts(cx_b).incoming_requests,
5787 &["user_a", "user_c"]
5788 );
5789 assert_eq!(
5790 client_c.summarize_contacts(cx_c).outgoing_requests,
5791 &["user_b"]
5792 );
5793
5794 // User B accepts the request from user A.
5795 client_b
5796 .user_store()
5797 .update(cx_b, |store, cx| {
5798 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5799 })
5800 .await
5801 .unwrap();
5802
5803 executor.run_until_parked();
5804
5805 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5806 let contacts_b = client_b.summarize_contacts(cx_b);
5807 assert_eq!(contacts_b.current, &["user_a"]);
5808 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5809 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5810 assert_eq!(contacts_b2.current, &["user_a"]);
5811 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5812
5813 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5814 let contacts_a = client_a.summarize_contacts(cx_a);
5815 assert_eq!(contacts_a.current, &["user_b"]);
5816 assert!(contacts_a.outgoing_requests.is_empty());
5817 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5818 assert_eq!(contacts_a2.current, &["user_b"]);
5819 assert!(contacts_a2.outgoing_requests.is_empty());
5820
5821 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5822 disconnect_and_reconnect(&client_a, cx_a).await;
5823 disconnect_and_reconnect(&client_b, cx_b).await;
5824 disconnect_and_reconnect(&client_c, cx_c).await;
5825 executor.run_until_parked();
5826 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5827 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5828 assert_eq!(
5829 client_b.summarize_contacts(cx_b).incoming_requests,
5830 &["user_c"]
5831 );
5832 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5833 assert_eq!(
5834 client_c.summarize_contacts(cx_c).outgoing_requests,
5835 &["user_b"]
5836 );
5837
5838 // User B rejects the request from user C.
5839 client_b
5840 .user_store()
5841 .update(cx_b, |store, cx| {
5842 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5843 })
5844 .await
5845 .unwrap();
5846
5847 executor.run_until_parked();
5848
5849 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5850 let contacts_b = client_b.summarize_contacts(cx_b);
5851 assert_eq!(contacts_b.current, &["user_a"]);
5852 assert!(contacts_b.incoming_requests.is_empty());
5853 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5854 assert_eq!(contacts_b2.current, &["user_a"]);
5855 assert!(contacts_b2.incoming_requests.is_empty());
5856
5857 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5858 let contacts_c = client_c.summarize_contacts(cx_c);
5859 assert!(contacts_c.current.is_empty());
5860 assert!(contacts_c.outgoing_requests.is_empty());
5861 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5862 assert!(contacts_c2.current.is_empty());
5863 assert!(contacts_c2.outgoing_requests.is_empty());
5864
5865 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5866 disconnect_and_reconnect(&client_a, cx_a).await;
5867 disconnect_and_reconnect(&client_b, cx_b).await;
5868 disconnect_and_reconnect(&client_c, cx_c).await;
5869 executor.run_until_parked();
5870 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5871 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5872 assert!(client_b
5873 .summarize_contacts(cx_b)
5874 .incoming_requests
5875 .is_empty());
5876 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5877 assert!(client_c
5878 .summarize_contacts(cx_c)
5879 .outgoing_requests
5880 .is_empty());
5881
5882 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
5883 client.disconnect(&cx.to_async());
5884 client.clear_contacts(cx).await;
5885 client
5886 .authenticate_and_connect(false, &cx.to_async())
5887 .await
5888 .unwrap();
5889 }
5890}
5891
5892#[gpui::test(iterations = 10)]
5893async fn test_join_call_after_screen_was_shared(
5894 executor: BackgroundExecutor,
5895 cx_a: &mut TestAppContext,
5896 cx_b: &mut TestAppContext,
5897) {
5898 let mut server = TestServer::start(executor.clone()).await;
5899
5900 let client_a = server.create_client(cx_a, "user_a").await;
5901 let client_b = server.create_client(cx_b, "user_b").await;
5902 server
5903 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5904 .await;
5905
5906 let active_call_a = cx_a.read(ActiveCall::global);
5907 let active_call_b = cx_b.read(ActiveCall::global);
5908
5909 // Call users B and C from client A.
5910 active_call_a
5911 .update(cx_a, |call, cx| {
5912 call.invite(client_b.user_id().unwrap(), None, cx)
5913 })
5914 .await
5915 .unwrap();
5916
5917 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
5918 executor.run_until_parked();
5919 assert_eq!(
5920 room_participants(&room_a, cx_a),
5921 RoomParticipants {
5922 remote: Default::default(),
5923 pending: vec!["user_b".to_string()]
5924 }
5925 );
5926
5927 // User B receives the call.
5928
5929 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
5930 let call_b = incoming_call_b.next().await.unwrap().unwrap();
5931 assert_eq!(call_b.calling_user.github_login, "user_a");
5932
5933 // User A shares their screen
5934 let display = MacOSDisplay::new();
5935 active_call_a
5936 .update(cx_a, |call, cx| {
5937 call.room().unwrap().update(cx, |room, cx| {
5938 room.set_display_sources(vec![display.clone()]);
5939 room.share_screen(cx)
5940 })
5941 })
5942 .await
5943 .unwrap();
5944
5945 client_b.user_store().update(cx_b, |user_store, _| {
5946 user_store.clear_cache();
5947 });
5948
5949 // User B joins the room
5950 active_call_b
5951 .update(cx_b, |call, cx| call.accept_incoming(cx))
5952 .await
5953 .unwrap();
5954
5955 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
5956 assert!(incoming_call_b.next().await.unwrap().is_none());
5957
5958 executor.run_until_parked();
5959 assert_eq!(
5960 room_participants(&room_a, cx_a),
5961 RoomParticipants {
5962 remote: vec!["user_b".to_string()],
5963 pending: vec![],
5964 }
5965 );
5966 assert_eq!(
5967 room_participants(&room_b, cx_b),
5968 RoomParticipants {
5969 remote: vec!["user_a".to_string()],
5970 pending: vec![],
5971 }
5972 );
5973
5974 // Ensure User B sees User A's screenshare.
5975
5976 room_b.read_with(cx_b, |room, _| {
5977 assert_eq!(
5978 room.remote_participants()
5979 .get(&client_a.user_id().unwrap())
5980 .unwrap()
5981 .video_tracks
5982 .len(),
5983 1
5984 );
5985 });
5986}
5987
5988#[gpui::test]
5989async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
5990 let mut server = TestServer::start(cx.executor().clone()).await;
5991 let client_a = server.create_client(cx, "user_a").await;
5992 let (_workspace_a, cx) = client_a.build_test_workspace(cx).await;
5993
5994 cx.simulate_resize(size(px(300.), px(300.)));
5995
5996 cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
5997 cx.update(|cx| cx.refresh());
5998
5999 let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
6000 let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
6001
6002 assert!(
6003 tab_bounds.intersects(&new_tab_button_bounds),
6004 "Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
6005 );
6006
6007 cx.simulate_event(MouseDownEvent {
6008 button: MouseButton::Right,
6009 position: new_tab_button_bounds.center(),
6010 modifiers: Modifiers::default(),
6011 click_count: 1,
6012 first_mouse: false,
6013 });
6014
6015 // regression test that the right click menu for tabs does not open.
6016 assert!(cx.debug_bounds("MENU_ITEM-Close").is_none());
6017
6018 let tab_bounds = cx.debug_bounds("TAB-1").unwrap();
6019 cx.simulate_event(MouseDownEvent {
6020 button: MouseButton::Right,
6021 position: tab_bounds.center(),
6022 modifiers: Modifiers::default(),
6023 click_count: 1,
6024 first_mouse: false,
6025 });
6026 assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
6027}
6028
6029#[gpui::test]
6030async fn test_cmd_k_left(cx: &mut TestAppContext) {
6031 let (_, client) = TestServer::start1(cx).await;
6032 let (workspace, cx) = client.build_test_workspace(cx).await;
6033
6034 cx.simulate_keystrokes("cmd-n");
6035 workspace.update(cx, |workspace, cx| {
6036 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
6037 });
6038 cx.simulate_keystrokes("cmd-k left");
6039 workspace.update(cx, |workspace, cx| {
6040 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6041 });
6042 cx.simulate_keystrokes("cmd-k");
6043 // sleep for longer than the timeout in keyboard shortcut handling
6044 // to verify that it doesn't fire in this case.
6045 cx.executor().advance_clock(Duration::from_secs(2));
6046 cx.simulate_keystrokes("left");
6047 workspace.update(cx, |workspace, cx| {
6048 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6049 });
6050}
6051
6052#[gpui::test]
6053async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppContext) {
6054 let (mut server, client) = TestServer::start1(cx1).await;
6055 let channel1 = server.make_public_channel("channel1", &client, cx1).await;
6056 let channel2 = server.make_public_channel("channel2", &client, cx1).await;
6057
6058 join_channel(channel1, &client, cx1).await.unwrap();
6059 drop(client);
6060
6061 let client2 = server.create_client(cx2, "user_a").await;
6062 join_channel(channel2, &client2, cx2).await.unwrap();
6063}