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