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