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