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::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 _, LineEnding, 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, 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::{AtomicBool, AtomicU32, 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 let project_id = active_call_a
3150 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3151 .await
3152 .unwrap();
3153
3154 // As client B, join that project and observe the local settings.
3155 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3156 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
3157 deterministic.run_until_parked();
3158 cx_b.read(|cx| {
3159 let store = cx.global::<SettingsStore>();
3160 assert_eq!(
3161 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3162 &[
3163 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3164 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3165 ]
3166 )
3167 });
3168
3169 // As client A, update a settings file. As Client B, see the changed settings.
3170 client_a
3171 .fs()
3172 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3173 .await;
3174 deterministic.run_until_parked();
3175 cx_b.read(|cx| {
3176 let store = cx.global::<SettingsStore>();
3177 assert_eq!(
3178 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3179 &[
3180 (Path::new("").into(), r#"{}"#.to_string()),
3181 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3182 ]
3183 )
3184 });
3185
3186 // As client A, create and remove some settings files. As client B, see the changed settings.
3187 client_a
3188 .fs()
3189 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3190 .await
3191 .unwrap();
3192 client_a
3193 .fs()
3194 .create_dir("/dir/b/.zed".as_ref())
3195 .await
3196 .unwrap();
3197 client_a
3198 .fs()
3199 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3200 .await;
3201 deterministic.run_until_parked();
3202 cx_b.read(|cx| {
3203 let store = cx.global::<SettingsStore>();
3204 assert_eq!(
3205 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3206 &[
3207 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3208 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3209 ]
3210 )
3211 });
3212
3213 // As client B, disconnect.
3214 server.forbid_connections();
3215 server.disconnect_client(client_b.peer_id().unwrap());
3216
3217 // As client A, change and remove settings files while client B is disconnected.
3218 client_a
3219 .fs()
3220 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3221 .await;
3222 client_a
3223 .fs()
3224 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3225 .await
3226 .unwrap();
3227 deterministic.run_until_parked();
3228
3229 // As client B, reconnect and see the changed settings.
3230 server.allow_connections();
3231 deterministic.advance_clock(RECEIVE_TIMEOUT);
3232 cx_b.read(|cx| {
3233 let store = cx.global::<SettingsStore>();
3234 assert_eq!(
3235 store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
3236 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3237 )
3238 });
3239}
3240
3241#[gpui::test(iterations = 10)]
3242async fn test_buffer_conflict_after_save(
3243 deterministic: Arc<Deterministic>,
3244 cx_a: &mut TestAppContext,
3245 cx_b: &mut TestAppContext,
3246) {
3247 deterministic.forbid_parking();
3248 let mut server = TestServer::start(&deterministic).await;
3249 let client_a = server.create_client(cx_a, "user_a").await;
3250 let client_b = server.create_client(cx_b, "user_b").await;
3251 server
3252 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3253 .await;
3254 let active_call_a = cx_a.read(ActiveCall::global);
3255
3256 client_a
3257 .fs()
3258 .insert_tree(
3259 "/dir",
3260 json!({
3261 "a.txt": "a-contents",
3262 }),
3263 )
3264 .await;
3265 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3266 let project_id = active_call_a
3267 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3268 .await
3269 .unwrap();
3270 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3271
3272 // Open a buffer as client B
3273 let buffer_b = project_b
3274 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3275 .await
3276 .unwrap();
3277
3278 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3279 buffer_b.read_with(cx_b, |buf, _| {
3280 assert!(buf.is_dirty());
3281 assert!(!buf.has_conflict());
3282 });
3283
3284 project_b
3285 .update(cx_b, |project, cx| {
3286 project.save_buffer(buffer_b.clone(), cx)
3287 })
3288 .await
3289 .unwrap();
3290 cx_a.foreground().forbid_parking();
3291 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3292 buffer_b.read_with(cx_b, |buf, _| {
3293 assert!(!buf.has_conflict());
3294 });
3295
3296 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3297 buffer_b.read_with(cx_b, |buf, _| {
3298 assert!(buf.is_dirty());
3299 assert!(!buf.has_conflict());
3300 });
3301}
3302
3303#[gpui::test(iterations = 10)]
3304async fn test_buffer_reloading(
3305 deterministic: Arc<Deterministic>,
3306 cx_a: &mut TestAppContext,
3307 cx_b: &mut TestAppContext,
3308) {
3309 deterministic.forbid_parking();
3310 let mut server = TestServer::start(&deterministic).await;
3311 let client_a = server.create_client(cx_a, "user_a").await;
3312 let client_b = server.create_client(cx_b, "user_b").await;
3313 server
3314 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3315 .await;
3316 let active_call_a = cx_a.read(ActiveCall::global);
3317
3318 client_a
3319 .fs()
3320 .insert_tree(
3321 "/dir",
3322 json!({
3323 "a.txt": "a\nb\nc",
3324 }),
3325 )
3326 .await;
3327 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3328 let project_id = active_call_a
3329 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3330 .await
3331 .unwrap();
3332 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3333
3334 // Open a buffer as client B
3335 let buffer_b = project_b
3336 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3337 .await
3338 .unwrap();
3339 buffer_b.read_with(cx_b, |buf, _| {
3340 assert!(!buf.is_dirty());
3341 assert!(!buf.has_conflict());
3342 assert_eq!(buf.line_ending(), LineEnding::Unix);
3343 });
3344
3345 let new_contents = Rope::from("d\ne\nf");
3346 client_a
3347 .fs()
3348 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3349 .await
3350 .unwrap();
3351 cx_a.foreground().run_until_parked();
3352 buffer_b.read_with(cx_b, |buf, _| {
3353 assert_eq!(buf.text(), new_contents.to_string());
3354 assert!(!buf.is_dirty());
3355 assert!(!buf.has_conflict());
3356 assert_eq!(buf.line_ending(), LineEnding::Windows);
3357 });
3358}
3359
3360#[gpui::test(iterations = 10)]
3361async fn test_editing_while_guest_opens_buffer(
3362 deterministic: Arc<Deterministic>,
3363 cx_a: &mut TestAppContext,
3364 cx_b: &mut TestAppContext,
3365) {
3366 deterministic.forbid_parking();
3367 let mut server = TestServer::start(&deterministic).await;
3368 let client_a = server.create_client(cx_a, "user_a").await;
3369 let client_b = server.create_client(cx_b, "user_b").await;
3370 server
3371 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3372 .await;
3373 let active_call_a = cx_a.read(ActiveCall::global);
3374
3375 client_a
3376 .fs()
3377 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3378 .await;
3379 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3380 let project_id = active_call_a
3381 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3382 .await
3383 .unwrap();
3384 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3385
3386 // Open a buffer as client A
3387 let buffer_a = project_a
3388 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3389 .await
3390 .unwrap();
3391
3392 // Start opening the same buffer as client B
3393 let buffer_b = cx_b
3394 .background()
3395 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3396
3397 // Edit the buffer as client A while client B is still opening it.
3398 cx_b.background().simulate_random_delay().await;
3399 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3400 cx_b.background().simulate_random_delay().await;
3401 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3402
3403 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3404 let buffer_b = buffer_b.await.unwrap();
3405 cx_a.foreground().run_until_parked();
3406 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3407}
3408
3409#[gpui::test]
3410async fn test_newline_above_or_below_does_not_move_guest_cursor(
3411 deterministic: Arc<Deterministic>,
3412 cx_a: &mut TestAppContext,
3413 cx_b: &mut TestAppContext,
3414) {
3415 deterministic.forbid_parking();
3416 let mut server = TestServer::start(&deterministic).await;
3417 let client_a = server.create_client(cx_a, "user_a").await;
3418 let client_b = server.create_client(cx_b, "user_b").await;
3419 server
3420 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3421 .await;
3422 let active_call_a = cx_a.read(ActiveCall::global);
3423
3424 client_a
3425 .fs()
3426 .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
3427 .await;
3428 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3429 let project_id = active_call_a
3430 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3431 .await
3432 .unwrap();
3433
3434 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3435
3436 // Open a buffer as client A
3437 let buffer_a = project_a
3438 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3439 .await
3440 .unwrap();
3441 let window_a = cx_a.add_window(|_| EmptyView);
3442 let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
3443 let mut editor_cx_a = EditorTestContext {
3444 cx: cx_a,
3445 window: window_a.into(),
3446 editor: editor_a,
3447 };
3448
3449 // Open a buffer as client B
3450 let buffer_b = project_b
3451 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3452 .await
3453 .unwrap();
3454 let window_b = cx_b.add_window(|_| EmptyView);
3455 let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
3456 let mut editor_cx_b = EditorTestContext {
3457 cx: cx_b,
3458 window: window_b.into(),
3459 editor: editor_b,
3460 };
3461
3462 // Test newline above
3463 editor_cx_a.set_selections_state(indoc! {"
3464 Some textˇ
3465 "});
3466 editor_cx_b.set_selections_state(indoc! {"
3467 Some textˇ
3468 "});
3469 editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
3470 deterministic.run_until_parked();
3471 editor_cx_a.assert_editor_state(indoc! {"
3472 ˇ
3473 Some text
3474 "});
3475 editor_cx_b.assert_editor_state(indoc! {"
3476
3477 Some textˇ
3478 "});
3479
3480 // Test newline below
3481 editor_cx_a.set_selections_state(indoc! {"
3482
3483 Some textˇ
3484 "});
3485 editor_cx_b.set_selections_state(indoc! {"
3486
3487 Some textˇ
3488 "});
3489 editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
3490 deterministic.run_until_parked();
3491 editor_cx_a.assert_editor_state(indoc! {"
3492
3493 Some text
3494 ˇ
3495 "});
3496 editor_cx_b.assert_editor_state(indoc! {"
3497
3498 Some textˇ
3499
3500 "});
3501}
3502
3503#[gpui::test(iterations = 10)]
3504async fn test_leaving_worktree_while_opening_buffer(
3505 deterministic: Arc<Deterministic>,
3506 cx_a: &mut TestAppContext,
3507 cx_b: &mut TestAppContext,
3508) {
3509 deterministic.forbid_parking();
3510 let mut server = TestServer::start(&deterministic).await;
3511 let client_a = server.create_client(cx_a, "user_a").await;
3512 let client_b = server.create_client(cx_b, "user_b").await;
3513 server
3514 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3515 .await;
3516 let active_call_a = cx_a.read(ActiveCall::global);
3517
3518 client_a
3519 .fs()
3520 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3521 .await;
3522 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3523 let project_id = active_call_a
3524 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3525 .await
3526 .unwrap();
3527 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3528
3529 // See that a guest has joined as client A.
3530 cx_a.foreground().run_until_parked();
3531 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3532
3533 // Begin opening a buffer as client B, but leave the project before the open completes.
3534 let buffer_b = cx_b
3535 .background()
3536 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3537 cx_b.update(|_| drop(project_b));
3538 drop(buffer_b);
3539
3540 // See that the guest has left.
3541 cx_a.foreground().run_until_parked();
3542 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3543}
3544
3545#[gpui::test(iterations = 10)]
3546async fn test_canceling_buffer_opening(
3547 deterministic: Arc<Deterministic>,
3548 cx_a: &mut TestAppContext,
3549 cx_b: &mut TestAppContext,
3550) {
3551 deterministic.forbid_parking();
3552
3553 let mut server = TestServer::start(&deterministic).await;
3554 let client_a = server.create_client(cx_a, "user_a").await;
3555 let client_b = server.create_client(cx_b, "user_b").await;
3556 server
3557 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3558 .await;
3559 let active_call_a = cx_a.read(ActiveCall::global);
3560
3561 client_a
3562 .fs()
3563 .insert_tree(
3564 "/dir",
3565 json!({
3566 "a.txt": "abc",
3567 }),
3568 )
3569 .await;
3570 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3571 let project_id = active_call_a
3572 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3573 .await
3574 .unwrap();
3575 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3576
3577 let buffer_a = project_a
3578 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3579 .await
3580 .unwrap();
3581
3582 // Open a buffer as client B but cancel after a random amount of time.
3583 let buffer_b = project_b.update(cx_b, |p, cx| {
3584 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3585 });
3586 deterministic.simulate_random_delay().await;
3587 drop(buffer_b);
3588
3589 // Try opening the same buffer again as client B, and ensure we can
3590 // still do it despite the cancellation above.
3591 let buffer_b = project_b
3592 .update(cx_b, |p, cx| {
3593 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3594 })
3595 .await
3596 .unwrap();
3597 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3598}
3599
3600#[gpui::test(iterations = 10)]
3601async fn test_leaving_project(
3602 deterministic: Arc<Deterministic>,
3603 cx_a: &mut TestAppContext,
3604 cx_b: &mut TestAppContext,
3605 cx_c: &mut TestAppContext,
3606) {
3607 deterministic.forbid_parking();
3608 let mut server = TestServer::start(&deterministic).await;
3609 let client_a = server.create_client(cx_a, "user_a").await;
3610 let client_b = server.create_client(cx_b, "user_b").await;
3611 let client_c = server.create_client(cx_c, "user_c").await;
3612 server
3613 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3614 .await;
3615 let active_call_a = cx_a.read(ActiveCall::global);
3616
3617 client_a
3618 .fs()
3619 .insert_tree(
3620 "/a",
3621 json!({
3622 "a.txt": "a-contents",
3623 "b.txt": "b-contents",
3624 }),
3625 )
3626 .await;
3627 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3628 let project_id = active_call_a
3629 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3630 .await
3631 .unwrap();
3632 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3633 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3634
3635 // Client A sees that a guest has joined.
3636 deterministic.run_until_parked();
3637 project_a.read_with(cx_a, |project, _| {
3638 assert_eq!(project.collaborators().len(), 2);
3639 });
3640 project_b1.read_with(cx_b, |project, _| {
3641 assert_eq!(project.collaborators().len(), 2);
3642 });
3643 project_c.read_with(cx_c, |project, _| {
3644 assert_eq!(project.collaborators().len(), 2);
3645 });
3646
3647 // Client B opens a buffer.
3648 let buffer_b1 = project_b1
3649 .update(cx_b, |project, cx| {
3650 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3651 project.open_buffer((worktree_id, "a.txt"), cx)
3652 })
3653 .await
3654 .unwrap();
3655 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3656
3657 // Drop client B's project and ensure client A and client C observe client B leaving.
3658 cx_b.update(|_| drop(project_b1));
3659 deterministic.run_until_parked();
3660 project_a.read_with(cx_a, |project, _| {
3661 assert_eq!(project.collaborators().len(), 1);
3662 });
3663 project_c.read_with(cx_c, |project, _| {
3664 assert_eq!(project.collaborators().len(), 1);
3665 });
3666
3667 // Client B re-joins the project and can open buffers as before.
3668 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3669 deterministic.run_until_parked();
3670 project_a.read_with(cx_a, |project, _| {
3671 assert_eq!(project.collaborators().len(), 2);
3672 });
3673 project_b2.read_with(cx_b, |project, _| {
3674 assert_eq!(project.collaborators().len(), 2);
3675 });
3676 project_c.read_with(cx_c, |project, _| {
3677 assert_eq!(project.collaborators().len(), 2);
3678 });
3679
3680 let buffer_b2 = project_b2
3681 .update(cx_b, |project, cx| {
3682 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3683 project.open_buffer((worktree_id, "a.txt"), cx)
3684 })
3685 .await
3686 .unwrap();
3687 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3688
3689 // Drop client B's connection and ensure client A and client C observe client B leaving.
3690 client_b.disconnect(&cx_b.to_async());
3691 deterministic.advance_clock(RECONNECT_TIMEOUT);
3692 project_a.read_with(cx_a, |project, _| {
3693 assert_eq!(project.collaborators().len(), 1);
3694 });
3695 project_b2.read_with(cx_b, |project, _| {
3696 assert!(project.is_read_only());
3697 });
3698 project_c.read_with(cx_c, |project, _| {
3699 assert_eq!(project.collaborators().len(), 1);
3700 });
3701
3702 // Client B can't join the project, unless they re-join the room.
3703 cx_b.spawn(|cx| {
3704 Project::remote(
3705 project_id,
3706 client_b.app_state.client.clone(),
3707 client_b.user_store().clone(),
3708 client_b.language_registry().clone(),
3709 FakeFs::new(cx.background()),
3710 cx,
3711 )
3712 })
3713 .await
3714 .unwrap_err();
3715
3716 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3717 client_c.wait_for_current_user(cx_c).await;
3718 server.forbid_connections();
3719 server.disconnect_client(client_c.peer_id().unwrap());
3720 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3721 deterministic.run_until_parked();
3722 project_a.read_with(cx_a, |project, _| {
3723 assert_eq!(project.collaborators().len(), 0);
3724 });
3725 project_b2.read_with(cx_b, |project, _| {
3726 assert!(project.is_read_only());
3727 });
3728 project_c.read_with(cx_c, |project, _| {
3729 assert!(project.is_read_only());
3730 });
3731}
3732
3733#[gpui::test(iterations = 10)]
3734async fn test_collaborating_with_diagnostics(
3735 deterministic: Arc<Deterministic>,
3736 cx_a: &mut TestAppContext,
3737 cx_b: &mut TestAppContext,
3738 cx_c: &mut TestAppContext,
3739) {
3740 deterministic.forbid_parking();
3741 let mut server = TestServer::start(&deterministic).await;
3742 let client_a = server.create_client(cx_a, "user_a").await;
3743 let client_b = server.create_client(cx_b, "user_b").await;
3744 let client_c = server.create_client(cx_c, "user_c").await;
3745 server
3746 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3747 .await;
3748 let active_call_a = cx_a.read(ActiveCall::global);
3749
3750 // Set up a fake language server.
3751 let mut language = Language::new(
3752 LanguageConfig {
3753 name: "Rust".into(),
3754 path_suffixes: vec!["rs".to_string()],
3755 ..Default::default()
3756 },
3757 Some(tree_sitter_rust::language()),
3758 );
3759 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3760 client_a.language_registry().add(Arc::new(language));
3761
3762 // Share a project as client A
3763 client_a
3764 .fs()
3765 .insert_tree(
3766 "/a",
3767 json!({
3768 "a.rs": "let one = two",
3769 "other.rs": "",
3770 }),
3771 )
3772 .await;
3773 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3774
3775 // Cause the language server to start.
3776 let _buffer = project_a
3777 .update(cx_a, |project, cx| {
3778 project.open_buffer(
3779 ProjectPath {
3780 worktree_id,
3781 path: Path::new("other.rs").into(),
3782 },
3783 cx,
3784 )
3785 })
3786 .await
3787 .unwrap();
3788
3789 // Simulate a language server reporting errors for a file.
3790 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3791 fake_language_server
3792 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3793 .await;
3794 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3795 lsp::PublishDiagnosticsParams {
3796 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3797 version: None,
3798 diagnostics: vec![lsp::Diagnostic {
3799 severity: Some(lsp::DiagnosticSeverity::WARNING),
3800 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3801 message: "message 0".to_string(),
3802 ..Default::default()
3803 }],
3804 },
3805 );
3806
3807 // Client A shares the project and, simultaneously, the language server
3808 // publishes a diagnostic. This is done to ensure that the server always
3809 // observes the latest diagnostics for a worktree.
3810 let project_id = active_call_a
3811 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3812 .await
3813 .unwrap();
3814 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3815 lsp::PublishDiagnosticsParams {
3816 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3817 version: None,
3818 diagnostics: vec![lsp::Diagnostic {
3819 severity: Some(lsp::DiagnosticSeverity::ERROR),
3820 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3821 message: "message 1".to_string(),
3822 ..Default::default()
3823 }],
3824 },
3825 );
3826
3827 // Join the worktree as client B.
3828 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3829
3830 // Wait for server to see the diagnostics update.
3831 deterministic.run_until_parked();
3832
3833 // Ensure client B observes the new diagnostics.
3834 project_b.read_with(cx_b, |project, cx| {
3835 assert_eq!(
3836 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3837 &[(
3838 ProjectPath {
3839 worktree_id,
3840 path: Arc::from(Path::new("a.rs")),
3841 },
3842 LanguageServerId(0),
3843 DiagnosticSummary {
3844 error_count: 1,
3845 warning_count: 0,
3846 ..Default::default()
3847 },
3848 )]
3849 )
3850 });
3851
3852 // Join project as client C and observe the diagnostics.
3853 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3854 let project_c_diagnostic_summaries =
3855 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3856 project.diagnostic_summaries(cx).collect::<Vec<_>>()
3857 })));
3858 project_c.update(cx_c, |_, cx| {
3859 let summaries = project_c_diagnostic_summaries.clone();
3860 cx.subscribe(&project_c, {
3861 move |p, _, event, cx| {
3862 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3863 *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
3864 }
3865 }
3866 })
3867 .detach();
3868 });
3869
3870 deterministic.run_until_parked();
3871 assert_eq!(
3872 project_c_diagnostic_summaries.borrow().as_slice(),
3873 &[(
3874 ProjectPath {
3875 worktree_id,
3876 path: Arc::from(Path::new("a.rs")),
3877 },
3878 LanguageServerId(0),
3879 DiagnosticSummary {
3880 error_count: 1,
3881 warning_count: 0,
3882 ..Default::default()
3883 },
3884 )]
3885 );
3886
3887 // Simulate a language server reporting more errors for a file.
3888 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3889 lsp::PublishDiagnosticsParams {
3890 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3891 version: None,
3892 diagnostics: vec![
3893 lsp::Diagnostic {
3894 severity: Some(lsp::DiagnosticSeverity::ERROR),
3895 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3896 message: "message 1".to_string(),
3897 ..Default::default()
3898 },
3899 lsp::Diagnostic {
3900 severity: Some(lsp::DiagnosticSeverity::WARNING),
3901 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3902 message: "message 2".to_string(),
3903 ..Default::default()
3904 },
3905 ],
3906 },
3907 );
3908
3909 // Clients B and C get the updated summaries
3910 deterministic.run_until_parked();
3911 project_b.read_with(cx_b, |project, cx| {
3912 assert_eq!(
3913 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3914 [(
3915 ProjectPath {
3916 worktree_id,
3917 path: Arc::from(Path::new("a.rs")),
3918 },
3919 LanguageServerId(0),
3920 DiagnosticSummary {
3921 error_count: 1,
3922 warning_count: 1,
3923 },
3924 )]
3925 );
3926 });
3927 project_c.read_with(cx_c, |project, cx| {
3928 assert_eq!(
3929 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3930 [(
3931 ProjectPath {
3932 worktree_id,
3933 path: Arc::from(Path::new("a.rs")),
3934 },
3935 LanguageServerId(0),
3936 DiagnosticSummary {
3937 error_count: 1,
3938 warning_count: 1,
3939 },
3940 )]
3941 );
3942 });
3943
3944 // Open the file with the errors on client B. They should be present.
3945 let buffer_b = cx_b
3946 .background()
3947 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3948 .await
3949 .unwrap();
3950
3951 buffer_b.read_with(cx_b, |buffer, _| {
3952 assert_eq!(
3953 buffer
3954 .snapshot()
3955 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3956 .collect::<Vec<_>>(),
3957 &[
3958 DiagnosticEntry {
3959 range: Point::new(0, 4)..Point::new(0, 7),
3960 diagnostic: Diagnostic {
3961 group_id: 2,
3962 message: "message 1".to_string(),
3963 severity: lsp::DiagnosticSeverity::ERROR,
3964 is_primary: true,
3965 ..Default::default()
3966 }
3967 },
3968 DiagnosticEntry {
3969 range: Point::new(0, 10)..Point::new(0, 13),
3970 diagnostic: Diagnostic {
3971 group_id: 3,
3972 severity: lsp::DiagnosticSeverity::WARNING,
3973 message: "message 2".to_string(),
3974 is_primary: true,
3975 ..Default::default()
3976 }
3977 }
3978 ]
3979 );
3980 });
3981
3982 // Simulate a language server reporting no errors for a file.
3983 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3984 lsp::PublishDiagnosticsParams {
3985 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3986 version: None,
3987 diagnostics: vec![],
3988 },
3989 );
3990 deterministic.run_until_parked();
3991 project_a.read_with(cx_a, |project, cx| {
3992 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3993 });
3994 project_b.read_with(cx_b, |project, cx| {
3995 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3996 });
3997 project_c.read_with(cx_c, |project, cx| {
3998 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3999 });
4000}
4001
4002#[gpui::test(iterations = 10)]
4003async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4004 deterministic: Arc<Deterministic>,
4005 cx_a: &mut TestAppContext,
4006 cx_b: &mut TestAppContext,
4007) {
4008 deterministic.forbid_parking();
4009 let mut server = TestServer::start(&deterministic).await;
4010 let client_a = server.create_client(cx_a, "user_a").await;
4011 let client_b = server.create_client(cx_b, "user_b").await;
4012 server
4013 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4014 .await;
4015
4016 // Set up a fake language server.
4017 let mut language = Language::new(
4018 LanguageConfig {
4019 name: "Rust".into(),
4020 path_suffixes: vec!["rs".to_string()],
4021 ..Default::default()
4022 },
4023 Some(tree_sitter_rust::language()),
4024 );
4025 let mut fake_language_servers = language
4026 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4027 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4028 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4029 ..Default::default()
4030 }))
4031 .await;
4032 client_a.language_registry().add(Arc::new(language));
4033
4034 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4035 client_a
4036 .fs()
4037 .insert_tree(
4038 "/test",
4039 json!({
4040 "one.rs": "const ONE: usize = 1;",
4041 "two.rs": "const TWO: usize = 2;",
4042 "three.rs": "const THREE: usize = 3;",
4043 "four.rs": "const FOUR: usize = 3;",
4044 "five.rs": "const FIVE: usize = 3;",
4045 }),
4046 )
4047 .await;
4048
4049 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
4050
4051 // Share a project as client A
4052 let active_call_a = cx_a.read(ActiveCall::global);
4053 let project_id = active_call_a
4054 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4055 .await
4056 .unwrap();
4057
4058 // Join the project as client B and open all three files.
4059 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4060 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4061 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
4062 }))
4063 .await
4064 .unwrap();
4065
4066 // Simulate a language server reporting errors for a file.
4067 let fake_language_server = fake_language_servers.next().await.unwrap();
4068 fake_language_server
4069 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4070 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4071 })
4072 .await
4073 .unwrap();
4074 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4075 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4076 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4077 lsp::WorkDoneProgressBegin {
4078 title: "Progress Began".into(),
4079 ..Default::default()
4080 },
4081 )),
4082 });
4083 for file_name in file_names {
4084 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4085 lsp::PublishDiagnosticsParams {
4086 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
4087 version: None,
4088 diagnostics: vec![lsp::Diagnostic {
4089 severity: Some(lsp::DiagnosticSeverity::WARNING),
4090 source: Some("the-disk-based-diagnostics-source".into()),
4091 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4092 message: "message one".to_string(),
4093 ..Default::default()
4094 }],
4095 },
4096 );
4097 }
4098 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4099 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4100 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4101 lsp::WorkDoneProgressEnd { message: None },
4102 )),
4103 });
4104
4105 // When the "disk base diagnostics finished" message is received, the buffers'
4106 // diagnostics are expected to be present.
4107 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4108 project_b.update(cx_b, {
4109 let project_b = project_b.clone();
4110 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4111 move |_, cx| {
4112 cx.subscribe(&project_b, move |_, _, event, cx| {
4113 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4114 disk_based_diagnostics_finished.store(true, SeqCst);
4115 for buffer in &guest_buffers {
4116 assert_eq!(
4117 buffer
4118 .read(cx)
4119 .snapshot()
4120 .diagnostics_in_range::<_, usize>(0..5, false)
4121 .count(),
4122 1,
4123 "expected a diagnostic for buffer {:?}",
4124 buffer.read(cx).file().unwrap().path(),
4125 );
4126 }
4127 }
4128 })
4129 .detach();
4130 }
4131 });
4132
4133 deterministic.run_until_parked();
4134 assert!(disk_based_diagnostics_finished.load(SeqCst));
4135}
4136
4137#[gpui::test(iterations = 10)]
4138async fn test_collaborating_with_completion(
4139 deterministic: Arc<Deterministic>,
4140 cx_a: &mut TestAppContext,
4141 cx_b: &mut TestAppContext,
4142) {
4143 deterministic.forbid_parking();
4144 let mut server = TestServer::start(&deterministic).await;
4145 let client_a = server.create_client(cx_a, "user_a").await;
4146 let client_b = server.create_client(cx_b, "user_b").await;
4147 server
4148 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4149 .await;
4150 let active_call_a = cx_a.read(ActiveCall::global);
4151
4152 // Set up a fake language server.
4153 let mut language = Language::new(
4154 LanguageConfig {
4155 name: "Rust".into(),
4156 path_suffixes: vec!["rs".to_string()],
4157 ..Default::default()
4158 },
4159 Some(tree_sitter_rust::language()),
4160 );
4161 let mut fake_language_servers = language
4162 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4163 capabilities: lsp::ServerCapabilities {
4164 completion_provider: Some(lsp::CompletionOptions {
4165 trigger_characters: Some(vec![".".to_string()]),
4166 ..Default::default()
4167 }),
4168 ..Default::default()
4169 },
4170 ..Default::default()
4171 }))
4172 .await;
4173 client_a.language_registry().add(Arc::new(language));
4174
4175 client_a
4176 .fs()
4177 .insert_tree(
4178 "/a",
4179 json!({
4180 "main.rs": "fn main() { a }",
4181 "other.rs": "",
4182 }),
4183 )
4184 .await;
4185 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4186 let project_id = active_call_a
4187 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4188 .await
4189 .unwrap();
4190 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4191
4192 // Open a file in an editor as the guest.
4193 let buffer_b = project_b
4194 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
4195 .await
4196 .unwrap();
4197 let window_b = cx_b.add_window(|_| EmptyView);
4198 let editor_b = window_b.add_view(cx_b, |cx| {
4199 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
4200 });
4201
4202 let fake_language_server = fake_language_servers.next().await.unwrap();
4203 cx_a.foreground().run_until_parked();
4204 buffer_b.read_with(cx_b, |buffer, _| {
4205 assert!(!buffer.completion_triggers().is_empty())
4206 });
4207
4208 // Type a completion trigger character as the guest.
4209 editor_b.update(cx_b, |editor, cx| {
4210 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
4211 editor.handle_input(".", cx);
4212 cx.focus(&editor_b);
4213 });
4214
4215 // Receive a completion request as the host's language server.
4216 // Return some completions from the host's language server.
4217 cx_a.foreground().start_waiting();
4218 fake_language_server
4219 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
4220 assert_eq!(
4221 params.text_document_position.text_document.uri,
4222 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4223 );
4224 assert_eq!(
4225 params.text_document_position.position,
4226 lsp::Position::new(0, 14),
4227 );
4228
4229 Ok(Some(lsp::CompletionResponse::Array(vec![
4230 lsp::CompletionItem {
4231 label: "first_method(…)".into(),
4232 detail: Some("fn(&mut self, B) -> C".into()),
4233 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4234 new_text: "first_method($1)".to_string(),
4235 range: lsp::Range::new(
4236 lsp::Position::new(0, 14),
4237 lsp::Position::new(0, 14),
4238 ),
4239 })),
4240 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4241 ..Default::default()
4242 },
4243 lsp::CompletionItem {
4244 label: "second_method(…)".into(),
4245 detail: Some("fn(&mut self, C) -> D<E>".into()),
4246 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4247 new_text: "second_method()".to_string(),
4248 range: lsp::Range::new(
4249 lsp::Position::new(0, 14),
4250 lsp::Position::new(0, 14),
4251 ),
4252 })),
4253 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4254 ..Default::default()
4255 },
4256 ])))
4257 })
4258 .next()
4259 .await
4260 .unwrap();
4261 cx_a.foreground().finish_waiting();
4262
4263 // Open the buffer on the host.
4264 let buffer_a = project_a
4265 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
4266 .await
4267 .unwrap();
4268 cx_a.foreground().run_until_parked();
4269 buffer_a.read_with(cx_a, |buffer, _| {
4270 assert_eq!(buffer.text(), "fn main() { a. }")
4271 });
4272
4273 // Confirm a completion on the guest.
4274 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
4275 editor_b.update(cx_b, |editor, cx| {
4276 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
4277 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
4278 });
4279
4280 // Return a resolved completion from the host's language server.
4281 // The resolved completion has an additional text edit.
4282 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
4283 |params, _| async move {
4284 assert_eq!(params.label, "first_method(…)");
4285 Ok(lsp::CompletionItem {
4286 label: "first_method(…)".into(),
4287 detail: Some("fn(&mut self, B) -> C".into()),
4288 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4289 new_text: "first_method($1)".to_string(),
4290 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
4291 })),
4292 additional_text_edits: Some(vec![lsp::TextEdit {
4293 new_text: "use d::SomeTrait;\n".to_string(),
4294 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4295 }]),
4296 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
4297 ..Default::default()
4298 })
4299 },
4300 );
4301
4302 // The additional edit is applied.
4303 cx_a.foreground().run_until_parked();
4304 buffer_a.read_with(cx_a, |buffer, _| {
4305 assert_eq!(
4306 buffer.text(),
4307 "use d::SomeTrait;\nfn main() { a.first_method() }"
4308 );
4309 });
4310 buffer_b.read_with(cx_b, |buffer, _| {
4311 assert_eq!(
4312 buffer.text(),
4313 "use d::SomeTrait;\nfn main() { a.first_method() }"
4314 );
4315 });
4316}
4317
4318#[gpui::test(iterations = 10)]
4319async fn test_reloading_buffer_manually(
4320 deterministic: Arc<Deterministic>,
4321 cx_a: &mut TestAppContext,
4322 cx_b: &mut TestAppContext,
4323) {
4324 deterministic.forbid_parking();
4325 let mut server = TestServer::start(&deterministic).await;
4326 let client_a = server.create_client(cx_a, "user_a").await;
4327 let client_b = server.create_client(cx_b, "user_b").await;
4328 server
4329 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4330 .await;
4331 let active_call_a = cx_a.read(ActiveCall::global);
4332
4333 client_a
4334 .fs()
4335 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4336 .await;
4337 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4338 let buffer_a = project_a
4339 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4340 .await
4341 .unwrap();
4342 let project_id = active_call_a
4343 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4344 .await
4345 .unwrap();
4346
4347 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4348
4349 let buffer_b = cx_b
4350 .background()
4351 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4352 .await
4353 .unwrap();
4354 buffer_b.update(cx_b, |buffer, cx| {
4355 buffer.edit([(4..7, "six")], None, cx);
4356 buffer.edit([(10..11, "6")], None, cx);
4357 assert_eq!(buffer.text(), "let six = 6;");
4358 assert!(buffer.is_dirty());
4359 assert!(!buffer.has_conflict());
4360 });
4361 cx_a.foreground().run_until_parked();
4362 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4363
4364 client_a
4365 .fs()
4366 .save(
4367 "/a/a.rs".as_ref(),
4368 &Rope::from("let seven = 7;"),
4369 LineEnding::Unix,
4370 )
4371 .await
4372 .unwrap();
4373 cx_a.foreground().run_until_parked();
4374 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4375 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4376
4377 project_b
4378 .update(cx_b, |project, cx| {
4379 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4380 })
4381 .await
4382 .unwrap();
4383 buffer_a.read_with(cx_a, |buffer, _| {
4384 assert_eq!(buffer.text(), "let seven = 7;");
4385 assert!(!buffer.is_dirty());
4386 assert!(!buffer.has_conflict());
4387 });
4388 buffer_b.read_with(cx_b, |buffer, _| {
4389 assert_eq!(buffer.text(), "let seven = 7;");
4390 assert!(!buffer.is_dirty());
4391 assert!(!buffer.has_conflict());
4392 });
4393
4394 buffer_a.update(cx_a, |buffer, cx| {
4395 // Undoing on the host is a no-op when the reload was initiated by the guest.
4396 buffer.undo(cx);
4397 assert_eq!(buffer.text(), "let seven = 7;");
4398 assert!(!buffer.is_dirty());
4399 assert!(!buffer.has_conflict());
4400 });
4401 buffer_b.update(cx_b, |buffer, cx| {
4402 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4403 buffer.undo(cx);
4404 assert_eq!(buffer.text(), "let six = 6;");
4405 assert!(buffer.is_dirty());
4406 assert!(!buffer.has_conflict());
4407 });
4408}
4409
4410#[gpui::test(iterations = 10)]
4411async fn test_formatting_buffer(
4412 deterministic: Arc<Deterministic>,
4413 cx_a: &mut TestAppContext,
4414 cx_b: &mut TestAppContext,
4415) {
4416 use project::FormatTrigger;
4417
4418 let mut server = TestServer::start(&deterministic).await;
4419 let client_a = server.create_client(cx_a, "user_a").await;
4420 let client_b = server.create_client(cx_b, "user_b").await;
4421 server
4422 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4423 .await;
4424 let active_call_a = cx_a.read(ActiveCall::global);
4425
4426 // Set up a fake language server.
4427 let mut language = Language::new(
4428 LanguageConfig {
4429 name: "Rust".into(),
4430 path_suffixes: vec!["rs".to_string()],
4431 ..Default::default()
4432 },
4433 Some(tree_sitter_rust::language()),
4434 );
4435 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4436 client_a.language_registry().add(Arc::new(language));
4437
4438 // Here we insert a fake tree with a directory that exists on disk. This is needed
4439 // because later we'll invoke a command, which requires passing a working directory
4440 // that points to a valid location on disk.
4441 let directory = env::current_dir().unwrap();
4442 client_a
4443 .fs()
4444 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4445 .await;
4446 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4447 let project_id = active_call_a
4448 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4449 .await
4450 .unwrap();
4451 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4452
4453 let buffer_b = cx_b
4454 .background()
4455 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4456 .await
4457 .unwrap();
4458
4459 let fake_language_server = fake_language_servers.next().await.unwrap();
4460 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4461 Ok(Some(vec![
4462 lsp::TextEdit {
4463 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4464 new_text: "h".to_string(),
4465 },
4466 lsp::TextEdit {
4467 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4468 new_text: "y".to_string(),
4469 },
4470 ]))
4471 });
4472
4473 project_b
4474 .update(cx_b, |project, cx| {
4475 project.format(
4476 HashSet::from_iter([buffer_b.clone()]),
4477 true,
4478 FormatTrigger::Save,
4479 cx,
4480 )
4481 })
4482 .await
4483 .unwrap();
4484
4485 // The edits from the LSP are applied, and a final newline is added.
4486 assert_eq!(
4487 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4488 "let honey = \"two\"\n"
4489 );
4490
4491 // Ensure buffer can be formatted using an external command. Notice how the
4492 // host's configuration is honored as opposed to using the guest's settings.
4493 cx_a.update(|cx| {
4494 cx.update_global(|store: &mut SettingsStore, cx| {
4495 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4496 file.defaults.formatter = Some(Formatter::External {
4497 command: "awk".into(),
4498 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4499 });
4500 });
4501 });
4502 });
4503 project_b
4504 .update(cx_b, |project, cx| {
4505 project.format(
4506 HashSet::from_iter([buffer_b.clone()]),
4507 true,
4508 FormatTrigger::Save,
4509 cx,
4510 )
4511 })
4512 .await
4513 .unwrap();
4514 assert_eq!(
4515 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4516 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4517 );
4518}
4519
4520#[gpui::test(iterations = 10)]
4521async fn test_definition(
4522 deterministic: Arc<Deterministic>,
4523 cx_a: &mut TestAppContext,
4524 cx_b: &mut TestAppContext,
4525) {
4526 deterministic.forbid_parking();
4527 let mut server = TestServer::start(&deterministic).await;
4528 let client_a = server.create_client(cx_a, "user_a").await;
4529 let client_b = server.create_client(cx_b, "user_b").await;
4530 server
4531 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4532 .await;
4533 let active_call_a = cx_a.read(ActiveCall::global);
4534
4535 // Set up a fake language server.
4536 let mut language = Language::new(
4537 LanguageConfig {
4538 name: "Rust".into(),
4539 path_suffixes: vec!["rs".to_string()],
4540 ..Default::default()
4541 },
4542 Some(tree_sitter_rust::language()),
4543 );
4544 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4545 client_a.language_registry().add(Arc::new(language));
4546
4547 client_a
4548 .fs()
4549 .insert_tree(
4550 "/root",
4551 json!({
4552 "dir-1": {
4553 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4554 },
4555 "dir-2": {
4556 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4557 "c.rs": "type T2 = usize;",
4558 }
4559 }),
4560 )
4561 .await;
4562 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4563 let project_id = active_call_a
4564 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4565 .await
4566 .unwrap();
4567 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4568
4569 // Open the file on client B.
4570 let buffer_b = cx_b
4571 .background()
4572 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4573 .await
4574 .unwrap();
4575
4576 // Request the definition of a symbol as the guest.
4577 let fake_language_server = fake_language_servers.next().await.unwrap();
4578 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4579 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4580 lsp::Location::new(
4581 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4582 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4583 ),
4584 )))
4585 });
4586
4587 let definitions_1 = project_b
4588 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4589 .await
4590 .unwrap();
4591 cx_b.read(|cx| {
4592 assert_eq!(definitions_1.len(), 1);
4593 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4594 let target_buffer = definitions_1[0].target.buffer.read(cx);
4595 assert_eq!(
4596 target_buffer.text(),
4597 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4598 );
4599 assert_eq!(
4600 definitions_1[0].target.range.to_point(target_buffer),
4601 Point::new(0, 6)..Point::new(0, 9)
4602 );
4603 });
4604
4605 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4606 // the previous call to `definition`.
4607 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4608 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4609 lsp::Location::new(
4610 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4611 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4612 ),
4613 )))
4614 });
4615
4616 let definitions_2 = project_b
4617 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4618 .await
4619 .unwrap();
4620 cx_b.read(|cx| {
4621 assert_eq!(definitions_2.len(), 1);
4622 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4623 let target_buffer = definitions_2[0].target.buffer.read(cx);
4624 assert_eq!(
4625 target_buffer.text(),
4626 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4627 );
4628 assert_eq!(
4629 definitions_2[0].target.range.to_point(target_buffer),
4630 Point::new(1, 6)..Point::new(1, 11)
4631 );
4632 });
4633 assert_eq!(
4634 definitions_1[0].target.buffer,
4635 definitions_2[0].target.buffer
4636 );
4637
4638 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4639 |req, _| async move {
4640 assert_eq!(
4641 req.text_document_position_params.position,
4642 lsp::Position::new(0, 7)
4643 );
4644 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4645 lsp::Location::new(
4646 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4647 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4648 ),
4649 )))
4650 },
4651 );
4652
4653 let type_definitions = project_b
4654 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4655 .await
4656 .unwrap();
4657 cx_b.read(|cx| {
4658 assert_eq!(type_definitions.len(), 1);
4659 let target_buffer = type_definitions[0].target.buffer.read(cx);
4660 assert_eq!(target_buffer.text(), "type T2 = usize;");
4661 assert_eq!(
4662 type_definitions[0].target.range.to_point(target_buffer),
4663 Point::new(0, 5)..Point::new(0, 7)
4664 );
4665 });
4666}
4667
4668#[gpui::test(iterations = 10)]
4669async fn test_references(
4670 deterministic: Arc<Deterministic>,
4671 cx_a: &mut TestAppContext,
4672 cx_b: &mut TestAppContext,
4673) {
4674 deterministic.forbid_parking();
4675 let mut server = TestServer::start(&deterministic).await;
4676 let client_a = server.create_client(cx_a, "user_a").await;
4677 let client_b = server.create_client(cx_b, "user_b").await;
4678 server
4679 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4680 .await;
4681 let active_call_a = cx_a.read(ActiveCall::global);
4682
4683 // Set up a fake language server.
4684 let mut language = Language::new(
4685 LanguageConfig {
4686 name: "Rust".into(),
4687 path_suffixes: vec!["rs".to_string()],
4688 ..Default::default()
4689 },
4690 Some(tree_sitter_rust::language()),
4691 );
4692 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4693 client_a.language_registry().add(Arc::new(language));
4694
4695 client_a
4696 .fs()
4697 .insert_tree(
4698 "/root",
4699 json!({
4700 "dir-1": {
4701 "one.rs": "const ONE: usize = 1;",
4702 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4703 },
4704 "dir-2": {
4705 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4706 }
4707 }),
4708 )
4709 .await;
4710 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4711 let project_id = active_call_a
4712 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4713 .await
4714 .unwrap();
4715 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4716
4717 // Open the file on client B.
4718 let buffer_b = cx_b
4719 .background()
4720 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4721 .await
4722 .unwrap();
4723
4724 // Request references to a symbol as the guest.
4725 let fake_language_server = fake_language_servers.next().await.unwrap();
4726 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4727 assert_eq!(
4728 params.text_document_position.text_document.uri.as_str(),
4729 "file:///root/dir-1/one.rs"
4730 );
4731 Ok(Some(vec![
4732 lsp::Location {
4733 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4734 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4735 },
4736 lsp::Location {
4737 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4738 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4739 },
4740 lsp::Location {
4741 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4742 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4743 },
4744 ]))
4745 });
4746
4747 let references = project_b
4748 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4749 .await
4750 .unwrap();
4751 cx_b.read(|cx| {
4752 assert_eq!(references.len(), 3);
4753 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4754
4755 let two_buffer = references[0].buffer.read(cx);
4756 let three_buffer = references[2].buffer.read(cx);
4757 assert_eq!(
4758 two_buffer.file().unwrap().path().as_ref(),
4759 Path::new("two.rs")
4760 );
4761 assert_eq!(references[1].buffer, references[0].buffer);
4762 assert_eq!(
4763 three_buffer.file().unwrap().full_path(cx),
4764 Path::new("/root/dir-2/three.rs")
4765 );
4766
4767 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4768 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4769 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4770 });
4771}
4772
4773#[gpui::test(iterations = 10)]
4774async fn test_project_search(
4775 deterministic: Arc<Deterministic>,
4776 cx_a: &mut TestAppContext,
4777 cx_b: &mut TestAppContext,
4778) {
4779 deterministic.forbid_parking();
4780 let mut server = TestServer::start(&deterministic).await;
4781 let client_a = server.create_client(cx_a, "user_a").await;
4782 let client_b = server.create_client(cx_b, "user_b").await;
4783 server
4784 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4785 .await;
4786 let active_call_a = cx_a.read(ActiveCall::global);
4787
4788 client_a
4789 .fs()
4790 .insert_tree(
4791 "/root",
4792 json!({
4793 "dir-1": {
4794 "a": "hello world",
4795 "b": "goodnight moon",
4796 "c": "a world of goo",
4797 "d": "world champion of clown world",
4798 },
4799 "dir-2": {
4800 "e": "disney world is fun",
4801 }
4802 }),
4803 )
4804 .await;
4805 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4806 let (worktree_2, _) = project_a
4807 .update(cx_a, |p, cx| {
4808 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4809 })
4810 .await
4811 .unwrap();
4812 worktree_2
4813 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4814 .await;
4815 let project_id = active_call_a
4816 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4817 .await
4818 .unwrap();
4819
4820 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4821
4822 // Perform a search as the guest.
4823 let results = project_b
4824 .update(cx_b, |project, cx| {
4825 project.search(
4826 SearchQuery::text("world", false, false, Vec::new(), Vec::new()),
4827 cx,
4828 )
4829 })
4830 .await
4831 .unwrap();
4832
4833 let mut ranges_by_path = results
4834 .into_iter()
4835 .map(|(buffer, ranges)| {
4836 buffer.read_with(cx_b, |buffer, cx| {
4837 let path = buffer.file().unwrap().full_path(cx);
4838 let offset_ranges = ranges
4839 .into_iter()
4840 .map(|range| range.to_offset(buffer))
4841 .collect::<Vec<_>>();
4842 (path, offset_ranges)
4843 })
4844 })
4845 .collect::<Vec<_>>();
4846 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4847
4848 assert_eq!(
4849 ranges_by_path,
4850 &[
4851 (PathBuf::from("dir-1/a"), vec![6..11]),
4852 (PathBuf::from("dir-1/c"), vec![2..7]),
4853 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4854 (PathBuf::from("dir-2/e"), vec![7..12]),
4855 ]
4856 );
4857}
4858
4859#[gpui::test(iterations = 10)]
4860async fn test_document_highlights(
4861 deterministic: Arc<Deterministic>,
4862 cx_a: &mut TestAppContext,
4863 cx_b: &mut TestAppContext,
4864) {
4865 deterministic.forbid_parking();
4866 let mut server = TestServer::start(&deterministic).await;
4867 let client_a = server.create_client(cx_a, "user_a").await;
4868 let client_b = server.create_client(cx_b, "user_b").await;
4869 server
4870 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4871 .await;
4872 let active_call_a = cx_a.read(ActiveCall::global);
4873
4874 client_a
4875 .fs()
4876 .insert_tree(
4877 "/root-1",
4878 json!({
4879 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4880 }),
4881 )
4882 .await;
4883
4884 // Set up a fake language server.
4885 let mut language = Language::new(
4886 LanguageConfig {
4887 name: "Rust".into(),
4888 path_suffixes: vec!["rs".to_string()],
4889 ..Default::default()
4890 },
4891 Some(tree_sitter_rust::language()),
4892 );
4893 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4894 client_a.language_registry().add(Arc::new(language));
4895
4896 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4897 let project_id = active_call_a
4898 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4899 .await
4900 .unwrap();
4901 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4902
4903 // Open the file on client B.
4904 let buffer_b = cx_b
4905 .background()
4906 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4907 .await
4908 .unwrap();
4909
4910 // Request document highlights as the guest.
4911 let fake_language_server = fake_language_servers.next().await.unwrap();
4912 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4913 |params, _| async move {
4914 assert_eq!(
4915 params
4916 .text_document_position_params
4917 .text_document
4918 .uri
4919 .as_str(),
4920 "file:///root-1/main.rs"
4921 );
4922 assert_eq!(
4923 params.text_document_position_params.position,
4924 lsp::Position::new(0, 34)
4925 );
4926 Ok(Some(vec![
4927 lsp::DocumentHighlight {
4928 kind: Some(lsp::DocumentHighlightKind::WRITE),
4929 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4930 },
4931 lsp::DocumentHighlight {
4932 kind: Some(lsp::DocumentHighlightKind::READ),
4933 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4934 },
4935 lsp::DocumentHighlight {
4936 kind: Some(lsp::DocumentHighlightKind::READ),
4937 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4938 },
4939 ]))
4940 },
4941 );
4942
4943 let highlights = project_b
4944 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4945 .await
4946 .unwrap();
4947 buffer_b.read_with(cx_b, |buffer, _| {
4948 let snapshot = buffer.snapshot();
4949
4950 let highlights = highlights
4951 .into_iter()
4952 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4953 .collect::<Vec<_>>();
4954 assert_eq!(
4955 highlights,
4956 &[
4957 (lsp::DocumentHighlightKind::WRITE, 10..16),
4958 (lsp::DocumentHighlightKind::READ, 32..38),
4959 (lsp::DocumentHighlightKind::READ, 41..47)
4960 ]
4961 )
4962 });
4963}
4964
4965#[gpui::test(iterations = 10)]
4966async fn test_lsp_hover(
4967 deterministic: Arc<Deterministic>,
4968 cx_a: &mut TestAppContext,
4969 cx_b: &mut TestAppContext,
4970) {
4971 deterministic.forbid_parking();
4972 let mut server = TestServer::start(&deterministic).await;
4973 let client_a = server.create_client(cx_a, "user_a").await;
4974 let client_b = server.create_client(cx_b, "user_b").await;
4975 server
4976 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4977 .await;
4978 let active_call_a = cx_a.read(ActiveCall::global);
4979
4980 client_a
4981 .fs()
4982 .insert_tree(
4983 "/root-1",
4984 json!({
4985 "main.rs": "use std::collections::HashMap;",
4986 }),
4987 )
4988 .await;
4989
4990 // Set up a fake language server.
4991 let mut language = Language::new(
4992 LanguageConfig {
4993 name: "Rust".into(),
4994 path_suffixes: vec!["rs".to_string()],
4995 ..Default::default()
4996 },
4997 Some(tree_sitter_rust::language()),
4998 );
4999 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5000 client_a.language_registry().add(Arc::new(language));
5001
5002 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5003 let project_id = active_call_a
5004 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5005 .await
5006 .unwrap();
5007 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5008
5009 // Open the file as the guest
5010 let buffer_b = cx_b
5011 .background()
5012 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
5013 .await
5014 .unwrap();
5015
5016 // Request hover information as the guest.
5017 let fake_language_server = fake_language_servers.next().await.unwrap();
5018 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
5019 |params, _| async move {
5020 assert_eq!(
5021 params
5022 .text_document_position_params
5023 .text_document
5024 .uri
5025 .as_str(),
5026 "file:///root-1/main.rs"
5027 );
5028 assert_eq!(
5029 params.text_document_position_params.position,
5030 lsp::Position::new(0, 22)
5031 );
5032 Ok(Some(lsp::Hover {
5033 contents: lsp::HoverContents::Array(vec![
5034 lsp::MarkedString::String("Test hover content.".to_string()),
5035 lsp::MarkedString::LanguageString(lsp::LanguageString {
5036 language: "Rust".to_string(),
5037 value: "let foo = 42;".to_string(),
5038 }),
5039 ]),
5040 range: Some(lsp::Range::new(
5041 lsp::Position::new(0, 22),
5042 lsp::Position::new(0, 29),
5043 )),
5044 }))
5045 },
5046 );
5047
5048 let hover_info = project_b
5049 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5050 .await
5051 .unwrap()
5052 .unwrap();
5053 buffer_b.read_with(cx_b, |buffer, _| {
5054 let snapshot = buffer.snapshot();
5055 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
5056 assert_eq!(
5057 hover_info.contents,
5058 vec![
5059 project::HoverBlock {
5060 text: "Test hover content.".to_string(),
5061 kind: HoverBlockKind::Markdown,
5062 },
5063 project::HoverBlock {
5064 text: "let foo = 42;".to_string(),
5065 kind: HoverBlockKind::Code {
5066 language: "Rust".to_string()
5067 },
5068 }
5069 ]
5070 );
5071 });
5072}
5073
5074#[gpui::test(iterations = 10)]
5075async fn test_project_symbols(
5076 deterministic: Arc<Deterministic>,
5077 cx_a: &mut TestAppContext,
5078 cx_b: &mut TestAppContext,
5079) {
5080 deterministic.forbid_parking();
5081 let mut server = TestServer::start(&deterministic).await;
5082 let client_a = server.create_client(cx_a, "user_a").await;
5083 let client_b = server.create_client(cx_b, "user_b").await;
5084 server
5085 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5086 .await;
5087 let active_call_a = cx_a.read(ActiveCall::global);
5088
5089 // Set up a fake language server.
5090 let mut language = Language::new(
5091 LanguageConfig {
5092 name: "Rust".into(),
5093 path_suffixes: vec!["rs".to_string()],
5094 ..Default::default()
5095 },
5096 Some(tree_sitter_rust::language()),
5097 );
5098 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5099 client_a.language_registry().add(Arc::new(language));
5100
5101 client_a
5102 .fs()
5103 .insert_tree(
5104 "/code",
5105 json!({
5106 "crate-1": {
5107 "one.rs": "const ONE: usize = 1;",
5108 },
5109 "crate-2": {
5110 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5111 },
5112 "private": {
5113 "passwords.txt": "the-password",
5114 }
5115 }),
5116 )
5117 .await;
5118 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
5119 let project_id = active_call_a
5120 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5121 .await
5122 .unwrap();
5123 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5124
5125 // Cause the language server to start.
5126 let _buffer = cx_b
5127 .background()
5128 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
5129 .await
5130 .unwrap();
5131
5132 let fake_language_server = fake_language_servers.next().await.unwrap();
5133 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
5134 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5135 #[allow(deprecated)]
5136 lsp::SymbolInformation {
5137 name: "TWO".into(),
5138 location: lsp::Location {
5139 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
5140 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5141 },
5142 kind: lsp::SymbolKind::CONSTANT,
5143 tags: None,
5144 container_name: None,
5145 deprecated: None,
5146 },
5147 ])))
5148 });
5149
5150 // Request the definition of a symbol as the guest.
5151 let symbols = project_b
5152 .update(cx_b, |p, cx| p.symbols("two", cx))
5153 .await
5154 .unwrap();
5155 assert_eq!(symbols.len(), 1);
5156 assert_eq!(symbols[0].name, "TWO");
5157
5158 // Open one of the returned symbols.
5159 let buffer_b_2 = project_b
5160 .update(cx_b, |project, cx| {
5161 project.open_buffer_for_symbol(&symbols[0], cx)
5162 })
5163 .await
5164 .unwrap();
5165 buffer_b_2.read_with(cx_b, |buffer, _| {
5166 assert_eq!(
5167 buffer.file().unwrap().path().as_ref(),
5168 Path::new("../crate-2/two.rs")
5169 );
5170 });
5171
5172 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5173 let mut fake_symbol = symbols[0].clone();
5174 fake_symbol.path.path = Path::new("/code/secrets").into();
5175 let error = project_b
5176 .update(cx_b, |project, cx| {
5177 project.open_buffer_for_symbol(&fake_symbol, cx)
5178 })
5179 .await
5180 .unwrap_err();
5181 assert!(error.to_string().contains("invalid symbol signature"));
5182}
5183
5184#[gpui::test(iterations = 10)]
5185async fn test_open_buffer_while_getting_definition_pointing_to_it(
5186 deterministic: Arc<Deterministic>,
5187 cx_a: &mut TestAppContext,
5188 cx_b: &mut TestAppContext,
5189 mut rng: StdRng,
5190) {
5191 deterministic.forbid_parking();
5192 let mut server = TestServer::start(&deterministic).await;
5193 let client_a = server.create_client(cx_a, "user_a").await;
5194 let client_b = server.create_client(cx_b, "user_b").await;
5195 server
5196 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5197 .await;
5198 let active_call_a = cx_a.read(ActiveCall::global);
5199
5200 // Set up a fake language server.
5201 let mut language = Language::new(
5202 LanguageConfig {
5203 name: "Rust".into(),
5204 path_suffixes: vec!["rs".to_string()],
5205 ..Default::default()
5206 },
5207 Some(tree_sitter_rust::language()),
5208 );
5209 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5210 client_a.language_registry().add(Arc::new(language));
5211
5212 client_a
5213 .fs()
5214 .insert_tree(
5215 "/root",
5216 json!({
5217 "a.rs": "const ONE: usize = b::TWO;",
5218 "b.rs": "const TWO: usize = 2",
5219 }),
5220 )
5221 .await;
5222 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5223 let project_id = active_call_a
5224 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5225 .await
5226 .unwrap();
5227 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5228
5229 let buffer_b1 = cx_b
5230 .background()
5231 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
5232 .await
5233 .unwrap();
5234
5235 let fake_language_server = fake_language_servers.next().await.unwrap();
5236 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5237 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5238 lsp::Location::new(
5239 lsp::Url::from_file_path("/root/b.rs").unwrap(),
5240 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5241 ),
5242 )))
5243 });
5244
5245 let definitions;
5246 let buffer_b2;
5247 if rng.gen() {
5248 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5249 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5250 } else {
5251 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5252 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5253 }
5254
5255 let buffer_b2 = buffer_b2.await.unwrap();
5256 let definitions = definitions.await.unwrap();
5257 assert_eq!(definitions.len(), 1);
5258 assert_eq!(definitions[0].target.buffer, buffer_b2);
5259}
5260
5261#[gpui::test(iterations = 10)]
5262async fn test_collaborating_with_code_actions(
5263 deterministic: Arc<Deterministic>,
5264 cx_a: &mut TestAppContext,
5265 cx_b: &mut TestAppContext,
5266) {
5267 deterministic.forbid_parking();
5268 let mut server = TestServer::start(&deterministic).await;
5269 let client_a = server.create_client(cx_a, "user_a").await;
5270 //
5271 let client_b = server.create_client(cx_b, "user_b").await;
5272 server
5273 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5274 .await;
5275 let active_call_a = cx_a.read(ActiveCall::global);
5276
5277 cx_b.update(editor::init);
5278
5279 // Set up a fake language server.
5280 let mut language = Language::new(
5281 LanguageConfig {
5282 name: "Rust".into(),
5283 path_suffixes: vec!["rs".to_string()],
5284 ..Default::default()
5285 },
5286 Some(tree_sitter_rust::language()),
5287 );
5288 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
5289 client_a.language_registry().add(Arc::new(language));
5290
5291 client_a
5292 .fs()
5293 .insert_tree(
5294 "/a",
5295 json!({
5296 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
5297 "other.rs": "pub fn foo() -> usize { 4 }",
5298 }),
5299 )
5300 .await;
5301 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5302 let project_id = active_call_a
5303 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5304 .await
5305 .unwrap();
5306
5307 // Join the project as client B.
5308 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5309 let window_b =
5310 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
5311 let workspace_b = window_b.root(cx_b);
5312 let editor_b = workspace_b
5313 .update(cx_b, |workspace, cx| {
5314 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
5315 })
5316 .await
5317 .unwrap()
5318 .downcast::<Editor>()
5319 .unwrap();
5320
5321 let mut fake_language_server = fake_language_servers.next().await.unwrap();
5322 fake_language_server
5323 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
5324 assert_eq!(
5325 params.text_document.uri,
5326 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5327 );
5328 assert_eq!(params.range.start, lsp::Position::new(0, 0));
5329 assert_eq!(params.range.end, lsp::Position::new(0, 0));
5330 Ok(None)
5331 })
5332 .next()
5333 .await;
5334
5335 // Move cursor to a location that contains code actions.
5336 editor_b.update(cx_b, |editor, cx| {
5337 editor.change_selections(None, cx, |s| {
5338 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
5339 });
5340 cx.focus(&editor_b);
5341 });
5342
5343 fake_language_server
5344 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
5345 assert_eq!(
5346 params.text_document.uri,
5347 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5348 );
5349 assert_eq!(params.range.start, lsp::Position::new(1, 31));
5350 assert_eq!(params.range.end, lsp::Position::new(1, 31));
5351
5352 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
5353 lsp::CodeAction {
5354 title: "Inline into all callers".to_string(),
5355 edit: Some(lsp::WorkspaceEdit {
5356 changes: Some(
5357 [
5358 (
5359 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5360 vec![lsp::TextEdit::new(
5361 lsp::Range::new(
5362 lsp::Position::new(1, 22),
5363 lsp::Position::new(1, 34),
5364 ),
5365 "4".to_string(),
5366 )],
5367 ),
5368 (
5369 lsp::Url::from_file_path("/a/other.rs").unwrap(),
5370 vec![lsp::TextEdit::new(
5371 lsp::Range::new(
5372 lsp::Position::new(0, 0),
5373 lsp::Position::new(0, 27),
5374 ),
5375 "".to_string(),
5376 )],
5377 ),
5378 ]
5379 .into_iter()
5380 .collect(),
5381 ),
5382 ..Default::default()
5383 }),
5384 data: Some(json!({
5385 "codeActionParams": {
5386 "range": {
5387 "start": {"line": 1, "column": 31},
5388 "end": {"line": 1, "column": 31},
5389 }
5390 }
5391 })),
5392 ..Default::default()
5393 },
5394 )]))
5395 })
5396 .next()
5397 .await;
5398
5399 // Toggle code actions and wait for them to display.
5400 editor_b.update(cx_b, |editor, cx| {
5401 editor.toggle_code_actions(
5402 &ToggleCodeActions {
5403 deployed_from_indicator: false,
5404 },
5405 cx,
5406 );
5407 });
5408 cx_a.foreground().run_until_parked();
5409 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
5410
5411 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
5412
5413 // Confirming the code action will trigger a resolve request.
5414 let confirm_action = workspace_b
5415 .update(cx_b, |workspace, cx| {
5416 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
5417 })
5418 .unwrap();
5419 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
5420 |_, _| async move {
5421 Ok(lsp::CodeAction {
5422 title: "Inline into all callers".to_string(),
5423 edit: Some(lsp::WorkspaceEdit {
5424 changes: Some(
5425 [
5426 (
5427 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5428 vec![lsp::TextEdit::new(
5429 lsp::Range::new(
5430 lsp::Position::new(1, 22),
5431 lsp::Position::new(1, 34),
5432 ),
5433 "4".to_string(),
5434 )],
5435 ),
5436 (
5437 lsp::Url::from_file_path("/a/other.rs").unwrap(),
5438 vec![lsp::TextEdit::new(
5439 lsp::Range::new(
5440 lsp::Position::new(0, 0),
5441 lsp::Position::new(0, 27),
5442 ),
5443 "".to_string(),
5444 )],
5445 ),
5446 ]
5447 .into_iter()
5448 .collect(),
5449 ),
5450 ..Default::default()
5451 }),
5452 ..Default::default()
5453 })
5454 },
5455 );
5456
5457 // After the action is confirmed, an editor containing both modified files is opened.
5458 confirm_action.await.unwrap();
5459 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5460 workspace
5461 .active_item(cx)
5462 .unwrap()
5463 .downcast::<Editor>()
5464 .unwrap()
5465 });
5466 code_action_editor.update(cx_b, |editor, cx| {
5467 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
5468 editor.undo(&Undo, cx);
5469 assert_eq!(
5470 editor.text(cx),
5471 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
5472 );
5473 editor.redo(&Redo, cx);
5474 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
5475 });
5476}
5477
5478#[gpui::test(iterations = 10)]
5479async fn test_collaborating_with_renames(
5480 deterministic: Arc<Deterministic>,
5481 cx_a: &mut TestAppContext,
5482 cx_b: &mut TestAppContext,
5483) {
5484 deterministic.forbid_parking();
5485 let mut server = TestServer::start(&deterministic).await;
5486 let client_a = server.create_client(cx_a, "user_a").await;
5487 let client_b = server.create_client(cx_b, "user_b").await;
5488 server
5489 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5490 .await;
5491 let active_call_a = cx_a.read(ActiveCall::global);
5492
5493 cx_b.update(editor::init);
5494
5495 // Set up a fake language server.
5496 let mut language = Language::new(
5497 LanguageConfig {
5498 name: "Rust".into(),
5499 path_suffixes: vec!["rs".to_string()],
5500 ..Default::default()
5501 },
5502 Some(tree_sitter_rust::language()),
5503 );
5504 let mut fake_language_servers = language
5505 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5506 capabilities: lsp::ServerCapabilities {
5507 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
5508 prepare_provider: Some(true),
5509 work_done_progress_options: Default::default(),
5510 })),
5511 ..Default::default()
5512 },
5513 ..Default::default()
5514 }))
5515 .await;
5516 client_a.language_registry().add(Arc::new(language));
5517
5518 client_a
5519 .fs()
5520 .insert_tree(
5521 "/dir",
5522 json!({
5523 "one.rs": "const ONE: usize = 1;",
5524 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
5525 }),
5526 )
5527 .await;
5528 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5529 let project_id = active_call_a
5530 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5531 .await
5532 .unwrap();
5533 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5534
5535 let window_b =
5536 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
5537 let workspace_b = window_b.root(cx_b);
5538 let editor_b = workspace_b
5539 .update(cx_b, |workspace, cx| {
5540 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
5541 })
5542 .await
5543 .unwrap()
5544 .downcast::<Editor>()
5545 .unwrap();
5546 let fake_language_server = fake_language_servers.next().await.unwrap();
5547
5548 // Move cursor to a location that can be renamed.
5549 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
5550 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
5551 editor.rename(&Rename, cx).unwrap()
5552 });
5553
5554 fake_language_server
5555 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
5556 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
5557 assert_eq!(params.position, lsp::Position::new(0, 7));
5558 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
5559 lsp::Position::new(0, 6),
5560 lsp::Position::new(0, 9),
5561 ))))
5562 })
5563 .next()
5564 .await
5565 .unwrap();
5566 prepare_rename.await.unwrap();
5567 editor_b.update(cx_b, |editor, cx| {
5568 use editor::ToOffset;
5569 let rename = editor.pending_rename().unwrap();
5570 let buffer = editor.buffer().read(cx).snapshot(cx);
5571 assert_eq!(
5572 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
5573 6..9
5574 );
5575 rename.editor.update(cx, |rename_editor, cx| {
5576 rename_editor.buffer().update(cx, |rename_buffer, cx| {
5577 rename_buffer.edit([(0..3, "THREE")], None, cx);
5578 });
5579 });
5580 });
5581
5582 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
5583 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
5584 });
5585 fake_language_server
5586 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
5587 assert_eq!(
5588 params.text_document_position.text_document.uri.as_str(),
5589 "file:///dir/one.rs"
5590 );
5591 assert_eq!(
5592 params.text_document_position.position,
5593 lsp::Position::new(0, 6)
5594 );
5595 assert_eq!(params.new_name, "THREE");
5596 Ok(Some(lsp::WorkspaceEdit {
5597 changes: Some(
5598 [
5599 (
5600 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
5601 vec![lsp::TextEdit::new(
5602 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5603 "THREE".to_string(),
5604 )],
5605 ),
5606 (
5607 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
5608 vec![
5609 lsp::TextEdit::new(
5610 lsp::Range::new(
5611 lsp::Position::new(0, 24),
5612 lsp::Position::new(0, 27),
5613 ),
5614 "THREE".to_string(),
5615 ),
5616 lsp::TextEdit::new(
5617 lsp::Range::new(
5618 lsp::Position::new(0, 35),
5619 lsp::Position::new(0, 38),
5620 ),
5621 "THREE".to_string(),
5622 ),
5623 ],
5624 ),
5625 ]
5626 .into_iter()
5627 .collect(),
5628 ),
5629 ..Default::default()
5630 }))
5631 })
5632 .next()
5633 .await
5634 .unwrap();
5635 confirm_rename.await.unwrap();
5636
5637 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5638 workspace
5639 .active_item(cx)
5640 .unwrap()
5641 .downcast::<Editor>()
5642 .unwrap()
5643 });
5644 rename_editor.update(cx_b, |editor, cx| {
5645 assert_eq!(
5646 editor.text(cx),
5647 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5648 );
5649 editor.undo(&Undo, cx);
5650 assert_eq!(
5651 editor.text(cx),
5652 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
5653 );
5654 editor.redo(&Redo, cx);
5655 assert_eq!(
5656 editor.text(cx),
5657 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5658 );
5659 });
5660
5661 // Ensure temporary rename edits cannot be undone/redone.
5662 editor_b.update(cx_b, |editor, cx| {
5663 editor.undo(&Undo, cx);
5664 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5665 editor.undo(&Undo, cx);
5666 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5667 editor.redo(&Redo, cx);
5668 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
5669 })
5670}
5671
5672#[gpui::test(iterations = 10)]
5673async fn test_language_server_statuses(
5674 deterministic: Arc<Deterministic>,
5675 cx_a: &mut TestAppContext,
5676 cx_b: &mut TestAppContext,
5677) {
5678 deterministic.forbid_parking();
5679 let mut server = TestServer::start(&deterministic).await;
5680 let client_a = server.create_client(cx_a, "user_a").await;
5681 let client_b = server.create_client(cx_b, "user_b").await;
5682 server
5683 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5684 .await;
5685 let active_call_a = cx_a.read(ActiveCall::global);
5686
5687 cx_b.update(editor::init);
5688
5689 // Set up a fake language server.
5690 let mut language = Language::new(
5691 LanguageConfig {
5692 name: "Rust".into(),
5693 path_suffixes: vec!["rs".to_string()],
5694 ..Default::default()
5695 },
5696 Some(tree_sitter_rust::language()),
5697 );
5698 let mut fake_language_servers = language
5699 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5700 name: "the-language-server",
5701 ..Default::default()
5702 }))
5703 .await;
5704 client_a.language_registry().add(Arc::new(language));
5705
5706 client_a
5707 .fs()
5708 .insert_tree(
5709 "/dir",
5710 json!({
5711 "main.rs": "const ONE: usize = 1;",
5712 }),
5713 )
5714 .await;
5715 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5716
5717 let _buffer_a = project_a
5718 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
5719 .await
5720 .unwrap();
5721
5722 let fake_language_server = fake_language_servers.next().await.unwrap();
5723 fake_language_server.start_progress("the-token").await;
5724 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5725 token: lsp::NumberOrString::String("the-token".to_string()),
5726 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5727 lsp::WorkDoneProgressReport {
5728 message: Some("the-message".to_string()),
5729 ..Default::default()
5730 },
5731 )),
5732 });
5733 deterministic.run_until_parked();
5734 project_a.read_with(cx_a, |project, _| {
5735 let status = project.language_server_statuses().next().unwrap();
5736 assert_eq!(status.name, "the-language-server");
5737 assert_eq!(status.pending_work.len(), 1);
5738 assert_eq!(
5739 status.pending_work["the-token"].message.as_ref().unwrap(),
5740 "the-message"
5741 );
5742 });
5743
5744 let project_id = active_call_a
5745 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5746 .await
5747 .unwrap();
5748 deterministic.run_until_parked();
5749 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5750 project_b.read_with(cx_b, |project, _| {
5751 let status = project.language_server_statuses().next().unwrap();
5752 assert_eq!(status.name, "the-language-server");
5753 });
5754
5755 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5756 token: lsp::NumberOrString::String("the-token".to_string()),
5757 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5758 lsp::WorkDoneProgressReport {
5759 message: Some("the-message-2".to_string()),
5760 ..Default::default()
5761 },
5762 )),
5763 });
5764 deterministic.run_until_parked();
5765 project_a.read_with(cx_a, |project, _| {
5766 let status = project.language_server_statuses().next().unwrap();
5767 assert_eq!(status.name, "the-language-server");
5768 assert_eq!(status.pending_work.len(), 1);
5769 assert_eq!(
5770 status.pending_work["the-token"].message.as_ref().unwrap(),
5771 "the-message-2"
5772 );
5773 });
5774 project_b.read_with(cx_b, |project, _| {
5775 let status = project.language_server_statuses().next().unwrap();
5776 assert_eq!(status.name, "the-language-server");
5777 assert_eq!(status.pending_work.len(), 1);
5778 assert_eq!(
5779 status.pending_work["the-token"].message.as_ref().unwrap(),
5780 "the-message-2"
5781 );
5782 });
5783}
5784
5785#[gpui::test(iterations = 10)]
5786async fn test_contacts(
5787 deterministic: Arc<Deterministic>,
5788 cx_a: &mut TestAppContext,
5789 cx_b: &mut TestAppContext,
5790 cx_c: &mut TestAppContext,
5791 cx_d: &mut TestAppContext,
5792) {
5793 deterministic.forbid_parking();
5794 let mut server = TestServer::start(&deterministic).await;
5795 let client_a = server.create_client(cx_a, "user_a").await;
5796 let client_b = server.create_client(cx_b, "user_b").await;
5797 let client_c = server.create_client(cx_c, "user_c").await;
5798 let client_d = server.create_client(cx_d, "user_d").await;
5799 server
5800 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5801 .await;
5802 let active_call_a = cx_a.read(ActiveCall::global);
5803 let active_call_b = cx_b.read(ActiveCall::global);
5804 let active_call_c = cx_c.read(ActiveCall::global);
5805 let _active_call_d = cx_d.read(ActiveCall::global);
5806
5807 deterministic.run_until_parked();
5808 assert_eq!(
5809 contacts(&client_a, cx_a),
5810 [
5811 ("user_b".to_string(), "online", "free"),
5812 ("user_c".to_string(), "online", "free")
5813 ]
5814 );
5815 assert_eq!(
5816 contacts(&client_b, cx_b),
5817 [
5818 ("user_a".to_string(), "online", "free"),
5819 ("user_c".to_string(), "online", "free")
5820 ]
5821 );
5822 assert_eq!(
5823 contacts(&client_c, cx_c),
5824 [
5825 ("user_a".to_string(), "online", "free"),
5826 ("user_b".to_string(), "online", "free")
5827 ]
5828 );
5829 assert_eq!(contacts(&client_d, cx_d), []);
5830
5831 server.disconnect_client(client_c.peer_id().unwrap());
5832 server.forbid_connections();
5833 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5834 assert_eq!(
5835 contacts(&client_a, cx_a),
5836 [
5837 ("user_b".to_string(), "online", "free"),
5838 ("user_c".to_string(), "offline", "free")
5839 ]
5840 );
5841 assert_eq!(
5842 contacts(&client_b, cx_b),
5843 [
5844 ("user_a".to_string(), "online", "free"),
5845 ("user_c".to_string(), "offline", "free")
5846 ]
5847 );
5848 assert_eq!(contacts(&client_c, cx_c), []);
5849 assert_eq!(contacts(&client_d, cx_d), []);
5850
5851 server.allow_connections();
5852 client_c
5853 .authenticate_and_connect(false, &cx_c.to_async())
5854 .await
5855 .unwrap();
5856
5857 deterministic.run_until_parked();
5858 assert_eq!(
5859 contacts(&client_a, cx_a),
5860 [
5861 ("user_b".to_string(), "online", "free"),
5862 ("user_c".to_string(), "online", "free")
5863 ]
5864 );
5865 assert_eq!(
5866 contacts(&client_b, cx_b),
5867 [
5868 ("user_a".to_string(), "online", "free"),
5869 ("user_c".to_string(), "online", "free")
5870 ]
5871 );
5872 assert_eq!(
5873 contacts(&client_c, cx_c),
5874 [
5875 ("user_a".to_string(), "online", "free"),
5876 ("user_b".to_string(), "online", "free")
5877 ]
5878 );
5879 assert_eq!(contacts(&client_d, cx_d), []);
5880
5881 active_call_a
5882 .update(cx_a, |call, cx| {
5883 call.invite(client_b.user_id().unwrap(), None, cx)
5884 })
5885 .await
5886 .unwrap();
5887 deterministic.run_until_parked();
5888 assert_eq!(
5889 contacts(&client_a, cx_a),
5890 [
5891 ("user_b".to_string(), "online", "busy"),
5892 ("user_c".to_string(), "online", "free")
5893 ]
5894 );
5895 assert_eq!(
5896 contacts(&client_b, cx_b),
5897 [
5898 ("user_a".to_string(), "online", "busy"),
5899 ("user_c".to_string(), "online", "free")
5900 ]
5901 );
5902 assert_eq!(
5903 contacts(&client_c, cx_c),
5904 [
5905 ("user_a".to_string(), "online", "busy"),
5906 ("user_b".to_string(), "online", "busy")
5907 ]
5908 );
5909 assert_eq!(contacts(&client_d, cx_d), []);
5910
5911 // Client B and client D become contacts while client B is being called.
5912 server
5913 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5914 .await;
5915 deterministic.run_until_parked();
5916 assert_eq!(
5917 contacts(&client_a, cx_a),
5918 [
5919 ("user_b".to_string(), "online", "busy"),
5920 ("user_c".to_string(), "online", "free")
5921 ]
5922 );
5923 assert_eq!(
5924 contacts(&client_b, cx_b),
5925 [
5926 ("user_a".to_string(), "online", "busy"),
5927 ("user_c".to_string(), "online", "free"),
5928 ("user_d".to_string(), "online", "free"),
5929 ]
5930 );
5931 assert_eq!(
5932 contacts(&client_c, cx_c),
5933 [
5934 ("user_a".to_string(), "online", "busy"),
5935 ("user_b".to_string(), "online", "busy")
5936 ]
5937 );
5938 assert_eq!(
5939 contacts(&client_d, cx_d),
5940 [("user_b".to_string(), "online", "busy")]
5941 );
5942
5943 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5944 deterministic.run_until_parked();
5945 assert_eq!(
5946 contacts(&client_a, cx_a),
5947 [
5948 ("user_b".to_string(), "online", "free"),
5949 ("user_c".to_string(), "online", "free")
5950 ]
5951 );
5952 assert_eq!(
5953 contacts(&client_b, cx_b),
5954 [
5955 ("user_a".to_string(), "online", "free"),
5956 ("user_c".to_string(), "online", "free"),
5957 ("user_d".to_string(), "online", "free")
5958 ]
5959 );
5960 assert_eq!(
5961 contacts(&client_c, cx_c),
5962 [
5963 ("user_a".to_string(), "online", "free"),
5964 ("user_b".to_string(), "online", "free")
5965 ]
5966 );
5967 assert_eq!(
5968 contacts(&client_d, cx_d),
5969 [("user_b".to_string(), "online", "free")]
5970 );
5971
5972 active_call_c
5973 .update(cx_c, |call, cx| {
5974 call.invite(client_a.user_id().unwrap(), None, cx)
5975 })
5976 .await
5977 .unwrap();
5978 deterministic.run_until_parked();
5979 assert_eq!(
5980 contacts(&client_a, cx_a),
5981 [
5982 ("user_b".to_string(), "online", "free"),
5983 ("user_c".to_string(), "online", "busy")
5984 ]
5985 );
5986 assert_eq!(
5987 contacts(&client_b, cx_b),
5988 [
5989 ("user_a".to_string(), "online", "busy"),
5990 ("user_c".to_string(), "online", "busy"),
5991 ("user_d".to_string(), "online", "free")
5992 ]
5993 );
5994 assert_eq!(
5995 contacts(&client_c, cx_c),
5996 [
5997 ("user_a".to_string(), "online", "busy"),
5998 ("user_b".to_string(), "online", "free")
5999 ]
6000 );
6001 assert_eq!(
6002 contacts(&client_d, cx_d),
6003 [("user_b".to_string(), "online", "free")]
6004 );
6005
6006 active_call_a
6007 .update(cx_a, |call, cx| call.accept_incoming(cx))
6008 .await
6009 .unwrap();
6010 deterministic.run_until_parked();
6011 assert_eq!(
6012 contacts(&client_a, cx_a),
6013 [
6014 ("user_b".to_string(), "online", "free"),
6015 ("user_c".to_string(), "online", "busy")
6016 ]
6017 );
6018 assert_eq!(
6019 contacts(&client_b, cx_b),
6020 [
6021 ("user_a".to_string(), "online", "busy"),
6022 ("user_c".to_string(), "online", "busy"),
6023 ("user_d".to_string(), "online", "free")
6024 ]
6025 );
6026 assert_eq!(
6027 contacts(&client_c, cx_c),
6028 [
6029 ("user_a".to_string(), "online", "busy"),
6030 ("user_b".to_string(), "online", "free")
6031 ]
6032 );
6033 assert_eq!(
6034 contacts(&client_d, cx_d),
6035 [("user_b".to_string(), "online", "free")]
6036 );
6037
6038 active_call_a
6039 .update(cx_a, |call, cx| {
6040 call.invite(client_b.user_id().unwrap(), None, cx)
6041 })
6042 .await
6043 .unwrap();
6044 deterministic.run_until_parked();
6045 assert_eq!(
6046 contacts(&client_a, cx_a),
6047 [
6048 ("user_b".to_string(), "online", "busy"),
6049 ("user_c".to_string(), "online", "busy")
6050 ]
6051 );
6052 assert_eq!(
6053 contacts(&client_b, cx_b),
6054 [
6055 ("user_a".to_string(), "online", "busy"),
6056 ("user_c".to_string(), "online", "busy"),
6057 ("user_d".to_string(), "online", "free")
6058 ]
6059 );
6060 assert_eq!(
6061 contacts(&client_c, cx_c),
6062 [
6063 ("user_a".to_string(), "online", "busy"),
6064 ("user_b".to_string(), "online", "busy")
6065 ]
6066 );
6067 assert_eq!(
6068 contacts(&client_d, cx_d),
6069 [("user_b".to_string(), "online", "busy")]
6070 );
6071
6072 active_call_a
6073 .update(cx_a, |call, cx| call.hang_up(cx))
6074 .await
6075 .unwrap();
6076 deterministic.run_until_parked();
6077 assert_eq!(
6078 contacts(&client_a, cx_a),
6079 [
6080 ("user_b".to_string(), "online", "free"),
6081 ("user_c".to_string(), "online", "free")
6082 ]
6083 );
6084 assert_eq!(
6085 contacts(&client_b, cx_b),
6086 [
6087 ("user_a".to_string(), "online", "free"),
6088 ("user_c".to_string(), "online", "free"),
6089 ("user_d".to_string(), "online", "free")
6090 ]
6091 );
6092 assert_eq!(
6093 contacts(&client_c, cx_c),
6094 [
6095 ("user_a".to_string(), "online", "free"),
6096 ("user_b".to_string(), "online", "free")
6097 ]
6098 );
6099 assert_eq!(
6100 contacts(&client_d, cx_d),
6101 [("user_b".to_string(), "online", "free")]
6102 );
6103
6104 active_call_a
6105 .update(cx_a, |call, cx| {
6106 call.invite(client_b.user_id().unwrap(), None, cx)
6107 })
6108 .await
6109 .unwrap();
6110 deterministic.run_until_parked();
6111 assert_eq!(
6112 contacts(&client_a, cx_a),
6113 [
6114 ("user_b".to_string(), "online", "busy"),
6115 ("user_c".to_string(), "online", "free")
6116 ]
6117 );
6118 assert_eq!(
6119 contacts(&client_b, cx_b),
6120 [
6121 ("user_a".to_string(), "online", "busy"),
6122 ("user_c".to_string(), "online", "free"),
6123 ("user_d".to_string(), "online", "free")
6124 ]
6125 );
6126 assert_eq!(
6127 contacts(&client_c, cx_c),
6128 [
6129 ("user_a".to_string(), "online", "busy"),
6130 ("user_b".to_string(), "online", "busy")
6131 ]
6132 );
6133 assert_eq!(
6134 contacts(&client_d, cx_d),
6135 [("user_b".to_string(), "online", "busy")]
6136 );
6137
6138 server.forbid_connections();
6139 server.disconnect_client(client_a.peer_id().unwrap());
6140 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
6141 assert_eq!(contacts(&client_a, cx_a), []);
6142 assert_eq!(
6143 contacts(&client_b, cx_b),
6144 [
6145 ("user_a".to_string(), "offline", "free"),
6146 ("user_c".to_string(), "online", "free"),
6147 ("user_d".to_string(), "online", "free")
6148 ]
6149 );
6150 assert_eq!(
6151 contacts(&client_c, cx_c),
6152 [
6153 ("user_a".to_string(), "offline", "free"),
6154 ("user_b".to_string(), "online", "free")
6155 ]
6156 );
6157 assert_eq!(
6158 contacts(&client_d, cx_d),
6159 [("user_b".to_string(), "online", "free")]
6160 );
6161
6162 // Test removing a contact
6163 client_b
6164 .user_store()
6165 .update(cx_b, |store, cx| {
6166 store.remove_contact(client_c.user_id().unwrap(), cx)
6167 })
6168 .await
6169 .unwrap();
6170 deterministic.run_until_parked();
6171 assert_eq!(
6172 contacts(&client_b, cx_b),
6173 [
6174 ("user_a".to_string(), "offline", "free"),
6175 ("user_d".to_string(), "online", "free")
6176 ]
6177 );
6178 assert_eq!(
6179 contacts(&client_c, cx_c),
6180 [("user_a".to_string(), "offline", "free"),]
6181 );
6182
6183 fn contacts(
6184 client: &TestClient,
6185 cx: &TestAppContext,
6186 ) -> Vec<(String, &'static str, &'static str)> {
6187 client.user_store().read_with(cx, |store, _| {
6188 store
6189 .contacts()
6190 .iter()
6191 .map(|contact| {
6192 (
6193 contact.user.github_login.clone(),
6194 if contact.online { "online" } else { "offline" },
6195 if contact.busy { "busy" } else { "free" },
6196 )
6197 })
6198 .collect()
6199 })
6200 }
6201}
6202
6203#[gpui::test(iterations = 10)]
6204async fn test_contact_requests(
6205 deterministic: Arc<Deterministic>,
6206 cx_a: &mut TestAppContext,
6207 cx_a2: &mut TestAppContext,
6208 cx_b: &mut TestAppContext,
6209 cx_b2: &mut TestAppContext,
6210 cx_c: &mut TestAppContext,
6211 cx_c2: &mut TestAppContext,
6212) {
6213 deterministic.forbid_parking();
6214
6215 // Connect to a server as 3 clients.
6216 let mut server = TestServer::start(&deterministic).await;
6217 let client_a = server.create_client(cx_a, "user_a").await;
6218 let client_a2 = server.create_client(cx_a2, "user_a").await;
6219 let client_b = server.create_client(cx_b, "user_b").await;
6220 let client_b2 = server.create_client(cx_b2, "user_b").await;
6221 let client_c = server.create_client(cx_c, "user_c").await;
6222 let client_c2 = server.create_client(cx_c2, "user_c").await;
6223
6224 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
6225 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
6226 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
6227
6228 // User A and User C request that user B become their contact.
6229 client_a
6230 .user_store()
6231 .update(cx_a, |store, cx| {
6232 store.request_contact(client_b.user_id().unwrap(), cx)
6233 })
6234 .await
6235 .unwrap();
6236 client_c
6237 .user_store()
6238 .update(cx_c, |store, cx| {
6239 store.request_contact(client_b.user_id().unwrap(), cx)
6240 })
6241 .await
6242 .unwrap();
6243 deterministic.run_until_parked();
6244
6245 // All users see the pending request appear in all their clients.
6246 assert_eq!(
6247 client_a.summarize_contacts(cx_a).outgoing_requests,
6248 &["user_b"]
6249 );
6250 assert_eq!(
6251 client_a2.summarize_contacts(cx_a2).outgoing_requests,
6252 &["user_b"]
6253 );
6254 assert_eq!(
6255 client_b.summarize_contacts(cx_b).incoming_requests,
6256 &["user_a", "user_c"]
6257 );
6258 assert_eq!(
6259 client_b2.summarize_contacts(cx_b2).incoming_requests,
6260 &["user_a", "user_c"]
6261 );
6262 assert_eq!(
6263 client_c.summarize_contacts(cx_c).outgoing_requests,
6264 &["user_b"]
6265 );
6266 assert_eq!(
6267 client_c2.summarize_contacts(cx_c2).outgoing_requests,
6268 &["user_b"]
6269 );
6270
6271 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
6272 disconnect_and_reconnect(&client_a, cx_a).await;
6273 disconnect_and_reconnect(&client_b, cx_b).await;
6274 disconnect_and_reconnect(&client_c, cx_c).await;
6275 deterministic.run_until_parked();
6276 assert_eq!(
6277 client_a.summarize_contacts(cx_a).outgoing_requests,
6278 &["user_b"]
6279 );
6280 assert_eq!(
6281 client_b.summarize_contacts(cx_b).incoming_requests,
6282 &["user_a", "user_c"]
6283 );
6284 assert_eq!(
6285 client_c.summarize_contacts(cx_c).outgoing_requests,
6286 &["user_b"]
6287 );
6288
6289 // User B accepts the request from user A.
6290 client_b
6291 .user_store()
6292 .update(cx_b, |store, cx| {
6293 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
6294 })
6295 .await
6296 .unwrap();
6297
6298 deterministic.run_until_parked();
6299
6300 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
6301 let contacts_b = client_b.summarize_contacts(cx_b);
6302 assert_eq!(contacts_b.current, &["user_a"]);
6303 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
6304 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6305 assert_eq!(contacts_b2.current, &["user_a"]);
6306 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
6307
6308 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
6309 let contacts_a = client_a.summarize_contacts(cx_a);
6310 assert_eq!(contacts_a.current, &["user_b"]);
6311 assert!(contacts_a.outgoing_requests.is_empty());
6312 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
6313 assert_eq!(contacts_a2.current, &["user_b"]);
6314 assert!(contacts_a2.outgoing_requests.is_empty());
6315
6316 // Contacts are present upon connecting (tested here via disconnect/reconnect)
6317 disconnect_and_reconnect(&client_a, cx_a).await;
6318 disconnect_and_reconnect(&client_b, cx_b).await;
6319 disconnect_and_reconnect(&client_c, cx_c).await;
6320 deterministic.run_until_parked();
6321 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6322 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6323 assert_eq!(
6324 client_b.summarize_contacts(cx_b).incoming_requests,
6325 &["user_c"]
6326 );
6327 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6328 assert_eq!(
6329 client_c.summarize_contacts(cx_c).outgoing_requests,
6330 &["user_b"]
6331 );
6332
6333 // User B rejects the request from user C.
6334 client_b
6335 .user_store()
6336 .update(cx_b, |store, cx| {
6337 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
6338 })
6339 .await
6340 .unwrap();
6341
6342 deterministic.run_until_parked();
6343
6344 // User B doesn't see user C as their contact, and the incoming request from them is removed.
6345 let contacts_b = client_b.summarize_contacts(cx_b);
6346 assert_eq!(contacts_b.current, &["user_a"]);
6347 assert!(contacts_b.incoming_requests.is_empty());
6348 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
6349 assert_eq!(contacts_b2.current, &["user_a"]);
6350 assert!(contacts_b2.incoming_requests.is_empty());
6351
6352 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
6353 let contacts_c = client_c.summarize_contacts(cx_c);
6354 assert!(contacts_c.current.is_empty());
6355 assert!(contacts_c.outgoing_requests.is_empty());
6356 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
6357 assert!(contacts_c2.current.is_empty());
6358 assert!(contacts_c2.outgoing_requests.is_empty());
6359
6360 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
6361 disconnect_and_reconnect(&client_a, cx_a).await;
6362 disconnect_and_reconnect(&client_b, cx_b).await;
6363 disconnect_and_reconnect(&client_c, cx_c).await;
6364 deterministic.run_until_parked();
6365 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
6366 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
6367 assert!(client_b
6368 .summarize_contacts(cx_b)
6369 .incoming_requests
6370 .is_empty());
6371 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6372 assert!(client_c
6373 .summarize_contacts(cx_c)
6374 .outgoing_requests
6375 .is_empty());
6376
6377 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6378 client.disconnect(&cx.to_async());
6379 client.clear_contacts(cx).await;
6380 client
6381 .authenticate_and_connect(false, &cx.to_async())
6382 .await
6383 .unwrap();
6384 }
6385}
6386
6387#[gpui::test(iterations = 10)]
6388async fn test_basic_following(
6389 deterministic: Arc<Deterministic>,
6390 cx_a: &mut TestAppContext,
6391 cx_b: &mut TestAppContext,
6392 cx_c: &mut TestAppContext,
6393 cx_d: &mut TestAppContext,
6394) {
6395 deterministic.forbid_parking();
6396
6397 let mut server = TestServer::start(&deterministic).await;
6398 let client_a = server.create_client(cx_a, "user_a").await;
6399 let client_b = server.create_client(cx_b, "user_b").await;
6400 let client_c = server.create_client(cx_c, "user_c").await;
6401 let client_d = server.create_client(cx_d, "user_d").await;
6402 server
6403 .create_room(&mut [
6404 (&client_a, cx_a),
6405 (&client_b, cx_b),
6406 (&client_c, cx_c),
6407 (&client_d, cx_d),
6408 ])
6409 .await;
6410 let active_call_a = cx_a.read(ActiveCall::global);
6411 let active_call_b = cx_b.read(ActiveCall::global);
6412
6413 cx_a.update(editor::init);
6414 cx_b.update(editor::init);
6415
6416 client_a
6417 .fs()
6418 .insert_tree(
6419 "/a",
6420 json!({
6421 "1.txt": "one\none\none",
6422 "2.txt": "two\ntwo\ntwo",
6423 "3.txt": "three\nthree\nthree",
6424 }),
6425 )
6426 .await;
6427 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6428 active_call_a
6429 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6430 .await
6431 .unwrap();
6432
6433 let project_id = active_call_a
6434 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6435 .await
6436 .unwrap();
6437 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6438 active_call_b
6439 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6440 .await
6441 .unwrap();
6442
6443 let window_a = client_a.build_workspace(&project_a, cx_a);
6444 let workspace_a = window_a.root(cx_a);
6445 let window_b = client_b.build_workspace(&project_b, cx_b);
6446 let workspace_b = window_b.root(cx_b);
6447
6448 // Client A opens some editors.
6449 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6450 let editor_a1 = workspace_a
6451 .update(cx_a, |workspace, cx| {
6452 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6453 })
6454 .await
6455 .unwrap()
6456 .downcast::<Editor>()
6457 .unwrap();
6458 let editor_a2 = workspace_a
6459 .update(cx_a, |workspace, cx| {
6460 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6461 })
6462 .await
6463 .unwrap()
6464 .downcast::<Editor>()
6465 .unwrap();
6466
6467 // Client B opens an editor.
6468 let editor_b1 = workspace_b
6469 .update(cx_b, |workspace, cx| {
6470 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6471 })
6472 .await
6473 .unwrap()
6474 .downcast::<Editor>()
6475 .unwrap();
6476
6477 let peer_id_a = client_a.peer_id().unwrap();
6478 let peer_id_b = client_b.peer_id().unwrap();
6479 let peer_id_c = client_c.peer_id().unwrap();
6480 let peer_id_d = client_d.peer_id().unwrap();
6481
6482 // Client A updates their selections in those editors
6483 editor_a1.update(cx_a, |editor, cx| {
6484 editor.handle_input("a", cx);
6485 editor.handle_input("b", cx);
6486 editor.handle_input("c", cx);
6487 editor.select_left(&Default::default(), cx);
6488 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
6489 });
6490 editor_a2.update(cx_a, |editor, cx| {
6491 editor.handle_input("d", cx);
6492 editor.handle_input("e", cx);
6493 editor.select_left(&Default::default(), cx);
6494 assert_eq!(editor.selections.ranges(cx), vec![2..1]);
6495 });
6496
6497 // When client B starts following client A, all visible view states are replicated to client B.
6498 workspace_b
6499 .update(cx_b, |workspace, cx| {
6500 workspace.toggle_follow(peer_id_a, cx).unwrap()
6501 })
6502 .await
6503 .unwrap();
6504
6505 cx_c.foreground().run_until_parked();
6506 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
6507 workspace
6508 .active_item(cx)
6509 .unwrap()
6510 .downcast::<Editor>()
6511 .unwrap()
6512 });
6513 assert_eq!(
6514 cx_b.read(|cx| editor_b2.project_path(cx)),
6515 Some((worktree_id, "2.txt").into())
6516 );
6517 assert_eq!(
6518 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
6519 vec![2..1]
6520 );
6521 assert_eq!(
6522 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
6523 vec![3..2]
6524 );
6525
6526 cx_c.foreground().run_until_parked();
6527 let active_call_c = cx_c.read(ActiveCall::global);
6528 let project_c = client_c.build_remote_project(project_id, cx_c).await;
6529 let window_c = client_c.build_workspace(&project_c, cx_c);
6530 let workspace_c = window_c.root(cx_c);
6531 active_call_c
6532 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
6533 .await
6534 .unwrap();
6535 drop(project_c);
6536
6537 // Client C also follows client A.
6538 workspace_c
6539 .update(cx_c, |workspace, cx| {
6540 workspace.toggle_follow(peer_id_a, cx).unwrap()
6541 })
6542 .await
6543 .unwrap();
6544
6545 cx_d.foreground().run_until_parked();
6546 let active_call_d = cx_d.read(ActiveCall::global);
6547 let project_d = client_d.build_remote_project(project_id, cx_d).await;
6548 let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
6549 active_call_d
6550 .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
6551 .await
6552 .unwrap();
6553 drop(project_d);
6554
6555 // All clients see that clients B and C are following client A.
6556 cx_c.foreground().run_until_parked();
6557 for (name, active_call, cx) in [
6558 ("A", &active_call_a, &cx_a),
6559 ("B", &active_call_b, &cx_b),
6560 ("C", &active_call_c, &cx_c),
6561 ("D", &active_call_d, &cx_d),
6562 ] {
6563 active_call.read_with(*cx, |call, cx| {
6564 let room = call.room().unwrap().read(cx);
6565 assert_eq!(
6566 room.followers_for(peer_id_a, project_id),
6567 &[peer_id_b, peer_id_c],
6568 "checking followers for A as {name}"
6569 );
6570 });
6571 }
6572
6573 // Client C unfollows client A.
6574 workspace_c.update(cx_c, |workspace, cx| {
6575 workspace.toggle_follow(peer_id_a, cx);
6576 });
6577
6578 // All clients see that clients B is following client A.
6579 cx_c.foreground().run_until_parked();
6580 for (name, active_call, cx) in [
6581 ("A", &active_call_a, &cx_a),
6582 ("B", &active_call_b, &cx_b),
6583 ("C", &active_call_c, &cx_c),
6584 ("D", &active_call_d, &cx_d),
6585 ] {
6586 active_call.read_with(*cx, |call, cx| {
6587 let room = call.room().unwrap().read(cx);
6588 assert_eq!(
6589 room.followers_for(peer_id_a, project_id),
6590 &[peer_id_b],
6591 "checking followers for A as {name}"
6592 );
6593 });
6594 }
6595
6596 // Client C re-follows client A.
6597 workspace_c.update(cx_c, |workspace, cx| {
6598 workspace.toggle_follow(peer_id_a, cx);
6599 });
6600
6601 // All clients see that clients B and C are following client A.
6602 cx_c.foreground().run_until_parked();
6603 for (name, active_call, cx) in [
6604 ("A", &active_call_a, &cx_a),
6605 ("B", &active_call_b, &cx_b),
6606 ("C", &active_call_c, &cx_c),
6607 ("D", &active_call_d, &cx_d),
6608 ] {
6609 active_call.read_with(*cx, |call, cx| {
6610 let room = call.room().unwrap().read(cx);
6611 assert_eq!(
6612 room.followers_for(peer_id_a, project_id),
6613 &[peer_id_b, peer_id_c],
6614 "checking followers for A as {name}"
6615 );
6616 });
6617 }
6618
6619 // Client D follows client C.
6620 workspace_d
6621 .update(cx_d, |workspace, cx| {
6622 workspace.toggle_follow(peer_id_c, cx).unwrap()
6623 })
6624 .await
6625 .unwrap();
6626
6627 // All clients see that D is following C
6628 cx_d.foreground().run_until_parked();
6629 for (name, active_call, cx) in [
6630 ("A", &active_call_a, &cx_a),
6631 ("B", &active_call_b, &cx_b),
6632 ("C", &active_call_c, &cx_c),
6633 ("D", &active_call_d, &cx_d),
6634 ] {
6635 active_call.read_with(*cx, |call, cx| {
6636 let room = call.room().unwrap().read(cx);
6637 assert_eq!(
6638 room.followers_for(peer_id_c, project_id),
6639 &[peer_id_d],
6640 "checking followers for C as {name}"
6641 );
6642 });
6643 }
6644
6645 // Client C closes the project.
6646 window_c.remove(cx_c);
6647 cx_c.drop_last(workspace_c);
6648
6649 // Clients A and B see that client B is following A, and client C is not present in the followers.
6650 cx_c.foreground().run_until_parked();
6651 for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
6652 active_call.read_with(*cx, |call, cx| {
6653 let room = call.room().unwrap().read(cx);
6654 assert_eq!(
6655 room.followers_for(peer_id_a, project_id),
6656 &[peer_id_b],
6657 "checking followers for A as {name}"
6658 );
6659 });
6660 }
6661
6662 // All clients see that no-one is following C
6663 for (name, active_call, cx) in [
6664 ("A", &active_call_a, &cx_a),
6665 ("B", &active_call_b, &cx_b),
6666 ("C", &active_call_c, &cx_c),
6667 ("D", &active_call_d, &cx_d),
6668 ] {
6669 active_call.read_with(*cx, |call, cx| {
6670 let room = call.room().unwrap().read(cx);
6671 assert_eq!(
6672 room.followers_for(peer_id_c, project_id),
6673 &[],
6674 "checking followers for C as {name}"
6675 );
6676 });
6677 }
6678
6679 // When client A activates a different editor, client B does so as well.
6680 workspace_a.update(cx_a, |workspace, cx| {
6681 workspace.activate_item(&editor_a1, cx)
6682 });
6683 deterministic.run_until_parked();
6684 workspace_b.read_with(cx_b, |workspace, cx| {
6685 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6686 });
6687
6688 // When client A opens a multibuffer, client B does so as well.
6689 let multibuffer_a = cx_a.add_model(|cx| {
6690 let buffer_a1 = project_a.update(cx, |project, cx| {
6691 project
6692 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
6693 .unwrap()
6694 });
6695 let buffer_a2 = project_a.update(cx, |project, cx| {
6696 project
6697 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
6698 .unwrap()
6699 });
6700 let mut result = MultiBuffer::new(0);
6701 result.push_excerpts(
6702 buffer_a1,
6703 [ExcerptRange {
6704 context: 0..3,
6705 primary: None,
6706 }],
6707 cx,
6708 );
6709 result.push_excerpts(
6710 buffer_a2,
6711 [ExcerptRange {
6712 context: 4..7,
6713 primary: None,
6714 }],
6715 cx,
6716 );
6717 result
6718 });
6719 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
6720 let editor =
6721 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
6722 workspace.add_item(Box::new(editor.clone()), cx);
6723 editor
6724 });
6725 deterministic.run_until_parked();
6726 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
6727 workspace
6728 .active_item(cx)
6729 .unwrap()
6730 .downcast::<Editor>()
6731 .unwrap()
6732 });
6733 assert_eq!(
6734 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
6735 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
6736 );
6737
6738 // When client A navigates back and forth, client B does so as well.
6739 workspace_a
6740 .update(cx_a, |workspace, cx| {
6741 workspace.go_back(workspace.active_pane().downgrade(), cx)
6742 })
6743 .await
6744 .unwrap();
6745 deterministic.run_until_parked();
6746 workspace_b.read_with(cx_b, |workspace, cx| {
6747 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6748 });
6749
6750 workspace_a
6751 .update(cx_a, |workspace, cx| {
6752 workspace.go_back(workspace.active_pane().downgrade(), cx)
6753 })
6754 .await
6755 .unwrap();
6756 deterministic.run_until_parked();
6757 workspace_b.read_with(cx_b, |workspace, cx| {
6758 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
6759 });
6760
6761 workspace_a
6762 .update(cx_a, |workspace, cx| {
6763 workspace.go_forward(workspace.active_pane().downgrade(), cx)
6764 })
6765 .await
6766 .unwrap();
6767 deterministic.run_until_parked();
6768 workspace_b.read_with(cx_b, |workspace, cx| {
6769 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6770 });
6771
6772 // Changes to client A's editor are reflected on client B.
6773 editor_a1.update(cx_a, |editor, cx| {
6774 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
6775 });
6776 deterministic.run_until_parked();
6777 editor_b1.read_with(cx_b, |editor, cx| {
6778 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
6779 });
6780
6781 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
6782 deterministic.run_until_parked();
6783 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
6784
6785 editor_a1.update(cx_a, |editor, cx| {
6786 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
6787 editor.set_scroll_position(vec2f(0., 100.), cx);
6788 });
6789 deterministic.run_until_parked();
6790 editor_b1.read_with(cx_b, |editor, cx| {
6791 assert_eq!(editor.selections.ranges(cx), &[3..3]);
6792 });
6793
6794 // After unfollowing, client B stops receiving updates from client A.
6795 workspace_b.update(cx_b, |workspace, cx| {
6796 workspace.unfollow(&workspace.active_pane().clone(), cx)
6797 });
6798 workspace_a.update(cx_a, |workspace, cx| {
6799 workspace.activate_item(&editor_a2, cx)
6800 });
6801 deterministic.run_until_parked();
6802 assert_eq!(
6803 workspace_b.read_with(cx_b, |workspace, cx| workspace
6804 .active_item(cx)
6805 .unwrap()
6806 .id()),
6807 editor_b1.id()
6808 );
6809
6810 // Client A starts following client B.
6811 workspace_a
6812 .update(cx_a, |workspace, cx| {
6813 workspace.toggle_follow(peer_id_b, cx).unwrap()
6814 })
6815 .await
6816 .unwrap();
6817 assert_eq!(
6818 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6819 Some(peer_id_b)
6820 );
6821 assert_eq!(
6822 workspace_a.read_with(cx_a, |workspace, cx| workspace
6823 .active_item(cx)
6824 .unwrap()
6825 .id()),
6826 editor_a1.id()
6827 );
6828
6829 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
6830 let display = MacOSDisplay::new();
6831 active_call_b
6832 .update(cx_b, |call, cx| call.set_location(None, cx))
6833 .await
6834 .unwrap();
6835 active_call_b
6836 .update(cx_b, |call, cx| {
6837 call.room().unwrap().update(cx, |room, cx| {
6838 room.set_display_sources(vec![display.clone()]);
6839 room.share_screen(cx)
6840 })
6841 })
6842 .await
6843 .unwrap();
6844 deterministic.run_until_parked();
6845 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
6846 workspace
6847 .active_item(cx)
6848 .unwrap()
6849 .downcast::<SharedScreen>()
6850 .unwrap()
6851 });
6852
6853 // Client B activates Zed again, which causes the previous editor to become focused again.
6854 active_call_b
6855 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6856 .await
6857 .unwrap();
6858 deterministic.run_until_parked();
6859 workspace_a.read_with(cx_a, |workspace, cx| {
6860 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
6861 });
6862
6863 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
6864 workspace_b.update(cx_b, |workspace, cx| {
6865 workspace.activate_item(&multibuffer_editor_b, cx)
6866 });
6867 deterministic.run_until_parked();
6868 workspace_a.read_with(cx_a, |workspace, cx| {
6869 assert_eq!(
6870 workspace.active_item(cx).unwrap().id(),
6871 multibuffer_editor_a.id()
6872 )
6873 });
6874
6875 // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
6876 let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
6877 workspace_b.update(cx_b, |workspace, cx| {
6878 workspace.add_panel(panel, cx);
6879 workspace.toggle_panel_focus::<TestPanel>(cx);
6880 });
6881 deterministic.run_until_parked();
6882 assert_eq!(
6883 workspace_a.read_with(cx_a, |workspace, cx| workspace
6884 .active_item(cx)
6885 .unwrap()
6886 .id()),
6887 shared_screen.id()
6888 );
6889
6890 // Toggling the focus back to the pane causes client A to return to the multibuffer.
6891 workspace_b.update(cx_b, |workspace, cx| {
6892 workspace.toggle_panel_focus::<TestPanel>(cx);
6893 });
6894 deterministic.run_until_parked();
6895 workspace_a.read_with(cx_a, |workspace, cx| {
6896 assert_eq!(
6897 workspace.active_item(cx).unwrap().id(),
6898 multibuffer_editor_a.id()
6899 )
6900 });
6901
6902 // Client B activates an item that doesn't implement following,
6903 // so the previously-opened screen-sharing item gets activated.
6904 let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
6905 workspace_b.update(cx_b, |workspace, cx| {
6906 workspace.active_pane().update(cx, |pane, cx| {
6907 pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
6908 })
6909 });
6910 deterministic.run_until_parked();
6911 assert_eq!(
6912 workspace_a.read_with(cx_a, |workspace, cx| workspace
6913 .active_item(cx)
6914 .unwrap()
6915 .id()),
6916 shared_screen.id()
6917 );
6918
6919 // Following interrupts when client B disconnects.
6920 client_b.disconnect(&cx_b.to_async());
6921 deterministic.advance_clock(RECONNECT_TIMEOUT);
6922 assert_eq!(
6923 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6924 None
6925 );
6926}
6927
6928#[gpui::test(iterations = 10)]
6929async fn test_join_call_after_screen_was_shared(
6930 deterministic: Arc<Deterministic>,
6931 cx_a: &mut TestAppContext,
6932 cx_b: &mut TestAppContext,
6933) {
6934 deterministic.forbid_parking();
6935 let mut server = TestServer::start(&deterministic).await;
6936
6937 let client_a = server.create_client(cx_a, "user_a").await;
6938 let client_b = server.create_client(cx_b, "user_b").await;
6939 server
6940 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6941 .await;
6942
6943 let active_call_a = cx_a.read(ActiveCall::global);
6944 let active_call_b = cx_b.read(ActiveCall::global);
6945
6946 // Call users B and C from client A.
6947 active_call_a
6948 .update(cx_a, |call, cx| {
6949 call.invite(client_b.user_id().unwrap(), None, cx)
6950 })
6951 .await
6952 .unwrap();
6953 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6954 deterministic.run_until_parked();
6955 assert_eq!(
6956 room_participants(&room_a, cx_a),
6957 RoomParticipants {
6958 remote: Default::default(),
6959 pending: vec!["user_b".to_string()]
6960 }
6961 );
6962
6963 // User B receives the call.
6964 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6965 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6966 assert_eq!(call_b.calling_user.github_login, "user_a");
6967
6968 // User A shares their screen
6969 let display = MacOSDisplay::new();
6970 active_call_a
6971 .update(cx_a, |call, cx| {
6972 call.room().unwrap().update(cx, |room, cx| {
6973 room.set_display_sources(vec![display.clone()]);
6974 room.share_screen(cx)
6975 })
6976 })
6977 .await
6978 .unwrap();
6979
6980 client_b.user_store().update(cx_b, |user_store, _| {
6981 user_store.clear_cache();
6982 });
6983
6984 // User B joins the room
6985 active_call_b
6986 .update(cx_b, |call, cx| call.accept_incoming(cx))
6987 .await
6988 .unwrap();
6989 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6990 assert!(incoming_call_b.next().await.unwrap().is_none());
6991
6992 deterministic.run_until_parked();
6993 assert_eq!(
6994 room_participants(&room_a, cx_a),
6995 RoomParticipants {
6996 remote: vec!["user_b".to_string()],
6997 pending: vec![],
6998 }
6999 );
7000 assert_eq!(
7001 room_participants(&room_b, cx_b),
7002 RoomParticipants {
7003 remote: vec!["user_a".to_string()],
7004 pending: vec![],
7005 }
7006 );
7007
7008 // Ensure User B sees User A's screenshare.
7009 room_b.read_with(cx_b, |room, _| {
7010 assert_eq!(
7011 room.remote_participants()
7012 .get(&client_a.user_id().unwrap())
7013 .unwrap()
7014 .video_tracks
7015 .len(),
7016 1
7017 );
7018 });
7019}
7020
7021#[gpui::test]
7022async fn test_following_tab_order(
7023 deterministic: Arc<Deterministic>,
7024 cx_a: &mut TestAppContext,
7025 cx_b: &mut TestAppContext,
7026) {
7027 let mut server = TestServer::start(&deterministic).await;
7028 let client_a = server.create_client(cx_a, "user_a").await;
7029 let client_b = server.create_client(cx_b, "user_b").await;
7030 server
7031 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7032 .await;
7033 let active_call_a = cx_a.read(ActiveCall::global);
7034 let active_call_b = cx_b.read(ActiveCall::global);
7035
7036 cx_a.update(editor::init);
7037 cx_b.update(editor::init);
7038
7039 client_a
7040 .fs()
7041 .insert_tree(
7042 "/a",
7043 json!({
7044 "1.txt": "one",
7045 "2.txt": "two",
7046 "3.txt": "three",
7047 }),
7048 )
7049 .await;
7050 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7051 active_call_a
7052 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7053 .await
7054 .unwrap();
7055
7056 let project_id = active_call_a
7057 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7058 .await
7059 .unwrap();
7060 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7061 active_call_b
7062 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7063 .await
7064 .unwrap();
7065
7066 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
7067 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
7068
7069 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
7070 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
7071
7072 let client_b_id = project_a.read_with(cx_a, |project, _| {
7073 project.collaborators().values().next().unwrap().peer_id
7074 });
7075
7076 //Open 1, 3 in that order on client A
7077 workspace_a
7078 .update(cx_a, |workspace, cx| {
7079 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
7080 })
7081 .await
7082 .unwrap();
7083 workspace_a
7084 .update(cx_a, |workspace, cx| {
7085 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
7086 })
7087 .await
7088 .unwrap();
7089
7090 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
7091 pane.update(cx, |pane, cx| {
7092 pane.items()
7093 .map(|item| {
7094 item.project_path(cx)
7095 .unwrap()
7096 .path
7097 .to_str()
7098 .unwrap()
7099 .to_owned()
7100 })
7101 .collect::<Vec<_>>()
7102 })
7103 };
7104
7105 //Verify that the tabs opened in the order we expect
7106 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
7107
7108 //Follow client B as client A
7109 workspace_a
7110 .update(cx_a, |workspace, cx| {
7111 workspace.toggle_follow(client_b_id, cx).unwrap()
7112 })
7113 .await
7114 .unwrap();
7115
7116 //Open just 2 on client B
7117 workspace_b
7118 .update(cx_b, |workspace, cx| {
7119 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
7120 })
7121 .await
7122 .unwrap();
7123 deterministic.run_until_parked();
7124
7125 // Verify that newly opened followed file is at the end
7126 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
7127
7128 //Open just 1 on client B
7129 workspace_b
7130 .update(cx_b, |workspace, cx| {
7131 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
7132 })
7133 .await
7134 .unwrap();
7135 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
7136 deterministic.run_until_parked();
7137
7138 // Verify that following into 1 did not reorder
7139 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
7140}
7141
7142#[gpui::test(iterations = 10)]
7143async fn test_peers_following_each_other(
7144 deterministic: Arc<Deterministic>,
7145 cx_a: &mut TestAppContext,
7146 cx_b: &mut TestAppContext,
7147) {
7148 deterministic.forbid_parking();
7149 let mut server = TestServer::start(&deterministic).await;
7150 let client_a = server.create_client(cx_a, "user_a").await;
7151 let client_b = server.create_client(cx_b, "user_b").await;
7152 server
7153 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7154 .await;
7155 let active_call_a = cx_a.read(ActiveCall::global);
7156 let active_call_b = cx_b.read(ActiveCall::global);
7157
7158 cx_a.update(editor::init);
7159 cx_b.update(editor::init);
7160
7161 // Client A shares a project.
7162 client_a
7163 .fs()
7164 .insert_tree(
7165 "/a",
7166 json!({
7167 "1.txt": "one",
7168 "2.txt": "two",
7169 "3.txt": "three",
7170 "4.txt": "four",
7171 }),
7172 )
7173 .await;
7174 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7175 active_call_a
7176 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7177 .await
7178 .unwrap();
7179 let project_id = active_call_a
7180 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7181 .await
7182 .unwrap();
7183
7184 // Client B joins the project.
7185 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7186 active_call_b
7187 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7188 .await
7189 .unwrap();
7190
7191 // Client A opens some editors.
7192 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
7193 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
7194 let _editor_a1 = workspace_a
7195 .update(cx_a, |workspace, cx| {
7196 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
7197 })
7198 .await
7199 .unwrap()
7200 .downcast::<Editor>()
7201 .unwrap();
7202
7203 // Client B opens an editor.
7204 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
7205 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
7206 let _editor_b1 = workspace_b
7207 .update(cx_b, |workspace, cx| {
7208 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
7209 })
7210 .await
7211 .unwrap()
7212 .downcast::<Editor>()
7213 .unwrap();
7214
7215 // Clients A and B follow each other in split panes
7216 workspace_a.update(cx_a, |workspace, cx| {
7217 workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
7218 });
7219 workspace_a
7220 .update(cx_a, |workspace, cx| {
7221 assert_ne!(*workspace.active_pane(), pane_a1);
7222 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
7223 workspace.toggle_follow(leader_id, cx).unwrap()
7224 })
7225 .await
7226 .unwrap();
7227 workspace_b.update(cx_b, |workspace, cx| {
7228 workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
7229 });
7230 workspace_b
7231 .update(cx_b, |workspace, cx| {
7232 assert_ne!(*workspace.active_pane(), pane_b1);
7233 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
7234 workspace.toggle_follow(leader_id, cx).unwrap()
7235 })
7236 .await
7237 .unwrap();
7238
7239 workspace_a.update(cx_a, |workspace, cx| {
7240 workspace.activate_next_pane(cx);
7241 });
7242 // Wait for focus effects to be fully flushed
7243 workspace_a.update(cx_a, |workspace, _| {
7244 assert_eq!(*workspace.active_pane(), pane_a1);
7245 });
7246
7247 workspace_a
7248 .update(cx_a, |workspace, cx| {
7249 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
7250 })
7251 .await
7252 .unwrap();
7253 workspace_b.update(cx_b, |workspace, cx| {
7254 workspace.activate_next_pane(cx);
7255 });
7256
7257 workspace_b
7258 .update(cx_b, |workspace, cx| {
7259 assert_eq!(*workspace.active_pane(), pane_b1);
7260 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
7261 })
7262 .await
7263 .unwrap();
7264 cx_a.foreground().run_until_parked();
7265
7266 // Ensure leader updates don't change the active pane of followers
7267 workspace_a.read_with(cx_a, |workspace, _| {
7268 assert_eq!(*workspace.active_pane(), pane_a1);
7269 });
7270 workspace_b.read_with(cx_b, |workspace, _| {
7271 assert_eq!(*workspace.active_pane(), pane_b1);
7272 });
7273
7274 // Ensure peers following each other doesn't cause an infinite loop.
7275 assert_eq!(
7276 workspace_a.read_with(cx_a, |workspace, cx| workspace
7277 .active_item(cx)
7278 .unwrap()
7279 .project_path(cx)),
7280 Some((worktree_id, "3.txt").into())
7281 );
7282 workspace_a.update(cx_a, |workspace, cx| {
7283 assert_eq!(
7284 workspace.active_item(cx).unwrap().project_path(cx),
7285 Some((worktree_id, "3.txt").into())
7286 );
7287 workspace.activate_next_pane(cx);
7288 });
7289
7290 workspace_a.update(cx_a, |workspace, cx| {
7291 assert_eq!(
7292 workspace.active_item(cx).unwrap().project_path(cx),
7293 Some((worktree_id, "4.txt").into())
7294 );
7295 });
7296
7297 workspace_b.update(cx_b, |workspace, cx| {
7298 assert_eq!(
7299 workspace.active_item(cx).unwrap().project_path(cx),
7300 Some((worktree_id, "4.txt").into())
7301 );
7302 workspace.activate_next_pane(cx);
7303 });
7304
7305 workspace_b.update(cx_b, |workspace, cx| {
7306 assert_eq!(
7307 workspace.active_item(cx).unwrap().project_path(cx),
7308 Some((worktree_id, "3.txt").into())
7309 );
7310 });
7311}
7312
7313#[gpui::test(iterations = 10)]
7314async fn test_auto_unfollowing(
7315 deterministic: Arc<Deterministic>,
7316 cx_a: &mut TestAppContext,
7317 cx_b: &mut TestAppContext,
7318) {
7319 deterministic.forbid_parking();
7320
7321 // 2 clients connect to a server.
7322 let mut server = TestServer::start(&deterministic).await;
7323 let client_a = server.create_client(cx_a, "user_a").await;
7324 let client_b = server.create_client(cx_b, "user_b").await;
7325 server
7326 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7327 .await;
7328 let active_call_a = cx_a.read(ActiveCall::global);
7329 let active_call_b = cx_b.read(ActiveCall::global);
7330
7331 cx_a.update(editor::init);
7332 cx_b.update(editor::init);
7333
7334 // Client A shares a project.
7335 client_a
7336 .fs()
7337 .insert_tree(
7338 "/a",
7339 json!({
7340 "1.txt": "one",
7341 "2.txt": "two",
7342 "3.txt": "three",
7343 }),
7344 )
7345 .await;
7346 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7347 active_call_a
7348 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7349 .await
7350 .unwrap();
7351
7352 let project_id = active_call_a
7353 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7354 .await
7355 .unwrap();
7356 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7357 active_call_b
7358 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7359 .await
7360 .unwrap();
7361
7362 // Client A opens some editors.
7363 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
7364 let _editor_a1 = workspace_a
7365 .update(cx_a, |workspace, cx| {
7366 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
7367 })
7368 .await
7369 .unwrap()
7370 .downcast::<Editor>()
7371 .unwrap();
7372
7373 // Client B starts following client A.
7374 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
7375 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
7376 let leader_id = project_b.read_with(cx_b, |project, _| {
7377 project.collaborators().values().next().unwrap().peer_id
7378 });
7379 workspace_b
7380 .update(cx_b, |workspace, cx| {
7381 workspace.toggle_follow(leader_id, cx).unwrap()
7382 })
7383 .await
7384 .unwrap();
7385 assert_eq!(
7386 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7387 Some(leader_id)
7388 );
7389 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
7390 workspace
7391 .active_item(cx)
7392 .unwrap()
7393 .downcast::<Editor>()
7394 .unwrap()
7395 });
7396
7397 // When client B moves, it automatically stops following client A.
7398 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
7399 assert_eq!(
7400 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7401 None
7402 );
7403
7404 workspace_b
7405 .update(cx_b, |workspace, cx| {
7406 workspace.toggle_follow(leader_id, cx).unwrap()
7407 })
7408 .await
7409 .unwrap();
7410 assert_eq!(
7411 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7412 Some(leader_id)
7413 );
7414
7415 // When client B edits, it automatically stops following client A.
7416 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
7417 assert_eq!(
7418 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7419 None
7420 );
7421
7422 workspace_b
7423 .update(cx_b, |workspace, cx| {
7424 workspace.toggle_follow(leader_id, cx).unwrap()
7425 })
7426 .await
7427 .unwrap();
7428 assert_eq!(
7429 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7430 Some(leader_id)
7431 );
7432
7433 // When client B scrolls, it automatically stops following client A.
7434 editor_b2.update(cx_b, |editor, cx| {
7435 editor.set_scroll_position(vec2f(0., 3.), cx)
7436 });
7437 assert_eq!(
7438 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7439 None
7440 );
7441
7442 workspace_b
7443 .update(cx_b, |workspace, cx| {
7444 workspace.toggle_follow(leader_id, cx).unwrap()
7445 })
7446 .await
7447 .unwrap();
7448 assert_eq!(
7449 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7450 Some(leader_id)
7451 );
7452
7453 // When client B activates a different pane, it continues following client A in the original pane.
7454 workspace_b.update(cx_b, |workspace, cx| {
7455 workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
7456 });
7457 assert_eq!(
7458 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7459 Some(leader_id)
7460 );
7461
7462 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
7463 assert_eq!(
7464 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7465 Some(leader_id)
7466 );
7467
7468 // When client B activates a different item in the original pane, it automatically stops following client A.
7469 workspace_b
7470 .update(cx_b, |workspace, cx| {
7471 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
7472 })
7473 .await
7474 .unwrap();
7475 assert_eq!(
7476 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7477 None
7478 );
7479}
7480
7481#[gpui::test(iterations = 10)]
7482async fn test_peers_simultaneously_following_each_other(
7483 deterministic: Arc<Deterministic>,
7484 cx_a: &mut TestAppContext,
7485 cx_b: &mut TestAppContext,
7486) {
7487 deterministic.forbid_parking();
7488
7489 let mut server = TestServer::start(&deterministic).await;
7490 let client_a = server.create_client(cx_a, "user_a").await;
7491 let client_b = server.create_client(cx_b, "user_b").await;
7492 server
7493 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7494 .await;
7495 let active_call_a = cx_a.read(ActiveCall::global);
7496
7497 cx_a.update(editor::init);
7498 cx_b.update(editor::init);
7499
7500 client_a.fs().insert_tree("/a", json!({})).await;
7501 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
7502 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
7503 let project_id = active_call_a
7504 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7505 .await
7506 .unwrap();
7507
7508 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7509 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
7510
7511 deterministic.run_until_parked();
7512 let client_a_id = project_b.read_with(cx_b, |project, _| {
7513 project.collaborators().values().next().unwrap().peer_id
7514 });
7515 let client_b_id = project_a.read_with(cx_a, |project, _| {
7516 project.collaborators().values().next().unwrap().peer_id
7517 });
7518
7519 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
7520 workspace.toggle_follow(client_b_id, cx).unwrap()
7521 });
7522 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
7523 workspace.toggle_follow(client_a_id, cx).unwrap()
7524 });
7525
7526 futures::try_join!(a_follow_b, b_follow_a).unwrap();
7527 workspace_a.read_with(cx_a, |workspace, _| {
7528 assert_eq!(
7529 workspace.leader_for_pane(workspace.active_pane()),
7530 Some(client_b_id)
7531 );
7532 });
7533 workspace_b.read_with(cx_b, |workspace, _| {
7534 assert_eq!(
7535 workspace.leader_for_pane(workspace.active_pane()),
7536 Some(client_a_id)
7537 );
7538 });
7539}
7540
7541#[gpui::test(iterations = 10)]
7542async fn test_on_input_format_from_host_to_guest(
7543 deterministic: Arc<Deterministic>,
7544 cx_a: &mut TestAppContext,
7545 cx_b: &mut TestAppContext,
7546) {
7547 deterministic.forbid_parking();
7548 let mut server = TestServer::start(&deterministic).await;
7549 let client_a = server.create_client(cx_a, "user_a").await;
7550 let client_b = server.create_client(cx_b, "user_b").await;
7551 server
7552 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7553 .await;
7554 let active_call_a = cx_a.read(ActiveCall::global);
7555
7556 // Set up a fake language server.
7557 let mut language = Language::new(
7558 LanguageConfig {
7559 name: "Rust".into(),
7560 path_suffixes: vec!["rs".to_string()],
7561 ..Default::default()
7562 },
7563 Some(tree_sitter_rust::language()),
7564 );
7565 let mut fake_language_servers = language
7566 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7567 capabilities: lsp::ServerCapabilities {
7568 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
7569 first_trigger_character: ":".to_string(),
7570 more_trigger_character: Some(vec![">".to_string()]),
7571 }),
7572 ..Default::default()
7573 },
7574 ..Default::default()
7575 }))
7576 .await;
7577 client_a.language_registry().add(Arc::new(language));
7578
7579 client_a
7580 .fs()
7581 .insert_tree(
7582 "/a",
7583 json!({
7584 "main.rs": "fn main() { a }",
7585 "other.rs": "// Test file",
7586 }),
7587 )
7588 .await;
7589 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7590 let project_id = active_call_a
7591 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7592 .await
7593 .unwrap();
7594 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7595
7596 // Open a file in an editor as the host.
7597 let buffer_a = project_a
7598 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
7599 .await
7600 .unwrap();
7601 let window_a = cx_a.add_window(|_| EmptyView);
7602 let editor_a = window_a.add_view(cx_a, |cx| {
7603 Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
7604 });
7605
7606 let fake_language_server = fake_language_servers.next().await.unwrap();
7607 cx_b.foreground().run_until_parked();
7608
7609 // Receive an OnTypeFormatting request as the host's language server.
7610 // Return some formattings from the host's language server.
7611 fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
7612 |params, _| async move {
7613 assert_eq!(
7614 params.text_document_position.text_document.uri,
7615 lsp::Url::from_file_path("/a/main.rs").unwrap(),
7616 );
7617 assert_eq!(
7618 params.text_document_position.position,
7619 lsp::Position::new(0, 14),
7620 );
7621
7622 Ok(Some(vec![lsp::TextEdit {
7623 new_text: "~<".to_string(),
7624 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
7625 }]))
7626 },
7627 );
7628
7629 // Open the buffer on the guest and see that the formattings worked
7630 let buffer_b = project_b
7631 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
7632 .await
7633 .unwrap();
7634
7635 // Type a on type formatting trigger character as the guest.
7636 editor_a.update(cx_a, |editor, cx| {
7637 cx.focus(&editor_a);
7638 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
7639 editor.handle_input(">", cx);
7640 });
7641
7642 cx_b.foreground().run_until_parked();
7643
7644 buffer_b.read_with(cx_b, |buffer, _| {
7645 assert_eq!(buffer.text(), "fn main() { a>~< }")
7646 });
7647
7648 // Undo should remove LSP edits first
7649 editor_a.update(cx_a, |editor, cx| {
7650 assert_eq!(editor.text(cx), "fn main() { a>~< }");
7651 editor.undo(&Undo, cx);
7652 assert_eq!(editor.text(cx), "fn main() { a> }");
7653 });
7654 cx_b.foreground().run_until_parked();
7655 buffer_b.read_with(cx_b, |buffer, _| {
7656 assert_eq!(buffer.text(), "fn main() { a> }")
7657 });
7658
7659 editor_a.update(cx_a, |editor, cx| {
7660 assert_eq!(editor.text(cx), "fn main() { a> }");
7661 editor.undo(&Undo, cx);
7662 assert_eq!(editor.text(cx), "fn main() { a }");
7663 });
7664 cx_b.foreground().run_until_parked();
7665 buffer_b.read_with(cx_b, |buffer, _| {
7666 assert_eq!(buffer.text(), "fn main() { a }")
7667 });
7668}
7669
7670#[gpui::test(iterations = 10)]
7671async fn test_on_input_format_from_guest_to_host(
7672 deterministic: Arc<Deterministic>,
7673 cx_a: &mut TestAppContext,
7674 cx_b: &mut TestAppContext,
7675) {
7676 deterministic.forbid_parking();
7677 let mut server = TestServer::start(&deterministic).await;
7678 let client_a = server.create_client(cx_a, "user_a").await;
7679 let client_b = server.create_client(cx_b, "user_b").await;
7680 server
7681 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7682 .await;
7683 let active_call_a = cx_a.read(ActiveCall::global);
7684
7685 // Set up a fake language server.
7686 let mut language = Language::new(
7687 LanguageConfig {
7688 name: "Rust".into(),
7689 path_suffixes: vec!["rs".to_string()],
7690 ..Default::default()
7691 },
7692 Some(tree_sitter_rust::language()),
7693 );
7694 let mut fake_language_servers = language
7695 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7696 capabilities: lsp::ServerCapabilities {
7697 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
7698 first_trigger_character: ":".to_string(),
7699 more_trigger_character: Some(vec![">".to_string()]),
7700 }),
7701 ..Default::default()
7702 },
7703 ..Default::default()
7704 }))
7705 .await;
7706 client_a.language_registry().add(Arc::new(language));
7707
7708 client_a
7709 .fs()
7710 .insert_tree(
7711 "/a",
7712 json!({
7713 "main.rs": "fn main() { a }",
7714 "other.rs": "// Test file",
7715 }),
7716 )
7717 .await;
7718 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7719 let project_id = active_call_a
7720 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7721 .await
7722 .unwrap();
7723 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7724
7725 // Open a file in an editor as the guest.
7726 let buffer_b = project_b
7727 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
7728 .await
7729 .unwrap();
7730 let window_b = cx_b.add_window(|_| EmptyView);
7731 let editor_b = window_b.add_view(cx_b, |cx| {
7732 Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
7733 });
7734
7735 let fake_language_server = fake_language_servers.next().await.unwrap();
7736 cx_a.foreground().run_until_parked();
7737 // Type a on type formatting trigger character as the guest.
7738 editor_b.update(cx_b, |editor, cx| {
7739 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
7740 editor.handle_input(":", cx);
7741 cx.focus(&editor_b);
7742 });
7743
7744 // Receive an OnTypeFormatting request as the host's language server.
7745 // Return some formattings from the host's language server.
7746 cx_a.foreground().start_waiting();
7747 fake_language_server
7748 .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
7749 assert_eq!(
7750 params.text_document_position.text_document.uri,
7751 lsp::Url::from_file_path("/a/main.rs").unwrap(),
7752 );
7753 assert_eq!(
7754 params.text_document_position.position,
7755 lsp::Position::new(0, 14),
7756 );
7757
7758 Ok(Some(vec![lsp::TextEdit {
7759 new_text: "~:".to_string(),
7760 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
7761 }]))
7762 })
7763 .next()
7764 .await
7765 .unwrap();
7766 cx_a.foreground().finish_waiting();
7767
7768 // Open the buffer on the host and see that the formattings worked
7769 let buffer_a = project_a
7770 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
7771 .await
7772 .unwrap();
7773 cx_a.foreground().run_until_parked();
7774 buffer_a.read_with(cx_a, |buffer, _| {
7775 assert_eq!(buffer.text(), "fn main() { a:~: }")
7776 });
7777
7778 // Undo should remove LSP edits first
7779 editor_b.update(cx_b, |editor, cx| {
7780 assert_eq!(editor.text(cx), "fn main() { a:~: }");
7781 editor.undo(&Undo, cx);
7782 assert_eq!(editor.text(cx), "fn main() { a: }");
7783 });
7784 cx_a.foreground().run_until_parked();
7785 buffer_a.read_with(cx_a, |buffer, _| {
7786 assert_eq!(buffer.text(), "fn main() { a: }")
7787 });
7788
7789 editor_b.update(cx_b, |editor, cx| {
7790 assert_eq!(editor.text(cx), "fn main() { a: }");
7791 editor.undo(&Undo, cx);
7792 assert_eq!(editor.text(cx), "fn main() { a }");
7793 });
7794 cx_a.foreground().run_until_parked();
7795 buffer_a.read_with(cx_a, |buffer, _| {
7796 assert_eq!(buffer.text(), "fn main() { a }")
7797 });
7798}
7799
7800#[gpui::test]
7801async fn test_mutual_editor_inlay_hint_cache_update(
7802 deterministic: Arc<Deterministic>,
7803 cx_a: &mut TestAppContext,
7804 cx_b: &mut TestAppContext,
7805) {
7806 deterministic.forbid_parking();
7807 let mut server = TestServer::start(&deterministic).await;
7808 let client_a = server.create_client(cx_a, "user_a").await;
7809 let client_b = server.create_client(cx_b, "user_b").await;
7810 server
7811 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7812 .await;
7813 let active_call_a = cx_a.read(ActiveCall::global);
7814 let active_call_b = cx_b.read(ActiveCall::global);
7815
7816 cx_a.update(editor::init);
7817 cx_b.update(editor::init);
7818
7819 cx_a.update(|cx| {
7820 cx.update_global(|store: &mut SettingsStore, cx| {
7821 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
7822 settings.defaults.inlay_hints = Some(InlayHintSettings {
7823 enabled: true,
7824 show_type_hints: true,
7825 show_parameter_hints: false,
7826 show_other_hints: true,
7827 })
7828 });
7829 });
7830 });
7831 cx_b.update(|cx| {
7832 cx.update_global(|store: &mut SettingsStore, cx| {
7833 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
7834 settings.defaults.inlay_hints = Some(InlayHintSettings {
7835 enabled: true,
7836 show_type_hints: true,
7837 show_parameter_hints: false,
7838 show_other_hints: true,
7839 })
7840 });
7841 });
7842 });
7843
7844 let mut language = Language::new(
7845 LanguageConfig {
7846 name: "Rust".into(),
7847 path_suffixes: vec!["rs".to_string()],
7848 ..Default::default()
7849 },
7850 Some(tree_sitter_rust::language()),
7851 );
7852 let mut fake_language_servers = language
7853 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7854 capabilities: lsp::ServerCapabilities {
7855 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
7856 ..Default::default()
7857 },
7858 ..Default::default()
7859 }))
7860 .await;
7861 let language = Arc::new(language);
7862 client_a.language_registry().add(Arc::clone(&language));
7863 client_b.language_registry().add(language);
7864
7865 client_a
7866 .fs()
7867 .insert_tree(
7868 "/a",
7869 json!({
7870 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
7871 "other.rs": "// Test file",
7872 }),
7873 )
7874 .await;
7875 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
7876 active_call_a
7877 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
7878 .await
7879 .unwrap();
7880 let project_id = active_call_a
7881 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7882 .await
7883 .unwrap();
7884
7885 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7886 active_call_b
7887 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
7888 .await
7889 .unwrap();
7890
7891 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
7892 cx_a.foreground().start_waiting();
7893
7894 let _buffer_a = project_a
7895 .update(cx_a, |project, cx| {
7896 project.open_local_buffer("/a/main.rs", cx)
7897 })
7898 .await
7899 .unwrap();
7900 let fake_language_server = fake_language_servers.next().await.unwrap();
7901 let next_call_id = Arc::new(AtomicU32::new(0));
7902 let editor_a = workspace_a
7903 .update(cx_a, |workspace, cx| {
7904 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
7905 })
7906 .await
7907 .unwrap()
7908 .downcast::<Editor>()
7909 .unwrap();
7910 fake_language_server
7911 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
7912 let task_next_call_id = Arc::clone(&next_call_id);
7913 async move {
7914 assert_eq!(
7915 params.text_document.uri,
7916 lsp::Url::from_file_path("/a/main.rs").unwrap(),
7917 );
7918 let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
7919 let mut new_hints = Vec::with_capacity(current_call_id as usize);
7920 loop {
7921 new_hints.push(lsp::InlayHint {
7922 position: lsp::Position::new(0, current_call_id),
7923 label: lsp::InlayHintLabel::String(current_call_id.to_string()),
7924 kind: None,
7925 text_edits: None,
7926 tooltip: None,
7927 padding_left: None,
7928 padding_right: None,
7929 data: None,
7930 });
7931 if current_call_id == 0 {
7932 break;
7933 }
7934 current_call_id -= 1;
7935 }
7936 Ok(Some(new_hints))
7937 }
7938 })
7939 .next()
7940 .await
7941 .unwrap();
7942
7943 cx_a.foreground().finish_waiting();
7944 cx_a.foreground().run_until_parked();
7945
7946 let mut edits_made = 1;
7947 editor_a.update(cx_a, |editor, _| {
7948 assert_eq!(
7949 vec!["0".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 edits_made,
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 cx_b.foreground().run_until_parked();
7971 editor_b.update(cx_b, |editor, _| {
7972 assert_eq!(
7973 vec!["0".to_string(), "1".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 edits_made,
7981 "Guest editor update the cache version after every cache/view change"
7982 );
7983 });
7984
7985 editor_b.update(cx_b, |editor, cx| {
7986 editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
7987 editor.handle_input(":", cx);
7988 cx.focus(&editor_b);
7989 edits_made += 1;
7990 });
7991 cx_a.foreground().run_until_parked();
7992 cx_b.foreground().run_until_parked();
7993 editor_a.update(cx_a, |editor, _| {
7994 assert_eq!(
7995 vec!["0".to_string(), "1".to_string(), "2".to_string()],
7996 extract_hint_labels(editor),
7997 "Host should get hints from the 1st edit and 1st LSP query"
7998 );
7999 let inlay_cache = editor.inlay_hint_cache();
8000 assert_eq!(inlay_cache.version(), edits_made);
8001 });
8002 editor_b.update(cx_b, |editor, _| {
8003 assert_eq!(
8004 vec![
8005 "0".to_string(),
8006 "1".to_string(),
8007 "2".to_string(),
8008 "3".to_string()
8009 ],
8010 extract_hint_labels(editor),
8011 "Guest should get hints the 1st edit and 2nd LSP query"
8012 );
8013 let inlay_cache = editor.inlay_hint_cache();
8014 assert_eq!(inlay_cache.version(), edits_made);
8015 });
8016
8017 editor_a.update(cx_a, |editor, cx| {
8018 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
8019 editor.handle_input("a change to increment both buffers' versions", cx);
8020 cx.focus(&editor_a);
8021 edits_made += 1;
8022 });
8023 cx_a.foreground().run_until_parked();
8024 cx_b.foreground().run_until_parked();
8025 editor_a.update(cx_a, |editor, _| {
8026 assert_eq!(
8027 vec![
8028 "0".to_string(),
8029 "1".to_string(),
8030 "2".to_string(),
8031 "3".to_string(),
8032 "4".to_string()
8033 ],
8034 extract_hint_labels(editor),
8035 "Host should get hints from 3rd edit, 5th LSP query: \
80364th query was made by guest (but not applied) due to cache invalidation logic"
8037 );
8038 let inlay_cache = editor.inlay_hint_cache();
8039 assert_eq!(inlay_cache.version(), edits_made);
8040 });
8041 editor_b.update(cx_b, |editor, _| {
8042 assert_eq!(
8043 vec![
8044 "0".to_string(),
8045 "1".to_string(),
8046 "2".to_string(),
8047 "3".to_string(),
8048 "4".to_string(),
8049 "5".to_string(),
8050 ],
8051 extract_hint_labels(editor),
8052 "Guest should get hints from 3rd edit, 6th LSP query"
8053 );
8054 let inlay_cache = editor.inlay_hint_cache();
8055 assert_eq!(inlay_cache.version(), edits_made);
8056 });
8057
8058 fake_language_server
8059 .request::<lsp::request::InlayHintRefreshRequest>(())
8060 .await
8061 .expect("inlay refresh request failed");
8062 edits_made += 1;
8063 cx_a.foreground().run_until_parked();
8064 cx_b.foreground().run_until_parked();
8065 editor_a.update(cx_a, |editor, _| {
8066 assert_eq!(
8067 vec![
8068 "0".to_string(),
8069 "1".to_string(),
8070 "2".to_string(),
8071 "3".to_string(),
8072 "4".to_string(),
8073 "5".to_string(),
8074 "6".to_string(),
8075 ],
8076 extract_hint_labels(editor),
8077 "Host should react to /refresh LSP request and get new hints from 7th LSP query"
8078 );
8079 let inlay_cache = editor.inlay_hint_cache();
8080 assert_eq!(
8081 inlay_cache.version(),
8082 edits_made,
8083 "Host should accepted all edits and bump its cache version every time"
8084 );
8085 });
8086 editor_b.update(cx_b, |editor, _| {
8087 assert_eq!(
8088 vec![
8089 "0".to_string(),
8090 "1".to_string(),
8091 "2".to_string(),
8092 "3".to_string(),
8093 "4".to_string(),
8094 "5".to_string(),
8095 "6".to_string(),
8096 "7".to_string(),
8097 ],
8098 extract_hint_labels(editor),
8099 "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query"
8100 );
8101 let inlay_cache = editor.inlay_hint_cache();
8102 assert_eq!(
8103 inlay_cache.version(),
8104 edits_made,
8105 "Guest should accepted all edits and bump its cache version every time"
8106 );
8107 });
8108}
8109
8110#[gpui::test]
8111async fn test_inlay_hint_refresh_is_forwarded(
8112 deterministic: Arc<Deterministic>,
8113 cx_a: &mut TestAppContext,
8114 cx_b: &mut TestAppContext,
8115) {
8116 deterministic.forbid_parking();
8117 let mut server = TestServer::start(&deterministic).await;
8118 let client_a = server.create_client(cx_a, "user_a").await;
8119 let client_b = server.create_client(cx_b, "user_b").await;
8120 server
8121 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
8122 .await;
8123 let active_call_a = cx_a.read(ActiveCall::global);
8124 let active_call_b = cx_b.read(ActiveCall::global);
8125
8126 cx_a.update(editor::init);
8127 cx_b.update(editor::init);
8128
8129 cx_a.update(|cx| {
8130 cx.update_global(|store: &mut SettingsStore, cx| {
8131 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
8132 settings.defaults.inlay_hints = Some(InlayHintSettings {
8133 enabled: false,
8134 show_type_hints: false,
8135 show_parameter_hints: false,
8136 show_other_hints: false,
8137 })
8138 });
8139 });
8140 });
8141 cx_b.update(|cx| {
8142 cx.update_global(|store: &mut SettingsStore, cx| {
8143 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
8144 settings.defaults.inlay_hints = Some(InlayHintSettings {
8145 enabled: true,
8146 show_type_hints: true,
8147 show_parameter_hints: true,
8148 show_other_hints: true,
8149 })
8150 });
8151 });
8152 });
8153
8154 let mut language = Language::new(
8155 LanguageConfig {
8156 name: "Rust".into(),
8157 path_suffixes: vec!["rs".to_string()],
8158 ..Default::default()
8159 },
8160 Some(tree_sitter_rust::language()),
8161 );
8162 let mut fake_language_servers = language
8163 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
8164 capabilities: lsp::ServerCapabilities {
8165 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
8166 ..Default::default()
8167 },
8168 ..Default::default()
8169 }))
8170 .await;
8171 let language = Arc::new(language);
8172 client_a.language_registry().add(Arc::clone(&language));
8173 client_b.language_registry().add(language);
8174
8175 client_a
8176 .fs()
8177 .insert_tree(
8178 "/a",
8179 json!({
8180 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
8181 "other.rs": "// Test file",
8182 }),
8183 )
8184 .await;
8185 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
8186 active_call_a
8187 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
8188 .await
8189 .unwrap();
8190 let project_id = active_call_a
8191 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
8192 .await
8193 .unwrap();
8194
8195 let project_b = client_b.build_remote_project(project_id, cx_b).await;
8196 active_call_b
8197 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
8198 .await
8199 .unwrap();
8200
8201 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
8202 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
8203 cx_a.foreground().start_waiting();
8204 cx_b.foreground().start_waiting();
8205
8206 let editor_a = workspace_a
8207 .update(cx_a, |workspace, cx| {
8208 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
8209 })
8210 .await
8211 .unwrap()
8212 .downcast::<Editor>()
8213 .unwrap();
8214
8215 let editor_b = workspace_b
8216 .update(cx_b, |workspace, cx| {
8217 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
8218 })
8219 .await
8220 .unwrap()
8221 .downcast::<Editor>()
8222 .unwrap();
8223
8224 let fake_language_server = fake_language_servers.next().await.unwrap();
8225 let next_call_id = Arc::new(AtomicU32::new(0));
8226 fake_language_server
8227 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
8228 let task_next_call_id = Arc::clone(&next_call_id);
8229 async move {
8230 assert_eq!(
8231 params.text_document.uri,
8232 lsp::Url::from_file_path("/a/main.rs").unwrap(),
8233 );
8234 let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
8235 let mut new_hints = Vec::with_capacity(current_call_id as usize);
8236 loop {
8237 new_hints.push(lsp::InlayHint {
8238 position: lsp::Position::new(0, current_call_id),
8239 label: lsp::InlayHintLabel::String(current_call_id.to_string()),
8240 kind: None,
8241 text_edits: None,
8242 tooltip: None,
8243 padding_left: None,
8244 padding_right: None,
8245 data: None,
8246 });
8247 if current_call_id == 0 {
8248 break;
8249 }
8250 current_call_id -= 1;
8251 }
8252 Ok(Some(new_hints))
8253 }
8254 })
8255 .next()
8256 .await
8257 .unwrap();
8258 cx_a.foreground().finish_waiting();
8259 cx_b.foreground().finish_waiting();
8260
8261 cx_a.foreground().run_until_parked();
8262 editor_a.update(cx_a, |editor, _| {
8263 assert!(
8264 extract_hint_labels(editor).is_empty(),
8265 "Host should get no hints due to them turned off"
8266 );
8267 let inlay_cache = editor.inlay_hint_cache();
8268 assert_eq!(
8269 inlay_cache.version(),
8270 0,
8271 "Host should not increment its cache version due to no changes",
8272 );
8273 });
8274
8275 let mut edits_made = 1;
8276 cx_b.foreground().run_until_parked();
8277 editor_b.update(cx_b, |editor, _| {
8278 assert_eq!(
8279 vec!["0".to_string()],
8280 extract_hint_labels(editor),
8281 "Client should get its first hints when opens an editor"
8282 );
8283 let inlay_cache = editor.inlay_hint_cache();
8284 assert_eq!(
8285 inlay_cache.version(),
8286 edits_made,
8287 "Guest editor update the cache version after every cache/view change"
8288 );
8289 });
8290
8291 fake_language_server
8292 .request::<lsp::request::InlayHintRefreshRequest>(())
8293 .await
8294 .expect("inlay refresh request failed");
8295 cx_a.foreground().run_until_parked();
8296 editor_a.update(cx_a, |editor, _| {
8297 assert!(
8298 extract_hint_labels(editor).is_empty(),
8299 "Host should get nop hints due to them turned off, even after the /refresh"
8300 );
8301 let inlay_cache = editor.inlay_hint_cache();
8302 assert_eq!(
8303 inlay_cache.version(),
8304 0,
8305 "Host should not increment its cache version due to no changes",
8306 );
8307 });
8308
8309 edits_made += 1;
8310 cx_b.foreground().run_until_parked();
8311 editor_b.update(cx_b, |editor, _| {
8312 assert_eq!(
8313 vec!["0".to_string(), "1".to_string(),],
8314 extract_hint_labels(editor),
8315 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
8316 );
8317 let inlay_cache = editor.inlay_hint_cache();
8318 assert_eq!(
8319 inlay_cache.version(),
8320 edits_made,
8321 "Guest should accepted all edits and bump its cache version every time"
8322 );
8323 });
8324}
8325
8326fn extract_hint_labels(editor: &Editor) -> Vec<String> {
8327 let mut labels = Vec::new();
8328 for hint in editor.inlay_hint_cache().hints() {
8329 match hint.label {
8330 project::InlayHintLabel::String(s) => labels.push(s),
8331 _ => unreachable!(),
8332 }
8333 }
8334 labels
8335}