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 prompt_store::PromptBuilder;
18
19use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
20use gpui::{
21 px, size, App, BackgroundExecutor, Entity, Modifiers, MouseButton, MouseDownEvent,
22 TestAppContext, UpdateGlobal,
23};
24use language::{
25 language_settings::{
26 AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
27 },
28 tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
29 Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
30};
31use lsp::LanguageServerId;
32use parking_lot::Mutex;
33use project::{
34 lsp_store::{FormatTrigger, LspFormatTarget},
35 search::{SearchQuery, SearchResult},
36 DiagnosticSummary, HoverBlockKind, Project, ProjectPath,
37};
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(|_| future::pending()));
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(|_| future::pending()));
1160 client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
1161 client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
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 buffer_remote_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(buffer_remote_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 = buffer_remote_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(buffer_remote_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 = buffer_remote_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 remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
2707 let buffer = buffer_remote_a.read(cx);
2708 assert_eq!(
2709 diff.base_text_string().as_deref(),
2710 Some(new_staged_text.as_str())
2711 );
2712 assert_hunks(
2713 diff.hunks_in_row_range(0..4, buffer, cx),
2714 buffer,
2715 &diff.base_text_string().unwrap(),
2716 &[(2..3, "", "three\n", DiffHunkStatus::added_none())],
2717 );
2718 });
2719
2720 remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
2721 let buffer = buffer_remote_a.read(cx);
2722 assert_eq!(
2723 diff.base_text_string().as_deref(),
2724 Some(new_committed_text.as_str())
2725 );
2726 assert_hunks(
2727 diff.hunks_in_row_range(0..4, buffer, cx),
2728 buffer,
2729 &diff.base_text_string().unwrap(),
2730 &[(
2731 1..2,
2732 "TWO_HUNDRED\n",
2733 "two\n",
2734 DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
2735 )],
2736 );
2737 });
2738
2739 // Nested git dir
2740 let staged_text = "
2741 one
2742 three
2743 "
2744 .unindent();
2745
2746 let new_staged_text = "
2747 one
2748 two
2749 "
2750 .unindent();
2751
2752 client_a.fs().set_index_for_repo(
2753 Path::new("/dir/sub/.git"),
2754 &[("b.txt".into(), staged_text.clone())],
2755 );
2756
2757 // Create the buffer
2758 let buffer_local_b = project_local
2759 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2760 .await
2761 .unwrap();
2762 let local_unstaged_diff_b = project_local
2763 .update(cx_a, |p, cx| {
2764 p.open_unstaged_diff(buffer_local_b.clone(), cx)
2765 })
2766 .await
2767 .unwrap();
2768
2769 // Wait for it to catch up to the new diff
2770 executor.run_until_parked();
2771 local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
2772 let buffer = buffer_local_b.read(cx);
2773 assert_eq!(
2774 diff.base_text_string().as_deref(),
2775 Some(staged_text.as_str())
2776 );
2777 assert_hunks(
2778 diff.hunks_in_row_range(0..4, buffer, cx),
2779 buffer,
2780 &diff.base_text_string().unwrap(),
2781 &[(1..2, "", "two\n", DiffHunkStatus::added_none())],
2782 );
2783 });
2784
2785 // Create remote buffer
2786 let buffer_remote_b = project_remote
2787 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2788 .await
2789 .unwrap();
2790 let remote_unstaged_diff_b = project_remote
2791 .update(cx_b, |p, cx| {
2792 p.open_unstaged_diff(buffer_remote_b.clone(), cx)
2793 })
2794 .await
2795 .unwrap();
2796
2797 executor.run_until_parked();
2798 remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
2799 let buffer = buffer_remote_b.read(cx);
2800 assert_eq!(
2801 diff.base_text_string().as_deref(),
2802 Some(staged_text.as_str())
2803 );
2804 assert_hunks(
2805 diff.hunks_in_row_range(0..4, buffer, cx),
2806 buffer,
2807 &staged_text,
2808 &[(1..2, "", "two\n", DiffHunkStatus::added_none())],
2809 );
2810 });
2811
2812 // Updatet the staged text
2813 client_a.fs().set_index_for_repo(
2814 Path::new("/dir/sub/.git"),
2815 &[("b.txt".into(), new_staged_text.clone())],
2816 );
2817
2818 // Wait for buffer_local_b to receive it
2819 executor.run_until_parked();
2820 local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
2821 let buffer = buffer_local_b.read(cx);
2822 assert_eq!(
2823 diff.base_text_string().as_deref(),
2824 Some(new_staged_text.as_str())
2825 );
2826 assert_hunks(
2827 diff.hunks_in_row_range(0..4, buffer, cx),
2828 buffer,
2829 &new_staged_text,
2830 &[(2..3, "", "three\n", DiffHunkStatus::added_none())],
2831 );
2832 });
2833
2834 remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
2835 let buffer = buffer_remote_b.read(cx);
2836 assert_eq!(
2837 diff.base_text_string().as_deref(),
2838 Some(new_staged_text.as_str())
2839 );
2840 assert_hunks(
2841 diff.hunks_in_row_range(0..4, buffer, cx),
2842 buffer,
2843 &new_staged_text,
2844 &[(2..3, "", "three\n", DiffHunkStatus::added_none())],
2845 );
2846 });
2847}
2848
2849#[gpui::test]
2850async fn test_git_branch_name(
2851 executor: BackgroundExecutor,
2852 cx_a: &mut TestAppContext,
2853 cx_b: &mut TestAppContext,
2854 cx_c: &mut TestAppContext,
2855) {
2856 let mut server = TestServer::start(executor.clone()).await;
2857 let client_a = server.create_client(cx_a, "user_a").await;
2858 let client_b = server.create_client(cx_b, "user_b").await;
2859 let client_c = server.create_client(cx_c, "user_c").await;
2860 server
2861 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2862 .await;
2863 let active_call_a = cx_a.read(ActiveCall::global);
2864
2865 client_a
2866 .fs()
2867 .insert_tree(
2868 "/dir",
2869 json!({
2870 ".git": {},
2871 }),
2872 )
2873 .await;
2874
2875 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2876 let project_id = active_call_a
2877 .update(cx_a, |call, cx| {
2878 call.share_project(project_local.clone(), cx)
2879 })
2880 .await
2881 .unwrap();
2882
2883 let project_remote = client_b.join_remote_project(project_id, cx_b).await;
2884 client_a
2885 .fs()
2886 .set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
2887
2888 // Wait for it to catch up to the new branch
2889 executor.run_until_parked();
2890
2891 #[track_caller]
2892 fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &App) {
2893 let branch_name = branch_name.map(Into::into);
2894 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2895 assert_eq!(worktrees.len(), 1);
2896 let worktree = worktrees[0].clone();
2897 let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap();
2898 assert_eq!(
2899 root_entry.branch().map(|branch| branch.name.to_string()),
2900 branch_name
2901 );
2902 }
2903
2904 // Smoke test branch reading
2905
2906 project_local.read_with(cx_a, |project, cx| {
2907 assert_branch(Some("branch-1"), project, cx)
2908 });
2909
2910 project_remote.read_with(cx_b, |project, cx| {
2911 assert_branch(Some("branch-1"), project, cx)
2912 });
2913
2914 client_a
2915 .fs()
2916 .set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
2917
2918 // Wait for buffer_local_a to receive it
2919 executor.run_until_parked();
2920
2921 // Smoke test branch reading
2922
2923 project_local.read_with(cx_a, |project, cx| {
2924 assert_branch(Some("branch-2"), project, cx)
2925 });
2926
2927 project_remote.read_with(cx_b, |project, cx| {
2928 assert_branch(Some("branch-2"), project, cx)
2929 });
2930
2931 let project_remote_c = client_c.join_remote_project(project_id, cx_c).await;
2932 executor.run_until_parked();
2933
2934 project_remote_c.read_with(cx_c, |project, cx| {
2935 assert_branch(Some("branch-2"), project, cx)
2936 });
2937}
2938
2939#[gpui::test]
2940async fn test_git_status_sync(
2941 executor: BackgroundExecutor,
2942 cx_a: &mut TestAppContext,
2943 cx_b: &mut TestAppContext,
2944 cx_c: &mut TestAppContext,
2945) {
2946 let mut server = TestServer::start(executor.clone()).await;
2947 let client_a = server.create_client(cx_a, "user_a").await;
2948 let client_b = server.create_client(cx_b, "user_b").await;
2949 let client_c = server.create_client(cx_c, "user_c").await;
2950 server
2951 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2952 .await;
2953 let active_call_a = cx_a.read(ActiveCall::global);
2954
2955 client_a
2956 .fs()
2957 .insert_tree(
2958 "/dir",
2959 json!({
2960 ".git": {},
2961 "a.txt": "a",
2962 "b.txt": "b",
2963 }),
2964 )
2965 .await;
2966
2967 const A_TXT: &str = "a.txt";
2968 const B_TXT: &str = "b.txt";
2969
2970 const A_STATUS_START: FileStatus = FileStatus::Tracked(TrackedStatus {
2971 index_status: StatusCode::Added,
2972 worktree_status: StatusCode::Modified,
2973 });
2974 const B_STATUS_START: FileStatus = FileStatus::Unmerged(UnmergedStatus {
2975 first_head: UnmergedStatusCode::Updated,
2976 second_head: UnmergedStatusCode::Deleted,
2977 });
2978
2979 client_a.fs().set_status_for_repo_via_git_operation(
2980 Path::new("/dir/.git"),
2981 &[
2982 (Path::new(A_TXT), A_STATUS_START),
2983 (Path::new(B_TXT), B_STATUS_START),
2984 ],
2985 );
2986
2987 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2988 let project_id = active_call_a
2989 .update(cx_a, |call, cx| {
2990 call.share_project(project_local.clone(), cx)
2991 })
2992 .await
2993 .unwrap();
2994
2995 let project_remote = client_b.join_remote_project(project_id, cx_b).await;
2996
2997 // Wait for it to catch up to the new status
2998 executor.run_until_parked();
2999
3000 #[track_caller]
3001 fn assert_status(
3002 file: &impl AsRef<Path>,
3003 status: Option<FileStatus>,
3004 project: &Project,
3005 cx: &App,
3006 ) {
3007 let file = file.as_ref();
3008 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
3009 assert_eq!(worktrees.len(), 1);
3010 let worktree = worktrees[0].clone();
3011 let snapshot = worktree.read(cx).snapshot();
3012 assert_eq!(snapshot.status_for_file(file), status);
3013 }
3014
3015 project_local.read_with(cx_a, |project, cx| {
3016 assert_status(&Path::new(A_TXT), Some(A_STATUS_START), project, cx);
3017 assert_status(&Path::new(B_TXT), Some(B_STATUS_START), project, cx);
3018 });
3019
3020 project_remote.read_with(cx_b, |project, cx| {
3021 assert_status(&Path::new(A_TXT), Some(A_STATUS_START), project, cx);
3022 assert_status(&Path::new(B_TXT), Some(B_STATUS_START), project, cx);
3023 });
3024
3025 const A_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus {
3026 index_status: StatusCode::Added,
3027 worktree_status: StatusCode::Unmodified,
3028 });
3029 const B_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus {
3030 index_status: StatusCode::Deleted,
3031 worktree_status: StatusCode::Unmodified,
3032 });
3033
3034 client_a.fs().set_status_for_repo_via_working_copy_change(
3035 Path::new("/dir/.git"),
3036 &[
3037 (Path::new(A_TXT), A_STATUS_END),
3038 (Path::new(B_TXT), B_STATUS_END),
3039 ],
3040 );
3041
3042 // Wait for buffer_local_a to receive it
3043 executor.run_until_parked();
3044
3045 // Smoke test status reading
3046
3047 project_local.read_with(cx_a, |project, cx| {
3048 assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
3049 assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
3050 });
3051
3052 project_remote.read_with(cx_b, |project, cx| {
3053 assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
3054 assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
3055 });
3056
3057 // And synchronization while joining
3058 let project_remote_c = client_c.join_remote_project(project_id, cx_c).await;
3059 executor.run_until_parked();
3060
3061 project_remote_c.read_with(cx_c, |project, cx| {
3062 assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
3063 assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
3064 });
3065}
3066
3067#[gpui::test(iterations = 10)]
3068async fn test_fs_operations(
3069 executor: BackgroundExecutor,
3070 cx_a: &mut TestAppContext,
3071 cx_b: &mut TestAppContext,
3072) {
3073 let mut server = TestServer::start(executor.clone()).await;
3074 let client_a = server.create_client(cx_a, "user_a").await;
3075 let client_b = server.create_client(cx_b, "user_b").await;
3076 server
3077 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3078 .await;
3079 let active_call_a = cx_a.read(ActiveCall::global);
3080
3081 client_a
3082 .fs()
3083 .insert_tree(
3084 "/dir",
3085 json!({
3086 "a.txt": "a-contents",
3087 "b.txt": "b-contents",
3088 }),
3089 )
3090 .await;
3091 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3092 let project_id = active_call_a
3093 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3094 .await
3095 .unwrap();
3096 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3097
3098 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
3099 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3100
3101 let entry = project_b
3102 .update(cx_b, |project, cx| {
3103 project.create_entry((worktree_id, "c.txt"), false, cx)
3104 })
3105 .await
3106 .unwrap()
3107 .to_included()
3108 .unwrap();
3109
3110 worktree_a.read_with(cx_a, |worktree, _| {
3111 assert_eq!(
3112 worktree
3113 .paths()
3114 .map(|p| p.to_string_lossy())
3115 .collect::<Vec<_>>(),
3116 ["a.txt", "b.txt", "c.txt"]
3117 );
3118 });
3119
3120 worktree_b.read_with(cx_b, |worktree, _| {
3121 assert_eq!(
3122 worktree
3123 .paths()
3124 .map(|p| p.to_string_lossy())
3125 .collect::<Vec<_>>(),
3126 ["a.txt", "b.txt", "c.txt"]
3127 );
3128 });
3129
3130 project_b
3131 .update(cx_b, |project, cx| {
3132 project.rename_entry(entry.id, Path::new("d.txt"), cx)
3133 })
3134 .await
3135 .unwrap()
3136 .to_included()
3137 .unwrap();
3138
3139 worktree_a.read_with(cx_a, |worktree, _| {
3140 assert_eq!(
3141 worktree
3142 .paths()
3143 .map(|p| p.to_string_lossy())
3144 .collect::<Vec<_>>(),
3145 ["a.txt", "b.txt", "d.txt"]
3146 );
3147 });
3148
3149 worktree_b.read_with(cx_b, |worktree, _| {
3150 assert_eq!(
3151 worktree
3152 .paths()
3153 .map(|p| p.to_string_lossy())
3154 .collect::<Vec<_>>(),
3155 ["a.txt", "b.txt", "d.txt"]
3156 );
3157 });
3158
3159 let dir_entry = project_b
3160 .update(cx_b, |project, cx| {
3161 project.create_entry((worktree_id, "DIR"), true, cx)
3162 })
3163 .await
3164 .unwrap()
3165 .to_included()
3166 .unwrap();
3167
3168 worktree_a.read_with(cx_a, |worktree, _| {
3169 assert_eq!(
3170 worktree
3171 .paths()
3172 .map(|p| p.to_string_lossy())
3173 .collect::<Vec<_>>(),
3174 ["DIR", "a.txt", "b.txt", "d.txt"]
3175 );
3176 });
3177
3178 worktree_b.read_with(cx_b, |worktree, _| {
3179 assert_eq!(
3180 worktree
3181 .paths()
3182 .map(|p| p.to_string_lossy())
3183 .collect::<Vec<_>>(),
3184 ["DIR", "a.txt", "b.txt", "d.txt"]
3185 );
3186 });
3187
3188 project_b
3189 .update(cx_b, |project, cx| {
3190 project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
3191 })
3192 .await
3193 .unwrap()
3194 .to_included()
3195 .unwrap();
3196
3197 project_b
3198 .update(cx_b, |project, cx| {
3199 project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
3200 })
3201 .await
3202 .unwrap()
3203 .to_included()
3204 .unwrap();
3205
3206 project_b
3207 .update(cx_b, |project, cx| {
3208 project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
3209 })
3210 .await
3211 .unwrap()
3212 .to_included()
3213 .unwrap();
3214
3215 worktree_a.read_with(cx_a, |worktree, _| {
3216 assert_eq!(
3217 worktree
3218 .paths()
3219 .map(|p| p.to_string_lossy())
3220 .collect::<Vec<_>>(),
3221 [
3222 "DIR",
3223 "DIR/SUBDIR",
3224 "DIR/SUBDIR/f.txt",
3225 "DIR/e.txt",
3226 "a.txt",
3227 "b.txt",
3228 "d.txt"
3229 ]
3230 );
3231 });
3232
3233 worktree_b.read_with(cx_b, |worktree, _| {
3234 assert_eq!(
3235 worktree
3236 .paths()
3237 .map(|p| p.to_string_lossy())
3238 .collect::<Vec<_>>(),
3239 [
3240 "DIR",
3241 "DIR/SUBDIR",
3242 "DIR/SUBDIR/f.txt",
3243 "DIR/e.txt",
3244 "a.txt",
3245 "b.txt",
3246 "d.txt"
3247 ]
3248 );
3249 });
3250
3251 project_b
3252 .update(cx_b, |project, cx| {
3253 project.copy_entry(entry.id, None, Path::new("f.txt"), cx)
3254 })
3255 .await
3256 .unwrap()
3257 .unwrap();
3258
3259 worktree_a.read_with(cx_a, |worktree, _| {
3260 assert_eq!(
3261 worktree
3262 .paths()
3263 .map(|p| p.to_string_lossy())
3264 .collect::<Vec<_>>(),
3265 [
3266 "DIR",
3267 "DIR/SUBDIR",
3268 "DIR/SUBDIR/f.txt",
3269 "DIR/e.txt",
3270 "a.txt",
3271 "b.txt",
3272 "d.txt",
3273 "f.txt"
3274 ]
3275 );
3276 });
3277
3278 worktree_b.read_with(cx_b, |worktree, _| {
3279 assert_eq!(
3280 worktree
3281 .paths()
3282 .map(|p| p.to_string_lossy())
3283 .collect::<Vec<_>>(),
3284 [
3285 "DIR",
3286 "DIR/SUBDIR",
3287 "DIR/SUBDIR/f.txt",
3288 "DIR/e.txt",
3289 "a.txt",
3290 "b.txt",
3291 "d.txt",
3292 "f.txt"
3293 ]
3294 );
3295 });
3296
3297 project_b
3298 .update(cx_b, |project, cx| {
3299 project.delete_entry(dir_entry.id, false, cx).unwrap()
3300 })
3301 .await
3302 .unwrap();
3303 executor.run_until_parked();
3304
3305 worktree_a.read_with(cx_a, |worktree, _| {
3306 assert_eq!(
3307 worktree
3308 .paths()
3309 .map(|p| p.to_string_lossy())
3310 .collect::<Vec<_>>(),
3311 ["a.txt", "b.txt", "d.txt", "f.txt"]
3312 );
3313 });
3314
3315 worktree_b.read_with(cx_b, |worktree, _| {
3316 assert_eq!(
3317 worktree
3318 .paths()
3319 .map(|p| p.to_string_lossy())
3320 .collect::<Vec<_>>(),
3321 ["a.txt", "b.txt", "d.txt", "f.txt"]
3322 );
3323 });
3324
3325 project_b
3326 .update(cx_b, |project, cx| {
3327 project.delete_entry(entry.id, false, cx).unwrap()
3328 })
3329 .await
3330 .unwrap();
3331
3332 worktree_a.read_with(cx_a, |worktree, _| {
3333 assert_eq!(
3334 worktree
3335 .paths()
3336 .map(|p| p.to_string_lossy())
3337 .collect::<Vec<_>>(),
3338 ["a.txt", "b.txt", "f.txt"]
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 ["a.txt", "b.txt", "f.txt"]
3349 );
3350 });
3351}
3352
3353#[gpui::test(iterations = 10)]
3354async fn test_local_settings(
3355 executor: BackgroundExecutor,
3356 cx_a: &mut TestAppContext,
3357 cx_b: &mut TestAppContext,
3358) {
3359 let mut server = TestServer::start(executor.clone()).await;
3360 let client_a = server.create_client(cx_a, "user_a").await;
3361 let client_b = server.create_client(cx_b, "user_b").await;
3362 server
3363 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3364 .await;
3365 let active_call_a = cx_a.read(ActiveCall::global);
3366
3367 // As client A, open a project that contains some local settings files
3368 client_a
3369 .fs()
3370 .insert_tree(
3371 "/dir",
3372 json!({
3373 ".zed": {
3374 "settings.json": r#"{ "tab_size": 2 }"#
3375 },
3376 "a": {
3377 ".zed": {
3378 "settings.json": r#"{ "tab_size": 8 }"#
3379 },
3380 "a.txt": "a-contents",
3381 },
3382 "b": {
3383 "b.txt": "b-contents",
3384 }
3385 }),
3386 )
3387 .await;
3388 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
3389 executor.run_until_parked();
3390 let project_id = active_call_a
3391 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3392 .await
3393 .unwrap();
3394 executor.run_until_parked();
3395
3396 // As client B, join that project and observe the local settings.
3397 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3398
3399 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3400 executor.run_until_parked();
3401 cx_b.read(|cx| {
3402 let store = cx.global::<SettingsStore>();
3403 assert_eq!(
3404 store
3405 .local_settings(worktree_b.read(cx).id())
3406 .collect::<Vec<_>>(),
3407 &[
3408 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3409 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3410 ]
3411 )
3412 });
3413
3414 // As client A, update a settings file. As Client B, see the changed settings.
3415 client_a
3416 .fs()
3417 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3418 .await;
3419 executor.run_until_parked();
3420 cx_b.read(|cx| {
3421 let store = cx.global::<SettingsStore>();
3422 assert_eq!(
3423 store
3424 .local_settings(worktree_b.read(cx).id())
3425 .collect::<Vec<_>>(),
3426 &[
3427 (Path::new("").into(), r#"{}"#.to_string()),
3428 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3429 ]
3430 )
3431 });
3432
3433 // As client A, create and remove some settings files. As client B, see the changed settings.
3434 client_a
3435 .fs()
3436 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3437 .await
3438 .unwrap();
3439 client_a
3440 .fs()
3441 .create_dir("/dir/b/.zed".as_ref())
3442 .await
3443 .unwrap();
3444 client_a
3445 .fs()
3446 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3447 .await;
3448 executor.run_until_parked();
3449 cx_b.read(|cx| {
3450 let store = cx.global::<SettingsStore>();
3451 assert_eq!(
3452 store
3453 .local_settings(worktree_b.read(cx).id())
3454 .collect::<Vec<_>>(),
3455 &[
3456 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3457 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3458 ]
3459 )
3460 });
3461
3462 // As client B, disconnect.
3463 server.forbid_connections();
3464 server.disconnect_client(client_b.peer_id().unwrap());
3465
3466 // As client A, change and remove settings files while client B is disconnected.
3467 client_a
3468 .fs()
3469 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3470 .await;
3471 client_a
3472 .fs()
3473 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3474 .await
3475 .unwrap();
3476 executor.run_until_parked();
3477
3478 // As client B, reconnect and see the changed settings.
3479 server.allow_connections();
3480 executor.advance_clock(RECEIVE_TIMEOUT);
3481 cx_b.read(|cx| {
3482 let store = cx.global::<SettingsStore>();
3483 assert_eq!(
3484 store
3485 .local_settings(worktree_b.read(cx).id())
3486 .collect::<Vec<_>>(),
3487 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3488 )
3489 });
3490}
3491
3492#[gpui::test(iterations = 10)]
3493async fn test_buffer_conflict_after_save(
3494 executor: BackgroundExecutor,
3495 cx_a: &mut TestAppContext,
3496 cx_b: &mut TestAppContext,
3497) {
3498 let mut server = TestServer::start(executor.clone()).await;
3499 let client_a = server.create_client(cx_a, "user_a").await;
3500 let client_b = server.create_client(cx_b, "user_b").await;
3501 server
3502 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3503 .await;
3504 let active_call_a = cx_a.read(ActiveCall::global);
3505
3506 client_a
3507 .fs()
3508 .insert_tree(
3509 "/dir",
3510 json!({
3511 "a.txt": "a-contents",
3512 }),
3513 )
3514 .await;
3515 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3516 let project_id = active_call_a
3517 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3518 .await
3519 .unwrap();
3520 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3521
3522 // Open a buffer as client B
3523 let buffer_b = project_b
3524 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3525 .await
3526 .unwrap();
3527
3528 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3529
3530 buffer_b.read_with(cx_b, |buf, _| {
3531 assert!(buf.is_dirty());
3532 assert!(!buf.has_conflict());
3533 });
3534
3535 project_b
3536 .update(cx_b, |project, cx| {
3537 project.save_buffer(buffer_b.clone(), cx)
3538 })
3539 .await
3540 .unwrap();
3541
3542 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3543
3544 buffer_b.read_with(cx_b, |buf, _| {
3545 assert!(!buf.has_conflict());
3546 });
3547
3548 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3549
3550 buffer_b.read_with(cx_b, |buf, _| {
3551 assert!(buf.is_dirty());
3552 assert!(!buf.has_conflict());
3553 });
3554}
3555
3556#[gpui::test(iterations = 10)]
3557async fn test_buffer_reloading(
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\nb\nc",
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.read_with(cx_b, |buf, _| {
3593 assert!(!buf.is_dirty());
3594 assert!(!buf.has_conflict());
3595 assert_eq!(buf.line_ending(), LineEnding::Unix);
3596 });
3597
3598 let new_contents = Rope::from("d\ne\nf");
3599 client_a
3600 .fs()
3601 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3602 .await
3603 .unwrap();
3604
3605 executor.run_until_parked();
3606
3607 buffer_b.read_with(cx_b, |buf, _| {
3608 assert_eq!(buf.text(), new_contents.to_string());
3609 assert!(!buf.is_dirty());
3610 assert!(!buf.has_conflict());
3611 assert_eq!(buf.line_ending(), LineEnding::Windows);
3612 });
3613}
3614
3615#[gpui::test(iterations = 10)]
3616async fn test_editing_while_guest_opens_buffer(
3617 executor: BackgroundExecutor,
3618 cx_a: &mut TestAppContext,
3619 cx_b: &mut TestAppContext,
3620) {
3621 let mut server = TestServer::start(executor.clone()).await;
3622 let client_a = server.create_client(cx_a, "user_a").await;
3623 let client_b = server.create_client(cx_b, "user_b").await;
3624 server
3625 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3626 .await;
3627 let active_call_a = cx_a.read(ActiveCall::global);
3628
3629 client_a
3630 .fs()
3631 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3632 .await;
3633 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3634 let project_id = active_call_a
3635 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3636 .await
3637 .unwrap();
3638 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3639
3640 // Open a buffer as client A
3641 let buffer_a = project_a
3642 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3643 .await
3644 .unwrap();
3645
3646 // Start opening the same buffer as client B
3647 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3648 let buffer_b = cx_b.executor().spawn(open_buffer);
3649
3650 // Edit the buffer as client A while client B is still opening it.
3651 cx_b.executor().simulate_random_delay().await;
3652 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3653 cx_b.executor().simulate_random_delay().await;
3654 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3655
3656 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3657 let buffer_b = buffer_b.await.unwrap();
3658 executor.run_until_parked();
3659
3660 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3661}
3662
3663#[gpui::test(iterations = 10)]
3664async fn test_leaving_worktree_while_opening_buffer(
3665 executor: BackgroundExecutor,
3666 cx_a: &mut TestAppContext,
3667 cx_b: &mut TestAppContext,
3668) {
3669 let mut server = TestServer::start(executor.clone()).await;
3670 let client_a = server.create_client(cx_a, "user_a").await;
3671 let client_b = server.create_client(cx_b, "user_b").await;
3672 server
3673 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3674 .await;
3675 let active_call_a = cx_a.read(ActiveCall::global);
3676
3677 client_a
3678 .fs()
3679 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3680 .await;
3681 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3682 let project_id = active_call_a
3683 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3684 .await
3685 .unwrap();
3686 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3687
3688 // See that a guest has joined as client A.
3689 executor.run_until_parked();
3690
3691 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3692
3693 // Begin opening a buffer as client B, but leave the project before the open completes.
3694 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3695 let buffer_b = cx_b.executor().spawn(open_buffer);
3696 cx_b.update(|_| drop(project_b));
3697 drop(buffer_b);
3698
3699 // See that the guest has left.
3700 executor.run_until_parked();
3701
3702 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3703}
3704
3705#[gpui::test(iterations = 10)]
3706async fn test_canceling_buffer_opening(
3707 executor: BackgroundExecutor,
3708 cx_a: &mut TestAppContext,
3709 cx_b: &mut TestAppContext,
3710) {
3711 let mut server = TestServer::start(executor.clone()).await;
3712 let client_a = server.create_client(cx_a, "user_a").await;
3713 let client_b = server.create_client(cx_b, "user_b").await;
3714 server
3715 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3716 .await;
3717 let active_call_a = cx_a.read(ActiveCall::global);
3718
3719 client_a
3720 .fs()
3721 .insert_tree(
3722 "/dir",
3723 json!({
3724 "a.txt": "abc",
3725 }),
3726 )
3727 .await;
3728 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3729 let project_id = active_call_a
3730 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3731 .await
3732 .unwrap();
3733 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3734
3735 let buffer_a = project_a
3736 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3737 .await
3738 .unwrap();
3739
3740 // Open a buffer as client B but cancel after a random amount of time.
3741 let buffer_b = project_b.update(cx_b, |p, cx| {
3742 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3743 });
3744 executor.simulate_random_delay().await;
3745 drop(buffer_b);
3746
3747 // Try opening the same buffer again as client B, and ensure we can
3748 // still do it despite the cancellation above.
3749 let buffer_b = project_b
3750 .update(cx_b, |p, cx| {
3751 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3752 })
3753 .await
3754 .unwrap();
3755
3756 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3757}
3758
3759#[gpui::test(iterations = 10)]
3760async fn test_leaving_project(
3761 executor: BackgroundExecutor,
3762 cx_a: &mut TestAppContext,
3763 cx_b: &mut TestAppContext,
3764 cx_c: &mut TestAppContext,
3765) {
3766 let mut server = TestServer::start(executor.clone()).await;
3767 let client_a = server.create_client(cx_a, "user_a").await;
3768 let client_b = server.create_client(cx_b, "user_b").await;
3769 let client_c = server.create_client(cx_c, "user_c").await;
3770 server
3771 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3772 .await;
3773 let active_call_a = cx_a.read(ActiveCall::global);
3774
3775 client_a
3776 .fs()
3777 .insert_tree(
3778 "/a",
3779 json!({
3780 "a.txt": "a-contents",
3781 "b.txt": "b-contents",
3782 }),
3783 )
3784 .await;
3785 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3786 let project_id = active_call_a
3787 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3788 .await
3789 .unwrap();
3790 let project_b1 = client_b.join_remote_project(project_id, cx_b).await;
3791 let project_c = client_c.join_remote_project(project_id, cx_c).await;
3792
3793 // Client A sees that a guest has joined.
3794 executor.run_until_parked();
3795
3796 project_a.read_with(cx_a, |project, _| {
3797 assert_eq!(project.collaborators().len(), 2);
3798 });
3799
3800 project_b1.read_with(cx_b, |project, _| {
3801 assert_eq!(project.collaborators().len(), 2);
3802 });
3803
3804 project_c.read_with(cx_c, |project, _| {
3805 assert_eq!(project.collaborators().len(), 2);
3806 });
3807
3808 // Client B opens a buffer.
3809 let buffer_b1 = project_b1
3810 .update(cx_b, |project, cx| {
3811 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3812 project.open_buffer((worktree_id, "a.txt"), cx)
3813 })
3814 .await
3815 .unwrap();
3816
3817 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3818
3819 // Drop client B's project and ensure client A and client C observe client B leaving.
3820 cx_b.update(|_| drop(project_b1));
3821 executor.run_until_parked();
3822
3823 project_a.read_with(cx_a, |project, _| {
3824 assert_eq!(project.collaborators().len(), 1);
3825 });
3826
3827 project_c.read_with(cx_c, |project, _| {
3828 assert_eq!(project.collaborators().len(), 1);
3829 });
3830
3831 // Client B re-joins the project and can open buffers as before.
3832 let project_b2 = client_b.join_remote_project(project_id, cx_b).await;
3833 executor.run_until_parked();
3834
3835 project_a.read_with(cx_a, |project, _| {
3836 assert_eq!(project.collaborators().len(), 2);
3837 });
3838
3839 project_b2.read_with(cx_b, |project, _| {
3840 assert_eq!(project.collaborators().len(), 2);
3841 });
3842
3843 project_c.read_with(cx_c, |project, _| {
3844 assert_eq!(project.collaborators().len(), 2);
3845 });
3846
3847 let buffer_b2 = project_b2
3848 .update(cx_b, |project, cx| {
3849 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3850 project.open_buffer((worktree_id, "a.txt"), cx)
3851 })
3852 .await
3853 .unwrap();
3854
3855 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3856
3857 project_a.read_with(cx_a, |project, _| {
3858 assert_eq!(project.collaborators().len(), 2);
3859 });
3860
3861 // Drop client B's connection and ensure client A and client C observe client B leaving.
3862 client_b.disconnect(&cx_b.to_async());
3863 executor.advance_clock(RECONNECT_TIMEOUT);
3864
3865 project_a.read_with(cx_a, |project, _| {
3866 assert_eq!(project.collaborators().len(), 1);
3867 });
3868
3869 project_b2.read_with(cx_b, |project, cx| {
3870 assert!(project.is_disconnected(cx));
3871 });
3872
3873 project_c.read_with(cx_c, |project, _| {
3874 assert_eq!(project.collaborators().len(), 1);
3875 });
3876
3877 // Client B can't join the project, unless they re-join the room.
3878 cx_b.spawn(|cx| {
3879 Project::in_room(
3880 project_id,
3881 client_b.app_state.client.clone(),
3882 client_b.user_store().clone(),
3883 client_b.language_registry().clone(),
3884 FakeFs::new(cx.background_executor().clone()),
3885 cx,
3886 )
3887 })
3888 .await
3889 .unwrap_err();
3890
3891 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3892 client_c.wait_for_current_user(cx_c).await;
3893 server.forbid_connections();
3894 server.disconnect_client(client_c.peer_id().unwrap());
3895 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3896 executor.run_until_parked();
3897
3898 project_a.read_with(cx_a, |project, _| {
3899 assert_eq!(project.collaborators().len(), 0);
3900 });
3901
3902 project_b2.read_with(cx_b, |project, cx| {
3903 assert!(project.is_disconnected(cx));
3904 });
3905
3906 project_c.read_with(cx_c, |project, cx| {
3907 assert!(project.is_disconnected(cx));
3908 });
3909}
3910
3911#[gpui::test(iterations = 10)]
3912async fn test_collaborating_with_diagnostics(
3913 executor: BackgroundExecutor,
3914 cx_a: &mut TestAppContext,
3915 cx_b: &mut TestAppContext,
3916 cx_c: &mut TestAppContext,
3917) {
3918 let mut server = TestServer::start(executor.clone()).await;
3919 let client_a = server.create_client(cx_a, "user_a").await;
3920 let client_b = server.create_client(cx_b, "user_b").await;
3921 let client_c = server.create_client(cx_c, "user_c").await;
3922 server
3923 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3924 .await;
3925 let active_call_a = cx_a.read(ActiveCall::global);
3926
3927 client_a.language_registry().add(Arc::new(Language::new(
3928 LanguageConfig {
3929 name: "Rust".into(),
3930 matcher: LanguageMatcher {
3931 path_suffixes: vec!["rs".to_string()],
3932 ..Default::default()
3933 },
3934 ..Default::default()
3935 },
3936 Some(tree_sitter_rust::LANGUAGE.into()),
3937 )));
3938 let mut fake_language_servers = client_a
3939 .language_registry()
3940 .register_fake_lsp("Rust", Default::default());
3941
3942 // Share a project as client A
3943 client_a
3944 .fs()
3945 .insert_tree(
3946 "/a",
3947 json!({
3948 "a.rs": "let one = two",
3949 "other.rs": "",
3950 }),
3951 )
3952 .await;
3953 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3954
3955 // Cause the language server to start.
3956 let _buffer = project_a
3957 .update(cx_a, |project, cx| {
3958 project.open_local_buffer_with_lsp("/a/other.rs", cx)
3959 })
3960 .await
3961 .unwrap();
3962
3963 // Simulate a language server reporting errors for a file.
3964 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3965 fake_language_server
3966 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3967 .await;
3968 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3969 &lsp::PublishDiagnosticsParams {
3970 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3971 version: None,
3972 diagnostics: vec![lsp::Diagnostic {
3973 severity: Some(lsp::DiagnosticSeverity::WARNING),
3974 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3975 message: "message 0".to_string(),
3976 ..Default::default()
3977 }],
3978 },
3979 );
3980
3981 // Client A shares the project and, simultaneously, the language server
3982 // publishes a diagnostic. This is done to ensure that the server always
3983 // observes the latest diagnostics for a worktree.
3984 let project_id = active_call_a
3985 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3986 .await
3987 .unwrap();
3988 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3989 &lsp::PublishDiagnosticsParams {
3990 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3991 version: None,
3992 diagnostics: vec![lsp::Diagnostic {
3993 severity: Some(lsp::DiagnosticSeverity::ERROR),
3994 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3995 message: "message 1".to_string(),
3996 ..Default::default()
3997 }],
3998 },
3999 );
4000
4001 // Join the worktree as client B.
4002 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4003
4004 // Wait for server to see the diagnostics update.
4005 executor.run_until_parked();
4006
4007 // Ensure client B observes the new diagnostics.
4008
4009 project_b.read_with(cx_b, |project, cx| {
4010 assert_eq!(
4011 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4012 &[(
4013 ProjectPath {
4014 worktree_id,
4015 path: Arc::from(Path::new("a.rs")),
4016 },
4017 LanguageServerId(0),
4018 DiagnosticSummary {
4019 error_count: 1,
4020 warning_count: 0,
4021 },
4022 )]
4023 )
4024 });
4025
4026 // Join project as client C and observe the diagnostics.
4027 let project_c = client_c.join_remote_project(project_id, cx_c).await;
4028 executor.run_until_parked();
4029 let project_c_diagnostic_summaries =
4030 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
4031 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
4032 })));
4033 project_c.update(cx_c, |_, cx| {
4034 let summaries = project_c_diagnostic_summaries.clone();
4035 cx.subscribe(&project_c, {
4036 move |p, _, event, cx| {
4037 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4038 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
4039 }
4040 }
4041 })
4042 .detach();
4043 });
4044
4045 executor.run_until_parked();
4046 assert_eq!(
4047 project_c_diagnostic_summaries.borrow().as_slice(),
4048 &[(
4049 ProjectPath {
4050 worktree_id,
4051 path: Arc::from(Path::new("a.rs")),
4052 },
4053 LanguageServerId(0),
4054 DiagnosticSummary {
4055 error_count: 1,
4056 warning_count: 0,
4057 },
4058 )]
4059 );
4060
4061 // Simulate a language server reporting more errors for a file.
4062 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4063 &lsp::PublishDiagnosticsParams {
4064 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
4065 version: None,
4066 diagnostics: vec![
4067 lsp::Diagnostic {
4068 severity: Some(lsp::DiagnosticSeverity::ERROR),
4069 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
4070 message: "message 1".to_string(),
4071 ..Default::default()
4072 },
4073 lsp::Diagnostic {
4074 severity: Some(lsp::DiagnosticSeverity::WARNING),
4075 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
4076 message: "message 2".to_string(),
4077 ..Default::default()
4078 },
4079 ],
4080 },
4081 );
4082
4083 // Clients B and C get the updated summaries
4084 executor.run_until_parked();
4085
4086 project_b.read_with(cx_b, |project, cx| {
4087 assert_eq!(
4088 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4089 [(
4090 ProjectPath {
4091 worktree_id,
4092 path: Arc::from(Path::new("a.rs")),
4093 },
4094 LanguageServerId(0),
4095 DiagnosticSummary {
4096 error_count: 1,
4097 warning_count: 1,
4098 },
4099 )]
4100 );
4101 });
4102
4103 project_c.read_with(cx_c, |project, cx| {
4104 assert_eq!(
4105 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4106 [(
4107 ProjectPath {
4108 worktree_id,
4109 path: Arc::from(Path::new("a.rs")),
4110 },
4111 LanguageServerId(0),
4112 DiagnosticSummary {
4113 error_count: 1,
4114 warning_count: 1,
4115 },
4116 )]
4117 );
4118 });
4119
4120 // Open the file with the errors on client B. They should be present.
4121 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4122 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4123
4124 buffer_b.read_with(cx_b, |buffer, _| {
4125 assert_eq!(
4126 buffer
4127 .snapshot()
4128 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
4129 .collect::<Vec<_>>(),
4130 &[
4131 DiagnosticEntry {
4132 range: Point::new(0, 4)..Point::new(0, 7),
4133 diagnostic: Diagnostic {
4134 group_id: 2,
4135 message: "message 1".to_string(),
4136 severity: lsp::DiagnosticSeverity::ERROR,
4137 is_primary: true,
4138 ..Default::default()
4139 }
4140 },
4141 DiagnosticEntry {
4142 range: Point::new(0, 10)..Point::new(0, 13),
4143 diagnostic: Diagnostic {
4144 group_id: 3,
4145 severity: lsp::DiagnosticSeverity::WARNING,
4146 message: "message 2".to_string(),
4147 is_primary: true,
4148 ..Default::default()
4149 }
4150 }
4151 ]
4152 );
4153 });
4154
4155 // Simulate a language server reporting no errors for a file.
4156 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4157 &lsp::PublishDiagnosticsParams {
4158 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
4159 version: None,
4160 diagnostics: vec![],
4161 },
4162 );
4163 executor.run_until_parked();
4164
4165 project_a.read_with(cx_a, |project, cx| {
4166 assert_eq!(
4167 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4168 []
4169 )
4170 });
4171
4172 project_b.read_with(cx_b, |project, cx| {
4173 assert_eq!(
4174 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4175 []
4176 )
4177 });
4178
4179 project_c.read_with(cx_c, |project, cx| {
4180 assert_eq!(
4181 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4182 []
4183 )
4184 });
4185}
4186
4187#[gpui::test(iterations = 10)]
4188async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4189 executor: BackgroundExecutor,
4190 cx_a: &mut TestAppContext,
4191 cx_b: &mut TestAppContext,
4192) {
4193 let mut server = TestServer::start(executor.clone()).await;
4194 let client_a = server.create_client(cx_a, "user_a").await;
4195 let client_b = server.create_client(cx_b, "user_b").await;
4196 server
4197 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4198 .await;
4199
4200 client_a.language_registry().add(rust_lang());
4201 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4202 "Rust",
4203 FakeLspAdapter {
4204 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4205 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4206 ..Default::default()
4207 },
4208 );
4209
4210 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4211 client_a
4212 .fs()
4213 .insert_tree(
4214 "/test",
4215 json!({
4216 "one.rs": "const ONE: usize = 1;",
4217 "two.rs": "const TWO: usize = 2;",
4218 "three.rs": "const THREE: usize = 3;",
4219 "four.rs": "const FOUR: usize = 3;",
4220 "five.rs": "const FIVE: usize = 3;",
4221 }),
4222 )
4223 .await;
4224
4225 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
4226
4227 // Share a project as client A
4228 let active_call_a = cx_a.read(ActiveCall::global);
4229 let project_id = active_call_a
4230 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4231 .await
4232 .unwrap();
4233
4234 // Join the project as client B and open all three files.
4235 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4236 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4237 project_b.update(cx_b, |p, cx| {
4238 p.open_buffer_with_lsp((worktree_id, file_name), cx)
4239 })
4240 }))
4241 .await
4242 .unwrap();
4243
4244 // Simulate a language server reporting errors for a file.
4245 let fake_language_server = fake_language_servers.next().await.unwrap();
4246 fake_language_server
4247 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4248 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4249 })
4250 .await
4251 .unwrap();
4252 fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
4253 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4254 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4255 lsp::WorkDoneProgressBegin {
4256 title: "Progress Began".into(),
4257 ..Default::default()
4258 },
4259 )),
4260 });
4261 for file_name in file_names {
4262 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4263 &lsp::PublishDiagnosticsParams {
4264 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
4265 version: None,
4266 diagnostics: vec![lsp::Diagnostic {
4267 severity: Some(lsp::DiagnosticSeverity::WARNING),
4268 source: Some("the-disk-based-diagnostics-source".into()),
4269 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4270 message: "message one".to_string(),
4271 ..Default::default()
4272 }],
4273 },
4274 );
4275 }
4276 fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
4277 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4278 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4279 lsp::WorkDoneProgressEnd { message: None },
4280 )),
4281 });
4282
4283 // When the "disk base diagnostics finished" message is received, the buffers'
4284 // diagnostics are expected to be present.
4285 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4286 project_b.update(cx_b, {
4287 let project_b = project_b.clone();
4288 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4289 move |_, cx| {
4290 cx.subscribe(&project_b, move |_, _, event, cx| {
4291 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4292 disk_based_diagnostics_finished.store(true, SeqCst);
4293 for (buffer, _) in &guest_buffers {
4294 assert_eq!(
4295 buffer
4296 .read(cx)
4297 .snapshot()
4298 .diagnostics_in_range::<_, usize>(0..5, false)
4299 .count(),
4300 1,
4301 "expected a diagnostic for buffer {:?}",
4302 buffer.read(cx).file().unwrap().path(),
4303 );
4304 }
4305 }
4306 })
4307 .detach();
4308 }
4309 });
4310
4311 executor.run_until_parked();
4312 assert!(disk_based_diagnostics_finished.load(SeqCst));
4313}
4314
4315#[gpui::test(iterations = 10)]
4316async fn test_reloading_buffer_manually(
4317 executor: BackgroundExecutor,
4318 cx_a: &mut TestAppContext,
4319 cx_b: &mut TestAppContext,
4320) {
4321 let mut server = TestServer::start(executor.clone()).await;
4322 let client_a = server.create_client(cx_a, "user_a").await;
4323 let client_b = server.create_client(cx_b, "user_b").await;
4324 server
4325 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4326 .await;
4327 let active_call_a = cx_a.read(ActiveCall::global);
4328
4329 client_a
4330 .fs()
4331 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4332 .await;
4333 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4334 let buffer_a = project_a
4335 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4336 .await
4337 .unwrap();
4338 let project_id = active_call_a
4339 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4340 .await
4341 .unwrap();
4342
4343 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4344
4345 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4346 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4347 buffer_b.update(cx_b, |buffer, cx| {
4348 buffer.edit([(4..7, "six")], None, cx);
4349 buffer.edit([(10..11, "6")], None, cx);
4350 assert_eq!(buffer.text(), "let six = 6;");
4351 assert!(buffer.is_dirty());
4352 assert!(!buffer.has_conflict());
4353 });
4354 executor.run_until_parked();
4355
4356 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4357
4358 client_a
4359 .fs()
4360 .save(
4361 "/a/a.rs".as_ref(),
4362 &Rope::from("let seven = 7;"),
4363 LineEnding::Unix,
4364 )
4365 .await
4366 .unwrap();
4367 executor.run_until_parked();
4368
4369 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4370
4371 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4372
4373 project_b
4374 .update(cx_b, |project, cx| {
4375 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4376 })
4377 .await
4378 .unwrap();
4379
4380 buffer_a.read_with(cx_a, |buffer, _| {
4381 assert_eq!(buffer.text(), "let seven = 7;");
4382 assert!(!buffer.is_dirty());
4383 assert!(!buffer.has_conflict());
4384 });
4385
4386 buffer_b.read_with(cx_b, |buffer, _| {
4387 assert_eq!(buffer.text(), "let seven = 7;");
4388 assert!(!buffer.is_dirty());
4389 assert!(!buffer.has_conflict());
4390 });
4391
4392 buffer_a.update(cx_a, |buffer, cx| {
4393 // Undoing on the host is a no-op when the reload was initiated by the guest.
4394 buffer.undo(cx);
4395 assert_eq!(buffer.text(), "let seven = 7;");
4396 assert!(!buffer.is_dirty());
4397 assert!(!buffer.has_conflict());
4398 });
4399 buffer_b.update(cx_b, |buffer, cx| {
4400 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4401 buffer.undo(cx);
4402 assert_eq!(buffer.text(), "let six = 6;");
4403 assert!(buffer.is_dirty());
4404 assert!(!buffer.has_conflict());
4405 });
4406}
4407
4408#[gpui::test(iterations = 10)]
4409async fn test_formatting_buffer(
4410 executor: BackgroundExecutor,
4411 cx_a: &mut TestAppContext,
4412 cx_b: &mut TestAppContext,
4413) {
4414 let mut server = TestServer::start(executor.clone()).await;
4415 let client_a = server.create_client(cx_a, "user_a").await;
4416 let client_b = server.create_client(cx_b, "user_b").await;
4417 server
4418 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4419 .await;
4420 let active_call_a = cx_a.read(ActiveCall::global);
4421
4422 client_a.language_registry().add(rust_lang());
4423 let mut fake_language_servers = client_a
4424 .language_registry()
4425 .register_fake_lsp("Rust", FakeLspAdapter::default());
4426
4427 // Here we insert a fake tree with a directory that exists on disk. This is needed
4428 // because later we'll invoke a command, which requires passing a working directory
4429 // that points to a valid location on disk.
4430 let directory = env::current_dir().unwrap();
4431 client_a
4432 .fs()
4433 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4434 .await;
4435 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4436 let project_id = active_call_a
4437 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4438 .await
4439 .unwrap();
4440 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4441
4442 let buffer_b = project_b
4443 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4444 .await
4445 .unwrap();
4446
4447 let _handle = project_b.update(cx_b, |project, cx| {
4448 project.register_buffer_with_language_servers(&buffer_b, cx)
4449 });
4450 let fake_language_server = fake_language_servers.next().await.unwrap();
4451 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4452 Ok(Some(vec![
4453 lsp::TextEdit {
4454 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4455 new_text: "h".to_string(),
4456 },
4457 lsp::TextEdit {
4458 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4459 new_text: "y".to_string(),
4460 },
4461 ]))
4462 });
4463
4464 project_b
4465 .update(cx_b, |project, cx| {
4466 project.format(
4467 HashSet::from_iter([buffer_b.clone()]),
4468 LspFormatTarget::Buffers,
4469 true,
4470 FormatTrigger::Save,
4471 cx,
4472 )
4473 })
4474 .await
4475 .unwrap();
4476
4477 // The edits from the LSP are applied, and a final newline is added.
4478 assert_eq!(
4479 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4480 "let honey = \"two\"\n"
4481 );
4482
4483 // Ensure buffer can be formatted using an external command. Notice how the
4484 // host's configuration is honored as opposed to using the guest's settings.
4485 cx_a.update(|cx| {
4486 SettingsStore::update_global(cx, |store, cx| {
4487 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4488 file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
4489 vec![Formatter::External {
4490 command: "awk".into(),
4491 arguments: Some(vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into()),
4492 }]
4493 .into(),
4494 )));
4495 });
4496 });
4497 });
4498
4499 executor.allow_parking();
4500 project_b
4501 .update(cx_b, |project, cx| {
4502 project.format(
4503 HashSet::from_iter([buffer_b.clone()]),
4504 LspFormatTarget::Buffers,
4505 true,
4506 FormatTrigger::Save,
4507 cx,
4508 )
4509 })
4510 .await
4511 .unwrap();
4512 assert_eq!(
4513 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4514 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4515 );
4516}
4517
4518#[gpui::test(iterations = 10)]
4519async fn test_prettier_formatting_buffer(
4520 executor: BackgroundExecutor,
4521 cx_a: &mut TestAppContext,
4522 cx_b: &mut TestAppContext,
4523) {
4524 let mut server = TestServer::start(executor.clone()).await;
4525 let client_a = server.create_client(cx_a, "user_a").await;
4526 let client_b = server.create_client(cx_b, "user_b").await;
4527 server
4528 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4529 .await;
4530 let active_call_a = cx_a.read(ActiveCall::global);
4531
4532 let test_plugin = "test_plugin";
4533
4534 client_a.language_registry().add(Arc::new(Language::new(
4535 LanguageConfig {
4536 name: "TypeScript".into(),
4537 matcher: LanguageMatcher {
4538 path_suffixes: vec!["ts".to_string()],
4539 ..Default::default()
4540 },
4541 ..Default::default()
4542 },
4543 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
4544 )));
4545 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4546 "TypeScript",
4547 FakeLspAdapter {
4548 prettier_plugins: vec![test_plugin],
4549 ..Default::default()
4550 },
4551 );
4552
4553 // Here we insert a fake tree with a directory that exists on disk. This is needed
4554 // because later we'll invoke a command, which requires passing a working directory
4555 // that points to a valid location on disk.
4556 let directory = env::current_dir().unwrap();
4557 let buffer_text = "let one = \"two\"";
4558 client_a
4559 .fs()
4560 .insert_tree(&directory, json!({ "a.ts": buffer_text }))
4561 .await;
4562 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4563 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4564 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
4565 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4566
4567 let project_id = active_call_a
4568 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4569 .await
4570 .unwrap();
4571 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4572 let (buffer_b, _) = project_b
4573 .update(cx_b, |p, cx| {
4574 p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
4575 })
4576 .await
4577 .unwrap();
4578
4579 cx_a.update(|cx| {
4580 SettingsStore::update_global(cx, |store, cx| {
4581 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4582 file.defaults.formatter = Some(SelectedFormatter::Auto);
4583 file.defaults.prettier = Some(PrettierSettings {
4584 allowed: true,
4585 ..PrettierSettings::default()
4586 });
4587 });
4588 });
4589 });
4590 cx_b.update(|cx| {
4591 SettingsStore::update_global(cx, |store, cx| {
4592 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4593 file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
4594 vec![Formatter::LanguageServer { name: None }].into(),
4595 )));
4596 file.defaults.prettier = Some(PrettierSettings {
4597 allowed: true,
4598 ..PrettierSettings::default()
4599 });
4600 });
4601 });
4602 });
4603 let fake_language_server = fake_language_servers.next().await.unwrap();
4604 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4605 panic!(
4606 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4607 )
4608 });
4609
4610 project_b
4611 .update(cx_b, |project, cx| {
4612 project.format(
4613 HashSet::from_iter([buffer_b.clone()]),
4614 LspFormatTarget::Buffers,
4615 true,
4616 FormatTrigger::Save,
4617 cx,
4618 )
4619 })
4620 .await
4621 .unwrap();
4622
4623 executor.run_until_parked();
4624 assert_eq!(
4625 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4626 buffer_text.to_string() + "\n" + prettier_format_suffix,
4627 "Prettier formatting was not applied to client buffer after client's request"
4628 );
4629
4630 project_a
4631 .update(cx_a, |project, cx| {
4632 project.format(
4633 HashSet::from_iter([buffer_a.clone()]),
4634 LspFormatTarget::Buffers,
4635 true,
4636 FormatTrigger::Manual,
4637 cx,
4638 )
4639 })
4640 .await
4641 .unwrap();
4642
4643 executor.run_until_parked();
4644 assert_eq!(
4645 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4646 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4647 "Prettier formatting was not applied to client buffer after host's request"
4648 );
4649}
4650
4651#[gpui::test(iterations = 10)]
4652async fn test_definition(
4653 executor: BackgroundExecutor,
4654 cx_a: &mut TestAppContext,
4655 cx_b: &mut TestAppContext,
4656) {
4657 let mut server = TestServer::start(executor.clone()).await;
4658 let client_a = server.create_client(cx_a, "user_a").await;
4659 let client_b = server.create_client(cx_b, "user_b").await;
4660 server
4661 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4662 .await;
4663 let active_call_a = cx_a.read(ActiveCall::global);
4664
4665 let mut fake_language_servers = client_a
4666 .language_registry()
4667 .register_fake_lsp("Rust", Default::default());
4668 client_a.language_registry().add(rust_lang());
4669
4670 client_a
4671 .fs()
4672 .insert_tree(
4673 "/root",
4674 json!({
4675 "dir-1": {
4676 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4677 },
4678 "dir-2": {
4679 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4680 "c.rs": "type T2 = usize;",
4681 }
4682 }),
4683 )
4684 .await;
4685 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4686 let project_id = active_call_a
4687 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4688 .await
4689 .unwrap();
4690 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4691
4692 // Open the file on client B.
4693 let (buffer_b, _handle) = project_b
4694 .update(cx_b, |p, cx| {
4695 p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
4696 })
4697 .await
4698 .unwrap();
4699
4700 // Request the definition of a symbol as the guest.
4701 let fake_language_server = fake_language_servers.next().await.unwrap();
4702 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4703 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4704 lsp::Location::new(
4705 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4706 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4707 ),
4708 )))
4709 });
4710
4711 let definitions_1 = project_b
4712 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4713 .await
4714 .unwrap();
4715 cx_b.read(|cx| {
4716 assert_eq!(definitions_1.len(), 1);
4717 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4718 let target_buffer = definitions_1[0].target.buffer.read(cx);
4719 assert_eq!(
4720 target_buffer.text(),
4721 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4722 );
4723 assert_eq!(
4724 definitions_1[0].target.range.to_point(target_buffer),
4725 Point::new(0, 6)..Point::new(0, 9)
4726 );
4727 });
4728
4729 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4730 // the previous call to `definition`.
4731 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4732 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4733 lsp::Location::new(
4734 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4735 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4736 ),
4737 )))
4738 });
4739
4740 let definitions_2 = project_b
4741 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4742 .await
4743 .unwrap();
4744 cx_b.read(|cx| {
4745 assert_eq!(definitions_2.len(), 1);
4746 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4747 let target_buffer = definitions_2[0].target.buffer.read(cx);
4748 assert_eq!(
4749 target_buffer.text(),
4750 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4751 );
4752 assert_eq!(
4753 definitions_2[0].target.range.to_point(target_buffer),
4754 Point::new(1, 6)..Point::new(1, 11)
4755 );
4756 });
4757 assert_eq!(
4758 definitions_1[0].target.buffer,
4759 definitions_2[0].target.buffer
4760 );
4761
4762 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4763 |req, _| async move {
4764 assert_eq!(
4765 req.text_document_position_params.position,
4766 lsp::Position::new(0, 7)
4767 );
4768 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4769 lsp::Location::new(
4770 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4771 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4772 ),
4773 )))
4774 },
4775 );
4776
4777 let type_definitions = project_b
4778 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4779 .await
4780 .unwrap();
4781 cx_b.read(|cx| {
4782 assert_eq!(type_definitions.len(), 1);
4783 let target_buffer = type_definitions[0].target.buffer.read(cx);
4784 assert_eq!(target_buffer.text(), "type T2 = usize;");
4785 assert_eq!(
4786 type_definitions[0].target.range.to_point(target_buffer),
4787 Point::new(0, 5)..Point::new(0, 7)
4788 );
4789 });
4790}
4791
4792#[gpui::test(iterations = 10)]
4793async fn test_references(
4794 executor: BackgroundExecutor,
4795 cx_a: &mut TestAppContext,
4796 cx_b: &mut TestAppContext,
4797) {
4798 let mut server = TestServer::start(executor.clone()).await;
4799 let client_a = server.create_client(cx_a, "user_a").await;
4800 let client_b = server.create_client(cx_b, "user_b").await;
4801 server
4802 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4803 .await;
4804 let active_call_a = cx_a.read(ActiveCall::global);
4805
4806 client_a.language_registry().add(rust_lang());
4807 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4808 "Rust",
4809 FakeLspAdapter {
4810 name: "my-fake-lsp-adapter",
4811 capabilities: lsp::ServerCapabilities {
4812 references_provider: Some(lsp::OneOf::Left(true)),
4813 ..Default::default()
4814 },
4815 ..Default::default()
4816 },
4817 );
4818
4819 client_a
4820 .fs()
4821 .insert_tree(
4822 "/root",
4823 json!({
4824 "dir-1": {
4825 "one.rs": "const ONE: usize = 1;",
4826 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4827 },
4828 "dir-2": {
4829 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4830 }
4831 }),
4832 )
4833 .await;
4834 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4835 let project_id = active_call_a
4836 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4837 .await
4838 .unwrap();
4839 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4840
4841 // Open the file on client B.
4842 let (buffer_b, _handle) = project_b
4843 .update(cx_b, |p, cx| {
4844 p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
4845 })
4846 .await
4847 .unwrap();
4848
4849 // Request references to a symbol as the guest.
4850 let fake_language_server = fake_language_servers.next().await.unwrap();
4851 let (lsp_response_tx, rx) = mpsc::unbounded::<Result<Option<Vec<lsp::Location>>>>();
4852 fake_language_server.handle_request::<lsp::request::References, _, _>({
4853 let rx = Arc::new(Mutex::new(Some(rx)));
4854 move |params, _| {
4855 assert_eq!(
4856 params.text_document_position.text_document.uri.as_str(),
4857 "file:///root/dir-1/one.rs"
4858 );
4859 let rx = rx.clone();
4860 async move {
4861 let mut response_rx = rx.lock().take().unwrap();
4862 let result = response_rx.next().await.unwrap();
4863 *rx.lock() = Some(response_rx);
4864 result
4865 }
4866 }
4867 });
4868
4869 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4870
4871 // User is informed that a request is pending.
4872 executor.run_until_parked();
4873 project_b.read_with(cx_b, |project, cx| {
4874 let status = project.language_server_statuses(cx).next().unwrap().1;
4875 assert_eq!(status.name, "my-fake-lsp-adapter");
4876 assert_eq!(
4877 status.pending_work.values().next().unwrap().message,
4878 Some("Finding references...".into())
4879 );
4880 });
4881
4882 // Cause the language server to respond.
4883 lsp_response_tx
4884 .unbounded_send(Ok(Some(vec![
4885 lsp::Location {
4886 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4887 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4888 },
4889 lsp::Location {
4890 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4891 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4892 },
4893 lsp::Location {
4894 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4895 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4896 },
4897 ])))
4898 .unwrap();
4899
4900 let references = references.await.unwrap();
4901 executor.run_until_parked();
4902 project_b.read_with(cx_b, |project, cx| {
4903 // User is informed that a request is no longer pending.
4904 let status = project.language_server_statuses(cx).next().unwrap().1;
4905 assert!(status.pending_work.is_empty());
4906
4907 assert_eq!(references.len(), 3);
4908 assert_eq!(project.worktrees(cx).count(), 2);
4909
4910 let two_buffer = references[0].buffer.read(cx);
4911 let three_buffer = references[2].buffer.read(cx);
4912 assert_eq!(
4913 two_buffer.file().unwrap().path().as_ref(),
4914 Path::new("two.rs")
4915 );
4916 assert_eq!(references[1].buffer, references[0].buffer);
4917 assert_eq!(
4918 three_buffer.file().unwrap().full_path(cx),
4919 Path::new("/root/dir-2/three.rs")
4920 );
4921
4922 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4923 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4924 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4925 });
4926
4927 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4928
4929 // User is informed that a request is pending.
4930 executor.run_until_parked();
4931 project_b.read_with(cx_b, |project, cx| {
4932 let status = project.language_server_statuses(cx).next().unwrap().1;
4933 assert_eq!(status.name, "my-fake-lsp-adapter");
4934 assert_eq!(
4935 status.pending_work.values().next().unwrap().message,
4936 Some("Finding references...".into())
4937 );
4938 });
4939
4940 // Cause the LSP request to fail.
4941 lsp_response_tx
4942 .unbounded_send(Err(anyhow!("can't find references")))
4943 .unwrap();
4944 references.await.unwrap_err();
4945
4946 // User is informed that the request is no longer pending.
4947 executor.run_until_parked();
4948 project_b.read_with(cx_b, |project, cx| {
4949 let status = project.language_server_statuses(cx).next().unwrap().1;
4950 assert!(status.pending_work.is_empty());
4951 });
4952}
4953
4954#[gpui::test(iterations = 10)]
4955async fn test_project_search(
4956 executor: BackgroundExecutor,
4957 cx_a: &mut TestAppContext,
4958 cx_b: &mut TestAppContext,
4959) {
4960 let mut server = TestServer::start(executor.clone()).await;
4961 let client_a = server.create_client(cx_a, "user_a").await;
4962 let client_b = server.create_client(cx_b, "user_b").await;
4963 server
4964 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4965 .await;
4966 let active_call_a = cx_a.read(ActiveCall::global);
4967
4968 client_a
4969 .fs()
4970 .insert_tree(
4971 "/root",
4972 json!({
4973 "dir-1": {
4974 "a": "hello world",
4975 "b": "goodnight moon",
4976 "c": "a world of goo",
4977 "d": "world champion of clown world",
4978 },
4979 "dir-2": {
4980 "e": "disney world is fun",
4981 }
4982 }),
4983 )
4984 .await;
4985 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4986 let (worktree_2, _) = project_a
4987 .update(cx_a, |p, cx| {
4988 p.find_or_create_worktree("/root/dir-2", true, cx)
4989 })
4990 .await
4991 .unwrap();
4992 worktree_2
4993 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4994 .await;
4995 let project_id = active_call_a
4996 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4997 .await
4998 .unwrap();
4999
5000 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5001
5002 // Perform a search as the guest.
5003 let mut results = HashMap::default();
5004 let search_rx = project_b.update(cx_b, |project, cx| {
5005 project.search(
5006 SearchQuery::text(
5007 "world",
5008 false,
5009 false,
5010 false,
5011 Default::default(),
5012 Default::default(),
5013 None,
5014 )
5015 .unwrap(),
5016 cx,
5017 )
5018 });
5019 while let Ok(result) = search_rx.recv().await {
5020 match result {
5021 SearchResult::Buffer { buffer, ranges } => {
5022 results.entry(buffer).or_insert(ranges);
5023 }
5024 SearchResult::LimitReached => {
5025 panic!("Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call.")
5026 }
5027 };
5028 }
5029
5030 let mut ranges_by_path = results
5031 .into_iter()
5032 .map(|(buffer, ranges)| {
5033 buffer.read_with(cx_b, |buffer, cx| {
5034 let path = buffer.file().unwrap().full_path(cx);
5035 let offset_ranges = ranges
5036 .into_iter()
5037 .map(|range| range.to_offset(buffer))
5038 .collect::<Vec<_>>();
5039 (path, offset_ranges)
5040 })
5041 })
5042 .collect::<Vec<_>>();
5043 ranges_by_path.sort_by_key(|(path, _)| path.clone());
5044
5045 assert_eq!(
5046 ranges_by_path,
5047 &[
5048 (PathBuf::from("dir-1/a"), vec![6..11]),
5049 (PathBuf::from("dir-1/c"), vec![2..7]),
5050 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
5051 (PathBuf::from("dir-2/e"), vec![7..12]),
5052 ]
5053 );
5054}
5055
5056#[gpui::test(iterations = 10)]
5057async fn test_document_highlights(
5058 executor: BackgroundExecutor,
5059 cx_a: &mut TestAppContext,
5060 cx_b: &mut TestAppContext,
5061) {
5062 let mut server = TestServer::start(executor.clone()).await;
5063 let client_a = server.create_client(cx_a, "user_a").await;
5064 let client_b = server.create_client(cx_b, "user_b").await;
5065 server
5066 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5067 .await;
5068 let active_call_a = cx_a.read(ActiveCall::global);
5069
5070 client_a
5071 .fs()
5072 .insert_tree(
5073 "/root-1",
5074 json!({
5075 "main.rs": "fn double(number: i32) -> i32 { number + number }",
5076 }),
5077 )
5078 .await;
5079
5080 let mut fake_language_servers = client_a
5081 .language_registry()
5082 .register_fake_lsp("Rust", Default::default());
5083 client_a.language_registry().add(rust_lang());
5084
5085 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5086 let project_id = active_call_a
5087 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5088 .await
5089 .unwrap();
5090 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5091
5092 // Open the file on client B.
5093 let (buffer_b, _handle) = project_b
5094 .update(cx_b, |p, cx| {
5095 p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
5096 })
5097 .await
5098 .unwrap();
5099
5100 // Request document highlights as the guest.
5101 let fake_language_server = fake_language_servers.next().await.unwrap();
5102 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
5103 |params, _| async move {
5104 assert_eq!(
5105 params
5106 .text_document_position_params
5107 .text_document
5108 .uri
5109 .as_str(),
5110 "file:///root-1/main.rs"
5111 );
5112 assert_eq!(
5113 params.text_document_position_params.position,
5114 lsp::Position::new(0, 34)
5115 );
5116 Ok(Some(vec![
5117 lsp::DocumentHighlight {
5118 kind: Some(lsp::DocumentHighlightKind::WRITE),
5119 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
5120 },
5121 lsp::DocumentHighlight {
5122 kind: Some(lsp::DocumentHighlightKind::READ),
5123 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
5124 },
5125 lsp::DocumentHighlight {
5126 kind: Some(lsp::DocumentHighlightKind::READ),
5127 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
5128 },
5129 ]))
5130 },
5131 );
5132
5133 let highlights = project_b
5134 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
5135 .await
5136 .unwrap();
5137
5138 buffer_b.read_with(cx_b, |buffer, _| {
5139 let snapshot = buffer.snapshot();
5140
5141 let highlights = highlights
5142 .into_iter()
5143 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
5144 .collect::<Vec<_>>();
5145 assert_eq!(
5146 highlights,
5147 &[
5148 (lsp::DocumentHighlightKind::WRITE, 10..16),
5149 (lsp::DocumentHighlightKind::READ, 32..38),
5150 (lsp::DocumentHighlightKind::READ, 41..47)
5151 ]
5152 )
5153 });
5154}
5155
5156#[gpui::test(iterations = 10)]
5157async fn test_lsp_hover(
5158 executor: BackgroundExecutor,
5159 cx_a: &mut TestAppContext,
5160 cx_b: &mut TestAppContext,
5161) {
5162 let mut server = TestServer::start(executor.clone()).await;
5163 let client_a = server.create_client(cx_a, "user_a").await;
5164 let client_b = server.create_client(cx_b, "user_b").await;
5165 server
5166 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5167 .await;
5168 let active_call_a = cx_a.read(ActiveCall::global);
5169
5170 client_a
5171 .fs()
5172 .insert_tree(
5173 "/root-1",
5174 json!({
5175 "main.rs": "use std::collections::HashMap;",
5176 }),
5177 )
5178 .await;
5179
5180 client_a.language_registry().add(rust_lang());
5181 let language_server_names = ["rust-analyzer", "CrabLang-ls"];
5182 let mut language_servers = [
5183 client_a.language_registry().register_fake_lsp(
5184 "Rust",
5185 FakeLspAdapter {
5186 name: "rust-analyzer",
5187 capabilities: lsp::ServerCapabilities {
5188 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5189 ..lsp::ServerCapabilities::default()
5190 },
5191 ..FakeLspAdapter::default()
5192 },
5193 ),
5194 client_a.language_registry().register_fake_lsp(
5195 "Rust",
5196 FakeLspAdapter {
5197 name: "CrabLang-ls",
5198 capabilities: lsp::ServerCapabilities {
5199 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5200 ..lsp::ServerCapabilities::default()
5201 },
5202 ..FakeLspAdapter::default()
5203 },
5204 ),
5205 ];
5206
5207 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5208 let project_id = active_call_a
5209 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5210 .await
5211 .unwrap();
5212 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5213
5214 // Open the file as the guest
5215 let (buffer_b, _handle) = project_b
5216 .update(cx_b, |p, cx| {
5217 p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
5218 })
5219 .await
5220 .unwrap();
5221
5222 let mut servers_with_hover_requests = HashMap::default();
5223 for i in 0..language_server_names.len() {
5224 let new_server = language_servers[i].next().await.unwrap_or_else(|| {
5225 panic!(
5226 "Failed to get language server #{i} with name {}",
5227 &language_server_names[i]
5228 )
5229 });
5230 let new_server_name = new_server.server.name();
5231 assert!(
5232 !servers_with_hover_requests.contains_key(&new_server_name),
5233 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5234 );
5235 match new_server_name.as_ref() {
5236 "CrabLang-ls" => {
5237 servers_with_hover_requests.insert(
5238 new_server_name.clone(),
5239 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5240 move |params, _| {
5241 assert_eq!(
5242 params
5243 .text_document_position_params
5244 .text_document
5245 .uri
5246 .as_str(),
5247 "file:///root-1/main.rs"
5248 );
5249 let name = new_server_name.clone();
5250 async move {
5251 Ok(Some(lsp::Hover {
5252 contents: lsp::HoverContents::Scalar(
5253 lsp::MarkedString::String(format!("{name} hover")),
5254 ),
5255 range: None,
5256 }))
5257 }
5258 },
5259 ),
5260 );
5261 }
5262 "rust-analyzer" => {
5263 servers_with_hover_requests.insert(
5264 new_server_name.clone(),
5265 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5266 |params, _| async move {
5267 assert_eq!(
5268 params
5269 .text_document_position_params
5270 .text_document
5271 .uri
5272 .as_str(),
5273 "file:///root-1/main.rs"
5274 );
5275 assert_eq!(
5276 params.text_document_position_params.position,
5277 lsp::Position::new(0, 22)
5278 );
5279 Ok(Some(lsp::Hover {
5280 contents: lsp::HoverContents::Array(vec![
5281 lsp::MarkedString::String("Test hover content.".to_string()),
5282 lsp::MarkedString::LanguageString(lsp::LanguageString {
5283 language: "Rust".to_string(),
5284 value: "let foo = 42;".to_string(),
5285 }),
5286 ]),
5287 range: Some(lsp::Range::new(
5288 lsp::Position::new(0, 22),
5289 lsp::Position::new(0, 29),
5290 )),
5291 }))
5292 },
5293 ),
5294 );
5295 }
5296 unexpected => panic!("Unexpected server name: {unexpected}"),
5297 }
5298 }
5299
5300 // Request hover information as the guest.
5301 let mut hovers = project_b
5302 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5303 .await;
5304 assert_eq!(
5305 hovers.len(),
5306 2,
5307 "Expected two hovers from both language servers, but got: {hovers:?}"
5308 );
5309
5310 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5311 |mut hover_request| async move {
5312 hover_request
5313 .next()
5314 .await
5315 .expect("All hover requests should have been triggered")
5316 },
5317 ))
5318 .await;
5319
5320 hovers.sort_by_key(|hover| hover.contents.len());
5321 let first_hover = hovers.first().cloned().unwrap();
5322 assert_eq!(
5323 first_hover.contents,
5324 vec![project::HoverBlock {
5325 text: "CrabLang-ls hover".to_string(),
5326 kind: HoverBlockKind::Markdown,
5327 },]
5328 );
5329 let second_hover = hovers.last().cloned().unwrap();
5330 assert_eq!(
5331 second_hover.contents,
5332 vec![
5333 project::HoverBlock {
5334 text: "Test hover content.".to_string(),
5335 kind: HoverBlockKind::Markdown,
5336 },
5337 project::HoverBlock {
5338 text: "let foo = 42;".to_string(),
5339 kind: HoverBlockKind::Code {
5340 language: "Rust".to_string()
5341 },
5342 }
5343 ]
5344 );
5345 buffer_b.read_with(cx_b, |buffer, _| {
5346 let snapshot = buffer.snapshot();
5347 assert_eq!(second_hover.range.unwrap().to_offset(&snapshot), 22..29);
5348 });
5349}
5350
5351#[gpui::test(iterations = 10)]
5352async fn test_project_symbols(
5353 executor: BackgroundExecutor,
5354 cx_a: &mut TestAppContext,
5355 cx_b: &mut TestAppContext,
5356) {
5357 let mut server = TestServer::start(executor.clone()).await;
5358 let client_a = server.create_client(cx_a, "user_a").await;
5359 let client_b = server.create_client(cx_b, "user_b").await;
5360 server
5361 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5362 .await;
5363 let active_call_a = cx_a.read(ActiveCall::global);
5364
5365 client_a.language_registry().add(rust_lang());
5366 let mut fake_language_servers = client_a
5367 .language_registry()
5368 .register_fake_lsp("Rust", Default::default());
5369
5370 client_a
5371 .fs()
5372 .insert_tree(
5373 "/code",
5374 json!({
5375 "crate-1": {
5376 "one.rs": "const ONE: usize = 1;",
5377 },
5378 "crate-2": {
5379 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5380 },
5381 "private": {
5382 "passwords.txt": "the-password",
5383 }
5384 }),
5385 )
5386 .await;
5387 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
5388 let project_id = active_call_a
5389 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5390 .await
5391 .unwrap();
5392 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5393
5394 // Cause the language server to start.
5395 let _buffer = project_b
5396 .update(cx_b, |p, cx| {
5397 p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
5398 })
5399 .await
5400 .unwrap();
5401
5402 let fake_language_server = fake_language_servers.next().await.unwrap();
5403 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
5404 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5405 #[allow(deprecated)]
5406 lsp::SymbolInformation {
5407 name: "TWO".into(),
5408 location: lsp::Location {
5409 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
5410 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5411 },
5412 kind: lsp::SymbolKind::CONSTANT,
5413 tags: None,
5414 container_name: None,
5415 deprecated: None,
5416 },
5417 ])))
5418 });
5419
5420 // Request the definition of a symbol as the guest.
5421 let symbols = project_b
5422 .update(cx_b, |p, cx| p.symbols("two", cx))
5423 .await
5424 .unwrap();
5425 assert_eq!(symbols.len(), 1);
5426 assert_eq!(symbols[0].name, "TWO");
5427
5428 // Open one of the returned symbols.
5429 let buffer_b_2 = project_b
5430 .update(cx_b, |project, cx| {
5431 project.open_buffer_for_symbol(&symbols[0], cx)
5432 })
5433 .await
5434 .unwrap();
5435
5436 buffer_b_2.read_with(cx_b, |buffer, cx| {
5437 assert_eq!(
5438 buffer.file().unwrap().full_path(cx),
5439 Path::new("/code/crate-2/two.rs")
5440 );
5441 });
5442
5443 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5444 let mut fake_symbol = symbols[0].clone();
5445 fake_symbol.path.path = Path::new("/code/secrets").into();
5446 let error = project_b
5447 .update(cx_b, |project, cx| {
5448 project.open_buffer_for_symbol(&fake_symbol, cx)
5449 })
5450 .await
5451 .unwrap_err();
5452 assert!(error.to_string().contains("invalid symbol signature"));
5453}
5454
5455#[gpui::test(iterations = 10)]
5456async fn test_open_buffer_while_getting_definition_pointing_to_it(
5457 executor: BackgroundExecutor,
5458 cx_a: &mut TestAppContext,
5459 cx_b: &mut TestAppContext,
5460 mut rng: StdRng,
5461) {
5462 let mut server = TestServer::start(executor.clone()).await;
5463 let client_a = server.create_client(cx_a, "user_a").await;
5464 let client_b = server.create_client(cx_b, "user_b").await;
5465 server
5466 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5467 .await;
5468 let active_call_a = cx_a.read(ActiveCall::global);
5469
5470 client_a.language_registry().add(rust_lang());
5471 let mut fake_language_servers = client_a
5472 .language_registry()
5473 .register_fake_lsp("Rust", Default::default());
5474
5475 client_a
5476 .fs()
5477 .insert_tree(
5478 "/root",
5479 json!({
5480 "a.rs": "const ONE: usize = b::TWO;",
5481 "b.rs": "const TWO: usize = 2",
5482 }),
5483 )
5484 .await;
5485 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5486 let project_id = active_call_a
5487 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5488 .await
5489 .unwrap();
5490 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5491
5492 let (buffer_b1, _lsp) = project_b
5493 .update(cx_b, |p, cx| {
5494 p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
5495 })
5496 .await
5497 .unwrap();
5498
5499 let fake_language_server = fake_language_servers.next().await.unwrap();
5500 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5501 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5502 lsp::Location::new(
5503 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5504 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5505 ),
5506 )))
5507 });
5508
5509 let definitions;
5510 let buffer_b2;
5511 if rng.gen() {
5512 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5513 (buffer_b2, _) = project_b
5514 .update(cx_b, |p, cx| {
5515 p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
5516 })
5517 .await
5518 .unwrap();
5519 } else {
5520 (buffer_b2, _) = project_b
5521 .update(cx_b, |p, cx| {
5522 p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
5523 })
5524 .await
5525 .unwrap();
5526 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5527 }
5528
5529 let definitions = definitions.await.unwrap();
5530 assert_eq!(definitions.len(), 1);
5531 assert_eq!(definitions[0].target.buffer, buffer_b2);
5532}
5533
5534#[gpui::test(iterations = 10)]
5535async fn test_contacts(
5536 executor: BackgroundExecutor,
5537 cx_a: &mut TestAppContext,
5538 cx_b: &mut TestAppContext,
5539 cx_c: &mut TestAppContext,
5540 cx_d: &mut TestAppContext,
5541) {
5542 let mut server = TestServer::start(executor.clone()).await;
5543 let client_a = server.create_client(cx_a, "user_a").await;
5544 let client_b = server.create_client(cx_b, "user_b").await;
5545 let client_c = server.create_client(cx_c, "user_c").await;
5546 let client_d = server.create_client(cx_d, "user_d").await;
5547 server
5548 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5549 .await;
5550 let active_call_a = cx_a.read(ActiveCall::global);
5551 let active_call_b = cx_b.read(ActiveCall::global);
5552 let active_call_c = cx_c.read(ActiveCall::global);
5553 let _active_call_d = cx_d.read(ActiveCall::global);
5554
5555 executor.run_until_parked();
5556 assert_eq!(
5557 contacts(&client_a, cx_a),
5558 [
5559 ("user_b".to_string(), "online", "free"),
5560 ("user_c".to_string(), "online", "free")
5561 ]
5562 );
5563 assert_eq!(
5564 contacts(&client_b, cx_b),
5565 [
5566 ("user_a".to_string(), "online", "free"),
5567 ("user_c".to_string(), "online", "free")
5568 ]
5569 );
5570 assert_eq!(
5571 contacts(&client_c, cx_c),
5572 [
5573 ("user_a".to_string(), "online", "free"),
5574 ("user_b".to_string(), "online", "free")
5575 ]
5576 );
5577 assert_eq!(contacts(&client_d, cx_d), []);
5578
5579 server.disconnect_client(client_c.peer_id().unwrap());
5580 server.forbid_connections();
5581 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5582 assert_eq!(
5583 contacts(&client_a, cx_a),
5584 [
5585 ("user_b".to_string(), "online", "free"),
5586 ("user_c".to_string(), "offline", "free")
5587 ]
5588 );
5589 assert_eq!(
5590 contacts(&client_b, cx_b),
5591 [
5592 ("user_a".to_string(), "online", "free"),
5593 ("user_c".to_string(), "offline", "free")
5594 ]
5595 );
5596 assert_eq!(contacts(&client_c, cx_c), []);
5597 assert_eq!(contacts(&client_d, cx_d), []);
5598
5599 server.allow_connections();
5600 client_c
5601 .authenticate_and_connect(false, &cx_c.to_async())
5602 .await
5603 .unwrap();
5604
5605 executor.run_until_parked();
5606 assert_eq!(
5607 contacts(&client_a, cx_a),
5608 [
5609 ("user_b".to_string(), "online", "free"),
5610 ("user_c".to_string(), "online", "free")
5611 ]
5612 );
5613 assert_eq!(
5614 contacts(&client_b, cx_b),
5615 [
5616 ("user_a".to_string(), "online", "free"),
5617 ("user_c".to_string(), "online", "free")
5618 ]
5619 );
5620 assert_eq!(
5621 contacts(&client_c, cx_c),
5622 [
5623 ("user_a".to_string(), "online", "free"),
5624 ("user_b".to_string(), "online", "free")
5625 ]
5626 );
5627 assert_eq!(contacts(&client_d, cx_d), []);
5628
5629 active_call_a
5630 .update(cx_a, |call, cx| {
5631 call.invite(client_b.user_id().unwrap(), None, cx)
5632 })
5633 .await
5634 .unwrap();
5635 executor.run_until_parked();
5636 assert_eq!(
5637 contacts(&client_a, cx_a),
5638 [
5639 ("user_b".to_string(), "online", "busy"),
5640 ("user_c".to_string(), "online", "free")
5641 ]
5642 );
5643 assert_eq!(
5644 contacts(&client_b, cx_b),
5645 [
5646 ("user_a".to_string(), "online", "busy"),
5647 ("user_c".to_string(), "online", "free")
5648 ]
5649 );
5650 assert_eq!(
5651 contacts(&client_c, cx_c),
5652 [
5653 ("user_a".to_string(), "online", "busy"),
5654 ("user_b".to_string(), "online", "busy")
5655 ]
5656 );
5657 assert_eq!(contacts(&client_d, cx_d), []);
5658
5659 // Client B and client D become contacts while client B is being called.
5660 server
5661 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5662 .await;
5663 executor.run_until_parked();
5664 assert_eq!(
5665 contacts(&client_a, cx_a),
5666 [
5667 ("user_b".to_string(), "online", "busy"),
5668 ("user_c".to_string(), "online", "free")
5669 ]
5670 );
5671 assert_eq!(
5672 contacts(&client_b, cx_b),
5673 [
5674 ("user_a".to_string(), "online", "busy"),
5675 ("user_c".to_string(), "online", "free"),
5676 ("user_d".to_string(), "online", "free"),
5677 ]
5678 );
5679 assert_eq!(
5680 contacts(&client_c, cx_c),
5681 [
5682 ("user_a".to_string(), "online", "busy"),
5683 ("user_b".to_string(), "online", "busy")
5684 ]
5685 );
5686 assert_eq!(
5687 contacts(&client_d, cx_d),
5688 [("user_b".to_string(), "online", "busy")]
5689 );
5690
5691 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5692 executor.run_until_parked();
5693 assert_eq!(
5694 contacts(&client_a, cx_a),
5695 [
5696 ("user_b".to_string(), "online", "free"),
5697 ("user_c".to_string(), "online", "free")
5698 ]
5699 );
5700 assert_eq!(
5701 contacts(&client_b, cx_b),
5702 [
5703 ("user_a".to_string(), "online", "free"),
5704 ("user_c".to_string(), "online", "free"),
5705 ("user_d".to_string(), "online", "free")
5706 ]
5707 );
5708 assert_eq!(
5709 contacts(&client_c, cx_c),
5710 [
5711 ("user_a".to_string(), "online", "free"),
5712 ("user_b".to_string(), "online", "free")
5713 ]
5714 );
5715 assert_eq!(
5716 contacts(&client_d, cx_d),
5717 [("user_b".to_string(), "online", "free")]
5718 );
5719
5720 active_call_c
5721 .update(cx_c, |call, cx| {
5722 call.invite(client_a.user_id().unwrap(), None, cx)
5723 })
5724 .await
5725 .unwrap();
5726 executor.run_until_parked();
5727 assert_eq!(
5728 contacts(&client_a, cx_a),
5729 [
5730 ("user_b".to_string(), "online", "free"),
5731 ("user_c".to_string(), "online", "busy")
5732 ]
5733 );
5734 assert_eq!(
5735 contacts(&client_b, cx_b),
5736 [
5737 ("user_a".to_string(), "online", "busy"),
5738 ("user_c".to_string(), "online", "busy"),
5739 ("user_d".to_string(), "online", "free")
5740 ]
5741 );
5742 assert_eq!(
5743 contacts(&client_c, cx_c),
5744 [
5745 ("user_a".to_string(), "online", "busy"),
5746 ("user_b".to_string(), "online", "free")
5747 ]
5748 );
5749 assert_eq!(
5750 contacts(&client_d, cx_d),
5751 [("user_b".to_string(), "online", "free")]
5752 );
5753
5754 active_call_a
5755 .update(cx_a, |call, cx| call.accept_incoming(cx))
5756 .await
5757 .unwrap();
5758 executor.run_until_parked();
5759 assert_eq!(
5760 contacts(&client_a, cx_a),
5761 [
5762 ("user_b".to_string(), "online", "free"),
5763 ("user_c".to_string(), "online", "busy")
5764 ]
5765 );
5766 assert_eq!(
5767 contacts(&client_b, cx_b),
5768 [
5769 ("user_a".to_string(), "online", "busy"),
5770 ("user_c".to_string(), "online", "busy"),
5771 ("user_d".to_string(), "online", "free")
5772 ]
5773 );
5774 assert_eq!(
5775 contacts(&client_c, cx_c),
5776 [
5777 ("user_a".to_string(), "online", "busy"),
5778 ("user_b".to_string(), "online", "free")
5779 ]
5780 );
5781 assert_eq!(
5782 contacts(&client_d, cx_d),
5783 [("user_b".to_string(), "online", "free")]
5784 );
5785
5786 active_call_a
5787 .update(cx_a, |call, cx| {
5788 call.invite(client_b.user_id().unwrap(), None, cx)
5789 })
5790 .await
5791 .unwrap();
5792 executor.run_until_parked();
5793 assert_eq!(
5794 contacts(&client_a, cx_a),
5795 [
5796 ("user_b".to_string(), "online", "busy"),
5797 ("user_c".to_string(), "online", "busy")
5798 ]
5799 );
5800 assert_eq!(
5801 contacts(&client_b, cx_b),
5802 [
5803 ("user_a".to_string(), "online", "busy"),
5804 ("user_c".to_string(), "online", "busy"),
5805 ("user_d".to_string(), "online", "free")
5806 ]
5807 );
5808 assert_eq!(
5809 contacts(&client_c, cx_c),
5810 [
5811 ("user_a".to_string(), "online", "busy"),
5812 ("user_b".to_string(), "online", "busy")
5813 ]
5814 );
5815 assert_eq!(
5816 contacts(&client_d, cx_d),
5817 [("user_b".to_string(), "online", "busy")]
5818 );
5819
5820 active_call_a
5821 .update(cx_a, |call, cx| call.hang_up(cx))
5822 .await
5823 .unwrap();
5824 executor.run_until_parked();
5825 assert_eq!(
5826 contacts(&client_a, cx_a),
5827 [
5828 ("user_b".to_string(), "online", "free"),
5829 ("user_c".to_string(), "online", "free")
5830 ]
5831 );
5832 assert_eq!(
5833 contacts(&client_b, cx_b),
5834 [
5835 ("user_a".to_string(), "online", "free"),
5836 ("user_c".to_string(), "online", "free"),
5837 ("user_d".to_string(), "online", "free")
5838 ]
5839 );
5840 assert_eq!(
5841 contacts(&client_c, cx_c),
5842 [
5843 ("user_a".to_string(), "online", "free"),
5844 ("user_b".to_string(), "online", "free")
5845 ]
5846 );
5847 assert_eq!(
5848 contacts(&client_d, cx_d),
5849 [("user_b".to_string(), "online", "free")]
5850 );
5851
5852 active_call_a
5853 .update(cx_a, |call, cx| {
5854 call.invite(client_b.user_id().unwrap(), None, cx)
5855 })
5856 .await
5857 .unwrap();
5858 executor.run_until_parked();
5859 assert_eq!(
5860 contacts(&client_a, cx_a),
5861 [
5862 ("user_b".to_string(), "online", "busy"),
5863 ("user_c".to_string(), "online", "free")
5864 ]
5865 );
5866 assert_eq!(
5867 contacts(&client_b, cx_b),
5868 [
5869 ("user_a".to_string(), "online", "busy"),
5870 ("user_c".to_string(), "online", "free"),
5871 ("user_d".to_string(), "online", "free")
5872 ]
5873 );
5874 assert_eq!(
5875 contacts(&client_c, cx_c),
5876 [
5877 ("user_a".to_string(), "online", "busy"),
5878 ("user_b".to_string(), "online", "busy")
5879 ]
5880 );
5881 assert_eq!(
5882 contacts(&client_d, cx_d),
5883 [("user_b".to_string(), "online", "busy")]
5884 );
5885
5886 server.forbid_connections();
5887 server.disconnect_client(client_a.peer_id().unwrap());
5888 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5889 assert_eq!(contacts(&client_a, cx_a), []);
5890 assert_eq!(
5891 contacts(&client_b, cx_b),
5892 [
5893 ("user_a".to_string(), "offline", "free"),
5894 ("user_c".to_string(), "online", "free"),
5895 ("user_d".to_string(), "online", "free")
5896 ]
5897 );
5898 assert_eq!(
5899 contacts(&client_c, cx_c),
5900 [
5901 ("user_a".to_string(), "offline", "free"),
5902 ("user_b".to_string(), "online", "free")
5903 ]
5904 );
5905 assert_eq!(
5906 contacts(&client_d, cx_d),
5907 [("user_b".to_string(), "online", "free")]
5908 );
5909
5910 // Test removing a contact
5911 client_b
5912 .user_store()
5913 .update(cx_b, |store, cx| {
5914 store.remove_contact(client_c.user_id().unwrap(), cx)
5915 })
5916 .await
5917 .unwrap();
5918 executor.run_until_parked();
5919 assert_eq!(
5920 contacts(&client_b, cx_b),
5921 [
5922 ("user_a".to_string(), "offline", "free"),
5923 ("user_d".to_string(), "online", "free")
5924 ]
5925 );
5926 assert_eq!(
5927 contacts(&client_c, cx_c),
5928 [("user_a".to_string(), "offline", "free"),]
5929 );
5930
5931 fn contacts(
5932 client: &TestClient,
5933 cx: &TestAppContext,
5934 ) -> Vec<(String, &'static str, &'static str)> {
5935 client.user_store().read_with(cx, |store, _| {
5936 store
5937 .contacts()
5938 .iter()
5939 .map(|contact| {
5940 (
5941 contact.user.github_login.clone(),
5942 if contact.online { "online" } else { "offline" },
5943 if contact.busy { "busy" } else { "free" },
5944 )
5945 })
5946 .collect()
5947 })
5948 }
5949}
5950
5951#[gpui::test(iterations = 10)]
5952async fn test_contact_requests(
5953 executor: BackgroundExecutor,
5954 cx_a: &mut TestAppContext,
5955 cx_a2: &mut TestAppContext,
5956 cx_b: &mut TestAppContext,
5957 cx_b2: &mut TestAppContext,
5958 cx_c: &mut TestAppContext,
5959 cx_c2: &mut TestAppContext,
5960) {
5961 // Connect to a server as 3 clients.
5962 let mut server = TestServer::start(executor.clone()).await;
5963 let client_a = server.create_client(cx_a, "user_a").await;
5964 let client_a2 = server.create_client(cx_a2, "user_a").await;
5965 let client_b = server.create_client(cx_b, "user_b").await;
5966 let client_b2 = server.create_client(cx_b2, "user_b").await;
5967 let client_c = server.create_client(cx_c, "user_c").await;
5968 let client_c2 = server.create_client(cx_c2, "user_c").await;
5969
5970 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5971 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5972 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5973
5974 // User A and User C request that user B become their contact.
5975 client_a
5976 .user_store()
5977 .update(cx_a, |store, cx| {
5978 store.request_contact(client_b.user_id().unwrap(), cx)
5979 })
5980 .await
5981 .unwrap();
5982 client_c
5983 .user_store()
5984 .update(cx_c, |store, cx| {
5985 store.request_contact(client_b.user_id().unwrap(), cx)
5986 })
5987 .await
5988 .unwrap();
5989 executor.run_until_parked();
5990
5991 // All users see the pending request appear in all their clients.
5992 assert_eq!(
5993 client_a.summarize_contacts(cx_a).outgoing_requests,
5994 &["user_b"]
5995 );
5996 assert_eq!(
5997 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5998 &["user_b"]
5999 );
6000 assert_eq!(
6001 client_b.summarize_contacts(cx_b).incoming_requests,
6002 &["user_a", "user_c"]
6003 );
6004 assert_eq!(
6005 client_b2.summarize_contacts(cx_b2).incoming_requests,
6006 &["user_a", "user_c"]
6007 );
6008 assert_eq!(
6009 client_c.summarize_contacts(cx_c).outgoing_requests,
6010 &["user_b"]
6011 );
6012 assert_eq!(
6013 client_c2.summarize_contacts(cx_c2).outgoing_requests,
6014 &["user_b"]
6015 );
6016
6017 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
6018 disconnect_and_reconnect(&client_a, cx_a).await;
6019 disconnect_and_reconnect(&client_b, cx_b).await;
6020 disconnect_and_reconnect(&client_c, cx_c).await;
6021 executor.run_until_parked();
6022 assert_eq!(
6023 client_a.summarize_contacts(cx_a).outgoing_requests,
6024 &["user_b"]
6025 );
6026 assert_eq!(
6027 client_b.summarize_contacts(cx_b).incoming_requests,
6028 &["user_a", "user_c"]
6029 );
6030 assert_eq!(
6031 client_c.summarize_contacts(cx_c).outgoing_requests,
6032 &["user_b"]
6033 );
6034
6035 // User B accepts the request from user A.
6036 client_b
6037 .user_store()
6038 .update(cx_b, |store, cx| {
6039 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
6040 })
6041 .await
6042 .unwrap();
6043
6044 executor.run_until_parked();
6045
6046 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
6047 let contacts_b = client_b.summarize_contacts(cx_b);
6048 assert_eq!(contacts_b.current, &["user_a"]);
6049 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
6050 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6051 assert_eq!(contacts_b2.current, &["user_a"]);
6052 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
6053
6054 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
6055 let contacts_a = client_a.summarize_contacts(cx_a);
6056 assert_eq!(contacts_a.current, &["user_b"]);
6057 assert!(contacts_a.outgoing_requests.is_empty());
6058 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
6059 assert_eq!(contacts_a2.current, &["user_b"]);
6060 assert!(contacts_a2.outgoing_requests.is_empty());
6061
6062 // Contacts are present upon connecting (tested here via disconnect/reconnect)
6063 disconnect_and_reconnect(&client_a, cx_a).await;
6064 disconnect_and_reconnect(&client_b, cx_b).await;
6065 disconnect_and_reconnect(&client_c, cx_c).await;
6066 executor.run_until_parked();
6067 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6068 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6069 assert_eq!(
6070 client_b.summarize_contacts(cx_b).incoming_requests,
6071 &["user_c"]
6072 );
6073 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6074 assert_eq!(
6075 client_c.summarize_contacts(cx_c).outgoing_requests,
6076 &["user_b"]
6077 );
6078
6079 // User B rejects the request from user C.
6080 client_b
6081 .user_store()
6082 .update(cx_b, |store, cx| {
6083 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
6084 })
6085 .await
6086 .unwrap();
6087
6088 executor.run_until_parked();
6089
6090 // User B doesn't see user C as their contact, and the incoming request from them is removed.
6091 let contacts_b = client_b.summarize_contacts(cx_b);
6092 assert_eq!(contacts_b.current, &["user_a"]);
6093 assert!(contacts_b.incoming_requests.is_empty());
6094 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6095 assert_eq!(contacts_b2.current, &["user_a"]);
6096 assert!(contacts_b2.incoming_requests.is_empty());
6097
6098 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
6099 let contacts_c = client_c.summarize_contacts(cx_c);
6100 assert!(contacts_c.current.is_empty());
6101 assert!(contacts_c.outgoing_requests.is_empty());
6102 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
6103 assert!(contacts_c2.current.is_empty());
6104 assert!(contacts_c2.outgoing_requests.is_empty());
6105
6106 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
6107 disconnect_and_reconnect(&client_a, cx_a).await;
6108 disconnect_and_reconnect(&client_b, cx_b).await;
6109 disconnect_and_reconnect(&client_c, cx_c).await;
6110 executor.run_until_parked();
6111 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6112 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6113 assert!(client_b
6114 .summarize_contacts(cx_b)
6115 .incoming_requests
6116 .is_empty());
6117 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6118 assert!(client_c
6119 .summarize_contacts(cx_c)
6120 .outgoing_requests
6121 .is_empty());
6122
6123 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6124 client.disconnect(&cx.to_async());
6125 client.clear_contacts(cx).await;
6126 client
6127 .authenticate_and_connect(false, &cx.to_async())
6128 .await
6129 .unwrap();
6130 }
6131}
6132
6133// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK
6134#[cfg(not(target_os = "macos"))]
6135#[gpui::test(iterations = 10)]
6136async fn test_join_call_after_screen_was_shared(
6137 executor: BackgroundExecutor,
6138 cx_a: &mut TestAppContext,
6139 cx_b: &mut TestAppContext,
6140) {
6141 let mut server = TestServer::start(executor.clone()).await;
6142
6143 let client_a = server.create_client(cx_a, "user_a").await;
6144 let client_b = server.create_client(cx_b, "user_b").await;
6145 server
6146 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6147 .await;
6148
6149 let active_call_a = cx_a.read(ActiveCall::global);
6150 let active_call_b = cx_b.read(ActiveCall::global);
6151
6152 // Call users B and C from client A.
6153 active_call_a
6154 .update(cx_a, |call, cx| {
6155 call.invite(client_b.user_id().unwrap(), None, cx)
6156 })
6157 .await
6158 .unwrap();
6159
6160 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6161 executor.run_until_parked();
6162 assert_eq!(
6163 room_participants(&room_a, cx_a),
6164 RoomParticipants {
6165 remote: Default::default(),
6166 pending: vec!["user_b".to_string()]
6167 }
6168 );
6169
6170 // User B receives the call.
6171
6172 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6173 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6174 assert_eq!(call_b.calling_user.github_login, "user_a");
6175
6176 // User A shares their screen
6177 let display = gpui::TestScreenCaptureSource::new();
6178 cx_a.set_screen_capture_sources(vec![display]);
6179 active_call_a
6180 .update(cx_a, |call, cx| {
6181 call.room()
6182 .unwrap()
6183 .update(cx, |room, cx| room.share_screen(cx))
6184 })
6185 .await
6186 .unwrap();
6187
6188 client_b.user_store().update(cx_b, |user_store, _| {
6189 user_store.clear_cache();
6190 });
6191
6192 // User B joins the room
6193 active_call_b
6194 .update(cx_b, |call, cx| call.accept_incoming(cx))
6195 .await
6196 .unwrap();
6197
6198 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6199 assert!(incoming_call_b.next().await.unwrap().is_none());
6200
6201 executor.run_until_parked();
6202 assert_eq!(
6203 room_participants(&room_a, cx_a),
6204 RoomParticipants {
6205 remote: vec!["user_b".to_string()],
6206 pending: vec![],
6207 }
6208 );
6209 assert_eq!(
6210 room_participants(&room_b, cx_b),
6211 RoomParticipants {
6212 remote: vec!["user_a".to_string()],
6213 pending: vec![],
6214 }
6215 );
6216
6217 // Ensure User B sees User A's screenshare.
6218
6219 room_b.read_with(cx_b, |room, _| {
6220 assert_eq!(
6221 room.remote_participants()
6222 .get(&client_a.user_id().unwrap())
6223 .unwrap()
6224 .video_tracks
6225 .len(),
6226 1
6227 );
6228 });
6229}
6230
6231#[gpui::test]
6232async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
6233 let mut server = TestServer::start(cx.executor().clone()).await;
6234 let client_a = server.create_client(cx, "user_a").await;
6235 let (_workspace_a, cx) = client_a.build_test_workspace(cx).await;
6236
6237 cx.simulate_resize(size(px(300.), px(300.)));
6238
6239 cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
6240 cx.update(|window, _cx| window.refresh());
6241
6242 let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
6243 let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
6244
6245 assert!(
6246 tab_bounds.intersects(&new_tab_button_bounds),
6247 "Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
6248 );
6249
6250 cx.simulate_event(MouseDownEvent {
6251 button: MouseButton::Right,
6252 position: new_tab_button_bounds.center(),
6253 modifiers: Modifiers::default(),
6254 click_count: 1,
6255 first_mouse: false,
6256 });
6257
6258 // regression test that the right click menu for tabs does not open.
6259 assert!(cx.debug_bounds("MENU_ITEM-Close").is_none());
6260
6261 let tab_bounds = cx.debug_bounds("TAB-1").unwrap();
6262 cx.simulate_event(MouseDownEvent {
6263 button: MouseButton::Right,
6264 position: tab_bounds.center(),
6265 modifiers: Modifiers::default(),
6266 click_count: 1,
6267 first_mouse: false,
6268 });
6269 assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
6270}
6271
6272#[gpui::test]
6273async fn test_pane_split_left(cx: &mut TestAppContext) {
6274 let (_, client) = TestServer::start1(cx).await;
6275 let (workspace, cx) = client.build_test_workspace(cx).await;
6276
6277 cx.simulate_keystrokes("cmd-n");
6278 workspace.update(cx, |workspace, cx| {
6279 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
6280 });
6281 cx.simulate_keystrokes("cmd-k left");
6282 workspace.update(cx, |workspace, cx| {
6283 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6284 });
6285 cx.simulate_keystrokes("cmd-k");
6286 // sleep for longer than the timeout in keyboard shortcut handling
6287 // to verify that it doesn't fire in this case.
6288 cx.executor().advance_clock(Duration::from_secs(2));
6289 cx.simulate_keystrokes("left");
6290 workspace.update(cx, |workspace, cx| {
6291 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6292 });
6293}
6294
6295#[gpui::test]
6296async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppContext) {
6297 let (mut server, client) = TestServer::start1(cx1).await;
6298 let channel1 = server.make_public_channel("channel1", &client, cx1).await;
6299 let channel2 = server.make_public_channel("channel2", &client, cx1).await;
6300
6301 join_channel(channel1, &client, cx1).await.unwrap();
6302 drop(client);
6303
6304 let client2 = server.create_client(cx2, "user_a").await;
6305 join_channel(channel2, &client2, cx2).await.unwrap();
6306}
6307
6308#[gpui::test]
6309async fn test_preview_tabs(cx: &mut TestAppContext) {
6310 let (_server, client) = TestServer::start1(cx).await;
6311 let (workspace, cx) = client.build_test_workspace(cx).await;
6312 let project = workspace.update(cx, |workspace, _| workspace.project().clone());
6313
6314 let worktree_id = project.update(cx, |project, cx| {
6315 project.worktrees(cx).next().unwrap().read(cx).id()
6316 });
6317
6318 let path_1 = ProjectPath {
6319 worktree_id,
6320 path: Path::new("1.txt").into(),
6321 };
6322 let path_2 = ProjectPath {
6323 worktree_id,
6324 path: Path::new("2.js").into(),
6325 };
6326 let path_3 = ProjectPath {
6327 worktree_id,
6328 path: Path::new("3.rs").into(),
6329 };
6330
6331 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6332
6333 let get_path = |pane: &Pane, idx: usize, cx: &App| {
6334 pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
6335 };
6336
6337 // Opening item 3 as a "permanent" tab
6338 workspace
6339 .update_in(cx, |workspace, window, cx| {
6340 workspace.open_path(path_3.clone(), None, false, window, cx)
6341 })
6342 .await
6343 .unwrap();
6344
6345 pane.update(cx, |pane, cx| {
6346 assert_eq!(pane.items_len(), 1);
6347 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6348 assert_eq!(pane.preview_item_id(), None);
6349
6350 assert!(!pane.can_navigate_backward());
6351 assert!(!pane.can_navigate_forward());
6352 });
6353
6354 // Open item 1 as preview
6355 workspace
6356 .update_in(cx, |workspace, window, cx| {
6357 workspace.open_path_preview(path_1.clone(), None, true, true, true, window, cx)
6358 })
6359 .await
6360 .unwrap();
6361
6362 pane.update(cx, |pane, cx| {
6363 assert_eq!(pane.items_len(), 2);
6364 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6365 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6366 assert_eq!(
6367 pane.preview_item_id(),
6368 Some(pane.items().nth(1).unwrap().item_id())
6369 );
6370
6371 assert!(pane.can_navigate_backward());
6372 assert!(!pane.can_navigate_forward());
6373 });
6374
6375 // Open item 2 as preview
6376 workspace
6377 .update_in(cx, |workspace, window, cx| {
6378 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6379 })
6380 .await
6381 .unwrap();
6382
6383 pane.update(cx, |pane, cx| {
6384 assert_eq!(pane.items_len(), 2);
6385 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6386 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6387 assert_eq!(
6388 pane.preview_item_id(),
6389 Some(pane.items().nth(1).unwrap().item_id())
6390 );
6391
6392 assert!(pane.can_navigate_backward());
6393 assert!(!pane.can_navigate_forward());
6394 });
6395
6396 // Going back should show item 1 as preview
6397 workspace
6398 .update_in(cx, |workspace, window, cx| {
6399 workspace.go_back(pane.downgrade(), window, cx)
6400 })
6401 .await
6402 .unwrap();
6403
6404 pane.update(cx, |pane, cx| {
6405 assert_eq!(pane.items_len(), 2);
6406 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6407 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6408 assert_eq!(
6409 pane.preview_item_id(),
6410 Some(pane.items().nth(1).unwrap().item_id())
6411 );
6412
6413 assert!(pane.can_navigate_backward());
6414 assert!(pane.can_navigate_forward());
6415 });
6416
6417 // Closing item 1
6418 pane.update_in(cx, |pane, window, cx| {
6419 pane.close_item_by_id(
6420 pane.active_item().unwrap().item_id(),
6421 workspace::SaveIntent::Skip,
6422 window,
6423 cx,
6424 )
6425 })
6426 .await
6427 .unwrap();
6428
6429 pane.update(cx, |pane, cx| {
6430 assert_eq!(pane.items_len(), 1);
6431 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6432 assert_eq!(pane.preview_item_id(), None);
6433
6434 assert!(pane.can_navigate_backward());
6435 assert!(!pane.can_navigate_forward());
6436 });
6437
6438 // Going back should show item 1 as preview
6439 workspace
6440 .update_in(cx, |workspace, window, cx| {
6441 workspace.go_back(pane.downgrade(), window, cx)
6442 })
6443 .await
6444 .unwrap();
6445
6446 pane.update(cx, |pane, cx| {
6447 assert_eq!(pane.items_len(), 2);
6448 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6449 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6450 assert_eq!(
6451 pane.preview_item_id(),
6452 Some(pane.items().nth(1).unwrap().item_id())
6453 );
6454
6455 assert!(pane.can_navigate_backward());
6456 assert!(pane.can_navigate_forward());
6457 });
6458
6459 // Close permanent tab
6460 pane.update_in(cx, |pane, window, cx| {
6461 let id = pane.items().next().unwrap().item_id();
6462 pane.close_item_by_id(id, workspace::SaveIntent::Skip, window, cx)
6463 })
6464 .await
6465 .unwrap();
6466
6467 pane.update(cx, |pane, cx| {
6468 assert_eq!(pane.items_len(), 1);
6469 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6470 assert_eq!(
6471 pane.preview_item_id(),
6472 Some(pane.items().next().unwrap().item_id())
6473 );
6474
6475 assert!(pane.can_navigate_backward());
6476 assert!(pane.can_navigate_forward());
6477 });
6478
6479 // Split pane to the right
6480 pane.update(cx, |pane, cx| {
6481 pane.split(workspace::SplitDirection::Right, cx);
6482 });
6483
6484 let right_pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6485
6486 pane.update(cx, |pane, cx| {
6487 assert_eq!(pane.items_len(), 1);
6488 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6489 assert_eq!(
6490 pane.preview_item_id(),
6491 Some(pane.items().next().unwrap().item_id())
6492 );
6493
6494 assert!(pane.can_navigate_backward());
6495 assert!(pane.can_navigate_forward());
6496 });
6497
6498 right_pane.update(cx, |pane, cx| {
6499 assert_eq!(pane.items_len(), 1);
6500 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6501 assert_eq!(pane.preview_item_id(), None);
6502
6503 assert!(!pane.can_navigate_backward());
6504 assert!(!pane.can_navigate_forward());
6505 });
6506
6507 // Open item 2 as preview in right pane
6508 workspace
6509 .update_in(cx, |workspace, window, cx| {
6510 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6511 })
6512 .await
6513 .unwrap();
6514
6515 pane.update(cx, |pane, cx| {
6516 assert_eq!(pane.items_len(), 1);
6517 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6518 assert_eq!(
6519 pane.preview_item_id(),
6520 Some(pane.items().next().unwrap().item_id())
6521 );
6522
6523 assert!(pane.can_navigate_backward());
6524 assert!(pane.can_navigate_forward());
6525 });
6526
6527 right_pane.update(cx, |pane, cx| {
6528 assert_eq!(pane.items_len(), 2);
6529 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6530 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6531 assert_eq!(
6532 pane.preview_item_id(),
6533 Some(pane.items().nth(1).unwrap().item_id())
6534 );
6535
6536 assert!(pane.can_navigate_backward());
6537 assert!(!pane.can_navigate_forward());
6538 });
6539
6540 // Focus left pane
6541 workspace.update_in(cx, |workspace, window, cx| {
6542 workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx)
6543 });
6544
6545 // Open item 2 as preview in left pane
6546 workspace
6547 .update_in(cx, |workspace, window, cx| {
6548 workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
6549 })
6550 .await
6551 .unwrap();
6552
6553 pane.update(cx, |pane, cx| {
6554 assert_eq!(pane.items_len(), 1);
6555 assert_eq!(get_path(pane, 0, cx), path_2.clone());
6556 assert_eq!(
6557 pane.preview_item_id(),
6558 Some(pane.items().next().unwrap().item_id())
6559 );
6560
6561 assert!(pane.can_navigate_backward());
6562 assert!(!pane.can_navigate_forward());
6563 });
6564
6565 right_pane.update(cx, |pane, cx| {
6566 assert_eq!(pane.items_len(), 2);
6567 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6568 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6569 assert_eq!(
6570 pane.preview_item_id(),
6571 Some(pane.items().nth(1).unwrap().item_id())
6572 );
6573
6574 assert!(pane.can_navigate_backward());
6575 assert!(!pane.can_navigate_forward());
6576 });
6577}
6578
6579#[gpui::test(iterations = 10)]
6580async fn test_context_collaboration_with_reconnect(
6581 executor: BackgroundExecutor,
6582 cx_a: &mut TestAppContext,
6583 cx_b: &mut TestAppContext,
6584) {
6585 let mut server = TestServer::start(executor.clone()).await;
6586 let client_a = server.create_client(cx_a, "user_a").await;
6587 let client_b = server.create_client(cx_b, "user_b").await;
6588 server
6589 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6590 .await;
6591 let active_call_a = cx_a.read(ActiveCall::global);
6592
6593 client_a.fs().insert_tree("/a", Default::default()).await;
6594 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
6595 let project_id = active_call_a
6596 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6597 .await
6598 .unwrap();
6599 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6600
6601 // Client A sees that a guest has joined.
6602 executor.run_until_parked();
6603
6604 project_a.read_with(cx_a, |project, _| {
6605 assert_eq!(project.collaborators().len(), 1);
6606 });
6607 project_b.read_with(cx_b, |project, _| {
6608 assert_eq!(project.collaborators().len(), 1);
6609 });
6610
6611 cx_a.update(context_server::init);
6612 cx_b.update(context_server::init);
6613 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
6614 let context_store_a = cx_a
6615 .update(|cx| {
6616 ContextStore::new(
6617 project_a.clone(),
6618 prompt_builder.clone(),
6619 Arc::new(SlashCommandWorkingSet::default()),
6620 cx,
6621 )
6622 })
6623 .await
6624 .unwrap();
6625 let context_store_b = cx_b
6626 .update(|cx| {
6627 ContextStore::new(
6628 project_b.clone(),
6629 prompt_builder.clone(),
6630 Arc::new(SlashCommandWorkingSet::default()),
6631 cx,
6632 )
6633 })
6634 .await
6635 .unwrap();
6636
6637 // Client A creates a new chats.
6638 let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
6639 executor.run_until_parked();
6640
6641 // Client B retrieves host's contexts and joins one.
6642 let context_b = context_store_b
6643 .update(cx_b, |store, cx| {
6644 let host_contexts = store.host_contexts().to_vec();
6645 assert_eq!(host_contexts.len(), 1);
6646 store.open_remote_context(host_contexts[0].id.clone(), cx)
6647 })
6648 .await
6649 .unwrap();
6650
6651 // Host and guest make changes
6652 context_a.update(cx_a, |context, cx| {
6653 context.buffer().update(cx, |buffer, cx| {
6654 buffer.edit([(0..0, "Host change\n")], None, cx)
6655 })
6656 });
6657 context_b.update(cx_b, |context, cx| {
6658 context.buffer().update(cx, |buffer, cx| {
6659 buffer.edit([(0..0, "Guest change\n")], None, cx)
6660 })
6661 });
6662 executor.run_until_parked();
6663 assert_eq!(
6664 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6665 "Guest change\nHost change\n"
6666 );
6667 assert_eq!(
6668 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6669 "Guest change\nHost change\n"
6670 );
6671
6672 // Disconnect client A and make some changes while disconnected.
6673 server.disconnect_client(client_a.peer_id().unwrap());
6674 server.forbid_connections();
6675 context_a.update(cx_a, |context, cx| {
6676 context.buffer().update(cx, |buffer, cx| {
6677 buffer.edit([(0..0, "Host offline change\n")], None, cx)
6678 })
6679 });
6680 context_b.update(cx_b, |context, cx| {
6681 context.buffer().update(cx, |buffer, cx| {
6682 buffer.edit([(0..0, "Guest offline change\n")], None, cx)
6683 })
6684 });
6685 executor.run_until_parked();
6686 assert_eq!(
6687 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6688 "Host offline change\nGuest change\nHost change\n"
6689 );
6690 assert_eq!(
6691 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6692 "Guest offline change\nGuest change\nHost change\n"
6693 );
6694
6695 // Allow client A to reconnect and verify that contexts converge.
6696 server.allow_connections();
6697 executor.advance_clock(RECEIVE_TIMEOUT);
6698 assert_eq!(
6699 context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
6700 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6701 );
6702 assert_eq!(
6703 context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
6704 "Guest offline change\nHost offline change\nGuest change\nHost change\n"
6705 );
6706
6707 // Client A disconnects without being able to reconnect. Context B becomes readonly.
6708 server.forbid_connections();
6709 server.disconnect_client(client_a.peer_id().unwrap());
6710 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6711 context_b.read_with(cx_b, |context, cx| {
6712 assert!(context.buffer().read(cx).read_only());
6713 });
6714}
6715
6716#[gpui::test]
6717async fn test_remote_git_branches(
6718 executor: BackgroundExecutor,
6719 cx_a: &mut TestAppContext,
6720 cx_b: &mut TestAppContext,
6721) {
6722 let mut server = TestServer::start(executor.clone()).await;
6723 let client_a = server.create_client(cx_a, "user_a").await;
6724 let client_b = server.create_client(cx_b, "user_b").await;
6725 server
6726 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6727 .await;
6728 let active_call_a = cx_a.read(ActiveCall::global);
6729
6730 client_a
6731 .fs()
6732 .insert_tree("/project", serde_json::json!({ ".git":{} }))
6733 .await;
6734 let branches = ["main", "dev", "feature-1"];
6735 client_a
6736 .fs()
6737 .insert_branches(Path::new("/project/.git"), &branches);
6738 let branches_set = branches
6739 .into_iter()
6740 .map(ToString::to_string)
6741 .collect::<HashSet<_>>();
6742
6743 let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
6744
6745 let project_id = active_call_a
6746 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6747 .await
6748 .unwrap();
6749 let project_b = client_b.join_remote_project(project_id, cx_b).await;
6750
6751 // Client A sees that a guest has joined and the repo has been populated
6752 executor.run_until_parked();
6753
6754 let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
6755
6756 let root_path = ProjectPath::root_path(worktree_id);
6757
6758 let branches_b = cx_b
6759 .update(|cx| repo_b.update(cx, |repository, _| repository.branches()))
6760 .await
6761 .unwrap()
6762 .unwrap();
6763
6764 let new_branch = branches[2];
6765
6766 let branches_b = branches_b
6767 .into_iter()
6768 .map(|branch| branch.name.to_string())
6769 .collect::<HashSet<_>>();
6770
6771 assert_eq!(branches_b, branches_set);
6772
6773 cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
6774 .await
6775 .unwrap()
6776 .unwrap();
6777
6778 executor.run_until_parked();
6779
6780 let host_branch = cx_a.update(|cx| {
6781 project_a.update(cx, |project, cx| {
6782 project.worktree_store().update(cx, |worktree_store, cx| {
6783 worktree_store
6784 .current_branch(root_path.clone(), cx)
6785 .unwrap()
6786 })
6787 })
6788 });
6789
6790 assert_eq!(host_branch.name, branches[2]);
6791
6792 // Also try creating a new branch
6793 cx_b.update(|cx| {
6794 repo_b
6795 .read(cx)
6796 .create_branch("totally-new-branch".to_string())
6797 })
6798 .await
6799 .unwrap()
6800 .unwrap();
6801
6802 cx_b.update(|cx| {
6803 repo_b
6804 .read(cx)
6805 .change_branch("totally-new-branch".to_string())
6806 })
6807 .await
6808 .unwrap()
6809 .unwrap();
6810
6811 executor.run_until_parked();
6812
6813 let host_branch = cx_a.update(|cx| {
6814 project_a.update(cx, |project, cx| {
6815 project.worktree_store().update(cx, |worktree_store, cx| {
6816 worktree_store.current_branch(root_path, cx).unwrap()
6817 })
6818 })
6819 });
6820
6821 assert_eq!(host_branch.name, "totally-new-branch");
6822}