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