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