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