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