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