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