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