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