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