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