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