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