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