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