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