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