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