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