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 = cx_b.update(|cx| Project::save_buffer(buffer_b.clone(), cx));
2248 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
2249 save_b.await.unwrap();
2250 assert_eq!(
2251 client_a.fs.load("/a/file1.rs".as_ref()).await.unwrap(),
2252 "hi-a, i-am-c, i-am-b, i-am-a"
2253 );
2254
2255 deterministic.run_until_parked();
2256 buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
2257 buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
2258 buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty()));
2259
2260 // Make changes on host's file system, see those changes on guest worktrees.
2261 client_a
2262 .fs
2263 .rename(
2264 "/a/file1.rs".as_ref(),
2265 "/a/file1.js".as_ref(),
2266 Default::default(),
2267 )
2268 .await
2269 .unwrap();
2270 client_a
2271 .fs
2272 .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
2273 .await
2274 .unwrap();
2275 client_a.fs.insert_file("/a/file4", "4".into()).await;
2276 deterministic.run_until_parked();
2277
2278 worktree_a.read_with(cx_a, |tree, _| {
2279 assert_eq!(
2280 tree.paths()
2281 .map(|p| p.to_string_lossy())
2282 .collect::<Vec<_>>(),
2283 ["file1.js", "file3", "file4"]
2284 )
2285 });
2286 worktree_b.read_with(cx_b, |tree, _| {
2287 assert_eq!(
2288 tree.paths()
2289 .map(|p| p.to_string_lossy())
2290 .collect::<Vec<_>>(),
2291 ["file1.js", "file3", "file4"]
2292 )
2293 });
2294 worktree_c.read_with(cx_c, |tree, _| {
2295 assert_eq!(
2296 tree.paths()
2297 .map(|p| p.to_string_lossy())
2298 .collect::<Vec<_>>(),
2299 ["file1.js", "file3", "file4"]
2300 )
2301 });
2302
2303 // Ensure buffer files are updated as well.
2304 buffer_a.read_with(cx_a, |buffer, _| {
2305 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2306 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2307 });
2308 buffer_b.read_with(cx_b, |buffer, _| {
2309 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2310 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2311 });
2312 buffer_c.read_with(cx_c, |buffer, _| {
2313 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2314 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2315 });
2316
2317 let new_buffer_a = project_a
2318 .update(cx_a, |p, cx| p.create_buffer("", None, cx))
2319 .unwrap();
2320 let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
2321 let new_buffer_b = project_b
2322 .update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx))
2323 .await
2324 .unwrap();
2325 new_buffer_b.read_with(cx_b, |buffer, _| {
2326 assert!(buffer.file().is_none());
2327 });
2328
2329 new_buffer_a.update(cx_a, |buffer, cx| {
2330 buffer.edit([(0..0, "ok")], None, cx);
2331 });
2332 project_a
2333 .update(cx_a, |project, cx| {
2334 project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
2335 })
2336 .await
2337 .unwrap();
2338
2339 deterministic.run_until_parked();
2340 new_buffer_b.read_with(cx_b, |buffer_b, _| {
2341 assert_eq!(
2342 buffer_b.file().unwrap().path().as_ref(),
2343 Path::new("file3.rs")
2344 );
2345
2346 new_buffer_a.read_with(cx_a, |buffer_a, _| {
2347 assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime());
2348 assert_eq!(buffer_b.saved_version(), buffer_a.saved_version());
2349 });
2350 });
2351}
2352
2353#[gpui::test(iterations = 10)]
2354async fn test_git_diff_base_change(
2355 deterministic: Arc<Deterministic>,
2356 cx_a: &mut TestAppContext,
2357 cx_b: &mut TestAppContext,
2358) {
2359 deterministic.forbid_parking();
2360 let mut server = TestServer::start(&deterministic).await;
2361 let client_a = server.create_client(cx_a, "user_a").await;
2362 let client_b = server.create_client(cx_b, "user_b").await;
2363 server
2364 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2365 .await;
2366 let active_call_a = cx_a.read(ActiveCall::global);
2367
2368 client_a
2369 .fs
2370 .insert_tree(
2371 "/dir",
2372 json!({
2373 ".git": {},
2374 "sub": {
2375 ".git": {},
2376 "b.txt": "
2377 one
2378 two
2379 three
2380 ".unindent(),
2381 },
2382 "a.txt": "
2383 one
2384 two
2385 three
2386 ".unindent(),
2387 }),
2388 )
2389 .await;
2390
2391 let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2392 let project_id = active_call_a
2393 .update(cx_a, |call, cx| {
2394 call.share_project(project_local.clone(), cx)
2395 })
2396 .await
2397 .unwrap();
2398
2399 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2400
2401 let diff_base = "
2402 one
2403 three
2404 "
2405 .unindent();
2406
2407 let new_diff_base = "
2408 one
2409 two
2410 "
2411 .unindent();
2412
2413 client_a
2414 .fs
2415 .as_fake()
2416 .set_index_for_repo(
2417 Path::new("/dir/.git"),
2418 &[(Path::new("a.txt"), diff_base.clone())],
2419 )
2420 .await;
2421
2422 // Create the buffer
2423 let buffer_local_a = project_local
2424 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2425 .await
2426 .unwrap();
2427
2428 // Wait for it to catch up to the new diff
2429 deterministic.run_until_parked();
2430
2431 // Smoke test diffing
2432 buffer_local_a.read_with(cx_a, |buffer, _| {
2433 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2434 git::diff::assert_hunks(
2435 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2436 &buffer,
2437 &diff_base,
2438 &[(1..2, "", "two\n")],
2439 );
2440 });
2441
2442 // Create remote buffer
2443 let buffer_remote_a = project_remote
2444 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2445 .await
2446 .unwrap();
2447
2448 // Wait remote buffer to catch up to the new diff
2449 deterministic.run_until_parked();
2450
2451 // Smoke test diffing
2452 buffer_remote_a.read_with(cx_b, |buffer, _| {
2453 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2454 git::diff::assert_hunks(
2455 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2456 &buffer,
2457 &diff_base,
2458 &[(1..2, "", "two\n")],
2459 );
2460 });
2461
2462 client_a
2463 .fs
2464 .as_fake()
2465 .set_index_for_repo(
2466 Path::new("/dir/.git"),
2467 &[(Path::new("a.txt"), new_diff_base.clone())],
2468 )
2469 .await;
2470
2471 // Wait for buffer_local_a to receive it
2472 deterministic.run_until_parked();
2473
2474 // Smoke test new diffing
2475 buffer_local_a.read_with(cx_a, |buffer, _| {
2476 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2477
2478 git::diff::assert_hunks(
2479 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2480 &buffer,
2481 &diff_base,
2482 &[(2..3, "", "three\n")],
2483 );
2484 });
2485
2486 // Smoke test B
2487 buffer_remote_a.read_with(cx_b, |buffer, _| {
2488 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2489 git::diff::assert_hunks(
2490 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2491 &buffer,
2492 &diff_base,
2493 &[(2..3, "", "three\n")],
2494 );
2495 });
2496
2497 //Nested git dir
2498
2499 let diff_base = "
2500 one
2501 three
2502 "
2503 .unindent();
2504
2505 let new_diff_base = "
2506 one
2507 two
2508 "
2509 .unindent();
2510
2511 client_a
2512 .fs
2513 .as_fake()
2514 .set_index_for_repo(
2515 Path::new("/dir/sub/.git"),
2516 &[(Path::new("b.txt"), diff_base.clone())],
2517 )
2518 .await;
2519
2520 // Create the buffer
2521 let buffer_local_b = project_local
2522 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2523 .await
2524 .unwrap();
2525
2526 // Wait for it to catch up to the new diff
2527 deterministic.run_until_parked();
2528
2529 // Smoke test diffing
2530 buffer_local_b.read_with(cx_a, |buffer, _| {
2531 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2532 git::diff::assert_hunks(
2533 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2534 &buffer,
2535 &diff_base,
2536 &[(1..2, "", "two\n")],
2537 );
2538 });
2539
2540 // Create remote buffer
2541 let buffer_remote_b = project_remote
2542 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2543 .await
2544 .unwrap();
2545
2546 // Wait remote buffer to catch up to the new diff
2547 deterministic.run_until_parked();
2548
2549 // Smoke test diffing
2550 buffer_remote_b.read_with(cx_b, |buffer, _| {
2551 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2552 git::diff::assert_hunks(
2553 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2554 &buffer,
2555 &diff_base,
2556 &[(1..2, "", "two\n")],
2557 );
2558 });
2559
2560 client_a
2561 .fs
2562 .as_fake()
2563 .set_index_for_repo(
2564 Path::new("/dir/sub/.git"),
2565 &[(Path::new("b.txt"), new_diff_base.clone())],
2566 )
2567 .await;
2568
2569 // Wait for buffer_local_b to receive it
2570 deterministic.run_until_parked();
2571
2572 // Smoke test new diffing
2573 buffer_local_b.read_with(cx_a, |buffer, _| {
2574 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2575 println!("{:?}", buffer.as_rope().to_string());
2576 println!("{:?}", buffer.diff_base());
2577 println!(
2578 "{:?}",
2579 buffer
2580 .snapshot()
2581 .git_diff_hunks_in_row_range(0..4, false)
2582 .collect::<Vec<_>>()
2583 );
2584
2585 git::diff::assert_hunks(
2586 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2587 &buffer,
2588 &diff_base,
2589 &[(2..3, "", "three\n")],
2590 );
2591 });
2592
2593 // Smoke test B
2594 buffer_remote_b.read_with(cx_b, |buffer, _| {
2595 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2596 git::diff::assert_hunks(
2597 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2598 &buffer,
2599 &diff_base,
2600 &[(2..3, "", "three\n")],
2601 );
2602 });
2603}
2604
2605#[gpui::test(iterations = 10)]
2606async fn test_fs_operations(
2607 deterministic: Arc<Deterministic>,
2608 cx_a: &mut TestAppContext,
2609 cx_b: &mut TestAppContext,
2610) {
2611 deterministic.forbid_parking();
2612 let mut server = TestServer::start(&deterministic).await;
2613 let client_a = server.create_client(cx_a, "user_a").await;
2614 let client_b = server.create_client(cx_b, "user_b").await;
2615 server
2616 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2617 .await;
2618 let active_call_a = cx_a.read(ActiveCall::global);
2619
2620 client_a
2621 .fs
2622 .insert_tree(
2623 "/dir",
2624 json!({
2625 "a.txt": "a-contents",
2626 "b.txt": "b-contents",
2627 }),
2628 )
2629 .await;
2630 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2631 let project_id = active_call_a
2632 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2633 .await
2634 .unwrap();
2635 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2636
2637 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
2638 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
2639
2640 let entry = project_b
2641 .update(cx_b, |project, cx| {
2642 project
2643 .create_entry((worktree_id, "c.txt"), false, cx)
2644 .unwrap()
2645 })
2646 .await
2647 .unwrap();
2648 worktree_a.read_with(cx_a, |worktree, _| {
2649 assert_eq!(
2650 worktree
2651 .paths()
2652 .map(|p| p.to_string_lossy())
2653 .collect::<Vec<_>>(),
2654 ["a.txt", "b.txt", "c.txt"]
2655 );
2656 });
2657 worktree_b.read_with(cx_b, |worktree, _| {
2658 assert_eq!(
2659 worktree
2660 .paths()
2661 .map(|p| p.to_string_lossy())
2662 .collect::<Vec<_>>(),
2663 ["a.txt", "b.txt", "c.txt"]
2664 );
2665 });
2666
2667 project_b
2668 .update(cx_b, |project, cx| {
2669 project.rename_entry(entry.id, Path::new("d.txt"), cx)
2670 })
2671 .unwrap()
2672 .await
2673 .unwrap();
2674 worktree_a.read_with(cx_a, |worktree, _| {
2675 assert_eq!(
2676 worktree
2677 .paths()
2678 .map(|p| p.to_string_lossy())
2679 .collect::<Vec<_>>(),
2680 ["a.txt", "b.txt", "d.txt"]
2681 );
2682 });
2683 worktree_b.read_with(cx_b, |worktree, _| {
2684 assert_eq!(
2685 worktree
2686 .paths()
2687 .map(|p| p.to_string_lossy())
2688 .collect::<Vec<_>>(),
2689 ["a.txt", "b.txt", "d.txt"]
2690 );
2691 });
2692
2693 let dir_entry = project_b
2694 .update(cx_b, |project, cx| {
2695 project
2696 .create_entry((worktree_id, "DIR"), true, cx)
2697 .unwrap()
2698 })
2699 .await
2700 .unwrap();
2701 worktree_a.read_with(cx_a, |worktree, _| {
2702 assert_eq!(
2703 worktree
2704 .paths()
2705 .map(|p| p.to_string_lossy())
2706 .collect::<Vec<_>>(),
2707 ["DIR", "a.txt", "b.txt", "d.txt"]
2708 );
2709 });
2710 worktree_b.read_with(cx_b, |worktree, _| {
2711 assert_eq!(
2712 worktree
2713 .paths()
2714 .map(|p| p.to_string_lossy())
2715 .collect::<Vec<_>>(),
2716 ["DIR", "a.txt", "b.txt", "d.txt"]
2717 );
2718 });
2719
2720 project_b
2721 .update(cx_b, |project, cx| {
2722 project
2723 .create_entry((worktree_id, "DIR/e.txt"), false, cx)
2724 .unwrap()
2725 })
2726 .await
2727 .unwrap();
2728 project_b
2729 .update(cx_b, |project, cx| {
2730 project
2731 .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
2732 .unwrap()
2733 })
2734 .await
2735 .unwrap();
2736 project_b
2737 .update(cx_b, |project, cx| {
2738 project
2739 .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
2740 .unwrap()
2741 })
2742 .await
2743 .unwrap();
2744 worktree_a.read_with(cx_a, |worktree, _| {
2745 assert_eq!(
2746 worktree
2747 .paths()
2748 .map(|p| p.to_string_lossy())
2749 .collect::<Vec<_>>(),
2750 [
2751 "DIR",
2752 "DIR/SUBDIR",
2753 "DIR/SUBDIR/f.txt",
2754 "DIR/e.txt",
2755 "a.txt",
2756 "b.txt",
2757 "d.txt"
2758 ]
2759 );
2760 });
2761 worktree_b.read_with(cx_b, |worktree, _| {
2762 assert_eq!(
2763 worktree
2764 .paths()
2765 .map(|p| p.to_string_lossy())
2766 .collect::<Vec<_>>(),
2767 [
2768 "DIR",
2769 "DIR/SUBDIR",
2770 "DIR/SUBDIR/f.txt",
2771 "DIR/e.txt",
2772 "a.txt",
2773 "b.txt",
2774 "d.txt"
2775 ]
2776 );
2777 });
2778
2779 project_b
2780 .update(cx_b, |project, cx| {
2781 project
2782 .copy_entry(entry.id, Path::new("f.txt"), cx)
2783 .unwrap()
2784 })
2785 .await
2786 .unwrap();
2787 worktree_a.read_with(cx_a, |worktree, _| {
2788 assert_eq!(
2789 worktree
2790 .paths()
2791 .map(|p| p.to_string_lossy())
2792 .collect::<Vec<_>>(),
2793 [
2794 "DIR",
2795 "DIR/SUBDIR",
2796 "DIR/SUBDIR/f.txt",
2797 "DIR/e.txt",
2798 "a.txt",
2799 "b.txt",
2800 "d.txt",
2801 "f.txt"
2802 ]
2803 );
2804 });
2805 worktree_b.read_with(cx_b, |worktree, _| {
2806 assert_eq!(
2807 worktree
2808 .paths()
2809 .map(|p| p.to_string_lossy())
2810 .collect::<Vec<_>>(),
2811 [
2812 "DIR",
2813 "DIR/SUBDIR",
2814 "DIR/SUBDIR/f.txt",
2815 "DIR/e.txt",
2816 "a.txt",
2817 "b.txt",
2818 "d.txt",
2819 "f.txt"
2820 ]
2821 );
2822 });
2823
2824 project_b
2825 .update(cx_b, |project, cx| {
2826 project.delete_entry(dir_entry.id, cx).unwrap()
2827 })
2828 .await
2829 .unwrap();
2830 deterministic.run_until_parked();
2831
2832 worktree_a.read_with(cx_a, |worktree, _| {
2833 assert_eq!(
2834 worktree
2835 .paths()
2836 .map(|p| p.to_string_lossy())
2837 .collect::<Vec<_>>(),
2838 ["a.txt", "b.txt", "d.txt", "f.txt"]
2839 );
2840 });
2841 worktree_b.read_with(cx_b, |worktree, _| {
2842 assert_eq!(
2843 worktree
2844 .paths()
2845 .map(|p| p.to_string_lossy())
2846 .collect::<Vec<_>>(),
2847 ["a.txt", "b.txt", "d.txt", "f.txt"]
2848 );
2849 });
2850
2851 project_b
2852 .update(cx_b, |project, cx| {
2853 project.delete_entry(entry.id, cx).unwrap()
2854 })
2855 .await
2856 .unwrap();
2857 worktree_a.read_with(cx_a, |worktree, _| {
2858 assert_eq!(
2859 worktree
2860 .paths()
2861 .map(|p| p.to_string_lossy())
2862 .collect::<Vec<_>>(),
2863 ["a.txt", "b.txt", "f.txt"]
2864 );
2865 });
2866 worktree_b.read_with(cx_b, |worktree, _| {
2867 assert_eq!(
2868 worktree
2869 .paths()
2870 .map(|p| p.to_string_lossy())
2871 .collect::<Vec<_>>(),
2872 ["a.txt", "b.txt", "f.txt"]
2873 );
2874 });
2875}
2876
2877#[gpui::test(iterations = 10)]
2878async fn test_buffer_conflict_after_save(
2879 deterministic: Arc<Deterministic>,
2880 cx_a: &mut TestAppContext,
2881 cx_b: &mut TestAppContext,
2882) {
2883 deterministic.forbid_parking();
2884 let mut server = TestServer::start(&deterministic).await;
2885 let client_a = server.create_client(cx_a, "user_a").await;
2886 let client_b = server.create_client(cx_b, "user_b").await;
2887 server
2888 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2889 .await;
2890 let active_call_a = cx_a.read(ActiveCall::global);
2891
2892 client_a
2893 .fs
2894 .insert_tree(
2895 "/dir",
2896 json!({
2897 "a.txt": "a-contents",
2898 }),
2899 )
2900 .await;
2901 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2902 let project_id = active_call_a
2903 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2904 .await
2905 .unwrap();
2906 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2907
2908 // Open a buffer as client B
2909 let buffer_b = project_b
2910 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2911 .await
2912 .unwrap();
2913
2914 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
2915 buffer_b.read_with(cx_b, |buf, _| {
2916 assert!(buf.is_dirty());
2917 assert!(!buf.has_conflict());
2918 });
2919
2920 cx_b.update(|cx| Project::save_buffer(buffer_b.clone(), cx))
2921 .await
2922 .unwrap();
2923 cx_a.foreground().forbid_parking();
2924 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
2925 buffer_b.read_with(cx_b, |buf, _| {
2926 assert!(!buf.has_conflict());
2927 });
2928
2929 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
2930 buffer_b.read_with(cx_b, |buf, _| {
2931 assert!(buf.is_dirty());
2932 assert!(!buf.has_conflict());
2933 });
2934}
2935
2936#[gpui::test(iterations = 10)]
2937async fn test_buffer_reloading(
2938 deterministic: Arc<Deterministic>,
2939 cx_a: &mut TestAppContext,
2940 cx_b: &mut TestAppContext,
2941) {
2942 deterministic.forbid_parking();
2943 let mut server = TestServer::start(&deterministic).await;
2944 let client_a = server.create_client(cx_a, "user_a").await;
2945 let client_b = server.create_client(cx_b, "user_b").await;
2946 server
2947 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2948 .await;
2949 let active_call_a = cx_a.read(ActiveCall::global);
2950
2951 client_a
2952 .fs
2953 .insert_tree(
2954 "/dir",
2955 json!({
2956 "a.txt": "a\nb\nc",
2957 }),
2958 )
2959 .await;
2960 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2961 let project_id = active_call_a
2962 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2963 .await
2964 .unwrap();
2965 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2966
2967 // Open a buffer as client B
2968 let buffer_b = project_b
2969 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2970 .await
2971 .unwrap();
2972 buffer_b.read_with(cx_b, |buf, _| {
2973 assert!(!buf.is_dirty());
2974 assert!(!buf.has_conflict());
2975 assert_eq!(buf.line_ending(), LineEnding::Unix);
2976 });
2977
2978 let new_contents = Rope::from("d\ne\nf");
2979 client_a
2980 .fs
2981 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
2982 .await
2983 .unwrap();
2984 cx_a.foreground().run_until_parked();
2985 buffer_b.read_with(cx_b, |buf, _| {
2986 assert_eq!(buf.text(), new_contents.to_string());
2987 assert!(!buf.is_dirty());
2988 assert!(!buf.has_conflict());
2989 assert_eq!(buf.line_ending(), LineEnding::Windows);
2990 });
2991}
2992
2993#[gpui::test(iterations = 10)]
2994async fn test_editing_while_guest_opens_buffer(
2995 deterministic: Arc<Deterministic>,
2996 cx_a: &mut TestAppContext,
2997 cx_b: &mut TestAppContext,
2998) {
2999 deterministic.forbid_parking();
3000 let mut server = TestServer::start(&deterministic).await;
3001 let client_a = server.create_client(cx_a, "user_a").await;
3002 let client_b = server.create_client(cx_b, "user_b").await;
3003 server
3004 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3005 .await;
3006 let active_call_a = cx_a.read(ActiveCall::global);
3007
3008 client_a
3009 .fs
3010 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3011 .await;
3012 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3013 let project_id = active_call_a
3014 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3015 .await
3016 .unwrap();
3017 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3018
3019 // Open a buffer as client A
3020 let buffer_a = project_a
3021 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3022 .await
3023 .unwrap();
3024
3025 // Start opening the same buffer as client B
3026 let buffer_b = cx_b
3027 .background()
3028 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3029
3030 // Edit the buffer as client A while client B is still opening it.
3031 cx_b.background().simulate_random_delay().await;
3032 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3033 cx_b.background().simulate_random_delay().await;
3034 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3035
3036 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3037 let buffer_b = buffer_b.await.unwrap();
3038 cx_a.foreground().run_until_parked();
3039 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3040}
3041
3042#[gpui::test(iterations = 10)]
3043async fn test_leaving_worktree_while_opening_buffer(
3044 deterministic: Arc<Deterministic>,
3045 cx_a: &mut TestAppContext,
3046 cx_b: &mut TestAppContext,
3047) {
3048 deterministic.forbid_parking();
3049 let mut server = TestServer::start(&deterministic).await;
3050 let client_a = server.create_client(cx_a, "user_a").await;
3051 let client_b = server.create_client(cx_b, "user_b").await;
3052 server
3053 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3054 .await;
3055 let active_call_a = cx_a.read(ActiveCall::global);
3056
3057 client_a
3058 .fs
3059 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3060 .await;
3061 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3062 let project_id = active_call_a
3063 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3064 .await
3065 .unwrap();
3066 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3067
3068 // See that a guest has joined as client A.
3069 cx_a.foreground().run_until_parked();
3070 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3071
3072 // Begin opening a buffer as client B, but leave the project before the open completes.
3073 let buffer_b = cx_b
3074 .background()
3075 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3076 cx_b.update(|_| drop(project_b));
3077 drop(buffer_b);
3078
3079 // See that the guest has left.
3080 cx_a.foreground().run_until_parked();
3081 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3082}
3083
3084#[gpui::test(iterations = 10)]
3085async fn test_canceling_buffer_opening(
3086 deterministic: Arc<Deterministic>,
3087 cx_a: &mut TestAppContext,
3088 cx_b: &mut TestAppContext,
3089) {
3090 deterministic.forbid_parking();
3091
3092 let mut server = TestServer::start(&deterministic).await;
3093 let client_a = server.create_client(cx_a, "user_a").await;
3094 let client_b = server.create_client(cx_b, "user_b").await;
3095 server
3096 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3097 .await;
3098 let active_call_a = cx_a.read(ActiveCall::global);
3099
3100 client_a
3101 .fs
3102 .insert_tree(
3103 "/dir",
3104 json!({
3105 "a.txt": "abc",
3106 }),
3107 )
3108 .await;
3109 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3110 let project_id = active_call_a
3111 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3112 .await
3113 .unwrap();
3114 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3115
3116 let buffer_a = project_a
3117 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3118 .await
3119 .unwrap();
3120
3121 // Open a buffer as client B but cancel after a random amount of time.
3122 let buffer_b = project_b.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx));
3123 deterministic.simulate_random_delay().await;
3124 drop(buffer_b);
3125
3126 // Try opening the same buffer again as client B, and ensure we can
3127 // still do it despite the cancellation above.
3128 let buffer_b = project_b
3129 .update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx))
3130 .await
3131 .unwrap();
3132 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3133}
3134
3135#[gpui::test(iterations = 10)]
3136async fn test_leaving_project(
3137 deterministic: Arc<Deterministic>,
3138 cx_a: &mut TestAppContext,
3139 cx_b: &mut TestAppContext,
3140 cx_c: &mut TestAppContext,
3141) {
3142 deterministic.forbid_parking();
3143 let mut server = TestServer::start(&deterministic).await;
3144 let client_a = server.create_client(cx_a, "user_a").await;
3145 let client_b = server.create_client(cx_b, "user_b").await;
3146 let client_c = server.create_client(cx_c, "user_c").await;
3147 server
3148 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3149 .await;
3150 let active_call_a = cx_a.read(ActiveCall::global);
3151
3152 client_a
3153 .fs
3154 .insert_tree(
3155 "/a",
3156 json!({
3157 "a.txt": "a-contents",
3158 "b.txt": "b-contents",
3159 }),
3160 )
3161 .await;
3162 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3163 let project_id = active_call_a
3164 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3165 .await
3166 .unwrap();
3167 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3168 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3169
3170 // Client A sees that a guest has joined.
3171 deterministic.run_until_parked();
3172 project_a.read_with(cx_a, |project, _| {
3173 assert_eq!(project.collaborators().len(), 2);
3174 });
3175 project_b1.read_with(cx_b, |project, _| {
3176 assert_eq!(project.collaborators().len(), 2);
3177 });
3178 project_c.read_with(cx_c, |project, _| {
3179 assert_eq!(project.collaborators().len(), 2);
3180 });
3181
3182 // Client B opens a buffer.
3183 let buffer_b1 = project_b1
3184 .update(cx_b, |project, cx| {
3185 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3186 project.open_buffer((worktree_id, "a.txt"), cx)
3187 })
3188 .await
3189 .unwrap();
3190 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3191
3192 // Drop client B's project and ensure client A and client C observe client B leaving.
3193 cx_b.update(|_| drop(project_b1));
3194 deterministic.run_until_parked();
3195 project_a.read_with(cx_a, |project, _| {
3196 assert_eq!(project.collaborators().len(), 1);
3197 });
3198 project_c.read_with(cx_c, |project, _| {
3199 assert_eq!(project.collaborators().len(), 1);
3200 });
3201
3202 // Client B re-joins the project and can open buffers as before.
3203 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3204 deterministic.run_until_parked();
3205 project_a.read_with(cx_a, |project, _| {
3206 assert_eq!(project.collaborators().len(), 2);
3207 });
3208 project_b2.read_with(cx_b, |project, _| {
3209 assert_eq!(project.collaborators().len(), 2);
3210 });
3211 project_c.read_with(cx_c, |project, _| {
3212 assert_eq!(project.collaborators().len(), 2);
3213 });
3214
3215 let buffer_b2 = project_b2
3216 .update(cx_b, |project, cx| {
3217 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3218 project.open_buffer((worktree_id, "a.txt"), cx)
3219 })
3220 .await
3221 .unwrap();
3222 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3223
3224 // Drop client B's connection and ensure client A and client C observe client B leaving.
3225 client_b.disconnect(&cx_b.to_async()).unwrap();
3226 deterministic.advance_clock(RECONNECT_TIMEOUT);
3227 project_a.read_with(cx_a, |project, _| {
3228 assert_eq!(project.collaborators().len(), 1);
3229 });
3230 project_b2.read_with(cx_b, |project, _| {
3231 assert!(project.is_read_only());
3232 });
3233 project_c.read_with(cx_c, |project, _| {
3234 assert_eq!(project.collaborators().len(), 1);
3235 });
3236
3237 // Client B can't join the project, unless they re-join the room.
3238 cx_b.spawn(|cx| {
3239 Project::remote(
3240 project_id,
3241 client_b.client.clone(),
3242 client_b.user_store.clone(),
3243 client_b.language_registry.clone(),
3244 FakeFs::new(cx.background()),
3245 cx,
3246 )
3247 })
3248 .await
3249 .unwrap_err();
3250
3251 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3252 client_c.wait_for_current_user(cx_c).await;
3253 server.forbid_connections();
3254 server.disconnect_client(client_c.peer_id().unwrap());
3255 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3256 deterministic.run_until_parked();
3257 project_a.read_with(cx_a, |project, _| {
3258 assert_eq!(project.collaborators().len(), 0);
3259 });
3260 project_b2.read_with(cx_b, |project, _| {
3261 assert!(project.is_read_only());
3262 });
3263 project_c.read_with(cx_c, |project, _| {
3264 assert!(project.is_read_only());
3265 });
3266}
3267
3268#[gpui::test(iterations = 10)]
3269async fn test_collaborating_with_diagnostics(
3270 deterministic: Arc<Deterministic>,
3271 cx_a: &mut TestAppContext,
3272 cx_b: &mut TestAppContext,
3273 cx_c: &mut TestAppContext,
3274) {
3275 deterministic.forbid_parking();
3276 let mut server = TestServer::start(&deterministic).await;
3277 let client_a = server.create_client(cx_a, "user_a").await;
3278 let client_b = server.create_client(cx_b, "user_b").await;
3279 let client_c = server.create_client(cx_c, "user_c").await;
3280 server
3281 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3282 .await;
3283 let active_call_a = cx_a.read(ActiveCall::global);
3284
3285 // Set up a fake language server.
3286 let mut language = Language::new(
3287 LanguageConfig {
3288 name: "Rust".into(),
3289 path_suffixes: vec!["rs".to_string()],
3290 ..Default::default()
3291 },
3292 Some(tree_sitter_rust::language()),
3293 );
3294 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3295 client_a.language_registry.add(Arc::new(language));
3296
3297 // Share a project as client A
3298 client_a
3299 .fs
3300 .insert_tree(
3301 "/a",
3302 json!({
3303 "a.rs": "let one = two",
3304 "other.rs": "",
3305 }),
3306 )
3307 .await;
3308 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3309
3310 // Cause the language server to start.
3311 let _buffer = project_a
3312 .update(cx_a, |project, cx| {
3313 project.open_buffer(
3314 ProjectPath {
3315 worktree_id,
3316 path: Path::new("other.rs").into(),
3317 },
3318 cx,
3319 )
3320 })
3321 .await
3322 .unwrap();
3323
3324 // Simulate a language server reporting errors for a file.
3325 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3326 fake_language_server
3327 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3328 .await;
3329 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3330 lsp::PublishDiagnosticsParams {
3331 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3332 version: None,
3333 diagnostics: vec![lsp::Diagnostic {
3334 severity: Some(lsp::DiagnosticSeverity::WARNING),
3335 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3336 message: "message 0".to_string(),
3337 ..Default::default()
3338 }],
3339 },
3340 );
3341
3342 // Client A shares the project and, simultaneously, the language server
3343 // publishes a diagnostic. This is done to ensure that the server always
3344 // observes the latest diagnostics for a worktree.
3345 let project_id = active_call_a
3346 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3347 .await
3348 .unwrap();
3349 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3350 lsp::PublishDiagnosticsParams {
3351 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3352 version: None,
3353 diagnostics: vec![lsp::Diagnostic {
3354 severity: Some(lsp::DiagnosticSeverity::ERROR),
3355 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3356 message: "message 1".to_string(),
3357 ..Default::default()
3358 }],
3359 },
3360 );
3361
3362 // Join the worktree as client B.
3363 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3364
3365 // Wait for server to see the diagnostics update.
3366 deterministic.run_until_parked();
3367
3368 // Ensure client B observes the new diagnostics.
3369 project_b.read_with(cx_b, |project, cx| {
3370 assert_eq!(
3371 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3372 &[(
3373 ProjectPath {
3374 worktree_id,
3375 path: Arc::from(Path::new("a.rs")),
3376 },
3377 DiagnosticSummary {
3378 error_count: 1,
3379 warning_count: 0,
3380 ..Default::default()
3381 },
3382 )]
3383 )
3384 });
3385
3386 // Join project as client C and observe the diagnostics.
3387 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3388 let project_c_diagnostic_summaries =
3389 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3390 project.diagnostic_summaries(cx).collect::<Vec<_>>()
3391 })));
3392 project_c.update(cx_c, |_, cx| {
3393 let summaries = project_c_diagnostic_summaries.clone();
3394 cx.subscribe(&project_c, {
3395 move |p, _, event, cx| {
3396 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3397 *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
3398 }
3399 }
3400 })
3401 .detach();
3402 });
3403
3404 deterministic.run_until_parked();
3405 assert_eq!(
3406 project_c_diagnostic_summaries.borrow().as_slice(),
3407 &[(
3408 ProjectPath {
3409 worktree_id,
3410 path: Arc::from(Path::new("a.rs")),
3411 },
3412 DiagnosticSummary {
3413 error_count: 1,
3414 warning_count: 0,
3415 ..Default::default()
3416 },
3417 )]
3418 );
3419
3420 // Simulate a language server reporting more errors for a file.
3421 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3422 lsp::PublishDiagnosticsParams {
3423 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3424 version: None,
3425 diagnostics: vec![
3426 lsp::Diagnostic {
3427 severity: Some(lsp::DiagnosticSeverity::ERROR),
3428 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3429 message: "message 1".to_string(),
3430 ..Default::default()
3431 },
3432 lsp::Diagnostic {
3433 severity: Some(lsp::DiagnosticSeverity::WARNING),
3434 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3435 message: "message 2".to_string(),
3436 ..Default::default()
3437 },
3438 ],
3439 },
3440 );
3441
3442 // Clients B and C get the updated summaries
3443 deterministic.run_until_parked();
3444 project_b.read_with(cx_b, |project, cx| {
3445 assert_eq!(
3446 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3447 [(
3448 ProjectPath {
3449 worktree_id,
3450 path: Arc::from(Path::new("a.rs")),
3451 },
3452 DiagnosticSummary {
3453 error_count: 1,
3454 warning_count: 1,
3455 ..Default::default()
3456 },
3457 )]
3458 );
3459 });
3460 project_c.read_with(cx_c, |project, cx| {
3461 assert_eq!(
3462 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3463 [(
3464 ProjectPath {
3465 worktree_id,
3466 path: Arc::from(Path::new("a.rs")),
3467 },
3468 DiagnosticSummary {
3469 error_count: 1,
3470 warning_count: 1,
3471 ..Default::default()
3472 },
3473 )]
3474 );
3475 });
3476
3477 // Open the file with the errors on client B. They should be present.
3478 let buffer_b = cx_b
3479 .background()
3480 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3481 .await
3482 .unwrap();
3483
3484 buffer_b.read_with(cx_b, |buffer, _| {
3485 assert_eq!(
3486 buffer
3487 .snapshot()
3488 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3489 .collect::<Vec<_>>(),
3490 &[
3491 DiagnosticEntry {
3492 range: Point::new(0, 4)..Point::new(0, 7),
3493 diagnostic: Diagnostic {
3494 group_id: 2,
3495 message: "message 1".to_string(),
3496 severity: lsp::DiagnosticSeverity::ERROR,
3497 is_primary: true,
3498 ..Default::default()
3499 }
3500 },
3501 DiagnosticEntry {
3502 range: Point::new(0, 10)..Point::new(0, 13),
3503 diagnostic: Diagnostic {
3504 group_id: 3,
3505 severity: lsp::DiagnosticSeverity::WARNING,
3506 message: "message 2".to_string(),
3507 is_primary: true,
3508 ..Default::default()
3509 }
3510 }
3511 ]
3512 );
3513 });
3514
3515 // Simulate a language server reporting no errors for a file.
3516 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3517 lsp::PublishDiagnosticsParams {
3518 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3519 version: None,
3520 diagnostics: vec![],
3521 },
3522 );
3523 deterministic.run_until_parked();
3524 project_a.read_with(cx_a, |project, cx| {
3525 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3526 });
3527 project_b.read_with(cx_b, |project, cx| {
3528 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3529 });
3530 project_c.read_with(cx_c, |project, cx| {
3531 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3532 });
3533}
3534
3535#[gpui::test(iterations = 10)]
3536async fn test_collaborating_with_completion(
3537 deterministic: Arc<Deterministic>,
3538 cx_a: &mut TestAppContext,
3539 cx_b: &mut TestAppContext,
3540) {
3541 deterministic.forbid_parking();
3542 let mut server = TestServer::start(&deterministic).await;
3543 let client_a = server.create_client(cx_a, "user_a").await;
3544 let client_b = server.create_client(cx_b, "user_b").await;
3545 server
3546 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3547 .await;
3548 let active_call_a = cx_a.read(ActiveCall::global);
3549
3550 // Set up a fake language server.
3551 let mut language = Language::new(
3552 LanguageConfig {
3553 name: "Rust".into(),
3554 path_suffixes: vec!["rs".to_string()],
3555 ..Default::default()
3556 },
3557 Some(tree_sitter_rust::language()),
3558 );
3559 let mut fake_language_servers = language
3560 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3561 capabilities: lsp::ServerCapabilities {
3562 completion_provider: Some(lsp::CompletionOptions {
3563 trigger_characters: Some(vec![".".to_string()]),
3564 ..Default::default()
3565 }),
3566 ..Default::default()
3567 },
3568 ..Default::default()
3569 }))
3570 .await;
3571 client_a.language_registry.add(Arc::new(language));
3572
3573 client_a
3574 .fs
3575 .insert_tree(
3576 "/a",
3577 json!({
3578 "main.rs": "fn main() { a }",
3579 "other.rs": "",
3580 }),
3581 )
3582 .await;
3583 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3584 let project_id = active_call_a
3585 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3586 .await
3587 .unwrap();
3588 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3589
3590 // Open a file in an editor as the guest.
3591 let buffer_b = project_b
3592 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3593 .await
3594 .unwrap();
3595 let (_, window_b) = cx_b.add_window(|_| EmptyView);
3596 let editor_b = cx_b.add_view(&window_b, |cx| {
3597 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
3598 });
3599
3600 let fake_language_server = fake_language_servers.next().await.unwrap();
3601 cx_a.foreground().run_until_parked();
3602 buffer_b.read_with(cx_b, |buffer, _| {
3603 assert!(!buffer.completion_triggers().is_empty())
3604 });
3605
3606 // Type a completion trigger character as the guest.
3607 editor_b.update(cx_b, |editor, cx| {
3608 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
3609 editor.handle_input(".", cx);
3610 cx.focus(&editor_b);
3611 });
3612
3613 // Receive a completion request as the host's language server.
3614 // Return some completions from the host's language server.
3615 cx_a.foreground().start_waiting();
3616 fake_language_server
3617 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
3618 assert_eq!(
3619 params.text_document_position.text_document.uri,
3620 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3621 );
3622 assert_eq!(
3623 params.text_document_position.position,
3624 lsp::Position::new(0, 14),
3625 );
3626
3627 Ok(Some(lsp::CompletionResponse::Array(vec![
3628 lsp::CompletionItem {
3629 label: "first_method(…)".into(),
3630 detail: Some("fn(&mut self, B) -> C".into()),
3631 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3632 new_text: "first_method($1)".to_string(),
3633 range: lsp::Range::new(
3634 lsp::Position::new(0, 14),
3635 lsp::Position::new(0, 14),
3636 ),
3637 })),
3638 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3639 ..Default::default()
3640 },
3641 lsp::CompletionItem {
3642 label: "second_method(…)".into(),
3643 detail: Some("fn(&mut self, C) -> D<E>".into()),
3644 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3645 new_text: "second_method()".to_string(),
3646 range: lsp::Range::new(
3647 lsp::Position::new(0, 14),
3648 lsp::Position::new(0, 14),
3649 ),
3650 })),
3651 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3652 ..Default::default()
3653 },
3654 ])))
3655 })
3656 .next()
3657 .await
3658 .unwrap();
3659 cx_a.foreground().finish_waiting();
3660
3661 // Open the buffer on the host.
3662 let buffer_a = project_a
3663 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3664 .await
3665 .unwrap();
3666 cx_a.foreground().run_until_parked();
3667 buffer_a.read_with(cx_a, |buffer, _| {
3668 assert_eq!(buffer.text(), "fn main() { a. }")
3669 });
3670
3671 // Confirm a completion on the guest.
3672 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
3673 editor_b.update(cx_b, |editor, cx| {
3674 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
3675 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
3676 });
3677
3678 // Return a resolved completion from the host's language server.
3679 // The resolved completion has an additional text edit.
3680 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
3681 |params, _| async move {
3682 assert_eq!(params.label, "first_method(…)");
3683 Ok(lsp::CompletionItem {
3684 label: "first_method(…)".into(),
3685 detail: Some("fn(&mut self, B) -> C".into()),
3686 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3687 new_text: "first_method($1)".to_string(),
3688 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
3689 })),
3690 additional_text_edits: Some(vec![lsp::TextEdit {
3691 new_text: "use d::SomeTrait;\n".to_string(),
3692 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3693 }]),
3694 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3695 ..Default::default()
3696 })
3697 },
3698 );
3699
3700 // The additional edit is applied.
3701 cx_a.foreground().run_until_parked();
3702 buffer_a.read_with(cx_a, |buffer, _| {
3703 assert_eq!(
3704 buffer.text(),
3705 "use d::SomeTrait;\nfn main() { a.first_method() }"
3706 );
3707 });
3708 buffer_b.read_with(cx_b, |buffer, _| {
3709 assert_eq!(
3710 buffer.text(),
3711 "use d::SomeTrait;\nfn main() { a.first_method() }"
3712 );
3713 });
3714}
3715
3716#[gpui::test(iterations = 10)]
3717async fn test_reloading_buffer_manually(
3718 deterministic: Arc<Deterministic>,
3719 cx_a: &mut TestAppContext,
3720 cx_b: &mut TestAppContext,
3721) {
3722 deterministic.forbid_parking();
3723 let mut server = TestServer::start(&deterministic).await;
3724 let client_a = server.create_client(cx_a, "user_a").await;
3725 let client_b = server.create_client(cx_b, "user_b").await;
3726 server
3727 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3728 .await;
3729 let active_call_a = cx_a.read(ActiveCall::global);
3730
3731 client_a
3732 .fs
3733 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
3734 .await;
3735 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3736 let buffer_a = project_a
3737 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
3738 .await
3739 .unwrap();
3740 let project_id = active_call_a
3741 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3742 .await
3743 .unwrap();
3744
3745 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3746
3747 let buffer_b = cx_b
3748 .background()
3749 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3750 .await
3751 .unwrap();
3752 buffer_b.update(cx_b, |buffer, cx| {
3753 buffer.edit([(4..7, "six")], None, cx);
3754 buffer.edit([(10..11, "6")], None, cx);
3755 assert_eq!(buffer.text(), "let six = 6;");
3756 assert!(buffer.is_dirty());
3757 assert!(!buffer.has_conflict());
3758 });
3759 cx_a.foreground().run_until_parked();
3760 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
3761
3762 client_a
3763 .fs
3764 .save(
3765 "/a/a.rs".as_ref(),
3766 &Rope::from("let seven = 7;"),
3767 LineEnding::Unix,
3768 )
3769 .await
3770 .unwrap();
3771 cx_a.foreground().run_until_parked();
3772 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
3773 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
3774
3775 project_b
3776 .update(cx_b, |project, cx| {
3777 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
3778 })
3779 .await
3780 .unwrap();
3781 buffer_a.read_with(cx_a, |buffer, _| {
3782 assert_eq!(buffer.text(), "let seven = 7;");
3783 assert!(!buffer.is_dirty());
3784 assert!(!buffer.has_conflict());
3785 });
3786 buffer_b.read_with(cx_b, |buffer, _| {
3787 assert_eq!(buffer.text(), "let seven = 7;");
3788 assert!(!buffer.is_dirty());
3789 assert!(!buffer.has_conflict());
3790 });
3791
3792 buffer_a.update(cx_a, |buffer, cx| {
3793 // Undoing on the host is a no-op when the reload was initiated by the guest.
3794 buffer.undo(cx);
3795 assert_eq!(buffer.text(), "let seven = 7;");
3796 assert!(!buffer.is_dirty());
3797 assert!(!buffer.has_conflict());
3798 });
3799 buffer_b.update(cx_b, |buffer, cx| {
3800 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
3801 buffer.undo(cx);
3802 assert_eq!(buffer.text(), "let six = 6;");
3803 assert!(buffer.is_dirty());
3804 assert!(!buffer.has_conflict());
3805 });
3806}
3807
3808#[gpui::test(iterations = 10)]
3809async fn test_formatting_buffer(
3810 deterministic: Arc<Deterministic>,
3811 cx_a: &mut TestAppContext,
3812 cx_b: &mut TestAppContext,
3813) {
3814 use project::FormatTrigger;
3815
3816 let mut server = TestServer::start(&deterministic).await;
3817 let client_a = server.create_client(cx_a, "user_a").await;
3818 let client_b = server.create_client(cx_b, "user_b").await;
3819 server
3820 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3821 .await;
3822 let active_call_a = cx_a.read(ActiveCall::global);
3823
3824 // Set up a fake language server.
3825 let mut language = Language::new(
3826 LanguageConfig {
3827 name: "Rust".into(),
3828 path_suffixes: vec!["rs".to_string()],
3829 ..Default::default()
3830 },
3831 Some(tree_sitter_rust::language()),
3832 );
3833 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3834 client_a.language_registry.add(Arc::new(language));
3835
3836 // Here we insert a fake tree with a directory that exists on disk. This is needed
3837 // because later we'll invoke a command, which requires passing a working directory
3838 // that points to a valid location on disk.
3839 let directory = env::current_dir().unwrap();
3840 client_a
3841 .fs
3842 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
3843 .await;
3844 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
3845 let project_id = active_call_a
3846 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3847 .await
3848 .unwrap();
3849 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3850
3851 let buffer_b = cx_b
3852 .background()
3853 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3854 .await
3855 .unwrap();
3856
3857 let fake_language_server = fake_language_servers.next().await.unwrap();
3858 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
3859 Ok(Some(vec![
3860 lsp::TextEdit {
3861 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
3862 new_text: "h".to_string(),
3863 },
3864 lsp::TextEdit {
3865 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
3866 new_text: "y".to_string(),
3867 },
3868 ]))
3869 });
3870
3871 project_b
3872 .update(cx_b, |project, cx| {
3873 project.format(
3874 HashSet::from_iter([buffer_b.clone()]),
3875 true,
3876 FormatTrigger::Save,
3877 cx,
3878 )
3879 })
3880 .await
3881 .unwrap();
3882 assert_eq!(
3883 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
3884 "let honey = \"two\""
3885 );
3886
3887 // Ensure buffer can be formatted using an external command. Notice how the
3888 // host's configuration is honored as opposed to using the guest's settings.
3889 cx_a.update(|cx| {
3890 cx.update_global(|settings: &mut Settings, _| {
3891 settings.editor_defaults.formatter = Some(Formatter::External {
3892 command: "awk".to_string(),
3893 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
3894 });
3895 });
3896 });
3897 project_b
3898 .update(cx_b, |project, cx| {
3899 project.format(
3900 HashSet::from_iter([buffer_b.clone()]),
3901 true,
3902 FormatTrigger::Save,
3903 cx,
3904 )
3905 })
3906 .await
3907 .unwrap();
3908 assert_eq!(
3909 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
3910 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
3911 );
3912}
3913
3914#[gpui::test(iterations = 10)]
3915async fn test_definition(
3916 deterministic: Arc<Deterministic>,
3917 cx_a: &mut TestAppContext,
3918 cx_b: &mut TestAppContext,
3919) {
3920 deterministic.forbid_parking();
3921 let mut server = TestServer::start(&deterministic).await;
3922 let client_a = server.create_client(cx_a, "user_a").await;
3923 let client_b = server.create_client(cx_b, "user_b").await;
3924 server
3925 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3926 .await;
3927 let active_call_a = cx_a.read(ActiveCall::global);
3928
3929 // Set up a fake language server.
3930 let mut language = Language::new(
3931 LanguageConfig {
3932 name: "Rust".into(),
3933 path_suffixes: vec!["rs".to_string()],
3934 ..Default::default()
3935 },
3936 Some(tree_sitter_rust::language()),
3937 );
3938 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3939 client_a.language_registry.add(Arc::new(language));
3940
3941 client_a
3942 .fs
3943 .insert_tree(
3944 "/root",
3945 json!({
3946 "dir-1": {
3947 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
3948 },
3949 "dir-2": {
3950 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
3951 "c.rs": "type T2 = usize;",
3952 }
3953 }),
3954 )
3955 .await;
3956 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
3957 let project_id = active_call_a
3958 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3959 .await
3960 .unwrap();
3961 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3962
3963 // Open the file on client B.
3964 let buffer_b = cx_b
3965 .background()
3966 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3967 .await
3968 .unwrap();
3969
3970 // Request the definition of a symbol as the guest.
3971 let fake_language_server = fake_language_servers.next().await.unwrap();
3972 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
3973 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
3974 lsp::Location::new(
3975 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
3976 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3977 ),
3978 )))
3979 });
3980
3981 let definitions_1 = project_b
3982 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
3983 .await
3984 .unwrap();
3985 cx_b.read(|cx| {
3986 assert_eq!(definitions_1.len(), 1);
3987 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
3988 let target_buffer = definitions_1[0].target.buffer.read(cx);
3989 assert_eq!(
3990 target_buffer.text(),
3991 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
3992 );
3993 assert_eq!(
3994 definitions_1[0].target.range.to_point(target_buffer),
3995 Point::new(0, 6)..Point::new(0, 9)
3996 );
3997 });
3998
3999 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4000 // the previous call to `definition`.
4001 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4002 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4003 lsp::Location::new(
4004 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4005 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4006 ),
4007 )))
4008 });
4009
4010 let definitions_2 = project_b
4011 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4012 .await
4013 .unwrap();
4014 cx_b.read(|cx| {
4015 assert_eq!(definitions_2.len(), 1);
4016 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4017 let target_buffer = definitions_2[0].target.buffer.read(cx);
4018 assert_eq!(
4019 target_buffer.text(),
4020 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4021 );
4022 assert_eq!(
4023 definitions_2[0].target.range.to_point(target_buffer),
4024 Point::new(1, 6)..Point::new(1, 11)
4025 );
4026 });
4027 assert_eq!(
4028 definitions_1[0].target.buffer,
4029 definitions_2[0].target.buffer
4030 );
4031
4032 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4033 |req, _| async move {
4034 assert_eq!(
4035 req.text_document_position_params.position,
4036 lsp::Position::new(0, 7)
4037 );
4038 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4039 lsp::Location::new(
4040 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4041 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4042 ),
4043 )))
4044 },
4045 );
4046
4047 let type_definitions = project_b
4048 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4049 .await
4050 .unwrap();
4051 cx_b.read(|cx| {
4052 assert_eq!(type_definitions.len(), 1);
4053 let target_buffer = type_definitions[0].target.buffer.read(cx);
4054 assert_eq!(target_buffer.text(), "type T2 = usize;");
4055 assert_eq!(
4056 type_definitions[0].target.range.to_point(target_buffer),
4057 Point::new(0, 5)..Point::new(0, 7)
4058 );
4059 });
4060}
4061
4062#[gpui::test(iterations = 10)]
4063async fn test_references(
4064 deterministic: Arc<Deterministic>,
4065 cx_a: &mut TestAppContext,
4066 cx_b: &mut TestAppContext,
4067) {
4068 deterministic.forbid_parking();
4069 let mut server = TestServer::start(&deterministic).await;
4070 let client_a = server.create_client(cx_a, "user_a").await;
4071 let client_b = server.create_client(cx_b, "user_b").await;
4072 server
4073 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4074 .await;
4075 let active_call_a = cx_a.read(ActiveCall::global);
4076
4077 // Set up a fake language server.
4078 let mut language = Language::new(
4079 LanguageConfig {
4080 name: "Rust".into(),
4081 path_suffixes: vec!["rs".to_string()],
4082 ..Default::default()
4083 },
4084 Some(tree_sitter_rust::language()),
4085 );
4086 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4087 client_a.language_registry.add(Arc::new(language));
4088
4089 client_a
4090 .fs
4091 .insert_tree(
4092 "/root",
4093 json!({
4094 "dir-1": {
4095 "one.rs": "const ONE: usize = 1;",
4096 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4097 },
4098 "dir-2": {
4099 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4100 }
4101 }),
4102 )
4103 .await;
4104 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4105 let project_id = active_call_a
4106 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4107 .await
4108 .unwrap();
4109 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4110
4111 // Open the file on client B.
4112 let buffer_b = cx_b
4113 .background()
4114 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4115 .await
4116 .unwrap();
4117
4118 // Request references to a symbol as the guest.
4119 let fake_language_server = fake_language_servers.next().await.unwrap();
4120 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4121 assert_eq!(
4122 params.text_document_position.text_document.uri.as_str(),
4123 "file:///root/dir-1/one.rs"
4124 );
4125 Ok(Some(vec![
4126 lsp::Location {
4127 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4128 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4129 },
4130 lsp::Location {
4131 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4132 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4133 },
4134 lsp::Location {
4135 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4136 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4137 },
4138 ]))
4139 });
4140
4141 let references = project_b
4142 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4143 .await
4144 .unwrap();
4145 cx_b.read(|cx| {
4146 assert_eq!(references.len(), 3);
4147 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4148
4149 let two_buffer = references[0].buffer.read(cx);
4150 let three_buffer = references[2].buffer.read(cx);
4151 assert_eq!(
4152 two_buffer.file().unwrap().path().as_ref(),
4153 Path::new("two.rs")
4154 );
4155 assert_eq!(references[1].buffer, references[0].buffer);
4156 assert_eq!(
4157 three_buffer.file().unwrap().full_path(cx),
4158 Path::new("/root/dir-2/three.rs")
4159 );
4160
4161 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4162 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4163 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4164 });
4165}
4166
4167#[gpui::test(iterations = 10)]
4168async fn test_project_search(
4169 deterministic: Arc<Deterministic>,
4170 cx_a: &mut TestAppContext,
4171 cx_b: &mut TestAppContext,
4172) {
4173 deterministic.forbid_parking();
4174 let mut server = TestServer::start(&deterministic).await;
4175 let client_a = server.create_client(cx_a, "user_a").await;
4176 let client_b = server.create_client(cx_b, "user_b").await;
4177 server
4178 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4179 .await;
4180 let active_call_a = cx_a.read(ActiveCall::global);
4181
4182 client_a
4183 .fs
4184 .insert_tree(
4185 "/root",
4186 json!({
4187 "dir-1": {
4188 "a": "hello world",
4189 "b": "goodnight moon",
4190 "c": "a world of goo",
4191 "d": "world champion of clown world",
4192 },
4193 "dir-2": {
4194 "e": "disney world is fun",
4195 }
4196 }),
4197 )
4198 .await;
4199 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4200 let (worktree_2, _) = project_a
4201 .update(cx_a, |p, cx| {
4202 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4203 })
4204 .await
4205 .unwrap();
4206 worktree_2
4207 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4208 .await;
4209 let project_id = active_call_a
4210 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4211 .await
4212 .unwrap();
4213
4214 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4215
4216 // Perform a search as the guest.
4217 let results = project_b
4218 .update(cx_b, |project, cx| {
4219 project.search(SearchQuery::text("world", false, false), cx)
4220 })
4221 .await
4222 .unwrap();
4223
4224 let mut ranges_by_path = results
4225 .into_iter()
4226 .map(|(buffer, ranges)| {
4227 buffer.read_with(cx_b, |buffer, cx| {
4228 let path = buffer.file().unwrap().full_path(cx);
4229 let offset_ranges = ranges
4230 .into_iter()
4231 .map(|range| range.to_offset(buffer))
4232 .collect::<Vec<_>>();
4233 (path, offset_ranges)
4234 })
4235 })
4236 .collect::<Vec<_>>();
4237 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4238
4239 assert_eq!(
4240 ranges_by_path,
4241 &[
4242 (PathBuf::from("dir-1/a"), vec![6..11]),
4243 (PathBuf::from("dir-1/c"), vec![2..7]),
4244 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4245 (PathBuf::from("dir-2/e"), vec![7..12]),
4246 ]
4247 );
4248}
4249
4250#[gpui::test(iterations = 10)]
4251async fn test_document_highlights(
4252 deterministic: Arc<Deterministic>,
4253 cx_a: &mut TestAppContext,
4254 cx_b: &mut TestAppContext,
4255) {
4256 deterministic.forbid_parking();
4257 let mut server = TestServer::start(&deterministic).await;
4258 let client_a = server.create_client(cx_a, "user_a").await;
4259 let client_b = server.create_client(cx_b, "user_b").await;
4260 server
4261 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4262 .await;
4263 let active_call_a = cx_a.read(ActiveCall::global);
4264
4265 client_a
4266 .fs
4267 .insert_tree(
4268 "/root-1",
4269 json!({
4270 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4271 }),
4272 )
4273 .await;
4274
4275 // Set up a fake language server.
4276 let mut language = Language::new(
4277 LanguageConfig {
4278 name: "Rust".into(),
4279 path_suffixes: vec!["rs".to_string()],
4280 ..Default::default()
4281 },
4282 Some(tree_sitter_rust::language()),
4283 );
4284 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4285 client_a.language_registry.add(Arc::new(language));
4286
4287 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4288 let project_id = active_call_a
4289 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4290 .await
4291 .unwrap();
4292 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4293
4294 // Open the file on client B.
4295 let buffer_b = cx_b
4296 .background()
4297 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4298 .await
4299 .unwrap();
4300
4301 // Request document highlights as the guest.
4302 let fake_language_server = fake_language_servers.next().await.unwrap();
4303 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4304 |params, _| async move {
4305 assert_eq!(
4306 params
4307 .text_document_position_params
4308 .text_document
4309 .uri
4310 .as_str(),
4311 "file:///root-1/main.rs"
4312 );
4313 assert_eq!(
4314 params.text_document_position_params.position,
4315 lsp::Position::new(0, 34)
4316 );
4317 Ok(Some(vec![
4318 lsp::DocumentHighlight {
4319 kind: Some(lsp::DocumentHighlightKind::WRITE),
4320 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4321 },
4322 lsp::DocumentHighlight {
4323 kind: Some(lsp::DocumentHighlightKind::READ),
4324 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4325 },
4326 lsp::DocumentHighlight {
4327 kind: Some(lsp::DocumentHighlightKind::READ),
4328 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4329 },
4330 ]))
4331 },
4332 );
4333
4334 let highlights = project_b
4335 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4336 .await
4337 .unwrap();
4338 buffer_b.read_with(cx_b, |buffer, _| {
4339 let snapshot = buffer.snapshot();
4340
4341 let highlights = highlights
4342 .into_iter()
4343 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4344 .collect::<Vec<_>>();
4345 assert_eq!(
4346 highlights,
4347 &[
4348 (lsp::DocumentHighlightKind::WRITE, 10..16),
4349 (lsp::DocumentHighlightKind::READ, 32..38),
4350 (lsp::DocumentHighlightKind::READ, 41..47)
4351 ]
4352 )
4353 });
4354}
4355
4356#[gpui::test(iterations = 10)]
4357async fn test_lsp_hover(
4358 deterministic: Arc<Deterministic>,
4359 cx_a: &mut TestAppContext,
4360 cx_b: &mut TestAppContext,
4361) {
4362 deterministic.forbid_parking();
4363 let mut server = TestServer::start(&deterministic).await;
4364 let client_a = server.create_client(cx_a, "user_a").await;
4365 let client_b = server.create_client(cx_b, "user_b").await;
4366 server
4367 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4368 .await;
4369 let active_call_a = cx_a.read(ActiveCall::global);
4370
4371 client_a
4372 .fs
4373 .insert_tree(
4374 "/root-1",
4375 json!({
4376 "main.rs": "use std::collections::HashMap;",
4377 }),
4378 )
4379 .await;
4380
4381 // Set up a fake language server.
4382 let mut language = Language::new(
4383 LanguageConfig {
4384 name: "Rust".into(),
4385 path_suffixes: vec!["rs".to_string()],
4386 ..Default::default()
4387 },
4388 Some(tree_sitter_rust::language()),
4389 );
4390 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4391 client_a.language_registry.add(Arc::new(language));
4392
4393 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4394 let project_id = active_call_a
4395 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4396 .await
4397 .unwrap();
4398 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4399
4400 // Open the file as the guest
4401 let buffer_b = cx_b
4402 .background()
4403 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4404 .await
4405 .unwrap();
4406
4407 // Request hover information as the guest.
4408 let fake_language_server = fake_language_servers.next().await.unwrap();
4409 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4410 |params, _| async move {
4411 assert_eq!(
4412 params
4413 .text_document_position_params
4414 .text_document
4415 .uri
4416 .as_str(),
4417 "file:///root-1/main.rs"
4418 );
4419 assert_eq!(
4420 params.text_document_position_params.position,
4421 lsp::Position::new(0, 22)
4422 );
4423 Ok(Some(lsp::Hover {
4424 contents: lsp::HoverContents::Array(vec![
4425 lsp::MarkedString::String("Test hover content.".to_string()),
4426 lsp::MarkedString::LanguageString(lsp::LanguageString {
4427 language: "Rust".to_string(),
4428 value: "let foo = 42;".to_string(),
4429 }),
4430 ]),
4431 range: Some(lsp::Range::new(
4432 lsp::Position::new(0, 22),
4433 lsp::Position::new(0, 29),
4434 )),
4435 }))
4436 },
4437 );
4438
4439 let hover_info = project_b
4440 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
4441 .await
4442 .unwrap()
4443 .unwrap();
4444 buffer_b.read_with(cx_b, |buffer, _| {
4445 let snapshot = buffer.snapshot();
4446 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
4447 assert_eq!(
4448 hover_info.contents,
4449 vec![
4450 project::HoverBlock {
4451 text: "Test hover content.".to_string(),
4452 language: None,
4453 },
4454 project::HoverBlock {
4455 text: "let foo = 42;".to_string(),
4456 language: Some("Rust".to_string()),
4457 }
4458 ]
4459 );
4460 });
4461}
4462
4463#[gpui::test(iterations = 10)]
4464async fn test_project_symbols(
4465 deterministic: Arc<Deterministic>,
4466 cx_a: &mut TestAppContext,
4467 cx_b: &mut TestAppContext,
4468) {
4469 deterministic.forbid_parking();
4470 let mut server = TestServer::start(&deterministic).await;
4471 let client_a = server.create_client(cx_a, "user_a").await;
4472 let client_b = server.create_client(cx_b, "user_b").await;
4473 server
4474 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4475 .await;
4476 let active_call_a = cx_a.read(ActiveCall::global);
4477
4478 // Set up a fake language server.
4479 let mut language = Language::new(
4480 LanguageConfig {
4481 name: "Rust".into(),
4482 path_suffixes: vec!["rs".to_string()],
4483 ..Default::default()
4484 },
4485 Some(tree_sitter_rust::language()),
4486 );
4487 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4488 client_a.language_registry.add(Arc::new(language));
4489
4490 client_a
4491 .fs
4492 .insert_tree(
4493 "/code",
4494 json!({
4495 "crate-1": {
4496 "one.rs": "const ONE: usize = 1;",
4497 },
4498 "crate-2": {
4499 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
4500 },
4501 "private": {
4502 "passwords.txt": "the-password",
4503 }
4504 }),
4505 )
4506 .await;
4507 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
4508 let project_id = active_call_a
4509 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4510 .await
4511 .unwrap();
4512 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4513
4514 // Cause the language server to start.
4515 let _buffer = cx_b
4516 .background()
4517 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4518 .await
4519 .unwrap();
4520
4521 let fake_language_server = fake_language_servers.next().await.unwrap();
4522 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
4523 #[allow(deprecated)]
4524 Ok(Some(vec![lsp::SymbolInformation {
4525 name: "TWO".into(),
4526 location: lsp::Location {
4527 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
4528 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4529 },
4530 kind: lsp::SymbolKind::CONSTANT,
4531 tags: None,
4532 container_name: None,
4533 deprecated: None,
4534 }]))
4535 });
4536
4537 // Request the definition of a symbol as the guest.
4538 let symbols = project_b
4539 .update(cx_b, |p, cx| p.symbols("two", cx))
4540 .await
4541 .unwrap();
4542 assert_eq!(symbols.len(), 1);
4543 assert_eq!(symbols[0].name, "TWO");
4544
4545 // Open one of the returned symbols.
4546 let buffer_b_2 = project_b
4547 .update(cx_b, |project, cx| {
4548 project.open_buffer_for_symbol(&symbols[0], cx)
4549 })
4550 .await
4551 .unwrap();
4552 buffer_b_2.read_with(cx_b, |buffer, _| {
4553 assert_eq!(
4554 buffer.file().unwrap().path().as_ref(),
4555 Path::new("../crate-2/two.rs")
4556 );
4557 });
4558
4559 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
4560 let mut fake_symbol = symbols[0].clone();
4561 fake_symbol.path.path = Path::new("/code/secrets").into();
4562 let error = project_b
4563 .update(cx_b, |project, cx| {
4564 project.open_buffer_for_symbol(&fake_symbol, cx)
4565 })
4566 .await
4567 .unwrap_err();
4568 assert!(error.to_string().contains("invalid symbol signature"));
4569}
4570
4571#[gpui::test(iterations = 10)]
4572async fn test_open_buffer_while_getting_definition_pointing_to_it(
4573 deterministic: Arc<Deterministic>,
4574 cx_a: &mut TestAppContext,
4575 cx_b: &mut TestAppContext,
4576 mut rng: StdRng,
4577) {
4578 deterministic.forbid_parking();
4579 let mut server = TestServer::start(&deterministic).await;
4580 let client_a = server.create_client(cx_a, "user_a").await;
4581 let client_b = server.create_client(cx_b, "user_b").await;
4582 server
4583 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4584 .await;
4585 let active_call_a = cx_a.read(ActiveCall::global);
4586
4587 // Set up a fake language server.
4588 let mut language = Language::new(
4589 LanguageConfig {
4590 name: "Rust".into(),
4591 path_suffixes: vec!["rs".to_string()],
4592 ..Default::default()
4593 },
4594 Some(tree_sitter_rust::language()),
4595 );
4596 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4597 client_a.language_registry.add(Arc::new(language));
4598
4599 client_a
4600 .fs
4601 .insert_tree(
4602 "/root",
4603 json!({
4604 "a.rs": "const ONE: usize = b::TWO;",
4605 "b.rs": "const TWO: usize = 2",
4606 }),
4607 )
4608 .await;
4609 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
4610 let project_id = active_call_a
4611 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4612 .await
4613 .unwrap();
4614 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4615
4616 let buffer_b1 = cx_b
4617 .background()
4618 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4619 .await
4620 .unwrap();
4621
4622 let fake_language_server = fake_language_servers.next().await.unwrap();
4623 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4624 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4625 lsp::Location::new(
4626 lsp::Url::from_file_path("/root/b.rs").unwrap(),
4627 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4628 ),
4629 )))
4630 });
4631
4632 let definitions;
4633 let buffer_b2;
4634 if rng.gen() {
4635 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
4636 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
4637 } else {
4638 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
4639 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
4640 }
4641
4642 let buffer_b2 = buffer_b2.await.unwrap();
4643 let definitions = definitions.await.unwrap();
4644 assert_eq!(definitions.len(), 1);
4645 assert_eq!(definitions[0].target.buffer, buffer_b2);
4646}
4647
4648#[gpui::test(iterations = 10)]
4649async fn test_collaborating_with_code_actions(
4650 deterministic: Arc<Deterministic>,
4651 cx_a: &mut TestAppContext,
4652 cx_b: &mut TestAppContext,
4653) {
4654 deterministic.forbid_parking();
4655 cx_b.update(editor::init);
4656 let mut server = TestServer::start(&deterministic).await;
4657 let client_a = server.create_client(cx_a, "user_a").await;
4658 let client_b = server.create_client(cx_b, "user_b").await;
4659 server
4660 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4661 .await;
4662 let active_call_a = cx_a.read(ActiveCall::global);
4663
4664 // Set up a fake language server.
4665 let mut language = Language::new(
4666 LanguageConfig {
4667 name: "Rust".into(),
4668 path_suffixes: vec!["rs".to_string()],
4669 ..Default::default()
4670 },
4671 Some(tree_sitter_rust::language()),
4672 );
4673 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4674 client_a.language_registry.add(Arc::new(language));
4675
4676 client_a
4677 .fs
4678 .insert_tree(
4679 "/a",
4680 json!({
4681 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
4682 "other.rs": "pub fn foo() -> usize { 4 }",
4683 }),
4684 )
4685 .await;
4686 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4687 let project_id = active_call_a
4688 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4689 .await
4690 .unwrap();
4691
4692 // Join the project as client B.
4693 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4694 let (_window_b, workspace_b) = cx_b.add_window(|cx| {
4695 Workspace::new(
4696 Default::default(),
4697 0,
4698 project_b.clone(),
4699 |_, _| unimplemented!(),
4700 cx,
4701 )
4702 });
4703 let editor_b = workspace_b
4704 .update(cx_b, |workspace, cx| {
4705 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
4706 })
4707 .await
4708 .unwrap()
4709 .downcast::<Editor>()
4710 .unwrap();
4711
4712 let mut fake_language_server = fake_language_servers.next().await.unwrap();
4713 fake_language_server
4714 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
4715 assert_eq!(
4716 params.text_document.uri,
4717 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4718 );
4719 assert_eq!(params.range.start, lsp::Position::new(0, 0));
4720 assert_eq!(params.range.end, lsp::Position::new(0, 0));
4721 Ok(None)
4722 })
4723 .next()
4724 .await;
4725
4726 // Move cursor to a location that contains code actions.
4727 editor_b.update(cx_b, |editor, cx| {
4728 editor.change_selections(None, cx, |s| {
4729 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
4730 });
4731 cx.focus(&editor_b);
4732 });
4733
4734 fake_language_server
4735 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
4736 assert_eq!(
4737 params.text_document.uri,
4738 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4739 );
4740 assert_eq!(params.range.start, lsp::Position::new(1, 31));
4741 assert_eq!(params.range.end, lsp::Position::new(1, 31));
4742
4743 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4744 lsp::CodeAction {
4745 title: "Inline into all callers".to_string(),
4746 edit: Some(lsp::WorkspaceEdit {
4747 changes: Some(
4748 [
4749 (
4750 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4751 vec![lsp::TextEdit::new(
4752 lsp::Range::new(
4753 lsp::Position::new(1, 22),
4754 lsp::Position::new(1, 34),
4755 ),
4756 "4".to_string(),
4757 )],
4758 ),
4759 (
4760 lsp::Url::from_file_path("/a/other.rs").unwrap(),
4761 vec![lsp::TextEdit::new(
4762 lsp::Range::new(
4763 lsp::Position::new(0, 0),
4764 lsp::Position::new(0, 27),
4765 ),
4766 "".to_string(),
4767 )],
4768 ),
4769 ]
4770 .into_iter()
4771 .collect(),
4772 ),
4773 ..Default::default()
4774 }),
4775 data: Some(json!({
4776 "codeActionParams": {
4777 "range": {
4778 "start": {"line": 1, "column": 31},
4779 "end": {"line": 1, "column": 31},
4780 }
4781 }
4782 })),
4783 ..Default::default()
4784 },
4785 )]))
4786 })
4787 .next()
4788 .await;
4789
4790 // Toggle code actions and wait for them to display.
4791 editor_b.update(cx_b, |editor, cx| {
4792 editor.toggle_code_actions(
4793 &ToggleCodeActions {
4794 deployed_from_indicator: false,
4795 },
4796 cx,
4797 );
4798 });
4799 cx_a.foreground().run_until_parked();
4800 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
4801
4802 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
4803
4804 // Confirming the code action will trigger a resolve request.
4805 let confirm_action = workspace_b
4806 .update(cx_b, |workspace, cx| {
4807 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
4808 })
4809 .unwrap();
4810 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
4811 |_, _| async move {
4812 Ok(lsp::CodeAction {
4813 title: "Inline into all callers".to_string(),
4814 edit: Some(lsp::WorkspaceEdit {
4815 changes: Some(
4816 [
4817 (
4818 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4819 vec![lsp::TextEdit::new(
4820 lsp::Range::new(
4821 lsp::Position::new(1, 22),
4822 lsp::Position::new(1, 34),
4823 ),
4824 "4".to_string(),
4825 )],
4826 ),
4827 (
4828 lsp::Url::from_file_path("/a/other.rs").unwrap(),
4829 vec![lsp::TextEdit::new(
4830 lsp::Range::new(
4831 lsp::Position::new(0, 0),
4832 lsp::Position::new(0, 27),
4833 ),
4834 "".to_string(),
4835 )],
4836 ),
4837 ]
4838 .into_iter()
4839 .collect(),
4840 ),
4841 ..Default::default()
4842 }),
4843 ..Default::default()
4844 })
4845 },
4846 );
4847
4848 // After the action is confirmed, an editor containing both modified files is opened.
4849 confirm_action.await.unwrap();
4850 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
4851 workspace
4852 .active_item(cx)
4853 .unwrap()
4854 .downcast::<Editor>()
4855 .unwrap()
4856 });
4857 code_action_editor.update(cx_b, |editor, cx| {
4858 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
4859 editor.undo(&Undo, cx);
4860 assert_eq!(
4861 editor.text(cx),
4862 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
4863 );
4864 editor.redo(&Redo, cx);
4865 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
4866 });
4867}
4868
4869#[gpui::test(iterations = 10)]
4870async fn test_collaborating_with_renames(
4871 deterministic: Arc<Deterministic>,
4872 cx_a: &mut TestAppContext,
4873 cx_b: &mut TestAppContext,
4874) {
4875 deterministic.forbid_parking();
4876 cx_b.update(editor::init);
4877 let mut server = TestServer::start(&deterministic).await;
4878 let client_a = server.create_client(cx_a, "user_a").await;
4879 let client_b = server.create_client(cx_b, "user_b").await;
4880 server
4881 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4882 .await;
4883 let active_call_a = cx_a.read(ActiveCall::global);
4884
4885 // Set up a fake language server.
4886 let mut language = Language::new(
4887 LanguageConfig {
4888 name: "Rust".into(),
4889 path_suffixes: vec!["rs".to_string()],
4890 ..Default::default()
4891 },
4892 Some(tree_sitter_rust::language()),
4893 );
4894 let mut fake_language_servers = language
4895 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4896 capabilities: lsp::ServerCapabilities {
4897 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
4898 prepare_provider: Some(true),
4899 work_done_progress_options: Default::default(),
4900 })),
4901 ..Default::default()
4902 },
4903 ..Default::default()
4904 }))
4905 .await;
4906 client_a.language_registry.add(Arc::new(language));
4907
4908 client_a
4909 .fs
4910 .insert_tree(
4911 "/dir",
4912 json!({
4913 "one.rs": "const ONE: usize = 1;",
4914 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
4915 }),
4916 )
4917 .await;
4918 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
4919 let project_id = active_call_a
4920 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4921 .await
4922 .unwrap();
4923 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4924
4925 let (_window_b, workspace_b) = cx_b.add_window(|cx| {
4926 Workspace::new(
4927 Default::default(),
4928 0,
4929 project_b.clone(),
4930 |_, _| unimplemented!(),
4931 cx,
4932 )
4933 });
4934 let editor_b = workspace_b
4935 .update(cx_b, |workspace, cx| {
4936 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
4937 })
4938 .await
4939 .unwrap()
4940 .downcast::<Editor>()
4941 .unwrap();
4942 let fake_language_server = fake_language_servers.next().await.unwrap();
4943
4944 // Move cursor to a location that can be renamed.
4945 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
4946 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
4947 editor.rename(&Rename, cx).unwrap()
4948 });
4949
4950 fake_language_server
4951 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
4952 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
4953 assert_eq!(params.position, lsp::Position::new(0, 7));
4954 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4955 lsp::Position::new(0, 6),
4956 lsp::Position::new(0, 9),
4957 ))))
4958 })
4959 .next()
4960 .await
4961 .unwrap();
4962 prepare_rename.await.unwrap();
4963 editor_b.update(cx_b, |editor, cx| {
4964 let rename = editor.pending_rename().unwrap();
4965 let buffer = editor.buffer().read(cx).snapshot(cx);
4966 assert_eq!(
4967 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
4968 6..9
4969 );
4970 rename.editor.update(cx, |rename_editor, cx| {
4971 rename_editor.buffer().update(cx, |rename_buffer, cx| {
4972 rename_buffer.edit([(0..3, "THREE")], None, cx);
4973 });
4974 });
4975 });
4976
4977 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
4978 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
4979 });
4980 fake_language_server
4981 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
4982 assert_eq!(
4983 params.text_document_position.text_document.uri.as_str(),
4984 "file:///dir/one.rs"
4985 );
4986 assert_eq!(
4987 params.text_document_position.position,
4988 lsp::Position::new(0, 6)
4989 );
4990 assert_eq!(params.new_name, "THREE");
4991 Ok(Some(lsp::WorkspaceEdit {
4992 changes: Some(
4993 [
4994 (
4995 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
4996 vec![lsp::TextEdit::new(
4997 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4998 "THREE".to_string(),
4999 )],
5000 ),
5001 (
5002 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
5003 vec![
5004 lsp::TextEdit::new(
5005 lsp::Range::new(
5006 lsp::Position::new(0, 24),
5007 lsp::Position::new(0, 27),
5008 ),
5009 "THREE".to_string(),
5010 ),
5011 lsp::TextEdit::new(
5012 lsp::Range::new(
5013 lsp::Position::new(0, 35),
5014 lsp::Position::new(0, 38),
5015 ),
5016 "THREE".to_string(),
5017 ),
5018 ],
5019 ),
5020 ]
5021 .into_iter()
5022 .collect(),
5023 ),
5024 ..Default::default()
5025 }))
5026 })
5027 .next()
5028 .await
5029 .unwrap();
5030 confirm_rename.await.unwrap();
5031
5032 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5033 workspace
5034 .active_item(cx)
5035 .unwrap()
5036 .downcast::<Editor>()
5037 .unwrap()
5038 });
5039 rename_editor.update(cx_b, |editor, cx| {
5040 assert_eq!(
5041 editor.text(cx),
5042 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5043 );
5044 editor.undo(&Undo, cx);
5045 assert_eq!(
5046 editor.text(cx),
5047 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
5048 );
5049 editor.redo(&Redo, cx);
5050 assert_eq!(
5051 editor.text(cx),
5052 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5053 );
5054 });
5055
5056 // Ensure temporary rename edits cannot be undone/redone.
5057 editor_b.update(cx_b, |editor, cx| {
5058 editor.undo(&Undo, cx);
5059 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5060 editor.undo(&Undo, cx);
5061 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5062 editor.redo(&Redo, cx);
5063 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
5064 })
5065}
5066
5067#[gpui::test(iterations = 10)]
5068async fn test_language_server_statuses(
5069 deterministic: Arc<Deterministic>,
5070 cx_a: &mut TestAppContext,
5071 cx_b: &mut TestAppContext,
5072) {
5073 deterministic.forbid_parking();
5074
5075 cx_b.update(editor::init);
5076 let mut server = TestServer::start(&deterministic).await;
5077 let client_a = server.create_client(cx_a, "user_a").await;
5078 let client_b = server.create_client(cx_b, "user_b").await;
5079 server
5080 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5081 .await;
5082 let active_call_a = cx_a.read(ActiveCall::global);
5083
5084 // Set up a fake language server.
5085 let mut language = Language::new(
5086 LanguageConfig {
5087 name: "Rust".into(),
5088 path_suffixes: vec!["rs".to_string()],
5089 ..Default::default()
5090 },
5091 Some(tree_sitter_rust::language()),
5092 );
5093 let mut fake_language_servers = language
5094 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5095 name: "the-language-server",
5096 ..Default::default()
5097 }))
5098 .await;
5099 client_a.language_registry.add(Arc::new(language));
5100
5101 client_a
5102 .fs
5103 .insert_tree(
5104 "/dir",
5105 json!({
5106 "main.rs": "const ONE: usize = 1;",
5107 }),
5108 )
5109 .await;
5110 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5111
5112 let _buffer_a = project_a
5113 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
5114 .await
5115 .unwrap();
5116
5117 let fake_language_server = fake_language_servers.next().await.unwrap();
5118 fake_language_server.start_progress("the-token").await;
5119 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5120 token: lsp::NumberOrString::String("the-token".to_string()),
5121 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5122 lsp::WorkDoneProgressReport {
5123 message: Some("the-message".to_string()),
5124 ..Default::default()
5125 },
5126 )),
5127 });
5128 deterministic.run_until_parked();
5129 project_a.read_with(cx_a, |project, _| {
5130 let status = project.language_server_statuses().next().unwrap();
5131 assert_eq!(status.name, "the-language-server");
5132 assert_eq!(status.pending_work.len(), 1);
5133 assert_eq!(
5134 status.pending_work["the-token"].message.as_ref().unwrap(),
5135 "the-message"
5136 );
5137 });
5138
5139 let project_id = active_call_a
5140 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5141 .await
5142 .unwrap();
5143 deterministic.run_until_parked();
5144 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5145 project_b.read_with(cx_b, |project, _| {
5146 let status = project.language_server_statuses().next().unwrap();
5147 assert_eq!(status.name, "the-language-server");
5148 });
5149
5150 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5151 token: lsp::NumberOrString::String("the-token".to_string()),
5152 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5153 lsp::WorkDoneProgressReport {
5154 message: Some("the-message-2".to_string()),
5155 ..Default::default()
5156 },
5157 )),
5158 });
5159 deterministic.run_until_parked();
5160 project_a.read_with(cx_a, |project, _| {
5161 let status = project.language_server_statuses().next().unwrap();
5162 assert_eq!(status.name, "the-language-server");
5163 assert_eq!(status.pending_work.len(), 1);
5164 assert_eq!(
5165 status.pending_work["the-token"].message.as_ref().unwrap(),
5166 "the-message-2"
5167 );
5168 });
5169 project_b.read_with(cx_b, |project, _| {
5170 let status = project.language_server_statuses().next().unwrap();
5171 assert_eq!(status.name, "the-language-server");
5172 assert_eq!(status.pending_work.len(), 1);
5173 assert_eq!(
5174 status.pending_work["the-token"].message.as_ref().unwrap(),
5175 "the-message-2"
5176 );
5177 });
5178}
5179
5180#[gpui::test(iterations = 10)]
5181async fn test_contacts(
5182 deterministic: Arc<Deterministic>,
5183 cx_a: &mut TestAppContext,
5184 cx_b: &mut TestAppContext,
5185 cx_c: &mut TestAppContext,
5186 cx_d: &mut TestAppContext,
5187) {
5188 deterministic.forbid_parking();
5189 let mut server = TestServer::start(&deterministic).await;
5190 let client_a = server.create_client(cx_a, "user_a").await;
5191 let client_b = server.create_client(cx_b, "user_b").await;
5192 let client_c = server.create_client(cx_c, "user_c").await;
5193 let client_d = server.create_client(cx_d, "user_d").await;
5194 server
5195 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5196 .await;
5197 let active_call_a = cx_a.read(ActiveCall::global);
5198 let active_call_b = cx_b.read(ActiveCall::global);
5199 let active_call_c = cx_c.read(ActiveCall::global);
5200 let _active_call_d = cx_d.read(ActiveCall::global);
5201
5202 deterministic.run_until_parked();
5203 assert_eq!(
5204 contacts(&client_a, cx_a),
5205 [
5206 ("user_b".to_string(), "online", "free"),
5207 ("user_c".to_string(), "online", "free")
5208 ]
5209 );
5210 assert_eq!(
5211 contacts(&client_b, cx_b),
5212 [
5213 ("user_a".to_string(), "online", "free"),
5214 ("user_c".to_string(), "online", "free")
5215 ]
5216 );
5217 assert_eq!(
5218 contacts(&client_c, cx_c),
5219 [
5220 ("user_a".to_string(), "online", "free"),
5221 ("user_b".to_string(), "online", "free")
5222 ]
5223 );
5224 assert_eq!(contacts(&client_d, cx_d), []);
5225
5226 server.disconnect_client(client_c.peer_id().unwrap());
5227 server.forbid_connections();
5228 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5229 assert_eq!(
5230 contacts(&client_a, cx_a),
5231 [
5232 ("user_b".to_string(), "online", "free"),
5233 ("user_c".to_string(), "offline", "free")
5234 ]
5235 );
5236 assert_eq!(
5237 contacts(&client_b, cx_b),
5238 [
5239 ("user_a".to_string(), "online", "free"),
5240 ("user_c".to_string(), "offline", "free")
5241 ]
5242 );
5243 assert_eq!(contacts(&client_c, cx_c), []);
5244 assert_eq!(contacts(&client_d, cx_d), []);
5245
5246 server.allow_connections();
5247 client_c
5248 .authenticate_and_connect(false, &cx_c.to_async())
5249 .await
5250 .unwrap();
5251
5252 deterministic.run_until_parked();
5253 assert_eq!(
5254 contacts(&client_a, cx_a),
5255 [
5256 ("user_b".to_string(), "online", "free"),
5257 ("user_c".to_string(), "online", "free")
5258 ]
5259 );
5260 assert_eq!(
5261 contacts(&client_b, cx_b),
5262 [
5263 ("user_a".to_string(), "online", "free"),
5264 ("user_c".to_string(), "online", "free")
5265 ]
5266 );
5267 assert_eq!(
5268 contacts(&client_c, cx_c),
5269 [
5270 ("user_a".to_string(), "online", "free"),
5271 ("user_b".to_string(), "online", "free")
5272 ]
5273 );
5274 assert_eq!(contacts(&client_d, cx_d), []);
5275
5276 active_call_a
5277 .update(cx_a, |call, cx| {
5278 call.invite(client_b.user_id().unwrap(), None, cx)
5279 })
5280 .await
5281 .unwrap();
5282 deterministic.run_until_parked();
5283 assert_eq!(
5284 contacts(&client_a, cx_a),
5285 [
5286 ("user_b".to_string(), "online", "busy"),
5287 ("user_c".to_string(), "online", "free")
5288 ]
5289 );
5290 assert_eq!(
5291 contacts(&client_b, cx_b),
5292 [
5293 ("user_a".to_string(), "online", "busy"),
5294 ("user_c".to_string(), "online", "free")
5295 ]
5296 );
5297 assert_eq!(
5298 contacts(&client_c, cx_c),
5299 [
5300 ("user_a".to_string(), "online", "busy"),
5301 ("user_b".to_string(), "online", "busy")
5302 ]
5303 );
5304 assert_eq!(contacts(&client_d, cx_d), []);
5305
5306 // Client B and client D become contacts while client B is being called.
5307 server
5308 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5309 .await;
5310 deterministic.run_until_parked();
5311 assert_eq!(
5312 contacts(&client_a, cx_a),
5313 [
5314 ("user_b".to_string(), "online", "busy"),
5315 ("user_c".to_string(), "online", "free")
5316 ]
5317 );
5318 assert_eq!(
5319 contacts(&client_b, cx_b),
5320 [
5321 ("user_a".to_string(), "online", "busy"),
5322 ("user_c".to_string(), "online", "free"),
5323 ("user_d".to_string(), "online", "free"),
5324 ]
5325 );
5326 assert_eq!(
5327 contacts(&client_c, cx_c),
5328 [
5329 ("user_a".to_string(), "online", "busy"),
5330 ("user_b".to_string(), "online", "busy")
5331 ]
5332 );
5333 assert_eq!(
5334 contacts(&client_d, cx_d),
5335 [("user_b".to_string(), "online", "busy")]
5336 );
5337
5338 active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
5339 deterministic.run_until_parked();
5340 assert_eq!(
5341 contacts(&client_a, cx_a),
5342 [
5343 ("user_b".to_string(), "online", "free"),
5344 ("user_c".to_string(), "online", "free")
5345 ]
5346 );
5347 assert_eq!(
5348 contacts(&client_b, cx_b),
5349 [
5350 ("user_a".to_string(), "online", "free"),
5351 ("user_c".to_string(), "online", "free"),
5352 ("user_d".to_string(), "online", "free")
5353 ]
5354 );
5355 assert_eq!(
5356 contacts(&client_c, cx_c),
5357 [
5358 ("user_a".to_string(), "online", "free"),
5359 ("user_b".to_string(), "online", "free")
5360 ]
5361 );
5362 assert_eq!(
5363 contacts(&client_d, cx_d),
5364 [("user_b".to_string(), "online", "free")]
5365 );
5366
5367 active_call_c
5368 .update(cx_c, |call, cx| {
5369 call.invite(client_a.user_id().unwrap(), None, cx)
5370 })
5371 .await
5372 .unwrap();
5373 deterministic.run_until_parked();
5374 assert_eq!(
5375 contacts(&client_a, cx_a),
5376 [
5377 ("user_b".to_string(), "online", "free"),
5378 ("user_c".to_string(), "online", "busy")
5379 ]
5380 );
5381 assert_eq!(
5382 contacts(&client_b, cx_b),
5383 [
5384 ("user_a".to_string(), "online", "busy"),
5385 ("user_c".to_string(), "online", "busy"),
5386 ("user_d".to_string(), "online", "free")
5387 ]
5388 );
5389 assert_eq!(
5390 contacts(&client_c, cx_c),
5391 [
5392 ("user_a".to_string(), "online", "busy"),
5393 ("user_b".to_string(), "online", "free")
5394 ]
5395 );
5396 assert_eq!(
5397 contacts(&client_d, cx_d),
5398 [("user_b".to_string(), "online", "free")]
5399 );
5400
5401 active_call_a
5402 .update(cx_a, |call, cx| call.accept_incoming(cx))
5403 .await
5404 .unwrap();
5405 deterministic.run_until_parked();
5406 assert_eq!(
5407 contacts(&client_a, cx_a),
5408 [
5409 ("user_b".to_string(), "online", "free"),
5410 ("user_c".to_string(), "online", "busy")
5411 ]
5412 );
5413 assert_eq!(
5414 contacts(&client_b, cx_b),
5415 [
5416 ("user_a".to_string(), "online", "busy"),
5417 ("user_c".to_string(), "online", "busy"),
5418 ("user_d".to_string(), "online", "free")
5419 ]
5420 );
5421 assert_eq!(
5422 contacts(&client_c, cx_c),
5423 [
5424 ("user_a".to_string(), "online", "busy"),
5425 ("user_b".to_string(), "online", "free")
5426 ]
5427 );
5428 assert_eq!(
5429 contacts(&client_d, cx_d),
5430 [("user_b".to_string(), "online", "free")]
5431 );
5432
5433 active_call_a
5434 .update(cx_a, |call, cx| {
5435 call.invite(client_b.user_id().unwrap(), None, cx)
5436 })
5437 .await
5438 .unwrap();
5439 deterministic.run_until_parked();
5440 assert_eq!(
5441 contacts(&client_a, cx_a),
5442 [
5443 ("user_b".to_string(), "online", "busy"),
5444 ("user_c".to_string(), "online", "busy")
5445 ]
5446 );
5447 assert_eq!(
5448 contacts(&client_b, cx_b),
5449 [
5450 ("user_a".to_string(), "online", "busy"),
5451 ("user_c".to_string(), "online", "busy"),
5452 ("user_d".to_string(), "online", "free")
5453 ]
5454 );
5455 assert_eq!(
5456 contacts(&client_c, cx_c),
5457 [
5458 ("user_a".to_string(), "online", "busy"),
5459 ("user_b".to_string(), "online", "busy")
5460 ]
5461 );
5462 assert_eq!(
5463 contacts(&client_d, cx_d),
5464 [("user_b".to_string(), "online", "busy")]
5465 );
5466
5467 active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
5468 deterministic.run_until_parked();
5469 assert_eq!(
5470 contacts(&client_a, cx_a),
5471 [
5472 ("user_b".to_string(), "online", "free"),
5473 ("user_c".to_string(), "online", "free")
5474 ]
5475 );
5476 assert_eq!(
5477 contacts(&client_b, cx_b),
5478 [
5479 ("user_a".to_string(), "online", "free"),
5480 ("user_c".to_string(), "online", "free"),
5481 ("user_d".to_string(), "online", "free")
5482 ]
5483 );
5484 assert_eq!(
5485 contacts(&client_c, cx_c),
5486 [
5487 ("user_a".to_string(), "online", "free"),
5488 ("user_b".to_string(), "online", "free")
5489 ]
5490 );
5491 assert_eq!(
5492 contacts(&client_d, cx_d),
5493 [("user_b".to_string(), "online", "free")]
5494 );
5495
5496 active_call_a
5497 .update(cx_a, |call, cx| {
5498 call.invite(client_b.user_id().unwrap(), None, cx)
5499 })
5500 .await
5501 .unwrap();
5502 deterministic.run_until_parked();
5503 assert_eq!(
5504 contacts(&client_a, cx_a),
5505 [
5506 ("user_b".to_string(), "online", "busy"),
5507 ("user_c".to_string(), "online", "free")
5508 ]
5509 );
5510 assert_eq!(
5511 contacts(&client_b, cx_b),
5512 [
5513 ("user_a".to_string(), "online", "busy"),
5514 ("user_c".to_string(), "online", "free"),
5515 ("user_d".to_string(), "online", "free")
5516 ]
5517 );
5518 assert_eq!(
5519 contacts(&client_c, cx_c),
5520 [
5521 ("user_a".to_string(), "online", "busy"),
5522 ("user_b".to_string(), "online", "busy")
5523 ]
5524 );
5525 assert_eq!(
5526 contacts(&client_d, cx_d),
5527 [("user_b".to_string(), "online", "busy")]
5528 );
5529
5530 server.forbid_connections();
5531 server.disconnect_client(client_a.peer_id().unwrap());
5532 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5533 assert_eq!(contacts(&client_a, cx_a), []);
5534 assert_eq!(
5535 contacts(&client_b, cx_b),
5536 [
5537 ("user_a".to_string(), "offline", "free"),
5538 ("user_c".to_string(), "online", "free"),
5539 ("user_d".to_string(), "online", "free")
5540 ]
5541 );
5542 assert_eq!(
5543 contacts(&client_c, cx_c),
5544 [
5545 ("user_a".to_string(), "offline", "free"),
5546 ("user_b".to_string(), "online", "free")
5547 ]
5548 );
5549 assert_eq!(
5550 contacts(&client_d, cx_d),
5551 [("user_b".to_string(), "online", "free")]
5552 );
5553
5554 // Test removing a contact
5555 client_b
5556 .user_store
5557 .update(cx_b, |store, cx| {
5558 store.remove_contact(client_c.user_id().unwrap(), cx)
5559 })
5560 .await
5561 .unwrap();
5562 deterministic.run_until_parked();
5563 assert_eq!(
5564 contacts(&client_b, cx_b),
5565 [
5566 ("user_a".to_string(), "offline", "free"),
5567 ("user_d".to_string(), "online", "free")
5568 ]
5569 );
5570 assert_eq!(
5571 contacts(&client_c, cx_c),
5572 [("user_a".to_string(), "offline", "free"),]
5573 );
5574
5575 fn contacts(
5576 client: &TestClient,
5577 cx: &TestAppContext,
5578 ) -> Vec<(String, &'static str, &'static str)> {
5579 client.user_store.read_with(cx, |store, _| {
5580 store
5581 .contacts()
5582 .iter()
5583 .map(|contact| {
5584 (
5585 contact.user.github_login.clone(),
5586 if contact.online { "online" } else { "offline" },
5587 if contact.busy { "busy" } else { "free" },
5588 )
5589 })
5590 .collect()
5591 })
5592 }
5593}
5594
5595#[gpui::test(iterations = 10)]
5596async fn test_contact_requests(
5597 deterministic: Arc<Deterministic>,
5598 cx_a: &mut TestAppContext,
5599 cx_a2: &mut TestAppContext,
5600 cx_b: &mut TestAppContext,
5601 cx_b2: &mut TestAppContext,
5602 cx_c: &mut TestAppContext,
5603 cx_c2: &mut TestAppContext,
5604) {
5605 deterministic.forbid_parking();
5606
5607 // Connect to a server as 3 clients.
5608 let mut server = TestServer::start(&deterministic).await;
5609 let client_a = server.create_client(cx_a, "user_a").await;
5610 let client_a2 = server.create_client(cx_a2, "user_a").await;
5611 let client_b = server.create_client(cx_b, "user_b").await;
5612 let client_b2 = server.create_client(cx_b2, "user_b").await;
5613 let client_c = server.create_client(cx_c, "user_c").await;
5614 let client_c2 = server.create_client(cx_c2, "user_c").await;
5615
5616 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5617 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5618 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5619
5620 // User A and User C request that user B become their contact.
5621 client_a
5622 .user_store
5623 .update(cx_a, |store, cx| {
5624 store.request_contact(client_b.user_id().unwrap(), cx)
5625 })
5626 .await
5627 .unwrap();
5628 client_c
5629 .user_store
5630 .update(cx_c, |store, cx| {
5631 store.request_contact(client_b.user_id().unwrap(), cx)
5632 })
5633 .await
5634 .unwrap();
5635 deterministic.run_until_parked();
5636
5637 // All users see the pending request appear in all their clients.
5638 assert_eq!(
5639 client_a.summarize_contacts(cx_a).outgoing_requests,
5640 &["user_b"]
5641 );
5642 assert_eq!(
5643 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5644 &["user_b"]
5645 );
5646 assert_eq!(
5647 client_b.summarize_contacts(cx_b).incoming_requests,
5648 &["user_a", "user_c"]
5649 );
5650 assert_eq!(
5651 client_b2.summarize_contacts(cx_b2).incoming_requests,
5652 &["user_a", "user_c"]
5653 );
5654 assert_eq!(
5655 client_c.summarize_contacts(cx_c).outgoing_requests,
5656 &["user_b"]
5657 );
5658 assert_eq!(
5659 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5660 &["user_b"]
5661 );
5662
5663 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5664 disconnect_and_reconnect(&client_a, cx_a).await;
5665 disconnect_and_reconnect(&client_b, cx_b).await;
5666 disconnect_and_reconnect(&client_c, cx_c).await;
5667 deterministic.run_until_parked();
5668 assert_eq!(
5669 client_a.summarize_contacts(cx_a).outgoing_requests,
5670 &["user_b"]
5671 );
5672 assert_eq!(
5673 client_b.summarize_contacts(cx_b).incoming_requests,
5674 &["user_a", "user_c"]
5675 );
5676 assert_eq!(
5677 client_c.summarize_contacts(cx_c).outgoing_requests,
5678 &["user_b"]
5679 );
5680
5681 // User B accepts the request from user A.
5682 client_b
5683 .user_store
5684 .update(cx_b, |store, cx| {
5685 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5686 })
5687 .await
5688 .unwrap();
5689
5690 deterministic.run_until_parked();
5691
5692 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5693 let contacts_b = client_b.summarize_contacts(cx_b);
5694 assert_eq!(contacts_b.current, &["user_a"]);
5695 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5696 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5697 assert_eq!(contacts_b2.current, &["user_a"]);
5698 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5699
5700 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5701 let contacts_a = client_a.summarize_contacts(cx_a);
5702 assert_eq!(contacts_a.current, &["user_b"]);
5703 assert!(contacts_a.outgoing_requests.is_empty());
5704 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5705 assert_eq!(contacts_a2.current, &["user_b"]);
5706 assert!(contacts_a2.outgoing_requests.is_empty());
5707
5708 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5709 disconnect_and_reconnect(&client_a, cx_a).await;
5710 disconnect_and_reconnect(&client_b, cx_b).await;
5711 disconnect_and_reconnect(&client_c, cx_c).await;
5712 deterministic.run_until_parked();
5713 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5714 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5715 assert_eq!(
5716 client_b.summarize_contacts(cx_b).incoming_requests,
5717 &["user_c"]
5718 );
5719 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5720 assert_eq!(
5721 client_c.summarize_contacts(cx_c).outgoing_requests,
5722 &["user_b"]
5723 );
5724
5725 // User B rejects the request from user C.
5726 client_b
5727 .user_store
5728 .update(cx_b, |store, cx| {
5729 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5730 })
5731 .await
5732 .unwrap();
5733
5734 deterministic.run_until_parked();
5735
5736 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5737 let contacts_b = client_b.summarize_contacts(cx_b);
5738 assert_eq!(contacts_b.current, &["user_a"]);
5739 assert!(contacts_b.incoming_requests.is_empty());
5740 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5741 assert_eq!(contacts_b2.current, &["user_a"]);
5742 assert!(contacts_b2.incoming_requests.is_empty());
5743
5744 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5745 let contacts_c = client_c.summarize_contacts(cx_c);
5746 assert!(contacts_c.current.is_empty());
5747 assert!(contacts_c.outgoing_requests.is_empty());
5748 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5749 assert!(contacts_c2.current.is_empty());
5750 assert!(contacts_c2.outgoing_requests.is_empty());
5751
5752 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5753 disconnect_and_reconnect(&client_a, cx_a).await;
5754 disconnect_and_reconnect(&client_b, cx_b).await;
5755 disconnect_and_reconnect(&client_c, cx_c).await;
5756 deterministic.run_until_parked();
5757 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5758 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5759 assert!(client_b
5760 .summarize_contacts(cx_b)
5761 .incoming_requests
5762 .is_empty());
5763 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5764 assert!(client_c
5765 .summarize_contacts(cx_c)
5766 .outgoing_requests
5767 .is_empty());
5768
5769 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
5770 client.disconnect(&cx.to_async()).unwrap();
5771 client.clear_contacts(cx).await;
5772 client
5773 .authenticate_and_connect(false, &cx.to_async())
5774 .await
5775 .unwrap();
5776 }
5777}
5778
5779#[gpui::test(iterations = 10)]
5780async fn test_following(
5781 deterministic: Arc<Deterministic>,
5782 cx_a: &mut TestAppContext,
5783 cx_b: &mut TestAppContext,
5784) {
5785 deterministic.forbid_parking();
5786 cx_a.update(editor::init);
5787 cx_b.update(editor::init);
5788
5789 let mut server = TestServer::start(&deterministic).await;
5790 let client_a = server.create_client(cx_a, "user_a").await;
5791 let client_b = server.create_client(cx_b, "user_b").await;
5792 server
5793 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5794 .await;
5795 let active_call_a = cx_a.read(ActiveCall::global);
5796 let active_call_b = cx_b.read(ActiveCall::global);
5797
5798 client_a
5799 .fs
5800 .insert_tree(
5801 "/a",
5802 json!({
5803 "1.txt": "one\none\none",
5804 "2.txt": "two\ntwo\ntwo",
5805 "3.txt": "three\nthree\nthree",
5806 }),
5807 )
5808 .await;
5809 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5810 active_call_a
5811 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5812 .await
5813 .unwrap();
5814
5815 let project_id = active_call_a
5816 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5817 .await
5818 .unwrap();
5819 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5820 active_call_b
5821 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5822 .await
5823 .unwrap();
5824
5825 // Client A opens some editors.
5826 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5827 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
5828 let editor_a1 = workspace_a
5829 .update(cx_a, |workspace, cx| {
5830 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5831 })
5832 .await
5833 .unwrap()
5834 .downcast::<Editor>()
5835 .unwrap();
5836 let editor_a2 = workspace_a
5837 .update(cx_a, |workspace, cx| {
5838 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
5839 })
5840 .await
5841 .unwrap()
5842 .downcast::<Editor>()
5843 .unwrap();
5844
5845 // Client B opens an editor.
5846 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5847 let editor_b1 = workspace_b
5848 .update(cx_b, |workspace, cx| {
5849 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5850 })
5851 .await
5852 .unwrap()
5853 .downcast::<Editor>()
5854 .unwrap();
5855
5856 let client_a_id = project_b.read_with(cx_b, |project, _| {
5857 project.collaborators().values().next().unwrap().peer_id
5858 });
5859 let client_b_id = project_a.read_with(cx_a, |project, _| {
5860 project.collaborators().values().next().unwrap().peer_id
5861 });
5862
5863 // When client B starts following client A, all visible view states are replicated to client B.
5864 editor_a1.update(cx_a, |editor, cx| {
5865 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
5866 });
5867 editor_a2.update(cx_a, |editor, cx| {
5868 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
5869 });
5870 workspace_b
5871 .update(cx_b, |workspace, cx| {
5872 workspace
5873 .toggle_follow(&ToggleFollow(client_a_id), cx)
5874 .unwrap()
5875 })
5876 .await
5877 .unwrap();
5878
5879 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
5880 workspace
5881 .active_item(cx)
5882 .unwrap()
5883 .downcast::<Editor>()
5884 .unwrap()
5885 });
5886 assert_eq!(
5887 cx_b.read(|cx| editor_b2.project_path(cx)),
5888 Some((worktree_id, "2.txt").into())
5889 );
5890 assert_eq!(
5891 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
5892 vec![2..3]
5893 );
5894 assert_eq!(
5895 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
5896 vec![0..1]
5897 );
5898
5899 // When client A activates a different editor, client B does so as well.
5900 workspace_a.update(cx_a, |workspace, cx| {
5901 workspace.activate_item(&editor_a1, cx)
5902 });
5903 deterministic.run_until_parked();
5904 workspace_b.read_with(cx_b, |workspace, cx| {
5905 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
5906 });
5907
5908 // When client A opens a multibuffer, client B does so as well.
5909 let multibuffer_a = cx_a.add_model(|cx| {
5910 let buffer_a1 = project_a.update(cx, |project, cx| {
5911 project
5912 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
5913 .unwrap()
5914 });
5915 let buffer_a2 = project_a.update(cx, |project, cx| {
5916 project
5917 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
5918 .unwrap()
5919 });
5920 let mut result = MultiBuffer::new(0);
5921 result.push_excerpts(
5922 buffer_a1,
5923 [ExcerptRange {
5924 context: 0..3,
5925 primary: None,
5926 }],
5927 cx,
5928 );
5929 result.push_excerpts(
5930 buffer_a2,
5931 [ExcerptRange {
5932 context: 4..7,
5933 primary: None,
5934 }],
5935 cx,
5936 );
5937 result
5938 });
5939 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
5940 let editor =
5941 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
5942 workspace.add_item(Box::new(editor.clone()), cx);
5943 editor
5944 });
5945 deterministic.run_until_parked();
5946 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
5947 workspace
5948 .active_item(cx)
5949 .unwrap()
5950 .downcast::<Editor>()
5951 .unwrap()
5952 });
5953 assert_eq!(
5954 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
5955 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
5956 );
5957
5958 // When client A navigates back and forth, client B does so as well.
5959 workspace_a
5960 .update(cx_a, |workspace, cx| {
5961 workspace::Pane::go_back(workspace, None, cx)
5962 })
5963 .await;
5964 deterministic.run_until_parked();
5965 workspace_b.read_with(cx_b, |workspace, cx| {
5966 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
5967 });
5968
5969 workspace_a
5970 .update(cx_a, |workspace, cx| {
5971 workspace::Pane::go_back(workspace, None, cx)
5972 })
5973 .await;
5974 deterministic.run_until_parked();
5975 workspace_b.read_with(cx_b, |workspace, cx| {
5976 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
5977 });
5978
5979 workspace_a
5980 .update(cx_a, |workspace, cx| {
5981 workspace::Pane::go_forward(workspace, None, cx)
5982 })
5983 .await;
5984 deterministic.run_until_parked();
5985 workspace_b.read_with(cx_b, |workspace, cx| {
5986 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
5987 });
5988
5989 // Changes to client A's editor are reflected on client B.
5990 editor_a1.update(cx_a, |editor, cx| {
5991 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
5992 });
5993 deterministic.run_until_parked();
5994 editor_b1.read_with(cx_b, |editor, cx| {
5995 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
5996 });
5997
5998 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
5999 deterministic.run_until_parked();
6000 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
6001
6002 editor_a1.update(cx_a, |editor, cx| {
6003 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
6004 editor.set_scroll_position(vec2f(0., 100.), cx);
6005 });
6006 deterministic.run_until_parked();
6007 editor_b1.read_with(cx_b, |editor, cx| {
6008 assert_eq!(editor.selections.ranges(cx), &[3..3]);
6009 });
6010
6011 // After unfollowing, client B stops receiving updates from client A.
6012 workspace_b.update(cx_b, |workspace, cx| {
6013 workspace.unfollow(&workspace.active_pane().clone(), cx)
6014 });
6015 workspace_a.update(cx_a, |workspace, cx| {
6016 workspace.activate_item(&editor_a2, cx)
6017 });
6018 deterministic.run_until_parked();
6019 assert_eq!(
6020 workspace_b.read_with(cx_b, |workspace, cx| workspace
6021 .active_item(cx)
6022 .unwrap()
6023 .id()),
6024 editor_b1.id()
6025 );
6026
6027 // Client A starts following client B.
6028 workspace_a
6029 .update(cx_a, |workspace, cx| {
6030 workspace
6031 .toggle_follow(&ToggleFollow(client_b_id), cx)
6032 .unwrap()
6033 })
6034 .await
6035 .unwrap();
6036 assert_eq!(
6037 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6038 Some(client_b_id)
6039 );
6040 assert_eq!(
6041 workspace_a.read_with(cx_a, |workspace, cx| workspace
6042 .active_item(cx)
6043 .unwrap()
6044 .id()),
6045 editor_a1.id()
6046 );
6047
6048 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
6049 let display = MacOSDisplay::new();
6050 active_call_b
6051 .update(cx_b, |call, cx| call.set_location(None, cx))
6052 .await
6053 .unwrap();
6054 active_call_b
6055 .update(cx_b, |call, cx| {
6056 call.room().unwrap().update(cx, |room, cx| {
6057 room.set_display_sources(vec![display.clone()]);
6058 room.share_screen(cx)
6059 })
6060 })
6061 .await
6062 .unwrap();
6063 deterministic.run_until_parked();
6064 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
6065 workspace
6066 .active_item(cx)
6067 .unwrap()
6068 .downcast::<SharedScreen>()
6069 .unwrap()
6070 });
6071
6072 // Client B activates Zed again, which causes the previous editor to become focused again.
6073 active_call_b
6074 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6075 .await
6076 .unwrap();
6077 deterministic.run_until_parked();
6078 workspace_a.read_with(cx_a, |workspace, cx| {
6079 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
6080 });
6081
6082 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
6083 workspace_b.update(cx_b, |workspace, cx| {
6084 workspace.activate_item(&multibuffer_editor_b, cx)
6085 });
6086 deterministic.run_until_parked();
6087 workspace_a.read_with(cx_a, |workspace, cx| {
6088 assert_eq!(
6089 workspace.active_item(cx).unwrap().id(),
6090 multibuffer_editor_a.id()
6091 )
6092 });
6093
6094 // Client B activates an external window again, and the previously-opened screen-sharing item
6095 // gets activated.
6096 active_call_b
6097 .update(cx_b, |call, cx| call.set_location(None, cx))
6098 .await
6099 .unwrap();
6100 deterministic.run_until_parked();
6101 assert_eq!(
6102 workspace_a.read_with(cx_a, |workspace, cx| workspace
6103 .active_item(cx)
6104 .unwrap()
6105 .id()),
6106 shared_screen.id()
6107 );
6108
6109 // Following interrupts when client B disconnects.
6110 client_b.disconnect(&cx_b.to_async()).unwrap();
6111 deterministic.advance_clock(RECONNECT_TIMEOUT);
6112 assert_eq!(
6113 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6114 None
6115 );
6116}
6117
6118#[gpui::test]
6119async fn test_following_tab_order(
6120 deterministic: Arc<Deterministic>,
6121 cx_a: &mut TestAppContext,
6122 cx_b: &mut TestAppContext,
6123) {
6124 cx_a.update(editor::init);
6125 cx_b.update(editor::init);
6126
6127 let mut server = TestServer::start(&deterministic).await;
6128 let client_a = server.create_client(cx_a, "user_a").await;
6129 let client_b = server.create_client(cx_b, "user_b").await;
6130 server
6131 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6132 .await;
6133 let active_call_a = cx_a.read(ActiveCall::global);
6134 let active_call_b = cx_b.read(ActiveCall::global);
6135
6136 client_a
6137 .fs
6138 .insert_tree(
6139 "/a",
6140 json!({
6141 "1.txt": "one",
6142 "2.txt": "two",
6143 "3.txt": "three",
6144 }),
6145 )
6146 .await;
6147 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6148 active_call_a
6149 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6150 .await
6151 .unwrap();
6152
6153 let project_id = active_call_a
6154 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6155 .await
6156 .unwrap();
6157 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6158 active_call_b
6159 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6160 .await
6161 .unwrap();
6162
6163 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6164 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6165
6166 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6167 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6168
6169 let client_b_id = project_a.read_with(cx_a, |project, _| {
6170 project.collaborators().values().next().unwrap().peer_id
6171 });
6172
6173 //Open 1, 3 in that order on client A
6174 workspace_a
6175 .update(cx_a, |workspace, cx| {
6176 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6177 })
6178 .await
6179 .unwrap();
6180 workspace_a
6181 .update(cx_a, |workspace, cx| {
6182 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6183 })
6184 .await
6185 .unwrap();
6186
6187 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
6188 pane.update(cx, |pane, cx| {
6189 pane.items()
6190 .map(|item| {
6191 item.project_path(cx)
6192 .unwrap()
6193 .path
6194 .to_str()
6195 .unwrap()
6196 .to_owned()
6197 })
6198 .collect::<Vec<_>>()
6199 })
6200 };
6201
6202 //Verify that the tabs opened in the order we expect
6203 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
6204
6205 //Follow client B as client A
6206 workspace_a
6207 .update(cx_a, |workspace, cx| {
6208 workspace
6209 .toggle_follow(&ToggleFollow(client_b_id), cx)
6210 .unwrap()
6211 })
6212 .await
6213 .unwrap();
6214
6215 //Open just 2 on client B
6216 workspace_b
6217 .update(cx_b, |workspace, cx| {
6218 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6219 })
6220 .await
6221 .unwrap();
6222 deterministic.run_until_parked();
6223
6224 // Verify that newly opened followed file is at the end
6225 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6226
6227 //Open just 1 on client B
6228 workspace_b
6229 .update(cx_b, |workspace, cx| {
6230 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6231 })
6232 .await
6233 .unwrap();
6234 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
6235 deterministic.run_until_parked();
6236
6237 // Verify that following into 1 did not reorder
6238 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6239}
6240
6241#[gpui::test(iterations = 10)]
6242async fn test_peers_following_each_other(
6243 deterministic: Arc<Deterministic>,
6244 cx_a: &mut TestAppContext,
6245 cx_b: &mut TestAppContext,
6246) {
6247 deterministic.forbid_parking();
6248 cx_a.update(editor::init);
6249 cx_b.update(editor::init);
6250
6251 let mut server = TestServer::start(&deterministic).await;
6252 let client_a = server.create_client(cx_a, "user_a").await;
6253 let client_b = server.create_client(cx_b, "user_b").await;
6254 server
6255 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6256 .await;
6257 let active_call_a = cx_a.read(ActiveCall::global);
6258 let active_call_b = cx_b.read(ActiveCall::global);
6259
6260 // Client A shares a project.
6261 client_a
6262 .fs
6263 .insert_tree(
6264 "/a",
6265 json!({
6266 "1.txt": "one",
6267 "2.txt": "two",
6268 "3.txt": "three",
6269 "4.txt": "four",
6270 }),
6271 )
6272 .await;
6273 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6274 active_call_a
6275 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6276 .await
6277 .unwrap();
6278 let project_id = active_call_a
6279 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6280 .await
6281 .unwrap();
6282
6283 // Client B joins the project.
6284 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6285 active_call_b
6286 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6287 .await
6288 .unwrap();
6289
6290 // Client A opens some editors.
6291 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6292 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6293 let _editor_a1 = workspace_a
6294 .update(cx_a, |workspace, cx| {
6295 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6296 })
6297 .await
6298 .unwrap()
6299 .downcast::<Editor>()
6300 .unwrap();
6301
6302 // Client B opens an editor.
6303 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6304 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6305 let _editor_b1 = workspace_b
6306 .update(cx_b, |workspace, cx| {
6307 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6308 })
6309 .await
6310 .unwrap()
6311 .downcast::<Editor>()
6312 .unwrap();
6313
6314 // Clients A and B follow each other in split panes
6315 workspace_a.update(cx_a, |workspace, cx| {
6316 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6317 let pane_a1 = pane_a1.clone();
6318 cx.defer(move |workspace, _| {
6319 assert_ne!(*workspace.active_pane(), pane_a1);
6320 });
6321 });
6322 workspace_a
6323 .update(cx_a, |workspace, cx| {
6324 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
6325 workspace
6326 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
6327 .unwrap()
6328 })
6329 .await
6330 .unwrap();
6331 workspace_b.update(cx_b, |workspace, cx| {
6332 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6333 let pane_b1 = pane_b1.clone();
6334 cx.defer(move |workspace, _| {
6335 assert_ne!(*workspace.active_pane(), pane_b1);
6336 });
6337 });
6338 workspace_b
6339 .update(cx_b, |workspace, cx| {
6340 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
6341 workspace
6342 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
6343 .unwrap()
6344 })
6345 .await
6346 .unwrap();
6347
6348 workspace_a.update(cx_a, |workspace, cx| {
6349 workspace.activate_next_pane(cx);
6350 });
6351 // Wait for focus effects to be fully flushed
6352 workspace_a.update(cx_a, |workspace, _| {
6353 assert_eq!(*workspace.active_pane(), pane_a1);
6354 });
6355
6356 workspace_a
6357 .update(cx_a, |workspace, cx| {
6358 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6359 })
6360 .await
6361 .unwrap();
6362 workspace_b.update(cx_b, |workspace, cx| {
6363 workspace.activate_next_pane(cx);
6364 });
6365
6366 workspace_b
6367 .update(cx_b, |workspace, cx| {
6368 assert_eq!(*workspace.active_pane(), pane_b1);
6369 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
6370 })
6371 .await
6372 .unwrap();
6373 cx_a.foreground().run_until_parked();
6374
6375 // Ensure leader updates don't change the active pane of followers
6376 workspace_a.read_with(cx_a, |workspace, _| {
6377 assert_eq!(*workspace.active_pane(), pane_a1);
6378 });
6379 workspace_b.read_with(cx_b, |workspace, _| {
6380 assert_eq!(*workspace.active_pane(), pane_b1);
6381 });
6382
6383 // Ensure peers following each other doesn't cause an infinite loop.
6384 assert_eq!(
6385 workspace_a.read_with(cx_a, |workspace, cx| workspace
6386 .active_item(cx)
6387 .unwrap()
6388 .project_path(cx)),
6389 Some((worktree_id, "3.txt").into())
6390 );
6391 workspace_a.update(cx_a, |workspace, cx| {
6392 assert_eq!(
6393 workspace.active_item(cx).unwrap().project_path(cx),
6394 Some((worktree_id, "3.txt").into())
6395 );
6396 workspace.activate_next_pane(cx);
6397 });
6398
6399 workspace_a.update(cx_a, |workspace, cx| {
6400 assert_eq!(
6401 workspace.active_item(cx).unwrap().project_path(cx),
6402 Some((worktree_id, "4.txt").into())
6403 );
6404 });
6405
6406 workspace_b.update(cx_b, |workspace, cx| {
6407 assert_eq!(
6408 workspace.active_item(cx).unwrap().project_path(cx),
6409 Some((worktree_id, "4.txt").into())
6410 );
6411 workspace.activate_next_pane(cx);
6412 });
6413
6414 workspace_b.update(cx_b, |workspace, cx| {
6415 assert_eq!(
6416 workspace.active_item(cx).unwrap().project_path(cx),
6417 Some((worktree_id, "3.txt").into())
6418 );
6419 });
6420}
6421
6422#[gpui::test(iterations = 10)]
6423async fn test_auto_unfollowing(
6424 deterministic: Arc<Deterministic>,
6425 cx_a: &mut TestAppContext,
6426 cx_b: &mut TestAppContext,
6427) {
6428 deterministic.forbid_parking();
6429 cx_a.update(editor::init);
6430 cx_b.update(editor::init);
6431
6432 // 2 clients connect to a server.
6433 let mut server = TestServer::start(&deterministic).await;
6434 let client_a = server.create_client(cx_a, "user_a").await;
6435 let client_b = server.create_client(cx_b, "user_b").await;
6436 server
6437 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6438 .await;
6439 let active_call_a = cx_a.read(ActiveCall::global);
6440 let active_call_b = cx_b.read(ActiveCall::global);
6441
6442 // Client A shares a project.
6443 client_a
6444 .fs
6445 .insert_tree(
6446 "/a",
6447 json!({
6448 "1.txt": "one",
6449 "2.txt": "two",
6450 "3.txt": "three",
6451 }),
6452 )
6453 .await;
6454 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6455 active_call_a
6456 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6457 .await
6458 .unwrap();
6459
6460 let project_id = active_call_a
6461 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6462 .await
6463 .unwrap();
6464 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6465 active_call_b
6466 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6467 .await
6468 .unwrap();
6469
6470 // Client A opens some editors.
6471 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6472 let _editor_a1 = workspace_a
6473 .update(cx_a, |workspace, cx| {
6474 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6475 })
6476 .await
6477 .unwrap()
6478 .downcast::<Editor>()
6479 .unwrap();
6480
6481 // Client B starts following client A.
6482 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6483 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6484 let leader_id = project_b.read_with(cx_b, |project, _| {
6485 project.collaborators().values().next().unwrap().peer_id
6486 });
6487 workspace_b
6488 .update(cx_b, |workspace, cx| {
6489 workspace
6490 .toggle_follow(&ToggleFollow(leader_id), cx)
6491 .unwrap()
6492 })
6493 .await
6494 .unwrap();
6495 assert_eq!(
6496 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6497 Some(leader_id)
6498 );
6499 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
6500 workspace
6501 .active_item(cx)
6502 .unwrap()
6503 .downcast::<Editor>()
6504 .unwrap()
6505 });
6506
6507 // When client B moves, it automatically stops following client A.
6508 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
6509 assert_eq!(
6510 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6511 None
6512 );
6513
6514 workspace_b
6515 .update(cx_b, |workspace, cx| {
6516 workspace
6517 .toggle_follow(&ToggleFollow(leader_id), cx)
6518 .unwrap()
6519 })
6520 .await
6521 .unwrap();
6522 assert_eq!(
6523 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6524 Some(leader_id)
6525 );
6526
6527 // When client B edits, it automatically stops following client A.
6528 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
6529 assert_eq!(
6530 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6531 None
6532 );
6533
6534 workspace_b
6535 .update(cx_b, |workspace, cx| {
6536 workspace
6537 .toggle_follow(&ToggleFollow(leader_id), cx)
6538 .unwrap()
6539 })
6540 .await
6541 .unwrap();
6542 assert_eq!(
6543 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6544 Some(leader_id)
6545 );
6546
6547 // When client B scrolls, it automatically stops following client A.
6548 editor_b2.update(cx_b, |editor, cx| {
6549 editor.set_scroll_position(vec2f(0., 3.), cx)
6550 });
6551 assert_eq!(
6552 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6553 None
6554 );
6555
6556 workspace_b
6557 .update(cx_b, |workspace, cx| {
6558 workspace
6559 .toggle_follow(&ToggleFollow(leader_id), cx)
6560 .unwrap()
6561 })
6562 .await
6563 .unwrap();
6564 assert_eq!(
6565 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6566 Some(leader_id)
6567 );
6568
6569 // When client B activates a different pane, it continues following client A in the original pane.
6570 workspace_b.update(cx_b, |workspace, cx| {
6571 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
6572 });
6573 assert_eq!(
6574 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6575 Some(leader_id)
6576 );
6577
6578 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
6579 assert_eq!(
6580 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6581 Some(leader_id)
6582 );
6583
6584 // When client B activates a different item in the original pane, it automatically stops following client A.
6585 workspace_b
6586 .update(cx_b, |workspace, cx| {
6587 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6588 })
6589 .await
6590 .unwrap();
6591 assert_eq!(
6592 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6593 None
6594 );
6595}
6596
6597#[gpui::test(iterations = 10)]
6598async fn test_peers_simultaneously_following_each_other(
6599 deterministic: Arc<Deterministic>,
6600 cx_a: &mut TestAppContext,
6601 cx_b: &mut TestAppContext,
6602) {
6603 deterministic.forbid_parking();
6604 cx_a.update(editor::init);
6605 cx_b.update(editor::init);
6606
6607 let mut server = TestServer::start(&deterministic).await;
6608 let client_a = server.create_client(cx_a, "user_a").await;
6609 let client_b = server.create_client(cx_b, "user_b").await;
6610 server
6611 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6612 .await;
6613 let active_call_a = cx_a.read(ActiveCall::global);
6614
6615 client_a.fs.insert_tree("/a", json!({})).await;
6616 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
6617 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6618 let project_id = active_call_a
6619 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6620 .await
6621 .unwrap();
6622
6623 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6624 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6625
6626 deterministic.run_until_parked();
6627 let client_a_id = project_b.read_with(cx_b, |project, _| {
6628 project.collaborators().values().next().unwrap().peer_id
6629 });
6630 let client_b_id = project_a.read_with(cx_a, |project, _| {
6631 project.collaborators().values().next().unwrap().peer_id
6632 });
6633
6634 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
6635 workspace
6636 .toggle_follow(&ToggleFollow(client_b_id), cx)
6637 .unwrap()
6638 });
6639 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
6640 workspace
6641 .toggle_follow(&ToggleFollow(client_a_id), cx)
6642 .unwrap()
6643 });
6644
6645 futures::try_join!(a_follow_b, b_follow_a).unwrap();
6646 workspace_a.read_with(cx_a, |workspace, _| {
6647 assert_eq!(
6648 workspace.leader_for_pane(workspace.active_pane()),
6649 Some(client_b_id)
6650 );
6651 });
6652 workspace_b.read_with(cx_b, |workspace, _| {
6653 assert_eq!(
6654 workspace.leader_for_pane(workspace.active_pane()),
6655 Some(client_a_id)
6656 );
6657 });
6658}
6659
6660#[derive(Debug, Eq, PartialEq)]
6661struct RoomParticipants {
6662 remote: Vec<String>,
6663 pending: Vec<String>,
6664}
6665
6666fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
6667 room.read_with(cx, |room, _| {
6668 let mut remote = room
6669 .remote_participants()
6670 .iter()
6671 .map(|(_, participant)| participant.user.github_login.clone())
6672 .collect::<Vec<_>>();
6673 let mut pending = room
6674 .pending_participants()
6675 .iter()
6676 .map(|user| user.github_login.clone())
6677 .collect::<Vec<_>>();
6678 remote.sort();
6679 pending.sort();
6680 RoomParticipants { remote, pending }
6681 })
6682}