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