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