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