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