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