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