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
3896 // The edits from the LSP are applied, and a final newline is added.
3897 assert_eq!(
3898 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
3899 "let honey = \"two\"\n"
3900 );
3901
3902 // Ensure buffer can be formatted using an external command. Notice how the
3903 // host's configuration is honored as opposed to using the guest's settings.
3904 cx_a.update(|cx| {
3905 cx.update_global(|settings: &mut Settings, _| {
3906 settings.editor_defaults.formatter = Some(Formatter::External {
3907 command: "awk".to_string(),
3908 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
3909 });
3910 });
3911 });
3912 project_b
3913 .update(cx_b, |project, cx| {
3914 project.format(
3915 HashSet::from_iter([buffer_b.clone()]),
3916 true,
3917 FormatTrigger::Save,
3918 cx,
3919 )
3920 })
3921 .await
3922 .unwrap();
3923 assert_eq!(
3924 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
3925 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
3926 );
3927}
3928
3929#[gpui::test(iterations = 10)]
3930async fn test_definition(
3931 deterministic: Arc<Deterministic>,
3932 cx_a: &mut TestAppContext,
3933 cx_b: &mut TestAppContext,
3934) {
3935 deterministic.forbid_parking();
3936 let mut server = TestServer::start(&deterministic).await;
3937 let client_a = server.create_client(cx_a, "user_a").await;
3938 let client_b = server.create_client(cx_b, "user_b").await;
3939 server
3940 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3941 .await;
3942 let active_call_a = cx_a.read(ActiveCall::global);
3943
3944 // Set up a fake language server.
3945 let mut language = Language::new(
3946 LanguageConfig {
3947 name: "Rust".into(),
3948 path_suffixes: vec!["rs".to_string()],
3949 ..Default::default()
3950 },
3951 Some(tree_sitter_rust::language()),
3952 );
3953 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3954 client_a.language_registry.add(Arc::new(language));
3955
3956 client_a
3957 .fs
3958 .insert_tree(
3959 "/root",
3960 json!({
3961 "dir-1": {
3962 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
3963 },
3964 "dir-2": {
3965 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
3966 "c.rs": "type T2 = usize;",
3967 }
3968 }),
3969 )
3970 .await;
3971 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
3972 let project_id = active_call_a
3973 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3974 .await
3975 .unwrap();
3976 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3977
3978 // Open the file on client B.
3979 let buffer_b = cx_b
3980 .background()
3981 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3982 .await
3983 .unwrap();
3984
3985 // Request the definition of a symbol as the guest.
3986 let fake_language_server = fake_language_servers.next().await.unwrap();
3987 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
3988 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
3989 lsp::Location::new(
3990 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
3991 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3992 ),
3993 )))
3994 });
3995
3996 let definitions_1 = project_b
3997 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
3998 .await
3999 .unwrap();
4000 cx_b.read(|cx| {
4001 assert_eq!(definitions_1.len(), 1);
4002 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4003 let target_buffer = definitions_1[0].target.buffer.read(cx);
4004 assert_eq!(
4005 target_buffer.text(),
4006 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4007 );
4008 assert_eq!(
4009 definitions_1[0].target.range.to_point(target_buffer),
4010 Point::new(0, 6)..Point::new(0, 9)
4011 );
4012 });
4013
4014 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4015 // the previous call to `definition`.
4016 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4017 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4018 lsp::Location::new(
4019 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4020 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4021 ),
4022 )))
4023 });
4024
4025 let definitions_2 = project_b
4026 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4027 .await
4028 .unwrap();
4029 cx_b.read(|cx| {
4030 assert_eq!(definitions_2.len(), 1);
4031 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4032 let target_buffer = definitions_2[0].target.buffer.read(cx);
4033 assert_eq!(
4034 target_buffer.text(),
4035 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4036 );
4037 assert_eq!(
4038 definitions_2[0].target.range.to_point(target_buffer),
4039 Point::new(1, 6)..Point::new(1, 11)
4040 );
4041 });
4042 assert_eq!(
4043 definitions_1[0].target.buffer,
4044 definitions_2[0].target.buffer
4045 );
4046
4047 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4048 |req, _| async move {
4049 assert_eq!(
4050 req.text_document_position_params.position,
4051 lsp::Position::new(0, 7)
4052 );
4053 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4054 lsp::Location::new(
4055 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4056 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4057 ),
4058 )))
4059 },
4060 );
4061
4062 let type_definitions = project_b
4063 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4064 .await
4065 .unwrap();
4066 cx_b.read(|cx| {
4067 assert_eq!(type_definitions.len(), 1);
4068 let target_buffer = type_definitions[0].target.buffer.read(cx);
4069 assert_eq!(target_buffer.text(), "type T2 = usize;");
4070 assert_eq!(
4071 type_definitions[0].target.range.to_point(target_buffer),
4072 Point::new(0, 5)..Point::new(0, 7)
4073 );
4074 });
4075}
4076
4077#[gpui::test(iterations = 10)]
4078async fn test_references(
4079 deterministic: Arc<Deterministic>,
4080 cx_a: &mut TestAppContext,
4081 cx_b: &mut TestAppContext,
4082) {
4083 deterministic.forbid_parking();
4084 let mut server = TestServer::start(&deterministic).await;
4085 let client_a = server.create_client(cx_a, "user_a").await;
4086 let client_b = server.create_client(cx_b, "user_b").await;
4087 server
4088 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4089 .await;
4090 let active_call_a = cx_a.read(ActiveCall::global);
4091
4092 // Set up a fake language server.
4093 let mut language = Language::new(
4094 LanguageConfig {
4095 name: "Rust".into(),
4096 path_suffixes: vec!["rs".to_string()],
4097 ..Default::default()
4098 },
4099 Some(tree_sitter_rust::language()),
4100 );
4101 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4102 client_a.language_registry.add(Arc::new(language));
4103
4104 client_a
4105 .fs
4106 .insert_tree(
4107 "/root",
4108 json!({
4109 "dir-1": {
4110 "one.rs": "const ONE: usize = 1;",
4111 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4112 },
4113 "dir-2": {
4114 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4115 }
4116 }),
4117 )
4118 .await;
4119 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4120 let project_id = active_call_a
4121 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4122 .await
4123 .unwrap();
4124 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4125
4126 // Open the file on client B.
4127 let buffer_b = cx_b
4128 .background()
4129 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4130 .await
4131 .unwrap();
4132
4133 // Request references to a symbol as the guest.
4134 let fake_language_server = fake_language_servers.next().await.unwrap();
4135 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4136 assert_eq!(
4137 params.text_document_position.text_document.uri.as_str(),
4138 "file:///root/dir-1/one.rs"
4139 );
4140 Ok(Some(vec![
4141 lsp::Location {
4142 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4143 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4144 },
4145 lsp::Location {
4146 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4147 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4148 },
4149 lsp::Location {
4150 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4151 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4152 },
4153 ]))
4154 });
4155
4156 let references = project_b
4157 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4158 .await
4159 .unwrap();
4160 cx_b.read(|cx| {
4161 assert_eq!(references.len(), 3);
4162 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4163
4164 let two_buffer = references[0].buffer.read(cx);
4165 let three_buffer = references[2].buffer.read(cx);
4166 assert_eq!(
4167 two_buffer.file().unwrap().path().as_ref(),
4168 Path::new("two.rs")
4169 );
4170 assert_eq!(references[1].buffer, references[0].buffer);
4171 assert_eq!(
4172 three_buffer.file().unwrap().full_path(cx),
4173 Path::new("/root/dir-2/three.rs")
4174 );
4175
4176 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4177 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4178 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4179 });
4180}
4181
4182#[gpui::test(iterations = 10)]
4183async fn test_project_search(
4184 deterministic: Arc<Deterministic>,
4185 cx_a: &mut TestAppContext,
4186 cx_b: &mut TestAppContext,
4187) {
4188 deterministic.forbid_parking();
4189 let mut server = TestServer::start(&deterministic).await;
4190 let client_a = server.create_client(cx_a, "user_a").await;
4191 let client_b = server.create_client(cx_b, "user_b").await;
4192 server
4193 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4194 .await;
4195 let active_call_a = cx_a.read(ActiveCall::global);
4196
4197 client_a
4198 .fs
4199 .insert_tree(
4200 "/root",
4201 json!({
4202 "dir-1": {
4203 "a": "hello world",
4204 "b": "goodnight moon",
4205 "c": "a world of goo",
4206 "d": "world champion of clown world",
4207 },
4208 "dir-2": {
4209 "e": "disney world is fun",
4210 }
4211 }),
4212 )
4213 .await;
4214 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4215 let (worktree_2, _) = project_a
4216 .update(cx_a, |p, cx| {
4217 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4218 })
4219 .await
4220 .unwrap();
4221 worktree_2
4222 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4223 .await;
4224 let project_id = active_call_a
4225 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4226 .await
4227 .unwrap();
4228
4229 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4230
4231 // Perform a search as the guest.
4232 let results = project_b
4233 .update(cx_b, |project, cx| {
4234 project.search(SearchQuery::text("world", false, false), cx)
4235 })
4236 .await
4237 .unwrap();
4238
4239 let mut ranges_by_path = results
4240 .into_iter()
4241 .map(|(buffer, ranges)| {
4242 buffer.read_with(cx_b, |buffer, cx| {
4243 let path = buffer.file().unwrap().full_path(cx);
4244 let offset_ranges = ranges
4245 .into_iter()
4246 .map(|range| range.to_offset(buffer))
4247 .collect::<Vec<_>>();
4248 (path, offset_ranges)
4249 })
4250 })
4251 .collect::<Vec<_>>();
4252 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4253
4254 assert_eq!(
4255 ranges_by_path,
4256 &[
4257 (PathBuf::from("dir-1/a"), vec![6..11]),
4258 (PathBuf::from("dir-1/c"), vec![2..7]),
4259 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4260 (PathBuf::from("dir-2/e"), vec![7..12]),
4261 ]
4262 );
4263}
4264
4265#[gpui::test(iterations = 10)]
4266async fn test_document_highlights(
4267 deterministic: Arc<Deterministic>,
4268 cx_a: &mut TestAppContext,
4269 cx_b: &mut TestAppContext,
4270) {
4271 deterministic.forbid_parking();
4272 let mut server = TestServer::start(&deterministic).await;
4273 let client_a = server.create_client(cx_a, "user_a").await;
4274 let client_b = server.create_client(cx_b, "user_b").await;
4275 server
4276 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4277 .await;
4278 let active_call_a = cx_a.read(ActiveCall::global);
4279
4280 client_a
4281 .fs
4282 .insert_tree(
4283 "/root-1",
4284 json!({
4285 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4286 }),
4287 )
4288 .await;
4289
4290 // Set up a fake language server.
4291 let mut language = Language::new(
4292 LanguageConfig {
4293 name: "Rust".into(),
4294 path_suffixes: vec!["rs".to_string()],
4295 ..Default::default()
4296 },
4297 Some(tree_sitter_rust::language()),
4298 );
4299 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4300 client_a.language_registry.add(Arc::new(language));
4301
4302 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4303 let project_id = active_call_a
4304 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4305 .await
4306 .unwrap();
4307 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4308
4309 // Open the file on client B.
4310 let buffer_b = cx_b
4311 .background()
4312 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4313 .await
4314 .unwrap();
4315
4316 // Request document highlights as the guest.
4317 let fake_language_server = fake_language_servers.next().await.unwrap();
4318 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4319 |params, _| async move {
4320 assert_eq!(
4321 params
4322 .text_document_position_params
4323 .text_document
4324 .uri
4325 .as_str(),
4326 "file:///root-1/main.rs"
4327 );
4328 assert_eq!(
4329 params.text_document_position_params.position,
4330 lsp::Position::new(0, 34)
4331 );
4332 Ok(Some(vec![
4333 lsp::DocumentHighlight {
4334 kind: Some(lsp::DocumentHighlightKind::WRITE),
4335 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4336 },
4337 lsp::DocumentHighlight {
4338 kind: Some(lsp::DocumentHighlightKind::READ),
4339 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4340 },
4341 lsp::DocumentHighlight {
4342 kind: Some(lsp::DocumentHighlightKind::READ),
4343 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4344 },
4345 ]))
4346 },
4347 );
4348
4349 let highlights = project_b
4350 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4351 .await
4352 .unwrap();
4353 buffer_b.read_with(cx_b, |buffer, _| {
4354 let snapshot = buffer.snapshot();
4355
4356 let highlights = highlights
4357 .into_iter()
4358 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4359 .collect::<Vec<_>>();
4360 assert_eq!(
4361 highlights,
4362 &[
4363 (lsp::DocumentHighlightKind::WRITE, 10..16),
4364 (lsp::DocumentHighlightKind::READ, 32..38),
4365 (lsp::DocumentHighlightKind::READ, 41..47)
4366 ]
4367 )
4368 });
4369}
4370
4371#[gpui::test(iterations = 10)]
4372async fn test_lsp_hover(
4373 deterministic: Arc<Deterministic>,
4374 cx_a: &mut TestAppContext,
4375 cx_b: &mut TestAppContext,
4376) {
4377 deterministic.forbid_parking();
4378 let mut server = TestServer::start(&deterministic).await;
4379 let client_a = server.create_client(cx_a, "user_a").await;
4380 let client_b = server.create_client(cx_b, "user_b").await;
4381 server
4382 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4383 .await;
4384 let active_call_a = cx_a.read(ActiveCall::global);
4385
4386 client_a
4387 .fs
4388 .insert_tree(
4389 "/root-1",
4390 json!({
4391 "main.rs": "use std::collections::HashMap;",
4392 }),
4393 )
4394 .await;
4395
4396 // Set up a fake language server.
4397 let mut language = Language::new(
4398 LanguageConfig {
4399 name: "Rust".into(),
4400 path_suffixes: vec!["rs".to_string()],
4401 ..Default::default()
4402 },
4403 Some(tree_sitter_rust::language()),
4404 );
4405 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4406 client_a.language_registry.add(Arc::new(language));
4407
4408 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4409 let project_id = active_call_a
4410 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4411 .await
4412 .unwrap();
4413 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4414
4415 // Open the file as the guest
4416 let buffer_b = cx_b
4417 .background()
4418 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4419 .await
4420 .unwrap();
4421
4422 // Request hover information as the guest.
4423 let fake_language_server = fake_language_servers.next().await.unwrap();
4424 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4425 |params, _| async move {
4426 assert_eq!(
4427 params
4428 .text_document_position_params
4429 .text_document
4430 .uri
4431 .as_str(),
4432 "file:///root-1/main.rs"
4433 );
4434 assert_eq!(
4435 params.text_document_position_params.position,
4436 lsp::Position::new(0, 22)
4437 );
4438 Ok(Some(lsp::Hover {
4439 contents: lsp::HoverContents::Array(vec![
4440 lsp::MarkedString::String("Test hover content.".to_string()),
4441 lsp::MarkedString::LanguageString(lsp::LanguageString {
4442 language: "Rust".to_string(),
4443 value: "let foo = 42;".to_string(),
4444 }),
4445 ]),
4446 range: Some(lsp::Range::new(
4447 lsp::Position::new(0, 22),
4448 lsp::Position::new(0, 29),
4449 )),
4450 }))
4451 },
4452 );
4453
4454 let hover_info = project_b
4455 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
4456 .await
4457 .unwrap()
4458 .unwrap();
4459 buffer_b.read_with(cx_b, |buffer, _| {
4460 let snapshot = buffer.snapshot();
4461 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
4462 assert_eq!(
4463 hover_info.contents,
4464 vec![
4465 project::HoverBlock {
4466 text: "Test hover content.".to_string(),
4467 language: None,
4468 },
4469 project::HoverBlock {
4470 text: "let foo = 42;".to_string(),
4471 language: Some("Rust".to_string()),
4472 }
4473 ]
4474 );
4475 });
4476}
4477
4478#[gpui::test(iterations = 10)]
4479async fn test_project_symbols(
4480 deterministic: Arc<Deterministic>,
4481 cx_a: &mut TestAppContext,
4482 cx_b: &mut TestAppContext,
4483) {
4484 deterministic.forbid_parking();
4485 let mut server = TestServer::start(&deterministic).await;
4486 let client_a = server.create_client(cx_a, "user_a").await;
4487 let client_b = server.create_client(cx_b, "user_b").await;
4488 server
4489 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4490 .await;
4491 let active_call_a = cx_a.read(ActiveCall::global);
4492
4493 // Set up a fake language server.
4494 let mut language = Language::new(
4495 LanguageConfig {
4496 name: "Rust".into(),
4497 path_suffixes: vec!["rs".to_string()],
4498 ..Default::default()
4499 },
4500 Some(tree_sitter_rust::language()),
4501 );
4502 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4503 client_a.language_registry.add(Arc::new(language));
4504
4505 client_a
4506 .fs
4507 .insert_tree(
4508 "/code",
4509 json!({
4510 "crate-1": {
4511 "one.rs": "const ONE: usize = 1;",
4512 },
4513 "crate-2": {
4514 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
4515 },
4516 "private": {
4517 "passwords.txt": "the-password",
4518 }
4519 }),
4520 )
4521 .await;
4522 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
4523 let project_id = active_call_a
4524 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4525 .await
4526 .unwrap();
4527 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4528
4529 // Cause the language server to start.
4530 let _buffer = cx_b
4531 .background()
4532 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4533 .await
4534 .unwrap();
4535
4536 let fake_language_server = fake_language_servers.next().await.unwrap();
4537 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
4538 #[allow(deprecated)]
4539 Ok(Some(vec![lsp::SymbolInformation {
4540 name: "TWO".into(),
4541 location: lsp::Location {
4542 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
4543 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4544 },
4545 kind: lsp::SymbolKind::CONSTANT,
4546 tags: None,
4547 container_name: None,
4548 deprecated: None,
4549 }]))
4550 });
4551
4552 // Request the definition of a symbol as the guest.
4553 let symbols = project_b
4554 .update(cx_b, |p, cx| p.symbols("two", cx))
4555 .await
4556 .unwrap();
4557 assert_eq!(symbols.len(), 1);
4558 assert_eq!(symbols[0].name, "TWO");
4559
4560 // Open one of the returned symbols.
4561 let buffer_b_2 = project_b
4562 .update(cx_b, |project, cx| {
4563 project.open_buffer_for_symbol(&symbols[0], cx)
4564 })
4565 .await
4566 .unwrap();
4567 buffer_b_2.read_with(cx_b, |buffer, _| {
4568 assert_eq!(
4569 buffer.file().unwrap().path().as_ref(),
4570 Path::new("../crate-2/two.rs")
4571 );
4572 });
4573
4574 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
4575 let mut fake_symbol = symbols[0].clone();
4576 fake_symbol.path.path = Path::new("/code/secrets").into();
4577 let error = project_b
4578 .update(cx_b, |project, cx| {
4579 project.open_buffer_for_symbol(&fake_symbol, cx)
4580 })
4581 .await
4582 .unwrap_err();
4583 assert!(error.to_string().contains("invalid symbol signature"));
4584}
4585
4586#[gpui::test(iterations = 10)]
4587async fn test_open_buffer_while_getting_definition_pointing_to_it(
4588 deterministic: Arc<Deterministic>,
4589 cx_a: &mut TestAppContext,
4590 cx_b: &mut TestAppContext,
4591 mut rng: StdRng,
4592) {
4593 deterministic.forbid_parking();
4594 let mut server = TestServer::start(&deterministic).await;
4595 let client_a = server.create_client(cx_a, "user_a").await;
4596 let client_b = server.create_client(cx_b, "user_b").await;
4597 server
4598 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4599 .await;
4600 let active_call_a = cx_a.read(ActiveCall::global);
4601
4602 // Set up a fake language server.
4603 let mut language = Language::new(
4604 LanguageConfig {
4605 name: "Rust".into(),
4606 path_suffixes: vec!["rs".to_string()],
4607 ..Default::default()
4608 },
4609 Some(tree_sitter_rust::language()),
4610 );
4611 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4612 client_a.language_registry.add(Arc::new(language));
4613
4614 client_a
4615 .fs
4616 .insert_tree(
4617 "/root",
4618 json!({
4619 "a.rs": "const ONE: usize = b::TWO;",
4620 "b.rs": "const TWO: usize = 2",
4621 }),
4622 )
4623 .await;
4624 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
4625 let project_id = active_call_a
4626 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4627 .await
4628 .unwrap();
4629 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4630
4631 let buffer_b1 = cx_b
4632 .background()
4633 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4634 .await
4635 .unwrap();
4636
4637 let fake_language_server = fake_language_servers.next().await.unwrap();
4638 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4639 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4640 lsp::Location::new(
4641 lsp::Url::from_file_path("/root/b.rs").unwrap(),
4642 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4643 ),
4644 )))
4645 });
4646
4647 let definitions;
4648 let buffer_b2;
4649 if rng.gen() {
4650 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
4651 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
4652 } else {
4653 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
4654 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
4655 }
4656
4657 let buffer_b2 = buffer_b2.await.unwrap();
4658 let definitions = definitions.await.unwrap();
4659 assert_eq!(definitions.len(), 1);
4660 assert_eq!(definitions[0].target.buffer, buffer_b2);
4661}
4662
4663#[gpui::test(iterations = 10)]
4664async fn test_collaborating_with_code_actions(
4665 deterministic: Arc<Deterministic>,
4666 cx_a: &mut TestAppContext,
4667 cx_b: &mut TestAppContext,
4668) {
4669 deterministic.forbid_parking();
4670 cx_b.update(editor::init);
4671 let mut server = TestServer::start(&deterministic).await;
4672 let client_a = server.create_client(cx_a, "user_a").await;
4673 let client_b = server.create_client(cx_b, "user_b").await;
4674 server
4675 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4676 .await;
4677 let active_call_a = cx_a.read(ActiveCall::global);
4678
4679 // Set up a fake language server.
4680 let mut language = Language::new(
4681 LanguageConfig {
4682 name: "Rust".into(),
4683 path_suffixes: vec!["rs".to_string()],
4684 ..Default::default()
4685 },
4686 Some(tree_sitter_rust::language()),
4687 );
4688 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4689 client_a.language_registry.add(Arc::new(language));
4690
4691 client_a
4692 .fs
4693 .insert_tree(
4694 "/a",
4695 json!({
4696 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
4697 "other.rs": "pub fn foo() -> usize { 4 }",
4698 }),
4699 )
4700 .await;
4701 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4702 let project_id = active_call_a
4703 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4704 .await
4705 .unwrap();
4706
4707 // Join the project as client B.
4708 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4709 let (_window_b, workspace_b) = cx_b.add_window(|cx| {
4710 Workspace::new(
4711 Default::default(),
4712 0,
4713 project_b.clone(),
4714 |_, _| unimplemented!(),
4715 cx,
4716 )
4717 });
4718 let editor_b = workspace_b
4719 .update(cx_b, |workspace, cx| {
4720 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
4721 })
4722 .await
4723 .unwrap()
4724 .downcast::<Editor>()
4725 .unwrap();
4726
4727 let mut fake_language_server = fake_language_servers.next().await.unwrap();
4728 fake_language_server
4729 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
4730 assert_eq!(
4731 params.text_document.uri,
4732 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4733 );
4734 assert_eq!(params.range.start, lsp::Position::new(0, 0));
4735 assert_eq!(params.range.end, lsp::Position::new(0, 0));
4736 Ok(None)
4737 })
4738 .next()
4739 .await;
4740
4741 // Move cursor to a location that contains code actions.
4742 editor_b.update(cx_b, |editor, cx| {
4743 editor.change_selections(None, cx, |s| {
4744 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
4745 });
4746 cx.focus(&editor_b);
4747 });
4748
4749 fake_language_server
4750 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
4751 assert_eq!(
4752 params.text_document.uri,
4753 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4754 );
4755 assert_eq!(params.range.start, lsp::Position::new(1, 31));
4756 assert_eq!(params.range.end, lsp::Position::new(1, 31));
4757
4758 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4759 lsp::CodeAction {
4760 title: "Inline into all callers".to_string(),
4761 edit: Some(lsp::WorkspaceEdit {
4762 changes: Some(
4763 [
4764 (
4765 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4766 vec![lsp::TextEdit::new(
4767 lsp::Range::new(
4768 lsp::Position::new(1, 22),
4769 lsp::Position::new(1, 34),
4770 ),
4771 "4".to_string(),
4772 )],
4773 ),
4774 (
4775 lsp::Url::from_file_path("/a/other.rs").unwrap(),
4776 vec![lsp::TextEdit::new(
4777 lsp::Range::new(
4778 lsp::Position::new(0, 0),
4779 lsp::Position::new(0, 27),
4780 ),
4781 "".to_string(),
4782 )],
4783 ),
4784 ]
4785 .into_iter()
4786 .collect(),
4787 ),
4788 ..Default::default()
4789 }),
4790 data: Some(json!({
4791 "codeActionParams": {
4792 "range": {
4793 "start": {"line": 1, "column": 31},
4794 "end": {"line": 1, "column": 31},
4795 }
4796 }
4797 })),
4798 ..Default::default()
4799 },
4800 )]))
4801 })
4802 .next()
4803 .await;
4804
4805 // Toggle code actions and wait for them to display.
4806 editor_b.update(cx_b, |editor, cx| {
4807 editor.toggle_code_actions(
4808 &ToggleCodeActions {
4809 deployed_from_indicator: false,
4810 },
4811 cx,
4812 );
4813 });
4814 cx_a.foreground().run_until_parked();
4815 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
4816
4817 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
4818
4819 // Confirming the code action will trigger a resolve request.
4820 let confirm_action = workspace_b
4821 .update(cx_b, |workspace, cx| {
4822 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
4823 })
4824 .unwrap();
4825 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
4826 |_, _| async move {
4827 Ok(lsp::CodeAction {
4828 title: "Inline into all callers".to_string(),
4829 edit: Some(lsp::WorkspaceEdit {
4830 changes: Some(
4831 [
4832 (
4833 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4834 vec![lsp::TextEdit::new(
4835 lsp::Range::new(
4836 lsp::Position::new(1, 22),
4837 lsp::Position::new(1, 34),
4838 ),
4839 "4".to_string(),
4840 )],
4841 ),
4842 (
4843 lsp::Url::from_file_path("/a/other.rs").unwrap(),
4844 vec![lsp::TextEdit::new(
4845 lsp::Range::new(
4846 lsp::Position::new(0, 0),
4847 lsp::Position::new(0, 27),
4848 ),
4849 "".to_string(),
4850 )],
4851 ),
4852 ]
4853 .into_iter()
4854 .collect(),
4855 ),
4856 ..Default::default()
4857 }),
4858 ..Default::default()
4859 })
4860 },
4861 );
4862
4863 // After the action is confirmed, an editor containing both modified files is opened.
4864 confirm_action.await.unwrap();
4865 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
4866 workspace
4867 .active_item(cx)
4868 .unwrap()
4869 .downcast::<Editor>()
4870 .unwrap()
4871 });
4872 code_action_editor.update(cx_b, |editor, cx| {
4873 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
4874 editor.undo(&Undo, cx);
4875 assert_eq!(
4876 editor.text(cx),
4877 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
4878 );
4879 editor.redo(&Redo, cx);
4880 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
4881 });
4882}
4883
4884#[gpui::test(iterations = 10)]
4885async fn test_collaborating_with_renames(
4886 deterministic: Arc<Deterministic>,
4887 cx_a: &mut TestAppContext,
4888 cx_b: &mut TestAppContext,
4889) {
4890 deterministic.forbid_parking();
4891 cx_b.update(editor::init);
4892 let mut server = TestServer::start(&deterministic).await;
4893 let client_a = server.create_client(cx_a, "user_a").await;
4894 let client_b = server.create_client(cx_b, "user_b").await;
4895 server
4896 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4897 .await;
4898 let active_call_a = cx_a.read(ActiveCall::global);
4899
4900 // Set up a fake language server.
4901 let mut language = Language::new(
4902 LanguageConfig {
4903 name: "Rust".into(),
4904 path_suffixes: vec!["rs".to_string()],
4905 ..Default::default()
4906 },
4907 Some(tree_sitter_rust::language()),
4908 );
4909 let mut fake_language_servers = language
4910 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4911 capabilities: lsp::ServerCapabilities {
4912 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
4913 prepare_provider: Some(true),
4914 work_done_progress_options: Default::default(),
4915 })),
4916 ..Default::default()
4917 },
4918 ..Default::default()
4919 }))
4920 .await;
4921 client_a.language_registry.add(Arc::new(language));
4922
4923 client_a
4924 .fs
4925 .insert_tree(
4926 "/dir",
4927 json!({
4928 "one.rs": "const ONE: usize = 1;",
4929 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
4930 }),
4931 )
4932 .await;
4933 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
4934 let project_id = active_call_a
4935 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4936 .await
4937 .unwrap();
4938 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4939
4940 let (_window_b, workspace_b) = cx_b.add_window(|cx| {
4941 Workspace::new(
4942 Default::default(),
4943 0,
4944 project_b.clone(),
4945 |_, _| unimplemented!(),
4946 cx,
4947 )
4948 });
4949 let editor_b = workspace_b
4950 .update(cx_b, |workspace, cx| {
4951 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
4952 })
4953 .await
4954 .unwrap()
4955 .downcast::<Editor>()
4956 .unwrap();
4957 let fake_language_server = fake_language_servers.next().await.unwrap();
4958
4959 // Move cursor to a location that can be renamed.
4960 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
4961 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
4962 editor.rename(&Rename, cx).unwrap()
4963 });
4964
4965 fake_language_server
4966 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
4967 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
4968 assert_eq!(params.position, lsp::Position::new(0, 7));
4969 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4970 lsp::Position::new(0, 6),
4971 lsp::Position::new(0, 9),
4972 ))))
4973 })
4974 .next()
4975 .await
4976 .unwrap();
4977 prepare_rename.await.unwrap();
4978 editor_b.update(cx_b, |editor, cx| {
4979 let rename = editor.pending_rename().unwrap();
4980 let buffer = editor.buffer().read(cx).snapshot(cx);
4981 assert_eq!(
4982 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
4983 6..9
4984 );
4985 rename.editor.update(cx, |rename_editor, cx| {
4986 rename_editor.buffer().update(cx, |rename_buffer, cx| {
4987 rename_buffer.edit([(0..3, "THREE")], None, cx);
4988 });
4989 });
4990 });
4991
4992 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
4993 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
4994 });
4995 fake_language_server
4996 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
4997 assert_eq!(
4998 params.text_document_position.text_document.uri.as_str(),
4999 "file:///dir/one.rs"
5000 );
5001 assert_eq!(
5002 params.text_document_position.position,
5003 lsp::Position::new(0, 6)
5004 );
5005 assert_eq!(params.new_name, "THREE");
5006 Ok(Some(lsp::WorkspaceEdit {
5007 changes: Some(
5008 [
5009 (
5010 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
5011 vec![lsp::TextEdit::new(
5012 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5013 "THREE".to_string(),
5014 )],
5015 ),
5016 (
5017 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
5018 vec![
5019 lsp::TextEdit::new(
5020 lsp::Range::new(
5021 lsp::Position::new(0, 24),
5022 lsp::Position::new(0, 27),
5023 ),
5024 "THREE".to_string(),
5025 ),
5026 lsp::TextEdit::new(
5027 lsp::Range::new(
5028 lsp::Position::new(0, 35),
5029 lsp::Position::new(0, 38),
5030 ),
5031 "THREE".to_string(),
5032 ),
5033 ],
5034 ),
5035 ]
5036 .into_iter()
5037 .collect(),
5038 ),
5039 ..Default::default()
5040 }))
5041 })
5042 .next()
5043 .await
5044 .unwrap();
5045 confirm_rename.await.unwrap();
5046
5047 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5048 workspace
5049 .active_item(cx)
5050 .unwrap()
5051 .downcast::<Editor>()
5052 .unwrap()
5053 });
5054 rename_editor.update(cx_b, |editor, cx| {
5055 assert_eq!(
5056 editor.text(cx),
5057 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5058 );
5059 editor.undo(&Undo, cx);
5060 assert_eq!(
5061 editor.text(cx),
5062 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
5063 );
5064 editor.redo(&Redo, cx);
5065 assert_eq!(
5066 editor.text(cx),
5067 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5068 );
5069 });
5070
5071 // Ensure temporary rename edits cannot be undone/redone.
5072 editor_b.update(cx_b, |editor, cx| {
5073 editor.undo(&Undo, cx);
5074 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5075 editor.undo(&Undo, cx);
5076 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5077 editor.redo(&Redo, cx);
5078 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
5079 })
5080}
5081
5082#[gpui::test(iterations = 10)]
5083async fn test_language_server_statuses(
5084 deterministic: Arc<Deterministic>,
5085 cx_a: &mut TestAppContext,
5086 cx_b: &mut TestAppContext,
5087) {
5088 deterministic.forbid_parking();
5089
5090 cx_b.update(editor::init);
5091 let mut server = TestServer::start(&deterministic).await;
5092 let client_a = server.create_client(cx_a, "user_a").await;
5093 let client_b = server.create_client(cx_b, "user_b").await;
5094 server
5095 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5096 .await;
5097 let active_call_a = cx_a.read(ActiveCall::global);
5098
5099 // Set up a fake language server.
5100 let mut language = Language::new(
5101 LanguageConfig {
5102 name: "Rust".into(),
5103 path_suffixes: vec!["rs".to_string()],
5104 ..Default::default()
5105 },
5106 Some(tree_sitter_rust::language()),
5107 );
5108 let mut fake_language_servers = language
5109 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5110 name: "the-language-server",
5111 ..Default::default()
5112 }))
5113 .await;
5114 client_a.language_registry.add(Arc::new(language));
5115
5116 client_a
5117 .fs
5118 .insert_tree(
5119 "/dir",
5120 json!({
5121 "main.rs": "const ONE: usize = 1;",
5122 }),
5123 )
5124 .await;
5125 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5126
5127 let _buffer_a = project_a
5128 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
5129 .await
5130 .unwrap();
5131
5132 let fake_language_server = fake_language_servers.next().await.unwrap();
5133 fake_language_server.start_progress("the-token").await;
5134 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5135 token: lsp::NumberOrString::String("the-token".to_string()),
5136 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5137 lsp::WorkDoneProgressReport {
5138 message: Some("the-message".to_string()),
5139 ..Default::default()
5140 },
5141 )),
5142 });
5143 deterministic.run_until_parked();
5144 project_a.read_with(cx_a, |project, _| {
5145 let status = project.language_server_statuses().next().unwrap();
5146 assert_eq!(status.name, "the-language-server");
5147 assert_eq!(status.pending_work.len(), 1);
5148 assert_eq!(
5149 status.pending_work["the-token"].message.as_ref().unwrap(),
5150 "the-message"
5151 );
5152 });
5153
5154 let project_id = active_call_a
5155 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5156 .await
5157 .unwrap();
5158 deterministic.run_until_parked();
5159 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5160 project_b.read_with(cx_b, |project, _| {
5161 let status = project.language_server_statuses().next().unwrap();
5162 assert_eq!(status.name, "the-language-server");
5163 });
5164
5165 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5166 token: lsp::NumberOrString::String("the-token".to_string()),
5167 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5168 lsp::WorkDoneProgressReport {
5169 message: Some("the-message-2".to_string()),
5170 ..Default::default()
5171 },
5172 )),
5173 });
5174 deterministic.run_until_parked();
5175 project_a.read_with(cx_a, |project, _| {
5176 let status = project.language_server_statuses().next().unwrap();
5177 assert_eq!(status.name, "the-language-server");
5178 assert_eq!(status.pending_work.len(), 1);
5179 assert_eq!(
5180 status.pending_work["the-token"].message.as_ref().unwrap(),
5181 "the-message-2"
5182 );
5183 });
5184 project_b.read_with(cx_b, |project, _| {
5185 let status = project.language_server_statuses().next().unwrap();
5186 assert_eq!(status.name, "the-language-server");
5187 assert_eq!(status.pending_work.len(), 1);
5188 assert_eq!(
5189 status.pending_work["the-token"].message.as_ref().unwrap(),
5190 "the-message-2"
5191 );
5192 });
5193}
5194
5195#[gpui::test(iterations = 10)]
5196async fn test_contacts(
5197 deterministic: Arc<Deterministic>,
5198 cx_a: &mut TestAppContext,
5199 cx_b: &mut TestAppContext,
5200 cx_c: &mut TestAppContext,
5201 cx_d: &mut TestAppContext,
5202) {
5203 deterministic.forbid_parking();
5204 let mut server = TestServer::start(&deterministic).await;
5205 let client_a = server.create_client(cx_a, "user_a").await;
5206 let client_b = server.create_client(cx_b, "user_b").await;
5207 let client_c = server.create_client(cx_c, "user_c").await;
5208 let client_d = server.create_client(cx_d, "user_d").await;
5209 server
5210 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5211 .await;
5212 let active_call_a = cx_a.read(ActiveCall::global);
5213 let active_call_b = cx_b.read(ActiveCall::global);
5214 let active_call_c = cx_c.read(ActiveCall::global);
5215 let _active_call_d = cx_d.read(ActiveCall::global);
5216
5217 deterministic.run_until_parked();
5218 assert_eq!(
5219 contacts(&client_a, cx_a),
5220 [
5221 ("user_b".to_string(), "online", "free"),
5222 ("user_c".to_string(), "online", "free")
5223 ]
5224 );
5225 assert_eq!(
5226 contacts(&client_b, cx_b),
5227 [
5228 ("user_a".to_string(), "online", "free"),
5229 ("user_c".to_string(), "online", "free")
5230 ]
5231 );
5232 assert_eq!(
5233 contacts(&client_c, cx_c),
5234 [
5235 ("user_a".to_string(), "online", "free"),
5236 ("user_b".to_string(), "online", "free")
5237 ]
5238 );
5239 assert_eq!(contacts(&client_d, cx_d), []);
5240
5241 server.disconnect_client(client_c.peer_id().unwrap());
5242 server.forbid_connections();
5243 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5244 assert_eq!(
5245 contacts(&client_a, cx_a),
5246 [
5247 ("user_b".to_string(), "online", "free"),
5248 ("user_c".to_string(), "offline", "free")
5249 ]
5250 );
5251 assert_eq!(
5252 contacts(&client_b, cx_b),
5253 [
5254 ("user_a".to_string(), "online", "free"),
5255 ("user_c".to_string(), "offline", "free")
5256 ]
5257 );
5258 assert_eq!(contacts(&client_c, cx_c), []);
5259 assert_eq!(contacts(&client_d, cx_d), []);
5260
5261 server.allow_connections();
5262 client_c
5263 .authenticate_and_connect(false, &cx_c.to_async())
5264 .await
5265 .unwrap();
5266
5267 deterministic.run_until_parked();
5268 assert_eq!(
5269 contacts(&client_a, cx_a),
5270 [
5271 ("user_b".to_string(), "online", "free"),
5272 ("user_c".to_string(), "online", "free")
5273 ]
5274 );
5275 assert_eq!(
5276 contacts(&client_b, cx_b),
5277 [
5278 ("user_a".to_string(), "online", "free"),
5279 ("user_c".to_string(), "online", "free")
5280 ]
5281 );
5282 assert_eq!(
5283 contacts(&client_c, cx_c),
5284 [
5285 ("user_a".to_string(), "online", "free"),
5286 ("user_b".to_string(), "online", "free")
5287 ]
5288 );
5289 assert_eq!(contacts(&client_d, cx_d), []);
5290
5291 active_call_a
5292 .update(cx_a, |call, cx| {
5293 call.invite(client_b.user_id().unwrap(), None, cx)
5294 })
5295 .await
5296 .unwrap();
5297 deterministic.run_until_parked();
5298 assert_eq!(
5299 contacts(&client_a, cx_a),
5300 [
5301 ("user_b".to_string(), "online", "busy"),
5302 ("user_c".to_string(), "online", "free")
5303 ]
5304 );
5305 assert_eq!(
5306 contacts(&client_b, cx_b),
5307 [
5308 ("user_a".to_string(), "online", "busy"),
5309 ("user_c".to_string(), "online", "free")
5310 ]
5311 );
5312 assert_eq!(
5313 contacts(&client_c, cx_c),
5314 [
5315 ("user_a".to_string(), "online", "busy"),
5316 ("user_b".to_string(), "online", "busy")
5317 ]
5318 );
5319 assert_eq!(contacts(&client_d, cx_d), []);
5320
5321 // Client B and client D become contacts while client B is being called.
5322 server
5323 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5324 .await;
5325 deterministic.run_until_parked();
5326 assert_eq!(
5327 contacts(&client_a, cx_a),
5328 [
5329 ("user_b".to_string(), "online", "busy"),
5330 ("user_c".to_string(), "online", "free")
5331 ]
5332 );
5333 assert_eq!(
5334 contacts(&client_b, cx_b),
5335 [
5336 ("user_a".to_string(), "online", "busy"),
5337 ("user_c".to_string(), "online", "free"),
5338 ("user_d".to_string(), "online", "free"),
5339 ]
5340 );
5341 assert_eq!(
5342 contacts(&client_c, cx_c),
5343 [
5344 ("user_a".to_string(), "online", "busy"),
5345 ("user_b".to_string(), "online", "busy")
5346 ]
5347 );
5348 assert_eq!(
5349 contacts(&client_d, cx_d),
5350 [("user_b".to_string(), "online", "busy")]
5351 );
5352
5353 active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
5354 deterministic.run_until_parked();
5355 assert_eq!(
5356 contacts(&client_a, cx_a),
5357 [
5358 ("user_b".to_string(), "online", "free"),
5359 ("user_c".to_string(), "online", "free")
5360 ]
5361 );
5362 assert_eq!(
5363 contacts(&client_b, cx_b),
5364 [
5365 ("user_a".to_string(), "online", "free"),
5366 ("user_c".to_string(), "online", "free"),
5367 ("user_d".to_string(), "online", "free")
5368 ]
5369 );
5370 assert_eq!(
5371 contacts(&client_c, cx_c),
5372 [
5373 ("user_a".to_string(), "online", "free"),
5374 ("user_b".to_string(), "online", "free")
5375 ]
5376 );
5377 assert_eq!(
5378 contacts(&client_d, cx_d),
5379 [("user_b".to_string(), "online", "free")]
5380 );
5381
5382 active_call_c
5383 .update(cx_c, |call, cx| {
5384 call.invite(client_a.user_id().unwrap(), None, cx)
5385 })
5386 .await
5387 .unwrap();
5388 deterministic.run_until_parked();
5389 assert_eq!(
5390 contacts(&client_a, cx_a),
5391 [
5392 ("user_b".to_string(), "online", "free"),
5393 ("user_c".to_string(), "online", "busy")
5394 ]
5395 );
5396 assert_eq!(
5397 contacts(&client_b, cx_b),
5398 [
5399 ("user_a".to_string(), "online", "busy"),
5400 ("user_c".to_string(), "online", "busy"),
5401 ("user_d".to_string(), "online", "free")
5402 ]
5403 );
5404 assert_eq!(
5405 contacts(&client_c, cx_c),
5406 [
5407 ("user_a".to_string(), "online", "busy"),
5408 ("user_b".to_string(), "online", "free")
5409 ]
5410 );
5411 assert_eq!(
5412 contacts(&client_d, cx_d),
5413 [("user_b".to_string(), "online", "free")]
5414 );
5415
5416 active_call_a
5417 .update(cx_a, |call, cx| call.accept_incoming(cx))
5418 .await
5419 .unwrap();
5420 deterministic.run_until_parked();
5421 assert_eq!(
5422 contacts(&client_a, cx_a),
5423 [
5424 ("user_b".to_string(), "online", "free"),
5425 ("user_c".to_string(), "online", "busy")
5426 ]
5427 );
5428 assert_eq!(
5429 contacts(&client_b, cx_b),
5430 [
5431 ("user_a".to_string(), "online", "busy"),
5432 ("user_c".to_string(), "online", "busy"),
5433 ("user_d".to_string(), "online", "free")
5434 ]
5435 );
5436 assert_eq!(
5437 contacts(&client_c, cx_c),
5438 [
5439 ("user_a".to_string(), "online", "busy"),
5440 ("user_b".to_string(), "online", "free")
5441 ]
5442 );
5443 assert_eq!(
5444 contacts(&client_d, cx_d),
5445 [("user_b".to_string(), "online", "free")]
5446 );
5447
5448 active_call_a
5449 .update(cx_a, |call, cx| {
5450 call.invite(client_b.user_id().unwrap(), None, cx)
5451 })
5452 .await
5453 .unwrap();
5454 deterministic.run_until_parked();
5455 assert_eq!(
5456 contacts(&client_a, cx_a),
5457 [
5458 ("user_b".to_string(), "online", "busy"),
5459 ("user_c".to_string(), "online", "busy")
5460 ]
5461 );
5462 assert_eq!(
5463 contacts(&client_b, cx_b),
5464 [
5465 ("user_a".to_string(), "online", "busy"),
5466 ("user_c".to_string(), "online", "busy"),
5467 ("user_d".to_string(), "online", "free")
5468 ]
5469 );
5470 assert_eq!(
5471 contacts(&client_c, cx_c),
5472 [
5473 ("user_a".to_string(), "online", "busy"),
5474 ("user_b".to_string(), "online", "busy")
5475 ]
5476 );
5477 assert_eq!(
5478 contacts(&client_d, cx_d),
5479 [("user_b".to_string(), "online", "busy")]
5480 );
5481
5482 active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
5483 deterministic.run_until_parked();
5484 assert_eq!(
5485 contacts(&client_a, cx_a),
5486 [
5487 ("user_b".to_string(), "online", "free"),
5488 ("user_c".to_string(), "online", "free")
5489 ]
5490 );
5491 assert_eq!(
5492 contacts(&client_b, cx_b),
5493 [
5494 ("user_a".to_string(), "online", "free"),
5495 ("user_c".to_string(), "online", "free"),
5496 ("user_d".to_string(), "online", "free")
5497 ]
5498 );
5499 assert_eq!(
5500 contacts(&client_c, cx_c),
5501 [
5502 ("user_a".to_string(), "online", "free"),
5503 ("user_b".to_string(), "online", "free")
5504 ]
5505 );
5506 assert_eq!(
5507 contacts(&client_d, cx_d),
5508 [("user_b".to_string(), "online", "free")]
5509 );
5510
5511 active_call_a
5512 .update(cx_a, |call, cx| {
5513 call.invite(client_b.user_id().unwrap(), None, cx)
5514 })
5515 .await
5516 .unwrap();
5517 deterministic.run_until_parked();
5518 assert_eq!(
5519 contacts(&client_a, cx_a),
5520 [
5521 ("user_b".to_string(), "online", "busy"),
5522 ("user_c".to_string(), "online", "free")
5523 ]
5524 );
5525 assert_eq!(
5526 contacts(&client_b, cx_b),
5527 [
5528 ("user_a".to_string(), "online", "busy"),
5529 ("user_c".to_string(), "online", "free"),
5530 ("user_d".to_string(), "online", "free")
5531 ]
5532 );
5533 assert_eq!(
5534 contacts(&client_c, cx_c),
5535 [
5536 ("user_a".to_string(), "online", "busy"),
5537 ("user_b".to_string(), "online", "busy")
5538 ]
5539 );
5540 assert_eq!(
5541 contacts(&client_d, cx_d),
5542 [("user_b".to_string(), "online", "busy")]
5543 );
5544
5545 server.forbid_connections();
5546 server.disconnect_client(client_a.peer_id().unwrap());
5547 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5548 assert_eq!(contacts(&client_a, cx_a), []);
5549 assert_eq!(
5550 contacts(&client_b, cx_b),
5551 [
5552 ("user_a".to_string(), "offline", "free"),
5553 ("user_c".to_string(), "online", "free"),
5554 ("user_d".to_string(), "online", "free")
5555 ]
5556 );
5557 assert_eq!(
5558 contacts(&client_c, cx_c),
5559 [
5560 ("user_a".to_string(), "offline", "free"),
5561 ("user_b".to_string(), "online", "free")
5562 ]
5563 );
5564 assert_eq!(
5565 contacts(&client_d, cx_d),
5566 [("user_b".to_string(), "online", "free")]
5567 );
5568
5569 // Test removing a contact
5570 client_b
5571 .user_store
5572 .update(cx_b, |store, cx| {
5573 store.remove_contact(client_c.user_id().unwrap(), cx)
5574 })
5575 .await
5576 .unwrap();
5577 deterministic.run_until_parked();
5578 assert_eq!(
5579 contacts(&client_b, cx_b),
5580 [
5581 ("user_a".to_string(), "offline", "free"),
5582 ("user_d".to_string(), "online", "free")
5583 ]
5584 );
5585 assert_eq!(
5586 contacts(&client_c, cx_c),
5587 [("user_a".to_string(), "offline", "free"),]
5588 );
5589
5590 fn contacts(
5591 client: &TestClient,
5592 cx: &TestAppContext,
5593 ) -> Vec<(String, &'static str, &'static str)> {
5594 client.user_store.read_with(cx, |store, _| {
5595 store
5596 .contacts()
5597 .iter()
5598 .map(|contact| {
5599 (
5600 contact.user.github_login.clone(),
5601 if contact.online { "online" } else { "offline" },
5602 if contact.busy { "busy" } else { "free" },
5603 )
5604 })
5605 .collect()
5606 })
5607 }
5608}
5609
5610#[gpui::test(iterations = 10)]
5611async fn test_contact_requests(
5612 deterministic: Arc<Deterministic>,
5613 cx_a: &mut TestAppContext,
5614 cx_a2: &mut TestAppContext,
5615 cx_b: &mut TestAppContext,
5616 cx_b2: &mut TestAppContext,
5617 cx_c: &mut TestAppContext,
5618 cx_c2: &mut TestAppContext,
5619) {
5620 deterministic.forbid_parking();
5621
5622 // Connect to a server as 3 clients.
5623 let mut server = TestServer::start(&deterministic).await;
5624 let client_a = server.create_client(cx_a, "user_a").await;
5625 let client_a2 = server.create_client(cx_a2, "user_a").await;
5626 let client_b = server.create_client(cx_b, "user_b").await;
5627 let client_b2 = server.create_client(cx_b2, "user_b").await;
5628 let client_c = server.create_client(cx_c, "user_c").await;
5629 let client_c2 = server.create_client(cx_c2, "user_c").await;
5630
5631 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5632 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5633 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5634
5635 // User A and User C request that user B become their contact.
5636 client_a
5637 .user_store
5638 .update(cx_a, |store, cx| {
5639 store.request_contact(client_b.user_id().unwrap(), cx)
5640 })
5641 .await
5642 .unwrap();
5643 client_c
5644 .user_store
5645 .update(cx_c, |store, cx| {
5646 store.request_contact(client_b.user_id().unwrap(), cx)
5647 })
5648 .await
5649 .unwrap();
5650 deterministic.run_until_parked();
5651
5652 // All users see the pending request appear in all their clients.
5653 assert_eq!(
5654 client_a.summarize_contacts(cx_a).outgoing_requests,
5655 &["user_b"]
5656 );
5657 assert_eq!(
5658 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5659 &["user_b"]
5660 );
5661 assert_eq!(
5662 client_b.summarize_contacts(cx_b).incoming_requests,
5663 &["user_a", "user_c"]
5664 );
5665 assert_eq!(
5666 client_b2.summarize_contacts(cx_b2).incoming_requests,
5667 &["user_a", "user_c"]
5668 );
5669 assert_eq!(
5670 client_c.summarize_contacts(cx_c).outgoing_requests,
5671 &["user_b"]
5672 );
5673 assert_eq!(
5674 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5675 &["user_b"]
5676 );
5677
5678 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5679 disconnect_and_reconnect(&client_a, cx_a).await;
5680 disconnect_and_reconnect(&client_b, cx_b).await;
5681 disconnect_and_reconnect(&client_c, cx_c).await;
5682 deterministic.run_until_parked();
5683 assert_eq!(
5684 client_a.summarize_contacts(cx_a).outgoing_requests,
5685 &["user_b"]
5686 );
5687 assert_eq!(
5688 client_b.summarize_contacts(cx_b).incoming_requests,
5689 &["user_a", "user_c"]
5690 );
5691 assert_eq!(
5692 client_c.summarize_contacts(cx_c).outgoing_requests,
5693 &["user_b"]
5694 );
5695
5696 // User B accepts the request from user A.
5697 client_b
5698 .user_store
5699 .update(cx_b, |store, cx| {
5700 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5701 })
5702 .await
5703 .unwrap();
5704
5705 deterministic.run_until_parked();
5706
5707 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5708 let contacts_b = client_b.summarize_contacts(cx_b);
5709 assert_eq!(contacts_b.current, &["user_a"]);
5710 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5711 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5712 assert_eq!(contacts_b2.current, &["user_a"]);
5713 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5714
5715 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5716 let contacts_a = client_a.summarize_contacts(cx_a);
5717 assert_eq!(contacts_a.current, &["user_b"]);
5718 assert!(contacts_a.outgoing_requests.is_empty());
5719 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5720 assert_eq!(contacts_a2.current, &["user_b"]);
5721 assert!(contacts_a2.outgoing_requests.is_empty());
5722
5723 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5724 disconnect_and_reconnect(&client_a, cx_a).await;
5725 disconnect_and_reconnect(&client_b, cx_b).await;
5726 disconnect_and_reconnect(&client_c, cx_c).await;
5727 deterministic.run_until_parked();
5728 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5729 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5730 assert_eq!(
5731 client_b.summarize_contacts(cx_b).incoming_requests,
5732 &["user_c"]
5733 );
5734 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5735 assert_eq!(
5736 client_c.summarize_contacts(cx_c).outgoing_requests,
5737 &["user_b"]
5738 );
5739
5740 // User B rejects the request from user C.
5741 client_b
5742 .user_store
5743 .update(cx_b, |store, cx| {
5744 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5745 })
5746 .await
5747 .unwrap();
5748
5749 deterministic.run_until_parked();
5750
5751 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5752 let contacts_b = client_b.summarize_contacts(cx_b);
5753 assert_eq!(contacts_b.current, &["user_a"]);
5754 assert!(contacts_b.incoming_requests.is_empty());
5755 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5756 assert_eq!(contacts_b2.current, &["user_a"]);
5757 assert!(contacts_b2.incoming_requests.is_empty());
5758
5759 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5760 let contacts_c = client_c.summarize_contacts(cx_c);
5761 assert!(contacts_c.current.is_empty());
5762 assert!(contacts_c.outgoing_requests.is_empty());
5763 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5764 assert!(contacts_c2.current.is_empty());
5765 assert!(contacts_c2.outgoing_requests.is_empty());
5766
5767 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5768 disconnect_and_reconnect(&client_a, cx_a).await;
5769 disconnect_and_reconnect(&client_b, cx_b).await;
5770 disconnect_and_reconnect(&client_c, cx_c).await;
5771 deterministic.run_until_parked();
5772 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5773 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5774 assert!(client_b
5775 .summarize_contacts(cx_b)
5776 .incoming_requests
5777 .is_empty());
5778 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5779 assert!(client_c
5780 .summarize_contacts(cx_c)
5781 .outgoing_requests
5782 .is_empty());
5783
5784 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
5785 client.disconnect(&cx.to_async());
5786 client.clear_contacts(cx).await;
5787 client
5788 .authenticate_and_connect(false, &cx.to_async())
5789 .await
5790 .unwrap();
5791 }
5792}
5793
5794#[gpui::test(iterations = 10)]
5795async fn test_following(
5796 deterministic: Arc<Deterministic>,
5797 cx_a: &mut TestAppContext,
5798 cx_b: &mut TestAppContext,
5799 cx_c: &mut TestAppContext,
5800) {
5801 deterministic.forbid_parking();
5802 cx_a.update(editor::init);
5803 cx_b.update(editor::init);
5804
5805 let mut server = TestServer::start(&deterministic).await;
5806 let client_a = server.create_client(cx_a, "user_a").await;
5807 let client_b = server.create_client(cx_b, "user_b").await;
5808 let client_c = server.create_client(cx_c, "user_c").await;
5809 server
5810 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5811 .await;
5812 server
5813 .make_contacts(&mut [(&client_a, cx_a), (&client_c, cx_c)])
5814 .await;
5815 let active_call_a = cx_a.read(ActiveCall::global);
5816 let active_call_b = cx_b.read(ActiveCall::global);
5817
5818 client_a
5819 .fs
5820 .insert_tree(
5821 "/a",
5822 json!({
5823 "1.txt": "one\none\none",
5824 "2.txt": "two\ntwo\ntwo",
5825 "3.txt": "three\nthree\nthree",
5826 }),
5827 )
5828 .await;
5829 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5830 active_call_a
5831 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5832 .await
5833 .unwrap();
5834
5835 let project_id = active_call_a
5836 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5837 .await
5838 .unwrap();
5839 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5840 active_call_b
5841 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5842 .await
5843 .unwrap();
5844
5845 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5846 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5847
5848 // Client A opens some editors.
5849 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
5850 let editor_a1 = workspace_a
5851 .update(cx_a, |workspace, cx| {
5852 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5853 })
5854 .await
5855 .unwrap()
5856 .downcast::<Editor>()
5857 .unwrap();
5858 let editor_a2 = workspace_a
5859 .update(cx_a, |workspace, cx| {
5860 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
5861 })
5862 .await
5863 .unwrap()
5864 .downcast::<Editor>()
5865 .unwrap();
5866
5867 // Client B opens an editor.
5868 let editor_b1 = workspace_b
5869 .update(cx_b, |workspace, cx| {
5870 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5871 })
5872 .await
5873 .unwrap()
5874 .downcast::<Editor>()
5875 .unwrap();
5876
5877 let peer_id_a = client_a.peer_id().unwrap();
5878 let peer_id_b = client_b.peer_id().unwrap();
5879 let peer_id_c = client_c.peer_id().unwrap();
5880
5881 // Client A updates their selections in those editors
5882 editor_a1.update(cx_a, |editor, cx| {
5883 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
5884 });
5885 editor_a2.update(cx_a, |editor, cx| {
5886 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
5887 });
5888
5889 // When client B starts following client A, all visible view states are replicated to client B.
5890 workspace_b
5891 .update(cx_b, |workspace, cx| {
5892 workspace
5893 .toggle_follow(&ToggleFollow(peer_id_a), cx)
5894 .unwrap()
5895 })
5896 .await
5897 .unwrap();
5898
5899 // Client A invites client C to the call.
5900 active_call_a
5901 .update(cx_a, |call, cx| {
5902 call.invite(client_c.current_user_id(cx_c).to_proto(), None, cx)
5903 })
5904 .await
5905 .unwrap();
5906 cx_c.foreground().run_until_parked();
5907 let active_call_c = cx_c.read(ActiveCall::global);
5908 active_call_c
5909 .update(cx_c, |call, cx| call.accept_incoming(cx))
5910 .await
5911 .unwrap();
5912 let project_c = client_c.build_remote_project(project_id, cx_c).await;
5913 let workspace_c = client_c.build_workspace(&project_c, cx_c);
5914 active_call_c
5915 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
5916 .await
5917 .unwrap();
5918
5919 // Client C also follows client A.
5920 workspace_c
5921 .update(cx_c, |workspace, cx| {
5922 workspace
5923 .toggle_follow(&ToggleFollow(peer_id_a), cx)
5924 .unwrap()
5925 })
5926 .await
5927 .unwrap();
5928
5929 // All clients see that clients B and C are following client A.
5930 cx_c.foreground().run_until_parked();
5931 for (name, active_call, cx) in [
5932 ("A", &active_call_a, &cx_a),
5933 ("B", &active_call_b, &cx_b),
5934 ("C", &active_call_c, &cx_c),
5935 ] {
5936 active_call.read_with(*cx, |call, cx| {
5937 let room = call.room().unwrap().read(cx);
5938 assert_eq!(
5939 room.followers_for(peer_id_a, project_id),
5940 &[peer_id_b, peer_id_c],
5941 "checking followers for A as {name}"
5942 );
5943 });
5944 }
5945
5946 // Client C unfollows client A.
5947 workspace_c.update(cx_c, |workspace, cx| {
5948 workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
5949 });
5950
5951 // All clients see that clients B is following client A.
5952 cx_c.foreground().run_until_parked();
5953 for (name, active_call, cx) in [
5954 ("A", &active_call_a, &cx_a),
5955 ("B", &active_call_b, &cx_b),
5956 ("C", &active_call_c, &cx_c),
5957 ] {
5958 active_call.read_with(*cx, |call, cx| {
5959 let room = call.room().unwrap().read(cx);
5960 assert_eq!(
5961 room.followers_for(peer_id_a, project_id),
5962 &[peer_id_b],
5963 "checking followers for A as {name}"
5964 );
5965 });
5966 }
5967
5968 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
5969 workspace
5970 .active_item(cx)
5971 .unwrap()
5972 .downcast::<Editor>()
5973 .unwrap()
5974 });
5975 assert_eq!(
5976 cx_b.read(|cx| editor_b2.project_path(cx)),
5977 Some((worktree_id, "2.txt").into())
5978 );
5979 assert_eq!(
5980 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
5981 vec![2..3]
5982 );
5983 assert_eq!(
5984 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
5985 vec![0..1]
5986 );
5987
5988 // When client A activates a different editor, client B does so as well.
5989 workspace_a.update(cx_a, |workspace, cx| {
5990 workspace.activate_item(&editor_a1, cx)
5991 });
5992 deterministic.run_until_parked();
5993 workspace_b.read_with(cx_b, |workspace, cx| {
5994 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
5995 });
5996
5997 // When client A opens a multibuffer, client B does so as well.
5998 let multibuffer_a = cx_a.add_model(|cx| {
5999 let buffer_a1 = project_a.update(cx, |project, cx| {
6000 project
6001 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
6002 .unwrap()
6003 });
6004 let buffer_a2 = project_a.update(cx, |project, cx| {
6005 project
6006 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
6007 .unwrap()
6008 });
6009 let mut result = MultiBuffer::new(0);
6010 result.push_excerpts(
6011 buffer_a1,
6012 [ExcerptRange {
6013 context: 0..3,
6014 primary: None,
6015 }],
6016 cx,
6017 );
6018 result.push_excerpts(
6019 buffer_a2,
6020 [ExcerptRange {
6021 context: 4..7,
6022 primary: None,
6023 }],
6024 cx,
6025 );
6026 result
6027 });
6028 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
6029 let editor =
6030 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
6031 workspace.add_item(Box::new(editor.clone()), cx);
6032 editor
6033 });
6034 deterministic.run_until_parked();
6035 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
6036 workspace
6037 .active_item(cx)
6038 .unwrap()
6039 .downcast::<Editor>()
6040 .unwrap()
6041 });
6042 assert_eq!(
6043 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
6044 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
6045 );
6046
6047 // When client A navigates back and forth, client B does so as well.
6048 workspace_a
6049 .update(cx_a, |workspace, cx| {
6050 workspace::Pane::go_back(workspace, None, cx)
6051 })
6052 .await;
6053 deterministic.run_until_parked();
6054 workspace_b.read_with(cx_b, |workspace, cx| {
6055 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6056 });
6057
6058 workspace_a
6059 .update(cx_a, |workspace, cx| {
6060 workspace::Pane::go_back(workspace, None, cx)
6061 })
6062 .await;
6063 deterministic.run_until_parked();
6064 workspace_b.read_with(cx_b, |workspace, cx| {
6065 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
6066 });
6067
6068 workspace_a
6069 .update(cx_a, |workspace, cx| {
6070 workspace::Pane::go_forward(workspace, None, cx)
6071 })
6072 .await;
6073 deterministic.run_until_parked();
6074 workspace_b.read_with(cx_b, |workspace, cx| {
6075 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6076 });
6077
6078 // Changes to client A's editor are reflected on client B.
6079 editor_a1.update(cx_a, |editor, cx| {
6080 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
6081 });
6082 deterministic.run_until_parked();
6083 editor_b1.read_with(cx_b, |editor, cx| {
6084 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
6085 });
6086
6087 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
6088 deterministic.run_until_parked();
6089 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
6090
6091 editor_a1.update(cx_a, |editor, cx| {
6092 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
6093 editor.set_scroll_position(vec2f(0., 100.), cx);
6094 });
6095 deterministic.run_until_parked();
6096 editor_b1.read_with(cx_b, |editor, cx| {
6097 assert_eq!(editor.selections.ranges(cx), &[3..3]);
6098 });
6099
6100 // After unfollowing, client B stops receiving updates from client A.
6101 workspace_b.update(cx_b, |workspace, cx| {
6102 workspace.unfollow(&workspace.active_pane().clone(), cx)
6103 });
6104 workspace_a.update(cx_a, |workspace, cx| {
6105 workspace.activate_item(&editor_a2, cx)
6106 });
6107 deterministic.run_until_parked();
6108 assert_eq!(
6109 workspace_b.read_with(cx_b, |workspace, cx| workspace
6110 .active_item(cx)
6111 .unwrap()
6112 .id()),
6113 editor_b1.id()
6114 );
6115
6116 // Client A starts following client B.
6117 workspace_a
6118 .update(cx_a, |workspace, cx| {
6119 workspace
6120 .toggle_follow(&ToggleFollow(peer_id_b), cx)
6121 .unwrap()
6122 })
6123 .await
6124 .unwrap();
6125 assert_eq!(
6126 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6127 Some(peer_id_b)
6128 );
6129 assert_eq!(
6130 workspace_a.read_with(cx_a, |workspace, cx| workspace
6131 .active_item(cx)
6132 .unwrap()
6133 .id()),
6134 editor_a1.id()
6135 );
6136
6137 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
6138 let display = MacOSDisplay::new();
6139 active_call_b
6140 .update(cx_b, |call, cx| call.set_location(None, cx))
6141 .await
6142 .unwrap();
6143 active_call_b
6144 .update(cx_b, |call, cx| {
6145 call.room().unwrap().update(cx, |room, cx| {
6146 room.set_display_sources(vec![display.clone()]);
6147 room.share_screen(cx)
6148 })
6149 })
6150 .await
6151 .unwrap();
6152 deterministic.run_until_parked();
6153 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
6154 workspace
6155 .active_item(cx)
6156 .unwrap()
6157 .downcast::<SharedScreen>()
6158 .unwrap()
6159 });
6160
6161 // Client B activates Zed again, which causes the previous editor to become focused again.
6162 active_call_b
6163 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6164 .await
6165 .unwrap();
6166 deterministic.run_until_parked();
6167 workspace_a.read_with(cx_a, |workspace, cx| {
6168 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
6169 });
6170
6171 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
6172 workspace_b.update(cx_b, |workspace, cx| {
6173 workspace.activate_item(&multibuffer_editor_b, cx)
6174 });
6175 deterministic.run_until_parked();
6176 workspace_a.read_with(cx_a, |workspace, cx| {
6177 assert_eq!(
6178 workspace.active_item(cx).unwrap().id(),
6179 multibuffer_editor_a.id()
6180 )
6181 });
6182
6183 // Client B activates an external window again, and the previously-opened screen-sharing item
6184 // gets activated.
6185 active_call_b
6186 .update(cx_b, |call, cx| call.set_location(None, cx))
6187 .await
6188 .unwrap();
6189 deterministic.run_until_parked();
6190 assert_eq!(
6191 workspace_a.read_with(cx_a, |workspace, cx| workspace
6192 .active_item(cx)
6193 .unwrap()
6194 .id()),
6195 shared_screen.id()
6196 );
6197
6198 // Following interrupts when client B disconnects.
6199 client_b.disconnect(&cx_b.to_async());
6200 deterministic.advance_clock(RECONNECT_TIMEOUT);
6201 assert_eq!(
6202 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6203 None
6204 );
6205}
6206
6207#[gpui::test]
6208async fn test_following_tab_order(
6209 deterministic: Arc<Deterministic>,
6210 cx_a: &mut TestAppContext,
6211 cx_b: &mut TestAppContext,
6212) {
6213 cx_a.update(editor::init);
6214 cx_b.update(editor::init);
6215
6216 let mut server = TestServer::start(&deterministic).await;
6217 let client_a = server.create_client(cx_a, "user_a").await;
6218 let client_b = server.create_client(cx_b, "user_b").await;
6219 server
6220 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6221 .await;
6222 let active_call_a = cx_a.read(ActiveCall::global);
6223 let active_call_b = cx_b.read(ActiveCall::global);
6224
6225 client_a
6226 .fs
6227 .insert_tree(
6228 "/a",
6229 json!({
6230 "1.txt": "one",
6231 "2.txt": "two",
6232 "3.txt": "three",
6233 }),
6234 )
6235 .await;
6236 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6237 active_call_a
6238 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6239 .await
6240 .unwrap();
6241
6242 let project_id = active_call_a
6243 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6244 .await
6245 .unwrap();
6246 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6247 active_call_b
6248 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6249 .await
6250 .unwrap();
6251
6252 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6253 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6254
6255 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6256 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6257
6258 let client_b_id = project_a.read_with(cx_a, |project, _| {
6259 project.collaborators().values().next().unwrap().peer_id
6260 });
6261
6262 //Open 1, 3 in that order on client A
6263 workspace_a
6264 .update(cx_a, |workspace, cx| {
6265 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6266 })
6267 .await
6268 .unwrap();
6269 workspace_a
6270 .update(cx_a, |workspace, cx| {
6271 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6272 })
6273 .await
6274 .unwrap();
6275
6276 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
6277 pane.update(cx, |pane, cx| {
6278 pane.items()
6279 .map(|item| {
6280 item.project_path(cx)
6281 .unwrap()
6282 .path
6283 .to_str()
6284 .unwrap()
6285 .to_owned()
6286 })
6287 .collect::<Vec<_>>()
6288 })
6289 };
6290
6291 //Verify that the tabs opened in the order we expect
6292 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
6293
6294 //Follow client B as client A
6295 workspace_a
6296 .update(cx_a, |workspace, cx| {
6297 workspace
6298 .toggle_follow(&ToggleFollow(client_b_id), cx)
6299 .unwrap()
6300 })
6301 .await
6302 .unwrap();
6303
6304 //Open just 2 on client B
6305 workspace_b
6306 .update(cx_b, |workspace, cx| {
6307 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6308 })
6309 .await
6310 .unwrap();
6311 deterministic.run_until_parked();
6312
6313 // Verify that newly opened followed file is at the end
6314 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6315
6316 //Open just 1 on client B
6317 workspace_b
6318 .update(cx_b, |workspace, cx| {
6319 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6320 })
6321 .await
6322 .unwrap();
6323 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
6324 deterministic.run_until_parked();
6325
6326 // Verify that following into 1 did not reorder
6327 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6328}
6329
6330#[gpui::test(iterations = 10)]
6331async fn test_peers_following_each_other(
6332 deterministic: Arc<Deterministic>,
6333 cx_a: &mut TestAppContext,
6334 cx_b: &mut TestAppContext,
6335) {
6336 deterministic.forbid_parking();
6337 cx_a.update(editor::init);
6338 cx_b.update(editor::init);
6339
6340 let mut server = TestServer::start(&deterministic).await;
6341 let client_a = server.create_client(cx_a, "user_a").await;
6342 let client_b = server.create_client(cx_b, "user_b").await;
6343 server
6344 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6345 .await;
6346 let active_call_a = cx_a.read(ActiveCall::global);
6347 let active_call_b = cx_b.read(ActiveCall::global);
6348
6349 // Client A shares a project.
6350 client_a
6351 .fs
6352 .insert_tree(
6353 "/a",
6354 json!({
6355 "1.txt": "one",
6356 "2.txt": "two",
6357 "3.txt": "three",
6358 "4.txt": "four",
6359 }),
6360 )
6361 .await;
6362 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6363 active_call_a
6364 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6365 .await
6366 .unwrap();
6367 let project_id = active_call_a
6368 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6369 .await
6370 .unwrap();
6371
6372 // Client B joins the project.
6373 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6374 active_call_b
6375 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6376 .await
6377 .unwrap();
6378
6379 // Client A opens some editors.
6380 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6381 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6382 let _editor_a1 = workspace_a
6383 .update(cx_a, |workspace, cx| {
6384 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6385 })
6386 .await
6387 .unwrap()
6388 .downcast::<Editor>()
6389 .unwrap();
6390
6391 // Client B opens an editor.
6392 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6393 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6394 let _editor_b1 = workspace_b
6395 .update(cx_b, |workspace, cx| {
6396 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6397 })
6398 .await
6399 .unwrap()
6400 .downcast::<Editor>()
6401 .unwrap();
6402
6403 // Clients A and B follow each other in split panes
6404 workspace_a.update(cx_a, |workspace, cx| {
6405 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6406 let pane_a1 = pane_a1.clone();
6407 cx.defer(move |workspace, _| {
6408 assert_ne!(*workspace.active_pane(), pane_a1);
6409 });
6410 });
6411 workspace_a
6412 .update(cx_a, |workspace, cx| {
6413 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
6414 workspace
6415 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
6416 .unwrap()
6417 })
6418 .await
6419 .unwrap();
6420 workspace_b.update(cx_b, |workspace, cx| {
6421 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6422 let pane_b1 = pane_b1.clone();
6423 cx.defer(move |workspace, _| {
6424 assert_ne!(*workspace.active_pane(), pane_b1);
6425 });
6426 });
6427 workspace_b
6428 .update(cx_b, |workspace, cx| {
6429 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
6430 workspace
6431 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
6432 .unwrap()
6433 })
6434 .await
6435 .unwrap();
6436
6437 workspace_a.update(cx_a, |workspace, cx| {
6438 workspace.activate_next_pane(cx);
6439 });
6440 // Wait for focus effects to be fully flushed
6441 workspace_a.update(cx_a, |workspace, _| {
6442 assert_eq!(*workspace.active_pane(), pane_a1);
6443 });
6444
6445 workspace_a
6446 .update(cx_a, |workspace, cx| {
6447 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6448 })
6449 .await
6450 .unwrap();
6451 workspace_b.update(cx_b, |workspace, cx| {
6452 workspace.activate_next_pane(cx);
6453 });
6454
6455 workspace_b
6456 .update(cx_b, |workspace, cx| {
6457 assert_eq!(*workspace.active_pane(), pane_b1);
6458 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
6459 })
6460 .await
6461 .unwrap();
6462 cx_a.foreground().run_until_parked();
6463
6464 // Ensure leader updates don't change the active pane of followers
6465 workspace_a.read_with(cx_a, |workspace, _| {
6466 assert_eq!(*workspace.active_pane(), pane_a1);
6467 });
6468 workspace_b.read_with(cx_b, |workspace, _| {
6469 assert_eq!(*workspace.active_pane(), pane_b1);
6470 });
6471
6472 // Ensure peers following each other doesn't cause an infinite loop.
6473 assert_eq!(
6474 workspace_a.read_with(cx_a, |workspace, cx| workspace
6475 .active_item(cx)
6476 .unwrap()
6477 .project_path(cx)),
6478 Some((worktree_id, "3.txt").into())
6479 );
6480 workspace_a.update(cx_a, |workspace, cx| {
6481 assert_eq!(
6482 workspace.active_item(cx).unwrap().project_path(cx),
6483 Some((worktree_id, "3.txt").into())
6484 );
6485 workspace.activate_next_pane(cx);
6486 });
6487
6488 workspace_a.update(cx_a, |workspace, cx| {
6489 assert_eq!(
6490 workspace.active_item(cx).unwrap().project_path(cx),
6491 Some((worktree_id, "4.txt").into())
6492 );
6493 });
6494
6495 workspace_b.update(cx_b, |workspace, cx| {
6496 assert_eq!(
6497 workspace.active_item(cx).unwrap().project_path(cx),
6498 Some((worktree_id, "4.txt").into())
6499 );
6500 workspace.activate_next_pane(cx);
6501 });
6502
6503 workspace_b.update(cx_b, |workspace, cx| {
6504 assert_eq!(
6505 workspace.active_item(cx).unwrap().project_path(cx),
6506 Some((worktree_id, "3.txt").into())
6507 );
6508 });
6509}
6510
6511#[gpui::test(iterations = 10)]
6512async fn test_auto_unfollowing(
6513 deterministic: Arc<Deterministic>,
6514 cx_a: &mut TestAppContext,
6515 cx_b: &mut TestAppContext,
6516) {
6517 deterministic.forbid_parking();
6518 cx_a.update(editor::init);
6519 cx_b.update(editor::init);
6520
6521 // 2 clients connect to a server.
6522 let mut server = TestServer::start(&deterministic).await;
6523 let client_a = server.create_client(cx_a, "user_a").await;
6524 let client_b = server.create_client(cx_b, "user_b").await;
6525 server
6526 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6527 .await;
6528 let active_call_a = cx_a.read(ActiveCall::global);
6529 let active_call_b = cx_b.read(ActiveCall::global);
6530
6531 // Client A shares a project.
6532 client_a
6533 .fs
6534 .insert_tree(
6535 "/a",
6536 json!({
6537 "1.txt": "one",
6538 "2.txt": "two",
6539 "3.txt": "three",
6540 }),
6541 )
6542 .await;
6543 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6544 active_call_a
6545 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6546 .await
6547 .unwrap();
6548
6549 let project_id = active_call_a
6550 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6551 .await
6552 .unwrap();
6553 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6554 active_call_b
6555 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6556 .await
6557 .unwrap();
6558
6559 // Client A opens some editors.
6560 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6561 let _editor_a1 = workspace_a
6562 .update(cx_a, |workspace, cx| {
6563 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6564 })
6565 .await
6566 .unwrap()
6567 .downcast::<Editor>()
6568 .unwrap();
6569
6570 // Client B starts following client A.
6571 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6572 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6573 let leader_id = project_b.read_with(cx_b, |project, _| {
6574 project.collaborators().values().next().unwrap().peer_id
6575 });
6576 workspace_b
6577 .update(cx_b, |workspace, cx| {
6578 workspace
6579 .toggle_follow(&ToggleFollow(leader_id), cx)
6580 .unwrap()
6581 })
6582 .await
6583 .unwrap();
6584 assert_eq!(
6585 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6586 Some(leader_id)
6587 );
6588 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
6589 workspace
6590 .active_item(cx)
6591 .unwrap()
6592 .downcast::<Editor>()
6593 .unwrap()
6594 });
6595
6596 // When client B moves, it automatically stops following client A.
6597 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
6598 assert_eq!(
6599 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6600 None
6601 );
6602
6603 workspace_b
6604 .update(cx_b, |workspace, cx| {
6605 workspace
6606 .toggle_follow(&ToggleFollow(leader_id), cx)
6607 .unwrap()
6608 })
6609 .await
6610 .unwrap();
6611 assert_eq!(
6612 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6613 Some(leader_id)
6614 );
6615
6616 // When client B edits, it automatically stops following client A.
6617 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
6618 assert_eq!(
6619 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6620 None
6621 );
6622
6623 workspace_b
6624 .update(cx_b, |workspace, cx| {
6625 workspace
6626 .toggle_follow(&ToggleFollow(leader_id), cx)
6627 .unwrap()
6628 })
6629 .await
6630 .unwrap();
6631 assert_eq!(
6632 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6633 Some(leader_id)
6634 );
6635
6636 // When client B scrolls, it automatically stops following client A.
6637 editor_b2.update(cx_b, |editor, cx| {
6638 editor.set_scroll_position(vec2f(0., 3.), cx)
6639 });
6640 assert_eq!(
6641 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6642 None
6643 );
6644
6645 workspace_b
6646 .update(cx_b, |workspace, cx| {
6647 workspace
6648 .toggle_follow(&ToggleFollow(leader_id), cx)
6649 .unwrap()
6650 })
6651 .await
6652 .unwrap();
6653 assert_eq!(
6654 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6655 Some(leader_id)
6656 );
6657
6658 // When client B activates a different pane, it continues following client A in the original pane.
6659 workspace_b.update(cx_b, |workspace, cx| {
6660 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
6661 });
6662 assert_eq!(
6663 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6664 Some(leader_id)
6665 );
6666
6667 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
6668 assert_eq!(
6669 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6670 Some(leader_id)
6671 );
6672
6673 // When client B activates a different item in the original pane, it automatically stops following client A.
6674 workspace_b
6675 .update(cx_b, |workspace, cx| {
6676 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6677 })
6678 .await
6679 .unwrap();
6680 assert_eq!(
6681 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6682 None
6683 );
6684}
6685
6686#[gpui::test(iterations = 10)]
6687async fn test_peers_simultaneously_following_each_other(
6688 deterministic: Arc<Deterministic>,
6689 cx_a: &mut TestAppContext,
6690 cx_b: &mut TestAppContext,
6691) {
6692 deterministic.forbid_parking();
6693 cx_a.update(editor::init);
6694 cx_b.update(editor::init);
6695
6696 let mut server = TestServer::start(&deterministic).await;
6697 let client_a = server.create_client(cx_a, "user_a").await;
6698 let client_b = server.create_client(cx_b, "user_b").await;
6699 server
6700 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6701 .await;
6702 let active_call_a = cx_a.read(ActiveCall::global);
6703
6704 client_a.fs.insert_tree("/a", json!({})).await;
6705 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
6706 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6707 let project_id = active_call_a
6708 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6709 .await
6710 .unwrap();
6711
6712 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6713 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6714
6715 deterministic.run_until_parked();
6716 let client_a_id = project_b.read_with(cx_b, |project, _| {
6717 project.collaborators().values().next().unwrap().peer_id
6718 });
6719 let client_b_id = project_a.read_with(cx_a, |project, _| {
6720 project.collaborators().values().next().unwrap().peer_id
6721 });
6722
6723 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
6724 workspace
6725 .toggle_follow(&ToggleFollow(client_b_id), cx)
6726 .unwrap()
6727 });
6728 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
6729 workspace
6730 .toggle_follow(&ToggleFollow(client_a_id), cx)
6731 .unwrap()
6732 });
6733
6734 futures::try_join!(a_follow_b, b_follow_a).unwrap();
6735 workspace_a.read_with(cx_a, |workspace, _| {
6736 assert_eq!(
6737 workspace.leader_for_pane(workspace.active_pane()),
6738 Some(client_b_id)
6739 );
6740 });
6741 workspace_b.read_with(cx_b, |workspace, _| {
6742 assert_eq!(
6743 workspace.leader_for_pane(workspace.active_pane()),
6744 Some(client_a_id)
6745 );
6746 });
6747}
6748
6749#[derive(Debug, Eq, PartialEq)]
6750struct RoomParticipants {
6751 remote: Vec<String>,
6752 pending: Vec<String>,
6753}
6754
6755fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
6756 room.read_with(cx, |room, _| {
6757 let mut remote = room
6758 .remote_participants()
6759 .iter()
6760 .map(|(_, participant)| participant.user.github_login.clone())
6761 .collect::<Vec<_>>();
6762 let mut pending = room
6763 .pending_participants()
6764 .iter()
6765 .map(|user| user.github_login.clone())
6766 .collect::<Vec<_>>();
6767 remote.sort();
6768 pending.sort();
6769 RoomParticipants { remote, pending }
6770 })
6771}