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),
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),
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),
2482 &buffer,
2483 &diff_base,
2484 &[(2..3, "", "three\n")],
2485 );
2486 });
2487
2488 // Smoke test B
2489 buffer_remote_a.read_with(cx_b, |buffer, _| {
2490 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2491 git::diff::assert_hunks(
2492 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2493 &buffer,
2494 &diff_base,
2495 &[(2..3, "", "three\n")],
2496 );
2497 });
2498
2499 //Nested git dir
2500
2501 let diff_base = "
2502 one
2503 three
2504 "
2505 .unindent();
2506
2507 let new_diff_base = "
2508 one
2509 two
2510 "
2511 .unindent();
2512
2513 client_a
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),
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),
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)
2584 .collect::<Vec<_>>()
2585 );
2586
2587 git::diff::assert_hunks(
2588 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
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),
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 deterministic.run_until_parked();
2689 project_remote_c.read_with(cx_c, |project, cx| {
2690 assert_branch(Some("branch-2"), project, cx)
2691 });
2692}
2693
2694#[gpui::test]
2695async fn test_git_status_sync(
2696 deterministic: Arc<Deterministic>,
2697 cx_a: &mut TestAppContext,
2698 cx_b: &mut TestAppContext,
2699 cx_c: &mut TestAppContext,
2700) {
2701 deterministic.forbid_parking();
2702 let mut server = TestServer::start(&deterministic).await;
2703 let client_a = server.create_client(cx_a, "user_a").await;
2704 let client_b = server.create_client(cx_b, "user_b").await;
2705 let client_c = server.create_client(cx_c, "user_c").await;
2706 server
2707 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2708 .await;
2709 let active_call_a = cx_a.read(ActiveCall::global);
2710
2711 client_a
2712 .fs
2713 .insert_tree(
2714 "/dir",
2715 json!({
2716 ".git": {},
2717 "a.txt": "a",
2718 "b.txt": "b",
2719 }),
2720 )
2721 .await;
2722
2723 const A_TXT: &'static str = "a.txt";
2724 const B_TXT: &'static str = "b.txt";
2725
2726 client_a
2727 .fs
2728 .as_fake()
2729 .set_status_for_repo(
2730 Path::new("/dir/.git"),
2731 &[
2732 (&Path::new(A_TXT), GitFileStatus::Added),
2733 (&Path::new(B_TXT), GitFileStatus::Added),
2734 ],
2735 )
2736 .await;
2737
2738 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2739 let project_id = active_call_a
2740 .update(cx_a, |call, cx| {
2741 call.share_project(project_local.clone(), cx)
2742 })
2743 .await
2744 .unwrap();
2745
2746 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2747
2748 // Wait for it to catch up to the new status
2749 deterministic.run_until_parked();
2750
2751 #[track_caller]
2752 fn assert_status(
2753 file: &impl AsRef<Path>,
2754 status: Option<GitFileStatus>,
2755 project: &Project,
2756 cx: &AppContext,
2757 ) {
2758 let file = file.as_ref();
2759 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2760 assert_eq!(worktrees.len(), 1);
2761 let worktree = worktrees[0].clone();
2762 let snapshot = worktree.read(cx).snapshot();
2763 let root_entry = snapshot.root_git_entry().unwrap();
2764 assert_eq!(root_entry.status_for_file(&snapshot, file), status);
2765 }
2766
2767 // Smoke test status reading
2768 project_local.read_with(cx_a, |project, cx| {
2769 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2770 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2771 });
2772 project_remote.read_with(cx_b, |project, cx| {
2773 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2774 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2775 });
2776
2777 client_a
2778 .fs
2779 .as_fake()
2780 .set_status_for_repo(
2781 Path::new("/dir/.git"),
2782 &[
2783 (&Path::new(A_TXT), GitFileStatus::Modified),
2784 (&Path::new(B_TXT), GitFileStatus::Modified),
2785 ],
2786 )
2787 .await;
2788
2789 // Wait for buffer_local_a to receive it
2790 deterministic.run_until_parked();
2791
2792 // Smoke test status reading
2793 project_local.read_with(cx_a, |project, cx| {
2794 assert_status(
2795 &Path::new(A_TXT),
2796 Some(GitFileStatus::Modified),
2797 project,
2798 cx,
2799 );
2800 assert_status(
2801 &Path::new(B_TXT),
2802 Some(GitFileStatus::Modified),
2803 project,
2804 cx,
2805 );
2806 });
2807 project_remote.read_with(cx_b, |project, cx| {
2808 assert_status(
2809 &Path::new(A_TXT),
2810 Some(GitFileStatus::Modified),
2811 project,
2812 cx,
2813 );
2814 assert_status(
2815 &Path::new(B_TXT),
2816 Some(GitFileStatus::Modified),
2817 project,
2818 cx,
2819 );
2820 });
2821
2822 // And synchronization while joining
2823 let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
2824 deterministic.run_until_parked();
2825
2826 project_remote_c.read_with(cx_c, |project, cx| {
2827 assert_status(
2828 &Path::new(A_TXT),
2829 Some(GitFileStatus::Modified),
2830 project,
2831 cx,
2832 );
2833 assert_status(
2834 &Path::new(B_TXT),
2835 Some(GitFileStatus::Modified),
2836 project,
2837 cx,
2838 );
2839 });
2840}
2841
2842#[gpui::test(iterations = 10)]
2843async fn test_fs_operations(
2844 deterministic: Arc<Deterministic>,
2845 cx_a: &mut TestAppContext,
2846 cx_b: &mut TestAppContext,
2847) {
2848 deterministic.forbid_parking();
2849 let mut server = TestServer::start(&deterministic).await;
2850 let client_a = server.create_client(cx_a, "user_a").await;
2851 let client_b = server.create_client(cx_b, "user_b").await;
2852 server
2853 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2854 .await;
2855 let active_call_a = cx_a.read(ActiveCall::global);
2856
2857 client_a
2858 .fs
2859 .insert_tree(
2860 "/dir",
2861 json!({
2862 "a.txt": "a-contents",
2863 "b.txt": "b-contents",
2864 }),
2865 )
2866 .await;
2867 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2868 let project_id = active_call_a
2869 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2870 .await
2871 .unwrap();
2872 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2873
2874 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
2875 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
2876
2877 let entry = project_b
2878 .update(cx_b, |project, cx| {
2879 project
2880 .create_entry((worktree_id, "c.txt"), false, cx)
2881 .unwrap()
2882 })
2883 .await
2884 .unwrap();
2885 worktree_a.read_with(cx_a, |worktree, _| {
2886 assert_eq!(
2887 worktree
2888 .paths()
2889 .map(|p| p.to_string_lossy())
2890 .collect::<Vec<_>>(),
2891 ["a.txt", "b.txt", "c.txt"]
2892 );
2893 });
2894 worktree_b.read_with(cx_b, |worktree, _| {
2895 assert_eq!(
2896 worktree
2897 .paths()
2898 .map(|p| p.to_string_lossy())
2899 .collect::<Vec<_>>(),
2900 ["a.txt", "b.txt", "c.txt"]
2901 );
2902 });
2903
2904 project_b
2905 .update(cx_b, |project, cx| {
2906 project.rename_entry(entry.id, Path::new("d.txt"), cx)
2907 })
2908 .unwrap()
2909 .await
2910 .unwrap();
2911 worktree_a.read_with(cx_a, |worktree, _| {
2912 assert_eq!(
2913 worktree
2914 .paths()
2915 .map(|p| p.to_string_lossy())
2916 .collect::<Vec<_>>(),
2917 ["a.txt", "b.txt", "d.txt"]
2918 );
2919 });
2920 worktree_b.read_with(cx_b, |worktree, _| {
2921 assert_eq!(
2922 worktree
2923 .paths()
2924 .map(|p| p.to_string_lossy())
2925 .collect::<Vec<_>>(),
2926 ["a.txt", "b.txt", "d.txt"]
2927 );
2928 });
2929
2930 let dir_entry = project_b
2931 .update(cx_b, |project, cx| {
2932 project
2933 .create_entry((worktree_id, "DIR"), true, cx)
2934 .unwrap()
2935 })
2936 .await
2937 .unwrap();
2938 worktree_a.read_with(cx_a, |worktree, _| {
2939 assert_eq!(
2940 worktree
2941 .paths()
2942 .map(|p| p.to_string_lossy())
2943 .collect::<Vec<_>>(),
2944 ["DIR", "a.txt", "b.txt", "d.txt"]
2945 );
2946 });
2947 worktree_b.read_with(cx_b, |worktree, _| {
2948 assert_eq!(
2949 worktree
2950 .paths()
2951 .map(|p| p.to_string_lossy())
2952 .collect::<Vec<_>>(),
2953 ["DIR", "a.txt", "b.txt", "d.txt"]
2954 );
2955 });
2956
2957 project_b
2958 .update(cx_b, |project, cx| {
2959 project
2960 .create_entry((worktree_id, "DIR/e.txt"), false, cx)
2961 .unwrap()
2962 })
2963 .await
2964 .unwrap();
2965 project_b
2966 .update(cx_b, |project, cx| {
2967 project
2968 .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
2969 .unwrap()
2970 })
2971 .await
2972 .unwrap();
2973 project_b
2974 .update(cx_b, |project, cx| {
2975 project
2976 .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
2977 .unwrap()
2978 })
2979 .await
2980 .unwrap();
2981 worktree_a.read_with(cx_a, |worktree, _| {
2982 assert_eq!(
2983 worktree
2984 .paths()
2985 .map(|p| p.to_string_lossy())
2986 .collect::<Vec<_>>(),
2987 [
2988 "DIR",
2989 "DIR/SUBDIR",
2990 "DIR/SUBDIR/f.txt",
2991 "DIR/e.txt",
2992 "a.txt",
2993 "b.txt",
2994 "d.txt"
2995 ]
2996 );
2997 });
2998 worktree_b.read_with(cx_b, |worktree, _| {
2999 assert_eq!(
3000 worktree
3001 .paths()
3002 .map(|p| p.to_string_lossy())
3003 .collect::<Vec<_>>(),
3004 [
3005 "DIR",
3006 "DIR/SUBDIR",
3007 "DIR/SUBDIR/f.txt",
3008 "DIR/e.txt",
3009 "a.txt",
3010 "b.txt",
3011 "d.txt"
3012 ]
3013 );
3014 });
3015
3016 project_b
3017 .update(cx_b, |project, cx| {
3018 project
3019 .copy_entry(entry.id, Path::new("f.txt"), cx)
3020 .unwrap()
3021 })
3022 .await
3023 .unwrap();
3024 worktree_a.read_with(cx_a, |worktree, _| {
3025 assert_eq!(
3026 worktree
3027 .paths()
3028 .map(|p| p.to_string_lossy())
3029 .collect::<Vec<_>>(),
3030 [
3031 "DIR",
3032 "DIR/SUBDIR",
3033 "DIR/SUBDIR/f.txt",
3034 "DIR/e.txt",
3035 "a.txt",
3036 "b.txt",
3037 "d.txt",
3038 "f.txt"
3039 ]
3040 );
3041 });
3042 worktree_b.read_with(cx_b, |worktree, _| {
3043 assert_eq!(
3044 worktree
3045 .paths()
3046 .map(|p| p.to_string_lossy())
3047 .collect::<Vec<_>>(),
3048 [
3049 "DIR",
3050 "DIR/SUBDIR",
3051 "DIR/SUBDIR/f.txt",
3052 "DIR/e.txt",
3053 "a.txt",
3054 "b.txt",
3055 "d.txt",
3056 "f.txt"
3057 ]
3058 );
3059 });
3060
3061 project_b
3062 .update(cx_b, |project, cx| {
3063 project.delete_entry(dir_entry.id, cx).unwrap()
3064 })
3065 .await
3066 .unwrap();
3067 deterministic.run_until_parked();
3068
3069 worktree_a.read_with(cx_a, |worktree, _| {
3070 assert_eq!(
3071 worktree
3072 .paths()
3073 .map(|p| p.to_string_lossy())
3074 .collect::<Vec<_>>(),
3075 ["a.txt", "b.txt", "d.txt", "f.txt"]
3076 );
3077 });
3078 worktree_b.read_with(cx_b, |worktree, _| {
3079 assert_eq!(
3080 worktree
3081 .paths()
3082 .map(|p| p.to_string_lossy())
3083 .collect::<Vec<_>>(),
3084 ["a.txt", "b.txt", "d.txt", "f.txt"]
3085 );
3086 });
3087
3088 project_b
3089 .update(cx_b, |project, cx| {
3090 project.delete_entry(entry.id, cx).unwrap()
3091 })
3092 .await
3093 .unwrap();
3094 worktree_a.read_with(cx_a, |worktree, _| {
3095 assert_eq!(
3096 worktree
3097 .paths()
3098 .map(|p| p.to_string_lossy())
3099 .collect::<Vec<_>>(),
3100 ["a.txt", "b.txt", "f.txt"]
3101 );
3102 });
3103 worktree_b.read_with(cx_b, |worktree, _| {
3104 assert_eq!(
3105 worktree
3106 .paths()
3107 .map(|p| p.to_string_lossy())
3108 .collect::<Vec<_>>(),
3109 ["a.txt", "b.txt", "f.txt"]
3110 );
3111 });
3112}
3113
3114#[gpui::test(iterations = 10)]
3115async fn test_buffer_conflict_after_save(
3116 deterministic: Arc<Deterministic>,
3117 cx_a: &mut TestAppContext,
3118 cx_b: &mut TestAppContext,
3119) {
3120 deterministic.forbid_parking();
3121 let mut server = TestServer::start(&deterministic).await;
3122 let client_a = server.create_client(cx_a, "user_a").await;
3123 let client_b = server.create_client(cx_b, "user_b").await;
3124 server
3125 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3126 .await;
3127 let active_call_a = cx_a.read(ActiveCall::global);
3128
3129 client_a
3130 .fs
3131 .insert_tree(
3132 "/dir",
3133 json!({
3134 "a.txt": "a-contents",
3135 }),
3136 )
3137 .await;
3138 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3139 let project_id = active_call_a
3140 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3141 .await
3142 .unwrap();
3143 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3144
3145 // Open a buffer as client B
3146 let buffer_b = project_b
3147 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3148 .await
3149 .unwrap();
3150
3151 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3152 buffer_b.read_with(cx_b, |buf, _| {
3153 assert!(buf.is_dirty());
3154 assert!(!buf.has_conflict());
3155 });
3156
3157 project_b
3158 .update(cx_b, |project, cx| {
3159 project.save_buffer(buffer_b.clone(), cx)
3160 })
3161 .await
3162 .unwrap();
3163 cx_a.foreground().forbid_parking();
3164 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3165 buffer_b.read_with(cx_b, |buf, _| {
3166 assert!(!buf.has_conflict());
3167 });
3168
3169 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3170 buffer_b.read_with(cx_b, |buf, _| {
3171 assert!(buf.is_dirty());
3172 assert!(!buf.has_conflict());
3173 });
3174}
3175
3176#[gpui::test(iterations = 10)]
3177async fn test_buffer_reloading(
3178 deterministic: Arc<Deterministic>,
3179 cx_a: &mut TestAppContext,
3180 cx_b: &mut TestAppContext,
3181) {
3182 deterministic.forbid_parking();
3183 let mut server = TestServer::start(&deterministic).await;
3184 let client_a = server.create_client(cx_a, "user_a").await;
3185 let client_b = server.create_client(cx_b, "user_b").await;
3186 server
3187 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3188 .await;
3189 let active_call_a = cx_a.read(ActiveCall::global);
3190
3191 client_a
3192 .fs
3193 .insert_tree(
3194 "/dir",
3195 json!({
3196 "a.txt": "a\nb\nc",
3197 }),
3198 )
3199 .await;
3200 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3201 let project_id = active_call_a
3202 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3203 .await
3204 .unwrap();
3205 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3206
3207 // Open a buffer as client B
3208 let buffer_b = project_b
3209 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3210 .await
3211 .unwrap();
3212 buffer_b.read_with(cx_b, |buf, _| {
3213 assert!(!buf.is_dirty());
3214 assert!(!buf.has_conflict());
3215 assert_eq!(buf.line_ending(), LineEnding::Unix);
3216 });
3217
3218 let new_contents = Rope::from("d\ne\nf");
3219 client_a
3220 .fs
3221 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3222 .await
3223 .unwrap();
3224 cx_a.foreground().run_until_parked();
3225 buffer_b.read_with(cx_b, |buf, _| {
3226 assert_eq!(buf.text(), new_contents.to_string());
3227 assert!(!buf.is_dirty());
3228 assert!(!buf.has_conflict());
3229 assert_eq!(buf.line_ending(), LineEnding::Windows);
3230 });
3231}
3232
3233#[gpui::test(iterations = 10)]
3234async fn test_editing_while_guest_opens_buffer(
3235 deterministic: Arc<Deterministic>,
3236 cx_a: &mut TestAppContext,
3237 cx_b: &mut TestAppContext,
3238) {
3239 deterministic.forbid_parking();
3240 let mut server = TestServer::start(&deterministic).await;
3241 let client_a = server.create_client(cx_a, "user_a").await;
3242 let client_b = server.create_client(cx_b, "user_b").await;
3243 server
3244 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3245 .await;
3246 let active_call_a = cx_a.read(ActiveCall::global);
3247
3248 client_a
3249 .fs
3250 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3251 .await;
3252 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3253 let project_id = active_call_a
3254 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3255 .await
3256 .unwrap();
3257 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3258
3259 // Open a buffer as client A
3260 let buffer_a = project_a
3261 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3262 .await
3263 .unwrap();
3264
3265 // Start opening the same buffer as client B
3266 let buffer_b = cx_b
3267 .background()
3268 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3269
3270 // Edit the buffer as client A while client B is still opening it.
3271 cx_b.background().simulate_random_delay().await;
3272 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3273 cx_b.background().simulate_random_delay().await;
3274 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3275
3276 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3277 let buffer_b = buffer_b.await.unwrap();
3278 cx_a.foreground().run_until_parked();
3279 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3280}
3281
3282#[gpui::test]
3283async fn test_newline_above_or_below_does_not_move_guest_cursor(
3284 deterministic: Arc<Deterministic>,
3285 cx_a: &mut TestAppContext,
3286 cx_b: &mut TestAppContext,
3287) {
3288 deterministic.forbid_parking();
3289 let mut server = TestServer::start(&deterministic).await;
3290 let client_a = server.create_client(cx_a, "user_a").await;
3291 let client_b = server.create_client(cx_b, "user_b").await;
3292 server
3293 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3294 .await;
3295 let active_call_a = cx_a.read(ActiveCall::global);
3296
3297 client_a
3298 .fs
3299 .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
3300 .await;
3301 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3302 let project_id = active_call_a
3303 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3304 .await
3305 .unwrap();
3306
3307 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3308
3309 // Open a buffer as client A
3310 let buffer_a = project_a
3311 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3312 .await
3313 .unwrap();
3314 let (window_a, _) = cx_a.add_window(|_| EmptyView);
3315 let editor_a = cx_a.add_view(window_a, |cx| {
3316 Editor::for_buffer(buffer_a, Some(project_a), cx)
3317 });
3318 let mut editor_cx_a = EditorTestContext {
3319 cx: cx_a,
3320 window_id: window_a,
3321 editor: editor_a,
3322 };
3323
3324 // Open a buffer as client B
3325 let buffer_b = project_b
3326 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3327 .await
3328 .unwrap();
3329 let (window_b, _) = cx_b.add_window(|_| EmptyView);
3330 let editor_b = cx_b.add_view(window_b, |cx| {
3331 Editor::for_buffer(buffer_b, Some(project_b), cx)
3332 });
3333 let mut editor_cx_b = EditorTestContext {
3334 cx: cx_b,
3335 window_id: window_b,
3336 editor: editor_b,
3337 };
3338
3339 // Test newline above
3340 editor_cx_a.set_selections_state(indoc! {"
3341 Some textˇ
3342 "});
3343 editor_cx_b.set_selections_state(indoc! {"
3344 Some textˇ
3345 "});
3346 editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
3347 deterministic.run_until_parked();
3348 editor_cx_a.assert_editor_state(indoc! {"
3349 ˇ
3350 Some text
3351 "});
3352 editor_cx_b.assert_editor_state(indoc! {"
3353
3354 Some textˇ
3355 "});
3356
3357 // Test newline below
3358 editor_cx_a.set_selections_state(indoc! {"
3359
3360 Some textˇ
3361 "});
3362 editor_cx_b.set_selections_state(indoc! {"
3363
3364 Some textˇ
3365 "});
3366 editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
3367 deterministic.run_until_parked();
3368 editor_cx_a.assert_editor_state(indoc! {"
3369
3370 Some text
3371 ˇ
3372 "});
3373 editor_cx_b.assert_editor_state(indoc! {"
3374
3375 Some textˇ
3376
3377 "});
3378}
3379
3380#[gpui::test(iterations = 10)]
3381async fn test_leaving_worktree_while_opening_buffer(
3382 deterministic: Arc<Deterministic>,
3383 cx_a: &mut TestAppContext,
3384 cx_b: &mut TestAppContext,
3385) {
3386 deterministic.forbid_parking();
3387 let mut server = TestServer::start(&deterministic).await;
3388 let client_a = server.create_client(cx_a, "user_a").await;
3389 let client_b = server.create_client(cx_b, "user_b").await;
3390 server
3391 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3392 .await;
3393 let active_call_a = cx_a.read(ActiveCall::global);
3394
3395 client_a
3396 .fs
3397 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3398 .await;
3399 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3400 let project_id = active_call_a
3401 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3402 .await
3403 .unwrap();
3404 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3405
3406 // See that a guest has joined as client A.
3407 cx_a.foreground().run_until_parked();
3408 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3409
3410 // Begin opening a buffer as client B, but leave the project before the open completes.
3411 let buffer_b = cx_b
3412 .background()
3413 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3414 cx_b.update(|_| drop(project_b));
3415 drop(buffer_b);
3416
3417 // See that the guest has left.
3418 cx_a.foreground().run_until_parked();
3419 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3420}
3421
3422#[gpui::test(iterations = 10)]
3423async fn test_canceling_buffer_opening(
3424 deterministic: Arc<Deterministic>,
3425 cx_a: &mut TestAppContext,
3426 cx_b: &mut TestAppContext,
3427) {
3428 deterministic.forbid_parking();
3429
3430 let mut server = TestServer::start(&deterministic).await;
3431 let client_a = server.create_client(cx_a, "user_a").await;
3432 let client_b = server.create_client(cx_b, "user_b").await;
3433 server
3434 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3435 .await;
3436 let active_call_a = cx_a.read(ActiveCall::global);
3437
3438 client_a
3439 .fs
3440 .insert_tree(
3441 "/dir",
3442 json!({
3443 "a.txt": "abc",
3444 }),
3445 )
3446 .await;
3447 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3448 let project_id = active_call_a
3449 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3450 .await
3451 .unwrap();
3452 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3453
3454 let buffer_a = project_a
3455 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3456 .await
3457 .unwrap();
3458
3459 // Open a buffer as client B but cancel after a random amount of time.
3460 let buffer_b = project_b.update(cx_b, |p, cx| {
3461 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3462 });
3463 deterministic.simulate_random_delay().await;
3464 drop(buffer_b);
3465
3466 // Try opening the same buffer again as client B, and ensure we can
3467 // still do it despite the cancellation above.
3468 let buffer_b = project_b
3469 .update(cx_b, |p, cx| {
3470 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3471 })
3472 .await
3473 .unwrap();
3474 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3475}
3476
3477#[gpui::test(iterations = 10)]
3478async fn test_leaving_project(
3479 deterministic: Arc<Deterministic>,
3480 cx_a: &mut TestAppContext,
3481 cx_b: &mut TestAppContext,
3482 cx_c: &mut TestAppContext,
3483) {
3484 deterministic.forbid_parking();
3485 let mut server = TestServer::start(&deterministic).await;
3486 let client_a = server.create_client(cx_a, "user_a").await;
3487 let client_b = server.create_client(cx_b, "user_b").await;
3488 let client_c = server.create_client(cx_c, "user_c").await;
3489 server
3490 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3491 .await;
3492 let active_call_a = cx_a.read(ActiveCall::global);
3493
3494 client_a
3495 .fs
3496 .insert_tree(
3497 "/a",
3498 json!({
3499 "a.txt": "a-contents",
3500 "b.txt": "b-contents",
3501 }),
3502 )
3503 .await;
3504 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3505 let project_id = active_call_a
3506 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3507 .await
3508 .unwrap();
3509 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3510 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3511
3512 // Client A sees that a guest has joined.
3513 deterministic.run_until_parked();
3514 project_a.read_with(cx_a, |project, _| {
3515 assert_eq!(project.collaborators().len(), 2);
3516 });
3517 project_b1.read_with(cx_b, |project, _| {
3518 assert_eq!(project.collaborators().len(), 2);
3519 });
3520 project_c.read_with(cx_c, |project, _| {
3521 assert_eq!(project.collaborators().len(), 2);
3522 });
3523
3524 // Client B opens a buffer.
3525 let buffer_b1 = project_b1
3526 .update(cx_b, |project, cx| {
3527 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3528 project.open_buffer((worktree_id, "a.txt"), cx)
3529 })
3530 .await
3531 .unwrap();
3532 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3533
3534 // Drop client B's project and ensure client A and client C observe client B leaving.
3535 cx_b.update(|_| drop(project_b1));
3536 deterministic.run_until_parked();
3537 project_a.read_with(cx_a, |project, _| {
3538 assert_eq!(project.collaborators().len(), 1);
3539 });
3540 project_c.read_with(cx_c, |project, _| {
3541 assert_eq!(project.collaborators().len(), 1);
3542 });
3543
3544 // Client B re-joins the project and can open buffers as before.
3545 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3546 deterministic.run_until_parked();
3547 project_a.read_with(cx_a, |project, _| {
3548 assert_eq!(project.collaborators().len(), 2);
3549 });
3550 project_b2.read_with(cx_b, |project, _| {
3551 assert_eq!(project.collaborators().len(), 2);
3552 });
3553 project_c.read_with(cx_c, |project, _| {
3554 assert_eq!(project.collaborators().len(), 2);
3555 });
3556
3557 let buffer_b2 = project_b2
3558 .update(cx_b, |project, cx| {
3559 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3560 project.open_buffer((worktree_id, "a.txt"), cx)
3561 })
3562 .await
3563 .unwrap();
3564 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3565
3566 // Drop client B's connection and ensure client A and client C observe client B leaving.
3567 client_b.disconnect(&cx_b.to_async());
3568 deterministic.advance_clock(RECONNECT_TIMEOUT);
3569 project_a.read_with(cx_a, |project, _| {
3570 assert_eq!(project.collaborators().len(), 1);
3571 });
3572 project_b2.read_with(cx_b, |project, _| {
3573 assert!(project.is_read_only());
3574 });
3575 project_c.read_with(cx_c, |project, _| {
3576 assert_eq!(project.collaborators().len(), 1);
3577 });
3578
3579 // Client B can't join the project, unless they re-join the room.
3580 cx_b.spawn(|cx| {
3581 Project::remote(
3582 project_id,
3583 client_b.client.clone(),
3584 client_b.user_store.clone(),
3585 client_b.language_registry.clone(),
3586 FakeFs::new(cx.background()),
3587 cx,
3588 )
3589 })
3590 .await
3591 .unwrap_err();
3592
3593 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3594 client_c.wait_for_current_user(cx_c).await;
3595 server.forbid_connections();
3596 server.disconnect_client(client_c.peer_id().unwrap());
3597 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3598 deterministic.run_until_parked();
3599 project_a.read_with(cx_a, |project, _| {
3600 assert_eq!(project.collaborators().len(), 0);
3601 });
3602 project_b2.read_with(cx_b, |project, _| {
3603 assert!(project.is_read_only());
3604 });
3605 project_c.read_with(cx_c, |project, _| {
3606 assert!(project.is_read_only());
3607 });
3608}
3609
3610#[gpui::test(iterations = 10)]
3611async fn test_collaborating_with_diagnostics(
3612 deterministic: Arc<Deterministic>,
3613 cx_a: &mut TestAppContext,
3614 cx_b: &mut TestAppContext,
3615 cx_c: &mut TestAppContext,
3616) {
3617 deterministic.forbid_parking();
3618 let mut server = TestServer::start(&deterministic).await;
3619 let client_a = server.create_client(cx_a, "user_a").await;
3620 let client_b = server.create_client(cx_b, "user_b").await;
3621 let client_c = server.create_client(cx_c, "user_c").await;
3622 server
3623 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3624 .await;
3625 let active_call_a = cx_a.read(ActiveCall::global);
3626
3627 // Set up a fake language server.
3628 let mut language = Language::new(
3629 LanguageConfig {
3630 name: "Rust".into(),
3631 path_suffixes: vec!["rs".to_string()],
3632 ..Default::default()
3633 },
3634 Some(tree_sitter_rust::language()),
3635 );
3636 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3637 client_a.language_registry.add(Arc::new(language));
3638
3639 // Share a project as client A
3640 client_a
3641 .fs
3642 .insert_tree(
3643 "/a",
3644 json!({
3645 "a.rs": "let one = two",
3646 "other.rs": "",
3647 }),
3648 )
3649 .await;
3650 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3651
3652 // Cause the language server to start.
3653 let _buffer = project_a
3654 .update(cx_a, |project, cx| {
3655 project.open_buffer(
3656 ProjectPath {
3657 worktree_id,
3658 path: Path::new("other.rs").into(),
3659 },
3660 cx,
3661 )
3662 })
3663 .await
3664 .unwrap();
3665
3666 // Simulate a language server reporting errors for a file.
3667 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3668 fake_language_server
3669 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3670 .await;
3671 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3672 lsp::PublishDiagnosticsParams {
3673 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3674 version: None,
3675 diagnostics: vec![lsp::Diagnostic {
3676 severity: Some(lsp::DiagnosticSeverity::WARNING),
3677 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3678 message: "message 0".to_string(),
3679 ..Default::default()
3680 }],
3681 },
3682 );
3683
3684 // Client A shares the project and, simultaneously, the language server
3685 // publishes a diagnostic. This is done to ensure that the server always
3686 // observes the latest diagnostics for a worktree.
3687 let project_id = active_call_a
3688 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3689 .await
3690 .unwrap();
3691 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3692 lsp::PublishDiagnosticsParams {
3693 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3694 version: None,
3695 diagnostics: vec![lsp::Diagnostic {
3696 severity: Some(lsp::DiagnosticSeverity::ERROR),
3697 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3698 message: "message 1".to_string(),
3699 ..Default::default()
3700 }],
3701 },
3702 );
3703
3704 // Join the worktree as client B.
3705 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3706
3707 // Wait for server to see the diagnostics update.
3708 deterministic.run_until_parked();
3709
3710 // Ensure client B observes the new diagnostics.
3711 project_b.read_with(cx_b, |project, cx| {
3712 assert_eq!(
3713 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3714 &[(
3715 ProjectPath {
3716 worktree_id,
3717 path: Arc::from(Path::new("a.rs")),
3718 },
3719 LanguageServerId(0),
3720 DiagnosticSummary {
3721 error_count: 1,
3722 warning_count: 0,
3723 ..Default::default()
3724 },
3725 )]
3726 )
3727 });
3728
3729 // Join project as client C and observe the diagnostics.
3730 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3731 let project_c_diagnostic_summaries =
3732 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3733 project.diagnostic_summaries(cx).collect::<Vec<_>>()
3734 })));
3735 project_c.update(cx_c, |_, cx| {
3736 let summaries = project_c_diagnostic_summaries.clone();
3737 cx.subscribe(&project_c, {
3738 move |p, _, event, cx| {
3739 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3740 *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
3741 }
3742 }
3743 })
3744 .detach();
3745 });
3746
3747 deterministic.run_until_parked();
3748 assert_eq!(
3749 project_c_diagnostic_summaries.borrow().as_slice(),
3750 &[(
3751 ProjectPath {
3752 worktree_id,
3753 path: Arc::from(Path::new("a.rs")),
3754 },
3755 LanguageServerId(0),
3756 DiagnosticSummary {
3757 error_count: 1,
3758 warning_count: 0,
3759 ..Default::default()
3760 },
3761 )]
3762 );
3763
3764 // Simulate a language server reporting more errors for a file.
3765 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3766 lsp::PublishDiagnosticsParams {
3767 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3768 version: None,
3769 diagnostics: vec![
3770 lsp::Diagnostic {
3771 severity: Some(lsp::DiagnosticSeverity::ERROR),
3772 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3773 message: "message 1".to_string(),
3774 ..Default::default()
3775 },
3776 lsp::Diagnostic {
3777 severity: Some(lsp::DiagnosticSeverity::WARNING),
3778 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3779 message: "message 2".to_string(),
3780 ..Default::default()
3781 },
3782 ],
3783 },
3784 );
3785
3786 // Clients B and C get the updated summaries
3787 deterministic.run_until_parked();
3788 project_b.read_with(cx_b, |project, cx| {
3789 assert_eq!(
3790 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3791 [(
3792 ProjectPath {
3793 worktree_id,
3794 path: Arc::from(Path::new("a.rs")),
3795 },
3796 LanguageServerId(0),
3797 DiagnosticSummary {
3798 error_count: 1,
3799 warning_count: 1,
3800 },
3801 )]
3802 );
3803 });
3804 project_c.read_with(cx_c, |project, cx| {
3805 assert_eq!(
3806 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3807 [(
3808 ProjectPath {
3809 worktree_id,
3810 path: Arc::from(Path::new("a.rs")),
3811 },
3812 LanguageServerId(0),
3813 DiagnosticSummary {
3814 error_count: 1,
3815 warning_count: 1,
3816 },
3817 )]
3818 );
3819 });
3820
3821 // Open the file with the errors on client B. They should be present.
3822 let buffer_b = cx_b
3823 .background()
3824 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3825 .await
3826 .unwrap();
3827
3828 buffer_b.read_with(cx_b, |buffer, _| {
3829 assert_eq!(
3830 buffer
3831 .snapshot()
3832 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3833 .collect::<Vec<_>>(),
3834 &[
3835 DiagnosticEntry {
3836 range: Point::new(0, 4)..Point::new(0, 7),
3837 diagnostic: Diagnostic {
3838 group_id: 2,
3839 message: "message 1".to_string(),
3840 severity: lsp::DiagnosticSeverity::ERROR,
3841 is_primary: true,
3842 ..Default::default()
3843 }
3844 },
3845 DiagnosticEntry {
3846 range: Point::new(0, 10)..Point::new(0, 13),
3847 diagnostic: Diagnostic {
3848 group_id: 3,
3849 severity: lsp::DiagnosticSeverity::WARNING,
3850 message: "message 2".to_string(),
3851 is_primary: true,
3852 ..Default::default()
3853 }
3854 }
3855 ]
3856 );
3857 });
3858
3859 // Simulate a language server reporting no errors for a file.
3860 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3861 lsp::PublishDiagnosticsParams {
3862 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3863 version: None,
3864 diagnostics: vec![],
3865 },
3866 );
3867 deterministic.run_until_parked();
3868 project_a.read_with(cx_a, |project, cx| {
3869 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3870 });
3871 project_b.read_with(cx_b, |project, cx| {
3872 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3873 });
3874 project_c.read_with(cx_c, |project, cx| {
3875 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3876 });
3877}
3878
3879#[gpui::test(iterations = 10)]
3880async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
3881 deterministic: Arc<Deterministic>,
3882 cx_a: &mut TestAppContext,
3883 cx_b: &mut TestAppContext,
3884) {
3885 deterministic.forbid_parking();
3886 let mut server = TestServer::start(&deterministic).await;
3887 let client_a = server.create_client(cx_a, "user_a").await;
3888 let client_b = server.create_client(cx_b, "user_b").await;
3889 server
3890 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3891 .await;
3892
3893 // Set up a fake language server.
3894 let mut language = Language::new(
3895 LanguageConfig {
3896 name: "Rust".into(),
3897 path_suffixes: vec!["rs".to_string()],
3898 ..Default::default()
3899 },
3900 Some(tree_sitter_rust::language()),
3901 );
3902 let mut fake_language_servers = language
3903 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3904 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
3905 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
3906 ..Default::default()
3907 }))
3908 .await;
3909 client_a.language_registry.add(Arc::new(language));
3910
3911 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
3912 client_a
3913 .fs
3914 .insert_tree(
3915 "/test",
3916 json!({
3917 "one.rs": "const ONE: usize = 1;",
3918 "two.rs": "const TWO: usize = 2;",
3919 "three.rs": "const THREE: usize = 3;",
3920 "four.rs": "const FOUR: usize = 3;",
3921 "five.rs": "const FIVE: usize = 3;",
3922 }),
3923 )
3924 .await;
3925
3926 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
3927
3928 // Share a project as client A
3929 let active_call_a = cx_a.read(ActiveCall::global);
3930 let project_id = active_call_a
3931 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3932 .await
3933 .unwrap();
3934
3935 // Join the project as client B and open all three files.
3936 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3937 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
3938 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
3939 }))
3940 .await
3941 .unwrap();
3942
3943 // Simulate a language server reporting errors for a file.
3944 let fake_language_server = fake_language_servers.next().await.unwrap();
3945 fake_language_server
3946 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
3947 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3948 })
3949 .await
3950 .unwrap();
3951 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3952 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3953 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
3954 lsp::WorkDoneProgressBegin {
3955 title: "Progress Began".into(),
3956 ..Default::default()
3957 },
3958 )),
3959 });
3960 for file_name in file_names {
3961 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3962 lsp::PublishDiagnosticsParams {
3963 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
3964 version: None,
3965 diagnostics: vec![lsp::Diagnostic {
3966 severity: Some(lsp::DiagnosticSeverity::WARNING),
3967 source: Some("the-disk-based-diagnostics-source".into()),
3968 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3969 message: "message one".to_string(),
3970 ..Default::default()
3971 }],
3972 },
3973 );
3974 }
3975 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3976 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3977 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
3978 lsp::WorkDoneProgressEnd { message: None },
3979 )),
3980 });
3981
3982 // When the "disk base diagnostics finished" message is received, the buffers'
3983 // diagnostics are expected to be present.
3984 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
3985 project_b.update(cx_b, {
3986 let project_b = project_b.clone();
3987 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
3988 move |_, cx| {
3989 cx.subscribe(&project_b, move |_, _, event, cx| {
3990 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3991 disk_based_diagnostics_finished.store(true, SeqCst);
3992 for buffer in &guest_buffers {
3993 assert_eq!(
3994 buffer
3995 .read(cx)
3996 .snapshot()
3997 .diagnostics_in_range::<_, usize>(0..5, false)
3998 .count(),
3999 1,
4000 "expected a diagnostic for buffer {:?}",
4001 buffer.read(cx).file().unwrap().path(),
4002 );
4003 }
4004 }
4005 })
4006 .detach();
4007 }
4008 });
4009
4010 deterministic.run_until_parked();
4011 assert!(disk_based_diagnostics_finished.load(SeqCst));
4012}
4013
4014#[gpui::test(iterations = 10)]
4015async fn test_collaborating_with_completion(
4016 deterministic: Arc<Deterministic>,
4017 cx_a: &mut TestAppContext,
4018 cx_b: &mut TestAppContext,
4019) {
4020 deterministic.forbid_parking();
4021 let mut server = TestServer::start(&deterministic).await;
4022 let client_a = server.create_client(cx_a, "user_a").await;
4023 let client_b = server.create_client(cx_b, "user_b").await;
4024 server
4025 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4026 .await;
4027 let active_call_a = cx_a.read(ActiveCall::global);
4028
4029 // Set up a fake language server.
4030 let mut language = Language::new(
4031 LanguageConfig {
4032 name: "Rust".into(),
4033 path_suffixes: vec!["rs".to_string()],
4034 ..Default::default()
4035 },
4036 Some(tree_sitter_rust::language()),
4037 );
4038 let mut fake_language_servers = language
4039 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4040 capabilities: lsp::ServerCapabilities {
4041 completion_provider: Some(lsp::CompletionOptions {
4042 trigger_characters: Some(vec![".".to_string()]),
4043 ..Default::default()
4044 }),
4045 ..Default::default()
4046 },
4047 ..Default::default()
4048 }))
4049 .await;
4050 client_a.language_registry.add(Arc::new(language));
4051
4052 client_a
4053 .fs
4054 .insert_tree(
4055 "/a",
4056 json!({
4057 "main.rs": "fn main() { a }",
4058 "other.rs": "",
4059 }),
4060 )
4061 .await;
4062 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4063 let project_id = active_call_a
4064 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4065 .await
4066 .unwrap();
4067 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4068
4069 // Open a file in an editor as the guest.
4070 let buffer_b = project_b
4071 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
4072 .await
4073 .unwrap();
4074 let (window_b, _) = cx_b.add_window(|_| EmptyView);
4075 let editor_b = cx_b.add_view(window_b, |cx| {
4076 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
4077 });
4078
4079 let fake_language_server = fake_language_servers.next().await.unwrap();
4080 cx_a.foreground().run_until_parked();
4081 buffer_b.read_with(cx_b, |buffer, _| {
4082 assert!(!buffer.completion_triggers().is_empty())
4083 });
4084
4085 // Type a completion trigger character as the guest.
4086 editor_b.update(cx_b, |editor, cx| {
4087 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
4088 editor.handle_input(".", cx);
4089 cx.focus(&editor_b);
4090 });
4091
4092 // Receive a completion request as the host's language server.
4093 // Return some completions from the host's language server.
4094 cx_a.foreground().start_waiting();
4095 fake_language_server
4096 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
4097 assert_eq!(
4098 params.text_document_position.text_document.uri,
4099 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4100 );
4101 assert_eq!(
4102 params.text_document_position.position,
4103 lsp::Position::new(0, 14),
4104 );
4105
4106 Ok(Some(lsp::CompletionResponse::Array(vec![
4107 lsp::CompletionItem {
4108 label: "first_method(…)".into(),
4109 detail: Some("fn(&mut self, B) -> C".into()),
4110 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4111 new_text: "first_method($1)".to_string(),
4112 range: lsp::Range::new(
4113 lsp::Position::new(0, 14),
4114 lsp::Position::new(0, 14),
4115 ),
4116 })),
4117 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4118 ..Default::default()
4119 },
4120 lsp::CompletionItem {
4121 label: "second_method(…)".into(),
4122 detail: Some("fn(&mut self, C) -> D<E>".into()),
4123 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4124 new_text: "second_method()".to_string(),
4125 range: lsp::Range::new(
4126 lsp::Position::new(0, 14),
4127 lsp::Position::new(0, 14),
4128 ),
4129 })),
4130 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4131 ..Default::default()
4132 },
4133 ])))
4134 })
4135 .next()
4136 .await
4137 .unwrap();
4138 cx_a.foreground().finish_waiting();
4139
4140 // Open the buffer on the host.
4141 let buffer_a = project_a
4142 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
4143 .await
4144 .unwrap();
4145 cx_a.foreground().run_until_parked();
4146 buffer_a.read_with(cx_a, |buffer, _| {
4147 assert_eq!(buffer.text(), "fn main() { a. }")
4148 });
4149
4150 // Confirm a completion on the guest.
4151 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
4152 editor_b.update(cx_b, |editor, cx| {
4153 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
4154 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
4155 });
4156
4157 // Return a resolved completion from the host's language server.
4158 // The resolved completion has an additional text edit.
4159 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
4160 |params, _| async move {
4161 assert_eq!(params.label, "first_method(…)");
4162 Ok(lsp::CompletionItem {
4163 label: "first_method(…)".into(),
4164 detail: Some("fn(&mut self, B) -> C".into()),
4165 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4166 new_text: "first_method($1)".to_string(),
4167 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
4168 })),
4169 additional_text_edits: Some(vec![lsp::TextEdit {
4170 new_text: "use d::SomeTrait;\n".to_string(),
4171 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4172 }]),
4173 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4174 ..Default::default()
4175 })
4176 },
4177 );
4178
4179 // The additional edit is applied.
4180 cx_a.foreground().run_until_parked();
4181 buffer_a.read_with(cx_a, |buffer, _| {
4182 assert_eq!(
4183 buffer.text(),
4184 "use d::SomeTrait;\nfn main() { a.first_method() }"
4185 );
4186 });
4187 buffer_b.read_with(cx_b, |buffer, _| {
4188 assert_eq!(
4189 buffer.text(),
4190 "use d::SomeTrait;\nfn main() { a.first_method() }"
4191 );
4192 });
4193}
4194
4195#[gpui::test(iterations = 10)]
4196async fn test_reloading_buffer_manually(
4197 deterministic: Arc<Deterministic>,
4198 cx_a: &mut TestAppContext,
4199 cx_b: &mut TestAppContext,
4200) {
4201 deterministic.forbid_parking();
4202 let mut server = TestServer::start(&deterministic).await;
4203 let client_a = server.create_client(cx_a, "user_a").await;
4204 let client_b = server.create_client(cx_b, "user_b").await;
4205 server
4206 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4207 .await;
4208 let active_call_a = cx_a.read(ActiveCall::global);
4209
4210 client_a
4211 .fs
4212 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4213 .await;
4214 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4215 let buffer_a = project_a
4216 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4217 .await
4218 .unwrap();
4219 let project_id = active_call_a
4220 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4221 .await
4222 .unwrap();
4223
4224 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4225
4226 let buffer_b = cx_b
4227 .background()
4228 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4229 .await
4230 .unwrap();
4231 buffer_b.update(cx_b, |buffer, cx| {
4232 buffer.edit([(4..7, "six")], None, cx);
4233 buffer.edit([(10..11, "6")], None, cx);
4234 assert_eq!(buffer.text(), "let six = 6;");
4235 assert!(buffer.is_dirty());
4236 assert!(!buffer.has_conflict());
4237 });
4238 cx_a.foreground().run_until_parked();
4239 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4240
4241 client_a
4242 .fs
4243 .save(
4244 "/a/a.rs".as_ref(),
4245 &Rope::from("let seven = 7;"),
4246 LineEnding::Unix,
4247 )
4248 .await
4249 .unwrap();
4250 cx_a.foreground().run_until_parked();
4251 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4252 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4253
4254 project_b
4255 .update(cx_b, |project, cx| {
4256 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4257 })
4258 .await
4259 .unwrap();
4260 buffer_a.read_with(cx_a, |buffer, _| {
4261 assert_eq!(buffer.text(), "let seven = 7;");
4262 assert!(!buffer.is_dirty());
4263 assert!(!buffer.has_conflict());
4264 });
4265 buffer_b.read_with(cx_b, |buffer, _| {
4266 assert_eq!(buffer.text(), "let seven = 7;");
4267 assert!(!buffer.is_dirty());
4268 assert!(!buffer.has_conflict());
4269 });
4270
4271 buffer_a.update(cx_a, |buffer, cx| {
4272 // Undoing on the host is a no-op when the reload was initiated by the guest.
4273 buffer.undo(cx);
4274 assert_eq!(buffer.text(), "let seven = 7;");
4275 assert!(!buffer.is_dirty());
4276 assert!(!buffer.has_conflict());
4277 });
4278 buffer_b.update(cx_b, |buffer, cx| {
4279 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4280 buffer.undo(cx);
4281 assert_eq!(buffer.text(), "let six = 6;");
4282 assert!(buffer.is_dirty());
4283 assert!(!buffer.has_conflict());
4284 });
4285}
4286
4287#[gpui::test(iterations = 10)]
4288async fn test_formatting_buffer(
4289 deterministic: Arc<Deterministic>,
4290 cx_a: &mut TestAppContext,
4291 cx_b: &mut TestAppContext,
4292) {
4293 use project::FormatTrigger;
4294
4295 let mut server = TestServer::start(&deterministic).await;
4296 let client_a = server.create_client(cx_a, "user_a").await;
4297 let client_b = server.create_client(cx_b, "user_b").await;
4298 server
4299 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4300 .await;
4301 let active_call_a = cx_a.read(ActiveCall::global);
4302
4303 // Set up a fake language server.
4304 let mut language = Language::new(
4305 LanguageConfig {
4306 name: "Rust".into(),
4307 path_suffixes: vec!["rs".to_string()],
4308 ..Default::default()
4309 },
4310 Some(tree_sitter_rust::language()),
4311 );
4312 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4313 client_a.language_registry.add(Arc::new(language));
4314
4315 // Here we insert a fake tree with a directory that exists on disk. This is needed
4316 // because later we'll invoke a command, which requires passing a working directory
4317 // that points to a valid location on disk.
4318 let directory = env::current_dir().unwrap();
4319 client_a
4320 .fs
4321 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4322 .await;
4323 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4324 let project_id = active_call_a
4325 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4326 .await
4327 .unwrap();
4328 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4329
4330 let buffer_b = cx_b
4331 .background()
4332 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4333 .await
4334 .unwrap();
4335
4336 let fake_language_server = fake_language_servers.next().await.unwrap();
4337 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4338 Ok(Some(vec![
4339 lsp::TextEdit {
4340 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4341 new_text: "h".to_string(),
4342 },
4343 lsp::TextEdit {
4344 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4345 new_text: "y".to_string(),
4346 },
4347 ]))
4348 });
4349
4350 project_b
4351 .update(cx_b, |project, cx| {
4352 project.format(
4353 HashSet::from_iter([buffer_b.clone()]),
4354 true,
4355 FormatTrigger::Save,
4356 cx,
4357 )
4358 })
4359 .await
4360 .unwrap();
4361
4362 // The edits from the LSP are applied, and a final newline is added.
4363 assert_eq!(
4364 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4365 "let honey = \"two\"\n"
4366 );
4367
4368 // Ensure buffer can be formatted using an external command. Notice how the
4369 // host's configuration is honored as opposed to using the guest's settings.
4370 cx_a.update(|cx| {
4371 cx.update_global(|settings: &mut Settings, _| {
4372 settings.editor_defaults.formatter = Some(Formatter::External {
4373 command: "awk".to_string(),
4374 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
4375 });
4376 });
4377 });
4378 project_b
4379 .update(cx_b, |project, cx| {
4380 project.format(
4381 HashSet::from_iter([buffer_b.clone()]),
4382 true,
4383 FormatTrigger::Save,
4384 cx,
4385 )
4386 })
4387 .await
4388 .unwrap();
4389 assert_eq!(
4390 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4391 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4392 );
4393}
4394
4395#[gpui::test(iterations = 10)]
4396async fn test_definition(
4397 deterministic: Arc<Deterministic>,
4398 cx_a: &mut TestAppContext,
4399 cx_b: &mut TestAppContext,
4400) {
4401 deterministic.forbid_parking();
4402 let mut server = TestServer::start(&deterministic).await;
4403 let client_a = server.create_client(cx_a, "user_a").await;
4404 let client_b = server.create_client(cx_b, "user_b").await;
4405 server
4406 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4407 .await;
4408 let active_call_a = cx_a.read(ActiveCall::global);
4409
4410 // Set up a fake language server.
4411 let mut language = Language::new(
4412 LanguageConfig {
4413 name: "Rust".into(),
4414 path_suffixes: vec!["rs".to_string()],
4415 ..Default::default()
4416 },
4417 Some(tree_sitter_rust::language()),
4418 );
4419 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4420 client_a.language_registry.add(Arc::new(language));
4421
4422 client_a
4423 .fs
4424 .insert_tree(
4425 "/root",
4426 json!({
4427 "dir-1": {
4428 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4429 },
4430 "dir-2": {
4431 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4432 "c.rs": "type T2 = usize;",
4433 }
4434 }),
4435 )
4436 .await;
4437 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4438 let project_id = active_call_a
4439 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4440 .await
4441 .unwrap();
4442 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4443
4444 // Open the file on client B.
4445 let buffer_b = cx_b
4446 .background()
4447 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4448 .await
4449 .unwrap();
4450
4451 // Request the definition of a symbol as the guest.
4452 let fake_language_server = fake_language_servers.next().await.unwrap();
4453 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4454 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4455 lsp::Location::new(
4456 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4457 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4458 ),
4459 )))
4460 });
4461
4462 let definitions_1 = project_b
4463 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4464 .await
4465 .unwrap();
4466 cx_b.read(|cx| {
4467 assert_eq!(definitions_1.len(), 1);
4468 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4469 let target_buffer = definitions_1[0].target.buffer.read(cx);
4470 assert_eq!(
4471 target_buffer.text(),
4472 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4473 );
4474 assert_eq!(
4475 definitions_1[0].target.range.to_point(target_buffer),
4476 Point::new(0, 6)..Point::new(0, 9)
4477 );
4478 });
4479
4480 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4481 // the previous call to `definition`.
4482 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4483 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4484 lsp::Location::new(
4485 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4486 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4487 ),
4488 )))
4489 });
4490
4491 let definitions_2 = project_b
4492 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4493 .await
4494 .unwrap();
4495 cx_b.read(|cx| {
4496 assert_eq!(definitions_2.len(), 1);
4497 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4498 let target_buffer = definitions_2[0].target.buffer.read(cx);
4499 assert_eq!(
4500 target_buffer.text(),
4501 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4502 );
4503 assert_eq!(
4504 definitions_2[0].target.range.to_point(target_buffer),
4505 Point::new(1, 6)..Point::new(1, 11)
4506 );
4507 });
4508 assert_eq!(
4509 definitions_1[0].target.buffer,
4510 definitions_2[0].target.buffer
4511 );
4512
4513 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4514 |req, _| async move {
4515 assert_eq!(
4516 req.text_document_position_params.position,
4517 lsp::Position::new(0, 7)
4518 );
4519 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4520 lsp::Location::new(
4521 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4522 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4523 ),
4524 )))
4525 },
4526 );
4527
4528 let type_definitions = project_b
4529 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4530 .await
4531 .unwrap();
4532 cx_b.read(|cx| {
4533 assert_eq!(type_definitions.len(), 1);
4534 let target_buffer = type_definitions[0].target.buffer.read(cx);
4535 assert_eq!(target_buffer.text(), "type T2 = usize;");
4536 assert_eq!(
4537 type_definitions[0].target.range.to_point(target_buffer),
4538 Point::new(0, 5)..Point::new(0, 7)
4539 );
4540 });
4541}
4542
4543#[gpui::test(iterations = 10)]
4544async fn test_references(
4545 deterministic: Arc<Deterministic>,
4546 cx_a: &mut TestAppContext,
4547 cx_b: &mut TestAppContext,
4548) {
4549 deterministic.forbid_parking();
4550 let mut server = TestServer::start(&deterministic).await;
4551 let client_a = server.create_client(cx_a, "user_a").await;
4552 let client_b = server.create_client(cx_b, "user_b").await;
4553 server
4554 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4555 .await;
4556 let active_call_a = cx_a.read(ActiveCall::global);
4557
4558 // Set up a fake language server.
4559 let mut language = Language::new(
4560 LanguageConfig {
4561 name: "Rust".into(),
4562 path_suffixes: vec!["rs".to_string()],
4563 ..Default::default()
4564 },
4565 Some(tree_sitter_rust::language()),
4566 );
4567 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4568 client_a.language_registry.add(Arc::new(language));
4569
4570 client_a
4571 .fs
4572 .insert_tree(
4573 "/root",
4574 json!({
4575 "dir-1": {
4576 "one.rs": "const ONE: usize = 1;",
4577 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4578 },
4579 "dir-2": {
4580 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4581 }
4582 }),
4583 )
4584 .await;
4585 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4586 let project_id = active_call_a
4587 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4588 .await
4589 .unwrap();
4590 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4591
4592 // Open the file on client B.
4593 let buffer_b = cx_b
4594 .background()
4595 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4596 .await
4597 .unwrap();
4598
4599 // Request references to a symbol as the guest.
4600 let fake_language_server = fake_language_servers.next().await.unwrap();
4601 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4602 assert_eq!(
4603 params.text_document_position.text_document.uri.as_str(),
4604 "file:///root/dir-1/one.rs"
4605 );
4606 Ok(Some(vec![
4607 lsp::Location {
4608 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4609 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4610 },
4611 lsp::Location {
4612 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4613 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4614 },
4615 lsp::Location {
4616 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4617 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4618 },
4619 ]))
4620 });
4621
4622 let references = project_b
4623 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4624 .await
4625 .unwrap();
4626 cx_b.read(|cx| {
4627 assert_eq!(references.len(), 3);
4628 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4629
4630 let two_buffer = references[0].buffer.read(cx);
4631 let three_buffer = references[2].buffer.read(cx);
4632 assert_eq!(
4633 two_buffer.file().unwrap().path().as_ref(),
4634 Path::new("two.rs")
4635 );
4636 assert_eq!(references[1].buffer, references[0].buffer);
4637 assert_eq!(
4638 three_buffer.file().unwrap().full_path(cx),
4639 Path::new("/root/dir-2/three.rs")
4640 );
4641
4642 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4643 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4644 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4645 });
4646}
4647
4648#[gpui::test(iterations = 10)]
4649async fn test_project_search(
4650 deterministic: Arc<Deterministic>,
4651 cx_a: &mut TestAppContext,
4652 cx_b: &mut TestAppContext,
4653) {
4654 deterministic.forbid_parking();
4655 let mut server = TestServer::start(&deterministic).await;
4656 let client_a = server.create_client(cx_a, "user_a").await;
4657 let client_b = server.create_client(cx_b, "user_b").await;
4658 server
4659 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4660 .await;
4661 let active_call_a = cx_a.read(ActiveCall::global);
4662
4663 client_a
4664 .fs
4665 .insert_tree(
4666 "/root",
4667 json!({
4668 "dir-1": {
4669 "a": "hello world",
4670 "b": "goodnight moon",
4671 "c": "a world of goo",
4672 "d": "world champion of clown world",
4673 },
4674 "dir-2": {
4675 "e": "disney world is fun",
4676 }
4677 }),
4678 )
4679 .await;
4680 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4681 let (worktree_2, _) = project_a
4682 .update(cx_a, |p, cx| {
4683 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4684 })
4685 .await
4686 .unwrap();
4687 worktree_2
4688 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4689 .await;
4690 let project_id = active_call_a
4691 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4692 .await
4693 .unwrap();
4694
4695 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4696
4697 // Perform a search as the guest.
4698 let results = project_b
4699 .update(cx_b, |project, cx| {
4700 project.search(
4701 SearchQuery::text("world", false, false, Vec::new(), Vec::new()),
4702 cx,
4703 )
4704 })
4705 .await
4706 .unwrap();
4707
4708 let mut ranges_by_path = results
4709 .into_iter()
4710 .map(|(buffer, ranges)| {
4711 buffer.read_with(cx_b, |buffer, cx| {
4712 let path = buffer.file().unwrap().full_path(cx);
4713 let offset_ranges = ranges
4714 .into_iter()
4715 .map(|range| range.to_offset(buffer))
4716 .collect::<Vec<_>>();
4717 (path, offset_ranges)
4718 })
4719 })
4720 .collect::<Vec<_>>();
4721 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4722
4723 assert_eq!(
4724 ranges_by_path,
4725 &[
4726 (PathBuf::from("dir-1/a"), vec![6..11]),
4727 (PathBuf::from("dir-1/c"), vec![2..7]),
4728 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4729 (PathBuf::from("dir-2/e"), vec![7..12]),
4730 ]
4731 );
4732}
4733
4734#[gpui::test(iterations = 10)]
4735async fn test_document_highlights(
4736 deterministic: Arc<Deterministic>,
4737 cx_a: &mut TestAppContext,
4738 cx_b: &mut TestAppContext,
4739) {
4740 deterministic.forbid_parking();
4741 let mut server = TestServer::start(&deterministic).await;
4742 let client_a = server.create_client(cx_a, "user_a").await;
4743 let client_b = server.create_client(cx_b, "user_b").await;
4744 server
4745 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4746 .await;
4747 let active_call_a = cx_a.read(ActiveCall::global);
4748
4749 client_a
4750 .fs
4751 .insert_tree(
4752 "/root-1",
4753 json!({
4754 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4755 }),
4756 )
4757 .await;
4758
4759 // Set up a fake language server.
4760 let mut language = Language::new(
4761 LanguageConfig {
4762 name: "Rust".into(),
4763 path_suffixes: vec!["rs".to_string()],
4764 ..Default::default()
4765 },
4766 Some(tree_sitter_rust::language()),
4767 );
4768 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4769 client_a.language_registry.add(Arc::new(language));
4770
4771 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4772 let project_id = active_call_a
4773 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4774 .await
4775 .unwrap();
4776 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4777
4778 // Open the file on client B.
4779 let buffer_b = cx_b
4780 .background()
4781 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4782 .await
4783 .unwrap();
4784
4785 // Request document highlights as the guest.
4786 let fake_language_server = fake_language_servers.next().await.unwrap();
4787 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4788 |params, _| async move {
4789 assert_eq!(
4790 params
4791 .text_document_position_params
4792 .text_document
4793 .uri
4794 .as_str(),
4795 "file:///root-1/main.rs"
4796 );
4797 assert_eq!(
4798 params.text_document_position_params.position,
4799 lsp::Position::new(0, 34)
4800 );
4801 Ok(Some(vec![
4802 lsp::DocumentHighlight {
4803 kind: Some(lsp::DocumentHighlightKind::WRITE),
4804 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4805 },
4806 lsp::DocumentHighlight {
4807 kind: Some(lsp::DocumentHighlightKind::READ),
4808 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4809 },
4810 lsp::DocumentHighlight {
4811 kind: Some(lsp::DocumentHighlightKind::READ),
4812 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4813 },
4814 ]))
4815 },
4816 );
4817
4818 let highlights = project_b
4819 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4820 .await
4821 .unwrap();
4822 buffer_b.read_with(cx_b, |buffer, _| {
4823 let snapshot = buffer.snapshot();
4824
4825 let highlights = highlights
4826 .into_iter()
4827 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4828 .collect::<Vec<_>>();
4829 assert_eq!(
4830 highlights,
4831 &[
4832 (lsp::DocumentHighlightKind::WRITE, 10..16),
4833 (lsp::DocumentHighlightKind::READ, 32..38),
4834 (lsp::DocumentHighlightKind::READ, 41..47)
4835 ]
4836 )
4837 });
4838}
4839
4840#[gpui::test(iterations = 10)]
4841async fn test_lsp_hover(
4842 deterministic: Arc<Deterministic>,
4843 cx_a: &mut TestAppContext,
4844 cx_b: &mut TestAppContext,
4845) {
4846 deterministic.forbid_parking();
4847 let mut server = TestServer::start(&deterministic).await;
4848 let client_a = server.create_client(cx_a, "user_a").await;
4849 let client_b = server.create_client(cx_b, "user_b").await;
4850 server
4851 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4852 .await;
4853 let active_call_a = cx_a.read(ActiveCall::global);
4854
4855 client_a
4856 .fs
4857 .insert_tree(
4858 "/root-1",
4859 json!({
4860 "main.rs": "use std::collections::HashMap;",
4861 }),
4862 )
4863 .await;
4864
4865 // Set up a fake language server.
4866 let mut language = Language::new(
4867 LanguageConfig {
4868 name: "Rust".into(),
4869 path_suffixes: vec!["rs".to_string()],
4870 ..Default::default()
4871 },
4872 Some(tree_sitter_rust::language()),
4873 );
4874 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4875 client_a.language_registry.add(Arc::new(language));
4876
4877 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4878 let project_id = active_call_a
4879 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4880 .await
4881 .unwrap();
4882 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4883
4884 // Open the file as the guest
4885 let buffer_b = cx_b
4886 .background()
4887 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4888 .await
4889 .unwrap();
4890
4891 // Request hover information as the guest.
4892 let fake_language_server = fake_language_servers.next().await.unwrap();
4893 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4894 |params, _| async move {
4895 assert_eq!(
4896 params
4897 .text_document_position_params
4898 .text_document
4899 .uri
4900 .as_str(),
4901 "file:///root-1/main.rs"
4902 );
4903 assert_eq!(
4904 params.text_document_position_params.position,
4905 lsp::Position::new(0, 22)
4906 );
4907 Ok(Some(lsp::Hover {
4908 contents: lsp::HoverContents::Array(vec![
4909 lsp::MarkedString::String("Test hover content.".to_string()),
4910 lsp::MarkedString::LanguageString(lsp::LanguageString {
4911 language: "Rust".to_string(),
4912 value: "let foo = 42;".to_string(),
4913 }),
4914 ]),
4915 range: Some(lsp::Range::new(
4916 lsp::Position::new(0, 22),
4917 lsp::Position::new(0, 29),
4918 )),
4919 }))
4920 },
4921 );
4922
4923 let hover_info = project_b
4924 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
4925 .await
4926 .unwrap()
4927 .unwrap();
4928 buffer_b.read_with(cx_b, |buffer, _| {
4929 let snapshot = buffer.snapshot();
4930 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
4931 assert_eq!(
4932 hover_info.contents,
4933 vec![
4934 project::HoverBlock {
4935 text: "Test hover content.".to_string(),
4936 kind: HoverBlockKind::Markdown,
4937 },
4938 project::HoverBlock {
4939 text: "let foo = 42;".to_string(),
4940 kind: HoverBlockKind::Code {
4941 language: "Rust".to_string()
4942 },
4943 }
4944 ]
4945 );
4946 });
4947}
4948
4949#[gpui::test(iterations = 10)]
4950async fn test_project_symbols(
4951 deterministic: Arc<Deterministic>,
4952 cx_a: &mut TestAppContext,
4953 cx_b: &mut TestAppContext,
4954) {
4955 deterministic.forbid_parking();
4956 let mut server = TestServer::start(&deterministic).await;
4957 let client_a = server.create_client(cx_a, "user_a").await;
4958 let client_b = server.create_client(cx_b, "user_b").await;
4959 server
4960 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4961 .await;
4962 let active_call_a = cx_a.read(ActiveCall::global);
4963
4964 // Set up a fake language server.
4965 let mut language = Language::new(
4966 LanguageConfig {
4967 name: "Rust".into(),
4968 path_suffixes: vec!["rs".to_string()],
4969 ..Default::default()
4970 },
4971 Some(tree_sitter_rust::language()),
4972 );
4973 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4974 client_a.language_registry.add(Arc::new(language));
4975
4976 client_a
4977 .fs
4978 .insert_tree(
4979 "/code",
4980 json!({
4981 "crate-1": {
4982 "one.rs": "const ONE: usize = 1;",
4983 },
4984 "crate-2": {
4985 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
4986 },
4987 "private": {
4988 "passwords.txt": "the-password",
4989 }
4990 }),
4991 )
4992 .await;
4993 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
4994 let project_id = active_call_a
4995 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4996 .await
4997 .unwrap();
4998 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4999
5000 // Cause the language server to start.
5001 let _buffer = cx_b
5002 .background()
5003 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
5004 .await
5005 .unwrap();
5006
5007 let fake_language_server = fake_language_servers.next().await.unwrap();
5008 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
5009 #[allow(deprecated)]
5010 Ok(Some(vec![lsp::SymbolInformation {
5011 name: "TWO".into(),
5012 location: lsp::Location {
5013 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
5014 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5015 },
5016 kind: lsp::SymbolKind::CONSTANT,
5017 tags: None,
5018 container_name: None,
5019 deprecated: None,
5020 }]))
5021 });
5022
5023 // Request the definition of a symbol as the guest.
5024 let symbols = project_b
5025 .update(cx_b, |p, cx| p.symbols("two", cx))
5026 .await
5027 .unwrap();
5028 assert_eq!(symbols.len(), 1);
5029 assert_eq!(symbols[0].name, "TWO");
5030
5031 // Open one of the returned symbols.
5032 let buffer_b_2 = project_b
5033 .update(cx_b, |project, cx| {
5034 project.open_buffer_for_symbol(&symbols[0], cx)
5035 })
5036 .await
5037 .unwrap();
5038 buffer_b_2.read_with(cx_b, |buffer, _| {
5039 assert_eq!(
5040 buffer.file().unwrap().path().as_ref(),
5041 Path::new("../crate-2/two.rs")
5042 );
5043 });
5044
5045 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5046 let mut fake_symbol = symbols[0].clone();
5047 fake_symbol.path.path = Path::new("/code/secrets").into();
5048 let error = project_b
5049 .update(cx_b, |project, cx| {
5050 project.open_buffer_for_symbol(&fake_symbol, cx)
5051 })
5052 .await
5053 .unwrap_err();
5054 assert!(error.to_string().contains("invalid symbol signature"));
5055}
5056
5057#[gpui::test(iterations = 10)]
5058async fn test_open_buffer_while_getting_definition_pointing_to_it(
5059 deterministic: Arc<Deterministic>,
5060 cx_a: &mut TestAppContext,
5061 cx_b: &mut TestAppContext,
5062 mut rng: StdRng,
5063) {
5064 deterministic.forbid_parking();
5065 let mut server = TestServer::start(&deterministic).await;
5066 let client_a = server.create_client(cx_a, "user_a").await;
5067 let client_b = server.create_client(cx_b, "user_b").await;
5068 server
5069 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5070 .await;
5071 let active_call_a = cx_a.read(ActiveCall::global);
5072
5073 // Set up a fake language server.
5074 let mut language = Language::new(
5075 LanguageConfig {
5076 name: "Rust".into(),
5077 path_suffixes: vec!["rs".to_string()],
5078 ..Default::default()
5079 },
5080 Some(tree_sitter_rust::language()),
5081 );
5082 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5083 client_a.language_registry.add(Arc::new(language));
5084
5085 client_a
5086 .fs
5087 .insert_tree(
5088 "/root",
5089 json!({
5090 "a.rs": "const ONE: usize = b::TWO;",
5091 "b.rs": "const TWO: usize = 2",
5092 }),
5093 )
5094 .await;
5095 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5096 let project_id = active_call_a
5097 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5098 .await
5099 .unwrap();
5100 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5101
5102 let buffer_b1 = cx_b
5103 .background()
5104 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
5105 .await
5106 .unwrap();
5107
5108 let fake_language_server = fake_language_servers.next().await.unwrap();
5109 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5110 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5111 lsp::Location::new(
5112 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5113 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5114 ),
5115 )))
5116 });
5117
5118 let definitions;
5119 let buffer_b2;
5120 if rng.gen() {
5121 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5122 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5123 } else {
5124 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5125 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5126 }
5127
5128 let buffer_b2 = buffer_b2.await.unwrap();
5129 let definitions = definitions.await.unwrap();
5130 assert_eq!(definitions.len(), 1);
5131 assert_eq!(definitions[0].target.buffer, buffer_b2);
5132}
5133
5134#[gpui::test(iterations = 10)]
5135async fn test_collaborating_with_code_actions(
5136 deterministic: Arc<Deterministic>,
5137 cx_a: &mut TestAppContext,
5138 cx_b: &mut TestAppContext,
5139) {
5140 deterministic.forbid_parking();
5141 cx_b.update(editor::init);
5142 let mut server = TestServer::start(&deterministic).await;
5143 let client_a = server.create_client(cx_a, "user_a").await;
5144 let client_b = server.create_client(cx_b, "user_b").await;
5145 server
5146 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5147 .await;
5148 let active_call_a = cx_a.read(ActiveCall::global);
5149
5150 // Set up a fake language server.
5151 let mut language = Language::new(
5152 LanguageConfig {
5153 name: "Rust".into(),
5154 path_suffixes: vec!["rs".to_string()],
5155 ..Default::default()
5156 },
5157 Some(tree_sitter_rust::language()),
5158 );
5159 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5160 client_a.language_registry.add(Arc::new(language));
5161
5162 client_a
5163 .fs
5164 .insert_tree(
5165 "/a",
5166 json!({
5167 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
5168 "other.rs": "pub fn foo() -> usize { 4 }",
5169 }),
5170 )
5171 .await;
5172 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5173 let project_id = active_call_a
5174 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5175 .await
5176 .unwrap();
5177
5178 // Join the project as client B.
5179 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5180 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
5181 let editor_b = workspace_b
5182 .update(cx_b, |workspace, cx| {
5183 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
5184 })
5185 .await
5186 .unwrap()
5187 .downcast::<Editor>()
5188 .unwrap();
5189
5190 let mut fake_language_server = fake_language_servers.next().await.unwrap();
5191 fake_language_server
5192 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
5193 assert_eq!(
5194 params.text_document.uri,
5195 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5196 );
5197 assert_eq!(params.range.start, lsp::Position::new(0, 0));
5198 assert_eq!(params.range.end, lsp::Position::new(0, 0));
5199 Ok(None)
5200 })
5201 .next()
5202 .await;
5203
5204 // Move cursor to a location that contains code actions.
5205 editor_b.update(cx_b, |editor, cx| {
5206 editor.change_selections(None, cx, |s| {
5207 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
5208 });
5209 cx.focus(&editor_b);
5210 });
5211
5212 fake_language_server
5213 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
5214 assert_eq!(
5215 params.text_document.uri,
5216 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5217 );
5218 assert_eq!(params.range.start, lsp::Position::new(1, 31));
5219 assert_eq!(params.range.end, lsp::Position::new(1, 31));
5220
5221 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
5222 lsp::CodeAction {
5223 title: "Inline into all callers".to_string(),
5224 edit: Some(lsp::WorkspaceEdit {
5225 changes: Some(
5226 [
5227 (
5228 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5229 vec![lsp::TextEdit::new(
5230 lsp::Range::new(
5231 lsp::Position::new(1, 22),
5232 lsp::Position::new(1, 34),
5233 ),
5234 "4".to_string(),
5235 )],
5236 ),
5237 (
5238 lsp::Url::from_file_path("/a/other.rs").unwrap(),
5239 vec![lsp::TextEdit::new(
5240 lsp::Range::new(
5241 lsp::Position::new(0, 0),
5242 lsp::Position::new(0, 27),
5243 ),
5244 "".to_string(),
5245 )],
5246 ),
5247 ]
5248 .into_iter()
5249 .collect(),
5250 ),
5251 ..Default::default()
5252 }),
5253 data: Some(json!({
5254 "codeActionParams": {
5255 "range": {
5256 "start": {"line": 1, "column": 31},
5257 "end": {"line": 1, "column": 31},
5258 }
5259 }
5260 })),
5261 ..Default::default()
5262 },
5263 )]))
5264 })
5265 .next()
5266 .await;
5267
5268 // Toggle code actions and wait for them to display.
5269 editor_b.update(cx_b, |editor, cx| {
5270 editor.toggle_code_actions(
5271 &ToggleCodeActions {
5272 deployed_from_indicator: false,
5273 },
5274 cx,
5275 );
5276 });
5277 cx_a.foreground().run_until_parked();
5278 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
5279
5280 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
5281
5282 // Confirming the code action will trigger a resolve request.
5283 let confirm_action = workspace_b
5284 .update(cx_b, |workspace, cx| {
5285 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
5286 })
5287 .unwrap();
5288 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
5289 |_, _| async move {
5290 Ok(lsp::CodeAction {
5291 title: "Inline into all callers".to_string(),
5292 edit: Some(lsp::WorkspaceEdit {
5293 changes: Some(
5294 [
5295 (
5296 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5297 vec![lsp::TextEdit::new(
5298 lsp::Range::new(
5299 lsp::Position::new(1, 22),
5300 lsp::Position::new(1, 34),
5301 ),
5302 "4".to_string(),
5303 )],
5304 ),
5305 (
5306 lsp::Url::from_file_path("/a/other.rs").unwrap(),
5307 vec![lsp::TextEdit::new(
5308 lsp::Range::new(
5309 lsp::Position::new(0, 0),
5310 lsp::Position::new(0, 27),
5311 ),
5312 "".to_string(),
5313 )],
5314 ),
5315 ]
5316 .into_iter()
5317 .collect(),
5318 ),
5319 ..Default::default()
5320 }),
5321 ..Default::default()
5322 })
5323 },
5324 );
5325
5326 // After the action is confirmed, an editor containing both modified files is opened.
5327 confirm_action.await.unwrap();
5328 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5329 workspace
5330 .active_item(cx)
5331 .unwrap()
5332 .downcast::<Editor>()
5333 .unwrap()
5334 });
5335 code_action_editor.update(cx_b, |editor, cx| {
5336 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
5337 editor.undo(&Undo, cx);
5338 assert_eq!(
5339 editor.text(cx),
5340 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
5341 );
5342 editor.redo(&Redo, cx);
5343 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
5344 });
5345}
5346
5347#[gpui::test(iterations = 10)]
5348async fn test_collaborating_with_renames(
5349 deterministic: Arc<Deterministic>,
5350 cx_a: &mut TestAppContext,
5351 cx_b: &mut TestAppContext,
5352) {
5353 deterministic.forbid_parking();
5354 cx_b.update(editor::init);
5355 let mut server = TestServer::start(&deterministic).await;
5356 let client_a = server.create_client(cx_a, "user_a").await;
5357 let client_b = server.create_client(cx_b, "user_b").await;
5358 server
5359 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5360 .await;
5361 let active_call_a = cx_a.read(ActiveCall::global);
5362
5363 // Set up a fake language server.
5364 let mut language = Language::new(
5365 LanguageConfig {
5366 name: "Rust".into(),
5367 path_suffixes: vec!["rs".to_string()],
5368 ..Default::default()
5369 },
5370 Some(tree_sitter_rust::language()),
5371 );
5372 let mut fake_language_servers = language
5373 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5374 capabilities: lsp::ServerCapabilities {
5375 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
5376 prepare_provider: Some(true),
5377 work_done_progress_options: Default::default(),
5378 })),
5379 ..Default::default()
5380 },
5381 ..Default::default()
5382 }))
5383 .await;
5384 client_a.language_registry.add(Arc::new(language));
5385
5386 client_a
5387 .fs
5388 .insert_tree(
5389 "/dir",
5390 json!({
5391 "one.rs": "const ONE: usize = 1;",
5392 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
5393 }),
5394 )
5395 .await;
5396 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5397 let project_id = active_call_a
5398 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5399 .await
5400 .unwrap();
5401 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5402
5403 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
5404 let editor_b = workspace_b
5405 .update(cx_b, |workspace, cx| {
5406 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
5407 })
5408 .await
5409 .unwrap()
5410 .downcast::<Editor>()
5411 .unwrap();
5412 let fake_language_server = fake_language_servers.next().await.unwrap();
5413
5414 // Move cursor to a location that can be renamed.
5415 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
5416 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
5417 editor.rename(&Rename, cx).unwrap()
5418 });
5419
5420 fake_language_server
5421 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
5422 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
5423 assert_eq!(params.position, lsp::Position::new(0, 7));
5424 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
5425 lsp::Position::new(0, 6),
5426 lsp::Position::new(0, 9),
5427 ))))
5428 })
5429 .next()
5430 .await
5431 .unwrap();
5432 prepare_rename.await.unwrap();
5433 editor_b.update(cx_b, |editor, cx| {
5434 let rename = editor.pending_rename().unwrap();
5435 let buffer = editor.buffer().read(cx).snapshot(cx);
5436 assert_eq!(
5437 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
5438 6..9
5439 );
5440 rename.editor.update(cx, |rename_editor, cx| {
5441 rename_editor.buffer().update(cx, |rename_buffer, cx| {
5442 rename_buffer.edit([(0..3, "THREE")], None, cx);
5443 });
5444 });
5445 });
5446
5447 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
5448 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
5449 });
5450 fake_language_server
5451 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
5452 assert_eq!(
5453 params.text_document_position.text_document.uri.as_str(),
5454 "file:///dir/one.rs"
5455 );
5456 assert_eq!(
5457 params.text_document_position.position,
5458 lsp::Position::new(0, 6)
5459 );
5460 assert_eq!(params.new_name, "THREE");
5461 Ok(Some(lsp::WorkspaceEdit {
5462 changes: Some(
5463 [
5464 (
5465 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
5466 vec![lsp::TextEdit::new(
5467 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5468 "THREE".to_string(),
5469 )],
5470 ),
5471 (
5472 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
5473 vec![
5474 lsp::TextEdit::new(
5475 lsp::Range::new(
5476 lsp::Position::new(0, 24),
5477 lsp::Position::new(0, 27),
5478 ),
5479 "THREE".to_string(),
5480 ),
5481 lsp::TextEdit::new(
5482 lsp::Range::new(
5483 lsp::Position::new(0, 35),
5484 lsp::Position::new(0, 38),
5485 ),
5486 "THREE".to_string(),
5487 ),
5488 ],
5489 ),
5490 ]
5491 .into_iter()
5492 .collect(),
5493 ),
5494 ..Default::default()
5495 }))
5496 })
5497 .next()
5498 .await
5499 .unwrap();
5500 confirm_rename.await.unwrap();
5501
5502 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5503 workspace
5504 .active_item(cx)
5505 .unwrap()
5506 .downcast::<Editor>()
5507 .unwrap()
5508 });
5509 rename_editor.update(cx_b, |editor, cx| {
5510 assert_eq!(
5511 editor.text(cx),
5512 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5513 );
5514 editor.undo(&Undo, cx);
5515 assert_eq!(
5516 editor.text(cx),
5517 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
5518 );
5519 editor.redo(&Redo, cx);
5520 assert_eq!(
5521 editor.text(cx),
5522 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5523 );
5524 });
5525
5526 // Ensure temporary rename edits cannot be undone/redone.
5527 editor_b.update(cx_b, |editor, cx| {
5528 editor.undo(&Undo, cx);
5529 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5530 editor.undo(&Undo, cx);
5531 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5532 editor.redo(&Redo, cx);
5533 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
5534 })
5535}
5536
5537#[gpui::test(iterations = 10)]
5538async fn test_language_server_statuses(
5539 deterministic: Arc<Deterministic>,
5540 cx_a: &mut TestAppContext,
5541 cx_b: &mut TestAppContext,
5542) {
5543 deterministic.forbid_parking();
5544
5545 cx_b.update(editor::init);
5546 let mut server = TestServer::start(&deterministic).await;
5547 let client_a = server.create_client(cx_a, "user_a").await;
5548 let client_b = server.create_client(cx_b, "user_b").await;
5549 server
5550 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5551 .await;
5552 let active_call_a = cx_a.read(ActiveCall::global);
5553
5554 // Set up a fake language server.
5555 let mut language = Language::new(
5556 LanguageConfig {
5557 name: "Rust".into(),
5558 path_suffixes: vec!["rs".to_string()],
5559 ..Default::default()
5560 },
5561 Some(tree_sitter_rust::language()),
5562 );
5563 let mut fake_language_servers = language
5564 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5565 name: "the-language-server",
5566 ..Default::default()
5567 }))
5568 .await;
5569 client_a.language_registry.add(Arc::new(language));
5570
5571 client_a
5572 .fs
5573 .insert_tree(
5574 "/dir",
5575 json!({
5576 "main.rs": "const ONE: usize = 1;",
5577 }),
5578 )
5579 .await;
5580 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5581
5582 let _buffer_a = project_a
5583 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
5584 .await
5585 .unwrap();
5586
5587 let fake_language_server = fake_language_servers.next().await.unwrap();
5588 fake_language_server.start_progress("the-token").await;
5589 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5590 token: lsp::NumberOrString::String("the-token".to_string()),
5591 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5592 lsp::WorkDoneProgressReport {
5593 message: Some("the-message".to_string()),
5594 ..Default::default()
5595 },
5596 )),
5597 });
5598 deterministic.run_until_parked();
5599 project_a.read_with(cx_a, |project, _| {
5600 let status = project.language_server_statuses().next().unwrap();
5601 assert_eq!(status.name, "the-language-server");
5602 assert_eq!(status.pending_work.len(), 1);
5603 assert_eq!(
5604 status.pending_work["the-token"].message.as_ref().unwrap(),
5605 "the-message"
5606 );
5607 });
5608
5609 let project_id = active_call_a
5610 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5611 .await
5612 .unwrap();
5613 deterministic.run_until_parked();
5614 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5615 project_b.read_with(cx_b, |project, _| {
5616 let status = project.language_server_statuses().next().unwrap();
5617 assert_eq!(status.name, "the-language-server");
5618 });
5619
5620 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5621 token: lsp::NumberOrString::String("the-token".to_string()),
5622 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5623 lsp::WorkDoneProgressReport {
5624 message: Some("the-message-2".to_string()),
5625 ..Default::default()
5626 },
5627 )),
5628 });
5629 deterministic.run_until_parked();
5630 project_a.read_with(cx_a, |project, _| {
5631 let status = project.language_server_statuses().next().unwrap();
5632 assert_eq!(status.name, "the-language-server");
5633 assert_eq!(status.pending_work.len(), 1);
5634 assert_eq!(
5635 status.pending_work["the-token"].message.as_ref().unwrap(),
5636 "the-message-2"
5637 );
5638 });
5639 project_b.read_with(cx_b, |project, _| {
5640 let status = project.language_server_statuses().next().unwrap();
5641 assert_eq!(status.name, "the-language-server");
5642 assert_eq!(status.pending_work.len(), 1);
5643 assert_eq!(
5644 status.pending_work["the-token"].message.as_ref().unwrap(),
5645 "the-message-2"
5646 );
5647 });
5648}
5649
5650#[gpui::test(iterations = 10)]
5651async fn test_contacts(
5652 deterministic: Arc<Deterministic>,
5653 cx_a: &mut TestAppContext,
5654 cx_b: &mut TestAppContext,
5655 cx_c: &mut TestAppContext,
5656 cx_d: &mut TestAppContext,
5657) {
5658 deterministic.forbid_parking();
5659 let mut server = TestServer::start(&deterministic).await;
5660 let client_a = server.create_client(cx_a, "user_a").await;
5661 let client_b = server.create_client(cx_b, "user_b").await;
5662 let client_c = server.create_client(cx_c, "user_c").await;
5663 let client_d = server.create_client(cx_d, "user_d").await;
5664 server
5665 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5666 .await;
5667 let active_call_a = cx_a.read(ActiveCall::global);
5668 let active_call_b = cx_b.read(ActiveCall::global);
5669 let active_call_c = cx_c.read(ActiveCall::global);
5670 let _active_call_d = cx_d.read(ActiveCall::global);
5671
5672 deterministic.run_until_parked();
5673 assert_eq!(
5674 contacts(&client_a, cx_a),
5675 [
5676 ("user_b".to_string(), "online", "free"),
5677 ("user_c".to_string(), "online", "free")
5678 ]
5679 );
5680 assert_eq!(
5681 contacts(&client_b, cx_b),
5682 [
5683 ("user_a".to_string(), "online", "free"),
5684 ("user_c".to_string(), "online", "free")
5685 ]
5686 );
5687 assert_eq!(
5688 contacts(&client_c, cx_c),
5689 [
5690 ("user_a".to_string(), "online", "free"),
5691 ("user_b".to_string(), "online", "free")
5692 ]
5693 );
5694 assert_eq!(contacts(&client_d, cx_d), []);
5695
5696 server.disconnect_client(client_c.peer_id().unwrap());
5697 server.forbid_connections();
5698 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5699 assert_eq!(
5700 contacts(&client_a, cx_a),
5701 [
5702 ("user_b".to_string(), "online", "free"),
5703 ("user_c".to_string(), "offline", "free")
5704 ]
5705 );
5706 assert_eq!(
5707 contacts(&client_b, cx_b),
5708 [
5709 ("user_a".to_string(), "online", "free"),
5710 ("user_c".to_string(), "offline", "free")
5711 ]
5712 );
5713 assert_eq!(contacts(&client_c, cx_c), []);
5714 assert_eq!(contacts(&client_d, cx_d), []);
5715
5716 server.allow_connections();
5717 client_c
5718 .authenticate_and_connect(false, &cx_c.to_async())
5719 .await
5720 .unwrap();
5721
5722 deterministic.run_until_parked();
5723 assert_eq!(
5724 contacts(&client_a, cx_a),
5725 [
5726 ("user_b".to_string(), "online", "free"),
5727 ("user_c".to_string(), "online", "free")
5728 ]
5729 );
5730 assert_eq!(
5731 contacts(&client_b, cx_b),
5732 [
5733 ("user_a".to_string(), "online", "free"),
5734 ("user_c".to_string(), "online", "free")
5735 ]
5736 );
5737 assert_eq!(
5738 contacts(&client_c, cx_c),
5739 [
5740 ("user_a".to_string(), "online", "free"),
5741 ("user_b".to_string(), "online", "free")
5742 ]
5743 );
5744 assert_eq!(contacts(&client_d, cx_d), []);
5745
5746 active_call_a
5747 .update(cx_a, |call, cx| {
5748 call.invite(client_b.user_id().unwrap(), None, cx)
5749 })
5750 .await
5751 .unwrap();
5752 deterministic.run_until_parked();
5753 assert_eq!(
5754 contacts(&client_a, cx_a),
5755 [
5756 ("user_b".to_string(), "online", "busy"),
5757 ("user_c".to_string(), "online", "free")
5758 ]
5759 );
5760 assert_eq!(
5761 contacts(&client_b, cx_b),
5762 [
5763 ("user_a".to_string(), "online", "busy"),
5764 ("user_c".to_string(), "online", "free")
5765 ]
5766 );
5767 assert_eq!(
5768 contacts(&client_c, cx_c),
5769 [
5770 ("user_a".to_string(), "online", "busy"),
5771 ("user_b".to_string(), "online", "busy")
5772 ]
5773 );
5774 assert_eq!(contacts(&client_d, cx_d), []);
5775
5776 // Client B and client D become contacts while client B is being called.
5777 server
5778 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5779 .await;
5780 deterministic.run_until_parked();
5781 assert_eq!(
5782 contacts(&client_a, cx_a),
5783 [
5784 ("user_b".to_string(), "online", "busy"),
5785 ("user_c".to_string(), "online", "free")
5786 ]
5787 );
5788 assert_eq!(
5789 contacts(&client_b, cx_b),
5790 [
5791 ("user_a".to_string(), "online", "busy"),
5792 ("user_c".to_string(), "online", "free"),
5793 ("user_d".to_string(), "online", "free"),
5794 ]
5795 );
5796 assert_eq!(
5797 contacts(&client_c, cx_c),
5798 [
5799 ("user_a".to_string(), "online", "busy"),
5800 ("user_b".to_string(), "online", "busy")
5801 ]
5802 );
5803 assert_eq!(
5804 contacts(&client_d, cx_d),
5805 [("user_b".to_string(), "online", "busy")]
5806 );
5807
5808 active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
5809 deterministic.run_until_parked();
5810 assert_eq!(
5811 contacts(&client_a, cx_a),
5812 [
5813 ("user_b".to_string(), "online", "free"),
5814 ("user_c".to_string(), "online", "free")
5815 ]
5816 );
5817 assert_eq!(
5818 contacts(&client_b, cx_b),
5819 [
5820 ("user_a".to_string(), "online", "free"),
5821 ("user_c".to_string(), "online", "free"),
5822 ("user_d".to_string(), "online", "free")
5823 ]
5824 );
5825 assert_eq!(
5826 contacts(&client_c, cx_c),
5827 [
5828 ("user_a".to_string(), "online", "free"),
5829 ("user_b".to_string(), "online", "free")
5830 ]
5831 );
5832 assert_eq!(
5833 contacts(&client_d, cx_d),
5834 [("user_b".to_string(), "online", "free")]
5835 );
5836
5837 active_call_c
5838 .update(cx_c, |call, cx| {
5839 call.invite(client_a.user_id().unwrap(), None, cx)
5840 })
5841 .await
5842 .unwrap();
5843 deterministic.run_until_parked();
5844 assert_eq!(
5845 contacts(&client_a, cx_a),
5846 [
5847 ("user_b".to_string(), "online", "free"),
5848 ("user_c".to_string(), "online", "busy")
5849 ]
5850 );
5851 assert_eq!(
5852 contacts(&client_b, cx_b),
5853 [
5854 ("user_a".to_string(), "online", "busy"),
5855 ("user_c".to_string(), "online", "busy"),
5856 ("user_d".to_string(), "online", "free")
5857 ]
5858 );
5859 assert_eq!(
5860 contacts(&client_c, cx_c),
5861 [
5862 ("user_a".to_string(), "online", "busy"),
5863 ("user_b".to_string(), "online", "free")
5864 ]
5865 );
5866 assert_eq!(
5867 contacts(&client_d, cx_d),
5868 [("user_b".to_string(), "online", "free")]
5869 );
5870
5871 active_call_a
5872 .update(cx_a, |call, cx| call.accept_incoming(cx))
5873 .await
5874 .unwrap();
5875 deterministic.run_until_parked();
5876 assert_eq!(
5877 contacts(&client_a, cx_a),
5878 [
5879 ("user_b".to_string(), "online", "free"),
5880 ("user_c".to_string(), "online", "busy")
5881 ]
5882 );
5883 assert_eq!(
5884 contacts(&client_b, cx_b),
5885 [
5886 ("user_a".to_string(), "online", "busy"),
5887 ("user_c".to_string(), "online", "busy"),
5888 ("user_d".to_string(), "online", "free")
5889 ]
5890 );
5891 assert_eq!(
5892 contacts(&client_c, cx_c),
5893 [
5894 ("user_a".to_string(), "online", "busy"),
5895 ("user_b".to_string(), "online", "free")
5896 ]
5897 );
5898 assert_eq!(
5899 contacts(&client_d, cx_d),
5900 [("user_b".to_string(), "online", "free")]
5901 );
5902
5903 active_call_a
5904 .update(cx_a, |call, cx| {
5905 call.invite(client_b.user_id().unwrap(), None, cx)
5906 })
5907 .await
5908 .unwrap();
5909 deterministic.run_until_parked();
5910 assert_eq!(
5911 contacts(&client_a, cx_a),
5912 [
5913 ("user_b".to_string(), "online", "busy"),
5914 ("user_c".to_string(), "online", "busy")
5915 ]
5916 );
5917 assert_eq!(
5918 contacts(&client_b, cx_b),
5919 [
5920 ("user_a".to_string(), "online", "busy"),
5921 ("user_c".to_string(), "online", "busy"),
5922 ("user_d".to_string(), "online", "free")
5923 ]
5924 );
5925 assert_eq!(
5926 contacts(&client_c, cx_c),
5927 [
5928 ("user_a".to_string(), "online", "busy"),
5929 ("user_b".to_string(), "online", "busy")
5930 ]
5931 );
5932 assert_eq!(
5933 contacts(&client_d, cx_d),
5934 [("user_b".to_string(), "online", "busy")]
5935 );
5936
5937 active_call_a
5938 .update(cx_a, |call, cx| call.hang_up(cx))
5939 .await
5940 .unwrap();
5941 deterministic.run_until_parked();
5942 assert_eq!(
5943 contacts(&client_a, cx_a),
5944 [
5945 ("user_b".to_string(), "online", "free"),
5946 ("user_c".to_string(), "online", "free")
5947 ]
5948 );
5949 assert_eq!(
5950 contacts(&client_b, cx_b),
5951 [
5952 ("user_a".to_string(), "online", "free"),
5953 ("user_c".to_string(), "online", "free"),
5954 ("user_d".to_string(), "online", "free")
5955 ]
5956 );
5957 assert_eq!(
5958 contacts(&client_c, cx_c),
5959 [
5960 ("user_a".to_string(), "online", "free"),
5961 ("user_b".to_string(), "online", "free")
5962 ]
5963 );
5964 assert_eq!(
5965 contacts(&client_d, cx_d),
5966 [("user_b".to_string(), "online", "free")]
5967 );
5968
5969 active_call_a
5970 .update(cx_a, |call, cx| {
5971 call.invite(client_b.user_id().unwrap(), None, cx)
5972 })
5973 .await
5974 .unwrap();
5975 deterministic.run_until_parked();
5976 assert_eq!(
5977 contacts(&client_a, cx_a),
5978 [
5979 ("user_b".to_string(), "online", "busy"),
5980 ("user_c".to_string(), "online", "free")
5981 ]
5982 );
5983 assert_eq!(
5984 contacts(&client_b, cx_b),
5985 [
5986 ("user_a".to_string(), "online", "busy"),
5987 ("user_c".to_string(), "online", "free"),
5988 ("user_d".to_string(), "online", "free")
5989 ]
5990 );
5991 assert_eq!(
5992 contacts(&client_c, cx_c),
5993 [
5994 ("user_a".to_string(), "online", "busy"),
5995 ("user_b".to_string(), "online", "busy")
5996 ]
5997 );
5998 assert_eq!(
5999 contacts(&client_d, cx_d),
6000 [("user_b".to_string(), "online", "busy")]
6001 );
6002
6003 server.forbid_connections();
6004 server.disconnect_client(client_a.peer_id().unwrap());
6005 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6006 assert_eq!(contacts(&client_a, cx_a), []);
6007 assert_eq!(
6008 contacts(&client_b, cx_b),
6009 [
6010 ("user_a".to_string(), "offline", "free"),
6011 ("user_c".to_string(), "online", "free"),
6012 ("user_d".to_string(), "online", "free")
6013 ]
6014 );
6015 assert_eq!(
6016 contacts(&client_c, cx_c),
6017 [
6018 ("user_a".to_string(), "offline", "free"),
6019 ("user_b".to_string(), "online", "free")
6020 ]
6021 );
6022 assert_eq!(
6023 contacts(&client_d, cx_d),
6024 [("user_b".to_string(), "online", "free")]
6025 );
6026
6027 // Test removing a contact
6028 client_b
6029 .user_store
6030 .update(cx_b, |store, cx| {
6031 store.remove_contact(client_c.user_id().unwrap(), cx)
6032 })
6033 .await
6034 .unwrap();
6035 deterministic.run_until_parked();
6036 assert_eq!(
6037 contacts(&client_b, cx_b),
6038 [
6039 ("user_a".to_string(), "offline", "free"),
6040 ("user_d".to_string(), "online", "free")
6041 ]
6042 );
6043 assert_eq!(
6044 contacts(&client_c, cx_c),
6045 [("user_a".to_string(), "offline", "free"),]
6046 );
6047
6048 fn contacts(
6049 client: &TestClient,
6050 cx: &TestAppContext,
6051 ) -> Vec<(String, &'static str, &'static str)> {
6052 client.user_store.read_with(cx, |store, _| {
6053 store
6054 .contacts()
6055 .iter()
6056 .map(|contact| {
6057 (
6058 contact.user.github_login.clone(),
6059 if contact.online { "online" } else { "offline" },
6060 if contact.busy { "busy" } else { "free" },
6061 )
6062 })
6063 .collect()
6064 })
6065 }
6066}
6067
6068#[gpui::test(iterations = 10)]
6069async fn test_contact_requests(
6070 deterministic: Arc<Deterministic>,
6071 cx_a: &mut TestAppContext,
6072 cx_a2: &mut TestAppContext,
6073 cx_b: &mut TestAppContext,
6074 cx_b2: &mut TestAppContext,
6075 cx_c: &mut TestAppContext,
6076 cx_c2: &mut TestAppContext,
6077) {
6078 deterministic.forbid_parking();
6079
6080 // Connect to a server as 3 clients.
6081 let mut server = TestServer::start(&deterministic).await;
6082 let client_a = server.create_client(cx_a, "user_a").await;
6083 let client_a2 = server.create_client(cx_a2, "user_a").await;
6084 let client_b = server.create_client(cx_b, "user_b").await;
6085 let client_b2 = server.create_client(cx_b2, "user_b").await;
6086 let client_c = server.create_client(cx_c, "user_c").await;
6087 let client_c2 = server.create_client(cx_c2, "user_c").await;
6088
6089 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
6090 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
6091 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
6092
6093 // User A and User C request that user B become their contact.
6094 client_a
6095 .user_store
6096 .update(cx_a, |store, cx| {
6097 store.request_contact(client_b.user_id().unwrap(), cx)
6098 })
6099 .await
6100 .unwrap();
6101 client_c
6102 .user_store
6103 .update(cx_c, |store, cx| {
6104 store.request_contact(client_b.user_id().unwrap(), cx)
6105 })
6106 .await
6107 .unwrap();
6108 deterministic.run_until_parked();
6109
6110 // All users see the pending request appear in all their clients.
6111 assert_eq!(
6112 client_a.summarize_contacts(cx_a).outgoing_requests,
6113 &["user_b"]
6114 );
6115 assert_eq!(
6116 client_a2.summarize_contacts(cx_a2).outgoing_requests,
6117 &["user_b"]
6118 );
6119 assert_eq!(
6120 client_b.summarize_contacts(cx_b).incoming_requests,
6121 &["user_a", "user_c"]
6122 );
6123 assert_eq!(
6124 client_b2.summarize_contacts(cx_b2).incoming_requests,
6125 &["user_a", "user_c"]
6126 );
6127 assert_eq!(
6128 client_c.summarize_contacts(cx_c).outgoing_requests,
6129 &["user_b"]
6130 );
6131 assert_eq!(
6132 client_c2.summarize_contacts(cx_c2).outgoing_requests,
6133 &["user_b"]
6134 );
6135
6136 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
6137 disconnect_and_reconnect(&client_a, cx_a).await;
6138 disconnect_and_reconnect(&client_b, cx_b).await;
6139 disconnect_and_reconnect(&client_c, cx_c).await;
6140 deterministic.run_until_parked();
6141 assert_eq!(
6142 client_a.summarize_contacts(cx_a).outgoing_requests,
6143 &["user_b"]
6144 );
6145 assert_eq!(
6146 client_b.summarize_contacts(cx_b).incoming_requests,
6147 &["user_a", "user_c"]
6148 );
6149 assert_eq!(
6150 client_c.summarize_contacts(cx_c).outgoing_requests,
6151 &["user_b"]
6152 );
6153
6154 // User B accepts the request from user A.
6155 client_b
6156 .user_store
6157 .update(cx_b, |store, cx| {
6158 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
6159 })
6160 .await
6161 .unwrap();
6162
6163 deterministic.run_until_parked();
6164
6165 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
6166 let contacts_b = client_b.summarize_contacts(cx_b);
6167 assert_eq!(contacts_b.current, &["user_a"]);
6168 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
6169 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6170 assert_eq!(contacts_b2.current, &["user_a"]);
6171 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
6172
6173 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
6174 let contacts_a = client_a.summarize_contacts(cx_a);
6175 assert_eq!(contacts_a.current, &["user_b"]);
6176 assert!(contacts_a.outgoing_requests.is_empty());
6177 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
6178 assert_eq!(contacts_a2.current, &["user_b"]);
6179 assert!(contacts_a2.outgoing_requests.is_empty());
6180
6181 // Contacts are present upon connecting (tested here via disconnect/reconnect)
6182 disconnect_and_reconnect(&client_a, cx_a).await;
6183 disconnect_and_reconnect(&client_b, cx_b).await;
6184 disconnect_and_reconnect(&client_c, cx_c).await;
6185 deterministic.run_until_parked();
6186 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6187 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6188 assert_eq!(
6189 client_b.summarize_contacts(cx_b).incoming_requests,
6190 &["user_c"]
6191 );
6192 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6193 assert_eq!(
6194 client_c.summarize_contacts(cx_c).outgoing_requests,
6195 &["user_b"]
6196 );
6197
6198 // User B rejects the request from user C.
6199 client_b
6200 .user_store
6201 .update(cx_b, |store, cx| {
6202 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
6203 })
6204 .await
6205 .unwrap();
6206
6207 deterministic.run_until_parked();
6208
6209 // User B doesn't see user C as their contact, and the incoming request from them is removed.
6210 let contacts_b = client_b.summarize_contacts(cx_b);
6211 assert_eq!(contacts_b.current, &["user_a"]);
6212 assert!(contacts_b.incoming_requests.is_empty());
6213 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6214 assert_eq!(contacts_b2.current, &["user_a"]);
6215 assert!(contacts_b2.incoming_requests.is_empty());
6216
6217 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
6218 let contacts_c = client_c.summarize_contacts(cx_c);
6219 assert!(contacts_c.current.is_empty());
6220 assert!(contacts_c.outgoing_requests.is_empty());
6221 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
6222 assert!(contacts_c2.current.is_empty());
6223 assert!(contacts_c2.outgoing_requests.is_empty());
6224
6225 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
6226 disconnect_and_reconnect(&client_a, cx_a).await;
6227 disconnect_and_reconnect(&client_b, cx_b).await;
6228 disconnect_and_reconnect(&client_c, cx_c).await;
6229 deterministic.run_until_parked();
6230 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6231 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6232 assert!(client_b
6233 .summarize_contacts(cx_b)
6234 .incoming_requests
6235 .is_empty());
6236 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6237 assert!(client_c
6238 .summarize_contacts(cx_c)
6239 .outgoing_requests
6240 .is_empty());
6241
6242 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6243 client.disconnect(&cx.to_async());
6244 client.clear_contacts(cx).await;
6245 client
6246 .authenticate_and_connect(false, &cx.to_async())
6247 .await
6248 .unwrap();
6249 }
6250}
6251
6252#[gpui::test(iterations = 10)]
6253async fn test_basic_following(
6254 deterministic: Arc<Deterministic>,
6255 cx_a: &mut TestAppContext,
6256 cx_b: &mut TestAppContext,
6257 cx_c: &mut TestAppContext,
6258 cx_d: &mut TestAppContext,
6259) {
6260 deterministic.forbid_parking();
6261 cx_a.update(editor::init);
6262 cx_b.update(editor::init);
6263
6264 let mut server = TestServer::start(&deterministic).await;
6265 let client_a = server.create_client(cx_a, "user_a").await;
6266 let client_b = server.create_client(cx_b, "user_b").await;
6267 let client_c = server.create_client(cx_c, "user_c").await;
6268 let client_d = server.create_client(cx_d, "user_d").await;
6269 server
6270 .create_room(&mut [
6271 (&client_a, cx_a),
6272 (&client_b, cx_b),
6273 (&client_c, cx_c),
6274 (&client_d, cx_d),
6275 ])
6276 .await;
6277 let active_call_a = cx_a.read(ActiveCall::global);
6278 let active_call_b = cx_b.read(ActiveCall::global);
6279
6280 client_a
6281 .fs
6282 .insert_tree(
6283 "/a",
6284 json!({
6285 "1.txt": "one\none\none",
6286 "2.txt": "two\ntwo\ntwo",
6287 "3.txt": "three\nthree\nthree",
6288 }),
6289 )
6290 .await;
6291 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6292 active_call_a
6293 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6294 .await
6295 .unwrap();
6296
6297 let project_id = active_call_a
6298 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6299 .await
6300 .unwrap();
6301 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6302 active_call_b
6303 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6304 .await
6305 .unwrap();
6306
6307 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6308 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6309
6310 // Client A opens some editors.
6311 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6312 let editor_a1 = workspace_a
6313 .update(cx_a, |workspace, cx| {
6314 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6315 })
6316 .await
6317 .unwrap()
6318 .downcast::<Editor>()
6319 .unwrap();
6320 let editor_a2 = workspace_a
6321 .update(cx_a, |workspace, cx| {
6322 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6323 })
6324 .await
6325 .unwrap()
6326 .downcast::<Editor>()
6327 .unwrap();
6328
6329 // Client B opens an editor.
6330 let editor_b1 = workspace_b
6331 .update(cx_b, |workspace, cx| {
6332 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6333 })
6334 .await
6335 .unwrap()
6336 .downcast::<Editor>()
6337 .unwrap();
6338
6339 let peer_id_a = client_a.peer_id().unwrap();
6340 let peer_id_b = client_b.peer_id().unwrap();
6341 let peer_id_c = client_c.peer_id().unwrap();
6342 let peer_id_d = client_d.peer_id().unwrap();
6343
6344 // Client A updates their selections in those editors
6345 editor_a1.update(cx_a, |editor, cx| {
6346 editor.handle_input("a", cx);
6347 editor.handle_input("b", cx);
6348 editor.handle_input("c", cx);
6349 editor.select_left(&Default::default(), cx);
6350 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
6351 });
6352 editor_a2.update(cx_a, |editor, cx| {
6353 editor.handle_input("d", cx);
6354 editor.handle_input("e", cx);
6355 editor.select_left(&Default::default(), cx);
6356 assert_eq!(editor.selections.ranges(cx), vec![2..1]);
6357 });
6358
6359 // When client B starts following client A, all visible view states are replicated to client B.
6360 workspace_b
6361 .update(cx_b, |workspace, cx| {
6362 workspace.toggle_follow(peer_id_a, cx).unwrap()
6363 })
6364 .await
6365 .unwrap();
6366
6367 cx_c.foreground().run_until_parked();
6368 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
6369 workspace
6370 .active_item(cx)
6371 .unwrap()
6372 .downcast::<Editor>()
6373 .unwrap()
6374 });
6375 assert_eq!(
6376 cx_b.read(|cx| editor_b2.project_path(cx)),
6377 Some((worktree_id, "2.txt").into())
6378 );
6379 assert_eq!(
6380 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
6381 vec![2..1]
6382 );
6383 assert_eq!(
6384 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
6385 vec![3..2]
6386 );
6387
6388 cx_c.foreground().run_until_parked();
6389 let active_call_c = cx_c.read(ActiveCall::global);
6390 let project_c = client_c.build_remote_project(project_id, cx_c).await;
6391 let workspace_c = client_c.build_workspace(&project_c, cx_c);
6392 active_call_c
6393 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
6394 .await
6395 .unwrap();
6396 drop(project_c);
6397
6398 // Client C also follows client A.
6399 workspace_c
6400 .update(cx_c, |workspace, cx| {
6401 workspace.toggle_follow(peer_id_a, cx).unwrap()
6402 })
6403 .await
6404 .unwrap();
6405
6406 cx_d.foreground().run_until_parked();
6407 let active_call_d = cx_d.read(ActiveCall::global);
6408 let project_d = client_d.build_remote_project(project_id, cx_d).await;
6409 let workspace_d = client_d.build_workspace(&project_d, cx_d);
6410 active_call_d
6411 .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
6412 .await
6413 .unwrap();
6414 drop(project_d);
6415
6416 // All clients see that clients B and C are following client A.
6417 cx_c.foreground().run_until_parked();
6418 for (name, active_call, cx) in [
6419 ("A", &active_call_a, &cx_a),
6420 ("B", &active_call_b, &cx_b),
6421 ("C", &active_call_c, &cx_c),
6422 ("D", &active_call_d, &cx_d),
6423 ] {
6424 active_call.read_with(*cx, |call, cx| {
6425 let room = call.room().unwrap().read(cx);
6426 assert_eq!(
6427 room.followers_for(peer_id_a, project_id),
6428 &[peer_id_b, peer_id_c],
6429 "checking followers for A as {name}"
6430 );
6431 });
6432 }
6433
6434 // Client C unfollows client A.
6435 workspace_c.update(cx_c, |workspace, cx| {
6436 workspace.toggle_follow(peer_id_a, cx);
6437 });
6438
6439 // All clients see that clients B is following client A.
6440 cx_c.foreground().run_until_parked();
6441 for (name, active_call, cx) in [
6442 ("A", &active_call_a, &cx_a),
6443 ("B", &active_call_b, &cx_b),
6444 ("C", &active_call_c, &cx_c),
6445 ("D", &active_call_d, &cx_d),
6446 ] {
6447 active_call.read_with(*cx, |call, cx| {
6448 let room = call.room().unwrap().read(cx);
6449 assert_eq!(
6450 room.followers_for(peer_id_a, project_id),
6451 &[peer_id_b],
6452 "checking followers for A as {name}"
6453 );
6454 });
6455 }
6456
6457 // Client C re-follows client A.
6458 workspace_c.update(cx_c, |workspace, cx| {
6459 workspace.toggle_follow(peer_id_a, cx);
6460 });
6461
6462 // All clients see that clients B and C are following client A.
6463 cx_c.foreground().run_until_parked();
6464 for (name, active_call, cx) in [
6465 ("A", &active_call_a, &cx_a),
6466 ("B", &active_call_b, &cx_b),
6467 ("C", &active_call_c, &cx_c),
6468 ("D", &active_call_d, &cx_d),
6469 ] {
6470 active_call.read_with(*cx, |call, cx| {
6471 let room = call.room().unwrap().read(cx);
6472 assert_eq!(
6473 room.followers_for(peer_id_a, project_id),
6474 &[peer_id_b, peer_id_c],
6475 "checking followers for A as {name}"
6476 );
6477 });
6478 }
6479
6480 // Client D follows client C.
6481 workspace_d
6482 .update(cx_d, |workspace, cx| {
6483 workspace.toggle_follow(peer_id_c, cx).unwrap()
6484 })
6485 .await
6486 .unwrap();
6487
6488 // All clients see that D is following C
6489 cx_d.foreground().run_until_parked();
6490 for (name, active_call, cx) in [
6491 ("A", &active_call_a, &cx_a),
6492 ("B", &active_call_b, &cx_b),
6493 ("C", &active_call_c, &cx_c),
6494 ("D", &active_call_d, &cx_d),
6495 ] {
6496 active_call.read_with(*cx, |call, cx| {
6497 let room = call.room().unwrap().read(cx);
6498 assert_eq!(
6499 room.followers_for(peer_id_c, project_id),
6500 &[peer_id_d],
6501 "checking followers for C as {name}"
6502 );
6503 });
6504 }
6505
6506 // Client C closes the project.
6507 cx_c.drop_last(workspace_c);
6508
6509 // Clients A and B see that client B is following A, and client C is not present in the followers.
6510 cx_c.foreground().run_until_parked();
6511 for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
6512 active_call.read_with(*cx, |call, cx| {
6513 let room = call.room().unwrap().read(cx);
6514 assert_eq!(
6515 room.followers_for(peer_id_a, project_id),
6516 &[peer_id_b],
6517 "checking followers for A as {name}"
6518 );
6519 });
6520 }
6521
6522 // All clients see that no-one is following C
6523 for (name, active_call, cx) in [
6524 ("A", &active_call_a, &cx_a),
6525 ("B", &active_call_b, &cx_b),
6526 ("C", &active_call_c, &cx_c),
6527 ("D", &active_call_d, &cx_d),
6528 ] {
6529 active_call.read_with(*cx, |call, cx| {
6530 let room = call.room().unwrap().read(cx);
6531 assert_eq!(
6532 room.followers_for(peer_id_c, project_id),
6533 &[],
6534 "checking followers for C as {name}"
6535 );
6536 });
6537 }
6538
6539 // When client A activates a different editor, client B does so as well.
6540 workspace_a.update(cx_a, |workspace, cx| {
6541 workspace.activate_item(&editor_a1, cx)
6542 });
6543 deterministic.run_until_parked();
6544 workspace_b.read_with(cx_b, |workspace, cx| {
6545 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6546 });
6547
6548 // When client A opens a multibuffer, client B does so as well.
6549 let multibuffer_a = cx_a.add_model(|cx| {
6550 let buffer_a1 = project_a.update(cx, |project, cx| {
6551 project
6552 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
6553 .unwrap()
6554 });
6555 let buffer_a2 = project_a.update(cx, |project, cx| {
6556 project
6557 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
6558 .unwrap()
6559 });
6560 let mut result = MultiBuffer::new(0);
6561 result.push_excerpts(
6562 buffer_a1,
6563 [ExcerptRange {
6564 context: 0..3,
6565 primary: None,
6566 }],
6567 cx,
6568 );
6569 result.push_excerpts(
6570 buffer_a2,
6571 [ExcerptRange {
6572 context: 4..7,
6573 primary: None,
6574 }],
6575 cx,
6576 );
6577 result
6578 });
6579 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
6580 let editor =
6581 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
6582 workspace.add_item(Box::new(editor.clone()), cx);
6583 editor
6584 });
6585 deterministic.run_until_parked();
6586 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
6587 workspace
6588 .active_item(cx)
6589 .unwrap()
6590 .downcast::<Editor>()
6591 .unwrap()
6592 });
6593 assert_eq!(
6594 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
6595 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
6596 );
6597
6598 // When client A navigates back and forth, client B does so as well.
6599 workspace_a
6600 .update(cx_a, |workspace, cx| {
6601 workspace::Pane::go_back(workspace, None, cx)
6602 })
6603 .await
6604 .unwrap();
6605 deterministic.run_until_parked();
6606 workspace_b.read_with(cx_b, |workspace, cx| {
6607 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6608 });
6609
6610 workspace_a
6611 .update(cx_a, |workspace, cx| {
6612 workspace::Pane::go_back(workspace, None, cx)
6613 })
6614 .await
6615 .unwrap();
6616 deterministic.run_until_parked();
6617 workspace_b.read_with(cx_b, |workspace, cx| {
6618 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
6619 });
6620
6621 workspace_a
6622 .update(cx_a, |workspace, cx| {
6623 workspace::Pane::go_forward(workspace, None, cx)
6624 })
6625 .await
6626 .unwrap();
6627 deterministic.run_until_parked();
6628 workspace_b.read_with(cx_b, |workspace, cx| {
6629 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6630 });
6631
6632 // Changes to client A's editor are reflected on client B.
6633 editor_a1.update(cx_a, |editor, cx| {
6634 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
6635 });
6636 deterministic.run_until_parked();
6637 editor_b1.read_with(cx_b, |editor, cx| {
6638 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
6639 });
6640
6641 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
6642 deterministic.run_until_parked();
6643 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
6644
6645 editor_a1.update(cx_a, |editor, cx| {
6646 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
6647 editor.set_scroll_position(vec2f(0., 100.), cx);
6648 });
6649 deterministic.run_until_parked();
6650 editor_b1.read_with(cx_b, |editor, cx| {
6651 assert_eq!(editor.selections.ranges(cx), &[3..3]);
6652 });
6653
6654 // After unfollowing, client B stops receiving updates from client A.
6655 workspace_b.update(cx_b, |workspace, cx| {
6656 workspace.unfollow(&workspace.active_pane().clone(), cx)
6657 });
6658 workspace_a.update(cx_a, |workspace, cx| {
6659 workspace.activate_item(&editor_a2, cx)
6660 });
6661 deterministic.run_until_parked();
6662 assert_eq!(
6663 workspace_b.read_with(cx_b, |workspace, cx| workspace
6664 .active_item(cx)
6665 .unwrap()
6666 .id()),
6667 editor_b1.id()
6668 );
6669
6670 // Client A starts following client B.
6671 workspace_a
6672 .update(cx_a, |workspace, cx| {
6673 workspace.toggle_follow(peer_id_b, cx).unwrap()
6674 })
6675 .await
6676 .unwrap();
6677 assert_eq!(
6678 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6679 Some(peer_id_b)
6680 );
6681 assert_eq!(
6682 workspace_a.read_with(cx_a, |workspace, cx| workspace
6683 .active_item(cx)
6684 .unwrap()
6685 .id()),
6686 editor_a1.id()
6687 );
6688
6689 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
6690 let display = MacOSDisplay::new();
6691 active_call_b
6692 .update(cx_b, |call, cx| call.set_location(None, cx))
6693 .await
6694 .unwrap();
6695 active_call_b
6696 .update(cx_b, |call, cx| {
6697 call.room().unwrap().update(cx, |room, cx| {
6698 room.set_display_sources(vec![display.clone()]);
6699 room.share_screen(cx)
6700 })
6701 })
6702 .await
6703 .unwrap();
6704 deterministic.run_until_parked();
6705 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
6706 workspace
6707 .active_item(cx)
6708 .unwrap()
6709 .downcast::<SharedScreen>()
6710 .unwrap()
6711 });
6712
6713 // Client B activates Zed again, which causes the previous editor to become focused again.
6714 active_call_b
6715 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6716 .await
6717 .unwrap();
6718 deterministic.run_until_parked();
6719 workspace_a.read_with(cx_a, |workspace, cx| {
6720 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
6721 });
6722
6723 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
6724 workspace_b.update(cx_b, |workspace, cx| {
6725 workspace.activate_item(&multibuffer_editor_b, cx)
6726 });
6727 deterministic.run_until_parked();
6728 workspace_a.read_with(cx_a, |workspace, cx| {
6729 assert_eq!(
6730 workspace.active_item(cx).unwrap().id(),
6731 multibuffer_editor_a.id()
6732 )
6733 });
6734
6735 // Client B activates an external window again, and the previously-opened screen-sharing item
6736 // gets activated.
6737 active_call_b
6738 .update(cx_b, |call, cx| call.set_location(None, cx))
6739 .await
6740 .unwrap();
6741 deterministic.run_until_parked();
6742 assert_eq!(
6743 workspace_a.read_with(cx_a, |workspace, cx| workspace
6744 .active_item(cx)
6745 .unwrap()
6746 .id()),
6747 shared_screen.id()
6748 );
6749
6750 // Following interrupts when client B disconnects.
6751 client_b.disconnect(&cx_b.to_async());
6752 deterministic.advance_clock(RECONNECT_TIMEOUT);
6753 assert_eq!(
6754 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6755 None
6756 );
6757}
6758
6759#[gpui::test(iterations = 10)]
6760async fn test_join_call_after_screen_was_shared(
6761 deterministic: Arc<Deterministic>,
6762 cx_a: &mut TestAppContext,
6763 cx_b: &mut TestAppContext,
6764) {
6765 deterministic.forbid_parking();
6766 let mut server = TestServer::start(&deterministic).await;
6767
6768 let client_a = server.create_client(cx_a, "user_a").await;
6769 let client_b = server.create_client(cx_b, "user_b").await;
6770 server
6771 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6772 .await;
6773
6774 let active_call_a = cx_a.read(ActiveCall::global);
6775 let active_call_b = cx_b.read(ActiveCall::global);
6776
6777 // Call users B and C from client A.
6778 active_call_a
6779 .update(cx_a, |call, cx| {
6780 call.invite(client_b.user_id().unwrap(), None, cx)
6781 })
6782 .await
6783 .unwrap();
6784 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6785 deterministic.run_until_parked();
6786 assert_eq!(
6787 room_participants(&room_a, cx_a),
6788 RoomParticipants {
6789 remote: Default::default(),
6790 pending: vec!["user_b".to_string()]
6791 }
6792 );
6793
6794 // User B receives the call.
6795 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6796 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6797 assert_eq!(call_b.calling_user.github_login, "user_a");
6798
6799 // User A shares their screen
6800 let display = MacOSDisplay::new();
6801 active_call_a
6802 .update(cx_a, |call, cx| {
6803 call.room().unwrap().update(cx, |room, cx| {
6804 room.set_display_sources(vec![display.clone()]);
6805 room.share_screen(cx)
6806 })
6807 })
6808 .await
6809 .unwrap();
6810
6811 client_b.user_store.update(cx_b, |user_store, _| {
6812 user_store.clear_cache();
6813 });
6814
6815 // User B joins the room
6816 active_call_b
6817 .update(cx_b, |call, cx| call.accept_incoming(cx))
6818 .await
6819 .unwrap();
6820 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6821 assert!(incoming_call_b.next().await.unwrap().is_none());
6822
6823 deterministic.run_until_parked();
6824 assert_eq!(
6825 room_participants(&room_a, cx_a),
6826 RoomParticipants {
6827 remote: vec!["user_b".to_string()],
6828 pending: vec![],
6829 }
6830 );
6831 assert_eq!(
6832 room_participants(&room_b, cx_b),
6833 RoomParticipants {
6834 remote: vec!["user_a".to_string()],
6835 pending: vec![],
6836 }
6837 );
6838
6839 // Ensure User B sees User A's screenshare.
6840 room_b.read_with(cx_b, |room, _| {
6841 assert_eq!(
6842 room.remote_participants()
6843 .get(&client_a.user_id().unwrap())
6844 .unwrap()
6845 .tracks
6846 .len(),
6847 1
6848 );
6849 });
6850}
6851
6852#[gpui::test]
6853async fn test_following_tab_order(
6854 deterministic: Arc<Deterministic>,
6855 cx_a: &mut TestAppContext,
6856 cx_b: &mut TestAppContext,
6857) {
6858 cx_a.update(editor::init);
6859 cx_b.update(editor::init);
6860
6861 let mut server = TestServer::start(&deterministic).await;
6862 let client_a = server.create_client(cx_a, "user_a").await;
6863 let client_b = server.create_client(cx_b, "user_b").await;
6864 server
6865 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6866 .await;
6867 let active_call_a = cx_a.read(ActiveCall::global);
6868 let active_call_b = cx_b.read(ActiveCall::global);
6869
6870 client_a
6871 .fs
6872 .insert_tree(
6873 "/a",
6874 json!({
6875 "1.txt": "one",
6876 "2.txt": "two",
6877 "3.txt": "three",
6878 }),
6879 )
6880 .await;
6881 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6882 active_call_a
6883 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6884 .await
6885 .unwrap();
6886
6887 let project_id = active_call_a
6888 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6889 .await
6890 .unwrap();
6891 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6892 active_call_b
6893 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6894 .await
6895 .unwrap();
6896
6897 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6898 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6899
6900 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6901 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6902
6903 let client_b_id = project_a.read_with(cx_a, |project, _| {
6904 project.collaborators().values().next().unwrap().peer_id
6905 });
6906
6907 //Open 1, 3 in that order on client A
6908 workspace_a
6909 .update(cx_a, |workspace, cx| {
6910 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6911 })
6912 .await
6913 .unwrap();
6914 workspace_a
6915 .update(cx_a, |workspace, cx| {
6916 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6917 })
6918 .await
6919 .unwrap();
6920
6921 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
6922 pane.update(cx, |pane, cx| {
6923 pane.items()
6924 .map(|item| {
6925 item.project_path(cx)
6926 .unwrap()
6927 .path
6928 .to_str()
6929 .unwrap()
6930 .to_owned()
6931 })
6932 .collect::<Vec<_>>()
6933 })
6934 };
6935
6936 //Verify that the tabs opened in the order we expect
6937 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
6938
6939 //Follow client B as client A
6940 workspace_a
6941 .update(cx_a, |workspace, cx| {
6942 workspace.toggle_follow(client_b_id, cx).unwrap()
6943 })
6944 .await
6945 .unwrap();
6946
6947 //Open just 2 on client B
6948 workspace_b
6949 .update(cx_b, |workspace, cx| {
6950 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6951 })
6952 .await
6953 .unwrap();
6954 deterministic.run_until_parked();
6955
6956 // Verify that newly opened followed file is at the end
6957 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6958
6959 //Open just 1 on client B
6960 workspace_b
6961 .update(cx_b, |workspace, cx| {
6962 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6963 })
6964 .await
6965 .unwrap();
6966 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
6967 deterministic.run_until_parked();
6968
6969 // Verify that following into 1 did not reorder
6970 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6971}
6972
6973#[gpui::test(iterations = 10)]
6974async fn test_peers_following_each_other(
6975 deterministic: Arc<Deterministic>,
6976 cx_a: &mut TestAppContext,
6977 cx_b: &mut TestAppContext,
6978) {
6979 deterministic.forbid_parking();
6980 cx_a.update(editor::init);
6981 cx_b.update(editor::init);
6982
6983 let mut server = TestServer::start(&deterministic).await;
6984 let client_a = server.create_client(cx_a, "user_a").await;
6985 let client_b = server.create_client(cx_b, "user_b").await;
6986 server
6987 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6988 .await;
6989 let active_call_a = cx_a.read(ActiveCall::global);
6990 let active_call_b = cx_b.read(ActiveCall::global);
6991
6992 // Client A shares a project.
6993 client_a
6994 .fs
6995 .insert_tree(
6996 "/a",
6997 json!({
6998 "1.txt": "one",
6999 "2.txt": "two",
7000 "3.txt": "three",
7001 "4.txt": "four",
7002 }),
7003 )
7004 .await;
7005 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7006 active_call_a
7007 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7008 .await
7009 .unwrap();
7010 let project_id = active_call_a
7011 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7012 .await
7013 .unwrap();
7014
7015 // Client B joins the project.
7016 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7017 active_call_b
7018 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7019 .await
7020 .unwrap();
7021
7022 // Client A opens some editors.
7023 let workspace_a = client_a.build_workspace(&project_a, cx_a);
7024 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
7025 let _editor_a1 = workspace_a
7026 .update(cx_a, |workspace, cx| {
7027 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
7028 })
7029 .await
7030 .unwrap()
7031 .downcast::<Editor>()
7032 .unwrap();
7033
7034 // Client B opens an editor.
7035 let workspace_b = client_b.build_workspace(&project_b, cx_b);
7036 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
7037 let _editor_b1 = workspace_b
7038 .update(cx_b, |workspace, cx| {
7039 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
7040 })
7041 .await
7042 .unwrap()
7043 .downcast::<Editor>()
7044 .unwrap();
7045
7046 // Clients A and B follow each other in split panes
7047 workspace_a.update(cx_a, |workspace, cx| {
7048 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7049 });
7050 workspace_a
7051 .update(cx_a, |workspace, cx| {
7052 assert_ne!(*workspace.active_pane(), pane_a1);
7053 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
7054 workspace.toggle_follow(leader_id, cx).unwrap()
7055 })
7056 .await
7057 .unwrap();
7058 workspace_b.update(cx_b, |workspace, cx| {
7059 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7060 });
7061 workspace_b
7062 .update(cx_b, |workspace, cx| {
7063 assert_ne!(*workspace.active_pane(), pane_b1);
7064 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
7065 workspace.toggle_follow(leader_id, cx).unwrap()
7066 })
7067 .await
7068 .unwrap();
7069
7070 workspace_a.update(cx_a, |workspace, cx| {
7071 workspace.activate_next_pane(cx);
7072 });
7073 // Wait for focus effects to be fully flushed
7074 workspace_a.update(cx_a, |workspace, _| {
7075 assert_eq!(*workspace.active_pane(), pane_a1);
7076 });
7077
7078 workspace_a
7079 .update(cx_a, |workspace, cx| {
7080 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
7081 })
7082 .await
7083 .unwrap();
7084 workspace_b.update(cx_b, |workspace, cx| {
7085 workspace.activate_next_pane(cx);
7086 });
7087
7088 workspace_b
7089 .update(cx_b, |workspace, cx| {
7090 assert_eq!(*workspace.active_pane(), pane_b1);
7091 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
7092 })
7093 .await
7094 .unwrap();
7095 cx_a.foreground().run_until_parked();
7096
7097 // Ensure leader updates don't change the active pane of followers
7098 workspace_a.read_with(cx_a, |workspace, _| {
7099 assert_eq!(*workspace.active_pane(), pane_a1);
7100 });
7101 workspace_b.read_with(cx_b, |workspace, _| {
7102 assert_eq!(*workspace.active_pane(), pane_b1);
7103 });
7104
7105 // Ensure peers following each other doesn't cause an infinite loop.
7106 assert_eq!(
7107 workspace_a.read_with(cx_a, |workspace, cx| workspace
7108 .active_item(cx)
7109 .unwrap()
7110 .project_path(cx)),
7111 Some((worktree_id, "3.txt").into())
7112 );
7113 workspace_a.update(cx_a, |workspace, cx| {
7114 assert_eq!(
7115 workspace.active_item(cx).unwrap().project_path(cx),
7116 Some((worktree_id, "3.txt").into())
7117 );
7118 workspace.activate_next_pane(cx);
7119 });
7120
7121 workspace_a.update(cx_a, |workspace, cx| {
7122 assert_eq!(
7123 workspace.active_item(cx).unwrap().project_path(cx),
7124 Some((worktree_id, "4.txt").into())
7125 );
7126 });
7127
7128 workspace_b.update(cx_b, |workspace, cx| {
7129 assert_eq!(
7130 workspace.active_item(cx).unwrap().project_path(cx),
7131 Some((worktree_id, "4.txt").into())
7132 );
7133 workspace.activate_next_pane(cx);
7134 });
7135
7136 workspace_b.update(cx_b, |workspace, cx| {
7137 assert_eq!(
7138 workspace.active_item(cx).unwrap().project_path(cx),
7139 Some((worktree_id, "3.txt").into())
7140 );
7141 });
7142}
7143
7144#[gpui::test(iterations = 10)]
7145async fn test_auto_unfollowing(
7146 deterministic: Arc<Deterministic>,
7147 cx_a: &mut TestAppContext,
7148 cx_b: &mut TestAppContext,
7149) {
7150 deterministic.forbid_parking();
7151 cx_a.update(editor::init);
7152 cx_b.update(editor::init);
7153
7154 // 2 clients connect to a server.
7155 let mut server = TestServer::start(&deterministic).await;
7156 let client_a = server.create_client(cx_a, "user_a").await;
7157 let client_b = server.create_client(cx_b, "user_b").await;
7158 server
7159 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7160 .await;
7161 let active_call_a = cx_a.read(ActiveCall::global);
7162 let active_call_b = cx_b.read(ActiveCall::global);
7163
7164 // Client A shares a project.
7165 client_a
7166 .fs
7167 .insert_tree(
7168 "/a",
7169 json!({
7170 "1.txt": "one",
7171 "2.txt": "two",
7172 "3.txt": "three",
7173 }),
7174 )
7175 .await;
7176 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7177 active_call_a
7178 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7179 .await
7180 .unwrap();
7181
7182 let project_id = active_call_a
7183 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7184 .await
7185 .unwrap();
7186 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7187 active_call_b
7188 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7189 .await
7190 .unwrap();
7191
7192 // Client A opens some editors.
7193 let workspace_a = client_a.build_workspace(&project_a, cx_a);
7194 let _editor_a1 = workspace_a
7195 .update(cx_a, |workspace, cx| {
7196 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
7197 })
7198 .await
7199 .unwrap()
7200 .downcast::<Editor>()
7201 .unwrap();
7202
7203 // Client B starts following client A.
7204 let workspace_b = client_b.build_workspace(&project_b, cx_b);
7205 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
7206 let leader_id = project_b.read_with(cx_b, |project, _| {
7207 project.collaborators().values().next().unwrap().peer_id
7208 });
7209 workspace_b
7210 .update(cx_b, |workspace, cx| {
7211 workspace.toggle_follow(leader_id, cx).unwrap()
7212 })
7213 .await
7214 .unwrap();
7215 assert_eq!(
7216 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7217 Some(leader_id)
7218 );
7219 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
7220 workspace
7221 .active_item(cx)
7222 .unwrap()
7223 .downcast::<Editor>()
7224 .unwrap()
7225 });
7226
7227 // When client B moves, it automatically stops following client A.
7228 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
7229 assert_eq!(
7230 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7231 None
7232 );
7233
7234 workspace_b
7235 .update(cx_b, |workspace, cx| {
7236 workspace.toggle_follow(leader_id, cx).unwrap()
7237 })
7238 .await
7239 .unwrap();
7240 assert_eq!(
7241 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7242 Some(leader_id)
7243 );
7244
7245 // When client B edits, it automatically stops following client A.
7246 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
7247 assert_eq!(
7248 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7249 None
7250 );
7251
7252 workspace_b
7253 .update(cx_b, |workspace, cx| {
7254 workspace.toggle_follow(leader_id, cx).unwrap()
7255 })
7256 .await
7257 .unwrap();
7258 assert_eq!(
7259 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7260 Some(leader_id)
7261 );
7262
7263 // When client B scrolls, it automatically stops following client A.
7264 editor_b2.update(cx_b, |editor, cx| {
7265 editor.set_scroll_position(vec2f(0., 3.), cx)
7266 });
7267 assert_eq!(
7268 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7269 None
7270 );
7271
7272 workspace_b
7273 .update(cx_b, |workspace, cx| {
7274 workspace.toggle_follow(leader_id, cx).unwrap()
7275 })
7276 .await
7277 .unwrap();
7278 assert_eq!(
7279 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7280 Some(leader_id)
7281 );
7282
7283 // When client B activates a different pane, it continues following client A in the original pane.
7284 workspace_b.update(cx_b, |workspace, cx| {
7285 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
7286 });
7287 assert_eq!(
7288 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7289 Some(leader_id)
7290 );
7291
7292 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
7293 assert_eq!(
7294 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7295 Some(leader_id)
7296 );
7297
7298 // When client B activates a different item in the original pane, it automatically stops following client A.
7299 workspace_b
7300 .update(cx_b, |workspace, cx| {
7301 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
7302 })
7303 .await
7304 .unwrap();
7305 assert_eq!(
7306 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7307 None
7308 );
7309}
7310
7311#[gpui::test(iterations = 10)]
7312async fn test_peers_simultaneously_following_each_other(
7313 deterministic: Arc<Deterministic>,
7314 cx_a: &mut TestAppContext,
7315 cx_b: &mut TestAppContext,
7316) {
7317 deterministic.forbid_parking();
7318 cx_a.update(editor::init);
7319 cx_b.update(editor::init);
7320
7321 let mut server = TestServer::start(&deterministic).await;
7322 let client_a = server.create_client(cx_a, "user_a").await;
7323 let client_b = server.create_client(cx_b, "user_b").await;
7324 server
7325 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7326 .await;
7327 let active_call_a = cx_a.read(ActiveCall::global);
7328
7329 client_a.fs.insert_tree("/a", json!({})).await;
7330 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
7331 let workspace_a = client_a.build_workspace(&project_a, cx_a);
7332 let project_id = active_call_a
7333 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7334 .await
7335 .unwrap();
7336
7337 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7338 let workspace_b = client_b.build_workspace(&project_b, cx_b);
7339
7340 deterministic.run_until_parked();
7341 let client_a_id = project_b.read_with(cx_b, |project, _| {
7342 project.collaborators().values().next().unwrap().peer_id
7343 });
7344 let client_b_id = project_a.read_with(cx_a, |project, _| {
7345 project.collaborators().values().next().unwrap().peer_id
7346 });
7347
7348 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
7349 workspace.toggle_follow(client_b_id, cx).unwrap()
7350 });
7351 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
7352 workspace.toggle_follow(client_a_id, cx).unwrap()
7353 });
7354
7355 futures::try_join!(a_follow_b, b_follow_a).unwrap();
7356 workspace_a.read_with(cx_a, |workspace, _| {
7357 assert_eq!(
7358 workspace.leader_for_pane(workspace.active_pane()),
7359 Some(client_b_id)
7360 );
7361 });
7362 workspace_b.read_with(cx_b, |workspace, _| {
7363 assert_eq!(
7364 workspace.leader_for_pane(workspace.active_pane()),
7365 Some(client_a_id)
7366 );
7367 });
7368}
7369
7370#[derive(Debug, Eq, PartialEq)]
7371struct RoomParticipants {
7372 remote: Vec<String>,
7373 pending: Vec<String>,
7374}
7375
7376fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
7377 room.read_with(cx, |room, _| {
7378 let mut remote = room
7379 .remote_participants()
7380 .iter()
7381 .map(|(_, participant)| participant.user.github_login.clone())
7382 .collect::<Vec<_>>();
7383 let mut pending = room
7384 .pending_participants()
7385 .iter()
7386 .map(|user| user.github_login.clone())
7387 .collect::<Vec<_>>();
7388 remote.sort();
7389 pending.sort();
7390 RoomParticipants { remote, pending }
7391 })
7392}