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