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