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