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