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