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