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