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