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.has_update_observer()));
1382
1383 project_b
1384 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1385 .await
1386 .unwrap();
1387
1388 // When client B leaves the room, the project becomes read-only.
1389 active_call_b
1390 .update(cx_b, |call, cx| call.hang_up(cx))
1391 .await
1392 .unwrap();
1393 executor.run_until_parked();
1394
1395 assert!(project_b.read_with(cx_b, |project, _| project.is_disconnected()));
1396
1397 // Client C opens the project.
1398 let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
1399
1400 // When client A unshares the project, client C's project becomes read-only.
1401 project_a
1402 .update(cx_a, |project, cx| project.unshare(cx))
1403 .unwrap();
1404 executor.run_until_parked();
1405
1406 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
1407
1408 assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected()));
1409
1410 // Client C can open the project again after client A re-shares.
1411 let project_id = active_call_a
1412 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1413 .await
1414 .unwrap();
1415 let project_c2 = client_c.build_dev_server_project(project_id, cx_c).await;
1416 executor.run_until_parked();
1417
1418 assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
1419 project_c2
1420 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1421 .await
1422 .unwrap();
1423
1424 // When client A (the host) leaves the room, the project gets unshared and guests are notified.
1425 active_call_a
1426 .update(cx_a, |call, cx| call.hang_up(cx))
1427 .await
1428 .unwrap();
1429 executor.run_until_parked();
1430
1431 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
1432
1433 project_c2.read_with(cx_c, |project, _| {
1434 assert!(project.is_disconnected());
1435 assert!(project.collaborators().is_empty());
1436 });
1437}
1438
1439#[gpui::test(iterations = 10)]
1440async fn test_project_reconnect(
1441 executor: BackgroundExecutor,
1442 cx_a: &mut TestAppContext,
1443 cx_b: &mut TestAppContext,
1444) {
1445 let mut server = TestServer::start(executor.clone()).await;
1446 let client_a = server.create_client(cx_a, "user_a").await;
1447 let client_b = server.create_client(cx_b, "user_b").await;
1448 server
1449 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1450 .await;
1451
1452 cx_b.update(editor::init);
1453
1454 client_a
1455 .fs()
1456 .insert_tree(
1457 "/root-1",
1458 json!({
1459 "dir1": {
1460 "a.txt": "a",
1461 "b.txt": "b",
1462 "subdir1": {
1463 "c.txt": "c",
1464 "d.txt": "d",
1465 "e.txt": "e",
1466 }
1467 },
1468 "dir2": {
1469 "v.txt": "v",
1470 },
1471 "dir3": {
1472 "w.txt": "w",
1473 "x.txt": "x",
1474 "y.txt": "y",
1475 },
1476 "dir4": {
1477 "z.txt": "z",
1478 },
1479 }),
1480 )
1481 .await;
1482 client_a
1483 .fs()
1484 .insert_tree(
1485 "/root-2",
1486 json!({
1487 "2.txt": "2",
1488 }),
1489 )
1490 .await;
1491 client_a
1492 .fs()
1493 .insert_tree(
1494 "/root-3",
1495 json!({
1496 "3.txt": "3",
1497 }),
1498 )
1499 .await;
1500
1501 let active_call_a = cx_a.read(ActiveCall::global);
1502 let (project_a1, _) = client_a.build_local_project("/root-1/dir1", cx_a).await;
1503 let (project_a2, _) = client_a.build_local_project("/root-2", cx_a).await;
1504 let (project_a3, _) = client_a.build_local_project("/root-3", cx_a).await;
1505 let worktree_a1 = project_a1.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
1506 let project1_id = active_call_a
1507 .update(cx_a, |call, cx| call.share_project(project_a1.clone(), cx))
1508 .await
1509 .unwrap();
1510 let project2_id = active_call_a
1511 .update(cx_a, |call, cx| call.share_project(project_a2.clone(), cx))
1512 .await
1513 .unwrap();
1514 let project3_id = active_call_a
1515 .update(cx_a, |call, cx| call.share_project(project_a3.clone(), cx))
1516 .await
1517 .unwrap();
1518
1519 let project_b1 = client_b.build_dev_server_project(project1_id, cx_b).await;
1520 let project_b2 = client_b.build_dev_server_project(project2_id, cx_b).await;
1521 let project_b3 = client_b.build_dev_server_project(project3_id, cx_b).await;
1522 executor.run_until_parked();
1523
1524 let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| {
1525 assert!(worktree.has_update_observer());
1526 worktree.id()
1527 });
1528 let (worktree_a2, _) = project_a1
1529 .update(cx_a, |p, cx| {
1530 p.find_or_create_local_worktree("/root-1/dir2", true, cx)
1531 })
1532 .await
1533 .unwrap();
1534 executor.run_until_parked();
1535
1536 let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| {
1537 assert!(tree.has_update_observer());
1538 tree.id()
1539 });
1540 executor.run_until_parked();
1541
1542 project_b1.read_with(cx_b, |project, cx| {
1543 assert!(project.worktree_for_id(worktree2_id, cx).is_some())
1544 });
1545
1546 let buffer_a1 = project_a1
1547 .update(cx_a, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
1548 .await
1549 .unwrap();
1550 let buffer_b1 = project_b1
1551 .update(cx_b, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx))
1552 .await
1553 .unwrap();
1554
1555 // Drop client A's connection.
1556 server.forbid_connections();
1557 server.disconnect_client(client_a.peer_id().unwrap());
1558 executor.advance_clock(RECEIVE_TIMEOUT);
1559
1560 project_a1.read_with(cx_a, |project, _| {
1561 assert!(project.is_shared());
1562 assert_eq!(project.collaborators().len(), 1);
1563 });
1564
1565 project_b1.read_with(cx_b, |project, _| {
1566 assert!(!project.is_disconnected());
1567 assert_eq!(project.collaborators().len(), 1);
1568 });
1569
1570 worktree_a1.read_with(cx_a, |tree, _| assert!(tree.has_update_observer()));
1571
1572 // While client A is disconnected, add and remove files from client A's project.
1573 client_a
1574 .fs()
1575 .insert_tree(
1576 "/root-1/dir1/subdir2",
1577 json!({
1578 "f.txt": "f-contents",
1579 "g.txt": "g-contents",
1580 "h.txt": "h-contents",
1581 "i.txt": "i-contents",
1582 }),
1583 )
1584 .await;
1585 client_a
1586 .fs()
1587 .remove_dir(
1588 "/root-1/dir1/subdir1".as_ref(),
1589 RemoveOptions {
1590 recursive: true,
1591 ..Default::default()
1592 },
1593 )
1594 .await
1595 .unwrap();
1596
1597 // While client A is disconnected, add and remove worktrees from client A's project.
1598 project_a1.update(cx_a, |project, cx| {
1599 project.remove_worktree(worktree2_id, cx)
1600 });
1601 let (worktree_a3, _) = project_a1
1602 .update(cx_a, |p, cx| {
1603 p.find_or_create_local_worktree("/root-1/dir3", true, cx)
1604 })
1605 .await
1606 .unwrap();
1607 worktree_a3
1608 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
1609 .await;
1610
1611 let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
1612 assert!(!tree.has_update_observer());
1613 tree.id()
1614 });
1615 executor.run_until_parked();
1616
1617 // While client A is disconnected, close project 2
1618 cx_a.update(|_| drop(project_a2));
1619
1620 // While client A is disconnected, mutate a buffer on both the host and the guest.
1621 buffer_a1.update(cx_a, |buf, cx| buf.edit([(0..0, "W")], None, cx));
1622 buffer_b1.update(cx_b, |buf, cx| buf.edit([(1..1, "Z")], None, cx));
1623 executor.run_until_parked();
1624
1625 // Client A reconnects. Their project is re-shared, and client B re-joins it.
1626 server.allow_connections();
1627 client_a
1628 .authenticate_and_connect(false, &cx_a.to_async())
1629 .await
1630 .unwrap();
1631 executor.run_until_parked();
1632
1633 project_a1.read_with(cx_a, |project, cx| {
1634 assert!(project.is_shared());
1635 assert!(worktree_a1.read(cx).has_update_observer());
1636 assert_eq!(
1637 worktree_a1
1638 .read(cx)
1639 .snapshot()
1640 .paths()
1641 .map(|p| p.to_str().unwrap())
1642 .collect::<Vec<_>>(),
1643 vec![
1644 "a.txt",
1645 "b.txt",
1646 "subdir2",
1647 "subdir2/f.txt",
1648 "subdir2/g.txt",
1649 "subdir2/h.txt",
1650 "subdir2/i.txt"
1651 ]
1652 );
1653 assert!(worktree_a3.read(cx).has_update_observer());
1654 assert_eq!(
1655 worktree_a3
1656 .read(cx)
1657 .snapshot()
1658 .paths()
1659 .map(|p| p.to_str().unwrap())
1660 .collect::<Vec<_>>(),
1661 vec!["w.txt", "x.txt", "y.txt"]
1662 );
1663 });
1664
1665 project_b1.read_with(cx_b, |project, cx| {
1666 assert!(!project.is_disconnected());
1667 assert_eq!(
1668 project
1669 .worktree_for_id(worktree1_id, cx)
1670 .unwrap()
1671 .read(cx)
1672 .snapshot()
1673 .paths()
1674 .map(|p| p.to_str().unwrap())
1675 .collect::<Vec<_>>(),
1676 vec![
1677 "a.txt",
1678 "b.txt",
1679 "subdir2",
1680 "subdir2/f.txt",
1681 "subdir2/g.txt",
1682 "subdir2/h.txt",
1683 "subdir2/i.txt"
1684 ]
1685 );
1686 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
1687 assert_eq!(
1688 project
1689 .worktree_for_id(worktree3_id, cx)
1690 .unwrap()
1691 .read(cx)
1692 .snapshot()
1693 .paths()
1694 .map(|p| p.to_str().unwrap())
1695 .collect::<Vec<_>>(),
1696 vec!["w.txt", "x.txt", "y.txt"]
1697 );
1698 });
1699
1700 project_b2.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
1701
1702 project_b3.read_with(cx_b, |project, _| assert!(!project.is_disconnected()));
1703
1704 buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
1705
1706 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
1707
1708 // Drop client B's connection.
1709 server.forbid_connections();
1710 server.disconnect_client(client_b.peer_id().unwrap());
1711 executor.advance_clock(RECEIVE_TIMEOUT);
1712
1713 // While client B is disconnected, add and remove files from client A's project
1714 client_a
1715 .fs()
1716 .insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into())
1717 .await;
1718 client_a
1719 .fs()
1720 .remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default())
1721 .await
1722 .unwrap();
1723
1724 // While client B is disconnected, add and remove worktrees from client A's project.
1725 let (worktree_a4, _) = project_a1
1726 .update(cx_a, |p, cx| {
1727 p.find_or_create_local_worktree("/root-1/dir4", true, cx)
1728 })
1729 .await
1730 .unwrap();
1731 executor.run_until_parked();
1732
1733 let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| {
1734 assert!(tree.has_update_observer());
1735 tree.id()
1736 });
1737 project_a1.update(cx_a, |project, cx| {
1738 project.remove_worktree(worktree3_id, cx)
1739 });
1740 executor.run_until_parked();
1741
1742 // While client B is disconnected, mutate a buffer on both the host and the guest.
1743 buffer_a1.update(cx_a, |buf, cx| buf.edit([(1..1, "X")], None, cx));
1744 buffer_b1.update(cx_b, |buf, cx| buf.edit([(2..2, "Y")], None, cx));
1745 executor.run_until_parked();
1746
1747 // While disconnected, close project 3
1748 cx_a.update(|_| drop(project_a3));
1749
1750 // Client B reconnects. They re-join the room and the remaining shared project.
1751 server.allow_connections();
1752 client_b
1753 .authenticate_and_connect(false, &cx_b.to_async())
1754 .await
1755 .unwrap();
1756 executor.run_until_parked();
1757
1758 project_b1.read_with(cx_b, |project, cx| {
1759 assert!(!project.is_disconnected());
1760 assert_eq!(
1761 project
1762 .worktree_for_id(worktree1_id, cx)
1763 .unwrap()
1764 .read(cx)
1765 .snapshot()
1766 .paths()
1767 .map(|p| p.to_str().unwrap())
1768 .collect::<Vec<_>>(),
1769 vec![
1770 "a.txt",
1771 "b.txt",
1772 "subdir2",
1773 "subdir2/f.txt",
1774 "subdir2/g.txt",
1775 "subdir2/h.txt",
1776 "subdir2/j.txt"
1777 ]
1778 );
1779 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
1780 assert_eq!(
1781 project
1782 .worktree_for_id(worktree4_id, cx)
1783 .unwrap()
1784 .read(cx)
1785 .snapshot()
1786 .paths()
1787 .map(|p| p.to_str().unwrap())
1788 .collect::<Vec<_>>(),
1789 vec!["z.txt"]
1790 );
1791 });
1792
1793 project_b3.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
1794
1795 buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
1796
1797 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
1798}
1799
1800#[gpui::test(iterations = 10)]
1801async fn test_active_call_events(
1802 executor: BackgroundExecutor,
1803 cx_a: &mut TestAppContext,
1804 cx_b: &mut TestAppContext,
1805) {
1806 let mut server = TestServer::start(executor.clone()).await;
1807 let client_a = server.create_client(cx_a, "user_a").await;
1808 let client_b = server.create_client(cx_b, "user_b").await;
1809 client_a.fs().insert_tree("/a", json!({})).await;
1810 client_b.fs().insert_tree("/b", json!({})).await;
1811
1812 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1813 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
1814
1815 server
1816 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1817 .await;
1818 let active_call_a = cx_a.read(ActiveCall::global);
1819 let active_call_b = cx_b.read(ActiveCall::global);
1820
1821 let events_a = active_call_events(cx_a);
1822 let events_b = active_call_events(cx_b);
1823
1824 let project_a_id = active_call_a
1825 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1826 .await
1827 .unwrap();
1828 executor.run_until_parked();
1829 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1830 assert_eq!(
1831 mem::take(&mut *events_b.borrow_mut()),
1832 vec![room::Event::RemoteProjectShared {
1833 owner: Arc::new(User {
1834 id: client_a.user_id().unwrap(),
1835 github_login: "user_a".to_string(),
1836 avatar_uri: "avatar_a".into(),
1837 }),
1838 project_id: project_a_id,
1839 worktree_root_names: vec!["a".to_string()],
1840 }]
1841 );
1842
1843 let project_b_id = active_call_b
1844 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1845 .await
1846 .unwrap();
1847 executor.run_until_parked();
1848 assert_eq!(
1849 mem::take(&mut *events_a.borrow_mut()),
1850 vec![room::Event::RemoteProjectShared {
1851 owner: Arc::new(User {
1852 id: client_b.user_id().unwrap(),
1853 github_login: "user_b".to_string(),
1854 avatar_uri: "avatar_b".into(),
1855 }),
1856 project_id: project_b_id,
1857 worktree_root_names: vec!["b".to_string()]
1858 }]
1859 );
1860 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1861
1862 // Sharing a project twice is idempotent.
1863 let project_b_id_2 = active_call_b
1864 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
1865 .await
1866 .unwrap();
1867 assert_eq!(project_b_id_2, project_b_id);
1868 executor.run_until_parked();
1869 assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
1870 assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
1871
1872 // Unsharing a project should dispatch the RemoteProjectUnshared event.
1873 active_call_a
1874 .update(cx_a, |call, cx| call.hang_up(cx))
1875 .await
1876 .unwrap();
1877 executor.run_until_parked();
1878
1879 assert_eq!(
1880 mem::take(&mut *events_a.borrow_mut()),
1881 vec![room::Event::RoomLeft { channel_id: None }]
1882 );
1883 assert_eq!(
1884 mem::take(&mut *events_b.borrow_mut()),
1885 vec![room::Event::RemoteProjectUnshared {
1886 project_id: project_a_id,
1887 }]
1888 );
1889}
1890
1891fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
1892 let events = Rc::new(RefCell::new(Vec::new()));
1893 let active_call = cx.read(ActiveCall::global);
1894 cx.update({
1895 let events = events.clone();
1896 |cx| {
1897 cx.subscribe(&active_call, move |_, event, _| {
1898 events.borrow_mut().push(event.clone())
1899 })
1900 .detach()
1901 }
1902 });
1903 events
1904}
1905
1906#[gpui::test]
1907async fn test_mute_deafen(
1908 executor: BackgroundExecutor,
1909 cx_a: &mut TestAppContext,
1910 cx_b: &mut TestAppContext,
1911 cx_c: &mut TestAppContext,
1912) {
1913 let mut server = TestServer::start(executor.clone()).await;
1914 let client_a = server.create_client(cx_a, "user_a").await;
1915 let client_b = server.create_client(cx_b, "user_b").await;
1916 let client_c = server.create_client(cx_c, "user_c").await;
1917
1918 server
1919 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1920 .await;
1921
1922 let active_call_a = cx_a.read(ActiveCall::global);
1923 let active_call_b = cx_b.read(ActiveCall::global);
1924 let active_call_c = cx_c.read(ActiveCall::global);
1925
1926 // User A calls user B, B answers.
1927 active_call_a
1928 .update(cx_a, |call, cx| {
1929 call.invite(client_b.user_id().unwrap(), None, cx)
1930 })
1931 .await
1932 .unwrap();
1933 executor.run_until_parked();
1934 active_call_b
1935 .update(cx_b, |call, cx| call.accept_incoming(cx))
1936 .await
1937 .unwrap();
1938 executor.run_until_parked();
1939
1940 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
1941 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
1942
1943 room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
1944 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1945
1946 // Users A and B are both muted.
1947 assert_eq!(
1948 participant_audio_state(&room_a, cx_a),
1949 &[ParticipantAudioState {
1950 user_id: client_b.user_id().unwrap(),
1951 is_muted: false,
1952 audio_tracks_playing: vec![true],
1953 }]
1954 );
1955 assert_eq!(
1956 participant_audio_state(&room_b, cx_b),
1957 &[ParticipantAudioState {
1958 user_id: client_a.user_id().unwrap(),
1959 is_muted: false,
1960 audio_tracks_playing: vec![true],
1961 }]
1962 );
1963
1964 // User A mutes
1965 room_a.update(cx_a, |room, cx| room.toggle_mute(cx));
1966 executor.run_until_parked();
1967
1968 // User A hears user B, but B doesn't hear A.
1969 room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
1970 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1971 assert_eq!(
1972 participant_audio_state(&room_a, cx_a),
1973 &[ParticipantAudioState {
1974 user_id: client_b.user_id().unwrap(),
1975 is_muted: false,
1976 audio_tracks_playing: vec![true],
1977 }]
1978 );
1979 assert_eq!(
1980 participant_audio_state(&room_b, cx_b),
1981 &[ParticipantAudioState {
1982 user_id: client_a.user_id().unwrap(),
1983 is_muted: true,
1984 audio_tracks_playing: vec![true],
1985 }]
1986 );
1987
1988 // User A deafens
1989 room_a.update(cx_a, |room, cx| room.toggle_deafen(cx));
1990 executor.run_until_parked();
1991
1992 // User A does not hear user B.
1993 room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
1994 room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
1995 assert_eq!(
1996 participant_audio_state(&room_a, cx_a),
1997 &[ParticipantAudioState {
1998 user_id: client_b.user_id().unwrap(),
1999 is_muted: false,
2000 audio_tracks_playing: vec![false],
2001 }]
2002 );
2003 assert_eq!(
2004 participant_audio_state(&room_b, cx_b),
2005 &[ParticipantAudioState {
2006 user_id: client_a.user_id().unwrap(),
2007 is_muted: true,
2008 audio_tracks_playing: vec![true],
2009 }]
2010 );
2011
2012 // User B calls user C, C joins.
2013 active_call_b
2014 .update(cx_b, |call, cx| {
2015 call.invite(client_c.user_id().unwrap(), None, cx)
2016 })
2017 .await
2018 .unwrap();
2019 executor.run_until_parked();
2020 active_call_c
2021 .update(cx_c, |call, cx| call.accept_incoming(cx))
2022 .await
2023 .unwrap();
2024 executor.run_until_parked();
2025
2026 // User A does not hear users B or C.
2027 assert_eq!(
2028 participant_audio_state(&room_a, cx_a),
2029 &[
2030 ParticipantAudioState {
2031 user_id: client_b.user_id().unwrap(),
2032 is_muted: false,
2033 audio_tracks_playing: vec![false],
2034 },
2035 ParticipantAudioState {
2036 user_id: client_c.user_id().unwrap(),
2037 is_muted: false,
2038 audio_tracks_playing: vec![false],
2039 }
2040 ]
2041 );
2042 assert_eq!(
2043 participant_audio_state(&room_b, cx_b),
2044 &[
2045 ParticipantAudioState {
2046 user_id: client_a.user_id().unwrap(),
2047 is_muted: true,
2048 audio_tracks_playing: vec![true],
2049 },
2050 ParticipantAudioState {
2051 user_id: client_c.user_id().unwrap(),
2052 is_muted: false,
2053 audio_tracks_playing: vec![true],
2054 }
2055 ]
2056 );
2057
2058 #[derive(PartialEq, Eq, Debug)]
2059 struct ParticipantAudioState {
2060 user_id: u64,
2061 is_muted: bool,
2062 audio_tracks_playing: Vec<bool>,
2063 }
2064
2065 fn participant_audio_state(
2066 room: &Model<Room>,
2067 cx: &TestAppContext,
2068 ) -> Vec<ParticipantAudioState> {
2069 room.read_with(cx, |room, _| {
2070 room.remote_participants()
2071 .iter()
2072 .map(|(user_id, participant)| ParticipantAudioState {
2073 user_id: *user_id,
2074 is_muted: participant.muted,
2075 audio_tracks_playing: participant
2076 .audio_tracks
2077 .values()
2078 .map(|track| track.is_playing())
2079 .collect(),
2080 })
2081 .collect::<Vec<_>>()
2082 })
2083 }
2084}
2085
2086#[gpui::test(iterations = 10)]
2087async fn test_room_location(
2088 executor: BackgroundExecutor,
2089 cx_a: &mut TestAppContext,
2090 cx_b: &mut TestAppContext,
2091) {
2092 let mut server = TestServer::start(executor.clone()).await;
2093 let client_a = server.create_client(cx_a, "user_a").await;
2094 let client_b = server.create_client(cx_b, "user_b").await;
2095 client_a.fs().insert_tree("/a", json!({})).await;
2096 client_b.fs().insert_tree("/b", json!({})).await;
2097
2098 let active_call_a = cx_a.read(ActiveCall::global);
2099 let active_call_b = cx_b.read(ActiveCall::global);
2100
2101 let a_notified = Rc::new(Cell::new(false));
2102 cx_a.update({
2103 let notified = a_notified.clone();
2104 |cx| {
2105 cx.observe(&active_call_a, move |_, _| notified.set(true))
2106 .detach()
2107 }
2108 });
2109
2110 let b_notified = Rc::new(Cell::new(false));
2111 cx_b.update({
2112 let b_notified = b_notified.clone();
2113 |cx| {
2114 cx.observe(&active_call_b, move |_, _| b_notified.set(true))
2115 .detach()
2116 }
2117 });
2118
2119 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
2120 active_call_a
2121 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2122 .await
2123 .unwrap();
2124 let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
2125
2126 server
2127 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2128 .await;
2129
2130 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
2131
2132 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
2133 executor.run_until_parked();
2134 assert!(a_notified.take());
2135 assert_eq!(
2136 participant_locations(&room_a, cx_a),
2137 vec![("user_b".to_string(), ParticipantLocation::External)]
2138 );
2139 assert!(b_notified.take());
2140 assert_eq!(
2141 participant_locations(&room_b, cx_b),
2142 vec![("user_a".to_string(), ParticipantLocation::UnsharedProject)]
2143 );
2144
2145 let project_a_id = active_call_a
2146 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2147 .await
2148 .unwrap();
2149 executor.run_until_parked();
2150 assert!(a_notified.take());
2151 assert_eq!(
2152 participant_locations(&room_a, cx_a),
2153 vec![("user_b".to_string(), ParticipantLocation::External)]
2154 );
2155 assert!(b_notified.take());
2156 assert_eq!(
2157 participant_locations(&room_b, cx_b),
2158 vec![(
2159 "user_a".to_string(),
2160 ParticipantLocation::SharedProject {
2161 project_id: project_a_id
2162 }
2163 )]
2164 );
2165
2166 let project_b_id = active_call_b
2167 .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
2168 .await
2169 .unwrap();
2170 executor.run_until_parked();
2171 assert!(a_notified.take());
2172 assert_eq!(
2173 participant_locations(&room_a, cx_a),
2174 vec![("user_b".to_string(), ParticipantLocation::External)]
2175 );
2176 assert!(b_notified.take());
2177 assert_eq!(
2178 participant_locations(&room_b, cx_b),
2179 vec![(
2180 "user_a".to_string(),
2181 ParticipantLocation::SharedProject {
2182 project_id: project_a_id
2183 }
2184 )]
2185 );
2186
2187 active_call_b
2188 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2189 .await
2190 .unwrap();
2191 executor.run_until_parked();
2192 assert!(a_notified.take());
2193 assert_eq!(
2194 participant_locations(&room_a, cx_a),
2195 vec![(
2196 "user_b".to_string(),
2197 ParticipantLocation::SharedProject {
2198 project_id: project_b_id
2199 }
2200 )]
2201 );
2202 assert!(b_notified.take());
2203 assert_eq!(
2204 participant_locations(&room_b, cx_b),
2205 vec![(
2206 "user_a".to_string(),
2207 ParticipantLocation::SharedProject {
2208 project_id: project_a_id
2209 }
2210 )]
2211 );
2212
2213 active_call_b
2214 .update(cx_b, |call, cx| call.set_location(None, cx))
2215 .await
2216 .unwrap();
2217 executor.run_until_parked();
2218 assert!(a_notified.take());
2219 assert_eq!(
2220 participant_locations(&room_a, cx_a),
2221 vec![("user_b".to_string(), ParticipantLocation::External)]
2222 );
2223 assert!(b_notified.take());
2224 assert_eq!(
2225 participant_locations(&room_b, cx_b),
2226 vec![(
2227 "user_a".to_string(),
2228 ParticipantLocation::SharedProject {
2229 project_id: project_a_id
2230 }
2231 )]
2232 );
2233
2234 fn participant_locations(
2235 room: &Model<Room>,
2236 cx: &TestAppContext,
2237 ) -> Vec<(String, ParticipantLocation)> {
2238 room.read_with(cx, |room, _| {
2239 room.remote_participants()
2240 .values()
2241 .map(|participant| {
2242 (
2243 participant.user.github_login.to_string(),
2244 participant.location,
2245 )
2246 })
2247 .collect()
2248 })
2249 }
2250}
2251
2252#[gpui::test(iterations = 10)]
2253async fn test_propagate_saves_and_fs_changes(
2254 executor: BackgroundExecutor,
2255 cx_a: &mut TestAppContext,
2256 cx_b: &mut TestAppContext,
2257 cx_c: &mut TestAppContext,
2258) {
2259 let mut server = TestServer::start(executor.clone()).await;
2260 let client_a = server.create_client(cx_a, "user_a").await;
2261 let client_b = server.create_client(cx_b, "user_b").await;
2262 let client_c = server.create_client(cx_c, "user_c").await;
2263
2264 server
2265 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2266 .await;
2267 let active_call_a = cx_a.read(ActiveCall::global);
2268
2269 let rust = Arc::new(Language::new(
2270 LanguageConfig {
2271 name: "Rust".into(),
2272 matcher: LanguageMatcher {
2273 path_suffixes: vec!["rs".to_string()],
2274 ..Default::default()
2275 },
2276 ..Default::default()
2277 },
2278 Some(tree_sitter_rust::language()),
2279 ));
2280 let javascript = Arc::new(Language::new(
2281 LanguageConfig {
2282 name: "JavaScript".into(),
2283 matcher: LanguageMatcher {
2284 path_suffixes: vec!["js".to_string()],
2285 ..Default::default()
2286 },
2287 ..Default::default()
2288 },
2289 Some(tree_sitter_rust::language()),
2290 ));
2291 for client in [&client_a, &client_b, &client_c] {
2292 client.language_registry().add(rust.clone());
2293 client.language_registry().add(javascript.clone());
2294 }
2295
2296 client_a
2297 .fs()
2298 .insert_tree(
2299 "/a",
2300 json!({
2301 "file1.rs": "",
2302 "file2": ""
2303 }),
2304 )
2305 .await;
2306 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2307
2308 let worktree_a = project_a.read_with(cx_a, |p, _| p.worktrees().next().unwrap());
2309 let project_id = active_call_a
2310 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2311 .await
2312 .unwrap();
2313
2314 // Join that worktree as clients B and C.
2315 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
2316 let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
2317
2318 let worktree_b = project_b.read_with(cx_b, |p, _| p.worktrees().next().unwrap());
2319
2320 let worktree_c = project_c.read_with(cx_c, |p, _| p.worktrees().next().unwrap());
2321
2322 // Open and edit a buffer as both guests B and C.
2323 let buffer_b = project_b
2324 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2325 .await
2326 .unwrap();
2327 let buffer_c = project_c
2328 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2329 .await
2330 .unwrap();
2331
2332 buffer_b.read_with(cx_b, |buffer, _| {
2333 assert_eq!(&*buffer.language().unwrap().name(), "Rust");
2334 });
2335
2336 buffer_c.read_with(cx_c, |buffer, _| {
2337 assert_eq!(&*buffer.language().unwrap().name(), "Rust");
2338 });
2339 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
2340 buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
2341
2342 // Open and edit that buffer as the host.
2343 let buffer_a = project_a
2344 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
2345 .await
2346 .unwrap();
2347
2348 executor.run_until_parked();
2349
2350 buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, "));
2351 buffer_a.update(cx_a, |buf, cx| {
2352 buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
2353 });
2354
2355 executor.run_until_parked();
2356
2357 buffer_a.read_with(cx_a, |buf, _| {
2358 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2359 });
2360
2361 buffer_b.read_with(cx_b, |buf, _| {
2362 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2363 });
2364
2365 buffer_c.read_with(cx_c, |buf, _| {
2366 assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
2367 });
2368
2369 // Edit the buffer as the host and concurrently save as guest B.
2370 let save_b = project_b.update(cx_b, |project, cx| {
2371 project.save_buffer(buffer_b.clone(), cx)
2372 });
2373 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
2374 save_b.await.unwrap();
2375 assert_eq!(
2376 client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(),
2377 "hi-a, i-am-c, i-am-b, i-am-a"
2378 );
2379
2380 executor.run_until_parked();
2381
2382 buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
2383
2384 buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
2385
2386 buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty()));
2387
2388 // Make changes on host's file system, see those changes on guest worktrees.
2389 client_a
2390 .fs()
2391 .rename(
2392 "/a/file1.rs".as_ref(),
2393 "/a/file1.js".as_ref(),
2394 Default::default(),
2395 )
2396 .await
2397 .unwrap();
2398 client_a
2399 .fs()
2400 .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
2401 .await
2402 .unwrap();
2403 client_a.fs().insert_file("/a/file4", "4".into()).await;
2404 executor.run_until_parked();
2405
2406 worktree_a.read_with(cx_a, |tree, _| {
2407 assert_eq!(
2408 tree.paths()
2409 .map(|p| p.to_string_lossy())
2410 .collect::<Vec<_>>(),
2411 ["file1.js", "file3", "file4"]
2412 )
2413 });
2414
2415 worktree_b.read_with(cx_b, |tree, _| {
2416 assert_eq!(
2417 tree.paths()
2418 .map(|p| p.to_string_lossy())
2419 .collect::<Vec<_>>(),
2420 ["file1.js", "file3", "file4"]
2421 )
2422 });
2423
2424 worktree_c.read_with(cx_c, |tree, _| {
2425 assert_eq!(
2426 tree.paths()
2427 .map(|p| p.to_string_lossy())
2428 .collect::<Vec<_>>(),
2429 ["file1.js", "file3", "file4"]
2430 )
2431 });
2432
2433 // Ensure buffer files are updated as well.
2434
2435 buffer_a.read_with(cx_a, |buffer, _| {
2436 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2437 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2438 });
2439
2440 buffer_b.read_with(cx_b, |buffer, _| {
2441 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2442 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2443 });
2444
2445 buffer_c.read_with(cx_c, |buffer, _| {
2446 assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
2447 assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
2448 });
2449
2450 let new_buffer_a = project_a
2451 .update(cx_a, |p, cx| p.create_buffer(cx))
2452 .await
2453 .unwrap();
2454
2455 let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
2456 let new_buffer_b = project_b
2457 .update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx))
2458 .await
2459 .unwrap();
2460
2461 new_buffer_b.read_with(cx_b, |buffer, _| {
2462 assert!(buffer.file().is_none());
2463 });
2464
2465 new_buffer_a.update(cx_a, |buffer, cx| {
2466 buffer.edit([(0..0, "ok")], None, cx);
2467 });
2468 project_a
2469 .update(cx_a, |project, cx| {
2470 let path = ProjectPath {
2471 path: Arc::from(Path::new("file3.rs")),
2472 worktree_id: worktree_a.read(cx).id(),
2473 };
2474
2475 project.save_buffer_as(new_buffer_a.clone(), path, cx)
2476 })
2477 .await
2478 .unwrap();
2479
2480 executor.run_until_parked();
2481
2482 new_buffer_b.read_with(cx_b, |buffer_b, _| {
2483 assert_eq!(
2484 buffer_b.file().unwrap().path().as_ref(),
2485 Path::new("file3.rs")
2486 );
2487
2488 new_buffer_a.read_with(cx_a, |buffer_a, _| {
2489 assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime());
2490 assert_eq!(buffer_b.saved_version(), buffer_a.saved_version());
2491 });
2492 });
2493}
2494
2495#[gpui::test(iterations = 10)]
2496async fn test_git_diff_base_change(
2497 executor: BackgroundExecutor,
2498 cx_a: &mut TestAppContext,
2499 cx_b: &mut TestAppContext,
2500) {
2501 let mut server = TestServer::start(executor.clone()).await;
2502 let client_a = server.create_client(cx_a, "user_a").await;
2503 let client_b = server.create_client(cx_b, "user_b").await;
2504 server
2505 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2506 .await;
2507 let active_call_a = cx_a.read(ActiveCall::global);
2508
2509 client_a
2510 .fs()
2511 .insert_tree(
2512 "/dir",
2513 json!({
2514 ".git": {},
2515 "sub": {
2516 ".git": {},
2517 "b.txt": "
2518 one
2519 two
2520 three
2521 ".unindent(),
2522 },
2523 "a.txt": "
2524 one
2525 two
2526 three
2527 ".unindent(),
2528 }),
2529 )
2530 .await;
2531
2532 let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2533 let project_id = active_call_a
2534 .update(cx_a, |call, cx| {
2535 call.share_project(project_local.clone(), cx)
2536 })
2537 .await
2538 .unwrap();
2539
2540 let project_remote = client_b.build_dev_server_project(project_id, cx_b).await;
2541
2542 let diff_base = "
2543 one
2544 three
2545 "
2546 .unindent();
2547
2548 let new_diff_base = "
2549 one
2550 two
2551 "
2552 .unindent();
2553
2554 client_a.fs().set_index_for_repo(
2555 Path::new("/dir/.git"),
2556 &[(Path::new("a.txt"), diff_base.clone())],
2557 );
2558
2559 // Create the buffer
2560 let buffer_local_a = project_local
2561 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2562 .await
2563 .unwrap();
2564
2565 // Wait for it to catch up to the new diff
2566 executor.run_until_parked();
2567
2568 // Smoke test diffing
2569
2570 buffer_local_a.read_with(cx_a, |buffer, _| {
2571 assert_eq!(
2572 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2573 Some(diff_base.as_str())
2574 );
2575 git::diff::assert_hunks(
2576 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2577 &buffer,
2578 &diff_base,
2579 &[(1..2, "", "two\n")],
2580 );
2581 });
2582
2583 // Create remote buffer
2584 let buffer_remote_a = project_remote
2585 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2586 .await
2587 .unwrap();
2588
2589 // Wait remote buffer to catch up to the new diff
2590 executor.run_until_parked();
2591
2592 // Smoke test diffing
2593
2594 buffer_remote_a.read_with(cx_b, |buffer, _| {
2595 assert_eq!(
2596 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2597 Some(diff_base.as_str())
2598 );
2599 git::diff::assert_hunks(
2600 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2601 &buffer,
2602 &diff_base,
2603 &[(1..2, "", "two\n")],
2604 );
2605 });
2606
2607 client_a.fs().set_index_for_repo(
2608 Path::new("/dir/.git"),
2609 &[(Path::new("a.txt"), new_diff_base.clone())],
2610 );
2611
2612 // Wait for buffer_local_a to receive it
2613 executor.run_until_parked();
2614
2615 // Smoke test new diffing
2616
2617 buffer_local_a.read_with(cx_a, |buffer, _| {
2618 assert_eq!(
2619 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2620 Some(new_diff_base.as_str())
2621 );
2622
2623 git::diff::assert_hunks(
2624 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2625 &buffer,
2626 &diff_base,
2627 &[(2..3, "", "three\n")],
2628 );
2629 });
2630
2631 // Smoke test B
2632
2633 buffer_remote_a.read_with(cx_b, |buffer, _| {
2634 assert_eq!(
2635 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2636 Some(new_diff_base.as_str())
2637 );
2638 git::diff::assert_hunks(
2639 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2640 &buffer,
2641 &diff_base,
2642 &[(2..3, "", "three\n")],
2643 );
2644 });
2645
2646 //Nested git dir
2647
2648 let diff_base = "
2649 one
2650 three
2651 "
2652 .unindent();
2653
2654 let new_diff_base = "
2655 one
2656 two
2657 "
2658 .unindent();
2659
2660 client_a.fs().set_index_for_repo(
2661 Path::new("/dir/sub/.git"),
2662 &[(Path::new("b.txt"), diff_base.clone())],
2663 );
2664
2665 // Create the buffer
2666 let buffer_local_b = project_local
2667 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2668 .await
2669 .unwrap();
2670
2671 // Wait for it to catch up to the new diff
2672 executor.run_until_parked();
2673
2674 // Smoke test diffing
2675
2676 buffer_local_b.read_with(cx_a, |buffer, _| {
2677 assert_eq!(
2678 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2679 Some(diff_base.as_str())
2680 );
2681 git::diff::assert_hunks(
2682 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2683 &buffer,
2684 &diff_base,
2685 &[(1..2, "", "two\n")],
2686 );
2687 });
2688
2689 // Create remote buffer
2690 let buffer_remote_b = project_remote
2691 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
2692 .await
2693 .unwrap();
2694
2695 // Wait remote buffer to catch up to the new diff
2696 executor.run_until_parked();
2697
2698 // Smoke test diffing
2699
2700 buffer_remote_b.read_with(cx_b, |buffer, _| {
2701 assert_eq!(
2702 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2703 Some(diff_base.as_str())
2704 );
2705 git::diff::assert_hunks(
2706 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2707 &buffer,
2708 &diff_base,
2709 &[(1..2, "", "two\n")],
2710 );
2711 });
2712
2713 client_a.fs().set_index_for_repo(
2714 Path::new("/dir/sub/.git"),
2715 &[(Path::new("b.txt"), new_diff_base.clone())],
2716 );
2717
2718 // Wait for buffer_local_b to receive it
2719 executor.run_until_parked();
2720
2721 // Smoke test new diffing
2722
2723 buffer_local_b.read_with(cx_a, |buffer, _| {
2724 assert_eq!(
2725 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2726 Some(new_diff_base.as_str())
2727 );
2728 println!("{:?}", buffer.as_rope().to_string());
2729 println!("{:?}", buffer.diff_base());
2730 println!(
2731 "{:?}",
2732 buffer
2733 .snapshot()
2734 .git_diff_hunks_in_row_range(0..4)
2735 .collect::<Vec<_>>()
2736 );
2737
2738 git::diff::assert_hunks(
2739 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2740 &buffer,
2741 &diff_base,
2742 &[(2..3, "", "three\n")],
2743 );
2744 });
2745
2746 // Smoke test B
2747
2748 buffer_remote_b.read_with(cx_b, |buffer, _| {
2749 assert_eq!(
2750 buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
2751 Some(new_diff_base.as_str())
2752 );
2753 git::diff::assert_hunks(
2754 buffer.snapshot().git_diff_hunks_in_row_range(0..4),
2755 &buffer,
2756 &diff_base,
2757 &[(2..3, "", "three\n")],
2758 );
2759 });
2760}
2761
2762#[gpui::test]
2763async fn test_git_branch_name(
2764 executor: BackgroundExecutor,
2765 cx_a: &mut TestAppContext,
2766 cx_b: &mut TestAppContext,
2767 cx_c: &mut TestAppContext,
2768) {
2769 let mut server = TestServer::start(executor.clone()).await;
2770 let client_a = server.create_client(cx_a, "user_a").await;
2771 let client_b = server.create_client(cx_b, "user_b").await;
2772 let client_c = server.create_client(cx_c, "user_c").await;
2773 server
2774 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2775 .await;
2776 let active_call_a = cx_a.read(ActiveCall::global);
2777
2778 client_a
2779 .fs()
2780 .insert_tree(
2781 "/dir",
2782 json!({
2783 ".git": {},
2784 }),
2785 )
2786 .await;
2787
2788 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2789 let project_id = active_call_a
2790 .update(cx_a, |call, cx| {
2791 call.share_project(project_local.clone(), cx)
2792 })
2793 .await
2794 .unwrap();
2795
2796 let project_remote = client_b.build_dev_server_project(project_id, cx_b).await;
2797 client_a
2798 .fs()
2799 .set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
2800
2801 // Wait for it to catch up to the new branch
2802 executor.run_until_parked();
2803
2804 #[track_caller]
2805 fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &AppContext) {
2806 let branch_name = branch_name.map(Into::into);
2807 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2808 assert_eq!(worktrees.len(), 1);
2809 let worktree = worktrees[0].clone();
2810 let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap();
2811 assert_eq!(root_entry.branch(), branch_name.map(Into::into));
2812 }
2813
2814 // Smoke test branch reading
2815
2816 project_local.read_with(cx_a, |project, cx| {
2817 assert_branch(Some("branch-1"), project, cx)
2818 });
2819
2820 project_remote.read_with(cx_b, |project, cx| {
2821 assert_branch(Some("branch-1"), project, cx)
2822 });
2823
2824 client_a
2825 .fs()
2826 .set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
2827
2828 // Wait for buffer_local_a to receive it
2829 executor.run_until_parked();
2830
2831 // Smoke test branch reading
2832
2833 project_local.read_with(cx_a, |project, cx| {
2834 assert_branch(Some("branch-2"), project, cx)
2835 });
2836
2837 project_remote.read_with(cx_b, |project, cx| {
2838 assert_branch(Some("branch-2"), project, cx)
2839 });
2840
2841 let project_remote_c = client_c.build_dev_server_project(project_id, cx_c).await;
2842 executor.run_until_parked();
2843
2844 project_remote_c.read_with(cx_c, |project, cx| {
2845 assert_branch(Some("branch-2"), project, cx)
2846 });
2847}
2848
2849#[gpui::test]
2850async fn test_git_status_sync(
2851 executor: BackgroundExecutor,
2852 cx_a: &mut TestAppContext,
2853 cx_b: &mut TestAppContext,
2854 cx_c: &mut TestAppContext,
2855) {
2856 let mut server = TestServer::start(executor.clone()).await;
2857 let client_a = server.create_client(cx_a, "user_a").await;
2858 let client_b = server.create_client(cx_b, "user_b").await;
2859 let client_c = server.create_client(cx_c, "user_c").await;
2860 server
2861 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2862 .await;
2863 let active_call_a = cx_a.read(ActiveCall::global);
2864
2865 client_a
2866 .fs()
2867 .insert_tree(
2868 "/dir",
2869 json!({
2870 ".git": {},
2871 "a.txt": "a",
2872 "b.txt": "b",
2873 }),
2874 )
2875 .await;
2876
2877 const A_TXT: &str = "a.txt";
2878 const B_TXT: &str = "b.txt";
2879
2880 client_a.fs().set_status_for_repo_via_git_operation(
2881 Path::new("/dir/.git"),
2882 &[
2883 (&Path::new(A_TXT), GitFileStatus::Added),
2884 (&Path::new(B_TXT), GitFileStatus::Added),
2885 ],
2886 );
2887
2888 let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2889 let project_id = active_call_a
2890 .update(cx_a, |call, cx| {
2891 call.share_project(project_local.clone(), cx)
2892 })
2893 .await
2894 .unwrap();
2895
2896 let project_remote = client_b.build_dev_server_project(project_id, cx_b).await;
2897
2898 // Wait for it to catch up to the new status
2899 executor.run_until_parked();
2900
2901 #[track_caller]
2902 fn assert_status(
2903 file: &impl AsRef<Path>,
2904 status: Option<GitFileStatus>,
2905 project: &Project,
2906 cx: &AppContext,
2907 ) {
2908 let file = file.as_ref();
2909 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
2910 assert_eq!(worktrees.len(), 1);
2911 let worktree = worktrees[0].clone();
2912 let snapshot = worktree.read(cx).snapshot();
2913 assert_eq!(snapshot.status_for_file(file), status);
2914 }
2915
2916 // Smoke test status reading
2917
2918 project_local.read_with(cx_a, |project, cx| {
2919 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2920 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2921 });
2922
2923 project_remote.read_with(cx_b, |project, cx| {
2924 assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
2925 assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
2926 });
2927
2928 client_a.fs().set_status_for_repo_via_working_copy_change(
2929 Path::new("/dir/.git"),
2930 &[
2931 (&Path::new(A_TXT), GitFileStatus::Modified),
2932 (&Path::new(B_TXT), GitFileStatus::Modified),
2933 ],
2934 );
2935
2936 // Wait for buffer_local_a to receive it
2937 executor.run_until_parked();
2938
2939 // Smoke test status reading
2940
2941 project_local.read_with(cx_a, |project, cx| {
2942 assert_status(
2943 &Path::new(A_TXT),
2944 Some(GitFileStatus::Modified),
2945 project,
2946 cx,
2947 );
2948 assert_status(
2949 &Path::new(B_TXT),
2950 Some(GitFileStatus::Modified),
2951 project,
2952 cx,
2953 );
2954 });
2955
2956 project_remote.read_with(cx_b, |project, cx| {
2957 assert_status(
2958 &Path::new(A_TXT),
2959 Some(GitFileStatus::Modified),
2960 project,
2961 cx,
2962 );
2963 assert_status(
2964 &Path::new(B_TXT),
2965 Some(GitFileStatus::Modified),
2966 project,
2967 cx,
2968 );
2969 });
2970
2971 // And synchronization while joining
2972 let project_remote_c = client_c.build_dev_server_project(project_id, cx_c).await;
2973 executor.run_until_parked();
2974
2975 project_remote_c.read_with(cx_c, |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
2991#[gpui::test(iterations = 10)]
2992async fn test_fs_operations(
2993 executor: BackgroundExecutor,
2994 cx_a: &mut TestAppContext,
2995 cx_b: &mut TestAppContext,
2996) {
2997 let mut server = TestServer::start(executor.clone()).await;
2998 let client_a = server.create_client(cx_a, "user_a").await;
2999 let client_b = server.create_client(cx_b, "user_b").await;
3000 server
3001 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3002 .await;
3003 let active_call_a = cx_a.read(ActiveCall::global);
3004
3005 client_a
3006 .fs()
3007 .insert_tree(
3008 "/dir",
3009 json!({
3010 "a.txt": "a-contents",
3011 "b.txt": "b-contents",
3012 }),
3013 )
3014 .await;
3015 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3016 let project_id = active_call_a
3017 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3018 .await
3019 .unwrap();
3020 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
3021
3022 let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
3023 let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
3024
3025 let entry = project_b
3026 .update(cx_b, |project, cx| {
3027 project.create_entry((worktree_id, "c.txt"), false, cx)
3028 })
3029 .await
3030 .unwrap()
3031 .to_included()
3032 .unwrap();
3033
3034 worktree_a.read_with(cx_a, |worktree, _| {
3035 assert_eq!(
3036 worktree
3037 .paths()
3038 .map(|p| p.to_string_lossy())
3039 .collect::<Vec<_>>(),
3040 ["a.txt", "b.txt", "c.txt"]
3041 );
3042 });
3043
3044 worktree_b.read_with(cx_b, |worktree, _| {
3045 assert_eq!(
3046 worktree
3047 .paths()
3048 .map(|p| p.to_string_lossy())
3049 .collect::<Vec<_>>(),
3050 ["a.txt", "b.txt", "c.txt"]
3051 );
3052 });
3053
3054 project_b
3055 .update(cx_b, |project, cx| {
3056 project.rename_entry(entry.id, Path::new("d.txt"), cx)
3057 })
3058 .await
3059 .unwrap()
3060 .to_included()
3061 .unwrap();
3062
3063 worktree_a.read_with(cx_a, |worktree, _| {
3064 assert_eq!(
3065 worktree
3066 .paths()
3067 .map(|p| p.to_string_lossy())
3068 .collect::<Vec<_>>(),
3069 ["a.txt", "b.txt", "d.txt"]
3070 );
3071 });
3072
3073 worktree_b.read_with(cx_b, |worktree, _| {
3074 assert_eq!(
3075 worktree
3076 .paths()
3077 .map(|p| p.to_string_lossy())
3078 .collect::<Vec<_>>(),
3079 ["a.txt", "b.txt", "d.txt"]
3080 );
3081 });
3082
3083 let dir_entry = project_b
3084 .update(cx_b, |project, cx| {
3085 project.create_entry((worktree_id, "DIR"), true, cx)
3086 })
3087 .await
3088 .unwrap()
3089 .to_included()
3090 .unwrap();
3091
3092 worktree_a.read_with(cx_a, |worktree, _| {
3093 assert_eq!(
3094 worktree
3095 .paths()
3096 .map(|p| p.to_string_lossy())
3097 .collect::<Vec<_>>(),
3098 ["DIR", "a.txt", "b.txt", "d.txt"]
3099 );
3100 });
3101
3102 worktree_b.read_with(cx_b, |worktree, _| {
3103 assert_eq!(
3104 worktree
3105 .paths()
3106 .map(|p| p.to_string_lossy())
3107 .collect::<Vec<_>>(),
3108 ["DIR", "a.txt", "b.txt", "d.txt"]
3109 );
3110 });
3111
3112 project_b
3113 .update(cx_b, |project, cx| {
3114 project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
3115 })
3116 .await
3117 .unwrap()
3118 .to_included()
3119 .unwrap();
3120
3121 project_b
3122 .update(cx_b, |project, cx| {
3123 project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
3124 })
3125 .await
3126 .unwrap()
3127 .to_included()
3128 .unwrap();
3129
3130 project_b
3131 .update(cx_b, |project, cx| {
3132 project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
3133 })
3134 .await
3135 .unwrap()
3136 .to_included()
3137 .unwrap();
3138
3139 worktree_a.read_with(cx_a, |worktree, _| {
3140 assert_eq!(
3141 worktree
3142 .paths()
3143 .map(|p| p.to_string_lossy())
3144 .collect::<Vec<_>>(),
3145 [
3146 "DIR",
3147 "DIR/SUBDIR",
3148 "DIR/SUBDIR/f.txt",
3149 "DIR/e.txt",
3150 "a.txt",
3151 "b.txt",
3152 "d.txt"
3153 ]
3154 );
3155 });
3156
3157 worktree_b.read_with(cx_b, |worktree, _| {
3158 assert_eq!(
3159 worktree
3160 .paths()
3161 .map(|p| p.to_string_lossy())
3162 .collect::<Vec<_>>(),
3163 [
3164 "DIR",
3165 "DIR/SUBDIR",
3166 "DIR/SUBDIR/f.txt",
3167 "DIR/e.txt",
3168 "a.txt",
3169 "b.txt",
3170 "d.txt"
3171 ]
3172 );
3173 });
3174
3175 project_b
3176 .update(cx_b, |project, cx| {
3177 project.copy_entry(entry.id, Path::new("f.txt"), cx)
3178 })
3179 .await
3180 .unwrap()
3181 .unwrap();
3182
3183 worktree_a.read_with(cx_a, |worktree, _| {
3184 assert_eq!(
3185 worktree
3186 .paths()
3187 .map(|p| p.to_string_lossy())
3188 .collect::<Vec<_>>(),
3189 [
3190 "DIR",
3191 "DIR/SUBDIR",
3192 "DIR/SUBDIR/f.txt",
3193 "DIR/e.txt",
3194 "a.txt",
3195 "b.txt",
3196 "d.txt",
3197 "f.txt"
3198 ]
3199 );
3200 });
3201
3202 worktree_b.read_with(cx_b, |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 project_b
3222 .update(cx_b, |project, cx| {
3223 project.delete_entry(dir_entry.id, false, cx).unwrap()
3224 })
3225 .await
3226 .unwrap();
3227 executor.run_until_parked();
3228
3229 worktree_a.read_with(cx_a, |worktree, _| {
3230 assert_eq!(
3231 worktree
3232 .paths()
3233 .map(|p| p.to_string_lossy())
3234 .collect::<Vec<_>>(),
3235 ["a.txt", "b.txt", "d.txt", "f.txt"]
3236 );
3237 });
3238
3239 worktree_b.read_with(cx_b, |worktree, _| {
3240 assert_eq!(
3241 worktree
3242 .paths()
3243 .map(|p| p.to_string_lossy())
3244 .collect::<Vec<_>>(),
3245 ["a.txt", "b.txt", "d.txt", "f.txt"]
3246 );
3247 });
3248
3249 project_b
3250 .update(cx_b, |project, cx| {
3251 project.delete_entry(entry.id, false, cx).unwrap()
3252 })
3253 .await
3254 .unwrap();
3255
3256 worktree_a.read_with(cx_a, |worktree, _| {
3257 assert_eq!(
3258 worktree
3259 .paths()
3260 .map(|p| p.to_string_lossy())
3261 .collect::<Vec<_>>(),
3262 ["a.txt", "b.txt", "f.txt"]
3263 );
3264 });
3265
3266 worktree_b.read_with(cx_b, |worktree, _| {
3267 assert_eq!(
3268 worktree
3269 .paths()
3270 .map(|p| p.to_string_lossy())
3271 .collect::<Vec<_>>(),
3272 ["a.txt", "b.txt", "f.txt"]
3273 );
3274 });
3275}
3276
3277#[gpui::test(iterations = 10)]
3278async fn test_local_settings(
3279 executor: BackgroundExecutor,
3280 cx_a: &mut TestAppContext,
3281 cx_b: &mut TestAppContext,
3282) {
3283 let mut server = TestServer::start(executor.clone()).await;
3284 let client_a = server.create_client(cx_a, "user_a").await;
3285 let client_b = server.create_client(cx_b, "user_b").await;
3286 server
3287 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3288 .await;
3289 let active_call_a = cx_a.read(ActiveCall::global);
3290
3291 // As client A, open a project that contains some local settings files
3292 client_a
3293 .fs()
3294 .insert_tree(
3295 "/dir",
3296 json!({
3297 ".zed": {
3298 "settings.json": r#"{ "tab_size": 2 }"#
3299 },
3300 "a": {
3301 ".zed": {
3302 "settings.json": r#"{ "tab_size": 8 }"#
3303 },
3304 "a.txt": "a-contents",
3305 },
3306 "b": {
3307 "b.txt": "b-contents",
3308 }
3309 }),
3310 )
3311 .await;
3312 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
3313 executor.run_until_parked();
3314 let project_id = active_call_a
3315 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3316 .await
3317 .unwrap();
3318 executor.run_until_parked();
3319
3320 // As client B, join that project and observe the local settings.
3321 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
3322
3323 let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
3324 executor.run_until_parked();
3325 cx_b.read(|cx| {
3326 let store = cx.global::<SettingsStore>();
3327 assert_eq!(
3328 store
3329 .local_settings(worktree_b.read(cx).id().to_usize())
3330 .collect::<Vec<_>>(),
3331 &[
3332 (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
3333 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3334 ]
3335 )
3336 });
3337
3338 // As client A, update a settings file. As Client B, see the changed settings.
3339 client_a
3340 .fs()
3341 .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
3342 .await;
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#"{}"#.to_string()),
3352 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3353 ]
3354 )
3355 });
3356
3357 // As client A, create and remove some settings files. As client B, see the changed settings.
3358 client_a
3359 .fs()
3360 .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
3361 .await
3362 .unwrap();
3363 client_a
3364 .fs()
3365 .create_dir("/dir/b/.zed".as_ref())
3366 .await
3367 .unwrap();
3368 client_a
3369 .fs()
3370 .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
3371 .await;
3372 executor.run_until_parked();
3373 cx_b.read(|cx| {
3374 let store = cx.global::<SettingsStore>();
3375 assert_eq!(
3376 store
3377 .local_settings(worktree_b.read(cx).id().to_usize())
3378 .collect::<Vec<_>>(),
3379 &[
3380 (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
3381 (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
3382 ]
3383 )
3384 });
3385
3386 // As client B, disconnect.
3387 server.forbid_connections();
3388 server.disconnect_client(client_b.peer_id().unwrap());
3389
3390 // As client A, change and remove settings files while client B is disconnected.
3391 client_a
3392 .fs()
3393 .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
3394 .await;
3395 client_a
3396 .fs()
3397 .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
3398 .await
3399 .unwrap();
3400 executor.run_until_parked();
3401
3402 // As client B, reconnect and see the changed settings.
3403 server.allow_connections();
3404 executor.advance_clock(RECEIVE_TIMEOUT);
3405 cx_b.read(|cx| {
3406 let store = cx.global::<SettingsStore>();
3407 assert_eq!(
3408 store
3409 .local_settings(worktree_b.read(cx).id().to_usize())
3410 .collect::<Vec<_>>(),
3411 &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
3412 )
3413 });
3414}
3415
3416#[gpui::test(iterations = 10)]
3417async fn test_buffer_conflict_after_save(
3418 executor: BackgroundExecutor,
3419 cx_a: &mut TestAppContext,
3420 cx_b: &mut TestAppContext,
3421) {
3422 let mut server = TestServer::start(executor.clone()).await;
3423 let client_a = server.create_client(cx_a, "user_a").await;
3424 let client_b = server.create_client(cx_b, "user_b").await;
3425 server
3426 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3427 .await;
3428 let active_call_a = cx_a.read(ActiveCall::global);
3429
3430 client_a
3431 .fs()
3432 .insert_tree(
3433 "/dir",
3434 json!({
3435 "a.txt": "a-contents",
3436 }),
3437 )
3438 .await;
3439 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3440 let project_id = active_call_a
3441 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3442 .await
3443 .unwrap();
3444 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
3445
3446 // Open a buffer as client B
3447 let buffer_b = project_b
3448 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3449 .await
3450 .unwrap();
3451
3452 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
3453
3454 buffer_b.read_with(cx_b, |buf, _| {
3455 assert!(buf.is_dirty());
3456 assert!(!buf.has_conflict());
3457 });
3458
3459 project_b
3460 .update(cx_b, |project, cx| {
3461 project.save_buffer(buffer_b.clone(), cx)
3462 })
3463 .await
3464 .unwrap();
3465
3466 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
3467
3468 buffer_b.read_with(cx_b, |buf, _| {
3469 assert!(!buf.has_conflict());
3470 });
3471
3472 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
3473
3474 buffer_b.read_with(cx_b, |buf, _| {
3475 assert!(buf.is_dirty());
3476 assert!(!buf.has_conflict());
3477 });
3478}
3479
3480#[gpui::test(iterations = 10)]
3481async fn test_buffer_reloading(
3482 executor: BackgroundExecutor,
3483 cx_a: &mut TestAppContext,
3484 cx_b: &mut TestAppContext,
3485) {
3486 let mut server = TestServer::start(executor.clone()).await;
3487 let client_a = server.create_client(cx_a, "user_a").await;
3488 let client_b = server.create_client(cx_b, "user_b").await;
3489 server
3490 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3491 .await;
3492 let active_call_a = cx_a.read(ActiveCall::global);
3493
3494 client_a
3495 .fs()
3496 .insert_tree(
3497 "/dir",
3498 json!({
3499 "a.txt": "a\nb\nc",
3500 }),
3501 )
3502 .await;
3503 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3504 let project_id = active_call_a
3505 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3506 .await
3507 .unwrap();
3508 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
3509
3510 // Open a buffer as client B
3511 let buffer_b = project_b
3512 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3513 .await
3514 .unwrap();
3515
3516 buffer_b.read_with(cx_b, |buf, _| {
3517 assert!(!buf.is_dirty());
3518 assert!(!buf.has_conflict());
3519 assert_eq!(buf.line_ending(), LineEnding::Unix);
3520 });
3521
3522 let new_contents = Rope::from("d\ne\nf");
3523 client_a
3524 .fs()
3525 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
3526 .await
3527 .unwrap();
3528
3529 executor.run_until_parked();
3530
3531 buffer_b.read_with(cx_b, |buf, _| {
3532 assert_eq!(buf.text(), new_contents.to_string());
3533 assert!(!buf.is_dirty());
3534 assert!(!buf.has_conflict());
3535 assert_eq!(buf.line_ending(), LineEnding::Windows);
3536 });
3537}
3538
3539#[gpui::test(iterations = 10)]
3540async fn test_editing_while_guest_opens_buffer(
3541 executor: BackgroundExecutor,
3542 cx_a: &mut TestAppContext,
3543 cx_b: &mut TestAppContext,
3544) {
3545 let mut server = TestServer::start(executor.clone()).await;
3546 let client_a = server.create_client(cx_a, "user_a").await;
3547 let client_b = server.create_client(cx_b, "user_b").await;
3548 server
3549 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3550 .await;
3551 let active_call_a = cx_a.read(ActiveCall::global);
3552
3553 client_a
3554 .fs()
3555 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3556 .await;
3557 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3558 let project_id = active_call_a
3559 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3560 .await
3561 .unwrap();
3562 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
3563
3564 // Open a buffer as client A
3565 let buffer_a = project_a
3566 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3567 .await
3568 .unwrap();
3569
3570 // Start opening the same buffer as client B
3571 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3572 let buffer_b = cx_b.executor().spawn(open_buffer);
3573
3574 // Edit the buffer as client A while client B is still opening it.
3575 cx_b.executor().simulate_random_delay().await;
3576 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
3577 cx_b.executor().simulate_random_delay().await;
3578 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
3579
3580 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
3581 let buffer_b = buffer_b.await.unwrap();
3582 executor.run_until_parked();
3583
3584 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
3585}
3586
3587#[gpui::test(iterations = 10)]
3588async fn test_leaving_worktree_while_opening_buffer(
3589 executor: BackgroundExecutor,
3590 cx_a: &mut TestAppContext,
3591 cx_b: &mut TestAppContext,
3592) {
3593 let mut server = TestServer::start(executor.clone()).await;
3594 let client_a = server.create_client(cx_a, "user_a").await;
3595 let client_b = server.create_client(cx_b, "user_b").await;
3596 server
3597 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3598 .await;
3599 let active_call_a = cx_a.read(ActiveCall::global);
3600
3601 client_a
3602 .fs()
3603 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
3604 .await;
3605 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3606 let project_id = active_call_a
3607 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3608 .await
3609 .unwrap();
3610 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
3611
3612 // See that a guest has joined as client A.
3613 executor.run_until_parked();
3614
3615 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
3616
3617 // Begin opening a buffer as client B, but leave the project before the open completes.
3618 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx));
3619 let buffer_b = cx_b.executor().spawn(open_buffer);
3620 cx_b.update(|_| drop(project_b));
3621 drop(buffer_b);
3622
3623 // See that the guest has left.
3624 executor.run_until_parked();
3625
3626 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
3627}
3628
3629#[gpui::test(iterations = 10)]
3630async fn test_canceling_buffer_opening(
3631 executor: BackgroundExecutor,
3632 cx_a: &mut TestAppContext,
3633 cx_b: &mut TestAppContext,
3634) {
3635 let mut server = TestServer::start(executor.clone()).await;
3636 let client_a = server.create_client(cx_a, "user_a").await;
3637 let client_b = server.create_client(cx_b, "user_b").await;
3638 server
3639 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3640 .await;
3641 let active_call_a = cx_a.read(ActiveCall::global);
3642
3643 client_a
3644 .fs()
3645 .insert_tree(
3646 "/dir",
3647 json!({
3648 "a.txt": "abc",
3649 }),
3650 )
3651 .await;
3652 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3653 let project_id = active_call_a
3654 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3655 .await
3656 .unwrap();
3657 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
3658
3659 let buffer_a = project_a
3660 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
3661 .await
3662 .unwrap();
3663
3664 // Open a buffer as client B but cancel after a random amount of time.
3665 let buffer_b = project_b.update(cx_b, |p, cx| {
3666 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3667 });
3668 executor.simulate_random_delay().await;
3669 drop(buffer_b);
3670
3671 // Try opening the same buffer again as client B, and ensure we can
3672 // still do it despite the cancellation above.
3673 let buffer_b = project_b
3674 .update(cx_b, |p, cx| {
3675 p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
3676 })
3677 .await
3678 .unwrap();
3679
3680 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
3681}
3682
3683#[gpui::test(iterations = 10)]
3684async fn test_leaving_project(
3685 executor: BackgroundExecutor,
3686 cx_a: &mut TestAppContext,
3687 cx_b: &mut TestAppContext,
3688 cx_c: &mut TestAppContext,
3689) {
3690 let mut server = TestServer::start(executor.clone()).await;
3691 let client_a = server.create_client(cx_a, "user_a").await;
3692 let client_b = server.create_client(cx_b, "user_b").await;
3693 let client_c = server.create_client(cx_c, "user_c").await;
3694 server
3695 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3696 .await;
3697 let active_call_a = cx_a.read(ActiveCall::global);
3698
3699 client_a
3700 .fs()
3701 .insert_tree(
3702 "/a",
3703 json!({
3704 "a.txt": "a-contents",
3705 "b.txt": "b-contents",
3706 }),
3707 )
3708 .await;
3709 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3710 let project_id = active_call_a
3711 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3712 .await
3713 .unwrap();
3714 let project_b1 = client_b.build_dev_server_project(project_id, cx_b).await;
3715 let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
3716
3717 // Client A sees that a guest has joined.
3718 executor.run_until_parked();
3719
3720 project_a.read_with(cx_a, |project, _| {
3721 assert_eq!(project.collaborators().len(), 2);
3722 });
3723
3724 project_b1.read_with(cx_b, |project, _| {
3725 assert_eq!(project.collaborators().len(), 2);
3726 });
3727
3728 project_c.read_with(cx_c, |project, _| {
3729 assert_eq!(project.collaborators().len(), 2);
3730 });
3731
3732 // Client B opens a buffer.
3733 let buffer_b1 = project_b1
3734 .update(cx_b, |project, cx| {
3735 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
3736 project.open_buffer((worktree_id, "a.txt"), cx)
3737 })
3738 .await
3739 .unwrap();
3740
3741 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3742
3743 // Drop client B's project and ensure client A and client C observe client B leaving.
3744 cx_b.update(|_| drop(project_b1));
3745 executor.run_until_parked();
3746
3747 project_a.read_with(cx_a, |project, _| {
3748 assert_eq!(project.collaborators().len(), 1);
3749 });
3750
3751 project_c.read_with(cx_c, |project, _| {
3752 assert_eq!(project.collaborators().len(), 1);
3753 });
3754
3755 // Client B re-joins the project and can open buffers as before.
3756 let project_b2 = client_b.build_dev_server_project(project_id, cx_b).await;
3757 executor.run_until_parked();
3758
3759 project_a.read_with(cx_a, |project, _| {
3760 assert_eq!(project.collaborators().len(), 2);
3761 });
3762
3763 project_b2.read_with(cx_b, |project, _| {
3764 assert_eq!(project.collaborators().len(), 2);
3765 });
3766
3767 project_c.read_with(cx_c, |project, _| {
3768 assert_eq!(project.collaborators().len(), 2);
3769 });
3770
3771 let buffer_b2 = project_b2
3772 .update(cx_b, |project, cx| {
3773 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
3774 project.open_buffer((worktree_id, "a.txt"), cx)
3775 })
3776 .await
3777 .unwrap();
3778
3779 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
3780
3781 project_a.read_with(cx_a, |project, _| {
3782 assert_eq!(project.collaborators().len(), 2);
3783 });
3784
3785 // Drop client B's connection and ensure client A and client C observe client B leaving.
3786 client_b.disconnect(&cx_b.to_async());
3787 executor.advance_clock(RECONNECT_TIMEOUT);
3788
3789 project_a.read_with(cx_a, |project, _| {
3790 assert_eq!(project.collaborators().len(), 1);
3791 });
3792
3793 project_b2.read_with(cx_b, |project, _| {
3794 assert!(project.is_disconnected());
3795 });
3796
3797 project_c.read_with(cx_c, |project, _| {
3798 assert_eq!(project.collaborators().len(), 1);
3799 });
3800
3801 // Client B can't join the project, unless they re-join the room.
3802 cx_b.spawn(|cx| {
3803 Project::in_room(
3804 project_id,
3805 client_b.app_state.client.clone(),
3806 client_b.user_store().clone(),
3807 client_b.language_registry().clone(),
3808 FakeFs::new(cx.background_executor().clone()),
3809 cx,
3810 )
3811 })
3812 .await
3813 .unwrap_err();
3814
3815 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
3816 client_c.wait_for_current_user(cx_c).await;
3817 server.forbid_connections();
3818 server.disconnect_client(client_c.peer_id().unwrap());
3819 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
3820 executor.run_until_parked();
3821
3822 project_a.read_with(cx_a, |project, _| {
3823 assert_eq!(project.collaborators().len(), 0);
3824 });
3825
3826 project_b2.read_with(cx_b, |project, _| {
3827 assert!(project.is_disconnected());
3828 });
3829
3830 project_c.read_with(cx_c, |project, _| {
3831 assert!(project.is_disconnected());
3832 });
3833}
3834
3835#[gpui::test(iterations = 10)]
3836async fn test_collaborating_with_diagnostics(
3837 executor: BackgroundExecutor,
3838 cx_a: &mut TestAppContext,
3839 cx_b: &mut TestAppContext,
3840 cx_c: &mut TestAppContext,
3841) {
3842 let mut server = TestServer::start(executor.clone()).await;
3843 let client_a = server.create_client(cx_a, "user_a").await;
3844 let client_b = server.create_client(cx_b, "user_b").await;
3845 let client_c = server.create_client(cx_c, "user_c").await;
3846 server
3847 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3848 .await;
3849 let active_call_a = cx_a.read(ActiveCall::global);
3850
3851 client_a.language_registry().add(Arc::new(Language::new(
3852 LanguageConfig {
3853 name: "Rust".into(),
3854 matcher: LanguageMatcher {
3855 path_suffixes: vec!["rs".to_string()],
3856 ..Default::default()
3857 },
3858 ..Default::default()
3859 },
3860 Some(tree_sitter_rust::language()),
3861 )));
3862 let mut fake_language_servers = client_a
3863 .language_registry()
3864 .register_fake_lsp_adapter("Rust", Default::default());
3865
3866 // Share a project as client A
3867 client_a
3868 .fs()
3869 .insert_tree(
3870 "/a",
3871 json!({
3872 "a.rs": "let one = two",
3873 "other.rs": "",
3874 }),
3875 )
3876 .await;
3877 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3878
3879 // Cause the language server to start.
3880 let _buffer = project_a
3881 .update(cx_a, |project, cx| {
3882 project.open_buffer(
3883 ProjectPath {
3884 worktree_id,
3885 path: Path::new("other.rs").into(),
3886 },
3887 cx,
3888 )
3889 })
3890 .await
3891 .unwrap();
3892
3893 // Simulate a language server reporting errors for a file.
3894 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3895 fake_language_server
3896 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3897 .await;
3898 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3899 lsp::PublishDiagnosticsParams {
3900 uri: lsp::Uri::from_file_path("/a/a.rs").unwrap().into(),
3901 version: None,
3902 diagnostics: vec![lsp::Diagnostic {
3903 severity: Some(lsp::DiagnosticSeverity::WARNING),
3904 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3905 message: "message 0".to_string(),
3906 ..Default::default()
3907 }],
3908 },
3909 );
3910
3911 // Client A shares the project and, simultaneously, the language server
3912 // publishes a diagnostic. This is done to ensure that the server always
3913 // observes the latest diagnostics for a worktree.
3914 let project_id = active_call_a
3915 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3916 .await
3917 .unwrap();
3918 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3919 lsp::PublishDiagnosticsParams {
3920 uri: lsp::Uri::from_file_path("/a/a.rs").unwrap().into(),
3921 version: None,
3922 diagnostics: vec![lsp::Diagnostic {
3923 severity: Some(lsp::DiagnosticSeverity::ERROR),
3924 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3925 message: "message 1".to_string(),
3926 ..Default::default()
3927 }],
3928 },
3929 );
3930
3931 // Join the worktree as client B.
3932 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
3933
3934 // Wait for server to see the diagnostics update.
3935 executor.run_until_parked();
3936
3937 // Ensure client B observes the new diagnostics.
3938
3939 project_b.read_with(cx_b, |project, cx| {
3940 assert_eq!(
3941 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
3942 &[(
3943 ProjectPath {
3944 worktree_id,
3945 path: Arc::from(Path::new("a.rs")),
3946 },
3947 LanguageServerId(0),
3948 DiagnosticSummary {
3949 error_count: 1,
3950 warning_count: 0,
3951 },
3952 )]
3953 )
3954 });
3955
3956 // Join project as client C and observe the diagnostics.
3957 let project_c = client_c.build_dev_server_project(project_id, cx_c).await;
3958 executor.run_until_parked();
3959 let project_c_diagnostic_summaries =
3960 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3961 project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
3962 })));
3963 project_c.update(cx_c, |_, cx| {
3964 let summaries = project_c_diagnostic_summaries.clone();
3965 cx.subscribe(&project_c, {
3966 move |p, _, event, cx| {
3967 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3968 *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
3969 }
3970 }
3971 })
3972 .detach();
3973 });
3974
3975 executor.run_until_parked();
3976 assert_eq!(
3977 project_c_diagnostic_summaries.borrow().as_slice(),
3978 &[(
3979 ProjectPath {
3980 worktree_id,
3981 path: Arc::from(Path::new("a.rs")),
3982 },
3983 LanguageServerId(0),
3984 DiagnosticSummary {
3985 error_count: 1,
3986 warning_count: 0,
3987 },
3988 )]
3989 );
3990
3991 // Simulate a language server reporting more errors for a file.
3992 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3993 lsp::PublishDiagnosticsParams {
3994 uri: lsp::Uri::from_file_path("/a/a.rs").unwrap().into(),
3995 version: None,
3996 diagnostics: vec![
3997 lsp::Diagnostic {
3998 severity: Some(lsp::DiagnosticSeverity::ERROR),
3999 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
4000 message: "message 1".to_string(),
4001 ..Default::default()
4002 },
4003 lsp::Diagnostic {
4004 severity: Some(lsp::DiagnosticSeverity::WARNING),
4005 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
4006 message: "message 2".to_string(),
4007 ..Default::default()
4008 },
4009 ],
4010 },
4011 );
4012
4013 // Clients B and C get the updated summaries
4014 executor.run_until_parked();
4015
4016 project_b.read_with(cx_b, |project, cx| {
4017 assert_eq!(
4018 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4019 [(
4020 ProjectPath {
4021 worktree_id,
4022 path: Arc::from(Path::new("a.rs")),
4023 },
4024 LanguageServerId(0),
4025 DiagnosticSummary {
4026 error_count: 1,
4027 warning_count: 1,
4028 },
4029 )]
4030 );
4031 });
4032
4033 project_c.read_with(cx_c, |project, cx| {
4034 assert_eq!(
4035 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4036 [(
4037 ProjectPath {
4038 worktree_id,
4039 path: Arc::from(Path::new("a.rs")),
4040 },
4041 LanguageServerId(0),
4042 DiagnosticSummary {
4043 error_count: 1,
4044 warning_count: 1,
4045 },
4046 )]
4047 );
4048 });
4049
4050 // Open the file with the errors on client B. They should be present.
4051 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4052 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4053
4054 buffer_b.read_with(cx_b, |buffer, _| {
4055 assert_eq!(
4056 buffer
4057 .snapshot()
4058 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
4059 .collect::<Vec<_>>(),
4060 &[
4061 DiagnosticEntry {
4062 range: Point::new(0, 4)..Point::new(0, 7),
4063 diagnostic: Diagnostic {
4064 group_id: 2,
4065 message: "message 1".to_string(),
4066 severity: lsp::DiagnosticSeverity::ERROR,
4067 is_primary: true,
4068 ..Default::default()
4069 }
4070 },
4071 DiagnosticEntry {
4072 range: Point::new(0, 10)..Point::new(0, 13),
4073 diagnostic: Diagnostic {
4074 group_id: 3,
4075 severity: lsp::DiagnosticSeverity::WARNING,
4076 message: "message 2".to_string(),
4077 is_primary: true,
4078 ..Default::default()
4079 }
4080 }
4081 ]
4082 );
4083 });
4084
4085 // Simulate a language server reporting no errors for a file.
4086 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4087 lsp::PublishDiagnosticsParams {
4088 uri: lsp::Uri::from_file_path("/a/a.rs").unwrap().into(),
4089 version: None,
4090 diagnostics: vec![],
4091 },
4092 );
4093 executor.run_until_parked();
4094
4095 project_a.read_with(cx_a, |project, cx| {
4096 assert_eq!(
4097 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4098 []
4099 )
4100 });
4101
4102 project_b.read_with(cx_b, |project, cx| {
4103 assert_eq!(
4104 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4105 []
4106 )
4107 });
4108
4109 project_c.read_with(cx_c, |project, cx| {
4110 assert_eq!(
4111 project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
4112 []
4113 )
4114 });
4115}
4116
4117#[gpui::test(iterations = 10)]
4118async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
4119 executor: BackgroundExecutor,
4120 cx_a: &mut TestAppContext,
4121 cx_b: &mut TestAppContext,
4122) {
4123 let mut server = TestServer::start(executor.clone()).await;
4124 let client_a = server.create_client(cx_a, "user_a").await;
4125 let client_b = server.create_client(cx_b, "user_b").await;
4126 server
4127 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4128 .await;
4129
4130 client_a.language_registry().add(rust_lang());
4131 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
4132 "Rust",
4133 FakeLspAdapter {
4134 disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
4135 disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
4136 ..Default::default()
4137 },
4138 );
4139
4140 let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
4141 client_a
4142 .fs()
4143 .insert_tree(
4144 "/test",
4145 json!({
4146 "one.rs": "const ONE: usize = 1;",
4147 "two.rs": "const TWO: usize = 2;",
4148 "three.rs": "const THREE: usize = 3;",
4149 "four.rs": "const FOUR: usize = 3;",
4150 "five.rs": "const FIVE: usize = 3;",
4151 }),
4152 )
4153 .await;
4154
4155 let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
4156
4157 // Share a project as client A
4158 let active_call_a = cx_a.read(ActiveCall::global);
4159 let project_id = active_call_a
4160 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4161 .await
4162 .unwrap();
4163
4164 // Join the project as client B and open all three files.
4165 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
4166 let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
4167 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
4168 }))
4169 .await
4170 .unwrap();
4171
4172 // Simulate a language server reporting errors for a file.
4173 let fake_language_server = fake_language_servers.next().await.unwrap();
4174 fake_language_server
4175 .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
4176 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4177 })
4178 .await
4179 .unwrap();
4180 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4181 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4182 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
4183 lsp::WorkDoneProgressBegin {
4184 title: "Progress Began".into(),
4185 ..Default::default()
4186 },
4187 )),
4188 });
4189 for file_name in file_names {
4190 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
4191 lsp::PublishDiagnosticsParams {
4192 uri: lsp::Uri::from_file_path(Path::new("/test").join(file_name))
4193 .unwrap()
4194 .into(),
4195 version: None,
4196 diagnostics: vec![lsp::Diagnostic {
4197 severity: Some(lsp::DiagnosticSeverity::WARNING),
4198 source: Some("the-disk-based-diagnostics-source".into()),
4199 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
4200 message: "message one".to_string(),
4201 ..Default::default()
4202 }],
4203 },
4204 );
4205 }
4206 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4207 token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
4208 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
4209 lsp::WorkDoneProgressEnd { message: None },
4210 )),
4211 });
4212
4213 // When the "disk base diagnostics finished" message is received, the buffers'
4214 // diagnostics are expected to be present.
4215 let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
4216 project_b.update(cx_b, {
4217 let project_b = project_b.clone();
4218 let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
4219 move |_, cx| {
4220 cx.subscribe(&project_b, move |_, _, event, cx| {
4221 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
4222 disk_based_diagnostics_finished.store(true, SeqCst);
4223 for buffer in &guest_buffers {
4224 assert_eq!(
4225 buffer
4226 .read(cx)
4227 .snapshot()
4228 .diagnostics_in_range::<_, usize>(0..5, false)
4229 .count(),
4230 1,
4231 "expected a diagnostic for buffer {:?}",
4232 buffer.read(cx).file().unwrap().path(),
4233 );
4234 }
4235 }
4236 })
4237 .detach();
4238 }
4239 });
4240
4241 executor.run_until_parked();
4242 assert!(disk_based_diagnostics_finished.load(SeqCst));
4243}
4244
4245#[gpui::test(iterations = 10)]
4246async fn test_reloading_buffer_manually(
4247 executor: BackgroundExecutor,
4248 cx_a: &mut TestAppContext,
4249 cx_b: &mut TestAppContext,
4250) {
4251 let mut server = TestServer::start(executor.clone()).await;
4252 let client_a = server.create_client(cx_a, "user_a").await;
4253 let client_b = server.create_client(cx_b, "user_b").await;
4254 server
4255 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4256 .await;
4257 let active_call_a = cx_a.read(ActiveCall::global);
4258
4259 client_a
4260 .fs()
4261 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
4262 .await;
4263 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4264 let buffer_a = project_a
4265 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
4266 .await
4267 .unwrap();
4268 let project_id = active_call_a
4269 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4270 .await
4271 .unwrap();
4272
4273 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
4274
4275 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4276 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4277 buffer_b.update(cx_b, |buffer, cx| {
4278 buffer.edit([(4..7, "six")], None, cx);
4279 buffer.edit([(10..11, "6")], None, cx);
4280 assert_eq!(buffer.text(), "let six = 6;");
4281 assert!(buffer.is_dirty());
4282 assert!(!buffer.has_conflict());
4283 });
4284 executor.run_until_parked();
4285
4286 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
4287
4288 client_a
4289 .fs()
4290 .save(
4291 "/a/a.rs".as_ref(),
4292 &Rope::from("let seven = 7;"),
4293 LineEnding::Unix,
4294 )
4295 .await
4296 .unwrap();
4297 executor.run_until_parked();
4298
4299 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
4300
4301 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
4302
4303 project_b
4304 .update(cx_b, |project, cx| {
4305 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
4306 })
4307 .await
4308 .unwrap();
4309
4310 buffer_a.read_with(cx_a, |buffer, _| {
4311 assert_eq!(buffer.text(), "let seven = 7;");
4312 assert!(!buffer.is_dirty());
4313 assert!(!buffer.has_conflict());
4314 });
4315
4316 buffer_b.read_with(cx_b, |buffer, _| {
4317 assert_eq!(buffer.text(), "let seven = 7;");
4318 assert!(!buffer.is_dirty());
4319 assert!(!buffer.has_conflict());
4320 });
4321
4322 buffer_a.update(cx_a, |buffer, cx| {
4323 // Undoing on the host is a no-op when the reload was initiated by the guest.
4324 buffer.undo(cx);
4325 assert_eq!(buffer.text(), "let seven = 7;");
4326 assert!(!buffer.is_dirty());
4327 assert!(!buffer.has_conflict());
4328 });
4329 buffer_b.update(cx_b, |buffer, cx| {
4330 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
4331 buffer.undo(cx);
4332 assert_eq!(buffer.text(), "let six = 6;");
4333 assert!(buffer.is_dirty());
4334 assert!(!buffer.has_conflict());
4335 });
4336}
4337
4338#[gpui::test(iterations = 10)]
4339async fn test_formatting_buffer(
4340 executor: BackgroundExecutor,
4341 cx_a: &mut TestAppContext,
4342 cx_b: &mut TestAppContext,
4343) {
4344 executor.allow_parking();
4345 let mut server = TestServer::start(executor.clone()).await;
4346 let client_a = server.create_client(cx_a, "user_a").await;
4347 let client_b = server.create_client(cx_b, "user_b").await;
4348 server
4349 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4350 .await;
4351 let active_call_a = cx_a.read(ActiveCall::global);
4352
4353 client_a.language_registry().add(rust_lang());
4354 let mut fake_language_servers = client_a
4355 .language_registry()
4356 .register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
4357
4358 // Here we insert a fake tree with a directory that exists on disk. This is needed
4359 // because later we'll invoke a command, which requires passing a working directory
4360 // that points to a valid location on disk.
4361 let directory = env::current_dir().unwrap();
4362 client_a
4363 .fs()
4364 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
4365 .await;
4366 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4367 let project_id = active_call_a
4368 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4369 .await
4370 .unwrap();
4371 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
4372
4373 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4374 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4375
4376 let fake_language_server = fake_language_servers.next().await.unwrap();
4377 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4378 Ok(Some(vec![
4379 lsp::TextEdit {
4380 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
4381 new_text: "h".to_string(),
4382 },
4383 lsp::TextEdit {
4384 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
4385 new_text: "y".to_string(),
4386 },
4387 ]))
4388 });
4389
4390 project_b
4391 .update(cx_b, |project, cx| {
4392 project.format(
4393 HashSet::from_iter([buffer_b.clone()]),
4394 true,
4395 FormatTrigger::Save,
4396 cx,
4397 )
4398 })
4399 .await
4400 .unwrap();
4401
4402 // The edits from the LSP are applied, and a final newline is added.
4403 assert_eq!(
4404 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4405 "let honey = \"two\"\n"
4406 );
4407
4408 // Ensure buffer can be formatted using an external command. Notice how the
4409 // host's configuration is honored as opposed to using the guest's settings.
4410 cx_a.update(|cx| {
4411 SettingsStore::update_global(cx, |store, cx| {
4412 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4413 file.defaults.formatter = Some(Formatter::External {
4414 command: "awk".into(),
4415 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
4416 });
4417 });
4418 });
4419 });
4420 project_b
4421 .update(cx_b, |project, cx| {
4422 project.format(
4423 HashSet::from_iter([buffer_b.clone()]),
4424 true,
4425 FormatTrigger::Save,
4426 cx,
4427 )
4428 })
4429 .await
4430 .unwrap();
4431 assert_eq!(
4432 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4433 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
4434 );
4435}
4436
4437#[gpui::test(iterations = 10)]
4438async fn test_prettier_formatting_buffer(
4439 executor: BackgroundExecutor,
4440 cx_a: &mut TestAppContext,
4441 cx_b: &mut TestAppContext,
4442) {
4443 let mut server = TestServer::start(executor.clone()).await;
4444 let client_a = server.create_client(cx_a, "user_a").await;
4445 let client_b = server.create_client(cx_b, "user_b").await;
4446 server
4447 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4448 .await;
4449 let active_call_a = cx_a.read(ActiveCall::global);
4450
4451 let test_plugin = "test_plugin";
4452
4453 client_a.language_registry().add(Arc::new(Language::new(
4454 LanguageConfig {
4455 name: "TypeScript".into(),
4456 matcher: LanguageMatcher {
4457 path_suffixes: vec!["ts".to_string()],
4458 ..Default::default()
4459 },
4460 ..Default::default()
4461 },
4462 Some(tree_sitter_rust::language()),
4463 )));
4464 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
4465 "TypeScript",
4466 FakeLspAdapter {
4467 prettier_plugins: vec![test_plugin],
4468 ..Default::default()
4469 },
4470 );
4471
4472 // Here we insert a fake tree with a directory that exists on disk. This is needed
4473 // because later we'll invoke a command, which requires passing a working directory
4474 // that points to a valid location on disk.
4475 let directory = env::current_dir().unwrap();
4476 let buffer_text = "let one = \"two\"";
4477 client_a
4478 .fs()
4479 .insert_tree(&directory, json!({ "a.ts": buffer_text }))
4480 .await;
4481 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
4482 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
4483 let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
4484 let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
4485
4486 let project_id = active_call_a
4487 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4488 .await
4489 .unwrap();
4490 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
4491 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
4492 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4493
4494 cx_a.update(|cx| {
4495 SettingsStore::update_global(cx, |store, cx| {
4496 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4497 file.defaults.formatter = Some(Formatter::Auto);
4498 file.defaults.prettier = Some(PrettierSettings {
4499 allowed: true,
4500 ..PrettierSettings::default()
4501 });
4502 });
4503 });
4504 });
4505 cx_b.update(|cx| {
4506 SettingsStore::update_global(cx, |store, cx| {
4507 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
4508 file.defaults.formatter = Some(Formatter::LanguageServer);
4509 file.defaults.prettier = Some(PrettierSettings {
4510 allowed: true,
4511 ..PrettierSettings::default()
4512 });
4513 });
4514 });
4515 });
4516 let fake_language_server = fake_language_servers.next().await.unwrap();
4517 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
4518 panic!(
4519 "Unexpected: prettier should be preferred since it's enabled and language supports it"
4520 )
4521 });
4522
4523 project_b
4524 .update(cx_b, |project, cx| {
4525 project.format(
4526 HashSet::from_iter([buffer_b.clone()]),
4527 true,
4528 FormatTrigger::Save,
4529 cx,
4530 )
4531 })
4532 .await
4533 .unwrap();
4534
4535 executor.run_until_parked();
4536 assert_eq!(
4537 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4538 buffer_text.to_string() + "\n" + prettier_format_suffix,
4539 "Prettier formatting was not applied to client buffer after client's request"
4540 );
4541
4542 project_a
4543 .update(cx_a, |project, cx| {
4544 project.format(
4545 HashSet::from_iter([buffer_a.clone()]),
4546 true,
4547 FormatTrigger::Manual,
4548 cx,
4549 )
4550 })
4551 .await
4552 .unwrap();
4553
4554 executor.run_until_parked();
4555 assert_eq!(
4556 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
4557 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
4558 "Prettier formatting was not applied to client buffer after host's request"
4559 );
4560}
4561
4562#[gpui::test(iterations = 10)]
4563async fn test_definition(
4564 executor: BackgroundExecutor,
4565 cx_a: &mut TestAppContext,
4566 cx_b: &mut TestAppContext,
4567) {
4568 let mut server = TestServer::start(executor.clone()).await;
4569 let client_a = server.create_client(cx_a, "user_a").await;
4570 let client_b = server.create_client(cx_b, "user_b").await;
4571 server
4572 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4573 .await;
4574 let active_call_a = cx_a.read(ActiveCall::global);
4575
4576 let mut fake_language_servers = client_a
4577 .language_registry()
4578 .register_fake_lsp_adapter("Rust", Default::default());
4579 client_a.language_registry().add(rust_lang());
4580
4581 client_a
4582 .fs()
4583 .insert_tree(
4584 "/root",
4585 json!({
4586 "dir-1": {
4587 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
4588 },
4589 "dir-2": {
4590 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
4591 "c.rs": "type T2 = usize;",
4592 }
4593 }),
4594 )
4595 .await;
4596 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4597 let project_id = active_call_a
4598 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4599 .await
4600 .unwrap();
4601 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
4602
4603 // Open the file on client B.
4604 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
4605 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4606
4607 // Request the definition of a symbol as the guest.
4608 let fake_language_server = fake_language_servers.next().await.unwrap();
4609 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4610 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4611 lsp::Location::new(
4612 lsp::Uri::from_file_path("/root/dir-2/b.rs").unwrap().into(),
4613 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4614 ),
4615 )))
4616 });
4617
4618 let definitions_1 = project_b
4619 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
4620 .await
4621 .unwrap();
4622 cx_b.read(|cx| {
4623 assert_eq!(definitions_1.len(), 1);
4624 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4625 let target_buffer = definitions_1[0].target.buffer.read(cx);
4626 assert_eq!(
4627 target_buffer.text(),
4628 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4629 );
4630 assert_eq!(
4631 definitions_1[0].target.range.to_point(target_buffer),
4632 Point::new(0, 6)..Point::new(0, 9)
4633 );
4634 });
4635
4636 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
4637 // the previous call to `definition`.
4638 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4639 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4640 lsp::Location::new(
4641 lsp::Uri::from_file_path("/root/dir-2/b.rs").unwrap().into(),
4642 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
4643 ),
4644 )))
4645 });
4646
4647 let definitions_2 = project_b
4648 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
4649 .await
4650 .unwrap();
4651 cx_b.read(|cx| {
4652 assert_eq!(definitions_2.len(), 1);
4653 assert_eq!(project_b.read(cx).worktrees().count(), 2);
4654 let target_buffer = definitions_2[0].target.buffer.read(cx);
4655 assert_eq!(
4656 target_buffer.text(),
4657 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
4658 );
4659 assert_eq!(
4660 definitions_2[0].target.range.to_point(target_buffer),
4661 Point::new(1, 6)..Point::new(1, 11)
4662 );
4663 });
4664 assert_eq!(
4665 definitions_1[0].target.buffer,
4666 definitions_2[0].target.buffer
4667 );
4668
4669 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
4670 |req, _| async move {
4671 assert_eq!(
4672 req.text_document_position_params.position,
4673 lsp::Position::new(0, 7)
4674 );
4675 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4676 lsp::Location::new(
4677 lsp::Uri::from_file_path("/root/dir-2/c.rs").unwrap().into(),
4678 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
4679 ),
4680 )))
4681 },
4682 );
4683
4684 let type_definitions = project_b
4685 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
4686 .await
4687 .unwrap();
4688 cx_b.read(|cx| {
4689 assert_eq!(type_definitions.len(), 1);
4690 let target_buffer = type_definitions[0].target.buffer.read(cx);
4691 assert_eq!(target_buffer.text(), "type T2 = usize;");
4692 assert_eq!(
4693 type_definitions[0].target.range.to_point(target_buffer),
4694 Point::new(0, 5)..Point::new(0, 7)
4695 );
4696 });
4697}
4698
4699#[gpui::test(iterations = 10)]
4700async fn test_references(
4701 executor: BackgroundExecutor,
4702 cx_a: &mut TestAppContext,
4703 cx_b: &mut TestAppContext,
4704) {
4705 let mut server = TestServer::start(executor.clone()).await;
4706 let client_a = server.create_client(cx_a, "user_a").await;
4707 let client_b = server.create_client(cx_b, "user_b").await;
4708 server
4709 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4710 .await;
4711 let active_call_a = cx_a.read(ActiveCall::global);
4712
4713 client_a.language_registry().add(rust_lang());
4714 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
4715 "Rust",
4716 FakeLspAdapter {
4717 name: "my-fake-lsp-adapter",
4718 capabilities: lsp::ServerCapabilities {
4719 references_provider: Some(lsp::OneOf::Left(true)),
4720 ..Default::default()
4721 },
4722 ..Default::default()
4723 },
4724 );
4725
4726 client_a
4727 .fs()
4728 .insert_tree(
4729 "/root",
4730 json!({
4731 "dir-1": {
4732 "one.rs": "const ONE: usize = 1;",
4733 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4734 },
4735 "dir-2": {
4736 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
4737 }
4738 }),
4739 )
4740 .await;
4741 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
4742 let project_id = active_call_a
4743 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4744 .await
4745 .unwrap();
4746 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
4747
4748 // Open the file on client B.
4749 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
4750 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
4751
4752 // Request references to a symbol as the guest.
4753 let fake_language_server = fake_language_servers.next().await.unwrap();
4754 let (lsp_response_tx, rx) = mpsc::unbounded::<Result<Option<Vec<lsp::Location>>>>();
4755 fake_language_server.handle_request::<lsp::request::References, _, _>({
4756 let rx = Arc::new(Mutex::new(Some(rx)));
4757 move |params, _| {
4758 assert_eq!(
4759 params.text_document_position.text_document.uri.as_str(),
4760 "file:///root/dir-1/one.rs"
4761 );
4762 let rx = rx.clone();
4763 async move {
4764 let mut response_rx = rx.lock().take().unwrap();
4765 let result = response_rx.next().await.unwrap();
4766 *rx.lock() = Some(response_rx);
4767 result
4768 }
4769 }
4770 });
4771
4772 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4773
4774 // User is informed that a request is pending.
4775 executor.run_until_parked();
4776 project_b.read_with(cx_b, |project, _| {
4777 let status = project.language_server_statuses().next().cloned().unwrap();
4778 assert_eq!(status.name, "my-fake-lsp-adapter");
4779 assert_eq!(
4780 status.pending_work.values().next().unwrap().message,
4781 Some("Finding references...".into())
4782 );
4783 });
4784
4785 // Cause the language server to respond.
4786 lsp_response_tx
4787 .unbounded_send(Ok(Some(vec![
4788 lsp::Location {
4789 uri: lsp::Uri::from_file_path("/root/dir-1/two.rs")
4790 .unwrap()
4791 .into(),
4792 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
4793 },
4794 lsp::Location {
4795 uri: lsp::Uri::from_file_path("/root/dir-1/two.rs")
4796 .unwrap()
4797 .into(),
4798 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
4799 },
4800 lsp::Location {
4801 uri: lsp::Uri::from_file_path("/root/dir-2/three.rs")
4802 .unwrap()
4803 .into(),
4804 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
4805 },
4806 ])))
4807 .unwrap();
4808
4809 let references = references.await.unwrap();
4810 executor.run_until_parked();
4811 project_b.read_with(cx_b, |project, cx| {
4812 // User is informed that a request is no longer pending.
4813 let status = project.language_server_statuses().next().unwrap();
4814 assert!(status.pending_work.is_empty());
4815
4816 assert_eq!(references.len(), 3);
4817 assert_eq!(project.worktrees().count(), 2);
4818
4819 let two_buffer = references[0].buffer.read(cx);
4820 let three_buffer = references[2].buffer.read(cx);
4821 assert_eq!(
4822 two_buffer.file().unwrap().path().as_ref(),
4823 Path::new("two.rs")
4824 );
4825 assert_eq!(references[1].buffer, references[0].buffer);
4826 assert_eq!(
4827 three_buffer.file().unwrap().full_path(cx),
4828 Path::new("/root/dir-2/three.rs")
4829 );
4830
4831 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
4832 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
4833 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
4834 });
4835
4836 let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx));
4837
4838 // User is informed that a request is pending.
4839 executor.run_until_parked();
4840 project_b.read_with(cx_b, |project, _| {
4841 let status = project.language_server_statuses().next().cloned().unwrap();
4842 assert_eq!(status.name, "my-fake-lsp-adapter");
4843 assert_eq!(
4844 status.pending_work.values().next().unwrap().message,
4845 Some("Finding references...".into())
4846 );
4847 });
4848
4849 // Cause the LSP request to fail.
4850 lsp_response_tx
4851 .unbounded_send(Err(anyhow!("can't find references")))
4852 .unwrap();
4853 references.await.unwrap_err();
4854
4855 // User is informed that the request is no longer pending.
4856 executor.run_until_parked();
4857 project_b.read_with(cx_b, |project, _| {
4858 let status = project.language_server_statuses().next().unwrap();
4859 assert!(status.pending_work.is_empty());
4860 });
4861}
4862
4863#[gpui::test(iterations = 10)]
4864async fn test_project_search(
4865 executor: BackgroundExecutor,
4866 cx_a: &mut TestAppContext,
4867 cx_b: &mut TestAppContext,
4868) {
4869 let mut server = TestServer::start(executor.clone()).await;
4870 let client_a = server.create_client(cx_a, "user_a").await;
4871 let client_b = server.create_client(cx_b, "user_b").await;
4872 server
4873 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4874 .await;
4875 let active_call_a = cx_a.read(ActiveCall::global);
4876
4877 client_a
4878 .fs()
4879 .insert_tree(
4880 "/root",
4881 json!({
4882 "dir-1": {
4883 "a": "hello world",
4884 "b": "goodnight moon",
4885 "c": "a world of goo",
4886 "d": "world champion of clown world",
4887 },
4888 "dir-2": {
4889 "e": "disney world is fun",
4890 }
4891 }),
4892 )
4893 .await;
4894 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
4895 let (worktree_2, _) = project_a
4896 .update(cx_a, |p, cx| {
4897 p.find_or_create_local_worktree("/root/dir-2", true, cx)
4898 })
4899 .await
4900 .unwrap();
4901 worktree_2
4902 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
4903 .await;
4904 let project_id = active_call_a
4905 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4906 .await
4907 .unwrap();
4908
4909 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
4910
4911 // Perform a search as the guest.
4912 let mut results = HashMap::default();
4913 let mut search_rx = project_b.update(cx_b, |project, cx| {
4914 project.search(
4915 SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
4916 cx,
4917 )
4918 });
4919 while let Some(result) = search_rx.next().await {
4920 match result {
4921 SearchResult::Buffer { buffer, ranges } => {
4922 results.entry(buffer).or_insert(ranges);
4923 }
4924 SearchResult::LimitReached => {
4925 panic!("Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call.")
4926 }
4927 };
4928 }
4929
4930 let mut ranges_by_path = results
4931 .into_iter()
4932 .map(|(buffer, ranges)| {
4933 buffer.read_with(cx_b, |buffer, cx| {
4934 let path = buffer.file().unwrap().full_path(cx);
4935 let offset_ranges = ranges
4936 .into_iter()
4937 .map(|range| range.to_offset(buffer))
4938 .collect::<Vec<_>>();
4939 (path, offset_ranges)
4940 })
4941 })
4942 .collect::<Vec<_>>();
4943 ranges_by_path.sort_by_key(|(path, _)| path.clone());
4944
4945 assert_eq!(
4946 ranges_by_path,
4947 &[
4948 (PathBuf::from("dir-1/a"), vec![6..11]),
4949 (PathBuf::from("dir-1/c"), vec![2..7]),
4950 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
4951 (PathBuf::from("dir-2/e"), vec![7..12]),
4952 ]
4953 );
4954}
4955
4956#[gpui::test(iterations = 10)]
4957async fn test_document_highlights(
4958 executor: BackgroundExecutor,
4959 cx_a: &mut TestAppContext,
4960 cx_b: &mut TestAppContext,
4961) {
4962 let mut server = TestServer::start(executor.clone()).await;
4963 let client_a = server.create_client(cx_a, "user_a").await;
4964 let client_b = server.create_client(cx_b, "user_b").await;
4965 server
4966 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4967 .await;
4968 let active_call_a = cx_a.read(ActiveCall::global);
4969
4970 client_a
4971 .fs()
4972 .insert_tree(
4973 "/root-1",
4974 json!({
4975 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4976 }),
4977 )
4978 .await;
4979
4980 let mut fake_language_servers = client_a
4981 .language_registry()
4982 .register_fake_lsp_adapter("Rust", Default::default());
4983 client_a.language_registry().add(rust_lang());
4984
4985 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4986 let project_id = active_call_a
4987 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4988 .await
4989 .unwrap();
4990 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
4991
4992 // Open the file on client B.
4993 let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
4994 let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
4995
4996 // Request document highlights as the guest.
4997 let fake_language_server = fake_language_servers.next().await.unwrap();
4998 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4999 |params, _| async move {
5000 assert_eq!(
5001 params
5002 .text_document_position_params
5003 .text_document
5004 .uri
5005 .as_str(),
5006 "file:///root-1/main.rs"
5007 );
5008 assert_eq!(
5009 params.text_document_position_params.position,
5010 lsp::Position::new(0, 34)
5011 );
5012 Ok(Some(vec![
5013 lsp::DocumentHighlight {
5014 kind: Some(lsp::DocumentHighlightKind::WRITE),
5015 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
5016 },
5017 lsp::DocumentHighlight {
5018 kind: Some(lsp::DocumentHighlightKind::READ),
5019 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
5020 },
5021 lsp::DocumentHighlight {
5022 kind: Some(lsp::DocumentHighlightKind::READ),
5023 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
5024 },
5025 ]))
5026 },
5027 );
5028
5029 let highlights = project_b
5030 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
5031 .await
5032 .unwrap();
5033
5034 buffer_b.read_with(cx_b, |buffer, _| {
5035 let snapshot = buffer.snapshot();
5036
5037 let highlights = highlights
5038 .into_iter()
5039 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
5040 .collect::<Vec<_>>();
5041 assert_eq!(
5042 highlights,
5043 &[
5044 (lsp::DocumentHighlightKind::WRITE, 10..16),
5045 (lsp::DocumentHighlightKind::READ, 32..38),
5046 (lsp::DocumentHighlightKind::READ, 41..47)
5047 ]
5048 )
5049 });
5050}
5051
5052#[gpui::test(iterations = 10)]
5053async fn test_lsp_hover(
5054 executor: BackgroundExecutor,
5055 cx_a: &mut TestAppContext,
5056 cx_b: &mut TestAppContext,
5057) {
5058 let mut server = TestServer::start(executor.clone()).await;
5059 let client_a = server.create_client(cx_a, "user_a").await;
5060 let client_b = server.create_client(cx_b, "user_b").await;
5061 server
5062 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5063 .await;
5064 let active_call_a = cx_a.read(ActiveCall::global);
5065
5066 client_a
5067 .fs()
5068 .insert_tree(
5069 "/root-1",
5070 json!({
5071 "main.rs": "use std::collections::HashMap;",
5072 }),
5073 )
5074 .await;
5075
5076 client_a.language_registry().add(rust_lang());
5077 let language_server_names = ["rust-analyzer", "CrabLang-ls"];
5078 let mut fake_language_servers = client_a
5079 .language_registry()
5080 .register_specific_fake_lsp_adapter(
5081 "Rust",
5082 true,
5083 FakeLspAdapter {
5084 name: "rust-analyzer",
5085 capabilities: lsp::ServerCapabilities {
5086 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5087 ..lsp::ServerCapabilities::default()
5088 },
5089 ..FakeLspAdapter::default()
5090 },
5091 );
5092 let _other_server = client_a
5093 .language_registry()
5094 .register_specific_fake_lsp_adapter(
5095 "Rust",
5096 false,
5097 FakeLspAdapter {
5098 name: "CrabLang-ls",
5099 capabilities: lsp::ServerCapabilities {
5100 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5101 ..lsp::ServerCapabilities::default()
5102 },
5103 ..FakeLspAdapter::default()
5104 },
5105 );
5106
5107 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
5108 let project_id = active_call_a
5109 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5110 .await
5111 .unwrap();
5112 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
5113
5114 // Open the file as the guest
5115 let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
5116 let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
5117
5118 let mut servers_with_hover_requests = HashMap::default();
5119 for i in 0..language_server_names.len() {
5120 let new_server = fake_language_servers.next().await.unwrap_or_else(|| {
5121 panic!(
5122 "Failed to get language server #{i} with name {}",
5123 &language_server_names[i]
5124 )
5125 });
5126 let new_server_name = new_server.server.name();
5127 assert!(
5128 !servers_with_hover_requests.contains_key(new_server_name),
5129 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5130 );
5131 let new_server_name = new_server_name.to_string();
5132 match new_server_name.as_str() {
5133 "CrabLang-ls" => {
5134 servers_with_hover_requests.insert(
5135 new_server_name.clone(),
5136 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5137 move |params, _| {
5138 assert_eq!(
5139 params
5140 .text_document_position_params
5141 .text_document
5142 .uri
5143 .as_str(),
5144 "file:///root-1/main.rs"
5145 );
5146 let name = new_server_name.clone();
5147 async move {
5148 Ok(Some(lsp::Hover {
5149 contents: lsp::HoverContents::Scalar(
5150 lsp::MarkedString::String(format!("{name} hover")),
5151 ),
5152 range: None,
5153 }))
5154 }
5155 },
5156 ),
5157 );
5158 }
5159 "rust-analyzer" => {
5160 servers_with_hover_requests.insert(
5161 new_server_name.clone(),
5162 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
5163 |params, _| async move {
5164 assert_eq!(
5165 params
5166 .text_document_position_params
5167 .text_document
5168 .uri
5169 .as_str(),
5170 "file:///root-1/main.rs"
5171 );
5172 assert_eq!(
5173 params.text_document_position_params.position,
5174 lsp::Position::new(0, 22)
5175 );
5176 Ok(Some(lsp::Hover {
5177 contents: lsp::HoverContents::Array(vec![
5178 lsp::MarkedString::String("Test hover content.".to_string()),
5179 lsp::MarkedString::LanguageString(lsp::LanguageString {
5180 language: "Rust".to_string(),
5181 value: "let foo = 42;".to_string(),
5182 }),
5183 ]),
5184 range: Some(lsp::Range::new(
5185 lsp::Position::new(0, 22),
5186 lsp::Position::new(0, 29),
5187 )),
5188 }))
5189 },
5190 ),
5191 );
5192 }
5193 unexpected => panic!("Unexpected server name: {unexpected}"),
5194 }
5195 }
5196
5197 // Request hover information as the guest.
5198 let mut hovers = project_b
5199 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
5200 .await;
5201 assert_eq!(
5202 hovers.len(),
5203 2,
5204 "Expected two hovers from both language servers, but got: {hovers:?}"
5205 );
5206
5207 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5208 |mut hover_request| async move {
5209 hover_request
5210 .next()
5211 .await
5212 .expect("All hover requests should have been triggered")
5213 },
5214 ))
5215 .await;
5216
5217 hovers.sort_by_key(|hover| hover.contents.len());
5218 let first_hover = hovers.first().cloned().unwrap();
5219 assert_eq!(
5220 first_hover.contents,
5221 vec![project::HoverBlock {
5222 text: "CrabLang-ls hover".to_string(),
5223 kind: HoverBlockKind::Markdown,
5224 },]
5225 );
5226 let second_hover = hovers.last().cloned().unwrap();
5227 assert_eq!(
5228 second_hover.contents,
5229 vec![
5230 project::HoverBlock {
5231 text: "Test hover content.".to_string(),
5232 kind: HoverBlockKind::Markdown,
5233 },
5234 project::HoverBlock {
5235 text: "let foo = 42;".to_string(),
5236 kind: HoverBlockKind::Code {
5237 language: "Rust".to_string()
5238 },
5239 }
5240 ]
5241 );
5242 buffer_b.read_with(cx_b, |buffer, _| {
5243 let snapshot = buffer.snapshot();
5244 assert_eq!(second_hover.range.unwrap().to_offset(&snapshot), 22..29);
5245 });
5246}
5247
5248#[gpui::test(iterations = 10)]
5249async fn test_project_symbols(
5250 executor: BackgroundExecutor,
5251 cx_a: &mut TestAppContext,
5252 cx_b: &mut TestAppContext,
5253) {
5254 let mut server = TestServer::start(executor.clone()).await;
5255 let client_a = server.create_client(cx_a, "user_a").await;
5256 let client_b = server.create_client(cx_b, "user_b").await;
5257 server
5258 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5259 .await;
5260 let active_call_a = cx_a.read(ActiveCall::global);
5261
5262 client_a.language_registry().add(rust_lang());
5263 let mut fake_language_servers = client_a
5264 .language_registry()
5265 .register_fake_lsp_adapter("Rust", Default::default());
5266
5267 client_a
5268 .fs()
5269 .insert_tree(
5270 "/code",
5271 json!({
5272 "crate-1": {
5273 "one.rs": "const ONE: usize = 1;",
5274 },
5275 "crate-2": {
5276 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
5277 },
5278 "private": {
5279 "passwords.txt": "the-password",
5280 }
5281 }),
5282 )
5283 .await;
5284 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
5285 let project_id = active_call_a
5286 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5287 .await
5288 .unwrap();
5289 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
5290
5291 // Cause the language server to start.
5292 let open_buffer_task =
5293 project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
5294 let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5295
5296 let fake_language_server = fake_language_servers.next().await.unwrap();
5297 fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
5298 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
5299 #[allow(deprecated)]
5300 lsp::SymbolInformation {
5301 name: "TWO".into(),
5302 location: lsp::Location {
5303 uri: lsp::Uri::from_file_path("/code/crate-2/two.rs")
5304 .unwrap()
5305 .into(),
5306 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5307 },
5308 kind: lsp::SymbolKind::CONSTANT,
5309 tags: None,
5310 container_name: None,
5311 deprecated: None,
5312 },
5313 ])))
5314 });
5315
5316 // Request the definition of a symbol as the guest.
5317 let symbols = project_b
5318 .update(cx_b, |p, cx| p.symbols("two", cx))
5319 .await
5320 .unwrap();
5321 assert_eq!(symbols.len(), 1);
5322 assert_eq!(symbols[0].name, "TWO");
5323
5324 // Open one of the returned symbols.
5325 let buffer_b_2 = project_b
5326 .update(cx_b, |project, cx| {
5327 project.open_buffer_for_symbol(&symbols[0], cx)
5328 })
5329 .await
5330 .unwrap();
5331
5332 buffer_b_2.read_with(cx_b, |buffer, cx| {
5333 assert_eq!(
5334 buffer.file().unwrap().full_path(cx),
5335 Path::new("/code/crate-2/two.rs")
5336 );
5337 });
5338
5339 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
5340 let mut fake_symbol = symbols[0].clone();
5341 fake_symbol.path.path = Path::new("/code/secrets").into();
5342 let error = project_b
5343 .update(cx_b, |project, cx| {
5344 project.open_buffer_for_symbol(&fake_symbol, cx)
5345 })
5346 .await
5347 .unwrap_err();
5348 assert!(error.to_string().contains("invalid symbol signature"));
5349}
5350
5351#[gpui::test(iterations = 10)]
5352async fn test_open_buffer_while_getting_definition_pointing_to_it(
5353 executor: BackgroundExecutor,
5354 cx_a: &mut TestAppContext,
5355 cx_b: &mut TestAppContext,
5356 mut rng: StdRng,
5357) {
5358 let mut server = TestServer::start(executor.clone()).await;
5359 let client_a = server.create_client(cx_a, "user_a").await;
5360 let client_b = server.create_client(cx_b, "user_b").await;
5361 server
5362 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5363 .await;
5364 let active_call_a = cx_a.read(ActiveCall::global);
5365
5366 client_a.language_registry().add(rust_lang());
5367 let mut fake_language_servers = client_a
5368 .language_registry()
5369 .register_fake_lsp_adapter("Rust", Default::default());
5370
5371 client_a
5372 .fs()
5373 .insert_tree(
5374 "/root",
5375 json!({
5376 "a.rs": "const ONE: usize = b::TWO;",
5377 "b.rs": "const TWO: usize = 2",
5378 }),
5379 )
5380 .await;
5381 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
5382 let project_id = active_call_a
5383 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5384 .await
5385 .unwrap();
5386 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
5387
5388 let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
5389 let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
5390
5391 let fake_language_server = fake_language_servers.next().await.unwrap();
5392 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
5393 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
5394 lsp::Location::new(
5395 lsp::Uri::from_file_path("/root/b.rs").unwrap().into(),
5396 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
5397 ),
5398 )))
5399 });
5400
5401 let definitions;
5402 let buffer_b2;
5403 if rng.gen() {
5404 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5405 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5406 } else {
5407 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
5408 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
5409 }
5410
5411 let buffer_b2 = buffer_b2.await.unwrap();
5412 let definitions = definitions.await.unwrap();
5413 assert_eq!(definitions.len(), 1);
5414 assert_eq!(definitions[0].target.buffer, buffer_b2);
5415}
5416
5417#[gpui::test(iterations = 10)]
5418async fn test_contacts(
5419 executor: BackgroundExecutor,
5420 cx_a: &mut TestAppContext,
5421 cx_b: &mut TestAppContext,
5422 cx_c: &mut TestAppContext,
5423 cx_d: &mut TestAppContext,
5424) {
5425 let mut server = TestServer::start(executor.clone()).await;
5426 let client_a = server.create_client(cx_a, "user_a").await;
5427 let client_b = server.create_client(cx_b, "user_b").await;
5428 let client_c = server.create_client(cx_c, "user_c").await;
5429 let client_d = server.create_client(cx_d, "user_d").await;
5430 server
5431 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
5432 .await;
5433 let active_call_a = cx_a.read(ActiveCall::global);
5434 let active_call_b = cx_b.read(ActiveCall::global);
5435 let active_call_c = cx_c.read(ActiveCall::global);
5436 let _active_call_d = cx_d.read(ActiveCall::global);
5437
5438 executor.run_until_parked();
5439 assert_eq!(
5440 contacts(&client_a, cx_a),
5441 [
5442 ("user_b".to_string(), "online", "free"),
5443 ("user_c".to_string(), "online", "free")
5444 ]
5445 );
5446 assert_eq!(
5447 contacts(&client_b, cx_b),
5448 [
5449 ("user_a".to_string(), "online", "free"),
5450 ("user_c".to_string(), "online", "free")
5451 ]
5452 );
5453 assert_eq!(
5454 contacts(&client_c, cx_c),
5455 [
5456 ("user_a".to_string(), "online", "free"),
5457 ("user_b".to_string(), "online", "free")
5458 ]
5459 );
5460 assert_eq!(contacts(&client_d, cx_d), []);
5461
5462 server.disconnect_client(client_c.peer_id().unwrap());
5463 server.forbid_connections();
5464 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5465 assert_eq!(
5466 contacts(&client_a, cx_a),
5467 [
5468 ("user_b".to_string(), "online", "free"),
5469 ("user_c".to_string(), "offline", "free")
5470 ]
5471 );
5472 assert_eq!(
5473 contacts(&client_b, cx_b),
5474 [
5475 ("user_a".to_string(), "online", "free"),
5476 ("user_c".to_string(), "offline", "free")
5477 ]
5478 );
5479 assert_eq!(contacts(&client_c, cx_c), []);
5480 assert_eq!(contacts(&client_d, cx_d), []);
5481
5482 server.allow_connections();
5483 client_c
5484 .authenticate_and_connect(false, &cx_c.to_async())
5485 .await
5486 .unwrap();
5487
5488 executor.run_until_parked();
5489 assert_eq!(
5490 contacts(&client_a, cx_a),
5491 [
5492 ("user_b".to_string(), "online", "free"),
5493 ("user_c".to_string(), "online", "free")
5494 ]
5495 );
5496 assert_eq!(
5497 contacts(&client_b, cx_b),
5498 [
5499 ("user_a".to_string(), "online", "free"),
5500 ("user_c".to_string(), "online", "free")
5501 ]
5502 );
5503 assert_eq!(
5504 contacts(&client_c, cx_c),
5505 [
5506 ("user_a".to_string(), "online", "free"),
5507 ("user_b".to_string(), "online", "free")
5508 ]
5509 );
5510 assert_eq!(contacts(&client_d, cx_d), []);
5511
5512 active_call_a
5513 .update(cx_a, |call, cx| {
5514 call.invite(client_b.user_id().unwrap(), None, cx)
5515 })
5516 .await
5517 .unwrap();
5518 executor.run_until_parked();
5519 assert_eq!(
5520 contacts(&client_a, cx_a),
5521 [
5522 ("user_b".to_string(), "online", "busy"),
5523 ("user_c".to_string(), "online", "free")
5524 ]
5525 );
5526 assert_eq!(
5527 contacts(&client_b, cx_b),
5528 [
5529 ("user_a".to_string(), "online", "busy"),
5530 ("user_c".to_string(), "online", "free")
5531 ]
5532 );
5533 assert_eq!(
5534 contacts(&client_c, cx_c),
5535 [
5536 ("user_a".to_string(), "online", "busy"),
5537 ("user_b".to_string(), "online", "busy")
5538 ]
5539 );
5540 assert_eq!(contacts(&client_d, cx_d), []);
5541
5542 // Client B and client D become contacts while client B is being called.
5543 server
5544 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5545 .await;
5546 executor.run_until_parked();
5547 assert_eq!(
5548 contacts(&client_a, cx_a),
5549 [
5550 ("user_b".to_string(), "online", "busy"),
5551 ("user_c".to_string(), "online", "free")
5552 ]
5553 );
5554 assert_eq!(
5555 contacts(&client_b, cx_b),
5556 [
5557 ("user_a".to_string(), "online", "busy"),
5558 ("user_c".to_string(), "online", "free"),
5559 ("user_d".to_string(), "online", "free"),
5560 ]
5561 );
5562 assert_eq!(
5563 contacts(&client_c, cx_c),
5564 [
5565 ("user_a".to_string(), "online", "busy"),
5566 ("user_b".to_string(), "online", "busy")
5567 ]
5568 );
5569 assert_eq!(
5570 contacts(&client_d, cx_d),
5571 [("user_b".to_string(), "online", "busy")]
5572 );
5573
5574 active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
5575 executor.run_until_parked();
5576 assert_eq!(
5577 contacts(&client_a, cx_a),
5578 [
5579 ("user_b".to_string(), "online", "free"),
5580 ("user_c".to_string(), "online", "free")
5581 ]
5582 );
5583 assert_eq!(
5584 contacts(&client_b, cx_b),
5585 [
5586 ("user_a".to_string(), "online", "free"),
5587 ("user_c".to_string(), "online", "free"),
5588 ("user_d".to_string(), "online", "free")
5589 ]
5590 );
5591 assert_eq!(
5592 contacts(&client_c, cx_c),
5593 [
5594 ("user_a".to_string(), "online", "free"),
5595 ("user_b".to_string(), "online", "free")
5596 ]
5597 );
5598 assert_eq!(
5599 contacts(&client_d, cx_d),
5600 [("user_b".to_string(), "online", "free")]
5601 );
5602
5603 active_call_c
5604 .update(cx_c, |call, cx| {
5605 call.invite(client_a.user_id().unwrap(), None, cx)
5606 })
5607 .await
5608 .unwrap();
5609 executor.run_until_parked();
5610 assert_eq!(
5611 contacts(&client_a, cx_a),
5612 [
5613 ("user_b".to_string(), "online", "free"),
5614 ("user_c".to_string(), "online", "busy")
5615 ]
5616 );
5617 assert_eq!(
5618 contacts(&client_b, cx_b),
5619 [
5620 ("user_a".to_string(), "online", "busy"),
5621 ("user_c".to_string(), "online", "busy"),
5622 ("user_d".to_string(), "online", "free")
5623 ]
5624 );
5625 assert_eq!(
5626 contacts(&client_c, cx_c),
5627 [
5628 ("user_a".to_string(), "online", "busy"),
5629 ("user_b".to_string(), "online", "free")
5630 ]
5631 );
5632 assert_eq!(
5633 contacts(&client_d, cx_d),
5634 [("user_b".to_string(), "online", "free")]
5635 );
5636
5637 active_call_a
5638 .update(cx_a, |call, cx| call.accept_incoming(cx))
5639 .await
5640 .unwrap();
5641 executor.run_until_parked();
5642 assert_eq!(
5643 contacts(&client_a, cx_a),
5644 [
5645 ("user_b".to_string(), "online", "free"),
5646 ("user_c".to_string(), "online", "busy")
5647 ]
5648 );
5649 assert_eq!(
5650 contacts(&client_b, cx_b),
5651 [
5652 ("user_a".to_string(), "online", "busy"),
5653 ("user_c".to_string(), "online", "busy"),
5654 ("user_d".to_string(), "online", "free")
5655 ]
5656 );
5657 assert_eq!(
5658 contacts(&client_c, cx_c),
5659 [
5660 ("user_a".to_string(), "online", "busy"),
5661 ("user_b".to_string(), "online", "free")
5662 ]
5663 );
5664 assert_eq!(
5665 contacts(&client_d, cx_d),
5666 [("user_b".to_string(), "online", "free")]
5667 );
5668
5669 active_call_a
5670 .update(cx_a, |call, cx| {
5671 call.invite(client_b.user_id().unwrap(), None, cx)
5672 })
5673 .await
5674 .unwrap();
5675 executor.run_until_parked();
5676 assert_eq!(
5677 contacts(&client_a, cx_a),
5678 [
5679 ("user_b".to_string(), "online", "busy"),
5680 ("user_c".to_string(), "online", "busy")
5681 ]
5682 );
5683 assert_eq!(
5684 contacts(&client_b, cx_b),
5685 [
5686 ("user_a".to_string(), "online", "busy"),
5687 ("user_c".to_string(), "online", "busy"),
5688 ("user_d".to_string(), "online", "free")
5689 ]
5690 );
5691 assert_eq!(
5692 contacts(&client_c, cx_c),
5693 [
5694 ("user_a".to_string(), "online", "busy"),
5695 ("user_b".to_string(), "online", "busy")
5696 ]
5697 );
5698 assert_eq!(
5699 contacts(&client_d, cx_d),
5700 [("user_b".to_string(), "online", "busy")]
5701 );
5702
5703 active_call_a
5704 .update(cx_a, |call, cx| call.hang_up(cx))
5705 .await
5706 .unwrap();
5707 executor.run_until_parked();
5708 assert_eq!(
5709 contacts(&client_a, cx_a),
5710 [
5711 ("user_b".to_string(), "online", "free"),
5712 ("user_c".to_string(), "online", "free")
5713 ]
5714 );
5715 assert_eq!(
5716 contacts(&client_b, cx_b),
5717 [
5718 ("user_a".to_string(), "online", "free"),
5719 ("user_c".to_string(), "online", "free"),
5720 ("user_d".to_string(), "online", "free")
5721 ]
5722 );
5723 assert_eq!(
5724 contacts(&client_c, cx_c),
5725 [
5726 ("user_a".to_string(), "online", "free"),
5727 ("user_b".to_string(), "online", "free")
5728 ]
5729 );
5730 assert_eq!(
5731 contacts(&client_d, cx_d),
5732 [("user_b".to_string(), "online", "free")]
5733 );
5734
5735 active_call_a
5736 .update(cx_a, |call, cx| {
5737 call.invite(client_b.user_id().unwrap(), None, cx)
5738 })
5739 .await
5740 .unwrap();
5741 executor.run_until_parked();
5742 assert_eq!(
5743 contacts(&client_a, cx_a),
5744 [
5745 ("user_b".to_string(), "online", "busy"),
5746 ("user_c".to_string(), "online", "free")
5747 ]
5748 );
5749 assert_eq!(
5750 contacts(&client_b, cx_b),
5751 [
5752 ("user_a".to_string(), "online", "busy"),
5753 ("user_c".to_string(), "online", "free"),
5754 ("user_d".to_string(), "online", "free")
5755 ]
5756 );
5757 assert_eq!(
5758 contacts(&client_c, cx_c),
5759 [
5760 ("user_a".to_string(), "online", "busy"),
5761 ("user_b".to_string(), "online", "busy")
5762 ]
5763 );
5764 assert_eq!(
5765 contacts(&client_d, cx_d),
5766 [("user_b".to_string(), "online", "busy")]
5767 );
5768
5769 server.forbid_connections();
5770 server.disconnect_client(client_a.peer_id().unwrap());
5771 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5772 assert_eq!(contacts(&client_a, cx_a), []);
5773 assert_eq!(
5774 contacts(&client_b, cx_b),
5775 [
5776 ("user_a".to_string(), "offline", "free"),
5777 ("user_c".to_string(), "online", "free"),
5778 ("user_d".to_string(), "online", "free")
5779 ]
5780 );
5781 assert_eq!(
5782 contacts(&client_c, cx_c),
5783 [
5784 ("user_a".to_string(), "offline", "free"),
5785 ("user_b".to_string(), "online", "free")
5786 ]
5787 );
5788 assert_eq!(
5789 contacts(&client_d, cx_d),
5790 [("user_b".to_string(), "online", "free")]
5791 );
5792
5793 // Test removing a contact
5794 client_b
5795 .user_store()
5796 .update(cx_b, |store, cx| {
5797 store.remove_contact(client_c.user_id().unwrap(), cx)
5798 })
5799 .await
5800 .unwrap();
5801 executor.run_until_parked();
5802 assert_eq!(
5803 contacts(&client_b, cx_b),
5804 [
5805 ("user_a".to_string(), "offline", "free"),
5806 ("user_d".to_string(), "online", "free")
5807 ]
5808 );
5809 assert_eq!(
5810 contacts(&client_c, cx_c),
5811 [("user_a".to_string(), "offline", "free"),]
5812 );
5813
5814 fn contacts(
5815 client: &TestClient,
5816 cx: &TestAppContext,
5817 ) -> Vec<(String, &'static str, &'static str)> {
5818 client.user_store().read_with(cx, |store, _| {
5819 store
5820 .contacts()
5821 .iter()
5822 .map(|contact| {
5823 (
5824 contact.user.github_login.clone(),
5825 if contact.online { "online" } else { "offline" },
5826 if contact.busy { "busy" } else { "free" },
5827 )
5828 })
5829 .collect()
5830 })
5831 }
5832}
5833
5834#[gpui::test(iterations = 10)]
5835async fn test_contact_requests(
5836 executor: BackgroundExecutor,
5837 cx_a: &mut TestAppContext,
5838 cx_a2: &mut TestAppContext,
5839 cx_b: &mut TestAppContext,
5840 cx_b2: &mut TestAppContext,
5841 cx_c: &mut TestAppContext,
5842 cx_c2: &mut TestAppContext,
5843) {
5844 // Connect to a server as 3 clients.
5845 let mut server = TestServer::start(executor.clone()).await;
5846 let client_a = server.create_client(cx_a, "user_a").await;
5847 let client_a2 = server.create_client(cx_a2, "user_a").await;
5848 let client_b = server.create_client(cx_b, "user_b").await;
5849 let client_b2 = server.create_client(cx_b2, "user_b").await;
5850 let client_c = server.create_client(cx_c, "user_c").await;
5851 let client_c2 = server.create_client(cx_c2, "user_c").await;
5852
5853 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5854 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5855 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5856
5857 // User A and User C request that user B become their contact.
5858 client_a
5859 .user_store()
5860 .update(cx_a, |store, cx| {
5861 store.request_contact(client_b.user_id().unwrap(), cx)
5862 })
5863 .await
5864 .unwrap();
5865 client_c
5866 .user_store()
5867 .update(cx_c, |store, cx| {
5868 store.request_contact(client_b.user_id().unwrap(), cx)
5869 })
5870 .await
5871 .unwrap();
5872 executor.run_until_parked();
5873
5874 // All users see the pending request appear in all their clients.
5875 assert_eq!(
5876 client_a.summarize_contacts(cx_a).outgoing_requests,
5877 &["user_b"]
5878 );
5879 assert_eq!(
5880 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5881 &["user_b"]
5882 );
5883 assert_eq!(
5884 client_b.summarize_contacts(cx_b).incoming_requests,
5885 &["user_a", "user_c"]
5886 );
5887 assert_eq!(
5888 client_b2.summarize_contacts(cx_b2).incoming_requests,
5889 &["user_a", "user_c"]
5890 );
5891 assert_eq!(
5892 client_c.summarize_contacts(cx_c).outgoing_requests,
5893 &["user_b"]
5894 );
5895 assert_eq!(
5896 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5897 &["user_b"]
5898 );
5899
5900 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5901 disconnect_and_reconnect(&client_a, cx_a).await;
5902 disconnect_and_reconnect(&client_b, cx_b).await;
5903 disconnect_and_reconnect(&client_c, cx_c).await;
5904 executor.run_until_parked();
5905 assert_eq!(
5906 client_a.summarize_contacts(cx_a).outgoing_requests,
5907 &["user_b"]
5908 );
5909 assert_eq!(
5910 client_b.summarize_contacts(cx_b).incoming_requests,
5911 &["user_a", "user_c"]
5912 );
5913 assert_eq!(
5914 client_c.summarize_contacts(cx_c).outgoing_requests,
5915 &["user_b"]
5916 );
5917
5918 // User B accepts the request from user A.
5919 client_b
5920 .user_store()
5921 .update(cx_b, |store, cx| {
5922 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5923 })
5924 .await
5925 .unwrap();
5926
5927 executor.run_until_parked();
5928
5929 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5930 let contacts_b = client_b.summarize_contacts(cx_b);
5931 assert_eq!(contacts_b.current, &["user_a"]);
5932 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5933 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5934 assert_eq!(contacts_b2.current, &["user_a"]);
5935 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5936
5937 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5938 let contacts_a = client_a.summarize_contacts(cx_a);
5939 assert_eq!(contacts_a.current, &["user_b"]);
5940 assert!(contacts_a.outgoing_requests.is_empty());
5941 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5942 assert_eq!(contacts_a2.current, &["user_b"]);
5943 assert!(contacts_a2.outgoing_requests.is_empty());
5944
5945 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5946 disconnect_and_reconnect(&client_a, cx_a).await;
5947 disconnect_and_reconnect(&client_b, cx_b).await;
5948 disconnect_and_reconnect(&client_c, cx_c).await;
5949 executor.run_until_parked();
5950 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5951 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5952 assert_eq!(
5953 client_b.summarize_contacts(cx_b).incoming_requests,
5954 &["user_c"]
5955 );
5956 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5957 assert_eq!(
5958 client_c.summarize_contacts(cx_c).outgoing_requests,
5959 &["user_b"]
5960 );
5961
5962 // User B rejects the request from user C.
5963 client_b
5964 .user_store()
5965 .update(cx_b, |store, cx| {
5966 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5967 })
5968 .await
5969 .unwrap();
5970
5971 executor.run_until_parked();
5972
5973 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5974 let contacts_b = client_b.summarize_contacts(cx_b);
5975 assert_eq!(contacts_b.current, &["user_a"]);
5976 assert!(contacts_b.incoming_requests.is_empty());
5977 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5978 assert_eq!(contacts_b2.current, &["user_a"]);
5979 assert!(contacts_b2.incoming_requests.is_empty());
5980
5981 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5982 let contacts_c = client_c.summarize_contacts(cx_c);
5983 assert!(contacts_c.current.is_empty());
5984 assert!(contacts_c.outgoing_requests.is_empty());
5985 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5986 assert!(contacts_c2.current.is_empty());
5987 assert!(contacts_c2.outgoing_requests.is_empty());
5988
5989 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5990 disconnect_and_reconnect(&client_a, cx_a).await;
5991 disconnect_and_reconnect(&client_b, cx_b).await;
5992 disconnect_and_reconnect(&client_c, cx_c).await;
5993 executor.run_until_parked();
5994 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5995 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5996 assert!(client_b
5997 .summarize_contacts(cx_b)
5998 .incoming_requests
5999 .is_empty());
6000 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
6001 assert!(client_c
6002 .summarize_contacts(cx_c)
6003 .outgoing_requests
6004 .is_empty());
6005
6006 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
6007 client.disconnect(&cx.to_async());
6008 client.clear_contacts(cx).await;
6009 client
6010 .authenticate_and_connect(false, &cx.to_async())
6011 .await
6012 .unwrap();
6013 }
6014}
6015
6016#[gpui::test(iterations = 10)]
6017async fn test_join_call_after_screen_was_shared(
6018 executor: BackgroundExecutor,
6019 cx_a: &mut TestAppContext,
6020 cx_b: &mut TestAppContext,
6021) {
6022 let mut server = TestServer::start(executor.clone()).await;
6023
6024 let client_a = server.create_client(cx_a, "user_a").await;
6025 let client_b = server.create_client(cx_b, "user_b").await;
6026 server
6027 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6028 .await;
6029
6030 let active_call_a = cx_a.read(ActiveCall::global);
6031 let active_call_b = cx_b.read(ActiveCall::global);
6032
6033 // Call users B and C from client A.
6034 active_call_a
6035 .update(cx_a, |call, cx| {
6036 call.invite(client_b.user_id().unwrap(), None, cx)
6037 })
6038 .await
6039 .unwrap();
6040
6041 let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
6042 executor.run_until_parked();
6043 assert_eq!(
6044 room_participants(&room_a, cx_a),
6045 RoomParticipants {
6046 remote: Default::default(),
6047 pending: vec!["user_b".to_string()]
6048 }
6049 );
6050
6051 // User B receives the call.
6052
6053 let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
6054 let call_b = incoming_call_b.next().await.unwrap().unwrap();
6055 assert_eq!(call_b.calling_user.github_login, "user_a");
6056
6057 // User A shares their screen
6058 let display = MacOSDisplay::new();
6059 active_call_a
6060 .update(cx_a, |call, cx| {
6061 call.room().unwrap().update(cx, |room, cx| {
6062 room.set_display_sources(vec![display.clone()]);
6063 room.share_screen(cx)
6064 })
6065 })
6066 .await
6067 .unwrap();
6068
6069 client_b.user_store().update(cx_b, |user_store, _| {
6070 user_store.clear_cache();
6071 });
6072
6073 // User B joins the room
6074 active_call_b
6075 .update(cx_b, |call, cx| call.accept_incoming(cx))
6076 .await
6077 .unwrap();
6078
6079 let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
6080 assert!(incoming_call_b.next().await.unwrap().is_none());
6081
6082 executor.run_until_parked();
6083 assert_eq!(
6084 room_participants(&room_a, cx_a),
6085 RoomParticipants {
6086 remote: vec!["user_b".to_string()],
6087 pending: vec![],
6088 }
6089 );
6090 assert_eq!(
6091 room_participants(&room_b, cx_b),
6092 RoomParticipants {
6093 remote: vec!["user_a".to_string()],
6094 pending: vec![],
6095 }
6096 );
6097
6098 // Ensure User B sees User A's screenshare.
6099
6100 room_b.read_with(cx_b, |room, _| {
6101 assert_eq!(
6102 room.remote_participants()
6103 .get(&client_a.user_id().unwrap())
6104 .unwrap()
6105 .video_tracks
6106 .len(),
6107 1
6108 );
6109 });
6110}
6111
6112#[gpui::test]
6113async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
6114 let mut server = TestServer::start(cx.executor().clone()).await;
6115 let client_a = server.create_client(cx, "user_a").await;
6116 let (_workspace_a, cx) = client_a.build_test_workspace(cx).await;
6117
6118 cx.simulate_resize(size(px(300.), px(300.)));
6119
6120 cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
6121 cx.update(|cx| cx.refresh());
6122
6123 let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
6124 let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
6125
6126 assert!(
6127 tab_bounds.intersects(&new_tab_button_bounds),
6128 "Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
6129 );
6130
6131 cx.simulate_event(MouseDownEvent {
6132 button: MouseButton::Right,
6133 position: new_tab_button_bounds.center(),
6134 modifiers: Modifiers::default(),
6135 click_count: 1,
6136 first_mouse: false,
6137 });
6138
6139 // regression test that the right click menu for tabs does not open.
6140 assert!(cx.debug_bounds("MENU_ITEM-Close").is_none());
6141
6142 let tab_bounds = cx.debug_bounds("TAB-1").unwrap();
6143 cx.simulate_event(MouseDownEvent {
6144 button: MouseButton::Right,
6145 position: tab_bounds.center(),
6146 modifiers: Modifiers::default(),
6147 click_count: 1,
6148 first_mouse: false,
6149 });
6150 assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
6151}
6152
6153#[gpui::test]
6154async fn test_pane_split_left(cx: &mut TestAppContext) {
6155 let (_, client) = TestServer::start1(cx).await;
6156 let (workspace, cx) = client.build_test_workspace(cx).await;
6157
6158 cx.simulate_keystrokes("cmd-n");
6159 workspace.update(cx, |workspace, cx| {
6160 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
6161 });
6162 cx.simulate_keystrokes("cmd-k left");
6163 workspace.update(cx, |workspace, cx| {
6164 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6165 });
6166 cx.simulate_keystrokes("cmd-k");
6167 // sleep for longer than the timeout in keyboard shortcut handling
6168 // to verify that it doesn't fire in this case.
6169 cx.executor().advance_clock(Duration::from_secs(2));
6170 cx.simulate_keystrokes("left");
6171 workspace.update(cx, |workspace, cx| {
6172 assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
6173 });
6174}
6175
6176#[gpui::test]
6177async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppContext) {
6178 let (mut server, client) = TestServer::start1(cx1).await;
6179 let channel1 = server.make_public_channel("channel1", &client, cx1).await;
6180 let channel2 = server.make_public_channel("channel2", &client, cx1).await;
6181
6182 join_channel(channel1, &client, cx1).await.unwrap();
6183 drop(client);
6184
6185 let client2 = server.create_client(cx2, "user_a").await;
6186 join_channel(channel2, &client2, cx2).await.unwrap();
6187}
6188
6189#[gpui::test]
6190async fn test_preview_tabs(cx: &mut TestAppContext) {
6191 let (_server, client) = TestServer::start1(cx).await;
6192 let (workspace, cx) = client.build_test_workspace(cx).await;
6193 let project = workspace.update(cx, |workspace, _| workspace.project().clone());
6194
6195 let worktree_id = project.update(cx, |project, cx| {
6196 project.worktrees().next().unwrap().read(cx).id()
6197 });
6198
6199 let path_1 = ProjectPath {
6200 worktree_id,
6201 path: Path::new("1.txt").into(),
6202 };
6203 let path_2 = ProjectPath {
6204 worktree_id,
6205 path: Path::new("2.js").into(),
6206 };
6207 let path_3 = ProjectPath {
6208 worktree_id,
6209 path: Path::new("3.rs").into(),
6210 };
6211
6212 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6213
6214 let get_path = |pane: &Pane, idx: usize, cx: &AppContext| {
6215 pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
6216 };
6217
6218 // Opening item 3 as a "permanent" tab
6219 workspace
6220 .update(cx, |workspace, cx| {
6221 workspace.open_path(path_3.clone(), None, false, cx)
6222 })
6223 .await
6224 .unwrap();
6225
6226 pane.update(cx, |pane, cx| {
6227 assert_eq!(pane.items_len(), 1);
6228 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6229 assert_eq!(pane.preview_item_id(), None);
6230
6231 assert!(!pane.can_navigate_backward());
6232 assert!(!pane.can_navigate_forward());
6233 });
6234
6235 // Open item 1 as preview
6236 workspace
6237 .update(cx, |workspace, cx| {
6238 workspace.open_path_preview(path_1.clone(), None, true, true, cx)
6239 })
6240 .await
6241 .unwrap();
6242
6243 pane.update(cx, |pane, cx| {
6244 assert_eq!(pane.items_len(), 2);
6245 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6246 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6247 assert_eq!(
6248 pane.preview_item_id(),
6249 Some(pane.items().nth(1).unwrap().item_id())
6250 );
6251
6252 assert!(pane.can_navigate_backward());
6253 assert!(!pane.can_navigate_forward());
6254 });
6255
6256 // Open item 2 as preview
6257 workspace
6258 .update(cx, |workspace, cx| {
6259 workspace.open_path_preview(path_2.clone(), None, true, true, cx)
6260 })
6261 .await
6262 .unwrap();
6263
6264 pane.update(cx, |pane, cx| {
6265 assert_eq!(pane.items_len(), 2);
6266 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6267 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6268 assert_eq!(
6269 pane.preview_item_id(),
6270 Some(pane.items().nth(1).unwrap().item_id())
6271 );
6272
6273 assert!(pane.can_navigate_backward());
6274 assert!(!pane.can_navigate_forward());
6275 });
6276
6277 // Going back should show item 1 as preview
6278 workspace
6279 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6280 .await
6281 .unwrap();
6282
6283 pane.update(cx, |pane, cx| {
6284 assert_eq!(pane.items_len(), 2);
6285 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6286 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6287 assert_eq!(
6288 pane.preview_item_id(),
6289 Some(pane.items().nth(1).unwrap().item_id())
6290 );
6291
6292 assert!(pane.can_navigate_backward());
6293 assert!(pane.can_navigate_forward());
6294 });
6295
6296 // Closing item 1
6297 pane.update(cx, |pane, cx| {
6298 pane.close_item_by_id(
6299 pane.active_item().unwrap().item_id(),
6300 workspace::SaveIntent::Skip,
6301 cx,
6302 )
6303 })
6304 .await
6305 .unwrap();
6306
6307 pane.update(cx, |pane, cx| {
6308 assert_eq!(pane.items_len(), 1);
6309 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6310 assert_eq!(pane.preview_item_id(), None);
6311
6312 assert!(pane.can_navigate_backward());
6313 assert!(!pane.can_navigate_forward());
6314 });
6315
6316 // Going back should show item 1 as preview
6317 workspace
6318 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6319 .await
6320 .unwrap();
6321
6322 pane.update(cx, |pane, cx| {
6323 assert_eq!(pane.items_len(), 2);
6324 assert_eq!(get_path(pane, 0, cx), path_3.clone());
6325 assert_eq!(get_path(pane, 1, cx), path_1.clone());
6326 assert_eq!(
6327 pane.preview_item_id(),
6328 Some(pane.items().nth(1).unwrap().item_id())
6329 );
6330
6331 assert!(pane.can_navigate_backward());
6332 assert!(pane.can_navigate_forward());
6333 });
6334
6335 // Close permanent tab
6336 pane.update(cx, |pane, cx| {
6337 let id = pane.items().nth(0).unwrap().item_id();
6338 pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
6339 })
6340 .await
6341 .unwrap();
6342
6343 pane.update(cx, |pane, cx| {
6344 assert_eq!(pane.items_len(), 1);
6345 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6346 assert_eq!(
6347 pane.preview_item_id(),
6348 Some(pane.items().nth(0).unwrap().item_id())
6349 );
6350
6351 assert!(pane.can_navigate_backward());
6352 assert!(pane.can_navigate_forward());
6353 });
6354
6355 // Split pane to the right
6356 pane.update(cx, |pane, cx| {
6357 pane.split(workspace::SplitDirection::Right, cx);
6358 });
6359
6360 let right_pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6361
6362 pane.update(cx, |pane, cx| {
6363 assert_eq!(pane.items_len(), 1);
6364 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6365 assert_eq!(
6366 pane.preview_item_id(),
6367 Some(pane.items().nth(0).unwrap().item_id())
6368 );
6369
6370 assert!(pane.can_navigate_backward());
6371 assert!(pane.can_navigate_forward());
6372 });
6373
6374 right_pane.update(cx, |pane, cx| {
6375 assert_eq!(pane.items_len(), 1);
6376 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6377 assert_eq!(pane.preview_item_id(), None);
6378
6379 assert!(!pane.can_navigate_backward());
6380 assert!(!pane.can_navigate_forward());
6381 });
6382
6383 // Open item 2 as preview in right pane
6384 workspace
6385 .update(cx, |workspace, cx| {
6386 workspace.open_path_preview(path_2.clone(), None, true, true, cx)
6387 })
6388 .await
6389 .unwrap();
6390
6391 pane.update(cx, |pane, cx| {
6392 assert_eq!(pane.items_len(), 1);
6393 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6394 assert_eq!(
6395 pane.preview_item_id(),
6396 Some(pane.items().nth(0).unwrap().item_id())
6397 );
6398
6399 assert!(pane.can_navigate_backward());
6400 assert!(pane.can_navigate_forward());
6401 });
6402
6403 right_pane.update(cx, |pane, cx| {
6404 assert_eq!(pane.items_len(), 2);
6405 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6406 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6407 assert_eq!(
6408 pane.preview_item_id(),
6409 Some(pane.items().nth(1).unwrap().item_id())
6410 );
6411
6412 assert!(pane.can_navigate_backward());
6413 assert!(!pane.can_navigate_forward());
6414 });
6415
6416 // Focus left pane
6417 workspace.update(cx, |workspace, cx| {
6418 workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx)
6419 });
6420
6421 // Open item 2 as preview in left pane
6422 workspace
6423 .update(cx, |workspace, cx| {
6424 workspace.open_path_preview(path_2.clone(), None, true, true, cx)
6425 })
6426 .await
6427 .unwrap();
6428
6429 pane.update(cx, |pane, cx| {
6430 assert_eq!(pane.items_len(), 1);
6431 assert_eq!(get_path(pane, 0, cx), path_2.clone());
6432 assert_eq!(
6433 pane.preview_item_id(),
6434 Some(pane.items().nth(0).unwrap().item_id())
6435 );
6436
6437 assert!(pane.can_navigate_backward());
6438 assert!(!pane.can_navigate_forward());
6439 });
6440
6441 right_pane.update(cx, |pane, cx| {
6442 assert_eq!(pane.items_len(), 2);
6443 assert_eq!(get_path(pane, 0, cx), path_1.clone());
6444 assert_eq!(get_path(pane, 1, cx), path_2.clone());
6445 assert_eq!(
6446 pane.preview_item_id(),
6447 Some(pane.items().nth(1).unwrap().item_id())
6448 );
6449
6450 assert!(pane.can_navigate_backward());
6451 assert!(!pane.can_navigate_forward());
6452 });
6453}