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