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