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