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, HoverBlockKind, Project, ProjectPath};
27use rand::prelude::*;
28use serde_json::json;
29use settings::{Formatter, Settings};
30use std::{
31 cell::{Cell, RefCell},
32 env, future, mem,
33 path::{Path, PathBuf},
34 rc::Rc,
35 sync::{
36 atomic::{AtomicBool, Ordering::SeqCst},
37 Arc,
38 },
39};
40use unindent::Unindent as _;
41use workspace::{
42 item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, ToggleFollow, Workspace,
43};
44
45#[ctor::ctor]
46fn init_logger() {
47 if std::env::var("RUST_LOG").is_ok() {
48 env_logger::init();
49 }
50}
51
52#[gpui::test(iterations = 10)]
53async fn test_basic_calls(
54 deterministic: Arc<Deterministic>,
55 cx_a: &mut TestAppContext,
56 cx_b: &mut TestAppContext,
57 cx_b2: &mut TestAppContext,
58 cx_c: &mut TestAppContext,
59) {
60 deterministic.forbid_parking();
61 let mut server = TestServer::start(&deterministic).await;
62
63 let client_a = server.create_client(cx_a, "user_a").await;
64 let client_b = server.create_client(cx_b, "user_b").await;
65 let client_c = server.create_client(cx_c, "user_c").await;
66 server
67 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
68 .await;
69
70 let active_call_a = cx_a.read(ActiveCall::global);
71 let active_call_b = cx_b.read(ActiveCall::global);
72 let active_call_c = cx_c.read(ActiveCall::global);
73
74 // Call user B from client A.
75 active_call_a
76 .update(cx_a, |call, cx| {
77 call.invite(client_b.user_id().unwrap(), None, cx)
78 })
79 .await
80 .unwrap();
81 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
82 deterministic.run_until_parked();
83 assert_eq!(
84 room_participants(&room_a, cx_a),
85 RoomParticipants {
86 remote: Default::default(),
87 pending: vec!["user_b".to_string()]
88 }
89 );
90
91 // User B receives the call.
92 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
93 let call_b = incoming_call_b.next().await.unwrap().unwrap();
94 assert_eq!(call_b.calling_user.github_login, "user_a");
95
96 // User B connects via another client and also receives a ring on the newly-connected client.
97 let _client_b2 = server.create_client(cx_b2, "user_b").await;
98 let active_call_b2 = cx_b2.read(ActiveCall::global);
99 let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
100 deterministic.run_until_parked();
101 let call_b2 = incoming_call_b2.next().await.unwrap().unwrap();
102 assert_eq!(call_b2.calling_user.github_login, "user_a");
103
104 // User B joins the room using the first client.
105 active_call_b
106 .update(cx_b, |call, cx| call.accept_incoming(cx))
107 .await
108 .unwrap();
109 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
110 assert!(incoming_call_b.next().await.unwrap().is_none());
111
112 deterministic.run_until_parked();
113 assert_eq!(
114 room_participants(&room_a, cx_a),
115 RoomParticipants {
116 remote: vec!["user_b".to_string()],
117 pending: Default::default()
118 }
119 );
120 assert_eq!(
121 room_participants(&room_b, cx_b),
122 RoomParticipants {
123 remote: vec!["user_a".to_string()],
124 pending: Default::default()
125 }
126 );
127
128 // Call user C from client B.
129 let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
130 active_call_b
131 .update(cx_b, |call, cx| {
132 call.invite(client_c.user_id().unwrap(), None, cx)
133 })
134 .await
135 .unwrap();
136
137 deterministic.run_until_parked();
138 assert_eq!(
139 room_participants(&room_a, cx_a),
140 RoomParticipants {
141 remote: vec!["user_b".to_string()],
142 pending: vec!["user_c".to_string()]
143 }
144 );
145 assert_eq!(
146 room_participants(&room_b, cx_b),
147 RoomParticipants {
148 remote: vec!["user_a".to_string()],
149 pending: vec!["user_c".to_string()]
150 }
151 );
152
153 // User C receives the call, but declines it.
154 let call_c = incoming_call_c.next().await.unwrap().unwrap();
155 assert_eq!(call_c.calling_user.github_login, "user_b");
156 active_call_c.update(cx_c, |call, _| call.decline_incoming().unwrap());
157 assert!(incoming_call_c.next().await.unwrap().is_none());
158
159 deterministic.run_until_parked();
160 assert_eq!(
161 room_participants(&room_a, cx_a),
162 RoomParticipants {
163 remote: vec!["user_b".to_string()],
164 pending: Default::default()
165 }
166 );
167 assert_eq!(
168 room_participants(&room_b, cx_b),
169 RoomParticipants {
170 remote: vec!["user_a".to_string()],
171 pending: Default::default()
172 }
173 );
174
175 // Call user C again from user A.
176 active_call_a
177 .update(cx_a, |call, cx| {
178 call.invite(client_c.user_id().unwrap(), None, cx)
179 })
180 .await
181 .unwrap();
182
183 deterministic.run_until_parked();
184 assert_eq!(
185 room_participants(&room_a, cx_a),
186 RoomParticipants {
187 remote: vec!["user_b".to_string()],
188 pending: vec!["user_c".to_string()]
189 }
190 );
191 assert_eq!(
192 room_participants(&room_b, cx_b),
193 RoomParticipants {
194 remote: vec!["user_a".to_string()],
195 pending: vec!["user_c".to_string()]
196 }
197 );
198
199 // User C accepts the call.
200 let call_c = incoming_call_c.next().await.unwrap().unwrap();
201 assert_eq!(call_c.calling_user.github_login, "user_a");
202 active_call_c
203 .update(cx_c, |call, cx| call.accept_incoming(cx))
204 .await
205 .unwrap();
206 assert!(incoming_call_c.next().await.unwrap().is_none());
207 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
208
209 deterministic.run_until_parked();
210 assert_eq!(
211 room_participants(&room_a, cx_a),
212 RoomParticipants {
213 remote: vec!["user_b".to_string(), "user_c".to_string()],
214 pending: Default::default()
215 }
216 );
217 assert_eq!(
218 room_participants(&room_b, cx_b),
219 RoomParticipants {
220 remote: vec!["user_a".to_string(), "user_c".to_string()],
221 pending: Default::default()
222 }
223 );
224 assert_eq!(
225 room_participants(&room_c, cx_c),
226 RoomParticipants {
227 remote: vec!["user_a".to_string(), "user_b".to_string()],
228 pending: Default::default()
229 }
230 );
231
232 // User A shares their screen
233 let display = MacOSDisplay::new();
234 let events_b = active_call_events(cx_b);
235 let events_c = active_call_events(cx_c);
236 active_call_a
237 .update(cx_a, |call, cx| {
238 call.room().unwrap().update(cx, |room, cx| {
239 room.set_display_sources(vec![display.clone()]);
240 room.share_screen(cx)
241 })
242 })
243 .await
244 .unwrap();
245
246 deterministic.run_until_parked();
247
248 // User B observes the remote screen sharing track.
249 assert_eq!(events_b.borrow().len(), 1);
250 let event_b = events_b.borrow().first().unwrap().clone();
251 if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
252 assert_eq!(participant_id, client_a.peer_id().unwrap());
253 room_b.read_with(cx_b, |room, _| {
254 assert_eq!(
255 room.remote_participants()[&client_a.user_id().unwrap()]
256 .tracks
257 .len(),
258 1
259 );
260 });
261 } else {
262 panic!("unexpected event")
263 }
264
265 // User C observes the remote screen sharing track.
266 assert_eq!(events_c.borrow().len(), 1);
267 let event_c = events_c.borrow().first().unwrap().clone();
268 if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
269 assert_eq!(participant_id, client_a.peer_id().unwrap());
270 room_c.read_with(cx_c, |room, _| {
271 assert_eq!(
272 room.remote_participants()[&client_a.user_id().unwrap()]
273 .tracks
274 .len(),
275 1
276 );
277 });
278 } else {
279 panic!("unexpected event")
280 }
281
282 // User A leaves the room.
283 active_call_a
284 .update(cx_a, |call, cx| {
285 let hang_up = call.hang_up(cx);
286 assert!(call.room().is_none());
287 hang_up
288 })
289 .await
290 .unwrap();
291 deterministic.run_until_parked();
292 assert_eq!(
293 room_participants(&room_a, cx_a),
294 RoomParticipants {
295 remote: Default::default(),
296 pending: Default::default()
297 }
298 );
299 assert_eq!(
300 room_participants(&room_b, cx_b),
301 RoomParticipants {
302 remote: vec!["user_c".to_string()],
303 pending: Default::default()
304 }
305 );
306 assert_eq!(
307 room_participants(&room_c, cx_c),
308 RoomParticipants {
309 remote: vec!["user_b".to_string()],
310 pending: Default::default()
311 }
312 );
313
314 // User B gets disconnected from the LiveKit server, which causes them
315 // to automatically leave the room. User C leaves the room as well because
316 // nobody else is in there.
317 server
318 .test_live_kit_server
319 .disconnect_client(client_b.user_id().unwrap().to_string())
320 .await;
321 deterministic.run_until_parked();
322 active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
323 active_call_c.read_with(cx_c, |call, _| assert!(call.room().is_none()));
324 assert_eq!(
325 room_participants(&room_a, cx_a),
326 RoomParticipants {
327 remote: Default::default(),
328 pending: Default::default()
329 }
330 );
331 assert_eq!(
332 room_participants(&room_b, cx_b),
333 RoomParticipants {
334 remote: Default::default(),
335 pending: Default::default()
336 }
337 );
338 assert_eq!(
339 room_participants(&room_c, cx_c),
340 RoomParticipants {
341 remote: Default::default(),
342 pending: Default::default()
343 }
344 );
345}
346
347#[gpui::test(iterations = 10)]
348async fn test_calling_multiple_users_simultaneously(
349 deterministic: Arc<Deterministic>,
350 cx_a: &mut TestAppContext,
351 cx_b: &mut TestAppContext,
352 cx_c: &mut TestAppContext,
353 cx_d: &mut TestAppContext,
354) {
355 deterministic.forbid_parking();
356 let mut server = TestServer::start(&deterministic).await;
357
358 let client_a = server.create_client(cx_a, "user_a").await;
359 let client_b = server.create_client(cx_b, "user_b").await;
360 let client_c = server.create_client(cx_c, "user_c").await;
361 let client_d = server.create_client(cx_d, "user_d").await;
362 server
363 .make_contacts(&mut [
364 (&client_a, cx_a),
365 (&client_b, cx_b),
366 (&client_c, cx_c),
367 (&client_d, cx_d),
368 ])
369 .await;
370
371 let active_call_a = cx_a.read(ActiveCall::global);
372 let active_call_b = cx_b.read(ActiveCall::global);
373 let active_call_c = cx_c.read(ActiveCall::global);
374 let active_call_d = cx_d.read(ActiveCall::global);
375
376 // Simultaneously call user B and user C from client A.
377 let b_invite = active_call_a.update(cx_a, |call, cx| {
378 call.invite(client_b.user_id().unwrap(), None, cx)
379 });
380 let c_invite = active_call_a.update(cx_a, |call, cx| {
381 call.invite(client_c.user_id().unwrap(), None, cx)
382 });
383 b_invite.await.unwrap();
384 c_invite.await.unwrap();
385
386 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
387 deterministic.run_until_parked();
388 assert_eq!(
389 room_participants(&room_a, cx_a),
390 RoomParticipants {
391 remote: Default::default(),
392 pending: vec!["user_b".to_string(), "user_c".to_string()]
393 }
394 );
395
396 // Call client D from client A.
397 active_call_a
398 .update(cx_a, |call, cx| {
399 call.invite(client_d.user_id().unwrap(), None, cx)
400 })
401 .await
402 .unwrap();
403 deterministic.run_until_parked();
404 assert_eq!(
405 room_participants(&room_a, cx_a),
406 RoomParticipants {
407 remote: Default::default(),
408 pending: vec![
409 "user_b".to_string(),
410 "user_c".to_string(),
411 "user_d".to_string()
412 ]
413 }
414 );
415
416 // Accept the call on all clients simultaneously.
417 let accept_b = active_call_b.update(cx_b, |call, cx| call.accept_incoming(cx));
418 let accept_c = active_call_c.update(cx_c, |call, cx| call.accept_incoming(cx));
419 let accept_d = active_call_d.update(cx_d, |call, cx| call.accept_incoming(cx));
420 accept_b.await.unwrap();
421 accept_c.await.unwrap();
422 accept_d.await.unwrap();
423
424 deterministic.run_until_parked();
425
426 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
427 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
428 let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
429 assert_eq!(
430 room_participants(&room_a, cx_a),
431 RoomParticipants {
432 remote: vec![
433 "user_b".to_string(),
434 "user_c".to_string(),
435 "user_d".to_string(),
436 ],
437 pending: Default::default()
438 }
439 );
440 assert_eq!(
441 room_participants(&room_b, cx_b),
442 RoomParticipants {
443 remote: vec![
444 "user_a".to_string(),
445 "user_c".to_string(),
446 "user_d".to_string(),
447 ],
448 pending: Default::default()
449 }
450 );
451 assert_eq!(
452 room_participants(&room_c, cx_c),
453 RoomParticipants {
454 remote: vec![
455 "user_a".to_string(),
456 "user_b".to_string(),
457 "user_d".to_string(),
458 ],
459 pending: Default::default()
460 }
461 );
462 assert_eq!(
463 room_participants(&room_d, cx_d),
464 RoomParticipants {
465 remote: vec![
466 "user_a".to_string(),
467 "user_b".to_string(),
468 "user_c".to_string(),
469 ],
470 pending: Default::default()
471 }
472 );
473}
474
475#[gpui::test(iterations = 10)]
476async fn test_room_uniqueness(
477 deterministic: Arc<Deterministic>,
478 cx_a: &mut TestAppContext,
479 cx_a2: &mut TestAppContext,
480 cx_b: &mut TestAppContext,
481 cx_b2: &mut TestAppContext,
482 cx_c: &mut TestAppContext,
483) {
484 deterministic.forbid_parking();
485 let mut server = TestServer::start(&deterministic).await;
486 let client_a = server.create_client(cx_a, "user_a").await;
487 let _client_a2 = server.create_client(cx_a2, "user_a").await;
488 let client_b = server.create_client(cx_b, "user_b").await;
489 let _client_b2 = server.create_client(cx_b2, "user_b").await;
490 let client_c = server.create_client(cx_c, "user_c").await;
491 server
492 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
493 .await;
494
495 let active_call_a = cx_a.read(ActiveCall::global);
496 let active_call_a2 = cx_a2.read(ActiveCall::global);
497 let active_call_b = cx_b.read(ActiveCall::global);
498 let active_call_b2 = cx_b2.read(ActiveCall::global);
499 let active_call_c = cx_c.read(ActiveCall::global);
500
501 // Call user B from client A.
502 active_call_a
503 .update(cx_a, |call, cx| {
504 call.invite(client_b.user_id().unwrap(), None, cx)
505 })
506 .await
507 .unwrap();
508
509 // Ensure a new room can't be created given user A just created one.
510 active_call_a2
511 .update(cx_a2, |call, cx| {
512 call.invite(client_c.user_id().unwrap(), None, cx)
513 })
514 .await
515 .unwrap_err();
516 active_call_a2.read_with(cx_a2, |call, _| assert!(call.room().is_none()));
517
518 // User B receives the call from user A.
519 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
520 let call_b1 = incoming_call_b.next().await.unwrap().unwrap();
521 assert_eq!(call_b1.calling_user.github_login, "user_a");
522
523 // Ensure calling users A and B from client C fails.
524 active_call_c
525 .update(cx_c, |call, cx| {
526 call.invite(client_a.user_id().unwrap(), None, cx)
527 })
528 .await
529 .unwrap_err();
530 active_call_c
531 .update(cx_c, |call, cx| {
532 call.invite(client_b.user_id().unwrap(), None, cx)
533 })
534 .await
535 .unwrap_err();
536
537 // Ensure User B can't create a room while they still have an incoming call.
538 active_call_b2
539 .update(cx_b2, |call, cx| {
540 call.invite(client_c.user_id().unwrap(), None, cx)
541 })
542 .await
543 .unwrap_err();
544 active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none()));
545
546 // User B joins the room and calling them after they've joined still fails.
547 active_call_b
548 .update(cx_b, |call, cx| call.accept_incoming(cx))
549 .await
550 .unwrap();
551 active_call_c
552 .update(cx_c, |call, cx| {
553 call.invite(client_b.user_id().unwrap(), None, cx)
554 })
555 .await
556 .unwrap_err();
557
558 // Ensure User B can't create a room while they belong to another room.
559 active_call_b2
560 .update(cx_b2, |call, cx| {
561 call.invite(client_c.user_id().unwrap(), None, cx)
562 })
563 .await
564 .unwrap_err();
565 active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none()));
566
567 // Client C can successfully call client B after client B leaves the room.
568 active_call_b
569 .update(cx_b, |call, cx| call.hang_up(cx))
570 .await
571 .unwrap();
572 deterministic.run_until_parked();
573 active_call_c
574 .update(cx_c, |call, cx| {
575 call.invite(client_b.user_id().unwrap(), None, cx)
576 })
577 .await
578 .unwrap();
579 deterministic.run_until_parked();
580 let call_b2 = incoming_call_b.next().await.unwrap().unwrap();
581 assert_eq!(call_b2.calling_user.github_login, "user_c");
582}
583
584#[gpui::test(iterations = 10)]
585async fn test_client_disconnecting_from_room(
586 deterministic: Arc<Deterministic>,
587 cx_a: &mut TestAppContext,
588 cx_b: &mut TestAppContext,
589) {
590 deterministic.forbid_parking();
591 let mut server = TestServer::start(&deterministic).await;
592 let client_a = server.create_client(cx_a, "user_a").await;
593 let client_b = server.create_client(cx_b, "user_b").await;
594 server
595 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
596 .await;
597
598 let active_call_a = cx_a.read(ActiveCall::global);
599 let active_call_b = cx_b.read(ActiveCall::global);
600
601 // Call user B from client A.
602 active_call_a
603 .update(cx_a, |call, cx| {
604 call.invite(client_b.user_id().unwrap(), None, cx)
605 })
606 .await
607 .unwrap();
608 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
609
610 // User B receives the call and joins the room.
611 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
612 incoming_call_b.next().await.unwrap().unwrap();
613 active_call_b
614 .update(cx_b, |call, cx| call.accept_incoming(cx))
615 .await
616 .unwrap();
617 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
618 deterministic.run_until_parked();
619 assert_eq!(
620 room_participants(&room_a, cx_a),
621 RoomParticipants {
622 remote: vec!["user_b".to_string()],
623 pending: Default::default()
624 }
625 );
626 assert_eq!(
627 room_participants(&room_b, cx_b),
628 RoomParticipants {
629 remote: vec!["user_a".to_string()],
630 pending: Default::default()
631 }
632 );
633
634 // User A automatically reconnects to the room upon disconnection.
635 server.disconnect_client(client_a.peer_id().unwrap());
636 deterministic.advance_clock(RECEIVE_TIMEOUT);
637 deterministic.run_until_parked();
638 assert_eq!(
639 room_participants(&room_a, cx_a),
640 RoomParticipants {
641 remote: vec!["user_b".to_string()],
642 pending: Default::default()
643 }
644 );
645 assert_eq!(
646 room_participants(&room_b, cx_b),
647 RoomParticipants {
648 remote: vec!["user_a".to_string()],
649 pending: Default::default()
650 }
651 );
652
653 // When user A disconnects, both client A and B clear their room on the active call.
654 server.forbid_connections();
655 server.disconnect_client(client_a.peer_id().unwrap());
656 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
657 active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
658 active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
659 assert_eq!(
660 room_participants(&room_a, cx_a),
661 RoomParticipants {
662 remote: Default::default(),
663 pending: Default::default()
664 }
665 );
666 assert_eq!(
667 room_participants(&room_b, cx_b),
668 RoomParticipants {
669 remote: Default::default(),
670 pending: Default::default()
671 }
672 );
673
674 // Allow user A to reconnect to the server.
675 server.allow_connections();
676 deterministic.advance_clock(RECEIVE_TIMEOUT);
677
678 // Call user B again from client A.
679 active_call_a
680 .update(cx_a, |call, cx| {
681 call.invite(client_b.user_id().unwrap(), None, cx)
682 })
683 .await
684 .unwrap();
685 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
686
687 // User B receives the call and joins the room.
688 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
689 incoming_call_b.next().await.unwrap().unwrap();
690 active_call_b
691 .update(cx_b, |call, cx| call.accept_incoming(cx))
692 .await
693 .unwrap();
694 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
695 deterministic.run_until_parked();
696 assert_eq!(
697 room_participants(&room_a, cx_a),
698 RoomParticipants {
699 remote: vec!["user_b".to_string()],
700 pending: Default::default()
701 }
702 );
703 assert_eq!(
704 room_participants(&room_b, cx_b),
705 RoomParticipants {
706 remote: vec!["user_a".to_string()],
707 pending: Default::default()
708 }
709 );
710
711 // User B gets disconnected from the LiveKit server, which causes it
712 // to automatically leave the room.
713 server
714 .test_live_kit_server
715 .disconnect_client(client_b.user_id().unwrap().to_string())
716 .await;
717 deterministic.run_until_parked();
718 active_call_a.update(cx_a, |call, _| assert!(call.room().is_none()));
719 active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
720 assert_eq!(
721 room_participants(&room_a, cx_a),
722 RoomParticipants {
723 remote: Default::default(),
724 pending: Default::default()
725 }
726 );
727 assert_eq!(
728 room_participants(&room_b, cx_b),
729 RoomParticipants {
730 remote: Default::default(),
731 pending: Default::default()
732 }
733 );
734}
735
736#[gpui::test(iterations = 10)]
737async fn test_server_restarts(
738 deterministic: Arc<Deterministic>,
739 cx_a: &mut TestAppContext,
740 cx_b: &mut TestAppContext,
741 cx_c: &mut TestAppContext,
742 cx_d: &mut TestAppContext,
743) {
744 deterministic.forbid_parking();
745 let mut server = TestServer::start(&deterministic).await;
746 let client_a = server.create_client(cx_a, "user_a").await;
747 client_a
748 .fs
749 .insert_tree("/a", json!({ "a.txt": "a-contents" }))
750 .await;
751
752 // Invite client B to collaborate on a project
753 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
754
755 let client_b = server.create_client(cx_b, "user_b").await;
756 let client_c = server.create_client(cx_c, "user_c").await;
757 let client_d = server.create_client(cx_d, "user_d").await;
758 server
759 .make_contacts(&mut [
760 (&client_a, cx_a),
761 (&client_b, cx_b),
762 (&client_c, cx_c),
763 (&client_d, cx_d),
764 ])
765 .await;
766
767 let active_call_a = cx_a.read(ActiveCall::global);
768 let active_call_b = cx_b.read(ActiveCall::global);
769 let active_call_c = cx_c.read(ActiveCall::global);
770 let active_call_d = cx_d.read(ActiveCall::global);
771
772 // User A calls users B, C, and D.
773 active_call_a
774 .update(cx_a, |call, cx| {
775 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
776 })
777 .await
778 .unwrap();
779 active_call_a
780 .update(cx_a, |call, cx| {
781 call.invite(client_c.user_id().unwrap(), Some(project_a.clone()), cx)
782 })
783 .await
784 .unwrap();
785 active_call_a
786 .update(cx_a, |call, cx| {
787 call.invite(client_d.user_id().unwrap(), Some(project_a.clone()), cx)
788 })
789 .await
790 .unwrap();
791 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
792
793 // User B receives the call and joins the room.
794 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
795 assert!(incoming_call_b.next().await.unwrap().is_some());
796 active_call_b
797 .update(cx_b, |call, cx| call.accept_incoming(cx))
798 .await
799 .unwrap();
800 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
801
802 // User C receives the call and joins the room.
803 let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
804 assert!(incoming_call_c.next().await.unwrap().is_some());
805 active_call_c
806 .update(cx_c, |call, cx| call.accept_incoming(cx))
807 .await
808 .unwrap();
809 let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
810
811 // User D receives the call but doesn't join the room yet.
812 let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming());
813 assert!(incoming_call_d.next().await.unwrap().is_some());
814
815 deterministic.run_until_parked();
816 assert_eq!(
817 room_participants(&room_a, cx_a),
818 RoomParticipants {
819 remote: vec!["user_b".to_string(), "user_c".to_string()],
820 pending: vec!["user_d".to_string()]
821 }
822 );
823 assert_eq!(
824 room_participants(&room_b, cx_b),
825 RoomParticipants {
826 remote: vec!["user_a".to_string(), "user_c".to_string()],
827 pending: vec!["user_d".to_string()]
828 }
829 );
830 assert_eq!(
831 room_participants(&room_c, cx_c),
832 RoomParticipants {
833 remote: vec!["user_a".to_string(), "user_b".to_string()],
834 pending: vec!["user_d".to_string()]
835 }
836 );
837
838 // The server is torn down.
839 server.reset().await;
840
841 // Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
842 client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
843 deterministic.advance_clock(RECONNECT_TIMEOUT);
844 assert_eq!(
845 room_participants(&room_a, cx_a),
846 RoomParticipants {
847 remote: vec!["user_b".to_string(), "user_c".to_string()],
848 pending: vec!["user_d".to_string()]
849 }
850 );
851 assert_eq!(
852 room_participants(&room_b, cx_b),
853 RoomParticipants {
854 remote: vec!["user_a".to_string(), "user_c".to_string()],
855 pending: vec!["user_d".to_string()]
856 }
857 );
858 assert_eq!(
859 room_participants(&room_c, cx_c),
860 RoomParticipants {
861 remote: vec![],
862 pending: vec![]
863 }
864 );
865
866 // User D is notified again of the incoming call and accepts it.
867 assert!(incoming_call_d.next().await.unwrap().is_some());
868 active_call_d
869 .update(cx_d, |call, cx| call.accept_incoming(cx))
870 .await
871 .unwrap();
872 deterministic.run_until_parked();
873 let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
874 assert_eq!(
875 room_participants(&room_a, cx_a),
876 RoomParticipants {
877 remote: vec![
878 "user_b".to_string(),
879 "user_c".to_string(),
880 "user_d".to_string(),
881 ],
882 pending: vec![]
883 }
884 );
885 assert_eq!(
886 room_participants(&room_b, cx_b),
887 RoomParticipants {
888 remote: vec![
889 "user_a".to_string(),
890 "user_c".to_string(),
891 "user_d".to_string(),
892 ],
893 pending: vec![]
894 }
895 );
896 assert_eq!(
897 room_participants(&room_c, cx_c),
898 RoomParticipants {
899 remote: vec![],
900 pending: vec![]
901 }
902 );
903 assert_eq!(
904 room_participants(&room_d, cx_d),
905 RoomParticipants {
906 remote: vec![
907 "user_a".to_string(),
908 "user_b".to_string(),
909 "user_c".to_string(),
910 ],
911 pending: vec![]
912 }
913 );
914
915 // The server finishes restarting, cleaning up stale connections.
916 server.start().await.unwrap();
917 deterministic.advance_clock(CLEANUP_TIMEOUT);
918 assert_eq!(
919 room_participants(&room_a, cx_a),
920 RoomParticipants {
921 remote: vec!["user_b".to_string(), "user_d".to_string()],
922 pending: vec![]
923 }
924 );
925 assert_eq!(
926 room_participants(&room_b, cx_b),
927 RoomParticipants {
928 remote: vec!["user_a".to_string(), "user_d".to_string()],
929 pending: vec![]
930 }
931 );
932 assert_eq!(
933 room_participants(&room_c, cx_c),
934 RoomParticipants {
935 remote: vec![],
936 pending: vec![]
937 }
938 );
939 assert_eq!(
940 room_participants(&room_d, cx_d),
941 RoomParticipants {
942 remote: vec!["user_a".to_string(), "user_b".to_string()],
943 pending: vec![]
944 }
945 );
946
947 // User D hangs up.
948 active_call_d
949 .update(cx_d, |call, cx| call.hang_up(cx))
950 .await
951 .unwrap();
952 deterministic.run_until_parked();
953 assert_eq!(
954 room_participants(&room_a, cx_a),
955 RoomParticipants {
956 remote: vec!["user_b".to_string()],
957 pending: vec![]
958 }
959 );
960 assert_eq!(
961 room_participants(&room_b, cx_b),
962 RoomParticipants {
963 remote: vec!["user_a".to_string()],
964 pending: vec![]
965 }
966 );
967 assert_eq!(
968 room_participants(&room_c, cx_c),
969 RoomParticipants {
970 remote: vec![],
971 pending: vec![]
972 }
973 );
974 assert_eq!(
975 room_participants(&room_d, cx_d),
976 RoomParticipants {
977 remote: vec![],
978 pending: vec![]
979 }
980 );
981
982 // User B calls user D again.
983 active_call_b
984 .update(cx_b, |call, cx| {
985 call.invite(client_d.user_id().unwrap(), None, cx)
986 })
987 .await
988 .unwrap();
989
990 // User D receives the call but doesn't join the room yet.
991 let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming());
992 assert!(incoming_call_d.next().await.unwrap().is_some());
993 deterministic.run_until_parked();
994 assert_eq!(
995 room_participants(&room_a, cx_a),
996 RoomParticipants {
997 remote: vec!["user_b".to_string()],
998 pending: vec!["user_d".to_string()]
999 }
1000 );
1001 assert_eq!(
1002 room_participants(&room_b, cx_b),
1003 RoomParticipants {
1004 remote: vec!["user_a".to_string()],
1005 pending: vec!["user_d".to_string()]
1006 }
1007 );
1008
1009 // The server is torn down.
1010 server.reset().await;
1011
1012 // Users A and B have troubles reconnecting, so they leave the room.
1013 client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
1014 client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
1015 client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
1016 deterministic.advance_clock(RECONNECT_TIMEOUT);
1017 assert_eq!(
1018 room_participants(&room_a, cx_a),
1019 RoomParticipants {
1020 remote: vec![],
1021 pending: vec![]
1022 }
1023 );
1024 assert_eq!(
1025 room_participants(&room_b, cx_b),
1026 RoomParticipants {
1027 remote: vec![],
1028 pending: vec![]
1029 }
1030 );
1031
1032 // User D is notified again of the incoming call but doesn't accept it.
1033 assert!(incoming_call_d.next().await.unwrap().is_some());
1034
1035 // The server finishes restarting, cleaning up stale connections and canceling the
1036 // call to user D because the room has become empty.
1037 server.start().await.unwrap();
1038 deterministic.advance_clock(CLEANUP_TIMEOUT);
1039 assert!(incoming_call_d.next().await.unwrap().is_none());
1040}
1041
1042#[gpui::test(iterations = 10)]
1043async fn test_calls_on_multiple_connections(
1044 deterministic: Arc<Deterministic>,
1045 cx_a: &mut TestAppContext,
1046 cx_b1: &mut TestAppContext,
1047 cx_b2: &mut TestAppContext,
1048) {
1049 deterministic.forbid_parking();
1050 let mut server = TestServer::start(&deterministic).await;
1051 let client_a = server.create_client(cx_a, "user_a").await;
1052 let client_b1 = server.create_client(cx_b1, "user_b").await;
1053 let client_b2 = server.create_client(cx_b2, "user_b").await;
1054 server
1055 .make_contacts(&mut [(&client_a, cx_a), (&client_b1, cx_b1)])
1056 .await;
1057
1058 let active_call_a = cx_a.read(ActiveCall::global);
1059 let active_call_b1 = cx_b1.read(ActiveCall::global);
1060 let active_call_b2 = cx_b2.read(ActiveCall::global);
1061 let mut incoming_call_b1 = active_call_b1.read_with(cx_b1, |call, _| call.incoming());
1062 let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
1063 assert!(incoming_call_b1.next().await.unwrap().is_none());
1064 assert!(incoming_call_b2.next().await.unwrap().is_none());
1065
1066 // Call user B from client A, ensuring both clients for user B ring.
1067 active_call_a
1068 .update(cx_a, |call, cx| {
1069 call.invite(client_b1.user_id().unwrap(), None, cx)
1070 })
1071 .await
1072 .unwrap();
1073 deterministic.run_until_parked();
1074 assert!(incoming_call_b1.next().await.unwrap().is_some());
1075 assert!(incoming_call_b2.next().await.unwrap().is_some());
1076
1077 // User B declines the call on one of the two connections, causing both connections
1078 // to stop ringing.
1079 active_call_b2.update(cx_b2, |call, _| call.decline_incoming().unwrap());
1080 deterministic.run_until_parked();
1081 assert!(incoming_call_b1.next().await.unwrap().is_none());
1082 assert!(incoming_call_b2.next().await.unwrap().is_none());
1083
1084 // Call user B again from client A.
1085 active_call_a
1086 .update(cx_a, |call, cx| {
1087 call.invite(client_b1.user_id().unwrap(), None, cx)
1088 })
1089 .await
1090 .unwrap();
1091 deterministic.run_until_parked();
1092 assert!(incoming_call_b1.next().await.unwrap().is_some());
1093 assert!(incoming_call_b2.next().await.unwrap().is_some());
1094
1095 // User B accepts the call on one of the two connections, causing both connections
1096 // to stop ringing.
1097 active_call_b2
1098 .update(cx_b2, |call, cx| call.accept_incoming(cx))
1099 .await
1100 .unwrap();
1101 deterministic.run_until_parked();
1102 assert!(incoming_call_b1.next().await.unwrap().is_none());
1103 assert!(incoming_call_b2.next().await.unwrap().is_none());
1104
1105 // User B disconnects the client that is not on the call. Everything should be fine.
1106 client_b1.disconnect(&cx_b1.to_async());
1107 deterministic.advance_clock(RECEIVE_TIMEOUT);
1108 client_b1
1109 .authenticate_and_connect(false, &cx_b1.to_async())
1110 .await
1111 .unwrap();
1112
1113 // User B hangs up, and user A calls them again.
1114 active_call_b2
1115 .update(cx_b2, |call, cx| call.hang_up(cx))
1116 .await
1117 .unwrap();
1118 deterministic.run_until_parked();
1119 active_call_a
1120 .update(cx_a, |call, cx| {
1121 call.invite(client_b1.user_id().unwrap(), None, cx)
1122 })
1123 .await
1124 .unwrap();
1125 deterministic.run_until_parked();
1126 assert!(incoming_call_b1.next().await.unwrap().is_some());
1127 assert!(incoming_call_b2.next().await.unwrap().is_some());
1128
1129 // User A cancels the call, causing both connections to stop ringing.
1130 active_call_a
1131 .update(cx_a, |call, cx| {
1132 call.cancel_invite(client_b1.user_id().unwrap(), cx)
1133 })
1134 .await
1135 .unwrap();
1136 deterministic.run_until_parked();
1137 assert!(incoming_call_b1.next().await.unwrap().is_none());
1138 assert!(incoming_call_b2.next().await.unwrap().is_none());
1139
1140 // User A calls user B again.
1141 active_call_a
1142 .update(cx_a, |call, cx| {
1143 call.invite(client_b1.user_id().unwrap(), None, cx)
1144 })
1145 .await
1146 .unwrap();
1147 deterministic.run_until_parked();
1148 assert!(incoming_call_b1.next().await.unwrap().is_some());
1149 assert!(incoming_call_b2.next().await.unwrap().is_some());
1150
1151 // User A hangs up, causing both connections to stop ringing.
1152 active_call_a
1153 .update(cx_a, |call, cx| call.hang_up(cx))
1154 .await
1155 .unwrap();
1156 deterministic.run_until_parked();
1157 assert!(incoming_call_b1.next().await.unwrap().is_none());
1158 assert!(incoming_call_b2.next().await.unwrap().is_none());
1159
1160 // User A calls user B again.
1161 active_call_a
1162 .update(cx_a, |call, cx| {
1163 call.invite(client_b1.user_id().unwrap(), None, cx)
1164 })
1165 .await
1166 .unwrap();
1167 deterministic.run_until_parked();
1168 assert!(incoming_call_b1.next().await.unwrap().is_some());
1169 assert!(incoming_call_b2.next().await.unwrap().is_some());
1170
1171 // User A disconnects, causing both connections to stop ringing.
1172 server.forbid_connections();
1173 server.disconnect_client(client_a.peer_id().unwrap());
1174 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1175 assert!(incoming_call_b1.next().await.unwrap().is_none());
1176 assert!(incoming_call_b2.next().await.unwrap().is_none());
1177
1178 // User A reconnects automatically, then calls user B again.
1179 server.allow_connections();
1180 deterministic.advance_clock(RECEIVE_TIMEOUT);
1181 active_call_a
1182 .update(cx_a, |call, cx| {
1183 call.invite(client_b1.user_id().unwrap(), None, cx)
1184 })
1185 .await
1186 .unwrap();
1187 deterministic.run_until_parked();
1188 assert!(incoming_call_b1.next().await.unwrap().is_some());
1189 assert!(incoming_call_b2.next().await.unwrap().is_some());
1190
1191 // User B disconnects all clients, causing user A to no longer see a pending call for them.
1192 server.forbid_connections();
1193 server.disconnect_client(client_b1.peer_id().unwrap());
1194 server.disconnect_client(client_b2.peer_id().unwrap());
1195 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1196 active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
1197}
1198
1199#[gpui::test(iterations = 10)]
1200async fn test_share_project(
1201 deterministic: Arc<Deterministic>,
1202 cx_a: &mut TestAppContext,
1203 cx_b: &mut TestAppContext,
1204 cx_c: &mut TestAppContext,
1205) {
1206 deterministic.forbid_parking();
1207 let (_, window_b) = cx_b.add_window(|_| EmptyView);
1208 let mut server = TestServer::start(&deterministic).await;
1209 let client_a = server.create_client(cx_a, "user_a").await;
1210 let client_b = server.create_client(cx_b, "user_b").await;
1211 let client_c = server.create_client(cx_c, "user_c").await;
1212 server
1213 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1214 .await;
1215 let active_call_a = cx_a.read(ActiveCall::global);
1216 let active_call_b = cx_b.read(ActiveCall::global);
1217 let active_call_c = cx_c.read(ActiveCall::global);
1218
1219 client_a
1220 .fs
1221 .insert_tree(
1222 "/a",
1223 json!({
1224 ".gitignore": "ignored-dir",
1225 "a.txt": "a-contents",
1226 "b.txt": "b-contents",
1227 "ignored-dir": {
1228 "c.txt": "",
1229 "d.txt": "",
1230 }
1231 }),
1232 )
1233 .await;
1234
1235 // Invite client B to collaborate on a project
1236 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1237 active_call_a
1238 .update(cx_a, |call, cx| {
1239 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1240 })
1241 .await
1242 .unwrap();
1243
1244 // Join that project as client B
1245 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1246 deterministic.run_until_parked();
1247 let call = incoming_call_b.borrow().clone().unwrap();
1248 assert_eq!(call.calling_user.github_login, "user_a");
1249 let initial_project = call.initial_project.unwrap();
1250 active_call_b
1251 .update(cx_b, |call, cx| call.accept_incoming(cx))
1252 .await
1253 .unwrap();
1254 let client_b_peer_id = client_b.peer_id().unwrap();
1255 let project_b = client_b
1256 .build_remote_project(initial_project.id, cx_b)
1257 .await;
1258 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1259
1260 deterministic.run_until_parked();
1261 project_a.read_with(cx_a, |project, _| {
1262 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1263 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1264 });
1265 project_b.read_with(cx_b, |project, cx| {
1266 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1267 assert_eq!(
1268 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1269 [
1270 Path::new(".gitignore"),
1271 Path::new("a.txt"),
1272 Path::new("b.txt"),
1273 Path::new("ignored-dir"),
1274 Path::new("ignored-dir/c.txt"),
1275 Path::new("ignored-dir/d.txt"),
1276 ]
1277 );
1278 });
1279
1280 // Open the same file as client B and client A.
1281 let buffer_b = project_b
1282 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1283 .await
1284 .unwrap();
1285 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1286 project_a.read_with(cx_a, |project, cx| {
1287 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1288 });
1289 let buffer_a = project_a
1290 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1291 .await
1292 .unwrap();
1293
1294 let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
1295
1296 // Client A sees client B's selection
1297 deterministic.run_until_parked();
1298 buffer_a.read_with(cx_a, |buffer, _| {
1299 buffer
1300 .snapshot()
1301 .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1302 .count()
1303 == 1
1304 });
1305
1306 // Edit the buffer as client B and see that edit as client A.
1307 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1308 deterministic.run_until_parked();
1309 buffer_a.read_with(cx_a, |buffer, _| {
1310 assert_eq!(buffer.text(), "ok, b-contents")
1311 });
1312
1313 // Client B can invite client C on a project shared by client A.
1314 active_call_b
1315 .update(cx_b, |call, cx| {
1316 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1317 })
1318 .await
1319 .unwrap();
1320
1321 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1322 deterministic.run_until_parked();
1323 let call = incoming_call_c.borrow().clone().unwrap();
1324 assert_eq!(call.calling_user.github_login, "user_b");
1325 let initial_project = call.initial_project.unwrap();
1326 active_call_c
1327 .update(cx_c, |call, cx| call.accept_incoming(cx))
1328 .await
1329 .unwrap();
1330 let _project_c = client_c
1331 .build_remote_project(initial_project.id, cx_c)
1332 .await;
1333
1334 // Client B closes the editor, and client A sees client B's selections removed.
1335 cx_b.update(move |_| drop(editor_b));
1336 deterministic.run_until_parked();
1337 buffer_a.read_with(cx_a, |buffer, _| {
1338 buffer
1339 .snapshot()
1340 .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1341 .count()
1342 == 0
1343 });
1344}
1345
1346#[gpui::test(iterations = 10)]
1347async fn test_unshare_project(
1348 deterministic: Arc<Deterministic>,
1349 cx_a: &mut TestAppContext,
1350 cx_b: &mut TestAppContext,
1351 cx_c: &mut TestAppContext,
1352) {
1353 deterministic.forbid_parking();
1354 let mut server = TestServer::start(&deterministic).await;
1355 let client_a = server.create_client(cx_a, "user_a").await;
1356 let client_b = server.create_client(cx_b, "user_b").await;
1357 let client_c = server.create_client(cx_c, "user_c").await;
1358 server
1359 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1360 .await;
1361
1362 let active_call_a = cx_a.read(ActiveCall::global);
1363 let active_call_b = cx_b.read(ActiveCall::global);
1364
1365 client_a
1366 .fs
1367 .insert_tree(
1368 "/a",
1369 json!({
1370 "a.txt": "a-contents",
1371 "b.txt": "b-contents",
1372 }),
1373 )
1374 .await;
1375
1376 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1377 let project_id = active_call_a
1378 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1379 .await
1380 .unwrap();
1381 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
1382 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1383 deterministic.run_until_parked();
1384 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
1385
1386 project_b
1387 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1388 .await
1389 .unwrap();
1390
1391 // When client B leaves the room, the project becomes read-only.
1392 active_call_b
1393 .update(cx_b, |call, cx| call.hang_up(cx))
1394 .await
1395 .unwrap();
1396 deterministic.run_until_parked();
1397 assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
1398
1399 // Client C opens the project.
1400 let project_c = client_c.build_remote_project(project_id, cx_c).await;
1401
1402 // When client A unshares the project, client C's project becomes read-only.
1403 project_a
1404 .update(cx_a, |project, cx| project.unshare(cx))
1405 .unwrap();
1406 deterministic.run_until_parked();
1407 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
1408 assert!(project_c.read_with(cx_c, |project, _| project.is_read_only()));
1409
1410 // Client C can open the project again after client A re-shares.
1411 let project_id = active_call_a
1412 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1413 .await
1414 .unwrap();
1415 let project_c2 = client_c.build_remote_project(project_id, cx_c).await;
1416 deterministic.run_until_parked();
1417 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
1418 project_c2
1419 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1420 .await
1421 .unwrap();
1422
1423 // When client A (the host) leaves the room, the project gets unshared and guests are notified.
1424 active_call_a
1425 .update(cx_a, |call, cx| call.hang_up(cx))
1426 .await
1427 .unwrap();
1428 deterministic.run_until_parked();
1429 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
1430 project_c2.read_with(cx_c, |project, _| {
1431 assert!(project.is_read_only());
1432 assert!(project.collaborators().is_empty());
1433 });
1434}
1435
1436#[gpui::test(iterations = 10)]
1437async fn test_host_disconnect(
1438 deterministic: Arc<Deterministic>,
1439 cx_a: &mut TestAppContext,
1440 cx_b: &mut TestAppContext,
1441 cx_c: &mut TestAppContext,
1442) {
1443 cx_b.update(editor::init);
1444 deterministic.forbid_parking();
1445 let mut server = TestServer::start(&deterministic).await;
1446 let client_a = server.create_client(cx_a, "user_a").await;
1447 let client_b = server.create_client(cx_b, "user_b").await;
1448 let client_c = server.create_client(cx_c, "user_c").await;
1449 server
1450 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1451 .await;
1452
1453 client_a
1454 .fs
1455 .insert_tree(
1456 "/a",
1457 json!({
1458 "a.txt": "a-contents",
1459 "b.txt": "b-contents",
1460 }),
1461 )
1462 .await;
1463
1464 let active_call_a = cx_a.read(ActiveCall::global);
1465 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1466 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
1467 let project_id = active_call_a
1468 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1469 .await
1470 .unwrap();
1471
1472 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1473 deterministic.run_until_parked();
1474 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
1475
1476 let (window_id_b, workspace_b) =
1477 cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
1478 let editor_b = workspace_b
1479 .update(cx_b, |workspace, cx| {
1480 workspace.open_path((worktree_id, "b.txt"), None, true, cx)
1481 })
1482 .await
1483 .unwrap()
1484 .downcast::<Editor>()
1485 .unwrap();
1486 assert!(cx_b
1487 .read_window(window_id_b, |cx| editor_b.is_focused(cx))
1488 .unwrap());
1489 editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
1490 assert!(cx_b.is_window_edited(workspace_b.window_id()));
1491
1492 // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
1493 server.forbid_connections();
1494 server.disconnect_client(client_a.peer_id().unwrap());
1495 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1496 project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
1497 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
1498 project_b.read_with(cx_b, |project, _| project.is_read_only());
1499 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
1500
1501 // Ensure client B's edited state is reset and that the whole window is blurred.
1502 cx_b.read_window(window_id_b, |cx| {
1503 assert_eq!(cx.focused_view_id(), None);
1504 });
1505 assert!(!cx_b.is_window_edited(workspace_b.window_id()));
1506
1507 // Ensure client B is not prompted to save edits when closing window after disconnecting.
1508 let can_close = workspace_b
1509 .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx))
1510 .await
1511 .unwrap();
1512 assert!(can_close);
1513
1514 // Allow client A to reconnect to the server.
1515 server.allow_connections();
1516 deterministic.advance_clock(RECEIVE_TIMEOUT);
1517
1518 // Client B calls client A again after they reconnected.
1519 let active_call_b = cx_b.read(ActiveCall::global);
1520 active_call_b
1521 .update(cx_b, |call, cx| {
1522 call.invite(client_a.user_id().unwrap(), None, cx)
1523 })
1524 .await
1525 .unwrap();
1526 deterministic.run_until_parked();
1527 active_call_a
1528 .update(cx_a, |call, cx| call.accept_incoming(cx))
1529 .await
1530 .unwrap();
1531
1532 active_call_a
1533 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1534 .await
1535 .unwrap();
1536
1537 // Drop client A's connection again. We should still unshare it successfully.
1538 server.forbid_connections();
1539 server.disconnect_client(client_a.peer_id().unwrap());
1540 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
1541 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
1542}
1543
1544#[gpui::test(iterations = 10)]
1545async fn test_project_reconnect(
1546 deterministic: Arc<Deterministic>,
1547 cx_a: &mut TestAppContext,
1548 cx_b: &mut TestAppContext,
1549) {
1550 cx_b.update(editor::init);
1551 deterministic.forbid_parking();
1552 let mut server = TestServer::start(&deterministic).await;
1553 let client_a = server.create_client(cx_a, "user_a").await;
1554 let client_b = server.create_client(cx_b, "user_b").await;
1555 server
1556 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1557 .await;
1558
1559 client_a
1560 .fs
1561 .insert_tree(
1562 "/root-1",
1563 json!({
1564 "dir1": {
1565 "a.txt": "a",
1566 "b.txt": "b",
1567 "subdir1": {
1568 "c.txt": "c",
1569 "d.txt": "d",
1570 "e.txt": "e",
1571 }
1572 },
1573 "dir2": {
1574 "v.txt": "v",
1575 },
1576 "dir3": {
1577 "w.txt": "w",
1578 "x.txt": "x",
1579 "y.txt": "y",
1580 },
1581 "dir4": {
1582 "z.txt": "z",
1583 },
1584 }),
1585 )
1586 .await;
1587 client_a
1588 .fs
1589 .insert_tree(
1590 "/root-2",
1591 json!({
1592 "2.txt": "2",
1593 }),
1594 )
1595 .await;
1596 client_a
1597 .fs
1598 .insert_tree(
1599 "/root-3",
1600 json!({
1601 "3.txt": "3",
1602 }),
1603 )
1604 .await;
1605
1606 let active_call_a = cx_a.read(ActiveCall::global);
1607 let (project_a1, _) = client_a.build_local_project("/root-1/dir1", cx_a).await;
1608 let (project_a2, _) = client_a.build_local_project("/root-2", cx_a).await;
1609 let (project_a3, _) = client_a.build_local_project("/root-3", cx_a).await;
1610 let worktree_a1 =
1611 project_a1.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
1612 let project1_id = active_call_a
1613 .update(cx_a, |call, cx| call.share_project(project_a1.clone(), cx))
1614 .await
1615 .unwrap();
1616 let project2_id = active_call_a
1617 .update(cx_a, |call, cx| call.share_project(project_a2.clone(), cx))
1618 .await
1619 .unwrap();
1620 let project3_id = active_call_a
1621 .update(cx_a, |call, cx| call.share_project(project_a3.clone(), cx))
1622 .await
1623 .unwrap();
1624
1625 let project_b1 = client_b.build_remote_project(project1_id, cx_b).await;
1626 let project_b2 = client_b.build_remote_project(project2_id, cx_b).await;
1627 let project_b3 = client_b.build_remote_project(project3_id, cx_b).await;
1628 deterministic.run_until_parked();
1629
1630 let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| {
1631 assert!(worktree.as_local().unwrap().is_shared());
1632 worktree.id()
1633 });
1634 let (worktree_a2, _) = project_a1
1635 .update(cx_a, |p, cx| {
1636 p.find_or_create_local_worktree("/root-1/dir2", true, cx)
1637 })
1638 .await
1639 .unwrap();
1640 deterministic.run_until_parked();
1641 let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| {
1642 assert!(tree.as_local().unwrap().is_shared());
1643 tree.id()
1644 });
1645 deterministic.run_until_parked();
1646 project_b1.read_with(cx_b, |project, cx| {
1647 assert!(project.worktree_for_id(worktree2_id, cx).is_some())
1648 });
1649
1650 let buffer_a1 = project_a1
1651 .update(cx_a, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
1652 .await
1653 .unwrap();
1654 let buffer_b1 = project_b1
1655 .update(cx_b, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
1656 .await
1657 .unwrap();
1658
1659 // Drop client A's connection.
1660 server.forbid_connections();
1661 server.disconnect_client(client_a.peer_id().unwrap());
1662 deterministic.advance_clock(RECEIVE_TIMEOUT);
1663 project_a1.read_with(cx_a, |project, _| {
1664 assert!(project.is_shared());
1665 assert_eq!(project.collaborators().len(), 1);
1666 });
1667 project_b1.read_with(cx_b, |project, _| {
1668 assert!(!project.is_read_only());
1669 assert_eq!(project.collaborators().len(), 1);
1670 });
1671 worktree_a1.read_with(cx_a, |tree, _| {
1672 assert!(tree.as_local().unwrap().is_shared())
1673 });
1674
1675 // While client A is disconnected, add and remove files from client A's project.
1676 client_a
1677 .fs
1678 .insert_tree(
1679 "/root-1/dir1/subdir2",
1680 json!({
1681 "f.txt": "f-contents",
1682 "g.txt": "g-contents",
1683 "h.txt": "h-contents",
1684 "i.txt": "i-contents",
1685 }),
1686 )
1687 .await;
1688 client_a
1689 .fs
1690 .remove_dir(
1691 "/root-1/dir1/subdir1".as_ref(),
1692 RemoveOptions {
1693 recursive: true,
1694 ..Default::default()
1695 },
1696 )
1697 .await
1698 .unwrap();
1699
1700 // While client A is disconnected, add and remove worktrees from client A's project.
1701 project_a1.update(cx_a, |project, cx| {
1702 project.remove_worktree(worktree2_id, cx)
1703 });
1704 let (worktree_a3, _) = project_a1
1705 .update(cx_a, |p, cx| {
1706 p.find_or_create_local_worktree("/root-1/dir3", true, cx)
1707 })
1708 .await
1709 .unwrap();
1710 worktree_a3
1711 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
1712 .await;
1713 let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
1714 assert!(!tree.as_local().unwrap().is_shared());
1715 tree.id()
1716 });
1717 deterministic.run_until_parked();
1718
1719 // While client A is disconnected, close project 2
1720 cx_a.update(|_| drop(project_a2));
1721
1722 // While client A is disconnected, mutate a buffer on both the host and the guest.
1723 buffer_a1.update(cx_a, |buf, cx| buf.edit([(0..0, "W")], None, cx));
1724 buffer_b1.update(cx_b, |buf, cx| buf.edit([(1..1, "Z")], None, cx));
1725 deterministic.run_until_parked();
1726
1727 // Client A reconnects. Their project is re-shared, and client B re-joins it.
1728 server.allow_connections();
1729 client_a
1730 .authenticate_and_connect(false, &cx_a.to_async())
1731 .await
1732 .unwrap();
1733 deterministic.run_until_parked();
1734 project_a1.read_with(cx_a, |project, cx| {
1735 assert!(project.is_shared());
1736 assert!(worktree_a1.read(cx).as_local().unwrap().is_shared());
1737 assert_eq!(
1738 worktree_a1
1739 .read(cx)
1740 .snapshot()
1741 .paths()
1742 .map(|p| p.to_str().unwrap())
1743 .collect::<Vec<_>>(),
1744 vec![
1745 "a.txt",
1746 "b.txt",
1747 "subdir2",
1748 "subdir2/f.txt",
1749 "subdir2/g.txt",
1750 "subdir2/h.txt",
1751 "subdir2/i.txt"
1752 ]
1753 );
1754 assert!(worktree_a3.read(cx).as_local().unwrap().is_shared());
1755 assert_eq!(
1756 worktree_a3
1757 .read(cx)
1758 .snapshot()
1759 .paths()
1760 .map(|p| p.to_str().unwrap())
1761 .collect::<Vec<_>>(),
1762 vec!["w.txt", "x.txt", "y.txt"]
1763 );
1764 });
1765 project_b1.read_with(cx_b, |project, cx| {
1766 assert!(!project.is_read_only());
1767 assert_eq!(
1768 project
1769 .worktree_for_id(worktree1_id, cx)
1770 .unwrap()
1771 .read(cx)
1772 .snapshot()
1773 .paths()
1774 .map(|p| p.to_str().unwrap())
1775 .collect::<Vec<_>>(),
1776 vec![
1777 "a.txt",
1778 "b.txt",
1779 "subdir2",
1780 "subdir2/f.txt",
1781 "subdir2/g.txt",
1782 "subdir2/h.txt",
1783 "subdir2/i.txt"
1784 ]
1785 );
1786 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
1787 assert_eq!(
1788 project
1789 .worktree_for_id(worktree3_id, cx)
1790 .unwrap()
1791 .read(cx)
1792 .snapshot()
1793 .paths()
1794 .map(|p| p.to_str().unwrap())
1795 .collect::<Vec<_>>(),
1796 vec!["w.txt", "x.txt", "y.txt"]
1797 );
1798 });
1799 project_b2.read_with(cx_b, |project, _| assert!(project.is_read_only()));
1800 project_b3.read_with(cx_b, |project, _| assert!(!project.is_read_only()));
1801 buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
1802 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
1803
1804 // Drop client B's connection.
1805 server.forbid_connections();
1806 server.disconnect_client(client_b.peer_id().unwrap());
1807 deterministic.advance_clock(RECEIVE_TIMEOUT);
1808
1809 // While client B is disconnected, add and remove files from client A's project
1810 client_a
1811 .fs
1812 .insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into())
1813 .await;
1814 client_a
1815 .fs
1816 .remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default())
1817 .await
1818 .unwrap();
1819
1820 // While client B is disconnected, add and remove worktrees from client A's project.
1821 let (worktree_a4, _) = project_a1
1822 .update(cx_a, |p, cx| {
1823 p.find_or_create_local_worktree("/root-1/dir4", true, cx)
1824 })
1825 .await
1826 .unwrap();
1827 deterministic.run_until_parked();
1828 let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| {
1829 assert!(tree.as_local().unwrap().is_shared());
1830 tree.id()
1831 });
1832 project_a1.update(cx_a, |project, cx| {
1833 project.remove_worktree(worktree3_id, cx)
1834 });
1835 deterministic.run_until_parked();
1836
1837 // While client B is disconnected, mutate a buffer on both the host and the guest.
1838 buffer_a1.update(cx_a, |buf, cx| buf.edit([(1..1, "X")], None, cx));
1839 buffer_b1.update(cx_b, |buf, cx| buf.edit([(2..2, "Y")], None, cx));
1840 deterministic.run_until_parked();
1841
1842 // While disconnected, close project 3
1843 cx_a.update(|_| drop(project_a3));
1844
1845 // Client B reconnects. They re-join the room and the remaining shared project.
1846 server.allow_connections();
1847 client_b
1848 .authenticate_and_connect(false, &cx_b.to_async())
1849 .await
1850 .unwrap();
1851 deterministic.run_until_parked();
1852 project_b1.read_with(cx_b, |project, cx| {
1853 assert!(!project.is_read_only());
1854 assert_eq!(
1855 project
1856 .worktree_for_id(worktree1_id, cx)
1857 .unwrap()
1858 .read(cx)
1859 .snapshot()
1860 .paths()
1861 .map(|p| p.to_str().unwrap())
1862 .collect::<Vec<_>>(),
1863 vec![
1864 "a.txt",
1865 "b.txt",
1866 "subdir2",
1867 "subdir2/f.txt",
1868 "subdir2/g.txt",
1869 "subdir2/h.txt",
1870 "subdir2/j.txt"
1871 ]
1872 );
1873 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
1874 assert_eq!(
1875 project
1876 .worktree_for_id(worktree4_id, cx)
1877 .unwrap()
1878 .read(cx)
1879 .snapshot()
1880 .paths()
1881 .map(|p| p.to_str().unwrap())
1882 .collect::<Vec<_>>(),
1883 vec!["z.txt"]
1884 );
1885 });
1886 project_b3.read_with(cx_b, |project, _| assert!(project.is_read_only()));
1887 buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
1888 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
1889}
1890
1891#[gpui::test(iterations = 10)]
1892async fn test_active_call_events(
1893 deterministic: Arc<Deterministic>,
1894 cx_a: &mut TestAppContext,
1895 cx_b: &mut TestAppContext,
1896) {
1897 deterministic.forbid_parking();
1898 let mut server = TestServer::start(&deterministic).await;
1899 let client_a = server.create_client(cx_a, "user_a").await;
1900 let client_b = server.create_client(cx_b, "user_b").await;
1901 client_a.fs.insert_tree("/a", json!({})).await;
1902 client_b.fs.insert_tree("/b", json!({})).await;
1903
1904 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1905 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1906
1907 server
1908 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1909 .await;
1910 let active_call_a = cx_a.read(ActiveCall::global);
1911 let active_call_b = cx_b.read(ActiveCall::global);
1912
1913 let events_a = active_call_events(cx_a);
1914 let events_b = active_call_events(cx_b);
1915
1916 let project_a_id = active_call_a
1917 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1918 .await
1919 .unwrap();
1920 deterministic.run_until_parked();
1921 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1922 assert_eq!(
1923 mem::take(&mut *events_b.borrow_mut()),
1924 vec![room::Event::RemoteProjectShared {
1925 owner: Arc::new(User {
1926 id: client_a.user_id().unwrap(),
1927 github_login: "user_a".to_string(),
1928 avatar: None,
1929 }),
1930 project_id: project_a_id,
1931 worktree_root_names: vec!["a".to_string()],
1932 }]
1933 );
1934
1935 let project_b_id = active_call_b
1936 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1937 .await
1938 .unwrap();
1939 deterministic.run_until_parked();
1940 assert_eq!(
1941 mem::take(&mut *events_a.borrow_mut()),
1942 vec![room::Event::RemoteProjectShared {
1943 owner: Arc::new(User {
1944 id: client_b.user_id().unwrap(),
1945 github_login: "user_b".to_string(),
1946 avatar: None,
1947 }),
1948 project_id: project_b_id,
1949 worktree_root_names: vec!["b".to_string()]
1950 }]
1951 );
1952 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1953
1954 // Sharing a project twice is idempotent.
1955 let project_b_id_2 = active_call_b
1956 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1957 .await
1958 .unwrap();
1959 assert_eq!(project_b_id_2, project_b_id);
1960 deterministic.run_until_parked();
1961 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1962 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1963}
1964
1965fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
1966 let events = Rc::new(RefCell::new(Vec::new()));
1967 let active_call = cx.read(ActiveCall::global);
1968 cx.update({
1969 let events = events.clone();
1970 |cx| {
1971 cx.subscribe(&active_call, move |_, event, _| {
1972 events.borrow_mut().push(event.clone())
1973 })
1974 .detach()
1975 }
1976 });
1977 events
1978}
1979
1980#[gpui::test(iterations = 10)]
1981async fn test_room_location(
1982 deterministic: Arc<Deterministic>,
1983 cx_a: &mut TestAppContext,
1984 cx_b: &mut TestAppContext,
1985) {
1986 deterministic.forbid_parking();
1987 let mut server = TestServer::start(&deterministic).await;
1988 let client_a = server.create_client(cx_a, "user_a").await;
1989 let client_b = server.create_client(cx_b, "user_b").await;
1990 client_a.fs.insert_tree("/a", json!({})).await;
1991 client_b.fs.insert_tree("/b", json!({})).await;
1992
1993 let active_call_a = cx_a.read(ActiveCall::global);
1994 let active_call_b = cx_b.read(ActiveCall::global);
1995
1996 let a_notified = Rc::new(Cell::new(false));
1997 cx_a.update({
1998 let notified = a_notified.clone();
1999 |cx| {
2000 cx.observe(&active_call_a, move |_, _| notified.set(true))
2001 .detach()
2002 }
2003 });
2004
2005 let b_notified = Rc::new(Cell::new(false));
2006 cx_b.update({
2007 let b_notified = b_notified.clone();
2008 |cx| {
2009 cx.observe(&active_call_b, move |_, _| b_notified.set(true))
2010 .detach()
2011 }
2012 });
2013
2014 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
2015 active_call_a
2016 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2017 .await
2018 .unwrap();
2019 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
2020
2021 server
2022 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2023 .await;
2024 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
2025 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
2026 deterministic.run_until_parked();
2027 assert!(a_notified.take());
2028 assert_eq!(
2029 participant_locations(&room_a, cx_a),
2030 vec![("user_b".to_string(), ParticipantLocation::External)]
2031 );
2032 assert!(b_notified.take());
2033 assert_eq!(
2034 participant_locations(&room_b, cx_b),
2035 vec![("user_a".to_string(), ParticipantLocation::UnsharedProject)]
2036 );
2037
2038 let project_a_id = active_call_a
2039 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2040 .await
2041 .unwrap();
2042 deterministic.run_until_parked();
2043 assert!(a_notified.take());
2044 assert_eq!(
2045 participant_locations(&room_a, cx_a),
2046 vec![("user_b".to_string(), ParticipantLocation::External)]
2047 );
2048 assert!(b_notified.take());
2049 assert_eq!(
2050 participant_locations(&room_b, cx_b),
2051 vec![(
2052 "user_a".to_string(),
2053 ParticipantLocation::SharedProject {
2054 project_id: project_a_id
2055 }
2056 )]
2057 );
2058
2059 let project_b_id = active_call_b
2060 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
2061 .await
2062 .unwrap();
2063 deterministic.run_until_parked();
2064 assert!(a_notified.take());
2065 assert_eq!(
2066 participant_locations(&room_a, cx_a),
2067 vec![("user_b".to_string(), ParticipantLocation::External)]
2068 );
2069 assert!(b_notified.take());
2070 assert_eq!(
2071 participant_locations(&room_b, cx_b),
2072 vec![(
2073 "user_a".to_string(),
2074 ParticipantLocation::SharedProject {
2075 project_id: project_a_id
2076 }
2077 )]
2078 );
2079
2080 active_call_b
2081 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2082 .await
2083 .unwrap();
2084 deterministic.run_until_parked();
2085 assert!(a_notified.take());
2086 assert_eq!(
2087 participant_locations(&room_a, cx_a),
2088 vec![(
2089 "user_b".to_string(),
2090 ParticipantLocation::SharedProject {
2091 project_id: project_b_id
2092 }
2093 )]
2094 );
2095 assert!(b_notified.take());
2096 assert_eq!(
2097 participant_locations(&room_b, cx_b),
2098 vec![(
2099 "user_a".to_string(),
2100 ParticipantLocation::SharedProject {
2101 project_id: project_a_id
2102 }
2103 )]
2104 );
2105
2106 active_call_b
2107 .update(cx_b, |call, cx| call.set_location(None, cx))
2108 .await
2109 .unwrap();
2110 deterministic.run_until_parked();
2111 assert!(a_notified.take());
2112 assert_eq!(
2113 participant_locations(&room_a, cx_a),
2114 vec![("user_b".to_string(), ParticipantLocation::External)]
2115 );
2116 assert!(b_notified.take());
2117 assert_eq!(
2118 participant_locations(&room_b, cx_b),
2119 vec![(
2120 "user_a".to_string(),
2121 ParticipantLocation::SharedProject {
2122 project_id: project_a_id
2123 }
2124 )]
2125 );
2126
2127 fn participant_locations(
2128 room: &ModelHandle<Room>,
2129 cx: &TestAppContext,
2130 ) -> Vec<(String, ParticipantLocation)> {
2131 room.read_with(cx, |room, _| {
2132 room.remote_participants()
2133 .values()
2134 .map(|participant| {
2135 (
2136 participant.user.github_login.to_string(),
2137 participant.location,
2138 )
2139 })
2140 .collect()
2141 })
2142 }
2143}
2144
2145#[gpui::test(iterations = 10)]
2146async fn test_propagate_saves_and_fs_changes(
2147 deterministic: Arc<Deterministic>,
2148 cx_a: &mut TestAppContext,
2149 cx_b: &mut TestAppContext,
2150 cx_c: &mut TestAppContext,
2151) {
2152 deterministic.forbid_parking();
2153 let mut server = TestServer::start(&deterministic).await;
2154 let client_a = server.create_client(cx_a, "user_a").await;
2155 let client_b = server.create_client(cx_b, "user_b").await;
2156 let client_c = server.create_client(cx_c, "user_c").await;
2157
2158 server
2159 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2160 .await;
2161 let active_call_a = cx_a.read(ActiveCall::global);
2162
2163 let rust = Arc::new(Language::new(
2164 LanguageConfig {
2165 name: "Rust".into(),
2166 path_suffixes: vec!["rs".to_string()],
2167 ..Default::default()
2168 },
2169 Some(tree_sitter_rust::language()),
2170 ));
2171 let javascript = Arc::new(Language::new(
2172 LanguageConfig {
2173 name: "JavaScript".into(),
2174 path_suffixes: vec!["js".to_string()],
2175 ..Default::default()
2176 },
2177 Some(tree_sitter_rust::language()),
2178 ));
2179 for client in [&client_a, &client_b, &client_c] {
2180 client.language_registry.add(rust.clone());
2181 client.language_registry.add(javascript.clone());
2182 }
2183
2184 client_a
2185 .fs
2186 .insert_tree(
2187 "/a",
2188 json!({
2189 "file1.rs": "",
2190 "file2": ""
2191 }),
2192 )
2193 .await;
2194 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2195 let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap());
2196 let project_id = active_call_a
2197 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2198 .await
2199 .unwrap();
2200
2201 // Join that worktree as clients B and C.
2202 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2203 let project_c = client_c.build_remote_project(project_id, cx_c).await;
2204 let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
2205 let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap());
2206
2207 // Open and edit a buffer as both guests B and C.
2208 let buffer_b = project_b
2209 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2210 .await
2211 .unwrap();
2212 let buffer_c = project_c
2213 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2214 .await
2215 .unwrap();
2216 buffer_b.read_with(cx_b, |buffer, _| {
2217 assert_eq!(&*buffer.language().unwrap().name(), "Rust");
2218 });
2219 buffer_c.read_with(cx_c, |buffer, _| {
2220 assert_eq!(&*buffer.language().unwrap().name(), "Rust");
2221 });
2222 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
2223 buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
2224
2225 // Open and edit that buffer as the host.
2226 let buffer_a = project_a
2227 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2228 .await
2229 .unwrap();
2230
2231 deterministic.run_until_parked();
2232 buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, "));
2233 buffer_a.update(cx_a, |buf, cx| {
2234 buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
2235 });
2236
2237 deterministic.run_until_parked();
2238 buffer_a.read_with(cx_a, |buf, _| {
2239 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2240 });
2241 buffer_b.read_with(cx_b, |buf, _| {
2242 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2243 });
2244 buffer_c.read_with(cx_c, |buf, _| {
2245 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2246 });
2247
2248 // Edit the buffer as the host and concurrently save as guest B.
2249 let save_b = project_b.update(cx_b, |project, cx| {
2250 project.save_buffer(buffer_b.clone(), cx)
2251 });
2252 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
2253 save_b.await.unwrap();
2254 assert_eq!(
2255 client_a.fs.load("/a/file1.rs".as_ref()).await.unwrap(),
2256 "hi-a, i-am-c, i-am-b, i-am-a"
2257 );
2258
2259 deterministic.run_until_parked();
2260 buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
2261 buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
2262 buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty()));
2263
2264 // Make changes on host's file system, see those changes on guest worktrees.
2265 client_a
2266 .fs
2267 .rename(
2268 "/a/file1.rs".as_ref(),
2269 "/a/file1.js".as_ref(),
2270 Default::default(),
2271 )
2272 .await
2273 .unwrap();
2274 client_a
2275 .fs
2276 .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
2277 .await
2278 .unwrap();
2279 client_a.fs.insert_file("/a/file4", "4".into()).await;
2280 deterministic.run_until_parked();
2281
2282 worktree_a.read_with(cx_a, |tree, _| {
2283 assert_eq!(
2284 tree.paths()
2285 .map(|p| p.to_string_lossy())
2286 .collect::<Vec<_>>(),
2287 ["file1.js", "file3", "file4"]
2288 )
2289 });
2290 worktree_b.read_with(cx_b, |tree, _| {
2291 assert_eq!(
2292 tree.paths()
2293 .map(|p| p.to_string_lossy())
2294 .collect::<Vec<_>>(),
2295 ["file1.js", "file3", "file4"]
2296 )
2297 });
2298 worktree_c.read_with(cx_c, |tree, _| {
2299 assert_eq!(
2300 tree.paths()
2301 .map(|p| p.to_string_lossy())
2302 .collect::<Vec<_>>(),
2303 ["file1.js", "file3", "file4"]
2304 )
2305 });
2306
2307 // Ensure buffer files are updated as well.
2308 buffer_a.read_with(cx_a, |buffer, _| {
2309 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2310 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2311 });
2312 buffer_b.read_with(cx_b, |buffer, _| {
2313 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2314 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2315 });
2316 buffer_c.read_with(cx_c, |buffer, _| {
2317 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2318 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2319 });
2320
2321 let new_buffer_a = project_a
2322 .update(cx_a, |p, cx| p.create_buffer("", None, cx))
2323 .unwrap();
2324 let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
2325 let new_buffer_b = project_b
2326 .update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx))
2327 .await
2328 .unwrap();
2329 new_buffer_b.read_with(cx_b, |buffer, _| {
2330 assert!(buffer.file().is_none());
2331 });
2332
2333 new_buffer_a.update(cx_a, |buffer, cx| {
2334 buffer.edit([(0..0, "ok")], None, cx);
2335 });
2336 project_a
2337 .update(cx_a, |project, cx| {
2338 project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
2339 })
2340 .await
2341 .unwrap();
2342
2343 deterministic.run_until_parked();
2344 new_buffer_b.read_with(cx_b, |buffer_b, _| {
2345 assert_eq!(
2346 buffer_b.file().unwrap().path().as_ref(),
2347 Path::new("file3.rs")
2348 );
2349
2350 new_buffer_a.read_with(cx_a, |buffer_a, _| {
2351 assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime());
2352 assert_eq!(buffer_b.saved_version(), buffer_a.saved_version());
2353 });
2354 });
2355}
2356
2357#[gpui::test(iterations = 10)]
2358async fn test_git_diff_base_change(
2359 deterministic: Arc<Deterministic>,
2360 cx_a: &mut TestAppContext,
2361 cx_b: &mut TestAppContext,
2362) {
2363 deterministic.forbid_parking();
2364 let mut server = TestServer::start(&deterministic).await;
2365 let client_a = server.create_client(cx_a, "user_a").await;
2366 let client_b = server.create_client(cx_b, "user_b").await;
2367 server
2368 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2369 .await;
2370 let active_call_a = cx_a.read(ActiveCall::global);
2371
2372 client_a
2373 .fs
2374 .insert_tree(
2375 "/dir",
2376 json!({
2377 ".git": {},
2378 "sub": {
2379 ".git": {},
2380 "b.txt": "
2381 one
2382 two
2383 three
2384 ".unindent(),
2385 },
2386 "a.txt": "
2387 one
2388 two
2389 three
2390 ".unindent(),
2391 }),
2392 )
2393 .await;
2394
2395 let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2396 let project_id = active_call_a
2397 .update(cx_a, |call, cx| {
2398 call.share_project(project_local.clone(), cx)
2399 })
2400 .await
2401 .unwrap();
2402
2403 let project_remote = client_b.build_remote_project(project_id, cx_b).await;
2404
2405 let diff_base = "
2406 one
2407 three
2408 "
2409 .unindent();
2410
2411 let new_diff_base = "
2412 one
2413 two
2414 "
2415 .unindent();
2416
2417 client_a
2418 .fs
2419 .as_fake()
2420 .set_index_for_repo(
2421 Path::new("/dir/.git"),
2422 &[(Path::new("a.txt"), diff_base.clone())],
2423 )
2424 .await;
2425
2426 // Create the buffer
2427 let buffer_local_a = project_local
2428 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2429 .await
2430 .unwrap();
2431
2432 // Wait for it to catch up to the new diff
2433 deterministic.run_until_parked();
2434
2435 // Smoke test diffing
2436 buffer_local_a.read_with(cx_a, |buffer, _| {
2437 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2438 git::diff::assert_hunks(
2439 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2440 &buffer,
2441 &diff_base,
2442 &[(1..2, "", "two\n")],
2443 );
2444 });
2445
2446 // Create remote buffer
2447 let buffer_remote_a = project_remote
2448 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2449 .await
2450 .unwrap();
2451
2452 // Wait remote buffer to catch up to the new diff
2453 deterministic.run_until_parked();
2454
2455 // Smoke test diffing
2456 buffer_remote_a.read_with(cx_b, |buffer, _| {
2457 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2458 git::diff::assert_hunks(
2459 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2460 &buffer,
2461 &diff_base,
2462 &[(1..2, "", "two\n")],
2463 );
2464 });
2465
2466 client_a
2467 .fs
2468 .as_fake()
2469 .set_index_for_repo(
2470 Path::new("/dir/.git"),
2471 &[(Path::new("a.txt"), new_diff_base.clone())],
2472 )
2473 .await;
2474
2475 // Wait for buffer_local_a to receive it
2476 deterministic.run_until_parked();
2477
2478 // Smoke test new diffing
2479 buffer_local_a.read_with(cx_a, |buffer, _| {
2480 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2481
2482 git::diff::assert_hunks(
2483 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2484 &buffer,
2485 &diff_base,
2486 &[(2..3, "", "three\n")],
2487 );
2488 });
2489
2490 // Smoke test B
2491 buffer_remote_a.read_with(cx_b, |buffer, _| {
2492 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2493 git::diff::assert_hunks(
2494 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2495 &buffer,
2496 &diff_base,
2497 &[(2..3, "", "three\n")],
2498 );
2499 });
2500
2501 //Nested git dir
2502
2503 let diff_base = "
2504 one
2505 three
2506 "
2507 .unindent();
2508
2509 let new_diff_base = "
2510 one
2511 two
2512 "
2513 .unindent();
2514
2515 client_a
2516 .fs
2517 .as_fake()
2518 .set_index_for_repo(
2519 Path::new("/dir/sub/.git"),
2520 &[(Path::new("b.txt"), diff_base.clone())],
2521 )
2522 .await;
2523
2524 // Create the buffer
2525 let buffer_local_b = project_local
2526 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2527 .await
2528 .unwrap();
2529
2530 // Wait for it to catch up to the new diff
2531 deterministic.run_until_parked();
2532
2533 // Smoke test diffing
2534 buffer_local_b.read_with(cx_a, |buffer, _| {
2535 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2536 git::diff::assert_hunks(
2537 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2538 &buffer,
2539 &diff_base,
2540 &[(1..2, "", "two\n")],
2541 );
2542 });
2543
2544 // Create remote buffer
2545 let buffer_remote_b = project_remote
2546 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2547 .await
2548 .unwrap();
2549
2550 // Wait remote buffer to catch up to the new diff
2551 deterministic.run_until_parked();
2552
2553 // Smoke test diffing
2554 buffer_remote_b.read_with(cx_b, |buffer, _| {
2555 assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
2556 git::diff::assert_hunks(
2557 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2558 &buffer,
2559 &diff_base,
2560 &[(1..2, "", "two\n")],
2561 );
2562 });
2563
2564 client_a
2565 .fs
2566 .as_fake()
2567 .set_index_for_repo(
2568 Path::new("/dir/sub/.git"),
2569 &[(Path::new("b.txt"), new_diff_base.clone())],
2570 )
2571 .await;
2572
2573 // Wait for buffer_local_b to receive it
2574 deterministic.run_until_parked();
2575
2576 // Smoke test new diffing
2577 buffer_local_b.read_with(cx_a, |buffer, _| {
2578 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2579 println!("{:?}", buffer.as_rope().to_string());
2580 println!("{:?}", buffer.diff_base());
2581 println!(
2582 "{:?}",
2583 buffer
2584 .snapshot()
2585 .git_diff_hunks_in_row_range(0..4, false)
2586 .collect::<Vec<_>>()
2587 );
2588
2589 git::diff::assert_hunks(
2590 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2591 &buffer,
2592 &diff_base,
2593 &[(2..3, "", "three\n")],
2594 );
2595 });
2596
2597 // Smoke test B
2598 buffer_remote_b.read_with(cx_b, |buffer, _| {
2599 assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
2600 git::diff::assert_hunks(
2601 buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
2602 &buffer,
2603 &diff_base,
2604 &[(2..3, "", "three\n")],
2605 );
2606 });
2607}
2608
2609#[gpui::test(iterations = 10)]
2610async fn test_fs_operations(
2611 deterministic: Arc<Deterministic>,
2612 cx_a: &mut TestAppContext,
2613 cx_b: &mut TestAppContext,
2614) {
2615 deterministic.forbid_parking();
2616 let mut server = TestServer::start(&deterministic).await;
2617 let client_a = server.create_client(cx_a, "user_a").await;
2618 let client_b = server.create_client(cx_b, "user_b").await;
2619 server
2620 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2621 .await;
2622 let active_call_a = cx_a.read(ActiveCall::global);
2623
2624 client_a
2625 .fs
2626 .insert_tree(
2627 "/dir",
2628 json!({
2629 "a.txt": "a-contents",
2630 "b.txt": "b-contents",
2631 }),
2632 )
2633 .await;
2634 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2635 let project_id = active_call_a
2636 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2637 .await
2638 .unwrap();
2639 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2640
2641 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
2642 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
2643
2644 let entry = project_b
2645 .update(cx_b, |project, cx| {
2646 project
2647 .create_entry((worktree_id, "c.txt"), false, cx)
2648 .unwrap()
2649 })
2650 .await
2651 .unwrap();
2652 worktree_a.read_with(cx_a, |worktree, _| {
2653 assert_eq!(
2654 worktree
2655 .paths()
2656 .map(|p| p.to_string_lossy())
2657 .collect::<Vec<_>>(),
2658 ["a.txt", "b.txt", "c.txt"]
2659 );
2660 });
2661 worktree_b.read_with(cx_b, |worktree, _| {
2662 assert_eq!(
2663 worktree
2664 .paths()
2665 .map(|p| p.to_string_lossy())
2666 .collect::<Vec<_>>(),
2667 ["a.txt", "b.txt", "c.txt"]
2668 );
2669 });
2670
2671 project_b
2672 .update(cx_b, |project, cx| {
2673 project.rename_entry(entry.id, Path::new("d.txt"), cx)
2674 })
2675 .unwrap()
2676 .await
2677 .unwrap();
2678 worktree_a.read_with(cx_a, |worktree, _| {
2679 assert_eq!(
2680 worktree
2681 .paths()
2682 .map(|p| p.to_string_lossy())
2683 .collect::<Vec<_>>(),
2684 ["a.txt", "b.txt", "d.txt"]
2685 );
2686 });
2687 worktree_b.read_with(cx_b, |worktree, _| {
2688 assert_eq!(
2689 worktree
2690 .paths()
2691 .map(|p| p.to_string_lossy())
2692 .collect::<Vec<_>>(),
2693 ["a.txt", "b.txt", "d.txt"]
2694 );
2695 });
2696
2697 let dir_entry = project_b
2698 .update(cx_b, |project, cx| {
2699 project
2700 .create_entry((worktree_id, "DIR"), true, cx)
2701 .unwrap()
2702 })
2703 .await
2704 .unwrap();
2705 worktree_a.read_with(cx_a, |worktree, _| {
2706 assert_eq!(
2707 worktree
2708 .paths()
2709 .map(|p| p.to_string_lossy())
2710 .collect::<Vec<_>>(),
2711 ["DIR", "a.txt", "b.txt", "d.txt"]
2712 );
2713 });
2714 worktree_b.read_with(cx_b, |worktree, _| {
2715 assert_eq!(
2716 worktree
2717 .paths()
2718 .map(|p| p.to_string_lossy())
2719 .collect::<Vec<_>>(),
2720 ["DIR", "a.txt", "b.txt", "d.txt"]
2721 );
2722 });
2723
2724 project_b
2725 .update(cx_b, |project, cx| {
2726 project
2727 .create_entry((worktree_id, "DIR/e.txt"), false, cx)
2728 .unwrap()
2729 })
2730 .await
2731 .unwrap();
2732 project_b
2733 .update(cx_b, |project, cx| {
2734 project
2735 .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
2736 .unwrap()
2737 })
2738 .await
2739 .unwrap();
2740 project_b
2741 .update(cx_b, |project, cx| {
2742 project
2743 .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
2744 .unwrap()
2745 })
2746 .await
2747 .unwrap();
2748 worktree_a.read_with(cx_a, |worktree, _| {
2749 assert_eq!(
2750 worktree
2751 .paths()
2752 .map(|p| p.to_string_lossy())
2753 .collect::<Vec<_>>(),
2754 [
2755 "DIR",
2756 "DIR/SUBDIR",
2757 "DIR/SUBDIR/f.txt",
2758 "DIR/e.txt",
2759 "a.txt",
2760 "b.txt",
2761 "d.txt"
2762 ]
2763 );
2764 });
2765 worktree_b.read_with(cx_b, |worktree, _| {
2766 assert_eq!(
2767 worktree
2768 .paths()
2769 .map(|p| p.to_string_lossy())
2770 .collect::<Vec<_>>(),
2771 [
2772 "DIR",
2773 "DIR/SUBDIR",
2774 "DIR/SUBDIR/f.txt",
2775 "DIR/e.txt",
2776 "a.txt",
2777 "b.txt",
2778 "d.txt"
2779 ]
2780 );
2781 });
2782
2783 project_b
2784 .update(cx_b, |project, cx| {
2785 project
2786 .copy_entry(entry.id, Path::new("f.txt"), cx)
2787 .unwrap()
2788 })
2789 .await
2790 .unwrap();
2791 worktree_a.read_with(cx_a, |worktree, _| {
2792 assert_eq!(
2793 worktree
2794 .paths()
2795 .map(|p| p.to_string_lossy())
2796 .collect::<Vec<_>>(),
2797 [
2798 "DIR",
2799 "DIR/SUBDIR",
2800 "DIR/SUBDIR/f.txt",
2801 "DIR/e.txt",
2802 "a.txt",
2803 "b.txt",
2804 "d.txt",
2805 "f.txt"
2806 ]
2807 );
2808 });
2809 worktree_b.read_with(cx_b, |worktree, _| {
2810 assert_eq!(
2811 worktree
2812 .paths()
2813 .map(|p| p.to_string_lossy())
2814 .collect::<Vec<_>>(),
2815 [
2816 "DIR",
2817 "DIR/SUBDIR",
2818 "DIR/SUBDIR/f.txt",
2819 "DIR/e.txt",
2820 "a.txt",
2821 "b.txt",
2822 "d.txt",
2823 "f.txt"
2824 ]
2825 );
2826 });
2827
2828 project_b
2829 .update(cx_b, |project, cx| {
2830 project.delete_entry(dir_entry.id, cx).unwrap()
2831 })
2832 .await
2833 .unwrap();
2834 deterministic.run_until_parked();
2835
2836 worktree_a.read_with(cx_a, |worktree, _| {
2837 assert_eq!(
2838 worktree
2839 .paths()
2840 .map(|p| p.to_string_lossy())
2841 .collect::<Vec<_>>(),
2842 ["a.txt", "b.txt", "d.txt", "f.txt"]
2843 );
2844 });
2845 worktree_b.read_with(cx_b, |worktree, _| {
2846 assert_eq!(
2847 worktree
2848 .paths()
2849 .map(|p| p.to_string_lossy())
2850 .collect::<Vec<_>>(),
2851 ["a.txt", "b.txt", "d.txt", "f.txt"]
2852 );
2853 });
2854
2855 project_b
2856 .update(cx_b, |project, cx| {
2857 project.delete_entry(entry.id, cx).unwrap()
2858 })
2859 .await
2860 .unwrap();
2861 worktree_a.read_with(cx_a, |worktree, _| {
2862 assert_eq!(
2863 worktree
2864 .paths()
2865 .map(|p| p.to_string_lossy())
2866 .collect::<Vec<_>>(),
2867 ["a.txt", "b.txt", "f.txt"]
2868 );
2869 });
2870 worktree_b.read_with(cx_b, |worktree, _| {
2871 assert_eq!(
2872 worktree
2873 .paths()
2874 .map(|p| p.to_string_lossy())
2875 .collect::<Vec<_>>(),
2876 ["a.txt", "b.txt", "f.txt"]
2877 );
2878 });
2879}
2880
2881#[gpui::test(iterations = 10)]
2882async fn test_buffer_conflict_after_save(
2883 deterministic: Arc<Deterministic>,
2884 cx_a: &mut TestAppContext,
2885 cx_b: &mut TestAppContext,
2886) {
2887 deterministic.forbid_parking();
2888 let mut server = TestServer::start(&deterministic).await;
2889 let client_a = server.create_client(cx_a, "user_a").await;
2890 let client_b = server.create_client(cx_b, "user_b").await;
2891 server
2892 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2893 .await;
2894 let active_call_a = cx_a.read(ActiveCall::global);
2895
2896 client_a
2897 .fs
2898 .insert_tree(
2899 "/dir",
2900 json!({
2901 "a.txt": "a-contents",
2902 }),
2903 )
2904 .await;
2905 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2906 let project_id = active_call_a
2907 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2908 .await
2909 .unwrap();
2910 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2911
2912 // Open a buffer as client B
2913 let buffer_b = project_b
2914 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2915 .await
2916 .unwrap();
2917
2918 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
2919 buffer_b.read_with(cx_b, |buf, _| {
2920 assert!(buf.is_dirty());
2921 assert!(!buf.has_conflict());
2922 });
2923
2924 project_b
2925 .update(cx_b, |project, cx| {
2926 project.save_buffer(buffer_b.clone(), cx)
2927 })
2928 .await
2929 .unwrap();
2930 cx_a.foreground().forbid_parking();
2931 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
2932 buffer_b.read_with(cx_b, |buf, _| {
2933 assert!(!buf.has_conflict());
2934 });
2935
2936 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
2937 buffer_b.read_with(cx_b, |buf, _| {
2938 assert!(buf.is_dirty());
2939 assert!(!buf.has_conflict());
2940 });
2941}
2942
2943#[gpui::test(iterations = 10)]
2944async fn test_buffer_reloading(
2945 deterministic: Arc<Deterministic>,
2946 cx_a: &mut TestAppContext,
2947 cx_b: &mut TestAppContext,
2948) {
2949 deterministic.forbid_parking();
2950 let mut server = TestServer::start(&deterministic).await;
2951 let client_a = server.create_client(cx_a, "user_a").await;
2952 let client_b = server.create_client(cx_b, "user_b").await;
2953 server
2954 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2955 .await;
2956 let active_call_a = cx_a.read(ActiveCall::global);
2957
2958 client_a
2959 .fs
2960 .insert_tree(
2961 "/dir",
2962 json!({
2963 "a.txt": "a\nb\nc",
2964 }),
2965 )
2966 .await;
2967 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2968 let project_id = active_call_a
2969 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2970 .await
2971 .unwrap();
2972 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2973
2974 // Open a buffer as client B
2975 let buffer_b = project_b
2976 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2977 .await
2978 .unwrap();
2979 buffer_b.read_with(cx_b, |buf, _| {
2980 assert!(!buf.is_dirty());
2981 assert!(!buf.has_conflict());
2982 assert_eq!(buf.line_ending(), LineEnding::Unix);
2983 });
2984
2985 let new_contents = Rope::from("d\ne\nf");
2986 client_a
2987 .fs
2988 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
2989 .await
2990 .unwrap();
2991 cx_a.foreground().run_until_parked();
2992 buffer_b.read_with(cx_b, |buf, _| {
2993 assert_eq!(buf.text(), new_contents.to_string());
2994 assert!(!buf.is_dirty());
2995 assert!(!buf.has_conflict());
2996 assert_eq!(buf.line_ending(), LineEnding::Windows);
2997 });
2998}
2999
3000#[gpui::test(iterations = 10)]
3001async fn test_editing_while_guest_opens_buffer(
3002 deterministic: Arc<Deterministic>,
3003 cx_a: &mut TestAppContext,
3004 cx_b: &mut TestAppContext,
3005) {
3006 deterministic.forbid_parking();
3007 let mut server = TestServer::start(&deterministic).await;
3008 let client_a = server.create_client(cx_a, "user_a").await;
3009 let client_b = server.create_client(cx_b, "user_b").await;
3010 server
3011 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3012 .await;
3013 let active_call_a = cx_a.read(ActiveCall::global);
3014
3015 client_a
3016 .fs
3017 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3018 .await;
3019 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3020 let project_id = active_call_a
3021 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3022 .await
3023 .unwrap();
3024 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3025
3026 // Open a buffer as client A
3027 let buffer_a = project_a
3028 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3029 .await
3030 .unwrap();
3031
3032 // Start opening the same buffer as client B
3033 let buffer_b = cx_b
3034 .background()
3035 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3036
3037 // Edit the buffer as client A while client B is still opening it.
3038 cx_b.background().simulate_random_delay().await;
3039 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3040 cx_b.background().simulate_random_delay().await;
3041 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3042
3043 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3044 let buffer_b = buffer_b.await.unwrap();
3045 cx_a.foreground().run_until_parked();
3046 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3047}
3048
3049#[gpui::test]
3050async fn test_newline_above_or_below_does_not_move_guest_cursor(
3051 deterministic: Arc<Deterministic>,
3052 cx_a: &mut TestAppContext,
3053 cx_b: &mut TestAppContext,
3054) {
3055 deterministic.forbid_parking();
3056 let mut server = TestServer::start(&deterministic).await;
3057 let client_a = server.create_client(cx_a, "user_a").await;
3058 let client_b = server.create_client(cx_b, "user_b").await;
3059 server
3060 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3061 .await;
3062 let active_call_a = cx_a.read(ActiveCall::global);
3063
3064 client_a
3065 .fs
3066 .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
3067 .await;
3068 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3069 let project_id = active_call_a
3070 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3071 .await
3072 .unwrap();
3073
3074 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3075
3076 // Open a buffer as client A
3077 let buffer_a = project_a
3078 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3079 .await
3080 .unwrap();
3081 let (_, window_a) = cx_a.add_window(|_| EmptyView);
3082 let editor_a = cx_a.add_view(&window_a, |cx| {
3083 Editor::for_buffer(buffer_a, Some(project_a), cx)
3084 });
3085 let mut editor_cx_a = EditorTestContext {
3086 cx: cx_a,
3087 window_id: window_a.id(),
3088 editor: editor_a,
3089 };
3090
3091 // Open a buffer as client B
3092 let buffer_b = project_b
3093 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3094 .await
3095 .unwrap();
3096 let (_, window_b) = cx_b.add_window(|_| EmptyView);
3097 let editor_b = cx_b.add_view(&window_b, |cx| {
3098 Editor::for_buffer(buffer_b, Some(project_b), cx)
3099 });
3100 let mut editor_cx_b = EditorTestContext {
3101 cx: cx_b,
3102 window_id: window_b.id(),
3103 editor: editor_b,
3104 };
3105
3106 // Test newline above
3107 editor_cx_a.set_selections_state(indoc! {"
3108 Some textˇ
3109 "});
3110 editor_cx_b.set_selections_state(indoc! {"
3111 Some textˇ
3112 "});
3113 editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
3114 deterministic.run_until_parked();
3115 editor_cx_a.assert_editor_state(indoc! {"
3116 ˇ
3117 Some text
3118 "});
3119 editor_cx_b.assert_editor_state(indoc! {"
3120
3121 Some textˇ
3122 "});
3123
3124 // Test newline below
3125 editor_cx_a.set_selections_state(indoc! {"
3126
3127 Some textˇ
3128 "});
3129 editor_cx_b.set_selections_state(indoc! {"
3130
3131 Some textˇ
3132 "});
3133 editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
3134 deterministic.run_until_parked();
3135 editor_cx_a.assert_editor_state(indoc! {"
3136
3137 Some text
3138 ˇ
3139 "});
3140 editor_cx_b.assert_editor_state(indoc! {"
3141
3142 Some textˇ
3143
3144 "});
3145}
3146
3147#[gpui::test(iterations = 10)]
3148async fn test_leaving_worktree_while_opening_buffer(
3149 deterministic: Arc<Deterministic>,
3150 cx_a: &mut TestAppContext,
3151 cx_b: &mut TestAppContext,
3152) {
3153 deterministic.forbid_parking();
3154 let mut server = TestServer::start(&deterministic).await;
3155 let client_a = server.create_client(cx_a, "user_a").await;
3156 let client_b = server.create_client(cx_b, "user_b").await;
3157 server
3158 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3159 .await;
3160 let active_call_a = cx_a.read(ActiveCall::global);
3161
3162 client_a
3163 .fs
3164 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3165 .await;
3166 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3167 let project_id = active_call_a
3168 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3169 .await
3170 .unwrap();
3171 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3172
3173 // See that a guest has joined as client A.
3174 cx_a.foreground().run_until_parked();
3175 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3176
3177 // Begin opening a buffer as client B, but leave the project before the open completes.
3178 let buffer_b = cx_b
3179 .background()
3180 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3181 cx_b.update(|_| drop(project_b));
3182 drop(buffer_b);
3183
3184 // See that the guest has left.
3185 cx_a.foreground().run_until_parked();
3186 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3187}
3188
3189#[gpui::test(iterations = 10)]
3190async fn test_canceling_buffer_opening(
3191 deterministic: Arc<Deterministic>,
3192 cx_a: &mut TestAppContext,
3193 cx_b: &mut TestAppContext,
3194) {
3195 deterministic.forbid_parking();
3196
3197 let mut server = TestServer::start(&deterministic).await;
3198 let client_a = server.create_client(cx_a, "user_a").await;
3199 let client_b = server.create_client(cx_b, "user_b").await;
3200 server
3201 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3202 .await;
3203 let active_call_a = cx_a.read(ActiveCall::global);
3204
3205 client_a
3206 .fs
3207 .insert_tree(
3208 "/dir",
3209 json!({
3210 "a.txt": "abc",
3211 }),
3212 )
3213 .await;
3214 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3215 let project_id = active_call_a
3216 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3217 .await
3218 .unwrap();
3219 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3220
3221 let buffer_a = project_a
3222 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3223 .await
3224 .unwrap();
3225
3226 // Open a buffer as client B but cancel after a random amount of time.
3227 let buffer_b = project_b.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx));
3228 deterministic.simulate_random_delay().await;
3229 drop(buffer_b);
3230
3231 // Try opening the same buffer again as client B, and ensure we can
3232 // still do it despite the cancellation above.
3233 let buffer_b = project_b
3234 .update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx))
3235 .await
3236 .unwrap();
3237 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3238}
3239
3240#[gpui::test(iterations = 10)]
3241async fn test_leaving_project(
3242 deterministic: Arc<Deterministic>,
3243 cx_a: &mut TestAppContext,
3244 cx_b: &mut TestAppContext,
3245 cx_c: &mut TestAppContext,
3246) {
3247 deterministic.forbid_parking();
3248 let mut server = TestServer::start(&deterministic).await;
3249 let client_a = server.create_client(cx_a, "user_a").await;
3250 let client_b = server.create_client(cx_b, "user_b").await;
3251 let client_c = server.create_client(cx_c, "user_c").await;
3252 server
3253 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3254 .await;
3255 let active_call_a = cx_a.read(ActiveCall::global);
3256
3257 client_a
3258 .fs
3259 .insert_tree(
3260 "/a",
3261 json!({
3262 "a.txt": "a-contents",
3263 "b.txt": "b-contents",
3264 }),
3265 )
3266 .await;
3267 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3268 let project_id = active_call_a
3269 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3270 .await
3271 .unwrap();
3272 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3273 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3274
3275 // Client A sees that a guest has joined.
3276 deterministic.run_until_parked();
3277 project_a.read_with(cx_a, |project, _| {
3278 assert_eq!(project.collaborators().len(), 2);
3279 });
3280 project_b1.read_with(cx_b, |project, _| {
3281 assert_eq!(project.collaborators().len(), 2);
3282 });
3283 project_c.read_with(cx_c, |project, _| {
3284 assert_eq!(project.collaborators().len(), 2);
3285 });
3286
3287 // Client B opens a buffer.
3288 let buffer_b1 = project_b1
3289 .update(cx_b, |project, cx| {
3290 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3291 project.open_buffer((worktree_id, "a.txt"), cx)
3292 })
3293 .await
3294 .unwrap();
3295 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3296
3297 // Drop client B's project and ensure client A and client C observe client B leaving.
3298 cx_b.update(|_| drop(project_b1));
3299 deterministic.run_until_parked();
3300 project_a.read_with(cx_a, |project, _| {
3301 assert_eq!(project.collaborators().len(), 1);
3302 });
3303 project_c.read_with(cx_c, |project, _| {
3304 assert_eq!(project.collaborators().len(), 1);
3305 });
3306
3307 // Client B re-joins the project and can open buffers as before.
3308 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3309 deterministic.run_until_parked();
3310 project_a.read_with(cx_a, |project, _| {
3311 assert_eq!(project.collaborators().len(), 2);
3312 });
3313 project_b2.read_with(cx_b, |project, _| {
3314 assert_eq!(project.collaborators().len(), 2);
3315 });
3316 project_c.read_with(cx_c, |project, _| {
3317 assert_eq!(project.collaborators().len(), 2);
3318 });
3319
3320 let buffer_b2 = project_b2
3321 .update(cx_b, |project, cx| {
3322 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3323 project.open_buffer((worktree_id, "a.txt"), cx)
3324 })
3325 .await
3326 .unwrap();
3327 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3328
3329 // Drop client B's connection and ensure client A and client C observe client B leaving.
3330 client_b.disconnect(&cx_b.to_async());
3331 deterministic.advance_clock(RECONNECT_TIMEOUT);
3332 project_a.read_with(cx_a, |project, _| {
3333 assert_eq!(project.collaborators().len(), 1);
3334 });
3335 project_b2.read_with(cx_b, |project, _| {
3336 assert!(project.is_read_only());
3337 });
3338 project_c.read_with(cx_c, |project, _| {
3339 assert_eq!(project.collaborators().len(), 1);
3340 });
3341
3342 // Client B can't join the project, unless they re-join the room.
3343 cx_b.spawn(|cx| {
3344 Project::remote(
3345 project_id,
3346 client_b.client.clone(),
3347 client_b.user_store.clone(),
3348 client_b.language_registry.clone(),
3349 FakeFs::new(cx.background()),
3350 cx,
3351 )
3352 })
3353 .await
3354 .unwrap_err();
3355
3356 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3357 client_c.wait_for_current_user(cx_c).await;
3358 server.forbid_connections();
3359 server.disconnect_client(client_c.peer_id().unwrap());
3360 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3361 deterministic.run_until_parked();
3362 project_a.read_with(cx_a, |project, _| {
3363 assert_eq!(project.collaborators().len(), 0);
3364 });
3365 project_b2.read_with(cx_b, |project, _| {
3366 assert!(project.is_read_only());
3367 });
3368 project_c.read_with(cx_c, |project, _| {
3369 assert!(project.is_read_only());
3370 });
3371}
3372
3373#[gpui::test(iterations = 10)]
3374async fn test_collaborating_with_diagnostics(
3375 deterministic: Arc<Deterministic>,
3376 cx_a: &mut TestAppContext,
3377 cx_b: &mut TestAppContext,
3378 cx_c: &mut TestAppContext,
3379) {
3380 deterministic.forbid_parking();
3381 let mut server = TestServer::start(&deterministic).await;
3382 let client_a = server.create_client(cx_a, "user_a").await;
3383 let client_b = server.create_client(cx_b, "user_b").await;
3384 let client_c = server.create_client(cx_c, "user_c").await;
3385 server
3386 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3387 .await;
3388 let active_call_a = cx_a.read(ActiveCall::global);
3389
3390 // Set up a fake language server.
3391 let mut language = Language::new(
3392 LanguageConfig {
3393 name: "Rust".into(),
3394 path_suffixes: vec!["rs".to_string()],
3395 ..Default::default()
3396 },
3397 Some(tree_sitter_rust::language()),
3398 );
3399 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3400 client_a.language_registry.add(Arc::new(language));
3401
3402 // Share a project as client A
3403 client_a
3404 .fs
3405 .insert_tree(
3406 "/a",
3407 json!({
3408 "a.rs": "let one = two",
3409 "other.rs": "",
3410 }),
3411 )
3412 .await;
3413 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3414
3415 // Cause the language server to start.
3416 let _buffer = project_a
3417 .update(cx_a, |project, cx| {
3418 project.open_buffer(
3419 ProjectPath {
3420 worktree_id,
3421 path: Path::new("other.rs").into(),
3422 },
3423 cx,
3424 )
3425 })
3426 .await
3427 .unwrap();
3428
3429 // Simulate a language server reporting errors for a file.
3430 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3431 fake_language_server
3432 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3433 .await;
3434 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3435 lsp::PublishDiagnosticsParams {
3436 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3437 version: None,
3438 diagnostics: vec![lsp::Diagnostic {
3439 severity: Some(lsp::DiagnosticSeverity::WARNING),
3440 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3441 message: "message 0".to_string(),
3442 ..Default::default()
3443 }],
3444 },
3445 );
3446
3447 // Client A shares the project and, simultaneously, the language server
3448 // publishes a diagnostic. This is done to ensure that the server always
3449 // observes the latest diagnostics for a worktree.
3450 let project_id = active_call_a
3451 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3452 .await
3453 .unwrap();
3454 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3455 lsp::PublishDiagnosticsParams {
3456 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3457 version: None,
3458 diagnostics: vec![lsp::Diagnostic {
3459 severity: Some(lsp::DiagnosticSeverity::ERROR),
3460 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3461 message: "message 1".to_string(),
3462 ..Default::default()
3463 }],
3464 },
3465 );
3466
3467 // Join the worktree as client B.
3468 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3469
3470 // Wait for server to see the diagnostics update.
3471 deterministic.run_until_parked();
3472
3473 // Ensure client B observes the new diagnostics.
3474 project_b.read_with(cx_b, |project, cx| {
3475 assert_eq!(
3476 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3477 &[(
3478 ProjectPath {
3479 worktree_id,
3480 path: Arc::from(Path::new("a.rs")),
3481 },
3482 LanguageServerId(0),
3483 DiagnosticSummary {
3484 error_count: 1,
3485 warning_count: 0,
3486 ..Default::default()
3487 },
3488 )]
3489 )
3490 });
3491
3492 // Join project as client C and observe the diagnostics.
3493 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3494 let project_c_diagnostic_summaries =
3495 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3496 project.diagnostic_summaries(cx).collect::<Vec<_>>()
3497 })));
3498 project_c.update(cx_c, |_, cx| {
3499 let summaries = project_c_diagnostic_summaries.clone();
3500 cx.subscribe(&project_c, {
3501 move |p, _, event, cx| {
3502 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3503 *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
3504 }
3505 }
3506 })
3507 .detach();
3508 });
3509
3510 deterministic.run_until_parked();
3511 assert_eq!(
3512 project_c_diagnostic_summaries.borrow().as_slice(),
3513 &[(
3514 ProjectPath {
3515 worktree_id,
3516 path: Arc::from(Path::new("a.rs")),
3517 },
3518 LanguageServerId(0),
3519 DiagnosticSummary {
3520 error_count: 1,
3521 warning_count: 0,
3522 ..Default::default()
3523 },
3524 )]
3525 );
3526
3527 // Simulate a language server reporting more errors for a file.
3528 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3529 lsp::PublishDiagnosticsParams {
3530 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3531 version: None,
3532 diagnostics: vec![
3533 lsp::Diagnostic {
3534 severity: Some(lsp::DiagnosticSeverity::ERROR),
3535 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3536 message: "message 1".to_string(),
3537 ..Default::default()
3538 },
3539 lsp::Diagnostic {
3540 severity: Some(lsp::DiagnosticSeverity::WARNING),
3541 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3542 message: "message 2".to_string(),
3543 ..Default::default()
3544 },
3545 ],
3546 },
3547 );
3548
3549 // Clients B and C get the updated summaries
3550 deterministic.run_until_parked();
3551 project_b.read_with(cx_b, |project, cx| {
3552 assert_eq!(
3553 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3554 [(
3555 ProjectPath {
3556 worktree_id,
3557 path: Arc::from(Path::new("a.rs")),
3558 },
3559 LanguageServerId(0),
3560 DiagnosticSummary {
3561 error_count: 1,
3562 warning_count: 1,
3563 },
3564 )]
3565 );
3566 });
3567 project_c.read_with(cx_c, |project, cx| {
3568 assert_eq!(
3569 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3570 [(
3571 ProjectPath {
3572 worktree_id,
3573 path: Arc::from(Path::new("a.rs")),
3574 },
3575 LanguageServerId(0),
3576 DiagnosticSummary {
3577 error_count: 1,
3578 warning_count: 1,
3579 },
3580 )]
3581 );
3582 });
3583
3584 // Open the file with the errors on client B. They should be present.
3585 let buffer_b = cx_b
3586 .background()
3587 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3588 .await
3589 .unwrap();
3590
3591 buffer_b.read_with(cx_b, |buffer, _| {
3592 assert_eq!(
3593 buffer
3594 .snapshot()
3595 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3596 .collect::<Vec<_>>(),
3597 &[
3598 DiagnosticEntry {
3599 range: Point::new(0, 4)..Point::new(0, 7),
3600 diagnostic: Diagnostic {
3601 group_id: 2,
3602 message: "message 1".to_string(),
3603 severity: lsp::DiagnosticSeverity::ERROR,
3604 is_primary: true,
3605 ..Default::default()
3606 }
3607 },
3608 DiagnosticEntry {
3609 range: Point::new(0, 10)..Point::new(0, 13),
3610 diagnostic: Diagnostic {
3611 group_id: 3,
3612 severity: lsp::DiagnosticSeverity::WARNING,
3613 message: "message 2".to_string(),
3614 is_primary: true,
3615 ..Default::default()
3616 }
3617 }
3618 ]
3619 );
3620 });
3621
3622 // Simulate a language server reporting no errors for a file.
3623 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3624 lsp::PublishDiagnosticsParams {
3625 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3626 version: None,
3627 diagnostics: vec![],
3628 },
3629 );
3630 deterministic.run_until_parked();
3631 project_a.read_with(cx_a, |project, cx| {
3632 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3633 });
3634 project_b.read_with(cx_b, |project, cx| {
3635 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3636 });
3637 project_c.read_with(cx_c, |project, cx| {
3638 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3639 });
3640}
3641
3642#[gpui::test(iterations = 10)]
3643async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
3644 deterministic: Arc<Deterministic>,
3645 cx_a: &mut TestAppContext,
3646 cx_b: &mut TestAppContext,
3647) {
3648 deterministic.forbid_parking();
3649 let mut server = TestServer::start(&deterministic).await;
3650 let client_a = server.create_client(cx_a, "user_a").await;
3651 let client_b = server.create_client(cx_b, "user_b").await;
3652 server
3653 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3654 .await;
3655
3656 // Set up a fake language server.
3657 let mut language = Language::new(
3658 LanguageConfig {
3659 name: "Rust".into(),
3660 path_suffixes: vec!["rs".to_string()],
3661 ..Default::default()
3662 },
3663 Some(tree_sitter_rust::language()),
3664 );
3665 let mut fake_language_servers = language
3666 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3667 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
3668 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
3669 ..Default::default()
3670 }))
3671 .await;
3672 client_a.language_registry.add(Arc::new(language));
3673
3674 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
3675 client_a
3676 .fs
3677 .insert_tree(
3678 "/test",
3679 json!({
3680 "one.rs": "const ONE: usize = 1;",
3681 "two.rs": "const TWO: usize = 2;",
3682 "three.rs": "const THREE: usize = 3;",
3683 "four.rs": "const FOUR: usize = 3;",
3684 "five.rs": "const FIVE: usize = 3;",
3685 }),
3686 )
3687 .await;
3688
3689 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
3690
3691 // Share a project as client A
3692 let active_call_a = cx_a.read(ActiveCall::global);
3693 let project_id = active_call_a
3694 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3695 .await
3696 .unwrap();
3697
3698 // Join the project as client B and open all three files.
3699 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3700 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
3701 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
3702 }))
3703 .await
3704 .unwrap();
3705
3706 // Simulate a language server reporting errors for a file.
3707 let fake_language_server = fake_language_servers.next().await.unwrap();
3708 fake_language_server
3709 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
3710 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3711 })
3712 .await
3713 .unwrap();
3714 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3715 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3716 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
3717 lsp::WorkDoneProgressBegin {
3718 title: "Progress Began".into(),
3719 ..Default::default()
3720 },
3721 )),
3722 });
3723 for file_name in file_names {
3724 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3725 lsp::PublishDiagnosticsParams {
3726 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
3727 version: None,
3728 diagnostics: vec![lsp::Diagnostic {
3729 severity: Some(lsp::DiagnosticSeverity::WARNING),
3730 source: Some("the-disk-based-diagnostics-source".into()),
3731 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3732 message: "message one".to_string(),
3733 ..Default::default()
3734 }],
3735 },
3736 );
3737 }
3738 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3739 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3740 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
3741 lsp::WorkDoneProgressEnd { message: None },
3742 )),
3743 });
3744
3745 // When the "disk base diagnostics finished" message is received, the buffers'
3746 // diagnostics are expected to be present.
3747 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
3748 project_b.update(cx_b, {
3749 let project_b = project_b.clone();
3750 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
3751 move |_, cx| {
3752 cx.subscribe(&project_b, move |_, _, event, cx| {
3753 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3754 disk_based_diagnostics_finished.store(true, SeqCst);
3755 for buffer in &guest_buffers {
3756 assert_eq!(
3757 buffer
3758 .read(cx)
3759 .snapshot()
3760 .diagnostics_in_range::<_, usize>(0..5, false)
3761 .count(),
3762 1,
3763 "expected a diagnostic for buffer {:?}",
3764 buffer.read(cx).file().unwrap().path(),
3765 );
3766 }
3767 }
3768 })
3769 .detach();
3770 }
3771 });
3772
3773 deterministic.run_until_parked();
3774 assert!(disk_based_diagnostics_finished.load(SeqCst));
3775}
3776
3777#[gpui::test(iterations = 10)]
3778async fn test_collaborating_with_completion(
3779 deterministic: Arc<Deterministic>,
3780 cx_a: &mut TestAppContext,
3781 cx_b: &mut TestAppContext,
3782) {
3783 deterministic.forbid_parking();
3784 let mut server = TestServer::start(&deterministic).await;
3785 let client_a = server.create_client(cx_a, "user_a").await;
3786 let client_b = server.create_client(cx_b, "user_b").await;
3787 server
3788 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3789 .await;
3790 let active_call_a = cx_a.read(ActiveCall::global);
3791
3792 // Set up a fake language server.
3793 let mut language = Language::new(
3794 LanguageConfig {
3795 name: "Rust".into(),
3796 path_suffixes: vec!["rs".to_string()],
3797 ..Default::default()
3798 },
3799 Some(tree_sitter_rust::language()),
3800 );
3801 let mut fake_language_servers = language
3802 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3803 capabilities: lsp::ServerCapabilities {
3804 completion_provider: Some(lsp::CompletionOptions {
3805 trigger_characters: Some(vec![".".to_string()]),
3806 ..Default::default()
3807 }),
3808 ..Default::default()
3809 },
3810 ..Default::default()
3811 }))
3812 .await;
3813 client_a.language_registry.add(Arc::new(language));
3814
3815 client_a
3816 .fs
3817 .insert_tree(
3818 "/a",
3819 json!({
3820 "main.rs": "fn main() { a }",
3821 "other.rs": "",
3822 }),
3823 )
3824 .await;
3825 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3826 let project_id = active_call_a
3827 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3828 .await
3829 .unwrap();
3830 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3831
3832 // Open a file in an editor as the guest.
3833 let buffer_b = project_b
3834 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3835 .await
3836 .unwrap();
3837 let (_, window_b) = cx_b.add_window(|_| EmptyView);
3838 let editor_b = cx_b.add_view(&window_b, |cx| {
3839 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
3840 });
3841
3842 let fake_language_server = fake_language_servers.next().await.unwrap();
3843 cx_a.foreground().run_until_parked();
3844 buffer_b.read_with(cx_b, |buffer, _| {
3845 assert!(!buffer.completion_triggers().is_empty())
3846 });
3847
3848 // Type a completion trigger character as the guest.
3849 editor_b.update(cx_b, |editor, cx| {
3850 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
3851 editor.handle_input(".", cx);
3852 cx.focus(&editor_b);
3853 });
3854
3855 // Receive a completion request as the host's language server.
3856 // Return some completions from the host's language server.
3857 cx_a.foreground().start_waiting();
3858 fake_language_server
3859 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
3860 assert_eq!(
3861 params.text_document_position.text_document.uri,
3862 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3863 );
3864 assert_eq!(
3865 params.text_document_position.position,
3866 lsp::Position::new(0, 14),
3867 );
3868
3869 Ok(Some(lsp::CompletionResponse::Array(vec![
3870 lsp::CompletionItem {
3871 label: "first_method(…)".into(),
3872 detail: Some("fn(&mut self, B) -> C".into()),
3873 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3874 new_text: "first_method($1)".to_string(),
3875 range: lsp::Range::new(
3876 lsp::Position::new(0, 14),
3877 lsp::Position::new(0, 14),
3878 ),
3879 })),
3880 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3881 ..Default::default()
3882 },
3883 lsp::CompletionItem {
3884 label: "second_method(…)".into(),
3885 detail: Some("fn(&mut self, C) -> D<E>".into()),
3886 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3887 new_text: "second_method()".to_string(),
3888 range: lsp::Range::new(
3889 lsp::Position::new(0, 14),
3890 lsp::Position::new(0, 14),
3891 ),
3892 })),
3893 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3894 ..Default::default()
3895 },
3896 ])))
3897 })
3898 .next()
3899 .await
3900 .unwrap();
3901 cx_a.foreground().finish_waiting();
3902
3903 // Open the buffer on the host.
3904 let buffer_a = project_a
3905 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3906 .await
3907 .unwrap();
3908 cx_a.foreground().run_until_parked();
3909 buffer_a.read_with(cx_a, |buffer, _| {
3910 assert_eq!(buffer.text(), "fn main() { a. }")
3911 });
3912
3913 // Confirm a completion on the guest.
3914 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
3915 editor_b.update(cx_b, |editor, cx| {
3916 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
3917 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
3918 });
3919
3920 // Return a resolved completion from the host's language server.
3921 // The resolved completion has an additional text edit.
3922 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
3923 |params, _| async move {
3924 assert_eq!(params.label, "first_method(…)");
3925 Ok(lsp::CompletionItem {
3926 label: "first_method(…)".into(),
3927 detail: Some("fn(&mut self, B) -> C".into()),
3928 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3929 new_text: "first_method($1)".to_string(),
3930 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
3931 })),
3932 additional_text_edits: Some(vec![lsp::TextEdit {
3933 new_text: "use d::SomeTrait;\n".to_string(),
3934 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3935 }]),
3936 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3937 ..Default::default()
3938 })
3939 },
3940 );
3941
3942 // The additional edit is applied.
3943 cx_a.foreground().run_until_parked();
3944 buffer_a.read_with(cx_a, |buffer, _| {
3945 assert_eq!(
3946 buffer.text(),
3947 "use d::SomeTrait;\nfn main() { a.first_method() }"
3948 );
3949 });
3950 buffer_b.read_with(cx_b, |buffer, _| {
3951 assert_eq!(
3952 buffer.text(),
3953 "use d::SomeTrait;\nfn main() { a.first_method() }"
3954 );
3955 });
3956}
3957
3958#[gpui::test(iterations = 10)]
3959async fn test_reloading_buffer_manually(
3960 deterministic: Arc<Deterministic>,
3961 cx_a: &mut TestAppContext,
3962 cx_b: &mut TestAppContext,
3963) {
3964 deterministic.forbid_parking();
3965 let mut server = TestServer::start(&deterministic).await;
3966 let client_a = server.create_client(cx_a, "user_a").await;
3967 let client_b = server.create_client(cx_b, "user_b").await;
3968 server
3969 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3970 .await;
3971 let active_call_a = cx_a.read(ActiveCall::global);
3972
3973 client_a
3974 .fs
3975 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
3976 .await;
3977 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3978 let buffer_a = project_a
3979 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
3980 .await
3981 .unwrap();
3982 let project_id = active_call_a
3983 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3984 .await
3985 .unwrap();
3986
3987 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3988
3989 let buffer_b = cx_b
3990 .background()
3991 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3992 .await
3993 .unwrap();
3994 buffer_b.update(cx_b, |buffer, cx| {
3995 buffer.edit([(4..7, "six")], None, cx);
3996 buffer.edit([(10..11, "6")], None, cx);
3997 assert_eq!(buffer.text(), "let six = 6;");
3998 assert!(buffer.is_dirty());
3999 assert!(!buffer.has_conflict());
4000 });
4001 cx_a.foreground().run_until_parked();
4002 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4003
4004 client_a
4005 .fs
4006 .save(
4007 "/a/a.rs".as_ref(),
4008 &Rope::from("let seven = 7;"),
4009 LineEnding::Unix,
4010 )
4011 .await
4012 .unwrap();
4013 cx_a.foreground().run_until_parked();
4014 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4015 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4016
4017 project_b
4018 .update(cx_b, |project, cx| {
4019 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4020 })
4021 .await
4022 .unwrap();
4023 buffer_a.read_with(cx_a, |buffer, _| {
4024 assert_eq!(buffer.text(), "let seven = 7;");
4025 assert!(!buffer.is_dirty());
4026 assert!(!buffer.has_conflict());
4027 });
4028 buffer_b.read_with(cx_b, |buffer, _| {
4029 assert_eq!(buffer.text(), "let seven = 7;");
4030 assert!(!buffer.is_dirty());
4031 assert!(!buffer.has_conflict());
4032 });
4033
4034 buffer_a.update(cx_a, |buffer, cx| {
4035 // Undoing on the host is a no-op when the reload was initiated by the guest.
4036 buffer.undo(cx);
4037 assert_eq!(buffer.text(), "let seven = 7;");
4038 assert!(!buffer.is_dirty());
4039 assert!(!buffer.has_conflict());
4040 });
4041 buffer_b.update(cx_b, |buffer, cx| {
4042 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4043 buffer.undo(cx);
4044 assert_eq!(buffer.text(), "let six = 6;");
4045 assert!(buffer.is_dirty());
4046 assert!(!buffer.has_conflict());
4047 });
4048}
4049
4050#[gpui::test(iterations = 10)]
4051async fn test_formatting_buffer(
4052 deterministic: Arc<Deterministic>,
4053 cx_a: &mut TestAppContext,
4054 cx_b: &mut TestAppContext,
4055) {
4056 use project::FormatTrigger;
4057
4058 let mut server = TestServer::start(&deterministic).await;
4059 let client_a = server.create_client(cx_a, "user_a").await;
4060 let client_b = server.create_client(cx_b, "user_b").await;
4061 server
4062 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4063 .await;
4064 let active_call_a = cx_a.read(ActiveCall::global);
4065
4066 // Set up a fake language server.
4067 let mut language = Language::new(
4068 LanguageConfig {
4069 name: "Rust".into(),
4070 path_suffixes: vec!["rs".to_string()],
4071 ..Default::default()
4072 },
4073 Some(tree_sitter_rust::language()),
4074 );
4075 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4076 client_a.language_registry.add(Arc::new(language));
4077
4078 // Here we insert a fake tree with a directory that exists on disk. This is needed
4079 // because later we'll invoke a command, which requires passing a working directory
4080 // that points to a valid location on disk.
4081 let directory = env::current_dir().unwrap();
4082 client_a
4083 .fs
4084 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4085 .await;
4086 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4087 let project_id = active_call_a
4088 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4089 .await
4090 .unwrap();
4091 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4092
4093 let buffer_b = cx_b
4094 .background()
4095 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4096 .await
4097 .unwrap();
4098
4099 let fake_language_server = fake_language_servers.next().await.unwrap();
4100 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4101 Ok(Some(vec![
4102 lsp::TextEdit {
4103 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4104 new_text: "h".to_string(),
4105 },
4106 lsp::TextEdit {
4107 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4108 new_text: "y".to_string(),
4109 },
4110 ]))
4111 });
4112
4113 project_b
4114 .update(cx_b, |project, cx| {
4115 project.format(
4116 HashSet::from_iter([buffer_b.clone()]),
4117 true,
4118 FormatTrigger::Save,
4119 cx,
4120 )
4121 })
4122 .await
4123 .unwrap();
4124
4125 // The edits from the LSP are applied, and a final newline is added.
4126 assert_eq!(
4127 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4128 "let honey = \"two\"\n"
4129 );
4130
4131 // Ensure buffer can be formatted using an external command. Notice how the
4132 // host's configuration is honored as opposed to using the guest's settings.
4133 cx_a.update(|cx| {
4134 cx.update_global(|settings: &mut Settings, _| {
4135 settings.editor_defaults.formatter = Some(Formatter::External {
4136 command: "awk".to_string(),
4137 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
4138 });
4139 });
4140 });
4141 project_b
4142 .update(cx_b, |project, cx| {
4143 project.format(
4144 HashSet::from_iter([buffer_b.clone()]),
4145 true,
4146 FormatTrigger::Save,
4147 cx,
4148 )
4149 })
4150 .await
4151 .unwrap();
4152 assert_eq!(
4153 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4154 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4155 );
4156}
4157
4158#[gpui::test(iterations = 10)]
4159async fn test_definition(
4160 deterministic: Arc<Deterministic>,
4161 cx_a: &mut TestAppContext,
4162 cx_b: &mut TestAppContext,
4163) {
4164 deterministic.forbid_parking();
4165 let mut server = TestServer::start(&deterministic).await;
4166 let client_a = server.create_client(cx_a, "user_a").await;
4167 let client_b = server.create_client(cx_b, "user_b").await;
4168 server
4169 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4170 .await;
4171 let active_call_a = cx_a.read(ActiveCall::global);
4172
4173 // Set up a fake language server.
4174 let mut language = Language::new(
4175 LanguageConfig {
4176 name: "Rust".into(),
4177 path_suffixes: vec!["rs".to_string()],
4178 ..Default::default()
4179 },
4180 Some(tree_sitter_rust::language()),
4181 );
4182 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4183 client_a.language_registry.add(Arc::new(language));
4184
4185 client_a
4186 .fs
4187 .insert_tree(
4188 "/root",
4189 json!({
4190 "dir-1": {
4191 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4192 },
4193 "dir-2": {
4194 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4195 "c.rs": "type T2 = usize;",
4196 }
4197 }),
4198 )
4199 .await;
4200 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4201 let project_id = active_call_a
4202 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4203 .await
4204 .unwrap();
4205 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4206
4207 // Open the file on client B.
4208 let buffer_b = cx_b
4209 .background()
4210 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4211 .await
4212 .unwrap();
4213
4214 // Request the definition of a symbol as the guest.
4215 let fake_language_server = fake_language_servers.next().await.unwrap();
4216 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4217 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4218 lsp::Location::new(
4219 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4220 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4221 ),
4222 )))
4223 });
4224
4225 let definitions_1 = project_b
4226 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4227 .await
4228 .unwrap();
4229 cx_b.read(|cx| {
4230 assert_eq!(definitions_1.len(), 1);
4231 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4232 let target_buffer = definitions_1[0].target.buffer.read(cx);
4233 assert_eq!(
4234 target_buffer.text(),
4235 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4236 );
4237 assert_eq!(
4238 definitions_1[0].target.range.to_point(target_buffer),
4239 Point::new(0, 6)..Point::new(0, 9)
4240 );
4241 });
4242
4243 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4244 // the previous call to `definition`.
4245 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4246 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4247 lsp::Location::new(
4248 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4249 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4250 ),
4251 )))
4252 });
4253
4254 let definitions_2 = project_b
4255 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4256 .await
4257 .unwrap();
4258 cx_b.read(|cx| {
4259 assert_eq!(definitions_2.len(), 1);
4260 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4261 let target_buffer = definitions_2[0].target.buffer.read(cx);
4262 assert_eq!(
4263 target_buffer.text(),
4264 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4265 );
4266 assert_eq!(
4267 definitions_2[0].target.range.to_point(target_buffer),
4268 Point::new(1, 6)..Point::new(1, 11)
4269 );
4270 });
4271 assert_eq!(
4272 definitions_1[0].target.buffer,
4273 definitions_2[0].target.buffer
4274 );
4275
4276 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4277 |req, _| async move {
4278 assert_eq!(
4279 req.text_document_position_params.position,
4280 lsp::Position::new(0, 7)
4281 );
4282 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4283 lsp::Location::new(
4284 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4285 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4286 ),
4287 )))
4288 },
4289 );
4290
4291 let type_definitions = project_b
4292 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4293 .await
4294 .unwrap();
4295 cx_b.read(|cx| {
4296 assert_eq!(type_definitions.len(), 1);
4297 let target_buffer = type_definitions[0].target.buffer.read(cx);
4298 assert_eq!(target_buffer.text(), "type T2 = usize;");
4299 assert_eq!(
4300 type_definitions[0].target.range.to_point(target_buffer),
4301 Point::new(0, 5)..Point::new(0, 7)
4302 );
4303 });
4304}
4305
4306#[gpui::test(iterations = 10)]
4307async fn test_references(
4308 deterministic: Arc<Deterministic>,
4309 cx_a: &mut TestAppContext,
4310 cx_b: &mut TestAppContext,
4311) {
4312 deterministic.forbid_parking();
4313 let mut server = TestServer::start(&deterministic).await;
4314 let client_a = server.create_client(cx_a, "user_a").await;
4315 let client_b = server.create_client(cx_b, "user_b").await;
4316 server
4317 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4318 .await;
4319 let active_call_a = cx_a.read(ActiveCall::global);
4320
4321 // Set up a fake language server.
4322 let mut language = Language::new(
4323 LanguageConfig {
4324 name: "Rust".into(),
4325 path_suffixes: vec!["rs".to_string()],
4326 ..Default::default()
4327 },
4328 Some(tree_sitter_rust::language()),
4329 );
4330 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4331 client_a.language_registry.add(Arc::new(language));
4332
4333 client_a
4334 .fs
4335 .insert_tree(
4336 "/root",
4337 json!({
4338 "dir-1": {
4339 "one.rs": "const ONE: usize = 1;",
4340 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4341 },
4342 "dir-2": {
4343 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4344 }
4345 }),
4346 )
4347 .await;
4348 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4349 let project_id = active_call_a
4350 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4351 .await
4352 .unwrap();
4353 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4354
4355 // Open the file on client B.
4356 let buffer_b = cx_b
4357 .background()
4358 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4359 .await
4360 .unwrap();
4361
4362 // Request references to a symbol as the guest.
4363 let fake_language_server = fake_language_servers.next().await.unwrap();
4364 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4365 assert_eq!(
4366 params.text_document_position.text_document.uri.as_str(),
4367 "file:///root/dir-1/one.rs"
4368 );
4369 Ok(Some(vec![
4370 lsp::Location {
4371 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4372 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4373 },
4374 lsp::Location {
4375 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4376 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4377 },
4378 lsp::Location {
4379 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4380 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4381 },
4382 ]))
4383 });
4384
4385 let references = project_b
4386 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4387 .await
4388 .unwrap();
4389 cx_b.read(|cx| {
4390 assert_eq!(references.len(), 3);
4391 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4392
4393 let two_buffer = references[0].buffer.read(cx);
4394 let three_buffer = references[2].buffer.read(cx);
4395 assert_eq!(
4396 two_buffer.file().unwrap().path().as_ref(),
4397 Path::new("two.rs")
4398 );
4399 assert_eq!(references[1].buffer, references[0].buffer);
4400 assert_eq!(
4401 three_buffer.file().unwrap().full_path(cx),
4402 Path::new("/root/dir-2/three.rs")
4403 );
4404
4405 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4406 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4407 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4408 });
4409}
4410
4411#[gpui::test(iterations = 10)]
4412async fn test_project_search(
4413 deterministic: Arc<Deterministic>,
4414 cx_a: &mut TestAppContext,
4415 cx_b: &mut TestAppContext,
4416) {
4417 deterministic.forbid_parking();
4418 let mut server = TestServer::start(&deterministic).await;
4419 let client_a = server.create_client(cx_a, "user_a").await;
4420 let client_b = server.create_client(cx_b, "user_b").await;
4421 server
4422 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4423 .await;
4424 let active_call_a = cx_a.read(ActiveCall::global);
4425
4426 client_a
4427 .fs
4428 .insert_tree(
4429 "/root",
4430 json!({
4431 "dir-1": {
4432 "a": "hello world",
4433 "b": "goodnight moon",
4434 "c": "a world of goo",
4435 "d": "world champion of clown world",
4436 },
4437 "dir-2": {
4438 "e": "disney world is fun",
4439 }
4440 }),
4441 )
4442 .await;
4443 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4444 let (worktree_2, _) = project_a
4445 .update(cx_a, |p, cx| {
4446 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4447 })
4448 .await
4449 .unwrap();
4450 worktree_2
4451 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4452 .await;
4453 let project_id = active_call_a
4454 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4455 .await
4456 .unwrap();
4457
4458 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4459
4460 // Perform a search as the guest.
4461 let results = project_b
4462 .update(cx_b, |project, cx| {
4463 project.search(SearchQuery::text("world", false, false), cx)
4464 })
4465 .await
4466 .unwrap();
4467
4468 let mut ranges_by_path = results
4469 .into_iter()
4470 .map(|(buffer, ranges)| {
4471 buffer.read_with(cx_b, |buffer, cx| {
4472 let path = buffer.file().unwrap().full_path(cx);
4473 let offset_ranges = ranges
4474 .into_iter()
4475 .map(|range| range.to_offset(buffer))
4476 .collect::<Vec<_>>();
4477 (path, offset_ranges)
4478 })
4479 })
4480 .collect::<Vec<_>>();
4481 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4482
4483 assert_eq!(
4484 ranges_by_path,
4485 &[
4486 (PathBuf::from("dir-1/a"), vec![6..11]),
4487 (PathBuf::from("dir-1/c"), vec![2..7]),
4488 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4489 (PathBuf::from("dir-2/e"), vec![7..12]),
4490 ]
4491 );
4492}
4493
4494#[gpui::test(iterations = 10)]
4495async fn test_document_highlights(
4496 deterministic: Arc<Deterministic>,
4497 cx_a: &mut TestAppContext,
4498 cx_b: &mut TestAppContext,
4499) {
4500 deterministic.forbid_parking();
4501 let mut server = TestServer::start(&deterministic).await;
4502 let client_a = server.create_client(cx_a, "user_a").await;
4503 let client_b = server.create_client(cx_b, "user_b").await;
4504 server
4505 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4506 .await;
4507 let active_call_a = cx_a.read(ActiveCall::global);
4508
4509 client_a
4510 .fs
4511 .insert_tree(
4512 "/root-1",
4513 json!({
4514 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4515 }),
4516 )
4517 .await;
4518
4519 // Set up a fake language server.
4520 let mut language = Language::new(
4521 LanguageConfig {
4522 name: "Rust".into(),
4523 path_suffixes: vec!["rs".to_string()],
4524 ..Default::default()
4525 },
4526 Some(tree_sitter_rust::language()),
4527 );
4528 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4529 client_a.language_registry.add(Arc::new(language));
4530
4531 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4532 let project_id = active_call_a
4533 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4534 .await
4535 .unwrap();
4536 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4537
4538 // Open the file on client B.
4539 let buffer_b = cx_b
4540 .background()
4541 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4542 .await
4543 .unwrap();
4544
4545 // Request document highlights as the guest.
4546 let fake_language_server = fake_language_servers.next().await.unwrap();
4547 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4548 |params, _| async move {
4549 assert_eq!(
4550 params
4551 .text_document_position_params
4552 .text_document
4553 .uri
4554 .as_str(),
4555 "file:///root-1/main.rs"
4556 );
4557 assert_eq!(
4558 params.text_document_position_params.position,
4559 lsp::Position::new(0, 34)
4560 );
4561 Ok(Some(vec![
4562 lsp::DocumentHighlight {
4563 kind: Some(lsp::DocumentHighlightKind::WRITE),
4564 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4565 },
4566 lsp::DocumentHighlight {
4567 kind: Some(lsp::DocumentHighlightKind::READ),
4568 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4569 },
4570 lsp::DocumentHighlight {
4571 kind: Some(lsp::DocumentHighlightKind::READ),
4572 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4573 },
4574 ]))
4575 },
4576 );
4577
4578 let highlights = project_b
4579 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4580 .await
4581 .unwrap();
4582 buffer_b.read_with(cx_b, |buffer, _| {
4583 let snapshot = buffer.snapshot();
4584
4585 let highlights = highlights
4586 .into_iter()
4587 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4588 .collect::<Vec<_>>();
4589 assert_eq!(
4590 highlights,
4591 &[
4592 (lsp::DocumentHighlightKind::WRITE, 10..16),
4593 (lsp::DocumentHighlightKind::READ, 32..38),
4594 (lsp::DocumentHighlightKind::READ, 41..47)
4595 ]
4596 )
4597 });
4598}
4599
4600#[gpui::test(iterations = 10)]
4601async fn test_lsp_hover(
4602 deterministic: Arc<Deterministic>,
4603 cx_a: &mut TestAppContext,
4604 cx_b: &mut TestAppContext,
4605) {
4606 deterministic.forbid_parking();
4607 let mut server = TestServer::start(&deterministic).await;
4608 let client_a = server.create_client(cx_a, "user_a").await;
4609 let client_b = server.create_client(cx_b, "user_b").await;
4610 server
4611 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4612 .await;
4613 let active_call_a = cx_a.read(ActiveCall::global);
4614
4615 client_a
4616 .fs
4617 .insert_tree(
4618 "/root-1",
4619 json!({
4620 "main.rs": "use std::collections::HashMap;",
4621 }),
4622 )
4623 .await;
4624
4625 // Set up a fake language server.
4626 let mut language = Language::new(
4627 LanguageConfig {
4628 name: "Rust".into(),
4629 path_suffixes: vec!["rs".to_string()],
4630 ..Default::default()
4631 },
4632 Some(tree_sitter_rust::language()),
4633 );
4634 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4635 client_a.language_registry.add(Arc::new(language));
4636
4637 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4638 let project_id = active_call_a
4639 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4640 .await
4641 .unwrap();
4642 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4643
4644 // Open the file as the guest
4645 let buffer_b = cx_b
4646 .background()
4647 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4648 .await
4649 .unwrap();
4650
4651 // Request hover information as the guest.
4652 let fake_language_server = fake_language_servers.next().await.unwrap();
4653 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4654 |params, _| async move {
4655 assert_eq!(
4656 params
4657 .text_document_position_params
4658 .text_document
4659 .uri
4660 .as_str(),
4661 "file:///root-1/main.rs"
4662 );
4663 assert_eq!(
4664 params.text_document_position_params.position,
4665 lsp::Position::new(0, 22)
4666 );
4667 Ok(Some(lsp::Hover {
4668 contents: lsp::HoverContents::Array(vec![
4669 lsp::MarkedString::String("Test hover content.".to_string()),
4670 lsp::MarkedString::LanguageString(lsp::LanguageString {
4671 language: "Rust".to_string(),
4672 value: "let foo = 42;".to_string(),
4673 }),
4674 ]),
4675 range: Some(lsp::Range::new(
4676 lsp::Position::new(0, 22),
4677 lsp::Position::new(0, 29),
4678 )),
4679 }))
4680 },
4681 );
4682
4683 let hover_info = project_b
4684 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
4685 .await
4686 .unwrap()
4687 .unwrap();
4688 buffer_b.read_with(cx_b, |buffer, _| {
4689 let snapshot = buffer.snapshot();
4690 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
4691 assert_eq!(
4692 hover_info.contents,
4693 vec![
4694 project::HoverBlock {
4695 text: "Test hover content.".to_string(),
4696 kind: HoverBlockKind::Markdown,
4697 },
4698 project::HoverBlock {
4699 text: "let foo = 42;".to_string(),
4700 kind: HoverBlockKind::Code {
4701 language: "Rust".to_string()
4702 },
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 .unwrap();
6371 deterministic.run_until_parked();
6372 workspace_b.read_with(cx_b, |workspace, cx| {
6373 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6374 });
6375
6376 workspace_a
6377 .update(cx_a, |workspace, cx| {
6378 workspace::Pane::go_back(workspace, None, cx)
6379 })
6380 .await
6381 .unwrap();
6382 deterministic.run_until_parked();
6383 workspace_b.read_with(cx_b, |workspace, cx| {
6384 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
6385 });
6386
6387 workspace_a
6388 .update(cx_a, |workspace, cx| {
6389 workspace::Pane::go_forward(workspace, None, cx)
6390 })
6391 .await
6392 .unwrap();
6393 deterministic.run_until_parked();
6394 workspace_b.read_with(cx_b, |workspace, cx| {
6395 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6396 });
6397
6398 // Changes to client A's editor are reflected on client B.
6399 editor_a1.update(cx_a, |editor, cx| {
6400 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
6401 });
6402 deterministic.run_until_parked();
6403 editor_b1.read_with(cx_b, |editor, cx| {
6404 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
6405 });
6406
6407 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
6408 deterministic.run_until_parked();
6409 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
6410
6411 editor_a1.update(cx_a, |editor, cx| {
6412 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
6413 editor.set_scroll_position(vec2f(0., 100.), cx);
6414 });
6415 deterministic.run_until_parked();
6416 editor_b1.read_with(cx_b, |editor, cx| {
6417 assert_eq!(editor.selections.ranges(cx), &[3..3]);
6418 });
6419
6420 // After unfollowing, client B stops receiving updates from client A.
6421 workspace_b.update(cx_b, |workspace, cx| {
6422 workspace.unfollow(&workspace.active_pane().clone(), cx)
6423 });
6424 workspace_a.update(cx_a, |workspace, cx| {
6425 workspace.activate_item(&editor_a2, cx)
6426 });
6427 deterministic.run_until_parked();
6428 assert_eq!(
6429 workspace_b.read_with(cx_b, |workspace, cx| workspace
6430 .active_item(cx)
6431 .unwrap()
6432 .id()),
6433 editor_b1.id()
6434 );
6435
6436 // Client A starts following client B.
6437 workspace_a
6438 .update(cx_a, |workspace, cx| {
6439 workspace
6440 .toggle_follow(&ToggleFollow(peer_id_b), cx)
6441 .unwrap()
6442 })
6443 .await
6444 .unwrap();
6445 assert_eq!(
6446 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6447 Some(peer_id_b)
6448 );
6449 assert_eq!(
6450 workspace_a.read_with(cx_a, |workspace, cx| workspace
6451 .active_item(cx)
6452 .unwrap()
6453 .id()),
6454 editor_a1.id()
6455 );
6456
6457 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
6458 let display = MacOSDisplay::new();
6459 active_call_b
6460 .update(cx_b, |call, cx| call.set_location(None, cx))
6461 .await
6462 .unwrap();
6463 active_call_b
6464 .update(cx_b, |call, cx| {
6465 call.room().unwrap().update(cx, |room, cx| {
6466 room.set_display_sources(vec![display.clone()]);
6467 room.share_screen(cx)
6468 })
6469 })
6470 .await
6471 .unwrap();
6472 deterministic.run_until_parked();
6473 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
6474 workspace
6475 .active_item(cx)
6476 .unwrap()
6477 .downcast::<SharedScreen>()
6478 .unwrap()
6479 });
6480
6481 // Client B activates Zed again, which causes the previous editor to become focused again.
6482 active_call_b
6483 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6484 .await
6485 .unwrap();
6486 deterministic.run_until_parked();
6487 workspace_a.read_with(cx_a, |workspace, cx| {
6488 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
6489 });
6490
6491 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
6492 workspace_b.update(cx_b, |workspace, cx| {
6493 workspace.activate_item(&multibuffer_editor_b, cx)
6494 });
6495 deterministic.run_until_parked();
6496 workspace_a.read_with(cx_a, |workspace, cx| {
6497 assert_eq!(
6498 workspace.active_item(cx).unwrap().id(),
6499 multibuffer_editor_a.id()
6500 )
6501 });
6502
6503 // Client B activates an external window again, and the previously-opened screen-sharing item
6504 // gets activated.
6505 active_call_b
6506 .update(cx_b, |call, cx| call.set_location(None, cx))
6507 .await
6508 .unwrap();
6509 deterministic.run_until_parked();
6510 assert_eq!(
6511 workspace_a.read_with(cx_a, |workspace, cx| workspace
6512 .active_item(cx)
6513 .unwrap()
6514 .id()),
6515 shared_screen.id()
6516 );
6517
6518 // Following interrupts when client B disconnects.
6519 client_b.disconnect(&cx_b.to_async());
6520 deterministic.advance_clock(RECONNECT_TIMEOUT);
6521 assert_eq!(
6522 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6523 None
6524 );
6525}
6526
6527#[gpui::test(iterations = 10)]
6528async fn test_join_call_after_screen_was_shared(
6529 deterministic: Arc<Deterministic>,
6530 cx_a: &mut TestAppContext,
6531 cx_b: &mut TestAppContext,
6532) {
6533 deterministic.forbid_parking();
6534 let mut server = TestServer::start(&deterministic).await;
6535
6536 let client_a = server.create_client(cx_a, "user_a").await;
6537 let client_b = server.create_client(cx_b, "user_b").await;
6538 server
6539 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6540 .await;
6541
6542 let active_call_a = cx_a.read(ActiveCall::global);
6543 let active_call_b = cx_b.read(ActiveCall::global);
6544
6545 // Call users B and C from client A.
6546 active_call_a
6547 .update(cx_a, |call, cx| {
6548 call.invite(client_b.user_id().unwrap(), None, cx)
6549 })
6550 .await
6551 .unwrap();
6552 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6553 deterministic.run_until_parked();
6554 assert_eq!(
6555 room_participants(&room_a, cx_a),
6556 RoomParticipants {
6557 remote: Default::default(),
6558 pending: vec!["user_b".to_string()]
6559 }
6560 );
6561
6562 // User B receives the call.
6563 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6564 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6565 assert_eq!(call_b.calling_user.github_login, "user_a");
6566
6567 // User A shares their screen
6568 let display = MacOSDisplay::new();
6569 active_call_a
6570 .update(cx_a, |call, cx| {
6571 call.room().unwrap().update(cx, |room, cx| {
6572 room.set_display_sources(vec![display.clone()]);
6573 room.share_screen(cx)
6574 })
6575 })
6576 .await
6577 .unwrap();
6578
6579 client_b.user_store.update(cx_b, |user_store, _| {
6580 user_store.clear_cache();
6581 });
6582
6583 // User B joins the room
6584 active_call_b
6585 .update(cx_b, |call, cx| call.accept_incoming(cx))
6586 .await
6587 .unwrap();
6588 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6589 assert!(incoming_call_b.next().await.unwrap().is_none());
6590
6591 deterministic.run_until_parked();
6592 assert_eq!(
6593 room_participants(&room_a, cx_a),
6594 RoomParticipants {
6595 remote: vec!["user_b".to_string()],
6596 pending: vec![],
6597 }
6598 );
6599 assert_eq!(
6600 room_participants(&room_b, cx_b),
6601 RoomParticipants {
6602 remote: vec!["user_a".to_string()],
6603 pending: vec![],
6604 }
6605 );
6606
6607 // Ensure User B sees User A's screenshare.
6608 room_b.read_with(cx_b, |room, _| {
6609 assert_eq!(
6610 room.remote_participants()
6611 .get(&client_a.user_id().unwrap())
6612 .unwrap()
6613 .tracks
6614 .len(),
6615 1
6616 );
6617 });
6618}
6619
6620#[gpui::test]
6621async fn test_following_tab_order(
6622 deterministic: Arc<Deterministic>,
6623 cx_a: &mut TestAppContext,
6624 cx_b: &mut TestAppContext,
6625) {
6626 cx_a.update(editor::init);
6627 cx_b.update(editor::init);
6628
6629 let mut server = TestServer::start(&deterministic).await;
6630 let client_a = server.create_client(cx_a, "user_a").await;
6631 let client_b = server.create_client(cx_b, "user_b").await;
6632 server
6633 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6634 .await;
6635 let active_call_a = cx_a.read(ActiveCall::global);
6636 let active_call_b = cx_b.read(ActiveCall::global);
6637
6638 client_a
6639 .fs
6640 .insert_tree(
6641 "/a",
6642 json!({
6643 "1.txt": "one",
6644 "2.txt": "two",
6645 "3.txt": "three",
6646 }),
6647 )
6648 .await;
6649 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6650 active_call_a
6651 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6652 .await
6653 .unwrap();
6654
6655 let project_id = active_call_a
6656 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6657 .await
6658 .unwrap();
6659 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6660 active_call_b
6661 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6662 .await
6663 .unwrap();
6664
6665 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6666 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6667
6668 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6669 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6670
6671 let client_b_id = project_a.read_with(cx_a, |project, _| {
6672 project.collaborators().values().next().unwrap().peer_id
6673 });
6674
6675 //Open 1, 3 in that order on client A
6676 workspace_a
6677 .update(cx_a, |workspace, cx| {
6678 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6679 })
6680 .await
6681 .unwrap();
6682 workspace_a
6683 .update(cx_a, |workspace, cx| {
6684 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6685 })
6686 .await
6687 .unwrap();
6688
6689 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
6690 pane.update(cx, |pane, cx| {
6691 pane.items()
6692 .map(|item| {
6693 item.project_path(cx)
6694 .unwrap()
6695 .path
6696 .to_str()
6697 .unwrap()
6698 .to_owned()
6699 })
6700 .collect::<Vec<_>>()
6701 })
6702 };
6703
6704 //Verify that the tabs opened in the order we expect
6705 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
6706
6707 //Follow client B as client A
6708 workspace_a
6709 .update(cx_a, |workspace, cx| {
6710 workspace
6711 .toggle_follow(&ToggleFollow(client_b_id), cx)
6712 .unwrap()
6713 })
6714 .await
6715 .unwrap();
6716
6717 //Open just 2 on client B
6718 workspace_b
6719 .update(cx_b, |workspace, cx| {
6720 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6721 })
6722 .await
6723 .unwrap();
6724 deterministic.run_until_parked();
6725
6726 // Verify that newly opened followed file is at the end
6727 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6728
6729 //Open just 1 on client B
6730 workspace_b
6731 .update(cx_b, |workspace, cx| {
6732 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6733 })
6734 .await
6735 .unwrap();
6736 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
6737 deterministic.run_until_parked();
6738
6739 // Verify that following into 1 did not reorder
6740 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6741}
6742
6743#[gpui::test(iterations = 10)]
6744async fn test_peers_following_each_other(
6745 deterministic: Arc<Deterministic>,
6746 cx_a: &mut TestAppContext,
6747 cx_b: &mut TestAppContext,
6748) {
6749 deterministic.forbid_parking();
6750 cx_a.update(editor::init);
6751 cx_b.update(editor::init);
6752
6753 let mut server = TestServer::start(&deterministic).await;
6754 let client_a = server.create_client(cx_a, "user_a").await;
6755 let client_b = server.create_client(cx_b, "user_b").await;
6756 server
6757 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6758 .await;
6759 let active_call_a = cx_a.read(ActiveCall::global);
6760 let active_call_b = cx_b.read(ActiveCall::global);
6761
6762 // Client A shares a project.
6763 client_a
6764 .fs
6765 .insert_tree(
6766 "/a",
6767 json!({
6768 "1.txt": "one",
6769 "2.txt": "two",
6770 "3.txt": "three",
6771 "4.txt": "four",
6772 }),
6773 )
6774 .await;
6775 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6776 active_call_a
6777 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6778 .await
6779 .unwrap();
6780 let project_id = active_call_a
6781 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6782 .await
6783 .unwrap();
6784
6785 // Client B joins the project.
6786 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6787 active_call_b
6788 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6789 .await
6790 .unwrap();
6791
6792 // Client A opens some editors.
6793 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6794 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6795 let _editor_a1 = workspace_a
6796 .update(cx_a, |workspace, cx| {
6797 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6798 })
6799 .await
6800 .unwrap()
6801 .downcast::<Editor>()
6802 .unwrap();
6803
6804 // Client B opens an editor.
6805 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6806 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6807 let _editor_b1 = workspace_b
6808 .update(cx_b, |workspace, cx| {
6809 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6810 })
6811 .await
6812 .unwrap()
6813 .downcast::<Editor>()
6814 .unwrap();
6815
6816 // Clients A and B follow each other in split panes
6817 workspace_a.update(cx_a, |workspace, cx| {
6818 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6819 let pane_a1 = pane_a1.clone();
6820 cx.defer(move |workspace, _| {
6821 assert_ne!(*workspace.active_pane(), pane_a1);
6822 });
6823 });
6824 workspace_a
6825 .update(cx_a, |workspace, cx| {
6826 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
6827 workspace
6828 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
6829 .unwrap()
6830 })
6831 .await
6832 .unwrap();
6833 workspace_b.update(cx_b, |workspace, cx| {
6834 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6835 let pane_b1 = pane_b1.clone();
6836 cx.defer(move |workspace, _| {
6837 assert_ne!(*workspace.active_pane(), pane_b1);
6838 });
6839 });
6840 workspace_b
6841 .update(cx_b, |workspace, cx| {
6842 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
6843 workspace
6844 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
6845 .unwrap()
6846 })
6847 .await
6848 .unwrap();
6849
6850 workspace_a.update(cx_a, |workspace, cx| {
6851 workspace.activate_next_pane(cx);
6852 });
6853 // Wait for focus effects to be fully flushed
6854 workspace_a.update(cx_a, |workspace, _| {
6855 assert_eq!(*workspace.active_pane(), pane_a1);
6856 });
6857
6858 workspace_a
6859 .update(cx_a, |workspace, cx| {
6860 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6861 })
6862 .await
6863 .unwrap();
6864 workspace_b.update(cx_b, |workspace, cx| {
6865 workspace.activate_next_pane(cx);
6866 });
6867
6868 workspace_b
6869 .update(cx_b, |workspace, cx| {
6870 assert_eq!(*workspace.active_pane(), pane_b1);
6871 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
6872 })
6873 .await
6874 .unwrap();
6875 cx_a.foreground().run_until_parked();
6876
6877 // Ensure leader updates don't change the active pane of followers
6878 workspace_a.read_with(cx_a, |workspace, _| {
6879 assert_eq!(*workspace.active_pane(), pane_a1);
6880 });
6881 workspace_b.read_with(cx_b, |workspace, _| {
6882 assert_eq!(*workspace.active_pane(), pane_b1);
6883 });
6884
6885 // Ensure peers following each other doesn't cause an infinite loop.
6886 assert_eq!(
6887 workspace_a.read_with(cx_a, |workspace, cx| workspace
6888 .active_item(cx)
6889 .unwrap()
6890 .project_path(cx)),
6891 Some((worktree_id, "3.txt").into())
6892 );
6893 workspace_a.update(cx_a, |workspace, cx| {
6894 assert_eq!(
6895 workspace.active_item(cx).unwrap().project_path(cx),
6896 Some((worktree_id, "3.txt").into())
6897 );
6898 workspace.activate_next_pane(cx);
6899 });
6900
6901 workspace_a.update(cx_a, |workspace, cx| {
6902 assert_eq!(
6903 workspace.active_item(cx).unwrap().project_path(cx),
6904 Some((worktree_id, "4.txt").into())
6905 );
6906 });
6907
6908 workspace_b.update(cx_b, |workspace, cx| {
6909 assert_eq!(
6910 workspace.active_item(cx).unwrap().project_path(cx),
6911 Some((worktree_id, "4.txt").into())
6912 );
6913 workspace.activate_next_pane(cx);
6914 });
6915
6916 workspace_b.update(cx_b, |workspace, cx| {
6917 assert_eq!(
6918 workspace.active_item(cx).unwrap().project_path(cx),
6919 Some((worktree_id, "3.txt").into())
6920 );
6921 });
6922}
6923
6924#[gpui::test(iterations = 10)]
6925async fn test_auto_unfollowing(
6926 deterministic: Arc<Deterministic>,
6927 cx_a: &mut TestAppContext,
6928 cx_b: &mut TestAppContext,
6929) {
6930 deterministic.forbid_parking();
6931 cx_a.update(editor::init);
6932 cx_b.update(editor::init);
6933
6934 // 2 clients connect to a server.
6935 let mut server = TestServer::start(&deterministic).await;
6936 let client_a = server.create_client(cx_a, "user_a").await;
6937 let client_b = server.create_client(cx_b, "user_b").await;
6938 server
6939 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6940 .await;
6941 let active_call_a = cx_a.read(ActiveCall::global);
6942 let active_call_b = cx_b.read(ActiveCall::global);
6943
6944 // Client A shares a project.
6945 client_a
6946 .fs
6947 .insert_tree(
6948 "/a",
6949 json!({
6950 "1.txt": "one",
6951 "2.txt": "two",
6952 "3.txt": "three",
6953 }),
6954 )
6955 .await;
6956 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6957 active_call_a
6958 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6959 .await
6960 .unwrap();
6961
6962 let project_id = active_call_a
6963 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6964 .await
6965 .unwrap();
6966 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6967 active_call_b
6968 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6969 .await
6970 .unwrap();
6971
6972 // Client A opens some editors.
6973 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6974 let _editor_a1 = workspace_a
6975 .update(cx_a, |workspace, cx| {
6976 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6977 })
6978 .await
6979 .unwrap()
6980 .downcast::<Editor>()
6981 .unwrap();
6982
6983 // Client B starts following client A.
6984 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6985 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6986 let leader_id = project_b.read_with(cx_b, |project, _| {
6987 project.collaborators().values().next().unwrap().peer_id
6988 });
6989 workspace_b
6990 .update(cx_b, |workspace, cx| {
6991 workspace
6992 .toggle_follow(&ToggleFollow(leader_id), cx)
6993 .unwrap()
6994 })
6995 .await
6996 .unwrap();
6997 assert_eq!(
6998 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6999 Some(leader_id)
7000 );
7001 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
7002 workspace
7003 .active_item(cx)
7004 .unwrap()
7005 .downcast::<Editor>()
7006 .unwrap()
7007 });
7008
7009 // When client B moves, it automatically stops following client A.
7010 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
7011 assert_eq!(
7012 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7013 None
7014 );
7015
7016 workspace_b
7017 .update(cx_b, |workspace, cx| {
7018 workspace
7019 .toggle_follow(&ToggleFollow(leader_id), cx)
7020 .unwrap()
7021 })
7022 .await
7023 .unwrap();
7024 assert_eq!(
7025 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7026 Some(leader_id)
7027 );
7028
7029 // When client B edits, it automatically stops following client A.
7030 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
7031 assert_eq!(
7032 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7033 None
7034 );
7035
7036 workspace_b
7037 .update(cx_b, |workspace, cx| {
7038 workspace
7039 .toggle_follow(&ToggleFollow(leader_id), cx)
7040 .unwrap()
7041 })
7042 .await
7043 .unwrap();
7044 assert_eq!(
7045 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7046 Some(leader_id)
7047 );
7048
7049 // When client B scrolls, it automatically stops following client A.
7050 editor_b2.update(cx_b, |editor, cx| {
7051 editor.set_scroll_position(vec2f(0., 3.), cx)
7052 });
7053 assert_eq!(
7054 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7055 None
7056 );
7057
7058 workspace_b
7059 .update(cx_b, |workspace, cx| {
7060 workspace
7061 .toggle_follow(&ToggleFollow(leader_id), cx)
7062 .unwrap()
7063 })
7064 .await
7065 .unwrap();
7066 assert_eq!(
7067 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7068 Some(leader_id)
7069 );
7070
7071 // When client B activates a different pane, it continues following client A in the original pane.
7072 workspace_b.update(cx_b, |workspace, cx| {
7073 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
7074 });
7075 assert_eq!(
7076 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7077 Some(leader_id)
7078 );
7079
7080 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
7081 assert_eq!(
7082 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7083 Some(leader_id)
7084 );
7085
7086 // When client B activates a different item in the original pane, it automatically stops following client A.
7087 workspace_b
7088 .update(cx_b, |workspace, cx| {
7089 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
7090 })
7091 .await
7092 .unwrap();
7093 assert_eq!(
7094 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7095 None
7096 );
7097}
7098
7099#[gpui::test(iterations = 10)]
7100async fn test_peers_simultaneously_following_each_other(
7101 deterministic: Arc<Deterministic>,
7102 cx_a: &mut TestAppContext,
7103 cx_b: &mut TestAppContext,
7104) {
7105 deterministic.forbid_parking();
7106 cx_a.update(editor::init);
7107 cx_b.update(editor::init);
7108
7109 let mut server = TestServer::start(&deterministic).await;
7110 let client_a = server.create_client(cx_a, "user_a").await;
7111 let client_b = server.create_client(cx_b, "user_b").await;
7112 server
7113 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7114 .await;
7115 let active_call_a = cx_a.read(ActiveCall::global);
7116
7117 client_a.fs.insert_tree("/a", json!({})).await;
7118 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
7119 let workspace_a = client_a.build_workspace(&project_a, cx_a);
7120 let project_id = active_call_a
7121 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7122 .await
7123 .unwrap();
7124
7125 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7126 let workspace_b = client_b.build_workspace(&project_b, cx_b);
7127
7128 deterministic.run_until_parked();
7129 let client_a_id = project_b.read_with(cx_b, |project, _| {
7130 project.collaborators().values().next().unwrap().peer_id
7131 });
7132 let client_b_id = project_a.read_with(cx_a, |project, _| {
7133 project.collaborators().values().next().unwrap().peer_id
7134 });
7135
7136 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
7137 workspace
7138 .toggle_follow(&ToggleFollow(client_b_id), cx)
7139 .unwrap()
7140 });
7141 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
7142 workspace
7143 .toggle_follow(&ToggleFollow(client_a_id), cx)
7144 .unwrap()
7145 });
7146
7147 futures::try_join!(a_follow_b, b_follow_a).unwrap();
7148 workspace_a.read_with(cx_a, |workspace, _| {
7149 assert_eq!(
7150 workspace.leader_for_pane(workspace.active_pane()),
7151 Some(client_b_id)
7152 );
7153 });
7154 workspace_b.read_with(cx_b, |workspace, _| {
7155 assert_eq!(
7156 workspace.leader_for_pane(workspace.active_pane()),
7157 Some(client_a_id)
7158 );
7159 });
7160}
7161
7162#[derive(Debug, Eq, PartialEq)]
7163struct RoomParticipants {
7164 remote: Vec<String>,
7165 pending: Vec<String>,
7166}
7167
7168fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
7169 room.read_with(cx, |room, _| {
7170 let mut remote = room
7171 .remote_participants()
7172 .iter()
7173 .map(|(_, participant)| participant.user.github_login.clone())
7174 .collect::<Vec<_>>();
7175 let mut pending = room
7176 .pending_participants()
7177 .iter()
7178 .map(|user| user.github_login.clone())
7179 .collect::<Vec<_>>();
7180 remote.sort();
7181 pending.sort();
7182 RoomParticipants { remote, pending }
7183 })
7184}