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