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