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