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