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, ModelHandle, TestAppContext,
17 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(iterations = 10)]
2608async fn test_fs_operations(
2609 deterministic: Arc<Deterministic>,
2610 cx_a: &mut TestAppContext,
2611 cx_b: &mut TestAppContext,
2612) {
2613 deterministic.forbid_parking();
2614 let mut server = TestServer::start(&deterministic).await;
2615 let client_a = server.create_client(cx_a, "user_a").await;
2616 let client_b = server.create_client(cx_b, "user_b").await;
2617 server
2618 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2619 .await;
2620 let active_call_a = cx_a.read(ActiveCall::global);
2621
2622 client_a
2623 .fs
2624 .insert_tree(
2625 "/dir",
2626 json!({
2627 "a.txt": "a-contents",
2628 "b.txt": "b-contents",
2629 }),
2630 )
2631 .await;
2632 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2633 let project_id = active_call_a
2634 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2635 .await
2636 .unwrap();
2637 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2638
2639 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
2640 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
2641
2642 let entry = project_b
2643 .update(cx_b, |project, cx| {
2644 project
2645 .create_entry((worktree_id, "c.txt"), false, cx)
2646 .unwrap()
2647 })
2648 .await
2649 .unwrap();
2650 worktree_a.read_with(cx_a, |worktree, _| {
2651 assert_eq!(
2652 worktree
2653 .paths()
2654 .map(|p| p.to_string_lossy())
2655 .collect::<Vec<_>>(),
2656 ["a.txt", "b.txt", "c.txt"]
2657 );
2658 });
2659 worktree_b.read_with(cx_b, |worktree, _| {
2660 assert_eq!(
2661 worktree
2662 .paths()
2663 .map(|p| p.to_string_lossy())
2664 .collect::<Vec<_>>(),
2665 ["a.txt", "b.txt", "c.txt"]
2666 );
2667 });
2668
2669 project_b
2670 .update(cx_b, |project, cx| {
2671 project.rename_entry(entry.id, Path::new("d.txt"), cx)
2672 })
2673 .unwrap()
2674 .await
2675 .unwrap();
2676 worktree_a.read_with(cx_a, |worktree, _| {
2677 assert_eq!(
2678 worktree
2679 .paths()
2680 .map(|p| p.to_string_lossy())
2681 .collect::<Vec<_>>(),
2682 ["a.txt", "b.txt", "d.txt"]
2683 );
2684 });
2685 worktree_b.read_with(cx_b, |worktree, _| {
2686 assert_eq!(
2687 worktree
2688 .paths()
2689 .map(|p| p.to_string_lossy())
2690 .collect::<Vec<_>>(),
2691 ["a.txt", "b.txt", "d.txt"]
2692 );
2693 });
2694
2695 let dir_entry = project_b
2696 .update(cx_b, |project, cx| {
2697 project
2698 .create_entry((worktree_id, "DIR"), true, cx)
2699 .unwrap()
2700 })
2701 .await
2702 .unwrap();
2703 worktree_a.read_with(cx_a, |worktree, _| {
2704 assert_eq!(
2705 worktree
2706 .paths()
2707 .map(|p| p.to_string_lossy())
2708 .collect::<Vec<_>>(),
2709 ["DIR", "a.txt", "b.txt", "d.txt"]
2710 );
2711 });
2712 worktree_b.read_with(cx_b, |worktree, _| {
2713 assert_eq!(
2714 worktree
2715 .paths()
2716 .map(|p| p.to_string_lossy())
2717 .collect::<Vec<_>>(),
2718 ["DIR", "a.txt", "b.txt", "d.txt"]
2719 );
2720 });
2721
2722 project_b
2723 .update(cx_b, |project, cx| {
2724 project
2725 .create_entry((worktree_id, "DIR/e.txt"), false, cx)
2726 .unwrap()
2727 })
2728 .await
2729 .unwrap();
2730 project_b
2731 .update(cx_b, |project, cx| {
2732 project
2733 .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
2734 .unwrap()
2735 })
2736 .await
2737 .unwrap();
2738 project_b
2739 .update(cx_b, |project, cx| {
2740 project
2741 .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
2742 .unwrap()
2743 })
2744 .await
2745 .unwrap();
2746 worktree_a.read_with(cx_a, |worktree, _| {
2747 assert_eq!(
2748 worktree
2749 .paths()
2750 .map(|p| p.to_string_lossy())
2751 .collect::<Vec<_>>(),
2752 [
2753 "DIR",
2754 "DIR/SUBDIR",
2755 "DIR/SUBDIR/f.txt",
2756 "DIR/e.txt",
2757 "a.txt",
2758 "b.txt",
2759 "d.txt"
2760 ]
2761 );
2762 });
2763 worktree_b.read_with(cx_b, |worktree, _| {
2764 assert_eq!(
2765 worktree
2766 .paths()
2767 .map(|p| p.to_string_lossy())
2768 .collect::<Vec<_>>(),
2769 [
2770 "DIR",
2771 "DIR/SUBDIR",
2772 "DIR/SUBDIR/f.txt",
2773 "DIR/e.txt",
2774 "a.txt",
2775 "b.txt",
2776 "d.txt"
2777 ]
2778 );
2779 });
2780
2781 project_b
2782 .update(cx_b, |project, cx| {
2783 project
2784 .copy_entry(entry.id, Path::new("f.txt"), 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 [
2796 "DIR",
2797 "DIR/SUBDIR",
2798 "DIR/SUBDIR/f.txt",
2799 "DIR/e.txt",
2800 "a.txt",
2801 "b.txt",
2802 "d.txt",
2803 "f.txt"
2804 ]
2805 );
2806 });
2807 worktree_b.read_with(cx_b, |worktree, _| {
2808 assert_eq!(
2809 worktree
2810 .paths()
2811 .map(|p| p.to_string_lossy())
2812 .collect::<Vec<_>>(),
2813 [
2814 "DIR",
2815 "DIR/SUBDIR",
2816 "DIR/SUBDIR/f.txt",
2817 "DIR/e.txt",
2818 "a.txt",
2819 "b.txt",
2820 "d.txt",
2821 "f.txt"
2822 ]
2823 );
2824 });
2825
2826 project_b
2827 .update(cx_b, |project, cx| {
2828 project.delete_entry(dir_entry.id, cx).unwrap()
2829 })
2830 .await
2831 .unwrap();
2832 deterministic.run_until_parked();
2833
2834 worktree_a.read_with(cx_a, |worktree, _| {
2835 assert_eq!(
2836 worktree
2837 .paths()
2838 .map(|p| p.to_string_lossy())
2839 .collect::<Vec<_>>(),
2840 ["a.txt", "b.txt", "d.txt", "f.txt"]
2841 );
2842 });
2843 worktree_b.read_with(cx_b, |worktree, _| {
2844 assert_eq!(
2845 worktree
2846 .paths()
2847 .map(|p| p.to_string_lossy())
2848 .collect::<Vec<_>>(),
2849 ["a.txt", "b.txt", "d.txt", "f.txt"]
2850 );
2851 });
2852
2853 project_b
2854 .update(cx_b, |project, cx| {
2855 project.delete_entry(entry.id, cx).unwrap()
2856 })
2857 .await
2858 .unwrap();
2859 worktree_a.read_with(cx_a, |worktree, _| {
2860 assert_eq!(
2861 worktree
2862 .paths()
2863 .map(|p| p.to_string_lossy())
2864 .collect::<Vec<_>>(),
2865 ["a.txt", "b.txt", "f.txt"]
2866 );
2867 });
2868 worktree_b.read_with(cx_b, |worktree, _| {
2869 assert_eq!(
2870 worktree
2871 .paths()
2872 .map(|p| p.to_string_lossy())
2873 .collect::<Vec<_>>(),
2874 ["a.txt", "b.txt", "f.txt"]
2875 );
2876 });
2877}
2878
2879#[gpui::test(iterations = 10)]
2880async fn test_buffer_conflict_after_save(
2881 deterministic: Arc<Deterministic>,
2882 cx_a: &mut TestAppContext,
2883 cx_b: &mut TestAppContext,
2884) {
2885 deterministic.forbid_parking();
2886 let mut server = TestServer::start(&deterministic).await;
2887 let client_a = server.create_client(cx_a, "user_a").await;
2888 let client_b = server.create_client(cx_b, "user_b").await;
2889 server
2890 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2891 .await;
2892 let active_call_a = cx_a.read(ActiveCall::global);
2893
2894 client_a
2895 .fs
2896 .insert_tree(
2897 "/dir",
2898 json!({
2899 "a.txt": "a-contents",
2900 }),
2901 )
2902 .await;
2903 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2904 let project_id = active_call_a
2905 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2906 .await
2907 .unwrap();
2908 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2909
2910 // Open a buffer as client B
2911 let buffer_b = project_b
2912 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2913 .await
2914 .unwrap();
2915
2916 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
2917 buffer_b.read_with(cx_b, |buf, _| {
2918 assert!(buf.is_dirty());
2919 assert!(!buf.has_conflict());
2920 });
2921
2922 project_b
2923 .update(cx_b, |project, cx| {
2924 project.save_buffer(buffer_b.clone(), cx)
2925 })
2926 .await
2927 .unwrap();
2928 cx_a.foreground().forbid_parking();
2929 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
2930 buffer_b.read_with(cx_b, |buf, _| {
2931 assert!(!buf.has_conflict());
2932 });
2933
2934 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
2935 buffer_b.read_with(cx_b, |buf, _| {
2936 assert!(buf.is_dirty());
2937 assert!(!buf.has_conflict());
2938 });
2939}
2940
2941#[gpui::test(iterations = 10)]
2942async fn test_buffer_reloading(
2943 deterministic: Arc<Deterministic>,
2944 cx_a: &mut TestAppContext,
2945 cx_b: &mut TestAppContext,
2946) {
2947 deterministic.forbid_parking();
2948 let mut server = TestServer::start(&deterministic).await;
2949 let client_a = server.create_client(cx_a, "user_a").await;
2950 let client_b = server.create_client(cx_b, "user_b").await;
2951 server
2952 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2953 .await;
2954 let active_call_a = cx_a.read(ActiveCall::global);
2955
2956 client_a
2957 .fs
2958 .insert_tree(
2959 "/dir",
2960 json!({
2961 "a.txt": "a\nb\nc",
2962 }),
2963 )
2964 .await;
2965 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2966 let project_id = active_call_a
2967 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2968 .await
2969 .unwrap();
2970 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2971
2972 // Open a buffer as client B
2973 let buffer_b = project_b
2974 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2975 .await
2976 .unwrap();
2977 buffer_b.read_with(cx_b, |buf, _| {
2978 assert!(!buf.is_dirty());
2979 assert!(!buf.has_conflict());
2980 assert_eq!(buf.line_ending(), LineEnding::Unix);
2981 });
2982
2983 let new_contents = Rope::from("d\ne\nf");
2984 client_a
2985 .fs
2986 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
2987 .await
2988 .unwrap();
2989 cx_a.foreground().run_until_parked();
2990 buffer_b.read_with(cx_b, |buf, _| {
2991 assert_eq!(buf.text(), new_contents.to_string());
2992 assert!(!buf.is_dirty());
2993 assert!(!buf.has_conflict());
2994 assert_eq!(buf.line_ending(), LineEnding::Windows);
2995 });
2996}
2997
2998#[gpui::test(iterations = 10)]
2999async fn test_editing_while_guest_opens_buffer(
3000 deterministic: Arc<Deterministic>,
3001 cx_a: &mut TestAppContext,
3002 cx_b: &mut TestAppContext,
3003) {
3004 deterministic.forbid_parking();
3005 let mut server = TestServer::start(&deterministic).await;
3006 let client_a = server.create_client(cx_a, "user_a").await;
3007 let client_b = server.create_client(cx_b, "user_b").await;
3008 server
3009 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3010 .await;
3011 let active_call_a = cx_a.read(ActiveCall::global);
3012
3013 client_a
3014 .fs
3015 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3016 .await;
3017 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3018 let project_id = active_call_a
3019 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3020 .await
3021 .unwrap();
3022 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3023
3024 // Open a buffer as client A
3025 let buffer_a = project_a
3026 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3027 .await
3028 .unwrap();
3029
3030 // Start opening the same buffer as client B
3031 let buffer_b = cx_b
3032 .background()
3033 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3034
3035 // Edit the buffer as client A while client B is still opening it.
3036 cx_b.background().simulate_random_delay().await;
3037 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3038 cx_b.background().simulate_random_delay().await;
3039 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3040
3041 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3042 let buffer_b = buffer_b.await.unwrap();
3043 cx_a.foreground().run_until_parked();
3044 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3045}
3046
3047#[gpui::test]
3048async fn test_newline_above_or_below_does_not_move_guest_cursor(
3049 deterministic: Arc<Deterministic>,
3050 cx_a: &mut TestAppContext,
3051 cx_b: &mut TestAppContext,
3052) {
3053 deterministic.forbid_parking();
3054 let mut server = TestServer::start(&deterministic).await;
3055 let client_a = server.create_client(cx_a, "user_a").await;
3056 let client_b = server.create_client(cx_b, "user_b").await;
3057 server
3058 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3059 .await;
3060 let active_call_a = cx_a.read(ActiveCall::global);
3061
3062 client_a
3063 .fs
3064 .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
3065 .await;
3066 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3067 let project_id = active_call_a
3068 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3069 .await
3070 .unwrap();
3071
3072 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3073
3074 // Open a buffer as client A
3075 let buffer_a = project_a
3076 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3077 .await
3078 .unwrap();
3079 let (_, window_a) = cx_a.add_window(|_| EmptyView);
3080 let editor_a = cx_a.add_view(&window_a, |cx| {
3081 Editor::for_buffer(buffer_a, Some(project_a), cx)
3082 });
3083 let mut editor_cx_a = EditorTestContext {
3084 cx: cx_a,
3085 window_id: window_a.id(),
3086 editor: editor_a,
3087 };
3088
3089 // Open a buffer as client B
3090 let buffer_b = project_b
3091 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3092 .await
3093 .unwrap();
3094 let (_, window_b) = cx_b.add_window(|_| EmptyView);
3095 let editor_b = cx_b.add_view(&window_b, |cx| {
3096 Editor::for_buffer(buffer_b, Some(project_b), cx)
3097 });
3098 let mut editor_cx_b = EditorTestContext {
3099 cx: cx_b,
3100 window_id: window_b.id(),
3101 editor: editor_b,
3102 };
3103
3104 // Test newline above
3105 editor_cx_a.set_selections_state(indoc! {"
3106 Some textˇ
3107 "});
3108 editor_cx_b.set_selections_state(indoc! {"
3109 Some textˇ
3110 "});
3111 editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
3112 deterministic.run_until_parked();
3113 editor_cx_a.assert_editor_state(indoc! {"
3114 ˇ
3115 Some text
3116 "});
3117 editor_cx_b.assert_editor_state(indoc! {"
3118
3119 Some textˇ
3120 "});
3121
3122 // Test newline below
3123 editor_cx_a.set_selections_state(indoc! {"
3124
3125 Some textˇ
3126 "});
3127 editor_cx_b.set_selections_state(indoc! {"
3128
3129 Some textˇ
3130 "});
3131 editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
3132 deterministic.run_until_parked();
3133 editor_cx_a.assert_editor_state(indoc! {"
3134
3135 Some text
3136 ˇ
3137 "});
3138 editor_cx_b.assert_editor_state(indoc! {"
3139
3140 Some textˇ
3141
3142 "});
3143}
3144
3145#[gpui::test(iterations = 10)]
3146async fn test_leaving_worktree_while_opening_buffer(
3147 deterministic: Arc<Deterministic>,
3148 cx_a: &mut TestAppContext,
3149 cx_b: &mut TestAppContext,
3150) {
3151 deterministic.forbid_parking();
3152 let mut server = TestServer::start(&deterministic).await;
3153 let client_a = server.create_client(cx_a, "user_a").await;
3154 let client_b = server.create_client(cx_b, "user_b").await;
3155 server
3156 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3157 .await;
3158 let active_call_a = cx_a.read(ActiveCall::global);
3159
3160 client_a
3161 .fs
3162 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3163 .await;
3164 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3165 let project_id = active_call_a
3166 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3167 .await
3168 .unwrap();
3169 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3170
3171 // See that a guest has joined as client A.
3172 cx_a.foreground().run_until_parked();
3173 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3174
3175 // Begin opening a buffer as client B, but leave the project before the open completes.
3176 let buffer_b = cx_b
3177 .background()
3178 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
3179 cx_b.update(|_| drop(project_b));
3180 drop(buffer_b);
3181
3182 // See that the guest has left.
3183 cx_a.foreground().run_until_parked();
3184 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3185}
3186
3187#[gpui::test(iterations = 10)]
3188async fn test_canceling_buffer_opening(
3189 deterministic: Arc<Deterministic>,
3190 cx_a: &mut TestAppContext,
3191 cx_b: &mut TestAppContext,
3192) {
3193 deterministic.forbid_parking();
3194
3195 let mut server = TestServer::start(&deterministic).await;
3196 let client_a = server.create_client(cx_a, "user_a").await;
3197 let client_b = server.create_client(cx_b, "user_b").await;
3198 server
3199 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3200 .await;
3201 let active_call_a = cx_a.read(ActiveCall::global);
3202
3203 client_a
3204 .fs
3205 .insert_tree(
3206 "/dir",
3207 json!({
3208 "a.txt": "abc",
3209 }),
3210 )
3211 .await;
3212 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3213 let project_id = active_call_a
3214 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3215 .await
3216 .unwrap();
3217 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3218
3219 let buffer_a = project_a
3220 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3221 .await
3222 .unwrap();
3223
3224 // Open a buffer as client B but cancel after a random amount of time.
3225 let buffer_b = project_b.update(cx_b, |p, cx| {
3226 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3227 });
3228 deterministic.simulate_random_delay().await;
3229 drop(buffer_b);
3230
3231 // Try opening the same buffer again as client B, and ensure we can
3232 // still do it despite the cancellation above.
3233 let buffer_b = project_b
3234 .update(cx_b, |p, cx| {
3235 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3236 })
3237 .await
3238 .unwrap();
3239 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3240}
3241
3242#[gpui::test(iterations = 10)]
3243async fn test_leaving_project(
3244 deterministic: Arc<Deterministic>,
3245 cx_a: &mut TestAppContext,
3246 cx_b: &mut TestAppContext,
3247 cx_c: &mut TestAppContext,
3248) {
3249 deterministic.forbid_parking();
3250 let mut server = TestServer::start(&deterministic).await;
3251 let client_a = server.create_client(cx_a, "user_a").await;
3252 let client_b = server.create_client(cx_b, "user_b").await;
3253 let client_c = server.create_client(cx_c, "user_c").await;
3254 server
3255 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3256 .await;
3257 let active_call_a = cx_a.read(ActiveCall::global);
3258
3259 client_a
3260 .fs
3261 .insert_tree(
3262 "/a",
3263 json!({
3264 "a.txt": "a-contents",
3265 "b.txt": "b-contents",
3266 }),
3267 )
3268 .await;
3269 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3270 let project_id = active_call_a
3271 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3272 .await
3273 .unwrap();
3274 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
3275 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3276
3277 // Client A sees that a guest has joined.
3278 deterministic.run_until_parked();
3279 project_a.read_with(cx_a, |project, _| {
3280 assert_eq!(project.collaborators().len(), 2);
3281 });
3282 project_b1.read_with(cx_b, |project, _| {
3283 assert_eq!(project.collaborators().len(), 2);
3284 });
3285 project_c.read_with(cx_c, |project, _| {
3286 assert_eq!(project.collaborators().len(), 2);
3287 });
3288
3289 // Client B opens a buffer.
3290 let buffer_b1 = project_b1
3291 .update(cx_b, |project, cx| {
3292 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3293 project.open_buffer((worktree_id, "a.txt"), cx)
3294 })
3295 .await
3296 .unwrap();
3297 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3298
3299 // Drop client B's project and ensure client A and client C observe client B leaving.
3300 cx_b.update(|_| drop(project_b1));
3301 deterministic.run_until_parked();
3302 project_a.read_with(cx_a, |project, _| {
3303 assert_eq!(project.collaborators().len(), 1);
3304 });
3305 project_c.read_with(cx_c, |project, _| {
3306 assert_eq!(project.collaborators().len(), 1);
3307 });
3308
3309 // Client B re-joins the project and can open buffers as before.
3310 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
3311 deterministic.run_until_parked();
3312 project_a.read_with(cx_a, |project, _| {
3313 assert_eq!(project.collaborators().len(), 2);
3314 });
3315 project_b2.read_with(cx_b, |project, _| {
3316 assert_eq!(project.collaborators().len(), 2);
3317 });
3318 project_c.read_with(cx_c, |project, _| {
3319 assert_eq!(project.collaborators().len(), 2);
3320 });
3321
3322 let buffer_b2 = project_b2
3323 .update(cx_b, |project, cx| {
3324 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3325 project.open_buffer((worktree_id, "a.txt"), cx)
3326 })
3327 .await
3328 .unwrap();
3329 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3330
3331 // Drop client B's connection and ensure client A and client C observe client B leaving.
3332 client_b.disconnect(&cx_b.to_async());
3333 deterministic.advance_clock(RECONNECT_TIMEOUT);
3334 project_a.read_with(cx_a, |project, _| {
3335 assert_eq!(project.collaborators().len(), 1);
3336 });
3337 project_b2.read_with(cx_b, |project, _| {
3338 assert!(project.is_read_only());
3339 });
3340 project_c.read_with(cx_c, |project, _| {
3341 assert_eq!(project.collaborators().len(), 1);
3342 });
3343
3344 // Client B can't join the project, unless they re-join the room.
3345 cx_b.spawn(|cx| {
3346 Project::remote(
3347 project_id,
3348 client_b.client.clone(),
3349 client_b.user_store.clone(),
3350 client_b.language_registry.clone(),
3351 FakeFs::new(cx.background()),
3352 cx,
3353 )
3354 })
3355 .await
3356 .unwrap_err();
3357
3358 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3359 client_c.wait_for_current_user(cx_c).await;
3360 server.forbid_connections();
3361 server.disconnect_client(client_c.peer_id().unwrap());
3362 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3363 deterministic.run_until_parked();
3364 project_a.read_with(cx_a, |project, _| {
3365 assert_eq!(project.collaborators().len(), 0);
3366 });
3367 project_b2.read_with(cx_b, |project, _| {
3368 assert!(project.is_read_only());
3369 });
3370 project_c.read_with(cx_c, |project, _| {
3371 assert!(project.is_read_only());
3372 });
3373}
3374
3375#[gpui::test(iterations = 10)]
3376async fn test_collaborating_with_diagnostics(
3377 deterministic: Arc<Deterministic>,
3378 cx_a: &mut TestAppContext,
3379 cx_b: &mut TestAppContext,
3380 cx_c: &mut TestAppContext,
3381) {
3382 deterministic.forbid_parking();
3383 let mut server = TestServer::start(&deterministic).await;
3384 let client_a = server.create_client(cx_a, "user_a").await;
3385 let client_b = server.create_client(cx_b, "user_b").await;
3386 let client_c = server.create_client(cx_c, "user_c").await;
3387 server
3388 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3389 .await;
3390 let active_call_a = cx_a.read(ActiveCall::global);
3391
3392 // Set up a fake language server.
3393 let mut language = Language::new(
3394 LanguageConfig {
3395 name: "Rust".into(),
3396 path_suffixes: vec!["rs".to_string()],
3397 ..Default::default()
3398 },
3399 Some(tree_sitter_rust::language()),
3400 );
3401 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3402 client_a.language_registry.add(Arc::new(language));
3403
3404 // Share a project as client A
3405 client_a
3406 .fs
3407 .insert_tree(
3408 "/a",
3409 json!({
3410 "a.rs": "let one = two",
3411 "other.rs": "",
3412 }),
3413 )
3414 .await;
3415 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3416
3417 // Cause the language server to start.
3418 let _buffer = project_a
3419 .update(cx_a, |project, cx| {
3420 project.open_buffer(
3421 ProjectPath {
3422 worktree_id,
3423 path: Path::new("other.rs").into(),
3424 },
3425 cx,
3426 )
3427 })
3428 .await
3429 .unwrap();
3430
3431 // Simulate a language server reporting errors for a file.
3432 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3433 fake_language_server
3434 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3435 .await;
3436 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3437 lsp::PublishDiagnosticsParams {
3438 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3439 version: None,
3440 diagnostics: vec![lsp::Diagnostic {
3441 severity: Some(lsp::DiagnosticSeverity::WARNING),
3442 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3443 message: "message 0".to_string(),
3444 ..Default::default()
3445 }],
3446 },
3447 );
3448
3449 // Client A shares the project and, simultaneously, the language server
3450 // publishes a diagnostic. This is done to ensure that the server always
3451 // observes the latest diagnostics for a worktree.
3452 let project_id = active_call_a
3453 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3454 .await
3455 .unwrap();
3456 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3457 lsp::PublishDiagnosticsParams {
3458 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3459 version: None,
3460 diagnostics: vec![lsp::Diagnostic {
3461 severity: Some(lsp::DiagnosticSeverity::ERROR),
3462 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3463 message: "message 1".to_string(),
3464 ..Default::default()
3465 }],
3466 },
3467 );
3468
3469 // Join the worktree as client B.
3470 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3471
3472 // Wait for server to see the diagnostics update.
3473 deterministic.run_until_parked();
3474
3475 // Ensure client B observes the new diagnostics.
3476 project_b.read_with(cx_b, |project, cx| {
3477 assert_eq!(
3478 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3479 &[(
3480 ProjectPath {
3481 worktree_id,
3482 path: Arc::from(Path::new("a.rs")),
3483 },
3484 LanguageServerId(0),
3485 DiagnosticSummary {
3486 error_count: 1,
3487 warning_count: 0,
3488 ..Default::default()
3489 },
3490 )]
3491 )
3492 });
3493
3494 // Join project as client C and observe the diagnostics.
3495 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3496 let project_c_diagnostic_summaries =
3497 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3498 project.diagnostic_summaries(cx).collect::<Vec<_>>()
3499 })));
3500 project_c.update(cx_c, |_, cx| {
3501 let summaries = project_c_diagnostic_summaries.clone();
3502 cx.subscribe(&project_c, {
3503 move |p, _, event, cx| {
3504 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3505 *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
3506 }
3507 }
3508 })
3509 .detach();
3510 });
3511
3512 deterministic.run_until_parked();
3513 assert_eq!(
3514 project_c_diagnostic_summaries.borrow().as_slice(),
3515 &[(
3516 ProjectPath {
3517 worktree_id,
3518 path: Arc::from(Path::new("a.rs")),
3519 },
3520 LanguageServerId(0),
3521 DiagnosticSummary {
3522 error_count: 1,
3523 warning_count: 0,
3524 ..Default::default()
3525 },
3526 )]
3527 );
3528
3529 // Simulate a language server reporting more errors for a file.
3530 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3531 lsp::PublishDiagnosticsParams {
3532 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3533 version: None,
3534 diagnostics: vec![
3535 lsp::Diagnostic {
3536 severity: Some(lsp::DiagnosticSeverity::ERROR),
3537 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3538 message: "message 1".to_string(),
3539 ..Default::default()
3540 },
3541 lsp::Diagnostic {
3542 severity: Some(lsp::DiagnosticSeverity::WARNING),
3543 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3544 message: "message 2".to_string(),
3545 ..Default::default()
3546 },
3547 ],
3548 },
3549 );
3550
3551 // Clients B and C get the updated summaries
3552 deterministic.run_until_parked();
3553 project_b.read_with(cx_b, |project, cx| {
3554 assert_eq!(
3555 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3556 [(
3557 ProjectPath {
3558 worktree_id,
3559 path: Arc::from(Path::new("a.rs")),
3560 },
3561 LanguageServerId(0),
3562 DiagnosticSummary {
3563 error_count: 1,
3564 warning_count: 1,
3565 },
3566 )]
3567 );
3568 });
3569 project_c.read_with(cx_c, |project, cx| {
3570 assert_eq!(
3571 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3572 [(
3573 ProjectPath {
3574 worktree_id,
3575 path: Arc::from(Path::new("a.rs")),
3576 },
3577 LanguageServerId(0),
3578 DiagnosticSummary {
3579 error_count: 1,
3580 warning_count: 1,
3581 },
3582 )]
3583 );
3584 });
3585
3586 // Open the file with the errors on client B. They should be present.
3587 let buffer_b = cx_b
3588 .background()
3589 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3590 .await
3591 .unwrap();
3592
3593 buffer_b.read_with(cx_b, |buffer, _| {
3594 assert_eq!(
3595 buffer
3596 .snapshot()
3597 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3598 .collect::<Vec<_>>(),
3599 &[
3600 DiagnosticEntry {
3601 range: Point::new(0, 4)..Point::new(0, 7),
3602 diagnostic: Diagnostic {
3603 group_id: 2,
3604 message: "message 1".to_string(),
3605 severity: lsp::DiagnosticSeverity::ERROR,
3606 is_primary: true,
3607 ..Default::default()
3608 }
3609 },
3610 DiagnosticEntry {
3611 range: Point::new(0, 10)..Point::new(0, 13),
3612 diagnostic: Diagnostic {
3613 group_id: 3,
3614 severity: lsp::DiagnosticSeverity::WARNING,
3615 message: "message 2".to_string(),
3616 is_primary: true,
3617 ..Default::default()
3618 }
3619 }
3620 ]
3621 );
3622 });
3623
3624 // Simulate a language server reporting no errors for a file.
3625 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3626 lsp::PublishDiagnosticsParams {
3627 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3628 version: None,
3629 diagnostics: vec![],
3630 },
3631 );
3632 deterministic.run_until_parked();
3633 project_a.read_with(cx_a, |project, cx| {
3634 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3635 });
3636 project_b.read_with(cx_b, |project, cx| {
3637 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3638 });
3639 project_c.read_with(cx_c, |project, cx| {
3640 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3641 });
3642}
3643
3644#[gpui::test(iterations = 10)]
3645async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
3646 deterministic: Arc<Deterministic>,
3647 cx_a: &mut TestAppContext,
3648 cx_b: &mut TestAppContext,
3649) {
3650 deterministic.forbid_parking();
3651 let mut server = TestServer::start(&deterministic).await;
3652 let client_a = server.create_client(cx_a, "user_a").await;
3653 let client_b = server.create_client(cx_b, "user_b").await;
3654 server
3655 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3656 .await;
3657
3658 // Set up a fake language server.
3659 let mut language = Language::new(
3660 LanguageConfig {
3661 name: "Rust".into(),
3662 path_suffixes: vec!["rs".to_string()],
3663 ..Default::default()
3664 },
3665 Some(tree_sitter_rust::language()),
3666 );
3667 let mut fake_language_servers = language
3668 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3669 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
3670 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
3671 ..Default::default()
3672 }))
3673 .await;
3674 client_a.language_registry.add(Arc::new(language));
3675
3676 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
3677 client_a
3678 .fs
3679 .insert_tree(
3680 "/test",
3681 json!({
3682 "one.rs": "const ONE: usize = 1;",
3683 "two.rs": "const TWO: usize = 2;",
3684 "three.rs": "const THREE: usize = 3;",
3685 "four.rs": "const FOUR: usize = 3;",
3686 "five.rs": "const FIVE: usize = 3;",
3687 }),
3688 )
3689 .await;
3690
3691 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
3692
3693 // Share a project as client A
3694 let active_call_a = cx_a.read(ActiveCall::global);
3695 let project_id = active_call_a
3696 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3697 .await
3698 .unwrap();
3699
3700 // Join the project as client B and open all three files.
3701 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3702 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
3703 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
3704 }))
3705 .await
3706 .unwrap();
3707
3708 // Simulate a language server reporting errors for a file.
3709 let fake_language_server = fake_language_servers.next().await.unwrap();
3710 fake_language_server
3711 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
3712 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3713 })
3714 .await
3715 .unwrap();
3716 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3717 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3718 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
3719 lsp::WorkDoneProgressBegin {
3720 title: "Progress Began".into(),
3721 ..Default::default()
3722 },
3723 )),
3724 });
3725 for file_name in file_names {
3726 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3727 lsp::PublishDiagnosticsParams {
3728 uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
3729 version: None,
3730 diagnostics: vec![lsp::Diagnostic {
3731 severity: Some(lsp::DiagnosticSeverity::WARNING),
3732 source: Some("the-disk-based-diagnostics-source".into()),
3733 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3734 message: "message one".to_string(),
3735 ..Default::default()
3736 }],
3737 },
3738 );
3739 }
3740 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3741 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
3742 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
3743 lsp::WorkDoneProgressEnd { message: None },
3744 )),
3745 });
3746
3747 // When the "disk base diagnostics finished" message is received, the buffers'
3748 // diagnostics are expected to be present.
3749 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
3750 project_b.update(cx_b, {
3751 let project_b = project_b.clone();
3752 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
3753 move |_, cx| {
3754 cx.subscribe(&project_b, move |_, _, event, cx| {
3755 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3756 disk_based_diagnostics_finished.store(true, SeqCst);
3757 for buffer in &guest_buffers {
3758 assert_eq!(
3759 buffer
3760 .read(cx)
3761 .snapshot()
3762 .diagnostics_in_range::<_, usize>(0..5, false)
3763 .count(),
3764 1,
3765 "expected a diagnostic for buffer {:?}",
3766 buffer.read(cx).file().unwrap().path(),
3767 );
3768 }
3769 }
3770 })
3771 .detach();
3772 }
3773 });
3774
3775 deterministic.run_until_parked();
3776 assert!(disk_based_diagnostics_finished.load(SeqCst));
3777}
3778
3779#[gpui::test(iterations = 10)]
3780async fn test_collaborating_with_completion(
3781 deterministic: Arc<Deterministic>,
3782 cx_a: &mut TestAppContext,
3783 cx_b: &mut TestAppContext,
3784) {
3785 deterministic.forbid_parking();
3786 let mut server = TestServer::start(&deterministic).await;
3787 let client_a = server.create_client(cx_a, "user_a").await;
3788 let client_b = server.create_client(cx_b, "user_b").await;
3789 server
3790 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3791 .await;
3792 let active_call_a = cx_a.read(ActiveCall::global);
3793
3794 // Set up a fake language server.
3795 let mut language = Language::new(
3796 LanguageConfig {
3797 name: "Rust".into(),
3798 path_suffixes: vec!["rs".to_string()],
3799 ..Default::default()
3800 },
3801 Some(tree_sitter_rust::language()),
3802 );
3803 let mut fake_language_servers = language
3804 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3805 capabilities: lsp::ServerCapabilities {
3806 completion_provider: Some(lsp::CompletionOptions {
3807 trigger_characters: Some(vec![".".to_string()]),
3808 ..Default::default()
3809 }),
3810 ..Default::default()
3811 },
3812 ..Default::default()
3813 }))
3814 .await;
3815 client_a.language_registry.add(Arc::new(language));
3816
3817 client_a
3818 .fs
3819 .insert_tree(
3820 "/a",
3821 json!({
3822 "main.rs": "fn main() { a }",
3823 "other.rs": "",
3824 }),
3825 )
3826 .await;
3827 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3828 let project_id = active_call_a
3829 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3830 .await
3831 .unwrap();
3832 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3833
3834 // Open a file in an editor as the guest.
3835 let buffer_b = project_b
3836 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3837 .await
3838 .unwrap();
3839 let (_, window_b) = cx_b.add_window(|_| EmptyView);
3840 let editor_b = cx_b.add_view(&window_b, |cx| {
3841 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
3842 });
3843
3844 let fake_language_server = fake_language_servers.next().await.unwrap();
3845 cx_a.foreground().run_until_parked();
3846 buffer_b.read_with(cx_b, |buffer, _| {
3847 assert!(!buffer.completion_triggers().is_empty())
3848 });
3849
3850 // Type a completion trigger character as the guest.
3851 editor_b.update(cx_b, |editor, cx| {
3852 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
3853 editor.handle_input(".", cx);
3854 cx.focus(&editor_b);
3855 });
3856
3857 // Receive a completion request as the host's language server.
3858 // Return some completions from the host's language server.
3859 cx_a.foreground().start_waiting();
3860 fake_language_server
3861 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
3862 assert_eq!(
3863 params.text_document_position.text_document.uri,
3864 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3865 );
3866 assert_eq!(
3867 params.text_document_position.position,
3868 lsp::Position::new(0, 14),
3869 );
3870
3871 Ok(Some(lsp::CompletionResponse::Array(vec![
3872 lsp::CompletionItem {
3873 label: "first_method(…)".into(),
3874 detail: Some("fn(&mut self, B) -> C".into()),
3875 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3876 new_text: "first_method($1)".to_string(),
3877 range: lsp::Range::new(
3878 lsp::Position::new(0, 14),
3879 lsp::Position::new(0, 14),
3880 ),
3881 })),
3882 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3883 ..Default::default()
3884 },
3885 lsp::CompletionItem {
3886 label: "second_method(…)".into(),
3887 detail: Some("fn(&mut self, C) -> D<E>".into()),
3888 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3889 new_text: "second_method()".to_string(),
3890 range: lsp::Range::new(
3891 lsp::Position::new(0, 14),
3892 lsp::Position::new(0, 14),
3893 ),
3894 })),
3895 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3896 ..Default::default()
3897 },
3898 ])))
3899 })
3900 .next()
3901 .await
3902 .unwrap();
3903 cx_a.foreground().finish_waiting();
3904
3905 // Open the buffer on the host.
3906 let buffer_a = project_a
3907 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3908 .await
3909 .unwrap();
3910 cx_a.foreground().run_until_parked();
3911 buffer_a.read_with(cx_a, |buffer, _| {
3912 assert_eq!(buffer.text(), "fn main() { a. }")
3913 });
3914
3915 // Confirm a completion on the guest.
3916 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
3917 editor_b.update(cx_b, |editor, cx| {
3918 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
3919 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
3920 });
3921
3922 // Return a resolved completion from the host's language server.
3923 // The resolved completion has an additional text edit.
3924 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
3925 |params, _| async move {
3926 assert_eq!(params.label, "first_method(…)");
3927 Ok(lsp::CompletionItem {
3928 label: "first_method(…)".into(),
3929 detail: Some("fn(&mut self, B) -> C".into()),
3930 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3931 new_text: "first_method($1)".to_string(),
3932 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
3933 })),
3934 additional_text_edits: Some(vec![lsp::TextEdit {
3935 new_text: "use d::SomeTrait;\n".to_string(),
3936 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3937 }]),
3938 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3939 ..Default::default()
3940 })
3941 },
3942 );
3943
3944 // The additional edit is applied.
3945 cx_a.foreground().run_until_parked();
3946 buffer_a.read_with(cx_a, |buffer, _| {
3947 assert_eq!(
3948 buffer.text(),
3949 "use d::SomeTrait;\nfn main() { a.first_method() }"
3950 );
3951 });
3952 buffer_b.read_with(cx_b, |buffer, _| {
3953 assert_eq!(
3954 buffer.text(),
3955 "use d::SomeTrait;\nfn main() { a.first_method() }"
3956 );
3957 });
3958}
3959
3960#[gpui::test(iterations = 10)]
3961async fn test_reloading_buffer_manually(
3962 deterministic: Arc<Deterministic>,
3963 cx_a: &mut TestAppContext,
3964 cx_b: &mut TestAppContext,
3965) {
3966 deterministic.forbid_parking();
3967 let mut server = TestServer::start(&deterministic).await;
3968 let client_a = server.create_client(cx_a, "user_a").await;
3969 let client_b = server.create_client(cx_b, "user_b").await;
3970 server
3971 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3972 .await;
3973 let active_call_a = cx_a.read(ActiveCall::global);
3974
3975 client_a
3976 .fs
3977 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
3978 .await;
3979 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3980 let buffer_a = project_a
3981 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
3982 .await
3983 .unwrap();
3984 let project_id = active_call_a
3985 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3986 .await
3987 .unwrap();
3988
3989 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3990
3991 let buffer_b = cx_b
3992 .background()
3993 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3994 .await
3995 .unwrap();
3996 buffer_b.update(cx_b, |buffer, cx| {
3997 buffer.edit([(4..7, "six")], None, cx);
3998 buffer.edit([(10..11, "6")], None, cx);
3999 assert_eq!(buffer.text(), "let six = 6;");
4000 assert!(buffer.is_dirty());
4001 assert!(!buffer.has_conflict());
4002 });
4003 cx_a.foreground().run_until_parked();
4004 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4005
4006 client_a
4007 .fs
4008 .save(
4009 "/a/a.rs".as_ref(),
4010 &Rope::from("let seven = 7;"),
4011 LineEnding::Unix,
4012 )
4013 .await
4014 .unwrap();
4015 cx_a.foreground().run_until_parked();
4016 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4017 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4018
4019 project_b
4020 .update(cx_b, |project, cx| {
4021 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4022 })
4023 .await
4024 .unwrap();
4025 buffer_a.read_with(cx_a, |buffer, _| {
4026 assert_eq!(buffer.text(), "let seven = 7;");
4027 assert!(!buffer.is_dirty());
4028 assert!(!buffer.has_conflict());
4029 });
4030 buffer_b.read_with(cx_b, |buffer, _| {
4031 assert_eq!(buffer.text(), "let seven = 7;");
4032 assert!(!buffer.is_dirty());
4033 assert!(!buffer.has_conflict());
4034 });
4035
4036 buffer_a.update(cx_a, |buffer, cx| {
4037 // Undoing on the host is a no-op when the reload was initiated by the guest.
4038 buffer.undo(cx);
4039 assert_eq!(buffer.text(), "let seven = 7;");
4040 assert!(!buffer.is_dirty());
4041 assert!(!buffer.has_conflict());
4042 });
4043 buffer_b.update(cx_b, |buffer, cx| {
4044 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4045 buffer.undo(cx);
4046 assert_eq!(buffer.text(), "let six = 6;");
4047 assert!(buffer.is_dirty());
4048 assert!(!buffer.has_conflict());
4049 });
4050}
4051
4052#[gpui::test(iterations = 10)]
4053async fn test_formatting_buffer(
4054 deterministic: Arc<Deterministic>,
4055 cx_a: &mut TestAppContext,
4056 cx_b: &mut TestAppContext,
4057) {
4058 use project::FormatTrigger;
4059
4060 let mut server = TestServer::start(&deterministic).await;
4061 let client_a = server.create_client(cx_a, "user_a").await;
4062 let client_b = server.create_client(cx_b, "user_b").await;
4063 server
4064 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4065 .await;
4066 let active_call_a = cx_a.read(ActiveCall::global);
4067
4068 // Set up a fake language server.
4069 let mut language = Language::new(
4070 LanguageConfig {
4071 name: "Rust".into(),
4072 path_suffixes: vec!["rs".to_string()],
4073 ..Default::default()
4074 },
4075 Some(tree_sitter_rust::language()),
4076 );
4077 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4078 client_a.language_registry.add(Arc::new(language));
4079
4080 // Here we insert a fake tree with a directory that exists on disk. This is needed
4081 // because later we'll invoke a command, which requires passing a working directory
4082 // that points to a valid location on disk.
4083 let directory = env::current_dir().unwrap();
4084 client_a
4085 .fs
4086 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4087 .await;
4088 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4089 let project_id = active_call_a
4090 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4091 .await
4092 .unwrap();
4093 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4094
4095 let buffer_b = cx_b
4096 .background()
4097 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4098 .await
4099 .unwrap();
4100
4101 let fake_language_server = fake_language_servers.next().await.unwrap();
4102 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4103 Ok(Some(vec![
4104 lsp::TextEdit {
4105 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4106 new_text: "h".to_string(),
4107 },
4108 lsp::TextEdit {
4109 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4110 new_text: "y".to_string(),
4111 },
4112 ]))
4113 });
4114
4115 project_b
4116 .update(cx_b, |project, cx| {
4117 project.format(
4118 HashSet::from_iter([buffer_b.clone()]),
4119 true,
4120 FormatTrigger::Save,
4121 cx,
4122 )
4123 })
4124 .await
4125 .unwrap();
4126
4127 // The edits from the LSP are applied, and a final newline is added.
4128 assert_eq!(
4129 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4130 "let honey = \"two\"\n"
4131 );
4132
4133 // Ensure buffer can be formatted using an external command. Notice how the
4134 // host's configuration is honored as opposed to using the guest's settings.
4135 cx_a.update(|cx| {
4136 cx.update_global(|settings: &mut Settings, _| {
4137 settings.editor_defaults.formatter = Some(Formatter::External {
4138 command: "awk".to_string(),
4139 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
4140 });
4141 });
4142 });
4143 project_b
4144 .update(cx_b, |project, cx| {
4145 project.format(
4146 HashSet::from_iter([buffer_b.clone()]),
4147 true,
4148 FormatTrigger::Save,
4149 cx,
4150 )
4151 })
4152 .await
4153 .unwrap();
4154 assert_eq!(
4155 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4156 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4157 );
4158}
4159
4160#[gpui::test(iterations = 10)]
4161async fn test_definition(
4162 deterministic: Arc<Deterministic>,
4163 cx_a: &mut TestAppContext,
4164 cx_b: &mut TestAppContext,
4165) {
4166 deterministic.forbid_parking();
4167 let mut server = TestServer::start(&deterministic).await;
4168 let client_a = server.create_client(cx_a, "user_a").await;
4169 let client_b = server.create_client(cx_b, "user_b").await;
4170 server
4171 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4172 .await;
4173 let active_call_a = cx_a.read(ActiveCall::global);
4174
4175 // Set up a fake language server.
4176 let mut language = Language::new(
4177 LanguageConfig {
4178 name: "Rust".into(),
4179 path_suffixes: vec!["rs".to_string()],
4180 ..Default::default()
4181 },
4182 Some(tree_sitter_rust::language()),
4183 );
4184 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4185 client_a.language_registry.add(Arc::new(language));
4186
4187 client_a
4188 .fs
4189 .insert_tree(
4190 "/root",
4191 json!({
4192 "dir-1": {
4193 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4194 },
4195 "dir-2": {
4196 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4197 "c.rs": "type T2 = usize;",
4198 }
4199 }),
4200 )
4201 .await;
4202 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4203 let project_id = active_call_a
4204 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4205 .await
4206 .unwrap();
4207 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4208
4209 // Open the file on client B.
4210 let buffer_b = cx_b
4211 .background()
4212 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4213 .await
4214 .unwrap();
4215
4216 // Request the definition of a symbol as the guest.
4217 let fake_language_server = fake_language_servers.next().await.unwrap();
4218 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4219 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4220 lsp::Location::new(
4221 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4222 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4223 ),
4224 )))
4225 });
4226
4227 let definitions_1 = project_b
4228 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4229 .await
4230 .unwrap();
4231 cx_b.read(|cx| {
4232 assert_eq!(definitions_1.len(), 1);
4233 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4234 let target_buffer = definitions_1[0].target.buffer.read(cx);
4235 assert_eq!(
4236 target_buffer.text(),
4237 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4238 );
4239 assert_eq!(
4240 definitions_1[0].target.range.to_point(target_buffer),
4241 Point::new(0, 6)..Point::new(0, 9)
4242 );
4243 });
4244
4245 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4246 // the previous call to `definition`.
4247 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4248 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4249 lsp::Location::new(
4250 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
4251 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4252 ),
4253 )))
4254 });
4255
4256 let definitions_2 = project_b
4257 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4258 .await
4259 .unwrap();
4260 cx_b.read(|cx| {
4261 assert_eq!(definitions_2.len(), 1);
4262 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4263 let target_buffer = definitions_2[0].target.buffer.read(cx);
4264 assert_eq!(
4265 target_buffer.text(),
4266 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4267 );
4268 assert_eq!(
4269 definitions_2[0].target.range.to_point(target_buffer),
4270 Point::new(1, 6)..Point::new(1, 11)
4271 );
4272 });
4273 assert_eq!(
4274 definitions_1[0].target.buffer,
4275 definitions_2[0].target.buffer
4276 );
4277
4278 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4279 |req, _| async move {
4280 assert_eq!(
4281 req.text_document_position_params.position,
4282 lsp::Position::new(0, 7)
4283 );
4284 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4285 lsp::Location::new(
4286 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
4287 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4288 ),
4289 )))
4290 },
4291 );
4292
4293 let type_definitions = project_b
4294 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4295 .await
4296 .unwrap();
4297 cx_b.read(|cx| {
4298 assert_eq!(type_definitions.len(), 1);
4299 let target_buffer = type_definitions[0].target.buffer.read(cx);
4300 assert_eq!(target_buffer.text(), "type T2 = usize;");
4301 assert_eq!(
4302 type_definitions[0].target.range.to_point(target_buffer),
4303 Point::new(0, 5)..Point::new(0, 7)
4304 );
4305 });
4306}
4307
4308#[gpui::test(iterations = 10)]
4309async fn test_references(
4310 deterministic: Arc<Deterministic>,
4311 cx_a: &mut TestAppContext,
4312 cx_b: &mut TestAppContext,
4313) {
4314 deterministic.forbid_parking();
4315 let mut server = TestServer::start(&deterministic).await;
4316 let client_a = server.create_client(cx_a, "user_a").await;
4317 let client_b = server.create_client(cx_b, "user_b").await;
4318 server
4319 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4320 .await;
4321 let active_call_a = cx_a.read(ActiveCall::global);
4322
4323 // Set up a fake language server.
4324 let mut language = Language::new(
4325 LanguageConfig {
4326 name: "Rust".into(),
4327 path_suffixes: vec!["rs".to_string()],
4328 ..Default::default()
4329 },
4330 Some(tree_sitter_rust::language()),
4331 );
4332 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4333 client_a.language_registry.add(Arc::new(language));
4334
4335 client_a
4336 .fs
4337 .insert_tree(
4338 "/root",
4339 json!({
4340 "dir-1": {
4341 "one.rs": "const ONE: usize = 1;",
4342 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4343 },
4344 "dir-2": {
4345 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4346 }
4347 }),
4348 )
4349 .await;
4350 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4351 let project_id = active_call_a
4352 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4353 .await
4354 .unwrap();
4355 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4356
4357 // Open the file on client B.
4358 let buffer_b = cx_b
4359 .background()
4360 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4361 .await
4362 .unwrap();
4363
4364 // Request references to a symbol as the guest.
4365 let fake_language_server = fake_language_servers.next().await.unwrap();
4366 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
4367 assert_eq!(
4368 params.text_document_position.text_document.uri.as_str(),
4369 "file:///root/dir-1/one.rs"
4370 );
4371 Ok(Some(vec![
4372 lsp::Location {
4373 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4374 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4375 },
4376 lsp::Location {
4377 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
4378 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4379 },
4380 lsp::Location {
4381 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
4382 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4383 },
4384 ]))
4385 });
4386
4387 let references = project_b
4388 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
4389 .await
4390 .unwrap();
4391 cx_b.read(|cx| {
4392 assert_eq!(references.len(), 3);
4393 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
4394
4395 let two_buffer = references[0].buffer.read(cx);
4396 let three_buffer = references[2].buffer.read(cx);
4397 assert_eq!(
4398 two_buffer.file().unwrap().path().as_ref(),
4399 Path::new("two.rs")
4400 );
4401 assert_eq!(references[1].buffer, references[0].buffer);
4402 assert_eq!(
4403 three_buffer.file().unwrap().full_path(cx),
4404 Path::new("/root/dir-2/three.rs")
4405 );
4406
4407 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4408 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4409 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4410 });
4411}
4412
4413#[gpui::test(iterations = 10)]
4414async fn test_project_search(
4415 deterministic: Arc<Deterministic>,
4416 cx_a: &mut TestAppContext,
4417 cx_b: &mut TestAppContext,
4418) {
4419 deterministic.forbid_parking();
4420 let mut server = TestServer::start(&deterministic).await;
4421 let client_a = server.create_client(cx_a, "user_a").await;
4422 let client_b = server.create_client(cx_b, "user_b").await;
4423 server
4424 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4425 .await;
4426 let active_call_a = cx_a.read(ActiveCall::global);
4427
4428 client_a
4429 .fs
4430 .insert_tree(
4431 "/root",
4432 json!({
4433 "dir-1": {
4434 "a": "hello world",
4435 "b": "goodnight moon",
4436 "c": "a world of goo",
4437 "d": "world champion of clown world",
4438 },
4439 "dir-2": {
4440 "e": "disney world is fun",
4441 }
4442 }),
4443 )
4444 .await;
4445 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4446 let (worktree_2, _) = project_a
4447 .update(cx_a, |p, cx| {
4448 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4449 })
4450 .await
4451 .unwrap();
4452 worktree_2
4453 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4454 .await;
4455 let project_id = active_call_a
4456 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4457 .await
4458 .unwrap();
4459
4460 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4461
4462 // Perform a search as the guest.
4463 let results = project_b
4464 .update(cx_b, |project, cx| {
4465 project.search(SearchQuery::text("world", false, false), cx)
4466 })
4467 .await
4468 .unwrap();
4469
4470 let mut ranges_by_path = results
4471 .into_iter()
4472 .map(|(buffer, ranges)| {
4473 buffer.read_with(cx_b, |buffer, cx| {
4474 let path = buffer.file().unwrap().full_path(cx);
4475 let offset_ranges = ranges
4476 .into_iter()
4477 .map(|range| range.to_offset(buffer))
4478 .collect::<Vec<_>>();
4479 (path, offset_ranges)
4480 })
4481 })
4482 .collect::<Vec<_>>();
4483 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4484
4485 assert_eq!(
4486 ranges_by_path,
4487 &[
4488 (PathBuf::from("dir-1/a"), vec![6..11]),
4489 (PathBuf::from("dir-1/c"), vec![2..7]),
4490 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4491 (PathBuf::from("dir-2/e"), vec![7..12]),
4492 ]
4493 );
4494}
4495
4496#[gpui::test(iterations = 10)]
4497async fn test_document_highlights(
4498 deterministic: Arc<Deterministic>,
4499 cx_a: &mut TestAppContext,
4500 cx_b: &mut TestAppContext,
4501) {
4502 deterministic.forbid_parking();
4503 let mut server = TestServer::start(&deterministic).await;
4504 let client_a = server.create_client(cx_a, "user_a").await;
4505 let client_b = server.create_client(cx_b, "user_b").await;
4506 server
4507 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4508 .await;
4509 let active_call_a = cx_a.read(ActiveCall::global);
4510
4511 client_a
4512 .fs
4513 .insert_tree(
4514 "/root-1",
4515 json!({
4516 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4517 }),
4518 )
4519 .await;
4520
4521 // Set up a fake language server.
4522 let mut language = Language::new(
4523 LanguageConfig {
4524 name: "Rust".into(),
4525 path_suffixes: vec!["rs".to_string()],
4526 ..Default::default()
4527 },
4528 Some(tree_sitter_rust::language()),
4529 );
4530 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4531 client_a.language_registry.add(Arc::new(language));
4532
4533 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4534 let project_id = active_call_a
4535 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4536 .await
4537 .unwrap();
4538 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4539
4540 // Open the file on client B.
4541 let buffer_b = cx_b
4542 .background()
4543 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4544 .await
4545 .unwrap();
4546
4547 // Request document highlights as the guest.
4548 let fake_language_server = fake_language_servers.next().await.unwrap();
4549 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4550 |params, _| async move {
4551 assert_eq!(
4552 params
4553 .text_document_position_params
4554 .text_document
4555 .uri
4556 .as_str(),
4557 "file:///root-1/main.rs"
4558 );
4559 assert_eq!(
4560 params.text_document_position_params.position,
4561 lsp::Position::new(0, 34)
4562 );
4563 Ok(Some(vec![
4564 lsp::DocumentHighlight {
4565 kind: Some(lsp::DocumentHighlightKind::WRITE),
4566 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4567 },
4568 lsp::DocumentHighlight {
4569 kind: Some(lsp::DocumentHighlightKind::READ),
4570 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4571 },
4572 lsp::DocumentHighlight {
4573 kind: Some(lsp::DocumentHighlightKind::READ),
4574 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4575 },
4576 ]))
4577 },
4578 );
4579
4580 let highlights = project_b
4581 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4582 .await
4583 .unwrap();
4584 buffer_b.read_with(cx_b, |buffer, _| {
4585 let snapshot = buffer.snapshot();
4586
4587 let highlights = highlights
4588 .into_iter()
4589 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4590 .collect::<Vec<_>>();
4591 assert_eq!(
4592 highlights,
4593 &[
4594 (lsp::DocumentHighlightKind::WRITE, 10..16),
4595 (lsp::DocumentHighlightKind::READ, 32..38),
4596 (lsp::DocumentHighlightKind::READ, 41..47)
4597 ]
4598 )
4599 });
4600}
4601
4602#[gpui::test(iterations = 10)]
4603async fn test_lsp_hover(
4604 deterministic: Arc<Deterministic>,
4605 cx_a: &mut TestAppContext,
4606 cx_b: &mut TestAppContext,
4607) {
4608 deterministic.forbid_parking();
4609 let mut server = TestServer::start(&deterministic).await;
4610 let client_a = server.create_client(cx_a, "user_a").await;
4611 let client_b = server.create_client(cx_b, "user_b").await;
4612 server
4613 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4614 .await;
4615 let active_call_a = cx_a.read(ActiveCall::global);
4616
4617 client_a
4618 .fs
4619 .insert_tree(
4620 "/root-1",
4621 json!({
4622 "main.rs": "use std::collections::HashMap;",
4623 }),
4624 )
4625 .await;
4626
4627 // Set up a fake language server.
4628 let mut language = Language::new(
4629 LanguageConfig {
4630 name: "Rust".into(),
4631 path_suffixes: vec!["rs".to_string()],
4632 ..Default::default()
4633 },
4634 Some(tree_sitter_rust::language()),
4635 );
4636 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4637 client_a.language_registry.add(Arc::new(language));
4638
4639 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4640 let project_id = active_call_a
4641 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4642 .await
4643 .unwrap();
4644 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4645
4646 // Open the file as the guest
4647 let buffer_b = cx_b
4648 .background()
4649 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4650 .await
4651 .unwrap();
4652
4653 // Request hover information as the guest.
4654 let fake_language_server = fake_language_servers.next().await.unwrap();
4655 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4656 |params, _| async move {
4657 assert_eq!(
4658 params
4659 .text_document_position_params
4660 .text_document
4661 .uri
4662 .as_str(),
4663 "file:///root-1/main.rs"
4664 );
4665 assert_eq!(
4666 params.text_document_position_params.position,
4667 lsp::Position::new(0, 22)
4668 );
4669 Ok(Some(lsp::Hover {
4670 contents: lsp::HoverContents::Array(vec![
4671 lsp::MarkedString::String("Test hover content.".to_string()),
4672 lsp::MarkedString::LanguageString(lsp::LanguageString {
4673 language: "Rust".to_string(),
4674 value: "let foo = 42;".to_string(),
4675 }),
4676 ]),
4677 range: Some(lsp::Range::new(
4678 lsp::Position::new(0, 22),
4679 lsp::Position::new(0, 29),
4680 )),
4681 }))
4682 },
4683 );
4684
4685 let hover_info = project_b
4686 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
4687 .await
4688 .unwrap()
4689 .unwrap();
4690 buffer_b.read_with(cx_b, |buffer, _| {
4691 let snapshot = buffer.snapshot();
4692 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
4693 assert_eq!(
4694 hover_info.contents,
4695 vec![
4696 project::HoverBlock {
4697 text: "Test hover content.".to_string(),
4698 kind: HoverBlockKind::Markdown,
4699 },
4700 project::HoverBlock {
4701 text: "let foo = 42;".to_string(),
4702 kind: HoverBlockKind::Code {
4703 language: "Rust".to_string()
4704 },
4705 }
4706 ]
4707 );
4708 });
4709}
4710
4711#[gpui::test(iterations = 10)]
4712async fn test_project_symbols(
4713 deterministic: Arc<Deterministic>,
4714 cx_a: &mut TestAppContext,
4715 cx_b: &mut TestAppContext,
4716) {
4717 deterministic.forbid_parking();
4718 let mut server = TestServer::start(&deterministic).await;
4719 let client_a = server.create_client(cx_a, "user_a").await;
4720 let client_b = server.create_client(cx_b, "user_b").await;
4721 server
4722 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4723 .await;
4724 let active_call_a = cx_a.read(ActiveCall::global);
4725
4726 // Set up a fake language server.
4727 let mut language = Language::new(
4728 LanguageConfig {
4729 name: "Rust".into(),
4730 path_suffixes: vec!["rs".to_string()],
4731 ..Default::default()
4732 },
4733 Some(tree_sitter_rust::language()),
4734 );
4735 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4736 client_a.language_registry.add(Arc::new(language));
4737
4738 client_a
4739 .fs
4740 .insert_tree(
4741 "/code",
4742 json!({
4743 "crate-1": {
4744 "one.rs": "const ONE: usize = 1;",
4745 },
4746 "crate-2": {
4747 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
4748 },
4749 "private": {
4750 "passwords.txt": "the-password",
4751 }
4752 }),
4753 )
4754 .await;
4755 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
4756 let project_id = active_call_a
4757 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4758 .await
4759 .unwrap();
4760 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4761
4762 // Cause the language server to start.
4763 let _buffer = cx_b
4764 .background()
4765 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4766 .await
4767 .unwrap();
4768
4769 let fake_language_server = fake_language_servers.next().await.unwrap();
4770 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
4771 #[allow(deprecated)]
4772 Ok(Some(vec![lsp::SymbolInformation {
4773 name: "TWO".into(),
4774 location: lsp::Location {
4775 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
4776 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4777 },
4778 kind: lsp::SymbolKind::CONSTANT,
4779 tags: None,
4780 container_name: None,
4781 deprecated: None,
4782 }]))
4783 });
4784
4785 // Request the definition of a symbol as the guest.
4786 let symbols = project_b
4787 .update(cx_b, |p, cx| p.symbols("two", cx))
4788 .await
4789 .unwrap();
4790 assert_eq!(symbols.len(), 1);
4791 assert_eq!(symbols[0].name, "TWO");
4792
4793 // Open one of the returned symbols.
4794 let buffer_b_2 = project_b
4795 .update(cx_b, |project, cx| {
4796 project.open_buffer_for_symbol(&symbols[0], cx)
4797 })
4798 .await
4799 .unwrap();
4800 buffer_b_2.read_with(cx_b, |buffer, _| {
4801 assert_eq!(
4802 buffer.file().unwrap().path().as_ref(),
4803 Path::new("../crate-2/two.rs")
4804 );
4805 });
4806
4807 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
4808 let mut fake_symbol = symbols[0].clone();
4809 fake_symbol.path.path = Path::new("/code/secrets").into();
4810 let error = project_b
4811 .update(cx_b, |project, cx| {
4812 project.open_buffer_for_symbol(&fake_symbol, cx)
4813 })
4814 .await
4815 .unwrap_err();
4816 assert!(error.to_string().contains("invalid symbol signature"));
4817}
4818
4819#[gpui::test(iterations = 10)]
4820async fn test_open_buffer_while_getting_definition_pointing_to_it(
4821 deterministic: Arc<Deterministic>,
4822 cx_a: &mut TestAppContext,
4823 cx_b: &mut TestAppContext,
4824 mut rng: StdRng,
4825) {
4826 deterministic.forbid_parking();
4827 let mut server = TestServer::start(&deterministic).await;
4828 let client_a = server.create_client(cx_a, "user_a").await;
4829 let client_b = server.create_client(cx_b, "user_b").await;
4830 server
4831 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4832 .await;
4833 let active_call_a = cx_a.read(ActiveCall::global);
4834
4835 // Set up a fake language server.
4836 let mut language = Language::new(
4837 LanguageConfig {
4838 name: "Rust".into(),
4839 path_suffixes: vec!["rs".to_string()],
4840 ..Default::default()
4841 },
4842 Some(tree_sitter_rust::language()),
4843 );
4844 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4845 client_a.language_registry.add(Arc::new(language));
4846
4847 client_a
4848 .fs
4849 .insert_tree(
4850 "/root",
4851 json!({
4852 "a.rs": "const ONE: usize = b::TWO;",
4853 "b.rs": "const TWO: usize = 2",
4854 }),
4855 )
4856 .await;
4857 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
4858 let project_id = active_call_a
4859 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4860 .await
4861 .unwrap();
4862 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4863
4864 let buffer_b1 = cx_b
4865 .background()
4866 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4867 .await
4868 .unwrap();
4869
4870 let fake_language_server = fake_language_servers.next().await.unwrap();
4871 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4872 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4873 lsp::Location::new(
4874 lsp::Url::from_file_path("/root/b.rs").unwrap(),
4875 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4876 ),
4877 )))
4878 });
4879
4880 let definitions;
4881 let buffer_b2;
4882 if rng.gen() {
4883 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
4884 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
4885 } else {
4886 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
4887 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
4888 }
4889
4890 let buffer_b2 = buffer_b2.await.unwrap();
4891 let definitions = definitions.await.unwrap();
4892 assert_eq!(definitions.len(), 1);
4893 assert_eq!(definitions[0].target.buffer, buffer_b2);
4894}
4895
4896#[gpui::test(iterations = 10)]
4897async fn test_collaborating_with_code_actions(
4898 deterministic: Arc<Deterministic>,
4899 cx_a: &mut TestAppContext,
4900 cx_b: &mut TestAppContext,
4901) {
4902 deterministic.forbid_parking();
4903 cx_b.update(editor::init);
4904 let mut server = TestServer::start(&deterministic).await;
4905 let client_a = server.create_client(cx_a, "user_a").await;
4906 let client_b = server.create_client(cx_b, "user_b").await;
4907 server
4908 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4909 .await;
4910 let active_call_a = cx_a.read(ActiveCall::global);
4911
4912 // Set up a fake language server.
4913 let mut language = Language::new(
4914 LanguageConfig {
4915 name: "Rust".into(),
4916 path_suffixes: vec!["rs".to_string()],
4917 ..Default::default()
4918 },
4919 Some(tree_sitter_rust::language()),
4920 );
4921 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4922 client_a.language_registry.add(Arc::new(language));
4923
4924 client_a
4925 .fs
4926 .insert_tree(
4927 "/a",
4928 json!({
4929 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
4930 "other.rs": "pub fn foo() -> usize { 4 }",
4931 }),
4932 )
4933 .await;
4934 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4935 let project_id = active_call_a
4936 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4937 .await
4938 .unwrap();
4939
4940 // Join the project as client B.
4941 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4942 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
4943 let editor_b = workspace_b
4944 .update(cx_b, |workspace, cx| {
4945 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
4946 })
4947 .await
4948 .unwrap()
4949 .downcast::<Editor>()
4950 .unwrap();
4951
4952 let mut fake_language_server = fake_language_servers.next().await.unwrap();
4953 fake_language_server
4954 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
4955 assert_eq!(
4956 params.text_document.uri,
4957 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4958 );
4959 assert_eq!(params.range.start, lsp::Position::new(0, 0));
4960 assert_eq!(params.range.end, lsp::Position::new(0, 0));
4961 Ok(None)
4962 })
4963 .next()
4964 .await;
4965
4966 // Move cursor to a location that contains code actions.
4967 editor_b.update(cx_b, |editor, cx| {
4968 editor.change_selections(None, cx, |s| {
4969 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
4970 });
4971 cx.focus(&editor_b);
4972 });
4973
4974 fake_language_server
4975 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
4976 assert_eq!(
4977 params.text_document.uri,
4978 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4979 );
4980 assert_eq!(params.range.start, lsp::Position::new(1, 31));
4981 assert_eq!(params.range.end, lsp::Position::new(1, 31));
4982
4983 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4984 lsp::CodeAction {
4985 title: "Inline into all callers".to_string(),
4986 edit: Some(lsp::WorkspaceEdit {
4987 changes: Some(
4988 [
4989 (
4990 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4991 vec![lsp::TextEdit::new(
4992 lsp::Range::new(
4993 lsp::Position::new(1, 22),
4994 lsp::Position::new(1, 34),
4995 ),
4996 "4".to_string(),
4997 )],
4998 ),
4999 (
5000 lsp::Url::from_file_path("/a/other.rs").unwrap(),
5001 vec![lsp::TextEdit::new(
5002 lsp::Range::new(
5003 lsp::Position::new(0, 0),
5004 lsp::Position::new(0, 27),
5005 ),
5006 "".to_string(),
5007 )],
5008 ),
5009 ]
5010 .into_iter()
5011 .collect(),
5012 ),
5013 ..Default::default()
5014 }),
5015 data: Some(json!({
5016 "codeActionParams": {
5017 "range": {
5018 "start": {"line": 1, "column": 31},
5019 "end": {"line": 1, "column": 31},
5020 }
5021 }
5022 })),
5023 ..Default::default()
5024 },
5025 )]))
5026 })
5027 .next()
5028 .await;
5029
5030 // Toggle code actions and wait for them to display.
5031 editor_b.update(cx_b, |editor, cx| {
5032 editor.toggle_code_actions(
5033 &ToggleCodeActions {
5034 deployed_from_indicator: false,
5035 },
5036 cx,
5037 );
5038 });
5039 cx_a.foreground().run_until_parked();
5040 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
5041
5042 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
5043
5044 // Confirming the code action will trigger a resolve request.
5045 let confirm_action = workspace_b
5046 .update(cx_b, |workspace, cx| {
5047 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
5048 })
5049 .unwrap();
5050 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
5051 |_, _| async move {
5052 Ok(lsp::CodeAction {
5053 title: "Inline into all callers".to_string(),
5054 edit: Some(lsp::WorkspaceEdit {
5055 changes: Some(
5056 [
5057 (
5058 lsp::Url::from_file_path("/a/main.rs").unwrap(),
5059 vec![lsp::TextEdit::new(
5060 lsp::Range::new(
5061 lsp::Position::new(1, 22),
5062 lsp::Position::new(1, 34),
5063 ),
5064 "4".to_string(),
5065 )],
5066 ),
5067 (
5068 lsp::Url::from_file_path("/a/other.rs").unwrap(),
5069 vec![lsp::TextEdit::new(
5070 lsp::Range::new(
5071 lsp::Position::new(0, 0),
5072 lsp::Position::new(0, 27),
5073 ),
5074 "".to_string(),
5075 )],
5076 ),
5077 ]
5078 .into_iter()
5079 .collect(),
5080 ),
5081 ..Default::default()
5082 }),
5083 ..Default::default()
5084 })
5085 },
5086 );
5087
5088 // After the action is confirmed, an editor containing both modified files is opened.
5089 confirm_action.await.unwrap();
5090 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5091 workspace
5092 .active_item(cx)
5093 .unwrap()
5094 .downcast::<Editor>()
5095 .unwrap()
5096 });
5097 code_action_editor.update(cx_b, |editor, cx| {
5098 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
5099 editor.undo(&Undo, cx);
5100 assert_eq!(
5101 editor.text(cx),
5102 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
5103 );
5104 editor.redo(&Redo, cx);
5105 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
5106 });
5107}
5108
5109#[gpui::test(iterations = 10)]
5110async fn test_collaborating_with_renames(
5111 deterministic: Arc<Deterministic>,
5112 cx_a: &mut TestAppContext,
5113 cx_b: &mut TestAppContext,
5114) {
5115 deterministic.forbid_parking();
5116 cx_b.update(editor::init);
5117 let mut server = TestServer::start(&deterministic).await;
5118 let client_a = server.create_client(cx_a, "user_a").await;
5119 let client_b = server.create_client(cx_b, "user_b").await;
5120 server
5121 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5122 .await;
5123 let active_call_a = cx_a.read(ActiveCall::global);
5124
5125 // Set up a fake language server.
5126 let mut language = Language::new(
5127 LanguageConfig {
5128 name: "Rust".into(),
5129 path_suffixes: vec!["rs".to_string()],
5130 ..Default::default()
5131 },
5132 Some(tree_sitter_rust::language()),
5133 );
5134 let mut fake_language_servers = language
5135 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5136 capabilities: lsp::ServerCapabilities {
5137 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
5138 prepare_provider: Some(true),
5139 work_done_progress_options: Default::default(),
5140 })),
5141 ..Default::default()
5142 },
5143 ..Default::default()
5144 }))
5145 .await;
5146 client_a.language_registry.add(Arc::new(language));
5147
5148 client_a
5149 .fs
5150 .insert_tree(
5151 "/dir",
5152 json!({
5153 "one.rs": "const ONE: usize = 1;",
5154 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
5155 }),
5156 )
5157 .await;
5158 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5159 let project_id = active_call_a
5160 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5161 .await
5162 .unwrap();
5163 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5164
5165 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
5166 let editor_b = workspace_b
5167 .update(cx_b, |workspace, cx| {
5168 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
5169 })
5170 .await
5171 .unwrap()
5172 .downcast::<Editor>()
5173 .unwrap();
5174 let fake_language_server = fake_language_servers.next().await.unwrap();
5175
5176 // Move cursor to a location that can be renamed.
5177 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
5178 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
5179 editor.rename(&Rename, cx).unwrap()
5180 });
5181
5182 fake_language_server
5183 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
5184 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
5185 assert_eq!(params.position, lsp::Position::new(0, 7));
5186 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
5187 lsp::Position::new(0, 6),
5188 lsp::Position::new(0, 9),
5189 ))))
5190 })
5191 .next()
5192 .await
5193 .unwrap();
5194 prepare_rename.await.unwrap();
5195 editor_b.update(cx_b, |editor, cx| {
5196 let rename = editor.pending_rename().unwrap();
5197 let buffer = editor.buffer().read(cx).snapshot(cx);
5198 assert_eq!(
5199 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
5200 6..9
5201 );
5202 rename.editor.update(cx, |rename_editor, cx| {
5203 rename_editor.buffer().update(cx, |rename_buffer, cx| {
5204 rename_buffer.edit([(0..3, "THREE")], None, cx);
5205 });
5206 });
5207 });
5208
5209 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
5210 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
5211 });
5212 fake_language_server
5213 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
5214 assert_eq!(
5215 params.text_document_position.text_document.uri.as_str(),
5216 "file:///dir/one.rs"
5217 );
5218 assert_eq!(
5219 params.text_document_position.position,
5220 lsp::Position::new(0, 6)
5221 );
5222 assert_eq!(params.new_name, "THREE");
5223 Ok(Some(lsp::WorkspaceEdit {
5224 changes: Some(
5225 [
5226 (
5227 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
5228 vec![lsp::TextEdit::new(
5229 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5230 "THREE".to_string(),
5231 )],
5232 ),
5233 (
5234 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
5235 vec![
5236 lsp::TextEdit::new(
5237 lsp::Range::new(
5238 lsp::Position::new(0, 24),
5239 lsp::Position::new(0, 27),
5240 ),
5241 "THREE".to_string(),
5242 ),
5243 lsp::TextEdit::new(
5244 lsp::Range::new(
5245 lsp::Position::new(0, 35),
5246 lsp::Position::new(0, 38),
5247 ),
5248 "THREE".to_string(),
5249 ),
5250 ],
5251 ),
5252 ]
5253 .into_iter()
5254 .collect(),
5255 ),
5256 ..Default::default()
5257 }))
5258 })
5259 .next()
5260 .await
5261 .unwrap();
5262 confirm_rename.await.unwrap();
5263
5264 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
5265 workspace
5266 .active_item(cx)
5267 .unwrap()
5268 .downcast::<Editor>()
5269 .unwrap()
5270 });
5271 rename_editor.update(cx_b, |editor, cx| {
5272 assert_eq!(
5273 editor.text(cx),
5274 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5275 );
5276 editor.undo(&Undo, cx);
5277 assert_eq!(
5278 editor.text(cx),
5279 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
5280 );
5281 editor.redo(&Redo, cx);
5282 assert_eq!(
5283 editor.text(cx),
5284 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
5285 );
5286 });
5287
5288 // Ensure temporary rename edits cannot be undone/redone.
5289 editor_b.update(cx_b, |editor, cx| {
5290 editor.undo(&Undo, cx);
5291 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5292 editor.undo(&Undo, cx);
5293 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
5294 editor.redo(&Redo, cx);
5295 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
5296 })
5297}
5298
5299#[gpui::test(iterations = 10)]
5300async fn test_language_server_statuses(
5301 deterministic: Arc<Deterministic>,
5302 cx_a: &mut TestAppContext,
5303 cx_b: &mut TestAppContext,
5304) {
5305 deterministic.forbid_parking();
5306
5307 cx_b.update(editor::init);
5308 let mut server = TestServer::start(&deterministic).await;
5309 let client_a = server.create_client(cx_a, "user_a").await;
5310 let client_b = server.create_client(cx_b, "user_b").await;
5311 server
5312 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5313 .await;
5314 let active_call_a = cx_a.read(ActiveCall::global);
5315
5316 // Set up a fake language server.
5317 let mut language = Language::new(
5318 LanguageConfig {
5319 name: "Rust".into(),
5320 path_suffixes: vec!["rs".to_string()],
5321 ..Default::default()
5322 },
5323 Some(tree_sitter_rust::language()),
5324 );
5325 let mut fake_language_servers = language
5326 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5327 name: "the-language-server",
5328 ..Default::default()
5329 }))
5330 .await;
5331 client_a.language_registry.add(Arc::new(language));
5332
5333 client_a
5334 .fs
5335 .insert_tree(
5336 "/dir",
5337 json!({
5338 "main.rs": "const ONE: usize = 1;",
5339 }),
5340 )
5341 .await;
5342 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
5343
5344 let _buffer_a = project_a
5345 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
5346 .await
5347 .unwrap();
5348
5349 let fake_language_server = fake_language_servers.next().await.unwrap();
5350 fake_language_server.start_progress("the-token").await;
5351 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5352 token: lsp::NumberOrString::String("the-token".to_string()),
5353 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5354 lsp::WorkDoneProgressReport {
5355 message: Some("the-message".to_string()),
5356 ..Default::default()
5357 },
5358 )),
5359 });
5360 deterministic.run_until_parked();
5361 project_a.read_with(cx_a, |project, _| {
5362 let status = project.language_server_statuses().next().unwrap();
5363 assert_eq!(status.name, "the-language-server");
5364 assert_eq!(status.pending_work.len(), 1);
5365 assert_eq!(
5366 status.pending_work["the-token"].message.as_ref().unwrap(),
5367 "the-message"
5368 );
5369 });
5370
5371 let project_id = active_call_a
5372 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5373 .await
5374 .unwrap();
5375 deterministic.run_until_parked();
5376 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5377 project_b.read_with(cx_b, |project, _| {
5378 let status = project.language_server_statuses().next().unwrap();
5379 assert_eq!(status.name, "the-language-server");
5380 });
5381
5382 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
5383 token: lsp::NumberOrString::String("the-token".to_string()),
5384 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
5385 lsp::WorkDoneProgressReport {
5386 message: Some("the-message-2".to_string()),
5387 ..Default::default()
5388 },
5389 )),
5390 });
5391 deterministic.run_until_parked();
5392 project_a.read_with(cx_a, |project, _| {
5393 let status = project.language_server_statuses().next().unwrap();
5394 assert_eq!(status.name, "the-language-server");
5395 assert_eq!(status.pending_work.len(), 1);
5396 assert_eq!(
5397 status.pending_work["the-token"].message.as_ref().unwrap(),
5398 "the-message-2"
5399 );
5400 });
5401 project_b.read_with(cx_b, |project, _| {
5402 let status = project.language_server_statuses().next().unwrap();
5403 assert_eq!(status.name, "the-language-server");
5404 assert_eq!(status.pending_work.len(), 1);
5405 assert_eq!(
5406 status.pending_work["the-token"].message.as_ref().unwrap(),
5407 "the-message-2"
5408 );
5409 });
5410}
5411
5412#[gpui::test(iterations = 10)]
5413async fn test_contacts(
5414 deterministic: Arc<Deterministic>,
5415 cx_a: &mut TestAppContext,
5416 cx_b: &mut TestAppContext,
5417 cx_c: &mut TestAppContext,
5418 cx_d: &mut TestAppContext,
5419) {
5420 deterministic.forbid_parking();
5421 let mut server = TestServer::start(&deterministic).await;
5422 let client_a = server.create_client(cx_a, "user_a").await;
5423 let client_b = server.create_client(cx_b, "user_b").await;
5424 let client_c = server.create_client(cx_c, "user_c").await;
5425 let client_d = server.create_client(cx_d, "user_d").await;
5426 server
5427 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5428 .await;
5429 let active_call_a = cx_a.read(ActiveCall::global);
5430 let active_call_b = cx_b.read(ActiveCall::global);
5431 let active_call_c = cx_c.read(ActiveCall::global);
5432 let _active_call_d = cx_d.read(ActiveCall::global);
5433
5434 deterministic.run_until_parked();
5435 assert_eq!(
5436 contacts(&client_a, cx_a),
5437 [
5438 ("user_b".to_string(), "online", "free"),
5439 ("user_c".to_string(), "online", "free")
5440 ]
5441 );
5442 assert_eq!(
5443 contacts(&client_b, cx_b),
5444 [
5445 ("user_a".to_string(), "online", "free"),
5446 ("user_c".to_string(), "online", "free")
5447 ]
5448 );
5449 assert_eq!(
5450 contacts(&client_c, cx_c),
5451 [
5452 ("user_a".to_string(), "online", "free"),
5453 ("user_b".to_string(), "online", "free")
5454 ]
5455 );
5456 assert_eq!(contacts(&client_d, cx_d), []);
5457
5458 server.disconnect_client(client_c.peer_id().unwrap());
5459 server.forbid_connections();
5460 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5461 assert_eq!(
5462 contacts(&client_a, cx_a),
5463 [
5464 ("user_b".to_string(), "online", "free"),
5465 ("user_c".to_string(), "offline", "free")
5466 ]
5467 );
5468 assert_eq!(
5469 contacts(&client_b, cx_b),
5470 [
5471 ("user_a".to_string(), "online", "free"),
5472 ("user_c".to_string(), "offline", "free")
5473 ]
5474 );
5475 assert_eq!(contacts(&client_c, cx_c), []);
5476 assert_eq!(contacts(&client_d, cx_d), []);
5477
5478 server.allow_connections();
5479 client_c
5480 .authenticate_and_connect(false, &cx_c.to_async())
5481 .await
5482 .unwrap();
5483
5484 deterministic.run_until_parked();
5485 assert_eq!(
5486 contacts(&client_a, cx_a),
5487 [
5488 ("user_b".to_string(), "online", "free"),
5489 ("user_c".to_string(), "online", "free")
5490 ]
5491 );
5492 assert_eq!(
5493 contacts(&client_b, cx_b),
5494 [
5495 ("user_a".to_string(), "online", "free"),
5496 ("user_c".to_string(), "online", "free")
5497 ]
5498 );
5499 assert_eq!(
5500 contacts(&client_c, cx_c),
5501 [
5502 ("user_a".to_string(), "online", "free"),
5503 ("user_b".to_string(), "online", "free")
5504 ]
5505 );
5506 assert_eq!(contacts(&client_d, cx_d), []);
5507
5508 active_call_a
5509 .update(cx_a, |call, cx| {
5510 call.invite(client_b.user_id().unwrap(), None, cx)
5511 })
5512 .await
5513 .unwrap();
5514 deterministic.run_until_parked();
5515 assert_eq!(
5516 contacts(&client_a, cx_a),
5517 [
5518 ("user_b".to_string(), "online", "busy"),
5519 ("user_c".to_string(), "online", "free")
5520 ]
5521 );
5522 assert_eq!(
5523 contacts(&client_b, cx_b),
5524 [
5525 ("user_a".to_string(), "online", "busy"),
5526 ("user_c".to_string(), "online", "free")
5527 ]
5528 );
5529 assert_eq!(
5530 contacts(&client_c, cx_c),
5531 [
5532 ("user_a".to_string(), "online", "busy"),
5533 ("user_b".to_string(), "online", "busy")
5534 ]
5535 );
5536 assert_eq!(contacts(&client_d, cx_d), []);
5537
5538 // Client B and client D become contacts while client B is being called.
5539 server
5540 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5541 .await;
5542 deterministic.run_until_parked();
5543 assert_eq!(
5544 contacts(&client_a, cx_a),
5545 [
5546 ("user_b".to_string(), "online", "busy"),
5547 ("user_c".to_string(), "online", "free")
5548 ]
5549 );
5550 assert_eq!(
5551 contacts(&client_b, cx_b),
5552 [
5553 ("user_a".to_string(), "online", "busy"),
5554 ("user_c".to_string(), "online", "free"),
5555 ("user_d".to_string(), "online", "free"),
5556 ]
5557 );
5558 assert_eq!(
5559 contacts(&client_c, cx_c),
5560 [
5561 ("user_a".to_string(), "online", "busy"),
5562 ("user_b".to_string(), "online", "busy")
5563 ]
5564 );
5565 assert_eq!(
5566 contacts(&client_d, cx_d),
5567 [("user_b".to_string(), "online", "busy")]
5568 );
5569
5570 active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
5571 deterministic.run_until_parked();
5572 assert_eq!(
5573 contacts(&client_a, cx_a),
5574 [
5575 ("user_b".to_string(), "online", "free"),
5576 ("user_c".to_string(), "online", "free")
5577 ]
5578 );
5579 assert_eq!(
5580 contacts(&client_b, cx_b),
5581 [
5582 ("user_a".to_string(), "online", "free"),
5583 ("user_c".to_string(), "online", "free"),
5584 ("user_d".to_string(), "online", "free")
5585 ]
5586 );
5587 assert_eq!(
5588 contacts(&client_c, cx_c),
5589 [
5590 ("user_a".to_string(), "online", "free"),
5591 ("user_b".to_string(), "online", "free")
5592 ]
5593 );
5594 assert_eq!(
5595 contacts(&client_d, cx_d),
5596 [("user_b".to_string(), "online", "free")]
5597 );
5598
5599 active_call_c
5600 .update(cx_c, |call, cx| {
5601 call.invite(client_a.user_id().unwrap(), None, cx)
5602 })
5603 .await
5604 .unwrap();
5605 deterministic.run_until_parked();
5606 assert_eq!(
5607 contacts(&client_a, cx_a),
5608 [
5609 ("user_b".to_string(), "online", "free"),
5610 ("user_c".to_string(), "online", "busy")
5611 ]
5612 );
5613 assert_eq!(
5614 contacts(&client_b, cx_b),
5615 [
5616 ("user_a".to_string(), "online", "busy"),
5617 ("user_c".to_string(), "online", "busy"),
5618 ("user_d".to_string(), "online", "free")
5619 ]
5620 );
5621 assert_eq!(
5622 contacts(&client_c, cx_c),
5623 [
5624 ("user_a".to_string(), "online", "busy"),
5625 ("user_b".to_string(), "online", "free")
5626 ]
5627 );
5628 assert_eq!(
5629 contacts(&client_d, cx_d),
5630 [("user_b".to_string(), "online", "free")]
5631 );
5632
5633 active_call_a
5634 .update(cx_a, |call, cx| call.accept_incoming(cx))
5635 .await
5636 .unwrap();
5637 deterministic.run_until_parked();
5638 assert_eq!(
5639 contacts(&client_a, cx_a),
5640 [
5641 ("user_b".to_string(), "online", "free"),
5642 ("user_c".to_string(), "online", "busy")
5643 ]
5644 );
5645 assert_eq!(
5646 contacts(&client_b, cx_b),
5647 [
5648 ("user_a".to_string(), "online", "busy"),
5649 ("user_c".to_string(), "online", "busy"),
5650 ("user_d".to_string(), "online", "free")
5651 ]
5652 );
5653 assert_eq!(
5654 contacts(&client_c, cx_c),
5655 [
5656 ("user_a".to_string(), "online", "busy"),
5657 ("user_b".to_string(), "online", "free")
5658 ]
5659 );
5660 assert_eq!(
5661 contacts(&client_d, cx_d),
5662 [("user_b".to_string(), "online", "free")]
5663 );
5664
5665 active_call_a
5666 .update(cx_a, |call, cx| {
5667 call.invite(client_b.user_id().unwrap(), None, cx)
5668 })
5669 .await
5670 .unwrap();
5671 deterministic.run_until_parked();
5672 assert_eq!(
5673 contacts(&client_a, cx_a),
5674 [
5675 ("user_b".to_string(), "online", "busy"),
5676 ("user_c".to_string(), "online", "busy")
5677 ]
5678 );
5679 assert_eq!(
5680 contacts(&client_b, cx_b),
5681 [
5682 ("user_a".to_string(), "online", "busy"),
5683 ("user_c".to_string(), "online", "busy"),
5684 ("user_d".to_string(), "online", "free")
5685 ]
5686 );
5687 assert_eq!(
5688 contacts(&client_c, cx_c),
5689 [
5690 ("user_a".to_string(), "online", "busy"),
5691 ("user_b".to_string(), "online", "busy")
5692 ]
5693 );
5694 assert_eq!(
5695 contacts(&client_d, cx_d),
5696 [("user_b".to_string(), "online", "busy")]
5697 );
5698
5699 active_call_a
5700 .update(cx_a, |call, cx| call.hang_up(cx))
5701 .await
5702 .unwrap();
5703 deterministic.run_until_parked();
5704 assert_eq!(
5705 contacts(&client_a, cx_a),
5706 [
5707 ("user_b".to_string(), "online", "free"),
5708 ("user_c".to_string(), "online", "free")
5709 ]
5710 );
5711 assert_eq!(
5712 contacts(&client_b, cx_b),
5713 [
5714 ("user_a".to_string(), "online", "free"),
5715 ("user_c".to_string(), "online", "free"),
5716 ("user_d".to_string(), "online", "free")
5717 ]
5718 );
5719 assert_eq!(
5720 contacts(&client_c, cx_c),
5721 [
5722 ("user_a".to_string(), "online", "free"),
5723 ("user_b".to_string(), "online", "free")
5724 ]
5725 );
5726 assert_eq!(
5727 contacts(&client_d, cx_d),
5728 [("user_b".to_string(), "online", "free")]
5729 );
5730
5731 active_call_a
5732 .update(cx_a, |call, cx| {
5733 call.invite(client_b.user_id().unwrap(), None, cx)
5734 })
5735 .await
5736 .unwrap();
5737 deterministic.run_until_parked();
5738 assert_eq!(
5739 contacts(&client_a, cx_a),
5740 [
5741 ("user_b".to_string(), "online", "busy"),
5742 ("user_c".to_string(), "online", "free")
5743 ]
5744 );
5745 assert_eq!(
5746 contacts(&client_b, cx_b),
5747 [
5748 ("user_a".to_string(), "online", "busy"),
5749 ("user_c".to_string(), "online", "free"),
5750 ("user_d".to_string(), "online", "free")
5751 ]
5752 );
5753 assert_eq!(
5754 contacts(&client_c, cx_c),
5755 [
5756 ("user_a".to_string(), "online", "busy"),
5757 ("user_b".to_string(), "online", "busy")
5758 ]
5759 );
5760 assert_eq!(
5761 contacts(&client_d, cx_d),
5762 [("user_b".to_string(), "online", "busy")]
5763 );
5764
5765 server.forbid_connections();
5766 server.disconnect_client(client_a.peer_id().unwrap());
5767 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5768 assert_eq!(contacts(&client_a, cx_a), []);
5769 assert_eq!(
5770 contacts(&client_b, cx_b),
5771 [
5772 ("user_a".to_string(), "offline", "free"),
5773 ("user_c".to_string(), "online", "free"),
5774 ("user_d".to_string(), "online", "free")
5775 ]
5776 );
5777 assert_eq!(
5778 contacts(&client_c, cx_c),
5779 [
5780 ("user_a".to_string(), "offline", "free"),
5781 ("user_b".to_string(), "online", "free")
5782 ]
5783 );
5784 assert_eq!(
5785 contacts(&client_d, cx_d),
5786 [("user_b".to_string(), "online", "free")]
5787 );
5788
5789 // Test removing a contact
5790 client_b
5791 .user_store
5792 .update(cx_b, |store, cx| {
5793 store.remove_contact(client_c.user_id().unwrap(), cx)
5794 })
5795 .await
5796 .unwrap();
5797 deterministic.run_until_parked();
5798 assert_eq!(
5799 contacts(&client_b, cx_b),
5800 [
5801 ("user_a".to_string(), "offline", "free"),
5802 ("user_d".to_string(), "online", "free")
5803 ]
5804 );
5805 assert_eq!(
5806 contacts(&client_c, cx_c),
5807 [("user_a".to_string(), "offline", "free"),]
5808 );
5809
5810 fn contacts(
5811 client: &TestClient,
5812 cx: &TestAppContext,
5813 ) -> Vec<(String, &'static str, &'static str)> {
5814 client.user_store.read_with(cx, |store, _| {
5815 store
5816 .contacts()
5817 .iter()
5818 .map(|contact| {
5819 (
5820 contact.user.github_login.clone(),
5821 if contact.online { "online" } else { "offline" },
5822 if contact.busy { "busy" } else { "free" },
5823 )
5824 })
5825 .collect()
5826 })
5827 }
5828}
5829
5830#[gpui::test(iterations = 10)]
5831async fn test_contact_requests(
5832 deterministic: Arc<Deterministic>,
5833 cx_a: &mut TestAppContext,
5834 cx_a2: &mut TestAppContext,
5835 cx_b: &mut TestAppContext,
5836 cx_b2: &mut TestAppContext,
5837 cx_c: &mut TestAppContext,
5838 cx_c2: &mut TestAppContext,
5839) {
5840 deterministic.forbid_parking();
5841
5842 // Connect to a server as 3 clients.
5843 let mut server = TestServer::start(&deterministic).await;
5844 let client_a = server.create_client(cx_a, "user_a").await;
5845 let client_a2 = server.create_client(cx_a2, "user_a").await;
5846 let client_b = server.create_client(cx_b, "user_b").await;
5847 let client_b2 = server.create_client(cx_b2, "user_b").await;
5848 let client_c = server.create_client(cx_c, "user_c").await;
5849 let client_c2 = server.create_client(cx_c2, "user_c").await;
5850
5851 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5852 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5853 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5854
5855 // User A and User C request that user B become their contact.
5856 client_a
5857 .user_store
5858 .update(cx_a, |store, cx| {
5859 store.request_contact(client_b.user_id().unwrap(), cx)
5860 })
5861 .await
5862 .unwrap();
5863 client_c
5864 .user_store
5865 .update(cx_c, |store, cx| {
5866 store.request_contact(client_b.user_id().unwrap(), cx)
5867 })
5868 .await
5869 .unwrap();
5870 deterministic.run_until_parked();
5871
5872 // All users see the pending request appear in all their clients.
5873 assert_eq!(
5874 client_a.summarize_contacts(cx_a).outgoing_requests,
5875 &["user_b"]
5876 );
5877 assert_eq!(
5878 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5879 &["user_b"]
5880 );
5881 assert_eq!(
5882 client_b.summarize_contacts(cx_b).incoming_requests,
5883 &["user_a", "user_c"]
5884 );
5885 assert_eq!(
5886 client_b2.summarize_contacts(cx_b2).incoming_requests,
5887 &["user_a", "user_c"]
5888 );
5889 assert_eq!(
5890 client_c.summarize_contacts(cx_c).outgoing_requests,
5891 &["user_b"]
5892 );
5893 assert_eq!(
5894 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5895 &["user_b"]
5896 );
5897
5898 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5899 disconnect_and_reconnect(&client_a, cx_a).await;
5900 disconnect_and_reconnect(&client_b, cx_b).await;
5901 disconnect_and_reconnect(&client_c, cx_c).await;
5902 deterministic.run_until_parked();
5903 assert_eq!(
5904 client_a.summarize_contacts(cx_a).outgoing_requests,
5905 &["user_b"]
5906 );
5907 assert_eq!(
5908 client_b.summarize_contacts(cx_b).incoming_requests,
5909 &["user_a", "user_c"]
5910 );
5911 assert_eq!(
5912 client_c.summarize_contacts(cx_c).outgoing_requests,
5913 &["user_b"]
5914 );
5915
5916 // User B accepts the request from user A.
5917 client_b
5918 .user_store
5919 .update(cx_b, |store, cx| {
5920 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5921 })
5922 .await
5923 .unwrap();
5924
5925 deterministic.run_until_parked();
5926
5927 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5928 let contacts_b = client_b.summarize_contacts(cx_b);
5929 assert_eq!(contacts_b.current, &["user_a"]);
5930 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5931 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5932 assert_eq!(contacts_b2.current, &["user_a"]);
5933 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5934
5935 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5936 let contacts_a = client_a.summarize_contacts(cx_a);
5937 assert_eq!(contacts_a.current, &["user_b"]);
5938 assert!(contacts_a.outgoing_requests.is_empty());
5939 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5940 assert_eq!(contacts_a2.current, &["user_b"]);
5941 assert!(contacts_a2.outgoing_requests.is_empty());
5942
5943 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5944 disconnect_and_reconnect(&client_a, cx_a).await;
5945 disconnect_and_reconnect(&client_b, cx_b).await;
5946 disconnect_and_reconnect(&client_c, cx_c).await;
5947 deterministic.run_until_parked();
5948 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5949 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5950 assert_eq!(
5951 client_b.summarize_contacts(cx_b).incoming_requests,
5952 &["user_c"]
5953 );
5954 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5955 assert_eq!(
5956 client_c.summarize_contacts(cx_c).outgoing_requests,
5957 &["user_b"]
5958 );
5959
5960 // User B rejects the request from user C.
5961 client_b
5962 .user_store
5963 .update(cx_b, |store, cx| {
5964 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5965 })
5966 .await
5967 .unwrap();
5968
5969 deterministic.run_until_parked();
5970
5971 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5972 let contacts_b = client_b.summarize_contacts(cx_b);
5973 assert_eq!(contacts_b.current, &["user_a"]);
5974 assert!(contacts_b.incoming_requests.is_empty());
5975 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5976 assert_eq!(contacts_b2.current, &["user_a"]);
5977 assert!(contacts_b2.incoming_requests.is_empty());
5978
5979 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5980 let contacts_c = client_c.summarize_contacts(cx_c);
5981 assert!(contacts_c.current.is_empty());
5982 assert!(contacts_c.outgoing_requests.is_empty());
5983 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5984 assert!(contacts_c2.current.is_empty());
5985 assert!(contacts_c2.outgoing_requests.is_empty());
5986
5987 // Incoming/outgoing requests are not 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!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5993 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5994 assert!(client_b
5995 .summarize_contacts(cx_b)
5996 .incoming_requests
5997 .is_empty());
5998 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5999 assert!(client_c
6000 .summarize_contacts(cx_c)
6001 .outgoing_requests
6002 .is_empty());
6003
6004 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6005 client.disconnect(&cx.to_async());
6006 client.clear_contacts(cx).await;
6007 client
6008 .authenticate_and_connect(false, &cx.to_async())
6009 .await
6010 .unwrap();
6011 }
6012}
6013
6014#[gpui::test(iterations = 10)]
6015async fn test_basic_following(
6016 deterministic: Arc<Deterministic>,
6017 cx_a: &mut TestAppContext,
6018 cx_b: &mut TestAppContext,
6019 cx_c: &mut TestAppContext,
6020 cx_d: &mut TestAppContext,
6021) {
6022 deterministic.forbid_parking();
6023 cx_a.update(editor::init);
6024 cx_b.update(editor::init);
6025
6026 let mut server = TestServer::start(&deterministic).await;
6027 let client_a = server.create_client(cx_a, "user_a").await;
6028 let client_b = server.create_client(cx_b, "user_b").await;
6029 let client_c = server.create_client(cx_c, "user_c").await;
6030 let client_d = server.create_client(cx_d, "user_d").await;
6031 server
6032 .create_room(&mut [
6033 (&client_a, cx_a),
6034 (&client_b, cx_b),
6035 (&client_c, cx_c),
6036 (&client_d, cx_d),
6037 ])
6038 .await;
6039 let active_call_a = cx_a.read(ActiveCall::global);
6040 let active_call_b = cx_b.read(ActiveCall::global);
6041
6042 client_a
6043 .fs
6044 .insert_tree(
6045 "/a",
6046 json!({
6047 "1.txt": "one\none\none",
6048 "2.txt": "two\ntwo\ntwo",
6049 "3.txt": "three\nthree\nthree",
6050 }),
6051 )
6052 .await;
6053 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6054 active_call_a
6055 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6056 .await
6057 .unwrap();
6058
6059 let project_id = active_call_a
6060 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6061 .await
6062 .unwrap();
6063 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6064 active_call_b
6065 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6066 .await
6067 .unwrap();
6068
6069 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6070 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6071
6072 // Client A opens some editors.
6073 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6074 let editor_a1 = workspace_a
6075 .update(cx_a, |workspace, cx| {
6076 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6077 })
6078 .await
6079 .unwrap()
6080 .downcast::<Editor>()
6081 .unwrap();
6082 let editor_a2 = workspace_a
6083 .update(cx_a, |workspace, cx| {
6084 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6085 })
6086 .await
6087 .unwrap()
6088 .downcast::<Editor>()
6089 .unwrap();
6090
6091 // Client B opens an editor.
6092 let editor_b1 = workspace_b
6093 .update(cx_b, |workspace, cx| {
6094 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6095 })
6096 .await
6097 .unwrap()
6098 .downcast::<Editor>()
6099 .unwrap();
6100
6101 let peer_id_a = client_a.peer_id().unwrap();
6102 let peer_id_b = client_b.peer_id().unwrap();
6103 let peer_id_c = client_c.peer_id().unwrap();
6104 let peer_id_d = client_d.peer_id().unwrap();
6105
6106 // Client A updates their selections in those editors
6107 editor_a1.update(cx_a, |editor, cx| {
6108 editor.handle_input("a", cx);
6109 editor.handle_input("b", cx);
6110 editor.handle_input("c", cx);
6111 editor.select_left(&Default::default(), cx);
6112 assert_eq!(editor.selections.ranges(cx), vec![3..2]);
6113 });
6114 editor_a2.update(cx_a, |editor, cx| {
6115 editor.handle_input("d", cx);
6116 editor.handle_input("e", cx);
6117 editor.select_left(&Default::default(), cx);
6118 assert_eq!(editor.selections.ranges(cx), vec![2..1]);
6119 });
6120
6121 // When client B starts following client A, all visible view states are replicated to client B.
6122 workspace_b
6123 .update(cx_b, |workspace, cx| {
6124 workspace.toggle_follow(peer_id_a, cx).unwrap()
6125 })
6126 .await
6127 .unwrap();
6128
6129 cx_c.foreground().run_until_parked();
6130 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
6131 workspace
6132 .active_item(cx)
6133 .unwrap()
6134 .downcast::<Editor>()
6135 .unwrap()
6136 });
6137 assert_eq!(
6138 cx_b.read(|cx| editor_b2.project_path(cx)),
6139 Some((worktree_id, "2.txt").into())
6140 );
6141 assert_eq!(
6142 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
6143 vec![2..1]
6144 );
6145 assert_eq!(
6146 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
6147 vec![3..2]
6148 );
6149
6150 cx_c.foreground().run_until_parked();
6151 let active_call_c = cx_c.read(ActiveCall::global);
6152 let project_c = client_c.build_remote_project(project_id, cx_c).await;
6153 let workspace_c = client_c.build_workspace(&project_c, cx_c);
6154 active_call_c
6155 .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
6156 .await
6157 .unwrap();
6158 drop(project_c);
6159
6160 // Client C also follows client A.
6161 workspace_c
6162 .update(cx_c, |workspace, cx| {
6163 workspace.toggle_follow(peer_id_a, cx).unwrap()
6164 })
6165 .await
6166 .unwrap();
6167
6168 cx_d.foreground().run_until_parked();
6169 let active_call_d = cx_d.read(ActiveCall::global);
6170 let project_d = client_d.build_remote_project(project_id, cx_d).await;
6171 let workspace_d = client_d.build_workspace(&project_d, cx_d);
6172 active_call_d
6173 .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
6174 .await
6175 .unwrap();
6176 drop(project_d);
6177
6178 // All clients see that clients B and C are following client A.
6179 cx_c.foreground().run_until_parked();
6180 for (name, active_call, cx) in [
6181 ("A", &active_call_a, &cx_a),
6182 ("B", &active_call_b, &cx_b),
6183 ("C", &active_call_c, &cx_c),
6184 ("D", &active_call_d, &cx_d),
6185 ] {
6186 active_call.read_with(*cx, |call, cx| {
6187 let room = call.room().unwrap().read(cx);
6188 assert_eq!(
6189 room.followers_for(peer_id_a, project_id),
6190 &[peer_id_b, peer_id_c],
6191 "checking followers for A as {name}"
6192 );
6193 });
6194 }
6195
6196 // Client C unfollows client A.
6197 workspace_c.update(cx_c, |workspace, cx| {
6198 workspace.toggle_follow(peer_id_a, cx);
6199 });
6200
6201 // All clients see that clients B is following client A.
6202 cx_c.foreground().run_until_parked();
6203 for (name, active_call, cx) in [
6204 ("A", &active_call_a, &cx_a),
6205 ("B", &active_call_b, &cx_b),
6206 ("C", &active_call_c, &cx_c),
6207 ("D", &active_call_d, &cx_d),
6208 ] {
6209 active_call.read_with(*cx, |call, cx| {
6210 let room = call.room().unwrap().read(cx);
6211 assert_eq!(
6212 room.followers_for(peer_id_a, project_id),
6213 &[peer_id_b],
6214 "checking followers for A as {name}"
6215 );
6216 });
6217 }
6218
6219 // Client C re-follows client A.
6220 workspace_c.update(cx_c, |workspace, cx| {
6221 workspace.toggle_follow(peer_id_a, cx);
6222 });
6223
6224 // All clients see that clients B and C are following client A.
6225 cx_c.foreground().run_until_parked();
6226 for (name, active_call, cx) in [
6227 ("A", &active_call_a, &cx_a),
6228 ("B", &active_call_b, &cx_b),
6229 ("C", &active_call_c, &cx_c),
6230 ("D", &active_call_d, &cx_d),
6231 ] {
6232 active_call.read_with(*cx, |call, cx| {
6233 let room = call.room().unwrap().read(cx);
6234 assert_eq!(
6235 room.followers_for(peer_id_a, project_id),
6236 &[peer_id_b, peer_id_c],
6237 "checking followers for A as {name}"
6238 );
6239 });
6240 }
6241
6242 // Client D follows client C.
6243 workspace_d
6244 .update(cx_d, |workspace, cx| {
6245 workspace.toggle_follow(peer_id_c, cx).unwrap()
6246 })
6247 .await
6248 .unwrap();
6249
6250 // All clients see that D is following C
6251 cx_d.foreground().run_until_parked();
6252 for (name, active_call, cx) in [
6253 ("A", &active_call_a, &cx_a),
6254 ("B", &active_call_b, &cx_b),
6255 ("C", &active_call_c, &cx_c),
6256 ("D", &active_call_d, &cx_d),
6257 ] {
6258 active_call.read_with(*cx, |call, cx| {
6259 let room = call.room().unwrap().read(cx);
6260 assert_eq!(
6261 room.followers_for(peer_id_c, project_id),
6262 &[peer_id_d],
6263 "checking followers for C as {name}"
6264 );
6265 });
6266 }
6267
6268 // Client C closes the project.
6269 cx_c.drop_last(workspace_c);
6270
6271 // Clients A and B see that client B is following A, and client C is not present in the followers.
6272 cx_c.foreground().run_until_parked();
6273 for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
6274 active_call.read_with(*cx, |call, cx| {
6275 let room = call.room().unwrap().read(cx);
6276 assert_eq!(
6277 room.followers_for(peer_id_a, project_id),
6278 &[peer_id_b],
6279 "checking followers for A as {name}"
6280 );
6281 });
6282 }
6283
6284 // All clients see that no-one is following C
6285 for (name, active_call, cx) in [
6286 ("A", &active_call_a, &cx_a),
6287 ("B", &active_call_b, &cx_b),
6288 ("C", &active_call_c, &cx_c),
6289 ("D", &active_call_d, &cx_d),
6290 ] {
6291 active_call.read_with(*cx, |call, cx| {
6292 let room = call.room().unwrap().read(cx);
6293 assert_eq!(
6294 room.followers_for(peer_id_c, project_id),
6295 &[],
6296 "checking followers for C as {name}"
6297 );
6298 });
6299 }
6300
6301 // When client A activates a different editor, client B does so as well.
6302 workspace_a.update(cx_a, |workspace, cx| {
6303 workspace.activate_item(&editor_a1, cx)
6304 });
6305 deterministic.run_until_parked();
6306 workspace_b.read_with(cx_b, |workspace, cx| {
6307 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6308 });
6309
6310 // When client A opens a multibuffer, client B does so as well.
6311 let multibuffer_a = cx_a.add_model(|cx| {
6312 let buffer_a1 = project_a.update(cx, |project, cx| {
6313 project
6314 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
6315 .unwrap()
6316 });
6317 let buffer_a2 = project_a.update(cx, |project, cx| {
6318 project
6319 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
6320 .unwrap()
6321 });
6322 let mut result = MultiBuffer::new(0);
6323 result.push_excerpts(
6324 buffer_a1,
6325 [ExcerptRange {
6326 context: 0..3,
6327 primary: None,
6328 }],
6329 cx,
6330 );
6331 result.push_excerpts(
6332 buffer_a2,
6333 [ExcerptRange {
6334 context: 4..7,
6335 primary: None,
6336 }],
6337 cx,
6338 );
6339 result
6340 });
6341 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
6342 let editor =
6343 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
6344 workspace.add_item(Box::new(editor.clone()), cx);
6345 editor
6346 });
6347 deterministic.run_until_parked();
6348 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
6349 workspace
6350 .active_item(cx)
6351 .unwrap()
6352 .downcast::<Editor>()
6353 .unwrap()
6354 });
6355 assert_eq!(
6356 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
6357 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
6358 );
6359
6360 // When client A navigates back and forth, client B does so as well.
6361 workspace_a
6362 .update(cx_a, |workspace, cx| {
6363 workspace::Pane::go_back(workspace, None, cx)
6364 })
6365 .await
6366 .unwrap();
6367 deterministic.run_until_parked();
6368 workspace_b.read_with(cx_b, |workspace, cx| {
6369 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6370 });
6371
6372 workspace_a
6373 .update(cx_a, |workspace, cx| {
6374 workspace::Pane::go_back(workspace, None, cx)
6375 })
6376 .await
6377 .unwrap();
6378 deterministic.run_until_parked();
6379 workspace_b.read_with(cx_b, |workspace, cx| {
6380 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
6381 });
6382
6383 workspace_a
6384 .update(cx_a, |workspace, cx| {
6385 workspace::Pane::go_forward(workspace, None, cx)
6386 })
6387 .await
6388 .unwrap();
6389 deterministic.run_until_parked();
6390 workspace_b.read_with(cx_b, |workspace, cx| {
6391 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
6392 });
6393
6394 // Changes to client A's editor are reflected on client B.
6395 editor_a1.update(cx_a, |editor, cx| {
6396 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
6397 });
6398 deterministic.run_until_parked();
6399 editor_b1.read_with(cx_b, |editor, cx| {
6400 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
6401 });
6402
6403 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
6404 deterministic.run_until_parked();
6405 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
6406
6407 editor_a1.update(cx_a, |editor, cx| {
6408 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
6409 editor.set_scroll_position(vec2f(0., 100.), cx);
6410 });
6411 deterministic.run_until_parked();
6412 editor_b1.read_with(cx_b, |editor, cx| {
6413 assert_eq!(editor.selections.ranges(cx), &[3..3]);
6414 });
6415
6416 // After unfollowing, client B stops receiving updates from client A.
6417 workspace_b.update(cx_b, |workspace, cx| {
6418 workspace.unfollow(&workspace.active_pane().clone(), cx)
6419 });
6420 workspace_a.update(cx_a, |workspace, cx| {
6421 workspace.activate_item(&editor_a2, cx)
6422 });
6423 deterministic.run_until_parked();
6424 assert_eq!(
6425 workspace_b.read_with(cx_b, |workspace, cx| workspace
6426 .active_item(cx)
6427 .unwrap()
6428 .id()),
6429 editor_b1.id()
6430 );
6431
6432 // Client A starts following client B.
6433 workspace_a
6434 .update(cx_a, |workspace, cx| {
6435 workspace.toggle_follow(peer_id_b, cx).unwrap()
6436 })
6437 .await
6438 .unwrap();
6439 assert_eq!(
6440 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6441 Some(peer_id_b)
6442 );
6443 assert_eq!(
6444 workspace_a.read_with(cx_a, |workspace, cx| workspace
6445 .active_item(cx)
6446 .unwrap()
6447 .id()),
6448 editor_a1.id()
6449 );
6450
6451 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
6452 let display = MacOSDisplay::new();
6453 active_call_b
6454 .update(cx_b, |call, cx| call.set_location(None, cx))
6455 .await
6456 .unwrap();
6457 active_call_b
6458 .update(cx_b, |call, cx| {
6459 call.room().unwrap().update(cx, |room, cx| {
6460 room.set_display_sources(vec![display.clone()]);
6461 room.share_screen(cx)
6462 })
6463 })
6464 .await
6465 .unwrap();
6466 deterministic.run_until_parked();
6467 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
6468 workspace
6469 .active_item(cx)
6470 .unwrap()
6471 .downcast::<SharedScreen>()
6472 .unwrap()
6473 });
6474
6475 // Client B activates Zed again, which causes the previous editor to become focused again.
6476 active_call_b
6477 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6478 .await
6479 .unwrap();
6480 deterministic.run_until_parked();
6481 workspace_a.read_with(cx_a, |workspace, cx| {
6482 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
6483 });
6484
6485 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
6486 workspace_b.update(cx_b, |workspace, cx| {
6487 workspace.activate_item(&multibuffer_editor_b, cx)
6488 });
6489 deterministic.run_until_parked();
6490 workspace_a.read_with(cx_a, |workspace, cx| {
6491 assert_eq!(
6492 workspace.active_item(cx).unwrap().id(),
6493 multibuffer_editor_a.id()
6494 )
6495 });
6496
6497 // Client B activates an external window again, and the previously-opened screen-sharing item
6498 // gets activated.
6499 active_call_b
6500 .update(cx_b, |call, cx| call.set_location(None, cx))
6501 .await
6502 .unwrap();
6503 deterministic.run_until_parked();
6504 assert_eq!(
6505 workspace_a.read_with(cx_a, |workspace, cx| workspace
6506 .active_item(cx)
6507 .unwrap()
6508 .id()),
6509 shared_screen.id()
6510 );
6511
6512 // Following interrupts when client B disconnects.
6513 client_b.disconnect(&cx_b.to_async());
6514 deterministic.advance_clock(RECONNECT_TIMEOUT);
6515 assert_eq!(
6516 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
6517 None
6518 );
6519}
6520
6521#[gpui::test(iterations = 10)]
6522async fn test_join_call_after_screen_was_shared(
6523 deterministic: Arc<Deterministic>,
6524 cx_a: &mut TestAppContext,
6525 cx_b: &mut TestAppContext,
6526) {
6527 deterministic.forbid_parking();
6528 let mut server = TestServer::start(&deterministic).await;
6529
6530 let client_a = server.create_client(cx_a, "user_a").await;
6531 let client_b = server.create_client(cx_b, "user_b").await;
6532 server
6533 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6534 .await;
6535
6536 let active_call_a = cx_a.read(ActiveCall::global);
6537 let active_call_b = cx_b.read(ActiveCall::global);
6538
6539 // Call users B and C from client A.
6540 active_call_a
6541 .update(cx_a, |call, cx| {
6542 call.invite(client_b.user_id().unwrap(), None, cx)
6543 })
6544 .await
6545 .unwrap();
6546 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6547 deterministic.run_until_parked();
6548 assert_eq!(
6549 room_participants(&room_a, cx_a),
6550 RoomParticipants {
6551 remote: Default::default(),
6552 pending: vec!["user_b".to_string()]
6553 }
6554 );
6555
6556 // User B receives the call.
6557 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6558 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6559 assert_eq!(call_b.calling_user.github_login, "user_a");
6560
6561 // User A shares their screen
6562 let display = MacOSDisplay::new();
6563 active_call_a
6564 .update(cx_a, |call, cx| {
6565 call.room().unwrap().update(cx, |room, cx| {
6566 room.set_display_sources(vec![display.clone()]);
6567 room.share_screen(cx)
6568 })
6569 })
6570 .await
6571 .unwrap();
6572
6573 client_b.user_store.update(cx_b, |user_store, _| {
6574 user_store.clear_cache();
6575 });
6576
6577 // User B joins the room
6578 active_call_b
6579 .update(cx_b, |call, cx| call.accept_incoming(cx))
6580 .await
6581 .unwrap();
6582 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6583 assert!(incoming_call_b.next().await.unwrap().is_none());
6584
6585 deterministic.run_until_parked();
6586 assert_eq!(
6587 room_participants(&room_a, cx_a),
6588 RoomParticipants {
6589 remote: vec!["user_b".to_string()],
6590 pending: vec![],
6591 }
6592 );
6593 assert_eq!(
6594 room_participants(&room_b, cx_b),
6595 RoomParticipants {
6596 remote: vec!["user_a".to_string()],
6597 pending: vec![],
6598 }
6599 );
6600
6601 // Ensure User B sees User A's screenshare.
6602 room_b.read_with(cx_b, |room, _| {
6603 assert_eq!(
6604 room.remote_participants()
6605 .get(&client_a.user_id().unwrap())
6606 .unwrap()
6607 .tracks
6608 .len(),
6609 1
6610 );
6611 });
6612}
6613
6614#[gpui::test]
6615async fn test_following_tab_order(
6616 deterministic: Arc<Deterministic>,
6617 cx_a: &mut TestAppContext,
6618 cx_b: &mut TestAppContext,
6619) {
6620 cx_a.update(editor::init);
6621 cx_b.update(editor::init);
6622
6623 let mut server = TestServer::start(&deterministic).await;
6624 let client_a = server.create_client(cx_a, "user_a").await;
6625 let client_b = server.create_client(cx_b, "user_b").await;
6626 server
6627 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6628 .await;
6629 let active_call_a = cx_a.read(ActiveCall::global);
6630 let active_call_b = cx_b.read(ActiveCall::global);
6631
6632 client_a
6633 .fs
6634 .insert_tree(
6635 "/a",
6636 json!({
6637 "1.txt": "one",
6638 "2.txt": "two",
6639 "3.txt": "three",
6640 }),
6641 )
6642 .await;
6643 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6644 active_call_a
6645 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6646 .await
6647 .unwrap();
6648
6649 let project_id = active_call_a
6650 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6651 .await
6652 .unwrap();
6653 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6654 active_call_b
6655 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6656 .await
6657 .unwrap();
6658
6659 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6660 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6661
6662 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6663 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6664
6665 let client_b_id = project_a.read_with(cx_a, |project, _| {
6666 project.collaborators().values().next().unwrap().peer_id
6667 });
6668
6669 //Open 1, 3 in that order on client A
6670 workspace_a
6671 .update(cx_a, |workspace, cx| {
6672 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6673 })
6674 .await
6675 .unwrap();
6676 workspace_a
6677 .update(cx_a, |workspace, cx| {
6678 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6679 })
6680 .await
6681 .unwrap();
6682
6683 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
6684 pane.update(cx, |pane, cx| {
6685 pane.items()
6686 .map(|item| {
6687 item.project_path(cx)
6688 .unwrap()
6689 .path
6690 .to_str()
6691 .unwrap()
6692 .to_owned()
6693 })
6694 .collect::<Vec<_>>()
6695 })
6696 };
6697
6698 //Verify that the tabs opened in the order we expect
6699 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
6700
6701 //Follow client B as client A
6702 workspace_a
6703 .update(cx_a, |workspace, cx| {
6704 workspace.toggle_follow(client_b_id, cx).unwrap()
6705 })
6706 .await
6707 .unwrap();
6708
6709 //Open just 2 on client B
6710 workspace_b
6711 .update(cx_b, |workspace, cx| {
6712 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6713 })
6714 .await
6715 .unwrap();
6716 deterministic.run_until_parked();
6717
6718 // Verify that newly opened followed file is at the end
6719 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6720
6721 //Open just 1 on client B
6722 workspace_b
6723 .update(cx_b, |workspace, cx| {
6724 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6725 })
6726 .await
6727 .unwrap();
6728 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
6729 deterministic.run_until_parked();
6730
6731 // Verify that following into 1 did not reorder
6732 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
6733}
6734
6735#[gpui::test(iterations = 10)]
6736async fn test_peers_following_each_other(
6737 deterministic: Arc<Deterministic>,
6738 cx_a: &mut TestAppContext,
6739 cx_b: &mut TestAppContext,
6740) {
6741 deterministic.forbid_parking();
6742 cx_a.update(editor::init);
6743 cx_b.update(editor::init);
6744
6745 let mut server = TestServer::start(&deterministic).await;
6746 let client_a = server.create_client(cx_a, "user_a").await;
6747 let client_b = server.create_client(cx_b, "user_b").await;
6748 server
6749 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6750 .await;
6751 let active_call_a = cx_a.read(ActiveCall::global);
6752 let active_call_b = cx_b.read(ActiveCall::global);
6753
6754 // Client A shares a project.
6755 client_a
6756 .fs
6757 .insert_tree(
6758 "/a",
6759 json!({
6760 "1.txt": "one",
6761 "2.txt": "two",
6762 "3.txt": "three",
6763 "4.txt": "four",
6764 }),
6765 )
6766 .await;
6767 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6768 active_call_a
6769 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6770 .await
6771 .unwrap();
6772 let project_id = active_call_a
6773 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6774 .await
6775 .unwrap();
6776
6777 // Client B joins the project.
6778 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6779 active_call_b
6780 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6781 .await
6782 .unwrap();
6783
6784 // Client A opens some editors.
6785 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6786 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6787 let _editor_a1 = workspace_a
6788 .update(cx_a, |workspace, cx| {
6789 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6790 })
6791 .await
6792 .unwrap()
6793 .downcast::<Editor>()
6794 .unwrap();
6795
6796 // Client B opens an editor.
6797 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6798 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6799 let _editor_b1 = 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 .downcast::<Editor>()
6806 .unwrap();
6807
6808 // Clients A and B follow each other in split panes
6809 workspace_a.update(cx_a, |workspace, cx| {
6810 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6811 let pane_a1 = pane_a1.clone();
6812 cx.defer(move |workspace, _| {
6813 assert_ne!(*workspace.active_pane(), pane_a1);
6814 });
6815 });
6816 workspace_a
6817 .update(cx_a, |workspace, cx| {
6818 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
6819 workspace.toggle_follow(leader_id, cx).unwrap()
6820 })
6821 .await
6822 .unwrap();
6823 workspace_b.update(cx_b, |workspace, cx| {
6824 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6825 let pane_b1 = pane_b1.clone();
6826 cx.defer(move |workspace, _| {
6827 assert_ne!(*workspace.active_pane(), pane_b1);
6828 });
6829 });
6830 workspace_b
6831 .update(cx_b, |workspace, cx| {
6832 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
6833 workspace.toggle_follow(leader_id, cx).unwrap()
6834 })
6835 .await
6836 .unwrap();
6837
6838 workspace_a.update(cx_a, |workspace, cx| {
6839 workspace.activate_next_pane(cx);
6840 });
6841 // Wait for focus effects to be fully flushed
6842 workspace_a.update(cx_a, |workspace, _| {
6843 assert_eq!(*workspace.active_pane(), pane_a1);
6844 });
6845
6846 workspace_a
6847 .update(cx_a, |workspace, cx| {
6848 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6849 })
6850 .await
6851 .unwrap();
6852 workspace_b.update(cx_b, |workspace, cx| {
6853 workspace.activate_next_pane(cx);
6854 });
6855
6856 workspace_b
6857 .update(cx_b, |workspace, cx| {
6858 assert_eq!(*workspace.active_pane(), pane_b1);
6859 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
6860 })
6861 .await
6862 .unwrap();
6863 cx_a.foreground().run_until_parked();
6864
6865 // Ensure leader updates don't change the active pane of followers
6866 workspace_a.read_with(cx_a, |workspace, _| {
6867 assert_eq!(*workspace.active_pane(), pane_a1);
6868 });
6869 workspace_b.read_with(cx_b, |workspace, _| {
6870 assert_eq!(*workspace.active_pane(), pane_b1);
6871 });
6872
6873 // Ensure peers following each other doesn't cause an infinite loop.
6874 assert_eq!(
6875 workspace_a.read_with(cx_a, |workspace, cx| workspace
6876 .active_item(cx)
6877 .unwrap()
6878 .project_path(cx)),
6879 Some((worktree_id, "3.txt").into())
6880 );
6881 workspace_a.update(cx_a, |workspace, cx| {
6882 assert_eq!(
6883 workspace.active_item(cx).unwrap().project_path(cx),
6884 Some((worktree_id, "3.txt").into())
6885 );
6886 workspace.activate_next_pane(cx);
6887 });
6888
6889 workspace_a.update(cx_a, |workspace, cx| {
6890 assert_eq!(
6891 workspace.active_item(cx).unwrap().project_path(cx),
6892 Some((worktree_id, "4.txt").into())
6893 );
6894 });
6895
6896 workspace_b.update(cx_b, |workspace, cx| {
6897 assert_eq!(
6898 workspace.active_item(cx).unwrap().project_path(cx),
6899 Some((worktree_id, "4.txt").into())
6900 );
6901 workspace.activate_next_pane(cx);
6902 });
6903
6904 workspace_b.update(cx_b, |workspace, cx| {
6905 assert_eq!(
6906 workspace.active_item(cx).unwrap().project_path(cx),
6907 Some((worktree_id, "3.txt").into())
6908 );
6909 });
6910}
6911
6912#[gpui::test(iterations = 10)]
6913async fn test_auto_unfollowing(
6914 deterministic: Arc<Deterministic>,
6915 cx_a: &mut TestAppContext,
6916 cx_b: &mut TestAppContext,
6917) {
6918 deterministic.forbid_parking();
6919 cx_a.update(editor::init);
6920 cx_b.update(editor::init);
6921
6922 // 2 clients connect to a server.
6923 let mut server = TestServer::start(&deterministic).await;
6924 let client_a = server.create_client(cx_a, "user_a").await;
6925 let client_b = server.create_client(cx_b, "user_b").await;
6926 server
6927 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6928 .await;
6929 let active_call_a = cx_a.read(ActiveCall::global);
6930 let active_call_b = cx_b.read(ActiveCall::global);
6931
6932 // Client A shares a project.
6933 client_a
6934 .fs
6935 .insert_tree(
6936 "/a",
6937 json!({
6938 "1.txt": "one",
6939 "2.txt": "two",
6940 "3.txt": "three",
6941 }),
6942 )
6943 .await;
6944 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6945 active_call_a
6946 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6947 .await
6948 .unwrap();
6949
6950 let project_id = active_call_a
6951 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6952 .await
6953 .unwrap();
6954 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6955 active_call_b
6956 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6957 .await
6958 .unwrap();
6959
6960 // Client A opens some editors.
6961 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6962 let _editor_a1 = workspace_a
6963 .update(cx_a, |workspace, cx| {
6964 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6965 })
6966 .await
6967 .unwrap()
6968 .downcast::<Editor>()
6969 .unwrap();
6970
6971 // Client B starts following client A.
6972 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6973 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6974 let leader_id = project_b.read_with(cx_b, |project, _| {
6975 project.collaborators().values().next().unwrap().peer_id
6976 });
6977 workspace_b
6978 .update(cx_b, |workspace, cx| {
6979 workspace.toggle_follow(leader_id, cx).unwrap()
6980 })
6981 .await
6982 .unwrap();
6983 assert_eq!(
6984 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6985 Some(leader_id)
6986 );
6987 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
6988 workspace
6989 .active_item(cx)
6990 .unwrap()
6991 .downcast::<Editor>()
6992 .unwrap()
6993 });
6994
6995 // When client B moves, it automatically stops following client A.
6996 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
6997 assert_eq!(
6998 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6999 None
7000 );
7001
7002 workspace_b
7003 .update(cx_b, |workspace, cx| {
7004 workspace.toggle_follow(leader_id, cx).unwrap()
7005 })
7006 .await
7007 .unwrap();
7008 assert_eq!(
7009 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7010 Some(leader_id)
7011 );
7012
7013 // When client B edits, it automatically stops following client A.
7014 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
7015 assert_eq!(
7016 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7017 None
7018 );
7019
7020 workspace_b
7021 .update(cx_b, |workspace, cx| {
7022 workspace.toggle_follow(leader_id, cx).unwrap()
7023 })
7024 .await
7025 .unwrap();
7026 assert_eq!(
7027 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7028 Some(leader_id)
7029 );
7030
7031 // When client B scrolls, it automatically stops following client A.
7032 editor_b2.update(cx_b, |editor, cx| {
7033 editor.set_scroll_position(vec2f(0., 3.), cx)
7034 });
7035 assert_eq!(
7036 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7037 None
7038 );
7039
7040 workspace_b
7041 .update(cx_b, |workspace, cx| {
7042 workspace.toggle_follow(leader_id, cx).unwrap()
7043 })
7044 .await
7045 .unwrap();
7046 assert_eq!(
7047 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7048 Some(leader_id)
7049 );
7050
7051 // When client B activates a different pane, it continues following client A in the original pane.
7052 workspace_b.update(cx_b, |workspace, cx| {
7053 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
7054 });
7055 assert_eq!(
7056 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7057 Some(leader_id)
7058 );
7059
7060 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
7061 assert_eq!(
7062 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7063 Some(leader_id)
7064 );
7065
7066 // When client B activates a different item in the original pane, it automatically stops following client A.
7067 workspace_b
7068 .update(cx_b, |workspace, cx| {
7069 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
7070 })
7071 .await
7072 .unwrap();
7073 assert_eq!(
7074 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
7075 None
7076 );
7077}
7078
7079#[gpui::test(iterations = 10)]
7080async fn test_peers_simultaneously_following_each_other(
7081 deterministic: Arc<Deterministic>,
7082 cx_a: &mut TestAppContext,
7083 cx_b: &mut TestAppContext,
7084) {
7085 deterministic.forbid_parking();
7086 cx_a.update(editor::init);
7087 cx_b.update(editor::init);
7088
7089 let mut server = TestServer::start(&deterministic).await;
7090 let client_a = server.create_client(cx_a, "user_a").await;
7091 let client_b = server.create_client(cx_b, "user_b").await;
7092 server
7093 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
7094 .await;
7095 let active_call_a = cx_a.read(ActiveCall::global);
7096
7097 client_a.fs.insert_tree("/a", json!({})).await;
7098 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
7099 let workspace_a = client_a.build_workspace(&project_a, cx_a);
7100 let project_id = active_call_a
7101 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
7102 .await
7103 .unwrap();
7104
7105 let project_b = client_b.build_remote_project(project_id, cx_b).await;
7106 let workspace_b = client_b.build_workspace(&project_b, cx_b);
7107
7108 deterministic.run_until_parked();
7109 let client_a_id = project_b.read_with(cx_b, |project, _| {
7110 project.collaborators().values().next().unwrap().peer_id
7111 });
7112 let client_b_id = project_a.read_with(cx_a, |project, _| {
7113 project.collaborators().values().next().unwrap().peer_id
7114 });
7115
7116 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
7117 workspace.toggle_follow(client_b_id, cx).unwrap()
7118 });
7119 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
7120 workspace.toggle_follow(client_a_id, cx).unwrap()
7121 });
7122
7123 futures::try_join!(a_follow_b, b_follow_a).unwrap();
7124 workspace_a.read_with(cx_a, |workspace, _| {
7125 assert_eq!(
7126 workspace.leader_for_pane(workspace.active_pane()),
7127 Some(client_b_id)
7128 );
7129 });
7130 workspace_b.read_with(cx_b, |workspace, _| {
7131 assert_eq!(
7132 workspace.leader_for_pane(workspace.active_pane()),
7133 Some(client_a_id)
7134 );
7135 });
7136}
7137
7138#[derive(Debug, Eq, PartialEq)]
7139struct RoomParticipants {
7140 remote: Vec<String>,
7141 pending: Vec<String>,
7142}
7143
7144fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
7145 room.read_with(cx, |room, _| {
7146 let mut remote = room
7147 .remote_participants()
7148 .iter()
7149 .map(|(_, participant)| participant.user.github_login.clone())
7150 .collect::<Vec<_>>();
7151 let mut pending = room
7152 .pending_participants()
7153 .iter()
7154 .map(|user| user.github_login.clone())
7155 .collect::<Vec<_>>();
7156 remote.sort();
7157 pending.sort();
7158 RoomParticipants { remote, pending }
7159 })
7160}