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