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