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