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