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