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