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