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