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