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