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