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