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