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
2419 .fs
2420 .as_fake()
2421 .set_index_for_repo(
2422 Path::new("/dir/.git"),
2423 &[(Path::new("a.txt"), diff_base.clone())],
2424 )
2425 .await;
2426
2427 // Create the buffer
2428 let buffer_local_a = project_local
2429 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2430 .await
2431 .unwrap();
2432
2433 // Wait for it to catch up to the new diff
2434 deterministic.run_until_parked();
2435
2436 // Smoke test diffing
2437 buffer_local_a.read_with(cx_a, |buffer, _| {
2438 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2439 git::diff::assert_hunks(
2440 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2441 &buffer,
2442 &diff_base,
2443 &[(1..2, "", "two\n")],
2444 );
2445 });
2446
2447 // Create remote buffer
2448 let buffer_remote_a = project_remote
2449 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2450 .await
2451 .unwrap();
2452
2453 // Wait remote buffer to catch up to the new diff
2454 deterministic.run_until_parked();
2455
2456 // Smoke test diffing
2457 buffer_remote_a.read_with(cx_b, |buffer, _| {
2458 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2459 git::diff::assert_hunks(
2460 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2461 &buffer,
2462 &diff_base,
2463 &[(1..2, "", "two\n")],
2464 );
2465 });
2466
2467 client_a
2468 .fs
2469 .as_fake()
2470 .set_index_for_repo(
2471 Path::new("/dir/.git"),
2472 &[(Path::new("a.txt"), new_diff_base.clone())],
2473 )
2474 .await;
2475
2476 // Wait for buffer_local_a to receive it
2477 deterministic.run_until_parked();
2478
2479 // Smoke test new diffing
2480 buffer_local_a.read_with(cx_a, |buffer, _| {
2481 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2482
2483 git::diff::assert_hunks(
2484 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2485 &buffer,
2486 &diff_base,
2487 &[(2..3, "", "three\n")],
2488 );
2489 });
2490
2491 // Smoke test B
2492 buffer_remote_a.read_with(cx_b, |buffer, _| {
2493 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2494 git::diff::assert_hunks(
2495 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2496 &buffer,
2497 &diff_base,
2498 &[(2..3, "", "three\n")],
2499 );
2500 });
2501
2502 //Nested git dir
2503
2504 let diff_base = "
2505 one
2506 three
2507 "
2508 .unindent();
2509
2510 let new_diff_base = "
2511 one
2512 two
2513 "
2514 .unindent();
2515
2516 client_a
2517 .fs
2518 .as_fake()
2519 .set_index_for_repo(
2520 Path::new("/dir/sub/.git"),
2521 &[(Path::new("b.txt"), diff_base.clone())],
2522 )
2523 .await;
2524
2525 // Create the buffer
2526 let buffer_local_b = project_local
2527 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2528 .await
2529 .unwrap();
2530
2531 // Wait for it to catch up to the new diff
2532 deterministic.run_until_parked();
2533
2534 // Smoke test diffing
2535 buffer_local_b.read_with(cx_a, |buffer, _| {
2536 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2537 git::diff::assert_hunks(
2538 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2539 &buffer,
2540 &diff_base,
2541 &[(1..2, "", "two\n")],
2542 );
2543 });
2544
2545 // Create remote buffer
2546 let buffer_remote_b = project_remote
2547 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2548 .await
2549 .unwrap();
2550
2551 // Wait remote buffer to catch up to the new diff
2552 deterministic.run_until_parked();
2553
2554 // Smoke test diffing
2555 buffer_remote_b.read_with(cx_b, |buffer, _| {
2556 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2557 git::diff::assert_hunks(
2558 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2559 &buffer,
2560 &diff_base,
2561 &[(1..2, "", "two\n")],
2562 );
2563 });
2564
2565 client_a
2566 .fs
2567 .as_fake()
2568 .set_index_for_repo(
2569 Path::new("/dir/sub/.git"),
2570 &[(Path::new("b.txt"), new_diff_base.clone())],
2571 )
2572 .await;
2573
2574 // Wait for buffer_local_b to receive it
2575 deterministic.run_until_parked();
2576
2577 // Smoke test new diffing
2578 buffer_local_b.read_with(cx_a, |buffer, _| {
2579 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2580 println!("{:?}", buffer.as_rope().to_string());
2581 println!("{:?}", buffer.diff_base());
2582 println!(
2583 "{:?}",
2584 buffer
2585 .snapshot()
2586 .git_diff_hunks_in_row_range(0..4)
2587 .collect::<Vec<_>>()
2588 );
2589
2590 git::diff::assert_hunks(
2591 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2592 &buffer,
2593 &diff_base,
2594 &[(2..3, "", "three\n")],
2595 );
2596 });
2597
2598 // Smoke test B
2599 buffer_remote_b.read_with(cx_b, |buffer, _| {
2600 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2601 git::diff::assert_hunks(
2602 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2603 &buffer,
2604 &diff_base,
2605 &[(2..3, "", "three\n")],
2606 );
2607 });
2608}
2609
2610#[gpui::test]
2611async fn test_git_branch_name(
2612 deterministic: Arc<Deterministic>,
2613 cx_a: &mut TestAppContext,
2614 cx_b: &mut TestAppContext,
2615 cx_c: &mut TestAppContext,
2616) {
2617 deterministic.forbid_parking();
2618 let mut server = TestServer::start(&deterministic).await;
2619 let client_a = server.create_client(cx_a, "user_a").await;
2620 let client_b = server.create_client(cx_b, "user_b").await;
2621 let client_c = server.create_client(cx_c, "user_c").await;
2622 server
2623 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2624 .await;
2625 let active_call_a = cx_a.read(ActiveCall::global);
2626
2627 client_a
2628 .fs
2629 .insert_tree(
2630 "/dir",
2631 json!({
2632 ".git": {},
2633 }),
2634 )
2635 .await;
2636
2637 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2638 let project_id = active_call_a
2639 .update(cx_a, |call, cx| {
2640 call.share_project(project_local.clone(), cx)
2641 })
2642 .await
2643 .unwrap();
2644
2645 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2646 client_a
2647 .fs
2648 .as_fake()
2649 .set_branch_name(Path::new("/dir/.git"), Some("branch-1"))
2650 .await;
2651
2652 // Wait for it to catch up to the new branch
2653 deterministic.run_until_parked();
2654
2655 #[track_caller]
2656 fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &AppContext) {
2657 let branch_name = branch_name.map(Into::into);
2658 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2659 assert_eq!(worktrees.len(), 1);
2660 let worktree = worktrees[0].clone();
2661 let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap();
2662 assert_eq!(root_entry.branch(), branch_name.map(Into::into));
2663 }
2664
2665 // Smoke test branch reading
2666 project_local.read_with(cx_a, |project, cx| {
2667 assert_branch(Some("branch-1"), project, cx)
2668 });
2669 project_remote.read_with(cx_b, |project, cx| {
2670 assert_branch(Some("branch-1"), project, cx)
2671 });
2672
2673 client_a
2674 .fs
2675 .as_fake()
2676 .set_branch_name(Path::new("/dir/.git"), Some("branch-2"))
2677 .await;
2678
2679 // Wait for buffer_local_a to receive it
2680 deterministic.run_until_parked();
2681
2682 // Smoke test branch reading
2683 project_local.read_with(cx_a, |project, cx| {
2684 assert_branch(Some("branch-2"), project, cx)
2685 });
2686 project_remote.read_with(cx_b, |project, cx| {
2687 assert_branch(Some("branch-2"), project, cx)
2688 });
2689
2690 let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
2691 deterministic.run_until_parked();
2692 project_remote_c.read_with(cx_c, |project, cx| {
2693 assert_branch(Some("branch-2"), project, cx)
2694 });
2695}
2696
2697#[gpui::test]
2698async fn test_git_status_sync(
2699 deterministic: Arc<Deterministic>,
2700 cx_a: &mut TestAppContext,
2701 cx_b: &mut TestAppContext,
2702 cx_c: &mut TestAppContext,
2703) {
2704 deterministic.forbid_parking();
2705 let mut server = TestServer::start(&deterministic).await;
2706 let client_a = server.create_client(cx_a, "user_a").await;
2707 let client_b = server.create_client(cx_b, "user_b").await;
2708 let client_c = server.create_client(cx_c, "user_c").await;
2709 server
2710 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2711 .await;
2712 let active_call_a = cx_a.read(ActiveCall::global);
2713
2714 client_a
2715 .fs
2716 .insert_tree(
2717 "/dir",
2718 json!({
2719 ".git": {},
2720 "a.txt": "a",
2721 "b.txt": "b",
2722 }),
2723 )
2724 .await;
2725
2726 const A_TXT: &'static str = "a.txt";
2727 const B_TXT: &'static str = "b.txt";
2728
2729 client_a
2730 .fs
2731 .as_fake()
2732 .set_status_for_repo(
2733 Path::new("/dir/.git"),
2734 &[
2735 (&Path::new(A_TXT), GitFileStatus::Added),
2736 (&Path::new(B_TXT), GitFileStatus::Added),
2737 ],
2738 )
2739 .await;
2740
2741 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2742 let project_id = active_call_a
2743 .update(cx_a, |call, cx| {
2744 call.share_project(project_local.clone(), cx)
2745 })
2746 .await
2747 .unwrap();
2748
2749 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2750
2751 // Wait for it to catch up to the new status
2752 deterministic.run_until_parked();
2753
2754 #[track_caller]
2755 fn assert_status(
2756 file: &impl AsRef<Path>,
2757 status: Option<GitFileStatus>,
2758 project: &Project,
2759 cx: &AppContext,
2760 ) {
2761 let file = file.as_ref();
2762 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2763 assert_eq!(worktrees.len(), 1);
2764 let worktree = worktrees[0].clone();
2765 let snapshot = worktree.read(cx).snapshot();
2766 let root_entry = snapshot.root_git_entry().unwrap();
2767 assert_eq!(root_entry.status_for_file(&snapshot, file), status);
2768 }
2769
2770 // Smoke test status reading
2771 project_local.read_with(cx_a, |project, cx| {
2772 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2773 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2774 });
2775 project_remote.read_with(cx_b, |project, cx| {
2776 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2777 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2778 });
2779
2780 client_a
2781 .fs
2782 .as_fake()
2783 .set_status_for_repo(
2784 Path::new("/dir/.git"),
2785 &[
2786 (&Path::new(A_TXT), GitFileStatus::Modified),
2787 (&Path::new(B_TXT), GitFileStatus::Modified),
2788 ],
2789 )
2790 .await;
2791
2792 // Wait for buffer_local_a to receive it
2793 deterministic.run_until_parked();
2794
2795 // Smoke test status reading
2796 project_local.read_with(cx_a, |project, cx| {
2797 assert_status(
2798 &Path::new(A_TXT),
2799 Some(GitFileStatus::Modified),
2800 project,
2801 cx,
2802 );
2803 assert_status(
2804 &Path::new(B_TXT),
2805 Some(GitFileStatus::Modified),
2806 project,
2807 cx,
2808 );
2809 });
2810 project_remote.read_with(cx_b, |project, cx| {
2811 assert_status(
2812 &Path::new(A_TXT),
2813 Some(GitFileStatus::Modified),
2814 project,
2815 cx,
2816 );
2817 assert_status(
2818 &Path::new(B_TXT),
2819 Some(GitFileStatus::Modified),
2820 project,
2821 cx,
2822 );
2823 });
2824
2825 // And synchronization while joining
2826 let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
2827 deterministic.run_until_parked();
2828
2829 project_remote_c.read_with(cx_c, |project, cx| {
2830 assert_status(
2831 &Path::new(A_TXT),
2832 Some(GitFileStatus::Modified),
2833 project,
2834 cx,
2835 );
2836 assert_status(
2837 &Path::new(B_TXT),
2838 Some(GitFileStatus::Modified),
2839 project,
2840 cx,
2841 );
2842 });
2843}
2844
2845#[gpui::test(iterations = 10)]
2846async fn test_fs_operations(
2847 deterministic: Arc<Deterministic>,
2848 cx_a: &mut TestAppContext,
2849 cx_b: &mut TestAppContext,
2850) {
2851 deterministic.forbid_parking();
2852 let mut server = TestServer::start(&deterministic).await;
2853 let client_a = server.create_client(cx_a, "user_a").await;
2854 let client_b = server.create_client(cx_b, "user_b").await;
2855 server
2856 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2857 .await;
2858 let active_call_a = cx_a.read(ActiveCall::global);
2859
2860 client_a
2861 .fs
2862 .insert_tree(
2863 "/dir",
2864 json!({
2865 "a.txt": "a-contents",
2866 "b.txt": "b-contents",
2867 }),
2868 )
2869 .await;
2870 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2871 let project_id = active_call_a
2872 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2873 .await
2874 .unwrap();
2875 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2876
2877 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
2878 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
2879
2880 let entry = project_b
2881 .update(cx_b, |project, cx| {
2882 project
2883 .create_entry((worktree_id, "c.txt"), false, cx)
2884 .unwrap()
2885 })
2886 .await
2887 .unwrap();
2888 worktree_a.read_with(cx_a, |worktree, _| {
2889 assert_eq!(
2890 worktree
2891 .paths()
2892 .map(|p| p.to_string_lossy())
2893 .collect::<Vec<_>>(),
2894 ["a.txt", "b.txt", "c.txt"]
2895 );
2896 });
2897 worktree_b.read_with(cx_b, |worktree, _| {
2898 assert_eq!(
2899 worktree
2900 .paths()
2901 .map(|p| p.to_string_lossy())
2902 .collect::<Vec<_>>(),
2903 ["a.txt", "b.txt", "c.txt"]
2904 );
2905 });
2906
2907 project_b
2908 .update(cx_b, |project, cx| {
2909 project.rename_entry(entry.id, Path::new("d.txt"), cx)
2910 })
2911 .unwrap()
2912 .await
2913 .unwrap();
2914 worktree_a.read_with(cx_a, |worktree, _| {
2915 assert_eq!(
2916 worktree
2917 .paths()
2918 .map(|p| p.to_string_lossy())
2919 .collect::<Vec<_>>(),
2920 ["a.txt", "b.txt", "d.txt"]
2921 );
2922 });
2923 worktree_b.read_with(cx_b, |worktree, _| {
2924 assert_eq!(
2925 worktree
2926 .paths()
2927 .map(|p| p.to_string_lossy())
2928 .collect::<Vec<_>>(),
2929 ["a.txt", "b.txt", "d.txt"]
2930 );
2931 });
2932
2933 let dir_entry = project_b
2934 .update(cx_b, |project, cx| {
2935 project
2936 .create_entry((worktree_id, "DIR"), true, cx)
2937 .unwrap()
2938 })
2939 .await
2940 .unwrap();
2941 worktree_a.read_with(cx_a, |worktree, _| {
2942 assert_eq!(
2943 worktree
2944 .paths()
2945 .map(|p| p.to_string_lossy())
2946 .collect::<Vec<_>>(),
2947 ["DIR", "a.txt", "b.txt", "d.txt"]
2948 );
2949 });
2950 worktree_b.read_with(cx_b, |worktree, _| {
2951 assert_eq!(
2952 worktree
2953 .paths()
2954 .map(|p| p.to_string_lossy())
2955 .collect::<Vec<_>>(),
2956 ["DIR", "a.txt", "b.txt", "d.txt"]
2957 );
2958 });
2959
2960 project_b
2961 .update(cx_b, |project, cx| {
2962 project
2963 .create_entry((worktree_id, "DIR/e.txt"), false, cx)
2964 .unwrap()
2965 })
2966 .await
2967 .unwrap();
2968 project_b
2969 .update(cx_b, |project, cx| {
2970 project
2971 .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
2972 .unwrap()
2973 })
2974 .await
2975 .unwrap();
2976 project_b
2977 .update(cx_b, |project, cx| {
2978 project
2979 .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
2980 .unwrap()
2981 })
2982 .await
2983 .unwrap();
2984 worktree_a.read_with(cx_a, |worktree, _| {
2985 assert_eq!(
2986 worktree
2987 .paths()
2988 .map(|p| p.to_string_lossy())
2989 .collect::<Vec<_>>(),
2990 [
2991 "DIR",
2992 "DIR/SUBDIR",
2993 "DIR/SUBDIR/f.txt",
2994 "DIR/e.txt",
2995 "a.txt",
2996 "b.txt",
2997 "d.txt"
2998 ]
2999 );
3000 });
3001 worktree_b.read_with(cx_b, |worktree, _| {
3002 assert_eq!(
3003 worktree
3004 .paths()
3005 .map(|p| p.to_string_lossy())
3006 .collect::<Vec<_>>(),
3007 [
3008 "DIR",
3009 "DIR/SUBDIR",
3010 "DIR/SUBDIR/f.txt",
3011 "DIR/e.txt",
3012 "a.txt",
3013 "b.txt",
3014 "d.txt"
3015 ]
3016 );
3017 });
3018
3019 project_b
3020 .update(cx_b, |project, cx| {
3021 project
3022 .copy_entry(entry.id, Path::new("f.txt"), cx)
3023 .unwrap()
3024 })
3025 .await
3026 .unwrap();
3027 worktree_a.read_with(cx_a, |worktree, _| {
3028 assert_eq!(
3029 worktree
3030 .paths()
3031 .map(|p| p.to_string_lossy())
3032 .collect::<Vec<_>>(),
3033 [
3034 "DIR",
3035 "DIR/SUBDIR",
3036 "DIR/SUBDIR/f.txt",
3037 "DIR/e.txt",
3038 "a.txt",
3039 "b.txt",
3040 "d.txt",
3041 "f.txt"
3042 ]
3043 );
3044 });
3045 worktree_b.read_with(cx_b, |worktree, _| {
3046 assert_eq!(
3047 worktree
3048 .paths()
3049 .map(|p| p.to_string_lossy())
3050 .collect::<Vec<_>>(),
3051 [
3052 "DIR",
3053 "DIR/SUBDIR",
3054 "DIR/SUBDIR/f.txt",
3055 "DIR/e.txt",
3056 "a.txt",
3057 "b.txt",
3058 "d.txt",
3059 "f.txt"
3060 ]
3061 );
3062 });
3063
3064 project_b
3065 .update(cx_b, |project, cx| {
3066 project.delete_entry(dir_entry.id, cx).unwrap()
3067 })
3068 .await
3069 .unwrap();
3070 deterministic.run_until_parked();
3071
3072 worktree_a.read_with(cx_a, |worktree, _| {
3073 assert_eq!(
3074 worktree
3075 .paths()
3076 .map(|p| p.to_string_lossy())
3077 .collect::<Vec<_>>(),
3078 ["a.txt", "b.txt", "d.txt", "f.txt"]
3079 );
3080 });
3081 worktree_b.read_with(cx_b, |worktree, _| {
3082 assert_eq!(
3083 worktree
3084 .paths()
3085 .map(|p| p.to_string_lossy())
3086 .collect::<Vec<_>>(),
3087 ["a.txt", "b.txt", "d.txt", "f.txt"]
3088 );
3089 });
3090
3091 project_b
3092 .update(cx_b, |project, cx| {
3093 project.delete_entry(entry.id, cx).unwrap()
3094 })
3095 .await
3096 .unwrap();
3097 worktree_a.read_with(cx_a, |worktree, _| {
3098 assert_eq!(
3099 worktree
3100 .paths()
3101 .map(|p| p.to_string_lossy())
3102 .collect::<Vec<_>>(),
3103 ["a.txt", "b.txt", "f.txt"]
3104 );
3105 });
3106 worktree_b.read_with(cx_b, |worktree, _| {
3107 assert_eq!(
3108 worktree
3109 .paths()
3110 .map(|p| p.to_string_lossy())
3111 .collect::<Vec<_>>(),
3112 ["a.txt", "b.txt", "f.txt"]
3113 );
3114 });
3115}
3116
3117#[gpui::test(iterations = 10)]
3118async fn test_local_settings(
3119 deterministic: Arc<Deterministic>,
3120 cx_a: &mut TestAppContext,
3121 cx_b: &mut TestAppContext,
3122) {
3123 deterministic.forbid_parking();
3124 let mut server = TestServer::start(&deterministic).await;
3125 let client_a = server.create_client(cx_a, "user_a").await;
3126 let client_b = server.create_client(cx_b, "user_b").await;
3127 server
3128 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3129 .await;
3130 let active_call_a = cx_a.read(ActiveCall::global);
3131
3132 // As client A, open a project that contains some local settings files
3133 client_a
3134 .fs
3135 .insert_tree(
3136 "/dir",
3137 json!({
3138 ".zed": {
3139 "settings.json": r#"{ "tab_size": 2 }"#
3140 },
3141 "a": {
3142 ".zed": {
3143 "settings.json": r#"{ "tab_size": 8 }"#
3144 },
3145 "a.txt": "a-contents",
3146 },
3147 "b": {
3148 "b.txt": "b-contents",
3149 }
3150 }),
3151 )
3152 .await;
3153 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
3154 let project_id = active_call_a
3155 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3156 .await
3157 .unwrap();
3158
3159 // As client B, join that project and observe the local settings.
3160 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3161 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3162 deterministic.run_until_parked();
3163 cx_b.read(|cx| {
3164 let store = cx.global::<SettingsStore>();
3165 assert_eq!(
3166 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3167 &[
3168 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3169 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3170 ]
3171 )
3172 });
3173
3174 // As client A, update a settings file. As Client B, see the changed settings.
3175 client_a
3176 .fs
3177 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3178 .await;
3179 deterministic.run_until_parked();
3180 cx_b.read(|cx| {
3181 let store = cx.global::<SettingsStore>();
3182 assert_eq!(
3183 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3184 &[
3185 (Path::new("").into(), r#"{}"#.to_string()),
3186 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3187 ]
3188 )
3189 });
3190
3191 // As client A, create and remove some settings files. As client B, see the changed settings.
3192 client_a
3193 .fs
3194 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3195 .await
3196 .unwrap();
3197 client_a
3198 .fs
3199 .create_dir("/dir/b/.zed".as_ref())
3200 .await
3201 .unwrap();
3202 client_a
3203 .fs
3204 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3205 .await;
3206 deterministic.run_until_parked();
3207 cx_b.read(|cx| {
3208 let store = cx.global::<SettingsStore>();
3209 assert_eq!(
3210 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3211 &[
3212 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3213 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3214 ]
3215 )
3216 });
3217
3218 // As client B, disconnect.
3219 server.forbid_connections();
3220 server.disconnect_client(client_b.peer_id().unwrap());
3221
3222 // As client A, change and remove settings files while client B is disconnected.
3223 client_a
3224 .fs
3225 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3226 .await;
3227 client_a
3228 .fs
3229 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3230 .await
3231 .unwrap();
3232 deterministic.run_until_parked();
3233
3234 // As client B, reconnect and see the changed settings.
3235 server.allow_connections();
3236 deterministic.advance_clock(RECEIVE_TIMEOUT);
3237 cx_b.read(|cx| {
3238 let store = cx.global::<SettingsStore>();
3239 assert_eq!(
3240 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3241 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3242 )
3243 });
3244}
3245
3246#[gpui::test(iterations = 10)]
3247async fn test_buffer_conflict_after_save(
3248 deterministic: Arc<Deterministic>,
3249 cx_a: &mut TestAppContext,
3250 cx_b: &mut TestAppContext,
3251) {
3252 deterministic.forbid_parking();
3253 let mut server = TestServer::start(&deterministic).await;
3254 let client_a = server.create_client(cx_a, "user_a").await;
3255 let client_b = server.create_client(cx_b, "user_b").await;
3256 server
3257 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3258 .await;
3259 let active_call_a = cx_a.read(ActiveCall::global);
3260
3261 client_a
3262 .fs
3263 .insert_tree(
3264 "/dir",
3265 json!({
3266 "a.txt": "a-contents",
3267 }),
3268 )
3269 .await;
3270 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3271 let project_id = active_call_a
3272 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3273 .await
3274 .unwrap();
3275 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3276
3277 // Open a buffer as client B
3278 let buffer_b = project_b
3279 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3280 .await
3281 .unwrap();
3282
3283 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3284 buffer_b.read_with(cx_b, |buf, _| {
3285 assert!(buf.is_dirty());
3286 assert!(!buf.has_conflict());
3287 });
3288
3289 project_b
3290 .update(cx_b, |project, cx| {
3291 project.save_buffer(buffer_b.clone(), cx)
3292 })
3293 .await
3294 .unwrap();
3295 cx_a.foreground().forbid_parking();
3296 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3297 buffer_b.read_with(cx_b, |buf, _| {
3298 assert!(!buf.has_conflict());
3299 });
3300
3301 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3302 buffer_b.read_with(cx_b, |buf, _| {
3303 assert!(buf.is_dirty());
3304 assert!(!buf.has_conflict());
3305 });
3306}
3307
3308#[gpui::test(iterations = 10)]
3309async fn test_buffer_reloading(
3310 deterministic: Arc<Deterministic>,
3311 cx_a: &mut TestAppContext,
3312 cx_b: &mut TestAppContext,
3313) {
3314 deterministic.forbid_parking();
3315 let mut server = TestServer::start(&deterministic).await;
3316 let client_a = server.create_client(cx_a, "user_a").await;
3317 let client_b = server.create_client(cx_b, "user_b").await;
3318 server
3319 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3320 .await;
3321 let active_call_a = cx_a.read(ActiveCall::global);
3322
3323 client_a
3324 .fs
3325 .insert_tree(
3326 "/dir",
3327 json!({
3328 "a.txt": "a\nb\nc",
3329 }),
3330 )
3331 .await;
3332 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3333 let project_id = active_call_a
3334 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3335 .await
3336 .unwrap();
3337 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3338
3339 // Open a buffer as client B
3340 let buffer_b = project_b
3341 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3342 .await
3343 .unwrap();
3344 buffer_b.read_with(cx_b, |buf, _| {
3345 assert!(!buf.is_dirty());
3346 assert!(!buf.has_conflict());
3347 assert_eq!(buf.line_ending(), LineEnding::Unix);
3348 });
3349
3350 let new_contents = Rope::from("d\ne\nf");
3351 client_a
3352 .fs
3353 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3354 .await
3355 .unwrap();
3356 cx_a.foreground().run_until_parked();
3357 buffer_b.read_with(cx_b, |buf, _| {
3358 assert_eq!(buf.text(), new_contents.to_string());
3359 assert!(!buf.is_dirty());
3360 assert!(!buf.has_conflict());
3361 assert_eq!(buf.line_ending(), LineEnding::Windows);
3362 });
3363}
3364
3365#[gpui::test(iterations = 10)]
3366async fn test_editing_while_guest_opens_buffer(
3367 deterministic: Arc<Deterministic>,
3368 cx_a: &mut TestAppContext,
3369 cx_b: &mut TestAppContext,
3370) {
3371 deterministic.forbid_parking();
3372 let mut server = TestServer::start(&deterministic).await;
3373 let client_a = server.create_client(cx_a, "user_a").await;
3374 let client_b = server.create_client(cx_b, "user_b").await;
3375 server
3376 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3377 .await;
3378 let active_call_a = cx_a.read(ActiveCall::global);
3379
3380 client_a
3381 .fs
3382 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3383 .await;
3384 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3385 let project_id = active_call_a
3386 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3387 .await
3388 .unwrap();
3389 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3390
3391 // Open a buffer as client A
3392 let buffer_a = project_a
3393 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3394 .await
3395 .unwrap();
3396
3397 // Start opening the same buffer as client B
3398 let buffer_b = cx_b
3399 .background()
3400 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3401
3402 // Edit the buffer as client A while client B is still opening it.
3403 cx_b.background().simulate_random_delay().await;
3404 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3405 cx_b.background().simulate_random_delay().await;
3406 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3407
3408 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3409 let buffer_b = buffer_b.await.unwrap();
3410 cx_a.foreground().run_until_parked();
3411 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3412}
3413
3414#[gpui::test]
3415async fn test_newline_above_or_below_does_not_move_guest_cursor(
3416 deterministic: Arc<Deterministic>,
3417 cx_a: &mut TestAppContext,
3418 cx_b: &mut TestAppContext,
3419) {
3420 deterministic.forbid_parking();
3421 let mut server = TestServer::start(&deterministic).await;
3422 let client_a = server.create_client(cx_a, "user_a").await;
3423 let client_b = server.create_client(cx_b, "user_b").await;
3424 server
3425 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3426 .await;
3427 let active_call_a = cx_a.read(ActiveCall::global);
3428
3429 client_a
3430 .fs
3431 .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
3432 .await;
3433 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3434 let project_id = active_call_a
3435 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3436 .await
3437 .unwrap();
3438
3439 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3440
3441 // Open a buffer as client A
3442 let buffer_a = project_a
3443 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3444 .await
3445 .unwrap();
3446 let (window_a, _) = cx_a.add_window(|_| EmptyView);
3447 let editor_a = cx_a.add_view(window_a, |cx| {
3448 Editor::for_buffer(buffer_a, Some(project_a), cx)
3449 });
3450 let mut editor_cx_a = EditorTestContext {
3451 cx: cx_a,
3452 window_id: window_a,
3453 editor: editor_a,
3454 };
3455
3456 // Open a buffer as client B
3457 let buffer_b = project_b
3458 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3459 .await
3460 .unwrap();
3461 let (window_b, _) = cx_b.add_window(|_| EmptyView);
3462 let editor_b = cx_b.add_view(window_b, |cx| {
3463 Editor::for_buffer(buffer_b, Some(project_b), cx)
3464 });
3465 let mut editor_cx_b = EditorTestContext {
3466 cx: cx_b,
3467 window_id: window_b,
3468 editor: editor_b,
3469 };
3470
3471 // Test newline above
3472 editor_cx_a.set_selections_state(indoc! {"
3473 Some textˇ
3474 "});
3475 editor_cx_b.set_selections_state(indoc! {"
3476 Some textˇ
3477 "});
3478 editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
3479 deterministic.run_until_parked();
3480 editor_cx_a.assert_editor_state(indoc! {"
3481 ˇ
3482 Some text
3483 "});
3484 editor_cx_b.assert_editor_state(indoc! {"
3485
3486 Some textˇ
3487 "});
3488
3489 // Test newline below
3490 editor_cx_a.set_selections_state(indoc! {"
3491
3492 Some textˇ
3493 "});
3494 editor_cx_b.set_selections_state(indoc! {"
3495
3496 Some textˇ
3497 "});
3498 editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
3499 deterministic.run_until_parked();
3500 editor_cx_a.assert_editor_state(indoc! {"
3501
3502 Some text
3503 ˇ
3504 "});
3505 editor_cx_b.assert_editor_state(indoc! {"
3506
3507 Some textˇ
3508
3509 "});
3510}
3511
3512#[gpui::test(iterations = 10)]
3513async fn test_leaving_worktree_while_opening_buffer(
3514 deterministic: Arc<Deterministic>,
3515 cx_a: &mut TestAppContext,
3516 cx_b: &mut TestAppContext,
3517) {
3518 deterministic.forbid_parking();
3519 let mut server = TestServer::start(&deterministic).await;
3520 let client_a = server.create_client(cx_a, "user_a").await;
3521 let client_b = server.create_client(cx_b, "user_b").await;
3522 server
3523 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3524 .await;
3525 let active_call_a = cx_a.read(ActiveCall::global);
3526
3527 client_a
3528 .fs
3529 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3530 .await;
3531 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3532 let project_id = active_call_a
3533 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3534 .await
3535 .unwrap();
3536 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3537
3538 // See that a guest has joined as client A.
3539 cx_a.foreground().run_until_parked();
3540 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3541
3542 // Begin opening a buffer as client B, but leave the project before the open completes.
3543 let buffer_b = cx_b
3544 .background()
3545 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3546 cx_b.update(|_| drop(project_b));
3547 drop(buffer_b);
3548
3549 // See that the guest has left.
3550 cx_a.foreground().run_until_parked();
3551 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3552}
3553
3554#[gpui::test(iterations = 10)]
3555async fn test_canceling_buffer_opening(
3556 deterministic: Arc<Deterministic>,
3557 cx_a: &mut TestAppContext,
3558 cx_b: &mut TestAppContext,
3559) {
3560 deterministic.forbid_parking();
3561
3562 let mut server = TestServer::start(&deterministic).await;
3563 let client_a = server.create_client(cx_a, "user_a").await;
3564 let client_b = server.create_client(cx_b, "user_b").await;
3565 server
3566 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3567 .await;
3568 let active_call_a = cx_a.read(ActiveCall::global);
3569
3570 client_a
3571 .fs
3572 .insert_tree(
3573 "/dir",
3574 json!({
3575 "a.txt": "abc",
3576 }),
3577 )
3578 .await;
3579 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3580 let project_id = active_call_a
3581 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3582 .await
3583 .unwrap();
3584 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3585
3586 let buffer_a = project_a
3587 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3588 .await
3589 .unwrap();
3590
3591 // Open a buffer as client B but cancel after a random amount of time.
3592 let buffer_b = project_b.update(cx_b, |p, cx| {
3593 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3594 });
3595 deterministic.simulate_random_delay().await;
3596 drop(buffer_b);
3597
3598 // Try opening the same buffer again as client B, and ensure we can
3599 // still do it despite the cancellation above.
3600 let buffer_b = project_b
3601 .update(cx_b, |p, cx| {
3602 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3603 })
3604 .await
3605 .unwrap();
3606 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3607}
3608
3609#[gpui::test(iterations = 10)]
3610async fn test_leaving_project(
3611 deterministic: Arc<Deterministic>,
3612 cx_a: &mut TestAppContext,
3613 cx_b: &mut TestAppContext,
3614 cx_c: &mut TestAppContext,
3615) {
3616 deterministic.forbid_parking();
3617 let mut server = TestServer::start(&deterministic).await;
3618 let client_a = server.create_client(cx_a, "user_a").await;
3619 let client_b = server.create_client(cx_b, "user_b").await;
3620 let client_c = server.create_client(cx_c, "user_c").await;
3621 server
3622 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3623 .await;
3624 let active_call_a = cx_a.read(ActiveCall::global);
3625
3626 client_a
3627 .fs
3628 .insert_tree(
3629 "/a",
3630 json!({
3631 "a.txt": "a-contents",
3632 "b.txt": "b-contents",
3633 }),
3634 )
3635 .await;
3636 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3637 let project_id = active_call_a
3638 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3639 .await
3640 .unwrap();
3641 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3642 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3643
3644 // Client A sees that a guest has joined.
3645 deterministic.run_until_parked();
3646 project_a.read_with(cx_a, |project, _| {
3647 assert_eq!(project.collaborators().len(), 2);
3648 });
3649 project_b1.read_with(cx_b, |project, _| {
3650 assert_eq!(project.collaborators().len(), 2);
3651 });
3652 project_c.read_with(cx_c, |project, _| {
3653 assert_eq!(project.collaborators().len(), 2);
3654 });
3655
3656 // Client B opens a buffer.
3657 let buffer_b1 = project_b1
3658 .update(cx_b, |project, cx| {
3659 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3660 project.open_buffer((worktree_id, "a.txt"), cx)
3661 })
3662 .await
3663 .unwrap();
3664 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3665
3666 // Drop client B's project and ensure client A and client C observe client B leaving.
3667 cx_b.update(|_| drop(project_b1));
3668 deterministic.run_until_parked();
3669 project_a.read_with(cx_a, |project, _| {
3670 assert_eq!(project.collaborators().len(), 1);
3671 });
3672 project_c.read_with(cx_c, |project, _| {
3673 assert_eq!(project.collaborators().len(), 1);
3674 });
3675
3676 // Client B re-joins the project and can open buffers as before.
3677 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3678 deterministic.run_until_parked();
3679 project_a.read_with(cx_a, |project, _| {
3680 assert_eq!(project.collaborators().len(), 2);
3681 });
3682 project_b2.read_with(cx_b, |project, _| {
3683 assert_eq!(project.collaborators().len(), 2);
3684 });
3685 project_c.read_with(cx_c, |project, _| {
3686 assert_eq!(project.collaborators().len(), 2);
3687 });
3688
3689 let buffer_b2 = project_b2
3690 .update(cx_b, |project, cx| {
3691 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3692 project.open_buffer((worktree_id, "a.txt"), cx)
3693 })
3694 .await
3695 .unwrap();
3696 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3697
3698 // Drop client B's connection and ensure client A and client C observe client B leaving.
3699 client_b.disconnect(&cx_b.to_async());
3700 deterministic.advance_clock(RECONNECT_TIMEOUT);
3701 project_a.read_with(cx_a, |project, _| {
3702 assert_eq!(project.collaborators().len(), 1);
3703 });
3704 project_b2.read_with(cx_b, |project, _| {
3705 assert!(project.is_read_only());
3706 });
3707 project_c.read_with(cx_c, |project, _| {
3708 assert_eq!(project.collaborators().len(), 1);
3709 });
3710
3711 // Client B can't join the project, unless they re-join the room.
3712 cx_b.spawn(|cx| {
3713 Project::remote(
3714 project_id,
3715 client_b.client.clone(),
3716 client_b.user_store.clone(),
3717 client_b.language_registry.clone(),
3718 FakeFs::new(cx.background()),
3719 cx,
3720 )
3721 })
3722 .await
3723 .unwrap_err();
3724
3725 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3726 client_c.wait_for_current_user(cx_c).await;
3727 server.forbid_connections();
3728 server.disconnect_client(client_c.peer_id().unwrap());
3729 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3730 deterministic.run_until_parked();
3731 project_a.read_with(cx_a, |project, _| {
3732 assert_eq!(project.collaborators().len(), 0);
3733 });
3734 project_b2.read_with(cx_b, |project, _| {
3735 assert!(project.is_read_only());
3736 });
3737 project_c.read_with(cx_c, |project, _| {
3738 assert!(project.is_read_only());
3739 });
3740}
3741
3742#[gpui::test(iterations = 10)]
3743async fn test_collaborating_with_diagnostics(
3744 deterministic: Arc<Deterministic>,
3745 cx_a: &mut TestAppContext,
3746 cx_b: &mut TestAppContext,
3747 cx_c: &mut TestAppContext,
3748) {
3749 deterministic.forbid_parking();
3750 let mut server = TestServer::start(&deterministic).await;
3751 let client_a = server.create_client(cx_a, "user_a").await;
3752 let client_b = server.create_client(cx_b, "user_b").await;
3753 let client_c = server.create_client(cx_c, "user_c").await;
3754 server
3755 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3756 .await;
3757 let active_call_a = cx_a.read(ActiveCall::global);
3758
3759 // Set up a fake language server.
3760 let mut language = Language::new(
3761 LanguageConfig {
3762 name: "Rust".into(),
3763 path_suffixes: vec!["rs".to_string()],
3764 ..Default::default()
3765 },
3766 Some(tree_sitter_rust::language()),
3767 );
3768 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3769 client_a.language_registry.add(Arc::new(language));
3770
3771 // Share a project as client A
3772 client_a
3773 .fs
3774 .insert_tree(
3775 "/a",
3776 json!({
3777 "a.rs": "let one = two",
3778 "other.rs": "",
3779 }),
3780 )
3781 .await;
3782 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3783
3784 // Cause the language server to start.
3785 let _buffer = project_a
3786 .update(cx_a, |project, cx| {
3787 project.open_buffer(
3788 ProjectPath {
3789 worktree_id,
3790 path: Path::new("other.rs").into(),
3791 },
3792 cx,
3793 )
3794 })
3795 .await
3796 .unwrap();
3797
3798 // Simulate a language server reporting errors for a file.
3799 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3800 fake_language_server
3801 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3802 .await;
3803 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3804 lsp::PublishDiagnosticsParams {
3805 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3806 version: None,
3807 diagnostics: vec![lsp::Diagnostic {
3808 severity: Some(lsp::DiagnosticSeverity::WARNING),
3809 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3810 message: "message 0".to_string(),
3811 ..Default::default()
3812 }],
3813 },
3814 );
3815
3816 // Client A shares the project and, simultaneously, the language server
3817 // publishes a diagnostic. This is done to ensure that the server always
3818 // observes the latest diagnostics for a worktree.
3819 let project_id = active_call_a
3820 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3821 .await
3822 .unwrap();
3823 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3824 lsp::PublishDiagnosticsParams {
3825 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3826 version: None,
3827 diagnostics: vec![lsp::Diagnostic {
3828 severity: Some(lsp::DiagnosticSeverity::ERROR),
3829 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3830 message: "message 1".to_string(),
3831 ..Default::default()
3832 }],
3833 },
3834 );
3835
3836 // Join the worktree as client B.
3837 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3838
3839 // Wait for server to see the diagnostics update.
3840 deterministic.run_until_parked();
3841
3842 // Ensure client B observes the new diagnostics.
3843 project_b.read_with(cx_b, |project, cx| {
3844 assert_eq!(
3845 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3846 &[(
3847 ProjectPath {
3848 worktree_id,
3849 path: Arc::from(Path::new("a.rs")),
3850 },
3851 LanguageServerId(0),
3852 DiagnosticSummary {
3853 error_count: 1,
3854 warning_count: 0,
3855 ..Default::default()
3856 },
3857 )]
3858 )
3859 });
3860
3861 // Join project as client C and observe the diagnostics.
3862 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3863 let project_c_diagnostic_summaries =
3864 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3865 project.diagnostic_summaries(cx).collect::<Vec<_>>()
3866 })));
3867 project_c.update(cx_c, |_, cx| {
3868 let summaries = project_c_diagnostic_summaries.clone();
3869 cx.subscribe(&project_c, {
3870 move |p, _, event, cx| {
3871 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3872 *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
3873 }
3874 }
3875 })
3876 .detach();
3877 });
3878
3879 deterministic.run_until_parked();
3880 assert_eq!(
3881 project_c_diagnostic_summaries.borrow().as_slice(),
3882 &[(
3883 ProjectPath {
3884 worktree_id,
3885 path: Arc::from(Path::new("a.rs")),
3886 },
3887 LanguageServerId(0),
3888 DiagnosticSummary {
3889 error_count: 1,
3890 warning_count: 0,
3891 ..Default::default()
3892 },
3893 )]
3894 );
3895
3896 // Simulate a language server reporting more errors for a file.
3897 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3898 lsp::PublishDiagnosticsParams {
3899 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3900 version: None,
3901 diagnostics: vec![
3902 lsp::Diagnostic {
3903 severity: Some(lsp::DiagnosticSeverity::ERROR),
3904 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3905 message: "message 1".to_string(),
3906 ..Default::default()
3907 },
3908 lsp::Diagnostic {
3909 severity: Some(lsp::DiagnosticSeverity::WARNING),
3910 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3911 message: "message 2".to_string(),
3912 ..Default::default()
3913 },
3914 ],
3915 },
3916 );
3917
3918 // Clients B and C get the updated summaries
3919 deterministic.run_until_parked();
3920 project_b.read_with(cx_b, |project, cx| {
3921 assert_eq!(
3922 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3923 [(
3924 ProjectPath {
3925 worktree_id,
3926 path: Arc::from(Path::new("a.rs")),
3927 },
3928 LanguageServerId(0),
3929 DiagnosticSummary {
3930 error_count: 1,
3931 warning_count: 1,
3932 },
3933 )]
3934 );
3935 });
3936 project_c.read_with(cx_c, |project, cx| {
3937 assert_eq!(
3938 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3939 [(
3940 ProjectPath {
3941 worktree_id,
3942 path: Arc::from(Path::new("a.rs")),
3943 },
3944 LanguageServerId(0),
3945 DiagnosticSummary {
3946 error_count: 1,
3947 warning_count: 1,
3948 },
3949 )]
3950 );
3951 });
3952
3953 // Open the file with the errors on client B. They should be present.
3954 let buffer_b = cx_b
3955 .background()
3956 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3957 .await
3958 .unwrap();
3959
3960 buffer_b.read_with(cx_b, |buffer, _| {
3961 assert_eq!(
3962 buffer
3963 .snapshot()
3964 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3965 .collect::<Vec<_>>(),
3966 &[
3967 DiagnosticEntry {
3968 range: Point::new(0, 4)..Point::new(0, 7),
3969 diagnostic: Diagnostic {
3970 group_id: 2,
3971 message: "message 1".to_string(),
3972 severity: lsp::DiagnosticSeverity::ERROR,
3973 is_primary: true,
3974 ..Default::default()
3975 }
3976 },
3977 DiagnosticEntry {
3978 range: Point::new(0, 10)..Point::new(0, 13),
3979 diagnostic: Diagnostic {
3980 group_id: 3,
3981 severity: lsp::DiagnosticSeverity::WARNING,
3982 message: "message 2".to_string(),
3983 is_primary: true,
3984 ..Default::default()
3985 }
3986 }
3987 ]
3988 );
3989 });
3990
3991 // Simulate a language server reporting no errors for a file.
3992 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3993 lsp::PublishDiagnosticsParams {
3994 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3995 version: None,
3996 diagnostics: vec![],
3997 },
3998 );
3999 deterministic.run_until_parked();
4000 project_a.read_with(cx_a, |project, cx| {
4001 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
4002 });
4003 project_b.read_with(cx_b, |project, cx| {
4004 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
4005 });
4006 project_c.read_with(cx_c, |project, cx| {
4007 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
4008 });
4009}
4010
4011#[gpui::test(iterations = 10)]
4012async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4013 deterministic: Arc<Deterministic>,
4014 cx_a: &mut TestAppContext,
4015 cx_b: &mut TestAppContext,
4016) {
4017 deterministic.forbid_parking();
4018 let mut server = TestServer::start(&deterministic).await;
4019 let client_a = server.create_client(cx_a, "user_a").await;
4020 let client_b = server.create_client(cx_b, "user_b").await;
4021 server
4022 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4023 .await;
4024
4025 // Set up a fake language server.
4026 let mut language = Language::new(
4027 LanguageConfig {
4028 name: "Rust".into(),
4029 path_suffixes: vec!["rs".to_string()],
4030 ..Default::default()
4031 },
4032 Some(tree_sitter_rust::language()),
4033 );
4034 let mut fake_language_servers = language
4035 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4036 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4037 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4038 ..Default::default()
4039 }))
4040 .await;
4041 client_a.language_registry.add(Arc::new(language));
4042
4043 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4044 client_a
4045 .fs
4046 .insert_tree(
4047 "/test",
4048 json!({
4049 "one.rs": "const ONE: usize = 1;",
4050 "two.rs": "const TWO: usize = 2;",
4051 "three.rs": "const THREE: usize = 3;",
4052 "four.rs": "const FOUR: usize = 3;",
4053 "five.rs": "const FIVE: usize = 3;",
4054 }),
4055 )
4056 .await;
4057
4058 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
4059
4060 // Share a project as client A
4061 let active_call_a = cx_a.read(ActiveCall::global);
4062 let project_id = active_call_a
4063 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4064 .await
4065 .unwrap();
4066
4067 // Join the project as client B and open all three files.
4068 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4069 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4070 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
4071 }))
4072 .await
4073 .unwrap();
4074
4075 // Simulate a language server reporting errors for a file.
4076 let fake_language_server = fake_language_servers.next().await.unwrap();
4077 fake_language_server
4078 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4079 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4080 })
4081 .await
4082 .unwrap();
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::Begin(
4086 lsp::WorkDoneProgressBegin {
4087 title: "Progress Began".into(),
4088 ..Default::default()
4089 },
4090 )),
4091 });
4092 for file_name in file_names {
4093 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4094 lsp::PublishDiagnosticsParams {
4095 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
4096 version: None,
4097 diagnostics: vec![lsp::Diagnostic {
4098 severity: Some(lsp::DiagnosticSeverity::WARNING),
4099 source: Some("the-disk-based-diagnostics-source".into()),
4100 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4101 message: "message one".to_string(),
4102 ..Default::default()
4103 }],
4104 },
4105 );
4106 }
4107 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4108 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4109 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4110 lsp::WorkDoneProgressEnd { message: None },
4111 )),
4112 });
4113
4114 // When the "disk base diagnostics finished" message is received, the buffers'
4115 // diagnostics are expected to be present.
4116 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4117 project_b.update(cx_b, {
4118 let project_b = project_b.clone();
4119 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4120 move |_, cx| {
4121 cx.subscribe(&project_b, move |_, _, event, cx| {
4122 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4123 disk_based_diagnostics_finished.store(true, SeqCst);
4124 for buffer in &guest_buffers {
4125 assert_eq!(
4126 buffer
4127 .read(cx)
4128 .snapshot()
4129 .diagnostics_in_range::<_, usize>(0..5, false)
4130 .count(),
4131 1,
4132 "expected a diagnostic for buffer {:?}",
4133 buffer.read(cx).file().unwrap().path(),
4134 );
4135 }
4136 }
4137 })
4138 .detach();
4139 }
4140 });
4141
4142 deterministic.run_until_parked();
4143 assert!(disk_based_diagnostics_finished.load(SeqCst));
4144}
4145
4146#[gpui::test(iterations = 10)]
4147async fn test_collaborating_with_completion(
4148 deterministic: Arc<Deterministic>,
4149 cx_a: &mut TestAppContext,
4150 cx_b: &mut TestAppContext,
4151) {
4152 deterministic.forbid_parking();
4153 let mut server = TestServer::start(&deterministic).await;
4154 let client_a = server.create_client(cx_a, "user_a").await;
4155 let client_b = server.create_client(cx_b, "user_b").await;
4156 server
4157 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4158 .await;
4159 let active_call_a = cx_a.read(ActiveCall::global);
4160
4161 // Set up a fake language server.
4162 let mut language = Language::new(
4163 LanguageConfig {
4164 name: "Rust".into(),
4165 path_suffixes: vec!["rs".to_string()],
4166 ..Default::default()
4167 },
4168 Some(tree_sitter_rust::language()),
4169 );
4170 let mut fake_language_servers = language
4171 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4172 capabilities: lsp::ServerCapabilities {
4173 completion_provider: Some(lsp::CompletionOptions {
4174 trigger_characters: Some(vec![".".to_string()]),
4175 ..Default::default()
4176 }),
4177 ..Default::default()
4178 },
4179 ..Default::default()
4180 }))
4181 .await;
4182 client_a.language_registry.add(Arc::new(language));
4183
4184 client_a
4185 .fs
4186 .insert_tree(
4187 "/a",
4188 json!({
4189 "main.rs": "fn main() { a }",
4190 "other.rs": "",
4191 }),
4192 )
4193 .await;
4194 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4195 let project_id = active_call_a
4196 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4197 .await
4198 .unwrap();
4199 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4200
4201 // Open a file in an editor as the guest.
4202 let buffer_b = project_b
4203 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
4204 .await
4205 .unwrap();
4206 let (window_b, _) = cx_b.add_window(|_| EmptyView);
4207 let editor_b = cx_b.add_view(window_b, |cx| {
4208 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
4209 });
4210
4211 let fake_language_server = fake_language_servers.next().await.unwrap();
4212 cx_a.foreground().run_until_parked();
4213 buffer_b.read_with(cx_b, |buffer, _| {
4214 assert!(!buffer.completion_triggers().is_empty())
4215 });
4216
4217 // Type a completion trigger character as the guest.
4218 editor_b.update(cx_b, |editor, cx| {
4219 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
4220 editor.handle_input(".", cx);
4221 cx.focus(&editor_b);
4222 });
4223
4224 // Receive a completion request as the host's language server.
4225 // Return some completions from the host's language server.
4226 cx_a.foreground().start_waiting();
4227 fake_language_server
4228 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
4229 assert_eq!(
4230 params.text_document_position.text_document.uri,
4231 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4232 );
4233 assert_eq!(
4234 params.text_document_position.position,
4235 lsp::Position::new(0, 14),
4236 );
4237
4238 Ok(Some(lsp::CompletionResponse::Array(vec![
4239 lsp::CompletionItem {
4240 label: "first_method(…)".into(),
4241 detail: Some("fn(&mut self, B) -> C".into()),
4242 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4243 new_text: "first_method($1)".to_string(),
4244 range: lsp::Range::new(
4245 lsp::Position::new(0, 14),
4246 lsp::Position::new(0, 14),
4247 ),
4248 })),
4249 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4250 ..Default::default()
4251 },
4252 lsp::CompletionItem {
4253 label: "second_method(…)".into(),
4254 detail: Some("fn(&mut self, C) -> D<E>".into()),
4255 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4256 new_text: "second_method()".to_string(),
4257 range: lsp::Range::new(
4258 lsp::Position::new(0, 14),
4259 lsp::Position::new(0, 14),
4260 ),
4261 })),
4262 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4263 ..Default::default()
4264 },
4265 ])))
4266 })
4267 .next()
4268 .await
4269 .unwrap();
4270 cx_a.foreground().finish_waiting();
4271
4272 // Open the buffer on the host.
4273 let buffer_a = project_a
4274 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
4275 .await
4276 .unwrap();
4277 cx_a.foreground().run_until_parked();
4278 buffer_a.read_with(cx_a, |buffer, _| {
4279 assert_eq!(buffer.text(), "fn main() { a. }")
4280 });
4281
4282 // Confirm a completion on the guest.
4283 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
4284 editor_b.update(cx_b, |editor, cx| {
4285 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
4286 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
4287 });
4288
4289 // Return a resolved completion from the host's language server.
4290 // The resolved completion has an additional text edit.
4291 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
4292 |params, _| async move {
4293 assert_eq!(params.label, "first_method(…)");
4294 Ok(lsp::CompletionItem {
4295 label: "first_method(…)".into(),
4296 detail: Some("fn(&mut self, B) -> C".into()),
4297 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4298 new_text: "first_method($1)".to_string(),
4299 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
4300 })),
4301 additional_text_edits: Some(vec![lsp::TextEdit {
4302 new_text: "use d::SomeTrait;\n".to_string(),
4303 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4304 }]),
4305 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4306 ..Default::default()
4307 })
4308 },
4309 );
4310
4311 // The additional edit is applied.
4312 cx_a.foreground().run_until_parked();
4313 buffer_a.read_with(cx_a, |buffer, _| {
4314 assert_eq!(
4315 buffer.text(),
4316 "use d::SomeTrait;\nfn main() { a.first_method() }"
4317 );
4318 });
4319 buffer_b.read_with(cx_b, |buffer, _| {
4320 assert_eq!(
4321 buffer.text(),
4322 "use d::SomeTrait;\nfn main() { a.first_method() }"
4323 );
4324 });
4325}
4326
4327#[gpui::test(iterations = 10)]
4328async fn test_reloading_buffer_manually(
4329 deterministic: Arc<Deterministic>,
4330 cx_a: &mut TestAppContext,
4331 cx_b: &mut TestAppContext,
4332) {
4333 deterministic.forbid_parking();
4334 let mut server = TestServer::start(&deterministic).await;
4335 let client_a = server.create_client(cx_a, "user_a").await;
4336 let client_b = server.create_client(cx_b, "user_b").await;
4337 server
4338 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4339 .await;
4340 let active_call_a = cx_a.read(ActiveCall::global);
4341
4342 client_a
4343 .fs
4344 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4345 .await;
4346 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4347 let buffer_a = project_a
4348 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4349 .await
4350 .unwrap();
4351 let project_id = active_call_a
4352 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4353 .await
4354 .unwrap();
4355
4356 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4357
4358 let buffer_b = cx_b
4359 .background()
4360 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4361 .await
4362 .unwrap();
4363 buffer_b.update(cx_b, |buffer, cx| {
4364 buffer.edit([(4..7, "six")], None, cx);
4365 buffer.edit([(10..11, "6")], None, cx);
4366 assert_eq!(buffer.text(), "let six = 6;");
4367 assert!(buffer.is_dirty());
4368 assert!(!buffer.has_conflict());
4369 });
4370 cx_a.foreground().run_until_parked();
4371 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4372
4373 client_a
4374 .fs
4375 .save(
4376 "/a/a.rs".as_ref(),
4377 &Rope::from("let seven = 7;"),
4378 LineEnding::Unix,
4379 )
4380 .await
4381 .unwrap();
4382 cx_a.foreground().run_until_parked();
4383 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4384 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4385
4386 project_b
4387 .update(cx_b, |project, cx| {
4388 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4389 })
4390 .await
4391 .unwrap();
4392 buffer_a.read_with(cx_a, |buffer, _| {
4393 assert_eq!(buffer.text(), "let seven = 7;");
4394 assert!(!buffer.is_dirty());
4395 assert!(!buffer.has_conflict());
4396 });
4397 buffer_b.read_with(cx_b, |buffer, _| {
4398 assert_eq!(buffer.text(), "let seven = 7;");
4399 assert!(!buffer.is_dirty());
4400 assert!(!buffer.has_conflict());
4401 });
4402
4403 buffer_a.update(cx_a, |buffer, cx| {
4404 // Undoing on the host is a no-op when the reload was initiated by the guest.
4405 buffer.undo(cx);
4406 assert_eq!(buffer.text(), "let seven = 7;");
4407 assert!(!buffer.is_dirty());
4408 assert!(!buffer.has_conflict());
4409 });
4410 buffer_b.update(cx_b, |buffer, cx| {
4411 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4412 buffer.undo(cx);
4413 assert_eq!(buffer.text(), "let six = 6;");
4414 assert!(buffer.is_dirty());
4415 assert!(!buffer.has_conflict());
4416 });
4417}
4418
4419#[gpui::test(iterations = 10)]
4420async fn test_formatting_buffer(
4421 deterministic: Arc<Deterministic>,
4422 cx_a: &mut TestAppContext,
4423 cx_b: &mut TestAppContext,
4424) {
4425 use project::FormatTrigger;
4426
4427 let mut server = TestServer::start(&deterministic).await;
4428 let client_a = server.create_client(cx_a, "user_a").await;
4429 let client_b = server.create_client(cx_b, "user_b").await;
4430 server
4431 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4432 .await;
4433 let active_call_a = cx_a.read(ActiveCall::global);
4434
4435 // Set up a fake language server.
4436 let mut language = Language::new(
4437 LanguageConfig {
4438 name: "Rust".into(),
4439 path_suffixes: vec!["rs".to_string()],
4440 ..Default::default()
4441 },
4442 Some(tree_sitter_rust::language()),
4443 );
4444 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4445 client_a.language_registry.add(Arc::new(language));
4446
4447 // Here we insert a fake tree with a directory that exists on disk. This is needed
4448 // because later we'll invoke a command, which requires passing a working directory
4449 // that points to a valid location on disk.
4450 let directory = env::current_dir().unwrap();
4451 client_a
4452 .fs
4453 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4454 .await;
4455 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4456 let project_id = active_call_a
4457 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4458 .await
4459 .unwrap();
4460 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4461
4462 let buffer_b = cx_b
4463 .background()
4464 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4465 .await
4466 .unwrap();
4467
4468 let fake_language_server = fake_language_servers.next().await.unwrap();
4469 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4470 Ok(Some(vec![
4471 lsp::TextEdit {
4472 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4473 new_text: "h".to_string(),
4474 },
4475 lsp::TextEdit {
4476 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4477 new_text: "y".to_string(),
4478 },
4479 ]))
4480 });
4481
4482 project_b
4483 .update(cx_b, |project, cx| {
4484 project.format(
4485 HashSet::from_iter([buffer_b.clone()]),
4486 true,
4487 FormatTrigger::Save,
4488 cx,
4489 )
4490 })
4491 .await
4492 .unwrap();
4493
4494 // The edits from the LSP are applied, and a final newline is added.
4495 assert_eq!(
4496 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4497 "let honey = \"two\"\n"
4498 );
4499
4500 // Ensure buffer can be formatted using an external command. Notice how the
4501 // host's configuration is honored as opposed to using the guest's settings.
4502 cx_a.update(|cx| {
4503 cx.update_global(|store: &mut SettingsStore, cx| {
4504 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4505 file.defaults.formatter = Some(Formatter::External {
4506 command: "awk".into(),
4507 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4508 });
4509 });
4510 });
4511 });
4512 project_b
4513 .update(cx_b, |project, cx| {
4514 project.format(
4515 HashSet::from_iter([buffer_b.clone()]),
4516 true,
4517 FormatTrigger::Save,
4518 cx,
4519 )
4520 })
4521 .await
4522 .unwrap();
4523 assert_eq!(
4524 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4525 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4526 );
4527}
4528
4529#[gpui::test(iterations = 10)]
4530async fn test_definition(
4531 deterministic: Arc<Deterministic>,
4532 cx_a: &mut TestAppContext,
4533 cx_b: &mut TestAppContext,
4534) {
4535 deterministic.forbid_parking();
4536 let mut server = TestServer::start(&deterministic).await;
4537 let client_a = server.create_client(cx_a, "user_a").await;
4538 let client_b = server.create_client(cx_b, "user_b").await;
4539 server
4540 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4541 .await;
4542 let active_call_a = cx_a.read(ActiveCall::global);
4543
4544 // Set up a fake language server.
4545 let mut language = Language::new(
4546 LanguageConfig {
4547 name: "Rust".into(),
4548 path_suffixes: vec!["rs".to_string()],
4549 ..Default::default()
4550 },
4551 Some(tree_sitter_rust::language()),
4552 );
4553 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4554 client_a.language_registry.add(Arc::new(language));
4555
4556 client_a
4557 .fs
4558 .insert_tree(
4559 "/root",
4560 json!({
4561 "dir-1": {
4562 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4563 },
4564 "dir-2": {
4565 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4566 "c.rs": "type T2 = usize;",
4567 }
4568 }),
4569 )
4570 .await;
4571 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4572 let project_id = active_call_a
4573 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4574 .await
4575 .unwrap();
4576 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4577
4578 // Open the file on client B.
4579 let buffer_b = cx_b
4580 .background()
4581 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4582 .await
4583 .unwrap();
4584
4585 // Request the definition of a symbol as the guest.
4586 let fake_language_server = fake_language_servers.next().await.unwrap();
4587 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4588 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4589 lsp::Location::new(
4590 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4591 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4592 ),
4593 )))
4594 });
4595
4596 let definitions_1 = project_b
4597 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4598 .await
4599 .unwrap();
4600 cx_b.read(|cx| {
4601 assert_eq!(definitions_1.len(), 1);
4602 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4603 let target_buffer = definitions_1[0].target.buffer.read(cx);
4604 assert_eq!(
4605 target_buffer.text(),
4606 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4607 );
4608 assert_eq!(
4609 definitions_1[0].target.range.to_point(target_buffer),
4610 Point::new(0, 6)..Point::new(0, 9)
4611 );
4612 });
4613
4614 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4615 // the previous call to `definition`.
4616 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4617 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4618 lsp::Location::new(
4619 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4620 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4621 ),
4622 )))
4623 });
4624
4625 let definitions_2 = project_b
4626 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4627 .await
4628 .unwrap();
4629 cx_b.read(|cx| {
4630 assert_eq!(definitions_2.len(), 1);
4631 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4632 let target_buffer = definitions_2[0].target.buffer.read(cx);
4633 assert_eq!(
4634 target_buffer.text(),
4635 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4636 );
4637 assert_eq!(
4638 definitions_2[0].target.range.to_point(target_buffer),
4639 Point::new(1, 6)..Point::new(1, 11)
4640 );
4641 });
4642 assert_eq!(
4643 definitions_1[0].target.buffer,
4644 definitions_2[0].target.buffer
4645 );
4646
4647 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4648 |req, _| async move {
4649 assert_eq!(
4650 req.text_document_position_params.position,
4651 lsp::Position::new(0, 7)
4652 );
4653 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4654 lsp::Location::new(
4655 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4656 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4657 ),
4658 )))
4659 },
4660 );
4661
4662 let type_definitions = project_b
4663 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4664 .await
4665 .unwrap();
4666 cx_b.read(|cx| {
4667 assert_eq!(type_definitions.len(), 1);
4668 let target_buffer = type_definitions[0].target.buffer.read(cx);
4669 assert_eq!(target_buffer.text(), "type T2 = usize;");
4670 assert_eq!(
4671 type_definitions[0].target.range.to_point(target_buffer),
4672 Point::new(0, 5)..Point::new(0, 7)
4673 );
4674 });
4675}
4676
4677#[gpui::test(iterations = 10)]
4678async fn test_references(
4679 deterministic: Arc<Deterministic>,
4680 cx_a: &mut TestAppContext,
4681 cx_b: &mut TestAppContext,
4682) {
4683 deterministic.forbid_parking();
4684 let mut server = TestServer::start(&deterministic).await;
4685 let client_a = server.create_client(cx_a, "user_a").await;
4686 let client_b = server.create_client(cx_b, "user_b").await;
4687 server
4688 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4689 .await;
4690 let active_call_a = cx_a.read(ActiveCall::global);
4691
4692 // Set up a fake language server.
4693 let mut language = Language::new(
4694 LanguageConfig {
4695 name: "Rust".into(),
4696 path_suffixes: vec!["rs".to_string()],
4697 ..Default::default()
4698 },
4699 Some(tree_sitter_rust::language()),
4700 );
4701 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4702 client_a.language_registry.add(Arc::new(language));
4703
4704 client_a
4705 .fs
4706 .insert_tree(
4707 "/root",
4708 json!({
4709 "dir-1": {
4710 "one.rs": "const ONE: usize = 1;",
4711 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4712 },
4713 "dir-2": {
4714 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4715 }
4716 }),
4717 )
4718 .await;
4719 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4720 let project_id = active_call_a
4721 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4722 .await
4723 .unwrap();
4724 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4725
4726 // Open the file on client B.
4727 let buffer_b = cx_b
4728 .background()
4729 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4730 .await
4731 .unwrap();
4732
4733 // Request references to a symbol as the guest.
4734 let fake_language_server = fake_language_servers.next().await.unwrap();
4735 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4736 assert_eq!(
4737 params.text_document_position.text_document.uri.as_str(),
4738 "file:///root/dir-1/one.rs"
4739 );
4740 Ok(Some(vec![
4741 lsp::Location {
4742 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4743 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4744 },
4745 lsp::Location {
4746 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4747 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4748 },
4749 lsp::Location {
4750 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4751 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4752 },
4753 ]))
4754 });
4755
4756 let references = project_b
4757 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4758 .await
4759 .unwrap();
4760 cx_b.read(|cx| {
4761 assert_eq!(references.len(), 3);
4762 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4763
4764 let two_buffer = references[0].buffer.read(cx);
4765 let three_buffer = references[2].buffer.read(cx);
4766 assert_eq!(
4767 two_buffer.file().unwrap().path().as_ref(),
4768 Path::new("two.rs")
4769 );
4770 assert_eq!(references[1].buffer, references[0].buffer);
4771 assert_eq!(
4772 three_buffer.file().unwrap().full_path(cx),
4773 Path::new("/root/dir-2/three.rs")
4774 );
4775
4776 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4777 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4778 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4779 });
4780}
4781
4782#[gpui::test(iterations = 10)]
4783async fn test_project_search(
4784 deterministic: Arc<Deterministic>,
4785 cx_a: &mut TestAppContext,
4786 cx_b: &mut TestAppContext,
4787) {
4788 deterministic.forbid_parking();
4789 let mut server = TestServer::start(&deterministic).await;
4790 let client_a = server.create_client(cx_a, "user_a").await;
4791 let client_b = server.create_client(cx_b, "user_b").await;
4792 server
4793 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4794 .await;
4795 let active_call_a = cx_a.read(ActiveCall::global);
4796
4797 client_a
4798 .fs
4799 .insert_tree(
4800 "/root",
4801 json!({
4802 "dir-1": {
4803 "a": "hello world",
4804 "b": "goodnight moon",
4805 "c": "a world of goo",
4806 "d": "world champion of clown world",
4807 },
4808 "dir-2": {
4809 "e": "disney world is fun",
4810 }
4811 }),
4812 )
4813 .await;
4814 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4815 let (worktree_2, _) = project_a
4816 .update(cx_a, |p, cx| {
4817 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4818 })
4819 .await
4820 .unwrap();
4821 worktree_2
4822 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4823 .await;
4824 let project_id = active_call_a
4825 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4826 .await
4827 .unwrap();
4828
4829 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4830
4831 // Perform a search as the guest.
4832 let results = project_b
4833 .update(cx_b, |project, cx| {
4834 project.search(
4835 SearchQuery::text("world", false, false, Vec::new(), Vec::new()),
4836 cx,
4837 )
4838 })
4839 .await
4840 .unwrap();
4841
4842 let mut ranges_by_path = results
4843 .into_iter()
4844 .map(|(buffer, ranges)| {
4845 buffer.read_with(cx_b, |buffer, cx| {
4846 let path = buffer.file().unwrap().full_path(cx);
4847 let offset_ranges = ranges
4848 .into_iter()
4849 .map(|range| range.to_offset(buffer))
4850 .collect::<Vec<_>>();
4851 (path, offset_ranges)
4852 })
4853 })
4854 .collect::<Vec<_>>();
4855 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4856
4857 assert_eq!(
4858 ranges_by_path,
4859 &[
4860 (PathBuf::from("dir-1/a"), vec![6..11]),
4861 (PathBuf::from("dir-1/c"), vec![2..7]),
4862 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4863 (PathBuf::from("dir-2/e"), vec![7..12]),
4864 ]
4865 );
4866}
4867
4868#[gpui::test(iterations = 10)]
4869async fn test_document_highlights(
4870 deterministic: Arc<Deterministic>,
4871 cx_a: &mut TestAppContext,
4872 cx_b: &mut TestAppContext,
4873) {
4874 deterministic.forbid_parking();
4875 let mut server = TestServer::start(&deterministic).await;
4876 let client_a = server.create_client(cx_a, "user_a").await;
4877 let client_b = server.create_client(cx_b, "user_b").await;
4878 server
4879 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4880 .await;
4881 let active_call_a = cx_a.read(ActiveCall::global);
4882
4883 client_a
4884 .fs
4885 .insert_tree(
4886 "/root-1",
4887 json!({
4888 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4889 }),
4890 )
4891 .await;
4892
4893 // Set up a fake language server.
4894 let mut language = Language::new(
4895 LanguageConfig {
4896 name: "Rust".into(),
4897 path_suffixes: vec!["rs".to_string()],
4898 ..Default::default()
4899 },
4900 Some(tree_sitter_rust::language()),
4901 );
4902 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4903 client_a.language_registry.add(Arc::new(language));
4904
4905 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4906 let project_id = active_call_a
4907 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4908 .await
4909 .unwrap();
4910 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4911
4912 // Open the file on client B.
4913 let buffer_b = cx_b
4914 .background()
4915 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4916 .await
4917 .unwrap();
4918
4919 // Request document highlights as the guest.
4920 let fake_language_server = fake_language_servers.next().await.unwrap();
4921 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4922 |params, _| async move {
4923 assert_eq!(
4924 params
4925 .text_document_position_params
4926 .text_document
4927 .uri
4928 .as_str(),
4929 "file:///root-1/main.rs"
4930 );
4931 assert_eq!(
4932 params.text_document_position_params.position,
4933 lsp::Position::new(0, 34)
4934 );
4935 Ok(Some(vec![
4936 lsp::DocumentHighlight {
4937 kind: Some(lsp::DocumentHighlightKind::WRITE),
4938 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4939 },
4940 lsp::DocumentHighlight {
4941 kind: Some(lsp::DocumentHighlightKind::READ),
4942 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4943 },
4944 lsp::DocumentHighlight {
4945 kind: Some(lsp::DocumentHighlightKind::READ),
4946 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4947 },
4948 ]))
4949 },
4950 );
4951
4952 let highlights = project_b
4953 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4954 .await
4955 .unwrap();
4956 buffer_b.read_with(cx_b, |buffer, _| {
4957 let snapshot = buffer.snapshot();
4958
4959 let highlights = highlights
4960 .into_iter()
4961 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4962 .collect::<Vec<_>>();
4963 assert_eq!(
4964 highlights,
4965 &[
4966 (lsp::DocumentHighlightKind::WRITE, 10..16),
4967 (lsp::DocumentHighlightKind::READ, 32..38),
4968 (lsp::DocumentHighlightKind::READ, 41..47)
4969 ]
4970 )
4971 });
4972}
4973
4974#[gpui::test(iterations = 10)]
4975async fn test_lsp_hover(
4976 deterministic: Arc<Deterministic>,
4977 cx_a: &mut TestAppContext,
4978 cx_b: &mut TestAppContext,
4979) {
4980 deterministic.forbid_parking();
4981 let mut server = TestServer::start(&deterministic).await;
4982 let client_a = server.create_client(cx_a, "user_a").await;
4983 let client_b = server.create_client(cx_b, "user_b").await;
4984 server
4985 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4986 .await;
4987 let active_call_a = cx_a.read(ActiveCall::global);
4988
4989 client_a
4990 .fs
4991 .insert_tree(
4992 "/root-1",
4993 json!({
4994 "main.rs": "use std::collections::HashMap;",
4995 }),
4996 )
4997 .await;
4998
4999 // Set up a fake language server.
5000 let mut language = Language::new(
5001 LanguageConfig {
5002 name: "Rust".into(),
5003 path_suffixes: vec!["rs".to_string()],
5004 ..Default::default()
5005 },
5006 Some(tree_sitter_rust::language()),
5007 );
5008 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5009 client_a.language_registry.add(Arc::new(language));
5010
5011 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5012 let project_id = active_call_a
5013 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5014 .await
5015 .unwrap();
5016 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5017
5018 // Open the file as the guest
5019 let buffer_b = cx_b
5020 .background()
5021 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
5022 .await
5023 .unwrap();
5024
5025 // Request hover information as the guest.
5026 let fake_language_server = fake_language_servers.next().await.unwrap();
5027 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
5028 |params, _| async move {
5029 assert_eq!(
5030 params
5031 .text_document_position_params
5032 .text_document
5033 .uri
5034 .as_str(),
5035 "file:///root-1/main.rs"
5036 );
5037 assert_eq!(
5038 params.text_document_position_params.position,
5039 lsp::Position::new(0, 22)
5040 );
5041 Ok(Some(lsp::Hover {
5042 contents: lsp::HoverContents::Array(vec![
5043 lsp::MarkedString::String("Test hover content.".to_string()),
5044 lsp::MarkedString::LanguageString(lsp::LanguageString {
5045 language: "Rust".to_string(),
5046 value: "let foo = 42;".to_string(),
5047 }),
5048 ]),
5049 range: Some(lsp::Range::new(
5050 lsp::Position::new(0, 22),
5051 lsp::Position::new(0, 29),
5052 )),
5053 }))
5054 },
5055 );
5056
5057 let hover_info = project_b
5058 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5059 .await
5060 .unwrap()
5061 .unwrap();
5062 buffer_b.read_with(cx_b, |buffer, _| {
5063 let snapshot = buffer.snapshot();
5064 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
5065 assert_eq!(
5066 hover_info.contents,
5067 vec![
5068 project::HoverBlock {
5069 text: "Test hover content.".to_string(),
5070 kind: HoverBlockKind::Markdown,
5071 },
5072 project::HoverBlock {
5073 text: "let foo = 42;".to_string(),
5074 kind: HoverBlockKind::Code {
5075 language: "Rust".to_string()
5076 },
5077 }
5078 ]
5079 );
5080 });
5081}
5082
5083#[gpui::test(iterations = 10)]
5084async fn test_project_symbols(
5085 deterministic: Arc<Deterministic>,
5086 cx_a: &mut TestAppContext,
5087 cx_b: &mut TestAppContext,
5088) {
5089 deterministic.forbid_parking();
5090 let mut server = TestServer::start(&deterministic).await;
5091 let client_a = server.create_client(cx_a, "user_a").await;
5092 let client_b = server.create_client(cx_b, "user_b").await;
5093 server
5094 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5095 .await;
5096 let active_call_a = cx_a.read(ActiveCall::global);
5097
5098 // Set up a fake language server.
5099 let mut language = Language::new(
5100 LanguageConfig {
5101 name: "Rust".into(),
5102 path_suffixes: vec!["rs".to_string()],
5103 ..Default::default()
5104 },
5105 Some(tree_sitter_rust::language()),
5106 );
5107 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5108 client_a.language_registry.add(Arc::new(language));
5109
5110 client_a
5111 .fs
5112 .insert_tree(
5113 "/code",
5114 json!({
5115 "crate-1": {
5116 "one.rs": "const ONE: usize = 1;",
5117 },
5118 "crate-2": {
5119 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5120 },
5121 "private": {
5122 "passwords.txt": "the-password",
5123 }
5124 }),
5125 )
5126 .await;
5127 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
5128 let project_id = active_call_a
5129 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5130 .await
5131 .unwrap();
5132 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5133
5134 // Cause the language server to start.
5135 let _buffer = cx_b
5136 .background()
5137 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
5138 .await
5139 .unwrap();
5140
5141 let fake_language_server = fake_language_servers.next().await.unwrap();
5142 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
5143 #[allow(deprecated)]
5144 Ok(Some(vec![lsp::SymbolInformation {
5145 name: "TWO".into(),
5146 location: lsp::Location {
5147 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
5148 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5149 },
5150 kind: lsp::SymbolKind::CONSTANT,
5151 tags: None,
5152 container_name: None,
5153 deprecated: None,
5154 }]))
5155 });
5156
5157 // Request the definition of a symbol as the guest.
5158 let symbols = project_b
5159 .update(cx_b, |p, cx| p.symbols("two", cx))
5160 .await
5161 .unwrap();
5162 assert_eq!(symbols.len(), 1);
5163 assert_eq!(symbols[0].name, "TWO");
5164
5165 // Open one of the returned symbols.
5166 let buffer_b_2 = project_b
5167 .update(cx_b, |project, cx| {
5168 project.open_buffer_for_symbol(&symbols[0], cx)
5169 })
5170 .await
5171 .unwrap();
5172 buffer_b_2.read_with(cx_b, |buffer, _| {
5173 assert_eq!(
5174 buffer.file().unwrap().path().as_ref(),
5175 Path::new("../crate-2/two.rs")
5176 );
5177 });
5178
5179 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5180 let mut fake_symbol = symbols[0].clone();
5181 fake_symbol.path.path = Path::new("/code/secrets").into();
5182 let error = project_b
5183 .update(cx_b, |project, cx| {
5184 project.open_buffer_for_symbol(&fake_symbol, cx)
5185 })
5186 .await
5187 .unwrap_err();
5188 assert!(error.to_string().contains("invalid symbol signature"));
5189}
5190
5191#[gpui::test(iterations = 10)]
5192async fn test_open_buffer_while_getting_definition_pointing_to_it(
5193 deterministic: Arc<Deterministic>,
5194 cx_a: &mut TestAppContext,
5195 cx_b: &mut TestAppContext,
5196 mut rng: StdRng,
5197) {
5198 deterministic.forbid_parking();
5199 let mut server = TestServer::start(&deterministic).await;
5200 let client_a = server.create_client(cx_a, "user_a").await;
5201 let client_b = server.create_client(cx_b, "user_b").await;
5202 server
5203 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5204 .await;
5205 let active_call_a = cx_a.read(ActiveCall::global);
5206
5207 // Set up a fake language server.
5208 let mut language = Language::new(
5209 LanguageConfig {
5210 name: "Rust".into(),
5211 path_suffixes: vec!["rs".to_string()],
5212 ..Default::default()
5213 },
5214 Some(tree_sitter_rust::language()),
5215 );
5216 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5217 client_a.language_registry.add(Arc::new(language));
5218
5219 client_a
5220 .fs
5221 .insert_tree(
5222 "/root",
5223 json!({
5224 "a.rs": "const ONE: usize = b::TWO;",
5225 "b.rs": "const TWO: usize = 2",
5226 }),
5227 )
5228 .await;
5229 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5230 let project_id = active_call_a
5231 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5232 .await
5233 .unwrap();
5234 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5235
5236 let buffer_b1 = cx_b
5237 .background()
5238 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
5239 .await
5240 .unwrap();
5241
5242 let fake_language_server = fake_language_servers.next().await.unwrap();
5243 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5244 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5245 lsp::Location::new(
5246 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5247 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5248 ),
5249 )))
5250 });
5251
5252 let definitions;
5253 let buffer_b2;
5254 if rng.gen() {
5255 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5256 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5257 } else {
5258 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5259 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5260 }
5261
5262 let buffer_b2 = buffer_b2.await.unwrap();
5263 let definitions = definitions.await.unwrap();
5264 assert_eq!(definitions.len(), 1);
5265 assert_eq!(definitions[0].target.buffer, buffer_b2);
5266}
5267
5268#[gpui::test(iterations = 10)]
5269async fn test_collaborating_with_code_actions(
5270 deterministic: Arc<Deterministic>,
5271 cx_a: &mut TestAppContext,
5272 cx_b: &mut TestAppContext,
5273) {
5274 deterministic.forbid_parking();
5275 let mut server = TestServer::start(&deterministic).await;
5276 let client_a = server.create_client(cx_a, "user_a").await;
5277 let client_b = server.create_client(cx_b, "user_b").await;
5278 server
5279 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5280 .await;
5281 let active_call_a = cx_a.read(ActiveCall::global);
5282
5283 cx_b.update(editor::init);
5284
5285 // Set up a fake language server.
5286 let mut language = Language::new(
5287 LanguageConfig {
5288 name: "Rust".into(),
5289 path_suffixes: vec!["rs".to_string()],
5290 ..Default::default()
5291 },
5292 Some(tree_sitter_rust::language()),
5293 );
5294 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5295 client_a.language_registry.add(Arc::new(language));
5296
5297 client_a
5298 .fs
5299 .insert_tree(
5300 "/a",
5301 json!({
5302 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
5303 "other.rs": "pub fn foo() -> usize { 4 }",
5304 }),
5305 )
5306 .await;
5307 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5308 let project_id = active_call_a
5309 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5310 .await
5311 .unwrap();
5312
5313 // Join the project as client B.
5314 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5315 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
5316 let editor_b = workspace_b
5317 .update(cx_b, |workspace, cx| {
5318 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
5319 })
5320 .await
5321 .unwrap()
5322 .downcast::<Editor>()
5323 .unwrap();
5324
5325 let mut fake_language_server = fake_language_servers.next().await.unwrap();
5326 fake_language_server
5327 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
5328 assert_eq!(
5329 params.text_document.uri,
5330 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5331 );
5332 assert_eq!(params.range.start, lsp::Position::new(0, 0));
5333 assert_eq!(params.range.end, lsp::Position::new(0, 0));
5334 Ok(None)
5335 })
5336 .next()
5337 .await;
5338
5339 // Move cursor to a location that contains code actions.
5340 editor_b.update(cx_b, |editor, cx| {
5341 editor.change_selections(None, cx, |s| {
5342 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
5343 });
5344 cx.focus(&editor_b);
5345 });
5346
5347 fake_language_server
5348 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
5349 assert_eq!(
5350 params.text_document.uri,
5351 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5352 );
5353 assert_eq!(params.range.start, lsp::Position::new(1, 31));
5354 assert_eq!(params.range.end, lsp::Position::new(1, 31));
5355
5356 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
5357 lsp::CodeAction {
5358 title: "Inline into all callers".to_string(),
5359 edit: Some(lsp::WorkspaceEdit {
5360 changes: Some(
5361 [
5362 (
5363 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5364 vec![lsp::TextEdit::new(
5365 lsp::Range::new(
5366 lsp::Position::new(1, 22),
5367 lsp::Position::new(1, 34),
5368 ),
5369 "4".to_string(),
5370 )],
5371 ),
5372 (
5373 lsp::Url::from_file_path("/a/other.rs").unwrap(),
5374 vec![lsp::TextEdit::new(
5375 lsp::Range::new(
5376 lsp::Position::new(0, 0),
5377 lsp::Position::new(0, 27),
5378 ),
5379 "".to_string(),
5380 )],
5381 ),
5382 ]
5383 .into_iter()
5384 .collect(),
5385 ),
5386 ..Default::default()
5387 }),
5388 data: Some(json!({
5389 "codeActionParams": {
5390 "range": {
5391 "start": {"line": 1, "column": 31},
5392 "end": {"line": 1, "column": 31},
5393 }
5394 }
5395 })),
5396 ..Default::default()
5397 },
5398 )]))
5399 })
5400 .next()
5401 .await;
5402
5403 // Toggle code actions and wait for them to display.
5404 editor_b.update(cx_b, |editor, cx| {
5405 editor.toggle_code_actions(
5406 &ToggleCodeActions {
5407 deployed_from_indicator: false,
5408 },
5409 cx,
5410 );
5411 });
5412 cx_a.foreground().run_until_parked();
5413 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
5414
5415 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
5416
5417 // Confirming the code action will trigger a resolve request.
5418 let confirm_action = workspace_b
5419 .update(cx_b, |workspace, cx| {
5420 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
5421 })
5422 .unwrap();
5423 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
5424 |_, _| async move {
5425 Ok(lsp::CodeAction {
5426 title: "Inline into all callers".to_string(),
5427 edit: Some(lsp::WorkspaceEdit {
5428 changes: Some(
5429 [
5430 (
5431 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5432 vec![lsp::TextEdit::new(
5433 lsp::Range::new(
5434 lsp::Position::new(1, 22),
5435 lsp::Position::new(1, 34),
5436 ),
5437 "4".to_string(),
5438 )],
5439 ),
5440 (
5441 lsp::Url::from_file_path("/a/other.rs").unwrap(),
5442 vec![lsp::TextEdit::new(
5443 lsp::Range::new(
5444 lsp::Position::new(0, 0),
5445 lsp::Position::new(0, 27),
5446 ),
5447 "".to_string(),
5448 )],
5449 ),
5450 ]
5451 .into_iter()
5452 .collect(),
5453 ),
5454 ..Default::default()
5455 }),
5456 ..Default::default()
5457 })
5458 },
5459 );
5460
5461 // After the action is confirmed, an editor containing both modified files is opened.
5462 confirm_action.await.unwrap();
5463 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5464 workspace
5465 .active_item(cx)
5466 .unwrap()
5467 .downcast::<Editor>()
5468 .unwrap()
5469 });
5470 code_action_editor.update(cx_b, |editor, cx| {
5471 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
5472 editor.undo(&Undo, cx);
5473 assert_eq!(
5474 editor.text(cx),
5475 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
5476 );
5477 editor.redo(&Redo, cx);
5478 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
5479 });
5480}
5481
5482#[gpui::test(iterations = 10)]
5483async fn test_collaborating_with_renames(
5484 deterministic: Arc<Deterministic>,
5485 cx_a: &mut TestAppContext,
5486 cx_b: &mut TestAppContext,
5487) {
5488 deterministic.forbid_parking();
5489 let mut server = TestServer::start(&deterministic).await;
5490 let client_a = server.create_client(cx_a, "user_a").await;
5491 let client_b = server.create_client(cx_b, "user_b").await;
5492 server
5493 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5494 .await;
5495 let active_call_a = cx_a.read(ActiveCall::global);
5496
5497 cx_b.update(editor::init);
5498
5499 // Set up a fake language server.
5500 let mut language = Language::new(
5501 LanguageConfig {
5502 name: "Rust".into(),
5503 path_suffixes: vec!["rs".to_string()],
5504 ..Default::default()
5505 },
5506 Some(tree_sitter_rust::language()),
5507 );
5508 let mut fake_language_servers = language
5509 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5510 capabilities: lsp::ServerCapabilities {
5511 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
5512 prepare_provider: Some(true),
5513 work_done_progress_options: Default::default(),
5514 })),
5515 ..Default::default()
5516 },
5517 ..Default::default()
5518 }))
5519 .await;
5520 client_a.language_registry.add(Arc::new(language));
5521
5522 client_a
5523 .fs
5524 .insert_tree(
5525 "/dir",
5526 json!({
5527 "one.rs": "const ONE: usize = 1;",
5528 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
5529 }),
5530 )
5531 .await;
5532 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5533 let project_id = active_call_a
5534 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5535 .await
5536 .unwrap();
5537 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5538
5539 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
5540 let editor_b = workspace_b
5541 .update(cx_b, |workspace, cx| {
5542 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
5543 })
5544 .await
5545 .unwrap()
5546 .downcast::<Editor>()
5547 .unwrap();
5548 let fake_language_server = fake_language_servers.next().await.unwrap();
5549
5550 // Move cursor to a location that can be renamed.
5551 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
5552 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
5553 editor.rename(&Rename, cx).unwrap()
5554 });
5555
5556 fake_language_server
5557 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
5558 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
5559 assert_eq!(params.position, lsp::Position::new(0, 7));
5560 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
5561 lsp::Position::new(0, 6),
5562 lsp::Position::new(0, 9),
5563 ))))
5564 })
5565 .next()
5566 .await
5567 .unwrap();
5568 prepare_rename.await.unwrap();
5569 editor_b.update(cx_b, |editor, cx| {
5570 let rename = editor.pending_rename().unwrap();
5571 let buffer = editor.buffer().read(cx).snapshot(cx);
5572 assert_eq!(
5573 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
5574 6..9
5575 );
5576 rename.editor.update(cx, |rename_editor, cx| {
5577 rename_editor.buffer().update(cx, |rename_buffer, cx| {
5578 rename_buffer.edit([(0..3, "THREE")], None, cx);
5579 });
5580 });
5581 });
5582
5583 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
5584 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
5585 });
5586 fake_language_server
5587 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
5588 assert_eq!(
5589 params.text_document_position.text_document.uri.as_str(),
5590 "file:///dir/one.rs"
5591 );
5592 assert_eq!(
5593 params.text_document_position.position,
5594 lsp::Position::new(0, 6)
5595 );
5596 assert_eq!(params.new_name, "THREE");
5597 Ok(Some(lsp::WorkspaceEdit {
5598 changes: Some(
5599 [
5600 (
5601 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
5602 vec![lsp::TextEdit::new(
5603 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5604 "THREE".to_string(),
5605 )],
5606 ),
5607 (
5608 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
5609 vec![
5610 lsp::TextEdit::new(
5611 lsp::Range::new(
5612 lsp::Position::new(0, 24),
5613 lsp::Position::new(0, 27),
5614 ),
5615 "THREE".to_string(),
5616 ),
5617 lsp::TextEdit::new(
5618 lsp::Range::new(
5619 lsp::Position::new(0, 35),
5620 lsp::Position::new(0, 38),
5621 ),
5622 "THREE".to_string(),
5623 ),
5624 ],
5625 ),
5626 ]
5627 .into_iter()
5628 .collect(),
5629 ),
5630 ..Default::default()
5631 }))
5632 })
5633 .next()
5634 .await
5635 .unwrap();
5636 confirm_rename.await.unwrap();
5637
5638 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5639 workspace
5640 .active_item(cx)
5641 .unwrap()
5642 .downcast::<Editor>()
5643 .unwrap()
5644 });
5645 rename_editor.update(cx_b, |editor, cx| {
5646 assert_eq!(
5647 editor.text(cx),
5648 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5649 );
5650 editor.undo(&Undo, cx);
5651 assert_eq!(
5652 editor.text(cx),
5653 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
5654 );
5655 editor.redo(&Redo, cx);
5656 assert_eq!(
5657 editor.text(cx),
5658 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5659 );
5660 });
5661
5662 // Ensure temporary rename edits cannot be undone/redone.
5663 editor_b.update(cx_b, |editor, cx| {
5664 editor.undo(&Undo, cx);
5665 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5666 editor.undo(&Undo, cx);
5667 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5668 editor.redo(&Redo, cx);
5669 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
5670 })
5671}
5672
5673#[gpui::test(iterations = 10)]
5674async fn test_language_server_statuses(
5675 deterministic: Arc<Deterministic>,
5676 cx_a: &mut TestAppContext,
5677 cx_b: &mut TestAppContext,
5678) {
5679 deterministic.forbid_parking();
5680 let mut server = TestServer::start(&deterministic).await;
5681 let client_a = server.create_client(cx_a, "user_a").await;
5682 let client_b = server.create_client(cx_b, "user_b").await;
5683 server
5684 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5685 .await;
5686 let active_call_a = cx_a.read(ActiveCall::global);
5687
5688 cx_b.update(editor::init);
5689
5690 // Set up a fake language server.
5691 let mut language = Language::new(
5692 LanguageConfig {
5693 name: "Rust".into(),
5694 path_suffixes: vec!["rs".to_string()],
5695 ..Default::default()
5696 },
5697 Some(tree_sitter_rust::language()),
5698 );
5699 let mut fake_language_servers = language
5700 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5701 name: "the-language-server",
5702 ..Default::default()
5703 }))
5704 .await;
5705 client_a.language_registry.add(Arc::new(language));
5706
5707 client_a
5708 .fs
5709 .insert_tree(
5710 "/dir",
5711 json!({
5712 "main.rs": "const ONE: usize = 1;",
5713 }),
5714 )
5715 .await;
5716 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5717
5718 let _buffer_a = project_a
5719 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
5720 .await
5721 .unwrap();
5722
5723 let fake_language_server = fake_language_servers.next().await.unwrap();
5724 fake_language_server.start_progress("the-token").await;
5725 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5726 token: lsp::NumberOrString::String("the-token".to_string()),
5727 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5728 lsp::WorkDoneProgressReport {
5729 message: Some("the-message".to_string()),
5730 ..Default::default()
5731 },
5732 )),
5733 });
5734 deterministic.run_until_parked();
5735 project_a.read_with(cx_a, |project, _| {
5736 let status = project.language_server_statuses().next().unwrap();
5737 assert_eq!(status.name, "the-language-server");
5738 assert_eq!(status.pending_work.len(), 1);
5739 assert_eq!(
5740 status.pending_work["the-token"].message.as_ref().unwrap(),
5741 "the-message"
5742 );
5743 });
5744
5745 let project_id = active_call_a
5746 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5747 .await
5748 .unwrap();
5749 deterministic.run_until_parked();
5750 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5751 project_b.read_with(cx_b, |project, _| {
5752 let status = project.language_server_statuses().next().unwrap();
5753 assert_eq!(status.name, "the-language-server");
5754 });
5755
5756 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5757 token: lsp::NumberOrString::String("the-token".to_string()),
5758 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5759 lsp::WorkDoneProgressReport {
5760 message: Some("the-message-2".to_string()),
5761 ..Default::default()
5762 },
5763 )),
5764 });
5765 deterministic.run_until_parked();
5766 project_a.read_with(cx_a, |project, _| {
5767 let status = project.language_server_statuses().next().unwrap();
5768 assert_eq!(status.name, "the-language-server");
5769 assert_eq!(status.pending_work.len(), 1);
5770 assert_eq!(
5771 status.pending_work["the-token"].message.as_ref().unwrap(),
5772 "the-message-2"
5773 );
5774 });
5775 project_b.read_with(cx_b, |project, _| {
5776 let status = project.language_server_statuses().next().unwrap();
5777 assert_eq!(status.name, "the-language-server");
5778 assert_eq!(status.pending_work.len(), 1);
5779 assert_eq!(
5780 status.pending_work["the-token"].message.as_ref().unwrap(),
5781 "the-message-2"
5782 );
5783 });
5784}
5785
5786#[gpui::test(iterations = 10)]
5787async fn test_contacts(
5788 deterministic: Arc<Deterministic>,
5789 cx_a: &mut TestAppContext,
5790 cx_b: &mut TestAppContext,
5791 cx_c: &mut TestAppContext,
5792 cx_d: &mut TestAppContext,
5793) {
5794 deterministic.forbid_parking();
5795 let mut server = TestServer::start(&deterministic).await;
5796 let client_a = server.create_client(cx_a, "user_a").await;
5797 let client_b = server.create_client(cx_b, "user_b").await;
5798 let client_c = server.create_client(cx_c, "user_c").await;
5799 let client_d = server.create_client(cx_d, "user_d").await;
5800 server
5801 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5802 .await;
5803 let active_call_a = cx_a.read(ActiveCall::global);
5804 let active_call_b = cx_b.read(ActiveCall::global);
5805 let active_call_c = cx_c.read(ActiveCall::global);
5806 let _active_call_d = cx_d.read(ActiveCall::global);
5807
5808 deterministic.run_until_parked();
5809 assert_eq!(
5810 contacts(&client_a, cx_a),
5811 [
5812 ("user_b".to_string(), "online", "free"),
5813 ("user_c".to_string(), "online", "free")
5814 ]
5815 );
5816 assert_eq!(
5817 contacts(&client_b, cx_b),
5818 [
5819 ("user_a".to_string(), "online", "free"),
5820 ("user_c".to_string(), "online", "free")
5821 ]
5822 );
5823 assert_eq!(
5824 contacts(&client_c, cx_c),
5825 [
5826 ("user_a".to_string(), "online", "free"),
5827 ("user_b".to_string(), "online", "free")
5828 ]
5829 );
5830 assert_eq!(contacts(&client_d, cx_d), []);
5831
5832 server.disconnect_client(client_c.peer_id().unwrap());
5833 server.forbid_connections();
5834 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5835 assert_eq!(
5836 contacts(&client_a, cx_a),
5837 [
5838 ("user_b".to_string(), "online", "free"),
5839 ("user_c".to_string(), "offline", "free")
5840 ]
5841 );
5842 assert_eq!(
5843 contacts(&client_b, cx_b),
5844 [
5845 ("user_a".to_string(), "online", "free"),
5846 ("user_c".to_string(), "offline", "free")
5847 ]
5848 );
5849 assert_eq!(contacts(&client_c, cx_c), []);
5850 assert_eq!(contacts(&client_d, cx_d), []);
5851
5852 server.allow_connections();
5853 client_c
5854 .authenticate_and_connect(false, &cx_c.to_async())
5855 .await
5856 .unwrap();
5857
5858 deterministic.run_until_parked();
5859 assert_eq!(
5860 contacts(&client_a, cx_a),
5861 [
5862 ("user_b".to_string(), "online", "free"),
5863 ("user_c".to_string(), "online", "free")
5864 ]
5865 );
5866 assert_eq!(
5867 contacts(&client_b, cx_b),
5868 [
5869 ("user_a".to_string(), "online", "free"),
5870 ("user_c".to_string(), "online", "free")
5871 ]
5872 );
5873 assert_eq!(
5874 contacts(&client_c, cx_c),
5875 [
5876 ("user_a".to_string(), "online", "free"),
5877 ("user_b".to_string(), "online", "free")
5878 ]
5879 );
5880 assert_eq!(contacts(&client_d, cx_d), []);
5881
5882 active_call_a
5883 .update(cx_a, |call, cx| {
5884 call.invite(client_b.user_id().unwrap(), None, cx)
5885 })
5886 .await
5887 .unwrap();
5888 deterministic.run_until_parked();
5889 assert_eq!(
5890 contacts(&client_a, cx_a),
5891 [
5892 ("user_b".to_string(), "online", "busy"),
5893 ("user_c".to_string(), "online", "free")
5894 ]
5895 );
5896 assert_eq!(
5897 contacts(&client_b, cx_b),
5898 [
5899 ("user_a".to_string(), "online", "busy"),
5900 ("user_c".to_string(), "online", "free")
5901 ]
5902 );
5903 assert_eq!(
5904 contacts(&client_c, cx_c),
5905 [
5906 ("user_a".to_string(), "online", "busy"),
5907 ("user_b".to_string(), "online", "busy")
5908 ]
5909 );
5910 assert_eq!(contacts(&client_d, cx_d), []);
5911
5912 // Client B and client D become contacts while client B is being called.
5913 server
5914 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5915 .await;
5916 deterministic.run_until_parked();
5917 assert_eq!(
5918 contacts(&client_a, cx_a),
5919 [
5920 ("user_b".to_string(), "online", "busy"),
5921 ("user_c".to_string(), "online", "free")
5922 ]
5923 );
5924 assert_eq!(
5925 contacts(&client_b, cx_b),
5926 [
5927 ("user_a".to_string(), "online", "busy"),
5928 ("user_c".to_string(), "online", "free"),
5929 ("user_d".to_string(), "online", "free"),
5930 ]
5931 );
5932 assert_eq!(
5933 contacts(&client_c, cx_c),
5934 [
5935 ("user_a".to_string(), "online", "busy"),
5936 ("user_b".to_string(), "online", "busy")
5937 ]
5938 );
5939 assert_eq!(
5940 contacts(&client_d, cx_d),
5941 [("user_b".to_string(), "online", "busy")]
5942 );
5943
5944 active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
5945 deterministic.run_until_parked();
5946 assert_eq!(
5947 contacts(&client_a, cx_a),
5948 [
5949 ("user_b".to_string(), "online", "free"),
5950 ("user_c".to_string(), "online", "free")
5951 ]
5952 );
5953 assert_eq!(
5954 contacts(&client_b, cx_b),
5955 [
5956 ("user_a".to_string(), "online", "free"),
5957 ("user_c".to_string(), "online", "free"),
5958 ("user_d".to_string(), "online", "free")
5959 ]
5960 );
5961 assert_eq!(
5962 contacts(&client_c, cx_c),
5963 [
5964 ("user_a".to_string(), "online", "free"),
5965 ("user_b".to_string(), "online", "free")
5966 ]
5967 );
5968 assert_eq!(
5969 contacts(&client_d, cx_d),
5970 [("user_b".to_string(), "online", "free")]
5971 );
5972
5973 active_call_c
5974 .update(cx_c, |call, cx| {
5975 call.invite(client_a.user_id().unwrap(), None, cx)
5976 })
5977 .await
5978 .unwrap();
5979 deterministic.run_until_parked();
5980 assert_eq!(
5981 contacts(&client_a, cx_a),
5982 [
5983 ("user_b".to_string(), "online", "free"),
5984 ("user_c".to_string(), "online", "busy")
5985 ]
5986 );
5987 assert_eq!(
5988 contacts(&client_b, cx_b),
5989 [
5990 ("user_a".to_string(), "online", "busy"),
5991 ("user_c".to_string(), "online", "busy"),
5992 ("user_d".to_string(), "online", "free")
5993 ]
5994 );
5995 assert_eq!(
5996 contacts(&client_c, cx_c),
5997 [
5998 ("user_a".to_string(), "online", "busy"),
5999 ("user_b".to_string(), "online", "free")
6000 ]
6001 );
6002 assert_eq!(
6003 contacts(&client_d, cx_d),
6004 [("user_b".to_string(), "online", "free")]
6005 );
6006
6007 active_call_a
6008 .update(cx_a, |call, cx| call.accept_incoming(cx))
6009 .await
6010 .unwrap();
6011 deterministic.run_until_parked();
6012 assert_eq!(
6013 contacts(&client_a, cx_a),
6014 [
6015 ("user_b".to_string(), "online", "free"),
6016 ("user_c".to_string(), "online", "busy")
6017 ]
6018 );
6019 assert_eq!(
6020 contacts(&client_b, cx_b),
6021 [
6022 ("user_a".to_string(), "online", "busy"),
6023 ("user_c".to_string(), "online", "busy"),
6024 ("user_d".to_string(), "online", "free")
6025 ]
6026 );
6027 assert_eq!(
6028 contacts(&client_c, cx_c),
6029 [
6030 ("user_a".to_string(), "online", "busy"),
6031 ("user_b".to_string(), "online", "free")
6032 ]
6033 );
6034 assert_eq!(
6035 contacts(&client_d, cx_d),
6036 [("user_b".to_string(), "online", "free")]
6037 );
6038
6039 active_call_a
6040 .update(cx_a, |call, cx| {
6041 call.invite(client_b.user_id().unwrap(), None, cx)
6042 })
6043 .await
6044 .unwrap();
6045 deterministic.run_until_parked();
6046 assert_eq!(
6047 contacts(&client_a, cx_a),
6048 [
6049 ("user_b".to_string(), "online", "busy"),
6050 ("user_c".to_string(), "online", "busy")
6051 ]
6052 );
6053 assert_eq!(
6054 contacts(&client_b, cx_b),
6055 [
6056 ("user_a".to_string(), "online", "busy"),
6057 ("user_c".to_string(), "online", "busy"),
6058 ("user_d".to_string(), "online", "free")
6059 ]
6060 );
6061 assert_eq!(
6062 contacts(&client_c, cx_c),
6063 [
6064 ("user_a".to_string(), "online", "busy"),
6065 ("user_b".to_string(), "online", "busy")
6066 ]
6067 );
6068 assert_eq!(
6069 contacts(&client_d, cx_d),
6070 [("user_b".to_string(), "online", "busy")]
6071 );
6072
6073 active_call_a
6074 .update(cx_a, |call, cx| call.hang_up(cx))
6075 .await
6076 .unwrap();
6077 deterministic.run_until_parked();
6078 assert_eq!(
6079 contacts(&client_a, cx_a),
6080 [
6081 ("user_b".to_string(), "online", "free"),
6082 ("user_c".to_string(), "online", "free")
6083 ]
6084 );
6085 assert_eq!(
6086 contacts(&client_b, cx_b),
6087 [
6088 ("user_a".to_string(), "online", "free"),
6089 ("user_c".to_string(), "online", "free"),
6090 ("user_d".to_string(), "online", "free")
6091 ]
6092 );
6093 assert_eq!(
6094 contacts(&client_c, cx_c),
6095 [
6096 ("user_a".to_string(), "online", "free"),
6097 ("user_b".to_string(), "online", "free")
6098 ]
6099 );
6100 assert_eq!(
6101 contacts(&client_d, cx_d),
6102 [("user_b".to_string(), "online", "free")]
6103 );
6104
6105 active_call_a
6106 .update(cx_a, |call, cx| {
6107 call.invite(client_b.user_id().unwrap(), None, cx)
6108 })
6109 .await
6110 .unwrap();
6111 deterministic.run_until_parked();
6112 assert_eq!(
6113 contacts(&client_a, cx_a),
6114 [
6115 ("user_b".to_string(), "online", "busy"),
6116 ("user_c".to_string(), "online", "free")
6117 ]
6118 );
6119 assert_eq!(
6120 contacts(&client_b, cx_b),
6121 [
6122 ("user_a".to_string(), "online", "busy"),
6123 ("user_c".to_string(), "online", "free"),
6124 ("user_d".to_string(), "online", "free")
6125 ]
6126 );
6127 assert_eq!(
6128 contacts(&client_c, cx_c),
6129 [
6130 ("user_a".to_string(), "online", "busy"),
6131 ("user_b".to_string(), "online", "busy")
6132 ]
6133 );
6134 assert_eq!(
6135 contacts(&client_d, cx_d),
6136 [("user_b".to_string(), "online", "busy")]
6137 );
6138
6139 server.forbid_connections();
6140 server.disconnect_client(client_a.peer_id().unwrap());
6141 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6142 assert_eq!(contacts(&client_a, cx_a), []);
6143 assert_eq!(
6144 contacts(&client_b, cx_b),
6145 [
6146 ("user_a".to_string(), "offline", "free"),
6147 ("user_c".to_string(), "online", "free"),
6148 ("user_d".to_string(), "online", "free")
6149 ]
6150 );
6151 assert_eq!(
6152 contacts(&client_c, cx_c),
6153 [
6154 ("user_a".to_string(), "offline", "free"),
6155 ("user_b".to_string(), "online", "free")
6156 ]
6157 );
6158 assert_eq!(
6159 contacts(&client_d, cx_d),
6160 [("user_b".to_string(), "online", "free")]
6161 );
6162
6163 // Test removing a contact
6164 client_b
6165 .user_store
6166 .update(cx_b, |store, cx| {
6167 store.remove_contact(client_c.user_id().unwrap(), cx)
6168 })
6169 .await
6170 .unwrap();
6171 deterministic.run_until_parked();
6172 assert_eq!(
6173 contacts(&client_b, cx_b),
6174 [
6175 ("user_a".to_string(), "offline", "free"),
6176 ("user_d".to_string(), "online", "free")
6177 ]
6178 );
6179 assert_eq!(
6180 contacts(&client_c, cx_c),
6181 [("user_a".to_string(), "offline", "free"),]
6182 );
6183
6184 fn contacts(
6185 client: &TestClient,
6186 cx: &TestAppContext,
6187 ) -> Vec<(String, &'static str, &'static str)> {
6188 client.user_store.read_with(cx, |store, _| {
6189 store
6190 .contacts()
6191 .iter()
6192 .map(|contact| {
6193 (
6194 contact.user.github_login.clone(),
6195 if contact.online { "online" } else { "offline" },
6196 if contact.busy { "busy" } else { "free" },
6197 )
6198 })
6199 .collect()
6200 })
6201 }
6202}
6203
6204#[gpui::test(iterations = 10)]
6205async fn test_contact_requests(
6206 deterministic: Arc<Deterministic>,
6207 cx_a: &mut TestAppContext,
6208 cx_a2: &mut TestAppContext,
6209 cx_b: &mut TestAppContext,
6210 cx_b2: &mut TestAppContext,
6211 cx_c: &mut TestAppContext,
6212 cx_c2: &mut TestAppContext,
6213) {
6214 deterministic.forbid_parking();
6215
6216 // Connect to a server as 3 clients.
6217 let mut server = TestServer::start(&deterministic).await;
6218 let client_a = server.create_client(cx_a, "user_a").await;
6219 let client_a2 = server.create_client(cx_a2, "user_a").await;
6220 let client_b = server.create_client(cx_b, "user_b").await;
6221 let client_b2 = server.create_client(cx_b2, "user_b").await;
6222 let client_c = server.create_client(cx_c, "user_c").await;
6223 let client_c2 = server.create_client(cx_c2, "user_c").await;
6224
6225 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
6226 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
6227 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
6228
6229 // User A and User C request that user B become their contact.
6230 client_a
6231 .user_store
6232 .update(cx_a, |store, cx| {
6233 store.request_contact(client_b.user_id().unwrap(), cx)
6234 })
6235 .await
6236 .unwrap();
6237 client_c
6238 .user_store
6239 .update(cx_c, |store, cx| {
6240 store.request_contact(client_b.user_id().unwrap(), cx)
6241 })
6242 .await
6243 .unwrap();
6244 deterministic.run_until_parked();
6245
6246 // All users see the pending request appear in all their clients.
6247 assert_eq!(
6248 client_a.summarize_contacts(cx_a).outgoing_requests,
6249 &["user_b"]
6250 );
6251 assert_eq!(
6252 client_a2.summarize_contacts(cx_a2).outgoing_requests,
6253 &["user_b"]
6254 );
6255 assert_eq!(
6256 client_b.summarize_contacts(cx_b).incoming_requests,
6257 &["user_a", "user_c"]
6258 );
6259 assert_eq!(
6260 client_b2.summarize_contacts(cx_b2).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 assert_eq!(
6268 client_c2.summarize_contacts(cx_c2).outgoing_requests,
6269 &["user_b"]
6270 );
6271
6272 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
6273 disconnect_and_reconnect(&client_a, cx_a).await;
6274 disconnect_and_reconnect(&client_b, cx_b).await;
6275 disconnect_and_reconnect(&client_c, cx_c).await;
6276 deterministic.run_until_parked();
6277 assert_eq!(
6278 client_a.summarize_contacts(cx_a).outgoing_requests,
6279 &["user_b"]
6280 );
6281 assert_eq!(
6282 client_b.summarize_contacts(cx_b).incoming_requests,
6283 &["user_a", "user_c"]
6284 );
6285 assert_eq!(
6286 client_c.summarize_contacts(cx_c).outgoing_requests,
6287 &["user_b"]
6288 );
6289
6290 // User B accepts the request from user A.
6291 client_b
6292 .user_store
6293 .update(cx_b, |store, cx| {
6294 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
6295 })
6296 .await
6297 .unwrap();
6298
6299 deterministic.run_until_parked();
6300
6301 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
6302 let contacts_b = client_b.summarize_contacts(cx_b);
6303 assert_eq!(contacts_b.current, &["user_a"]);
6304 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
6305 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6306 assert_eq!(contacts_b2.current, &["user_a"]);
6307 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
6308
6309 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
6310 let contacts_a = client_a.summarize_contacts(cx_a);
6311 assert_eq!(contacts_a.current, &["user_b"]);
6312 assert!(contacts_a.outgoing_requests.is_empty());
6313 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
6314 assert_eq!(contacts_a2.current, &["user_b"]);
6315 assert!(contacts_a2.outgoing_requests.is_empty());
6316
6317 // Contacts are present upon connecting (tested here via disconnect/reconnect)
6318 disconnect_and_reconnect(&client_a, cx_a).await;
6319 disconnect_and_reconnect(&client_b, cx_b).await;
6320 disconnect_and_reconnect(&client_c, cx_c).await;
6321 deterministic.run_until_parked();
6322 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6323 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6324 assert_eq!(
6325 client_b.summarize_contacts(cx_b).incoming_requests,
6326 &["user_c"]
6327 );
6328 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6329 assert_eq!(
6330 client_c.summarize_contacts(cx_c).outgoing_requests,
6331 &["user_b"]
6332 );
6333
6334 // User B rejects the request from user C.
6335 client_b
6336 .user_store
6337 .update(cx_b, |store, cx| {
6338 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
6339 })
6340 .await
6341 .unwrap();
6342
6343 deterministic.run_until_parked();
6344
6345 // User B doesn't see user C as their contact, and the incoming request from them is removed.
6346 let contacts_b = client_b.summarize_contacts(cx_b);
6347 assert_eq!(contacts_b.current, &["user_a"]);
6348 assert!(contacts_b.incoming_requests.is_empty());
6349 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6350 assert_eq!(contacts_b2.current, &["user_a"]);
6351 assert!(contacts_b2.incoming_requests.is_empty());
6352
6353 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
6354 let contacts_c = client_c.summarize_contacts(cx_c);
6355 assert!(contacts_c.current.is_empty());
6356 assert!(contacts_c.outgoing_requests.is_empty());
6357 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
6358 assert!(contacts_c2.current.is_empty());
6359 assert!(contacts_c2.outgoing_requests.is_empty());
6360
6361 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
6362 disconnect_and_reconnect(&client_a, cx_a).await;
6363 disconnect_and_reconnect(&client_b, cx_b).await;
6364 disconnect_and_reconnect(&client_c, cx_c).await;
6365 deterministic.run_until_parked();
6366 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6367 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6368 assert!(client_b
6369 .summarize_contacts(cx_b)
6370 .incoming_requests
6371 .is_empty());
6372 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6373 assert!(client_c
6374 .summarize_contacts(cx_c)
6375 .outgoing_requests
6376 .is_empty());
6377
6378 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6379 client.disconnect(&cx.to_async());
6380 client.clear_contacts(cx).await;
6381 client
6382 .authenticate_and_connect(false, &cx.to_async())
6383 .await
6384 .unwrap();
6385 }
6386}
6387
6388#[gpui::test(iterations = 10)]
6389async fn test_basic_following(
6390 deterministic: Arc<Deterministic>,
6391 cx_a: &mut TestAppContext,
6392 cx_b: &mut TestAppContext,
6393 cx_c: &mut TestAppContext,
6394 cx_d: &mut TestAppContext,
6395) {
6396 deterministic.forbid_parking();
6397
6398 let mut server = TestServer::start(&deterministic).await;
6399 let client_a = server.create_client(cx_a, "user_a").await;
6400 let client_b = server.create_client(cx_b, "user_b").await;
6401 let client_c = server.create_client(cx_c, "user_c").await;
6402 let client_d = server.create_client(cx_d, "user_d").await;
6403 server
6404 .create_room(&mut [
6405 (&client_a, cx_a),
6406 (&client_b, cx_b),
6407 (&client_c, cx_c),
6408 (&client_d, cx_d),
6409 ])
6410 .await;
6411 let active_call_a = cx_a.read(ActiveCall::global);
6412 let active_call_b = cx_b.read(ActiveCall::global);
6413
6414 cx_a.update(editor::init);
6415 cx_b.update(editor::init);
6416
6417 client_a
6418 .fs
6419 .insert_tree(
6420 "/a",
6421 json!({
6422 "1.txt": "one\none\none",
6423 "2.txt": "two\ntwo\ntwo",
6424 "3.txt": "three\nthree\nthree",
6425 }),
6426 )
6427 .await;
6428 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6429 active_call_a
6430 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6431 .await
6432 .unwrap();
6433
6434 let project_id = active_call_a
6435 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6436 .await
6437 .unwrap();
6438 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6439 active_call_b
6440 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6441 .await
6442 .unwrap();
6443
6444 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6445 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6446
6447 // Client A opens some editors.
6448 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6449 let editor_a1 = workspace_a
6450 .update(cx_a, |workspace, cx| {
6451 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6452 })
6453 .await
6454 .unwrap()
6455 .downcast::<Editor>()
6456 .unwrap();
6457 let editor_a2 = workspace_a
6458 .update(cx_a, |workspace, cx| {
6459 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6460 })
6461 .await
6462 .unwrap()
6463 .downcast::<Editor>()
6464 .unwrap();
6465
6466 // Client B opens an editor.
6467 let editor_b1 = workspace_b
6468 .update(cx_b, |workspace, cx| {
6469 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6470 })
6471 .await
6472 .unwrap()
6473 .downcast::<Editor>()
6474 .unwrap();
6475
6476 let peer_id_a = client_a.peer_id().unwrap();
6477 let peer_id_b = client_b.peer_id().unwrap();
6478 let peer_id_c = client_c.peer_id().unwrap();
6479 let peer_id_d = client_d.peer_id().unwrap();
6480
6481 // Client A updates their selections in those editors
6482 editor_a1.update(cx_a, |editor, cx| {
6483 editor.handle_input("a", cx);
6484 editor.handle_input("b", cx);
6485 editor.handle_input("c", cx);
6486 editor.select_left(&Default::default(), cx);
6487 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
6488 });
6489 editor_a2.update(cx_a, |editor, cx| {
6490 editor.handle_input("d", cx);
6491 editor.handle_input("e", cx);
6492 editor.select_left(&Default::default(), cx);
6493 assert_eq!(editor.selections.ranges(cx), vec![2..1]);
6494 });
6495
6496 // When client B starts following client A, all visible view states are replicated to client B.
6497 workspace_b
6498 .update(cx_b, |workspace, cx| {
6499 workspace.toggle_follow(peer_id_a, cx).unwrap()
6500 })
6501 .await
6502 .unwrap();
6503
6504 cx_c.foreground().run_until_parked();
6505 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
6506 workspace
6507 .active_item(cx)
6508 .unwrap()
6509 .downcast::<Editor>()
6510 .unwrap()
6511 });
6512 assert_eq!(
6513 cx_b.read(|cx| editor_b2.project_path(cx)),
6514 Some((worktree_id, "2.txt").into())
6515 );
6516 assert_eq!(
6517 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
6518 vec![2..1]
6519 );
6520 assert_eq!(
6521 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
6522 vec![3..2]
6523 );
6524
6525 cx_c.foreground().run_until_parked();
6526 let active_call_c = cx_c.read(ActiveCall::global);
6527 let project_c = client_c.build_remote_project(project_id, cx_c).await;
6528 let workspace_c = client_c.build_workspace(&project_c, cx_c);
6529 active_call_c
6530 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
6531 .await
6532 .unwrap();
6533 drop(project_c);
6534
6535 // Client C also follows client A.
6536 workspace_c
6537 .update(cx_c, |workspace, cx| {
6538 workspace.toggle_follow(peer_id_a, cx).unwrap()
6539 })
6540 .await
6541 .unwrap();
6542
6543 cx_d.foreground().run_until_parked();
6544 let active_call_d = cx_d.read(ActiveCall::global);
6545 let project_d = client_d.build_remote_project(project_id, cx_d).await;
6546 let workspace_d = client_d.build_workspace(&project_d, cx_d);
6547 active_call_d
6548 .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
6549 .await
6550 .unwrap();
6551 drop(project_d);
6552
6553 // All clients see that clients B and C are following client A.
6554 cx_c.foreground().run_until_parked();
6555 for (name, active_call, cx) in [
6556 ("A", &active_call_a, &cx_a),
6557 ("B", &active_call_b, &cx_b),
6558 ("C", &active_call_c, &cx_c),
6559 ("D", &active_call_d, &cx_d),
6560 ] {
6561 active_call.read_with(*cx, |call, cx| {
6562 let room = call.room().unwrap().read(cx);
6563 assert_eq!(
6564 room.followers_for(peer_id_a, project_id),
6565 &[peer_id_b, peer_id_c],
6566 "checking followers for A as {name}"
6567 );
6568 });
6569 }
6570
6571 // Client C unfollows client A.
6572 workspace_c.update(cx_c, |workspace, cx| {
6573 workspace.toggle_follow(peer_id_a, cx);
6574 });
6575
6576 // All clients see that clients B is following client A.
6577 cx_c.foreground().run_until_parked();
6578 for (name, active_call, cx) in [
6579 ("A", &active_call_a, &cx_a),
6580 ("B", &active_call_b, &cx_b),
6581 ("C", &active_call_c, &cx_c),
6582 ("D", &active_call_d, &cx_d),
6583 ] {
6584 active_call.read_with(*cx, |call, cx| {
6585 let room = call.room().unwrap().read(cx);
6586 assert_eq!(
6587 room.followers_for(peer_id_a, project_id),
6588 &[peer_id_b],
6589 "checking followers for A as {name}"
6590 );
6591 });
6592 }
6593
6594 // Client C re-follows client A.
6595 workspace_c.update(cx_c, |workspace, cx| {
6596 workspace.toggle_follow(peer_id_a, cx);
6597 });
6598
6599 // All clients see that clients B and C are following client A.
6600 cx_c.foreground().run_until_parked();
6601 for (name, active_call, cx) in [
6602 ("A", &active_call_a, &cx_a),
6603 ("B", &active_call_b, &cx_b),
6604 ("C", &active_call_c, &cx_c),
6605 ("D", &active_call_d, &cx_d),
6606 ] {
6607 active_call.read_with(*cx, |call, cx| {
6608 let room = call.room().unwrap().read(cx);
6609 assert_eq!(
6610 room.followers_for(peer_id_a, project_id),
6611 &[peer_id_b, peer_id_c],
6612 "checking followers for A as {name}"
6613 );
6614 });
6615 }
6616
6617 // Client D follows client C.
6618 workspace_d
6619 .update(cx_d, |workspace, cx| {
6620 workspace.toggle_follow(peer_id_c, cx).unwrap()
6621 })
6622 .await
6623 .unwrap();
6624
6625 // All clients see that D is following C
6626 cx_d.foreground().run_until_parked();
6627 for (name, active_call, cx) in [
6628 ("A", &active_call_a, &cx_a),
6629 ("B", &active_call_b, &cx_b),
6630 ("C", &active_call_c, &cx_c),
6631 ("D", &active_call_d, &cx_d),
6632 ] {
6633 active_call.read_with(*cx, |call, cx| {
6634 let room = call.room().unwrap().read(cx);
6635 assert_eq!(
6636 room.followers_for(peer_id_c, project_id),
6637 &[peer_id_d],
6638 "checking followers for C as {name}"
6639 );
6640 });
6641 }
6642
6643 // Client C closes the project.
6644 cx_c.drop_last(workspace_c);
6645
6646 // Clients A and B see that client B is following A, and client C is not present in the followers.
6647 cx_c.foreground().run_until_parked();
6648 for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
6649 active_call.read_with(*cx, |call, cx| {
6650 let room = call.room().unwrap().read(cx);
6651 assert_eq!(
6652 room.followers_for(peer_id_a, project_id),
6653 &[peer_id_b],
6654 "checking followers for A as {name}"
6655 );
6656 });
6657 }
6658
6659 // All clients see that no-one is following C
6660 for (name, active_call, cx) in [
6661 ("A", &active_call_a, &cx_a),
6662 ("B", &active_call_b, &cx_b),
6663 ("C", &active_call_c, &cx_c),
6664 ("D", &active_call_d, &cx_d),
6665 ] {
6666 active_call.read_with(*cx, |call, cx| {
6667 let room = call.room().unwrap().read(cx);
6668 assert_eq!(
6669 room.followers_for(peer_id_c, project_id),
6670 &[],
6671 "checking followers for C as {name}"
6672 );
6673 });
6674 }
6675
6676 // When client A activates a different editor, client B does so as well.
6677 workspace_a.update(cx_a, |workspace, cx| {
6678 workspace.activate_item(&editor_a1, cx)
6679 });
6680 deterministic.run_until_parked();
6681 workspace_b.read_with(cx_b, |workspace, cx| {
6682 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6683 });
6684
6685 // When client A opens a multibuffer, client B does so as well.
6686 let multibuffer_a = cx_a.add_model(|cx| {
6687 let buffer_a1 = project_a.update(cx, |project, cx| {
6688 project
6689 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
6690 .unwrap()
6691 });
6692 let buffer_a2 = project_a.update(cx, |project, cx| {
6693 project
6694 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
6695 .unwrap()
6696 });
6697 let mut result = MultiBuffer::new(0);
6698 result.push_excerpts(
6699 buffer_a1,
6700 [ExcerptRange {
6701 context: 0..3,
6702 primary: None,
6703 }],
6704 cx,
6705 );
6706 result.push_excerpts(
6707 buffer_a2,
6708 [ExcerptRange {
6709 context: 4..7,
6710 primary: None,
6711 }],
6712 cx,
6713 );
6714 result
6715 });
6716 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
6717 let editor =
6718 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
6719 workspace.add_item(Box::new(editor.clone()), cx);
6720 editor
6721 });
6722 deterministic.run_until_parked();
6723 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
6724 workspace
6725 .active_item(cx)
6726 .unwrap()
6727 .downcast::<Editor>()
6728 .unwrap()
6729 });
6730 assert_eq!(
6731 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
6732 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
6733 );
6734
6735 // When client A navigates back and forth, client B does so as well.
6736 workspace_a
6737 .update(cx_a, |workspace, cx| {
6738 workspace::Pane::go_back(workspace, None, 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 workspace_a
6748 .update(cx_a, |workspace, cx| {
6749 workspace::Pane::go_back(workspace, None, cx)
6750 })
6751 .await
6752 .unwrap();
6753 deterministic.run_until_parked();
6754 workspace_b.read_with(cx_b, |workspace, cx| {
6755 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
6756 });
6757
6758 workspace_a
6759 .update(cx_a, |workspace, cx| {
6760 workspace::Pane::go_forward(workspace, None, cx)
6761 })
6762 .await
6763 .unwrap();
6764 deterministic.run_until_parked();
6765 workspace_b.read_with(cx_b, |workspace, cx| {
6766 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6767 });
6768
6769 // Changes to client A's editor are reflected on client B.
6770 editor_a1.update(cx_a, |editor, cx| {
6771 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
6772 });
6773 deterministic.run_until_parked();
6774 editor_b1.read_with(cx_b, |editor, cx| {
6775 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
6776 });
6777
6778 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
6779 deterministic.run_until_parked();
6780 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
6781
6782 editor_a1.update(cx_a, |editor, cx| {
6783 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
6784 editor.set_scroll_position(vec2f(0., 100.), cx);
6785 });
6786 deterministic.run_until_parked();
6787 editor_b1.read_with(cx_b, |editor, cx| {
6788 assert_eq!(editor.selections.ranges(cx), &[3..3]);
6789 });
6790
6791 // After unfollowing, client B stops receiving updates from client A.
6792 workspace_b.update(cx_b, |workspace, cx| {
6793 workspace.unfollow(&workspace.active_pane().clone(), cx)
6794 });
6795 workspace_a.update(cx_a, |workspace, cx| {
6796 workspace.activate_item(&editor_a2, cx)
6797 });
6798 deterministic.run_until_parked();
6799 assert_eq!(
6800 workspace_b.read_with(cx_b, |workspace, cx| workspace
6801 .active_item(cx)
6802 .unwrap()
6803 .id()),
6804 editor_b1.id()
6805 );
6806
6807 // Client A starts following client B.
6808 workspace_a
6809 .update(cx_a, |workspace, cx| {
6810 workspace.toggle_follow(peer_id_b, cx).unwrap()
6811 })
6812 .await
6813 .unwrap();
6814 assert_eq!(
6815 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6816 Some(peer_id_b)
6817 );
6818 assert_eq!(
6819 workspace_a.read_with(cx_a, |workspace, cx| workspace
6820 .active_item(cx)
6821 .unwrap()
6822 .id()),
6823 editor_a1.id()
6824 );
6825
6826 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
6827 let display = MacOSDisplay::new();
6828 active_call_b
6829 .update(cx_b, |call, cx| call.set_location(None, cx))
6830 .await
6831 .unwrap();
6832 active_call_b
6833 .update(cx_b, |call, cx| {
6834 call.room().unwrap().update(cx, |room, cx| {
6835 room.set_display_sources(vec![display.clone()]);
6836 room.share_screen(cx)
6837 })
6838 })
6839 .await
6840 .unwrap();
6841 deterministic.run_until_parked();
6842 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
6843 workspace
6844 .active_item(cx)
6845 .unwrap()
6846 .downcast::<SharedScreen>()
6847 .unwrap()
6848 });
6849
6850 // Client B activates Zed again, which causes the previous editor to become focused again.
6851 active_call_b
6852 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6853 .await
6854 .unwrap();
6855 deterministic.run_until_parked();
6856 workspace_a.read_with(cx_a, |workspace, cx| {
6857 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
6858 });
6859
6860 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
6861 workspace_b.update(cx_b, |workspace, cx| {
6862 workspace.activate_item(&multibuffer_editor_b, cx)
6863 });
6864 deterministic.run_until_parked();
6865 workspace_a.read_with(cx_a, |workspace, cx| {
6866 assert_eq!(
6867 workspace.active_item(cx).unwrap().id(),
6868 multibuffer_editor_a.id()
6869 )
6870 });
6871
6872 // Client B activates an external window again, and the previously-opened screen-sharing item
6873 // gets activated.
6874 active_call_b
6875 .update(cx_b, |call, cx| call.set_location(None, cx))
6876 .await
6877 .unwrap();
6878 deterministic.run_until_parked();
6879 assert_eq!(
6880 workspace_a.read_with(cx_a, |workspace, cx| workspace
6881 .active_item(cx)
6882 .unwrap()
6883 .id()),
6884 shared_screen.id()
6885 );
6886
6887 // Following interrupts when client B disconnects.
6888 client_b.disconnect(&cx_b.to_async());
6889 deterministic.advance_clock(RECONNECT_TIMEOUT);
6890 assert_eq!(
6891 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6892 None
6893 );
6894}
6895
6896#[gpui::test(iterations = 10)]
6897async fn test_join_call_after_screen_was_shared(
6898 deterministic: Arc<Deterministic>,
6899 cx_a: &mut TestAppContext,
6900 cx_b: &mut TestAppContext,
6901) {
6902 deterministic.forbid_parking();
6903 let mut server = TestServer::start(&deterministic).await;
6904
6905 let client_a = server.create_client(cx_a, "user_a").await;
6906 let client_b = server.create_client(cx_b, "user_b").await;
6907 server
6908 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6909 .await;
6910
6911 let active_call_a = cx_a.read(ActiveCall::global);
6912 let active_call_b = cx_b.read(ActiveCall::global);
6913
6914 // Call users B and C from client A.
6915 active_call_a
6916 .update(cx_a, |call, cx| {
6917 call.invite(client_b.user_id().unwrap(), None, cx)
6918 })
6919 .await
6920 .unwrap();
6921 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6922 deterministic.run_until_parked();
6923 assert_eq!(
6924 room_participants(&room_a, cx_a),
6925 RoomParticipants {
6926 remote: Default::default(),
6927 pending: vec!["user_b".to_string()]
6928 }
6929 );
6930
6931 // User B receives the call.
6932 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6933 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6934 assert_eq!(call_b.calling_user.github_login, "user_a");
6935
6936 // User A shares their screen
6937 let display = MacOSDisplay::new();
6938 active_call_a
6939 .update(cx_a, |call, cx| {
6940 call.room().unwrap().update(cx, |room, cx| {
6941 room.set_display_sources(vec![display.clone()]);
6942 room.share_screen(cx)
6943 })
6944 })
6945 .await
6946 .unwrap();
6947
6948 client_b.user_store.update(cx_b, |user_store, _| {
6949 user_store.clear_cache();
6950 });
6951
6952 // User B joins the room
6953 active_call_b
6954 .update(cx_b, |call, cx| call.accept_incoming(cx))
6955 .await
6956 .unwrap();
6957 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6958 assert!(incoming_call_b.next().await.unwrap().is_none());
6959
6960 deterministic.run_until_parked();
6961 assert_eq!(
6962 room_participants(&room_a, cx_a),
6963 RoomParticipants {
6964 remote: vec!["user_b".to_string()],
6965 pending: vec![],
6966 }
6967 );
6968 assert_eq!(
6969 room_participants(&room_b, cx_b),
6970 RoomParticipants {
6971 remote: vec!["user_a".to_string()],
6972 pending: vec![],
6973 }
6974 );
6975
6976 // Ensure User B sees User A's screenshare.
6977 room_b.read_with(cx_b, |room, _| {
6978 assert_eq!(
6979 room.remote_participants()
6980 .get(&client_a.user_id().unwrap())
6981 .unwrap()
6982 .tracks
6983 .len(),
6984 1
6985 );
6986 });
6987}
6988
6989#[gpui::test]
6990async fn test_following_tab_order(
6991 deterministic: Arc<Deterministic>,
6992 cx_a: &mut TestAppContext,
6993 cx_b: &mut TestAppContext,
6994) {
6995 let mut server = TestServer::start(&deterministic).await;
6996 let client_a = server.create_client(cx_a, "user_a").await;
6997 let client_b = server.create_client(cx_b, "user_b").await;
6998 server
6999 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7000 .await;
7001 let active_call_a = cx_a.read(ActiveCall::global);
7002 let active_call_b = cx_b.read(ActiveCall::global);
7003
7004 cx_a.update(editor::init);
7005 cx_b.update(editor::init);
7006
7007 client_a
7008 .fs
7009 .insert_tree(
7010 "/a",
7011 json!({
7012 "1.txt": "one",
7013 "2.txt": "two",
7014 "3.txt": "three",
7015 }),
7016 )
7017 .await;
7018 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7019 active_call_a
7020 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7021 .await
7022 .unwrap();
7023
7024 let project_id = active_call_a
7025 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7026 .await
7027 .unwrap();
7028 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7029 active_call_b
7030 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7031 .await
7032 .unwrap();
7033
7034 let workspace_a = client_a.build_workspace(&project_a, cx_a);
7035 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
7036
7037 let workspace_b = client_b.build_workspace(&project_b, cx_b);
7038 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
7039
7040 let client_b_id = project_a.read_with(cx_a, |project, _| {
7041 project.collaborators().values().next().unwrap().peer_id
7042 });
7043
7044 //Open 1, 3 in that order on client A
7045 workspace_a
7046 .update(cx_a, |workspace, cx| {
7047 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
7048 })
7049 .await
7050 .unwrap();
7051 workspace_a
7052 .update(cx_a, |workspace, cx| {
7053 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
7054 })
7055 .await
7056 .unwrap();
7057
7058 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
7059 pane.update(cx, |pane, cx| {
7060 pane.items()
7061 .map(|item| {
7062 item.project_path(cx)
7063 .unwrap()
7064 .path
7065 .to_str()
7066 .unwrap()
7067 .to_owned()
7068 })
7069 .collect::<Vec<_>>()
7070 })
7071 };
7072
7073 //Verify that the tabs opened in the order we expect
7074 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
7075
7076 //Follow client B as client A
7077 workspace_a
7078 .update(cx_a, |workspace, cx| {
7079 workspace.toggle_follow(client_b_id, cx).unwrap()
7080 })
7081 .await
7082 .unwrap();
7083
7084 //Open just 2 on client B
7085 workspace_b
7086 .update(cx_b, |workspace, cx| {
7087 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
7088 })
7089 .await
7090 .unwrap();
7091 deterministic.run_until_parked();
7092
7093 // Verify that newly opened followed file is at the end
7094 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
7095
7096 //Open just 1 on client B
7097 workspace_b
7098 .update(cx_b, |workspace, cx| {
7099 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
7100 })
7101 .await
7102 .unwrap();
7103 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
7104 deterministic.run_until_parked();
7105
7106 // Verify that following into 1 did not reorder
7107 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
7108}
7109
7110#[gpui::test(iterations = 10)]
7111async fn test_peers_following_each_other(
7112 deterministic: Arc<Deterministic>,
7113 cx_a: &mut TestAppContext,
7114 cx_b: &mut TestAppContext,
7115) {
7116 deterministic.forbid_parking();
7117 let mut server = TestServer::start(&deterministic).await;
7118 let client_a = server.create_client(cx_a, "user_a").await;
7119 let client_b = server.create_client(cx_b, "user_b").await;
7120 server
7121 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7122 .await;
7123 let active_call_a = cx_a.read(ActiveCall::global);
7124 let active_call_b = cx_b.read(ActiveCall::global);
7125
7126 cx_a.update(editor::init);
7127 cx_b.update(editor::init);
7128
7129 // Client A shares a project.
7130 client_a
7131 .fs
7132 .insert_tree(
7133 "/a",
7134 json!({
7135 "1.txt": "one",
7136 "2.txt": "two",
7137 "3.txt": "three",
7138 "4.txt": "four",
7139 }),
7140 )
7141 .await;
7142 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7143 active_call_a
7144 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7145 .await
7146 .unwrap();
7147 let project_id = active_call_a
7148 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7149 .await
7150 .unwrap();
7151
7152 // Client B joins the project.
7153 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7154 active_call_b
7155 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7156 .await
7157 .unwrap();
7158
7159 // Client A opens some editors.
7160 let workspace_a = client_a.build_workspace(&project_a, cx_a);
7161 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
7162 let _editor_a1 = workspace_a
7163 .update(cx_a, |workspace, cx| {
7164 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
7165 })
7166 .await
7167 .unwrap()
7168 .downcast::<Editor>()
7169 .unwrap();
7170
7171 // Client B opens an editor.
7172 let workspace_b = client_b.build_workspace(&project_b, cx_b);
7173 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
7174 let _editor_b1 = workspace_b
7175 .update(cx_b, |workspace, cx| {
7176 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
7177 })
7178 .await
7179 .unwrap()
7180 .downcast::<Editor>()
7181 .unwrap();
7182
7183 // Clients A and B follow each other in split panes
7184 workspace_a.update(cx_a, |workspace, cx| {
7185 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7186 });
7187 workspace_a
7188 .update(cx_a, |workspace, cx| {
7189 assert_ne!(*workspace.active_pane(), pane_a1);
7190 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
7191 workspace.toggle_follow(leader_id, cx).unwrap()
7192 })
7193 .await
7194 .unwrap();
7195 workspace_b.update(cx_b, |workspace, cx| {
7196 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7197 });
7198 workspace_b
7199 .update(cx_b, |workspace, cx| {
7200 assert_ne!(*workspace.active_pane(), pane_b1);
7201 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
7202 workspace.toggle_follow(leader_id, cx).unwrap()
7203 })
7204 .await
7205 .unwrap();
7206
7207 workspace_a.update(cx_a, |workspace, cx| {
7208 workspace.activate_next_pane(cx);
7209 });
7210 // Wait for focus effects to be fully flushed
7211 workspace_a.update(cx_a, |workspace, _| {
7212 assert_eq!(*workspace.active_pane(), pane_a1);
7213 });
7214
7215 workspace_a
7216 .update(cx_a, |workspace, cx| {
7217 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
7218 })
7219 .await
7220 .unwrap();
7221 workspace_b.update(cx_b, |workspace, cx| {
7222 workspace.activate_next_pane(cx);
7223 });
7224
7225 workspace_b
7226 .update(cx_b, |workspace, cx| {
7227 assert_eq!(*workspace.active_pane(), pane_b1);
7228 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
7229 })
7230 .await
7231 .unwrap();
7232 cx_a.foreground().run_until_parked();
7233
7234 // Ensure leader updates don't change the active pane of followers
7235 workspace_a.read_with(cx_a, |workspace, _| {
7236 assert_eq!(*workspace.active_pane(), pane_a1);
7237 });
7238 workspace_b.read_with(cx_b, |workspace, _| {
7239 assert_eq!(*workspace.active_pane(), pane_b1);
7240 });
7241
7242 // Ensure peers following each other doesn't cause an infinite loop.
7243 assert_eq!(
7244 workspace_a.read_with(cx_a, |workspace, cx| workspace
7245 .active_item(cx)
7246 .unwrap()
7247 .project_path(cx)),
7248 Some((worktree_id, "3.txt").into())
7249 );
7250 workspace_a.update(cx_a, |workspace, cx| {
7251 assert_eq!(
7252 workspace.active_item(cx).unwrap().project_path(cx),
7253 Some((worktree_id, "3.txt").into())
7254 );
7255 workspace.activate_next_pane(cx);
7256 });
7257
7258 workspace_a.update(cx_a, |workspace, cx| {
7259 assert_eq!(
7260 workspace.active_item(cx).unwrap().project_path(cx),
7261 Some((worktree_id, "4.txt").into())
7262 );
7263 });
7264
7265 workspace_b.update(cx_b, |workspace, cx| {
7266 assert_eq!(
7267 workspace.active_item(cx).unwrap().project_path(cx),
7268 Some((worktree_id, "4.txt").into())
7269 );
7270 workspace.activate_next_pane(cx);
7271 });
7272
7273 workspace_b.update(cx_b, |workspace, cx| {
7274 assert_eq!(
7275 workspace.active_item(cx).unwrap().project_path(cx),
7276 Some((worktree_id, "3.txt").into())
7277 );
7278 });
7279}
7280
7281#[gpui::test(iterations = 10)]
7282async fn test_auto_unfollowing(
7283 deterministic: Arc<Deterministic>,
7284 cx_a: &mut TestAppContext,
7285 cx_b: &mut TestAppContext,
7286) {
7287 deterministic.forbid_parking();
7288
7289 // 2 clients connect to a server.
7290 let mut server = TestServer::start(&deterministic).await;
7291 let client_a = server.create_client(cx_a, "user_a").await;
7292 let client_b = server.create_client(cx_b, "user_b").await;
7293 server
7294 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7295 .await;
7296 let active_call_a = cx_a.read(ActiveCall::global);
7297 let active_call_b = cx_b.read(ActiveCall::global);
7298
7299 cx_a.update(editor::init);
7300 cx_b.update(editor::init);
7301
7302 // Client A shares a project.
7303 client_a
7304 .fs
7305 .insert_tree(
7306 "/a",
7307 json!({
7308 "1.txt": "one",
7309 "2.txt": "two",
7310 "3.txt": "three",
7311 }),
7312 )
7313 .await;
7314 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7315 active_call_a
7316 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7317 .await
7318 .unwrap();
7319
7320 let project_id = active_call_a
7321 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7322 .await
7323 .unwrap();
7324 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7325 active_call_b
7326 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7327 .await
7328 .unwrap();
7329
7330 // Client A opens some editors.
7331 let workspace_a = client_a.build_workspace(&project_a, cx_a);
7332 let _editor_a1 = workspace_a
7333 .update(cx_a, |workspace, cx| {
7334 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
7335 })
7336 .await
7337 .unwrap()
7338 .downcast::<Editor>()
7339 .unwrap();
7340
7341 // Client B starts following client A.
7342 let workspace_b = client_b.build_workspace(&project_b, cx_b);
7343 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
7344 let leader_id = project_b.read_with(cx_b, |project, _| {
7345 project.collaborators().values().next().unwrap().peer_id
7346 });
7347 workspace_b
7348 .update(cx_b, |workspace, cx| {
7349 workspace.toggle_follow(leader_id, cx).unwrap()
7350 })
7351 .await
7352 .unwrap();
7353 assert_eq!(
7354 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7355 Some(leader_id)
7356 );
7357 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
7358 workspace
7359 .active_item(cx)
7360 .unwrap()
7361 .downcast::<Editor>()
7362 .unwrap()
7363 });
7364
7365 // When client B moves, it automatically stops following client A.
7366 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
7367 assert_eq!(
7368 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7369 None
7370 );
7371
7372 workspace_b
7373 .update(cx_b, |workspace, cx| {
7374 workspace.toggle_follow(leader_id, cx).unwrap()
7375 })
7376 .await
7377 .unwrap();
7378 assert_eq!(
7379 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7380 Some(leader_id)
7381 );
7382
7383 // When client B edits, it automatically stops following client A.
7384 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
7385 assert_eq!(
7386 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7387 None
7388 );
7389
7390 workspace_b
7391 .update(cx_b, |workspace, cx| {
7392 workspace.toggle_follow(leader_id, cx).unwrap()
7393 })
7394 .await
7395 .unwrap();
7396 assert_eq!(
7397 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7398 Some(leader_id)
7399 );
7400
7401 // When client B scrolls, it automatically stops following client A.
7402 editor_b2.update(cx_b, |editor, cx| {
7403 editor.set_scroll_position(vec2f(0., 3.), cx)
7404 });
7405 assert_eq!(
7406 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7407 None
7408 );
7409
7410 workspace_b
7411 .update(cx_b, |workspace, cx| {
7412 workspace.toggle_follow(leader_id, cx).unwrap()
7413 })
7414 .await
7415 .unwrap();
7416 assert_eq!(
7417 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7418 Some(leader_id)
7419 );
7420
7421 // When client B activates a different pane, it continues following client A in the original pane.
7422 workspace_b.update(cx_b, |workspace, cx| {
7423 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
7424 });
7425 assert_eq!(
7426 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7427 Some(leader_id)
7428 );
7429
7430 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
7431 assert_eq!(
7432 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7433 Some(leader_id)
7434 );
7435
7436 // When client B activates a different item in the original pane, it automatically stops following client A.
7437 workspace_b
7438 .update(cx_b, |workspace, cx| {
7439 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
7440 })
7441 .await
7442 .unwrap();
7443 assert_eq!(
7444 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7445 None
7446 );
7447}
7448
7449#[gpui::test(iterations = 10)]
7450async fn test_peers_simultaneously_following_each_other(
7451 deterministic: Arc<Deterministic>,
7452 cx_a: &mut TestAppContext,
7453 cx_b: &mut TestAppContext,
7454) {
7455 deterministic.forbid_parking();
7456
7457 let mut server = TestServer::start(&deterministic).await;
7458 let client_a = server.create_client(cx_a, "user_a").await;
7459 let client_b = server.create_client(cx_b, "user_b").await;
7460 server
7461 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7462 .await;
7463 let active_call_a = cx_a.read(ActiveCall::global);
7464
7465 cx_a.update(editor::init);
7466 cx_b.update(editor::init);
7467
7468 client_a.fs.insert_tree("/a", json!({})).await;
7469 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
7470 let workspace_a = client_a.build_workspace(&project_a, cx_a);
7471 let project_id = active_call_a
7472 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7473 .await
7474 .unwrap();
7475
7476 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7477 let workspace_b = client_b.build_workspace(&project_b, cx_b);
7478
7479 deterministic.run_until_parked();
7480 let client_a_id = project_b.read_with(cx_b, |project, _| {
7481 project.collaborators().values().next().unwrap().peer_id
7482 });
7483 let client_b_id = project_a.read_with(cx_a, |project, _| {
7484 project.collaborators().values().next().unwrap().peer_id
7485 });
7486
7487 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
7488 workspace.toggle_follow(client_b_id, cx).unwrap()
7489 });
7490 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
7491 workspace.toggle_follow(client_a_id, cx).unwrap()
7492 });
7493
7494 futures::try_join!(a_follow_b, b_follow_a).unwrap();
7495 workspace_a.read_with(cx_a, |workspace, _| {
7496 assert_eq!(
7497 workspace.leader_for_pane(workspace.active_pane()),
7498 Some(client_b_id)
7499 );
7500 });
7501 workspace_b.read_with(cx_b, |workspace, _| {
7502 assert_eq!(
7503 workspace.leader_for_pane(workspace.active_pane()),
7504 Some(client_a_id)
7505 );
7506 });
7507}
7508
7509#[gpui::test(iterations = 10)]
7510async fn test_on_input_format_from_host_to_guest(
7511 deterministic: Arc<Deterministic>,
7512 cx_a: &mut TestAppContext,
7513 cx_b: &mut TestAppContext,
7514) {
7515 deterministic.forbid_parking();
7516 let mut server = TestServer::start(&deterministic).await;
7517 let client_a = server.create_client(cx_a, "user_a").await;
7518 let client_b = server.create_client(cx_b, "user_b").await;
7519 server
7520 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7521 .await;
7522 let active_call_a = cx_a.read(ActiveCall::global);
7523
7524 // Set up a fake language server.
7525 let mut language = Language::new(
7526 LanguageConfig {
7527 name: "Rust".into(),
7528 path_suffixes: vec!["rs".to_string()],
7529 ..Default::default()
7530 },
7531 Some(tree_sitter_rust::language()),
7532 );
7533 let mut fake_language_servers = language
7534 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7535 capabilities: lsp::ServerCapabilities {
7536 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
7537 first_trigger_character: ":".to_string(),
7538 more_trigger_character: Some(vec![">".to_string()]),
7539 }),
7540 ..Default::default()
7541 },
7542 ..Default::default()
7543 }))
7544 .await;
7545 client_a.language_registry.add(Arc::new(language));
7546
7547 client_a
7548 .fs
7549 .insert_tree(
7550 "/a",
7551 json!({
7552 "main.rs": "fn main() { a }",
7553 "other.rs": "// Test file",
7554 }),
7555 )
7556 .await;
7557 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7558 let project_id = active_call_a
7559 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7560 .await
7561 .unwrap();
7562 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7563
7564 // Open a file in an editor as the host.
7565 let buffer_a = project_a
7566 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
7567 .await
7568 .unwrap();
7569 let (window_a, _) = cx_a.add_window(|_| EmptyView);
7570 let editor_a = cx_a.add_view(window_a, |cx| {
7571 Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
7572 });
7573
7574 let fake_language_server = fake_language_servers.next().await.unwrap();
7575 cx_b.foreground().run_until_parked();
7576
7577 // Receive an OnTypeFormatting request as the host's language server.
7578 // Return some formattings from the host's language server.
7579 fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
7580 |params, _| async move {
7581 assert_eq!(
7582 params.text_document_position.text_document.uri,
7583 lsp::Url::from_file_path("/a/main.rs").unwrap(),
7584 );
7585 assert_eq!(
7586 params.text_document_position.position,
7587 lsp::Position::new(0, 14),
7588 );
7589
7590 Ok(Some(vec![lsp::TextEdit {
7591 new_text: "~<".to_string(),
7592 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
7593 }]))
7594 },
7595 );
7596
7597 // Open the buffer on the guest and see that the formattings worked
7598 let buffer_b = project_b
7599 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
7600 .await
7601 .unwrap();
7602
7603 // Type a on type formatting trigger character as the guest.
7604 editor_a.update(cx_a, |editor, cx| {
7605 cx.focus(&editor_a);
7606 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
7607 editor.handle_input(">", cx);
7608 });
7609
7610 cx_b.foreground().run_until_parked();
7611
7612 buffer_b.read_with(cx_b, |buffer, _| {
7613 assert_eq!(buffer.text(), "fn main() { a>~< }")
7614 });
7615
7616 // Undo should remove LSP edits first
7617 editor_a.update(cx_a, |editor, cx| {
7618 assert_eq!(editor.text(cx), "fn main() { a>~< }");
7619 editor.undo(&Undo, cx);
7620 assert_eq!(editor.text(cx), "fn main() { a> }");
7621 });
7622 cx_b.foreground().run_until_parked();
7623 buffer_b.read_with(cx_b, |buffer, _| {
7624 assert_eq!(buffer.text(), "fn main() { a> }")
7625 });
7626
7627 editor_a.update(cx_a, |editor, cx| {
7628 assert_eq!(editor.text(cx), "fn main() { a> }");
7629 editor.undo(&Undo, cx);
7630 assert_eq!(editor.text(cx), "fn main() { a }");
7631 });
7632 cx_b.foreground().run_until_parked();
7633 buffer_b.read_with(cx_b, |buffer, _| {
7634 assert_eq!(buffer.text(), "fn main() { a }")
7635 });
7636}
7637
7638#[gpui::test(iterations = 10)]
7639async fn test_on_input_format_from_guest_to_host(
7640 deterministic: Arc<Deterministic>,
7641 cx_a: &mut TestAppContext,
7642 cx_b: &mut TestAppContext,
7643) {
7644 deterministic.forbid_parking();
7645 let mut server = TestServer::start(&deterministic).await;
7646 let client_a = server.create_client(cx_a, "user_a").await;
7647 let client_b = server.create_client(cx_b, "user_b").await;
7648 server
7649 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7650 .await;
7651 let active_call_a = cx_a.read(ActiveCall::global);
7652
7653 // Set up a fake language server.
7654 let mut language = Language::new(
7655 LanguageConfig {
7656 name: "Rust".into(),
7657 path_suffixes: vec!["rs".to_string()],
7658 ..Default::default()
7659 },
7660 Some(tree_sitter_rust::language()),
7661 );
7662 let mut fake_language_servers = language
7663 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7664 capabilities: lsp::ServerCapabilities {
7665 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
7666 first_trigger_character: ":".to_string(),
7667 more_trigger_character: Some(vec![">".to_string()]),
7668 }),
7669 ..Default::default()
7670 },
7671 ..Default::default()
7672 }))
7673 .await;
7674 client_a.language_registry.add(Arc::new(language));
7675
7676 client_a
7677 .fs
7678 .insert_tree(
7679 "/a",
7680 json!({
7681 "main.rs": "fn main() { a }",
7682 "other.rs": "// Test file",
7683 }),
7684 )
7685 .await;
7686 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7687 let project_id = active_call_a
7688 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7689 .await
7690 .unwrap();
7691 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7692
7693 // Open a file in an editor as the guest.
7694 let buffer_b = project_b
7695 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
7696 .await
7697 .unwrap();
7698 let (window_b, _) = cx_b.add_window(|_| EmptyView);
7699 let editor_b = cx_b.add_view(window_b, |cx| {
7700 Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
7701 });
7702
7703 let fake_language_server = fake_language_servers.next().await.unwrap();
7704 cx_a.foreground().run_until_parked();
7705 // Type a on type formatting trigger character as the guest.
7706 editor_b.update(cx_b, |editor, cx| {
7707 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
7708 editor.handle_input(":", cx);
7709 cx.focus(&editor_b);
7710 });
7711
7712 // Receive an OnTypeFormatting request as the host's language server.
7713 // Return some formattings from the host's language server.
7714 cx_a.foreground().start_waiting();
7715 fake_language_server
7716 .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
7717 assert_eq!(
7718 params.text_document_position.text_document.uri,
7719 lsp::Url::from_file_path("/a/main.rs").unwrap(),
7720 );
7721 assert_eq!(
7722 params.text_document_position.position,
7723 lsp::Position::new(0, 14),
7724 );
7725
7726 Ok(Some(vec![lsp::TextEdit {
7727 new_text: "~:".to_string(),
7728 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
7729 }]))
7730 })
7731 .next()
7732 .await
7733 .unwrap();
7734 cx_a.foreground().finish_waiting();
7735
7736 // Open the buffer on the host and see that the formattings worked
7737 let buffer_a = project_a
7738 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
7739 .await
7740 .unwrap();
7741 cx_a.foreground().run_until_parked();
7742 buffer_a.read_with(cx_a, |buffer, _| {
7743 assert_eq!(buffer.text(), "fn main() { a:~: }")
7744 });
7745
7746 // Undo should remove LSP edits first
7747 editor_b.update(cx_b, |editor, cx| {
7748 assert_eq!(editor.text(cx), "fn main() { a:~: }");
7749 editor.undo(&Undo, cx);
7750 assert_eq!(editor.text(cx), "fn main() { a: }");
7751 });
7752 cx_a.foreground().run_until_parked();
7753 buffer_a.read_with(cx_a, |buffer, _| {
7754 assert_eq!(buffer.text(), "fn main() { a: }")
7755 });
7756
7757 editor_b.update(cx_b, |editor, cx| {
7758 assert_eq!(editor.text(cx), "fn main() { a: }");
7759 editor.undo(&Undo, cx);
7760 assert_eq!(editor.text(cx), "fn main() { a }");
7761 });
7762 cx_a.foreground().run_until_parked();
7763 buffer_a.read_with(cx_a, |buffer, _| {
7764 assert_eq!(buffer.text(), "fn main() { a }")
7765 });
7766}
7767
7768#[derive(Debug, Eq, PartialEq)]
7769struct RoomParticipants {
7770 remote: Vec<String>,
7771 pending: Vec<String>,
7772}
7773
7774fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
7775 room.read_with(cx, |room, _| {
7776 let mut remote = room
7777 .remote_participants()
7778 .iter()
7779 .map(|(_, participant)| participant.user.github_login.clone())
7780 .collect::<Vec<_>>();
7781 let mut pending = room
7782 .pending_participants()
7783 .iter()
7784 .map(|user| user.github_login.clone())
7785 .collect::<Vec<_>>();
7786 remote.sort();
7787 pending.sort();
7788 RoomParticipants { remote, pending }
7789 })
7790}