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