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
1871fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
1872 let events = Rc::new(RefCell::new(Vec::new()));
1873 let active_call = cx.read(ActiveCall::global);
1874 cx.update({
1875 let events = events.clone();
1876 |cx| {
1877 cx.subscribe(&active_call, move |_, event, _| {
1878 events.borrow_mut().push(event.clone())
1879 })
1880 .detach()
1881 }
1882 });
1883 events
1884}
1885
1886#[gpui::test]
1887async fn test_mute_deafen(
1888 executor: BackgroundExecutor,
1889 cx_a: &mut TestAppContext,
1890 cx_b: &mut TestAppContext,
1891 cx_c: &mut TestAppContext,
1892) {
1893 let mut server = TestServer::start(executor.clone()).await;
1894 let client_a = server.create_client(cx_a, "user_a").await;
1895 let client_b = server.create_client(cx_b, "user_b").await;
1896 let client_c = server.create_client(cx_c, "user_c").await;
1897
1898 server
1899 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1900 .await;
1901
1902 let active_call_a = cx_a.read(ActiveCall::global);
1903 let active_call_b = cx_b.read(ActiveCall::global);
1904 let active_call_c = cx_c.read(ActiveCall::global);
1905
1906 // User A calls user B, B answers.
1907 active_call_a
1908 .update(cx_a, |call, cx| {
1909 call.invite(client_b.user_id().unwrap(), None, cx)
1910 })
1911 .await
1912 .unwrap();
1913 executor.run_until_parked();
1914 active_call_b
1915 .update(cx_b, |call, cx| call.accept_incoming(cx))
1916 .await
1917 .unwrap();
1918 executor.run_until_parked();
1919
1920 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
1921 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
1922
1923 room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
1924 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1925
1926 // Users A and B are both muted.
1927 assert_eq!(
1928 participant_audio_state(&room_a, cx_a),
1929 &[ParticipantAudioState {
1930 user_id: client_b.user_id().unwrap(),
1931 is_muted: false,
1932 audio_tracks_playing: vec![true],
1933 }]
1934 );
1935 assert_eq!(
1936 participant_audio_state(&room_b, cx_b),
1937 &[ParticipantAudioState {
1938 user_id: client_a.user_id().unwrap(),
1939 is_muted: false,
1940 audio_tracks_playing: vec![true],
1941 }]
1942 );
1943
1944 // User A mutes
1945 room_a.update(cx_a, |room, cx| room.toggle_mute(cx));
1946 executor.run_until_parked();
1947
1948 // User A hears user B, but B doesn't hear A.
1949 room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
1950 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1951 assert_eq!(
1952 participant_audio_state(&room_a, cx_a),
1953 &[ParticipantAudioState {
1954 user_id: client_b.user_id().unwrap(),
1955 is_muted: false,
1956 audio_tracks_playing: vec![true],
1957 }]
1958 );
1959 assert_eq!(
1960 participant_audio_state(&room_b, cx_b),
1961 &[ParticipantAudioState {
1962 user_id: client_a.user_id().unwrap(),
1963 is_muted: true,
1964 audio_tracks_playing: vec![true],
1965 }]
1966 );
1967
1968 // User A deafens
1969 room_a.update(cx_a, |room, cx| room.toggle_deafen(cx));
1970 executor.run_until_parked();
1971
1972 // User A does not hear user B.
1973 room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
1974 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1975 assert_eq!(
1976 participant_audio_state(&room_a, cx_a),
1977 &[ParticipantAudioState {
1978 user_id: client_b.user_id().unwrap(),
1979 is_muted: false,
1980 audio_tracks_playing: vec![false],
1981 }]
1982 );
1983 assert_eq!(
1984 participant_audio_state(&room_b, cx_b),
1985 &[ParticipantAudioState {
1986 user_id: client_a.user_id().unwrap(),
1987 is_muted: true,
1988 audio_tracks_playing: vec![true],
1989 }]
1990 );
1991
1992 // User B calls user C, C joins.
1993 active_call_b
1994 .update(cx_b, |call, cx| {
1995 call.invite(client_c.user_id().unwrap(), None, cx)
1996 })
1997 .await
1998 .unwrap();
1999 executor.run_until_parked();
2000 active_call_c
2001 .update(cx_c, |call, cx| call.accept_incoming(cx))
2002 .await
2003 .unwrap();
2004 executor.run_until_parked();
2005
2006 // User A does not hear users B or C.
2007 assert_eq!(
2008 participant_audio_state(&room_a, cx_a),
2009 &[
2010 ParticipantAudioState {
2011 user_id: client_b.user_id().unwrap(),
2012 is_muted: false,
2013 audio_tracks_playing: vec![false],
2014 },
2015 ParticipantAudioState {
2016 user_id: client_c.user_id().unwrap(),
2017 is_muted: false,
2018 audio_tracks_playing: vec![false],
2019 }
2020 ]
2021 );
2022 assert_eq!(
2023 participant_audio_state(&room_b, cx_b),
2024 &[
2025 ParticipantAudioState {
2026 user_id: client_a.user_id().unwrap(),
2027 is_muted: true,
2028 audio_tracks_playing: vec![true],
2029 },
2030 ParticipantAudioState {
2031 user_id: client_c.user_id().unwrap(),
2032 is_muted: false,
2033 audio_tracks_playing: vec![true],
2034 }
2035 ]
2036 );
2037
2038 #[derive(PartialEq, Eq, Debug)]
2039 struct ParticipantAudioState {
2040 user_id: u64,
2041 is_muted: bool,
2042 audio_tracks_playing: Vec<bool>,
2043 }
2044
2045 fn participant_audio_state(
2046 room: &Model<Room>,
2047 cx: &TestAppContext,
2048 ) -> Vec<ParticipantAudioState> {
2049 room.read_with(cx, |room, _| {
2050 room.remote_participants()
2051 .iter()
2052 .map(|(user_id, participant)| ParticipantAudioState {
2053 user_id: *user_id,
2054 is_muted: participant.muted,
2055 audio_tracks_playing: participant
2056 .audio_tracks
2057 .values()
2058 .map(|track| track.is_playing())
2059 .collect(),
2060 })
2061 .collect::<Vec<_>>()
2062 })
2063 }
2064}
2065
2066#[gpui::test(iterations = 10)]
2067async fn test_room_location(
2068 executor: BackgroundExecutor,
2069 cx_a: &mut TestAppContext,
2070 cx_b: &mut TestAppContext,
2071) {
2072 let mut server = TestServer::start(executor.clone()).await;
2073 let client_a = server.create_client(cx_a, "user_a").await;
2074 let client_b = server.create_client(cx_b, "user_b").await;
2075 client_a.fs().insert_tree("/a", json!({})).await;
2076 client_b.fs().insert_tree("/b", json!({})).await;
2077
2078 let active_call_a = cx_a.read(ActiveCall::global);
2079 let active_call_b = cx_b.read(ActiveCall::global);
2080
2081 let a_notified = Rc::new(Cell::new(false));
2082 cx_a.update({
2083 let notified = a_notified.clone();
2084 |cx| {
2085 cx.observe(&active_call_a, move |_, _| notified.set(true))
2086 .detach()
2087 }
2088 });
2089
2090 let b_notified = Rc::new(Cell::new(false));
2091 cx_b.update({
2092 let b_notified = b_notified.clone();
2093 |cx| {
2094 cx.observe(&active_call_b, move |_, _| b_notified.set(true))
2095 .detach()
2096 }
2097 });
2098
2099 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
2100 active_call_a
2101 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2102 .await
2103 .unwrap();
2104 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
2105
2106 server
2107 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2108 .await;
2109
2110 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
2111
2112 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
2113 executor.run_until_parked();
2114 assert!(a_notified.take());
2115 assert_eq!(
2116 participant_locations(&room_a, cx_a),
2117 vec![("user_b".to_string(), ParticipantLocation::External)]
2118 );
2119 assert!(b_notified.take());
2120 assert_eq!(
2121 participant_locations(&room_b, cx_b),
2122 vec![("user_a".to_string(), ParticipantLocation::UnsharedProject)]
2123 );
2124
2125 let project_a_id = active_call_a
2126 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2127 .await
2128 .unwrap();
2129 executor.run_until_parked();
2130 assert!(a_notified.take());
2131 assert_eq!(
2132 participant_locations(&room_a, cx_a),
2133 vec![("user_b".to_string(), ParticipantLocation::External)]
2134 );
2135 assert!(b_notified.take());
2136 assert_eq!(
2137 participant_locations(&room_b, cx_b),
2138 vec![(
2139 "user_a".to_string(),
2140 ParticipantLocation::SharedProject {
2141 project_id: project_a_id
2142 }
2143 )]
2144 );
2145
2146 let project_b_id = active_call_b
2147 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
2148 .await
2149 .unwrap();
2150 executor.run_until_parked();
2151 assert!(a_notified.take());
2152 assert_eq!(
2153 participant_locations(&room_a, cx_a),
2154 vec![("user_b".to_string(), ParticipantLocation::External)]
2155 );
2156 assert!(b_notified.take());
2157 assert_eq!(
2158 participant_locations(&room_b, cx_b),
2159 vec![(
2160 "user_a".to_string(),
2161 ParticipantLocation::SharedProject {
2162 project_id: project_a_id
2163 }
2164 )]
2165 );
2166
2167 active_call_b
2168 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2169 .await
2170 .unwrap();
2171 executor.run_until_parked();
2172 assert!(a_notified.take());
2173 assert_eq!(
2174 participant_locations(&room_a, cx_a),
2175 vec![(
2176 "user_b".to_string(),
2177 ParticipantLocation::SharedProject {
2178 project_id: project_b_id
2179 }
2180 )]
2181 );
2182 assert!(b_notified.take());
2183 assert_eq!(
2184 participant_locations(&room_b, cx_b),
2185 vec![(
2186 "user_a".to_string(),
2187 ParticipantLocation::SharedProject {
2188 project_id: project_a_id
2189 }
2190 )]
2191 );
2192
2193 active_call_b
2194 .update(cx_b, |call, cx| call.set_location(None, cx))
2195 .await
2196 .unwrap();
2197 executor.run_until_parked();
2198 assert!(a_notified.take());
2199 assert_eq!(
2200 participant_locations(&room_a, cx_a),
2201 vec![("user_b".to_string(), ParticipantLocation::External)]
2202 );
2203 assert!(b_notified.take());
2204 assert_eq!(
2205 participant_locations(&room_b, cx_b),
2206 vec![(
2207 "user_a".to_string(),
2208 ParticipantLocation::SharedProject {
2209 project_id: project_a_id
2210 }
2211 )]
2212 );
2213
2214 fn participant_locations(
2215 room: &Model<Room>,
2216 cx: &TestAppContext,
2217 ) -> Vec<(String, ParticipantLocation)> {
2218 room.read_with(cx, |room, _| {
2219 room.remote_participants()
2220 .values()
2221 .map(|participant| {
2222 (
2223 participant.user.github_login.to_string(),
2224 participant.location,
2225 )
2226 })
2227 .collect()
2228 })
2229 }
2230}
2231
2232#[gpui::test(iterations = 10)]
2233async fn test_propagate_saves_and_fs_changes(
2234 executor: BackgroundExecutor,
2235 cx_a: &mut TestAppContext,
2236 cx_b: &mut TestAppContext,
2237 cx_c: &mut TestAppContext,
2238) {
2239 let mut server = TestServer::start(executor.clone()).await;
2240 let client_a = server.create_client(cx_a, "user_a").await;
2241 let client_b = server.create_client(cx_b, "user_b").await;
2242 let client_c = server.create_client(cx_c, "user_c").await;
2243
2244 server
2245 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2246 .await;
2247 let active_call_a = cx_a.read(ActiveCall::global);
2248
2249 let rust = Arc::new(Language::new(
2250 LanguageConfig {
2251 name: "Rust".into(),
2252 matcher: LanguageMatcher {
2253 path_suffixes: vec!["rs".to_string()],
2254 ..Default::default()
2255 },
2256 ..Default::default()
2257 },
2258 Some(tree_sitter_rust::language()),
2259 ));
2260 let javascript = Arc::new(Language::new(
2261 LanguageConfig {
2262 name: "JavaScript".into(),
2263 matcher: LanguageMatcher {
2264 path_suffixes: vec!["js".to_string()],
2265 ..Default::default()
2266 },
2267 ..Default::default()
2268 },
2269 Some(tree_sitter_rust::language()),
2270 ));
2271 for client in [&client_a, &client_b, &client_c] {
2272 client.language_registry().add(rust.clone());
2273 client.language_registry().add(javascript.clone());
2274 }
2275
2276 client_a
2277 .fs()
2278 .insert_tree(
2279 "/a",
2280 json!({
2281 "file1.rs": "",
2282 "file2": ""
2283 }),
2284 )
2285 .await;
2286 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2287
2288 let worktree_a = project_a.read_with(cx_a, |p, _| p.worktrees().next().unwrap());
2289 let project_id = active_call_a
2290 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2291 .await
2292 .unwrap();
2293
2294 // Join that worktree as clients B and C.
2295 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2296 let project_c = client_c.build_remote_project(project_id, cx_c).await;
2297
2298 let worktree_b = project_b.read_with(cx_b, |p, _| p.worktrees().next().unwrap());
2299
2300 let worktree_c = project_c.read_with(cx_c, |p, _| p.worktrees().next().unwrap());
2301
2302 // Open and edit a buffer as both guests B and C.
2303 let buffer_b = project_b
2304 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2305 .await
2306 .unwrap();
2307 let buffer_c = project_c
2308 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2309 .await
2310 .unwrap();
2311
2312 buffer_b.read_with(cx_b, |buffer, _| {
2313 assert_eq!(&*buffer.language().unwrap().name(), "Rust");
2314 });
2315
2316 buffer_c.read_with(cx_c, |buffer, _| {
2317 assert_eq!(&*buffer.language().unwrap().name(), "Rust");
2318 });
2319 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
2320 buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
2321
2322 // Open and edit that buffer as the host.
2323 let buffer_a = project_a
2324 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2325 .await
2326 .unwrap();
2327
2328 executor.run_until_parked();
2329
2330 buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, "));
2331 buffer_a.update(cx_a, |buf, cx| {
2332 buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
2333 });
2334
2335 executor.run_until_parked();
2336
2337 buffer_a.read_with(cx_a, |buf, _| {
2338 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2339 });
2340
2341 buffer_b.read_with(cx_b, |buf, _| {
2342 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2343 });
2344
2345 buffer_c.read_with(cx_c, |buf, _| {
2346 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2347 });
2348
2349 // Edit the buffer as the host and concurrently save as guest B.
2350 let save_b = project_b.update(cx_b, |project, cx| {
2351 project.save_buffer(buffer_b.clone(), cx)
2352 });
2353 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
2354 save_b.await.unwrap();
2355 assert_eq!(
2356 client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(),
2357 "hi-a, i-am-c, i-am-b, i-am-a"
2358 );
2359
2360 executor.run_until_parked();
2361
2362 buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
2363
2364 buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
2365
2366 buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty()));
2367
2368 // Make changes on host's file system, see those changes on guest worktrees.
2369 client_a
2370 .fs()
2371 .rename(
2372 "/a/file1.rs".as_ref(),
2373 "/a/file1.js".as_ref(),
2374 Default::default(),
2375 )
2376 .await
2377 .unwrap();
2378 client_a
2379 .fs()
2380 .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
2381 .await
2382 .unwrap();
2383 client_a.fs().insert_file("/a/file4", "4".into()).await;
2384 executor.run_until_parked();
2385
2386 worktree_a.read_with(cx_a, |tree, _| {
2387 assert_eq!(
2388 tree.paths()
2389 .map(|p| p.to_string_lossy())
2390 .collect::<Vec<_>>(),
2391 ["file1.js", "file3", "file4"]
2392 )
2393 });
2394
2395 worktree_b.read_with(cx_b, |tree, _| {
2396 assert_eq!(
2397 tree.paths()
2398 .map(|p| p.to_string_lossy())
2399 .collect::<Vec<_>>(),
2400 ["file1.js", "file3", "file4"]
2401 )
2402 });
2403
2404 worktree_c.read_with(cx_c, |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 // Ensure buffer files are updated as well.
2414
2415 buffer_a.read_with(cx_a, |buffer, _| {
2416 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2417 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2418 });
2419
2420 buffer_b.read_with(cx_b, |buffer, _| {
2421 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2422 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2423 });
2424
2425 buffer_c.read_with(cx_c, |buffer, _| {
2426 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2427 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2428 });
2429
2430 let new_buffer_a = project_a
2431 .update(cx_a, |p, cx| p.create_buffer("", None, cx))
2432 .unwrap();
2433
2434 let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
2435 let new_buffer_b = project_b
2436 .update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx))
2437 .await
2438 .unwrap();
2439
2440 new_buffer_b.read_with(cx_b, |buffer, _| {
2441 assert!(buffer.file().is_none());
2442 });
2443
2444 new_buffer_a.update(cx_a, |buffer, cx| {
2445 buffer.edit([(0..0, "ok")], None, cx);
2446 });
2447 project_a
2448 .update(cx_a, |project, cx| {
2449 project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
2450 })
2451 .await
2452 .unwrap();
2453
2454 executor.run_until_parked();
2455
2456 new_buffer_b.read_with(cx_b, |buffer_b, _| {
2457 assert_eq!(
2458 buffer_b.file().unwrap().path().as_ref(),
2459 Path::new("file3.rs")
2460 );
2461
2462 new_buffer_a.read_with(cx_a, |buffer_a, _| {
2463 assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime());
2464 assert_eq!(buffer_b.saved_version(), buffer_a.saved_version());
2465 });
2466 });
2467}
2468
2469#[gpui::test(iterations = 10)]
2470async fn test_git_diff_base_change(
2471 executor: BackgroundExecutor,
2472 cx_a: &mut TestAppContext,
2473 cx_b: &mut TestAppContext,
2474) {
2475 let mut server = TestServer::start(executor.clone()).await;
2476 let client_a = server.create_client(cx_a, "user_a").await;
2477 let client_b = server.create_client(cx_b, "user_b").await;
2478 server
2479 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2480 .await;
2481 let active_call_a = cx_a.read(ActiveCall::global);
2482
2483 client_a
2484 .fs()
2485 .insert_tree(
2486 "/dir",
2487 json!({
2488 ".git": {},
2489 "sub": {
2490 ".git": {},
2491 "b.txt": "
2492 one
2493 two
2494 three
2495 ".unindent(),
2496 },
2497 "a.txt": "
2498 one
2499 two
2500 three
2501 ".unindent(),
2502 }),
2503 )
2504 .await;
2505
2506 let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2507 let project_id = active_call_a
2508 .update(cx_a, |call, cx| {
2509 call.share_project(project_local.clone(), cx)
2510 })
2511 .await
2512 .unwrap();
2513
2514 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2515
2516 let diff_base = "
2517 one
2518 three
2519 "
2520 .unindent();
2521
2522 let new_diff_base = "
2523 one
2524 two
2525 "
2526 .unindent();
2527
2528 client_a.fs().set_index_for_repo(
2529 Path::new("/dir/.git"),
2530 &[(Path::new("a.txt"), diff_base.clone())],
2531 );
2532
2533 // Create the buffer
2534 let buffer_local_a = project_local
2535 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2536 .await
2537 .unwrap();
2538
2539 // Wait for it to catch up to the new diff
2540 executor.run_until_parked();
2541
2542 // Smoke test diffing
2543
2544 buffer_local_a.read_with(cx_a, |buffer, _| {
2545 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2546 git::diff::assert_hunks(
2547 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2548 &buffer,
2549 &diff_base,
2550 &[(1..2, "", "two\n")],
2551 );
2552 });
2553
2554 // Create remote buffer
2555 let buffer_remote_a = project_remote
2556 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2557 .await
2558 .unwrap();
2559
2560 // Wait remote buffer to catch up to the new diff
2561 executor.run_until_parked();
2562
2563 // Smoke test diffing
2564
2565 buffer_remote_a.read_with(cx_b, |buffer, _| {
2566 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2567 git::diff::assert_hunks(
2568 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2569 &buffer,
2570 &diff_base,
2571 &[(1..2, "", "two\n")],
2572 );
2573 });
2574
2575 client_a.fs().set_index_for_repo(
2576 Path::new("/dir/.git"),
2577 &[(Path::new("a.txt"), new_diff_base.clone())],
2578 );
2579
2580 // Wait for buffer_local_a to receive it
2581 executor.run_until_parked();
2582
2583 // Smoke test new diffing
2584
2585 buffer_local_a.read_with(cx_a, |buffer, _| {
2586 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2587
2588 git::diff::assert_hunks(
2589 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2590 &buffer,
2591 &diff_base,
2592 &[(2..3, "", "three\n")],
2593 );
2594 });
2595
2596 // Smoke test B
2597
2598 buffer_remote_a.read_with(cx_b, |buffer, _| {
2599 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2600 git::diff::assert_hunks(
2601 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2602 &buffer,
2603 &diff_base,
2604 &[(2..3, "", "three\n")],
2605 );
2606 });
2607
2608 //Nested git dir
2609
2610 let diff_base = "
2611 one
2612 three
2613 "
2614 .unindent();
2615
2616 let new_diff_base = "
2617 one
2618 two
2619 "
2620 .unindent();
2621
2622 client_a.fs().set_index_for_repo(
2623 Path::new("/dir/sub/.git"),
2624 &[(Path::new("b.txt"), diff_base.clone())],
2625 );
2626
2627 // Create the buffer
2628 let buffer_local_b = project_local
2629 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2630 .await
2631 .unwrap();
2632
2633 // Wait for it to catch up to the new diff
2634 executor.run_until_parked();
2635
2636 // Smoke test diffing
2637
2638 buffer_local_b.read_with(cx_a, |buffer, _| {
2639 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2640 git::diff::assert_hunks(
2641 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2642 &buffer,
2643 &diff_base,
2644 &[(1..2, "", "two\n")],
2645 );
2646 });
2647
2648 // Create remote buffer
2649 let buffer_remote_b = project_remote
2650 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2651 .await
2652 .unwrap();
2653
2654 // Wait remote buffer to catch up to the new diff
2655 executor.run_until_parked();
2656
2657 // Smoke test diffing
2658
2659 buffer_remote_b.read_with(cx_b, |buffer, _| {
2660 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2661 git::diff::assert_hunks(
2662 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2663 &buffer,
2664 &diff_base,
2665 &[(1..2, "", "two\n")],
2666 );
2667 });
2668
2669 client_a.fs().set_index_for_repo(
2670 Path::new("/dir/sub/.git"),
2671 &[(Path::new("b.txt"), new_diff_base.clone())],
2672 );
2673
2674 // Wait for buffer_local_b to receive it
2675 executor.run_until_parked();
2676
2677 // Smoke test new diffing
2678
2679 buffer_local_b.read_with(cx_a, |buffer, _| {
2680 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2681 println!("{:?}", buffer.as_rope().to_string());
2682 println!("{:?}", buffer.diff_base());
2683 println!(
2684 "{:?}",
2685 buffer
2686 .snapshot()
2687 .git_diff_hunks_in_row_range(0..4)
2688 .collect::<Vec<_>>()
2689 );
2690
2691 git::diff::assert_hunks(
2692 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2693 &buffer,
2694 &diff_base,
2695 &[(2..3, "", "three\n")],
2696 );
2697 });
2698
2699 // Smoke test B
2700
2701 buffer_remote_b.read_with(cx_b, |buffer, _| {
2702 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2703 git::diff::assert_hunks(
2704 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2705 &buffer,
2706 &diff_base,
2707 &[(2..3, "", "three\n")],
2708 );
2709 });
2710}
2711
2712#[gpui::test]
2713async fn test_git_branch_name(
2714 executor: BackgroundExecutor,
2715 cx_a: &mut TestAppContext,
2716 cx_b: &mut TestAppContext,
2717 cx_c: &mut TestAppContext,
2718) {
2719 let mut server = TestServer::start(executor.clone()).await;
2720 let client_a = server.create_client(cx_a, "user_a").await;
2721 let client_b = server.create_client(cx_b, "user_b").await;
2722 let client_c = server.create_client(cx_c, "user_c").await;
2723 server
2724 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2725 .await;
2726 let active_call_a = cx_a.read(ActiveCall::global);
2727
2728 client_a
2729 .fs()
2730 .insert_tree(
2731 "/dir",
2732 json!({
2733 ".git": {},
2734 }),
2735 )
2736 .await;
2737
2738 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2739 let project_id = active_call_a
2740 .update(cx_a, |call, cx| {
2741 call.share_project(project_local.clone(), cx)
2742 })
2743 .await
2744 .unwrap();
2745
2746 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2747 client_a
2748 .fs()
2749 .set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
2750
2751 // Wait for it to catch up to the new branch
2752 executor.run_until_parked();
2753
2754 #[track_caller]
2755 fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &AppContext) {
2756 let branch_name = branch_name.map(Into::into);
2757 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2758 assert_eq!(worktrees.len(), 1);
2759 let worktree = worktrees[0].clone();
2760 let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap();
2761 assert_eq!(root_entry.branch(), branch_name.map(Into::into));
2762 }
2763
2764 // Smoke test branch reading
2765
2766 project_local.read_with(cx_a, |project, cx| {
2767 assert_branch(Some("branch-1"), project, cx)
2768 });
2769
2770 project_remote.read_with(cx_b, |project, cx| {
2771 assert_branch(Some("branch-1"), project, cx)
2772 });
2773
2774 client_a
2775 .fs()
2776 .set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
2777
2778 // Wait for buffer_local_a to receive it
2779 executor.run_until_parked();
2780
2781 // Smoke test branch reading
2782
2783 project_local.read_with(cx_a, |project, cx| {
2784 assert_branch(Some("branch-2"), project, cx)
2785 });
2786
2787 project_remote.read_with(cx_b, |project, cx| {
2788 assert_branch(Some("branch-2"), project, cx)
2789 });
2790
2791 let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
2792 executor.run_until_parked();
2793
2794 project_remote_c.read_with(cx_c, |project, cx| {
2795 assert_branch(Some("branch-2"), project, cx)
2796 });
2797}
2798
2799#[gpui::test]
2800async fn test_git_status_sync(
2801 executor: BackgroundExecutor,
2802 cx_a: &mut TestAppContext,
2803 cx_b: &mut TestAppContext,
2804 cx_c: &mut TestAppContext,
2805) {
2806 let mut server = TestServer::start(executor.clone()).await;
2807 let client_a = server.create_client(cx_a, "user_a").await;
2808 let client_b = server.create_client(cx_b, "user_b").await;
2809 let client_c = server.create_client(cx_c, "user_c").await;
2810 server
2811 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2812 .await;
2813 let active_call_a = cx_a.read(ActiveCall::global);
2814
2815 client_a
2816 .fs()
2817 .insert_tree(
2818 "/dir",
2819 json!({
2820 ".git": {},
2821 "a.txt": "a",
2822 "b.txt": "b",
2823 }),
2824 )
2825 .await;
2826
2827 const A_TXT: &str = "a.txt";
2828 const B_TXT: &str = "b.txt";
2829
2830 client_a.fs().set_status_for_repo_via_git_operation(
2831 Path::new("/dir/.git"),
2832 &[
2833 (&Path::new(A_TXT), GitFileStatus::Added),
2834 (&Path::new(B_TXT), GitFileStatus::Added),
2835 ],
2836 );
2837
2838 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2839 let project_id = active_call_a
2840 .update(cx_a, |call, cx| {
2841 call.share_project(project_local.clone(), cx)
2842 })
2843 .await
2844 .unwrap();
2845
2846 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2847
2848 // Wait for it to catch up to the new status
2849 executor.run_until_parked();
2850
2851 #[track_caller]
2852 fn assert_status(
2853 file: &impl AsRef<Path>,
2854 status: Option<GitFileStatus>,
2855 project: &Project,
2856 cx: &AppContext,
2857 ) {
2858 let file = file.as_ref();
2859 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2860 assert_eq!(worktrees.len(), 1);
2861 let worktree = worktrees[0].clone();
2862 let snapshot = worktree.read(cx).snapshot();
2863 assert_eq!(snapshot.status_for_file(file), status);
2864 }
2865
2866 // Smoke test status reading
2867
2868 project_local.read_with(cx_a, |project, cx| {
2869 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2870 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2871 });
2872
2873 project_remote.read_with(cx_b, |project, cx| {
2874 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2875 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2876 });
2877
2878 client_a.fs().set_status_for_repo_via_working_copy_change(
2879 Path::new("/dir/.git"),
2880 &[
2881 (&Path::new(A_TXT), GitFileStatus::Modified),
2882 (&Path::new(B_TXT), GitFileStatus::Modified),
2883 ],
2884 );
2885
2886 // Wait for buffer_local_a to receive it
2887 executor.run_until_parked();
2888
2889 // Smoke test status reading
2890
2891 project_local.read_with(cx_a, |project, cx| {
2892 assert_status(
2893 &Path::new(A_TXT),
2894 Some(GitFileStatus::Modified),
2895 project,
2896 cx,
2897 );
2898 assert_status(
2899 &Path::new(B_TXT),
2900 Some(GitFileStatus::Modified),
2901 project,
2902 cx,
2903 );
2904 });
2905
2906 project_remote.read_with(cx_b, |project, cx| {
2907 assert_status(
2908 &Path::new(A_TXT),
2909 Some(GitFileStatus::Modified),
2910 project,
2911 cx,
2912 );
2913 assert_status(
2914 &Path::new(B_TXT),
2915 Some(GitFileStatus::Modified),
2916 project,
2917 cx,
2918 );
2919 });
2920
2921 // And synchronization while joining
2922 let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
2923 executor.run_until_parked();
2924
2925 project_remote_c.read_with(cx_c, |project, cx| {
2926 assert_status(
2927 &Path::new(A_TXT),
2928 Some(GitFileStatus::Modified),
2929 project,
2930 cx,
2931 );
2932 assert_status(
2933 &Path::new(B_TXT),
2934 Some(GitFileStatus::Modified),
2935 project,
2936 cx,
2937 );
2938 });
2939}
2940
2941#[gpui::test(iterations = 10)]
2942async fn test_fs_operations(
2943 executor: BackgroundExecutor,
2944 cx_a: &mut TestAppContext,
2945 cx_b: &mut TestAppContext,
2946) {
2947 let mut server = TestServer::start(executor.clone()).await;
2948 let client_a = server.create_client(cx_a, "user_a").await;
2949 let client_b = server.create_client(cx_b, "user_b").await;
2950 server
2951 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2952 .await;
2953 let active_call_a = cx_a.read(ActiveCall::global);
2954
2955 client_a
2956 .fs()
2957 .insert_tree(
2958 "/dir",
2959 json!({
2960 "a.txt": "a-contents",
2961 "b.txt": "b-contents",
2962 }),
2963 )
2964 .await;
2965 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2966 let project_id = active_call_a
2967 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2968 .await
2969 .unwrap();
2970 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2971
2972 let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
2973
2974 let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
2975
2976 let entry = project_b
2977 .update(cx_b, |project, cx| {
2978 project.create_entry((worktree_id, "c.txt"), false, cx)
2979 })
2980 .await
2981 .unwrap()
2982 .unwrap();
2983
2984 worktree_a.read_with(cx_a, |worktree, _| {
2985 assert_eq!(
2986 worktree
2987 .paths()
2988 .map(|p| p.to_string_lossy())
2989 .collect::<Vec<_>>(),
2990 ["a.txt", "b.txt", "c.txt"]
2991 );
2992 });
2993
2994 worktree_b.read_with(cx_b, |worktree, _| {
2995 assert_eq!(
2996 worktree
2997 .paths()
2998 .map(|p| p.to_string_lossy())
2999 .collect::<Vec<_>>(),
3000 ["a.txt", "b.txt", "c.txt"]
3001 );
3002 });
3003
3004 project_b
3005 .update(cx_b, |project, cx| {
3006 project.rename_entry(entry.id, Path::new("d.txt"), cx)
3007 })
3008 .await
3009 .unwrap()
3010 .unwrap();
3011
3012 worktree_a.read_with(cx_a, |worktree, _| {
3013 assert_eq!(
3014 worktree
3015 .paths()
3016 .map(|p| p.to_string_lossy())
3017 .collect::<Vec<_>>(),
3018 ["a.txt", "b.txt", "d.txt"]
3019 );
3020 });
3021
3022 worktree_b.read_with(cx_b, |worktree, _| {
3023 assert_eq!(
3024 worktree
3025 .paths()
3026 .map(|p| p.to_string_lossy())
3027 .collect::<Vec<_>>(),
3028 ["a.txt", "b.txt", "d.txt"]
3029 );
3030 });
3031
3032 let dir_entry = project_b
3033 .update(cx_b, |project, cx| {
3034 project.create_entry((worktree_id, "DIR"), true, cx)
3035 })
3036 .await
3037 .unwrap()
3038 .unwrap();
3039
3040 worktree_a.read_with(cx_a, |worktree, _| {
3041 assert_eq!(
3042 worktree
3043 .paths()
3044 .map(|p| p.to_string_lossy())
3045 .collect::<Vec<_>>(),
3046 ["DIR", "a.txt", "b.txt", "d.txt"]
3047 );
3048 });
3049
3050 worktree_b.read_with(cx_b, |worktree, _| {
3051 assert_eq!(
3052 worktree
3053 .paths()
3054 .map(|p| p.to_string_lossy())
3055 .collect::<Vec<_>>(),
3056 ["DIR", "a.txt", "b.txt", "d.txt"]
3057 );
3058 });
3059
3060 project_b
3061 .update(cx_b, |project, cx| {
3062 project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
3063 })
3064 .await
3065 .unwrap()
3066 .unwrap();
3067 project_b
3068 .update(cx_b, |project, cx| {
3069 project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
3070 })
3071 .await
3072 .unwrap()
3073 .unwrap();
3074 project_b
3075 .update(cx_b, |project, cx| {
3076 project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
3077 })
3078 .await
3079 .unwrap()
3080 .unwrap();
3081
3082 worktree_a.read_with(cx_a, |worktree, _| {
3083 assert_eq!(
3084 worktree
3085 .paths()
3086 .map(|p| p.to_string_lossy())
3087 .collect::<Vec<_>>(),
3088 [
3089 "DIR",
3090 "DIR/SUBDIR",
3091 "DIR/SUBDIR/f.txt",
3092 "DIR/e.txt",
3093 "a.txt",
3094 "b.txt",
3095 "d.txt"
3096 ]
3097 );
3098 });
3099
3100 worktree_b.read_with(cx_b, |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 project_b
3119 .update(cx_b, |project, cx| {
3120 project.copy_entry(entry.id, Path::new("f.txt"), cx)
3121 })
3122 .await
3123 .unwrap()
3124 .unwrap();
3125
3126 worktree_a.read_with(cx_a, |worktree, _| {
3127 assert_eq!(
3128 worktree
3129 .paths()
3130 .map(|p| p.to_string_lossy())
3131 .collect::<Vec<_>>(),
3132 [
3133 "DIR",
3134 "DIR/SUBDIR",
3135 "DIR/SUBDIR/f.txt",
3136 "DIR/e.txt",
3137 "a.txt",
3138 "b.txt",
3139 "d.txt",
3140 "f.txt"
3141 ]
3142 );
3143 });
3144
3145 worktree_b.read_with(cx_b, |worktree, _| {
3146 assert_eq!(
3147 worktree
3148 .paths()
3149 .map(|p| p.to_string_lossy())
3150 .collect::<Vec<_>>(),
3151 [
3152 "DIR",
3153 "DIR/SUBDIR",
3154 "DIR/SUBDIR/f.txt",
3155 "DIR/e.txt",
3156 "a.txt",
3157 "b.txt",
3158 "d.txt",
3159 "f.txt"
3160 ]
3161 );
3162 });
3163
3164 project_b
3165 .update(cx_b, |project, cx| {
3166 project.delete_entry(dir_entry.id, cx).unwrap()
3167 })
3168 .await
3169 .unwrap();
3170 executor.run_until_parked();
3171
3172 worktree_a.read_with(cx_a, |worktree, _| {
3173 assert_eq!(
3174 worktree
3175 .paths()
3176 .map(|p| p.to_string_lossy())
3177 .collect::<Vec<_>>(),
3178 ["a.txt", "b.txt", "d.txt", "f.txt"]
3179 );
3180 });
3181
3182 worktree_b.read_with(cx_b, |worktree, _| {
3183 assert_eq!(
3184 worktree
3185 .paths()
3186 .map(|p| p.to_string_lossy())
3187 .collect::<Vec<_>>(),
3188 ["a.txt", "b.txt", "d.txt", "f.txt"]
3189 );
3190 });
3191
3192 project_b
3193 .update(cx_b, |project, cx| {
3194 project.delete_entry(entry.id, cx).unwrap()
3195 })
3196 .await
3197 .unwrap();
3198
3199 worktree_a.read_with(cx_a, |worktree, _| {
3200 assert_eq!(
3201 worktree
3202 .paths()
3203 .map(|p| p.to_string_lossy())
3204 .collect::<Vec<_>>(),
3205 ["a.txt", "b.txt", "f.txt"]
3206 );
3207 });
3208
3209 worktree_b.read_with(cx_b, |worktree, _| {
3210 assert_eq!(
3211 worktree
3212 .paths()
3213 .map(|p| p.to_string_lossy())
3214 .collect::<Vec<_>>(),
3215 ["a.txt", "b.txt", "f.txt"]
3216 );
3217 });
3218}
3219
3220#[gpui::test(iterations = 10)]
3221async fn test_local_settings(
3222 executor: BackgroundExecutor,
3223 cx_a: &mut TestAppContext,
3224 cx_b: &mut TestAppContext,
3225) {
3226 let mut server = TestServer::start(executor.clone()).await;
3227 let client_a = server.create_client(cx_a, "user_a").await;
3228 let client_b = server.create_client(cx_b, "user_b").await;
3229 server
3230 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3231 .await;
3232 let active_call_a = cx_a.read(ActiveCall::global);
3233
3234 // As client A, open a project that contains some local settings files
3235 client_a
3236 .fs()
3237 .insert_tree(
3238 "/dir",
3239 json!({
3240 ".zed": {
3241 "settings.json": r#"{ "tab_size": 2 }"#
3242 },
3243 "a": {
3244 ".zed": {
3245 "settings.json": r#"{ "tab_size": 8 }"#
3246 },
3247 "a.txt": "a-contents",
3248 },
3249 "b": {
3250 "b.txt": "b-contents",
3251 }
3252 }),
3253 )
3254 .await;
3255 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
3256 executor.run_until_parked();
3257 let project_id = active_call_a
3258 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3259 .await
3260 .unwrap();
3261 executor.run_until_parked();
3262
3263 // As client B, join that project and observe the local settings.
3264 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3265
3266 let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
3267 executor.run_until_parked();
3268 cx_b.read(|cx| {
3269 let store = cx.global::<SettingsStore>();
3270 assert_eq!(
3271 store
3272 .local_settings(worktree_b.read(cx).id().to_usize())
3273 .collect::<Vec<_>>(),
3274 &[
3275 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3276 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3277 ]
3278 )
3279 });
3280
3281 // As client A, update a settings file. As Client B, see the changed settings.
3282 client_a
3283 .fs()
3284 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3285 .await;
3286 executor.run_until_parked();
3287 cx_b.read(|cx| {
3288 let store = cx.global::<SettingsStore>();
3289 assert_eq!(
3290 store
3291 .local_settings(worktree_b.read(cx).id().to_usize())
3292 .collect::<Vec<_>>(),
3293 &[
3294 (Path::new("").into(), r#"{}"#.to_string()),
3295 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3296 ]
3297 )
3298 });
3299
3300 // As client A, create and remove some settings files. As client B, see the changed settings.
3301 client_a
3302 .fs()
3303 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3304 .await
3305 .unwrap();
3306 client_a
3307 .fs()
3308 .create_dir("/dir/b/.zed".as_ref())
3309 .await
3310 .unwrap();
3311 client_a
3312 .fs()
3313 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3314 .await;
3315 executor.run_until_parked();
3316 cx_b.read(|cx| {
3317 let store = cx.global::<SettingsStore>();
3318 assert_eq!(
3319 store
3320 .local_settings(worktree_b.read(cx).id().to_usize())
3321 .collect::<Vec<_>>(),
3322 &[
3323 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3324 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3325 ]
3326 )
3327 });
3328
3329 // As client B, disconnect.
3330 server.forbid_connections();
3331 server.disconnect_client(client_b.peer_id().unwrap());
3332
3333 // As client A, change and remove settings files while client B is disconnected.
3334 client_a
3335 .fs()
3336 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3337 .await;
3338 client_a
3339 .fs()
3340 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3341 .await
3342 .unwrap();
3343 executor.run_until_parked();
3344
3345 // As client B, reconnect and see the changed settings.
3346 server.allow_connections();
3347 executor.advance_clock(RECEIVE_TIMEOUT);
3348 cx_b.read(|cx| {
3349 let store = cx.global::<SettingsStore>();
3350 assert_eq!(
3351 store
3352 .local_settings(worktree_b.read(cx).id().to_usize())
3353 .collect::<Vec<_>>(),
3354 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3355 )
3356 });
3357}
3358
3359#[gpui::test(iterations = 10)]
3360async fn test_buffer_conflict_after_save(
3361 executor: BackgroundExecutor,
3362 cx_a: &mut TestAppContext,
3363 cx_b: &mut TestAppContext,
3364) {
3365 let mut server = TestServer::start(executor.clone()).await;
3366 let client_a = server.create_client(cx_a, "user_a").await;
3367 let client_b = server.create_client(cx_b, "user_b").await;
3368 server
3369 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3370 .await;
3371 let active_call_a = cx_a.read(ActiveCall::global);
3372
3373 client_a
3374 .fs()
3375 .insert_tree(
3376 "/dir",
3377 json!({
3378 "a.txt": "a-contents",
3379 }),
3380 )
3381 .await;
3382 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3383 let project_id = active_call_a
3384 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3385 .await
3386 .unwrap();
3387 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3388
3389 // Open a buffer as client B
3390 let buffer_b = project_b
3391 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3392 .await
3393 .unwrap();
3394
3395 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3396
3397 buffer_b.read_with(cx_b, |buf, _| {
3398 assert!(buf.is_dirty());
3399 assert!(!buf.has_conflict());
3400 });
3401
3402 project_b
3403 .update(cx_b, |project, cx| {
3404 project.save_buffer(buffer_b.clone(), cx)
3405 })
3406 .await
3407 .unwrap();
3408
3409 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3410
3411 buffer_b.read_with(cx_b, |buf, _| {
3412 assert!(!buf.has_conflict());
3413 });
3414
3415 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3416
3417 buffer_b.read_with(cx_b, |buf, _| {
3418 assert!(buf.is_dirty());
3419 assert!(!buf.has_conflict());
3420 });
3421}
3422
3423#[gpui::test(iterations = 10)]
3424async fn test_buffer_reloading(
3425 executor: BackgroundExecutor,
3426 cx_a: &mut TestAppContext,
3427 cx_b: &mut TestAppContext,
3428) {
3429 let mut server = TestServer::start(executor.clone()).await;
3430 let client_a = server.create_client(cx_a, "user_a").await;
3431 let client_b = server.create_client(cx_b, "user_b").await;
3432 server
3433 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3434 .await;
3435 let active_call_a = cx_a.read(ActiveCall::global);
3436
3437 client_a
3438 .fs()
3439 .insert_tree(
3440 "/dir",
3441 json!({
3442 "a.txt": "a\nb\nc",
3443 }),
3444 )
3445 .await;
3446 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3447 let project_id = active_call_a
3448 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3449 .await
3450 .unwrap();
3451 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3452
3453 // Open a buffer as client B
3454 let buffer_b = project_b
3455 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3456 .await
3457 .unwrap();
3458
3459 buffer_b.read_with(cx_b, |buf, _| {
3460 assert!(!buf.is_dirty());
3461 assert!(!buf.has_conflict());
3462 assert_eq!(buf.line_ending(), LineEnding::Unix);
3463 });
3464
3465 let new_contents = Rope::from("d\ne\nf");
3466 client_a
3467 .fs()
3468 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3469 .await
3470 .unwrap();
3471
3472 executor.run_until_parked();
3473
3474 buffer_b.read_with(cx_b, |buf, _| {
3475 assert_eq!(buf.text(), new_contents.to_string());
3476 assert!(!buf.is_dirty());
3477 assert!(!buf.has_conflict());
3478 assert_eq!(buf.line_ending(), LineEnding::Windows);
3479 });
3480}
3481
3482#[gpui::test(iterations = 10)]
3483async fn test_editing_while_guest_opens_buffer(
3484 executor: BackgroundExecutor,
3485 cx_a: &mut TestAppContext,
3486 cx_b: &mut TestAppContext,
3487) {
3488 let mut server = TestServer::start(executor.clone()).await;
3489 let client_a = server.create_client(cx_a, "user_a").await;
3490 let client_b = server.create_client(cx_b, "user_b").await;
3491 server
3492 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3493 .await;
3494 let active_call_a = cx_a.read(ActiveCall::global);
3495
3496 client_a
3497 .fs()
3498 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3499 .await;
3500 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3501 let project_id = active_call_a
3502 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3503 .await
3504 .unwrap();
3505 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3506
3507 // Open a buffer as client A
3508 let buffer_a = project_a
3509 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3510 .await
3511 .unwrap();
3512
3513 // Start opening the same buffer as client B
3514 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3515 let buffer_b = cx_b.executor().spawn(open_buffer);
3516
3517 // Edit the buffer as client A while client B is still opening it.
3518 cx_b.executor().simulate_random_delay().await;
3519 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3520 cx_b.executor().simulate_random_delay().await;
3521 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3522
3523 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3524 let buffer_b = buffer_b.await.unwrap();
3525 executor.run_until_parked();
3526
3527 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3528}
3529
3530#[gpui::test(iterations = 10)]
3531async fn test_leaving_worktree_while_opening_buffer(
3532 executor: BackgroundExecutor,
3533 cx_a: &mut TestAppContext,
3534 cx_b: &mut TestAppContext,
3535) {
3536 let mut server = TestServer::start(executor.clone()).await;
3537 let client_a = server.create_client(cx_a, "user_a").await;
3538 let client_b = server.create_client(cx_b, "user_b").await;
3539 server
3540 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3541 .await;
3542 let active_call_a = cx_a.read(ActiveCall::global);
3543
3544 client_a
3545 .fs()
3546 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3547 .await;
3548 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3549 let project_id = active_call_a
3550 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3551 .await
3552 .unwrap();
3553 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3554
3555 // See that a guest has joined as client A.
3556 executor.run_until_parked();
3557
3558 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3559
3560 // Begin opening a buffer as client B, but leave the project before the open completes.
3561 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3562 let buffer_b = cx_b.executor().spawn(open_buffer);
3563 cx_b.update(|_| drop(project_b));
3564 drop(buffer_b);
3565
3566 // See that the guest has left.
3567 executor.run_until_parked();
3568
3569 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3570}
3571
3572#[gpui::test(iterations = 10)]
3573async fn test_canceling_buffer_opening(
3574 executor: BackgroundExecutor,
3575 cx_a: &mut TestAppContext,
3576 cx_b: &mut TestAppContext,
3577) {
3578 let mut server = TestServer::start(executor.clone()).await;
3579 let client_a = server.create_client(cx_a, "user_a").await;
3580 let client_b = server.create_client(cx_b, "user_b").await;
3581 server
3582 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3583 .await;
3584 let active_call_a = cx_a.read(ActiveCall::global);
3585
3586 client_a
3587 .fs()
3588 .insert_tree(
3589 "/dir",
3590 json!({
3591 "a.txt": "abc",
3592 }),
3593 )
3594 .await;
3595 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3596 let project_id = active_call_a
3597 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3598 .await
3599 .unwrap();
3600 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3601
3602 let buffer_a = project_a
3603 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3604 .await
3605 .unwrap();
3606
3607 // Open a buffer as client B but cancel after a random amount of time.
3608 let buffer_b = project_b.update(cx_b, |p, cx| {
3609 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3610 });
3611 executor.simulate_random_delay().await;
3612 drop(buffer_b);
3613
3614 // Try opening the same buffer again as client B, and ensure we can
3615 // still do it despite the cancellation above.
3616 let buffer_b = project_b
3617 .update(cx_b, |p, cx| {
3618 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3619 })
3620 .await
3621 .unwrap();
3622
3623 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3624}
3625
3626#[gpui::test(iterations = 10)]
3627async fn test_leaving_project(
3628 executor: BackgroundExecutor,
3629 cx_a: &mut TestAppContext,
3630 cx_b: &mut TestAppContext,
3631 cx_c: &mut TestAppContext,
3632) {
3633 let mut server = TestServer::start(executor.clone()).await;
3634 let client_a = server.create_client(cx_a, "user_a").await;
3635 let client_b = server.create_client(cx_b, "user_b").await;
3636 let client_c = server.create_client(cx_c, "user_c").await;
3637 server
3638 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3639 .await;
3640 let active_call_a = cx_a.read(ActiveCall::global);
3641
3642 client_a
3643 .fs()
3644 .insert_tree(
3645 "/a",
3646 json!({
3647 "a.txt": "a-contents",
3648 "b.txt": "b-contents",
3649 }),
3650 )
3651 .await;
3652 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3653 let project_id = active_call_a
3654 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3655 .await
3656 .unwrap();
3657 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3658 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3659
3660 // Client A sees that a guest has joined.
3661 executor.run_until_parked();
3662
3663 project_a.read_with(cx_a, |project, _| {
3664 assert_eq!(project.collaborators().len(), 2);
3665 });
3666
3667 project_b1.read_with(cx_b, |project, _| {
3668 assert_eq!(project.collaborators().len(), 2);
3669 });
3670
3671 project_c.read_with(cx_c, |project, _| {
3672 assert_eq!(project.collaborators().len(), 2);
3673 });
3674
3675 // Client B opens a buffer.
3676 let buffer_b1 = project_b1
3677 .update(cx_b, |project, cx| {
3678 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
3679 project.open_buffer((worktree_id, "a.txt"), cx)
3680 })
3681 .await
3682 .unwrap();
3683
3684 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3685
3686 // Drop client B's project and ensure client A and client C observe client B leaving.
3687 cx_b.update(|_| drop(project_b1));
3688 executor.run_until_parked();
3689
3690 project_a.read_with(cx_a, |project, _| {
3691 assert_eq!(project.collaborators().len(), 1);
3692 });
3693
3694 project_c.read_with(cx_c, |project, _| {
3695 assert_eq!(project.collaborators().len(), 1);
3696 });
3697
3698 // Client B re-joins the project and can open buffers as before.
3699 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3700 executor.run_until_parked();
3701
3702 project_a.read_with(cx_a, |project, _| {
3703 assert_eq!(project.collaborators().len(), 2);
3704 });
3705
3706 project_b2.read_with(cx_b, |project, _| {
3707 assert_eq!(project.collaborators().len(), 2);
3708 });
3709
3710 project_c.read_with(cx_c, |project, _| {
3711 assert_eq!(project.collaborators().len(), 2);
3712 });
3713
3714 let buffer_b2 = project_b2
3715 .update(cx_b, |project, cx| {
3716 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
3717 project.open_buffer((worktree_id, "a.txt"), cx)
3718 })
3719 .await
3720 .unwrap();
3721
3722 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3723
3724 // Drop client B's connection and ensure client A and client C observe client B leaving.
3725 client_b.disconnect(&cx_b.to_async());
3726 executor.advance_clock(RECONNECT_TIMEOUT);
3727
3728 project_a.read_with(cx_a, |project, _| {
3729 assert_eq!(project.collaborators().len(), 1);
3730 });
3731
3732 project_b2.read_with(cx_b, |project, _| {
3733 assert!(project.is_disconnected());
3734 });
3735
3736 project_c.read_with(cx_c, |project, _| {
3737 assert_eq!(project.collaborators().len(), 1);
3738 });
3739
3740 // Client B can't join the project, unless they re-join the room.
3741 cx_b.spawn(|cx| {
3742 Project::remote(
3743 project_id,
3744 client_b.app_state.client.clone(),
3745 client_b.user_store().clone(),
3746 client_b.language_registry().clone(),
3747 FakeFs::new(cx.background_executor().clone()),
3748 cx,
3749 )
3750 })
3751 .await
3752 .unwrap_err();
3753
3754 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3755 client_c.wait_for_current_user(cx_c).await;
3756 server.forbid_connections();
3757 server.disconnect_client(client_c.peer_id().unwrap());
3758 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3759 executor.run_until_parked();
3760
3761 project_a.read_with(cx_a, |project, _| {
3762 assert_eq!(project.collaborators().len(), 0);
3763 });
3764
3765 project_b2.read_with(cx_b, |project, _| {
3766 assert!(project.is_disconnected());
3767 });
3768
3769 project_c.read_with(cx_c, |project, _| {
3770 assert!(project.is_disconnected());
3771 });
3772}
3773
3774#[gpui::test(iterations = 10)]
3775async fn test_collaborating_with_diagnostics(
3776 executor: BackgroundExecutor,
3777 cx_a: &mut TestAppContext,
3778 cx_b: &mut TestAppContext,
3779 cx_c: &mut TestAppContext,
3780) {
3781 let mut server = TestServer::start(executor.clone()).await;
3782 let client_a = server.create_client(cx_a, "user_a").await;
3783 let client_b = server.create_client(cx_b, "user_b").await;
3784 let client_c = server.create_client(cx_c, "user_c").await;
3785 server
3786 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3787 .await;
3788 let active_call_a = cx_a.read(ActiveCall::global);
3789
3790 client_a.language_registry().add(Arc::new(Language::new(
3791 LanguageConfig {
3792 name: "Rust".into(),
3793 matcher: LanguageMatcher {
3794 path_suffixes: vec!["rs".to_string()],
3795 ..Default::default()
3796 },
3797 ..Default::default()
3798 },
3799 Some(tree_sitter_rust::language()),
3800 )));
3801 let mut fake_language_servers = client_a
3802 .language_registry()
3803 .register_fake_lsp_adapter("Rust", Default::default());
3804
3805 // Share a project as client A
3806 client_a
3807 .fs()
3808 .insert_tree(
3809 "/a",
3810 json!({
3811 "a.rs": "let one = two",
3812 "other.rs": "",
3813 }),
3814 )
3815 .await;
3816 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3817
3818 // Cause the language server to start.
3819 let _buffer = project_a
3820 .update(cx_a, |project, cx| {
3821 project.open_buffer(
3822 ProjectPath {
3823 worktree_id,
3824 path: Path::new("other.rs").into(),
3825 },
3826 cx,
3827 )
3828 })
3829 .await
3830 .unwrap();
3831
3832 // Simulate a language server reporting errors for a file.
3833 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3834 fake_language_server
3835 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3836 .await;
3837 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3838 lsp::PublishDiagnosticsParams {
3839 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3840 version: None,
3841 diagnostics: vec![lsp::Diagnostic {
3842 severity: Some(lsp::DiagnosticSeverity::WARNING),
3843 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3844 message: "message 0".to_string(),
3845 ..Default::default()
3846 }],
3847 },
3848 );
3849
3850 // Client A shares the project and, simultaneously, the language server
3851 // publishes a diagnostic. This is done to ensure that the server always
3852 // observes the latest diagnostics for a worktree.
3853 let project_id = active_call_a
3854 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3855 .await
3856 .unwrap();
3857 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3858 lsp::PublishDiagnosticsParams {
3859 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3860 version: None,
3861 diagnostics: vec![lsp::Diagnostic {
3862 severity: Some(lsp::DiagnosticSeverity::ERROR),
3863 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3864 message: "message 1".to_string(),
3865 ..Default::default()
3866 }],
3867 },
3868 );
3869
3870 // Join the worktree as client B.
3871 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3872
3873 // Wait for server to see the diagnostics update.
3874 executor.run_until_parked();
3875
3876 // Ensure client B observes the new diagnostics.
3877
3878 project_b.read_with(cx_b, |project, cx| {
3879 assert_eq!(
3880 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3881 &[(
3882 ProjectPath {
3883 worktree_id,
3884 path: Arc::from(Path::new("a.rs")),
3885 },
3886 LanguageServerId(0),
3887 DiagnosticSummary {
3888 error_count: 1,
3889 warning_count: 0,
3890 },
3891 )]
3892 )
3893 });
3894
3895 // Join project as client C and observe the diagnostics.
3896 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3897 executor.run_until_parked();
3898 let project_c_diagnostic_summaries =
3899 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3900 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
3901 })));
3902 project_c.update(cx_c, |_, cx| {
3903 let summaries = project_c_diagnostic_summaries.clone();
3904 cx.subscribe(&project_c, {
3905 move |p, _, event, cx| {
3906 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3907 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
3908 }
3909 }
3910 })
3911 .detach();
3912 });
3913
3914 executor.run_until_parked();
3915 assert_eq!(
3916 project_c_diagnostic_summaries.borrow().as_slice(),
3917 &[(
3918 ProjectPath {
3919 worktree_id,
3920 path: Arc::from(Path::new("a.rs")),
3921 },
3922 LanguageServerId(0),
3923 DiagnosticSummary {
3924 error_count: 1,
3925 warning_count: 0,
3926 },
3927 )]
3928 );
3929
3930 // Simulate a language server reporting more errors for a file.
3931 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3932 lsp::PublishDiagnosticsParams {
3933 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3934 version: None,
3935 diagnostics: vec![
3936 lsp::Diagnostic {
3937 severity: Some(lsp::DiagnosticSeverity::ERROR),
3938 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3939 message: "message 1".to_string(),
3940 ..Default::default()
3941 },
3942 lsp::Diagnostic {
3943 severity: Some(lsp::DiagnosticSeverity::WARNING),
3944 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3945 message: "message 2".to_string(),
3946 ..Default::default()
3947 },
3948 ],
3949 },
3950 );
3951
3952 // Clients B and C get the updated summaries
3953 executor.run_until_parked();
3954
3955 project_b.read_with(cx_b, |project, cx| {
3956 assert_eq!(
3957 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3958 [(
3959 ProjectPath {
3960 worktree_id,
3961 path: Arc::from(Path::new("a.rs")),
3962 },
3963 LanguageServerId(0),
3964 DiagnosticSummary {
3965 error_count: 1,
3966 warning_count: 1,
3967 },
3968 )]
3969 );
3970 });
3971
3972 project_c.read_with(cx_c, |project, cx| {
3973 assert_eq!(
3974 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3975 [(
3976 ProjectPath {
3977 worktree_id,
3978 path: Arc::from(Path::new("a.rs")),
3979 },
3980 LanguageServerId(0),
3981 DiagnosticSummary {
3982 error_count: 1,
3983 warning_count: 1,
3984 },
3985 )]
3986 );
3987 });
3988
3989 // Open the file with the errors on client B. They should be present.
3990 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
3991 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
3992
3993 buffer_b.read_with(cx_b, |buffer, _| {
3994 assert_eq!(
3995 buffer
3996 .snapshot()
3997 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3998 .collect::<Vec<_>>(),
3999 &[
4000 DiagnosticEntry {
4001 range: Point::new(0, 4)..Point::new(0, 7),
4002 diagnostic: Diagnostic {
4003 group_id: 2,
4004 message: "message 1".to_string(),
4005 severity: lsp::DiagnosticSeverity::ERROR,
4006 is_primary: true,
4007 ..Default::default()
4008 }
4009 },
4010 DiagnosticEntry {
4011 range: Point::new(0, 10)..Point::new(0, 13),
4012 diagnostic: Diagnostic {
4013 group_id: 3,
4014 severity: lsp::DiagnosticSeverity::WARNING,
4015 message: "message 2".to_string(),
4016 is_primary: true,
4017 ..Default::default()
4018 }
4019 }
4020 ]
4021 );
4022 });
4023
4024 // Simulate a language server reporting no errors for a file.
4025 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4026 lsp::PublishDiagnosticsParams {
4027 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
4028 version: None,
4029 diagnostics: vec![],
4030 },
4031 );
4032 executor.run_until_parked();
4033
4034 project_a.read_with(cx_a, |project, cx| {
4035 assert_eq!(
4036 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4037 []
4038 )
4039 });
4040
4041 project_b.read_with(cx_b, |project, cx| {
4042 assert_eq!(
4043 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4044 []
4045 )
4046 });
4047
4048 project_c.read_with(cx_c, |project, cx| {
4049 assert_eq!(
4050 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4051 []
4052 )
4053 });
4054}
4055
4056#[gpui::test(iterations = 10)]
4057async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4058 executor: BackgroundExecutor,
4059 cx_a: &mut TestAppContext,
4060 cx_b: &mut TestAppContext,
4061) {
4062 let mut server = TestServer::start(executor.clone()).await;
4063 let client_a = server.create_client(cx_a, "user_a").await;
4064 let client_b = server.create_client(cx_b, "user_b").await;
4065 server
4066 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4067 .await;
4068
4069 client_a.language_registry().add(rust_lang());
4070 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
4071 "Rust",
4072 FakeLspAdapter {
4073 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4074 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4075 ..Default::default()
4076 },
4077 );
4078
4079 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4080 client_a
4081 .fs()
4082 .insert_tree(
4083 "/test",
4084 json!({
4085 "one.rs": "const ONE: usize = 1;",
4086 "two.rs": "const TWO: usize = 2;",
4087 "three.rs": "const THREE: usize = 3;",
4088 "four.rs": "const FOUR: usize = 3;",
4089 "five.rs": "const FIVE: usize = 3;",
4090 }),
4091 )
4092 .await;
4093
4094 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
4095
4096 // Share a project as client A
4097 let active_call_a = cx_a.read(ActiveCall::global);
4098 let project_id = active_call_a
4099 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4100 .await
4101 .unwrap();
4102
4103 // Join the project as client B and open all three files.
4104 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4105 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4106 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
4107 }))
4108 .await
4109 .unwrap();
4110
4111 // Simulate a language server reporting errors for a file.
4112 let fake_language_server = fake_language_servers.next().await.unwrap();
4113 fake_language_server
4114 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4115 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4116 })
4117 .await
4118 .unwrap();
4119 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4120 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4121 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4122 lsp::WorkDoneProgressBegin {
4123 title: "Progress Began".into(),
4124 ..Default::default()
4125 },
4126 )),
4127 });
4128 for file_name in file_names {
4129 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4130 lsp::PublishDiagnosticsParams {
4131 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
4132 version: None,
4133 diagnostics: vec![lsp::Diagnostic {
4134 severity: Some(lsp::DiagnosticSeverity::WARNING),
4135 source: Some("the-disk-based-diagnostics-source".into()),
4136 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4137 message: "message one".to_string(),
4138 ..Default::default()
4139 }],
4140 },
4141 );
4142 }
4143 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4144 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4145 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4146 lsp::WorkDoneProgressEnd { message: None },
4147 )),
4148 });
4149
4150 // When the "disk base diagnostics finished" message is received, the buffers'
4151 // diagnostics are expected to be present.
4152 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4153 project_b.update(cx_b, {
4154 let project_b = project_b.clone();
4155 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4156 move |_, cx| {
4157 cx.subscribe(&project_b, move |_, _, event, cx| {
4158 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4159 disk_based_diagnostics_finished.store(true, SeqCst);
4160 for buffer in &guest_buffers {
4161 assert_eq!(
4162 buffer
4163 .read(cx)
4164 .snapshot()
4165 .diagnostics_in_range::<_, usize>(0..5, false)
4166 .count(),
4167 1,
4168 "expected a diagnostic for buffer {:?}",
4169 buffer.read(cx).file().unwrap().path(),
4170 );
4171 }
4172 }
4173 })
4174 .detach();
4175 }
4176 });
4177
4178 executor.run_until_parked();
4179 assert!(disk_based_diagnostics_finished.load(SeqCst));
4180}
4181
4182#[gpui::test(iterations = 10)]
4183async fn test_reloading_buffer_manually(
4184 executor: BackgroundExecutor,
4185 cx_a: &mut TestAppContext,
4186 cx_b: &mut TestAppContext,
4187) {
4188 let mut server = TestServer::start(executor.clone()).await;
4189 let client_a = server.create_client(cx_a, "user_a").await;
4190 let client_b = server.create_client(cx_b, "user_b").await;
4191 server
4192 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4193 .await;
4194 let active_call_a = cx_a.read(ActiveCall::global);
4195
4196 client_a
4197 .fs()
4198 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4199 .await;
4200 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4201 let buffer_a = project_a
4202 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4203 .await
4204 .unwrap();
4205 let project_id = active_call_a
4206 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4207 .await
4208 .unwrap();
4209
4210 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4211
4212 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4213 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4214 buffer_b.update(cx_b, |buffer, cx| {
4215 buffer.edit([(4..7, "six")], None, cx);
4216 buffer.edit([(10..11, "6")], None, cx);
4217 assert_eq!(buffer.text(), "let six = 6;");
4218 assert!(buffer.is_dirty());
4219 assert!(!buffer.has_conflict());
4220 });
4221 executor.run_until_parked();
4222
4223 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4224
4225 client_a
4226 .fs()
4227 .save(
4228 "/a/a.rs".as_ref(),
4229 &Rope::from("let seven = 7;"),
4230 LineEnding::Unix,
4231 )
4232 .await
4233 .unwrap();
4234 executor.run_until_parked();
4235
4236 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4237
4238 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4239
4240 project_b
4241 .update(cx_b, |project, cx| {
4242 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4243 })
4244 .await
4245 .unwrap();
4246
4247 buffer_a.read_with(cx_a, |buffer, _| {
4248 assert_eq!(buffer.text(), "let seven = 7;");
4249 assert!(!buffer.is_dirty());
4250 assert!(!buffer.has_conflict());
4251 });
4252
4253 buffer_b.read_with(cx_b, |buffer, _| {
4254 assert_eq!(buffer.text(), "let seven = 7;");
4255 assert!(!buffer.is_dirty());
4256 assert!(!buffer.has_conflict());
4257 });
4258
4259 buffer_a.update(cx_a, |buffer, cx| {
4260 // Undoing on the host is a no-op when the reload was initiated by the guest.
4261 buffer.undo(cx);
4262 assert_eq!(buffer.text(), "let seven = 7;");
4263 assert!(!buffer.is_dirty());
4264 assert!(!buffer.has_conflict());
4265 });
4266 buffer_b.update(cx_b, |buffer, cx| {
4267 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4268 buffer.undo(cx);
4269 assert_eq!(buffer.text(), "let six = 6;");
4270 assert!(buffer.is_dirty());
4271 assert!(!buffer.has_conflict());
4272 });
4273}
4274
4275#[gpui::test(iterations = 10)]
4276async fn test_formatting_buffer(
4277 executor: BackgroundExecutor,
4278 cx_a: &mut TestAppContext,
4279 cx_b: &mut TestAppContext,
4280) {
4281 executor.allow_parking();
4282 let mut server = TestServer::start(executor.clone()).await;
4283 let client_a = server.create_client(cx_a, "user_a").await;
4284 let client_b = server.create_client(cx_b, "user_b").await;
4285 server
4286 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4287 .await;
4288 let active_call_a = cx_a.read(ActiveCall::global);
4289
4290 client_a.language_registry().add(rust_lang());
4291 let mut fake_language_servers = client_a
4292 .language_registry()
4293 .register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
4294
4295 // Here we insert a fake tree with a directory that exists on disk. This is needed
4296 // because later we'll invoke a command, which requires passing a working directory
4297 // that points to a valid location on disk.
4298 let directory = env::current_dir().unwrap();
4299 client_a
4300 .fs()
4301 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4302 .await;
4303 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4304 let project_id = active_call_a
4305 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4306 .await
4307 .unwrap();
4308 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4309
4310 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4311 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4312
4313 let fake_language_server = fake_language_servers.next().await.unwrap();
4314 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4315 Ok(Some(vec![
4316 lsp::TextEdit {
4317 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4318 new_text: "h".to_string(),
4319 },
4320 lsp::TextEdit {
4321 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4322 new_text: "y".to_string(),
4323 },
4324 ]))
4325 });
4326
4327 project_b
4328 .update(cx_b, |project, cx| {
4329 project.format(
4330 HashSet::from_iter([buffer_b.clone()]),
4331 true,
4332 FormatTrigger::Save,
4333 cx,
4334 )
4335 })
4336 .await
4337 .unwrap();
4338
4339 // The edits from the LSP are applied, and a final newline is added.
4340 assert_eq!(
4341 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4342 "let honey = \"two\"\n"
4343 );
4344
4345 // Ensure buffer can be formatted using an external command. Notice how the
4346 // host's configuration is honored as opposed to using the guest's settings.
4347 cx_a.update(|cx| {
4348 cx.update_global(|store: &mut SettingsStore, cx| {
4349 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4350 file.defaults.formatter = Some(Formatter::External {
4351 command: "awk".into(),
4352 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4353 });
4354 });
4355 });
4356 });
4357 project_b
4358 .update(cx_b, |project, cx| {
4359 project.format(
4360 HashSet::from_iter([buffer_b.clone()]),
4361 true,
4362 FormatTrigger::Save,
4363 cx,
4364 )
4365 })
4366 .await
4367 .unwrap();
4368 assert_eq!(
4369 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4370 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4371 );
4372}
4373
4374#[gpui::test(iterations = 10)]
4375async fn test_prettier_formatting_buffer(
4376 executor: BackgroundExecutor,
4377 cx_a: &mut TestAppContext,
4378 cx_b: &mut TestAppContext,
4379) {
4380 let mut server = TestServer::start(executor.clone()).await;
4381 let client_a = server.create_client(cx_a, "user_a").await;
4382 let client_b = server.create_client(cx_b, "user_b").await;
4383 server
4384 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4385 .await;
4386 let active_call_a = cx_a.read(ActiveCall::global);
4387
4388 let test_plugin = "test_plugin";
4389
4390 client_a.language_registry().add(Arc::new(Language::new(
4391 LanguageConfig {
4392 name: "Rust".into(),
4393 matcher: LanguageMatcher {
4394 path_suffixes: vec!["rs".to_string()],
4395 ..Default::default()
4396 },
4397 prettier_parser_name: Some("test_parser".to_string()),
4398 ..Default::default()
4399 },
4400 Some(tree_sitter_rust::language()),
4401 )));
4402 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
4403 "Rust",
4404 FakeLspAdapter {
4405 prettier_plugins: vec![test_plugin],
4406 ..Default::default()
4407 },
4408 );
4409
4410 // Here we insert a fake tree with a directory that exists on disk. This is needed
4411 // because later we'll invoke a command, which requires passing a working directory
4412 // that points to a valid location on disk.
4413 let directory = env::current_dir().unwrap();
4414 let buffer_text = "let one = \"two\"";
4415 client_a
4416 .fs()
4417 .insert_tree(&directory, json!({ "a.rs": buffer_text }))
4418 .await;
4419 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4420 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4421 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4422 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4423
4424 let project_id = active_call_a
4425 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4426 .await
4427 .unwrap();
4428 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4429 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4430 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4431
4432 cx_a.update(|cx| {
4433 cx.update_global(|store: &mut SettingsStore, cx| {
4434 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4435 file.defaults.formatter = Some(Formatter::Auto);
4436 });
4437 });
4438 });
4439 cx_b.update(|cx| {
4440 cx.update_global(|store: &mut SettingsStore, cx| {
4441 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4442 file.defaults.formatter = Some(Formatter::LanguageServer);
4443 });
4444 });
4445 });
4446 let fake_language_server = fake_language_servers.next().await.unwrap();
4447 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4448 panic!(
4449 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4450 )
4451 });
4452
4453 project_b
4454 .update(cx_b, |project, cx| {
4455 project.format(
4456 HashSet::from_iter([buffer_b.clone()]),
4457 true,
4458 FormatTrigger::Save,
4459 cx,
4460 )
4461 })
4462 .await
4463 .unwrap();
4464
4465 executor.run_until_parked();
4466 assert_eq!(
4467 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4468 buffer_text.to_string() + "\n" + prettier_format_suffix,
4469 "Prettier formatting was not applied to client buffer after client's request"
4470 );
4471
4472 project_a
4473 .update(cx_a, |project, cx| {
4474 project.format(
4475 HashSet::from_iter([buffer_a.clone()]),
4476 true,
4477 FormatTrigger::Manual,
4478 cx,
4479 )
4480 })
4481 .await
4482 .unwrap();
4483
4484 executor.run_until_parked();
4485 assert_eq!(
4486 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4487 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4488 "Prettier formatting was not applied to client buffer after host's request"
4489 );
4490}
4491
4492#[gpui::test(iterations = 10)]
4493async fn test_definition(
4494 executor: BackgroundExecutor,
4495 cx_a: &mut TestAppContext,
4496 cx_b: &mut TestAppContext,
4497) {
4498 let mut server = TestServer::start(executor.clone()).await;
4499 let client_a = server.create_client(cx_a, "user_a").await;
4500 let client_b = server.create_client(cx_b, "user_b").await;
4501 server
4502 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4503 .await;
4504 let active_call_a = cx_a.read(ActiveCall::global);
4505
4506 let mut fake_language_servers = client_a
4507 .language_registry()
4508 .register_fake_lsp_adapter("Rust", Default::default());
4509 client_a.language_registry().add(rust_lang());
4510
4511 client_a
4512 .fs()
4513 .insert_tree(
4514 "/root",
4515 json!({
4516 "dir-1": {
4517 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4518 },
4519 "dir-2": {
4520 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4521 "c.rs": "type T2 = usize;",
4522 }
4523 }),
4524 )
4525 .await;
4526 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4527 let project_id = active_call_a
4528 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4529 .await
4530 .unwrap();
4531 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4532
4533 // Open the file on client B.
4534 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4535 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4536
4537 // Request the definition of a symbol as the guest.
4538 let fake_language_server = fake_language_servers.next().await.unwrap();
4539 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4540 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4541 lsp::Location::new(
4542 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4543 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4544 ),
4545 )))
4546 });
4547
4548 let definitions_1 = project_b
4549 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4550 .await
4551 .unwrap();
4552 cx_b.read(|cx| {
4553 assert_eq!(definitions_1.len(), 1);
4554 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4555 let target_buffer = definitions_1[0].target.buffer.read(cx);
4556 assert_eq!(
4557 target_buffer.text(),
4558 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4559 );
4560 assert_eq!(
4561 definitions_1[0].target.range.to_point(target_buffer),
4562 Point::new(0, 6)..Point::new(0, 9)
4563 );
4564 });
4565
4566 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4567 // the previous call to `definition`.
4568 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4569 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4570 lsp::Location::new(
4571 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4572 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4573 ),
4574 )))
4575 });
4576
4577 let definitions_2 = project_b
4578 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4579 .await
4580 .unwrap();
4581 cx_b.read(|cx| {
4582 assert_eq!(definitions_2.len(), 1);
4583 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4584 let target_buffer = definitions_2[0].target.buffer.read(cx);
4585 assert_eq!(
4586 target_buffer.text(),
4587 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4588 );
4589 assert_eq!(
4590 definitions_2[0].target.range.to_point(target_buffer),
4591 Point::new(1, 6)..Point::new(1, 11)
4592 );
4593 });
4594 assert_eq!(
4595 definitions_1[0].target.buffer,
4596 definitions_2[0].target.buffer
4597 );
4598
4599 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4600 |req, _| async move {
4601 assert_eq!(
4602 req.text_document_position_params.position,
4603 lsp::Position::new(0, 7)
4604 );
4605 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4606 lsp::Location::new(
4607 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4608 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4609 ),
4610 )))
4611 },
4612 );
4613
4614 let type_definitions = project_b
4615 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4616 .await
4617 .unwrap();
4618 cx_b.read(|cx| {
4619 assert_eq!(type_definitions.len(), 1);
4620 let target_buffer = type_definitions[0].target.buffer.read(cx);
4621 assert_eq!(target_buffer.text(), "type T2 = usize;");
4622 assert_eq!(
4623 type_definitions[0].target.range.to_point(target_buffer),
4624 Point::new(0, 5)..Point::new(0, 7)
4625 );
4626 });
4627}
4628
4629#[gpui::test(iterations = 10)]
4630async fn test_references(
4631 executor: BackgroundExecutor,
4632 cx_a: &mut TestAppContext,
4633 cx_b: &mut TestAppContext,
4634) {
4635 let mut server = TestServer::start(executor.clone()).await;
4636 let client_a = server.create_client(cx_a, "user_a").await;
4637 let client_b = server.create_client(cx_b, "user_b").await;
4638 server
4639 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4640 .await;
4641 let active_call_a = cx_a.read(ActiveCall::global);
4642
4643 client_a.language_registry().add(rust_lang());
4644 let mut fake_language_servers = client_a
4645 .language_registry()
4646 .register_fake_lsp_adapter("Rust", Default::default());
4647
4648 client_a
4649 .fs()
4650 .insert_tree(
4651 "/root",
4652 json!({
4653 "dir-1": {
4654 "one.rs": "const ONE: usize = 1;",
4655 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4656 },
4657 "dir-2": {
4658 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4659 }
4660 }),
4661 )
4662 .await;
4663 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4664 let project_id = active_call_a
4665 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4666 .await
4667 .unwrap();
4668 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4669
4670 // Open the file on client B.
4671 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4672 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4673
4674 // Request references to a symbol as the guest.
4675 let fake_language_server = fake_language_servers.next().await.unwrap();
4676 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4677 assert_eq!(
4678 params.text_document_position.text_document.uri.as_str(),
4679 "file:///root/dir-1/one.rs"
4680 );
4681 Ok(Some(vec![
4682 lsp::Location {
4683 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4684 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4685 },
4686 lsp::Location {
4687 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4688 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4689 },
4690 lsp::Location {
4691 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4692 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4693 },
4694 ]))
4695 });
4696
4697 let references = project_b
4698 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4699 .await
4700 .unwrap();
4701 cx_b.read(|cx| {
4702 assert_eq!(references.len(), 3);
4703 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4704
4705 let two_buffer = references[0].buffer.read(cx);
4706 let three_buffer = references[2].buffer.read(cx);
4707 assert_eq!(
4708 two_buffer.file().unwrap().path().as_ref(),
4709 Path::new("two.rs")
4710 );
4711 assert_eq!(references[1].buffer, references[0].buffer);
4712 assert_eq!(
4713 three_buffer.file().unwrap().full_path(cx),
4714 Path::new("/root/dir-2/three.rs")
4715 );
4716
4717 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4718 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4719 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4720 });
4721}
4722
4723#[gpui::test(iterations = 10)]
4724async fn test_project_search(
4725 executor: BackgroundExecutor,
4726 cx_a: &mut TestAppContext,
4727 cx_b: &mut TestAppContext,
4728) {
4729 let mut server = TestServer::start(executor.clone()).await;
4730 let client_a = server.create_client(cx_a, "user_a").await;
4731 let client_b = server.create_client(cx_b, "user_b").await;
4732 server
4733 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4734 .await;
4735 let active_call_a = cx_a.read(ActiveCall::global);
4736
4737 client_a
4738 .fs()
4739 .insert_tree(
4740 "/root",
4741 json!({
4742 "dir-1": {
4743 "a": "hello world",
4744 "b": "goodnight moon",
4745 "c": "a world of goo",
4746 "d": "world champion of clown world",
4747 },
4748 "dir-2": {
4749 "e": "disney world is fun",
4750 }
4751 }),
4752 )
4753 .await;
4754 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4755 let (worktree_2, _) = project_a
4756 .update(cx_a, |p, cx| {
4757 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4758 })
4759 .await
4760 .unwrap();
4761 worktree_2
4762 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4763 .await;
4764 let project_id = active_call_a
4765 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4766 .await
4767 .unwrap();
4768
4769 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4770
4771 // Perform a search as the guest.
4772 let mut results = HashMap::default();
4773 let mut search_rx = project_b.update(cx_b, |project, cx| {
4774 project.search(
4775 SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
4776 cx,
4777 )
4778 });
4779 while let Some(result) = search_rx.next().await {
4780 match result {
4781 SearchResult::Buffer { buffer, ranges } => {
4782 results.entry(buffer).or_insert(ranges);
4783 }
4784 SearchResult::LimitReached => {
4785 panic!("Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call.")
4786 }
4787 };
4788 }
4789
4790 let mut ranges_by_path = results
4791 .into_iter()
4792 .map(|(buffer, ranges)| {
4793 buffer.read_with(cx_b, |buffer, cx| {
4794 let path = buffer.file().unwrap().full_path(cx);
4795 let offset_ranges = ranges
4796 .into_iter()
4797 .map(|range| range.to_offset(buffer))
4798 .collect::<Vec<_>>();
4799 (path, offset_ranges)
4800 })
4801 })
4802 .collect::<Vec<_>>();
4803 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4804
4805 assert_eq!(
4806 ranges_by_path,
4807 &[
4808 (PathBuf::from("dir-1/a"), vec![6..11]),
4809 (PathBuf::from("dir-1/c"), vec![2..7]),
4810 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4811 (PathBuf::from("dir-2/e"), vec![7..12]),
4812 ]
4813 );
4814}
4815
4816#[gpui::test(iterations = 10)]
4817async fn test_document_highlights(
4818 executor: BackgroundExecutor,
4819 cx_a: &mut TestAppContext,
4820 cx_b: &mut TestAppContext,
4821) {
4822 let mut server = TestServer::start(executor.clone()).await;
4823 let client_a = server.create_client(cx_a, "user_a").await;
4824 let client_b = server.create_client(cx_b, "user_b").await;
4825 server
4826 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4827 .await;
4828 let active_call_a = cx_a.read(ActiveCall::global);
4829
4830 client_a
4831 .fs()
4832 .insert_tree(
4833 "/root-1",
4834 json!({
4835 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4836 }),
4837 )
4838 .await;
4839
4840 let mut fake_language_servers = client_a
4841 .language_registry()
4842 .register_fake_lsp_adapter("Rust", Default::default());
4843 client_a.language_registry().add(rust_lang());
4844
4845 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4846 let project_id = active_call_a
4847 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4848 .await
4849 .unwrap();
4850 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4851
4852 // Open the file on client B.
4853 let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4854 let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
4855
4856 // Request document highlights as the guest.
4857 let fake_language_server = fake_language_servers.next().await.unwrap();
4858 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4859 |params, _| async move {
4860 assert_eq!(
4861 params
4862 .text_document_position_params
4863 .text_document
4864 .uri
4865 .as_str(),
4866 "file:///root-1/main.rs"
4867 );
4868 assert_eq!(
4869 params.text_document_position_params.position,
4870 lsp::Position::new(0, 34)
4871 );
4872 Ok(Some(vec![
4873 lsp::DocumentHighlight {
4874 kind: Some(lsp::DocumentHighlightKind::WRITE),
4875 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4876 },
4877 lsp::DocumentHighlight {
4878 kind: Some(lsp::DocumentHighlightKind::READ),
4879 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4880 },
4881 lsp::DocumentHighlight {
4882 kind: Some(lsp::DocumentHighlightKind::READ),
4883 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4884 },
4885 ]))
4886 },
4887 );
4888
4889 let highlights = project_b
4890 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4891 .await
4892 .unwrap();
4893
4894 buffer_b.read_with(cx_b, |buffer, _| {
4895 let snapshot = buffer.snapshot();
4896
4897 let highlights = highlights
4898 .into_iter()
4899 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4900 .collect::<Vec<_>>();
4901 assert_eq!(
4902 highlights,
4903 &[
4904 (lsp::DocumentHighlightKind::WRITE, 10..16),
4905 (lsp::DocumentHighlightKind::READ, 32..38),
4906 (lsp::DocumentHighlightKind::READ, 41..47)
4907 ]
4908 )
4909 });
4910}
4911
4912#[gpui::test(iterations = 10)]
4913async fn test_lsp_hover(
4914 executor: BackgroundExecutor,
4915 cx_a: &mut TestAppContext,
4916 cx_b: &mut TestAppContext,
4917) {
4918 let mut server = TestServer::start(executor.clone()).await;
4919 let client_a = server.create_client(cx_a, "user_a").await;
4920 let client_b = server.create_client(cx_b, "user_b").await;
4921 server
4922 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4923 .await;
4924 let active_call_a = cx_a.read(ActiveCall::global);
4925
4926 client_a
4927 .fs()
4928 .insert_tree(
4929 "/root-1",
4930 json!({
4931 "main.rs": "use std::collections::HashMap;",
4932 }),
4933 )
4934 .await;
4935
4936 client_a.language_registry().add(rust_lang());
4937 let language_server_names = ["rust-analyzer", "CrabLang-ls"];
4938 let mut fake_language_servers = client_a
4939 .language_registry()
4940 .register_specific_fake_lsp_adapter(
4941 "Rust",
4942 true,
4943 FakeLspAdapter {
4944 name: "rust-analyzer",
4945 capabilities: lsp::ServerCapabilities {
4946 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4947 ..lsp::ServerCapabilities::default()
4948 },
4949 ..FakeLspAdapter::default()
4950 },
4951 );
4952 let _other_server = client_a
4953 .language_registry()
4954 .register_specific_fake_lsp_adapter(
4955 "Rust",
4956 false,
4957 FakeLspAdapter {
4958 name: "CrabLang-ls",
4959 capabilities: lsp::ServerCapabilities {
4960 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4961 ..lsp::ServerCapabilities::default()
4962 },
4963 ..FakeLspAdapter::default()
4964 },
4965 );
4966
4967 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4968 let project_id = active_call_a
4969 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4970 .await
4971 .unwrap();
4972 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4973
4974 // Open the file as the guest
4975 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4976 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4977
4978 let mut servers_with_hover_requests = HashMap::default();
4979 for i in 0..language_server_names.len() {
4980 let new_server = fake_language_servers.next().await.unwrap_or_else(|| {
4981 panic!(
4982 "Failed to get language server #{i} with name {}",
4983 &language_server_names[i]
4984 )
4985 });
4986 let new_server_name = new_server.server.name();
4987 assert!(
4988 !servers_with_hover_requests.contains_key(new_server_name),
4989 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
4990 );
4991 let new_server_name = new_server_name.to_string();
4992 match new_server_name.as_str() {
4993 "CrabLang-ls" => {
4994 servers_with_hover_requests.insert(
4995 new_server_name.clone(),
4996 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
4997 move |params, _| {
4998 assert_eq!(
4999 params
5000 .text_document_position_params
5001 .text_document
5002 .uri
5003 .as_str(),
5004 "file:///root-1/main.rs"
5005 );
5006 let name = new_server_name.clone();
5007 async move {
5008 Ok(Some(lsp::Hover {
5009 contents: lsp::HoverContents::Scalar(
5010 lsp::MarkedString::String(format!("{name} hover")),
5011 ),
5012 range: None,
5013 }))
5014 }
5015 },
5016 ),
5017 );
5018 }
5019 "rust-analyzer" => {
5020 servers_with_hover_requests.insert(
5021 new_server_name.clone(),
5022 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5023 |params, _| async move {
5024 assert_eq!(
5025 params
5026 .text_document_position_params
5027 .text_document
5028 .uri
5029 .as_str(),
5030 "file:///root-1/main.rs"
5031 );
5032 assert_eq!(
5033 params.text_document_position_params.position,
5034 lsp::Position::new(0, 22)
5035 );
5036 Ok(Some(lsp::Hover {
5037 contents: lsp::HoverContents::Array(vec![
5038 lsp::MarkedString::String("Test hover content.".to_string()),
5039 lsp::MarkedString::LanguageString(lsp::LanguageString {
5040 language: "Rust".to_string(),
5041 value: "let foo = 42;".to_string(),
5042 }),
5043 ]),
5044 range: Some(lsp::Range::new(
5045 lsp::Position::new(0, 22),
5046 lsp::Position::new(0, 29),
5047 )),
5048 }))
5049 },
5050 ),
5051 );
5052 }
5053 unexpected => panic!("Unexpected server name: {unexpected}"),
5054 }
5055 }
5056
5057 // Request hover information as the guest.
5058 let mut hovers = project_b
5059 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5060 .await;
5061 assert_eq!(
5062 hovers.len(),
5063 2,
5064 "Expected two hovers from both language servers, but got: {hovers:?}"
5065 );
5066
5067 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5068 |mut hover_request| async move {
5069 hover_request
5070 .next()
5071 .await
5072 .expect("All hover requests should have been triggered")
5073 },
5074 ))
5075 .await;
5076
5077 hovers.sort_by_key(|hover| hover.contents.len());
5078 let first_hover = hovers.first().cloned().unwrap();
5079 assert_eq!(
5080 first_hover.contents,
5081 vec![project::HoverBlock {
5082 text: "CrabLang-ls hover".to_string(),
5083 kind: HoverBlockKind::Markdown,
5084 },]
5085 );
5086 let second_hover = hovers.last().cloned().unwrap();
5087 assert_eq!(
5088 second_hover.contents,
5089 vec![
5090 project::HoverBlock {
5091 text: "Test hover content.".to_string(),
5092 kind: HoverBlockKind::Markdown,
5093 },
5094 project::HoverBlock {
5095 text: "let foo = 42;".to_string(),
5096 kind: HoverBlockKind::Code {
5097 language: "Rust".to_string()
5098 },
5099 }
5100 ]
5101 );
5102 buffer_b.read_with(cx_b, |buffer, _| {
5103 let snapshot = buffer.snapshot();
5104 assert_eq!(second_hover.range.unwrap().to_offset(&snapshot), 22..29);
5105 });
5106}
5107
5108#[gpui::test(iterations = 10)]
5109async fn test_project_symbols(
5110 executor: BackgroundExecutor,
5111 cx_a: &mut TestAppContext,
5112 cx_b: &mut TestAppContext,
5113) {
5114 let mut server = TestServer::start(executor.clone()).await;
5115 let client_a = server.create_client(cx_a, "user_a").await;
5116 let client_b = server.create_client(cx_b, "user_b").await;
5117 server
5118 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5119 .await;
5120 let active_call_a = cx_a.read(ActiveCall::global);
5121
5122 client_a.language_registry().add(rust_lang());
5123 let mut fake_language_servers = client_a
5124 .language_registry()
5125 .register_fake_lsp_adapter("Rust", Default::default());
5126
5127 client_a
5128 .fs()
5129 .insert_tree(
5130 "/code",
5131 json!({
5132 "crate-1": {
5133 "one.rs": "const ONE: usize = 1;",
5134 },
5135 "crate-2": {
5136 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5137 },
5138 "private": {
5139 "passwords.txt": "the-password",
5140 }
5141 }),
5142 )
5143 .await;
5144 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
5145 let project_id = active_call_a
5146 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5147 .await
5148 .unwrap();
5149 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5150
5151 // Cause the language server to start.
5152 let open_buffer_task =
5153 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
5154 let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5155
5156 let fake_language_server = fake_language_servers.next().await.unwrap();
5157 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
5158 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5159 #[allow(deprecated)]
5160 lsp::SymbolInformation {
5161 name: "TWO".into(),
5162 location: lsp::Location {
5163 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
5164 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5165 },
5166 kind: lsp::SymbolKind::CONSTANT,
5167 tags: None,
5168 container_name: None,
5169 deprecated: None,
5170 },
5171 ])))
5172 });
5173
5174 // Request the definition of a symbol as the guest.
5175 let symbols = project_b
5176 .update(cx_b, |p, cx| p.symbols("two", cx))
5177 .await
5178 .unwrap();
5179 assert_eq!(symbols.len(), 1);
5180 assert_eq!(symbols[0].name, "TWO");
5181
5182 // Open one of the returned symbols.
5183 let buffer_b_2 = project_b
5184 .update(cx_b, |project, cx| {
5185 project.open_buffer_for_symbol(&symbols[0], cx)
5186 })
5187 .await
5188 .unwrap();
5189
5190 buffer_b_2.read_with(cx_b, |buffer, cx| {
5191 assert_eq!(
5192 buffer.file().unwrap().full_path(cx),
5193 Path::new("/code/crate-2/two.rs")
5194 );
5195 });
5196
5197 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5198 let mut fake_symbol = symbols[0].clone();
5199 fake_symbol.path.path = Path::new("/code/secrets").into();
5200 let error = project_b
5201 .update(cx_b, |project, cx| {
5202 project.open_buffer_for_symbol(&fake_symbol, cx)
5203 })
5204 .await
5205 .unwrap_err();
5206 assert!(error.to_string().contains("invalid symbol signature"));
5207}
5208
5209#[gpui::test(iterations = 10)]
5210async fn test_open_buffer_while_getting_definition_pointing_to_it(
5211 executor: BackgroundExecutor,
5212 cx_a: &mut TestAppContext,
5213 cx_b: &mut TestAppContext,
5214 mut rng: StdRng,
5215) {
5216 let mut server = TestServer::start(executor.clone()).await;
5217 let client_a = server.create_client(cx_a, "user_a").await;
5218 let client_b = server.create_client(cx_b, "user_b").await;
5219 server
5220 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5221 .await;
5222 let active_call_a = cx_a.read(ActiveCall::global);
5223
5224 client_a.language_registry().add(rust_lang());
5225 let mut fake_language_servers = client_a
5226 .language_registry()
5227 .register_fake_lsp_adapter("Rust", Default::default());
5228
5229 client_a
5230 .fs()
5231 .insert_tree(
5232 "/root",
5233 json!({
5234 "a.rs": "const ONE: usize = b::TWO;",
5235 "b.rs": "const TWO: usize = 2",
5236 }),
5237 )
5238 .await;
5239 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5240 let project_id = active_call_a
5241 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5242 .await
5243 .unwrap();
5244 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5245
5246 let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
5247 let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5248
5249 let fake_language_server = fake_language_servers.next().await.unwrap();
5250 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5251 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5252 lsp::Location::new(
5253 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5254 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5255 ),
5256 )))
5257 });
5258
5259 let definitions;
5260 let buffer_b2;
5261 if rng.gen() {
5262 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5263 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5264 } else {
5265 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5266 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5267 }
5268
5269 let buffer_b2 = buffer_b2.await.unwrap();
5270 let definitions = definitions.await.unwrap();
5271 assert_eq!(definitions.len(), 1);
5272 assert_eq!(definitions[0].target.buffer, buffer_b2);
5273}
5274
5275#[gpui::test(iterations = 10)]
5276async fn test_contacts(
5277 executor: BackgroundExecutor,
5278 cx_a: &mut TestAppContext,
5279 cx_b: &mut TestAppContext,
5280 cx_c: &mut TestAppContext,
5281 cx_d: &mut TestAppContext,
5282) {
5283 let mut server = TestServer::start(executor.clone()).await;
5284 let client_a = server.create_client(cx_a, "user_a").await;
5285 let client_b = server.create_client(cx_b, "user_b").await;
5286 let client_c = server.create_client(cx_c, "user_c").await;
5287 let client_d = server.create_client(cx_d, "user_d").await;
5288 server
5289 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5290 .await;
5291 let active_call_a = cx_a.read(ActiveCall::global);
5292 let active_call_b = cx_b.read(ActiveCall::global);
5293 let active_call_c = cx_c.read(ActiveCall::global);
5294 let _active_call_d = cx_d.read(ActiveCall::global);
5295
5296 executor.run_until_parked();
5297 assert_eq!(
5298 contacts(&client_a, cx_a),
5299 [
5300 ("user_b".to_string(), "online", "free"),
5301 ("user_c".to_string(), "online", "free")
5302 ]
5303 );
5304 assert_eq!(
5305 contacts(&client_b, cx_b),
5306 [
5307 ("user_a".to_string(), "online", "free"),
5308 ("user_c".to_string(), "online", "free")
5309 ]
5310 );
5311 assert_eq!(
5312 contacts(&client_c, cx_c),
5313 [
5314 ("user_a".to_string(), "online", "free"),
5315 ("user_b".to_string(), "online", "free")
5316 ]
5317 );
5318 assert_eq!(contacts(&client_d, cx_d), []);
5319
5320 server.disconnect_client(client_c.peer_id().unwrap());
5321 server.forbid_connections();
5322 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5323 assert_eq!(
5324 contacts(&client_a, cx_a),
5325 [
5326 ("user_b".to_string(), "online", "free"),
5327 ("user_c".to_string(), "offline", "free")
5328 ]
5329 );
5330 assert_eq!(
5331 contacts(&client_b, cx_b),
5332 [
5333 ("user_a".to_string(), "online", "free"),
5334 ("user_c".to_string(), "offline", "free")
5335 ]
5336 );
5337 assert_eq!(contacts(&client_c, cx_c), []);
5338 assert_eq!(contacts(&client_d, cx_d), []);
5339
5340 server.allow_connections();
5341 client_c
5342 .authenticate_and_connect(false, &cx_c.to_async())
5343 .await
5344 .unwrap();
5345
5346 executor.run_until_parked();
5347 assert_eq!(
5348 contacts(&client_a, cx_a),
5349 [
5350 ("user_b".to_string(), "online", "free"),
5351 ("user_c".to_string(), "online", "free")
5352 ]
5353 );
5354 assert_eq!(
5355 contacts(&client_b, cx_b),
5356 [
5357 ("user_a".to_string(), "online", "free"),
5358 ("user_c".to_string(), "online", "free")
5359 ]
5360 );
5361 assert_eq!(
5362 contacts(&client_c, cx_c),
5363 [
5364 ("user_a".to_string(), "online", "free"),
5365 ("user_b".to_string(), "online", "free")
5366 ]
5367 );
5368 assert_eq!(contacts(&client_d, cx_d), []);
5369
5370 active_call_a
5371 .update(cx_a, |call, cx| {
5372 call.invite(client_b.user_id().unwrap(), None, cx)
5373 })
5374 .await
5375 .unwrap();
5376 executor.run_until_parked();
5377 assert_eq!(
5378 contacts(&client_a, cx_a),
5379 [
5380 ("user_b".to_string(), "online", "busy"),
5381 ("user_c".to_string(), "online", "free")
5382 ]
5383 );
5384 assert_eq!(
5385 contacts(&client_b, cx_b),
5386 [
5387 ("user_a".to_string(), "online", "busy"),
5388 ("user_c".to_string(), "online", "free")
5389 ]
5390 );
5391 assert_eq!(
5392 contacts(&client_c, cx_c),
5393 [
5394 ("user_a".to_string(), "online", "busy"),
5395 ("user_b".to_string(), "online", "busy")
5396 ]
5397 );
5398 assert_eq!(contacts(&client_d, cx_d), []);
5399
5400 // Client B and client D become contacts while client B is being called.
5401 server
5402 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5403 .await;
5404 executor.run_until_parked();
5405 assert_eq!(
5406 contacts(&client_a, cx_a),
5407 [
5408 ("user_b".to_string(), "online", "busy"),
5409 ("user_c".to_string(), "online", "free")
5410 ]
5411 );
5412 assert_eq!(
5413 contacts(&client_b, cx_b),
5414 [
5415 ("user_a".to_string(), "online", "busy"),
5416 ("user_c".to_string(), "online", "free"),
5417 ("user_d".to_string(), "online", "free"),
5418 ]
5419 );
5420 assert_eq!(
5421 contacts(&client_c, cx_c),
5422 [
5423 ("user_a".to_string(), "online", "busy"),
5424 ("user_b".to_string(), "online", "busy")
5425 ]
5426 );
5427 assert_eq!(
5428 contacts(&client_d, cx_d),
5429 [("user_b".to_string(), "online", "busy")]
5430 );
5431
5432 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5433 executor.run_until_parked();
5434 assert_eq!(
5435 contacts(&client_a, cx_a),
5436 [
5437 ("user_b".to_string(), "online", "free"),
5438 ("user_c".to_string(), "online", "free")
5439 ]
5440 );
5441 assert_eq!(
5442 contacts(&client_b, cx_b),
5443 [
5444 ("user_a".to_string(), "online", "free"),
5445 ("user_c".to_string(), "online", "free"),
5446 ("user_d".to_string(), "online", "free")
5447 ]
5448 );
5449 assert_eq!(
5450 contacts(&client_c, cx_c),
5451 [
5452 ("user_a".to_string(), "online", "free"),
5453 ("user_b".to_string(), "online", "free")
5454 ]
5455 );
5456 assert_eq!(
5457 contacts(&client_d, cx_d),
5458 [("user_b".to_string(), "online", "free")]
5459 );
5460
5461 active_call_c
5462 .update(cx_c, |call, cx| {
5463 call.invite(client_a.user_id().unwrap(), None, cx)
5464 })
5465 .await
5466 .unwrap();
5467 executor.run_until_parked();
5468 assert_eq!(
5469 contacts(&client_a, cx_a),
5470 [
5471 ("user_b".to_string(), "online", "free"),
5472 ("user_c".to_string(), "online", "busy")
5473 ]
5474 );
5475 assert_eq!(
5476 contacts(&client_b, cx_b),
5477 [
5478 ("user_a".to_string(), "online", "busy"),
5479 ("user_c".to_string(), "online", "busy"),
5480 ("user_d".to_string(), "online", "free")
5481 ]
5482 );
5483 assert_eq!(
5484 contacts(&client_c, cx_c),
5485 [
5486 ("user_a".to_string(), "online", "busy"),
5487 ("user_b".to_string(), "online", "free")
5488 ]
5489 );
5490 assert_eq!(
5491 contacts(&client_d, cx_d),
5492 [("user_b".to_string(), "online", "free")]
5493 );
5494
5495 active_call_a
5496 .update(cx_a, |call, cx| call.accept_incoming(cx))
5497 .await
5498 .unwrap();
5499 executor.run_until_parked();
5500 assert_eq!(
5501 contacts(&client_a, cx_a),
5502 [
5503 ("user_b".to_string(), "online", "free"),
5504 ("user_c".to_string(), "online", "busy")
5505 ]
5506 );
5507 assert_eq!(
5508 contacts(&client_b, cx_b),
5509 [
5510 ("user_a".to_string(), "online", "busy"),
5511 ("user_c".to_string(), "online", "busy"),
5512 ("user_d".to_string(), "online", "free")
5513 ]
5514 );
5515 assert_eq!(
5516 contacts(&client_c, cx_c),
5517 [
5518 ("user_a".to_string(), "online", "busy"),
5519 ("user_b".to_string(), "online", "free")
5520 ]
5521 );
5522 assert_eq!(
5523 contacts(&client_d, cx_d),
5524 [("user_b".to_string(), "online", "free")]
5525 );
5526
5527 active_call_a
5528 .update(cx_a, |call, cx| {
5529 call.invite(client_b.user_id().unwrap(), None, cx)
5530 })
5531 .await
5532 .unwrap();
5533 executor.run_until_parked();
5534 assert_eq!(
5535 contacts(&client_a, cx_a),
5536 [
5537 ("user_b".to_string(), "online", "busy"),
5538 ("user_c".to_string(), "online", "busy")
5539 ]
5540 );
5541 assert_eq!(
5542 contacts(&client_b, cx_b),
5543 [
5544 ("user_a".to_string(), "online", "busy"),
5545 ("user_c".to_string(), "online", "busy"),
5546 ("user_d".to_string(), "online", "free")
5547 ]
5548 );
5549 assert_eq!(
5550 contacts(&client_c, cx_c),
5551 [
5552 ("user_a".to_string(), "online", "busy"),
5553 ("user_b".to_string(), "online", "busy")
5554 ]
5555 );
5556 assert_eq!(
5557 contacts(&client_d, cx_d),
5558 [("user_b".to_string(), "online", "busy")]
5559 );
5560
5561 active_call_a
5562 .update(cx_a, |call, cx| call.hang_up(cx))
5563 .await
5564 .unwrap();
5565 executor.run_until_parked();
5566 assert_eq!(
5567 contacts(&client_a, cx_a),
5568 [
5569 ("user_b".to_string(), "online", "free"),
5570 ("user_c".to_string(), "online", "free")
5571 ]
5572 );
5573 assert_eq!(
5574 contacts(&client_b, cx_b),
5575 [
5576 ("user_a".to_string(), "online", "free"),
5577 ("user_c".to_string(), "online", "free"),
5578 ("user_d".to_string(), "online", "free")
5579 ]
5580 );
5581 assert_eq!(
5582 contacts(&client_c, cx_c),
5583 [
5584 ("user_a".to_string(), "online", "free"),
5585 ("user_b".to_string(), "online", "free")
5586 ]
5587 );
5588 assert_eq!(
5589 contacts(&client_d, cx_d),
5590 [("user_b".to_string(), "online", "free")]
5591 );
5592
5593 active_call_a
5594 .update(cx_a, |call, cx| {
5595 call.invite(client_b.user_id().unwrap(), None, cx)
5596 })
5597 .await
5598 .unwrap();
5599 executor.run_until_parked();
5600 assert_eq!(
5601 contacts(&client_a, cx_a),
5602 [
5603 ("user_b".to_string(), "online", "busy"),
5604 ("user_c".to_string(), "online", "free")
5605 ]
5606 );
5607 assert_eq!(
5608 contacts(&client_b, cx_b),
5609 [
5610 ("user_a".to_string(), "online", "busy"),
5611 ("user_c".to_string(), "online", "free"),
5612 ("user_d".to_string(), "online", "free")
5613 ]
5614 );
5615 assert_eq!(
5616 contacts(&client_c, cx_c),
5617 [
5618 ("user_a".to_string(), "online", "busy"),
5619 ("user_b".to_string(), "online", "busy")
5620 ]
5621 );
5622 assert_eq!(
5623 contacts(&client_d, cx_d),
5624 [("user_b".to_string(), "online", "busy")]
5625 );
5626
5627 server.forbid_connections();
5628 server.disconnect_client(client_a.peer_id().unwrap());
5629 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5630 assert_eq!(contacts(&client_a, cx_a), []);
5631 assert_eq!(
5632 contacts(&client_b, cx_b),
5633 [
5634 ("user_a".to_string(), "offline", "free"),
5635 ("user_c".to_string(), "online", "free"),
5636 ("user_d".to_string(), "online", "free")
5637 ]
5638 );
5639 assert_eq!(
5640 contacts(&client_c, cx_c),
5641 [
5642 ("user_a".to_string(), "offline", "free"),
5643 ("user_b".to_string(), "online", "free")
5644 ]
5645 );
5646 assert_eq!(
5647 contacts(&client_d, cx_d),
5648 [("user_b".to_string(), "online", "free")]
5649 );
5650
5651 // Test removing a contact
5652 client_b
5653 .user_store()
5654 .update(cx_b, |store, cx| {
5655 store.remove_contact(client_c.user_id().unwrap(), cx)
5656 })
5657 .await
5658 .unwrap();
5659 executor.run_until_parked();
5660 assert_eq!(
5661 contacts(&client_b, cx_b),
5662 [
5663 ("user_a".to_string(), "offline", "free"),
5664 ("user_d".to_string(), "online", "free")
5665 ]
5666 );
5667 assert_eq!(
5668 contacts(&client_c, cx_c),
5669 [("user_a".to_string(), "offline", "free"),]
5670 );
5671
5672 fn contacts(
5673 client: &TestClient,
5674 cx: &TestAppContext,
5675 ) -> Vec<(String, &'static str, &'static str)> {
5676 client.user_store().read_with(cx, |store, _| {
5677 store
5678 .contacts()
5679 .iter()
5680 .map(|contact| {
5681 (
5682 contact.user.github_login.clone(),
5683 if contact.online { "online" } else { "offline" },
5684 if contact.busy { "busy" } else { "free" },
5685 )
5686 })
5687 .collect()
5688 })
5689 }
5690}
5691
5692#[gpui::test(iterations = 10)]
5693async fn test_contact_requests(
5694 executor: BackgroundExecutor,
5695 cx_a: &mut TestAppContext,
5696 cx_a2: &mut TestAppContext,
5697 cx_b: &mut TestAppContext,
5698 cx_b2: &mut TestAppContext,
5699 cx_c: &mut TestAppContext,
5700 cx_c2: &mut TestAppContext,
5701) {
5702 // Connect to a server as 3 clients.
5703 let mut server = TestServer::start(executor.clone()).await;
5704 let client_a = server.create_client(cx_a, "user_a").await;
5705 let client_a2 = server.create_client(cx_a2, "user_a").await;
5706 let client_b = server.create_client(cx_b, "user_b").await;
5707 let client_b2 = server.create_client(cx_b2, "user_b").await;
5708 let client_c = server.create_client(cx_c, "user_c").await;
5709 let client_c2 = server.create_client(cx_c2, "user_c").await;
5710
5711 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5712 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5713 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5714
5715 // User A and User C request that user B become their contact.
5716 client_a
5717 .user_store()
5718 .update(cx_a, |store, cx| {
5719 store.request_contact(client_b.user_id().unwrap(), cx)
5720 })
5721 .await
5722 .unwrap();
5723 client_c
5724 .user_store()
5725 .update(cx_c, |store, cx| {
5726 store.request_contact(client_b.user_id().unwrap(), cx)
5727 })
5728 .await
5729 .unwrap();
5730 executor.run_until_parked();
5731
5732 // All users see the pending request appear in all their clients.
5733 assert_eq!(
5734 client_a.summarize_contacts(cx_a).outgoing_requests,
5735 &["user_b"]
5736 );
5737 assert_eq!(
5738 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5739 &["user_b"]
5740 );
5741 assert_eq!(
5742 client_b.summarize_contacts(cx_b).incoming_requests,
5743 &["user_a", "user_c"]
5744 );
5745 assert_eq!(
5746 client_b2.summarize_contacts(cx_b2).incoming_requests,
5747 &["user_a", "user_c"]
5748 );
5749 assert_eq!(
5750 client_c.summarize_contacts(cx_c).outgoing_requests,
5751 &["user_b"]
5752 );
5753 assert_eq!(
5754 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5755 &["user_b"]
5756 );
5757
5758 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5759 disconnect_and_reconnect(&client_a, cx_a).await;
5760 disconnect_and_reconnect(&client_b, cx_b).await;
5761 disconnect_and_reconnect(&client_c, cx_c).await;
5762 executor.run_until_parked();
5763 assert_eq!(
5764 client_a.summarize_contacts(cx_a).outgoing_requests,
5765 &["user_b"]
5766 );
5767 assert_eq!(
5768 client_b.summarize_contacts(cx_b).incoming_requests,
5769 &["user_a", "user_c"]
5770 );
5771 assert_eq!(
5772 client_c.summarize_contacts(cx_c).outgoing_requests,
5773 &["user_b"]
5774 );
5775
5776 // User B accepts the request from user A.
5777 client_b
5778 .user_store()
5779 .update(cx_b, |store, cx| {
5780 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5781 })
5782 .await
5783 .unwrap();
5784
5785 executor.run_until_parked();
5786
5787 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5788 let contacts_b = client_b.summarize_contacts(cx_b);
5789 assert_eq!(contacts_b.current, &["user_a"]);
5790 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5791 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5792 assert_eq!(contacts_b2.current, &["user_a"]);
5793 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5794
5795 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5796 let contacts_a = client_a.summarize_contacts(cx_a);
5797 assert_eq!(contacts_a.current, &["user_b"]);
5798 assert!(contacts_a.outgoing_requests.is_empty());
5799 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5800 assert_eq!(contacts_a2.current, &["user_b"]);
5801 assert!(contacts_a2.outgoing_requests.is_empty());
5802
5803 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5804 disconnect_and_reconnect(&client_a, cx_a).await;
5805 disconnect_and_reconnect(&client_b, cx_b).await;
5806 disconnect_and_reconnect(&client_c, cx_c).await;
5807 executor.run_until_parked();
5808 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5809 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5810 assert_eq!(
5811 client_b.summarize_contacts(cx_b).incoming_requests,
5812 &["user_c"]
5813 );
5814 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5815 assert_eq!(
5816 client_c.summarize_contacts(cx_c).outgoing_requests,
5817 &["user_b"]
5818 );
5819
5820 // User B rejects the request from user C.
5821 client_b
5822 .user_store()
5823 .update(cx_b, |store, cx| {
5824 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5825 })
5826 .await
5827 .unwrap();
5828
5829 executor.run_until_parked();
5830
5831 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5832 let contacts_b = client_b.summarize_contacts(cx_b);
5833 assert_eq!(contacts_b.current, &["user_a"]);
5834 assert!(contacts_b.incoming_requests.is_empty());
5835 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5836 assert_eq!(contacts_b2.current, &["user_a"]);
5837 assert!(contacts_b2.incoming_requests.is_empty());
5838
5839 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5840 let contacts_c = client_c.summarize_contacts(cx_c);
5841 assert!(contacts_c.current.is_empty());
5842 assert!(contacts_c.outgoing_requests.is_empty());
5843 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5844 assert!(contacts_c2.current.is_empty());
5845 assert!(contacts_c2.outgoing_requests.is_empty());
5846
5847 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5848 disconnect_and_reconnect(&client_a, cx_a).await;
5849 disconnect_and_reconnect(&client_b, cx_b).await;
5850 disconnect_and_reconnect(&client_c, cx_c).await;
5851 executor.run_until_parked();
5852 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5853 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5854 assert!(client_b
5855 .summarize_contacts(cx_b)
5856 .incoming_requests
5857 .is_empty());
5858 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5859 assert!(client_c
5860 .summarize_contacts(cx_c)
5861 .outgoing_requests
5862 .is_empty());
5863
5864 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
5865 client.disconnect(&cx.to_async());
5866 client.clear_contacts(cx).await;
5867 client
5868 .authenticate_and_connect(false, &cx.to_async())
5869 .await
5870 .unwrap();
5871 }
5872}
5873
5874#[gpui::test(iterations = 10)]
5875async fn test_join_call_after_screen_was_shared(
5876 executor: BackgroundExecutor,
5877 cx_a: &mut TestAppContext,
5878 cx_b: &mut TestAppContext,
5879) {
5880 let mut server = TestServer::start(executor.clone()).await;
5881
5882 let client_a = server.create_client(cx_a, "user_a").await;
5883 let client_b = server.create_client(cx_b, "user_b").await;
5884 server
5885 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5886 .await;
5887
5888 let active_call_a = cx_a.read(ActiveCall::global);
5889 let active_call_b = cx_b.read(ActiveCall::global);
5890
5891 // Call users B and C from client A.
5892 active_call_a
5893 .update(cx_a, |call, cx| {
5894 call.invite(client_b.user_id().unwrap(), None, cx)
5895 })
5896 .await
5897 .unwrap();
5898
5899 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
5900 executor.run_until_parked();
5901 assert_eq!(
5902 room_participants(&room_a, cx_a),
5903 RoomParticipants {
5904 remote: Default::default(),
5905 pending: vec!["user_b".to_string()]
5906 }
5907 );
5908
5909 // User B receives the call.
5910
5911 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
5912 let call_b = incoming_call_b.next().await.unwrap().unwrap();
5913 assert_eq!(call_b.calling_user.github_login, "user_a");
5914
5915 // User A shares their screen
5916 let display = MacOSDisplay::new();
5917 active_call_a
5918 .update(cx_a, |call, cx| {
5919 call.room().unwrap().update(cx, |room, cx| {
5920 room.set_display_sources(vec![display.clone()]);
5921 room.share_screen(cx)
5922 })
5923 })
5924 .await
5925 .unwrap();
5926
5927 client_b.user_store().update(cx_b, |user_store, _| {
5928 user_store.clear_cache();
5929 });
5930
5931 // User B joins the room
5932 active_call_b
5933 .update(cx_b, |call, cx| call.accept_incoming(cx))
5934 .await
5935 .unwrap();
5936
5937 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
5938 assert!(incoming_call_b.next().await.unwrap().is_none());
5939
5940 executor.run_until_parked();
5941 assert_eq!(
5942 room_participants(&room_a, cx_a),
5943 RoomParticipants {
5944 remote: vec!["user_b".to_string()],
5945 pending: vec![],
5946 }
5947 );
5948 assert_eq!(
5949 room_participants(&room_b, cx_b),
5950 RoomParticipants {
5951 remote: vec!["user_a".to_string()],
5952 pending: vec![],
5953 }
5954 );
5955
5956 // Ensure User B sees User A's screenshare.
5957
5958 room_b.read_with(cx_b, |room, _| {
5959 assert_eq!(
5960 room.remote_participants()
5961 .get(&client_a.user_id().unwrap())
5962 .unwrap()
5963 .video_tracks
5964 .len(),
5965 1
5966 );
5967 });
5968}
5969
5970#[gpui::test]
5971async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
5972 let mut server = TestServer::start(cx.executor().clone()).await;
5973 let client_a = server.create_client(cx, "user_a").await;
5974 let (_workspace_a, cx) = client_a.build_test_workspace(cx).await;
5975
5976 cx.simulate_resize(size(px(300.), px(300.)));
5977
5978 cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
5979 cx.update(|cx| cx.refresh());
5980
5981 let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
5982 let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
5983
5984 assert!(
5985 tab_bounds.intersects(&new_tab_button_bounds),
5986 "Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
5987 );
5988
5989 cx.simulate_event(MouseDownEvent {
5990 button: MouseButton::Right,
5991 position: new_tab_button_bounds.center(),
5992 modifiers: Modifiers::default(),
5993 click_count: 1,
5994 first_mouse: false,
5995 });
5996
5997 // regression test that the right click menu for tabs does not open.
5998 assert!(cx.debug_bounds("MENU_ITEM-Close").is_none());
5999
6000 let tab_bounds = cx.debug_bounds("TAB-1").unwrap();
6001 cx.simulate_event(MouseDownEvent {
6002 button: MouseButton::Right,
6003 position: tab_bounds.center(),
6004 modifiers: Modifiers::default(),
6005 click_count: 1,
6006 first_mouse: false,
6007 });
6008 assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
6009}
6010
6011#[gpui::test]
6012async fn test_cmd_k_left(cx: &mut TestAppContext) {
6013 let (_, client) = TestServer::start1(cx).await;
6014 let (workspace, cx) = client.build_test_workspace(cx).await;
6015
6016 cx.simulate_keystrokes("cmd-n");
6017 workspace.update(cx, |workspace, cx| {
6018 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
6019 });
6020 cx.simulate_keystrokes("cmd-k left");
6021 workspace.update(cx, |workspace, cx| {
6022 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6023 });
6024 cx.simulate_keystrokes("cmd-k");
6025 // sleep for longer than the timeout in keyboard shortcut handling
6026 // to verify that it doesn't fire in this case.
6027 cx.executor().advance_clock(Duration::from_secs(2));
6028 cx.simulate_keystrokes("left");
6029 workspace.update(cx, |workspace, cx| {
6030 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6031 });
6032}
6033
6034#[gpui::test]
6035async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppContext) {
6036 let (mut server, client) = TestServer::start1(cx1).await;
6037 let channel1 = server.make_public_channel("channel1", &client, cx1).await;
6038 let channel2 = server.make_public_channel("channel2", &client, cx1).await;
6039
6040 join_channel(channel1, &client, cx1).await.unwrap();
6041 drop(client);
6042
6043 let client2 = server.create_client(cx2, "user_a").await;
6044 join_channel(channel2, &client2, cx2).await.unwrap();
6045}