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