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