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.create_entry((worktree_id, "c.txt"), false, cx)
2985 })
2986 .await
2987 .unwrap()
2988 .unwrap();
2989 worktree_a.read_with(cx_a, |worktree, _| {
2990 assert_eq!(
2991 worktree
2992 .paths()
2993 .map(|p| p.to_string_lossy())
2994 .collect::<Vec<_>>(),
2995 ["a.txt", "b.txt", "c.txt"]
2996 );
2997 });
2998 worktree_b.read_with(cx_b, |worktree, _| {
2999 assert_eq!(
3000 worktree
3001 .paths()
3002 .map(|p| p.to_string_lossy())
3003 .collect::<Vec<_>>(),
3004 ["a.txt", "b.txt", "c.txt"]
3005 );
3006 });
3007
3008 project_b
3009 .update(cx_b, |project, cx| {
3010 project.rename_entry(entry.id, Path::new("d.txt"), cx)
3011 })
3012 .await
3013 .unwrap();
3014 worktree_a.read_with(cx_a, |worktree, _| {
3015 assert_eq!(
3016 worktree
3017 .paths()
3018 .map(|p| p.to_string_lossy())
3019 .collect::<Vec<_>>(),
3020 ["a.txt", "b.txt", "d.txt"]
3021 );
3022 });
3023 worktree_b.read_with(cx_b, |worktree, _| {
3024 assert_eq!(
3025 worktree
3026 .paths()
3027 .map(|p| p.to_string_lossy())
3028 .collect::<Vec<_>>(),
3029 ["a.txt", "b.txt", "d.txt"]
3030 );
3031 });
3032
3033 let dir_entry = project_b
3034 .update(cx_b, |project, cx| {
3035 project.create_entry((worktree_id, "DIR"), true, cx)
3036 })
3037 .await
3038 .unwrap()
3039 .unwrap();
3040 worktree_a.read_with(cx_a, |worktree, _| {
3041 assert_eq!(
3042 worktree
3043 .paths()
3044 .map(|p| p.to_string_lossy())
3045 .collect::<Vec<_>>(),
3046 ["DIR", "a.txt", "b.txt", "d.txt"]
3047 );
3048 });
3049 worktree_b.read_with(cx_b, |worktree, _| {
3050 assert_eq!(
3051 worktree
3052 .paths()
3053 .map(|p| p.to_string_lossy())
3054 .collect::<Vec<_>>(),
3055 ["DIR", "a.txt", "b.txt", "d.txt"]
3056 );
3057 });
3058
3059 project_b
3060 .update(cx_b, |project, cx| {
3061 project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
3062 })
3063 .await
3064 .unwrap();
3065 project_b
3066 .update(cx_b, |project, cx| {
3067 project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
3068 })
3069 .await
3070 .unwrap();
3071 project_b
3072 .update(cx_b, |project, cx| {
3073 project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
3074 })
3075 .await
3076 .unwrap();
3077 worktree_a.read_with(cx_a, |worktree, _| {
3078 assert_eq!(
3079 worktree
3080 .paths()
3081 .map(|p| p.to_string_lossy())
3082 .collect::<Vec<_>>(),
3083 [
3084 "DIR",
3085 "DIR/SUBDIR",
3086 "DIR/SUBDIR/f.txt",
3087 "DIR/e.txt",
3088 "a.txt",
3089 "b.txt",
3090 "d.txt"
3091 ]
3092 );
3093 });
3094 worktree_b.read_with(cx_b, |worktree, _| {
3095 assert_eq!(
3096 worktree
3097 .paths()
3098 .map(|p| p.to_string_lossy())
3099 .collect::<Vec<_>>(),
3100 [
3101 "DIR",
3102 "DIR/SUBDIR",
3103 "DIR/SUBDIR/f.txt",
3104 "DIR/e.txt",
3105 "a.txt",
3106 "b.txt",
3107 "d.txt"
3108 ]
3109 );
3110 });
3111
3112 project_b
3113 .update(cx_b, |project, cx| {
3114 project.copy_entry(entry.id, Path::new("f.txt"), cx)
3115 })
3116 .await
3117 .unwrap();
3118 worktree_a.read_with(cx_a, |worktree, _| {
3119 assert_eq!(
3120 worktree
3121 .paths()
3122 .map(|p| p.to_string_lossy())
3123 .collect::<Vec<_>>(),
3124 [
3125 "DIR",
3126 "DIR/SUBDIR",
3127 "DIR/SUBDIR/f.txt",
3128 "DIR/e.txt",
3129 "a.txt",
3130 "b.txt",
3131 "d.txt",
3132 "f.txt"
3133 ]
3134 );
3135 });
3136 worktree_b.read_with(cx_b, |worktree, _| {
3137 assert_eq!(
3138 worktree
3139 .paths()
3140 .map(|p| p.to_string_lossy())
3141 .collect::<Vec<_>>(),
3142 [
3143 "DIR",
3144 "DIR/SUBDIR",
3145 "DIR/SUBDIR/f.txt",
3146 "DIR/e.txt",
3147 "a.txt",
3148 "b.txt",
3149 "d.txt",
3150 "f.txt"
3151 ]
3152 );
3153 });
3154
3155 project_b
3156 .update(cx_b, |project, cx| {
3157 project.delete_entry(dir_entry.id, cx).unwrap()
3158 })
3159 .await
3160 .unwrap();
3161 deterministic.run_until_parked();
3162
3163 worktree_a.read_with(cx_a, |worktree, _| {
3164 assert_eq!(
3165 worktree
3166 .paths()
3167 .map(|p| p.to_string_lossy())
3168 .collect::<Vec<_>>(),
3169 ["a.txt", "b.txt", "d.txt", "f.txt"]
3170 );
3171 });
3172 worktree_b.read_with(cx_b, |worktree, _| {
3173 assert_eq!(
3174 worktree
3175 .paths()
3176 .map(|p| p.to_string_lossy())
3177 .collect::<Vec<_>>(),
3178 ["a.txt", "b.txt", "d.txt", "f.txt"]
3179 );
3180 });
3181
3182 project_b
3183 .update(cx_b, |project, cx| {
3184 project.delete_entry(entry.id, cx).unwrap()
3185 })
3186 .await
3187 .unwrap();
3188 worktree_a.read_with(cx_a, |worktree, _| {
3189 assert_eq!(
3190 worktree
3191 .paths()
3192 .map(|p| p.to_string_lossy())
3193 .collect::<Vec<_>>(),
3194 ["a.txt", "b.txt", "f.txt"]
3195 );
3196 });
3197 worktree_b.read_with(cx_b, |worktree, _| {
3198 assert_eq!(
3199 worktree
3200 .paths()
3201 .map(|p| p.to_string_lossy())
3202 .collect::<Vec<_>>(),
3203 ["a.txt", "b.txt", "f.txt"]
3204 );
3205 });
3206}
3207
3208#[gpui::test(iterations = 10)]
3209async fn test_local_settings(
3210 deterministic: Arc<Deterministic>,
3211 cx_a: &mut TestAppContext,
3212 cx_b: &mut TestAppContext,
3213) {
3214 deterministic.forbid_parking();
3215 let mut server = TestServer::start(&deterministic).await;
3216 let client_a = server.create_client(cx_a, "user_a").await;
3217 let client_b = server.create_client(cx_b, "user_b").await;
3218 server
3219 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3220 .await;
3221 let active_call_a = cx_a.read(ActiveCall::global);
3222
3223 // As client A, open a project that contains some local settings files
3224 client_a
3225 .fs()
3226 .insert_tree(
3227 "/dir",
3228 json!({
3229 ".zed": {
3230 "settings.json": r#"{ "tab_size": 2 }"#
3231 },
3232 "a": {
3233 ".zed": {
3234 "settings.json": r#"{ "tab_size": 8 }"#
3235 },
3236 "a.txt": "a-contents",
3237 },
3238 "b": {
3239 "b.txt": "b-contents",
3240 }
3241 }),
3242 )
3243 .await;
3244 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
3245 deterministic.run_until_parked();
3246 let project_id = active_call_a
3247 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3248 .await
3249 .unwrap();
3250
3251 // As client B, join that project and observe the local settings.
3252 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3253 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3254 deterministic.run_until_parked();
3255 cx_b.read(|cx| {
3256 let store = cx.global::<SettingsStore>();
3257 assert_eq!(
3258 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3259 &[
3260 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3261 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3262 ]
3263 )
3264 });
3265
3266 // As client A, update a settings file. As Client B, see the changed settings.
3267 client_a
3268 .fs()
3269 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3270 .await;
3271 deterministic.run_until_parked();
3272 cx_b.read(|cx| {
3273 let store = cx.global::<SettingsStore>();
3274 assert_eq!(
3275 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3276 &[
3277 (Path::new("").into(), r#"{}"#.to_string()),
3278 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3279 ]
3280 )
3281 });
3282
3283 // As client A, create and remove some settings files. As client B, see the changed settings.
3284 client_a
3285 .fs()
3286 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3287 .await
3288 .unwrap();
3289 client_a
3290 .fs()
3291 .create_dir("/dir/b/.zed".as_ref())
3292 .await
3293 .unwrap();
3294 client_a
3295 .fs()
3296 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3297 .await;
3298 deterministic.run_until_parked();
3299 cx_b.read(|cx| {
3300 let store = cx.global::<SettingsStore>();
3301 assert_eq!(
3302 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3303 &[
3304 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3305 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3306 ]
3307 )
3308 });
3309
3310 // As client B, disconnect.
3311 server.forbid_connections();
3312 server.disconnect_client(client_b.peer_id().unwrap());
3313
3314 // As client A, change and remove settings files while client B is disconnected.
3315 client_a
3316 .fs()
3317 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3318 .await;
3319 client_a
3320 .fs()
3321 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3322 .await
3323 .unwrap();
3324 deterministic.run_until_parked();
3325
3326 // As client B, reconnect and see the changed settings.
3327 server.allow_connections();
3328 deterministic.advance_clock(RECEIVE_TIMEOUT);
3329 cx_b.read(|cx| {
3330 let store = cx.global::<SettingsStore>();
3331 assert_eq!(
3332 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3333 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3334 )
3335 });
3336}
3337
3338#[gpui::test(iterations = 10)]
3339async fn test_buffer_conflict_after_save(
3340 deterministic: Arc<Deterministic>,
3341 cx_a: &mut TestAppContext,
3342 cx_b: &mut TestAppContext,
3343) {
3344 deterministic.forbid_parking();
3345 let mut server = TestServer::start(&deterministic).await;
3346 let client_a = server.create_client(cx_a, "user_a").await;
3347 let client_b = server.create_client(cx_b, "user_b").await;
3348 server
3349 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3350 .await;
3351 let active_call_a = cx_a.read(ActiveCall::global);
3352
3353 client_a
3354 .fs()
3355 .insert_tree(
3356 "/dir",
3357 json!({
3358 "a.txt": "a-contents",
3359 }),
3360 )
3361 .await;
3362 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3363 let project_id = active_call_a
3364 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3365 .await
3366 .unwrap();
3367 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3368
3369 // Open a buffer as client B
3370 let buffer_b = project_b
3371 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3372 .await
3373 .unwrap();
3374
3375 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3376 buffer_b.read_with(cx_b, |buf, _| {
3377 assert!(buf.is_dirty());
3378 assert!(!buf.has_conflict());
3379 });
3380
3381 project_b
3382 .update(cx_b, |project, cx| {
3383 project.save_buffer(buffer_b.clone(), cx)
3384 })
3385 .await
3386 .unwrap();
3387 cx_a.foreground().forbid_parking();
3388 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3389 buffer_b.read_with(cx_b, |buf, _| {
3390 assert!(!buf.has_conflict());
3391 });
3392
3393 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3394 buffer_b.read_with(cx_b, |buf, _| {
3395 assert!(buf.is_dirty());
3396 assert!(!buf.has_conflict());
3397 });
3398}
3399
3400#[gpui::test(iterations = 10)]
3401async fn test_buffer_reloading(
3402 deterministic: Arc<Deterministic>,
3403 cx_a: &mut TestAppContext,
3404 cx_b: &mut TestAppContext,
3405) {
3406 deterministic.forbid_parking();
3407 let mut server = TestServer::start(&deterministic).await;
3408 let client_a = server.create_client(cx_a, "user_a").await;
3409 let client_b = server.create_client(cx_b, "user_b").await;
3410 server
3411 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3412 .await;
3413 let active_call_a = cx_a.read(ActiveCall::global);
3414
3415 client_a
3416 .fs()
3417 .insert_tree(
3418 "/dir",
3419 json!({
3420 "a.txt": "a\nb\nc",
3421 }),
3422 )
3423 .await;
3424 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3425 let project_id = active_call_a
3426 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3427 .await
3428 .unwrap();
3429 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3430
3431 // Open a buffer as client B
3432 let buffer_b = project_b
3433 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3434 .await
3435 .unwrap();
3436 buffer_b.read_with(cx_b, |buf, _| {
3437 assert!(!buf.is_dirty());
3438 assert!(!buf.has_conflict());
3439 assert_eq!(buf.line_ending(), LineEnding::Unix);
3440 });
3441
3442 let new_contents = Rope::from("d\ne\nf");
3443 client_a
3444 .fs()
3445 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3446 .await
3447 .unwrap();
3448 cx_a.foreground().run_until_parked();
3449 buffer_b.read_with(cx_b, |buf, _| {
3450 assert_eq!(buf.text(), new_contents.to_string());
3451 assert!(!buf.is_dirty());
3452 assert!(!buf.has_conflict());
3453 assert_eq!(buf.line_ending(), LineEnding::Windows);
3454 });
3455}
3456
3457#[gpui::test(iterations = 10)]
3458async fn test_editing_while_guest_opens_buffer(
3459 deterministic: Arc<Deterministic>,
3460 cx_a: &mut TestAppContext,
3461 cx_b: &mut TestAppContext,
3462) {
3463 deterministic.forbid_parking();
3464 let mut server = TestServer::start(&deterministic).await;
3465 let client_a = server.create_client(cx_a, "user_a").await;
3466 let client_b = server.create_client(cx_b, "user_b").await;
3467 server
3468 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3469 .await;
3470 let active_call_a = cx_a.read(ActiveCall::global);
3471
3472 client_a
3473 .fs()
3474 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3475 .await;
3476 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3477 let project_id = active_call_a
3478 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3479 .await
3480 .unwrap();
3481 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3482
3483 // Open a buffer as client A
3484 let buffer_a = project_a
3485 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3486 .await
3487 .unwrap();
3488
3489 // Start opening the same buffer as client B
3490 let buffer_b = cx_b
3491 .background()
3492 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3493
3494 // Edit the buffer as client A while client B is still opening it.
3495 cx_b.background().simulate_random_delay().await;
3496 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3497 cx_b.background().simulate_random_delay().await;
3498 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3499
3500 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3501 let buffer_b = buffer_b.await.unwrap();
3502 cx_a.foreground().run_until_parked();
3503 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3504}
3505
3506#[gpui::test]
3507async fn test_newline_above_or_below_does_not_move_guest_cursor(
3508 deterministic: Arc<Deterministic>,
3509 cx_a: &mut TestAppContext,
3510 cx_b: &mut TestAppContext,
3511) {
3512 deterministic.forbid_parking();
3513 let mut server = TestServer::start(&deterministic).await;
3514 let client_a = server.create_client(cx_a, "user_a").await;
3515 let client_b = server.create_client(cx_b, "user_b").await;
3516 server
3517 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3518 .await;
3519 let active_call_a = cx_a.read(ActiveCall::global);
3520
3521 client_a
3522 .fs()
3523 .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
3524 .await;
3525 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3526 let project_id = active_call_a
3527 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3528 .await
3529 .unwrap();
3530
3531 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3532
3533 // Open a buffer as client A
3534 let buffer_a = project_a
3535 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3536 .await
3537 .unwrap();
3538 let window_a = cx_a.add_window(|_| EmptyView);
3539 let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
3540 let mut editor_cx_a = EditorTestContext {
3541 cx: cx_a,
3542 window: window_a.into(),
3543 editor: editor_a,
3544 };
3545
3546 // Open a buffer as client B
3547 let buffer_b = project_b
3548 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3549 .await
3550 .unwrap();
3551 let window_b = cx_b.add_window(|_| EmptyView);
3552 let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
3553 let mut editor_cx_b = EditorTestContext {
3554 cx: cx_b,
3555 window: window_b.into(),
3556 editor: editor_b,
3557 };
3558
3559 // Test newline above
3560 editor_cx_a.set_selections_state(indoc! {"
3561 Some textˇ
3562 "});
3563 editor_cx_b.set_selections_state(indoc! {"
3564 Some textˇ
3565 "});
3566 editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
3567 deterministic.run_until_parked();
3568 editor_cx_a.assert_editor_state(indoc! {"
3569 ˇ
3570 Some text
3571 "});
3572 editor_cx_b.assert_editor_state(indoc! {"
3573
3574 Some textˇ
3575 "});
3576
3577 // Test newline below
3578 editor_cx_a.set_selections_state(indoc! {"
3579
3580 Some textˇ
3581 "});
3582 editor_cx_b.set_selections_state(indoc! {"
3583
3584 Some textˇ
3585 "});
3586 editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
3587 deterministic.run_until_parked();
3588 editor_cx_a.assert_editor_state(indoc! {"
3589
3590 Some text
3591 ˇ
3592 "});
3593 editor_cx_b.assert_editor_state(indoc! {"
3594
3595 Some textˇ
3596
3597 "});
3598}
3599
3600#[gpui::test(iterations = 10)]
3601async fn test_leaving_worktree_while_opening_buffer(
3602 deterministic: Arc<Deterministic>,
3603 cx_a: &mut TestAppContext,
3604 cx_b: &mut TestAppContext,
3605) {
3606 deterministic.forbid_parking();
3607 let mut server = TestServer::start(&deterministic).await;
3608 let client_a = server.create_client(cx_a, "user_a").await;
3609 let client_b = server.create_client(cx_b, "user_b").await;
3610 server
3611 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3612 .await;
3613 let active_call_a = cx_a.read(ActiveCall::global);
3614
3615 client_a
3616 .fs()
3617 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3618 .await;
3619 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3620 let project_id = active_call_a
3621 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3622 .await
3623 .unwrap();
3624 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3625
3626 // See that a guest has joined as client A.
3627 cx_a.foreground().run_until_parked();
3628 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3629
3630 // Begin opening a buffer as client B, but leave the project before the open completes.
3631 let buffer_b = cx_b
3632 .background()
3633 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3634 cx_b.update(|_| drop(project_b));
3635 drop(buffer_b);
3636
3637 // See that the guest has left.
3638 cx_a.foreground().run_until_parked();
3639 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3640}
3641
3642#[gpui::test(iterations = 10)]
3643async fn test_canceling_buffer_opening(
3644 deterministic: Arc<Deterministic>,
3645 cx_a: &mut TestAppContext,
3646 cx_b: &mut TestAppContext,
3647) {
3648 deterministic.forbid_parking();
3649
3650 let mut server = TestServer::start(&deterministic).await;
3651 let client_a = server.create_client(cx_a, "user_a").await;
3652 let client_b = server.create_client(cx_b, "user_b").await;
3653 server
3654 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3655 .await;
3656 let active_call_a = cx_a.read(ActiveCall::global);
3657
3658 client_a
3659 .fs()
3660 .insert_tree(
3661 "/dir",
3662 json!({
3663 "a.txt": "abc",
3664 }),
3665 )
3666 .await;
3667 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3668 let project_id = active_call_a
3669 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3670 .await
3671 .unwrap();
3672 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3673
3674 let buffer_a = project_a
3675 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3676 .await
3677 .unwrap();
3678
3679 // Open a buffer as client B but cancel after a random amount of time.
3680 let buffer_b = project_b.update(cx_b, |p, cx| {
3681 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3682 });
3683 deterministic.simulate_random_delay().await;
3684 drop(buffer_b);
3685
3686 // Try opening the same buffer again as client B, and ensure we can
3687 // still do it despite the cancellation above.
3688 let buffer_b = project_b
3689 .update(cx_b, |p, cx| {
3690 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3691 })
3692 .await
3693 .unwrap();
3694 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3695}
3696
3697#[gpui::test(iterations = 10)]
3698async fn test_leaving_project(
3699 deterministic: Arc<Deterministic>,
3700 cx_a: &mut TestAppContext,
3701 cx_b: &mut TestAppContext,
3702 cx_c: &mut TestAppContext,
3703) {
3704 deterministic.forbid_parking();
3705 let mut server = TestServer::start(&deterministic).await;
3706 let client_a = server.create_client(cx_a, "user_a").await;
3707 let client_b = server.create_client(cx_b, "user_b").await;
3708 let client_c = server.create_client(cx_c, "user_c").await;
3709 server
3710 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3711 .await;
3712 let active_call_a = cx_a.read(ActiveCall::global);
3713
3714 client_a
3715 .fs()
3716 .insert_tree(
3717 "/a",
3718 json!({
3719 "a.txt": "a-contents",
3720 "b.txt": "b-contents",
3721 }),
3722 )
3723 .await;
3724 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3725 let project_id = active_call_a
3726 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3727 .await
3728 .unwrap();
3729 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3730 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3731
3732 // Client A sees that a guest has joined.
3733 deterministic.run_until_parked();
3734 project_a.read_with(cx_a, |project, _| {
3735 assert_eq!(project.collaborators().len(), 2);
3736 });
3737 project_b1.read_with(cx_b, |project, _| {
3738 assert_eq!(project.collaborators().len(), 2);
3739 });
3740 project_c.read_with(cx_c, |project, _| {
3741 assert_eq!(project.collaborators().len(), 2);
3742 });
3743
3744 // Client B opens a buffer.
3745 let buffer_b1 = project_b1
3746 .update(cx_b, |project, cx| {
3747 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3748 project.open_buffer((worktree_id, "a.txt"), cx)
3749 })
3750 .await
3751 .unwrap();
3752 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3753
3754 // Drop client B's project and ensure client A and client C observe client B leaving.
3755 cx_b.update(|_| drop(project_b1));
3756 deterministic.run_until_parked();
3757 project_a.read_with(cx_a, |project, _| {
3758 assert_eq!(project.collaborators().len(), 1);
3759 });
3760 project_c.read_with(cx_c, |project, _| {
3761 assert_eq!(project.collaborators().len(), 1);
3762 });
3763
3764 // Client B re-joins the project and can open buffers as before.
3765 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3766 deterministic.run_until_parked();
3767 project_a.read_with(cx_a, |project, _| {
3768 assert_eq!(project.collaborators().len(), 2);
3769 });
3770 project_b2.read_with(cx_b, |project, _| {
3771 assert_eq!(project.collaborators().len(), 2);
3772 });
3773 project_c.read_with(cx_c, |project, _| {
3774 assert_eq!(project.collaborators().len(), 2);
3775 });
3776
3777 let buffer_b2 = project_b2
3778 .update(cx_b, |project, cx| {
3779 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3780 project.open_buffer((worktree_id, "a.txt"), cx)
3781 })
3782 .await
3783 .unwrap();
3784 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3785
3786 // Drop client B's connection and ensure client A and client C observe client B leaving.
3787 client_b.disconnect(&cx_b.to_async());
3788 deterministic.advance_clock(RECONNECT_TIMEOUT);
3789 project_a.read_with(cx_a, |project, _| {
3790 assert_eq!(project.collaborators().len(), 1);
3791 });
3792 project_b2.read_with(cx_b, |project, _| {
3793 assert!(project.is_read_only());
3794 });
3795 project_c.read_with(cx_c, |project, _| {
3796 assert_eq!(project.collaborators().len(), 1);
3797 });
3798
3799 // Client B can't join the project, unless they re-join the room.
3800 cx_b.spawn(|cx| {
3801 Project::remote(
3802 project_id,
3803 client_b.app_state.client.clone(),
3804 client_b.user_store().clone(),
3805 client_b.language_registry().clone(),
3806 FakeFs::new(cx.background()),
3807 cx,
3808 )
3809 })
3810 .await
3811 .unwrap_err();
3812
3813 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3814 client_c.wait_for_current_user(cx_c).await;
3815 server.forbid_connections();
3816 server.disconnect_client(client_c.peer_id().unwrap());
3817 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3818 deterministic.run_until_parked();
3819 project_a.read_with(cx_a, |project, _| {
3820 assert_eq!(project.collaborators().len(), 0);
3821 });
3822 project_b2.read_with(cx_b, |project, _| {
3823 assert!(project.is_read_only());
3824 });
3825 project_c.read_with(cx_c, |project, _| {
3826 assert!(project.is_read_only());
3827 });
3828}
3829
3830#[gpui::test(iterations = 10)]
3831async fn test_collaborating_with_diagnostics(
3832 deterministic: Arc<Deterministic>,
3833 cx_a: &mut TestAppContext,
3834 cx_b: &mut TestAppContext,
3835 cx_c: &mut TestAppContext,
3836) {
3837 deterministic.forbid_parking();
3838 let mut server = TestServer::start(&deterministic).await;
3839 let client_a = server.create_client(cx_a, "user_a").await;
3840 let client_b = server.create_client(cx_b, "user_b").await;
3841 let client_c = server.create_client(cx_c, "user_c").await;
3842 server
3843 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3844 .await;
3845 let active_call_a = cx_a.read(ActiveCall::global);
3846
3847 // Set up a fake language server.
3848 let mut language = Language::new(
3849 LanguageConfig {
3850 name: "Rust".into(),
3851 path_suffixes: vec!["rs".to_string()],
3852 ..Default::default()
3853 },
3854 Some(tree_sitter_rust::language()),
3855 );
3856 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3857 client_a.language_registry().add(Arc::new(language));
3858
3859 // Share a project as client A
3860 client_a
3861 .fs()
3862 .insert_tree(
3863 "/a",
3864 json!({
3865 "a.rs": "let one = two",
3866 "other.rs": "",
3867 }),
3868 )
3869 .await;
3870 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3871
3872 // Cause the language server to start.
3873 let _buffer = project_a
3874 .update(cx_a, |project, cx| {
3875 project.open_buffer(
3876 ProjectPath {
3877 worktree_id,
3878 path: Path::new("other.rs").into(),
3879 },
3880 cx,
3881 )
3882 })
3883 .await
3884 .unwrap();
3885
3886 // Simulate a language server reporting errors for a file.
3887 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3888 fake_language_server
3889 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3890 .await;
3891 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3892 lsp::PublishDiagnosticsParams {
3893 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3894 version: None,
3895 diagnostics: vec![lsp::Diagnostic {
3896 severity: Some(lsp::DiagnosticSeverity::WARNING),
3897 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3898 message: "message 0".to_string(),
3899 ..Default::default()
3900 }],
3901 },
3902 );
3903
3904 // Client A shares the project and, simultaneously, the language server
3905 // publishes a diagnostic. This is done to ensure that the server always
3906 // observes the latest diagnostics for a worktree.
3907 let project_id = active_call_a
3908 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3909 .await
3910 .unwrap();
3911 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3912 lsp::PublishDiagnosticsParams {
3913 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3914 version: None,
3915 diagnostics: vec![lsp::Diagnostic {
3916 severity: Some(lsp::DiagnosticSeverity::ERROR),
3917 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3918 message: "message 1".to_string(),
3919 ..Default::default()
3920 }],
3921 },
3922 );
3923
3924 // Join the worktree as client B.
3925 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3926
3927 // Wait for server to see the diagnostics update.
3928 deterministic.run_until_parked();
3929
3930 // Ensure client B observes the new diagnostics.
3931 project_b.read_with(cx_b, |project, cx| {
3932 assert_eq!(
3933 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3934 &[(
3935 ProjectPath {
3936 worktree_id,
3937 path: Arc::from(Path::new("a.rs")),
3938 },
3939 LanguageServerId(0),
3940 DiagnosticSummary {
3941 error_count: 1,
3942 warning_count: 0,
3943 ..Default::default()
3944 },
3945 )]
3946 )
3947 });
3948
3949 // Join project as client C and observe the diagnostics.
3950 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3951 let project_c_diagnostic_summaries =
3952 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3953 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
3954 })));
3955 project_c.update(cx_c, |_, cx| {
3956 let summaries = project_c_diagnostic_summaries.clone();
3957 cx.subscribe(&project_c, {
3958 move |p, _, event, cx| {
3959 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3960 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
3961 }
3962 }
3963 })
3964 .detach();
3965 });
3966
3967 deterministic.run_until_parked();
3968 assert_eq!(
3969 project_c_diagnostic_summaries.borrow().as_slice(),
3970 &[(
3971 ProjectPath {
3972 worktree_id,
3973 path: Arc::from(Path::new("a.rs")),
3974 },
3975 LanguageServerId(0),
3976 DiagnosticSummary {
3977 error_count: 1,
3978 warning_count: 0,
3979 ..Default::default()
3980 },
3981 )]
3982 );
3983
3984 // Simulate a language server reporting more errors for a file.
3985 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3986 lsp::PublishDiagnosticsParams {
3987 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3988 version: None,
3989 diagnostics: vec![
3990 lsp::Diagnostic {
3991 severity: Some(lsp::DiagnosticSeverity::ERROR),
3992 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3993 message: "message 1".to_string(),
3994 ..Default::default()
3995 },
3996 lsp::Diagnostic {
3997 severity: Some(lsp::DiagnosticSeverity::WARNING),
3998 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3999 message: "message 2".to_string(),
4000 ..Default::default()
4001 },
4002 ],
4003 },
4004 );
4005
4006 // Clients B and C get the updated summaries
4007 deterministic.run_until_parked();
4008 project_b.read_with(cx_b, |project, cx| {
4009 assert_eq!(
4010 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4011 [(
4012 ProjectPath {
4013 worktree_id,
4014 path: Arc::from(Path::new("a.rs")),
4015 },
4016 LanguageServerId(0),
4017 DiagnosticSummary {
4018 error_count: 1,
4019 warning_count: 1,
4020 },
4021 )]
4022 );
4023 });
4024 project_c.read_with(cx_c, |project, cx| {
4025 assert_eq!(
4026 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4027 [(
4028 ProjectPath {
4029 worktree_id,
4030 path: Arc::from(Path::new("a.rs")),
4031 },
4032 LanguageServerId(0),
4033 DiagnosticSummary {
4034 error_count: 1,
4035 warning_count: 1,
4036 },
4037 )]
4038 );
4039 });
4040
4041 // Open the file with the errors on client B. They should be present.
4042 let buffer_b = cx_b
4043 .background()
4044 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4045 .await
4046 .unwrap();
4047
4048 buffer_b.read_with(cx_b, |buffer, _| {
4049 assert_eq!(
4050 buffer
4051 .snapshot()
4052 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
4053 .collect::<Vec<_>>(),
4054 &[
4055 DiagnosticEntry {
4056 range: Point::new(0, 4)..Point::new(0, 7),
4057 diagnostic: Diagnostic {
4058 group_id: 2,
4059 message: "message 1".to_string(),
4060 severity: lsp::DiagnosticSeverity::ERROR,
4061 is_primary: true,
4062 ..Default::default()
4063 }
4064 },
4065 DiagnosticEntry {
4066 range: Point::new(0, 10)..Point::new(0, 13),
4067 diagnostic: Diagnostic {
4068 group_id: 3,
4069 severity: lsp::DiagnosticSeverity::WARNING,
4070 message: "message 2".to_string(),
4071 is_primary: true,
4072 ..Default::default()
4073 }
4074 }
4075 ]
4076 );
4077 });
4078
4079 // Simulate a language server reporting no errors for a file.
4080 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4081 lsp::PublishDiagnosticsParams {
4082 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
4083 version: None,
4084 diagnostics: vec![],
4085 },
4086 );
4087 deterministic.run_until_parked();
4088 project_a.read_with(cx_a, |project, cx| {
4089 assert_eq!(
4090 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4091 []
4092 )
4093 });
4094 project_b.read_with(cx_b, |project, cx| {
4095 assert_eq!(
4096 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4097 []
4098 )
4099 });
4100 project_c.read_with(cx_c, |project, cx| {
4101 assert_eq!(
4102 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4103 []
4104 )
4105 });
4106}
4107
4108#[gpui::test(iterations = 10)]
4109async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4110 deterministic: Arc<Deterministic>,
4111 cx_a: &mut TestAppContext,
4112 cx_b: &mut TestAppContext,
4113) {
4114 deterministic.forbid_parking();
4115 let mut server = TestServer::start(&deterministic).await;
4116 let client_a = server.create_client(cx_a, "user_a").await;
4117 let client_b = server.create_client(cx_b, "user_b").await;
4118 server
4119 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4120 .await;
4121
4122 // Set up a fake language server.
4123 let mut language = Language::new(
4124 LanguageConfig {
4125 name: "Rust".into(),
4126 path_suffixes: vec!["rs".to_string()],
4127 ..Default::default()
4128 },
4129 Some(tree_sitter_rust::language()),
4130 );
4131 let mut fake_language_servers = language
4132 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4133 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4134 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4135 ..Default::default()
4136 }))
4137 .await;
4138 client_a.language_registry().add(Arc::new(language));
4139
4140 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4141 client_a
4142 .fs()
4143 .insert_tree(
4144 "/test",
4145 json!({
4146 "one.rs": "const ONE: usize = 1;",
4147 "two.rs": "const TWO: usize = 2;",
4148 "three.rs": "const THREE: usize = 3;",
4149 "four.rs": "const FOUR: usize = 3;",
4150 "five.rs": "const FIVE: usize = 3;",
4151 }),
4152 )
4153 .await;
4154
4155 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
4156
4157 // Share a project as client A
4158 let active_call_a = cx_a.read(ActiveCall::global);
4159 let project_id = active_call_a
4160 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4161 .await
4162 .unwrap();
4163
4164 // Join the project as client B and open all three files.
4165 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4166 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4167 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
4168 }))
4169 .await
4170 .unwrap();
4171
4172 // Simulate a language server reporting errors for a file.
4173 let fake_language_server = fake_language_servers.next().await.unwrap();
4174 fake_language_server
4175 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4176 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4177 })
4178 .await
4179 .unwrap();
4180 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4181 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4182 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4183 lsp::WorkDoneProgressBegin {
4184 title: "Progress Began".into(),
4185 ..Default::default()
4186 },
4187 )),
4188 });
4189 for file_name in file_names {
4190 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4191 lsp::PublishDiagnosticsParams {
4192 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
4193 version: None,
4194 diagnostics: vec![lsp::Diagnostic {
4195 severity: Some(lsp::DiagnosticSeverity::WARNING),
4196 source: Some("the-disk-based-diagnostics-source".into()),
4197 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4198 message: "message one".to_string(),
4199 ..Default::default()
4200 }],
4201 },
4202 );
4203 }
4204 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4205 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4206 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4207 lsp::WorkDoneProgressEnd { message: None },
4208 )),
4209 });
4210
4211 // When the "disk base diagnostics finished" message is received, the buffers'
4212 // diagnostics are expected to be present.
4213 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4214 project_b.update(cx_b, {
4215 let project_b = project_b.clone();
4216 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4217 move |_, cx| {
4218 cx.subscribe(&project_b, move |_, _, event, cx| {
4219 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4220 disk_based_diagnostics_finished.store(true, SeqCst);
4221 for buffer in &guest_buffers {
4222 assert_eq!(
4223 buffer
4224 .read(cx)
4225 .snapshot()
4226 .diagnostics_in_range::<_, usize>(0..5, false)
4227 .count(),
4228 1,
4229 "expected a diagnostic for buffer {:?}",
4230 buffer.read(cx).file().unwrap().path(),
4231 );
4232 }
4233 }
4234 })
4235 .detach();
4236 }
4237 });
4238
4239 deterministic.run_until_parked();
4240 assert!(disk_based_diagnostics_finished.load(SeqCst));
4241}
4242
4243#[gpui::test(iterations = 10)]
4244async fn test_collaborating_with_completion(
4245 deterministic: Arc<Deterministic>,
4246 cx_a: &mut TestAppContext,
4247 cx_b: &mut TestAppContext,
4248) {
4249 deterministic.forbid_parking();
4250 let mut server = TestServer::start(&deterministic).await;
4251 let client_a = server.create_client(cx_a, "user_a").await;
4252 let client_b = server.create_client(cx_b, "user_b").await;
4253 server
4254 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4255 .await;
4256 let active_call_a = cx_a.read(ActiveCall::global);
4257
4258 // Set up a fake language server.
4259 let mut language = Language::new(
4260 LanguageConfig {
4261 name: "Rust".into(),
4262 path_suffixes: vec!["rs".to_string()],
4263 ..Default::default()
4264 },
4265 Some(tree_sitter_rust::language()),
4266 );
4267 let mut fake_language_servers = language
4268 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4269 capabilities: lsp::ServerCapabilities {
4270 completion_provider: Some(lsp::CompletionOptions {
4271 trigger_characters: Some(vec![".".to_string()]),
4272 resolve_provider: Some(true),
4273 ..Default::default()
4274 }),
4275 ..Default::default()
4276 },
4277 ..Default::default()
4278 }))
4279 .await;
4280 client_a.language_registry().add(Arc::new(language));
4281
4282 client_a
4283 .fs()
4284 .insert_tree(
4285 "/a",
4286 json!({
4287 "main.rs": "fn main() { a }",
4288 "other.rs": "",
4289 }),
4290 )
4291 .await;
4292 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4293 let project_id = active_call_a
4294 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4295 .await
4296 .unwrap();
4297 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4298
4299 // Open a file in an editor as the guest.
4300 let buffer_b = project_b
4301 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
4302 .await
4303 .unwrap();
4304 let window_b = cx_b.add_window(|_| EmptyView);
4305 let editor_b = window_b.add_view(cx_b, |cx| {
4306 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
4307 });
4308
4309 let fake_language_server = fake_language_servers.next().await.unwrap();
4310 cx_a.foreground().run_until_parked();
4311 buffer_b.read_with(cx_b, |buffer, _| {
4312 assert!(!buffer.completion_triggers().is_empty())
4313 });
4314
4315 // Type a completion trigger character as the guest.
4316 editor_b.update(cx_b, |editor, cx| {
4317 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
4318 editor.handle_input(".", cx);
4319 cx.focus(&editor_b);
4320 });
4321
4322 // Receive a completion request as the host's language server.
4323 // Return some completions from the host's language server.
4324 cx_a.foreground().start_waiting();
4325 fake_language_server
4326 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
4327 assert_eq!(
4328 params.text_document_position.text_document.uri,
4329 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4330 );
4331 assert_eq!(
4332 params.text_document_position.position,
4333 lsp::Position::new(0, 14),
4334 );
4335
4336 Ok(Some(lsp::CompletionResponse::Array(vec![
4337 lsp::CompletionItem {
4338 label: "first_method(…)".into(),
4339 detail: Some("fn(&mut self, B) -> C".into()),
4340 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4341 new_text: "first_method($1)".to_string(),
4342 range: lsp::Range::new(
4343 lsp::Position::new(0, 14),
4344 lsp::Position::new(0, 14),
4345 ),
4346 })),
4347 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4348 ..Default::default()
4349 },
4350 lsp::CompletionItem {
4351 label: "second_method(…)".into(),
4352 detail: Some("fn(&mut self, C) -> D<E>".into()),
4353 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4354 new_text: "second_method()".to_string(),
4355 range: lsp::Range::new(
4356 lsp::Position::new(0, 14),
4357 lsp::Position::new(0, 14),
4358 ),
4359 })),
4360 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4361 ..Default::default()
4362 },
4363 ])))
4364 })
4365 .next()
4366 .await
4367 .unwrap();
4368 cx_a.foreground().finish_waiting();
4369
4370 // Open the buffer on the host.
4371 let buffer_a = project_a
4372 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
4373 .await
4374 .unwrap();
4375 cx_a.foreground().run_until_parked();
4376 buffer_a.read_with(cx_a, |buffer, _| {
4377 assert_eq!(buffer.text(), "fn main() { a. }")
4378 });
4379
4380 // Confirm a completion on the guest.
4381 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
4382 editor_b.update(cx_b, |editor, cx| {
4383 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
4384 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
4385 });
4386
4387 // Return a resolved completion from the host's language server.
4388 // The resolved completion has an additional text edit.
4389 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
4390 |params, _| async move {
4391 assert_eq!(params.label, "first_method(…)");
4392 Ok(lsp::CompletionItem {
4393 label: "first_method(…)".into(),
4394 detail: Some("fn(&mut self, B) -> C".into()),
4395 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4396 new_text: "first_method($1)".to_string(),
4397 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
4398 })),
4399 additional_text_edits: Some(vec![lsp::TextEdit {
4400 new_text: "use d::SomeTrait;\n".to_string(),
4401 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4402 }]),
4403 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4404 ..Default::default()
4405 })
4406 },
4407 );
4408
4409 // The additional edit is applied.
4410 cx_a.foreground().run_until_parked();
4411 buffer_a.read_with(cx_a, |buffer, _| {
4412 assert_eq!(
4413 buffer.text(),
4414 "use d::SomeTrait;\nfn main() { a.first_method() }"
4415 );
4416 });
4417 buffer_b.read_with(cx_b, |buffer, _| {
4418 assert_eq!(
4419 buffer.text(),
4420 "use d::SomeTrait;\nfn main() { a.first_method() }"
4421 );
4422 });
4423}
4424
4425#[gpui::test(iterations = 10)]
4426async fn test_reloading_buffer_manually(
4427 deterministic: Arc<Deterministic>,
4428 cx_a: &mut TestAppContext,
4429 cx_b: &mut TestAppContext,
4430) {
4431 deterministic.forbid_parking();
4432 let mut server = TestServer::start(&deterministic).await;
4433 let client_a = server.create_client(cx_a, "user_a").await;
4434 let client_b = server.create_client(cx_b, "user_b").await;
4435 server
4436 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4437 .await;
4438 let active_call_a = cx_a.read(ActiveCall::global);
4439
4440 client_a
4441 .fs()
4442 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4443 .await;
4444 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4445 let buffer_a = project_a
4446 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4447 .await
4448 .unwrap();
4449 let project_id = active_call_a
4450 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4451 .await
4452 .unwrap();
4453
4454 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4455
4456 let buffer_b = cx_b
4457 .background()
4458 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4459 .await
4460 .unwrap();
4461 buffer_b.update(cx_b, |buffer, cx| {
4462 buffer.edit([(4..7, "six")], None, cx);
4463 buffer.edit([(10..11, "6")], None, cx);
4464 assert_eq!(buffer.text(), "let six = 6;");
4465 assert!(buffer.is_dirty());
4466 assert!(!buffer.has_conflict());
4467 });
4468 cx_a.foreground().run_until_parked();
4469 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4470
4471 client_a
4472 .fs()
4473 .save(
4474 "/a/a.rs".as_ref(),
4475 &Rope::from("let seven = 7;"),
4476 LineEnding::Unix,
4477 )
4478 .await
4479 .unwrap();
4480 cx_a.foreground().run_until_parked();
4481 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4482 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4483
4484 project_b
4485 .update(cx_b, |project, cx| {
4486 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4487 })
4488 .await
4489 .unwrap();
4490 buffer_a.read_with(cx_a, |buffer, _| {
4491 assert_eq!(buffer.text(), "let seven = 7;");
4492 assert!(!buffer.is_dirty());
4493 assert!(!buffer.has_conflict());
4494 });
4495 buffer_b.read_with(cx_b, |buffer, _| {
4496 assert_eq!(buffer.text(), "let seven = 7;");
4497 assert!(!buffer.is_dirty());
4498 assert!(!buffer.has_conflict());
4499 });
4500
4501 buffer_a.update(cx_a, |buffer, cx| {
4502 // Undoing on the host is a no-op when the reload was initiated by the guest.
4503 buffer.undo(cx);
4504 assert_eq!(buffer.text(), "let seven = 7;");
4505 assert!(!buffer.is_dirty());
4506 assert!(!buffer.has_conflict());
4507 });
4508 buffer_b.update(cx_b, |buffer, cx| {
4509 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4510 buffer.undo(cx);
4511 assert_eq!(buffer.text(), "let six = 6;");
4512 assert!(buffer.is_dirty());
4513 assert!(!buffer.has_conflict());
4514 });
4515}
4516
4517#[gpui::test(iterations = 10)]
4518async fn test_formatting_buffer(
4519 deterministic: Arc<Deterministic>,
4520 cx_a: &mut TestAppContext,
4521 cx_b: &mut TestAppContext,
4522) {
4523 let mut server = TestServer::start(&deterministic).await;
4524 let client_a = server.create_client(cx_a, "user_a").await;
4525 let client_b = server.create_client(cx_b, "user_b").await;
4526 server
4527 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4528 .await;
4529 let active_call_a = cx_a.read(ActiveCall::global);
4530
4531 // Set up a fake language server.
4532 let mut language = Language::new(
4533 LanguageConfig {
4534 name: "Rust".into(),
4535 path_suffixes: vec!["rs".to_string()],
4536 ..Default::default()
4537 },
4538 Some(tree_sitter_rust::language()),
4539 );
4540 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4541 client_a.language_registry().add(Arc::new(language));
4542
4543 // Here we insert a fake tree with a directory that exists on disk. This is needed
4544 // because later we'll invoke a command, which requires passing a working directory
4545 // that points to a valid location on disk.
4546 let directory = env::current_dir().unwrap();
4547 client_a
4548 .fs()
4549 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4550 .await;
4551 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4552 let project_id = active_call_a
4553 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4554 .await
4555 .unwrap();
4556 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4557
4558 let buffer_b = cx_b
4559 .background()
4560 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4561 .await
4562 .unwrap();
4563
4564 let fake_language_server = fake_language_servers.next().await.unwrap();
4565 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4566 Ok(Some(vec![
4567 lsp::TextEdit {
4568 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4569 new_text: "h".to_string(),
4570 },
4571 lsp::TextEdit {
4572 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4573 new_text: "y".to_string(),
4574 },
4575 ]))
4576 });
4577
4578 project_b
4579 .update(cx_b, |project, cx| {
4580 project.format(
4581 HashSet::from_iter([buffer_b.clone()]),
4582 true,
4583 FormatTrigger::Save,
4584 cx,
4585 )
4586 })
4587 .await
4588 .unwrap();
4589
4590 // The edits from the LSP are applied, and a final newline is added.
4591 assert_eq!(
4592 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4593 "let honey = \"two\"\n"
4594 );
4595
4596 // Ensure buffer can be formatted using an external command. Notice how the
4597 // host's configuration is honored as opposed to using the guest's settings.
4598 cx_a.update(|cx| {
4599 cx.update_global(|store: &mut SettingsStore, cx| {
4600 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4601 file.defaults.formatter = Some(Formatter::External {
4602 command: "awk".into(),
4603 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4604 });
4605 });
4606 });
4607 });
4608 project_b
4609 .update(cx_b, |project, cx| {
4610 project.format(
4611 HashSet::from_iter([buffer_b.clone()]),
4612 true,
4613 FormatTrigger::Save,
4614 cx,
4615 )
4616 })
4617 .await
4618 .unwrap();
4619 assert_eq!(
4620 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4621 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4622 );
4623}
4624
4625#[gpui::test(iterations = 10)]
4626async fn test_prettier_formatting_buffer(
4627 deterministic: Arc<Deterministic>,
4628 cx_a: &mut TestAppContext,
4629 cx_b: &mut TestAppContext,
4630) {
4631 let mut server = TestServer::start(&deterministic).await;
4632 let client_a = server.create_client(cx_a, "user_a").await;
4633 let client_b = server.create_client(cx_b, "user_b").await;
4634 server
4635 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4636 .await;
4637 let active_call_a = cx_a.read(ActiveCall::global);
4638
4639 // Set up a fake language server.
4640 let mut language = Language::new(
4641 LanguageConfig {
4642 name: "Rust".into(),
4643 path_suffixes: vec!["rs".to_string()],
4644 prettier_parser_name: Some("test_parser".to_string()),
4645 ..Default::default()
4646 },
4647 Some(tree_sitter_rust::language()),
4648 );
4649 let test_plugin = "test_plugin";
4650 let mut fake_language_servers = language
4651 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4652 prettier_plugins: vec![test_plugin],
4653 ..Default::default()
4654 }))
4655 .await;
4656 let language = Arc::new(language);
4657 client_a.language_registry().add(Arc::clone(&language));
4658
4659 // Here we insert a fake tree with a directory that exists on disk. This is needed
4660 // because later we'll invoke a command, which requires passing a working directory
4661 // that points to a valid location on disk.
4662 let directory = env::current_dir().unwrap();
4663 let buffer_text = "let one = \"two\"";
4664 client_a
4665 .fs()
4666 .insert_tree(&directory, json!({ "a.rs": buffer_text }))
4667 .await;
4668 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4669 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4670 let buffer_a = cx_a
4671 .background()
4672 .spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4673 .await
4674 .unwrap();
4675
4676 let project_id = active_call_a
4677 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4678 .await
4679 .unwrap();
4680 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4681 let buffer_b = cx_b
4682 .background()
4683 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4684 .await
4685 .unwrap();
4686
4687 cx_a.update(|cx| {
4688 cx.update_global(|store: &mut SettingsStore, cx| {
4689 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4690 file.defaults.formatter = Some(Formatter::Auto);
4691 });
4692 });
4693 });
4694 cx_b.update(|cx| {
4695 cx.update_global(|store: &mut SettingsStore, cx| {
4696 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4697 file.defaults.formatter = Some(Formatter::LanguageServer);
4698 });
4699 });
4700 });
4701 let fake_language_server = fake_language_servers.next().await.unwrap();
4702 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4703 panic!(
4704 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4705 )
4706 });
4707
4708 project_b
4709 .update(cx_b, |project, cx| {
4710 project.format(
4711 HashSet::from_iter([buffer_b.clone()]),
4712 true,
4713 FormatTrigger::Save,
4714 cx,
4715 )
4716 })
4717 .await
4718 .unwrap();
4719 cx_a.foreground().run_until_parked();
4720 cx_b.foreground().run_until_parked();
4721 assert_eq!(
4722 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4723 buffer_text.to_string() + "\n" + prettier_format_suffix,
4724 "Prettier formatting was not applied to client buffer after client's request"
4725 );
4726
4727 project_a
4728 .update(cx_a, |project, cx| {
4729 project.format(
4730 HashSet::from_iter([buffer_a.clone()]),
4731 true,
4732 FormatTrigger::Manual,
4733 cx,
4734 )
4735 })
4736 .await
4737 .unwrap();
4738 cx_a.foreground().run_until_parked();
4739 cx_b.foreground().run_until_parked();
4740 assert_eq!(
4741 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4742 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4743 "Prettier formatting was not applied to client buffer after host's request"
4744 );
4745}
4746
4747#[gpui::test(iterations = 10)]
4748async fn test_definition(
4749 deterministic: Arc<Deterministic>,
4750 cx_a: &mut TestAppContext,
4751 cx_b: &mut TestAppContext,
4752) {
4753 deterministic.forbid_parking();
4754 let mut server = TestServer::start(&deterministic).await;
4755 let client_a = server.create_client(cx_a, "user_a").await;
4756 let client_b = server.create_client(cx_b, "user_b").await;
4757 server
4758 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4759 .await;
4760 let active_call_a = cx_a.read(ActiveCall::global);
4761
4762 // Set up a fake language server.
4763 let mut language = Language::new(
4764 LanguageConfig {
4765 name: "Rust".into(),
4766 path_suffixes: vec!["rs".to_string()],
4767 ..Default::default()
4768 },
4769 Some(tree_sitter_rust::language()),
4770 );
4771 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4772 client_a.language_registry().add(Arc::new(language));
4773
4774 client_a
4775 .fs()
4776 .insert_tree(
4777 "/root",
4778 json!({
4779 "dir-1": {
4780 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4781 },
4782 "dir-2": {
4783 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4784 "c.rs": "type T2 = usize;",
4785 }
4786 }),
4787 )
4788 .await;
4789 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4790 let project_id = active_call_a
4791 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4792 .await
4793 .unwrap();
4794 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4795
4796 // Open the file on client B.
4797 let buffer_b = cx_b
4798 .background()
4799 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4800 .await
4801 .unwrap();
4802
4803 // Request the definition of a symbol as the guest.
4804 let fake_language_server = fake_language_servers.next().await.unwrap();
4805 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4806 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4807 lsp::Location::new(
4808 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4809 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4810 ),
4811 )))
4812 });
4813
4814 let definitions_1 = project_b
4815 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4816 .await
4817 .unwrap();
4818 cx_b.read(|cx| {
4819 assert_eq!(definitions_1.len(), 1);
4820 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4821 let target_buffer = definitions_1[0].target.buffer.read(cx);
4822 assert_eq!(
4823 target_buffer.text(),
4824 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4825 );
4826 assert_eq!(
4827 definitions_1[0].target.range.to_point(target_buffer),
4828 Point::new(0, 6)..Point::new(0, 9)
4829 );
4830 });
4831
4832 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4833 // the previous call to `definition`.
4834 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4835 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4836 lsp::Location::new(
4837 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4838 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4839 ),
4840 )))
4841 });
4842
4843 let definitions_2 = project_b
4844 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4845 .await
4846 .unwrap();
4847 cx_b.read(|cx| {
4848 assert_eq!(definitions_2.len(), 1);
4849 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4850 let target_buffer = definitions_2[0].target.buffer.read(cx);
4851 assert_eq!(
4852 target_buffer.text(),
4853 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4854 );
4855 assert_eq!(
4856 definitions_2[0].target.range.to_point(target_buffer),
4857 Point::new(1, 6)..Point::new(1, 11)
4858 );
4859 });
4860 assert_eq!(
4861 definitions_1[0].target.buffer,
4862 definitions_2[0].target.buffer
4863 );
4864
4865 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4866 |req, _| async move {
4867 assert_eq!(
4868 req.text_document_position_params.position,
4869 lsp::Position::new(0, 7)
4870 );
4871 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4872 lsp::Location::new(
4873 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4874 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4875 ),
4876 )))
4877 },
4878 );
4879
4880 let type_definitions = project_b
4881 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4882 .await
4883 .unwrap();
4884 cx_b.read(|cx| {
4885 assert_eq!(type_definitions.len(), 1);
4886 let target_buffer = type_definitions[0].target.buffer.read(cx);
4887 assert_eq!(target_buffer.text(), "type T2 = usize;");
4888 assert_eq!(
4889 type_definitions[0].target.range.to_point(target_buffer),
4890 Point::new(0, 5)..Point::new(0, 7)
4891 );
4892 });
4893}
4894
4895#[gpui::test(iterations = 10)]
4896async fn test_references(
4897 deterministic: Arc<Deterministic>,
4898 cx_a: &mut TestAppContext,
4899 cx_b: &mut TestAppContext,
4900) {
4901 deterministic.forbid_parking();
4902 let mut server = TestServer::start(&deterministic).await;
4903 let client_a = server.create_client(cx_a, "user_a").await;
4904 let client_b = server.create_client(cx_b, "user_b").await;
4905 server
4906 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4907 .await;
4908 let active_call_a = cx_a.read(ActiveCall::global);
4909
4910 // Set up a fake language server.
4911 let mut language = Language::new(
4912 LanguageConfig {
4913 name: "Rust".into(),
4914 path_suffixes: vec!["rs".to_string()],
4915 ..Default::default()
4916 },
4917 Some(tree_sitter_rust::language()),
4918 );
4919 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4920 client_a.language_registry().add(Arc::new(language));
4921
4922 client_a
4923 .fs()
4924 .insert_tree(
4925 "/root",
4926 json!({
4927 "dir-1": {
4928 "one.rs": "const ONE: usize = 1;",
4929 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4930 },
4931 "dir-2": {
4932 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4933 }
4934 }),
4935 )
4936 .await;
4937 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4938 let project_id = active_call_a
4939 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4940 .await
4941 .unwrap();
4942 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4943
4944 // Open the file on client B.
4945 let buffer_b = cx_b
4946 .background()
4947 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4948 .await
4949 .unwrap();
4950
4951 // Request references to a symbol as the guest.
4952 let fake_language_server = fake_language_servers.next().await.unwrap();
4953 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4954 assert_eq!(
4955 params.text_document_position.text_document.uri.as_str(),
4956 "file:///root/dir-1/one.rs"
4957 );
4958 Ok(Some(vec![
4959 lsp::Location {
4960 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4961 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4962 },
4963 lsp::Location {
4964 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4965 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4966 },
4967 lsp::Location {
4968 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4969 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4970 },
4971 ]))
4972 });
4973
4974 let references = project_b
4975 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4976 .await
4977 .unwrap();
4978 cx_b.read(|cx| {
4979 assert_eq!(references.len(), 3);
4980 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4981
4982 let two_buffer = references[0].buffer.read(cx);
4983 let three_buffer = references[2].buffer.read(cx);
4984 assert_eq!(
4985 two_buffer.file().unwrap().path().as_ref(),
4986 Path::new("two.rs")
4987 );
4988 assert_eq!(references[1].buffer, references[0].buffer);
4989 assert_eq!(
4990 three_buffer.file().unwrap().full_path(cx),
4991 Path::new("/root/dir-2/three.rs")
4992 );
4993
4994 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4995 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4996 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4997 });
4998}
4999
5000#[gpui::test(iterations = 10)]
5001async fn test_project_search(
5002 deterministic: Arc<Deterministic>,
5003 cx_a: &mut TestAppContext,
5004 cx_b: &mut TestAppContext,
5005) {
5006 deterministic.forbid_parking();
5007 let mut server = TestServer::start(&deterministic).await;
5008 let client_a = server.create_client(cx_a, "user_a").await;
5009 let client_b = server.create_client(cx_b, "user_b").await;
5010 server
5011 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5012 .await;
5013 let active_call_a = cx_a.read(ActiveCall::global);
5014
5015 client_a
5016 .fs()
5017 .insert_tree(
5018 "/root",
5019 json!({
5020 "dir-1": {
5021 "a": "hello world",
5022 "b": "goodnight moon",
5023 "c": "a world of goo",
5024 "d": "world champion of clown world",
5025 },
5026 "dir-2": {
5027 "e": "disney world is fun",
5028 }
5029 }),
5030 )
5031 .await;
5032 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
5033 let (worktree_2, _) = project_a
5034 .update(cx_a, |p, cx| {
5035 p.find_or_create_local_worktree("/root/dir-2", true, cx)
5036 })
5037 .await
5038 .unwrap();
5039 worktree_2
5040 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
5041 .await;
5042 let project_id = active_call_a
5043 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5044 .await
5045 .unwrap();
5046
5047 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5048
5049 // Perform a search as the guest.
5050 let mut results = HashMap::default();
5051 let mut search_rx = project_b.update(cx_b, |project, cx| {
5052 project.search(
5053 SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
5054 cx,
5055 )
5056 });
5057 while let Some((buffer, ranges)) = search_rx.next().await {
5058 results.entry(buffer).or_insert(ranges);
5059 }
5060
5061 let mut ranges_by_path = results
5062 .into_iter()
5063 .map(|(buffer, ranges)| {
5064 buffer.read_with(cx_b, |buffer, cx| {
5065 let path = buffer.file().unwrap().full_path(cx);
5066 let offset_ranges = ranges
5067 .into_iter()
5068 .map(|range| range.to_offset(buffer))
5069 .collect::<Vec<_>>();
5070 (path, offset_ranges)
5071 })
5072 })
5073 .collect::<Vec<_>>();
5074 ranges_by_path.sort_by_key(|(path, _)| path.clone());
5075
5076 assert_eq!(
5077 ranges_by_path,
5078 &[
5079 (PathBuf::from("dir-1/a"), vec![6..11]),
5080 (PathBuf::from("dir-1/c"), vec![2..7]),
5081 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
5082 (PathBuf::from("dir-2/e"), vec![7..12]),
5083 ]
5084 );
5085}
5086
5087#[gpui::test(iterations = 10)]
5088async fn test_document_highlights(
5089 deterministic: Arc<Deterministic>,
5090 cx_a: &mut TestAppContext,
5091 cx_b: &mut TestAppContext,
5092) {
5093 deterministic.forbid_parking();
5094 let mut server = TestServer::start(&deterministic).await;
5095 let client_a = server.create_client(cx_a, "user_a").await;
5096 let client_b = server.create_client(cx_b, "user_b").await;
5097 server
5098 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5099 .await;
5100 let active_call_a = cx_a.read(ActiveCall::global);
5101
5102 client_a
5103 .fs()
5104 .insert_tree(
5105 "/root-1",
5106 json!({
5107 "main.rs": "fn double(number: i32) -> i32 { number + number }",
5108 }),
5109 )
5110 .await;
5111
5112 // Set up a fake language server.
5113 let mut language = Language::new(
5114 LanguageConfig {
5115 name: "Rust".into(),
5116 path_suffixes: vec!["rs".to_string()],
5117 ..Default::default()
5118 },
5119 Some(tree_sitter_rust::language()),
5120 );
5121 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5122 client_a.language_registry().add(Arc::new(language));
5123
5124 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5125 let project_id = active_call_a
5126 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5127 .await
5128 .unwrap();
5129 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5130
5131 // Open the file on client B.
5132 let buffer_b = cx_b
5133 .background()
5134 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
5135 .await
5136 .unwrap();
5137
5138 // Request document highlights as the guest.
5139 let fake_language_server = fake_language_servers.next().await.unwrap();
5140 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
5141 |params, _| async move {
5142 assert_eq!(
5143 params
5144 .text_document_position_params
5145 .text_document
5146 .uri
5147 .as_str(),
5148 "file:///root-1/main.rs"
5149 );
5150 assert_eq!(
5151 params.text_document_position_params.position,
5152 lsp::Position::new(0, 34)
5153 );
5154 Ok(Some(vec![
5155 lsp::DocumentHighlight {
5156 kind: Some(lsp::DocumentHighlightKind::WRITE),
5157 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
5158 },
5159 lsp::DocumentHighlight {
5160 kind: Some(lsp::DocumentHighlightKind::READ),
5161 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
5162 },
5163 lsp::DocumentHighlight {
5164 kind: Some(lsp::DocumentHighlightKind::READ),
5165 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
5166 },
5167 ]))
5168 },
5169 );
5170
5171 let highlights = project_b
5172 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
5173 .await
5174 .unwrap();
5175 buffer_b.read_with(cx_b, |buffer, _| {
5176 let snapshot = buffer.snapshot();
5177
5178 let highlights = highlights
5179 .into_iter()
5180 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
5181 .collect::<Vec<_>>();
5182 assert_eq!(
5183 highlights,
5184 &[
5185 (lsp::DocumentHighlightKind::WRITE, 10..16),
5186 (lsp::DocumentHighlightKind::READ, 32..38),
5187 (lsp::DocumentHighlightKind::READ, 41..47)
5188 ]
5189 )
5190 });
5191}
5192
5193#[gpui::test(iterations = 10)]
5194async fn test_lsp_hover(
5195 deterministic: Arc<Deterministic>,
5196 cx_a: &mut TestAppContext,
5197 cx_b: &mut TestAppContext,
5198) {
5199 deterministic.forbid_parking();
5200 let mut server = TestServer::start(&deterministic).await;
5201 let client_a = server.create_client(cx_a, "user_a").await;
5202 let client_b = server.create_client(cx_b, "user_b").await;
5203 server
5204 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5205 .await;
5206 let active_call_a = cx_a.read(ActiveCall::global);
5207
5208 client_a
5209 .fs()
5210 .insert_tree(
5211 "/root-1",
5212 json!({
5213 "main.rs": "use std::collections::HashMap;",
5214 }),
5215 )
5216 .await;
5217
5218 // Set up a fake language server.
5219 let mut language = Language::new(
5220 LanguageConfig {
5221 name: "Rust".into(),
5222 path_suffixes: vec!["rs".to_string()],
5223 ..Default::default()
5224 },
5225 Some(tree_sitter_rust::language()),
5226 );
5227 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5228 client_a.language_registry().add(Arc::new(language));
5229
5230 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5231 let project_id = active_call_a
5232 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5233 .await
5234 .unwrap();
5235 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5236
5237 // Open the file as the guest
5238 let buffer_b = cx_b
5239 .background()
5240 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
5241 .await
5242 .unwrap();
5243
5244 // Request hover information as the guest.
5245 let fake_language_server = fake_language_servers.next().await.unwrap();
5246 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
5247 |params, _| async move {
5248 assert_eq!(
5249 params
5250 .text_document_position_params
5251 .text_document
5252 .uri
5253 .as_str(),
5254 "file:///root-1/main.rs"
5255 );
5256 assert_eq!(
5257 params.text_document_position_params.position,
5258 lsp::Position::new(0, 22)
5259 );
5260 Ok(Some(lsp::Hover {
5261 contents: lsp::HoverContents::Array(vec![
5262 lsp::MarkedString::String("Test hover content.".to_string()),
5263 lsp::MarkedString::LanguageString(lsp::LanguageString {
5264 language: "Rust".to_string(),
5265 value: "let foo = 42;".to_string(),
5266 }),
5267 ]),
5268 range: Some(lsp::Range::new(
5269 lsp::Position::new(0, 22),
5270 lsp::Position::new(0, 29),
5271 )),
5272 }))
5273 },
5274 );
5275
5276 let hover_info = project_b
5277 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5278 .await
5279 .unwrap()
5280 .unwrap();
5281 buffer_b.read_with(cx_b, |buffer, _| {
5282 let snapshot = buffer.snapshot();
5283 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
5284 assert_eq!(
5285 hover_info.contents,
5286 vec![
5287 project::HoverBlock {
5288 text: "Test hover content.".to_string(),
5289 kind: HoverBlockKind::Markdown,
5290 },
5291 project::HoverBlock {
5292 text: "let foo = 42;".to_string(),
5293 kind: HoverBlockKind::Code {
5294 language: "Rust".to_string()
5295 },
5296 }
5297 ]
5298 );
5299 });
5300}
5301
5302#[gpui::test(iterations = 10)]
5303async fn test_project_symbols(
5304 deterministic: Arc<Deterministic>,
5305 cx_a: &mut TestAppContext,
5306 cx_b: &mut TestAppContext,
5307) {
5308 deterministic.forbid_parking();
5309 let mut server = TestServer::start(&deterministic).await;
5310 let client_a = server.create_client(cx_a, "user_a").await;
5311 let client_b = server.create_client(cx_b, "user_b").await;
5312 server
5313 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5314 .await;
5315 let active_call_a = cx_a.read(ActiveCall::global);
5316
5317 // Set up a fake language server.
5318 let mut language = Language::new(
5319 LanguageConfig {
5320 name: "Rust".into(),
5321 path_suffixes: vec!["rs".to_string()],
5322 ..Default::default()
5323 },
5324 Some(tree_sitter_rust::language()),
5325 );
5326 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5327 client_a.language_registry().add(Arc::new(language));
5328
5329 client_a
5330 .fs()
5331 .insert_tree(
5332 "/code",
5333 json!({
5334 "crate-1": {
5335 "one.rs": "const ONE: usize = 1;",
5336 },
5337 "crate-2": {
5338 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5339 },
5340 "private": {
5341 "passwords.txt": "the-password",
5342 }
5343 }),
5344 )
5345 .await;
5346 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
5347 let project_id = active_call_a
5348 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5349 .await
5350 .unwrap();
5351 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5352
5353 // Cause the language server to start.
5354 let _buffer = cx_b
5355 .background()
5356 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
5357 .await
5358 .unwrap();
5359
5360 let fake_language_server = fake_language_servers.next().await.unwrap();
5361 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
5362 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5363 #[allow(deprecated)]
5364 lsp::SymbolInformation {
5365 name: "TWO".into(),
5366 location: lsp::Location {
5367 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
5368 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5369 },
5370 kind: lsp::SymbolKind::CONSTANT,
5371 tags: None,
5372 container_name: None,
5373 deprecated: None,
5374 },
5375 ])))
5376 });
5377
5378 // Request the definition of a symbol as the guest.
5379 let symbols = project_b
5380 .update(cx_b, |p, cx| p.symbols("two", cx))
5381 .await
5382 .unwrap();
5383 assert_eq!(symbols.len(), 1);
5384 assert_eq!(symbols[0].name, "TWO");
5385
5386 // Open one of the returned symbols.
5387 let buffer_b_2 = project_b
5388 .update(cx_b, |project, cx| {
5389 project.open_buffer_for_symbol(&symbols[0], cx)
5390 })
5391 .await
5392 .unwrap();
5393 buffer_b_2.read_with(cx_b, |buffer, _| {
5394 assert_eq!(
5395 buffer.file().unwrap().path().as_ref(),
5396 Path::new("../crate-2/two.rs")
5397 );
5398 });
5399
5400 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5401 let mut fake_symbol = symbols[0].clone();
5402 fake_symbol.path.path = Path::new("/code/secrets").into();
5403 let error = project_b
5404 .update(cx_b, |project, cx| {
5405 project.open_buffer_for_symbol(&fake_symbol, cx)
5406 })
5407 .await
5408 .unwrap_err();
5409 assert!(error.to_string().contains("invalid symbol signature"));
5410}
5411
5412#[gpui::test(iterations = 10)]
5413async fn test_open_buffer_while_getting_definition_pointing_to_it(
5414 deterministic: Arc<Deterministic>,
5415 cx_a: &mut TestAppContext,
5416 cx_b: &mut TestAppContext,
5417 mut rng: StdRng,
5418) {
5419 deterministic.forbid_parking();
5420 let mut server = TestServer::start(&deterministic).await;
5421 let client_a = server.create_client(cx_a, "user_a").await;
5422 let client_b = server.create_client(cx_b, "user_b").await;
5423 server
5424 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5425 .await;
5426 let active_call_a = cx_a.read(ActiveCall::global);
5427
5428 // Set up a fake language server.
5429 let mut language = Language::new(
5430 LanguageConfig {
5431 name: "Rust".into(),
5432 path_suffixes: vec!["rs".to_string()],
5433 ..Default::default()
5434 },
5435 Some(tree_sitter_rust::language()),
5436 );
5437 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5438 client_a.language_registry().add(Arc::new(language));
5439
5440 client_a
5441 .fs()
5442 .insert_tree(
5443 "/root",
5444 json!({
5445 "a.rs": "const ONE: usize = b::TWO;",
5446 "b.rs": "const TWO: usize = 2",
5447 }),
5448 )
5449 .await;
5450 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5451 let project_id = active_call_a
5452 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5453 .await
5454 .unwrap();
5455 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5456
5457 let buffer_b1 = cx_b
5458 .background()
5459 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
5460 .await
5461 .unwrap();
5462
5463 let fake_language_server = fake_language_servers.next().await.unwrap();
5464 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5465 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5466 lsp::Location::new(
5467 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5468 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5469 ),
5470 )))
5471 });
5472
5473 let definitions;
5474 let buffer_b2;
5475 if rng.gen() {
5476 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5477 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5478 } else {
5479 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5480 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5481 }
5482
5483 let buffer_b2 = buffer_b2.await.unwrap();
5484 let definitions = definitions.await.unwrap();
5485 assert_eq!(definitions.len(), 1);
5486 assert_eq!(definitions[0].target.buffer, buffer_b2);
5487}
5488
5489#[gpui::test(iterations = 10)]
5490async fn test_collaborating_with_code_actions(
5491 deterministic: Arc<Deterministic>,
5492 cx_a: &mut TestAppContext,
5493 cx_b: &mut TestAppContext,
5494) {
5495 deterministic.forbid_parking();
5496 let mut server = TestServer::start(&deterministic).await;
5497 let client_a = server.create_client(cx_a, "user_a").await;
5498 //
5499 let client_b = server.create_client(cx_b, "user_b").await;
5500 server
5501 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5502 .await;
5503 let active_call_a = cx_a.read(ActiveCall::global);
5504
5505 cx_b.update(editor::init);
5506
5507 // Set up a fake language server.
5508 let mut language = Language::new(
5509 LanguageConfig {
5510 name: "Rust".into(),
5511 path_suffixes: vec!["rs".to_string()],
5512 ..Default::default()
5513 },
5514 Some(tree_sitter_rust::language()),
5515 );
5516 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5517 client_a.language_registry().add(Arc::new(language));
5518
5519 client_a
5520 .fs()
5521 .insert_tree(
5522 "/a",
5523 json!({
5524 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
5525 "other.rs": "pub fn foo() -> usize { 4 }",
5526 }),
5527 )
5528 .await;
5529 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5530 let project_id = active_call_a
5531 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5532 .await
5533 .unwrap();
5534
5535 // Join the project as client B.
5536 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5537 let window_b =
5538 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
5539 let workspace_b = window_b.root(cx_b);
5540 let editor_b = workspace_b
5541 .update(cx_b, |workspace, cx| {
5542 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
5543 })
5544 .await
5545 .unwrap()
5546 .downcast::<Editor>()
5547 .unwrap();
5548
5549 let mut fake_language_server = fake_language_servers.next().await.unwrap();
5550 let mut requests = fake_language_server
5551 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
5552 assert_eq!(
5553 params.text_document.uri,
5554 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5555 );
5556 assert_eq!(params.range.start, lsp::Position::new(0, 0));
5557 assert_eq!(params.range.end, lsp::Position::new(0, 0));
5558 Ok(None)
5559 });
5560 deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
5561 requests.next().await;
5562
5563 // Move cursor to a location that contains code actions.
5564 editor_b.update(cx_b, |editor, cx| {
5565 editor.change_selections(None, cx, |s| {
5566 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
5567 });
5568 cx.focus(&editor_b);
5569 });
5570
5571 let mut requests = fake_language_server
5572 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
5573 assert_eq!(
5574 params.text_document.uri,
5575 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5576 );
5577 assert_eq!(params.range.start, lsp::Position::new(1, 31));
5578 assert_eq!(params.range.end, lsp::Position::new(1, 31));
5579
5580 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
5581 lsp::CodeAction {
5582 title: "Inline into all callers".to_string(),
5583 edit: Some(lsp::WorkspaceEdit {
5584 changes: Some(
5585 [
5586 (
5587 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5588 vec![lsp::TextEdit::new(
5589 lsp::Range::new(
5590 lsp::Position::new(1, 22),
5591 lsp::Position::new(1, 34),
5592 ),
5593 "4".to_string(),
5594 )],
5595 ),
5596 (
5597 lsp::Url::from_file_path("/a/other.rs").unwrap(),
5598 vec![lsp::TextEdit::new(
5599 lsp::Range::new(
5600 lsp::Position::new(0, 0),
5601 lsp::Position::new(0, 27),
5602 ),
5603 "".to_string(),
5604 )],
5605 ),
5606 ]
5607 .into_iter()
5608 .collect(),
5609 ),
5610 ..Default::default()
5611 }),
5612 data: Some(json!({
5613 "codeActionParams": {
5614 "range": {
5615 "start": {"line": 1, "column": 31},
5616 "end": {"line": 1, "column": 31},
5617 }
5618 }
5619 })),
5620 ..Default::default()
5621 },
5622 )]))
5623 });
5624 deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
5625 requests.next().await;
5626
5627 // Toggle code actions and wait for them to display.
5628 editor_b.update(cx_b, |editor, cx| {
5629 editor.toggle_code_actions(
5630 &ToggleCodeActions {
5631 deployed_from_indicator: false,
5632 },
5633 cx,
5634 );
5635 });
5636 cx_a.foreground().run_until_parked();
5637 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
5638
5639 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
5640
5641 // Confirming the code action will trigger a resolve request.
5642 let confirm_action = workspace_b
5643 .update(cx_b, |workspace, cx| {
5644 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
5645 })
5646 .unwrap();
5647 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
5648 |_, _| async move {
5649 Ok(lsp::CodeAction {
5650 title: "Inline into all callers".to_string(),
5651 edit: Some(lsp::WorkspaceEdit {
5652 changes: Some(
5653 [
5654 (
5655 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5656 vec![lsp::TextEdit::new(
5657 lsp::Range::new(
5658 lsp::Position::new(1, 22),
5659 lsp::Position::new(1, 34),
5660 ),
5661 "4".to_string(),
5662 )],
5663 ),
5664 (
5665 lsp::Url::from_file_path("/a/other.rs").unwrap(),
5666 vec![lsp::TextEdit::new(
5667 lsp::Range::new(
5668 lsp::Position::new(0, 0),
5669 lsp::Position::new(0, 27),
5670 ),
5671 "".to_string(),
5672 )],
5673 ),
5674 ]
5675 .into_iter()
5676 .collect(),
5677 ),
5678 ..Default::default()
5679 }),
5680 ..Default::default()
5681 })
5682 },
5683 );
5684
5685 // After the action is confirmed, an editor containing both modified files is opened.
5686 confirm_action.await.unwrap();
5687 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5688 workspace
5689 .active_item(cx)
5690 .unwrap()
5691 .downcast::<Editor>()
5692 .unwrap()
5693 });
5694 code_action_editor.update(cx_b, |editor, cx| {
5695 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
5696 editor.undo(&Undo, cx);
5697 assert_eq!(
5698 editor.text(cx),
5699 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
5700 );
5701 editor.redo(&Redo, cx);
5702 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
5703 });
5704}
5705
5706#[gpui::test(iterations = 10)]
5707async fn test_collaborating_with_renames(
5708 deterministic: Arc<Deterministic>,
5709 cx_a: &mut TestAppContext,
5710 cx_b: &mut TestAppContext,
5711) {
5712 deterministic.forbid_parking();
5713 let mut server = TestServer::start(&deterministic).await;
5714 let client_a = server.create_client(cx_a, "user_a").await;
5715 let client_b = server.create_client(cx_b, "user_b").await;
5716 server
5717 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5718 .await;
5719 let active_call_a = cx_a.read(ActiveCall::global);
5720
5721 cx_b.update(editor::init);
5722
5723 // Set up a fake language server.
5724 let mut language = Language::new(
5725 LanguageConfig {
5726 name: "Rust".into(),
5727 path_suffixes: vec!["rs".to_string()],
5728 ..Default::default()
5729 },
5730 Some(tree_sitter_rust::language()),
5731 );
5732 let mut fake_language_servers = language
5733 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5734 capabilities: lsp::ServerCapabilities {
5735 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
5736 prepare_provider: Some(true),
5737 work_done_progress_options: Default::default(),
5738 })),
5739 ..Default::default()
5740 },
5741 ..Default::default()
5742 }))
5743 .await;
5744 client_a.language_registry().add(Arc::new(language));
5745
5746 client_a
5747 .fs()
5748 .insert_tree(
5749 "/dir",
5750 json!({
5751 "one.rs": "const ONE: usize = 1;",
5752 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
5753 }),
5754 )
5755 .await;
5756 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5757 let project_id = active_call_a
5758 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5759 .await
5760 .unwrap();
5761 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5762
5763 let window_b =
5764 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
5765 let workspace_b = window_b.root(cx_b);
5766 let editor_b = workspace_b
5767 .update(cx_b, |workspace, cx| {
5768 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
5769 })
5770 .await
5771 .unwrap()
5772 .downcast::<Editor>()
5773 .unwrap();
5774 let fake_language_server = fake_language_servers.next().await.unwrap();
5775
5776 // Move cursor to a location that can be renamed.
5777 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
5778 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
5779 editor.rename(&Rename, cx).unwrap()
5780 });
5781
5782 fake_language_server
5783 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
5784 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
5785 assert_eq!(params.position, lsp::Position::new(0, 7));
5786 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
5787 lsp::Position::new(0, 6),
5788 lsp::Position::new(0, 9),
5789 ))))
5790 })
5791 .next()
5792 .await
5793 .unwrap();
5794 prepare_rename.await.unwrap();
5795 editor_b.update(cx_b, |editor, cx| {
5796 use editor::ToOffset;
5797 let rename = editor.pending_rename().unwrap();
5798 let buffer = editor.buffer().read(cx).snapshot(cx);
5799 assert_eq!(
5800 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
5801 6..9
5802 );
5803 rename.editor.update(cx, |rename_editor, cx| {
5804 rename_editor.buffer().update(cx, |rename_buffer, cx| {
5805 rename_buffer.edit([(0..3, "THREE")], None, cx);
5806 });
5807 });
5808 });
5809
5810 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
5811 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
5812 });
5813 fake_language_server
5814 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
5815 assert_eq!(
5816 params.text_document_position.text_document.uri.as_str(),
5817 "file:///dir/one.rs"
5818 );
5819 assert_eq!(
5820 params.text_document_position.position,
5821 lsp::Position::new(0, 6)
5822 );
5823 assert_eq!(params.new_name, "THREE");
5824 Ok(Some(lsp::WorkspaceEdit {
5825 changes: Some(
5826 [
5827 (
5828 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
5829 vec![lsp::TextEdit::new(
5830 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5831 "THREE".to_string(),
5832 )],
5833 ),
5834 (
5835 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
5836 vec![
5837 lsp::TextEdit::new(
5838 lsp::Range::new(
5839 lsp::Position::new(0, 24),
5840 lsp::Position::new(0, 27),
5841 ),
5842 "THREE".to_string(),
5843 ),
5844 lsp::TextEdit::new(
5845 lsp::Range::new(
5846 lsp::Position::new(0, 35),
5847 lsp::Position::new(0, 38),
5848 ),
5849 "THREE".to_string(),
5850 ),
5851 ],
5852 ),
5853 ]
5854 .into_iter()
5855 .collect(),
5856 ),
5857 ..Default::default()
5858 }))
5859 })
5860 .next()
5861 .await
5862 .unwrap();
5863 confirm_rename.await.unwrap();
5864
5865 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5866 workspace
5867 .active_item(cx)
5868 .unwrap()
5869 .downcast::<Editor>()
5870 .unwrap()
5871 });
5872 rename_editor.update(cx_b, |editor, cx| {
5873 assert_eq!(
5874 editor.text(cx),
5875 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5876 );
5877 editor.undo(&Undo, cx);
5878 assert_eq!(
5879 editor.text(cx),
5880 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
5881 );
5882 editor.redo(&Redo, cx);
5883 assert_eq!(
5884 editor.text(cx),
5885 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5886 );
5887 });
5888
5889 // Ensure temporary rename edits cannot be undone/redone.
5890 editor_b.update(cx_b, |editor, cx| {
5891 editor.undo(&Undo, cx);
5892 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5893 editor.undo(&Undo, cx);
5894 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5895 editor.redo(&Redo, cx);
5896 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
5897 })
5898}
5899
5900#[gpui::test(iterations = 10)]
5901async fn test_language_server_statuses(
5902 deterministic: Arc<Deterministic>,
5903 cx_a: &mut TestAppContext,
5904 cx_b: &mut TestAppContext,
5905) {
5906 deterministic.forbid_parking();
5907 let mut server = TestServer::start(&deterministic).await;
5908 let client_a = server.create_client(cx_a, "user_a").await;
5909 let client_b = server.create_client(cx_b, "user_b").await;
5910 server
5911 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5912 .await;
5913 let active_call_a = cx_a.read(ActiveCall::global);
5914
5915 cx_b.update(editor::init);
5916
5917 // Set up a fake language server.
5918 let mut language = Language::new(
5919 LanguageConfig {
5920 name: "Rust".into(),
5921 path_suffixes: vec!["rs".to_string()],
5922 ..Default::default()
5923 },
5924 Some(tree_sitter_rust::language()),
5925 );
5926 let mut fake_language_servers = language
5927 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5928 name: "the-language-server",
5929 ..Default::default()
5930 }))
5931 .await;
5932 client_a.language_registry().add(Arc::new(language));
5933
5934 client_a
5935 .fs()
5936 .insert_tree(
5937 "/dir",
5938 json!({
5939 "main.rs": "const ONE: usize = 1;",
5940 }),
5941 )
5942 .await;
5943 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5944
5945 let _buffer_a = project_a
5946 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
5947 .await
5948 .unwrap();
5949
5950 let fake_language_server = fake_language_servers.next().await.unwrap();
5951 fake_language_server.start_progress("the-token").await;
5952 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5953 token: lsp::NumberOrString::String("the-token".to_string()),
5954 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5955 lsp::WorkDoneProgressReport {
5956 message: Some("the-message".to_string()),
5957 ..Default::default()
5958 },
5959 )),
5960 });
5961 deterministic.run_until_parked();
5962 project_a.read_with(cx_a, |project, _| {
5963 let status = project.language_server_statuses().next().unwrap();
5964 assert_eq!(status.name, "the-language-server");
5965 assert_eq!(status.pending_work.len(), 1);
5966 assert_eq!(
5967 status.pending_work["the-token"].message.as_ref().unwrap(),
5968 "the-message"
5969 );
5970 });
5971
5972 let project_id = active_call_a
5973 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5974 .await
5975 .unwrap();
5976 deterministic.run_until_parked();
5977 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5978 project_b.read_with(cx_b, |project, _| {
5979 let status = project.language_server_statuses().next().unwrap();
5980 assert_eq!(status.name, "the-language-server");
5981 });
5982
5983 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5984 token: lsp::NumberOrString::String("the-token".to_string()),
5985 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5986 lsp::WorkDoneProgressReport {
5987 message: Some("the-message-2".to_string()),
5988 ..Default::default()
5989 },
5990 )),
5991 });
5992 deterministic.run_until_parked();
5993 project_a.read_with(cx_a, |project, _| {
5994 let status = project.language_server_statuses().next().unwrap();
5995 assert_eq!(status.name, "the-language-server");
5996 assert_eq!(status.pending_work.len(), 1);
5997 assert_eq!(
5998 status.pending_work["the-token"].message.as_ref().unwrap(),
5999 "the-message-2"
6000 );
6001 });
6002 project_b.read_with(cx_b, |project, _| {
6003 let status = project.language_server_statuses().next().unwrap();
6004 assert_eq!(status.name, "the-language-server");
6005 assert_eq!(status.pending_work.len(), 1);
6006 assert_eq!(
6007 status.pending_work["the-token"].message.as_ref().unwrap(),
6008 "the-message-2"
6009 );
6010 });
6011}
6012
6013#[gpui::test(iterations = 10)]
6014async fn test_contacts(
6015 deterministic: Arc<Deterministic>,
6016 cx_a: &mut TestAppContext,
6017 cx_b: &mut TestAppContext,
6018 cx_c: &mut TestAppContext,
6019 cx_d: &mut TestAppContext,
6020) {
6021 deterministic.forbid_parking();
6022 let mut server = TestServer::start(&deterministic).await;
6023 let client_a = server.create_client(cx_a, "user_a").await;
6024 let client_b = server.create_client(cx_b, "user_b").await;
6025 let client_c = server.create_client(cx_c, "user_c").await;
6026 let client_d = server.create_client(cx_d, "user_d").await;
6027 server
6028 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
6029 .await;
6030 let active_call_a = cx_a.read(ActiveCall::global);
6031 let active_call_b = cx_b.read(ActiveCall::global);
6032 let active_call_c = cx_c.read(ActiveCall::global);
6033 let _active_call_d = cx_d.read(ActiveCall::global);
6034
6035 deterministic.run_until_parked();
6036 assert_eq!(
6037 contacts(&client_a, cx_a),
6038 [
6039 ("user_b".to_string(), "online", "free"),
6040 ("user_c".to_string(), "online", "free")
6041 ]
6042 );
6043 assert_eq!(
6044 contacts(&client_b, cx_b),
6045 [
6046 ("user_a".to_string(), "online", "free"),
6047 ("user_c".to_string(), "online", "free")
6048 ]
6049 );
6050 assert_eq!(
6051 contacts(&client_c, cx_c),
6052 [
6053 ("user_a".to_string(), "online", "free"),
6054 ("user_b".to_string(), "online", "free")
6055 ]
6056 );
6057 assert_eq!(contacts(&client_d, cx_d), []);
6058
6059 server.disconnect_client(client_c.peer_id().unwrap());
6060 server.forbid_connections();
6061 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6062 assert_eq!(
6063 contacts(&client_a, cx_a),
6064 [
6065 ("user_b".to_string(), "online", "free"),
6066 ("user_c".to_string(), "offline", "free")
6067 ]
6068 );
6069 assert_eq!(
6070 contacts(&client_b, cx_b),
6071 [
6072 ("user_a".to_string(), "online", "free"),
6073 ("user_c".to_string(), "offline", "free")
6074 ]
6075 );
6076 assert_eq!(contacts(&client_c, cx_c), []);
6077 assert_eq!(contacts(&client_d, cx_d), []);
6078
6079 server.allow_connections();
6080 client_c
6081 .authenticate_and_connect(false, &cx_c.to_async())
6082 .await
6083 .unwrap();
6084
6085 deterministic.run_until_parked();
6086 assert_eq!(
6087 contacts(&client_a, cx_a),
6088 [
6089 ("user_b".to_string(), "online", "free"),
6090 ("user_c".to_string(), "online", "free")
6091 ]
6092 );
6093 assert_eq!(
6094 contacts(&client_b, cx_b),
6095 [
6096 ("user_a".to_string(), "online", "free"),
6097 ("user_c".to_string(), "online", "free")
6098 ]
6099 );
6100 assert_eq!(
6101 contacts(&client_c, cx_c),
6102 [
6103 ("user_a".to_string(), "online", "free"),
6104 ("user_b".to_string(), "online", "free")
6105 ]
6106 );
6107 assert_eq!(contacts(&client_d, cx_d), []);
6108
6109 active_call_a
6110 .update(cx_a, |call, cx| {
6111 call.invite(client_b.user_id().unwrap(), None, cx)
6112 })
6113 .await
6114 .unwrap();
6115 deterministic.run_until_parked();
6116 assert_eq!(
6117 contacts(&client_a, cx_a),
6118 [
6119 ("user_b".to_string(), "online", "busy"),
6120 ("user_c".to_string(), "online", "free")
6121 ]
6122 );
6123 assert_eq!(
6124 contacts(&client_b, cx_b),
6125 [
6126 ("user_a".to_string(), "online", "busy"),
6127 ("user_c".to_string(), "online", "free")
6128 ]
6129 );
6130 assert_eq!(
6131 contacts(&client_c, cx_c),
6132 [
6133 ("user_a".to_string(), "online", "busy"),
6134 ("user_b".to_string(), "online", "busy")
6135 ]
6136 );
6137 assert_eq!(contacts(&client_d, cx_d), []);
6138
6139 // Client B and client D become contacts while client B is being called.
6140 server
6141 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
6142 .await;
6143 deterministic.run_until_parked();
6144 assert_eq!(
6145 contacts(&client_a, cx_a),
6146 [
6147 ("user_b".to_string(), "online", "busy"),
6148 ("user_c".to_string(), "online", "free")
6149 ]
6150 );
6151 assert_eq!(
6152 contacts(&client_b, cx_b),
6153 [
6154 ("user_a".to_string(), "online", "busy"),
6155 ("user_c".to_string(), "online", "free"),
6156 ("user_d".to_string(), "online", "free"),
6157 ]
6158 );
6159 assert_eq!(
6160 contacts(&client_c, cx_c),
6161 [
6162 ("user_a".to_string(), "online", "busy"),
6163 ("user_b".to_string(), "online", "busy")
6164 ]
6165 );
6166 assert_eq!(
6167 contacts(&client_d, cx_d),
6168 [("user_b".to_string(), "online", "busy")]
6169 );
6170
6171 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
6172 deterministic.run_until_parked();
6173 assert_eq!(
6174 contacts(&client_a, cx_a),
6175 [
6176 ("user_b".to_string(), "online", "free"),
6177 ("user_c".to_string(), "online", "free")
6178 ]
6179 );
6180 assert_eq!(
6181 contacts(&client_b, cx_b),
6182 [
6183 ("user_a".to_string(), "online", "free"),
6184 ("user_c".to_string(), "online", "free"),
6185 ("user_d".to_string(), "online", "free")
6186 ]
6187 );
6188 assert_eq!(
6189 contacts(&client_c, cx_c),
6190 [
6191 ("user_a".to_string(), "online", "free"),
6192 ("user_b".to_string(), "online", "free")
6193 ]
6194 );
6195 assert_eq!(
6196 contacts(&client_d, cx_d),
6197 [("user_b".to_string(), "online", "free")]
6198 );
6199
6200 active_call_c
6201 .update(cx_c, |call, cx| {
6202 call.invite(client_a.user_id().unwrap(), None, cx)
6203 })
6204 .await
6205 .unwrap();
6206 deterministic.run_until_parked();
6207 assert_eq!(
6208 contacts(&client_a, cx_a),
6209 [
6210 ("user_b".to_string(), "online", "free"),
6211 ("user_c".to_string(), "online", "busy")
6212 ]
6213 );
6214 assert_eq!(
6215 contacts(&client_b, cx_b),
6216 [
6217 ("user_a".to_string(), "online", "busy"),
6218 ("user_c".to_string(), "online", "busy"),
6219 ("user_d".to_string(), "online", "free")
6220 ]
6221 );
6222 assert_eq!(
6223 contacts(&client_c, cx_c),
6224 [
6225 ("user_a".to_string(), "online", "busy"),
6226 ("user_b".to_string(), "online", "free")
6227 ]
6228 );
6229 assert_eq!(
6230 contacts(&client_d, cx_d),
6231 [("user_b".to_string(), "online", "free")]
6232 );
6233
6234 active_call_a
6235 .update(cx_a, |call, cx| call.accept_incoming(cx))
6236 .await
6237 .unwrap();
6238 deterministic.run_until_parked();
6239 assert_eq!(
6240 contacts(&client_a, cx_a),
6241 [
6242 ("user_b".to_string(), "online", "free"),
6243 ("user_c".to_string(), "online", "busy")
6244 ]
6245 );
6246 assert_eq!(
6247 contacts(&client_b, cx_b),
6248 [
6249 ("user_a".to_string(), "online", "busy"),
6250 ("user_c".to_string(), "online", "busy"),
6251 ("user_d".to_string(), "online", "free")
6252 ]
6253 );
6254 assert_eq!(
6255 contacts(&client_c, cx_c),
6256 [
6257 ("user_a".to_string(), "online", "busy"),
6258 ("user_b".to_string(), "online", "free")
6259 ]
6260 );
6261 assert_eq!(
6262 contacts(&client_d, cx_d),
6263 [("user_b".to_string(), "online", "free")]
6264 );
6265
6266 active_call_a
6267 .update(cx_a, |call, cx| {
6268 call.invite(client_b.user_id().unwrap(), None, cx)
6269 })
6270 .await
6271 .unwrap();
6272 deterministic.run_until_parked();
6273 assert_eq!(
6274 contacts(&client_a, cx_a),
6275 [
6276 ("user_b".to_string(), "online", "busy"),
6277 ("user_c".to_string(), "online", "busy")
6278 ]
6279 );
6280 assert_eq!(
6281 contacts(&client_b, cx_b),
6282 [
6283 ("user_a".to_string(), "online", "busy"),
6284 ("user_c".to_string(), "online", "busy"),
6285 ("user_d".to_string(), "online", "free")
6286 ]
6287 );
6288 assert_eq!(
6289 contacts(&client_c, cx_c),
6290 [
6291 ("user_a".to_string(), "online", "busy"),
6292 ("user_b".to_string(), "online", "busy")
6293 ]
6294 );
6295 assert_eq!(
6296 contacts(&client_d, cx_d),
6297 [("user_b".to_string(), "online", "busy")]
6298 );
6299
6300 active_call_a
6301 .update(cx_a, |call, cx| call.hang_up(cx))
6302 .await
6303 .unwrap();
6304 deterministic.run_until_parked();
6305 assert_eq!(
6306 contacts(&client_a, cx_a),
6307 [
6308 ("user_b".to_string(), "online", "free"),
6309 ("user_c".to_string(), "online", "free")
6310 ]
6311 );
6312 assert_eq!(
6313 contacts(&client_b, cx_b),
6314 [
6315 ("user_a".to_string(), "online", "free"),
6316 ("user_c".to_string(), "online", "free"),
6317 ("user_d".to_string(), "online", "free")
6318 ]
6319 );
6320 assert_eq!(
6321 contacts(&client_c, cx_c),
6322 [
6323 ("user_a".to_string(), "online", "free"),
6324 ("user_b".to_string(), "online", "free")
6325 ]
6326 );
6327 assert_eq!(
6328 contacts(&client_d, cx_d),
6329 [("user_b".to_string(), "online", "free")]
6330 );
6331
6332 active_call_a
6333 .update(cx_a, |call, cx| {
6334 call.invite(client_b.user_id().unwrap(), None, cx)
6335 })
6336 .await
6337 .unwrap();
6338 deterministic.run_until_parked();
6339 assert_eq!(
6340 contacts(&client_a, cx_a),
6341 [
6342 ("user_b".to_string(), "online", "busy"),
6343 ("user_c".to_string(), "online", "free")
6344 ]
6345 );
6346 assert_eq!(
6347 contacts(&client_b, cx_b),
6348 [
6349 ("user_a".to_string(), "online", "busy"),
6350 ("user_c".to_string(), "online", "free"),
6351 ("user_d".to_string(), "online", "free")
6352 ]
6353 );
6354 assert_eq!(
6355 contacts(&client_c, cx_c),
6356 [
6357 ("user_a".to_string(), "online", "busy"),
6358 ("user_b".to_string(), "online", "busy")
6359 ]
6360 );
6361 assert_eq!(
6362 contacts(&client_d, cx_d),
6363 [("user_b".to_string(), "online", "busy")]
6364 );
6365
6366 server.forbid_connections();
6367 server.disconnect_client(client_a.peer_id().unwrap());
6368 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6369 assert_eq!(contacts(&client_a, cx_a), []);
6370 assert_eq!(
6371 contacts(&client_b, cx_b),
6372 [
6373 ("user_a".to_string(), "offline", "free"),
6374 ("user_c".to_string(), "online", "free"),
6375 ("user_d".to_string(), "online", "free")
6376 ]
6377 );
6378 assert_eq!(
6379 contacts(&client_c, cx_c),
6380 [
6381 ("user_a".to_string(), "offline", "free"),
6382 ("user_b".to_string(), "online", "free")
6383 ]
6384 );
6385 assert_eq!(
6386 contacts(&client_d, cx_d),
6387 [("user_b".to_string(), "online", "free")]
6388 );
6389
6390 // Test removing a contact
6391 client_b
6392 .user_store()
6393 .update(cx_b, |store, cx| {
6394 store.remove_contact(client_c.user_id().unwrap(), cx)
6395 })
6396 .await
6397 .unwrap();
6398 deterministic.run_until_parked();
6399 assert_eq!(
6400 contacts(&client_b, cx_b),
6401 [
6402 ("user_a".to_string(), "offline", "free"),
6403 ("user_d".to_string(), "online", "free")
6404 ]
6405 );
6406 assert_eq!(
6407 contacts(&client_c, cx_c),
6408 [("user_a".to_string(), "offline", "free"),]
6409 );
6410
6411 fn contacts(
6412 client: &TestClient,
6413 cx: &TestAppContext,
6414 ) -> Vec<(String, &'static str, &'static str)> {
6415 client.user_store().read_with(cx, |store, _| {
6416 store
6417 .contacts()
6418 .iter()
6419 .map(|contact| {
6420 (
6421 contact.user.github_login.clone(),
6422 if contact.online { "online" } else { "offline" },
6423 if contact.busy { "busy" } else { "free" },
6424 )
6425 })
6426 .collect()
6427 })
6428 }
6429}
6430
6431#[gpui::test(iterations = 10)]
6432async fn test_contact_requests(
6433 deterministic: Arc<Deterministic>,
6434 cx_a: &mut TestAppContext,
6435 cx_a2: &mut TestAppContext,
6436 cx_b: &mut TestAppContext,
6437 cx_b2: &mut TestAppContext,
6438 cx_c: &mut TestAppContext,
6439 cx_c2: &mut TestAppContext,
6440) {
6441 deterministic.forbid_parking();
6442
6443 // Connect to a server as 3 clients.
6444 let mut server = TestServer::start(&deterministic).await;
6445 let client_a = server.create_client(cx_a, "user_a").await;
6446 let client_a2 = server.create_client(cx_a2, "user_a").await;
6447 let client_b = server.create_client(cx_b, "user_b").await;
6448 let client_b2 = server.create_client(cx_b2, "user_b").await;
6449 let client_c = server.create_client(cx_c, "user_c").await;
6450 let client_c2 = server.create_client(cx_c2, "user_c").await;
6451
6452 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
6453 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
6454 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
6455
6456 // User A and User C request that user B become their contact.
6457 client_a
6458 .user_store()
6459 .update(cx_a, |store, cx| {
6460 store.request_contact(client_b.user_id().unwrap(), cx)
6461 })
6462 .await
6463 .unwrap();
6464 client_c
6465 .user_store()
6466 .update(cx_c, |store, cx| {
6467 store.request_contact(client_b.user_id().unwrap(), cx)
6468 })
6469 .await
6470 .unwrap();
6471 deterministic.run_until_parked();
6472
6473 // All users see the pending request appear in all their clients.
6474 assert_eq!(
6475 client_a.summarize_contacts(cx_a).outgoing_requests,
6476 &["user_b"]
6477 );
6478 assert_eq!(
6479 client_a2.summarize_contacts(cx_a2).outgoing_requests,
6480 &["user_b"]
6481 );
6482 assert_eq!(
6483 client_b.summarize_contacts(cx_b).incoming_requests,
6484 &["user_a", "user_c"]
6485 );
6486 assert_eq!(
6487 client_b2.summarize_contacts(cx_b2).incoming_requests,
6488 &["user_a", "user_c"]
6489 );
6490 assert_eq!(
6491 client_c.summarize_contacts(cx_c).outgoing_requests,
6492 &["user_b"]
6493 );
6494 assert_eq!(
6495 client_c2.summarize_contacts(cx_c2).outgoing_requests,
6496 &["user_b"]
6497 );
6498
6499 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
6500 disconnect_and_reconnect(&client_a, cx_a).await;
6501 disconnect_and_reconnect(&client_b, cx_b).await;
6502 disconnect_and_reconnect(&client_c, cx_c).await;
6503 deterministic.run_until_parked();
6504 assert_eq!(
6505 client_a.summarize_contacts(cx_a).outgoing_requests,
6506 &["user_b"]
6507 );
6508 assert_eq!(
6509 client_b.summarize_contacts(cx_b).incoming_requests,
6510 &["user_a", "user_c"]
6511 );
6512 assert_eq!(
6513 client_c.summarize_contacts(cx_c).outgoing_requests,
6514 &["user_b"]
6515 );
6516
6517 // User B accepts the request from user A.
6518 client_b
6519 .user_store()
6520 .update(cx_b, |store, cx| {
6521 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
6522 })
6523 .await
6524 .unwrap();
6525
6526 deterministic.run_until_parked();
6527
6528 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
6529 let contacts_b = client_b.summarize_contacts(cx_b);
6530 assert_eq!(contacts_b.current, &["user_a"]);
6531 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
6532 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6533 assert_eq!(contacts_b2.current, &["user_a"]);
6534 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
6535
6536 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
6537 let contacts_a = client_a.summarize_contacts(cx_a);
6538 assert_eq!(contacts_a.current, &["user_b"]);
6539 assert!(contacts_a.outgoing_requests.is_empty());
6540 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
6541 assert_eq!(contacts_a2.current, &["user_b"]);
6542 assert!(contacts_a2.outgoing_requests.is_empty());
6543
6544 // Contacts are present upon connecting (tested here via disconnect/reconnect)
6545 disconnect_and_reconnect(&client_a, cx_a).await;
6546 disconnect_and_reconnect(&client_b, cx_b).await;
6547 disconnect_and_reconnect(&client_c, cx_c).await;
6548 deterministic.run_until_parked();
6549 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6550 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6551 assert_eq!(
6552 client_b.summarize_contacts(cx_b).incoming_requests,
6553 &["user_c"]
6554 );
6555 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6556 assert_eq!(
6557 client_c.summarize_contacts(cx_c).outgoing_requests,
6558 &["user_b"]
6559 );
6560
6561 // User B rejects the request from user C.
6562 client_b
6563 .user_store()
6564 .update(cx_b, |store, cx| {
6565 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
6566 })
6567 .await
6568 .unwrap();
6569
6570 deterministic.run_until_parked();
6571
6572 // User B doesn't see user C as their contact, and the incoming request from them is removed.
6573 let contacts_b = client_b.summarize_contacts(cx_b);
6574 assert_eq!(contacts_b.current, &["user_a"]);
6575 assert!(contacts_b.incoming_requests.is_empty());
6576 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6577 assert_eq!(contacts_b2.current, &["user_a"]);
6578 assert!(contacts_b2.incoming_requests.is_empty());
6579
6580 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
6581 let contacts_c = client_c.summarize_contacts(cx_c);
6582 assert!(contacts_c.current.is_empty());
6583 assert!(contacts_c.outgoing_requests.is_empty());
6584 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
6585 assert!(contacts_c2.current.is_empty());
6586 assert!(contacts_c2.outgoing_requests.is_empty());
6587
6588 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
6589 disconnect_and_reconnect(&client_a, cx_a).await;
6590 disconnect_and_reconnect(&client_b, cx_b).await;
6591 disconnect_and_reconnect(&client_c, cx_c).await;
6592 deterministic.run_until_parked();
6593 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6594 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6595 assert!(client_b
6596 .summarize_contacts(cx_b)
6597 .incoming_requests
6598 .is_empty());
6599 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6600 assert!(client_c
6601 .summarize_contacts(cx_c)
6602 .outgoing_requests
6603 .is_empty());
6604
6605 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6606 client.disconnect(&cx.to_async());
6607 client.clear_contacts(cx).await;
6608 client
6609 .authenticate_and_connect(false, &cx.to_async())
6610 .await
6611 .unwrap();
6612 }
6613}
6614
6615#[gpui::test(iterations = 10)]
6616async fn test_join_call_after_screen_was_shared(
6617 deterministic: Arc<Deterministic>,
6618 cx_a: &mut TestAppContext,
6619 cx_b: &mut TestAppContext,
6620) {
6621 deterministic.forbid_parking();
6622 let mut server = TestServer::start(&deterministic).await;
6623
6624 let client_a = server.create_client(cx_a, "user_a").await;
6625 let client_b = server.create_client(cx_b, "user_b").await;
6626 server
6627 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6628 .await;
6629
6630 let active_call_a = cx_a.read(ActiveCall::global);
6631 let active_call_b = cx_b.read(ActiveCall::global);
6632
6633 // Call users B and C from client A.
6634 active_call_a
6635 .update(cx_a, |call, cx| {
6636 call.invite(client_b.user_id().unwrap(), None, cx)
6637 })
6638 .await
6639 .unwrap();
6640 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6641 deterministic.run_until_parked();
6642 assert_eq!(
6643 room_participants(&room_a, cx_a),
6644 RoomParticipants {
6645 remote: Default::default(),
6646 pending: vec!["user_b".to_string()]
6647 }
6648 );
6649
6650 // User B receives the call.
6651 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6652 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6653 assert_eq!(call_b.calling_user.github_login, "user_a");
6654
6655 // User A shares their screen
6656 let display = MacOSDisplay::new();
6657 active_call_a
6658 .update(cx_a, |call, cx| {
6659 call.room().unwrap().update(cx, |room, cx| {
6660 room.set_display_sources(vec![display.clone()]);
6661 room.share_screen(cx)
6662 })
6663 })
6664 .await
6665 .unwrap();
6666
6667 client_b.user_store().update(cx_b, |user_store, _| {
6668 user_store.clear_cache();
6669 });
6670
6671 // User B joins the room
6672 active_call_b
6673 .update(cx_b, |call, cx| call.accept_incoming(cx))
6674 .await
6675 .unwrap();
6676 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6677 assert!(incoming_call_b.next().await.unwrap().is_none());
6678
6679 deterministic.run_until_parked();
6680 assert_eq!(
6681 room_participants(&room_a, cx_a),
6682 RoomParticipants {
6683 remote: vec!["user_b".to_string()],
6684 pending: vec![],
6685 }
6686 );
6687 assert_eq!(
6688 room_participants(&room_b, cx_b),
6689 RoomParticipants {
6690 remote: vec!["user_a".to_string()],
6691 pending: vec![],
6692 }
6693 );
6694
6695 // Ensure User B sees User A's screenshare.
6696 room_b.read_with(cx_b, |room, _| {
6697 assert_eq!(
6698 room.remote_participants()
6699 .get(&client_a.user_id().unwrap())
6700 .unwrap()
6701 .video_tracks
6702 .len(),
6703 1
6704 );
6705 });
6706}
6707
6708#[gpui::test(iterations = 10)]
6709async fn test_on_input_format_from_host_to_guest(
6710 deterministic: Arc<Deterministic>,
6711 cx_a: &mut TestAppContext,
6712 cx_b: &mut TestAppContext,
6713) {
6714 deterministic.forbid_parking();
6715 let mut server = TestServer::start(&deterministic).await;
6716 let client_a = server.create_client(cx_a, "user_a").await;
6717 let client_b = server.create_client(cx_b, "user_b").await;
6718 server
6719 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6720 .await;
6721 let active_call_a = cx_a.read(ActiveCall::global);
6722
6723 // Set up a fake language server.
6724 let mut language = Language::new(
6725 LanguageConfig {
6726 name: "Rust".into(),
6727 path_suffixes: vec!["rs".to_string()],
6728 ..Default::default()
6729 },
6730 Some(tree_sitter_rust::language()),
6731 );
6732 let mut fake_language_servers = language
6733 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
6734 capabilities: lsp::ServerCapabilities {
6735 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
6736 first_trigger_character: ":".to_string(),
6737 more_trigger_character: Some(vec![">".to_string()]),
6738 }),
6739 ..Default::default()
6740 },
6741 ..Default::default()
6742 }))
6743 .await;
6744 client_a.language_registry().add(Arc::new(language));
6745
6746 client_a
6747 .fs()
6748 .insert_tree(
6749 "/a",
6750 json!({
6751 "main.rs": "fn main() { a }",
6752 "other.rs": "// Test file",
6753 }),
6754 )
6755 .await;
6756 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6757 let project_id = active_call_a
6758 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6759 .await
6760 .unwrap();
6761 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6762
6763 // Open a file in an editor as the host.
6764 let buffer_a = project_a
6765 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
6766 .await
6767 .unwrap();
6768 let window_a = cx_a.add_window(|_| EmptyView);
6769 let editor_a = window_a.add_view(cx_a, |cx| {
6770 Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
6771 });
6772
6773 let fake_language_server = fake_language_servers.next().await.unwrap();
6774 cx_b.foreground().run_until_parked();
6775
6776 // Receive an OnTypeFormatting request as the host's language server.
6777 // Return some formattings from the host's language server.
6778 fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
6779 |params, _| async move {
6780 assert_eq!(
6781 params.text_document_position.text_document.uri,
6782 lsp::Url::from_file_path("/a/main.rs").unwrap(),
6783 );
6784 assert_eq!(
6785 params.text_document_position.position,
6786 lsp::Position::new(0, 14),
6787 );
6788
6789 Ok(Some(vec![lsp::TextEdit {
6790 new_text: "~<".to_string(),
6791 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
6792 }]))
6793 },
6794 );
6795
6796 // Open the buffer on the guest and see that the formattings worked
6797 let buffer_b = project_b
6798 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
6799 .await
6800 .unwrap();
6801
6802 // Type a on type formatting trigger character as the guest.
6803 editor_a.update(cx_a, |editor, cx| {
6804 cx.focus(&editor_a);
6805 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
6806 editor.handle_input(">", cx);
6807 });
6808
6809 cx_b.foreground().run_until_parked();
6810
6811 buffer_b.read_with(cx_b, |buffer, _| {
6812 assert_eq!(buffer.text(), "fn main() { a>~< }")
6813 });
6814
6815 // Undo should remove LSP edits first
6816 editor_a.update(cx_a, |editor, cx| {
6817 assert_eq!(editor.text(cx), "fn main() { a>~< }");
6818 editor.undo(&Undo, cx);
6819 assert_eq!(editor.text(cx), "fn main() { a> }");
6820 });
6821 cx_b.foreground().run_until_parked();
6822 buffer_b.read_with(cx_b, |buffer, _| {
6823 assert_eq!(buffer.text(), "fn main() { a> }")
6824 });
6825
6826 editor_a.update(cx_a, |editor, cx| {
6827 assert_eq!(editor.text(cx), "fn main() { a> }");
6828 editor.undo(&Undo, cx);
6829 assert_eq!(editor.text(cx), "fn main() { a }");
6830 });
6831 cx_b.foreground().run_until_parked();
6832 buffer_b.read_with(cx_b, |buffer, _| {
6833 assert_eq!(buffer.text(), "fn main() { a }")
6834 });
6835}
6836
6837#[gpui::test(iterations = 10)]
6838async fn test_on_input_format_from_guest_to_host(
6839 deterministic: Arc<Deterministic>,
6840 cx_a: &mut TestAppContext,
6841 cx_b: &mut TestAppContext,
6842) {
6843 deterministic.forbid_parking();
6844 let mut server = TestServer::start(&deterministic).await;
6845 let client_a = server.create_client(cx_a, "user_a").await;
6846 let client_b = server.create_client(cx_b, "user_b").await;
6847 server
6848 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6849 .await;
6850 let active_call_a = cx_a.read(ActiveCall::global);
6851
6852 // Set up a fake language server.
6853 let mut language = Language::new(
6854 LanguageConfig {
6855 name: "Rust".into(),
6856 path_suffixes: vec!["rs".to_string()],
6857 ..Default::default()
6858 },
6859 Some(tree_sitter_rust::language()),
6860 );
6861 let mut fake_language_servers = language
6862 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
6863 capabilities: lsp::ServerCapabilities {
6864 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
6865 first_trigger_character: ":".to_string(),
6866 more_trigger_character: Some(vec![">".to_string()]),
6867 }),
6868 ..Default::default()
6869 },
6870 ..Default::default()
6871 }))
6872 .await;
6873 client_a.language_registry().add(Arc::new(language));
6874
6875 client_a
6876 .fs()
6877 .insert_tree(
6878 "/a",
6879 json!({
6880 "main.rs": "fn main() { a }",
6881 "other.rs": "// Test file",
6882 }),
6883 )
6884 .await;
6885 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6886 let project_id = active_call_a
6887 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6888 .await
6889 .unwrap();
6890 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6891
6892 // Open a file in an editor as the guest.
6893 let buffer_b = project_b
6894 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
6895 .await
6896 .unwrap();
6897 let window_b = cx_b.add_window(|_| EmptyView);
6898 let editor_b = window_b.add_view(cx_b, |cx| {
6899 Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
6900 });
6901
6902 let fake_language_server = fake_language_servers.next().await.unwrap();
6903 cx_a.foreground().run_until_parked();
6904 // Type a on type formatting trigger character as the guest.
6905 editor_b.update(cx_b, |editor, cx| {
6906 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
6907 editor.handle_input(":", cx);
6908 cx.focus(&editor_b);
6909 });
6910
6911 // Receive an OnTypeFormatting request as the host's language server.
6912 // Return some formattings from the host's language server.
6913 cx_a.foreground().start_waiting();
6914 fake_language_server
6915 .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
6916 assert_eq!(
6917 params.text_document_position.text_document.uri,
6918 lsp::Url::from_file_path("/a/main.rs").unwrap(),
6919 );
6920 assert_eq!(
6921 params.text_document_position.position,
6922 lsp::Position::new(0, 14),
6923 );
6924
6925 Ok(Some(vec![lsp::TextEdit {
6926 new_text: "~:".to_string(),
6927 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
6928 }]))
6929 })
6930 .next()
6931 .await
6932 .unwrap();
6933 cx_a.foreground().finish_waiting();
6934
6935 // Open the buffer on the host and see that the formattings worked
6936 let buffer_a = project_a
6937 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
6938 .await
6939 .unwrap();
6940 cx_a.foreground().run_until_parked();
6941 buffer_a.read_with(cx_a, |buffer, _| {
6942 assert_eq!(buffer.text(), "fn main() { a:~: }")
6943 });
6944
6945 // Undo should remove LSP edits first
6946 editor_b.update(cx_b, |editor, cx| {
6947 assert_eq!(editor.text(cx), "fn main() { a:~: }");
6948 editor.undo(&Undo, cx);
6949 assert_eq!(editor.text(cx), "fn main() { a: }");
6950 });
6951 cx_a.foreground().run_until_parked();
6952 buffer_a.read_with(cx_a, |buffer, _| {
6953 assert_eq!(buffer.text(), "fn main() { a: }")
6954 });
6955
6956 editor_b.update(cx_b, |editor, cx| {
6957 assert_eq!(editor.text(cx), "fn main() { a: }");
6958 editor.undo(&Undo, cx);
6959 assert_eq!(editor.text(cx), "fn main() { a }");
6960 });
6961 cx_a.foreground().run_until_parked();
6962 buffer_a.read_with(cx_a, |buffer, _| {
6963 assert_eq!(buffer.text(), "fn main() { a }")
6964 });
6965}
6966
6967#[gpui::test(iterations = 10)]
6968async fn test_mutual_editor_inlay_hint_cache_update(
6969 deterministic: Arc<Deterministic>,
6970 cx_a: &mut TestAppContext,
6971 cx_b: &mut TestAppContext,
6972) {
6973 deterministic.forbid_parking();
6974 let mut server = TestServer::start(&deterministic).await;
6975 let client_a = server.create_client(cx_a, "user_a").await;
6976 let client_b = server.create_client(cx_b, "user_b").await;
6977 server
6978 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6979 .await;
6980 let active_call_a = cx_a.read(ActiveCall::global);
6981 let active_call_b = cx_b.read(ActiveCall::global);
6982
6983 cx_a.update(editor::init);
6984 cx_b.update(editor::init);
6985
6986 cx_a.update(|cx| {
6987 cx.update_global(|store: &mut SettingsStore, cx| {
6988 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
6989 settings.defaults.inlay_hints = Some(InlayHintSettings {
6990 enabled: true,
6991 show_type_hints: true,
6992 show_parameter_hints: false,
6993 show_other_hints: true,
6994 })
6995 });
6996 });
6997 });
6998 cx_b.update(|cx| {
6999 cx.update_global(|store: &mut SettingsStore, cx| {
7000 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
7001 settings.defaults.inlay_hints = Some(InlayHintSettings {
7002 enabled: true,
7003 show_type_hints: true,
7004 show_parameter_hints: false,
7005 show_other_hints: true,
7006 })
7007 });
7008 });
7009 });
7010
7011 let mut language = Language::new(
7012 LanguageConfig {
7013 name: "Rust".into(),
7014 path_suffixes: vec!["rs".to_string()],
7015 ..Default::default()
7016 },
7017 Some(tree_sitter_rust::language()),
7018 );
7019 let mut fake_language_servers = language
7020 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7021 capabilities: lsp::ServerCapabilities {
7022 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
7023 ..Default::default()
7024 },
7025 ..Default::default()
7026 }))
7027 .await;
7028 let language = Arc::new(language);
7029 client_a.language_registry().add(Arc::clone(&language));
7030 client_b.language_registry().add(language);
7031
7032 // Client A opens a project.
7033 client_a
7034 .fs()
7035 .insert_tree(
7036 "/a",
7037 json!({
7038 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
7039 "other.rs": "// Test file",
7040 }),
7041 )
7042 .await;
7043 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7044 active_call_a
7045 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7046 .await
7047 .unwrap();
7048 let project_id = active_call_a
7049 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7050 .await
7051 .unwrap();
7052
7053 // Client B joins the project
7054 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7055 active_call_b
7056 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7057 .await
7058 .unwrap();
7059
7060 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
7061 cx_a.foreground().start_waiting();
7062
7063 // The host opens a rust file.
7064 let _buffer_a = project_a
7065 .update(cx_a, |project, cx| {
7066 project.open_local_buffer("/a/main.rs", cx)
7067 })
7068 .await
7069 .unwrap();
7070 let fake_language_server = fake_language_servers.next().await.unwrap();
7071 let editor_a = workspace_a
7072 .update(cx_a, |workspace, cx| {
7073 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
7074 })
7075 .await
7076 .unwrap()
7077 .downcast::<Editor>()
7078 .unwrap();
7079
7080 // Set up the language server to return an additional inlay hint on each request.
7081 let edits_made = Arc::new(AtomicUsize::new(0));
7082 let closure_edits_made = Arc::clone(&edits_made);
7083 fake_language_server
7084 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
7085 let task_edits_made = Arc::clone(&closure_edits_made);
7086 async move {
7087 assert_eq!(
7088 params.text_document.uri,
7089 lsp::Url::from_file_path("/a/main.rs").unwrap(),
7090 );
7091 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
7092 Ok(Some(vec![lsp::InlayHint {
7093 position: lsp::Position::new(0, edits_made as u32),
7094 label: lsp::InlayHintLabel::String(edits_made.to_string()),
7095 kind: None,
7096 text_edits: None,
7097 tooltip: None,
7098 padding_left: None,
7099 padding_right: None,
7100 data: None,
7101 }]))
7102 }
7103 })
7104 .next()
7105 .await
7106 .unwrap();
7107
7108 deterministic.run_until_parked();
7109
7110 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
7111 editor_a.update(cx_a, |editor, _| {
7112 assert_eq!(
7113 vec![initial_edit.to_string()],
7114 extract_hint_labels(editor),
7115 "Host should get its first hints when opens an editor"
7116 );
7117 let inlay_cache = editor.inlay_hint_cache();
7118 assert_eq!(
7119 inlay_cache.version(),
7120 1,
7121 "Host editor update the cache version after every cache/view change",
7122 );
7123 });
7124 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
7125 let editor_b = workspace_b
7126 .update(cx_b, |workspace, cx| {
7127 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
7128 })
7129 .await
7130 .unwrap()
7131 .downcast::<Editor>()
7132 .unwrap();
7133
7134 deterministic.run_until_parked();
7135 editor_b.update(cx_b, |editor, _| {
7136 assert_eq!(
7137 vec![initial_edit.to_string()],
7138 extract_hint_labels(editor),
7139 "Client should get its first hints when opens an editor"
7140 );
7141 let inlay_cache = editor.inlay_hint_cache();
7142 assert_eq!(
7143 inlay_cache.version(),
7144 1,
7145 "Guest editor update the cache version after every cache/view change"
7146 );
7147 });
7148
7149 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
7150 editor_b.update(cx_b, |editor, cx| {
7151 editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
7152 editor.handle_input(":", cx);
7153 cx.focus(&editor_b);
7154 });
7155
7156 deterministic.run_until_parked();
7157 editor_a.update(cx_a, |editor, _| {
7158 assert_eq!(
7159 vec![after_client_edit.to_string()],
7160 extract_hint_labels(editor),
7161 );
7162 let inlay_cache = editor.inlay_hint_cache();
7163 assert_eq!(inlay_cache.version(), 2);
7164 });
7165 editor_b.update(cx_b, |editor, _| {
7166 assert_eq!(
7167 vec![after_client_edit.to_string()],
7168 extract_hint_labels(editor),
7169 );
7170 let inlay_cache = editor.inlay_hint_cache();
7171 assert_eq!(inlay_cache.version(), 2);
7172 });
7173
7174 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
7175 editor_a.update(cx_a, |editor, cx| {
7176 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
7177 editor.handle_input("a change to increment both buffers' versions", cx);
7178 cx.focus(&editor_a);
7179 });
7180
7181 deterministic.run_until_parked();
7182 editor_a.update(cx_a, |editor, _| {
7183 assert_eq!(
7184 vec![after_host_edit.to_string()],
7185 extract_hint_labels(editor),
7186 );
7187 let inlay_cache = editor.inlay_hint_cache();
7188 assert_eq!(inlay_cache.version(), 3);
7189 });
7190 editor_b.update(cx_b, |editor, _| {
7191 assert_eq!(
7192 vec![after_host_edit.to_string()],
7193 extract_hint_labels(editor),
7194 );
7195 let inlay_cache = editor.inlay_hint_cache();
7196 assert_eq!(inlay_cache.version(), 3);
7197 });
7198
7199 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
7200 fake_language_server
7201 .request::<lsp::request::InlayHintRefreshRequest>(())
7202 .await
7203 .expect("inlay refresh request failed");
7204
7205 deterministic.run_until_parked();
7206 editor_a.update(cx_a, |editor, _| {
7207 assert_eq!(
7208 vec![after_special_edit_for_refresh.to_string()],
7209 extract_hint_labels(editor),
7210 "Host should react to /refresh LSP request"
7211 );
7212 let inlay_cache = editor.inlay_hint_cache();
7213 assert_eq!(
7214 inlay_cache.version(),
7215 4,
7216 "Host should accepted all edits and bump its cache version every time"
7217 );
7218 });
7219 editor_b.update(cx_b, |editor, _| {
7220 assert_eq!(
7221 vec![after_special_edit_for_refresh.to_string()],
7222 extract_hint_labels(editor),
7223 "Guest should get a /refresh LSP request propagated by host"
7224 );
7225 let inlay_cache = editor.inlay_hint_cache();
7226 assert_eq!(
7227 inlay_cache.version(),
7228 4,
7229 "Guest should accepted all edits and bump its cache version every time"
7230 );
7231 });
7232}
7233
7234#[gpui::test(iterations = 10)]
7235async fn test_inlay_hint_refresh_is_forwarded(
7236 deterministic: Arc<Deterministic>,
7237 cx_a: &mut TestAppContext,
7238 cx_b: &mut TestAppContext,
7239) {
7240 deterministic.forbid_parking();
7241 let mut server = TestServer::start(&deterministic).await;
7242 let client_a = server.create_client(cx_a, "user_a").await;
7243 let client_b = server.create_client(cx_b, "user_b").await;
7244 server
7245 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7246 .await;
7247 let active_call_a = cx_a.read(ActiveCall::global);
7248 let active_call_b = cx_b.read(ActiveCall::global);
7249
7250 cx_a.update(editor::init);
7251 cx_b.update(editor::init);
7252
7253 cx_a.update(|cx| {
7254 cx.update_global(|store: &mut SettingsStore, cx| {
7255 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
7256 settings.defaults.inlay_hints = Some(InlayHintSettings {
7257 enabled: false,
7258 show_type_hints: false,
7259 show_parameter_hints: false,
7260 show_other_hints: false,
7261 })
7262 });
7263 });
7264 });
7265 cx_b.update(|cx| {
7266 cx.update_global(|store: &mut SettingsStore, cx| {
7267 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
7268 settings.defaults.inlay_hints = Some(InlayHintSettings {
7269 enabled: true,
7270 show_type_hints: true,
7271 show_parameter_hints: true,
7272 show_other_hints: true,
7273 })
7274 });
7275 });
7276 });
7277
7278 let mut language = Language::new(
7279 LanguageConfig {
7280 name: "Rust".into(),
7281 path_suffixes: vec!["rs".to_string()],
7282 ..Default::default()
7283 },
7284 Some(tree_sitter_rust::language()),
7285 );
7286 let mut fake_language_servers = language
7287 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7288 capabilities: lsp::ServerCapabilities {
7289 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
7290 ..Default::default()
7291 },
7292 ..Default::default()
7293 }))
7294 .await;
7295 let language = Arc::new(language);
7296 client_a.language_registry().add(Arc::clone(&language));
7297 client_b.language_registry().add(language);
7298
7299 client_a
7300 .fs()
7301 .insert_tree(
7302 "/a",
7303 json!({
7304 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
7305 "other.rs": "// Test file",
7306 }),
7307 )
7308 .await;
7309 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7310 active_call_a
7311 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7312 .await
7313 .unwrap();
7314 let project_id = active_call_a
7315 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7316 .await
7317 .unwrap();
7318
7319 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7320 active_call_b
7321 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7322 .await
7323 .unwrap();
7324
7325 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
7326 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
7327 cx_a.foreground().start_waiting();
7328 cx_b.foreground().start_waiting();
7329
7330 let editor_a = workspace_a
7331 .update(cx_a, |workspace, cx| {
7332 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
7333 })
7334 .await
7335 .unwrap()
7336 .downcast::<Editor>()
7337 .unwrap();
7338
7339 let editor_b = workspace_b
7340 .update(cx_b, |workspace, cx| {
7341 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
7342 })
7343 .await
7344 .unwrap()
7345 .downcast::<Editor>()
7346 .unwrap();
7347
7348 let other_hints = Arc::new(AtomicBool::new(false));
7349 let fake_language_server = fake_language_servers.next().await.unwrap();
7350 let closure_other_hints = Arc::clone(&other_hints);
7351 fake_language_server
7352 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
7353 let task_other_hints = Arc::clone(&closure_other_hints);
7354 async move {
7355 assert_eq!(
7356 params.text_document.uri,
7357 lsp::Url::from_file_path("/a/main.rs").unwrap(),
7358 );
7359 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
7360 let character = if other_hints { 0 } else { 2 };
7361 let label = if other_hints {
7362 "other hint"
7363 } else {
7364 "initial hint"
7365 };
7366 Ok(Some(vec![lsp::InlayHint {
7367 position: lsp::Position::new(0, character),
7368 label: lsp::InlayHintLabel::String(label.to_string()),
7369 kind: None,
7370 text_edits: None,
7371 tooltip: None,
7372 padding_left: None,
7373 padding_right: None,
7374 data: None,
7375 }]))
7376 }
7377 })
7378 .next()
7379 .await
7380 .unwrap();
7381 cx_a.foreground().finish_waiting();
7382 cx_b.foreground().finish_waiting();
7383
7384 cx_a.foreground().run_until_parked();
7385 editor_a.update(cx_a, |editor, _| {
7386 assert!(
7387 extract_hint_labels(editor).is_empty(),
7388 "Host should get no hints due to them turned off"
7389 );
7390 let inlay_cache = editor.inlay_hint_cache();
7391 assert_eq!(
7392 inlay_cache.version(),
7393 0,
7394 "Turned off hints should not generate version updates"
7395 );
7396 });
7397
7398 cx_b.foreground().run_until_parked();
7399 editor_b.update(cx_b, |editor, _| {
7400 assert_eq!(
7401 vec!["initial hint".to_string()],
7402 extract_hint_labels(editor),
7403 "Client should get its first hints when opens an editor"
7404 );
7405 let inlay_cache = editor.inlay_hint_cache();
7406 assert_eq!(
7407 inlay_cache.version(),
7408 1,
7409 "Should update cache verison after first hints"
7410 );
7411 });
7412
7413 other_hints.fetch_or(true, atomic::Ordering::Release);
7414 fake_language_server
7415 .request::<lsp::request::InlayHintRefreshRequest>(())
7416 .await
7417 .expect("inlay refresh request failed");
7418 cx_a.foreground().run_until_parked();
7419 editor_a.update(cx_a, |editor, _| {
7420 assert!(
7421 extract_hint_labels(editor).is_empty(),
7422 "Host should get nop hints due to them turned off, even after the /refresh"
7423 );
7424 let inlay_cache = editor.inlay_hint_cache();
7425 assert_eq!(
7426 inlay_cache.version(),
7427 0,
7428 "Turned off hints should not generate version updates, again"
7429 );
7430 });
7431
7432 cx_b.foreground().run_until_parked();
7433 editor_b.update(cx_b, |editor, _| {
7434 assert_eq!(
7435 vec!["other hint".to_string()],
7436 extract_hint_labels(editor),
7437 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
7438 );
7439 let inlay_cache = editor.inlay_hint_cache();
7440 assert_eq!(
7441 inlay_cache.version(),
7442 2,
7443 "Guest should accepted all edits and bump its cache version every time"
7444 );
7445 });
7446}
7447
7448fn extract_hint_labels(editor: &Editor) -> Vec<String> {
7449 let mut labels = Vec::new();
7450 for hint in editor.inlay_hint_cache().hints() {
7451 match hint.label {
7452 project::InlayHintLabel::String(s) => labels.push(s),
7453 _ => unreachable!(),
7454 }
7455 }
7456 labels
7457}