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