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