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